Docker Image signing and attestation with Cosign

This article takes a detailed look at Cosign, one of several tools for creating and verifying Docker images and adding attestations such as build provenance.

Docker Image signing and attestation series

This article is part of a multi-part series:

Introduction

Cosign is one of several open-source tools developed in the Sigstore project. Sigstore is a graduated OpenSSF-backed project with the mission to provide free and effortless build provenance to as many artifact registries as possible. Thus, it is not limited to Docker/container images. For instance, Python packages can now be pushed to PyPi with Sigstore signatures (see announcement), and the macOS package manager Homebrew is also adopting it (announcement).

Under Sigstore, various open source projects (other than Cosign) have been created, e.g. Fulcio & Rekor (that simplify “keyless signing”), Policy Controller (to verify signed images in Kubernetes), gitsign (to use keyless Sigstore signatures to sign Git commits), or Sigstore SDKs for various languages (so that you can build tools that sign or verify arbitrary artifacts).

Certificate-based vs. keyless signing

Certificate-based signing approaches (used by tools like Notation or Docker Content Trust) have various problems that Sigstore aims to solve:

  • Identity: as a consumer, you want to verify that the real (expected) publisher signed an artifact. Doing this via (long-lived) public-private key pairs is just a proxy, where possession of the private key is (supposedly) proving your identity. This proxy metaphor breaks down once a malicious actor (e.g. a hacker) stole a copy of the private key, and can henceforth claim to have the identity of the real publisher.
  • Key management: keeping private keys secure (so they cannot be lost or stolen) is difficult. Similarly, it’s difficult to make the public key easily accessible for users, while also protecting it from being manipulated by a malicious actor.
  • Key revocation: If the key pair is compromised, how do you distribute new keys in a way that convinces users of your legitimacy and that you’re not an attacker?

To solve these problems, Sigstore introduces “keyless” signing, which no longer needs long-lived key pair/signing certificates.

