Skip to content

Key based signing using cosign and Rekor

Flavio Castelli edited this page Oct 20, 2021 · 1 revision

This document explains how container images (or other artifacts) can be signed using cosign. Later the signature will be published into the Rekor transparency log.

The signature will not be done using the experimental cosign key-less procedure. Hence, Fulcio's web PKI is not going to be used.

Generating the key pair and signing

Generate a pair of keys using cosign:

cosign generate-key-pair

Sign the container image and enable rekor integration:

COSIGN_EXPERIMENTAL=1 cosign sign -key cosign.key registry-testing.svc.lan/busybox:latest

Now, inside of the remote registry we have two tags for that repository:

crane ls registry-testing.svc.lan/busybox
latest
sha256-f3cfc9d0dbf931d3db4685ec659b7ac68e2a578219da4aae65427886e649b06b.sig

So far, this looks exactly when signing without the Rekor integration.

The signature object

Let's look at the manifest of the signature object:

crane manifest registry-testing.svc.lan/busybox:sha256-f3cfc9d0dbf931d3db4685ec659b7ac68e2a578219da4aae65427886e649b06b.sig | jq

This produces the following output:

{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 233,
    "digest": "sha256:6be03ee91610ddcd1b1c5b664bb2ef319a85b4ca9ebee1d64d87729c5fd83a91"
  },
  "layers": [
    {
      "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
      "size": 248,
      "digest": "sha256:3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9",
      "annotations": {
        "dev.cosignproject.cosign/signature": "MEYCIQDWWxPQa3XFUsPbyTY+n+bZu/6Pwhg5WwyYDQtEfQho9wIhAPkKW7eub8b7BX+YbbRac8TwwIrK5KxvdtQ6NuoD+ivW",
        "dev.sigstore.cosign/bundle": "{\"SignedEntryTimestamp\":\"MEUCIDx9M+yRpD0O47/Mzm8NAPCbtqy4uiTkLWWexW0bo4jZAiEA1wwueIW8XzJWNkut5y9snYj7UOfbMmUXp7fH3CzJmWg=\",\"Payload\":{\"body\":\"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzYWY0NDE0ZDIwYzllMWNiNzZjY2M3MmFhZThiMjQyMTY2ZGFiZTZhZjUzMWE0YTc5MGRiOGUyZjBlNWVlN2M5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURXV3hQUWEzWEZVc1BieVRZK24rYlp1LzZQd2hnNVd3eVlEUXRFZlFobzl3SWhBUGtLVzdldWI4YjdCWCtZYmJSYWM4VHd3SXJLNUt4dmR0UTZOdW9EK2l2VyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRFdG9SRGRHTlU5TGVUYzNXalU0TWxrMmFEQjFNVW96UjA1Qkt3cHJkbFZ6YURSbFMzQmtNV3gzYTBSQmVtWkdSSE0zZVZoRlJYaHpSV3RRVUhWcFVVcENaV3hFVkRZNGJqZFFSRWxYUWk5UlJWazNiWEpCUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19\",\"integratedTime\":1634714179,\"logIndex\":783606,\"logID\":\"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d\"}}"
      }
    }
  ]
}

The relevant part are stored inside of the layers section. Each signature will produce a new layer.

Note well: from now on, we will refer to the registry artifact storing the signature by using an environment variable named: COSIGN_IMAGE:

export COSIGN_IMAGE="registry-testing.svc.lan/busybox:sha256-f3cfc9d0dbf931d3db4685ec659b7ac68e2a578219da4aae65427886e649b06b.sig"

Let's look closer at the only layer that belongs to the manifest, this can be done with the following command:

crane manifest $COSIGN_IMAGE | \
  jq '.layers[0]'

Which will produce the following output:

