Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

On this page

Countersign an attestation

A countersigned attestation is one DSSE envelope with two or more signatures over the same payload. The usual shape is a CI job that signs with its short-lived platform certificate (keyless Fulcio, bound to the job's identity) and a key your organization controls, such as an AWS KMS key, that adds its own signature. A policy can then require both, so neither the CI identity nor the key alone is enough to produce evidence that passes.

Sign with both

cilock run and cilock sign sign with exactly one signer unless you pass --countersign:

cilock run \
  --platform-url https://platform.testifysec.com \
  --signer-kms-ref awskms:///alias/release-countersign \
  --countersign \
  --step build -o build.att.json -- make release

In a GitHub Actions job with permissions: id-token: write (or after cilock login on a workstation) the platform keyless certificate signs, and the KMS key countersigns. Each signature carries its own RFC 3161 timestamp from the platform TSA, or from --timestamp-servers when you set it.

Why it is opt-in: without --countersign, an explicit --signer-* key replaces the keyless identity a login or CI job would otherwise add. That rule keeps a logged-in cilock sign -k key.pem from quietly putting your login identity on the envelope as well. With --countersign cilock keeps both, and it refuses to run when fewer than two signers resolve, so a job that lost its OIDC permission fails before the build instead of shipping a single signature.

Two --signer-* providers work the same way, for example a file key and a KMS key. cilock verify --vsa-outfile signs the VSA with every --signer-* provider you pass.

The AWS, GCP and Azure KMS providers are compiled into cilock-all and custom builds, not the default cilock binary. See build a custom CI/lock.

Require both in the policy

Set threshold on the step to the number of functionaries that must sign:

{
  "expires": "2027-01-01T00:00:00Z",
  "roots": {
    "platform-fulcio": { "certificate": "<base64 PEM of the platform Fulcio root>" }
  },
  "timestampauthorities": {
    "platform-tsa": { "certificate": "<base64 PEM of the platform TSA root>" }
  },
  "publickeys": {
    "<keyid of the KMS public key>": {
      "keyid": "<keyid of the KMS public key>",
      "key": "<base64 PEM of the KMS public key>"
    }
  },
  "steps": {
    "build": {
      "name": "build",
      "threshold": 2,
      "functionaries": [
        {
          "type": "root",
          "certConstraint": {
            "roots": ["platform-fulcio"],
            "commonname": "*", "dnsnames": ["*"], "emails": ["*"], "organizations": ["*"],
            "uris": ["https://github.com/acme/app/.github/workflows/release.yml@refs/heads/main"]
          }
        },
        { "type": "publickey", "publickeyid": "<keyid of the KMS public key>" }
      ],
      "attestations": [{ "type": "https://aflock.ai/attestations/command-run/v0.1" }]
    }
  }
}

Get the KMS public key with aws kms get-public-key --key-id alias/release-countersign --output text --query PublicKey | base64 -d > kms.der, convert it with openssl pkey -pubin -inform DER -in kms.der -out kms.pub, and read its key ID with cilock keyid show kms.pub. Embedding the public key this way lets cilock verify check the countersignature without AWS credentials.

threshold counts functionaries matched to distinct keys:

  • Omitted, 0 or 1: any one functionary, which is how every existing policy behaves.
  • Equal to the number of functionaries: all of them.
  • Anything in between: N of M.

One key fills one slot. The same key listed under two key IDs, or presented once in a certificate and once raw, counts once. A signature that does not verify does not count. If the step has a timestampConstraint, each counted signature has to meet it with its own timestamp, so an untimestamped countersignature cannot borrow the first signer's time.

When too few distinct functionaries signed, the collection is rejected with a reason that names the functionaries no signature satisfied:

step build requires signatures from 2 distinct functionaries, each with its own key; found 1;
no distinct signature satisfied: functionaries[1] (publickeyid 0f240c81...)

Steps that read this step through artifactsFrom or attestationsFrom only see collections that met the threshold.

A verifier older than this field does not know it. Under the v0.1 policy type it ignores threshold and accepts any one functionary. Sign the policy as https://aflock.ai/policy/v0.2 (cilock sign -t https://aflock.ai/policy/v0.2), which older verifiers decode strictly and refuse, or make sure every verifier that reads the policy is current.

Write the certificate constraint tightly

Two keyless certificates from the same CI identity are two different keys, because Fulcio issues a fresh key per signature. If two root functionaries in a 2-of-2 step accept the same identity, one job can satisfy both by signing twice. Give each functionary constraints that only its own signer meets, or pair one certificate functionary with one key you control, as in the example above.

Reference generated from the product documentation. Match commands and support details to your installed release.