Skip to content

signatures not discoverable by cosign - #299

Merged
handcaught merged 1 commit into
mainfrom
signatures-not-discoverable-by-cosign
Aug 3, 2026
Merged

signatures not discoverable by cosign#299
handcaught merged 1 commit into
mainfrom
signatures-not-discoverable-by-cosign

Conversation

@handcaught

Copy link
Copy Markdown
Member

signatures-not-discoverable-by-cosign

Repository: brokenbots/criteria

Adapters published by criteria adapter publish are correctly signed, and
criteria verifies them. No external tool can find those signatures.
cosign verify reports no signatures found against every adapter we publish.

Confirmed behavior

Measured on 2026-08-02 against ghcr.io/brokenbots/criteria-adapter-mcp:0.1.0,
published by the Publish MCP Adapter workflow (run 30781750899, keyless).

The signature itself is correct and standards-compliant. Fetching the
signature manifest sha256:58b4b537290c8c6ecb6cf00e3b45f31522283b1e737e3d16f4ca3d266306d9c7:

{
  "artifactType": null,
  "config":  "application/vnd.oci.empty.v1+json",
  "subject": "sha256:5d0d712d3fca3e2e42d880edfac867f5a58e071a9d58c2fc8611679e72e90b8a",
  "layers": [{
    "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
    "annotations": [
      "dev.cosignproject.cosign/signature",
      "dev.sigstore.cosign/bundle",
      "dev.sigstore.cosign/certificate"
    ]
  }]
}

The payload binds the artifact's manifest digest, the Fulcio certificate and
Rekor bundle are present under the standard annotation keys, and subject
correctly references the signed artifact. Criteria's own verifier reads all of
it — a clean-cache criteria adapter pull reports:

