diff --git a/README.md b/README.md index 7671233..07b7f8f 100644 --- a/README.md +++ b/README.md @@ -669,9 +669,10 @@ geth keychain bundle-extract --in ./chain.sscb --out ./chain.jsonl The verifier reports active authority devices/keys, disclosed and incomplete profiles, the head digest, and current attester/backend anchor thresholds. It auto-detects JSONL interchange or the canonical `.sscb` bundle. The bundle is -the deterministic on-disk/full-snapshot distribution format and can be hosted -unchanged at `/.well-known/sshsigchain/v1//chain.sscb` by a static -HTTP server. HTTP and every other distribution backend remain untrusted inputs. +the single deterministic on-disk and distribution file and can be hosted +unchanged at any explicitly configured HTTP(S) URL, including by a static file +server. There is no well-known path or URL-based discovery. HTTP and every +other distribution backend remain untrusted inputs. Local append workflows, accepted-head persistence, concrete fetchers, and concrete anchor adapters remain follow-up work. Until they exist, do not substitute an unpinned checkpoint, HTTP response, or local operation-log view diff --git a/crates/geth-cli/src/lib.rs b/crates/geth-cli/src/lib.rs index 95eab94..97df142 100644 --- a/crates/geth-cli/src/lib.rs +++ b/crates/geth-cli/src/lib.rs @@ -4183,7 +4183,6 @@ fn print_response(response: ControlResponse, output: OutputMode) -> Result<()> { claims, receipts, bundle_hash, - static_http_path, note, } => { println!("input: {}", input.display()); @@ -4194,7 +4193,6 @@ fn print_response(response: ControlResponse, output: OutputMode) -> Result<()> { println!("claims: {claims}"); println!("receipts: {receipts}"); println!("bundle_hash: {bundle_hash}"); - println!("static_http_path: {static_http_path}"); eprintln!("note: {note}"); } ControlResponse::KeychainExplained { subject, lines } => { diff --git a/crates/geth-control/src/lib.rs b/crates/geth-control/src/lib.rs index f967a2d..f124be8 100644 --- a/crates/geth-control/src/lib.rs +++ b/crates/geth-control/src/lib.rs @@ -779,7 +779,6 @@ pub enum ControlResponse { claims: usize, receipts: usize, bundle_hash: String, - static_http_path: String, note: String, }, KeychainExplained { diff --git a/crates/geth-keychain/src/lib.rs b/crates/geth-keychain/src/lib.rs index e72f37e..0fde0dc 100644 --- a/crates/geth-keychain/src/lib.rs +++ b/crates/geth-keychain/src/lib.rs @@ -25,9 +25,9 @@ pub use sshsigchain::{ SshSigchainVerification, SshSigchainVerifier, VerifiedAnchoredHistory, authority_key_id, canonical_bundle_hash, decode_canonical_bundle, decode_sshsigchain_jsonl, encode_canonical_bundle, encode_sshsigchain_jsonl, key_proof_signing_bytes, - profile_payload_commitment, select_anchored_head, static_http_bundle_path, - verify_anchor_receipts, verify_anchored_history, verify_canonical_bundle_records, - verify_head_claim, verify_sshsigchain, + profile_payload_commitment, select_anchored_head, verify_anchor_receipts, + verify_anchored_history, verify_canonical_bundle_records, verify_head_claim, + verify_sshsigchain, }; use geth_types::{ diff --git a/crates/geth-keychain/src/sshsigchain.rs b/crates/geth-keychain/src/sshsigchain.rs index 10f06b9..441e8f7 100644 --- a/crates/geth-keychain/src/sshsigchain.rs +++ b/crates/geth-keychain/src/sshsigchain.rs @@ -15,7 +15,7 @@ pub use bundle::{ CANONICAL_BUNDLE_EXTENSION, CANONICAL_BUNDLE_MEDIA_TYPE, CanonicalSshSigchainBundle, DistributionEndpoint, DistributionError, MAX_CANONICAL_BUNDLE_BYTES, SshSigchainBundleSource, canonical_bundle_hash, decode_canonical_bundle, encode_canonical_bundle, - static_http_bundle_path, verify_canonical_bundle_records, + verify_canonical_bundle_records, }; pub const SSH_SIGCHAIN_VERSION: u8 = 1; diff --git a/crates/geth-keychain/src/sshsigchain/bundle.rs b/crates/geth-keychain/src/sshsigchain/bundle.rs index 003dd0b..f905b8d 100644 --- a/crates/geth-keychain/src/sshsigchain/bundle.rs +++ b/crates/geth-keychain/src/sshsigchain/bundle.rs @@ -28,8 +28,9 @@ pub struct CanonicalSshSigchainBundle { pub receipts: Vec, } -/// Locates an untrusted distribution source. It is routing configuration, not -/// a trust anchor; integrity and authority come from SSHSIGCHAIN verification. +/// Locates an untrusted distribution source. It is exact routing +/// configuration, not a discovery mechanism or trust anchor; acceptance comes +/// from SSHSIGCHAIN verification against the independently pinned trust tuple. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DistributionEndpoint { @@ -111,14 +112,6 @@ pub enum DistributionError { Protocol(#[from] SshSigchainError), } -#[must_use] -pub fn static_http_bundle_path(chain_id: ChainId) -> String { - format!( - "/.well-known/sshsigchain/v1/{}/chain.{CANONICAL_BUNDLE_EXTENSION}", - chain_id.to_hex() - ) -} - #[must_use] pub fn canonical_bundle_hash(bytes: &[u8]) -> Digest { let mut input = Vec::with_capacity(BUNDLE_HASH_DOMAIN.len() + bytes.len()); @@ -740,6 +733,15 @@ fn validate_object_size(bytes: &[u8]) -> Result<(), DistributionError> { mod tests { use super::*; + #[test] + fn distribution_locator_is_arbitrary_exact_routing_configuration() { + let locator = "https://cdn.example.test/custom/latest.bin?channel=stable"; + let endpoint = + DistributionEndpoint::new("primary", "http", locator).expect("distribution endpoint"); + + assert_eq!(endpoint.locator, locator); + } + const ROOT_KEY: &str = "ssh-ed25519 AQID"; fn signed_genesis() -> SshSigchainRecord { @@ -778,7 +780,7 @@ mod tests { .with_signature(vec![4, 5, 6]) .expect("claim signature"); let receipt = AnchorReceipt { - backend_id: "static-http".to_owned(), + backend_id: "http-primary".to_owned(), claim_hash: claim.claim_hash().expect("claim hash"), evidence: b"receipt".to_vec(), }; @@ -897,7 +899,7 @@ mod tests { required_classes: vec!["http".to_owned()], backend_threshold: 1, backends: vec![AnchorBackendPolicy { - backend_id: "static-http".to_owned(), + backend_id: "http-primary".to_owned(), class: "http".to_owned(), locator: "https://example.test".to_owned(), weight: 1, @@ -932,18 +934,6 @@ mod tests { assert_eq!(decode_canonical_bundle(&bytes).expect("decode"), bundle); } - #[test] - fn static_http_path_is_stable_and_contains_only_lowercase_chain_id() { - assert_eq!( - static_http_bundle_path(ChainId([0xab; 32])), - concat!( - "/.well-known/sshsigchain/v1/", - "abababababababababababababababababababababababababababababababab/", - "chain.sscb" - ) - ); - } - #[test] fn canonical_bundle_matches_published_base_vector() { let record = SshSigchainRecord::unsigned( diff --git a/crates/geth-node/src/lib.rs b/crates/geth-node/src/lib.rs index cb8a58d..c7cf383 100644 --- a/crates/geth-node/src/lib.rs +++ b/crates/geth-node/src/lib.rs @@ -6575,7 +6575,6 @@ pub fn handle_request( claims: 0, receipts: 0, bundle_hash: geth_keychain::canonical_bundle_hash(&bytes).to_hex(), - static_http_path: geth_keychain::static_http_bundle_path(chain_id), note: "wrote the canonical SSHSIGCHAIN bundle; the root public key remains an out-of-band trust input and is intentionally not embedded".to_owned(), }) } @@ -6594,7 +6593,6 @@ pub fn handle_request( claims: bundle.claims.len(), receipts: bundle.receipts.len(), bundle_hash: geth_keychain::canonical_bundle_hash(&bytes).to_hex(), - static_http_path: geth_keychain::static_http_bundle_path(bundle.chain_id), note: "extracted record JSONL including available disclosures; head claims and anchor receipts remain in the canonical bundle because JSONL carries records only".to_owned(), }) } diff --git a/docs/adr/0018-sshsigchain.md b/docs/adr/0018-sshsigchain.md index ceda5fc..2d6ffac 100644 --- a/docs/adr/0018-sshsigchain.md +++ b/docs/adr/0018-sshsigchain.md @@ -41,7 +41,8 @@ SSHSIGCHAIN v1 is redesigned before deployment: discloseable payloads, claims, and receipts without embedding root trust; - JSONL is noncanonical human-facing interchange; and - distribution backends return untrusted bundle bytes through one interface, - with a static HTTP profile that requires no server application. + with a single-file static HTTP profile at an exact operator-configured URL + that requires no server application, well-known discovery, or TOFU. The generic core defines backend interfaces and deterministic policy behavior, not Nostr, HTTP, blockchain, or transparency-log clients. Those adapters belong diff --git a/docs/architecture.md b/docs/architecture.md index a2cbbe5..ede0c65 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -496,10 +496,11 @@ SSHSIGCHAIN's canonical `.sscb` bundle is the portable on-disk and full-snapshot distribution boundary. It deterministically stores signed record objects, separate disclosures, head claims, and anchor receipts without embedding the root key. JSONL remains inspection/interchange only. Bundle sources implement a -backend-neutral untrusted fetch interface; the static HTTP profile publishes -the same bytes below `/.well-known/sshsigchain/v1//chain.sscb`. -Fetching never grants trust or selects a head. Geth may use Iroh rather than -HTTP operationally without changing the format or verification path. +backend-neutral untrusted fetch interface; the static HTTP profile fetches the +same single file from an exact operator-configured URL. It defines no +well-known path or discovery rule. Fetching never grants trust or selects a +head. Geth may use Iroh rather than HTTP operationally without changing the +format or verification path. New devices can use the node enrollment flow instead of hand-editing keychain state. `geth node enroll join` explicitly imports an owner admin public key as diff --git a/docs/roadmap.md b/docs/roadmap.md index 7617c9f..613182a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -584,9 +584,9 @@ resource-scoped capability decisions. clearly preserving claims/receipts only in the canonical bundle. - `[x]` `verify-sigchain` auto-detects JSONL or canonical bundles and binds a bundle's namespace and chain ID to local trust. - - `[x]` Specify the backend-neutral distribution-source contract and a static - HTTP full-snapshot profile at - `/.well-known/sshsigchain/v1//chain.sscb`. + - `[x]` Specify the backend-neutral distribution-source contract and a + single-file static HTTP full-snapshot profile at an arbitrary, explicitly + configured URL, without well-known discovery or URL-based trust. - `[ ]` Implement automatic distribution fetchers. - Acceptance criteria: an HTTP fetcher enforces streaming limits, conditional caching, redirect policy, trust-tuple matching, accepted-head diff --git a/docs/sigchain-keychain.md b/docs/sigchain-keychain.md index 47090ca..2426cda 100644 --- a/docs/sigchain-keychain.md +++ b/docs/sigchain-keychain.md @@ -114,11 +114,11 @@ geth keychain bundle-extract --in chain.sscb --out chain.jsonl The canonical bundle keeps disclosures separate from signed record objects and sorts every object class deterministically. Adding a disclosure changes the -bundle hash but not the chain head. The same file is the simple static HTTP -distribution artifact at -`/.well-known/sshsigchain/v1//chain.sscb`. Distribution is untrusted: -the configured root, authority replay, anchor policy, locally accepted head, -and fork checks still decide acceptance. +bundle hash but not the chain head. The same single file is the static HTTP +distribution artifact at any explicitly configured URL; there is no well-known +path, URL derivation, or trust-on-first-use. Distribution is untrusted: the +configured root, authority replay, anchor policy, locally accepted head, and +fork checks still decide acceptance. ## Local commands diff --git a/docs/sshsigchain.md b/docs/sshsigchain.md index 97467fa..105a174 100644 --- a/docs/sshsigchain.md +++ b/docs/sshsigchain.md @@ -446,12 +446,12 @@ anchoring, cached-head, and fork checks. ### 10.1 Static HTTP profile -The static HTTP profile needs no server application or directory listing. For -chain ID ``, publish the bundle at: - -```text -/.well-known/sshsigchain/v1//chain.sscb -``` +The static HTTP profile needs no server application, directory listing, +well-known path, or discovery protocol. Its `locator` is the exact arbitrary +HTTP(S) URL of one canonical `.sscb` file. Implementations MUST NOT derive the +URL from the chain ID, treat a URL as root-key discovery, or use a successful +fetch as trust-on-first-use. The chain ID, namespace, and root public key are +already pinned independently of the URL. The response media type SHOULD be: @@ -459,23 +459,29 @@ The response media type SHOULD be: application/vnd.sshsigchain.bundle.v1 ``` -A client performs an ordinary `GET`, accepts a successful full response, caps -bytes while streaming, decodes the canonical bundle, requires its chain ID and -namespace to match local trust, verifies the chain, verifies applicable head -claims and receipts, then applies cached-head/fork rules. `404` means the source -currently has no bundle. Servers SHOULD replace `chain.sscb` atomically. +A client performs an ordinary `GET` of the exact configured URL, accepts a +successful full response, caps bytes while streaming, decodes the canonical +bundle, requires its chain ID and namespace to match local trust, verifies the +chain, verifies applicable head claims and receipts, then applies +cached-head/fork rules. `404` means the source currently has no bundle. Servers +SHOULD replace the file atomically. -Servers MAY expose the lowercase bundle hash as a strong `ETag`; clients MAY use -`If-None-Match`. HTTP cache validators are performance hints, not trust. HTTPS -protects privacy, locators, and availability against network interference, but -SSHSIGCHAIN verification remains mandatory even over authenticated HTTPS. -Plain HTTP remains integrity-safe only in the narrow sense that tampering is -detected; it offers no confidentiality or reliable availability. +Servers SHOULD expose the lowercase bundle hash as a strong `ETag`; clients +SHOULD use `If-None-Match`. A mutable latest-version URL SHOULD use +`Cache-Control: no-cache` or a suitably short freshness lifetime so ordinary +HTTP caches and CDNs can store the file and revalidate it. HTTP cache validators +are performance hints, not trust. HTTPS protects privacy, locators, and +availability against network interference, but SSHSIGCHAIN verification +remains mandatory even over authenticated HTTPS. Plain HTTP remains +integrity-safe only in the narrow sense that tampering is detected; it offers +no confidentiality or reliable availability. -This full-snapshot profile intentionally favors deployment with a static file -server. A later chunked profile may add immutable hash-addressed objects and a -small mutable manifest, but it must preserve the object encodings and all -verification rules above. +Servers MAY support byte ranges, and clients MAY use them to resume or +parallelize transfer of the single file. The v1 bundle has no authenticated +random-access index: a client MUST assemble, decode, hash, and verify the +complete canonical bundle before accepting any state. A `206 Partial Content` +response is therefore a download optimization, not a partial-chain or +per-object protocol. This profile deliberately remains one file. ## 11. JSONL interchange