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