Skip to main content

tecdsa_core/
hd.rs

1//! BIP32 non-hardened public derivation for the 2-of-2 key (the same scheme as the reference
2//! implementation): the client adds `δ` to its share, so `X' = X + δ·G` and the server share
3//! stays unchanged. `δ` is the sum of the BIP32 tweaks along the path and is computed from the
4//! public key and the chain code only, so both parties get the same value.
5use alloc::vec::Vec;
6use k256::{ProjectivePoint, Scalar};
7use sha2::{Digest, Sha512};
8
9use crate::{
10    curve::{decode_scalar, encode_point},
11    error::{Error, Result},
12};
13
14/// Deepest path accepted (BIP44 needs 5).
15pub const MAX_PATH_DEPTH: usize = 16;
16
17const HARDENED: u32 = 1 << 31;
18
19/// Chain code and a path of non-hardened child indices.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct HdPath {
22    chain_code: [u8; 32],
23    path: Vec<u32>,
24}
25
26/// Child public key `X + δ·G` (33-byte compressed) for the compressed master key `x_pub`.
27pub fn derive_child_public_key(
28    x_pub: &[u8],
29    chain_code: [u8; 32],
30    path: Vec<u32>,
31) -> Result<[u8; 33]> {
32    let x = crate::curve::decode_point(x_pub)?;
33    let delta = HdPath::new(chain_code, path)?.delta(&x)?;
34    Ok(encode_point(&(x + ProjectivePoint::GENERATOR * delta)))
35}
36
37impl HdPath {
38    /// Hardened indices cannot be derived without the private key and are rejected.
39    pub fn new(chain_code: [u8; 32], path: Vec<u32>) -> Result<Self> {
40        if path.is_empty() || path.len() > MAX_PATH_DEPTH || path.iter().any(|i| i & HARDENED != 0)
41        {
42            return Err(Error::OutOfRange);
43        }
44        Ok(Self { chain_code, path })
45    }
46
47    pub fn chain_code(&self) -> &[u8; 32] {
48        &self.chain_code
49    }
50
51    pub fn path(&self) -> &[u32] {
52        &self.path
53    }
54
55    /// `δ` such that the child key is `X + δ·G`.
56    pub fn delta(&self, x_pub: &ProjectivePoint) -> Result<Scalar> {
57        let mut chain = self.chain_code;
58        let mut parent = *x_pub;
59        let mut delta = Scalar::ZERO;
60        for index in &self.path {
61            let mut data = [0u8; 37];
62            data[..33].copy_from_slice(&encode_point(&parent));
63            data[33..].copy_from_slice(&index.to_be_bytes());
64            let i = hmac_sha512(&chain, &data);
65            let tweak = decode_scalar(&i[..32])?; // IL ≥ q: BIP32 says "invalid child"
66            let child = parent + ProjectivePoint::GENERATOR * tweak;
67            if child == ProjectivePoint::IDENTITY {
68                return Err(Error::OutOfRange);
69            }
70            delta += tweak;
71            chain.copy_from_slice(&i[32..]);
72            parent = child;
73        }
74        Ok(delta)
75    }
76}
77
78fn hmac_sha512(key: &[u8; 32], data: &[u8]) -> [u8; 64] {
79    let mut ipad = [0x36u8; 128];
80    let mut opad = [0x5cu8; 128];
81    for (i, k) in key.iter().enumerate() {
82        ipad[i] ^= k;
83        opad[i] ^= k;
84    }
85    let mut inner = Sha512::new();
86    inner.update(ipad);
87    inner.update(data);
88    let inner = inner.finalize();
89    let mut outer = Sha512::new();
90    outer.update(opad);
91    outer.update(inner);
92    outer.finalize().into()
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::curve::decode_point;
99
100    fn hex(s: &str) -> Vec<u8> {
101        (0..s.len())
102            .step_by(2)
103            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
104            .collect()
105    }
106
107    #[test]
108    fn child_public_key_matches_delta() {
109        let x = ProjectivePoint::GENERATOR * Scalar::from(5u64);
110        let chain = [3u8; 32];
111        let child = derive_child_public_key(&encode_point(&x), chain, alloc::vec![7, 9]).unwrap();
112        let delta = HdPath::new(chain, alloc::vec![7, 9])
113            .unwrap()
114            .delta(&x)
115            .unwrap();
116        assert_eq!(
117            child,
118            encode_point(&(x + ProjectivePoint::GENERATOR * delta))
119        );
120        assert!(derive_child_public_key(&encode_point(&x), chain, alloc::vec![1 << 31]).is_err());
121        assert!(derive_child_public_key(&[2u8; 5], chain, alloc::vec![1]).is_err());
122    }
123
124    /// BIP32 test vector 2: m → m/0 (non-hardened).
125    #[test]
126    fn bip32_vector_2_first_level() {
127        let master = decode_point(&hex(
128            "03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7",
129        ))
130        .unwrap();
131        let chain: [u8; 32] =
132            hex("60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689")
133                .try_into()
134                .unwrap();
135        let hd = HdPath::new(chain, alloc::vec![0]).unwrap();
136        let child = master + ProjectivePoint::GENERATOR * hd.delta(&master).unwrap();
137        assert_eq!(
138            encode_point(&child).to_vec(),
139            hex("02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea")
140        );
141    }
142
143    #[test]
144    fn rejects_hardened_empty_and_deep_paths() {
145        assert!(HdPath::new([0; 32], alloc::vec![]).is_err());
146        assert!(HdPath::new([0; 32], alloc::vec![HARDENED]).is_err());
147        assert!(HdPath::new([0; 32], alloc::vec![1, HARDENED + 5]).is_err());
148        assert!(HdPath::new([0; 32], alloc::vec![0; MAX_PATH_DEPTH + 1]).is_err());
149        assert!(HdPath::new([0; 32], alloc::vec![44, 0, 0, 0, 7]).is_ok());
150    }
151
152    #[test]
153    fn path_order_and_chain_code_matter() {
154        let x = ProjectivePoint::GENERATOR * Scalar::from(12345u64);
155        let a = HdPath::new([1; 32], alloc::vec![1, 2])
156            .unwrap()
157            .delta(&x)
158            .unwrap();
159        let b = HdPath::new([1; 32], alloc::vec![2, 1])
160            .unwrap()
161            .delta(&x)
162            .unwrap();
163        let c = HdPath::new([2; 32], alloc::vec![1, 2])
164            .unwrap()
165            .delta(&x)
166            .unwrap();
167        assert!(a != b && a != c);
168    }
169}