This is a simplified summary of the keyless signing flow:

  • When you run “cosign sign <artifact>”, Cosign generates a new public-private key pair, keeping it only in memory (not storing it on disk).
  • Cosign initiates an OIDC flow to get a (cryptographically verifiable) OIDC token (that attests the identity of whoever runs cosign) from one of the many OIDC issuers that Fulcio already trusts, see here for the complete list. This flow could either be completed by a human who manually logs into their Microsoft/GitHub/ etc. account, so the OIDC token would include the human’s email address and the URL of the chosen OIDC token issuer. Or it could be a service account token using fully automated means (without human interaction), e.g. a GitHub Actions runner could get such a token from GitHub’s own OIDC issuer (https://token.actions.githubusercontent.com), where the identity of the OIDC token is the full URL to the GitHub Actions workflow that runs “cosign sign”.
  • Cosign sends a Certificate Signing Request (that contains the OIDC token and the in-memory public key) to a Certificate Authority, Fulcio. Fulcio validates the OIDC token and returns a temporary signing certificate that is only valid for 10 minutes. Fulcio adds that certificate to its public immutable Certificate Transparency log. By default, Cosign uses a free-to-use “public good” instance of Fulcio, but you could also self-host Fulcio on your own servers, if necessary.
  • Cosign signs the artifact using the in-memory private key, and uploads the artifact’s digest, signature, and signing certificate to another public log, the Rekor ledger (again, using a public good instance by default, or you could self-host it).

As a consumer, when you run “cosign verify <artifact> --certificate-identity <email-address-or-URI> --certificate-oidc-issuer https://…”, Cosign performs these steps:

  • Retrieve the signature from the image registry
  • Verify that the signature is cryptographically valid
  • Check with Rekor that the signature is known there. If it is, also check with Fulcio that the certificate is known to Fulcio.
  • Verify that the identity (that is part of the signature’s certificate) matches the provided CLI arguments
    (--certificate-oidc-issuer, --certificate-identity)

In summary, these are the pros and cons of keyless signing:

  • Cons of keyless signing: single point of failure of signing infrastructure (Fulcio and Rekor). If that infrastructure is down, you cannot verify signatures. Self-hosting options are available but suffer from the same problem.
  • Pros of keyless signing: simplified key management and verification – the consumer only needs to configure two (human-readable) strings: the issuer URL and the identity (email address or URL)

Deep dive

Let’s examine the commands and the manifests or ledger entries they produce.

Signing images

To sign an image (using the keyless approach), run a command such as
cosign sign [--recursive] registry.com/some/image@sha256:1234…
where the hash points to either an image manifest (i.e., an image for a specific OS+CPU architecture) or an image index (which was created for multi-platform images, in this case, you should use cosign’s --recursive flag to instruct cosign to recursively sign the platform-specific image manifests referenced by your provided image index).

As discussed above, if you run the command in an interactive terminal, cosign first asks you to log in with a supported SSO provider (e.g. Microsoft) in the browser, to retrieve your OIDC token. Or, if you run cosign in a CI/CD pipeline, it will auto-detect available issuers (such as GitHub or GitLab) and retrieve the OIDC token without user interaction.

Cosign then creates various manifests and pushes them, creating a new tag named “sha256-<1234…>.sig”, as described in the Cosign Signature Specification. Cosign does not modify the manifest(s) of the referenced image it signs.

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 233,
    "digest": "sha256:7284d69ccd6e9d8331ee4c613142a48cc5aa5aa441fab363dda0aa3c1596e984"
  },
  "layers": [
    {
      "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
      "size": 230,
      "digest": "sha256:487a29c7be72c8f4bdddb25c6ecf7ac8a911e2db8bff6f6a397608555730a10c",
      "annotations": {
        "dev.cosignproject.cosign/signature": "MEUCIQC0TXimL2/X969/HWy8SE7Oe5W4hXEfeAdwLdfFOhZ4GAIgLdVLW5bgslcFuF0JRv3HnDPtj3uq612B2LoCiLlJ4qI=",
        "dev.sigstore.cosign/bundle": "{\"SignedEntryTimestamp\":\"MEQCIFZif1EPuR+F4AvhHztcHf2f/BbRnU8cpMYw6jxl8VJ2AiAMHTauWZy3j22WnhBWDxQp3dMcR1RIlZG9xjPIZst8+A==\",\"Payload\":{\"body\":\"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0ODdhMjljN2JlNzJjOGY0YmRkZGIyNWM2ZWNmN2FjOGE5MTFlMmRiOGJmZjZmNmEzOTc2MDg1NTU3MzBhMTBjIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUMwVFhpbUwyL1g5NjkvSFd5OFNFN09lNVc0aFhFZmVBZHdMZGZGT2haNEdBSWdMZFZMVzViZ3NsY0Z1RjBKUnYzSG5EUHRqM3VxNjEyQjJMb0NpTGxKNHFJPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTjZSRU5EUVd4TFowRjNTVUpCWjBsVlFraHpXbVp1YjNKRlVuZEtMMDlMUkRjNGJUQmxORVJtU25BMGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFZkMDFVUlRGTlZFMTVUV3BWTlZkb1kwNU5hbFYzVFZSRk1VMVVUWHBOYWxVMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZVVnpWM1FrWnNiV0pzU2pJeGJERmlWVloyYlZCRE5XSnVhbU5GWmtOaVpXMVRkRXdLVEc1WmNsUlJUVVF5V1VwcE1FSmpNMmMyTXpad1NtMU1jRTlRTlROV1UzSklLM1EyWmxKRU9XMUtZbVZrZGtwbVJIRlBRMEZZUlhkblowWjBUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZYU1hCS0NraHRTbXRCWTBGMVVIVllZMnA1TTFGWlZucGxPR3RaZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoQldVUldVakJTUVZGSUwwSkNTWGRGU1VWUFlsaE9iMXBYZEhaa01FSnVZbGhuZFZwSFZYZE1RVmxMUzNkWlFrSkJSMFIyZWtGQ1FWRlJaUXBoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZaRmRKZFZreU9YUk1NbmgyV2pKc2RVd3lPV2hrV0ZKdlRVTTBSME5wYzBkQlVWRkNaemM0ZDBGUlowVkpRWGRsQ21GSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkRXd5ZUhaYU1teDFUREk1YUdSWVVtOU5TVWRLUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJYzBVS1pWRkNNMEZJVlVFelZEQjNZWE5pU0VWVVNtcEhValJqYlZkak0wRnhTa3RZY21wbFVFc3pMMmcwY0hsblF6aHdOMjgwUVVGQlIxVmhhVWQwVkhkQlFRcENRVTFCVW1wQ1JVRnBRalIzZVhWTlJscDJkVEJsV1ZGamFtVktOSGRKWlVkT2IySmFkMnhZYUdSalNITk5iMDlyWm5SUVMzZEpaMU41TlVNNWQyTmhDa016VFhwdGNFVnVOWFZEZVRkWFlYWkpMeXN6TUV0elZERk5jMVZyY0c5SFRUTkpkME5uV1VsTGIxcEplbW93UlVGM1RVUmhRVUYzV2xGSmVFRk9VamdLVUUweFdrTm5TMWg1T1ZRM1ZYSlZaR2hCVGt4Q1pUWldWRU5sZGs5dFEwMUlWVlo1WmtNMWQwWkdkRWd5WTA1NWVHMW9WV2hOUkhwcVdGUnFOMEZKZHdwYWRGVk5jRTVKVVdOM1JtdDZPSEJOTkZGVU9HbG1ieXQ1YnpkSVMwSlNkVnBQY1c5NmNtRkZXR1J3WjJkYVVXRnlUSEVyTlZkR09IZGpkRnA1Vkc5U0NpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19\",\"integratedTime\":1736947381,\"logIndex\":162584782,\"logID\":\"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d\"}}",
        "dev.sigstore.cosign/certificate": "-----BEGIN CERTIFICATE-----\nMIICzDCCAlKgAwIBAgIUBHsZfnorERwJ/OKD78m0e4DfJp4wCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjUwMTE1MTMyMjU5WhcNMjUwMTE1MTMzMjU5WjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAETW5wBFlmblJ21l1bUVvmPC5bnjcEfCbemStL\nLnYrTQMD2YJi0Bc3g636pJmLpOP53VSrH+t6fRD9mJbedvJfDqOCAXEwggFtMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUWIpJ\nHmJkAcAuPuXcjy3QYVze8kYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wHAYDVR0RAQH/BBIwEIEObXNoZWtvd0BnbXguZGUwLAYKKwYBBAGDvzABAQQe\naHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAwe\naHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsE\neQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGUaiGtTwAA\nBAMARjBEAiB4wyuMFZvu0eYQcjeJ4wIeGNobZwlXhdcHsMoOkftPKwIgSy5C9wca\nC3MzmpEn5uCy7WavI/+30KsT1MsUkpoGM3IwCgYIKoZIzj0EAwMDaAAwZQIxANR8\nPM1ZCgKXy9T7UrUdhANLBe6VTCevOmCMHUVyfC5wFFtH2cNyxmhUhMDzjXTj7AIw\nZtUMpNIQcwFkz8pM4QT8ifo+yo7HKBRuZOqozraEXdpggZQarLq+5WF8wctZyToR\n-----END CERTIFICATE-----\n",
        "dev.sigstore.cosign/chain": "-----BEGIN CERTIFICATE-----\nMIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7\n7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS\n0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB\nBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp\nKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI\nzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR\nnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP\nmygUY7Ii2zbdCdliiow=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7\nXeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex\nX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j\nYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY\nwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ\nKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\nWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\nTNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n-----END CERTIFICATE-----"
      }
    }
  ]
}Code language: JSON / JSON with Comments (json)

