Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
00113c0
chore(2712): open lane for anonymous read path
MichaelTaylor3d Aug 11, 2026
91a0c97
refactor(host): make the store identity representable as absent
MichaelTaylor3d Aug 11, 2026
ba00ed3
fix(serve): carry no store identity on the read path
MichaelTaylor3d Aug 11, 2026
d7b74bf
test(guest): an anonymous host cannot serve attestation-gated content
MichaelTaylor3d Aug 11, 2026
8a2fa41
docs(spec): state that serving committed content consults no identity
MichaelTaylor3d Aug 11, 2026
ddc1558
test(cli): pin `cat` reading without a store identity
MichaelTaylor3d Aug 11, 2026
ad9355a
feat(cli): classify an absent store identity as IDENTITY_UNAVAILABLE
MichaelTaylor3d Aug 11, 2026
92c9ee6
chore: gitignore the per-worktree GitNexus index
MichaelTaylor3d Aug 11, 2026
ab370e1
test(serve): pin that serving survives a missing host key while signi…
MichaelTaylor3d Aug 11, 2026
ff24818
test(serve): derive the expected signer key without a crate-private l…
MichaelTaylor3d Aug 11, 2026
ef7138c
test(serve): use expect_err for the fail-closed signing assertion
MichaelTaylor3d Aug 11, 2026
fd18e90
fix(host): derive has_host_public_key from installed state, not a dep…
MichaelTaylor3d Aug 11, 2026
e4c9f88
refactor(host)!: make a host identity whole-or-absent, and test the a…
MichaelTaylor3d Aug 11, 2026
10cb32f
test(serve): re-point the source-scan ban at the live tokens, and cov…
MichaelTaylor3d Aug 11, 2026
ea6e736
docs(guest): name the field the anonymous-host double mirrors
MichaelTaylor3d Aug 11, 2026
7186643
style(host): rustfmt the host_deps result expression
MichaelTaylor3d Aug 11, 2026
e8b1234
test(host): cover the blind-serve identity-mismatch refusal, and ban …
MichaelTaylor3d Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@
# Rendered whitepaper HTML intermediate (the .md source and .pdf are kept).
/docs/whitepaper/digstore-whitepaper.html
.testcredentials
.claude/worktrees/
.claude/worktrees/
# GitNexus code-intelligence index (per-worktree, regenerable)
.gitnexus/
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exclude = ["crates/digstore-prover/guest", "crates/dig-client-wasm"]

[workspace.package]
edition = "2021"
version = "0.24.0"
version = "0.25.0"
license = "GPL-2.0-only"

[workspace.dependencies]
Expand Down
21 changes: 19 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,7 +700,7 @@ bound-induced export failure (timeout vs fuel exhaustion), not as an opaque engi
Each export call is armed with its own fresh budget; a serve sequence (alloc → call → read →
dealloc) is deliberately NOT a single combined budget.

### 13.6 Host identity: never substituted, and required to attest, sign, or push
### 13.6 Host identity: never substituted, required to attest, sign, or push, and not consulted to read

A store's host identity is its BLS signing key (`signing_key.bin`) and the trusted host keys
(`trusted_keys.json`) persisted at init.
Expand All @@ -710,6 +710,17 @@ A store's host identity is its BLS signing key (`signing_key.bin`) and the trust
carries no identity at all rather than a weaker one, and an all-zero public key is a nonexistent
identity rather than a weak one.
- A host MUST refuse to attest, sign, or push when the identity is absent or unreadable.
- Serving committed content consumes NO host identity. A host MUST carry none on that path, MUST NOT
read the identity files to take it, and MUST NOT refuse a read because the identity is absent,
unreadable, or malformed. Reading DIG content requires no account and no key (§14), so a missing
identity is not a reason to withhold content that is already committed and merkle-verifiable
against its trusted root.
- Where a host does carry no identity, that absence MUST be representable as absence rather than
encoded as a placeholder value, so that a path which later begins consuming the identity fails
closed instead of accepting a key nobody controls.
- Making the identity optional MUST NOT make any gate optional. Where the content gate does require
attestation (§12.2), a host holding no identity MUST fail that gate closed and return a decoy,
exactly as a host presenting an untrusted or invalid key does.
- The signing key MUST be exactly 32 bytes. A shorter or longer file is malformed and MUST be
reported as a corrupt-identity error — never truncated, never padded, and never handed to key
derivation, which is permitted to abort the process on a short seed.
Expand All @@ -720,7 +731,13 @@ The substitution ban holds regardless of whether a given path currently verifies
loads. A path that does not consume the identity today MUST NOT be treated as licence to supply a
placeholder, because the placeholder becomes forgeable identity the moment any consumer begins
verifying it. Where a path genuinely does not need an identity, the correct expression is to carry
no identity at all — not to carry a fabricated one.
no identity at all — not to carry a fabricated one, and not to refuse the operation.

