wip(trust): park agent's signed-manifest module + release-root key off main

Moved here so main stays clean for the v1.7.98 release. Contains the trust/
module (canonical.rs, did.rs, signed_doc.rs) + seed::derive_release_root_ed25519.
Not wired into the build yet. Continue this work on this branch.
This commit is contained in:
archipelago
2026-06-16 11:22:24 -04:00
parent ee46a856de
commit 0fef808671
6 changed files with 482 additions and 0 deletions
+54
View File
@@ -8,6 +8,8 @@
//! ├── HKDF(seed, "archipelago/node/ed25519/v1") → Node Ed25519 → did:key
//! ├── HKDF(seed, "archipelago/nostr-node/secp256k1/v1") → Node Nostr key
//! ├── HKDF(seed, "archipelago/fips/secp256k1/v1") → FIPS mesh transport key
//! ├── HKDF(seed, "archipelago/release/root/ed25519/v1") → Release-root signing key
//! │ (publisher-only; nodes pin the PUBLIC key — see trust::anchor)
//! ├── HKDF(seed, "archipelago/identity/{i}/ed25519/v1") → Identity i Ed25519
//! ├── BIP-32 m/44'/1237'/0'/0/{i} → Identity i Nostr (NIP-06)
//! ├── BIP-32 m/84'/0'/0' → Bitcoin Core wallet
@@ -34,6 +36,7 @@ const NODE_ED25519_INFO: &[u8] = b"archipelago/node/ed25519/v1";
const NODE_NOSTR_INFO: &[u8] = b"archipelago/nostr-node/secp256k1/v1";
const FIPS_KEY_INFO: &[u8] = b"archipelago/fips/secp256k1/v1";
const LND_ENTROPY_INFO: &[u8] = b"archipelago/lnd/entropy/v1";
const RELEASE_ROOT_ED25519_INFO: &[u8] = b"archipelago/release/root/ed25519/v1";
// ─── MasterSeed ─────────────────────────────────────────────────────────
@@ -88,6 +91,21 @@ pub fn derive_node_ed25519(seed: &MasterSeed) -> Result<SigningKey> {
Ok(SigningKey::from_bytes(&derived))
}
/// Derive the fleet **release-root** Ed25519 signing key.
///
/// This is a *publisher-side* derivation: only the holder of the release master
/// seed runs it (e.g. in the signing ceremony). Fleet nodes never derive this —
/// they pin the corresponding PUBLIC key as a trust anchor (see
/// `crate::trust::anchor`) and use it to verify signed manifests/catalogs.
///
/// Keeping it seed-derived means the signing key is reproducible from a
/// backed-up mnemonic (disaster recovery) rather than a loose key file, and it
/// is domain-separated from every node/identity key by its HKDF info string.
pub fn derive_release_root_ed25519(seed: &MasterSeed) -> Result<SigningKey> {
let derived = hkdf_derive_32(seed.as_bytes(), RELEASE_ROOT_ED25519_INFO)?;
Ok(SigningKey::from_bytes(&derived))
}
/// Derive an identity's Ed25519 signing key by index.
pub fn derive_identity_ed25519(seed: &MasterSeed, index: u32) -> Result<SigningKey> {
let info = format!("archipelago/identity/{}/ed25519/v1", index);
@@ -561,4 +579,40 @@ mod tests {
);
}
#[test]
fn test_release_root_deterministic_and_domain_separated() {
let (_, seed) = MasterSeed::from_mnemonic_words(TEST_MNEMONIC).unwrap();
let a = derive_release_root_ed25519(&seed).unwrap();
let b = derive_release_root_ed25519(&seed).unwrap();
assert_eq!(
a.verifying_key().as_bytes(),
b.verifying_key().as_bytes(),
"Same mnemonic must produce the same release-root key"
);
// Must NOT collide with the node key — different HKDF domain.
let node = derive_node_ed25519(&seed).unwrap();
assert_ne!(
a.verifying_key().as_bytes(),
node.verifying_key().as_bytes(),
"Release-root key must be domain-separated from the node key"
);
}
#[test]
fn test_release_root_known_answer() {
// KAT pins the derivation so the signing ceremony, the pinned anchor,
// and any external verifier agree on the bytes for a given mnemonic.
let (_, seed) = MasterSeed::from_mnemonic_words(TEST_MNEMONIC).unwrap();
let key = derive_release_root_ed25519(&seed).unwrap();
assert_eq!(
hex::encode(key.to_bytes()),
"__RELEASE_ROOT_PRIV_HEX__",
"release-root private key KAT"
);
assert_eq!(
hex::encode(key.verifying_key().to_bytes()),
"__RELEASE_ROOT_PUB_HEX__",
"release-root public key KAT"
);
}
}