Skip to content

Commit 7c8f30f

Browse files
authored
Harden cosign verification and sync canonical specs (#29)
* Harden cosign verification and sync canonical specs * Refactor verify args to satisfy clippy
1 parent 63e57fb commit 7c8f30f

8 files changed

Lines changed: 662 additions & 116 deletions

File tree

SPEC.md

Lines changed: 20 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,29 @@
11
# Provenact Specification (v0)
22

3-
This file is the top-level specification index for Provenact v0.
4-
Normative detail lives under `spec/`.
5-
Repository scope boundaries are defined in `AGENTS.md`.
6-
7-
## Current Focus
8-
9-
As of 2026-02-06, project focus is to stabilize core skill execution contracts
10-
before expanding ecosystem surface area.
11-
12-
Priority order:
13-
1. Native skill contract stability (manifest, policy, receipt).
14-
2. Conformance coverage and deterministic behavior hardening.
15-
3. One frictionless golden CLI workflow.
16-
4. Optional ecosystem adapters only after core contract gates pass.
3+
This file is the normative index for Provenact contracts.
174

185
## Scope
196

207
Provenact is a secure execution substrate for immutable, verifiable skills.
21-
Provenact includes:
22-
- skill packaging
8+
9+
In scope:
10+
- skill packaging and identity
2311
- signing and verification
2412
- capability-gated WASM execution
25-
- execution receipts and auditability
13+
- deterministic receipts and auditability
2614

27-
Provenact does not include:
28-
- agents
29-
- planners
30-
- schedulers
31-
- workflow orchestration
32-
- autonomous decision loops
15+
Out of scope:
16+
- planners, schedulers, and orchestration loops
17+
- autonomous agent control logic
18+
- long-lived memory or autonomous state machines
3319

3420
## Normative Sources
3521

36-
The following files are normative for v0:
22+
The following files define the `v0` contract:
23+
3724
- `spec/v0.md`
3825
- `spec/v0/skill-manifest.schema.json`
39-
- `spec/v0/pipeline-dag.schema.json`
26+
- `spec/v0/pipeline.schema.json`
4027
- `spec/threat-model.md`
4128
- `spec/compatibility.md`
4229
- `spec/hashing.md`
@@ -56,65 +43,16 @@ The following files are normative for v0:
5643
- `spec/registry/registry.md`
5744
- `spec/registry/snapshot.schema.json`
5845

59-
## v0 Cryptographic Profile
60-
61-
- Hash: SHA-256
62-
- Signature algorithm: Ed25519
63-
- Digest prefix format: `sha256:<hex>`
64-
- Registry transport checksum format: `<32 lowercase hex chars>` in registry entry `md5` fields
65-
- Skill artifact authority: `manifest.artifact`
66-
- Exact hash/signature preimages: `spec/hashing.md`
67-
68-
## Canonicalization Rules
69-
70-
Any hashed JSON document must be serialized with RFC 8785 (JCS), UTF-8 encoded.
71-
72-
No implicit fields may be included in hashed payloads.
73-
Timestamps are excluded from hash inputs unless explicitly stated.
74-
75-
## Capability Model
76-
77-
Capabilities are deny-by-default and requested in manifest metadata.
78-
Declared capabilities are not automatically granted.
79-
Runtime enforcement is mandatory.
80-
81-
## Required Runtime Verification Sequence
82-
83-
Before execution, runtime must:
84-
1. verify `skill.wasm` hash against `manifest.artifact`
85-
2. verify signature records in `signatures.json`
86-
3. enforce local policy against requested capabilities
87-
4. execute only if checks pass
88-
89-
## Execution Receipts
90-
91-
Each successful execution MUST produce a receipt with at least:
92-
- artifact hash
93-
- input hash
94-
- output hash
95-
- capabilities used
96-
- timestamp
97-
- receipt hash
98-
99-
Receipt hashing must follow the canonicalization rules above.
100-
Receipt shape is defined by `spec/execution-receipt.schema.json`.
101-
Failed executions SHOULD emit a best-effort failure transcript outside the
102-
success receipt schema.
46+
## Versioning
10347

104-
CLI contract: `provenact-cli run` requires `--receipt`; there is no successful run
105-
path without writing a receipt file.
48+
Versioning and compatibility policy is defined in
49+
`docs/versioning-policy.md`.
10650

107-
## v0 Non-Goals
51+
## Conformance Artifacts
10852

109-
- blockchain anchoring
110-
- zero-knowledge proofs
111-
- multi-signer quorum policy logic
112-
- native non-WASM execution
113-
- UI orchestration features
53+
Deterministic vectors for the normative contract are published in
54+
`test-vectors/` and enforced by `npm run conformance`.
11455

115-
## Expansion Gate (Post-v0/v1 Drafts)
56+
## Policy Boundary
11657

117-
New integration surfaces (including adapters) MUST wait until all are true:
118-
1. Draft schemas for manifest v1 and receipt v1 exist and are reviewable.
119-
2. Core verification and receipt behavior are covered by conformance vectors.
120-
3. Golden `pack -> sign -> verify -> run -> receipt-verify` workflow is stable.
58+
Provenact executes verified skills. Agent orchestration belongs outside this repo.

cli/provenact-cli/src/install.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use std::fs;
22
use std::io::{Cursor, Read};
33
use std::path::{Path, PathBuf};
4-
use std::time::{SystemTime, UNIX_EPOCH};
54
use std::time::Duration;
5+
use std::time::{SystemTime, UNIX_EPOCH};
66

77
use provenact_verifier::{
88
compute_manifest_hash, enforce_capability_ceiling, parse_manifest_json, parse_policy_document,

cli/provenact-cli/src/main.rs

Lines changed: 133 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use keys::{parse_public_keys, parse_signing_key, verify_keys_digest};
4141
use preflight::{load_verified_bundle, read_manifest_and_signatures};
4242
use runtime_exec::execute_wasm;
4343

44-
const USAGE: &str = "usage:\n provenact-cli verify --bundle <bundle-dir> --keys <public-keys.json> --keys-digest <sha256:...> [--require-cosign --oci-ref <oci-ref>] [--allow-experimental]\n provenact-cli inspect --bundle <bundle-dir> [--allow-experimental]\n provenact-cli pack --bundle <bundle-dir> --wasm <skill.wasm> --manifest <manifest.json> [--allow-experimental]\n provenact-cli archive --bundle <bundle-dir> --output <skill.tar.zst>\n provenact-cli sign --bundle <bundle-dir> --signer <signer-id> --secret-key <ed25519-secret-key-file> [--allow-experimental]\n provenact-cli install --artifact <path|file://...|http(s)://...|oci://...> [--keys <public-keys.json> --keys-digest <sha256:...>] [--policy <policy.{json|yaml}>] [--require-signatures] [--allow-insecure-http] [--allow-experimental]\n provenact-cli export agentskills --agent <claude|codex|cursor> --scope <user|repo|admin>\n provenact-cli run --bundle <bundle-dir> --keys <public-keys.json> --keys-digest <sha256:...> --policy <policy.{json|yaml}> --input <input-file> --receipt <receipt.json> [--receipt-format <v0|v1-draft>] [--require-cosign --oci-ref <oci-ref>] [--allow-experimental]\n provenact-cli verify-receipt --receipt <receipt.json>\n provenact-cli verify-registry-entry --artifact <artifact-bytes-file> --sha256 <sha256:...> --md5 <32-lowercase-hex>\n provenact-cli experimental-validate-manifest-v1 --manifest <manifest.json>\n provenact-cli experimental-validate-receipt-v1 --receipt <receipt.json>";
44+
const USAGE: &str = "usage:\n provenact-cli verify --bundle <bundle-dir> --keys <public-keys.json> --keys-digest <sha256:...> [--require-cosign --oci-ref <oci-ref> --cosign-key <cosign.pub> --cosign-cert-identity <identity> --cosign-cert-oidc-issuer <issuer>] [--allow-experimental]\n provenact-cli inspect --bundle <bundle-dir> [--allow-experimental]\n provenact-cli pack --bundle <bundle-dir> --wasm <skill.wasm> --manifest <manifest.json> [--allow-experimental]\n provenact-cli archive --bundle <bundle-dir> --output <skill.tar.zst>\n provenact-cli sign --bundle <bundle-dir> --signer <signer-id> --secret-key <ed25519-secret-key-file> [--allow-experimental]\n provenact-cli install --artifact <path|file://...|http(s)://...|oci://...> [--keys <public-keys.json> --keys-digest <sha256:...>] [--policy <policy.{json|yaml}>] [--require-signatures] [--allow-insecure-http] [--allow-experimental]\n provenact-cli export agentskills --agent <claude|codex|cursor> --scope <user|repo|admin>\n provenact-cli run --bundle <bundle-dir> --keys <public-keys.json> --keys-digest <sha256:...> --policy <policy.{json|yaml}> --input <input-file> --receipt <receipt.json> [--receipt-format <v0|v1-draft>] [--require-cosign --oci-ref <oci-ref> --cosign-key <cosign.pub> --cosign-cert-identity <identity> --cosign-cert-oidc-issuer <issuer>] [--allow-experimental]\n provenact-cli verify-receipt --receipt <receipt.json>\n provenact-cli verify-registry-entry --artifact <artifact-bytes-file> --sha256 <sha256:...> --md5 <32-lowercase-hex>\n provenact-cli experimental-validate-manifest-v1 --manifest <manifest.json>\n provenact-cli experimental-validate-receipt-v1 --receipt <receipt.json>";
4545
const EXPERIMENTAL_SCHEMA_VERSION: &str = "1.0.0-draft";
4646
const BUNDLE_META_SCHEMA_VERSION: &str = "1.0.0";
4747
const RECEIPT_TIMESTAMP_STRATEGY_LOCAL: &str = "local_untrusted_unix_seconds";
@@ -129,24 +129,38 @@ fn run_export(args: &[String]) -> Result<(), String> {
129129
fn run_verify(args: &[String]) -> Result<(), String> {
130130
let parsed = parse_flags_with_switches(
131131
args,
132-
&["--bundle", "--keys", "--keys-digest", "--oci-ref"],
132+
&[
133+
"--bundle",
134+
"--keys",
135+
"--keys-digest",
136+
"--oci-ref",
137+
"--cosign-key",
138+
"--cosign-cert-identity",
139+
"--cosign-cert-oidc-issuer",
140+
],
133141
&["--allow-experimental", "--require-cosign"],
134142
USAGE,
135143
)?;
136144
let bundle_dir = required_path(&parsed, "--bundle", USAGE)?;
137145
let keys_path = required_path(&parsed, "--keys", USAGE)?;
138146
let keys_digest = required_string(&parsed, "--keys-digest", USAGE)?;
139147
let oci_ref = optional_string(&parsed, "--oci-ref");
148+
let cosign_key = optional_string(&parsed, "--cosign-key");
149+
let cosign_cert_identity = optional_string(&parsed, "--cosign-cert-identity");
150+
let cosign_cert_oidc_issuer = optional_string(&parsed, "--cosign-cert-oidc-issuer");
140151
let require_cosign = has_switch(&parsed, "--require-cosign");
141152
let allow_experimental = has_switch(&parsed, "--allow-experimental");
142-
verify_bundle(
143-
&bundle_dir,
144-
&keys_path,
145-
&keys_digest,
153+
verify_bundle(VerifyBundleArgs {
154+
bundle_dir,
155+
keys_path,
156+
keys_digest,
146157
require_cosign,
147-
oci_ref.as_deref(),
158+
oci_ref,
159+
cosign_key,
160+
cosign_cert_identity,
161+
cosign_cert_oidc_issuer,
148162
allow_experimental,
149-
)
163+
})
150164
}
151165

152166
fn run_inspect(args: &[String]) -> Result<(), String> {
@@ -260,6 +274,9 @@ fn run_execute(args: &[String]) -> Result<(), String> {
260274
"--keys",
261275
"--keys-digest",
262276
"--oci-ref",
277+
"--cosign-key",
278+
"--cosign-cert-identity",
279+
"--cosign-cert-oidc-issuer",
263280
"--policy",
264281
"--input",
265282
"--receipt",
@@ -272,6 +289,9 @@ fn run_execute(args: &[String]) -> Result<(), String> {
272289
let keys_path = required_path(&parsed, "--keys", USAGE)?;
273290
let keys_digest = required_string(&parsed, "--keys-digest", USAGE)?;
274291
let oci_ref = optional_string(&parsed, "--oci-ref");
292+
let cosign_key = optional_string(&parsed, "--cosign-key");
293+
let cosign_cert_identity = optional_string(&parsed, "--cosign-cert-identity");
294+
let cosign_cert_oidc_issuer = optional_string(&parsed, "--cosign-cert-oidc-issuer");
275295
let require_cosign = has_switch(&parsed, "--require-cosign");
276296
let policy_path = required_path(&parsed, "--policy", USAGE)?;
277297
let input_path = required_path(&parsed, "--input", USAGE)?;
@@ -285,6 +305,9 @@ fn run_execute(args: &[String]) -> Result<(), String> {
285305
keys_digest,
286306
require_cosign,
287307
oci_ref,
308+
cosign_key,
309+
cosign_cert_identity,
310+
cosign_cert_oidc_issuer,
288311
policy_path,
289312
input_path,
290313
receipt_path,
@@ -319,31 +342,71 @@ fn run_validate_receipt_v1_cmd(args: &[String]) -> Result<(), String> {
319342
validate_receipt_v1_file(&receipt_path)
320343
}
321344

322-
fn verify_bundle(
323-
bundle_dir: &Path,
324-
keys_path: &Path,
325-
keys_digest: &str,
345+
struct VerifyBundleArgs {
346+
bundle_dir: PathBuf,
347+
keys_path: PathBuf,
348+
keys_digest: String,
326349
require_cosign: bool,
327-
oci_ref: Option<&str>,
350+
oci_ref: Option<String>,
351+
cosign_key: Option<String>,
352+
cosign_cert_identity: Option<String>,
353+
cosign_cert_oidc_issuer: Option<String>,
328354
allow_experimental: bool,
329-
) -> Result<(), String> {
355+
}
356+
357+
fn verify_bundle(args: VerifyBundleArgs) -> Result<(), String> {
358+
let VerifyBundleArgs {
359+
bundle_dir,
360+
keys_path,
361+
keys_digest,
362+
require_cosign,
363+
oci_ref,
364+
cosign_key,
365+
cosign_cert_identity,
366+
cosign_cert_oidc_issuer,
367+
allow_experimental,
368+
} = args;
330369
let started = Instant::now();
331370
let result = (|| {
332371
let preflight_started = Instant::now();
333-
let bundle = load_verified_bundle(bundle_dir)?;
372+
let bundle = load_verified_bundle(&bundle_dir)?;
334373
require_manifest_schema_allowed(&bundle.manifest, allow_experimental)?;
335374
let preflight_ms = preflight_started.elapsed().as_millis() as u64;
336375

337376
let trust_started = Instant::now();
338-
let keys_raw = read_file_limited(keys_path, MAX_JSON_BYTES, "public-keys.json")?;
339-
verify_keys_digest(&keys_raw, keys_digest)?;
377+
let keys_raw = read_file_limited(&keys_path, MAX_JSON_BYTES, "public-keys.json")?;
378+
verify_keys_digest(&keys_raw, &keys_digest)?;
340379
let public_keys = parse_public_keys(&keys_raw)?;
341380
verify_signatures(&bundle.signatures, &public_keys).map_err(|e| e.to_string())?;
342-
if require_cosign || oci_ref.is_some() {
343-
let Some(ref_value) = oci_ref else {
344-
return Err("--oci-ref is required when --require-cosign is set".to_string());
381+
if require_cosign
382+
|| oci_ref.is_some()
383+
|| cosign_key.is_some()
384+
|| cosign_cert_identity.is_some()
385+
|| cosign_cert_oidc_issuer.is_some()
386+
{
387+
let Some(ref_value) = oci_ref.as_deref() else {
388+
return Err(
389+
"--oci-ref is required when cosign verification is configured".to_string(),
390+
);
391+
};
392+
let Some(cosign_key_path) = cosign_key.as_deref() else {
393+
return Err(
394+
"--cosign-key is required when cosign verification is configured".to_string(),
395+
);
396+
};
397+
let Some(cert_identity) = cosign_cert_identity.as_deref() else {
398+
return Err(
399+
"--cosign-cert-identity is required when cosign verification is configured"
400+
.to_string(),
401+
);
402+
};
403+
let Some(cert_oidc_issuer) = cosign_cert_oidc_issuer.as_deref() else {
404+
return Err(
405+
"--cosign-cert-oidc-issuer is required when cosign verification is configured"
406+
.to_string(),
407+
);
345408
};
346-
verify_cosign_oci_ref(ref_value)?;
409+
verify_cosign_oci_ref(ref_value, cosign_key_path, cert_identity, cert_oidc_issuer)?;
347410
}
348411
let trust_ms = trust_started.elapsed().as_millis() as u64;
349412

@@ -571,6 +634,9 @@ struct RunBundleArgs {
571634
keys_digest: String,
572635
require_cosign: bool,
573636
oci_ref: Option<String>,
637+
cosign_key: Option<String>,
638+
cosign_cert_identity: Option<String>,
639+
cosign_cert_oidc_issuer: Option<String>,
574640
policy_path: PathBuf,
575641
input_path: PathBuf,
576642
receipt_path: PathBuf,
@@ -585,6 +651,9 @@ fn run_bundle(args: RunBundleArgs) -> Result<(), String> {
585651
keys_digest,
586652
require_cosign,
587653
oci_ref,
654+
cosign_key,
655+
cosign_cert_identity,
656+
cosign_cert_oidc_issuer,
588657
policy_path,
589658
input_path,
590659
receipt_path,
@@ -612,11 +681,35 @@ fn run_bundle(args: RunBundleArgs) -> Result<(), String> {
612681

613682
let public_keys = parse_public_keys(&keys_raw)?;
614683
verify_signatures(&bundle.signatures, &public_keys).map_err(|e| e.to_string())?;
615-
if require_cosign || oci_ref.is_some() {
684+
if require_cosign
685+
|| oci_ref.is_some()
686+
|| cosign_key.is_some()
687+
|| cosign_cert_identity.is_some()
688+
|| cosign_cert_oidc_issuer.is_some()
689+
{
616690
let Some(ref_value) = oci_ref.as_deref() else {
617-
return Err("--oci-ref is required when --require-cosign is set".to_string());
691+
return Err(
692+
"--oci-ref is required when cosign verification is configured".to_string(),
693+
);
618694
};
619-
verify_cosign_oci_ref(ref_value)?;
695+
let Some(cosign_key_path) = cosign_key.as_deref() else {
696+
return Err(
697+
"--cosign-key is required when cosign verification is configured".to_string(),
698+
);
699+
};
700+
let Some(cert_identity) = cosign_cert_identity.as_deref() else {
701+
return Err(
702+
"--cosign-cert-identity is required when cosign verification is configured"
703+
.to_string(),
704+
);
705+
};
706+
let Some(cert_oidc_issuer) = cosign_cert_oidc_issuer.as_deref() else {
707+
return Err(
708+
"--cosign-cert-oidc-issuer is required when cosign verification is configured"
709+
.to_string(),
710+
);
711+
};
712+
verify_cosign_oci_ref(ref_value, cosign_key_path, cert_identity, cert_oidc_issuer)?;
620713
}
621714

622715
let policy = parse_policy_document(&policy_raw).map_err(|e| e.to_string())?;
@@ -1003,9 +1096,23 @@ fn emit_obs(event: &str, fields: Vec<(&str, Value)>) {
10031096
eprintln!("{}", Value::Object(record));
10041097
}
10051098

1006-
fn verify_cosign_oci_ref(oci_ref: &str) -> Result<(), String> {
1099+
fn verify_cosign_oci_ref(
1100+
oci_ref: &str,
1101+
cosign_key: &str,
1102+
cert_identity: &str,
1103+
cert_oidc_issuer: &str,
1104+
) -> Result<(), String> {
10071105
let output = Command::new("cosign")
1008-
.args(["verify", oci_ref])
1106+
.args([
1107+
"verify",
1108+
"--key",
1109+
cosign_key,
1110+
"--certificate-identity",
1111+
cert_identity,
1112+
"--certificate-oidc-issuer",
1113+
cert_oidc_issuer,
1114+
oci_ref,
1115+
])
10091116
.output()
10101117
.map_err(|e| format!("cosign verify invocation failed: {e}"))?;
10111118
if !output.status.success() {

0 commit comments

Comments
 (0)