/ crypto / derive-secret / src / lib.rs
lib.rs
  1  //! Scheme for deriving deterministic secret keys
  2  //!
  3  //! `DerivableSecret` represents a secret key that can be used to derive child
  4  //! secret keys. A root key secret can be used to derives child
  5  //! keys from it, which can have child keys derived from them, recursively.
  6  //!
  7  //! The `DerivableSecret` struct in this implementation is only used for
  8  //! deriving secret keys, not public keys. This allows supporting multiple
  9  //! crypto schemes for the different cryptographic operations used across the
 10  //! different modules:
 11  //!
 12  //! * secp256k1 for bitcoin deposit addresses, redeem keys and contract keys for
 13  //!   lightning,
 14  //! * bls12-381 for the guardians' threshold signature scheme,
 15  //! * chacha20-poly1305 for symmetric encryption used for backups.
 16  use std::fmt::Formatter;
 17  
 18  use bls12_381::Scalar;
 19  use fedimint_core::config::FederationId;
 20  use fedimint_core::encoding::{Decodable, Encodable};
 21  use fedimint_core::BitcoinHash;
 22  use hkdf::hashes::Sha512;
 23  use hkdf::Hkdf;
 24  use ring::aead;
 25  use secp256k1_zkp::{KeyPair, Secp256k1, Signing};
 26  
 27  const CHILD_TAG: &[u8; 8] = b"childkey";
 28  const SECP256K1_TAG: &[u8; 8] = b"secp256k";
 29  const BLS12_381_TAG: &[u8; 8] = b"bls12381";
 30  const CHACHA20_POLY1305: &[u8; 8] = b"c20p1305";
 31  const RAW_BYTES: &[u8; 8] = b"rawbytes";
 32  
 33  /// Describes a child key of a [`DerivableSecret`]
 34  #[derive(Debug, Copy, Clone, Encodable, Decodable)]
 35  pub struct ChildId(pub u64);
 36  
 37  /// A secret that can have child-subkey derived from it.
 38  #[derive(Clone)]
 39  pub struct DerivableSecret {
 40      /// Derivation level, root = 0, every `child_key` increments it
 41      level: usize,
 42      /// An instance of the HKDF (Hash-based Key Derivation
 43      ///   Function) with SHA-512 as the underlying hash function. It is used to
 44      ///   derive child keys.
 45      // TODO: wrap in some secret protecting wrappers maybe?
 46      kdf: Hkdf<Sha512>,
 47  }
 48  
 49  impl DerivableSecret {
 50      /// Derive root secret key from a secret material and salt.
 51      ///
 52      /// The `salt` is just additional data t used
 53      /// as an additional input to the HKDF.
 54      pub fn new_root(root_key: &[u8], salt: &[u8]) -> Self {
 55          DerivableSecret {
 56              level: 0,
 57              kdf: Hkdf::new(root_key, Some(salt)),
 58          }
 59      }
 60  
 61      /// Get derivation level
 62      ///
 63      /// This is useful for ensuring a correct derivation level is used,
 64      /// in various places.
 65      ///
 66      /// Root keys start at `0`, and every derived key increments it.
 67      pub fn level(&self) -> usize {
 68          self.level
 69      }
 70  
 71      pub fn child_key(&self, cid: ChildId) -> DerivableSecret {
 72          DerivableSecret {
 73              level: self.level + 1,
 74              kdf: Hkdf::from_prk(self.kdf.derive_hmac(&tagged_derive(CHILD_TAG, cid))),
 75          }
 76      }
 77  
 78      /// Derive a federation-ID-based child key from self.
 79      ///
 80      /// This is useful to ensure that the same root secret is not reused
 81      /// across multiple `fedimint-client` instances for different federations.
 82      ///
 83      /// We reset the level to 0 here since `fedimint-client` expects its root
 84      /// secret to be at that level.
 85      pub fn federation_key(&self, federation_id: &FederationId) -> DerivableSecret {
 86          DerivableSecret {
 87              level: 0,
 88              kdf: Hkdf::from_prk(
 89                  self.kdf.derive_hmac(&tagged_derive(
 90                      &federation_id.0.to_byte_array()[..8]
 91                          .try_into()
 92                          .expect("Slice with length 8"),
 93                      ChildId(0),
 94                  )),
 95              ),
 96          }
 97      }
 98  
 99      /// secp256k1 keys are used for bitcoin deposit addresses, redeem keys and
100      /// contract keys for lightning.
101      pub fn to_secp_key<C: Signing>(self, ctx: &Secp256k1<C>) -> KeyPair {
102          for key_try in 0u64.. {
103              let secret = self
104                  .kdf
105                  .derive::<32>(&tagged_derive(SECP256K1_TAG, ChildId(key_try)));
106              // The secret not forming a valid key is highly unlikely, this approach is the
107              // same used when generating a random secp key.
108              if let Ok(key) = KeyPair::from_seckey_slice(ctx, &secret) {
109                  return key;
110              }
111          }
112  
113          unreachable!("If key generation fails this often something else has to be wrong.")
114      }
115  
116      /// bls12-381 keys are used for the guardians' threshold signature scheme,
117      /// and most importantly for its use for the blinding keys for e-cash notes.
118      pub fn to_bls12_381_key(&self) -> Scalar {
119          Scalar::from_bytes_wide(&self.kdf.derive(&tagged_derive(BLS12_381_TAG, ChildId(0))))
120      }
121  
122      // `ring` does not support any way to get raw bytes from a key,
123      // so we need to be able to get just the raw bytes here, so we can serialize
124      // them, and convert to ring type from it.
125      pub fn to_chacha20_poly1305_key_raw(&self) -> [u8; 32] {
126          self.kdf
127              .derive::<32>(&tagged_derive(CHACHA20_POLY1305, ChildId(0)))
128      }
129  
130      pub fn to_chacha20_poly1305_key(&self) -> aead::UnboundKey {
131          aead::UnboundKey::new(
132              &aead::CHACHA20_POLY1305,
133              &self.to_chacha20_poly1305_key_raw(),
134          )
135          .expect("created key")
136      }
137  
138      /// Generate a pseudo-random byte array from the derivable secret.
139      pub fn to_random_bytes<const LEN: usize>(&self) -> [u8; LEN] {
140          self.kdf.derive(&tagged_derive(RAW_BYTES, ChildId(0)))
141      }
142  }
143  
144  fn tagged_derive(tag: &[u8; 8], derivation: ChildId) -> [u8; 16] {
145      let mut derivation_info = [0u8; 16];
146      derivation_info[0..8].copy_from_slice(&tag[..]);
147      // The endianness isn't important here because we just need some bytes, but
148      // let's use the default for this project (big endian)
149      derivation_info[8..16].copy_from_slice(&derivation.0.to_be_bytes()[..]);
150      derivation_info
151  }
152  
153  impl std::fmt::Debug for DerivableSecret {
154      fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
155          write!(f, "DerivableSecret#")?;
156          bitcoin_hashes::hex::format_hex(
157              &self
158                  .kdf
159                  .derive::<8>(b"just a debug fingerprint derivation salt"),
160              f,
161          )
162      }
163  }