Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ A manifest is driven through the full chain: signature (Stages 2-6), canary (Sta
- `--now` sets the verified-time reference for the canary and origin-expiry checks (defaults to the current system clock).
- `--fetched-onion` is the onion address the manifest was fetched from; with it, Stage 9 origin binding runs. Omit to skip Stage 9.
- `--content-index` is the served `/content_index.json`; when the manifest declares `content_root`, Stage 9b verifies it (and its absence with a declared `content_root` surfaces the fetch failure).
- `--expected-runtime-pubkey` is the manifest's `canary.runtime_pubkey`; for a content or transaction document, it is the key the signature is checked against. Without it, a content/transaction has no authorized key and reports a signature rejection.
- `--expected-runtime-pubkey` is the manifest's `canary.runtime_pubkey`; for a content or transaction document, it is the key the signature is checked against. Without it, there is no authorized key and the document is rejected with `E_SIG_INVALID_KEY` (distinct from `E_SIG_VERIFICATION`, a bad signature).

Skipped stages are reported, so an accept is never mistaken for a full-pipeline pass.

Expand Down
10 changes: 6 additions & 4 deletions examples/blog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,12 @@ Steps 5 and 6 both report `accept`; step 5 also prints `canary_state: Fresh`.
A content document carries no key of its own: it is signed by a runtime key, and
only the manifest says which runtime key is authorized (its
`canary.runtime_pubkey`). So verifying `post.json` standalone, without
`--expected-runtime-pubkey`, reports a signature rejection - there is no
authorized key to check against. That is the whole point of the manifest:
identity and authorization live there, not in the individual document. This is
also where the canary, origin binding, and content-index checks run.
`--expected-runtime-pubkey`, is rejected with `E_SIG_INVALID_KEY` - there is no
authorized key to check against. (This is distinct from `E_SIG_VERIFICATION`, an
actual bad signature, so automation can tell the two apart.) That is the whole
point of the manifest: identity and authorization live there, not in the
individual document. This is also where the canary, origin binding, and
content-index checks run.

## What gets rejected

Expand Down
44 changes: 25 additions & 19 deletions src/commands/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use entangled_core::document::{
use entangled_core::types::keys::RuntimePubkey;
use entangled_core::types::manifest::OnionAddress;
use entangled_core::types::timestamp::EntangledTimestamp;
use entangled_core::validation::Diagnostic;
use entangled_core::validation::{Diagnostic, DiagnosticCode, DocumentKindLabel};

use crate::cli::VerifyArgs;
use crate::commands::{Error, Outcome};
Expand Down Expand Up @@ -133,51 +133,57 @@ fn verify_manifest(
}

fn verify_content(args: &VerifyArgs, bytes: &[u8]) -> Result<Outcome, Error> {
let (runtime_pk, has_key) = runtime_key(args)?;
let runtime_pk = match runtime_key(args)? {
Some(k) => k,
None => return report_reject(&no_runtime_key(DocumentKindLabel::Content)),
};
match parse_and_verify_content(bytes, &runtime_pk) {
Ok(_) => {
println!("verdict: accept");
print_runtime_note(has_key);
Ok(Outcome::Success)
}
Err(d) => report_reject(&d),
}
}

fn verify_transaction(args: &VerifyArgs, bytes: &[u8]) -> Result<Outcome, Error> {
let (runtime_pk, has_key) = runtime_key(args)?;
let runtime_pk = match runtime_key(args)? {
Some(k) => k,
None => return report_reject(&no_runtime_key(DocumentKindLabel::Transaction)),
};
match parse_and_verify_transaction(bytes, &runtime_pk, None) {
Ok(_) => {
println!("verdict: accept");
print_runtime_note(has_key);
Ok(Outcome::Success)
}
Err(d) => report_reject(&d),
}
}

/// Resolve the runtime key to verify a content/transaction signature against:
/// the manifest-authorized key from `--expected-runtime-pubkey` when given, or
/// a placeholder otherwise (in which case the signature check has no authorized
/// key and will reject). Returns the key and whether a real one was supplied.
fn runtime_key(args: &VerifyArgs) -> Result<(RuntimePubkey, bool), Error> {
/// The manifest-authorized runtime key from `--expected-runtime-pubkey`, or
/// `None` when it was not supplied. `None` is not a signature failure: there is
/// no key to verify against, reported as `E_SIG_INVALID_KEY` so automation can
/// tell "no manifest context" apart from "bad signature".
fn runtime_key(args: &VerifyArgs) -> Result<Option<RuntimePubkey>, Error> {
match args.expected_runtime_pubkey.as_deref() {
Some(b64) => {
let key = RuntimePubkey::try_from(b64)
.map_err(|e| format!("--expected-runtime-pubkey is invalid: {e}"))?;
Ok((key, true))
Ok(Some(key))
}
None => Ok((RuntimePubkey::from_bytes([0u8; 32]), false)),
None => Ok(None),
}
}

fn print_runtime_note(has_key: bool) {
if !has_key {
println!(
"note: no authorizing runtime key given; pass --expected-runtime-pubkey \
(the manifest's canary.runtime_pubkey) to verify against the manifest"
);
}
/// The diagnostic for "no authorized runtime key available", per section 11:
/// distinct from a signature that decoded but failed to verify.
fn no_runtime_key(kind: DocumentKindLabel) -> Diagnostic {
Diagnostic::new(
DiagnosticCode::ESigInvalidKey,
kind,
"no authorizing runtime key available; pass --expected-runtime-pubkey \
(the manifest's canary.runtime_pubkey) to verify against the manifest",
)
}

fn report_reject(diag: &Diagnostic) -> Result<Outcome, Error> {
Expand Down
20 changes: 20 additions & 0 deletions tests/exit_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,23 @@ fn verify_accept_exits_zero() {
.expect("run verify");
assert!(status.success(), "an accepted document must exit 0");
}

/// Without an authorizing runtime key, the reject must be E_SIG_INVALID_KEY
/// ("no manifest context"), not E_SIG_VERIFICATION ("bad signature"), so
/// automation can tell the two apart.
#[test]
fn verify_no_key_reports_invalid_key_not_sig_failure() {
let out = tool()
.args(["verify", "--input", POST])
.output()
.expect("run verify");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("E_SIG_INVALID_KEY"),
"expected E_SIG_INVALID_KEY, got:\n{stdout}"
);
assert!(
!stdout.contains("E_SIG_VERIFICATION"),
"missing key must not surface as a signature failure:\n{stdout}"
);
}
Loading