# Countersign an attestation

Source: https://www.testifysec.com/docs/cilock/guides/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.

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

 

```bash
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](https://www.testifysec.com/docs/cilock/guides/build-a-custom-cilock).

 

## Require both in the policy

 

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

 

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

 

```text
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.
