Skip to content

Migrate to Cosign v3 #1282

@apyrgio

Description

@apyrgio

Cosign v3 has recently been released, and with that, the new Sigstore bundle format is the default for signing and verification operations.

In order to migrate to Cosign v3, we must first list the operations we were doing in Cosign v2, and spot any differences. In this particular case, there are lots of differences, so the migration will be very tricky.

Tip

To enforce Cosign v3 to fallback to the previous mode, we can use the following CLI options:

              "--new-bundle-format=false",
              "--use-signing-config=false",

Signing an image (cosign sign)

In Cosign v2, a signature for an image with digest sha256:06a6b4f6b07e1c0867cdb2a7b2a72c49490aa083c8514a6ca7f59559b87254c8 would be uploaded as a separate manifest in the container registry, with the following tag: sha256-06a6b4f6b07e1c0867cdb2a7b2a72c49490aa083c8514a6ca7f59559b87254c8.sig. See:

In Cosign v3, things are a little different. Check this example out:

First, Cosign uploads a manifest with a tag like this: sha256-06a6b4f6b07e1c0867cdb2a7b2a72c49490aa083c8514a6ca7f59559b87254c8. Notice that there's no .sig suffix. This manifest contains the following:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 893,
      "digest": "sha256:703b83e7d2b932af90c9efe3e122628ac01bc56a3bf826c097de1c50df31e7d1",
      "artifactType": "application/vnd.oci.empty.v1+json"
    },
  ]
}

Then, Cosign uploads a blob (here sha256:703b83e7d2b932af90c9efe3e122628ac01bc56a3bf826c097de1c50df31e7d1) which contains the following:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.empty.v1+json",
    "size": 2,
    "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
    "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json"
  },
  "layers": [
    {
      "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
      "size": 3953,
      "digest": "sha256:d1e9df2a25c331b4ea98e6c811c5750da5f0255a567d0810950e7a4b50fd22ba"
    }
  ],
  "annotations": {
    "dev.sigstore.bundle.content": "dsse-envelope",
    "dev.sigstore.bundle.predicateType": "https://sigstore.dev/cosign/sign/v1",
    "org.opencontainers.image.created": "2025-10-14T07:51:16Z"
  },
  "subject": {
    "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
    "size": 683,
    "digest": "sha256:06a6b4f6b07e1c0867cdb2a7b2a72c49490aa083c8514a6ca7f59559b87254c8"
  },
  "artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json"
}

