CoCo Keyprovider is a very simple keyprovider tool, which can help to generate CoCo-compatible encrypted images. The encrypted image can be decrypted using the following Key Broker Client (KBC):
- cc-kbc
- offline-fs-kbc
- online-sev-kbc
- sample kbc (toy KBC still supported for historical reason)
The following guide will help make an encrypted image using skopeo and CoCo keyprovider, inspect the image as well as decrypt it.
A docker image provides prebuilt CoCo keyprovider and skopeo to simplify image encryption:
$ docker run ghcr.io/confidential-containers/staged-images/coco-keyprovider /encrypt.sh -h
usage: /encrypt.sh [-k <b64-encoded key>] [-i <key id>] [-s <source>] [-d <destination>]Source and destination have to be provided as container/image transport URIs.
This example will encrypt an image from docker/library and buffer the resulting encrypted image in a local ./output folder:
head -c 32 /dev/urandom | openssl enc > image_key
mkdir output
docker run -v "$PWD/output:/output" ghcr.io/confidential-containers/staged-images/coco-keyprovider:latest /encrypt.sh \
-k "$(base64 < image_key)" \
-i kbs:///some/key/id \
-s docker://nginx:stable \
-d dir:/outputThe image can then be pushed to a registry using skopeo:
skopeo copy dir:output docker://ghcr.io/confidential-containers/nginx-encryptedAlternatively, an authorization file can be mounted to the container to be able to access private registries directly:
docker run -v ~/.docker/config.json:/root/.docker/config.json ghcr.io/confidential-containers/staged-images/coco-keyprovider:latest /encrypt.sh \
-k "$(base64 < image_key)" \
-i kbs:///some/key/id \
-s docker://private.registry.io/nginx:stable \
-d docker://private.registry.io/nginx:encryptedBuild and run CoCo keyprovider at localhost on port 50000:
$ cd attestation-agent/coco_keyprovider
$ RUST_LOG=coco_keyprovider cargo run --release -- --socket 127.0.0.1:50000 &Skopeo leverages the Ocicrypt library to encrypt/decrypt images. Create an Ocicrypt keyprovider configuration file as shown below and export the OCICRYPT_KEYPROVIDER_CONFIG variable:
$ cat <<EOF > ocicrypt.conf
{
"key-providers": {
"attestation-agent": {
"grpc": "127.0.0.1:50000"
}}}
EOF
$ export OCICRYPT_KEYPROVIDER_CONFIG="$(pwd)/ocicrypt.conf"Use the skopeo's copy command to copy the original image and encrypt it. This tool may copy images from/to different storages using various transport protocols (e.g. docker, oci, docker-archive,...etc) but not all storages support encrypted images. Also beware that skopeo will not warn that the image was left unencrypted in case of failure. In our experience, two scenarious will produce a correct encrypted image:
- copying to oci image on the current directory
- copying directly to a remote image registry using the docker protocol, as long as the destination registry support encrypted images. Examples of image registry services that support encrypted images are docker.io and ghcr.io.
The following example copy busybox and encrypt to busybox_encrypted image in the current directory:
$ skopeo copy --insecure-policy --encryption-key provider:attestation-agent:<parameters> docker://busybox oci:busybox_encryptedOr we can directly push the encrypted image to the remote image registry:
$ skopeo copy --insecure-policy --encryption-key provider:attestation-agent:<parameters> docker://busybox docker://docker.io/myrepo/busybox:encryptedOn the examples above the --insecure-policy option is not needed for encryption, it only disables the system's trust policy to avoid any errors with image validation, so it can be ommitted if your system's policy is properly configured. The --encryption-key specifies the encryption protocol that will be explained on the next section.
As shown on the previous section, the encryption protocol (--encryption-key) passed to skopeo has the provider:attestation-agent:<parameters> format.
The <parameters> is a key-value list separated by double colons (e.g. key1=value1::key2=value2). Here are the defined keys:
sample: Not required. Eithertrueorfalse. If not set, usefalse. This value indicates whether the hardcoded encryption key is used. This works the same way assample keyprovider.keyid: Required ifsampleis not enabled. It is a Key Broker Service (KBS) Resource URI (see the specification below). When decryption occurs, thekeyidvalue is used to index the Key Encryption Key (KEK).keypath: Required ifsampleis not enabled. A local filesystem path, absolute path recommended. Specify the KEK to encrypt the image in local filesystem. KEK will be read from filesystem and then used to encrypt the image. This key's length must be 32 bytes.algorithm: Not required. Indicate the encryption algorithm used. EitherA256GCMorA256CTR. If not provided, useA256GCMby default as it is AEAD scheme.
The keyid parameter refers an KBS Resource URI and must follow one of the following formats,
kbs:///<repository>/<type>/<tag>kbs://<kbs-addr>/<repository>/<type>/<tag><kbs-addr>/<repository>/<type>/<tag>/<repository>/<type>/<tag>
Where:
kbs-addr: is theaddress[:port]of the KBSrepository: is the resource's repository (e.g. docker.io)type: is the resource type (e.g. key)tag: is the resource tag or identifier (e.g. key_id1)
This section contain encrypting examples.
Let's start with the simplest example possible, which is to encrypt an image using the sample key provider:
$ skopeo copy --insecure-policy --encryption-key provider:attestation-agent:sample=true docker://busybox oci:busybox_encrypted:sampleFor offline-fs-kbc and online-sev-kbc the KBS address is ommitted and the encryption key created upfront. This key can be then provisioned in a KBS as, for example, on the simple-kbs for online-sev-kbc.
So create a random 32-bytes key file:
$ head -c32 < /dev/random > key1Use key of path key1, and keyid kbs:///default/key/key_id1 to encrypt an image. In this way sample is disabled, and will use A256GCM (AES-256-GCM):
$ skopeo copy --insecure-policy --encryption-key provider:attestation-agent:keypath=$(pwd)/key1::keyid=kbs:///default/key/key_id1::algorithm=A256GCM docker://busybox oci:busybox_encrypted:defaultIf not sure about whether the image is encrypted, we can export the image to check whether it is encrypted.
The examples one and two of the previous section, we can find the OCI image in the busybox directory at the current directory.
Note : If the image is pushed to a registry, use the "docker://" transport protocol instead on the examples below.
You can use skopeo's inspect command to print low-level information of the image:
$ skopeo inspect oci:busybox_encrypted:default
{
"Digest": "sha256:28d649e5c1fb00b5a2cfdc8a0e95057a17addf80797ce2a6b45d89964b35b968",
"RepoTags": [],
"Created": "2023-05-11T22:48:43.533857581Z",
"DockerVersion": "",
"Labels": null,
"Architecture": "amd64",
"Os": "linux",
"Layers": [
"sha256:4d7ebe01f6574c525dc52ad6506d19aac1ad14eb783955cc1df93fda14073ae1"
],
"LayersData": [
{
"MIMEType": "application/vnd.oci.image.layer.v1.tar+gzip+encrypted",
"Digest": "sha256:4d7ebe01f6574c525dc52ad6506d19aac1ad14eb783955cc1df93fda14073ae1",
"Size": 2590751,
"Annotations": {
"org.opencontainers.image.enc.keys.provider.attestation-agent": "eyJraWQiOiJrYnM6Ly8vZGVmYXVsdC9rZXkva2V5X2lkMSIsIndyYXBwZWRfZGF0YSI6IjNFZ1FidzZDUlY0YmlrMnNLM3RrTUpweWNWV0RVZXVIY1luZ1drZFd1K0swQXpDUGY3dFlCQ1oxSGxXaWFsZmdFdEQxdDBuc3N2YS81aElFbUxPbXZjYXJ2SGlNdERyRElhY1JIdElOTHFyUUpCZUY1M2Q4MTN1L0dDK3prL3RHeEF3ZVd6ZTR1S0VROG1qc2hyMytiYll3RUhKdVFyM3VncWlXRTlnNUhndU1HVmVFZ2ZReWR2dS9TZmVYMmZSeTRQWmtGcjhWbkQ3WjRrNUhXVkhaTWY0U21oSUhhUnlVa1NoT3B4dVdQcG54OW9IaGJSMEdKd2Zwb3l4TzRydEpYaTI4ODVxZ1Uya0dVaFo2RTJTbmgrQT0iLCJpdiI6IjRlekxiZU1RZEVrWS9pdUYiLCJ3cmFwX3R5cGUiOiJBMjU2R0NNIn0=",
"org.opencontainers.image.enc.pubopts": "eyJjaXBoZXIiOiJBRVNfMjU2X0NUUl9ITUFDX1NIQTI1NiIsImhtYWMiOiJQM08rQ1lhRGNSNkJteEpvdWlFV0lYM05XN1l2Nk5UcEp5dmhlNlBmbFA4PSIsImNpcGhlcm9wdGlvbnMiOnt9fQ=="
}
}
],
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
]
}We can see that the layer's MIMEType is application/vnd.oci.image.layer.v1.tar+gzip+encrypted, which means the layer is encrypted.
The org.opencontainers.image.enc.keys.provider.attestation-agent layer's annotation contains (base64 encoded) the used encryption parameters, and org.opencontainers.image.enc.pubopts the cipher options. Those are used by the kbc on the decryption process.
You can inspect content of the layer's annotations as:
$ skopeo inspect oci:busybox_encrypted:default | jq -r '.LayersData[].Annotations."org.opencontainers.image.enc.keys.provider.attestation-agent"' | base64 -d
{"kid":"kbs:///default/key/key_id1","wrapped_data":"3EgQbw6CRV4bik2sK3tkMJpycVWDUeuHcYngWkdWu+K0AzCPf7tYBCZ1HlWialfgEtD1t0nssva/5hIEmLOmvcarvHiMtDrDIacRHtINLqrQJBeF53d813u/GC+zk/tGxAweWze4uKEQ8mjshr3+bbYwEHJuQr3ugqiWE9g5HguMGVeEgfQydvu/SfeX2fRy4PZkFr8VnD7Z4k5HWVHZMf4SmhIHaRyUkShOpxuWPpnx9oHhbR0GJwfpoyxO4rtJXi2885qgU2kGUhZ6E2Snh+A=","iv":"4ezLbeMQdEkY/iuF","wrap_type":"A256GCM"}
$ skopeo inspect oci:busybox_encrypted:default | jq -r '.LayersData[].Annotations."org.opencontainers.image.enc.pubopts"' | base64 -d
{"cipher":"AES_256_CTR_HMAC_SHA256","hmac":"P3O+CYaDcR6BmxJouiEWIX3NW7Yv6NTpJyvhe6PflP8=","cipheroptions":{}}Another way to ensure the image is encrypted is to use offline_fs_kbc to test, which will be described in the following section.
Let's show how the image created on example two can be decrypted.
Build and run Attestation Agent (AA) at localhost on port 48888:
$ cd attestation-agent
$ make KBC=offline_fs_kbc && make DESTDIR="$(pwd)" install
$ RUST_LOG=attestation_agent ./attestation-agent --keyprovider_sock 127.0.0.1:48888 &Create a new ocicrypt.conf and re-export OCICRYPT_KEYPROVIDER_CONFIG:
$ cat <<EOF > ocicrypt.conf
{
"key-providers": {
"attestation-agent": {
"grpc": "127.0.0.1:48888"
}}}
EOF
$ export OCICRYPT_KEYPROVIDER_CONFIG="$(pwd)/ocicrypt.conf"Ensure the key is recorded in the /etc/aa-offline_fs_kbc-keys.json. In the example two in encryption, "default/key/key_id1":"<base64-encoded-key>" should be included in /etc/aa-offline_fs_kbc-keys.json:
$ cd attestation-agent/coco_keyprovider
$ ENC_KEY_BASE64="$(cat key1 | base64)"
$ cat <<EOF > aa-offline_fs_kbc-keys.json
{
"default/key/key_id1": "${ENC_KEY_BASE64}"
}
EOF
$ sudo cp aa-offline_fs_kbc-keys.json /etc/Decrypt the image:
$ skopeo copy --insecure-policy --decryption-key provider:attestation-agent:offline_fs_kbc::null oci:busybox_encrypted:default oci:busybox_decryptedThe decrypted image should have the layers's MIMEType equal to application/vnd.oci.image.layer.v1.tar+gzip as:
$ skopeo inspect oci:busybox_decrypted | jq -r '.LayersData[].MIMEType'
application/vnd.oci.image.layer.v1.tar+gziptools/generate_keys.sh is a helper script to generate ten different base64-encoded KEK. Also, it can help to generate the aa-offline_fs_kbc-keys.json used by offline-fs-kbc.
Warning: jq is required, so please install jq before use the script. For example on ubuntu, apt install jq could help.
tools/generate_keys.sh generate > aa-offline_fs_kbc-keys.jsonyou will get a randomly generated aa-offline_fs_kbc-keys.json like the following
{
"default/key/key_id7": "gH4b2MYb/lGrYRVmrf3qtNo46mzC6C87cN5hdRt+Wvg=",
"default/key/key_id6": "cemWcxJw9fG11Y3WhrzcZNKBWebdFt7qQDsIa6h6kJI=",
"default/key/key_id10": "FePQCdgX/UONTIOl6eQP+7Itq5fdwr8JPufkmypbMWI=",
"default/key/key_id5": "uDAikSR/Hadk7RXkA86DBsQs8eekP/dkLkXyuVjyLf8=",
"default/key/key_id4": "5PN33vLpX+UO82Ryl/BhZNm5+suhTrBEqdWTDi1wqVU=",
"default/key/key_id3": "Y8ptYp5WgJxXdhe5tIS+ZIoSlVup1DUHu63MyHOsxsA=",
"default/key/key_id2": "sIDb9K+J7IEfYQMbcDzEYz8t+hABQlHSR+55SIAgsUw=",
"default/key/key_id1": "TfyYGL/gBbtXwgDmx3a6N6WxFJFamcSRUFlpBPXu0f4=",
"default/key/key_id9": "CfGZPsmKq1pkzraBpMAsbBZGIGlbWUepu4eR/Z4exnE=",
"default/key/key_id8": "eXIlv83nTjfyeLZcCvTda9ypYIYj83eGjbqjoVFAQHA="
}
If we want to use key of id default/key/key_id1 in aa-offline_fs_kbc-keys.json to encrypt an image, we firstly export the key content to file key1
tools/generate_keys.sh export aa-offline_fs_kbc-keys.json default/key/key_id1 key1
Warning : As in current code we do not actually use the
<kbs-addr>in a KBS Resource URI. When using skopeo to encrypt the image, we can specify thekeyidin formatkbs:///<repo>/<type>/<tag>, e.g.kbs:///default/key/key_id1.