{
  "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
  "size": 248,
  "digest": "sha256:3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9",
  "annotations": {
    "dev.cosignproject.cosign/signature": "MEYCIQDWWxPQa3XFUsPbyTY+n+bZu/6Pwhg5WwyYDQtEfQho9wIhAPkKW7eub8b7BX+YbbRac8TwwIrK5KxvdtQ6NuoD+ivW",
    "dev.sigstore.cosign/bundle": "{\"SignedEntryTimestamp\":\"MEUCIDx9M+yRpD0O47/Mzm8NAPCbtqy4uiTkLWWexW0bo4jZAiEA1wwueIW8XzJWNkut5y9snYj7UOfbMmUXp7fH3CzJmWg=\",\"Payload\":{\"body\":\"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzYWY0NDE0ZDIwYzllMWNiNzZjY2M3MmFhZThiMjQyMTY2ZGFiZTZhZjUzMWE0YTc5MGRiOGUyZjBlNWVlN2M5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURXV3hQUWEzWEZVc1BieVRZK24rYlp1LzZQd2hnNVd3eVlEUXRFZlFobzl3SWhBUGtLVzdldWI4YjdCWCtZYmJSYWM4VHd3SXJLNUt4dmR0UTZOdW9EK2l2VyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRFdG9SRGRHTlU5TGVUYzNXalU0TWxrMmFEQjFNVW96UjA1Qkt3cHJkbFZ6YURSbFMzQmtNV3gzYTBSQmVtWkdSSE0zZVZoRlJYaHpSV3RRVUhWcFVVcENaV3hFVkRZNGJqZFFSRWxYUWk5UlJWazNiWEpCUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19\",\"integratedTime\":1634714179,\"logIndex\":783606,\"logID\":\"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d\"}}"
  }
}

The annotations.dev.cosignproject.cosign/signature key contains the signature produced by signing the Simple Signing document with the private key.

The Simple Signing document

The actual Simple Signing document is contained inside of the layer, we can fetch it by using the layer digest:

crane blob registry-testing.svc.lan/busybox@sha256:3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9 | jq

This will produce the following output:

{
  "critical": {
    "identity": {
      "docker-reference": "registry-testing.svc.lan/busybox"
    },
    "image": {
      "docker-manifest-digest": "sha256:f3cfc9d0dbf931d3db4685ec659b7ac68e2a578219da4aae65427886e649b06b"
    },
    "type": "cosign container image signature"
  },
  "optional": null
}

Looking at this document, we don't care about the critical.identity.docker-reference, this value be different from the actual location of the container image we're trying to verify. For example, this would change when the container image is copied to another registry. The only thing we care about is the critical.image.docker-manifest-digest.

This value must be equal to the manifest digest of the container image that has been signed:

crane digest registry-testing.svc.lan/busybox:latest
sha256:f3cfc9d0dbf931d3db4685ec659b7ac68e2a578219da4aae65427886e649b06b

In this case, the values are equal. That means the Simple Signing document has been created for the actual image we want to verify.

However, this is not enough, we have to ensure this Simple Signing document has actually been signed by someone we trust.

We have to go back to the first - and only - layer defined inside of the manifest of the signature object. That's the JSON output we got some lines above:

{
  "mediaType": "application/vnd.dev.cosign.simplesigning.v1+json",
  "size": 248,
  "digest": "sha256:3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9",
  "annotations": {
    "dev.cosignproject.cosign/signature": "MEYCIQDWWxPQa3XFUsPbyTY+n+bZu/6Pwhg5WwyYDQtEfQho9wIhAPkKW7eub8b7BX+YbbRac8TwwIrK5KxvdtQ6NuoD+ivW",
    "dev.sigstore.cosign/bundle": "{\"SignedEntryTimestamp\":\"MEUCIDx9M+yRpD0O47/Mzm8NAPCbtqy4uiTkLWWexW0bo4jZAiEA1wwueIW8XzJWNkut5y9snYj7UOfbMmUXp7fH3CzJmWg=\",\"Payload\":{\"body\":\"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzYWY0NDE0ZDIwYzllMWNiNzZjY2M3MmFhZThiMjQyMTY2ZGFiZTZhZjUzMWE0YTc5MGRiOGUyZjBlNWVlN2M5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURXV3hQUWEzWEZVc1BieVRZK24rYlp1LzZQd2hnNVd3eVlEUXRFZlFobzl3SWhBUGtLVzdldWI4YjdCWCtZYmJSYWM4VHd3SXJLNUt4dmR0UTZOdW9EK2l2VyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRFdG9SRGRHTlU5TGVUYzNXalU0TWxrMmFEQjFNVW96UjA1Qkt3cHJkbFZ6YURSbFMzQmtNV3gzYTBSQmVtWkdSSE0zZVZoRlJYaHpSV3RRVUhWcFVVcENaV3hFVkRZNGJqZFFSRWxYUWk5UlJWazNiWEpCUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19\",\"integratedTime\":1634714179,\"logIndex\":783606,\"logID\":\"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d\"}}"
  }
}

