Managing Disconnected Signatures Using cosign

Andrew Block
6 min readFeb 3, 2022

--

Cosign is a tool within the sigstore project that greatly simplifies how content is signed and verified by storing signatures from container images and other types in OCI registries. By storing signatures with a predictable name as a related OCI artifact within a registry, the content can be leveraged by both consumers and producers of the signed material. However, there are situations where there may be not only a desire, but a requirement to forgo the storage of signatures in this default fashion. Fortunately, these scenarios for storing content produced with cosign can be adopted without compromising the integrity of the signing and verification process.

Signatures produced by cosign by default are stored within the same OCI registry as another tag with a predictive name. For example, a signed image located at docker.io/myuser/myimage:latest will have its signature stored at docker.io/myuser/myimage:sha256-<DIGEST>.sig. Let’s first verify this assertion by signing a container image. To simplify the commands used throughout, set two environment variables within the terminal containing the name of the image and tag.

export COSIGN_BLOG_IMAGE=docker.io/myuser/myimageexport COSIGN_BLOG_TAG=latest

To sign content using cosign, a public/private keypair must be generated. Execute the following command to generate a keypair in the current directory. Provide a password for the private key when prompted:

cosign generate-key-pair

The result will generate files named cosign.key and cosign.pub in the directory.

Now, sign the image:

cosign sign --key=cosign.key $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

With the signature published, we can verify the signature using the cosign verify command by providing the location of the public key used to sign the image as well as the image:

cosign verify --key=cosign.pub $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

Displayed is the verification results which will indicate that a valid signature was located on the image:

Verification for $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG --The following checks were performed on each of these signatures:- The cosign claims were validated- The signatures were verified against the specified public key- Any certificates were verified against the Fulcio roots.
[{"critical":{"identity":{"docker-reference":"$COSIGN_BLOG_IMAGE"},"image":{"docker-manifest-digest":"sha256:<DIGEST>"},"type":"cosign container image signature"},"optional":null}]

With an understanding of the basic steps to sign and verify an image, let’s investigate how cosign performs the verification task in further detail as it will be helpful in supporting alternate workflows.

During the signing process, content is pushed to a separate tag as described previously. Key details including, but not limited to the generated signature are stored as a layer within the image manifest. We can confirm this by viewing this manifest of the artifact.

First, we need to determine the location of the image containing the signature produced by the execution of cosign sign. This can be obtained by using the cosign triangulate command as shown below:

cosign triangulate $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

A result similar to the following will be displayed.

$COSIGN_BLOG_IMAGE:sha256-<DIGEST>.sig

There are several tools that can be used to interact with remote registries including crane and skopeo. In this instance, skoepo will be used, but feel free to use any tool of your choosing.

View the manifest of the image containing the signature:

skopeo inspect --raw docker://$(cosign triangulate $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG) | jq -r

The jq utility was used here to provide a more suitable output format which is shown below.

{  "schemaVersion": 2,  "mediaType": "application/vnd.oci.image.manifest.v1+json",  "config": {    "mediaType": "application/vnd.oci.image.config.v1+json",    "size": 233,    "digest": "sha256:5dd9225e5c9acc08ead9ff1e7d4073c444c50f8e11ca82a545e3bac8fa011677"  },  "layers": [    {      "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",      "size": 261,      "digest": "sha256:37627a11fd49489be88b327536908d5bd268677cfb88ee3470c202c96847d36a",      "annotations": {        "dev.cosignproject.cosign/signature": "MEUCICpLOoOnauDb2IOOpR45EnIY43+jX51Ha6yadUwZDh/sAiEA0hLMN9y2W+qe23QcsfIr+ch23Yup1yQ1STSibZh/u18="      }    }  ]}

As illustrated by the output above, cosign appends content to each layer of the image which allows for multiple signatures to be attached. Since this image was only signed once, only one layer is present. The signature is present within an annotation called dev.cosignproject.cosign/signature. This field can be extracted by using a combination of skopeo, cosign and jq as shown below.

skopeo inspect  --raw docker://$(cosign triangulate $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG) | jq -r '.layers[0].annotations["dev.cosignproject.cosign/signature"]'

When verifying content, cosign by, default, communicates with the remote registry to obtain the resources needed to perform the verification process. Since we have a method of retrieving the signature, we can instruct cosign to refrain from involving the remote signature store and to instead provide it from a local source. Let’s demonstrate how this can be accomplished.

First, save the signature to a file called image.sig using the preceding command redirecting the output to a file:

