Merge eca2d9e1ea2a5e7a80941227f76551b538fe3198 into d3390efc6db215cef35ba1d6d1f5e13277fe9597

This commit is contained in:
jonesmarvin8 2026-06-01 19:23:11 +00:00 committed by GitHub
commit f2bfca90fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 130 additions and 127 deletions

View File

@ -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<u32>,
}
@ -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);
}
}

View File

@ -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
])));
}

View File

@ -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<Self, NssaError> {
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
]
);
}
}