Skip to content
Open
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
21 changes: 17 additions & 4 deletions crates/rvm-rvf/src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ impl VerificationReport {
#[derive(Debug, Clone, Default)]
pub struct VerifyOptions {
/// Ed25519 public keys whose signatures are accepted. With none supplied,
/// signature checks are recorded as skipped rather than passed — an
/// unverifiable signature is never treated as a valid one.
/// non-executable signature checks are recorded as skipped rather than
/// passed. Executable signatures without a trust anchor fail strict
/// eligibility: unverifiable code is never treated as verified code.
pub trusted_keys: Vec<[u8; 32]>,
/// Permit executable segments with no signature. Off by default; exists
/// for unsigned development builds, which are labeled as such and are not
Expand Down Expand Up @@ -395,15 +396,15 @@ fn signature_record(data: &[u8], seg: &ParsedSegment, opts: &VerifyOptions) -> V
if footer.sig_algo != SIG_ALGO_ED25519 || footer.sig_length != ED25519_SIGNATURE_LENGTH {
return segment_record(
CheckKind::Signature,
Outcome::Skip,
signature_incomplete_outcome(seg),
seg,
DetailCode::UnsupportedSignatureAlgorithm,
);
}
if opts.trusted_keys.is_empty() {
return segment_record(
CheckKind::Signature,
Outcome::Skip,
signature_incomplete_outcome(seg),
seg,
DetailCode::NoTrustedKey,
);
Expand All @@ -422,6 +423,18 @@ fn signature_record(data: &[u8], seg: &ParsedSegment, opts: &VerifyOptions) -> V
)
}

/// Missing verifier support may remain informational for signed data, but it
/// is a hard refusal for code. ADR-155 requires every executable segment to be
/// authenticated before load; a signature footer alone proves no publisher
/// identity.
const fn signature_incomplete_outcome(seg: &ParsedSegment) -> Outcome {
if seg.is_executable() {
Outcome::Fail
} else {
Outcome::Skip
}
}

/// Whether any trusted key signed this segment.
fn signature_verifies(
data: &[u8],
Expand Down
26 changes: 24 additions & 2 deletions crates/rvm-rvf/src/verify_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,30 @@ fn a_signature_is_skipped_not_passed_when_no_key_is_trusted() {
record_for(&report, CheckKind::Signature).detail,
DetailCode::NoTrustedKey
);
// A skip is not a failure, and the executable is signed, so this passes.
assert!(report.ok, "{:?}", report.failures());
// A distribution verifier without a trust anchor cannot establish who
// signed executable code. Preserve the precise diagnostic, but refuse to
// turn the incomplete check into an execution-eligible report.
assert!(!report.ok);
}

#[test]
fn an_unsupported_executable_signature_algorithm_is_not_execution_eligible() {
let kp = TestKeypair::deterministic(8);
let mut data = manifest_only();
data.extend(signed_segment(SEG_TYPE_WASM, b"\0asm", 6, &kp));

let segments = crate::container::walk(&data).unwrap();
let footer = segments
.iter()
.find(|segment| segment.header.seg_type == SEG_TYPE_WASM)
.and_then(|segment| segment.footer.clone())
.unwrap();
data[footer.start..footer.start + 2].copy_from_slice(&99u16.to_le_bytes());

let report = verify(&data, &VerifyOptions::with_trusted_keys(vec![kp.public])).unwrap();
let signature = record_for(&report, CheckKind::Signature);
assert_eq!(signature.detail, DetailCode::UnsupportedSignatureAlgorithm);
assert!(!report.ok);
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions crates/rvm-rvf/src/witness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ mod tests {
fn a_skip_is_recorded_as_escalated_not_as_a_pass() {
let kp = TestKeypair::deterministic(7);
let mut data = manifest_only();
data.extend(signed_segment(SEG_TYPE_WASM, b"\0asm", 2, &kp));
// No trusted key, so the signature check skips.
data.extend(signed_segment(SEG_TYPE_META, b"metadata", 2, &kp));
// A non-executable signature with no trusted key remains informational.
let report = verify(&data, &VerifyOptions::default()).unwrap();

let skipped = report
Expand Down
5 changes: 5 additions & 0 deletions docs/adr/ADR-155-rvf-execution-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

**Status**: Accepted
**Date**: 2026-08-05
**Updated**: 2026-08-05 — strict executable verification now requires a supported signature algorithm and at least one configured trusted key; incomplete trust checks are witnessed refusals, not execution-eligible skips (rvm#20).
**Authors**: Claude Code
**Supersedes**: None
**Related**: ADR-132 (Hypervisor Core), ADR-134 (Witness Schema), ADR-135 (Proof Verifier), ADR-140 (Agent Runtime Adapter), ADR-149 (RVF Integration), RuVector ADR-284 (RVF Execution Contract), RuVector ADR-285 (Hosted RVM Security Boundary), RuVector ADR-286 (Capability Schema Mapping), RuVector ADR-287 (WASM Component Model), RuVector ADR-291 (Runtime Compatibility and Version Negotiation)
Expand Down Expand Up @@ -119,6 +120,10 @@ inspection and packaging tooling:
independently against its manifest-declared hash.
3. **Reject unsigned executable segments by default.** A model or data segment
may be policy-permitted unsigned; an executable one may not.
A signed executable is also rejected when no configured trust anchor can
authenticate it, or when its signature algorithm is unsupported. These
conditions retain precise witness diagnostics but never produce an
execution-eligible verification report.
4. **Never execute RVF content during inspection or packaging.** `rvm inspect`
and `rvm verify` are first-class operations distinct from `rvm run` precisely
so that pointing a scanner at a hostile artifact is safe. This is the runtime
Expand Down