Skip to main content

iris_crypto/
slip10.rs

1use arrayvec::ArrayVec;
2use hmac::{Hmac, Mac};
3use iris_ztd::crypto::cheetah::{ch_add, ch_scal_big, A_GEN, G_ORDER};
4use iris_ztd::U256;
5use sha2::Sha512;
6
7use crate::cheetah::{PrivateKey, PublicKey};
8
9fn hmac_sha512(key: &[u8], data: &[u8]) -> [u8; 64] {
10    let mut mac = Hmac::<Sha512>::new_from_slice(key).unwrap();
11    mac.update(data);
12    mac.finalize().into_bytes().into()
13}
14
15/// SLIP-10 Extended Key (private or public key + chain code)
16#[derive(Debug, Clone)]
17pub struct ExtendedKey {
18    pub private_key: Option<PrivateKey>,
19    pub public_key: PublicKey,
20    pub chain_code: [u8; 32],
21}
22
23impl ExtendedKey {
24    /// Derive a child key at the given index using SLIP-10
25    pub fn derive_child(&self, index: u32) -> ExtendedKey {
26        let hardened = index >= (1 << 31);
27
28        let mut data = ArrayVec::<_, { 1 + 96 + 4 }>::new();
29        if hardened {
30            let private_key = self
31                .private_key
32                .as_ref()
33                .expect("Cannot derive hardened child without private key");
34            data.push(0x00);
35            data.try_extend_from_slice(&private_key.to_be_bytes())
36                .unwrap();
37            data.try_extend_from_slice(&index.to_be_bytes()).unwrap();
38        } else {
39            data.push(0x01);
40            data.try_extend_from_slice(&self.public_key.as_slip10_bytes())
41                .unwrap();
42            data.try_extend_from_slice(&index.to_be_bytes()).unwrap();
43        }
44        let mut result = hmac_sha512(&self.chain_code, &data);
45
46        loop {
47            let left = U256::from_be_slice(&result[..32]);
48            let mut chain_code = [0u8; 32];
49            chain_code.copy_from_slice(&result[32..]);
50
51            if left < G_ORDER {
52                match self.private_key.as_ref() {
53                    Some(pk) => {
54                        let s = left.add_mod(&pk.0, &G_ORDER);
55                        if s != U256::ZERO {
56                            let private_key = PrivateKey(s);
57                            let public_key = private_key.public_key();
58                            return ExtendedKey {
59                                private_key: Some(private_key),
60                                public_key,
61                                chain_code,
62                            };
63                        }
64                    }
65                    None => {
66                        let mut point = ch_scal_big(&left, &A_GEN).unwrap();
67                        point = ch_add(&point, &self.public_key.0).unwrap();
68                        if !point.inf {
69                            return ExtendedKey {
70                                private_key: None,
71                                public_key: PublicKey(point),
72                                chain_code,
73                            };
74                        }
75                    }
76                }
77            }
78            // Invalid key: rehash 0x01 || right || index
79            let mut data = ArrayVec::<_, { 1 + 32 + 4 }>::new();
80            data.push(0x01);
81            data.try_extend_from_slice(&chain_code).unwrap();
82            data.try_extend_from_slice(&index.to_be_bytes()).unwrap();
83            result = hmac_sha512(&self.chain_code, &data);
84        }
85    }
86}
87
88pub fn derive_master_key(seed: &[u8]) -> ExtendedKey {
89    const DOMAIN_SEPARATOR: &[u8] = b"Nockchain seed";
90    let mut result = hmac_sha512(DOMAIN_SEPARATOR, seed);
91    loop {
92        let s = U256::from_be_slice(&result[..32]);
93        let mut chain_code = [0u8; 32];
94        chain_code.copy_from_slice(&result[32..]);
95        if s < G_ORDER && s != U256::ZERO {
96            let private_key = PrivateKey(s);
97            let public_key = private_key.public_key();
98            return ExtendedKey {
99                private_key: Some(private_key),
100                public_key,
101                chain_code,
102            };
103        }
104        result = hmac_sha512(DOMAIN_SEPARATOR, &result[..]);
105    }
106}
107
108#[cfg(feature = "alloc")]
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use alloc::vec::Vec;
113    use bip39::Mnemonic;
114    use iris_ztd::{Belt, Hashable, NounEncode};
115
116    #[cfg(feature = "alloc")]
117    fn from_b58(s: &str) -> Vec<u8> {
118        bs58::decode(s).into_vec().unwrap()
119    }
120
121    #[test]
122    fn test_nockchain_wallet_vector() {
123        // Test vectors from:
124        //   nockchain-wallet keygen
125        // and:
126        //   nockchain-wallet derive-child 0
127        //   nockchain-wallet derive-child --hardened 0
128        let mnemonic = Mnemonic::parse("clutch inmate mango seek attract credit illegal popular term loyal fiber output trumpet lucky garbage merge menu certain dynamic aim trip fantasy master unveil").unwrap();
129        let key = derive_master_key(&mnemonic.to_seed(""));
130        assert_eq!(
131            key.private_key.as_ref().unwrap().to_be_bytes().to_vec(),
132            from_b58("3MoHxVXWAr9qny12Sw8ZZtrgEBFcZegQQVkwYyePb9LZ")
133        );
134        assert_eq!(
135            key.chain_code[..],
136            from_b58("3NhBRdy7vRw8vKQ5RnR3CNcD43WDn5Ky7mhhotqUcaiR")
137        );
138
139        let child_key = key.derive_child(0);
140        assert_eq!(
141            child_key.private_key.unwrap().to_be_bytes().to_vec(),
142            from_b58("6AifHLAuT1MxnFsoCwjKNFaBze91DXFDV1rRLefkzPEK")
143        );
144        assert_eq!(
145            child_key.chain_code[..],
146            from_b58("8NL75o1uwMpGFcLRrnFt9adTyExwK9MP6RL8h2jAKEVD")
147        );
148
149        let hardened_child_key = key.derive_child(1 << 31);
150        assert_eq!(
151            hardened_child_key
152                .private_key
153                .unwrap()
154                .to_be_bytes()
155                .to_vec(),
156            from_b58("CpMAmcgN1V6Majtx2HC7ULLXD9psA3Gg3nMye3JpKpH")
157        );
158        assert_eq!(
159            hardened_child_key.chain_code[..],
160            from_b58("8x7zh5LQA7tsFQQ3qsPfYGgFzQkoizGhLqLK7iKTGj3R")
161        );
162    }
163
164    #[test]
165    fn test_nockchain_message_vector() {
166        // Test vector from: nockchain-wallet sign-message "hello"
167        let mnemonic = Mnemonic::parse("kangaroo gap pair wonder grid version winter burden garment resemble object trap survey custom mask fiber anger hospital conduct draft page hello embark core").unwrap();
168        let sig = &derive_master_key(&mnemonic.to_seed(""))
169            .private_key
170            .unwrap()
171            .sign(&Belt::from_bytes(b"hello").to_noun().hash());
172        assert_eq!(
173            sig.to_noun().hash().to_bytes().to_vec(),
174            from_b58("AJMwSLmz1k9YnDa1iQTVbpz4Jr4hZojxeCHiqLY7TnjYLxXjZtmbskw")
175        );
176    }
177}