Skip to content
ocx
install

cosign Parity

This page explains how far OCX and cosign interoperate on signatures. It covers which commands verify which artifacts, where the two tools deliberately differ, and what evidence backs each claim.

You may already sign container images with cosign and be asked to adopt a second tool. The question that decides it is whether the signature OCX writes is one your cosign verify step accepts. The reverse matters too: does OCX accept the signature your existing pipeline writes?

Both hold. OCX and cosign read and write the same Sigstore documents. One is a bundle v0.3 published as an OCI referrer. The others are the sha256-<hex>.sig and sha256-<hex>.att sidecar tags. Neither tool needs a flag to read what the other wrote, and a signature does not have to be re-issued to cross between them.

Where OCX diverges, it is a design decision with a reason, recorded here rather than smoothed over. For how signing works at all, see Signing. This page assumes it.

The recording below signs a package with OCX and verifies it with upstream cosign. It then signs a second package with cosign and verifies it with OCX, against one registry, one Fulcio and one Rekor.

OCX bundles are ordinary Sigstore bundles, and cosign reads and writes the same document. Two rules decide whether a given artifact crosses between the tools.

cosign 3.0 or later is required, and compatibility with earlier cosign versions is deliberately not offered. Bundle v0.3’s single-certificate profile is what cosign 3.x enforces. Cosign versions before 3.0 expect the v0.2 x509CertificateChain shape. OCX emits only the v0.3 shape. The verify path still reads both, so a v0.2 bundle keeps verifying.

Discovery needs no opt-in in either direction. One thing does have to be right. cosign must be given the platform manifest’s digest. A package tag resolves to its index, where no signature lives. See Addressing a Package.

The bundle itself interoperates in both directions, and OCX’s acceptance suite covers both against the same local Fulcio and Rekor. This is how cosign verifies a bundle that ocx produced:

Terminal window
cosign verify-blob \
--bundle ocx-bundle.json \
--trusted-root trusted_root.json \
--certificate-identity ci@example.com \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
manifest.json

In the other direction, a bundle from cosign sign-blob --bundle pushed as a referrer of the subject manifest verifies under ocx package verify unchanged.

For a self-hosted Fulcio and Rekor, note that cosign 3 removed --fulcio-url and --rekor-url from its signing commands. The endpoints come from a signing config instead. Create one with cosign signing-config create --out signing-config.json, then pass --signing-config. sign-blob also needs --trusted-root, because it verifies the Rekor entry it just created before writing the bundle.

Four independent choices bound interop:

  • which tool signs
  • which wire shape carries the signature
  • whether the signer is keyless or a key pair
  • whether the registry implements the OCI 1.1 Referrers API or only the fallback tag.

Every combination of those four works, in both directions. The tables below are not a summary of intent. Each row is a test that publishes a real package, signs it, and verifies it with the other tool. The test then corrupts one byte of the signature and requires the refusal. See What Proves This.

Wire shapeSignerRegistrycosign verify
Bundle referrerKeylessReferrers APIAccepts, transparency-log check included
Bundle referrerKeylessFallback tagAccepts, transparency-log check included
Bundle referrer--keyReferrers APIAccepts with --key and --insecure-ignore-tlog
Bundle referrer--keyFallback tagAccepts with --key and --insecure-ignore-tlog
sha256-<hex>.sig sidecarKeylessReferrers APIAccepts, transparency-log check included
sha256-<hex>.sig sidecarKeylessFallback tagAccepts, transparency-log check included
sha256-<hex>.sig sidecar--keyReferrers APIAccepts with --key and --insecure-ignore-tlog
sha256-<hex>.sig sidecar--keyFallback tagAccepts with --key and --insecure-ignore-tlog

The default bundle format needs no opt-in on either side. cosign discovers an OCX bundle through the Referrers API where the registry has one. Where it does not, cosign uses the sha256-<hex> fallback index. --signature-format simplesigning (or both) writes the sidecar as well, for a consumer that reads only that shape. It is not needed to make discovery work.

A key-mode signature carries no transparency-log entry unless one was requested (the Rekor rule). So cosign needs --insecure-ignore-tlog to accept one, the same flag it needs for its own key-mode signatures. Under keyless, no such flag is involved in either direction.

Wire shapeSignerRegistryocx package verify
Bundle referrerKeylessReferrers APIAccepts, discovery_method referrers_api
Bundle referrerKeylessFallback tagAccepts, discovery_method fallback_tag
Bundle referrer--keyReferrers APIAccepts with --key, discovery_method referrers_api
Bundle referrer--keyFallback tagAccepts with --key, discovery_method fallback_tag
sha256-<hex>.sig sidecarKeylessReferrers APIAccepts with --allow-unlogged-signature
sha256-<hex>.sig sidecarKeylessFallback tagAccepts with --allow-unlogged-signature
sha256-<hex>.sig sidecar--keyReferrers APIAccepts with --key, discovery_method sidecar_tag
sha256-<hex>.sig sidecar--keyFallback tagAccepts with --key, discovery_method sidecar_tag