We basically have to verify that, given:

  • The Simple Signing document shown above (without the "pretty-print make-up" provided by jq)
  • The public key of the signer we trust

The signature produced will match the value defined inside of annotations.dev.cosignproject.cosign/signature. In this case, the string MEYCIQDWWxPQ....

cosign bundle

Now we can take a look at the value referenced inside of the annotation called dev.sigstore.cosign/bundle. This annotation is produced by cosign only when the Rekor integration is enabled:

crane manifest $COSIGN_IMAGE | \
  jq '.layers[0].annotations."dev.sigstore.cosign/bundle" | fromjson'

We had to use the fromjson directive of jq because the value referenced by dev.sigstore.cosign/bundle is a string containing escaped JSON.

This is the output produced by the command:

{
  "SignedEntryTimestamp": "MEUCIDx9M+yRpD0O47/Mzm8NAPCbtqy4uiTkLWWexW0bo4jZAiEA1wwueIW8XzJWNkut5y9snYj7UOfbMmUXp7fH3CzJmWg=",
  "Payload": {
    "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzYWY0NDE0ZDIwYzllMWNiNzZjY2M3MmFhZThiMjQyMTY2ZGFiZTZhZjUzMWE0YTc5MGRiOGUyZjBlNWVlN2M5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURXV3hQUWEzWEZVc1BieVRZK24rYlp1LzZQd2hnNVd3eVlEUXRFZlFobzl3SWhBUGtLVzdldWI4YjdCWCtZYmJSYWM4VHd3SXJLNUt4dmR0UTZOdW9EK2l2VyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRFdG9SRGRHTlU5TGVUYzNXalU0TWxrMmFEQjFNVW96UjA1Qkt3cHJkbFZ6YURSbFMzQmtNV3gzYTBSQmVtWkdSSE0zZVZoRlJYaHpSV3RRVUhWcFVVcENaV3hFVkRZNGJqZFFSRWxYUWk5UlJWazNiWEpCUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19",
    "integratedTime": 1634714179,
    "logIndex": 783606,
    "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
  }
}

OPEN QUESTIONS:

  • Which values are used to produce the SignedEntryTimestamp? Does it have to be verified?

The Payload.body is a base64 encoded string, let's take a look at it:

crane manifest $COSIGN_IMAGE | \
  jq '.layers[0].annotations."dev.sigstore.cosign/bundle" | fromjson | .Payload.body | @base64d | fromjson'

This is the output produced:

{
  "apiVersion": "0.0.1",
  "kind": "rekord",
  "spec": {
    "data": {
      "hash": {
        "algorithm": "sha256",
        "value": "3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9"
      }
    },
    "signature": {
      "content": "MEYCIQDWWxPQa3XFUsPbyTY+n+bZu/6Pwhg5WwyYDQtEfQho9wIhAPkKW7eub8b7BX+YbbRac8TwwIrK5KxvdtQ6NuoD+ivW",
      "format": "x509",
      "publicKey": {
        "content": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFTEtoRDdGNU9LeTc3WjU4Mlk2aDB1MUozR05BKwprdlVzaDRlS3BkMWx3a0RBemZGRHM3eVhFRXhzRWtQUHVpUUpCZWxEVDY4bjdQRElXQi9RRVk3bXJBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
      }
    }
  }
}

This is a rekord object, with apiVersion equal to 0.0.1.

There are many interesting things inside of this object.

Simple Signing checksum

The signature contains a checksum under the spec.data.hash.value field.

crane manifest $COSIGN_IMAGE | \
  jq '.layers[0].annotations."dev.sigstore.cosign/bundle" | fromjson | .Payload.body | @base64d | fromjson | .spec.data.hash.value'

This will print "3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9", which is the checksum of the Simple Signing document associated with the image.

In our case, we can obtain that by doing:

crane blob registry-testing.svc.lan/busybox@sha256:3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9 | sha256sum
3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9  -

As we can seen the checksum reported inside of the annotation signature matches with the actual one from the registry.

The Signature object

The object has an attribute signature.content which is, again, the signature produced by the private key when signing the Simple Signing object associated with the image.

The public key associated to the private key that was used to sign the artifact is embedded into the publicKey.content field. The value is base64 encoded.

We can get it with the following command:

crane manifest $COSIGN_IMAGE | \
  jq -r '.layers[0].annotations."dev.sigstore.cosign/bundle" | fromjson | .Payload.body | @base64d | fromjson | .spec.signature.publicKey.content | @base64d'

