Skip to content

OCI container signing example

This example shows how to sign OCI container images using HSM-backed keys. Two approaches are supported: DigestSigning (simple key pair) and DetachedSignature (certificate-based).

This example uses the reference python client and the cosign tool to perform different actions.

The example is divided to following stages:

  • Prerequisites and tools
  • Product configuration for DigestSigning
  • Product configuration for DetachedSignature
  • Cosign workflow for container signing
  • Verification

Authentication

In order to use the reference python client a valid JWT token is needed. How to obtain one is explained in more detail in the authentication chapter. New token must be requested if the current token expires. ( roughly 1 hour of validity)

(venv) $ az login --allow-no-subscriptions --only-show-error
A web browser has been opened at https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.

[1]    N/A(tenant level account)  <redacted>  <redacted>

The default is marked with an *; the default tenant is '<redacted>' and subscription is 'N/A(tenant level account)' (<redacted>).

Select a subscription and tenant (Type a number or Enter for no changes): 1

Tenant: <redacted>
Subscription: N/A(tenant level account) (<redacted>)


(venv) $ az account get-access-token --resource api://<redacted>
{
  "accessToken": "<redacted>",
  "expiresOn": "2025-06-04 15:18:10.000000",
  "expires_on": 1749039490,
  "subscription": "<redacted>",
  "tenant": "<redacted>",
  "tokenType": "Bearer"
}
(venv) $ export TOKEN=<redacted>

Overview

Two approaches are supported for OCI signing:

  1. DigestSigning - Uses an HSM-backed key pair created within the product operation. Suitable for simple signing workflows.
  2. DetachedSignature - Uses a code-signing certificate (externally or internally issued) with the private key stored in the HSM. Suitable when certificate-based trust chains are required.

Prerequisites

  • Product created with a configuration supporting OCI signing (DigestSigning or DetachedSignature operation type)
  • Container image pushed to a registry with the URI available
  • cosign installed on the client machine (tested with version 3.0.4)
  • Necessary approvals configured in the product

OCI Signing with DigestSigning

This approach creates an HSM-backed key pair for signing digests. The public key can be exported for verification.

Product Configuration

Create a product with a DigestSigning operation type:

{
    "name": "$PRODUCTNAME",
    "description": "Product for OCI container signing",
    "productType": "Production",
    "enabled": true,
    "caInfo": [],
    "rndKeys": [],
    "productOperations": [
        {
            "name": "OCI digest signing",
            "description": "RSA key for OCI container signing",
            "operationType": "DigestSigning",
            "token": {
                "name": "OCI signing key",
                "description": "HSM token for OCI container signing",
                "keyType": "RSA4096"
            },
            "approvalRule": {
                "name": "Rule for access",
                "description": "Rule for OCI signing",
                "allowedGroups": [
                    "$WRITERGROUP"
                ],
                "approvalGroups": [
                    "$APPROVERGROUP"
                ],
                "blanketGroups": []
            }
        }
    ]
}

Replace $PRODUCTNAME, $WRITERGROUP, and $APPROVERGROUP with your values.

Retrieve Public Key

After product creation and approval, retrieve the public key for client-side verification:

