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:
- DigestSigning - Uses an HSM-backed key pair created within the product operation. Suitable for simple signing workflows.
- 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:
- Get the CSR from the created product
- Have the CSR signed by your external CA
- 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-tlogflag 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).