From 5feefc2f4bae565e6753b97bc58e875d793bc437 Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Sun, 2 Aug 2026 23:01:23 -0400 Subject: [PATCH] fix: preserve legacy signer continuity --- Cargo.lock | 4 +- crates/frameshift-cli/Cargo.toml | 2 +- crates/frameshift-client/src/registry.rs | 60 ++++++++++++++++++------ crates/frameshift-mcp/Cargo.toml | 2 +- docs/API_COMPATIBILITY.md | 8 ++++ 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e67fa64..97c191f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2072,7 +2072,7 @@ dependencies = [ [[package]] name = "frameshift-cli" -version = "0.10.1" +version = "0.10.2" dependencies = [ "chrono", "clap", @@ -2216,7 +2216,7 @@ dependencies = [ [[package]] name = "frameshift-mcp" -version = "0.10.1" +version = "0.10.2" dependencies = [ "frameshift-capabilities", "frameshift-client", diff --git a/crates/frameshift-cli/Cargo.toml b/crates/frameshift-cli/Cargo.toml index fda2f2e..fa9dba8 100644 --- a/crates/frameshift-cli/Cargo.toml +++ b/crates/frameshift-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frameshift-cli" -version = "0.10.1" +version = "0.10.2" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/frameshift-client/src/registry.rs b/crates/frameshift-client/src/registry.rs index 2a8beaf..d8c8297 100644 --- a/crates/frameshift-client/src/registry.rs +++ b/crates/frameshift-client/src/registry.rs @@ -599,20 +599,11 @@ fn check_or_create_registry_trust( &record.publisher_key, record.publisher_key_id.as_deref(), ) { - (None, None, _) => { - if record - .legacy_author - .as_ref() - .is_some_and(|legacy| legacy.handle != author) - { - return Err(ClientError::RegistryOwnershipInvalid { - pack, - detail: "legacy author handle does not match the signed pack manifest" - .to_string(), - }); - } - check_or_create_author_pin(data_root, registry, author, pubkey) - } + // Legacy author summaries are current presentation metadata, not + // immutable signing evidence. Historical archives may retain an older + // signed handle after the registry profile is renamed; exact signer-key + // continuity remains enforced by the author pin below. + (None, None, _) => check_or_create_author_pin(data_root, registry, author, pubkey), (Some(publisher), None, None) => { let legacy_author = record.legacy_author.as_ref().ok_or_else(|| { ClientError::RegistryOwnershipInvalid { @@ -1318,6 +1309,47 @@ mod tests { .expect("legacy response with the pinned signer must retain continuity"); } + /// A renamed legacy profile cannot invalidate an immutable signed archive. + #[test] + fn legacy_author_presentation_rename_retains_signer_key_continuity() { + let temp = tempfile::tempdir().unwrap(); + let mut legacy = ownership_version_record( + "4a128e72-cc91-4721-b452-943ce736799b", + "cc56ea2b-991d-46eb-a94f-936a9b071a4a", + EnrolledPublisherKeyState::Active, + [7_u8; 32], + ); + legacy.publisher = None; + legacy.publisher_key_id = None; + legacy.publisher_key = None; + legacy.legacy_author = Some(RegistryLegacyAuthorSummary { + handle: "renamed-profile".to_string(), + display_name: Some("Renamed Profile".to_string()), + }); + + check_or_create_registry_trust( + temp.path(), + "https://registry.example", + "historical-signed-handle", + &[7_u8; 32], + &legacy, + ) + .expect("legacy presentation rename must preserve the verified signer"); + + let error = check_or_create_registry_trust( + temp.path(), + "https://registry.example", + "historical-signed-handle", + &[8_u8; 32], + &legacy, + ) + .expect_err("legacy presentation rename must not permit signer substitution"); + assert!(matches!( + error, + ClientError::RegistryAuthorKeyChanged { .. } + )); + } + /// A linked pack may retain exact legacy trust for an unlinked historical version. #[test] fn publisher_with_legacy_historical_version_uses_author_key_pin() { diff --git a/crates/frameshift-mcp/Cargo.toml b/crates/frameshift-mcp/Cargo.toml index 7346c15..37a29f7 100644 --- a/crates/frameshift-mcp/Cargo.toml +++ b/crates/frameshift-mcp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "frameshift-mcp" -version = "0.10.1" +version = "0.10.2" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/docs/API_COMPATIBILITY.md b/docs/API_COMPATIBILITY.md index 427f777..d306423 100644 --- a/docs/API_COMPATIBILITY.md +++ b/docs/API_COMPATIBILITY.md @@ -75,6 +75,14 @@ JSON location. They add these optional objects: - Successful account-backed publish responses may add `publisher` and `publisher_key`. +`legacy_author` is current presentation metadata for a legacy signing key. It is +not immutable authority for a historical version because a profile handle can +change after an archive is signed. Clients authenticate an unlinked legacy +version with the requested record identity, archive content hash, Ed25519 +signature, exact manifest-to-record signer key, and the locally pinned signer +key. A changed `legacy_author` handle does not rewrite or invalidate that signed +history. + Released v0.10 clients continue to browse, install, and verify because they ignore unknown response fields and still verify the retained `author_pubkey`, signature, and content hash. `PUBLISHER_OWNERSHIP_READS=false` disables read enrichment and