Verify reads all three discovery paths with no flag. It reports which one carried the signature it accepted in the signatures[] array of ocx --format json package verify.

The two keyless sidecar rows are the one place a flag is unavoidable, and it is not an OCX quirk. cosign attach signature writes no transparency-log annotation, so cosign refuses those artifacts too. It needs --insecure-ignore-tlog for exactly the same reason. The two tools agree on the artifact and on the opt-out. See The Keyless Sidecar Rule for what the opt-out costs.

DSSE-enveloped in-toto attestations interoperate as bundle referrers, in both directions, on registries with and without the Referrers API.

DirectionCommandResult
OCX attestscosign verify-attestationAccepts; --check-claims validates the subject binding
cosign attestsocx package verify --attestation --type <TYPE>Accepts; predicate type resolved and matched

Bundle-referrer interop is tested keyless only. Key-mode attestation uses the same signing path as key-mode signatures, which is covered above. No test drives a key-mode attestation across the two tools through the bundle shape.

ocx package verify --attestation also reads the legacy sha256-<hex>.att sidecar tag that cosign attach attestation writes. That shape is reachable by tag and by nothing else. Its manifest declares neither artifactType nor subject, and cosign publishes no attestation artifact type at all, so no Referrers listing can reach it. OCX looks for it only under --attestation, and only once nothing bundle-shaped verified. An ordinary run never pays the extra request.

The two .att key models rest on different evidence. The key-mode shape is pinned to real cosign attach attestation output, captured as a committed fixture.

The keyless shape is not producible by cosign v3.1.1 at all. attach attestation takes no --certificate, and attest no longer writes the tag. So unit tests cover it. They repackage cosign’s own certificate, envelope and log entry into the measured layer shape.

Nothing in the suite verifies a keyless .att that cosign itself emitted, because cosign cannot emit one.

A --type that does not match what the artifact carries is a not-found condition on both sides, not a verification failure. OCX exits 79 (attestation_not_found). cosign reports that none of the attestations matched the requested predicate type.

cosign attach sbom writes the SBOM document itself to a sha256-<hex>.sbom tag. This is a third tag-only shape, and it is not a signature. ocx package sbom reads it.

DirectionCommandResult
cosign attachesocx package sbom --no-verifyLists the document verified: false, labelled by the layer’s own media type
cosign attachesocx package sbom --verifyRefuses: unsigned_rejected_by_policy, exit 77

The two rows are one contract, not a limitation. cosign attach sbom signs nothing. It prints “Attaching SBOMs this way does not sign them”. No cosign command signs that tag afterwards, so an attached SBOM has no signer for a policy to check.

Under --no-verify OCX lists it exactly as it lists any other unsigned attachment. Under --verify it refuses it exactly as it refuses an unsigned referrer. There is no third mode in which the shape verifies. A signed SBOM is an attestation: cosign attest --predicate sbom.json, which is the bundle-referrer shape in the table above.

Discovery is by tag and by nothing else, for the same structural reason .att is. The manifest declares neither artifactType nor subject, so no Referrers listing reaches it. What differs from .att is the layer. An .sbom layer keeps the SBOM document’s own media type rather than carrying a signature. That is why the reader is a document reader on the listing path, not a third signature reader.

The layer type OCX reads is cosign’s, which is not always the registered spelling:

  • --type spdx, cosign’s default, writes text/spdx+json. OCX’s own attach path writes application/spdx+json.
  • --type cyclonedx --input-format xml writes application/vnd.cyclonedx+xml.
  • Both of those list.
  • --type syft writes application/vnd.syft+json. OCX refuses it by name (sbom_media_type_unsupported, exit 65), because no in-toto predicate type names syft’s native format and there is nothing to label it with.

There is no reverse direction. OCX has no writer for an unsigned SBOM sidecar and does not intend one. ocx package push --sbom and ocx package attest produce a signed attestation instead.

Also read: cosign’s OCI 1.1 SBOM referrer. COSIGN_EXPERIMENTAL=1 cosign attach sbom --registry-referrers-mode oci-1-1 writes it with artifactType: application/vnd.dev.cosign.artifact.sbom.v1+json and types its layer by the document. Both shapes land in the same listing.

An OCX package is a multi-platform OCI index, so a signature can sit on the index or on one platform manifest beneath it. -p decides which. It also decides the reference cosign has to be given.