The subject field of the blob refers to the image digest (here 06a6b4f6b07e1c0867cdb2a7b2a72c49490aa083c8514a6ca7f59559b87254c8). The type of the blob is application/vnd.dev.sigstore.bundle.v0.3+json . Finally, there's a layer with a digest (here sha256:d1e9df2a25c331b4ea98e6c811c5750da5f0255a567d0810950e7a4b50fd22ba), which points to the actual Sigstore bundle:

 {
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "verificationMaterial": {
    "publicKey": {
      "hint": "hl3AZsTnpVTmBmQaqUqv8CMvygd7kPXYd15z7qOkbYQ="
    },
    "tlogEntries": [
      {
        "logIndex": "604917295",
        "logId": {
          "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
        },
        "kindVersion": {
          "kind": "dsse",
          "version": "0.0.1"
        },
        "integratedTime": "1760428276",
        "inclusionPromise": {
          "signedEntryTimestamp": "MEQCIBaCFsiWOQU37AZdk/J919Ej5yjkLCg0JewLD6NQc10UAiAoBDa0ZIwEYsGhUffUcR/3FMjA+5prk7g2hKDf3ykN2Q=="
        },
        "inclusionProof": {
          "logIndex": "483013033",
          "rootHash": "FyZgG1H/eqxzhF+LaZa7+l4w3V4X3+UJF7MI8+CZcMs=",
          "treeSize": "483013034",
          "hashes": [
            "SC3X65ZKJc0W4tlGF9Rgj+nI1ir3HKuqOSryc1zFs+I=",
            "+Frr740Vk2+33m7ICa2NCZ0CLAH5QhHxMLJpa5p+OT0=",
            "LEVzGukBzfCrTfrmf/siuktmktr2/Fdnh/eNHVAh58I=",
            "H3u6/URvJmZ8hXjZ2scpT207TBMBYmZ5RBPzmly0XiM=",
            "r/MqT3yTeqxh/yi4y9Uv+aoMNdUxxLj5J00GSse3TKQ=",
            "hHg4W7SjKnWOHKyxAZMBkS+GcllPjEil0ck7oSF5QG0=",
            "B6K+YdsKuYYXoJvMYqUEdkq2fyRxGzJom6rm1kjUtuc=",
            "GTUlkpJbTNTY8xtqE/MhghdkBKK6ZPr9U7/JaeYjDcY=",
            "16A7PhETK7AyfinzrxSKFuQepP6RPUsEtNEZk0cpap8=",
            "8nCds1SUzxGg2Xoa+M1tOFTwx+1BGtp8TDMSS4P54xQ=",
            "V5lOdefY1WOOt4iQp7tZoyj1beBDVi24KsEMcgsqZds=",
            "2Wv4GiithwNukRKV06clevnQQYCzXmSS/+/OJtXgsXQ=",
            "1mfy94KpcItqshH9+gwqV6jccupcaMpVsF28New8zDY=",
            "vS7O4ozHIQZJWBiov+mkpI27GE8zAmVCEkRcP3NDyNE="
          ],
          "checkpoint": {
            "envelope": "rekor.sigstore.dev - 1193050959916656506\n483013034\nFyZgG1H/eqxzhF+LaZa7+l4w3V4X3+UJF7MI8+CZcMs=\n\n— rekor.sigstore.dev wNI9ajBFAiEA5nCskAzLwrTHiJyS1lkxYLSEiQcvr/kDWb2CY52Vm3oCIBeM50HlLhcLHJb31A+df4qg6YpvgWIgzUPkXck7CnU+\n"
          }
        },
        "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiOTZiODE0NGQ4YTU3YTIwOGI5YzA4OGJkM2Q2ZWE3ZmJkODU3MTI2ZDJmZTMyM2M2ZDU5ZTdlYTRmZDY2MzA4ZCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImRmZjM2ZDljY2RmZDI0ZmMzZmQ1ZjYzYTFjNjdkOTNkMTQ0M2RkZWE2ZTZhYjM2NGQwZDRiYzllZTdhZTU0M2YifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lCdDJkajlERWJCa0Y5UFU4eDNaTGdEZ09HVCtWaXlEVDJCeEtFTlR6SEdSQWlFQTRhUHZMUUlUb0wrR2syaUNtSUxwN09nWGFLQitQakZrZVlKTzl4UEphaXc9IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJRVlVKTVNVTWdTMFZaTFMwdExTMEtUVVpyZDBWM1dVaExiMXBKZW1vd1EwRlJXVWxMYjFwSmVtb3dSRUZSWTBSUlowRkZUR1ZQZDI1c2IyRlJja053VjNoSVJXODVNM1JWVTFOek5EVk9NQXBYU0hCMFFWQXdkemM0UkZZd2IwZFhTWFl3VEV0d1QyTnBWVFZTY2twUE1FWmxTMWhNZW1WUk5VODNaM0p2VDJoSlVXWkdabGxDZG1SQlBUMEtMUzB0TFMxRlRrUWdVRlZDVEVsRElFdEZXUzB0TFMwdENnPT0ifV19fQ=="
      }
    ],
    "timestampVerificationData": {
      "rfc3161Timestamps": [
        {
          "signedTimestamp": "MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgmWgKapx6T1va8kuuG0F8I4ZTPmcasViP8DGUZZ/3DA8CFQDr42r7HDdX4WoymWxgYdgWAYHjWRgPMjAyNTEwMTQwNzUxMTZaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI1MTAxNDA3NTExNlowLwYJKoZIhvcNAQkEMSIEIOHTFK0PxcS08Av7SGGxjulCP4Yb3BriRs2A2lRNmMn9MIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRnMGUCMFzCz2jBwQWDfenvy7GtUDV9GlTUsZN1z8pcxgz2ZdGaCgNrx6Nx+Mr8Vv7U5fi8mQIxAJcjniUrZ79LDmdsnD9SnWuNPe5KiBJ7GlsBrzJX07KvAh0rzfVObv89CPiqfHAJ5Q=="
        }
      ]
    }
  },
  "dsseEnvelope": {
    "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCAic3ViamVjdCI6W3siZGlnZXN0Ijp7InNoYTI1NiI6IjA2YTZiNGY2YjA3ZTFjMDg2N2NkYjJhN2IyYTcyYzQ5NDkwYWEwODNjODUxNGE2Y2E3ZjU5NTU5Yjg3MjU0YzgifX1dLCAicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2lnc3RvcmUuZGV2L2Nvc2lnbi9zaWduL3YxIn0=",
    "payloadType": "application/vnd.in-toto+json",
    "signatures": [
      {
        "sig": "MEUCIBt2dj9DEbBkF9PU8x3ZLgDgOGT+ViyDT2BxKENTzHGRAiEA4aPvLQIToL+Gk2iCmILp7OgXaKB+PjFkeYJO9xPJaiw="
      }
    ]
  }
}