Relevant things to point out:

  • In this example, the layers array contains only one entry, but cosign supports signing an image multiple times, which would result in additional entries in layers.
  • The manifest of the config object (7284d69ccd…) does not contain anything relevant.
  • The manifest that the digest of the first layer entry points to (487a29c7be…) only contains a JSON that tells us the digest of the image itself (1234…).
  • In the annotations block, the first entry is the signature itself. The other entries encode the signing certificate (including the entire chain) and the Rekor log ID. Part of the certificate is the subject/identity, an email address in this case. This is how it looks like when we decode it with openssl:
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            04:7b:19:7e:7a:2b:11:1c:09:fc:e2:83:ef:c9:b4:7b:80:df:26:9e
        Signature Algorithm: ecdsa-with-SHA384
        Issuer: O = sigstore.dev, CN = sigstore-intermediate
        Validity
            Not Before: Jan 15 13:22:59 2025 GMT
            Not After : Jan 15 13:32:59 2025 GMT
        Subject: 
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:4d:6e:70:04:59:66:6e:52:76:d6:5d:5b:51:5b:
                    e6:3c:2e:5b:9e:37:04:7c:26:de:99:2b:4b:2e:76:
                    2b:4d:03:03:d9:82:62:d0:17:37:83:ad:fa:a4:99:
                    8b:a4:e3:f9:dd:54:ab:1f:eb:7a:7d:10:fd:98:96:
                    de:76:f2:5f:0e
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage: 
                Code Signing
            X509v3 Subject Key Identifier: 
                58:8A:49:1E:62:64:01:C0:2E:3E:E5:DC:8F:2D:D0:61:5C:DE:F2:46
            X509v3 Authority Key Identifier: 
                DF:D3:E9:CF:56:24:11:96:F9:A8:D8:E9:28:55:A2:C6:2E:18:64:3F
            X509v3 Subject Alternative Name: critical
                email:some@mail.de
            Fulcio Issuer: 
                https://github.com/login/oauth
            1.3.6.1.4.1.57264.1.8: 
                0c:1e:68:74:74:70:73:3a:2f:2f:67:69:74:68:75:62:2e:63:6f:6d:2f:6c:6f:67:69:6e:2f:6f:61:75:74:68
            CT Precertificate SCTs: 
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : DD:3D:30:6A:C6:C7:11:32:63:19:1E:1C:99:67:37:02:
                                A2:4A:5E:B8:DE:3C:AD:FF:87:8A:72:80:2F:29:EE:8E
                    Timestamp : Jan 15 13:22:59.535 2025 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:44:02:20:78:C3:2B:8C:15:9B:EE:D1:E6:10:72:37:
                                89:E3:02:1E:18:DA:1B:67:09:57:85:D7:07:B0:CA:0E:
                                91:FB:4F:2B:02:20:4B:2E:42:F7:07:1A:0B:73:33:9A:
                                91:27:E6:E0:B2:ED:66:AF:23:FF:B7:D0:AB:13:D4:CB:
                                14:92:9A:06:33:72
    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:65:02:31:00:d4:7c:3c:cd:59:0a:02:97:cb:d4:fb:52:b5:
        1d:84:03:4b:05:ee:95:4c:27:af:3a:60:8c:1d:45:72:7c:2e:
        70:14:5b:47:d9:c3:72:c6:68:54:84:c0:f3:8d:74:e3:ec:02:
        30:66:d5:0c:a4:d2:10:73:01:64:cf:ca:4c:e1:04:fc:89:fa:
        3e:ca:8e:c7:28:14:6e:64:ea:a8:ce:b6:84:5d:da:60:81:94:
        1a:ac:ba:be:e5:61:7c:c1:cb:59:c9:3a:11Code language: JavaScript (javascript)

