Files
archy/core/archipelago/src/trust/anchor.rs
T

96 lines
3.9 KiB
Rust

//! The fleet's pinned **release-root** trust anchor.
//!
//! Every node ships the release-root *public* key. Signed manifests and the app
//! catalog must be signed by the corresponding private key (derived once, in
//! the signing ceremony, via `seed::derive_release_root_ed25519`). Pinning the
//! key in the binary is what makes a swapped-in mirror key detectable.
//!
//! Until the ceremony runs against the real release master seed, the pinned
//! constant is `None`. While `None`, signature verification still runs and
//! still rejects tampered documents, but it cannot enforce signer *identity*
//! (see `signed_doc::SignatureStatus::anchored`). Set
//! `ARCHY_RELEASE_ROOT_PUBKEY` (64-char hex) to pin a key at runtime for
//! staging/test fleets before the constant is baked in.
use ed25519_dalek::VerifyingKey;
/// Hex of the pinned Ed25519 release-root public key (32 bytes / 64 hex chars).
///
/// ROTATED 2026-08-04 to did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT.
///
/// The previous root (z6Mkkid…q7ur, pinned 2026-07-02) was exposed in a chat
/// transcript and is treated as compromised.
///
/// Rotation is ORDERING-CRITICAL. Nodes pin the OLD key, so the release that
/// carries this change must itself be signed with the OLD key — that is the
/// only signature a node running the previous binary will accept. Only the
/// release AFTER it may be signed with the new key. Signing the rotation
/// release with the new key makes every node reject it and ends OTA
/// fleet-wide, recoverable only by touching each node by hand.
///
/// Verified before pinning: this hex and the did:key above are the same
/// keypair (the did:key encodes exactly these 32 bytes), checked with a
/// decoder round-tripped against the previous known-good pair. An earlier
/// candidate hex was rejected because it did not match the stated DID.
/// The
/// corresponding mnemonic is held offline by the publisher — see
/// `docs/workstream-b-signing-runbook.md`. Regenerate/verify with:
/// `RELEASE_MASTER_MNEMONIC=… archipelago ceremony pubkey`.
pub const RELEASE_ROOT_PUBKEY_HEX: Option<&str> =
Some("1578adccf137024159dd936f44a56e8869ac7775785962f7e92e2faf2c034418");
const ENV_OVERRIDE: &str = "ARCHY_RELEASE_ROOT_PUBKEY";
/// Resolve the pinned release-root public key, if any.
///
/// Runtime env override wins over the baked-in constant so a test fleet can pin
/// a ceremony key without a rebuild. Malformed values are ignored (treated as
/// "not pinned") rather than crashing the node.
pub fn release_root_pubkey() -> Option<VerifyingKey> {
if let Ok(hex_str) = std::env::var(ENV_OVERRIDE) {
if let Some(key) = parse_pubkey_hex(hex_str.trim()) {
return Some(key);
}
tracing::warn!(
"{} is set but not a valid 32-byte hex Ed25519 key; ignoring",
ENV_OVERRIDE
);
}
RELEASE_ROOT_PUBKEY_HEX.and_then(parse_pubkey_hex)
}
fn parse_pubkey_hex(s: &str) -> Option<VerifyingKey> {
let bytes = hex::decode(s).ok()?;
let arr: [u8; 32] = bytes.as_slice().try_into().ok()?;
VerifyingKey::from_bytes(&arr).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pinned_constant_parses_to_a_valid_key() {
// The release-root anchor is pinned (ceremony 2026-07-02); it must be
// present and a well-formed 32-byte Ed25519 key.
let hex = RELEASE_ROOT_PUBKEY_HEX.expect("release-root anchor must be pinned");
assert!(
parse_pubkey_hex(hex).is_some(),
"pinned RELEASE_ROOT_PUBKEY_HEX is not a valid Ed25519 key"
);
}
#[test]
fn parses_valid_hex() {
let key = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]).verifying_key();
let parsed = parse_pubkey_hex(&hex::encode(key.to_bytes())).unwrap();
assert_eq!(parsed.as_bytes(), key.as_bytes());
}
#[test]
fn rejects_malformed_hex() {
assert!(parse_pubkey_hex("nothex").is_none());
assert!(parse_pubkey_hex("abcd").is_none());
}
}