feat(trust): pin release-root anchor + ship signed app-catalog
Pin RELEASE_ROOT_PUBKEY_HEX from the 2026-07-02 release-root signing ceremony (signer did:key: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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8b6485078a
commit
1977bdefb5
@@ -149,6 +149,18 @@ mod tests {
|
||||
doc
|
||||
}
|
||||
|
||||
/// Pin `test_key` as the release-root anchor for this process via the env
|
||||
/// override. Needed because the baked-in `RELEASE_ROOT_PUBKEY_HEX` is the
|
||||
/// real ceremony key, which no unit test can produce signatures for — so to
|
||||
/// exercise the anchored-verification path we pin a key we can sign with.
|
||||
/// Every call sets the same value, so parallel tests stay consistent.
|
||||
fn pin_test_key_as_anchor() {
|
||||
std::env::set_var(
|
||||
"ARCHY_RELEASE_ROOT_PUBKEY",
|
||||
hex::encode(test_key().verifying_key().to_bytes()),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsigned_document_reports_unsigned() {
|
||||
let doc = json!({"schema": 1, "apps": {}});
|
||||
@@ -156,18 +168,31 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_verifies() {
|
||||
fn roundtrip_verifies_and_anchors_to_pinned_key() {
|
||||
// With the anchor pinned to the signer, verification succeeds AND
|
||||
// reports anchored == true (signer identity confirmed).
|
||||
pin_test_key_as_anchor();
|
||||
let signed = sign_into(&test_key(), json!({"schema": 1, "n": 42}));
|
||||
match verify_detached(&signed).unwrap() {
|
||||
// No anchor pinned in the default test build → anchored == false.
|
||||
SignatureStatus::Verified { anchored, .. } => assert!(!anchored),
|
||||
SignatureStatus::Verified { anchored, .. } => assert!(anchored),
|
||||
other => panic!("expected Verified, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_from_non_anchor_key_is_rejected() {
|
||||
// A self-consistent signature from a key that is NOT the pinned anchor
|
||||
// must hard-reject — this is what stops a mirror swapping in its own key.
|
||||
pin_test_key_as_anchor();
|
||||
let other_key = SigningKey::from_bytes(&[11u8; 32]);
|
||||
let signed = sign_into(&other_key, json!({"schema": 1, "n": 42}));
|
||||
assert!(verify_detached(&signed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_survives_key_reordering() {
|
||||
// Re-emitting the document with shuffled keys must not break the sig.
|
||||
pin_test_key_as_anchor();
|
||||
let signed = sign_into(&test_key(), json!({"b": 2, "a": 1}));
|
||||
let reparsed: Value =
|
||||
serde_json::from_str(&serde_json::to_string(&signed).unwrap()).unwrap();
|
||||
@@ -179,6 +204,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tampered_payload_is_rejected() {
|
||||
// Pin the signer so verification reaches the signature check (not an
|
||||
// anchor-identity short-circuit), proving tamper detection itself.
|
||||
pin_test_key_as_anchor();
|
||||
let mut signed = sign_into(&test_key(), json!({"schema": 1, "n": 42}));
|
||||
signed
|
||||
.as_object_mut()
|
||||
|
||||
Reference in New Issue
Block a user