The annotations block also contains the Rekor Log ID, which you can look up in the browser (see here for the above example). That entry contains the details of the OIDC token used when requesting the temporary signing certificate (in the above example, only my email address and that GitHub was the OIDC issuer). If you call cosign in an automated CI/CD flow (e.g. GitHub or GitLab), the OIDC token will contain much more useful information, including the workflow URL or Git commit (see here for an example). This means that in this case, cosign not only signed your image but also provided build provenance to some degree.

Verifying images

To (manually) verify a signed image that was signed using keyless signing, use a command such as “cosign verify <registry.com/image:tag-or-hash> --certificate-identity some@mail.com --certificate-oidc-issuer https://foobar.com”.

The verification only makes sense if you, the verifier, know who was supposed to sign the image. If you used incorrect values for --certificate-identity or --certificate-oidc-issuer, cosign will print an error message, which includes the identity (subject) and issuer of available signatures. If these values look correct to you, repeat the cosign verify command with the issuer and identity values from the error message.

Key-based signing & verification

Cosign also supports signing and verifying images using the “traditional” key-based approach, using a self-generated, long-living public-private key pair. This avoids the need for Rekor and Fulcio and can be appropriate for network-isolated environments, e.g. in an enterprise/corporate setting. This “offline signing” works as follows:

  • cosign generate-key-pair” creates the files cosign.key and cosign.pub locally. Alternatively, you can also generate the key pair in a cloud-based KMS, e.g. Azure KeyVault, see the documentation for details
  • cosign sign --key cosign.key <image-reference> --tlog-upload=false” signs the image, without making entries in Rekors transparency log
  • cosign verify --key cosign.pub <image-reference> --insecure-ignore-tlog=true” verifies the image

