diff --git a/key_protocol/src/key_management/key_tree/keys_public.rs b/key_protocol/src/key_management/key_tree/keys_public.rs index 4671795d..de4fb726 100644 --- a/key_protocol/src/key_management/key_tree/keys_public.rs +++ b/key_protocol/src/key_management/key_tree/keys_public.rs @@ -1,4 +1,4 @@ -use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}; +use k256::elliptic_curve::PrimeField as _; use serde::{Deserialize, Serialize}; use crate::key_management::key_tree::traits::KeyTreeNode; @@ -6,9 +6,13 @@ use crate::key_management::key_tree::traits::KeyTreeNode; #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(any(test, feature = "test_utils"), derive(PartialEq, Eq))] pub struct ChildKeysPublic { - pub csk: nssa::PrivateKey, - pub cpk: nssa::PublicKey, - pub ccc: [u8; 32], + /// Secret key for public account. + pub sk: nssa::PrivateKey, + /// Schnorr secret key. + pub ssk: nssa::PrivateKey, + /// Schnorr public key. + pub pk: nssa::PublicKey, + pub cc: [u8; 32], /// Can be [`None`] if root. pub cci: Option, } @@ -18,19 +22,24 @@ impl ChildKeysPublic { pub fn root(seed: [u8; 64]) -> Self { let hash_value = hmac_sha512::HMAC::mac(seed, "LEE_master_pub"); - let csk = nssa::PrivateKey::try_new( + let sk = nssa::PrivateKey::try_new( *hash_value .first_chunk::<32>() .expect("hash_value is 64 bytes, must be safe to get first 32"), ) .expect("Expect a valid Private Key"); - let ccc = *hash_value.last_chunk::<32>().unwrap(); - let cpk = nssa::PublicKey::new_from_private_key(&csk); + let ssk = nssa::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::root()`: Invalid private key produced from `tweak`"); + + let cc = *hash_value + .last_chunk::<32>() + .expect("hash_value is 64 bytes, must be safe to get last 32"); + let pk = nssa::PublicKey::new_from_private_key(&ssk); Self { - csk, - cpk, - ccc, + sk, + ssk, + pk, + cc, cci: None, } } @@ -39,61 +48,53 @@ impl ChildKeysPublic { pub fn nth_child(&self, cci: u32) -> Self { let hash_value = self.compute_hash_value(cci); - let csk = nssa::PrivateKey::try_new({ - let hash_value = hash_value + let lhs = k256::Scalar::from_repr( + (*hash_value .first_chunk::<32>() - .expect("hash_value is 64 bytes, must be safe to get first 32"); + .expect("hash_value is 64 bytes, must be safe to get first 32")) + .into(), + ) + .expect("Expect a valid k256 scalar"); + let rhs = + k256::Scalar::from_repr((*self.sk.value()).into()).expect("Expect a valid k256 scalar"); - let value_1 = - k256::Scalar::from_repr((*hash_value).into()).expect("Expect a valid k256 scalar"); - let value_2 = k256::Scalar::from_repr((*self.csk.value()).into()) - .expect("Expect a valid k256 scalar"); + let sk = nssa::PrivateKey::try_new(lhs.add(&rhs).to_bytes().into()) + .expect("Expect a valid private key"); - let sum = value_1.add(&value_2); - sum.to_bytes().into() - }) - .expect("Expect a valid private key"); + let ssk = nssa::PrivateKey::tweak(sk.value()).expect("`key_protocol::key_management::keys_public::nth_child()`: Invalid private key produced from `tweak`"); - let ccc = *hash_value + let cc = *hash_value .last_chunk::<32>() .expect("hash_value is 64 bytes, must be safe to get last 32"); - let cpk = nssa::PublicKey::new_from_private_key(&csk); + let pk = nssa::PublicKey::new_from_private_key(&ssk); Self { - csk, - cpk, - ccc, + sk, + ssk, + pk, + cc, cci: Some(cci), } } #[must_use] pub fn account_id(&self) -> nssa::AccountId { - nssa::AccountId::from(&self.cpk) + nssa::AccountId::from(&self.pk) } fn compute_hash_value(&self, cci: u32) -> [u8; 64] { let mut hash_input = vec![]; - - if ((2_u32).pow(31)).cmp(&cci) == std::cmp::Ordering::Greater { - // Non-harden. - // BIP-032 compatibility requires 1-byte header from the public_key; - // Not stored in `self.cpk.value()`. - let sk = k256::SecretKey::from_bytes(self.csk.value().into()) - .expect("32 bytes, within curve order"); - let pk = sk.public_key(); - hash_input.extend_from_slice(pk.to_encoded_point(true).as_bytes()); - } else { - // Harden. - hash_input.extend_from_slice(&[0_u8]); - hash_input.extend_from_slice(self.csk.value()); - } + // Simplified key logic by only supporting harden keys. + // Non-harden keys would require access to untweaked public keys associated to `sk`s. + // Thus, not PQ secure. + hash_input.extend_from_slice(&[0_u8]); + hash_input.extend_from_slice(self.sk.value()); #[expect(clippy::big_endian_bytes, reason = "BIP-032 uses big endian")] hash_input.extend_from_slice(&cci.to_be_bytes()); - hmac_sha512::HMAC::mac(hash_input, self.ccc) + hmac_sha512::HMAC::mac(hash_input, self.cc) } } @@ -103,7 +104,7 @@ impl ChildKeysPublic { )] impl<'a> From<&'a ChildKeysPublic> for &'a nssa::PrivateKey { fn from(value: &'a ChildKeysPublic) -> Self { - &value.csk + &value.ssk } } @@ -137,30 +138,37 @@ mod tests { ]; let keys = ChildKeysPublic::root(seed); - let expected_ccc = [ + let expected_cc = [ 238, 94, 84, 154, 56, 224, 80, 218, 133, 249, 179, 222, 9, 24, 17, 252, 120, 127, 222, 13, 146, 126, 232, 239, 113, 9, 194, 219, 190, 48, 187, 155, ]; - let expected_csk: PrivateKey = PrivateKey::try_new([ + let expected_sk: PrivateKey = PrivateKey::try_new([ 40, 35, 239, 19, 53, 178, 250, 55, 115, 12, 34, 3, 153, 153, 72, 170, 190, 36, 172, 36, 202, 148, 181, 228, 35, 222, 58, 84, 156, 24, 146, 86, ]) .unwrap(); - let expected_cpk: PublicKey = PublicKey::try_new([ - 219, 141, 130, 105, 11, 203, 187, 124, 112, 75, 223, 22, 11, 164, 153, 127, 59, 247, - 244, 166, 75, 66, 242, 224, 35, 156, 161, 75, 41, 51, 76, 245, + let expected_ssk: PrivateKey = PrivateKey::try_new([ + 207, 4, 246, 223, 104, 72, 19, 85, 14, 122, 194, 82, 32, 163, 60, 57, 8, 25, 209, 91, + 254, 107, 76, 238, 31, 68, 236, 192, 154, 78, 105, 118, ]) .unwrap(); - assert!(expected_ccc == keys.ccc); - assert!(expected_csk == keys.csk); - assert!(expected_cpk == keys.cpk); + let expected_pk: PublicKey = PublicKey::try_new([ + 188, 163, 203, 45, 151, 154, 230, 254, 123, 114, 158, 130, 19, 182, 164, 143, 150, 131, + 176, 7, 27, 58, 204, 116, 5, 247, 0, 255, 111, 160, 52, 201, + ]) + .unwrap(); + + assert!(expected_cc == keys.cc); + assert!(expected_ssk == keys.ssk); + assert!(expected_sk == keys.sk); + assert!(expected_pk == keys.pk); } #[test] - fn harden_child_keys_generation() { + fn child_keys_generation() { let seed = [ 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, @@ -171,93 +179,32 @@ mod tests { let cci = (2_u32).pow(31) + 13; let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); - let expected_ccc = [ + let expected_cc = [ 149, 226, 13, 4, 194, 12, 69, 29, 9, 234, 209, 119, 98, 4, 128, 91, 37, 103, 192, 31, 130, 126, 123, 20, 90, 34, 173, 209, 101, 248, 155, 36, ]; - let expected_csk: PrivateKey = PrivateKey::try_new([ + let expected_sk: PrivateKey = PrivateKey::try_new([ 9, 65, 33, 228, 25, 82, 219, 117, 91, 217, 11, 223, 144, 85, 246, 26, 123, 216, 107, 213, 33, 52, 188, 22, 198, 246, 71, 46, 245, 174, 16, 47, ]) .unwrap(); - let expected_cpk: PublicKey = PublicKey::try_new([ - 142, 143, 238, 159, 105, 165, 224, 252, 108, 62, 53, 209, 176, 219, 249, 38, 90, 241, - 201, 81, 194, 146, 236, 5, 83, 152, 238, 243, 138, 16, 229, 15, + let expected_ssk: PrivateKey = PrivateKey::try_new([ + 100, 37, 212, 81, 40, 233, 72, 156, 177, 139, 50, 114, 136, 157, 202, 132, 203, 246, + 252, 242, 13, 81, 42, 100, 159, 240, 187, 252, 202, 108, 25, 105, ]) .unwrap(); - assert!(expected_ccc == child_keys.ccc); - assert!(expected_csk == child_keys.csk); - assert!(expected_cpk == child_keys.cpk); - } - - #[test] - fn nonharden_child_keys_generation() { - let seed = [ - 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, - 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, - 22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, - 187, 148, 92, 44, 253, 210, 37, - ]; - let root_keys = ChildKeysPublic::root(seed); - let cci = 13; - let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); - - let expected_ccc = [ - 79, 228, 242, 119, 211, 203, 198, 175, 95, 36, 4, 234, 139, 45, 137, 138, 54, 211, 187, - 16, 28, 79, 80, 232, 216, 101, 145, 19, 101, 220, 217, 141, - ]; - - let expected_csk: PrivateKey = PrivateKey::try_new([ - 185, 147, 32, 242, 145, 91, 123, 77, 42, 33, 134, 84, 12, 165, 117, 70, 158, 201, 95, - 153, 14, 12, 92, 235, 128, 156, 194, 169, 68, 35, 165, 127, + let expected_pk: PublicKey = PublicKey::try_new([ + 210, 59, 119, 137, 21, 153, 82, 22, 195, 82, 12, 16, 80, 156, 125, 199, 19, 173, 46, + 224, 213, 144, 165, 126, 70, 129, 171, 141, 77, 212, 108, 233, ]) .unwrap(); - let expected_cpk: PublicKey = PublicKey::try_new([ - 119, 16, 145, 121, 97, 244, 186, 35, 136, 34, 140, 171, 206, 139, 11, 208, 207, 121, - 158, 45, 28, 22, 140, 98, 161, 179, 212, 173, 238, 220, 2, 34, - ]) - .unwrap(); - - assert!(expected_ccc == child_keys.ccc); - assert!(expected_csk == child_keys.csk); - assert!(expected_cpk == child_keys.cpk); - } - - #[test] - fn edge_case_child_keys_generation_2_power_31() { - let seed = [ - 88, 189, 37, 237, 199, 125, 151, 226, 69, 153, 165, 113, 191, 69, 188, 221, 9, 34, 173, - 134, 61, 109, 34, 103, 121, 39, 237, 14, 107, 194, 24, 194, 191, 14, 237, 185, 12, 87, - 22, 227, 38, 71, 17, 144, 251, 118, 217, 115, 33, 222, 201, 61, 203, 246, 121, 214, 6, - 187, 148, 92, 44, 253, 210, 37, - ]; - let root_keys = ChildKeysPublic::root(seed); - let cci = (2_u32).pow(31); //equivant to 0, thus non-harden. - let child_keys = ChildKeysPublic::nth_child(&root_keys, cci); - - let expected_ccc = [ - 221, 208, 47, 189, 174, 152, 33, 25, 151, 114, 233, 191, 57, 15, 40, 140, 46, 87, 126, - 58, 215, 40, 246, 111, 166, 113, 183, 145, 173, 11, 27, 182, - ]; - - let expected_csk: PrivateKey = PrivateKey::try_new([ - 223, 29, 87, 189, 126, 24, 117, 225, 190, 57, 0, 143, 207, 168, 231, 139, 170, 192, 81, - 254, 126, 10, 115, 42, 141, 157, 70, 171, 199, 231, 198, 132, - ]) - .unwrap(); - - let expected_cpk: PublicKey = PublicKey::try_new([ - 96, 123, 245, 51, 214, 216, 215, 205, 70, 145, 105, 221, 166, 169, 122, 27, 94, 112, - 228, 110, 249, 177, 85, 173, 180, 248, 185, 199, 112, 246, 83, 33, - ]) - .unwrap(); - - assert!(expected_ccc == child_keys.ccc); - assert!(expected_csk == child_keys.csk); - assert!(expected_cpk == child_keys.cpk); + assert!(expected_cc == child_keys.cc); + assert!(expected_ssk == child_keys.ssk); + assert!(expected_sk == child_keys.sk); + assert!(expected_pk == child_keys.pk); } } diff --git a/key_protocol/src/key_management/key_tree/mod.rs b/key_protocol/src/key_management/key_tree/mod.rs index 3635c65c..544c1a30 100644 --- a/key_protocol/src/key_management/key_tree/mod.rs +++ b/key_protocol/src/key_management/key_tree/mod.rs @@ -347,8 +347,8 @@ mod tests { assert!(tree.key_map.contains_key(&ChainIndex::root())); assert!(tree.account_id_map.contains_key(&AccountId::new([ - 172, 82, 222, 249, 164, 16, 148, 184, 219, 56, 92, 145, 203, 220, 251, 89, 214, 178, - 38, 30, 108, 202, 251, 241, 148, 200, 125, 185, 93, 227, 189, 247 + 10, 231, 159, 65, 236, 46, 205, 5, 172, 89, 250, 29, 123, 195, 212, 137, 155, 111, 40, + 120, 53, 28, 124, 54, 224, 170, 119, 208, 2, 72, 75, 50 ]))); } diff --git a/nssa/src/signature/private_key.rs b/nssa/src/signature/private_key.rs index 1bfecf80..c4cc0771 100644 --- a/nssa/src/signature/private_key.rs +++ b/nssa/src/signature/private_key.rs @@ -1,6 +1,8 @@ use std::str::FromStr; +use k256::elliptic_curve::{PrimeField as _, sec1::ToEncodedPoint as _}; use rand::{Rng as _, rngs::OsRng}; +use risc0_zkvm::sha::{Impl, Sha256 as _}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use crate::error::NssaError; @@ -60,6 +62,31 @@ impl PrivateKey { pub const fn value(&self) -> &[u8; 32] { &self.0 } + + /// `tweak` produces the "tweaked secret key" (`sk`) given a public account's `ssk`. + /// We use "tweaked keys" to shield the public accounts' `ssk` against quantum threats. + /// The "tweaked keys" are used for Schnorr Signatures (BIP-340). + /// The usage of these keys will be greatly reduced once LEE is upgraded to use a PQ signatures. + pub fn tweak(value: &[u8; 32]) -> Result { + if !Self::is_valid_key(*value) { + return Err(NssaError::InvalidPrivateKey); + } + + let sk = k256::SecretKey::from_slice(value).map_err(|_e| NssaError::InvalidPrivateKey)?; + + let hashed: [u8; 32] = Impl::hash_bytes(sk.public_key().to_encoded_point(true).as_bytes()) + .as_bytes() + .try_into() + .expect("Sha256 outputs a 32-byte array"); + + let sk = sk.to_nonzero_scalar(); + + let scalar = k256::Scalar::from_repr(hashed.into()) + .into_option() + .ok_or(NssaError::InvalidPrivateKey)?; + + Self::try_new(sk.add(&scalar).to_bytes().into()) + } } #[cfg(test)] @@ -75,4 +102,33 @@ mod tests { fn produce_key() { let _key = PrivateKey::new_os_random(); } + + #[test] + fn tweak_rejects_zero_key() { + assert!(matches!( + PrivateKey::tweak(&[0_u8; 32]), + Err(NssaError::InvalidPrivateKey) + )); + } + + // tweak: 0xFF…FF exceeds the secp256k1 curve order + #[test] + fn tweak_rejects_out_of_range_key() { + assert!(matches!( + PrivateKey::tweak(&[0xFF; 32]), + Err(NssaError::InvalidPrivateKey) + )); + } + + #[test] + fn tweak_deterministic() { + let tweaked = PrivateKey::tweak(&[1_u8; 32]).unwrap(); + assert_eq!( + tweaked.value(), + &[ + 242, 210, 33, 19, 65, 108, 136, 176, 179, 128, 110, 210, 107, 193, 168, 112, 206, + 171, 86, 238, 131, 10, 39, 36, 44, 39, 246, 20, 46, 193, 204, 66 + ] + ); + } }