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
271 changes: 261 additions & 10 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ name = "ai-catalog"
path = "src/main.rs"

[dependencies]
ai-catalog = "0.3.0"
ai-catalog-discover = "0.3.0"
ai-catalog-oci = "0.3.0"
ai-catalog-trust = "0.3.0"
ai-catalog-validate = "0.3.0"
ai-catalog = "0.4.0"
ai-catalog-discover = "0.4.0"
ai-catalog-oci = "0.4.0"
ai-catalog-trust = "0.4.0"
ai-catalog-validate = "0.4.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
colored = "3"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ stdout. Omitting `--output` writes to the current directory.

`--scope` follows the same name-or-URI rules as `show`.

If the entry's trust manifest has a `subject`, the content is checked against
`subject.digest` first, and a mismatch fails the pull. Signatures are not verified.
If the entry has a `digest`, the content is checked against it first, and a
mismatch fails the pull. Signatures are not verified.

When the identifier resolves to a nested catalog entry
(`application/ai-catalog+json`), `--media-type` is required:
Expand Down Expand Up @@ -484,7 +484,7 @@ variants are restricted to OCI-sourced entries. See

When `--cosign-key` is supplied to `oci export-layout` or `oci push`, the CLI:

1. Canonicalizes each entry trust manifest (key-sorted JSON, `signature` field stripped)
1. Canonicalizes each entry trust manifest (key-sorted JSON, `signature.jws` and `additionalSignatures` stripped)
2. Signs the canonical blob with `cosign sign-blob`
3. Attaches three OCI referrer artifacts to each signed entry:
- the canonical trust manifest (`application/vnd.ai-catalog.trust-manifest.v1+json`)
Expand Down
23 changes: 12 additions & 11 deletions demo/consumer-walkthrough.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,18 @@ try {
"tags": ["agent", "chat", "nlp", "a2a"],
"url": "https://example.com/agents/conversational-v1.json",
"version": "1.0.0",
"trustManifest": {
"identity": "urn:demo:agent:conversational-v1",
"identityType": "urn",
"attestations": [
{
"type": "SOC2-Type2",
"uri": "https://trust.example.com/reports/soc2-2026.pdf",
"digest": "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
}
]
}
"trustManifests": [
{
"contributor": "urn:demo:agent:conversational-v1",
"attestations": [
{
"type": "SOC2-Type2",
"uri": "https://trust.example.com/reports/soc2-2026.pdf",
"digest": "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
}
]
}
]
},
{
"identifier": "urn:demo:model:embeddings-v2",
Expand Down
23 changes: 12 additions & 11 deletions demo/consumer-walkthrough.sh
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,18 @@ cat > "$root_catalog_path" <<EOF
"tags": ["agent", "chat", "nlp", "a2a"],
"url": "https://example.com/agents/conversational-v1.json",
"version": "1.0.0",
"trustManifest": {
"identity": "urn:demo:agent:conversational-v1",
"identityType": "urn",
"attestations": [
{
"type": "SOC2-Type2",
"uri": "https://trust.example.com/reports/soc2-2026.pdf",
"digest": "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
}
]
}
"trustManifests": [
{
"contributor": "urn:demo:agent:conversational-v1",
"attestations": [
{
"type": "SOC2-Type2",
"uri": "https://trust.example.com/reports/soc2-2026.pdf",
"digest": "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
}
]
}
]
},
{
"identifier": "urn:demo:model:embeddings-v2",
Expand Down
14 changes: 8 additions & 6 deletions demo/oci-layout-walkthrough.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -266,13 +266,15 @@ try {
"name": "inline",
"version": 1
},
"trustManifest": {
"identity": "urn:example:inline",
"trustSchema": {
"identifier": "urn:ai-catalog:trust-schema:minimal:v1",
"version": "1.0"
"trustManifests": [
{
"contributor": "urn:example:inline",
"trustSchema": {
"identifier": "urn:ai-catalog:trust-schema:minimal:v1",
"version": "1.0"
}
}
}
]
}
]
}
Expand Down
14 changes: 8 additions & 6 deletions demo/oci-layout-walkthrough.sh
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,15 @@ cat > "$catalog_json" <<'EOF'
"name": "inline",
"version": 1
},
"trustManifest": {
"identity": "urn:example:inline",
"trustSchema": {
"identifier": "urn:ai-catalog:trust-schema:minimal:v1",
"version": "1.0"
"trustManifests": [
{
"contributor": "urn:example:inline",
"trustSchema": {
"identifier": "urn:ai-catalog:trust-schema:minimal:v1",
"version": "1.0"
}
}
}
]
}
]
}
Expand Down
18 changes: 10 additions & 8 deletions demo/trust-walkthrough.sh
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,15 @@ cat > "$catalog_json" <<'EOF'
"type": "application/a2a-agent-card+json",
"description": "An A2A agent whose trust manifest will be signed in this walkthrough",
"url": "https://example.com/agents/trusted-v1.json",
"trustManifest": {
"identity": "urn:demo:agent:trusted-v1",
"trustSchema": {
"identifier": "urn:ai-catalog:trust-schema:minimal:v1",
"version": "1.0"
"trustManifests": [
{
"contributor": "urn:demo:agent:trusted-v1",
"trustSchema": {
"identifier": "urn:ai-catalog:trust-schema:minimal:v1",
"version": "1.0"
}
}
}
]
},
{
"identifier": "urn:demo:model:unsigned",
Expand Down Expand Up @@ -213,7 +215,7 @@ tm_config_digest="$(manifest_config_digest "$tm_manifest_json")"
payload_file="$tmp_root/canonical-trust-manifest.json"
"$ORAS_BIN" blob fetch --oci-layout --output "$payload_file" "${layout_dir}@${tm_config_digest}"

echo "Canonical trust manifest (key-sorted JSON, no 'signature' field):"
echo "Canonical trust manifest (key-sorted JSON, no 'signature.jws' member):"
cat "$payload_file"

# ── Step 7 ─────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -248,7 +250,7 @@ fi
# ── Step 9 ────────────────────────────────────────────────────────────────
step "9. Demonstrate tamper detection — modify the payload and re-verify"
tampered_file="$tmp_root/tampered-trust-manifest.json"
# Replace the identity value with a different string
# Replace the contributor value with a different string
sed 's/urn:demo:agent:trusted-v1/urn:attacker:agent:evil/g' "$payload_file" > "$tampered_file"

echo "Tampered payload:"
Expand Down
25 changes: 13 additions & 12 deletions examples/root-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,19 @@
"identifier": "did:web:acme-corp.com",
"displayName": "Acme Corp"
},
"trustManifest": {
"identity": "did:web:acme-corp.com",
"identityType": "did",
"attestations": [
{
"type": "SOC2-Type2",
"uri": "https://trust.acme-corp.com/reports/soc2-2026.pdf",
"digest": "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
"size": 245760
}
]
},
"trustManifests": [
{
"contributor": "did:web:acme-corp.com",
"attestations": [
{
"type": "SOC2-Type2",
"uri": "https://trust.acme-corp.com/reports/soc2-2026.pdf",
"digest": "sha256:a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
"size": 245760
}
]
}
],
"data": {
"specVersion": "1.0",
"entries": [
Expand Down
1 change: 1 addition & 0 deletions src/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl CacheManager {
}),
entries: vec![],
signature: None,
additional_signatures: None,
extensions: None,
extra_fields: Default::default(),
});
Expand Down
3 changes: 2 additions & 1 deletion src/commands/catalog_add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ fn add(cache: &CacheManager, fetcher: &dyn Fetcher, name: &str, input: &str) ->
updated_at: Some(chrono::Utc::now().to_rfc3339()),
extensions: Some(extensions),
publisher: None,
trust_manifest: None,
digest: None,
trust_manifests: Vec::new(),
extra_fields: Default::default(),
};
let mut registry = cache.read_registry()?;
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(crate) fn catalog_of(entries: impl IntoIterator<Item = CatalogEntry>) -> AiC
host: None,
entries,
signature: None,
additional_signatures: None,
extensions: None,
extra_fields: Default::default(),
}
Expand Down
3 changes: 2 additions & 1 deletion src/commands/oci_add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ pub fn execute(name: &str, layout_path: &str, ref_name: Option<&str>) -> Result<
updated_at: Some(chrono::Utc::now().to_rfc3339()),
extensions: Some(extensions),
publisher: None,
trust_manifest: None,
digest: None,
trust_manifests: Vec::new(),
extra_fields: Default::default(),
};