Creating attestations

Using the command “cosign attest --type <predicate-type> --predicate <path-to-attestation-file> <image-ref-with-tag-or-hash>” you tell cosign to package the content of the attestation file (e.g. a CycloneDX-formatted SBOM, see --predicate argument) in some wrapper manifests. Cosign also signs them cryptographically, and uploads them to the image registry, creating a new tag named “sha256-<image-hash>.att” for your image (notice the “.att” ending, in contrast to the “.sig” ending that Cosign creates when signing an image, as explained above). Cosign leaves the manifests of the referenced image itself as-is. This is the same behavior cosign has when signing images.

The file format that Cosign uses for the (wrapper) manifests is in-toto attestations. The “in-toto lingo” uses the term “Predicate” for the claim you want to attest (e.g. the SBOM). In-toto attestations can be generic, but the in-toto attestation framework also predefines various Predicate types, see here for a list. Cosign maps the parameter --type to these predefined Predicate types (--type can have the values slsaprovenance, slsaprovenance02, slsaprovenance1, link, spdx, spdxjson, cyclonedx, vuln, or openvex). You can also set --type to an arbitrary string, typically a URI such as “https://example.com/CodeReview/v1“.

Let’s take a look at the data created by the command “cosign attest --predicate some.json --type spdxjson ttl.sh/aalptest@sha256:c10f729849a3b03cbf222e2220245dd44c39a06d444aa32cc30a35c4c1aba59d”:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 233,
    "digest": "sha256:ccaa13e2b4ff4f350f2198847a769bf9391e8e9626bcbff3eb978fbcdd499814"
  },
  "layers": [
    {
      "mediaType": "application/vnd.dsse.envelope.v1+json",
      "size": 125112,
      "digest": "sha256:0635641a52582662da2015a6bc1133fa3cadde1a4eacef8b995573e3243fe69c",
      "annotations": {
        "predicateType": "https://spdx.dev/Document",
        "dev.cosignproject.cosign/signature": "",
        "dev.sigstore.cosign/bundle": "{\"SignedEntryTimestamp\":\"MEUCIExjBOiMpnfwrUwKdrXauKo0tFq4QtdFl/eD6MP3C5E/AiEA2jdEnVCdF8snkHKcFp1+cTXi2jU0YQ5wuqIqn1e4akM=\",\"Payload\":{\"body\":\"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiMDYzNTY0MWE1MjU4MjY2MmRhMjAxNWE2YmMxMTMzZmEzY2FkZGUxYTRlYWNlZjhiOTk1NTczZTMyNDNmZTY5YyJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjZhZjAzZWY3NGVhNTc2ZjFkNjNhMGQ3MzI1NTAwZjc4NzA3YzVhZTU4OGYxNTQzYjYzOWQ0YzdmMmUzZjlmNWYifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVZQ0lRRGk0NDBHOU9ScTdKdm5HOVZQczd0dG41cUgrV0s2ZVdDcHNUU1o3VFdOWmdJaEFKZjdmSm1SalE0TEJUMDdOSWxvTEoxQlZIM0owVzlXS3hqL0RjbXRuRitDIiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VONlJFTkRRV3hMWjBGM1NVSkJaMGxWVnl0bFMyTjVVREkyU0dablVGcGtMMG8wYWpCVksweDJjRmhKZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmQwMVVSVFJOVkVsNVQxUkpkMWRvWTA1TmFsVjNUVlJGTkUxVVNYcFBWRWwzVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVXdWMnhNUTBkU01HVjVWRlpoZUcxcVNGRXJNMWRsVEdFMFNuWTNaemhLVVRsalFXVUtiMlJrVFROb1JWWktkRnBUV1NzeVJGWXJiWFpsYmxONmVqVlhlbkJsZGtOMWFsZ3JjMnR0ZHpaUlJHZDNNVlU0V1hGUFEwRllSWGRuWjBaMFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVXZibGxYQ2pSMlFWRkpSRlZxTmxneVYwaFJOWEJrYTAxbFVrUmpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMGhCV1VSV1VqQlNRVkZJTDBKQ1NYZEZTVVZQWWxoT2IxcFhkSFprTUVKdVlsaG5kVnBIVlhkTVFWbExTM2RaUWtKQlIwUjJla0ZDUVZGUlpRcGhTRkl3WTBoTk5reDVPVzVoV0ZKdlpGZEpkVmt5T1hSTU1uaDJXakpzZFV3eU9XaGtXRkp2VFVNMFIwTnBjMGRCVVZGQ1p6YzRkMEZSWjBWSlFYZGxDbUZJVWpCalNFMDJUSGs1Ym1GWVVtOWtWMGwxV1RJNWRFd3llSFphTW14MVRESTVhR1JZVW05TlNVZEtRbWR2Y2tKblJVVkJaRm8xUVdkUlEwSkljMFVLWlZGQ00wRklWVUV6VkRCM1lYTmlTRVZVU21wSFVqUmpiVmRqTTBGeFNrdFljbXBsVUVzekwyZzBjSGxuUXpod04yODBRVUZCUjFWbFYwOW9kV2RCUVFwQ1FVMUJVbXBDUlVGcFFXVkVNRkExYjBSTVUzTktWRmhPYVZCa1VYUnZiR0YzTWtReEsydHZSRmxaVWxOS2NGZFFOWFpGWWtGSloxTnVZbFpQU0hGWUNsZzBhbXBRTm1nd1RXYzVRV01yTUhORk0yVm9NVUZWWkZCTldVVkJXVlJRY3l0UmQwTm5XVWxMYjFwSmVtb3dSVUYzVFVSaFFVRjNXbEZKZDJVNWNEUUtOV0pFSzNoeFRFMXBOR3BhYjFOdUwydFBjbE54YVM5T1NVczRaa0ZUVWxkSk0ya3ZObUpNZUd0aU56Um9lSFpqT1hCRlozUnlhMUE0ZHprMVFXcEZRUXBvTlhRNFpFSnljR1V5ZEZwVlVUbEpiVzlhYW5SVVYyUjBXRzVoV21aUldITnFkbmxEVUhGTFVUTnROazQwYkhoelJsUm9kWFZYY0dkVGVrUjBVRGx4Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0=\",\"integratedTime\":1737203361,\"logIndex\":163396239,\"logID\":\"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d\"}}",
        "dev.sigstore.cosign/certificate": "-----BEGIN CERTIFICATE-----\nMIICzDCCAlKgAwIBAgIUW+eKcyP26HfgPZd/J4j0U+LvpXIwCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjUwMTE4MTIyOTIwWhcNMjUwMTE4MTIzOTIwWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAE0WlLCGR0eyTVaxmjHQ+3WeLa4Jv7g8JQ9cAe\noddM3hEVJtZSY+2DV+mvenSzz5WzpevCujX+skmw6QDgw1U8YqOCAXEwggFtMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU/nYW\n4vAQIDUj6X2WHQ5pdkMeRDcwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wHAYDVR0RAQH/BBIwEIEObXNoZWtvd0BnbXguZGUwLAYKKwYBBAGDvzABAQQe\naHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAwe\naHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsE\neQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGUeWOhugAA\nBAMARjBEAiAeD0P5oDLSsJTXNiPdQtolaw2D1+koDYYRSJpWP5vEbAIgSnbVOHqX\nX4jjP6h0Mg9Ac+0sE3eh1AUdPMYEAYTPs+QwCgYIKoZIzj0EAwMDaAAwZQIwe9p4\n5bD+xqLMi4jZoSn/kOrSqi/NIK8fASRWI3i/6bLxkb74hxvc9pEgtrkP8w95AjEA\nh5t8dBrpe2tZUQ9ImoZjtTWdtXnaZfQXsjvyCPqKQ3m6N4lxsFThuuWpgSzDtP9q\n-----END CERTIFICATE-----\n",
        "dev.sigstore.cosign/chain": "-----BEGIN CERTIFICATE-----\nMIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C\nAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7\n7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS\n0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB\nBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp\nKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI\nzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR\nnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP\nmygUY7Ii2zbdCdliiow=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw\nKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y\nMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7\nXeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex\nX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j\nYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY\nwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ\nKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM\nWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9\nTNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ\n-----END CERTIFICATE-----"
      }
    }
  ]
}
Code language: JSON / JSON with Comments (json)