(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 product \
    get -I $PRODUCTID

The public key can be extracted from the product operation's token information.

OCI Signing with DetachedSignature

This approach uses a code-signing certificate for signing. The certificate can be issued externally or through LAAVAT's PKI.

Product Configuration for DetachedSignature

First, create a PKI profile for the code-signing certificate. See Digest signing with detached signature profile for profile examples.

Create a product with a DetachedSignature operation type:

{
    "name": "$PRODUCTNAME",
    "description": "Product for OCI container signing with detached signature",
    "productType": "Production",
    "enabled": true,
    "caInfo": [
        {
            "CN": "OCI signing certificate for $PRODUCTNAME",
            "description": "End-Entity certificate for OCI container signing",
            "externalRoot": true,
            "useCase": "CodeSigning",
            "keyType": "RSA2048",
            "certificateType": "ENDENTITY",
            "profileID": "$ENDSIGPROFILEID"
        }
    ],
    "rndKeys": [],
    "productOperations": [
        {
            "name": "OCI detached signature",
            "description": "Generate detached signature for OCI containers",
            "operationType": "DetachedSignature",
            "approvalRule": {
                "name": "OCI signing rule",
                "description": "Rule for OCI container signing",
                "allowedGroups": [
                    "$WRITERGROUP"
                ],
                "approvalGroups": [
                    "$APPROVERGROUP"
                ],
                "blanketGroups": []
            }
        }
    ]
}

Replace $PRODUCTNAME, $ENDSIGPROFILEID, $WRITERGROUP, and $APPROVERGROUP with your values.

Certificate Issuance

For externally rooted certificates (externalRoot: true), you need to:

  1. Get the CSR from the created product
  2. Have the CSR signed by your external CA
  3. Upload the certificate chain back to the product

See the detached signing example for detailed steps.

Retrieve Signing Certificate and Public Key

To retrieve the signing certificate, you first need to register a client for the product. See Clients for detailed instructions on client registration.

1. Generate a client key pair (if not already done):

openssl genrsa -out client.private 2048
openssl rsa -in client.private -pubout -out client.public

2. Register the client for the product and get approval.

See Client Registration for the registration process.

3. Fetch product secrets using the registered client:

(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 \
    secrets get -P $PRODUCTID -C client.private -O /tmp/prod.json

4. Extract the signing certificate and public key:

First, list available certificates in the product to find the end entity certificate CN:

jq -r '.caInfo[] | .cn' /tmp/prod.json

Then extract the PKCS7 chain for your end entity certificate (replace YOUR_CERTIFICATE_CN with the actual CN):

# Extract the PKCS7 chain for the signing certificate from the JSON output
jq -r '.caInfo[] | select(.cn=="YOUR_CERTIFICATE_CN") | .pkcs7chain' \
    /tmp/prod.json | base64 -d > chain.p7b

# List all certificates in the chain (end entity certificate is typically listed first)
openssl pkcs7 -in chain.p7b -inform PEM -print_certs

# Copy the end entity certificate to endentity.pem

# Extract the public key from the end entity certificate
openssl x509 -in endentity.pem -pubkey -noout > publickey.pem

Cosign Workflow for Container Signing

Both DigestSigning and DetachedSignature approaches use the same client-side workflow with cosign.

Step 1: Generate Payload

Generate the signature payload from the container image:

IMAGE=your-registry.com/your-image@sha256:abc123...
cosign generate $IMAGE > payload.json

Step 2: Calculate Digest

Calculate the SHA256 digest of the payload in base64 format:

DIGEST=$(sha256sum < payload.json | cut -d" " -f1 | xxd -r -p | base64 -w 0)
echo $DIGEST

Step 3: Sign the Digest

Submit the digest for signing using the signing-tool:

(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 imagesigning add DigestSigning \
    -N "OCI signing request" -D "Container image signing" \
    -P $PRODUCTID --operid $OPERATIONID \
    -p "$DIGEST" -H SHA256
  • $PRODUCTID - Your product ID
  • $OPERATIONID - The operation ID for DigestSigning or DetachedSignature
  • $DIGEST - The base64-encoded SHA256 digest from Step 2

The command returns a request ID ($REQUEST_ID) that is used in subsequent steps.

If approval is required, approve the request:

(venv) $ signing-tool -c -t $APPROVERTOKEN -a https://app.laavat.io/<CustomerName>/api/v1 imagesigning approve \
    -I $REQUEST_ID

Download the signature after approval (or after auto-approval):

(venv) $ signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 imagesigning get \
    -I $REQUEST_ID -O signature.b64 --skipBase64

Step 4: Attach Signature to Container

Attach the signature to the container image:

cosign attach signature --payload payload.json --signature signature.b64 $IMAGE

Complete Example Script

Here is a complete example combining all steps:

#!/bin/bash
set -e

# Configuration
IMAGE="your-registry.com/your-image@sha256:abc123..."
PRODUCTID="your-product-id"
OPERATIONID="your-operation-id"
TOKEN="your-jwt-token"
API_URL="https://app.laavat.io/<CustomerName>/api/v1"

# Step 1: Generate payload
echo "Generating payload..."
cosign generate $IMAGE > payload.json

# Step 2: Calculate digest
echo "Calculating digest..."
DIGEST=$(sha256sum < payload.json | cut -d" " -f1 | xxd -r -p | base64 -w 0)

# Step 3: Sign the digest
echo "Signing digest..."
REQUEST_ID=$(signing-tool -c -t $TOKEN -a $API_URL imagesigning add DigestSigning \
    -N "OCI signing request" -D "Container image signing" \
    -P $PRODUCTID --operid $OPERATIONID \
    -p "$DIGEST" -H SHA256 | jq -r '.id')

# Step 4: Download signature
echo "Downloading signature..."
signing-tool -c -t $TOKEN -a $API_URL imagesigning get \
    -I $REQUEST_ID -O signature.b64 --skipBase64

# Step 5: Attach signature
echo "Attaching signature to container..."
cosign attach signature --payload payload.json --signature signature.b64 $IMAGE

echo "Container signed successfully!"

Verification

Verification with cosign

To verify a signed container using the public key:

cosign verify --key publickey.pem --insecure-ignore-tlog $IMAGE

Notes:

  • The --insecure-ignore-tlog flag is required because attached signatures from external signing tools are not uploaded to the Sigstore transparency log.
  • For DigestSigning, the public key can be retrieved from the product operation's token information (see Retrieve Public Key).
  • For DetachedSignature, the public key is extracted from the signing certificate (see Retrieve Signing Certificate and Public Key).

Verification with Skopeo

Skopeo can verify container signatures during image copy operations using a policy file.

1. Create a policy.json file:

Cosign-style signatures with a public key:

{
    "default": [
        {
            "type": "reject"
        }
    ],
    "transports": {
        "docker": {
            "your-registry.com": [
                {
                    "type": "sigstoreSigned",
                    "keyPath": "/path/to/publickey.pem",
                    "signedIdentity": {
                        "type": "matchRepository"
                    }
                }
            ]
        }
    }
}

2. Copy and verify the image:

skopeo copy --policy /path/to/policy.json \
    docker://your-registry.com/your-image:tag \
    docker://destination-registry.com/your-image:tag

To inspect signatures without copying:

skopeo inspect --raw docker://your-registry.com/your-image:tag

Reference Client Usage Summary

Sign a container (DigestSigning)

# Generate payload and calculate digest
cosign generate $IMAGE > payload.json
DIGEST=$(sha256sum < payload.json | cut -d" " -f1 | xxd -r -p | base64 -w 0)

# Submit signing request
signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 imagesigning add DigestSigning \
    -N "OCI signing request" -D "Container image signing" \
    -P $PRODUCTID --operid $OPERATIONID -p "$DIGEST" -H SHA256

# Download signature (after approval if required)
signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 imagesigning get \
    -I $REQUEST_ID -O signature.b64 --skipBase64

# Attach
cosign attach signature --payload payload.json --signature signature.b64 $IMAGE

Sign a container (DetachedSignature)

# Generate payload and calculate digest
cosign generate $IMAGE > payload.json
DIGEST=$(sha256sum < payload.json | cut -d" " -f1 | xxd -r -p | base64 -w 0)

# Submit signing request
signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 imagesigning add DigestSigning \
    -N "OCI signing request" -D "Container image signing" \
    -P $PRODUCTID --operid $OPERATIONID -p "$DIGEST" -H SHA256

# Download signature (after approval if required)
signing-tool -c -t $TOKEN -a https://app.laavat.io/<CustomerName>/api/v1 imagesigning get \
    -I $REQUEST_ID -O signature.b64 --skipBase64

# Attach
cosign attach signature --payload payload.json --signature signature.b64 $IMAGE

Note: Both use DigestSigning as the imagesigning command type. The difference is in the product configuration (operation type and whether certificates are involved).