> Markdown version of https://docs.laavat.io/exampleflows/ocisigning/ from the LAAVAT PKI and Signing Platform documentation. All pages: https://docs.laavat.io/llms.txt

# 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](https://docs.sigstore.dev/cosign/signing/signing_with_containers/) 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.](https://docs.laavat.io/api/authentication/)
New token must be requested if the current token expires. ( roughly 1 hour of validity)

```bash
(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](https://docs.sigstore.dev/cosign/system_config/installation/) 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:

```json
{
    "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:

```bash
(venv) $ signing-tool -n test.ini 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](https://docs.laavat.io/usage/productguide/pki-examples/#example-of-digest-signing-with-detached-signature-profile) for profile examples.

Create a product with a `DetachedSignature` operation type:

```json
{
    "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](https://docs.laavat.io/exampleflows/detachedsigning/#issue-certificate-using-openssl) 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](https://docs.laavat.io/usage/clientguide/clients/) for detailed instructions on client registration.

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

```bash
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](https://docs.laavat.io/usage/clientguide/clients/#example-usage-with-reference-client-package-add-a-client-for-the-product) for the registration process.

**3. Fetch product secrets using the registered client:**

```bash
(venv) $ signing-tool -n test.ini \
    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:

```bash
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):

```bash
# 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:

```bash
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:

```bash
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:

```bash
(venv) $ signing-tool -n test.ini 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:

```bash
(venv) $ signing-tool -n approver.ini imagesigning approve \
    -I $REQUEST_ID
```

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

```bash
(venv) $ signing-tool -n test.ini imagesigning get \
    -I $REQUEST_ID -O signature.b64 --skipBase64
```

### Step 4: Attach Signature to Container

Attach the signature to the container image:

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

### Complete Example Script

Here is a complete example combining all steps:

```bash
#!/bin/bash
set -e

# Configuration
IMAGE="your-registry.com/your-image@sha256:abc123..."
PRODUCTID="your-product-id"
OPERATIONID="your-operation-id"
API_URL="https://app.laavat.io/<CustomerName>/api/v1"
# Point TOKEN_FILE at the file your CI secret manager exposes the JWT as.
# Passing the token via a file (-t @"$TOKEN_FILE") or stdin (-t @-) keeps it
# off the command line and out of the process list.
TOKEN_FILE="/run/secrets/laavat-token"

# 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 --json -c -t @"$TOKEN_FILE" -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_FILE" -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:

```bash
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](https://docs.laavat.io/exampleflows/ocisigning/#retrieve-public-key)).
- For **DetachedSignature**, the public key is extracted from the signing certificate (see [Retrieve Signing Certificate and Public Key](https://docs.laavat.io/exampleflows/ocisigning/#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:

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

**2. Copy and verify the image:**

```bash
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:

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

## Reference Client Usage Summary

### Sign a container (DigestSigning)

```bash
# 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 -n test.ini 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 -n test.ini imagesigning get \
    -I $REQUEST_ID -O signature.b64 --skipBase64

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

### Sign a container (DetachedSignature)

```bash
# 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 -n test.ini 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 -n test.ini 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).
