- docs
- Concepts
- cosign Parity
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.
cosign Interoperability
Section titled “cosign Interoperability”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:
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.jsonIn 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.
What Interoperates
Section titled “What Interoperates”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.
OCX signs, cosign verifies
Section titled “OCX signs, cosign verifies”| Wire shape | Signer | Registry | cosign verify |
|---|---|---|---|
| Bundle referrer | Keyless | Referrers API | Accepts, transparency-log check included |
| Bundle referrer | Keyless | Fallback tag | Accepts, transparency-log check included |
| Bundle referrer | --key | Referrers API | Accepts with --key and --insecure-ignore-tlog |
| Bundle referrer | --key | Fallback tag | Accepts with --key and --insecure-ignore-tlog |
sha256-<hex>.sig sidecar | Keyless | Referrers API | Accepts, transparency-log check included |
sha256-<hex>.sig sidecar | Keyless | Fallback tag | Accepts, transparency-log check included |
sha256-<hex>.sig sidecar | --key | Referrers API | Accepts with --key and --insecure-ignore-tlog |
sha256-<hex>.sig sidecar | --key | Fallback tag | Accepts 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.
cosign signs, OCX verifies
Section titled “cosign signs, OCX verifies”| Wire shape | Signer | Registry | ocx package verify |
|---|---|---|---|
| Bundle referrer | Keyless | Referrers API | Accepts, discovery_method referrers_api |
| Bundle referrer | Keyless | Fallback tag | Accepts, discovery_method fallback_tag |
| Bundle referrer | --key | Referrers API | Accepts with --key, discovery_method referrers_api |
| Bundle referrer | --key | Fallback tag | Accepts with --key, discovery_method fallback_tag |
sha256-<hex>.sig sidecar | Keyless | Referrers API | Accepts with --allow-unlogged-signature |
sha256-<hex>.sig sidecar | Keyless | Fallback tag | Accepts with --allow-unlogged-signature |
sha256-<hex>.sig sidecar | --key | Referrers API | Accepts with --key, discovery_method sidecar_tag |
sha256-<hex>.sig sidecar | --key | Fallback tag | Accepts 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.
Attestations
Section titled “Attestations”DSSE-enveloped in-toto attestations interoperate as bundle referrers, in both directions, on registries with and without the Referrers API.
| Direction | Command | Result |
|---|---|---|
| OCX attests | cosign verify-attestation | Accepts; --check-claims validates the subject binding |
| cosign attests | ocx 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.
Attached SBOMs
Section titled “Attached SBOMs”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.
| Direction | Command | Result |
|---|---|---|
| cosign attaches | ocx package sbom --no-verify | Lists the document verified: false, labelled by the layer’s own media type |
| cosign attaches | ocx package sbom --verify | Refuses: 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, writestext/spdx+json. OCX’s own attach path writesapplication/spdx+json.--type cyclonedx --input-format xmlwritesapplication/vnd.cyclonedx+xml.- Both of those list.
--type syftwritesapplication/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.
Addressing a Package
Section titled “Addressing a Package”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.
cosign verify \ --certificate-identity ci@example.com \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ registry.example/acme/tool:1.0.0ocx 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:
# --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}"Where OCX Differs
Section titled “Where OCX Differs”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.
| Behaviour | cosign | OCX | Why |
|---|---|---|---|
| Rekor upload under a key | On by default | Off by default; --rekor-upload opts in | rekor_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 keyless | Opt-out available | Mandatory; --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.
The Keyless Sidecar Rule
Section titled “The Keyless Sidecar Rule”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
hashedrekordbody for a.sigand adssebody 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
notBeforewould 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.
Known Gaps
Section titled “Known Gaps”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.predicateTypeandorg.opencontainers.image.createdfrom the referrer descriptor (sigstore/cosign#4641).artifactTypesurvives. 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 signandocx package attestrefuse 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 onsha256, 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 copydoes not carry sidecar-tag signatures.copy --referrersfollows referrer chains, and asha256-<hex>.sigor.attsidecar 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://andk8s://are recognised by name and refused with exit 82 (unsupported_key_backend). That holds for--key, for akey = "…"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.
What Proves This
Section titled “What Proves This”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:
- Publish a package to a registry of the cell’s kind: Zot for the Referrers API, CNCF Distribution for the fallback tag.
- Sign it with the cell’s tool, wire shape and key model.
- Assert exactly one signature is discoverable, so the cell cannot pass on a second artifact left behind by an earlier step.
- Assert the other tool accepts it.
- Corrupt one byte of the signature, and assert the corruption reached the registry.
- 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 signwithout-psigns the index. Every cell in the matrix signs one platform manifest with-pand verifies against its digest, so the tag-reference path is not measured. cosign attach sbomin 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=falseunder keyless. --allow-unlogged-signaturebeing 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
.attrefusal and its opt-out. Unit tests cover both against a fixture that repackages cosign’s own certificate, envelope and log entry into the measured.attlayer shape. It is not an artifact cosign emitted, because cosign v3.1.1 cannot emit a keyless.att(Attestations). The key-mode.attis pinned to realcosign attach attestationoutput. - The
--resolverecipe in Addressing a Package. Both halves are tested separately:--resolveyields 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.
See Also
Section titled “See Also”- Signing: how signing works, covering trust roots, bundle storage, identity matching, keyless versus a key
package signreference: every flag, including--signature-formatand--rekor-uploadpackage verifyreference: every flag, thesignatures[]report shape, and every exit code- Self-hosted Sigstore: running your own Fulcio and Rekor, as the recording above does
- Configuration reference,
[[trust.policy]]: pinning an identity once instead of passing it per command