confium_tc_keys/
threshold_bip32.rs1use p256::elliptic_curve::PrimeField;
8use p256::{FieldBytes, Scalar};
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub struct PathIndex {
15 pub index: u32,
16 pub hardened: bool,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct DerivationPath {
22 pub components: Vec<PathIndex>,
23}
24
25impl DerivationPath {
26 pub fn new() -> Self {
27 Self {
28 components: Vec::new(),
29 }
30 }
31
32 pub fn push(&mut self, index: u32, hardened: bool) -> &mut Self {
33 self.components.push(PathIndex { index, hardened });
34 self
35 }
36
37 pub fn depth(&self) -> usize {
38 self.components.len()
39 }
40}
41
42impl Default for DerivationPath {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
54 loop {
55 if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
56 return s;
57 }
58 let mut h = Sha256::new();
59 h.update(b"confium-scalar-reduce-v1");
60 h.update(bytes);
61 bytes = h.finalize().into();
62 }
63}
64
65pub fn derive_child_scalar(parent: &Scalar, component: &PathIndex) -> Scalar {
66 let parent_bytes = parent.to_repr();
67 let mut hasher = Sha256::new();
68 hasher.update(b"confium-bip32-v1");
69 hasher.update(parent_bytes);
70 if component.hardened {
71 hasher.update(b"H");
72 } else {
73 hasher.update(b"N");
74 }
75 hasher.update(component.index.to_be_bytes());
76 let hash = hasher.finalize();
77
78 let bytes: [u8; 32] = hash.into();
79 reduce_to_scalar(bytes)
80}
81
82pub fn derive_path(parent: &Scalar, path: &DerivationPath) -> Scalar {
84 let mut current = *parent;
85 for component in &path.components {
86 current = derive_child_scalar(¤t, component);
87 }
88 current
89}
90
91pub fn derive_party_share(parent_share: &Scalar, party_idx: u32, path: &DerivationPath) -> Scalar {
95 let mut current = *parent_share;
96 for component in &path.components {
97 let mut c = *component;
98 let mixed_index = c.index.wrapping_add(party_idx);
100 c.index = mixed_index;
101 current = derive_child_scalar(¤t, &c);
102 }
103 current
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use getrandom::SysRng;
110 use p256::elliptic_curve::Field;
111 use p256::elliptic_curve::rand_core::UnwrapErr;
112
113 fn random_scalar() -> Scalar {
114 Scalar::random(&mut UnwrapErr(SysRng))
115 }
116
117 #[test]
118 fn derivation_is_deterministic() {
119 let parent = random_scalar();
120 let mut path = DerivationPath::new();
121 path.push(0, false).push(1, true);
122 let child1 = derive_path(&parent, &path);
123 let child2 = derive_path(&parent, &path);
124 assert_eq!(child1, child2);
125 }
126
127 #[test]
128 fn different_parents_different_children() {
129 let p1 = random_scalar();
130 let p2 = random_scalar();
131 let path = DerivationPath::new();
132 let c1 = derive_path(&p1, &path);
133 let c2 = derive_path(&p2, &path);
134 assert_ne!(c1, c2);
135 }
136
137 #[test]
138 fn different_paths_different_children() {
139 let parent = random_scalar();
140 let mut path1 = DerivationPath::new();
141 path1.push(0, false);
142 let mut path2 = DerivationPath::new();
143 path2.push(1, false);
144 assert_ne!(derive_path(&parent, &path1), derive_path(&parent, &path2));
145 }
146
147 #[test]
148 fn hardened_vs_unhardened_differ() {
149 let parent = random_scalar();
150 let mut h = DerivationPath::new();
151 h.push(0, true);
152 let mut n = DerivationPath::new();
153 n.push(0, false);
154 assert_ne!(derive_path(&parent, &h), derive_path(&parent, &n));
155 }
156
157 #[test]
158 fn empty_path_returns_parent() {
159 let parent = random_scalar();
160 let path = DerivationPath::new();
161 assert_eq!(derive_path(&parent, &path), parent);
162 }
163
164 #[test]
165 fn depth_counts_components() {
166 let mut path = DerivationPath::new();
167 assert_eq!(path.depth(), 0);
168 path.push(0, false);
169 assert_eq!(path.depth(), 1);
170 path.push(1, true);
171 assert_eq!(path.depth(), 2);
172 }
173
174 #[test]
175 fn party_shares_differ_by_party() {
176 let parent = random_scalar();
177 let mut path = DerivationPath::new();
178 path.push(0, false);
179 let s1 = derive_party_share(&parent, 1, &path);
180 let s2 = derive_party_share(&parent, 2, &path);
181 assert_ne!(s1, s2);
182 }
183
184 #[test]
185 fn party_share_deterministic() {
186 let parent = random_scalar();
187 let mut path = DerivationPath::new();
188 path.push(42, true);
189 let s1 = derive_party_share(&parent, 1, &path);
190 let s2 = derive_party_share(&parent, 1, &path);
191 assert_eq!(s1, s2);
192 }
193
194 #[test]
195 fn deep_path_works() {
196 let parent = random_scalar();
197 let mut path = DerivationPath::new();
198 for i in 0..10 {
199 path.push(i, i % 2 == 0);
200 }
201 let child = derive_path(&parent, &path);
202 let _bytes = child.to_repr();
204 }
205
206 #[test]
207 fn path_serializes() {
208 let mut path = DerivationPath::new();
209 path.push(0, false).push(1, true);
210 let json = serde_json::to_string(&path).unwrap();
211 assert!(json.contains("components"));
212 }
213}