As you can see, the structure of the “.att” manifest is the same as the one of a signed image (the “.sig” manifest).

{
  "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvd...<lots-more-bytes>",
  "payloadType": "application/vnd.in-toto+json",
  "signatures": [
    {
      "keyid": "",
      "sig": "MEYCIQDi440G9ORq7JvnG9VPs7ttn5qH+WK6eWCpsTSZ7TWNZgIhAJf7fJmRjQ4LBT07NIloLJ1BVH3J0W9WKxj/DcmtnF+C"
    }
  ]
}
Code language: JSON / JSON with Comments (json)

The decoded payload looks as follows:

{
  "subject": [
    {
      "name": "ttl.sh/aalptest",
      "digest": {
        "sha256": "c10f729849a3b03cbf222e2220245dd44c39a06d444aa32cc30a35c4c1aba59d"
      }
    }
  ],
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://spdx.dev/Document",
  "predicate": { ...content of the SPDX SBOM JSON ... }
}Code language: JSON / JSON with Comments (json)

Generic attestations

You can also omit the “--type <attestation-type>” argument. Then, Cosign sets the predicateType to “https://cosign.sigstore.dev/attestation/v1”. I would only do so if you want to attest something that is not (yet) part of the predefined in-toto predicate types. The file can even be a binary file. In contrast, if you specify a Predicate --type Cosign will parse the predicate file and validate that it is valid JSON.