Signer: https://github.com/brokenbots/criteria/.github/workflows/publish-adapter-mcp.yml@refs/tags/mcp/v0.1.0
        (issuer: https://token.actions.githubusercontent.com)

The defect is that artifactType is unset. With no artifactType, the
registry derives the referrer's type from the config media type. The OCI 1.1
referrers fallback index published at tag
sha256-5d0d712d3fca3e2e42d880edfac867f5a58e071a9d58c2fc8611679e72e90b8a
therefore advertises the signature as:

{ "artifactType": "application/vnd.oci.empty.v1+json",
  "digest": "sha256:58b4b537290c8c6ecb6cf00e3b45f31522283b1e737e3d16f4ca3d266306d9c7" }

Cosign discovers signatures by filtering referrers on artifact type. Its own
source names the value it looks for:

const wantArtifactType = "application/vnd.dev.cosign.artifact.sig.v1+json"

An application/vnd.oci.empty.v1+json referrer does not match, so cosign
concludes there is no signature. Verified:

$ cosign verify --certificate-identity-regexp '.*' \
    --certificate-oidc-issuer https://token.actions.githubusercontent.com \
    ghcr.io/brokenbots/criteria-adapter-mcp:0.1.0
Error: no signatures found

The manifest is built in internal/adapter/publish/sign.go,
buildSignatureManifest (~line 237). It sets MediaType, Config, Subject,
and Layers, and never sets ArtifactType.

This affects every adapter published by this code path —
criteria-adapter-shell, criteria-adapter-copilot, and
criteria-adapter-noop are all published the same way and are all
externally unverifiable today.

Already correct — do not regress these

  1. The signing payload. Simple-signing over the artifact's manifest digest,
    payload.Critical.Type = "cosign container image signature", layer media type
    ctypes.SimpleSigningMediaType. Correct; do not change the payload format.
  2. The annotation keys. dev.cosignproject.cosign/signature,
    dev.sigstore.cosign/certificate, dev.sigstore.cosign/chain,
    dev.sigstore.cosign/bundle. These are what cosign reads once it finds the
    manifest. Do not rename them.
  3. The subject binding to the signed artifact's descriptor.
  4. The empty-JSON config descriptor. The existing comment records that GHCR
    rejects a push with a zero-value config descriptor (HTTP 500). Whatever
    changes, the push must continue to succeed against GHCR.
  5. Criteria's own verification path. internal/adapter/signing/verify.go
    must keep verifying artifacts published before and after this change. Existing
    published adapters must not become unverifiable by criteria.
  6. Digest pinning. .criteria.lock.hcl digests must continue to resolve.

Required behavior

  1. A signature published by criteria adapter publish must be discoverable
    and verifiable by cosign verify
    , for both keyless and explicit-key
    signing, without any criteria-specific flag or configuration on the verifying
    side.

  2. The signature manifest must declare the artifact type cosign looks for.
    Take the value from the cosign dependency rather than hardcoding a string
    literal if the module exports it; github.com/sigstore/cosign/v3 is already a
    direct dependency and ctypes is already imported by this file. If no
    exported constant exists, define one in criteria with a comment naming the
    cosign source it mirrors.

  3. Signatures must remain discoverable on registries that do not implement the
    referrers API.
    Establish whether the referrers fallback index alone is
    sufficient for cosign, or whether the legacy sha256-<digest>.sig tag must
    also be published. Determine this by testing against a real registry, not by
    reading the spec — then implement whichever is required. If both are needed,
    publish both.

  4. Previously published adapters remain verifiable by criteria. This change
    alters what is written at publish time; it must not alter how criteria reads
    artifacts already in registries.

Verification

Exercisable in-cycle — do these:

  • Publish a signed artifact to a local registry with an explicit key, then run
    cosign verify --key <pub> <ref> and show it exits 0.
  • Publish an unsigned artifact to the same registry, run the same
    cosign verify, and show it exits non-zero. Use a different manifest
    digest for the unsigned case, so a signature attached to the signed digest
    cannot satisfy the unsigned verify.
  • Show criteria adapter pull still verifying a newly published artifact.
  • Show criteria adapter pull still verifying
    ghcr.io/brokenbots/criteria-adapter-mcp:0.1.0, which was published before
    this change. This is the backward-compatibility check and it is the one most
    likely to be skipped.
  • make test and make lint clean.

Not exercisable in-cycle — state as unverified:

  • Keyless signing produces a cosign-verifiable artifact on GHCR. Keyless needs
    CI OIDC. Name it as an operator step: republish an adapter after this lands
    and run cosign verify with the certificate identity pinned.

Out of scope

  • Changing transitionDefaultMode (internal/cli/verification.go), which is
    signing.ModeWarn under decision D-WS46-1 pending WS48. Separate concern.
  • Re-signing or re-publishing already-published adapters.
  • The signature payload format, annotation keys, or the trust roots used.

Set the OCI artifact type cosign filters on
(application/vnd.dev.cosign.artifact.sig.v1+json) in the signature manifest,
and also publish the legacy sha256-<digest>.sig tag. Without these, cosign
verify reports no signatures found for adapters published by criteria.

The change keeps the existing signing payload, annotation keys, subject
binding, and empty-JSON config descriptor unchanged so criteria's own
verifier continues to accept both pre-existing and newly-published
adapters.

Tests added/updated:
- assert ArtifactType in buildSignatureManifest tests
- unit test for the legacy .sig tag shape
- unit test proving signArtifact tags the signature manifest when the
  store supports tagging

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@brokenbot brokenbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. Signature manifests now declare artifactType=application/vnd.dev.cosign.artifact.sig.v1+json and publish the legacy sha256-.sig tag, so cosign can discover+verify criteria-published signatures on both OCI-1.1 referrer and legacy-tag paths.

I exercised every in-cycle verification step myself (cosign installed, local registry:2.8, Ed25519 keypair):

  • Signed publish -> cosign verify --key exits 0 (default legacy-tag path AND COSIGN_EXPERIMENTAL=1 referrers path).
  • Unsigned publish (distinct manifest digest) -> cosign verify exits non-zero ('no signatures found').
  • Fallback referrers index entry now carries the correct artifactType (was application/vnd.oci.empty.v1+json).
  • criteria adapter pull verifies a newly published signed artifact.
  • criteria adapter pull verifies ghcr.io/brokenbots/criteria-adapter-mcp:0.1.0 (pre-change, keyless) -- backward compat holds; verify.go untouched.
  • make test and make lint clean; all CI gates green.

No 'already correct' guarantees regressed: payload, annotation keys, subject binding, empty-JSON config, criteria verification path, and lock digest pinning all unchanged.

Follow-up doc nits (non-blocking): the cosignSignatureArtifactType comment cites 'wantArtifactType in write_test.go', which does not exist in cosign v3.1.0 -- the real source is ociexperimental.ArtifactType("sig") in internal/pkg/oci/remote/remote.go + pkg/cosign/verify.go. The legacyCosignSignatureTag godoc reads 'sha256--.sig' but the actual format is '-.sig' (e.g. sha256-.sig).

Keyless-on-GHCR remains an operator step (not exercisable in-cycle) per the spec.

@handcaught
handcaught merged commit 9a04c3e into main Aug 3, 2026
12 checks passed
@handcaught
handcaught deleted the signatures-not-discoverable-by-cosign branch August 3, 2026 04:35
brokenbot pushed a commit that referenced this pull request Aug 3, 2026
Republish criteria-adapter-mcp so its signature carries the cosign
artifactType and legacy .sig tag added in #299. The 0.1.0 artifact was
published before that fix and is not discoverable by cosign verify.

Co-authored-by: ci <ci@localhost>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants