aztec_crypto/
keys.rs

1//! Key derivation for the Aztec protocol.
2//!
3//! Implements the full key hierarchy: master secret keys via SHA-512,
4//! master public keys via Grumpkin scalar multiplication, and
5//! app-scoped keys via Poseidon2.
6
7use aztec_core::constants::domain_separator;
8use aztec_core::grumpkin;
9use aztec_core::hash::poseidon2_hash_with_separator;
10use aztec_core::types::{AztecAddress, Fr, GrumpkinScalar, Point, PublicKeys};
11
12use crate::sha512::sha512_to_grumpkin_scalar;
13
14// ---------------------------------------------------------------------------
15// Master key derivation (Sub-step 7.3)
16// ---------------------------------------------------------------------------
17
18/// Derive the master nullifier hiding key from a secret key.
19pub fn derive_master_nullifier_hiding_key(secret_key: &Fr) -> GrumpkinScalar {
20    sha512_to_grumpkin_scalar(secret_key, domain_separator::NHK_M)
21}
22
23/// Derive the master incoming viewing secret key from a secret key.
24pub fn derive_master_incoming_viewing_secret_key(secret_key: &Fr) -> GrumpkinScalar {
25    sha512_to_grumpkin_scalar(secret_key, domain_separator::IVSK_M)
26}
27
28/// Derive the master outgoing viewing secret key from a secret key.
29pub fn derive_master_outgoing_viewing_secret_key(secret_key: &Fr) -> GrumpkinScalar {
30    sha512_to_grumpkin_scalar(secret_key, domain_separator::OVSK_M)
31}
32
33/// Derive the master tagging secret key from a secret key.
34pub fn derive_master_tagging_secret_key(secret_key: &Fr) -> GrumpkinScalar {
35    sha512_to_grumpkin_scalar(secret_key, domain_separator::TSK_M)
36}
37
38/// Derive the signing key from a secret key.
39///
40/// Currently uses the same derivation as IVSK_M (see TS TODO #5837).
41pub fn derive_signing_key(secret_key: &Fr) -> GrumpkinScalar {
42    sha512_to_grumpkin_scalar(secret_key, domain_separator::IVSK_M)
43}
44
45// ---------------------------------------------------------------------------
46// Public key derivation (Sub-step 7.4)
47// ---------------------------------------------------------------------------
48
49/// Derive a Grumpkin public key from a secret key via scalar multiplication.
50///
51/// `public_key = secret_key * G`
52pub fn derive_public_key_from_secret_key(secret_key: &GrumpkinScalar) -> Point {
53    let g = grumpkin::generator();
54    grumpkin::scalar_mul(secret_key, &g)
55}
56
57// ---------------------------------------------------------------------------
58// Full key set derivation (Sub-step 7.5)
59// ---------------------------------------------------------------------------
60
61/// The complete set of derived keys from a secret key.
62pub struct DerivedKeys {
63    /// Master nullifier hiding key (secret).
64    pub master_nullifier_hiding_key: GrumpkinScalar,
65    /// Master incoming viewing secret key.
66    pub master_incoming_viewing_secret_key: GrumpkinScalar,
67    /// Master outgoing viewing secret key.
68    pub master_outgoing_viewing_secret_key: GrumpkinScalar,
69    /// Master tagging secret key.
70    pub master_tagging_secret_key: GrumpkinScalar,
71    /// The four master public keys.
72    pub public_keys: PublicKeys,
73}
74
75/// Derive the complete key set from a secret key.
76pub fn derive_keys(secret_key: &Fr) -> DerivedKeys {
77    // 1. Derive master secret keys via SHA-512
78    let nhk_m = derive_master_nullifier_hiding_key(secret_key);
79    let ivsk_m = derive_master_incoming_viewing_secret_key(secret_key);
80    let ovsk_m = derive_master_outgoing_viewing_secret_key(secret_key);
81    let tsk_m = derive_master_tagging_secret_key(secret_key);
82
83    // 2. Derive master public keys via Grumpkin scalar multiplication
84    let npk_m = derive_public_key_from_secret_key(&nhk_m);
85    let ivpk_m = derive_public_key_from_secret_key(&ivsk_m);
86    let ovpk_m = derive_public_key_from_secret_key(&ovsk_m);
87    let tpk_m = derive_public_key_from_secret_key(&tsk_m);
88
89    DerivedKeys {
90        master_nullifier_hiding_key: nhk_m,
91        master_incoming_viewing_secret_key: ivsk_m,
92        master_outgoing_viewing_secret_key: ovsk_m,
93        master_tagging_secret_key: tsk_m,
94        public_keys: PublicKeys {
95            master_nullifier_public_key: npk_m,
96            master_incoming_viewing_public_key: ivpk_m,
97            master_outgoing_viewing_public_key: ovpk_m,
98            master_tagging_public_key: tpk_m,
99        },
100    }
101}
102
103// ---------------------------------------------------------------------------
104// App-scoped key derivation (Sub-step 7.6)
105// ---------------------------------------------------------------------------
106
107/// Identifies a key type for app-scoped derivation.
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum KeyType {
110    Nullifier,
111    IncomingViewing,
112    OutgoingViewing,
113    Tagging,
114}
115
116impl KeyType {
117    pub fn domain_separator(&self) -> u32 {
118        match self {
119            KeyType::Nullifier => domain_separator::NHK_M,
120            KeyType::IncomingViewing => domain_separator::IVSK_M,
121            KeyType::OutgoingViewing => domain_separator::OVSK_M,
122            KeyType::Tagging => domain_separator::TSK_M,
123        }
124    }
125}
126
127/// Compute an app-scoped secret key from a master key and app address.
128///
129/// `app_key = poseidon2([master_key.hi, master_key.lo, app], domain_separator)`
130pub fn compute_app_secret_key(
131    master_key: &GrumpkinScalar,
132    app: &AztecAddress,
133    key_type: KeyType,
134) -> Fr {
135    let hi = master_key.hi();
136    let lo = master_key.lo();
137    let separator = key_type.domain_separator();
138    poseidon2_hash_with_separator(&[hi, lo, Fr::from(*app)], separator)
139}
140
141/// Compute the app-scoped nullifier hiding key.
142pub fn compute_app_nullifier_hiding_key(
143    master_nullifier_hiding_key: &GrumpkinScalar,
144    app: &AztecAddress,
145) -> Fr {
146    compute_app_secret_key(master_nullifier_hiding_key, app, KeyType::Nullifier)
147}
148
149/// Compute the app-scoped outgoing viewing secret key.
150///
151/// Returns `GrumpkinScalar` (Fq) because the result is used for scalar
152/// multiplication when encrypting outgoing note logs.
153pub fn compute_ovsk_app(
154    master_outgoing_viewing_key: &GrumpkinScalar,
155    app: &AztecAddress,
156) -> GrumpkinScalar {
157    let fr_result =
158        compute_app_secret_key(master_outgoing_viewing_key, app, KeyType::OutgoingViewing);
159    // Intentional Fr -> Fq conversion. Distribution is not perfectly uniform
160    // but 2*(q-r)/q is negligibly small.
161    GrumpkinScalar::from_be_bytes_mod_order(&fr_result.to_be_bytes())
162}
163
164#[cfg(test)]
165#[allow(clippy::expect_used)]
166mod tests {
167    use super::*;
168
169    // TS test vectors from key_store.test.ts with secret_key = 8923n
170
171    #[test]
172    fn master_key_derivation_nhk_m() {
173        let sk = Fr::from(8923u64);
174        let nhk_m = derive_master_nullifier_hiding_key(&sk);
175        let expected = GrumpkinScalar::from_hex(
176            "0x26dd6f83a99b5b1cea47692f40b7aece47756a1a5e93138c5b8f7e7afd36ed1a",
177        )
178        .expect("valid hex");
179        assert_eq!(nhk_m, expected);
180    }
181
182    #[test]
183    fn master_key_derivation_ivsk_m() {
184        let sk = Fr::from(8923u64);
185        let ivsk_m = derive_master_incoming_viewing_secret_key(&sk);
186        let expected = GrumpkinScalar::from_hex(
187            "0x0d3e4402946f2f712d942e1a3962b12fc521effc39fe93777f91285f1ad414cb",
188        )
189        .expect("valid hex");
190        assert_eq!(ivsk_m, expected);
191    }
192
193    #[test]
194    fn public_key_derivation_npk_m() {
195        let sk = Fr::from(8923u64);
196        let nhk_m = derive_master_nullifier_hiding_key(&sk);
197        let npk_m = derive_public_key_from_secret_key(&nhk_m);
198        let expected_x =
199            Fr::from_hex("0x0d86b380f66ec74d32bb04d98f5b2dcef6d92f344e65604a21640f87fb6d078e")
200                .expect("valid hex");
201        let expected_y =
202            Fr::from_hex("0x2b68df4d20985b71c252746a3f2cc5af32b5f0c32739b94f166dfa230f50397b")
203                .expect("valid hex");
204        assert_eq!(npk_m.x, expected_x);
205        assert_eq!(npk_m.y, expected_y);
206        assert!(!npk_m.is_infinite);
207    }
208
209    #[test]
210    fn public_key_derivation_ivpk_m() {
211        let sk = Fr::from(8923u64);
212        let ivsk_m = derive_master_incoming_viewing_secret_key(&sk);
213        let ivpk_m = derive_public_key_from_secret_key(&ivsk_m);
214        let expected_x =
215            Fr::from_hex("0x0e0eb5bc3eb9959d6e05cbc0e37b2fa4cfb113c1db651c384907547f1f867010")
216                .expect("valid hex");
217        let expected_y =
218            Fr::from_hex("0x1db2e49c6845619ba432a951d86de2d41680157b0f54556246916900c0fcdcf2")
219                .expect("valid hex");
220        assert_eq!(ivpk_m.x, expected_x);
221        assert_eq!(ivpk_m.y, expected_y);
222    }
223
224    #[test]
225    fn public_key_derivation_ovpk_m() {
226        let sk = Fr::from(8923u64);
227        let ovsk_m = derive_master_outgoing_viewing_secret_key(&sk);
228        let ovpk_m = derive_public_key_from_secret_key(&ovsk_m);
229        let expected_x =
230            Fr::from_hex("0x2721eaed30c0c9fae14c2ca4af7668a46278762d4a6066ab7a5defcc242f559c")
231                .expect("valid hex");
232        let expected_y =
233            Fr::from_hex("0x0bd0c4b0ec90ebafe511f20e818fb359a1322ab0f02fe3ebec95af5df502015d")
234                .expect("valid hex");
235        assert_eq!(ovpk_m.x, expected_x);
236        assert_eq!(ovpk_m.y, expected_y);
237    }
238
239    #[test]
240    fn public_key_derivation_tpk_m() {
241        let sk = Fr::from(8923u64);
242        let tsk_m = derive_master_tagging_secret_key(&sk);
243        let tpk_m = derive_public_key_from_secret_key(&tsk_m);
244        let expected_x =
245            Fr::from_hex("0x0fabb6adca7c2bf7f6202c65fe2785096efb317897bc545c427635a61d536955")
246                .expect("valid hex");
247        let expected_y =
248            Fr::from_hex("0x2cc356e6e5b68fd64d33c96fad7bb1394956c53930fefdf0bb536812ec604459")
249                .expect("valid hex");
250        assert_eq!(tpk_m.x, expected_x);
251        assert_eq!(tpk_m.y, expected_y);
252    }
253
254    #[test]
255    fn derive_keys_full_round_trip() {
256        let sk = Fr::from(8923u64);
257        let derived = derive_keys(&sk);
258
259        // Verify nhk_m
260        let expected_nhk_m = GrumpkinScalar::from_hex(
261            "0x26dd6f83a99b5b1cea47692f40b7aece47756a1a5e93138c5b8f7e7afd36ed1a",
262        )
263        .expect("valid hex");
264        assert_eq!(derived.master_nullifier_hiding_key, expected_nhk_m);
265
266        // Verify npk_m
267        let expected_npk_x =
268            Fr::from_hex("0x0d86b380f66ec74d32bb04d98f5b2dcef6d92f344e65604a21640f87fb6d078e")
269                .expect("valid hex");
270        assert_eq!(
271            derived.public_keys.master_nullifier_public_key.x,
272            expected_npk_x
273        );
274
275        // Verify ivpk_m
276        let expected_ivpk_x =
277            Fr::from_hex("0x0e0eb5bc3eb9959d6e05cbc0e37b2fa4cfb113c1db651c384907547f1f867010")
278                .expect("valid hex");
279        assert_eq!(
280            derived.public_keys.master_incoming_viewing_public_key.x,
281            expected_ivpk_x
282        );
283
284        // Verify public_keys_hash is non-zero
285        let pk_hash = derived.public_keys.hash();
286        assert!(!pk_hash.is_zero());
287    }
288
289    #[test]
290    fn app_nullifier_hiding_key() {
291        let sk = Fr::from(8923u64);
292        let nhk_m = derive_master_nullifier_hiding_key(&sk);
293        let app = AztecAddress::from(624u64);
294        let app_nhk = compute_app_nullifier_hiding_key(&nhk_m, &app);
295        let expected =
296            Fr::from_hex("0x165cc265d187ed42f0e3f5adbb5a0055a77e205daeb68dd1735796ee402e502f")
297                .expect("valid hex");
298        assert_eq!(app_nhk, expected);
299    }
300
301    #[test]
302    fn app_ovsk() {
303        let sk = Fr::from(8923u64);
304        let ovsk_m = derive_master_outgoing_viewing_secret_key(&sk);
305        let app = AztecAddress::from(624u64);
306        let ovsk_app = compute_ovsk_app(&ovsk_m, &app);
307        let expected = GrumpkinScalar::from_hex(
308            "0x058452c94b1d8540a39d9343758fc132af3401237bd1ac2a16c37462a173954a",
309        )
310        .expect("valid hex");
311        assert_eq!(ovsk_app, expected);
312    }
313
314    #[test]
315    fn signing_key_equals_ivsk() {
316        let sk = Fr::from(8923u64);
317        let ivsk = derive_master_incoming_viewing_secret_key(&sk);
318        let signing = derive_signing_key(&sk);
319        assert_eq!(ivsk, signing);
320    }
321
322    #[test]
323    fn fq_hi_lo_split() {
324        let sk = Fr::from(8923u64);
325        let nhk_m = derive_master_nullifier_hiding_key(&sk);
326
327        let hi = nhk_m.hi();
328        let lo = nhk_m.lo();
329
330        // Both should be non-zero for a typical key
331        assert!(!hi.is_zero());
332        assert!(!lo.is_zero());
333
334        // Reconstruct: (hi << 128) | lo should equal original (within Fq bounds)
335        let hi_bytes = hi.to_be_bytes();
336        let lo_bytes = lo.to_be_bytes();
337        let mut reconstructed = [0u8; 32];
338        // hi occupies bytes [0..16], lo occupies bytes [16..32]
339        reconstructed[..16].copy_from_slice(&hi_bytes[16..]);
340        reconstructed[16..].copy_from_slice(&lo_bytes[16..]);
341        let original_bytes = nhk_m.to_be_bytes();
342        assert_eq!(reconstructed, original_bytes);
343    }
344}