Policies Polyglot: Evaluating Custom Predicates

Posted on March 20, 2024

Attestations are a wonderful way to attach metadata to container images in a secure manner. One of the most popular formats is SLSA Provenance which is used to provide information on how the image was created. Our Hitchhiker’s Guide demonstrates how to write policies to assert the contents of the SLSA Provenance. Here, we expand on that approach to assert the contents of any attestation format, even completely made up ones.

Before getting started, let’s make sure we have an image that is already signed and has a SLSA Provenance attestation. We will also need access to the signing key used. The Hitchhiker’s Guide walks through the process. If you want to try out the commands in this blog post, start there.

When we talk about different attestation formats, what we are really saying is different predicate types of an in-toto attestation. In addition to SLSA Provenance, there are a few other common types. As suggested earlier, in this blog post we will use a made up predicate type to both demystify predicates and highlight Enterprise Contract’s flexibility in validating attestations.

Let’s get this started!

First, we create a predicate.json file with arbitrary JSON data. This holds the contents of our custom predicate.

{"bacon_style": "crispy", "bacon_count": 6}

Second, we create a new attestation for our image using predicate.json as the predicate for our fabricated predicate type.

cosign attest --predicate predicate.json --type 'https://bacon/v42' --key cosign.key $REPOSITORY:latest

Next, we want to write a policy rule that asserts the contents of the “bacon” predicate associated with a container image. To do so, it helps to visualize how EC structures this data. The ec validate image command supports an output format for this specific purpose.

ec validate image --public-key cosign.pub --image $REPOSITORY:latest --output yaml \
  --output policy-input=input.json

Once the command finishes, the input.json file contains the complete input as seen by the policy rules. Use jq to inspect it.

$ jq '.attestations[1].statement.predicate' input.json
{
    "bacon_style": "crispy",
    "bacon_count": 6
}

The “crispy” value can be accessed via the input.attestations[1].statement.predicate.bacon_style variable from within a policy rule.

Things are getting exciting!

Create a new directory to hold the new policies we are about to write, e.g. mkdir policies. Then create the file bacon.rego inside that directory.

package bacon

import rego.v1

# METADATA
# title: Style
# description: Verify the Bacon attestation has the expected style.
# custom:
#   short_name: style
deny contains result if {
    some error in _errors
    result := {"code": "bacon.style", "msg": error}
}

_expected_style := "crispy"

_errors contains error if {
    count(_bacon_attestations) == 0
    error := "No bacon attestations found"
}

_errors contains error if {
    some attestation in _bacon_attestations
    not attestation.statement.predicate.bacon_style
    error := "Bacon attestation does not set the 'bacon_style' attribute"
}

_errors contains error if {
    some attestation in _bacon_attestations
    got := attestation.statement.predicate.bacon_style
    got != _expected_style
    error := sprintf("Bacon must be %q! Found %q bacon", [_expected_style, got])
}

_bacon_attestations := [attestation |
    some attestation in input.attestations
    attestation.statement.predicateType == "https://bacon/v42"
]

(Check out the docs for more information on how to author policy rules 🔥)

Next, we create a policy configuration, policy.yaml, to use the policy rules above.

---
sources:
  - policy:
      - /tmp/policies

Note: Change /tmp/policies to the absolute path to the directory where bacon.rego was saved.

Finally, let’s validate our image conforms to the policy config.

ec validate image --public-key cosign.pub --policy policy.yaml --image $REPOSITORY:latest \
  --output yaml --show-successes
components:
- attestations:
  - predicateBuildType: https://localhost/dummy-type
    predicateType: https://slsa.dev/provenance/v0.2
    signatures:
    - keyid: ""
      sig: MEUCIFMtiIiz0h9+zJpc5MfwavZ2/BIxuhIig5uoePcQ+nOHAiEAzSCKAOH5irMG1bG5HNkVzZLOyDOV3SiIIrU6YCTz668=
    type: https://in-toto.io/Statement/v0.1
  - predicateType: https://bacon/v42
    signatures:
    - keyid: ""
      sig: MEQCIHzmTK9YRU/PPfFjxRP6oSFNXyMIbAXEnQNP7GcCIjsbAiBVI8NtWYcvjg7/GmFC9Ce1e0XSh/mS5i5USHAX5I12tA==
    type: https://in-toto.io/Statement/v0.1
  containerImage: localhost:5000/ec-zero-to-hero@sha256:b5430d3d447434c795a508036e5046e41c009039be5b3f656f121c2426500d1e
  name: Unnamed
  signatures:
  - keyid: ""
    sig: MEQCIGIxpCboUYXF/fw6OuKmpM1Svi/0q+URD7oarLsji2+nAiBe3rgmWYOCa7sVpc2K5DKsef9hDigSlOHt6tl8v/8/JA==
  source: {}
  success: true
  successes:
  - metadata:
      code: builtin.attestation.signature_check
    msg: Pass
  - metadata:
      code: builtin.attestation.syntax_check
    msg: Pass
  - metadata:
      code: builtin.image.signature_check
    msg: Pass
  - metadata:
      code: bacon.style
    msg: Pass
ec-version: v0.3.2687-370a1a7
effective-time: "2024-03-20T18:58:24.532182311Z"
key: |
  -----BEGIN PUBLIC KEY-----
  MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaxhfip26cuIfpDEVII0ilOHvFkee
  igKbV5APr6OHYEstl9uJ8keON3VyzGfRB/2FUzr92J4ZN3YQUsTaiGc/HQ==
  -----END PUBLIC KEY-----
policy:
  publicKey: cosign.pub
  sources:
  - policy:
    - /tmp/policies
success: true

Success!

Change the value of the _expected_style variable in the bacon.rego file to any other value, e.g. "chunky". Run the exact EC command again to see what a violation looks like.

components:
- ...
  violations:
  - metadata:
      code: bacon.style
    msg: Bacon must be "chunky"! Found "crispy" bacon
...
success: false

As we can see the Enterprise Contract can be used to perform advanced checks on any kind of attestation 👌. What will you use it for? 🤔