ratify-project / ratify

Artifact Ratification Framework
https://ratify.dev
Apache License 2.0
225 stars 62 forks source link

Support in-toto as a provider to allow users to use an existing attestation framework #777

Open FeynmanZhou opened 1 year ago

FeynmanZhou commented 1 year ago

What would you like to be added?

in-toto provides a framework to protect the integrity of the software supply chain. It has an Attestation framework that provides a specification for generating verifiable claims about any aspect of how a piece of software is produced. Users can then validate the origins of the software before using it, and establish trust in its supply chain, using in-toto attestations.

The typical workflows of sign and verify attestation are as follows:

If Ratify has a plugin provider for in-toto, it will be able to extract the attestation predicate in a way that REGO policies can be validated easily. The extraction logic shouldn't be something the end user has to be concerned with. A well-defined in-toto framework/envelope for embedding the attestation predicate can help users easily validate the origins of the software.

We still need to investigate the use cases with in-toto and figure out the attestation verification scenarios with Ratify.

Anything else you would like to add?

No response

Are you willing to submit PRs to contribute to this feature?

akashsinghal commented 1 year ago

Some initial thoughts I had. Disclaimer: I'm very new to in-toto so my understanding is quite limited.

The envelope, which usually packages the output of a step defined in the security chain, contains a predicate. There is support for custom predicates but there are more common pre-defined predicates such as SBOM and other provenance standards like SLSA.

Like @FeynmanZhou mentions, the simplest integration would be to support a separate in-toto external verifier. From Ratify's perspective, I assume that the envelope will be the artifact verified instead of the individual predicate like we have support for now (SBOM, Licensechecker, etc.). This would allow the us to support other envelope types easily in the future too. If we go down this route, there are some design considerations we have to resolve:

  1. Will there be a separate in-toto verifier per predicate type? Ratify already has support for multiple "predicate" verifiers directly. However, since the artifact itself is the envelope, the existing verifiers will not work out of the box. Currently there's no way for a verifier to call another verifier which would mean that we could potentially have duplicate verifiers (apart from the envelope extraction logic in each).

  2. Does artifact type for the envelope needs to be dependent on the underlying predicate type? Ratify matches verifiers to artifacts by using the artifact's artifactType field. If we have multiple in-toto envelope artifacts with different predicates, we would need a predicate-specific artifact type to distinguish which verifier should be run. Note: There might be ways around this using an annotation on the artifact to hint to predicate type but we'd need to flush this out.

The other implementation approach would be to consider building envelope support built into ratify. Logically, this might make more sense since the envelope predicate aligns closest with a Ratify verifier. This approach, however, might require quite a bit of change. Curious what others thoughts are about this? cc: @binbin-li @susanshi

akashsinghal commented 1 year ago

There's recently been more interest in supporting this. I think we need to consider maybe a first class Attestation type to handle scenarios where attestation are used. Intoto is just one attestation type. Notation does not support intoto and may introduce their own in the future.

akashsinghal commented 1 year ago

@susanshi @FeynmanZhou should we use this issue for tracking attestation work or make a new one?

FeynmanZhou commented 11 months ago

@susanshi @FeynmanZhou should we use this issue for tracking attestation work or make a new one?

@dasiths Could you pls help confirm which attestation predicate type used in your scenario? If that is different from this issue, we can create a new one to track.

dasiths commented 11 months ago

@susanshi @FeynmanZhou should we use this issue for tracking attestation work or make a new one?

@dasiths Could you pls help confirm which attestation predicate type used in your scenario? If that is different from this issue, we can create a new one to track.

Yes this is the scenario we would like to cover. We use a mix of SLSA SPDX, SLSA Provenance and other custom predicates types in our own use cases. While the policies for known in-toto predicates can be pre defined, it would be useful to allow the end user to specify...

A thing to also consider is that there could be multiple attestations of the same type signed with the same key. So your policy needs to be smart about parsing all, any one or select one based on a condition.

Here is an example of a REGO policy that we use. This policy checks for a custom predicate type. It even reads a value from the environment. It only looks at the latest predicate from a list of all matching ones.

We use cosign to find the matching attestations that have been signed using a particular key, then collect all of those attestations to a json array before passing that array as the input to the REGO policy (using opa eval).

package policy_validation

default allow = false

default deny = false

default latest_match = null

default latest_ts = null

uat_type_param := opa.runtime().env.UAT_TYPE
uat_type_to_match := concat("", ["https://www.yourorg.com/public/ssc/spec/attestations/uat_check/v1/", uat_type_param])

matching_objects := [obj |
    obj := input[_]
    obj.predicateType == uat_type_to_match
]

matches_found {
    count(matching_objects) > 0
}

latest_ts := max([val | val := to_number(matching_objects[_].predicate.timestamp)])

latest_match := [obj | obj := matching_objects[_]; to_number(obj.predicate.timestamp) == latest_ts][0]

latest_is_allowed {
    latest_ts != null
    latest_match != null
    latest_match.predicate.result == "passed"
}

allow {
    matches_found
    latest_is_allowed
}

err[msg] {
    not matches_found
    msg := sprintf("No matching statement(s) were found: %v", [input])
}

err[msg] {
    matches_found
    not latest_is_allowed
    msg := sprintf("A matching statement was found but they did not pass the UAT check: %v", [input])
}

We can then subsequently look at the known allow, deny and error output shapes.

      if ! (opa eval -i "$extractedPayloadFile" -d "$policy_file" 'data.policy_validation' > "$validationResultFile"); then
          echo "[Error] Policy validation failed"
          cat "$validationResultFile"
          exit 1
      fi

      echo "Result from validating policy against attestation statement:"
      < "$validationResultFile" jq .

      allowed="$(< "$validationResultFile" jq -r '.result[0].expressions[0].value.allow')"
      echo "Allow=$allowed"

      denied="$(< "$validationResultFile" jq -r '.result[0].expressions[0].value.deny')"
      echo "Deny=$denied"

      errors="$(< "$validationResultFile" jq -r '.result[0].expressions[0].value.err')"