The above changes in the way signatures are stored will require some moderate changes in GHCR signer as well.

Aside from the above, it seems that Cosign v3 does not respect the COSIGN_REPOSITORY envvar anymore (see sigstore/cosign#4464). In order to circumvent this issue in GHCR signer, we would need to copy the container image to our local registry using crane copy, and then perform the signing operation.

Verifying a signature for a digest (cosign verify-blob)

In Cosign v2, we had to do some shenanigans in order to convert the stored signature in a bundle format that cosign verify-blob could use:

def to_bundle(self) -> Dict:
"""Convert a cosign-download signature to the format expected by cosign bundle."""
bundle = self.bundle
payload = self.bundle_payload
sig = self.signature
return {
"base64Signature": sig["Base64Signature"],
"Payload": sig["Payload"],
"cert": sig["Cert"],
"chain": sig["Chain"],
"rekorBundle": {
"SignedEntryTimestamp": bundle["SignedEntryTimestamp"],
"Payload": {
"body": payload["body"],
"integratedTime": payload["integratedTime"],
"logIndex": payload["logIndex"],
"logID": payload["logID"],
},
},
"RFC3161Timestamp": sig["RFC3161Timestamp"],
}

def verify_blob(pubkey: Path, bundle: str, payload: str) -> None:
cmd = [
_COSIGN_BINARY,
"verify-blob",
"--offline",
"--key",
str(pubkey.absolute()),
"--bundle",
bundle,
payload,
]

This operation is now much easier with Cosign v3 since the following works:

cosign -d verify-blob --key <key> --bundle <bundle> sha256:<digest>

The key changes are the following:

  1. We can use the Sigstore bundle as is.
  2. We don't need to specify a payload, only the image digest

Also, we can perform a fully offline verification with:

cosign verify-blob \
  --key share/freedomofpress-dangerzone.pub \
  --bundle image.bundle \
  --trusted-root ~/.sigstore/root/tuf-repo-cdn.sigstore.dev/targets/trusted_root.json \
  sha256:06a6b4f6b07e1c0867cdb2a7b2a72c49490aa083c8514a6ca7f59559b87254c8 

Verifying a local image (cosign verify --local-image)

In Cosign v2, we could verify a local image with:

def verify_local_image(oci_image_folder: Path, pubkey: Path) -> None:
"""Verify the given path against the given public key"""
cmd = [
_COSIGN_BINARY,
"verify",
"--key",
str(pubkey),
"--offline",
"--local-image",
str(oci_image_folder),
]

The equivalent currently fails in Cosign v3 with a segfault (sigstore/cosign#4468)

Downloading image signatures (cosign download signature)

In Cosign v2, we could download image signatures with:

def download_signature(image: str, digest: str) -> list[str]:
env = os.environ.copy()
disable_registry_auth(env)
try:
process = subprocess_run(
[
_COSIGN_BINARY,
"download",
"signature",
f"{image}@sha256:{digest}",
],
env=env,
capture_output=True,
check=True,
)

In Cosign v3 there's no equivalent yet (sigstore/cosign#4470)

Save an image with its signatures (cosign save)

In Cosign v2, we could save a container image, along with its signatures, with:

def save(arch_image: str, destination: Path) -> None:
process = subprocess_run(
[_COSIGN_BINARY, "save", arch_image, "--dir", str(destination.absolute())],
capture_output=True,
)

In Cosign v3 there's no equivalent yet (sigstore/cosign#4470)

Verify attestation (cosign verify-attestation)

(Not tested yet)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions