mirror of
https://github.com/logos-blockchain/lssa.git
synced 2026-04-16 08:03:14 +00:00
feat: add GroupKeyHolder for GMS key management
This commit is contained in:
parent
10687d6562
commit
dbd8ee122b
159
key_protocol/src/key_management/group_key_holder.rs
Normal file
159
key_protocol/src/key_management/group_key_holder.rs
Normal file
@ -0,0 +1,159 @@
|
||||
use nssa_core::program::PdaSeed;
|
||||
use rand::{RngCore as _, rngs::OsRng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, digest::FixedOutput as _};
|
||||
|
||||
use super::secret_holders::{PrivateKeyHolder, SecretSpendingKey};
|
||||
|
||||
/// Manages shared viewing keys for a group of controllers owning private PDAs.
|
||||
///
|
||||
/// The Group Master Secret (GMS) is a 32-byte random value shared among controllers.
|
||||
/// Each private PDA owned by the group gets a unique key pair derived from the GMS
|
||||
/// at a deterministic index computed from the PDA seed.
|
||||
///
|
||||
/// The GMS must be distributed off-chain (sealed under each controller's viewing
|
||||
/// public key). The wallet is responsible for encrypting the GMS at rest.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GroupKeyHolder {
|
||||
gms: [u8; 32],
|
||||
}
|
||||
|
||||
impl GroupKeyHolder {
|
||||
/// Create a new group with a fresh random GMS.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
let mut gms = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut gms);
|
||||
Self { gms }
|
||||
}
|
||||
|
||||
/// Restore from an existing GMS (received from another controller).
|
||||
#[must_use]
|
||||
pub const fn from_gms(gms: [u8; 32]) -> Self {
|
||||
Self { gms }
|
||||
}
|
||||
|
||||
/// Export the raw GMS for distribution to other controllers.
|
||||
#[must_use]
|
||||
pub const fn gms(&self) -> &[u8; 32] {
|
||||
&self.gms
|
||||
}
|
||||
|
||||
/// Derive the group's `SecretSpendingKey` from the GMS.
|
||||
///
|
||||
/// Uses a domain-separated SHA256 hash with a group-specific prefix,
|
||||
/// bypassing the 2048-round HMAC chain used for personal seeds.
|
||||
fn secret_spending_key(&self) -> SecretSpendingKey {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"LEE/group/ssk/v0");
|
||||
hasher.update(self.gms);
|
||||
SecretSpendingKey(hasher.finalize_fixed().into())
|
||||
}
|
||||
|
||||
/// Derive keys for a specific PDA.
|
||||
///
|
||||
/// The index is deterministic from the PDA seed, so all controllers holding
|
||||
/// the same GMS independently derive the same keys for the same PDA.
|
||||
#[must_use]
|
||||
pub fn derive_keys_for_pda(&self, pda_seed: &PdaSeed) -> PrivateKeyHolder {
|
||||
let index = Self::pda_key_index(pda_seed);
|
||||
self.secret_spending_key()
|
||||
.produce_private_key_holder(Some(index))
|
||||
}
|
||||
|
||||
/// Compute a deterministic `u32` index from a PDA seed.
|
||||
fn pda_key_index(pda_seed: &PdaSeed) -> u32 {
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(b"LEE/pda-index");
|
||||
hasher.update(pda_seed.as_bytes());
|
||||
let hash: [u8; 32] = hasher.finalize_fixed().into();
|
||||
u32::from_le_bytes([hash[0], hash[1], hash[2], hash[3]])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nssa_core::NullifierPublicKey;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Two holders from the same GMS derive identical keys for the same PDA seed.
|
||||
#[test]
|
||||
fn same_gms_same_seed_produces_same_keys() {
|
||||
let gms = [42u8; 32];
|
||||
let holder_a = GroupKeyHolder::from_gms(gms);
|
||||
let holder_b = GroupKeyHolder::from_gms(gms);
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
|
||||
let keys_a = holder_a.derive_keys_for_pda(&seed);
|
||||
let keys_b = holder_b.derive_keys_for_pda(&seed);
|
||||
|
||||
assert_eq!(
|
||||
keys_a.generate_nullifier_public_key().to_byte_array(),
|
||||
keys_b.generate_nullifier_public_key().to_byte_array(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Different PDA seeds produce different keys from the same GMS.
|
||||
#[test]
|
||||
fn same_gms_different_seed_produces_different_keys() {
|
||||
let holder = GroupKeyHolder::from_gms([42u8; 32]);
|
||||
let seed_a = PdaSeed::new([1; 32]);
|
||||
let seed_b = PdaSeed::new([2; 32]);
|
||||
|
||||
let npk_a = holder
|
||||
.derive_keys_for_pda(&seed_a)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder
|
||||
.derive_keys_for_pda(&seed_b)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk_a.to_byte_array(), npk_b.to_byte_array());
|
||||
}
|
||||
|
||||
/// Different GMS produce different keys for the same PDA seed.
|
||||
#[test]
|
||||
fn different_gms_same_seed_produces_different_keys() {
|
||||
let holder_a = GroupKeyHolder::from_gms([42u8; 32]);
|
||||
let holder_b = GroupKeyHolder::from_gms([99u8; 32]);
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
|
||||
let npk_a = holder_a
|
||||
.derive_keys_for_pda(&seed)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_b = holder_b
|
||||
.derive_keys_for_pda(&seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk_a.to_byte_array(), npk_b.to_byte_array());
|
||||
}
|
||||
|
||||
/// GMS round-trip: export and restore produces the same keys.
|
||||
#[test]
|
||||
fn gms_round_trip() {
|
||||
let original = GroupKeyHolder::from_gms([7u8; 32]);
|
||||
let restored = GroupKeyHolder::from_gms(*original.gms());
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
|
||||
let npk_original = original
|
||||
.derive_keys_for_pda(&seed)
|
||||
.generate_nullifier_public_key();
|
||||
let npk_restored = restored
|
||||
.derive_keys_for_pda(&seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_eq!(npk_original.to_byte_array(), npk_restored.to_byte_array());
|
||||
}
|
||||
|
||||
/// The derived `NullifierPublicKey` is non-zero (sanity check).
|
||||
#[test]
|
||||
fn derived_npk_is_non_zero() {
|
||||
let holder = GroupKeyHolder::from_gms([42u8; 32]);
|
||||
let seed = PdaSeed::new([1; 32]);
|
||||
let npk = holder
|
||||
.derive_keys_for_pda(&seed)
|
||||
.generate_nullifier_public_key();
|
||||
|
||||
assert_ne!(npk, NullifierPublicKey([0; 32]));
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ use secret_holders::{PrivateKeyHolder, SecretSpendingKey, SeedHolder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod ephemeral_key_holder;
|
||||
pub mod group_key_holder;
|
||||
pub mod key_tree;
|
||||
pub mod secret_holders;
|
||||
|
||||
|
||||
@ -35,6 +35,11 @@ impl PdaSeed {
|
||||
pub const fn new(value: [u8; 32]) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&ProgramId, &PdaSeed)> for AccountId {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user