This will produce the following output:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELKhD7F5OKy77Z582Y6h0u1J3GNA+
kvUsh4eKpd1lwkDAzfFDs7yXEExsEkPPuiQJBelDT68n7PDIWB/QEY7mrA==
-----END PUBLIC KEY-----

This is exactly the same value stored inside of the cosign.pub file we created at the beginning of the document.

Rekor metadata

Let's go back to the contents of the dev.sigstore.cosign/bundle layer:

{
  "SignedEntryTimestamp": "MEUCIDx9M+yRpD0O47/Mzm8NAPCbtqy4uiTkLWWexW0bo4jZAiEA1wwueIW8XzJWNkut5y9snYj7UOfbMmUXp7fH3CzJmWg=",
  "Payload": {
    "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoicmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzYWY0NDE0ZDIwYzllMWNiNzZjY2M3MmFhZThiMjQyMTY2ZGFiZTZhZjUzMWE0YTc5MGRiOGUyZjBlNWVlN2M5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURXV3hQUWEzWEZVc1BieVRZK24rYlp1LzZQd2hnNVd3eVlEUXRFZlFobzl3SWhBUGtLVzdldWI4YjdCWCtZYmJSYWM4VHd3SXJLNUt4dmR0UTZOdW9EK2l2VyIsImZvcm1hdCI6Ing1MDkiLCJwdWJsaWNLZXkiOnsiY29udGVudCI6IkxTMHRMUzFDUlVkSlRpQlFWVUpNU1VNZ1MwVlpMUzB0TFMwS1RVWnJkMFYzV1VoTGIxcEplbW93UTBGUldVbExiMXBKZW1vd1JFRlJZMFJSWjBGRlRFdG9SRGRHTlU5TGVUYzNXalU0TWxrMmFEQjFNVW96UjA1Qkt3cHJkbFZ6YURSbFMzQmtNV3gzYTBSQmVtWkdSSE0zZVZoRlJYaHpSV3RRVUhWcFVVcENaV3hFVkRZNGJqZFFSRWxYUWk5UlJWazNiWEpCUFQwS0xTMHRMUzFGVGtRZ1VGVkNURWxESUV0RldTMHRMUzB0Q2c9PSJ9fX19",
    "integratedTime": 1634714179,
    "logIndex": 783606,
    "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
  }
}

There's an important field that points back to Rekor, the Payload.logIndex:

crane manifest $COSIGN_IMAGE | \
  jq '.layers[0].annotations."dev.sigstore.cosign/bundle" | fromjson | .Payload.logIndex'

This will return the index of this signature inside of the Rekor transparency log. In this case, the index is 783606.

We can now use the rekor cli to fetch this entry from the central transparency log server:

rekor get --log-index 783606 --format json | jq

Which produces the following output:

{
  "Attestation": "",
  "AttestationType": "",
  "Body": {
    "RekordObj": {
      "data": {
        "hash": {
          "algorithm": "sha256",
          "value": "3af4414d20c9e1cb76ccc72aae8b242166dabe6af531a4a790db8e2f0e5ee7c9"
        }
      },
      "signature": {
        "content": "MEYCIQDWWxPQa3XFUsPbyTY+n+bZu/6Pwhg5WwyYDQtEfQho9wIhAPkKW7eub8b7BX+YbbRac8TwwIrK5KxvdtQ6NuoD+ivW",
        "format": "x509",
        "publicKey": {
          "content": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFTEtoRDdGNU9LeTc3WjU4Mlk2aDB1MUozR05BKwprdlVzaDRlS3BkMWx3a0RBemZGRHM3eVhFRXhzRWtQUHVpUUpCZWxEVDY4bjdQRElXQi9RRVk3bXJBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="
        }
      }
    }
  },
  "LogIndex": 783606,
  "IntegratedTime": 1634714179,
  "UUID": "4ec21e0b9bc52a3fff10ccc6bdd12aeb480a7da7fa9578f88f3227e532bcfbe2",
  "LogID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"
}

As we can see, the signature record (the Body.RekorObj) is equal to the object we fetched from our local registry. That means nobody tampered the signature object we have inside of our local registry.

OPEN QUESTION: I guess that means we have to compare the remote rekor object with the one fetched from the local registry, and make sure no changes have been done?

Other open questions

  • In this scenario, we must keep the public key we used to sign the software around. This is needed to later verify the different signatures. We cannot just use the public key that is embedded into the transparency log. Is that correct?