ocx package sign with no -p signs the index itself, the same shape cosign uses for a multi-platform tag. Nothing special is needed on the cosign side. The tag resolves to the index, which is where the signature is.

Terminal window
cosign verify \
--certificate-identity ci@example.com \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
registry.example/acme/tool:1.0.0

ocx package sign -p linux/amd64 narrows the subject to that platform’s manifest instead. ocx package verify -p linux/amd64 resolves it back the same way. cosign has no notion of a package and resolves whatever reference you hand it. So the tag still resolves to the index, where this signature is not, and cosign reports a discovery failure that reads like an interop problem. Give it the platform manifest’s own digest:

Terminal window
# --resolve is required: it walks index -> manifest and reports the manifest.
digest=$(ocx --format json package inspect --resolve -p linux/amd64 \
registry.example/acme/tool:1.0.0 | jq -r '.packages[0].pinned_digest')
cosign verify \
--certificate-identity ci@example.com \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
"registry.example/acme/tool@${digest}"

Two behaviours differ from cosign’s defaults. Both are deliberate, and neither changes the wire format. An artifact produced under either still verifies with the other tool.

BehaviourcosignOCXWhy
Rekor upload under a keyOn by defaultOff by default; --rekor-upload opts inrekor_url defaults to the public log. An on-by-default key path would publish a private artifact’s digest and signer identity to a world-readable log the first time it ran, the opposite of what choosing a key over keyless usually means
Rekor upload under keylessOpt-out availableMandatory; --no-rekor-upload is a usage error (exit 64)A Fulcio certificate lives about ten minutes. The log timestamp is the only lasting proof the signature happened while it was valid, so a keyless signature without one is unverifiable later by construction

Two things that look like divergences are not. Both tools omit the DSSE keyid member. cosign matches candidate signatures on it, so writing one makes cosign verify --key accept none of them. OCX names the key in verificationMaterial.publicKey.hint, where cosign does.

And on a registry without the Referrers API, both tools write the sha256-<digest> fallback index rather than refusing. See Publishing a Referrer.

On the read side, keyid is a lookup hint and never a security decision. OCX accepts an envelope whose keyid is absent, empty, or hostile, and the value decides nothing.

A sha256-<hex>.sig or sha256-<hex>.att sidecar carries its verification material in layer annotations. For a keyless signature the dev.sigstore.cosign/bundle annotation, the offline Rekor entry, is required. A sidecar without it is refused with exit 65 (signature_invalid).

Both shapes are held to one gate. The .att reader was built against the .sig path rather than reimplemented beside it. No shape verifies on weaker evidence than the other.

The reason is the certificate’s lifetime. Fulcio issues a certificate valid for roughly ten minutes. The transparency-log entry’s integratedTime is the only thing that places the signature inside that window. Without an entry there is no instant to check the certificate against. A certificate that expired months ago stays as acceptable as one issued a moment ago.

--allow-unlogged-signature accepts the sidecar anyway. It exists for air-gapped CI, where the entry could not be fetched or was never written.

What it stops checking. Three checks do not run. They are skipped, not satisfied by a substitute:

  • The Signed Entry Timestamp is not verified against the log’s public key.
  • The logged body is not bound to the artifact. That check requires the logged entry to name this payload’s digest and carry this signature. It is a hashedrekord body for a .sig and a dsse body for an .att. Without it, a real entry for a different artifact would look the same as an entry about the bytes in hand.
  • The certificate validity window is not evaluated at all. There is no trustworthy instant to evaluate it at. Taking the instant from the certificate’s own notBefore would be circular, because a check that asks the certificate when it was valid can never fail.

What still runs. The certificate chain to the trust root, the SCT, the signature over the subject digest, and identity matching all still run. Identity matching uses --certificate-identity and --certificate-oidc-issuer, or a matching [[trust.policy]]. On an .att, the statement’s subject binding and any builder pin the policy carries run too. A tampered signature is still refused under the flag.

The practical consequence is this. Under --allow-unlogged-signature a signature is proven to come from a certificate the trust root vouches for. It is not proven to have been made while that certificate was valid. An accepted signature reports neither signed_at nor rekor_log_index, because there is no timestamp to report. The absence of those fields is how a downstream consumer tells the two cases apart.

The flag is inert everywhere else. It does not affect the bundle path, where transparency evidence stays mandatory under keyless. It does not affect key-mode verification of either sidecar shape, which never reads the annotation. A key signature’s trust rests on the committed public key rather than on a signing instant.