skopeo inspect  --raw docker://$(cosign triangulate $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG) | jq -r '.layers[0].annotations["dev.cosignproject.cosign/signature"]' > image.sig

Next, we can perform the same verification process against the image as performed previously except the location of the file containing the signature is provided as an argument instead of obtaining the signature from the remote image.

cosign verify --signature=image.sig --key=cosign.pub $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

Disconnected Signatures

The ability to accomplish the same steps to verify the integrity of an image using cosign without referring to a remote signature source affords additional options when determining how image signatures will be managed. There are several situations for which the default functionality of cosign of storing signing assets within an associated tag within the same repository cannot be facilitated. Examples include avoiding having container image content and signature content stored within the same repository or the fact that some container registries still do not support the use of OCI artifacts resulting in failures when attempting to store signatures.

Regardless of the cause, cosign provides several alternative approaches to accomplish the same goal by enabling signatures from being detached from the source repository.

The first option is to store signatures in a separate container registry from the image being signed to avoid having mixed content within the same repository. Storing images in a separate repository is accomplished through the use of the COSIGN_REPOSITORY environment variable. So for example, if the container image that was being signed was located at in the docker.io/myuser/myimage container and there as a desire to store signatures in the docker.io/myuser/myimage_signatures repository, the following command could be used which specifies the location of the signature repository using the COSIGN_REPOSITORY environment variable:

COSIGN_REPOSITORY=docker.io/myuser/myimage_signatures cosign sign --key=cosign.key docker.io/myuser/myimage:latest

You should be able to confirm that the signatures were pushed to the alternate repository instead of residing alongside the repository of the image being signed.

However, cosign also contains support for avoiding uploading signature content to a remote repository altogether. This is ideal for situations where there is either a desire or a need to restrict publishing of content related to image signatures. Instead, signatures and their associated certificates (if applicable) can be stored locally and can then be used when verifying. Let’s see this in action.

To avoid uploading signatures, use the --upload=false flag when signing an image using. Additional flags are available to store both the signature and the certificate produced locally as files and can be used by specifying the --output-signature and --output-certificate parameters when signing content.

Execute the following command to store the signature produced by signing the image to a file called disconnected.sig and the certificate to a file called disconnected.crt.

cosign sign --upload=false --output-signature=disconnected.sig --output-certificate=disconnected.crt --key=cosign.key $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

Confirm that both the signature and certificate files were stored locally. The certificate in this case is the public key associated with the key that was used to sign the image. Now, verify the image by referencing these files as shown below:

cosign verify --signature=disconnected.sig --key=disconnected.crt $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

Signing and verifying an image without remote storage in an OCI registry!

Disconnected Keyless Signatures

Instead of generating a public/private key pair and providing the private key when signing an image, a signing certificate can be requested from fulcio, a hosted service that provides short lived certificates. These ephemeral keys greatly simplifies and increases the security posture when signing content by avoiding having to manage long lived signing keys. Full support for fulcio integration is not yet available, so the COSIGN_EXPERIMENTAL environment variable must be set.

Execute the following command to request a singing key from fulcio, but continue to refrain from uploading the signature to a remote OCI registry.

COSIGN_EXPERIMENTAL=1 cosign sign --upload=false --output-signature=disconnected-fulcio.sig --output-certificate=disconnected-fulcio.crt $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

A browser window will be launched to allow you to authenticate against an identity provider of your choosing. Alternate flows are available via OIDC to remove the manual authentication step. Once this process is complete, a signing certificate will be retrieved, the container image signed, and the resulting signature and certificate stored as disconnected-fulcio.sig and disconnected-fulcio.crt respectively.

You can review the content of the generated certificate by executing the following command:

openssl x509 -in disconnected-fulcio.crt -noout -text

When reviewing the certificate, take note of the X509v3 Subject Alternative Name field which contains details related to the authenticated user.

Finally, lets’ go ahead and verify the image based on the fulcio based signature and certificate.

cosign verify --signature=disconnected-fulcio.sig --cert=disconnected-fulcio.crt $COSIGN_BLOG_IMAGE:$COSIGN_BLOG_TAG

Once again, we get the reassurance that our image has been signed, but this time thanks to the integration with fulcio, there is no longer a need to manage signing keys; all without having to upload content to a remote registry.

Cosign provides the capabilities to quickly and easily sign and verify content. By being able to store and verify signatures and their associated assets locally, new workflows can be unlocked to make use of proper security practices regardless of the operating environment.

--

--