Pin RELEASE_ROOT_PUBKEY_HEX from the 2026-07-02 release-root signing ceremony
(signer did🔑z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur) so nodes verify
the publisher identity of the app-catalog. Sign releases/app-catalog.json in place.
Fix two floats that made the catalog unsignable: archy-btcpay-db manifest version
-> string, fedimint-clientd cpu_limit 0.25 -> 1 (u32). Add scripts/sign-catalog.sh
helper, the 1.8.0 release-hardening plan/tracker, and the commit-and-push project
rule in CLAUDE.md.
Backward-compatible: old binaries still accept the signed catalog; the pinned-anchor
binary ships in the next build/OTA.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
3.1 KiB
Rust
81 lines
3.1 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).
|
|
///
|
|
/// Pinned 2026-07-02 from the release-root signing ceremony
|
|
/// (signer did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur). 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("5d15cbee8a108f7dd288c02d29a1d9d71f198acc99186aad8008b4f28d469951");
|
|
|
|
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());
|
|
}
|
|
}
|