These two rules are one rule seen from both sides, and neither implies the other. Refusing a read
over a missing identity is not a stricter form of not substituting one: it withholds content whose
integrity does not depend on the host at all, while leaving every path that DOES consume an identity
exactly as safe as it was. Conversely, tolerating a missing identity on the read path grants nothing
to the signing paths, which continue to require one.

## 14. Client → node resolution (the origin)

Expand Down
34 changes: 34 additions & 0 deletions crates/digstore-cli/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@ pub enum CliError {
/// message so the user knows WHICH command to retry.
operation: String,
},
/// The store's own identity material (`signing_key.bin` /
/// `trusted_keys.json`) could not be read or is malformed.
///
/// Raised ONLY by the paths that genuinely consume an identity — signing a
/// proof, pushing. Reading committed content does not consume one and never
/// raises this: the guest's content path does not consult the host identity,
/// so refusing a read over a missing key would cost availability and buy no
/// security.
///
/// Nothing branches on this variant for control flow. It exists so an
/// operator is told WHICH file is unreadable instead of "the system cannot
/// find the file specified", and so a §6.2 machine consumer can classify the
/// failure without matching on prose.
#[error("store identity unavailable at {path}: {detail}")]
IdentityUnavailable { path: String, detail: String },
#[error(transparent)]
Other(#[from] anyhow::Error),
}
Expand Down Expand Up @@ -119,6 +134,7 @@ impl CliError {
CliError::TooLarge(_) => 17,
CliError::NeedsConsolidation { .. } => 18,
CliError::NoLocalNode { .. } => 19,
CliError::IdentityUnavailable { .. } => 20,
CliError::Other(_) => 1,
}
}
Expand Down Expand Up @@ -148,6 +164,7 @@ impl CliError {
CliError::TooLarge(_) => "TOO_LARGE",
CliError::NeedsConsolidation { .. } => "NEEDS_CONSOLIDATION",
CliError::NoLocalNode { .. } => "NO_LOCAL_NODE",
CliError::IdentityUnavailable { .. } => "IDENTITY_UNAVAILABLE",
CliError::Other(_) => "ERROR",
}
}
Expand Down Expand Up @@ -218,6 +235,11 @@ impl CliError {
19,
"no DIG node is running on this machine and this operation needs one; start or install dig-node",
),
(
"IDENTITY_UNAVAILABLE",
20,
"the store's own identity file could not be read; re-create the store identity (reading committed content does not need one)",
),
]
}

Expand All @@ -229,6 +251,10 @@ impl CliError {
// instructions in full; a hint here would only repeat them.
CliError::NoLocalNode { .. } => Some("run `dig-node status` to check your node".into()),
CliError::NonFastForward => Some("run `digstore pull` first, then push".into()),
CliError::IdentityUnavailable { path, .. } => Some(format!(
"check that {path} exists and is readable by this user; reading committed \
content does not need it, only signing does"
)),
CliError::Unauthorized(_) => Some("check your credentials / store signing key".into()),
CliError::NotFound(_) => Some("run `digstore log` to list capsules and keys".into()),
CliError::NoSeed => Some("run `digstore seed import` to set up your seed".into()),
Expand Down Expand Up @@ -343,6 +369,10 @@ mod tests {
required: 50,
cap: 50,
},
CliError::IdentityUnavailable {
path: "signing_key.bin".into(),
detail: "x".into(),
},
];
let mut codes: Vec<i32> = errs.iter().map(|e| e.exit_code()).collect();
let n = codes.len();
Expand Down Expand Up @@ -389,6 +419,10 @@ mod tests {
required: 50,
cap: 50,
},
CliError::IdentityUnavailable {
path: "signing_key.bin".into(),
detail: "x".into(),
},
];
let mut codes: Vec<&str> = errs.iter().map(|e| e.code()).collect();
let n = codes.len();
Expand Down
Loading
Loading