Expand Down
20 changes: 8 additions & 12 deletions src/commands/pull.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ fn write_entry(
entry.identifier
)));
};
let verified = verify_subject_digest(entry, served.as_deref())?;
let verified = verify_entry_digest(entry, served.as_deref())?;

if output_path == Some("-") {
std::io::stdout().write_all(&bytes)?;
Expand All @@ -182,18 +182,14 @@ fn write_entry(
Ok(())
}

/// Check the content against `subject.digest` of the entry's trust manifest.
/// Check the content against the entry's `digest`.
///
/// For an artifact behind a `url` the digest covers the bytes as served
/// (`served`); for inline `data` it covers the JCS form of the value. Returns
/// whether there was a digest to check. This says nothing about who published
/// the content: signatures are not verified.
fn verify_subject_digest(entry: &CatalogEntry, served: Option<&[u8]>) -> Result<bool> {
let Some(subject) = entry
.trust_manifest
.as_ref()
.and_then(|manifest| manifest.subject.as_ref())
else {
fn verify_entry_digest(entry: &CatalogEntry, served: Option<&[u8]>) -> Result<bool> {
let Some(digest) = entry.digest.as_deref() else {
return Ok(false);
};
let canonical;
Expand All @@ -207,14 +203,14 @@ fn verify_subject_digest(entry: &CatalogEntry, served: Option<&[u8]>) -> Result<
(None, Some(bytes)) => bytes,
(None, None) => return Ok(false),
};
match ai_catalog_trust::verify_digest(&subject.digest, content) {
match ai_catalog_trust::verify_digest(digest, content) {
Ok(true) => Ok(true),
Ok(false) => Err(Error::Other(format!(
"content of \"{}\" does not match its subject.digest {}",
entry.identifier, subject.digest
"content of \"{}\" does not match its digest {}",
entry.identifier, digest
))),
Err(e) => Err(Error::Other(format!(
"cannot check subject.digest of \"{}\": {e}",
"cannot check digest of \"{}\": {e}",
entry.identifier
))),
}
Expand Down
15 changes: 11 additions & 4 deletions src/commands/show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,17 @@ fn print_entry_table(found: &ResolvedEntry, cache: &CacheManager) {
publisher.identifier
);
}
if let Some(trust) = &entry.trust_manifest {
println!(" {} {}", "Trust identity:".bold(), trust.identity);
if trust.signature.is_some() {
println!(" {} present", "Signature:".bold());
if let Some(digest) = &entry.digest {
println!(" {} {}", "Digest:".bold(), digest);
}
for trust in &entry.trust_manifests {
println!(" {} {}", "Trust contributor:".bold(), trust.contributor);
if let Some(signature) = &trust.signature {
println!(
" {} present (profile {}, not verified)",
"Signature:".bold(),
signature.profile
);
}
if !trust.attestations.is_empty() {
println!(
Expand Down
24 changes: 14 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -677,11 +677,11 @@ fn attach_cosign_material(
.entries
.iter()
.zip(artifacts.index.manifests.iter())
.filter_map(|(entry, descriptor)| {
.flat_map(|(entry, descriptor)| {
entry
.trust_manifest
.clone()
.map(|trust_manifest| (descriptor.digest.clone(), trust_manifest))
.trust_manifests
.iter()
.map(|trust_manifest| (descriptor.digest.clone(), trust_manifest.clone()))
})
.collect::<Vec<_>>();

Expand All @@ -704,7 +704,7 @@ fn attach_cosign_material(
attach_cosign_verification_artifacts(
artifacts,
&subject_digest,
&trust_manifest.identity,
&trust_manifest.contributor,
&payload_digest,
&signature,
&public_key,
Expand All @@ -715,8 +715,8 @@ fn attach_cosign_material(
Ok(catalog
.entries
.iter()
.filter(|entry| entry.trust_manifest.is_some())
.count())
.map(|entry| entry.trust_manifests.len())
.sum())
}

fn load_cosign_public_key(
Expand Down Expand Up @@ -1249,9 +1249,13 @@ mod tests {
"displayName": "Example Model",
"type": "application/json",
"data": { "name": "example" },
"trustManifest": {
"identity": "urn:example:model"
}
"trustManifests": [{
"contributor": "urn:example:model",
"trustSchema": {
"identifier": "urn:trust:example",
"version": "1.0"
}
}]
}
]
}"#;
Expand Down
Loading
Loading