Skip to main content
View Markdown ↗

Copy this page

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

On this page

Make a recovery requirement executable

Evaluate successful and failed rehearsals against the same signed recovery policy.

Jump to the written guide ↓

Follow along

Goal: evaluate both recovery results against a signed policy, including the failed rehearsal. The command's success condition belongs in the policy; signing a result alone does not make it acceptable.

The commands below were run end to end, offline, with CI/lock 4.6.2. They use a local file key for the producer and the policy signer so that every result can be checked without a platform. The last section covers the differences when your policy is signed by a platform identity.

Before you start

Complete recording recovery evidence. You need its rehearsal script, which writes out/restore-report.json on every run, and the policy schema for reference. You also need jq and OpenSSL 3. The LibreSSL openssl that ships with macOS cannot generate ed25519 keys, so install OpenSSL 3 and put it first on your PATH.

What the policy binds to

A verification names a subject: the thing the evidence must be about. Choose the report the rehearsal wrote, not the commit it ran from.

SubjectWhat it identifiesWhat it cannot tell apart
The report's SHA-256 (-f gap-report.json)The exact bytes a rehearsal run producedTwo runs that wrote byte-identical reports. Put a run identifier or timestamp in the report so each run's digest is its own.
The commit (--subjects sha1:<commit>)Git context. Every run at that commit carries it.A passing run and a failing run at the same commit. Offered both records, cilock verify passes on the passing one.

The commit is still recorded by the git attestor and is worth reading. It says what source the rehearsal ran from. It does not say which rehearsal result you are looking at.

-f matches the digest against every file the step recorded, inputs as well as outputs. A report copy kept in the working directory is an input of every later run, and its bytes can then satisfy the binding for the wrong record. So keep the copies outside the rehearsal repository, and delete out/ before each run so that each report is new output, as below.

1. Create the keys

Keep the keys, records and report copies in a directory outside the rehearsal repository. Run this from the repository:

mkdir -p ../recovery-evidence
openssl genpkey -algorithm ed25519 -out ../recovery-evidence/producer.pem
openssl pkey -in ../recovery-evidence/producer.pem -pubout \
  -out ../recovery-evidence/producer.pub.pem
openssl genpkey -algorithm ed25519 -out ../recovery-evidence/policy-signer.pem
openssl pkey -in ../recovery-evidence/policy-signer.pem -pubout \
  -out ../recovery-evidence/policy-signer.pub.pem

Expected: two key pairs. producer.pem signs rehearsal evidence. policy-signer.pem signs the policy and is the only key a verifier needs. In a real deployment these belong to different people, and neither file is committed.

2. Record both rehearsals with the producer key

In the rehearsal repository:

rm -rf out
cilock run --step recovery-rehearsal -k ../recovery-evidence/producer.pem \
  --platform-url "" -o ../recovery-evidence/rehearsal.json \
  -- ./scripts/restore-rehearsal.sh
cp out/restore-report.json ../recovery-evidence/rehearsal-report.json

rm -rf out
REHEARSAL_LATE_WRITES=37 cilock run --step recovery-rehearsal \
  -k ../recovery-evidence/producer.pem --platform-url "" \
  --ignore-command-exit-code -o ../recovery-evidence/gap.json \
  -- ./scripts/restore-rehearsal.sh
cp out/restore-report.json ../recovery-evidence/gap-report.json

Expected: two signed records and two copies of the report in ../recovery-evidence. Each run starts without a report, so the report it writes is new output, and the copies outside the repository are never an input of a later run. --platform-url "" keeps signing on the file key and uploads nothing. Run the remaining steps in that directory:

cd ../recovery-evidence

3. Write the policy

The rule denies any rehearsal whose command exited nonzero:

cat > recovery.rego <<'EOF'
package commandrun

import rego.v1

deny contains msg if {
  input.exitcode != 0
  msg := "the restore did not recover every row"
}
EOF

It trusts your rehearsal to detect missing data; it does not query the database itself. The policy attaches the rule to the command-run attestation and names the producer key as the only accepted signer of evidence:

KEYID=$(openssl dgst -sha256 -r producer.pub.pem | cut -d' ' -f1)
jq -n --arg id "$KEYID" \
  --arg key "$(base64 < producer.pub.pem | tr -d '\n')" \
  --arg rule "$(base64 < recovery.rego | tr -d '\n')" '{
  expires: "2030-12-31T00:00:00Z",
  steps: {"recovery-rehearsal": {
    name: "recovery-rehearsal",
    attestations: [
      {type: "https://aflock.ai/attestations/material/v0.3", regopolicies: []},
      {type: "https://aflock.ai/attestations/command-run/v0.2",
       regopolicies: [{name: "restore recovered every row", module: $rule}]},
      {type: "https://aflock.ai/attestations/product/v0.3", regopolicies: []}
    ],
    functionaries: [{type: "publickey", publickeyid: $id}]
  }},
  publickeys: {($id): {keyid: $id, key: $key}}
}' > recovery.json

