iris_crypto/
slip10.rs

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