simplify sigchain http distribution
Some checks failed
CI / fmt, clippy, docs (push) Failing after 5s
CI / test (ubuntu-latest) (push) Failing after 5s
CI / iroh integration smoke tests (push) Failing after 4s
CodeQL / Analyze Rust (push) Failing after 5s
Security / RustSec cargo-audit (push) Failing after 4s
CI / test (macos-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled

This commit is contained in:
Eric Wendland 2026-07-19 03:43:06 +02:00
commit 538b52bdb6
12 changed files with 66 additions and 72 deletions

View file

@ -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-id>/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

View file

@ -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 } => {

View file

@ -779,7 +779,6 @@ pub enum ControlResponse {
claims: usize,
receipts: usize,
bundle_hash: String,
static_http_path: String,
note: String,
},
KeychainExplained {

View file

@ -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::{

View file

@ -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;

View file

@ -28,8 +28,9 @@ pub struct CanonicalSshSigchainBundle {
pub receipts: Vec<AnchorReceipt>,
}
/// 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(

View file

@ -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(),
})
}

View file

@ -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

View file

@ -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-id>/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

View file

@ -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-id>/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

View file

@ -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-id>/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

View file

@ -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 `<lowercase-chain-id>`, publish the bundle at:
```text
/.well-known/sshsigchain/v1/<lowercase-chain-id>/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