kobe_primitives/
slip10.rs1use alloc::format;
9use alloc::string::String;
10
11use ed25519_dalek::SigningKey;
12use hmac::{Hmac, KeyInit, Mac};
13use sha2::Sha512;
14use zeroize::Zeroizing;
15
16use crate::DeriveError;
17
18type HmacSha512 = Hmac<Sha512>;
20
21const ED25519_CURVE: &[u8] = b"ed25519 seed";
23
24#[non_exhaustive]
29pub struct DerivedKey {
30 pub private_key: Zeroizing<[u8; 32]>,
32 pub chain_code: Zeroizing<[u8; 32]>,
34}
35
36impl core::fmt::Debug for DerivedKey {
37 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38 let pk = self.to_signing_key().verifying_key();
39 f.debug_struct("DerivedKey")
40 .field("public_key", &hex::encode(pk.as_bytes()))
41 .finish_non_exhaustive()
42 }
43}
44
45impl DerivedKey {
46 pub fn from_seed(seed: &[u8]) -> Result<Self, DeriveError> {
52 let mut mac = HmacSha512::new_from_slice(ED25519_CURVE)
53 .map_err(|_| DeriveError::Crypto(String::from("slip10: invalid seed length")))?;
54 mac.update(seed);
55 let result = mac.finalize().into_bytes();
56
57 let (pk_bytes, cc_bytes) = result.split_at(32);
58 let mut private_key = Zeroizing::new([0u8; 32]);
59 let mut chain_code = Zeroizing::new([0u8; 32]);
60 private_key.copy_from_slice(pk_bytes);
61 chain_code.copy_from_slice(cc_bytes);
62
63 Ok(Self {
64 private_key,
65 chain_code,
66 })
67 }
68
69 pub fn derive_hardened(&self, index: u32) -> Result<Self, DeriveError> {
78 let hardened_index = index | 0x8000_0000;
79
80 let mut mac = HmacSha512::new_from_slice(&*self.chain_code).map_err(|_| {
81 DeriveError::Crypto(String::from("slip10: chain code HMAC init failed"))
82 })?;
83 mac.update(&[0x00]);
84 mac.update(&*self.private_key);
85 mac.update(&hardened_index.to_be_bytes());
86 let result = mac.finalize().into_bytes();
87
88 let (pk_bytes, cc_bytes) = result.split_at(32);
89 let mut private_key = Zeroizing::new([0u8; 32]);
90 let mut chain_code = Zeroizing::new([0u8; 32]);
91 private_key.copy_from_slice(pk_bytes);
92 chain_code.copy_from_slice(cc_bytes);
93
94 Ok(Self {
95 private_key,
96 chain_code,
97 })
98 }
99
100 pub fn derive_path(seed: &[u8], path: &str) -> Result<Self, DeriveError> {
109 let trimmed = path.trim();
110 let remainder = if trimmed == "m" {
111 ""
112 } else if let Some(rest) = trimmed.strip_prefix("m/") {
113 rest
114 } else {
115 return Err(DeriveError::Path(String::from(
116 "slip10: path must start with 'm/' or be exactly 'm'",
117 )));
118 };
119
120 let mut current = Self::from_seed(seed)?;
121 for component in remainder.split('/').filter(|s| !s.is_empty()) {
122 let index = parse_path_component(component)?;
123 current = current.derive_hardened(index)?;
124 }
125 Ok(current)
126 }
127
128 #[must_use]
130 pub fn to_signing_key(&self) -> SigningKey {
131 SigningKey::from_bytes(&self.private_key)
132 }
133}
134
135fn parse_path_component(component: &str) -> Result<u32, DeriveError> {
137 let stripped = component.trim_end_matches('\'').trim_end_matches('h');
138 stripped
139 .parse::<u32>()
140 .map_err(|_| DeriveError::Path(format!("slip10: invalid path component: {component}")))
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 const TV1_SEED: &str = "000102030405060708090a0b0c0d0e0f";
150
151 #[test]
152 fn slip10_vector1_chain_m() {
153 let seed = hex::decode(TV1_SEED).unwrap();
154 let master = DerivedKey::from_seed(&seed).unwrap();
155 assert_eq!(
156 hex::encode(*master.private_key),
157 "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7"
158 );
159 assert_eq!(
160 hex::encode(*master.chain_code),
161 "90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb"
162 );
163 }
164
165 #[test]
166 fn slip10_vector1_chain_m_0h() {
167 let seed = hex::decode(TV1_SEED).unwrap();
168 let derived = DerivedKey::derive_path(&seed, "m/0'").unwrap();
169 assert_eq!(
170 hex::encode(*derived.private_key),
171 "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3"
172 );
173 assert_eq!(
174 hex::encode(*derived.chain_code),
175 "8b59aa11380b624e81507a27fedda59fea6d0b779a778918a2fd3590e16e9c69"
176 );
177 }
178
179 #[test]
180 fn slip10_vector1_chain_m_0h_1h() {
181 let seed = hex::decode(TV1_SEED).unwrap();
182 let derived = DerivedKey::derive_path(&seed, "m/0'/1'").unwrap();
183 assert_eq!(
184 hex::encode(*derived.private_key),
185 "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2"
186 );
187 assert_eq!(
188 hex::encode(*derived.chain_code),
189 "a320425f77d1b5c2505a6b1b27382b37368ee640e3557c315416801243552f14"
190 );
191 }
192
193 #[test]
194 fn slip10_vector1_chain_m_0h_1h_2h() {
195 let seed = hex::decode(TV1_SEED).unwrap();
196 let derived = DerivedKey::derive_path(&seed, "m/0'/1'/2'").unwrap();
197 assert_eq!(
198 hex::encode(*derived.private_key),
199 "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9"
200 );
201 assert_eq!(
202 hex::encode(*derived.chain_code),
203 "2e69929e00b5ab250f49c3fb1c12f252de4fed2c1db88387094a0f8c4c9ccd6c"
204 );
205 }
206
207 #[test]
208 fn slip10_vector1_chain_m_0h_1h_2h_2h() {
209 let seed = hex::decode(TV1_SEED).unwrap();
210 let derived = DerivedKey::derive_path(&seed, "m/0'/1'/2'/2'").unwrap();
211 assert_eq!(
212 hex::encode(*derived.private_key),
213 "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662"
214 );
215 assert_eq!(
216 hex::encode(*derived.chain_code),
217 "8f6d87f93d750e0efccda017d662a1b31a266e4a6f5993b15f5c1f07f74dd5cc"
218 );
219 }
220
221 #[test]
222 fn slip10_vector1_chain_m_0h_1h_2h_2h_1000000000h() {
223 let seed = hex::decode(TV1_SEED).unwrap();
224 let derived = DerivedKey::derive_path(&seed, "m/0'/1'/2'/2'/1000000000'").unwrap();
225 assert_eq!(
226 hex::encode(*derived.private_key),
227 "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793"
228 );
229 assert_eq!(
230 hex::encode(*derived.chain_code),
231 "68789923a0cac2cd5a29172a475fe9e0fb14cd6adb5ad98a3fa70333e7afa230"
232 );
233 }
234
235 const TV2_SEED: &str = "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542";
237
238 #[test]
239 fn slip10_vector2_chain_m() {
240 let seed = hex::decode(TV2_SEED).unwrap();
241 let master = DerivedKey::from_seed(&seed).unwrap();
242 assert_eq!(
243 hex::encode(*master.private_key),
244 "171cb88b1b3c1db25add599712e36245d75bc65a1a5c9e18d76f9f2b1eab4012"
245 );
246 assert_eq!(
247 hex::encode(*master.chain_code),
248 "ef70a74db9c3a5af931b5fe73ed8e1a53464133654fd55e7a66f8570b8e33c3b"
249 );
250 }
251
252 #[test]
253 fn slip10_vector2_chain_m_0h() {
254 let seed = hex::decode(TV2_SEED).unwrap();
255 let derived = DerivedKey::derive_path(&seed, "m/0'").unwrap();
256 assert_eq!(
257 hex::encode(*derived.private_key),
258 "1559eb2bbec5790b0c65d8693e4d0875b1747f4970ae8b650486ed7470845635"
259 );
260 assert_eq!(
261 hex::encode(*derived.chain_code),
262 "0b78a3226f915c082bf118f83618a618ab6dec793752624cbeb622acb562862d"
263 );
264 }
265
266 #[test]
267 fn master_only_path() {
268 let seed = hex::decode(TV1_SEED).unwrap();
269 let m = DerivedKey::from_seed(&seed).unwrap();
270 let m2 = DerivedKey::derive_path(&seed, "m").unwrap();
271 assert_eq!(*m.private_key, *m2.private_key);
272 assert_eq!(*m.chain_code, *m2.chain_code);
273 }
274
275 #[test]
276 fn h_suffix_accepted() {
277 let seed = hex::decode(TV1_SEED).unwrap();
278 let a = DerivedKey::derive_path(&seed, "m/0'").unwrap();
279 let b = DerivedKey::derive_path(&seed, "m/0h").unwrap();
280 assert_eq!(*a.private_key, *b.private_key);
281 }
282
283 #[test]
284 fn different_indices_produce_different_keys() {
285 let seed = hex::decode(TV1_SEED).unwrap();
286 let k0 = DerivedKey::derive_path(&seed, "m/44'/501'/0'").unwrap();
287 let k1 = DerivedKey::derive_path(&seed, "m/44'/501'/1'").unwrap();
288 assert_ne!(*k0.private_key, *k1.private_key);
289 }
290
291 #[test]
292 fn invalid_path_rejected() {
293 let seed = hex::decode(TV1_SEED).unwrap();
294 assert!(DerivedKey::derive_path(&seed, "bad/path").is_err());
295 assert!(DerivedKey::derive_path(&seed, "m/abc").is_err());
296 }
297
298 #[test]
299 fn signing_key_roundtrip() {
300 let seed = hex::decode(TV1_SEED).unwrap();
301 let derived = DerivedKey::from_seed(&seed).unwrap();
302 let sk = derived.to_signing_key();
303 assert_eq!(sk.to_bytes(), *derived.private_key);
304 }
305}