Skip to main content

fedimint_derive_secret/
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.
16use std::fmt::Formatter;
17
18use bls12_381::Scalar;
19use fedimint_core::config::FederationId;
20use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
21use fedimint_core::module::registry::ModuleDecoderRegistry;
22use fedimint_core::secp256k1::{Keypair, Secp256k1, Signing};
23use hkdf::hashes::Sha512;
24use hkdf::{BitcoinHash, Hkdf};
25use ring::aead;
26
27const CHILD_TAG: &[u8; 8] = b"childkey";
28const SECP256K1_TAG: &[u8; 8] = b"secp256k";
29const BLS12_381_TAG: &[u8; 8] = b"bls12381";
30const CHACHA20_POLY1305: &[u8; 8] = b"c20p1305";
31const RAW_BYTES: &[u8; 8] = b"rawbytes";
32
33/// Describes a child key of a [`DerivableSecret`]
34#[derive(Debug, Copy, Clone, Encodable, Decodable)]
35pub struct ChildId(pub u64);
36
37/// A secret that can have child-subkey derived from it.
38#[derive(Clone)]
39pub 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
49impl 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 tweaked child key from self using arbitrary bytes.
79    ///
80    /// Similar to [`child_key`](Self::child_key) but accepts arbitrary bytes
81    /// as the tweak input instead of a [`ChildId`]. This increments the
82    /// derivation level since it produces a new derived key.
83    pub fn tweak(&self, tweak: &[u8]) -> DerivableSecret {
84        DerivableSecret {
85            level: self.level + 1,
86            kdf: Hkdf::from_prk(self.kdf.derive_hmac(tweak)),
87        }
88    }
89
90    /// Derive a federation-ID-based child key from self.
91    ///
92    /// This is useful to ensure that the same root secret is not reused
93    /// across multiple `fedimint-client` instances for different federations.
94    ///
95    /// We reset the level to 0 here since `fedimint-client` expects its root
96    /// secret to be at that level.
97    pub fn federation_key(&self, federation_id: &FederationId) -> DerivableSecret {
98        DerivableSecret {
99            level: 0,
100            kdf: Hkdf::from_prk(
101                self.kdf.derive_hmac(&tagged_derive(
102                    &federation_id.0.to_byte_array()[..8]
103                        .try_into()
104                        .expect("Slice with length 8"),
105                    ChildId(0),
106                )),
107            ),
108        }
109    }
110
111    /// secp256k1 keys are used for bitcoin deposit addresses, redeem keys and
112    /// contract keys for lightning.
113    pub fn to_secp_key<C: Signing>(self, ctx: &Secp256k1<C>) -> Keypair {
114        for key_try in 0u64.. {
115            let secret = self
116                .kdf
117                .derive::<32>(&tagged_derive(SECP256K1_TAG, ChildId(key_try)));
118            // The secret not forming a valid key is highly unlikely, this approach is the
119            // same used when generating a random secp key.
120            if let Ok(key) = Keypair::from_seckey_slice(ctx, &secret) {
121                return key;
122            }
123        }
124
125        unreachable!("If key generation fails this often something else has to be wrong.")
126    }
127
128    /// bls12-381 keys are used for the guardians' threshold signature scheme,
129    /// and most importantly for its use for the blinding keys for e-cash notes.
130    pub fn to_bls12_381_key(&self) -> Scalar {
131        Scalar::from_bytes_wide(&self.kdf.derive(&tagged_derive(BLS12_381_TAG, ChildId(0))))
132    }
133
134    // `ring` does not support any way to get raw bytes from a key,
135    // so we need to be able to get just the raw bytes here, so we can serialize
136    // them, and convert to ring type from it.
137    pub fn to_chacha20_poly1305_key_raw(&self) -> [u8; 32] {
138        self.kdf
139            .derive::<32>(&tagged_derive(CHACHA20_POLY1305, ChildId(0)))
140    }
141
142    pub fn to_chacha20_poly1305_key(&self) -> aead::UnboundKey {
143        aead::UnboundKey::new(
144            &aead::CHACHA20_POLY1305,
145            &self.to_chacha20_poly1305_key_raw(),
146        )
147        .expect("created key")
148    }
149
150    /// Generate a pseudo-random byte array from the derivable secret.
151    pub fn to_random_bytes<const LEN: usize>(&self) -> [u8; LEN] {
152        self.kdf.derive(&tagged_derive(RAW_BYTES, ChildId(0)))
153    }
154}
155
156fn tagged_derive(tag: &[u8; 8], derivation: ChildId) -> [u8; 16] {
157    let mut derivation_info = [0u8; 16];
158    derivation_info[0..8].copy_from_slice(&tag[..]);
159    // The endianness isn't important here because we just need some bytes, but
160    // let's use the default for this project (big endian)
161    derivation_info[8..16].copy_from_slice(&derivation.0.to_be_bytes()[..]);
162    derivation_info
163}
164
165impl std::fmt::Debug for DerivableSecret {
166    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
167        write!(f, "DerivableSecret#")?;
168        fedimint_core::format_hex(
169            &self
170                .kdf
171                .derive::<8>(b"just a debug fingerprint derivation salt"),
172            f,
173        )
174    }
175}
176
177impl Encodable for DerivableSecret {
178    fn consensus_encode<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
179        (self.level as u64).consensus_encode(writer)?;
180        self.kdf.to_prk_bytes().consensus_encode(writer)
181    }
182}
183
184impl Decodable for DerivableSecret {
185    fn consensus_decode_partial<R: std::io::Read>(
186        reader: &mut R,
187        modules: &ModuleDecoderRegistry,
188    ) -> Result<Self, DecodeError> {
189        let level_u64 = u64::consensus_decode_partial(reader, modules)?;
190        let level = level_u64
191            .try_into()
192            .map_err(|_| DecodeError::from_str("DerivableSecret level out of range"))?;
193        let prk = <[u8; 64]>::consensus_decode_partial(reader, modules)?;
194
195        Ok(Self {
196            level,
197            kdf: Hkdf::from_prk_bytes(prk),
198        })
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::{ChildId, Decodable, DerivableSecret, Encodable};
205
206    #[test]
207    fn consensus_roundtrip_preserves_derivation() {
208        let original = DerivableSecret::new_root(b"root key", b"salt")
209            .child_key(ChildId(1))
210            .child_key(ChildId(2));
211
212        let encoded = original.consensus_encode_to_vec();
213        let decoded = DerivableSecret::consensus_decode_whole(&encoded, &Default::default())
214            .expect("decode should succeed");
215
216        assert_eq!(original.level(), decoded.level());
217        assert_eq!(
218            original.to_random_bytes::<32>(),
219            decoded.to_random_bytes::<32>()
220        );
221        assert_eq!(
222            original.child_key(ChildId(3)).to_random_bytes::<32>(),
223            decoded.child_key(ChildId(3)).to_random_bytes::<32>()
224        );
225    }
226}