Five things do not work, or do not work fully. They are gaps rather than divergences. Nothing here is a decision OCX would defend, and each is a place where prose could easily overclaim.

  • cosign’s fallback-tag write drops annotations. On a registry without the Referrers API, cosign’s own fallback index loses dev.sigstore.bundle.content, dev.sigstore.bundle.predicateType and org.opencontainers.image.created from the referrer descriptor (sigstore/cosign#4641). artifactType survives. OCX reads these artifacts regardless, and OCX’s own fallback write preserves all four. The reader is not weakened to match.
  • Signing is sha256-only. ocx package sign and ocx package attest refuse a subject addressed by sha384 or sha512 with exit 65 (subject_digest_unsupported), before anything is published or logged. cosign is sha256-only in the same places. The in-toto Statement binds on sha256, and the sidecar tag truncates the digest to 64 characters. The alternative is a signature that publishes and then cannot be verified. Verification is unaffected, so an already-published artifact reads back as written.
  • ocx package copy does not carry sidecar-tag signatures. copy --referrers follows referrer chains, and a sha256-<hex>.sig or .att sidecar is an ordinary tag rather than a referrer. So a simplesigning signature does not survive a mirror copy. The referrer-attached bundle shape does. Re-sign at the destination when the mirrored artifact needs a sidecar.
  • KMS key backends are not implemented. awskms://, gcpkms://, azurekms://, hashivault:// and k8s:// are recognised by name and refused with exit 82 (unsupported_key_backend). That holds for --key, for a key = "…" signer in a matched [[trust.policy]], and for a managed-config payload. cosign implements all five. The distinct slug lets a script tell “not built yet” from a malformed config (78) or a missing file (74) without parsing stderr.
  • Rekor v1 only. OCX targets Rekor v1 entries. A bundle from a Rekor v2 instance is rejected with exit 65 (rekor_set_absent_tsa_present). This bounds interop with any cosign configured against a v2 log. See Current Limitations.

Every accept and refuse on this page is a test in the acceptance suite, run against upstream cosign. The suite uses the real binary from ghcr.io/sigstore/cosign/cosign:v3.1.1, pinned to an exact version so a passing run is attributable to one. It drives a live Fulcio, Rekor and registry.

The suite covers 16 image-level cells (the two eight-row tables in What Interoperates) and 7 attestation and attached-SBOM cells. Twelve of the image cells carry one artifact through all six of the steps below. The four key-mode cosign-signs cells assert the refusal half against a second, identically-constructed artifact in a shared parametrized test.

The attached-SBOM cell (Attached SBOMs) runs a shorter sequence, because nothing in it is signed and so nothing can be corrupted. It substitutes a control. The same command on the same subject exits 79 before cosign attaches anything. So the listing that follows is attributable to the attachment, not to a reader that reports something for every subject.

Each full cell runs these steps in order:

  1. Publish a package to a registry of the cell’s kind: Zot for the Referrers API, CNCF Distribution for the fallback tag.
  2. Sign it with the cell’s tool, wire shape and key model.
  3. Assert exactly one signature is discoverable, so the cell cannot pass on a second artifact left behind by an earlier step.
  4. Assert the other tool accepts it.
  5. Corrupt one byte of the signature, and assert the corruption reached the registry.
  6. Assert the other tool refuses it, with an exact exit code and an exact message.

Step 6 is what makes the green meaningful. A test that only asserts acceptance passes just as well against a verifier that accepts everything. Pinning the refusal’s exit code and message gives the check a reachable red state. The corruption in step 5 is verified to have landed rather than assumed.

Claims on this page that no test backs, stated rather than dropped:

  • Key-mode attestation across the two tools. The attestation table is keyless only.
  • Index-level signing verified by tag, the first recipe in Addressing a Package. The source shows that ocx package sign without -p signs the index. Every cell in the matrix signs one platform manifest with -p and verifies against its digest, so the tag-reference path is not measured.
  • cosign attach sbom in either direction (Known Gaps).
  • cosign’s own keyless Rekor opt-out, asserted in the divergence table from cosign’s documentation. Nothing here drives cosign sign --tlog-upload=false under keyless.
  • --allow-unlogged-signature being inert elsewhere. The source shows the flag reaches only the sidecar verifier, so it cannot lift a bundle-path or key-mode refusal. No test asserts it.
  • Rekor v2 rejection with exit 65. The mapping is in the source. No Rekor v2 instance is exercised.
  • The keyless .att refusal and its opt-out. Unit tests cover both against a fixture that repackages cosign’s own certificate, envelope and log entry into the measured .att layer shape. It is not an artifact cosign emitted, because cosign v3.1.1 cannot emit a keyless .att (Attestations). The key-mode .att is pinned to real cosign attach attestation output.
  • The --resolve recipe in Addressing a Package. Both halves are tested separately: --resolve yields a per-platform digest, and cosign verifies a manifest digest. No test pipes one into the other.
  • The registry support lists in Signing are drawn from vendor documentation, not exercised here. The suite runs against Zot and CNCF Distribution only, one registry per capability.