Verifying attestations

To verify the authenticity of the signature of an attestation, run the command “cosign verify-attestation <image-ref-with-digest> --certificate-identity some@mail.com --certificate-oidc-issuer https://…

It’s also possible to validate the content (and Predicate type) using the CUE or Rego language. See here for examples. Alternatively, you can use the command
cosign download attestation <image-ref-with-digest> [--predicate-type] > output.json
to download the attestation (which has the same data format as the manifest of 0635641a52…).

Automated verification

Verification of image signatures (and attestations) created by Cosign/Sigstore is supported by the following open-source projects, which are all Kubernetes operators. Their default behavior is an “enforce mode” which ensures that the images are not deployed to your Kubernetes cluster in case their verification fails. Alternatively, these operators have an optional “audit” mode which only warns you about failed validations, but will still deploy the (invalid) images.

  • Kyverno (docs): supports verifying Cosign-created image signatures and attestations
  • FluxCD can verify Cosign-created image signatures (docs)
  • OPA Gatekeeper with Ratify (see here): supports verifying Cosign image signatures
    • Note: while Ratify supports validating SBOMs and vulnerability reports, Ratify requires that you attach these attestations via “oras attach” (which creates a tag of the form “sha256-<digest-hash>”). Since Cosign uses a different form of attaching the attestation (where the tag is named “sha256-<digest-hash>.att”), Ratify does not recognize the attestations and thus cannot verify them.

Each solution uses a slightly different syntax or approach to configure the “trust policy”, which defines which signing certificates/identities you trust.

Another solution is configuring Harbor (see docs) to refuse image pulls of unsigned images. Unfortunately, you cannot configure a trust policy in Harbor, meaning you cannot configure the specific signing identities you deem trustworthy. This makes the feature rather useless.

Finally, you can run “cosign verify” in CI/CD workflows. However, there is the “hen and egg” problem that you need to obtain a genuine cosign binary to begin with before you can use it. This could be as easy as hard-coding a curl download and hardcoding the binary’s corresponding checksum and verifying it. However, this would require regular maintenance. You can instead use a more elaborate “bootstrap approach”, as done by Cosign’s installer GitHub action, see here for details.

Conclusion

In the area of signing container images or adding attestations, Cosign is a very solid and easy-to-use solution. It has widespread adoption by other projects (that verify Cosign signatures), e.g. by Kubernetes Operators like Kyverno.

I should mention that you can also use Cosign to sign ordinary files (“blobs”), see the documentation.

Impending change of signature format

As this write-up explains, “cosign sign”, so far, uses a Cosign Signature Specification that describes how image signatures are represented and implemented (with media-type
application/vnd.dev.cosign.simplesigning.v1+json”, as seen above).

Since then, Sigstore created a more elaborate Cosign Bundle Specification for attestations.

As of 03/2025, it seems that:

  • cosign sign” will sooner or later use that attestation bundle spec to encode image signatures. This means that you will need to resign your images once that happens
  • if you sign blobs (using “cosign sign-blob” that signs local files), you can use the new bundle format already today, using the CLI argument “--new-bundle-format=true

Leave a Comment