Expected: recovery.json with one step, one rule and one functionary. Read it before signing. Set expires to the end of the period your team will accept this policy.

4. Sign the policy

cilock sign -f recovery.json -o recovery.signed.json \
  -t https://aflock.ai/policy/v0.1 -k policy-signer.pem --platform-url ""

Expected: a signed policy envelope. This does not publish the policy or activate it at a Pushgate repository. Those are separate operations with separate authorization.

5. Verify the successful rehearsal

cilock verify --offline -p recovery.signed.json -k policy-signer.pub.pem \
  -a rehearsal.json -f rehearsal-report.json --format json

Expected: exit status 0 and a verdict naming the report:

"passed": true,
"verdict": "passed",
"step": "recovery-rehearsal",
"matchedSubject": "sha256:…",
"observedSubjectName": "out/restore-report.json",

-f computes the SHA-256 of rehearsal-report.json. The record passes only because it lists those exact bytes as a product of the recovery-rehearsal step.

6. Verify the failed rehearsal

cilock verify --offline -p recovery.signed.json -k policy-signer.pub.pem \
  -a gap.json -f gap-report.json --format json

Expected: exit status 1, and stderr names the rule:

collection rejected: "recovery-rehearsal", Reason: "collection validation failed:
 - rego policy evaluation failed for attestor type https://aflock.ai/attestations/command-run/v0.2:
   policy was denied due to: the restore did not recover every row"

This is the only result that demonstrates the recovery rule. Keep stderr visible.

7. Tell the rule's denial from every other failure

Every failure below exits 1, and most print "verdict": "denied" on stdout. The JSON verdict does not say why. Read the reason on stderr before you conclude the recovery rule fired.

What went wrongExampleThe reason on stderr
The recovery rule denied the runStep 6rego policy evaluation failed … the restore did not recover every row
Wrong subject: the report is not this record's product-a rehearsal.json -f gap-report.jsonsupplied artifact digest(s) [sha256:…] not present in any subject of step "recovery-rehearsal" collection
Wrong producer: the record is signed by a key the policy does not nameA record made with another keycollection rejected … no valid signatures for the provided verifiers found for keyids
No policy key, embedded trust offOmit -k, add --no-embedded-trustmust supply a public key, CA certificates, a verifier, or a cilock built with embedded policy trust (no verdict is printed)
No policy key, or the wrong oneOmit -k, or pass -k producer.pub.pemfailed to verify policy signature: could not verify policy
The policy has expiredSign a copy with a past expiresfailed to verify policy: policy expired on …

The wrong-producer row holds even when the report bytes are identical. Binding to the report says which result is being judged. The functionary says who may vouch for it.

Using your platform's signer

When an authorized person signs the policy through your platform, two things change:

  • The evidence functionary. An enrolled agent or CI job signs with a certificate, not a file key. The functionary must constrain that certificate's identity; see the policy schema.
  • The policy trust. Replace -k policy-signer.pub.pem with the trust material and expected signer your administrator supplies through a trusted channel. Do not derive the expected identity from the evidence you are verifying. Replace both example values below with the ones your team authorized; do not assume every deployment uses this issuer path.
[email protected]
POLICY_ISSUER=https://platform.example/fulcio/oidc
cilock verify --offline -p recovery.signed.json \
  --policy-ca-roots platform-trust.pem \
  --policy-timestamp-servers platform-trust.pem \
  --policy-emails "$POLICY_SIGNER_EMAIL" \
  --policy-fulcio-oidc-issuer "$POLICY_ISSUER" \
  -a gap.json -f gap-report.json --format json

cilock policy draft --hydrate-local can fill a hand-written policy with your platform's roots before signing. Inspect the hydrated file before signing; do not blindly accept newly discovered roots. Keep -f and the report: the subject binding does not change with the signer.

Troubleshooting and next steps

If both results pass, read the signed policy and confirm the rule is attached to the command-run attestation. If both fail, find the reason in the table above before anything else. Keep the signed policy, both records and both reports for review. Offline verification of these files does not establish that every appliance feature works without network access.

Browse the other walkthroughs