fedimint_derive_secret/
lib.rs1use 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#[derive(Debug, Copy, Clone, Encodable, Decodable)]
35pub struct ChildId(pub u64);
36
37#[derive(Clone)]
39pub struct DerivableSecret {
40 level: usize,
42 kdf: Hkdf<Sha512>,
47}
48
49impl DerivableSecret {
50 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 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 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 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 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 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 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 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 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 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}