├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature-request.md │ └── bug-report.md └── workflows │ └── test.yml ├── README.md ├── policy ├── mapping.yaml ├── vsa │ ├── policy.rego │ └── policy_test.rego └── full │ ├── policy_test.rego │ └── policy.rego ├── NOTICE ├── template ├── bash.txt ├── makefile.txt ├── dockerfile.txt └── go.txt ├── test.sh ├── tar-scrubber.go ├── image-signer-verifier.sh ├── integration-test.sh ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE └── slsa.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @docker/supply-chain-security 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doi-image-policy 2 | 3 | This repository contains `policy.rego` for DOI. 4 | 5 | Run `test.sh` to run the tests. You must be logged in to the AWS sandbox for this to work. 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ### Tests 6 | 7 | 8 | 9 | ### Issue 10 | 11 | 12 | -------------------------------------------------------------------------------- /policy/mapping.yaml: -------------------------------------------------------------------------------- 1 | # This mapping file exists solely for integration tests. 2 | 3 | version: v1 4 | kind: policy-mapping 5 | policies: 6 | - id: docker-official-images 7 | files: 8 | - path: full/policy.rego 9 | - id: docker-official-images-vsa 10 | files: 11 | - path: vsa/policy.rego 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | **Describe the solution you'd like** 10 | [A clear and concise description of what you want to happen.] 11 | 12 | **Anything else you would like to add:** 13 | [Miscellaneous information that will assist in solving the issue.] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **What steps did you take and what happened:** 10 | [A clear and concise description of what the bug is.] 11 | 12 | **What did you expect to happen:** 13 | 14 | **Anything else you would like to add:** 15 | [Miscellaneous information that will assist in solving the issue.] 16 | 17 | **Environment:** 18 | 19 | - DOI image policy version: 20 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Docker DOI Image Policy 2 | Copyright Docker DOI Image Policy authors 3 | 4 | This product includes software developed at Docker, Inc. (https://www.docker.com). 5 | 6 | The following is courtesy of our legal counsel: 7 | 8 | Use and transfer of Docker may be subject to certain restrictions by the 9 | United States and other governments. 10 | It is your responsibility to ensure that your use and/or transfer does not 11 | violate applicable laws. 12 | 13 | For more information, please see https://www.bis.doc.gov 14 | 15 | See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. 16 | -------------------------------------------------------------------------------- /template/bash.txt: -------------------------------------------------------------------------------- 1 | # Copyright Docker DOI Image Policy authors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /template/makefile.txt: -------------------------------------------------------------------------------- 1 | # Copyright Docker DOI Image Policy authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /template/dockerfile.txt: -------------------------------------------------------------------------------- 1 | # Copyright Docker DOI Image Policy authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /template/go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Docker DOI Image Policy authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test policy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: 8 | jobs: 9 | test: 10 | permissions: 11 | contents: read 12 | id-token: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Authenticate to AWS 17 | uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 #v4.0.2 18 | with: 19 | aws-region: ${{ vars.AWS_REGION }} 20 | role-to-assume: ${{ vars.AWS_ROLE_TO_ASSUME }} 21 | - run: ./test.sh 22 | - run: ./integration-test.sh 23 | env: 24 | AWS_KMS_ARN: ${{ vars.AWS_KMS_ARN }} 25 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Docker DOI Image Policy authors 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | fails=0 19 | 20 | ./image-signer-verifier.sh test -d policy/full "$@" || ((fails++)) 21 | ./image-signer-verifier.sh test -d policy/vsa "$@" || ((fails++)) 22 | 23 | if [ $fails -gt 0 ]; then 24 | echo "Failed $fails tests" 25 | exit 1 26 | fi 27 | echo "All tests passed" 28 | -------------------------------------------------------------------------------- /tar-scrubber.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Docker DOI Image Policy authors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "archive/tar" 21 | "io" 22 | "os" 23 | ) 24 | 25 | func main() { 26 | in := tar.NewReader(os.Stdin) 27 | 28 | out := tar.NewWriter(os.Stdout) 29 | defer out.Flush() // note: flush instead of close to avoid the empty block at EOF 30 | 31 | for { 32 | hdr, err := in.Next() 33 | if err == io.EOF { 34 | break 35 | } 36 | if err != nil { 37 | panic(err) 38 | } 39 | hdr.Uname = "" 40 | hdr.Gname = "" 41 | if err := out.WriteHeader(hdr); err != nil { 42 | panic(err) 43 | } 44 | if _, err := io.Copy(out, in); err != nil { 45 | panic(err) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /image-signer-verifier.sh: -------------------------------------------------------------------------------- 1 | # Copyright Docker DOI Image Policy authors 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | isv_image="docker/image-signer-verifier:0.5.20@sha256:c3017e07df0b5c0f7f50c0f73fef866ce673a780c59c201f1b564f83b5d5fb93" 15 | #isv_image="isv:latest" 16 | 17 | mkdir -p $HOME/.local/tmp/sigstore 18 | 19 | docker run \ 20 | --rm \ 21 | -e AWS_PROFILE \ 22 | -e AWS_ACCESS_KEY_ID \ 23 | -e AWS_SECRET_ACCESS_KEY \ 24 | -e AWS_SESSION_TOKEN \ 25 | -e AWS_REGION \ 26 | -e AWS_CONFIG_FILE=/.aws/config \ 27 | -v $HOME/.local/tmp:/tmp \ 28 | -v $HOME/.local/tmp/sigstore:/.sigstore \ 29 | -v $HOME/.aws:/.aws:ro \ 30 | -v $PWD/policy:/policy \ 31 | -u $(id -u):$(id -g) \ 32 | --network host \ 33 | $isv_image \ 34 | "$@" 35 | -------------------------------------------------------------------------------- /integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Docker DOI Image Policy authors 4 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -eo pipefail 18 | 19 | # TODO: These tests currently sign unsigned DOI attestations using the staging DOI key. This public key is in the policies, so the verification passes. 20 | # Once there are DOI with attestations signed with production keys, the policies should be updated to use those keys, and we will no longer need to sign the attestations in this script. 21 | 22 | # Define functions 23 | function check_command () { 24 | command -v "$1" >/dev/null 2>&1 || { echo >&2 "This script requires $1 but it's not installed. Aborting."; exit 1; } 25 | } 26 | 27 | function login_to_aws () { 28 | if ! aws sts get-caller-identity > /dev/null 2>&1; then 29 | echo "SSO session has expired or is not valid. Logging in using AWS SSO." 30 | aws sso login 31 | else 32 | echo "SSO session is still valid." 33 | fi 34 | } 35 | 36 | function start_registry () { 37 | echo "Starting the registry..." 38 | docker run --rm -d -p 5000:5000 --name registry registry:2 39 | } 40 | 41 | function stop_registry () { 42 | echo "Stopping the registry..." 43 | docker stop registry 44 | } 45 | 46 | function sign_image () { 47 | echo "Signing the attestations on $INPUT_IMAGE and storing in $REFERRERS_REPO..." 48 | ./image-signer-verifier.sh sign -i "$INPUT_IMAGE" \ 49 | --referrers-dest "$REFERRERS_REPO" \ 50 | --kms-key-ref "$AWS_KMS_ARN" --kms-region "$AWS_REGION" 51 | } 52 | 53 | function verify_image () { 54 | echo "Verifying the attestations for $INPUT_IMAGE and storing a VSA in $REFERRERS_REPO..." 55 | ./image-signer-verifier.sh verify -i "$INPUT_IMAGE" \ 56 | --referrers-dest "$REFERRERS_REPO" \ 57 | --referrers-source "$REFERRERS_REPO" \ 58 | --vsa --kms-key-ref "$AWS_KMS_ARN" \ 59 | --kms-region "$AWS_REGION" --tuf=false --policy-dir "$POLICY_PATH" --platform "linux/amd64" \ 60 | --policy-id "$POLICY_ID" 61 | } 62 | 63 | function verify_image_vsa () { 64 | echo "Verifying the VSA for $INPUT_IMAGE..." 65 | ./image-signer-verifier.sh verify -i "$INPUT_IMAGE" \ 66 | --referrers-source "$REFERRERS_REPO" \ 67 | --tuf=false --policy-dir "$POLICY_PATH" --platform "linux/amd64" \ 68 | --policy-id "$VSA_POLICY_ID" 69 | } 70 | 71 | # Check required commands 72 | check_command aws 73 | check_command docker 74 | 75 | # Configuration 76 | if [ -z "$AWS_SESSION_TOKEN" ]; then 77 | export AWS_PROFILE=${AWS_PROFILE:-"sandbox"} 78 | export AWS_REGION=${AWS_REGION:-"us-east-1"} 79 | fi 80 | AWS_KMS_ARN=${AWS_KMS_ARN:-"arn:aws:kms:us-east-1:175142243308:alias/doi-signing"} 81 | 82 | TEST_IMAGE_REPO="nginx" 83 | TEST_IMAGE_TAG="1.27.0-alpine-slim" 84 | INPUT_IMAGE="docker://$TEST_IMAGE_REPO:$TEST_IMAGE_TAG" 85 | REFERRERS_REPO="docker://localhost:5000/$TEST_IMAGE_REPO" 86 | POLICY_PATH="policy" 87 | POLICY_ID="docker-official-images" 88 | VSA_POLICY_ID="docker-official-images-vsa" 89 | 90 | # Run steps 91 | login_to_aws 92 | 93 | start_registry 94 | trap stop_registry EXIT 95 | 96 | sign_image 97 | verify_image 98 | verify_image_vsa 99 | 100 | echo "Process completed successfully." 101 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported by submitting an [incident report](https://docs.google.com/forms/d/e/1FAIpQLScezna1ZXRPzC_phSDoPEF4c5nvw8yQW-vvtI8xHjv-BB9MOg/viewform?c=0&w=1). 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 125 | at [https://www.contributor-covenant.org/translations][translations]. 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 129 | [Mozilla CoC]: https://github.com/mozilla/diversity 130 | [FAQ]: https://www.contributor-covenant.org/faq 131 | [translations]: https://www.contributor-covenant.org/translations 132 | -------------------------------------------------------------------------------- /policy/vsa/policy.rego: -------------------------------------------------------------------------------- 1 | package attest 2 | 3 | import rego.v1 4 | 5 | split_digest := split(input.digest, ":") 6 | 7 | digest_type := split_digest[0] 8 | 9 | digest := split_digest[1] 10 | 11 | keys := [ 12 | { 13 | "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", 14 | "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----", 15 | "from": "2023-12-15T14:00:00Z", 16 | "to": null, 17 | "status": "active", 18 | "signing-format": "dssev1", 19 | }, 20 | { 21 | "id": "b281835e00059de24fb06bd6db06eb0e4a33d7bd7210d7027c209f14b19e812a", 22 | "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgE4Jz6FrLc3lp/YRlbuwOjK4n6ac\njVkSDAmFhi3Ir2Jy+cKeEB7iRPcLvBy9qoMZ9E93m1NdWY6KtDo+Qi52Rg==\n-----END PUBLIC KEY-----", 23 | "from": "2023-12-15T14:00:00Z", 24 | "to": null, 25 | "status": "active", 26 | "signing-format": "dssev1", 27 | }, 28 | ] 29 | 30 | verify_opts := {"keys": keys} 31 | 32 | verify_attestation(att) := attest.verify(att, verify_opts) 33 | 34 | attestations contains att if { 35 | result := attest.fetch("https://slsa.dev/verification_summary/v1") 36 | not result.error 37 | some att in result.value 38 | } 39 | 40 | signed_statements contains statement if { 41 | some att in attestations 42 | result := verify_attestation(att) 43 | not result.error 44 | statement := result.value 45 | } 46 | 47 | id(statement) := crypto.sha256(json.marshal(statement)) 48 | 49 | subjects contains subject if { 50 | some statement in signed_statements 51 | some subject in statement.subject 52 | } 53 | 54 | global_violations contains v if { 55 | count(attestations) == 0 56 | v := { 57 | "type": "missing_attestation", 58 | "description": "No https://slsa.dev/verification_summary/v1 attestation found", 59 | "attestation": null, 60 | "details": {}, 61 | } 62 | } 63 | 64 | # we need to key this by statement_id rather than statement because we can't 65 | # use an object as a key due to a bug(?) in OPA: https://github.com/open-policy-agent/opa/issues/6736 66 | statement_violations[statement_id] contains v if { 67 | some att in attestations 68 | result := verify_attestation(att) 69 | err := result.error 70 | statement := unsafe_statement_from_attestation(att) 71 | statement_id := id(statement) 72 | v := { 73 | "type": "unsigned_statement", 74 | "description": sprintf("Statement is not correctly signed: %v", [err]), 75 | "attestation": statement, 76 | "details": {"error": err}, 77 | } 78 | } 79 | 80 | statement_violations[statement_id] contains v if { 81 | some statement in signed_statements 82 | statement_id := id(statement) 83 | v := field_value_does_not_equal(statement, "verificationResult", "PASSED", "wrong_verification_result") 84 | } 85 | 86 | # TODO: add to statement_violations if there are statements that have an incorrect resource_uri 87 | # this should match the input.purl, but we really only care about the repo name and the digest 88 | # we need to receive the input.purl as a parsed object so we can compare only the parts we care about 89 | 90 | statement_violations[statement_id] contains v if { 91 | some statement in signed_statements 92 | statement_id := id(statement) 93 | v := field_value_does_not_equal(statement, "verifier.id", "docker-official-images", "wrong_verifier") 94 | } 95 | 96 | statement_violations[statement_id] contains v if { 97 | some statement in signed_statements 98 | statement_id := id(statement) 99 | v := field_value_does_not_equal(statement, "policy.uri", "https://docker.com/official/policy/v0.1", "wrong_policy_uri") 100 | } 101 | 102 | statement_violations[statement_id] contains v if { 103 | some statement in signed_statements 104 | statement_id := id(statement) 105 | v := array_field_does_not_contain(statement, "verifiedLevels", "SLSA_BUILD_LEVEL_3", "wrong_verified_levels") 106 | } 107 | 108 | bad_statements contains statement if { 109 | some statement in signed_statements 110 | statement_id := id(statement) 111 | statement_violations[statement_id] 112 | } 113 | 114 | good_statements := signed_statements - bad_statements 115 | 116 | all_violations contains v if { 117 | some v in global_violations 118 | } 119 | 120 | all_violations contains v if { 121 | some violations in statement_violations 122 | some v in violations 123 | } 124 | 125 | result := { 126 | "success": allow, 127 | "violations": all_violations, 128 | "summary": { 129 | "subjects": subjects, 130 | "slsa_levels": ["SLSA_BUILD_LEVEL_3"], 131 | "verifier": "docker-official-images", 132 | "policy_uri": "https://docker.com/official/policy/v0.1", 133 | }, 134 | } 135 | 136 | default allow := false 137 | 138 | allow if { 139 | count(good_statements) > 0 140 | } 141 | 142 | field_value_does_not_equal(statement, field, expected, type) := v if { 143 | path := split(field, ".") 144 | actual := object.get(statement.predicate, path, null) 145 | expected != actual 146 | v := is_not_violation(statement, field, expected, actual, type) 147 | } 148 | 149 | array_field_does_not_contain(statement, field, expected, type) := v if { 150 | path := split(field, ".") 151 | actual := object.get(statement.predicate, path, null) 152 | not expected in actual 153 | v := not_contains_violation(statement, field, expected, actual, type) 154 | } 155 | 156 | is_not_violation(statement, field, expected, actual, type) := { 157 | "type": type, 158 | "description": sprintf("%v is not %v", [field, expected]), 159 | "attestation": statement, 160 | "details": { 161 | "field": field, 162 | "actual": actual, 163 | "expected": expected, 164 | }, 165 | } 166 | 167 | not_contains_violation(statement, field, expected, actual, type) := { 168 | "type": type, 169 | "description": sprintf("%v does not contain %v", [field, expected]), 170 | "attestation": statement, 171 | "details": { 172 | "field": field, 173 | "actual": actual, 174 | "expected": expected, 175 | }, 176 | } 177 | 178 | # This is unsafe because we're not checking the signature on the attestation, 179 | # do not call this unless you've already verified the attestation or you need the 180 | # statement for some other reason 181 | unsafe_statement_from_attestation(att) := statement if { 182 | payload := att.payload 183 | statement := json.unmarshal(base64.decode(payload)) 184 | } 185 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to doi-image-policy 2 | 3 | This guide will help you to find out how to contribute. 4 | 5 | This page contains information about reporting issues as well as some tips and guidelines useful to experienced open source contributors. Finally, make sure you read our [community guidelines](#community-guidelines) before you start participating. 6 | 7 | ## Topics 8 | 9 | - [Contribute to doi-image-policy](#contribute-to-doi-image-policy) 10 | - [Topics](#topics) 11 | - [Reporting security issues](#reporting-security-issues) 12 | - [Reporting other issues](#reporting-other-issues) 13 | - [How to report a bug](#how-to-report-a-bug) 14 | - [Quick contribution tips and guidelines](#quick-contribution-tips-and-guidelines) 15 | - [Contribution flow](#contribution-flow) 16 | - [Format of the commit message](#format-of-the-commit-message) 17 | - [Code review process](#code-review-process) 18 | - [Tips for contributors](#tips-for-contributors) 19 | 20 | ## Reporting security issues 21 | 22 | The doi-image-policy maintainers take security seriously. If you discover a security issue, please bring it to their attention right away! 23 | 24 | Please **DO NOT** file a public issue, instead send your report privately to [security@docker.com](mailto:security@docker.com). 25 | 26 | Security reports are greatly appreciated and we will publicly thank you for it, although we keep your name confidential if you request it. We also like to send gifts—if you're into schwag, make sure to let us know. We currently do not offer a paid security bounty program, but are not ruling it out in the future. 27 | 28 | ## Reporting other issues 29 | 30 | A great way to contribute to the project is to send a detailed report when you encounter an issue. We always appreciate a well-written, thorough bug report, and will thank you for it! 31 | 32 | Check that [our issue database](https://github.com/docker/doi-image-policy/issues) doesn't already include that problem or suggestion before submitting an issue. If you find a match, you can use the "subscribe" button to get notified on updates. Do _not_ leave random "+1" or "I have this too" comments. Those comments can become annoying very quickly. Instead, use [GitHub reactions](https://docs.github.com/en/free-pro-team@latest/github/writing-on-github/using-emojis). 33 | 34 | ### How to report a bug 35 | 36 | - **Use a clear and descriptive title** for the issue to identify the problem. 37 | - **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. 38 | - **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). 39 | - **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 40 | - **Explain which behavior you expected to see instead and why.** 41 | - **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. 42 | - **If the problem is related to performance or memory**, include a [CPU profile capture](https://blog.golang.org/profiling-go-programs) with your report. 43 | - **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened. 44 | - **Include the version of doi-image-policy you are using**. 45 | - **Include the name and version of the OS you're using**. 46 | 47 | ## Quick contribution tips and guidelines 48 | 49 | This section gives a brief overview of how to propose a change to doi-image-policy. 50 | 51 | ### Contribution flow 52 | 53 | 1. Fork the repository on GitHub. 54 | 2. Create a topic branch from where you want to base your work. 55 | 3. Make commits of logical units. 56 | 4. Make sure your commit messages are in the proper format (see below). 57 | 5. Push your changes to a topic branch in your fork of the repository. 58 | 6. Submit a pull request to the original repository. 59 | 60 | ### Format of the commit message 61 | 62 | We follow a rough convention for commit messages [borrowed from Angular](https://www.conventionalcommits.org/en/v1.0.0/). 63 | 64 | - **feat**: A new feature 65 | - **fix**: A bug fix 66 | - **docs**: Documentation only changes 67 | - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 68 | - **refactor**: A code change that neither fixes a bug nor adds a feature 69 | - **perf**: A code change that improves performance 70 | - **test**: Adding missing or correcting existing tests 71 | - **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 72 | 73 | ### Code review process 74 | 75 | All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. 76 | 77 | ### Tips for contributors 78 | 79 | 1. All code should be formatted with `gofmt -s`. 80 | 2. All code should pass the default levels of [`golint`](https://github.com/golang/lint). 81 | 3. All code should follow the guidelines covered in [Effective Go](http://golang.org/doc/effective_go.html) and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). 82 | 4. Comment the code. Tell us the why, the history, and the context. 83 | 5. Document _all_ public declarations and methods. Declare expectations, caveats, and anything else that may be important. If a type gets exported, having the comments already there will ensure it's ready. 84 | 6. Variable name length should be proportional to its context and no longer. `noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`. In practice, short methods will have short variable names and globals will have longer names. 85 | 7. No underscores in package names. If you need a compound name, step back, and re-examine why you need a compound name. If you still think you need a compound name, lose the underscore. 86 | 8. No utils or helpers packages. If a function is not general enough to warrant its own package, it has not been written generally enough to be a part of a util package. Just leave it unexported and well-documented. 87 | 9. All tests should run with `go test` and outside tooling should not be required. No, we don't need another unit testing framework. 88 | 10. Even though we call these "rules" above, they are actually just guidelines. Since you've read all the rules, you now know that. 89 | 90 | If you are having trouble getting into the mood of idiomatic Go, we recommend reading through [Effective Go](https://go.dev/doc/effective_go). The [Go Blog](https://go.dev/blog/) is also a great resource. Drinking the kool-aid is a lot easier than going thirsty. 91 | -------------------------------------------------------------------------------- /policy/full/policy_test.rego: -------------------------------------------------------------------------------- 1 | package attest 2 | 3 | import rego.v1 4 | 5 | config := {"keys": []} 6 | 7 | purl := "pkg:docker/library/alpine:1.2.3" 8 | 9 | statement := {"subject": [{"name": purl, "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}}]} 10 | 11 | input_digest := "sha256:dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0" 12 | 13 | mock_verify_envelope({"name": "provenance_valid"}, k) := value_object({ 14 | "type": "https://in-toto.io/Statement/v0.1", 15 | "predicateType": "https://slsa.dev/provenance/v0.2", 16 | "subject": [{ 17 | "name": purl, 18 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 19 | }], 20 | "predicate": { 21 | "buildType": "https://mobyproject.org/buildkit@v1", 22 | "metadata": {"completeness": {"materials": true}}, 23 | }, 24 | }) 25 | 26 | mock_verify_envelope({"name": "provenance_wrong_predicate_type"}, k) := value_object({ 27 | "type": "https://in-toto.io/Statement/v0.1", 28 | "predicateType": "https://slsa.dev/provenance/v1", 29 | "subject": [{ 30 | "name": purl, 31 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 32 | }], 33 | "predicate": { 34 | "buildType": "https://mobyproject.org/buildkit@v1", 35 | "metadata": {"completeness": {"materials": true}}, 36 | }, 37 | }) 38 | 39 | mock_verify_envelope({"name": "provenance_wrong_build_type"}, k) := value_object({ 40 | "type": "https://in-toto.io/Statement/v0.1", 41 | "predicateType": "https://slsa.dev/provenance/v0.2", 42 | "subject": [{ 43 | "name": purl, 44 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 45 | }], 46 | "predicate": { 47 | "buildType": "some nonsense", 48 | "metadata": {"completeness": {"materials": true}}, 49 | }, 50 | }) 51 | 52 | mock_verify_envelope({"name": "provenance_incomplete_materials"}, k) := value_object({ 53 | "type": "https://in-toto.io/Statement/v0.1", 54 | "predicateType": "https://slsa.dev/provenance/v0.2", 55 | "subject": [{ 56 | "name": purl, 57 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 58 | }], 59 | "predicate": { 60 | "buildType": "https://mobyproject.org/buildkit@v1", 61 | "metadata": {"completeness": {"materials": false}}, 62 | }, 63 | }) 64 | 65 | mock_verify_envelope({"name": "sbom_valid"}, k) := value_object({ 66 | "type": "https://in-toto.io/Statement/v0.1", 67 | "predicateType": "https://spdx.dev/Document", 68 | "subject": [{ 69 | "name": purl, 70 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 71 | }], 72 | "predicate": {"SPDXID": "SPDXRef-DOCUMENT"}, 73 | }) 74 | 75 | mock_verify_envelope({"name": "sbom_wrong_spdxid"}, k) := value_object({ 76 | "type": "https://in-toto.io/Statement/v0.1", 77 | "predicateType": "https://spdx.dev/Document", 78 | "subject": [{ 79 | "name": purl, 80 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 81 | }], 82 | "predicate": {"SPDXID": "not the one"}, 83 | }) 84 | 85 | mock_verify_envelope({"name": "unsigned", "payload": _}, _) := error_object("signature is not valid") 86 | 87 | test_with_valid_provenance_and_sbom if { 88 | r := result with provenance_attestations as {{"name": "provenance_valid"}} 89 | with sbom_attestations as {{"name": "sbom_valid"}} 90 | with attest.verify as mock_verify_envelope 91 | with input.digest as input_digest 92 | with input.purl as purl 93 | with input.isCanonical as false 94 | 95 | r.success 96 | count(r.violations) == 0 97 | } 98 | 99 | test_with_provenance_with_wrong_predicate_type if { 100 | r := result with provenance_attestations as {{"name": "provenance_wrong_predicate_type"}} 101 | with sbom_attestations as {{"name": "sbom_valid"}} 102 | with attest.verify as mock_verify_envelope 103 | with input.digest as input_digest 104 | with input.purl as purl 105 | with input.isCanonical as false 106 | 107 | not r.success 108 | count(r.violations) == 1 109 | some v in r.violations 110 | print(yaml.marshal(v)) 111 | v.type == "wrong_predicate_type" 112 | v.description == "predicateType is not https://slsa.dev/provenance/v0.2" 113 | } 114 | 115 | test_with_provenance_with_wrong_build_type if { 116 | r := result with provenance_attestations as {{"name": "provenance_wrong_build_type"}} 117 | with sbom_attestations as {{"name": "sbom_valid"}} 118 | with attest.verify as mock_verify_envelope 119 | with input.digest as input_digest 120 | with input.purl as purl 121 | with input.isCanonical as false 122 | 123 | not r.success 124 | count(r.violations) == 1 125 | some v in r.violations 126 | v.type == "wrong_build_type" 127 | v.description == "buildType is not https://mobyproject.org/buildkit@v1" 128 | } 129 | 130 | test_with_provenance_with_incomplete_materials if { 131 | r := result with provenance_attestations as {{"name": "provenance_incomplete_materials"}} 132 | with sbom_attestations as {{"name": "sbom_valid"}} 133 | with attest.verify as mock_verify_envelope 134 | with input.digest as input_digest 135 | with input.purl as purl 136 | with input.isCanonical as false 137 | 138 | not r.success 139 | count(r.violations) == 1 140 | some v in r.violations 141 | v.type == "incomplete_materials" 142 | v.description == "metadata.completeness.materials is not true" 143 | } 144 | 145 | test_with_sbom_with_wrong_spdxid if { 146 | r := result with provenance_attestations as {{"name": "provenance_valid"}} 147 | with sbom_attestations as {{"name": "sbom_wrong_spdxid"}} 148 | with attest.verify as mock_verify_envelope 149 | with input.digest as input_digest 150 | with input.purl as purl 151 | with input.isCanonical as false 152 | 153 | not r.success 154 | count(r.violations) == 1 155 | some v in r.violations 156 | v.type == "wrong_spdx_id" 157 | v.description == "SPDXID is not SPDXRef-DOCUMENT" 158 | } 159 | 160 | test_with_valid_and_invalid_statements if { 161 | r := result with provenance_attestations as {{"name": "provenance_valid"}, {"name": "provenance_incomplete_materials"}} 162 | with sbom_attestations as {{"name": "sbom_valid"}, {"name": "sbom_wrong_spdxid"}} 163 | with attest.verify as mock_verify_envelope 164 | with input.digest as input_digest 165 | with input.purl as purl 166 | with input.isCanonical as false 167 | 168 | r.success 169 | count(r.violations) == 2 170 | } 171 | 172 | test_with_multiple_invalid_statements if { 173 | r := result with provenance_attestations as {{"name": "provenance_wrong_build_type"}, {"name": "provenance_incomplete_materials"}} 174 | with sbom_attestations as {{"name": "sbom_wrong_spdxid"}} 175 | with attest.verify as mock_verify_envelope 176 | with input.digest as input_digest 177 | with input.purl as purl 178 | with input.isCanonical as false 179 | 180 | not r.success 181 | count(r.violations) == 3 182 | } 183 | 184 | test_with_no_attestations if { 185 | r := result with attest.fetch as value_object(set()) 186 | with attest.verify as mock_verify_envelope 187 | with input.digest as input_digest 188 | with input.purl as purl 189 | with input.isCanonical as false 190 | 191 | not r.success 192 | count(r.violations) == 2 193 | 194 | some prov_v in r.violations 195 | prov_v.type == "missing_attestation" 196 | prov_v.description == "No https://slsa.dev/provenance/v0.2 attestation found" 197 | 198 | some sbom_v in r.violations 199 | sbom_v.type == "missing_attestation" 200 | sbom_v.description == "No https://spdx.dev/Document attestation found" 201 | } 202 | 203 | test_with_unsigned_attestation if { 204 | encoded_payload := base64.encode(json.marshal(statement)) 205 | r := result with attest.fetch as value_object({{"name": "unsigned", "payload": encoded_payload}}) 206 | with attest.verify as mock_verify_envelope 207 | with input.digest as input_digest 208 | with input.purl as purl 209 | with input.isCanonical as false 210 | 211 | not r.success 212 | count(r.violations) == 1 213 | some v in r.violations 214 | v.type == "unsigned_statement" 215 | v.description == "Statement is not correctly signed: signature is not valid" 216 | v.attestation == statement 217 | } 218 | 219 | layout_digest := "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620" 220 | 221 | outout_purl := "pkg:docker/test-image@test?platform=linux%2Famd64" 222 | 223 | value_object(x) := {"value": x} 224 | 225 | error_object(x) := {"error": x} 226 | -------------------------------------------------------------------------------- /policy/full/policy.rego: -------------------------------------------------------------------------------- 1 | package attest 2 | 3 | import rego.v1 4 | 5 | split_digest := split(input.digest, ":") 6 | 7 | digest_type := split_digest[0] 8 | 9 | digest := split_digest[1] 10 | 11 | keys := [ 12 | { 13 | "id": "a0c296026645799b2a297913878e81b0aefff2a0c301e97232f717e14402f3e4", 14 | "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgH23D1i2+ZIOtVjmfB7iFvX8AhVN\n9CPJ4ie9axw+WRHozGnRy99U2dRge3zueBBg2MweF0zrToXGig2v3YOrdw==\n-----END PUBLIC KEY-----", 15 | "from": "2023-12-15T14:00:00Z", 16 | "to": null, 17 | "status": "active", 18 | "signing-format": "dssev1", 19 | }, 20 | { 21 | "id": "b281835e00059de24fb06bd6db06eb0e4a33d7bd7210d7027c209f14b19e812a", 22 | "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgE4Jz6FrLc3lp/YRlbuwOjK4n6ac\njVkSDAmFhi3Ir2Jy+cKeEB7iRPcLvBy9qoMZ9E93m1NdWY6KtDo+Qi52Rg==\n-----END PUBLIC KEY-----", 23 | "from": "2023-12-15T14:00:00Z", 24 | "to": null, 25 | "status": "active", 26 | "signing-format": "dssev1", 27 | }, 28 | ] 29 | 30 | verify_opts := {"keys": keys} 31 | 32 | verify_attestation(att) := attest.verify(att, verify_opts) 33 | 34 | provenance_attestations contains att if { 35 | # TODO: this should take the media type as it doesn't actually check the predicate type 36 | result := attest.fetch("https://slsa.dev/provenance/v0.2") 37 | not result.error 38 | some att in result.value 39 | } 40 | 41 | provenance_signed_statements contains statement if { 42 | some att in provenance_attestations 43 | result := verify_attestation(att) 44 | not result.error 45 | statement := result.value 46 | } 47 | 48 | provenance_subjects contains subject if { 49 | some statement in provenance_signed_statements 50 | some subject in statement.subject 51 | } 52 | 53 | # we need to key this by statement_id rather than statement because we can't 54 | # use an object as a key due to a bug(?) in OPA: https://github.com/open-policy-agent/opa/issues/6736 55 | provenance_statement_violations[statement_id] contains v if { 56 | some att in provenance_attestations 57 | result := verify_attestation(att) 58 | err := result.error 59 | statement := unsafe_statement_from_attestation(att) 60 | statement_id := id(statement) 61 | v := { 62 | "type": "unsigned_statement", 63 | "description": sprintf("Statement is not correctly signed: %v", [err]), 64 | "attestation": statement, 65 | "details": {"error": err}, 66 | } 67 | } 68 | 69 | provenance_statement_violations[statement_id] contains v if { 70 | some statement in provenance_signed_statements 71 | statement_id := id(statement) 72 | statement.predicateType != "https://slsa.dev/provenance/v0.2" 73 | v := is_not_violation(statement, "predicateType", "https://slsa.dev/provenance/v0.2", statement.predicateType, "wrong_predicate_type") 74 | } 75 | 76 | provenance_statement_violations[statement_id] contains v if { 77 | some statement in provenance_signed_statements 78 | statement_id := id(statement) 79 | v := field_value_does_not_equal(statement, "buildType", "https://mobyproject.org/buildkit@v1", "wrong_build_type") 80 | } 81 | 82 | provenance_statement_violations[statement_id] contains v if { 83 | some statement in provenance_signed_statements 84 | statement_id := id(statement) 85 | v := field_value_does_not_equal(statement, "metadata.completeness.materials", true, "incomplete_materials") 86 | } 87 | 88 | bad_provenance_statements contains statement if { 89 | some statement in provenance_signed_statements 90 | statement_id := id(statement) 91 | provenance_statement_violations[statement_id] 92 | } 93 | 94 | good_provenance_statements := provenance_signed_statements - bad_provenance_statements 95 | 96 | sbom_attestations contains att if { 97 | result := attest.fetch("https://spdx.dev/Document") 98 | not result.error 99 | some att in result.value 100 | } 101 | 102 | sbom_signed_statements contains statement if { 103 | some att in sbom_attestations 104 | result := verify_attestation(att) 105 | not result.error 106 | statement := result.value 107 | } 108 | 109 | sbom_subjects contains subject if { 110 | some statement in sbom_signed_statements 111 | some subject in statement.subject 112 | } 113 | 114 | # we need to key this by statement_id rather than statement because we can't 115 | # use an object as a key due to a bug(?) in OPA: https://github.com/open-policy-agent/opa/issues/6736 116 | sbom_statement_violations[statement_id] contains v if { 117 | some att in sbom_attestations 118 | result := verify_attestation(att) 119 | err := result.error 120 | statement := unsafe_statement_from_attestation(att) 121 | statement_id := id(statement) 122 | v := { 123 | "type": "unsigned_statement", 124 | "description": sprintf("Statement is not correctly signed: %v", [err]), 125 | "attestation": statement, 126 | "details": {"error": err}, 127 | } 128 | } 129 | 130 | sbom_statement_violations[statement_id] contains v if { 131 | some statement in sbom_signed_statements 132 | statement_id := id(statement) 133 | statement.predicate_type != "https://spdx.dev/Document" 134 | v := is_not_violation(statement, "predicateType", "https://spdx.dev/Document", statement.predicate_type, "wrong_predicate_type") 135 | } 136 | 137 | sbom_statement_violations[statement_id] contains v if { 138 | some statement in sbom_signed_statements 139 | statement_id := id(statement) 140 | v := field_value_does_not_equal(statement, "SPDXID", "SPDXRef-DOCUMENT", "wrong_spdx_id") 141 | } 142 | 143 | bad_sbom_statements contains statement if { 144 | some statement in sbom_signed_statements 145 | statement_id := id(statement) 146 | sbom_statement_violations[statement_id] 147 | } 148 | 149 | good_sbom_statements := sbom_signed_statements - bad_sbom_statements 150 | 151 | global_violations contains v if { 152 | count(sbom_attestations) == 0 153 | v := { 154 | "type": "missing_attestation", 155 | "description": "No https://slsa.dev/provenance/v0.2 attestation found", 156 | "attestation": null, 157 | "details": {}, 158 | } 159 | } 160 | 161 | global_violations contains v if { 162 | count(provenance_attestations) == 0 163 | v := { 164 | "type": "missing_attestation", 165 | "description": "No https://spdx.dev/Document attestation found", 166 | "attestation": null, 167 | "details": {}, 168 | } 169 | } 170 | 171 | all_violations contains v if { 172 | some v in global_violations 173 | } 174 | 175 | all_violations contains v if { 176 | some violations in sbom_statement_violations 177 | some v in violations 178 | } 179 | 180 | all_violations contains v if { 181 | some violations in provenance_statement_violations 182 | some v in violations 183 | } 184 | 185 | subjects := union({sbom_subjects, provenance_subjects}) 186 | 187 | result := { 188 | "success": allow, 189 | "violations": all_violations, 190 | "summary": { 191 | "subjects": subjects, 192 | "slsa_levels": ["SLSA_BUILD_LEVEL_3"], 193 | "verifier": "docker-official-images", 194 | "policy_uri": "https://docker.com/official/policy/v0.1", 195 | }, 196 | } 197 | 198 | default allow := false 199 | 200 | allow if { 201 | count(good_sbom_statements) > 0 202 | count(good_provenance_statements) > 0 203 | } 204 | 205 | id(statement) := crypto.sha256(json.marshal(statement)) 206 | 207 | field_value_does_not_equal(statement, field, expected, type) := v if { 208 | path := split(field, ".") 209 | actual := object.get(statement.predicate, path, null) 210 | expected != actual 211 | v := is_not_violation(statement, field, expected, actual, type) 212 | } 213 | 214 | array_field_does_not_contain(statement, field, expected, type) := v if { 215 | path := split(field, ".") 216 | actual := object.get(statement.predicate, path, null) 217 | not expected in actual 218 | v := not_contains_violation(statement, field, expected, actual, type) 219 | } 220 | 221 | is_not_violation(statement, field, expected, actual, type) := { 222 | "type": type, 223 | "description": sprintf("%v is not %v", [field, expected]), 224 | "attestation": statement, 225 | "details": { 226 | "field": field, 227 | "actual": actual, 228 | "expected": expected, 229 | }, 230 | } 231 | 232 | not_contains_violation(statement, field, expected, actual, type) := { 233 | "type": type, 234 | "description": sprintf("%v does not contain %v", [field, expected]), 235 | "attestation": statement, 236 | "details": { 237 | "field": field, 238 | "actual": actual, 239 | "expected": expected, 240 | }, 241 | } 242 | 243 | # This is unsafe because we're not checking the signature on the attestation, 244 | # do not call this unless you've already verified the attestation or you need the 245 | # statement for some other reason 246 | unsafe_statement_from_attestation(att) := statement if { 247 | payload := att.payload 248 | statement := json.unmarshal(base64.decode(payload)) 249 | } 250 | -------------------------------------------------------------------------------- /policy/vsa/policy_test.rego: -------------------------------------------------------------------------------- 1 | package attest 2 | 3 | import rego.v1 4 | 5 | config := {"keys": []} 6 | 7 | purl := "pkg:docker/library/alpine:1.2.3" 8 | 9 | statement := {"subject": [{"name": purl, "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}}]} 10 | 11 | input_digest := "sha256:dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0" 12 | 13 | mock_verify_envelope({"name": "valid"}, k) := value_object({ 14 | "type": "https://in-toto.io/Statement/v0.1", 15 | "predicateType": "https://slsa.dev/verification_summary/v1", 16 | "subject": [{ 17 | "name": purl, 18 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 19 | }], 20 | "predicate": { 21 | "policy": {"uri": "https://docker.com/official/policy/v0.1"}, 22 | "resourceUri": "pkg:docker/test-image@test?digest=sha256%3A7c43c2a4affcff17f3d756058e335fcde7249aa7014047251b5fe512b6bff213&platform=linux%2Famd64", 23 | "timeVerified": "2024-05-24T12:44:03Z", 24 | "verificationResult": "PASSED", 25 | "verifiedLevels": ["SLSA_BUILD_LEVEL_3"], 26 | "verifier": {"id": "docker-official-images"}, 27 | }, 28 | }) 29 | 30 | mock_verify_envelope({"name": "wrong_verification_result"}, k) := value_object({ 31 | "type": "https://in-toto.io/Statement/v0.1", 32 | "predicateType": "https://slsa.dev/verification_summary/v1", 33 | "subject": [{ 34 | "name": purl, 35 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 36 | }], 37 | "predicate": { 38 | "policy": {"uri": "https://docker.com/official/policy/v0.1"}, 39 | "resourceUri": "pkg:docker/test-image@test?digest=sha256%3A7c43c2a4affcff17f3d756058e335fcde7249aa7014047251b5fe512b6bff213&platform=linux%2Famd64", 40 | "timeVerified": "2024-05-24T12:44:03Z", 41 | "verificationResult": "FAILED", 42 | "verifiedLevels": ["SLSA_BUILD_LEVEL_3"], 43 | "verifier": {"id": "docker-official-images"}, 44 | }, 45 | }) 46 | 47 | mock_verify_envelope({"name": "wrong_verifier"}, k) := value_object({ 48 | "type": "https://in-toto.io/Statement/v0.1", 49 | "predicateType": "https://slsa.dev/verification_summary/v1", 50 | "subject": [{ 51 | "name": purl, 52 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 53 | }], 54 | "predicate": { 55 | "policy": {"uri": "https://docker.com/official/policy/v0.1"}, 56 | "resourceUri": "pkg:docker/test-image@test?digest=sha256%3A7c43c2a4affcff17f3d756058e335fcde7249aa7014047251b5fe512b6bff213&platform=linux%2Famd64", 57 | "timeVerified": "2024-05-24T12:44:03Z", 58 | "verificationResult": "PASSED", 59 | "verifiedLevels": ["SLSA_BUILD_LEVEL_3"], 60 | "verifier": {"id": "wrong-verifier"}, 61 | }, 62 | }) 63 | 64 | mock_verify_envelope({"name": "wrong_policy_uri"}, k) := value_object({ 65 | "type": "https://in-toto.io/Statement/v0.1", 66 | "predicateType": "https://slsa.dev/verification_summary/v1", 67 | "subject": [{ 68 | "name": purl, 69 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 70 | }], 71 | "predicate": { 72 | "policy": {"uri": "https://fakedocker.com/official/policy/v0.1"}, 73 | "resourceUri": "pkg:docker/test-image@test?digest=sha256%3A7c43c2a4affcff17f3d756058e335fcde7249aa7014047251b5fe512b6bff213&platform=linux%2Famd64", 74 | "timeVerified": "2024-05-24T12:44:03Z", 75 | "verificationResult": "PASSED", 76 | "verifiedLevels": ["SLSA_BUILD_LEVEL_3"], 77 | "verifier": {"id": "docker-official-images"}, 78 | }, 79 | }) 80 | 81 | mock_verify_envelope({"name": "wrong_verified_levels"}, k) := value_object({ 82 | "type": "https://in-toto.io/Statement/v0.1", 83 | "predicateType": "https://slsa.dev/verification_summary/v1", 84 | "subject": [{ 85 | "name": purl, 86 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 87 | }], 88 | "predicate": { 89 | "policy": {"uri": "https://docker.com/official/policy/v0.1"}, 90 | "resourceUri": "pkg:docker/test-image@test?digest=sha256%3A7c43c2a4affcff17f3d756058e335fcde7249aa7014047251b5fe512b6bff213&platform=linux%2Famd64", 91 | "timeVerified": "2024-05-24T12:44:03Z", 92 | "verificationResult": "PASSED", 93 | "verifiedLevels": ["SLSA_BUILD_LEVEL_2"], 94 | "verifier": {"id": "docker-official-images"}, 95 | }, 96 | }) 97 | 98 | mock_verify_envelope({"name": "no_verified_level"}, k) := value_object({ 99 | "type": "https://in-toto.io/Statement/v0.1", 100 | "predicateType": "https://slsa.dev/verification_summary/v1", 101 | "subject": [{ 102 | "name": purl, 103 | "digest": {"sha256": "dea014f47cd49d694d3a68564eb9e6ae38a7ee9624fd52ec05ccbef3f3fab8a0"}, 104 | }], 105 | "predicate": { 106 | "policy": {"uri": "https://docker.com/official/policy/v0.1"}, 107 | "resourceUri": "pkg:docker/test-image@test?digest=sha256%3A7c43c2a4affcff17f3d756058e335fcde7249aa7014047251b5fe512b6bff213&platform=linux%2Famd64", 108 | "timeVerified": "2024-05-24T12:44:03Z", 109 | "verificationResult": "PASSED", 110 | "verifiedLevels": [], 111 | "verifier": {"id": "docker-official-images"}, 112 | }, 113 | }) 114 | 115 | mock_verify_envelope({"name": "unsigned", "payload": _}, _) := error_object("signature is not valid") 116 | 117 | test_with_valid_statement_only if { 118 | r := result with attest.fetch as value_object({{"name": "valid"}}) 119 | with attest.verify as mock_verify_envelope 120 | with input.digest as input_digest 121 | with input.purl as purl 122 | with input.isCanonical as false 123 | 124 | r.success 125 | count(r.violations) == 0 126 | } 127 | 128 | test_with_wrong_verification_result if { 129 | r := result with attest.fetch as value_object({{"name": "wrong_verification_result"}}) 130 | with attest.verify as mock_verify_envelope 131 | with input.digest as input_digest 132 | with input.purl as purl 133 | with input.isCanonical as false 134 | 135 | not r.success 136 | count(r.violations) == 1 137 | some v in r.violations 138 | v.type == "wrong_verification_result" 139 | v.description == "verificationResult is not PASSED" 140 | } 141 | 142 | test_with_wrong_verifier if { 143 | r := result with attest.fetch as value_object({{"name": "wrong_verifier"}}) 144 | with attest.verify as mock_verify_envelope 145 | with input.digest as input_digest 146 | with input.purl as purl 147 | with input.isCanonical as false 148 | 149 | not r.success 150 | count(r.violations) == 1 151 | some v in r.violations 152 | v.type == "wrong_verifier" 153 | v.description == "verifier.id is not docker-official-images" 154 | } 155 | 156 | test_with_wrong_policy_uri if { 157 | r := result with attest.fetch as value_object({{"name": "wrong_policy_uri"}}) 158 | with attest.verify as mock_verify_envelope 159 | with input.digest as input_digest 160 | with input.purl as purl 161 | with input.isCanonical as false 162 | 163 | not r.success 164 | count(r.violations) == 1 165 | some v in r.violations 166 | v.type == "wrong_policy_uri" 167 | v.description == "policy.uri is not https://docker.com/official/policy/v0.1" 168 | } 169 | 170 | test_with_wrong_verified_level if { 171 | r := result with attest.fetch as value_object({{"name": "wrong_verified_levels"}}) 172 | with attest.verify as mock_verify_envelope 173 | with input.digest as input_digest 174 | with input.purl as purl 175 | with input.isCanonical as false 176 | 177 | not r.success 178 | count(r.violations) == 1 179 | some v in r.violations 180 | v.type == "wrong_verified_levels" 181 | v.description == "verifiedLevels does not contain SLSA_BUILD_LEVEL_3" 182 | } 183 | 184 | test_with_no_verified_level if { 185 | r := result with attest.fetch as value_object({{"name": "no_verified_level"}}) 186 | with attest.verify as mock_verify_envelope 187 | with input.digest as input_digest 188 | with input.purl as purl 189 | with input.isCanonical as false 190 | 191 | not r.success 192 | count(r.violations) == 1 193 | some v in r.violations 194 | v.type == "wrong_verified_levels" 195 | v.description == "verifiedLevels does not contain SLSA_BUILD_LEVEL_3" 196 | } 197 | 198 | test_with_valid_and_invalid_statements if { 199 | r := result with attest.fetch as value_object({{"name": "valid"}, {"name": "wrong_verification_result"}}) 200 | with attest.verify as mock_verify_envelope 201 | with input.digest as input_digest 202 | with input.purl as purl 203 | with input.isCanonical as false 204 | 205 | r.success 206 | count(r.violations) == 1 207 | } 208 | 209 | test_with_multiple_invalid_statements if { 210 | r := result with attest.fetch as value_object({{"name": "wrong_verification_result"}, {"name": "wrong_verifier"}}) 211 | with attest.verify as mock_verify_envelope 212 | with input.digest as input_digest 213 | with input.purl as purl 214 | with input.isCanonical as false 215 | 216 | not r.success 217 | count(r.violations) == 2 218 | } 219 | 220 | test_with_no_attestations if { 221 | r := result with attest.fetch as value_object(set()) 222 | with attest.verify as mock_verify_envelope 223 | with input.digest as input_digest 224 | with input.purl as purl 225 | with input.isCanonical as false 226 | 227 | not r.success 228 | count(r.violations) == 1 229 | some v in r.violations 230 | v.type == "missing_attestation" 231 | v.description == "No https://slsa.dev/verification_summary/v1 attestation found" 232 | } 233 | 234 | test_with_unsigned_attestation if { 235 | encoded_payload := base64.encode(json.marshal(statement)) 236 | r := result with attest.fetch as value_object({{"name": "unsigned", "payload": encoded_payload}}) 237 | with attest.verify as mock_verify_envelope 238 | with input.digest as input_digest 239 | with input.purl as purl 240 | with input.isCanonical as false 241 | 242 | not r.success 243 | count(r.violations) == 1 244 | some v in r.violations 245 | v.type == "unsigned_statement" 246 | v.description == "Statement is not correctly signed: signature is not valid" 247 | v.attestation == statement 248 | } 249 | 250 | layout_digest := "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620" 251 | 252 | outout_purl := "pkg:docker/test-image@test?platform=linux%2Famd64" 253 | 254 | value_object(x) := {"value": x} 255 | 256 | error_object(x) := {"error": x} 257 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /slsa.md: -------------------------------------------------------------------------------- 1 | # DOI GitHub Actions SLSA Builder Evaluation 2 | 3 | This is a self-evaluation of the [SLSA v1.0](https://slsa.dev/spec/v1.0/) build level for Docker Official Images (DOI) GitHub Actions (GHA) Workflow trusted builder. 4 | 5 | | Builder ID | `https://github.com/docker-library/meta/.github/workflows/build.yml@refs/heads/main` | 6 | | ---------- | ------------------------------------------------------------------------------------ | 7 | 8 | ## Applicability 9 | 10 | A subset of DOI builds run on the [meta](https://github.com/docker-library/meta) GHA [workflow](https://github.com/docker-library/meta/blob/HEAD/.github/workflows/build.yml). 11 | 12 | Applicable builds are limited to the following image platforms: 13 | 14 | - `linux/amd64` 15 | - `linux/386` 16 | 17 | The list of all applicable images can be found in [subset.txt](https://github.com/docker-library/meta/blob/HEAD/subset.txt). 18 | 19 | ## Build Level Assertion 20 | 21 | The DOI build platform can be trusted to produce Build Level 3 artifacts on images that are built by the `meta` GHA workflow due to the strengthened unforgeability and isolation controls for provenance generation as detailed in the following sections. 22 | 23 | ## Build Model 24 | 25 | DOI GHA workflow builder is modeled around the [GHA workflow build type](https://actions.github.io/buildtypes/workflow/v1). 26 | 27 | The DOI build platform in its entirety extends beyond the GHA workflow, but we choose to limit the trusted boundary for this analysis to the DOI GHA workflow. 28 | In this model, the GHA workflow itself is the trusted control plane and all other platform automation outside it are considered untrusted external parameters and dependencies that are "to be verified" according to a set of expectations that we define as the source of truth for DOI builds. 29 | 30 | ### Build Platform Components 31 | 32 | Build platform components are defined according to the [SLSA v1.0 spec terminology](https://slsa.dev/spec/v1.0/verifying-systems#build-platform-components) below. 33 | 34 | #### Control Plane 35 | 36 | The control plane for the DOI GHA workflow builder is the [meta GHA workflow file](https://github.com/docker-library/meta/blob/HEAD/.github/workflows/build.yml). 37 | 38 | This GHA workflow orchestrates each build stage and is operated by administrators that have privilege to modify the control plane. 39 | The workflow is made available to the public for transparency and is version controlled in git with branch protection controls requiring two person review by maintainers of the DOI build platform. 40 | 41 | ##### Isolation 42 | 43 | Provenance generation and signing is sufficiently isolated from the build environment through the use of separate GitHub-hosted runner VMs via [jobs](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/using-jobs-in-a-workflow). 44 | 45 | ##### Unforgeability 46 | 47 | Access to signing operations is provided by [OIDC](https://openid.net/) authentication of the control plane (GHA workflow) to KMS managed keys that store key material in a Hardware Security Module (HSM). 48 | Only trusted jobs in the GHA workflow are provided access to the ID Token to obtain tokens for KMS signing. 49 | Specifically, the job that handles the build environment is NOT granted permission to request ID Tokens on behalf of the workflow. 50 | This provides sufficient security controls to ensure unforgeable provenance generation. 51 | 52 | Use of the KMS is audited and alerts are in place for unauthorized access by entities outside of the DOI GHA workflow identity. 53 | 54 | #### Cache 55 | 56 | Build artifacts are passed between jobs in the workflow using a combination of [workflow artifacts](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/storing-and-sharing-data-from-a-workflow) and [job outputs](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/passing-information-between-jobs). 57 | 58 | Job outputs are treated as a trusted channel to distribute the cryptographic digest of artifacts so that each job can verify the integrity of build artifacts cached outside of the trusted builder boundary in workflow artifact storage. 59 | 60 | #### External Parameters 61 | 62 | In addition to what is defined by the [GHA build type](https://actions.github.io/buildtypes/workflow/v1), all workflow inputs are considered external parameters for this build model. 63 | 64 | These inputs are verified according to a set of expectations about them for authentic DOI builds. 65 | See [#verifying-expectations-for-doi-builds](#verifying-expectations-for-doi-builds). 66 | 67 | #### Build Environment 68 | 69 | The build environment is a `job` within the DOI GHA workflow that is named `build`. 70 | The build job is isolated from the control plane and other jobs within the workflow with the use of GitHub-hosted runner virtual machines. 71 | 72 | The build environment cannot influence provenance generation because provenance is generated by the control plane in a separate job. 73 | 74 | Access to key material and signing operations is not permitted for the build environment through the use of [job permissions](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token#setting-the-github_token-permissions-for-a-specific-job). 75 | The `build` job is not granted `id-token` permissions, therefore it cannot generate a workload identity token to authenticate with the signing KMS. 76 | 77 | #### Resolved Dependencies 78 | 79 | The minimum required resolved dependency for a DOI GHA workflow is the resolved git commit URI for the workflow run on the [meta](https://github.com/docker-library/meta) repository. 80 | 81 | The [official-images](https://github.com/docker-library/official-images) and [meta-scripts](https://github.com/docker-library/meta-scripts) repositories are dependencies for DOI builds and are [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in the meta repository. 82 | Therefore, the commit references for those repositories are obtained by the URI for the meta repository git commit. 83 | 84 | #### Outputs 85 | 86 | Artifacts are output from the build system as OCI images. 87 | Images are stored in an OCI registry as build "outputs" that are later collected by a separate process that deploys them to the image architecture and library namespaces. 88 | 89 | The provenance attestation generated by the trusted builder captures the cryptographic digest of the image as a subject of the predicate, such that if the image was modified after being output, the verification of the provenance attestation would fail. 90 | 91 | ## Verifying Expectations for DOI Builds 92 | 93 | To verify that the provenance of a DOI build is authentic, expectations for a DOI build must be defined and checked. 94 | 95 | Expectations for provenance verification are defined by source. 96 | 97 | The source of truth for DOI builds is defined by the official-images [library manifest files](https://github.com/docker-library/official-images/tree/HEAD/library). 98 | Library manifest files are managed by image maintainers and approved by platform administrators. 99 | Contents of the manifest file define a set of inputs for each Official Image build (e.g. source repository, explicit full git commit hash, Dockerfile path, etc.). 100 | 101 | The combination of expectations from library manifest files and GHA workflow context can be used to verify an image's provenance to determine if the artifact is genuine. 102 | 103 | ### Steps to verify DOI provenance 104 | 105 | > :bulb: **Tip:** These generic steps are enumerated for informational purposes, see [#verification-summary-attestations](#verification-summary-attestations) on how to verify a precomputed summary of provenance verification 106 | 107 | 1. Check SLSA build level 108 | 109 | 1. Verify the signature of the provenance attestation [DSSE envelope](https://github.com/secure-systems-lab/dsse) using the trust store published in [Docker's TUF root](https://github.com/docker/tuf) 110 | 2. Verify the SLSA build level that maps to the `builder.id` and public key used to verify the envelope signature is at least `SLSA_BUILD_LEVEL_3` in the trust store 111 | 3. Verify the statement's `subject` matches the digest and [PURL](https://github.com/package-url/purl-spec) of the target image 112 | 4. Verify the `predicateType` is `https://slsa.dev/provenance/v1` 113 | 114 | 2. Check expectations 115 | 1. Verify the statement's `subject` platform is one of the [applicable image platforms](#applicability) for the `builder.id` 116 | 2. Verify that the `buildType` is `https://github.com/actions/buildtypes/tree/main/workflow/v1` 117 | 3. Verify the external parameters 118 | > :memo: **Note:** We can ignore all workflow inputs except for the `buildId` input (this is the metadata used to invoke the build) 119 | 1. Using the meta repository resolved dependency commit ref 120 | 1. download the build metadata (`builds.json`) at the repository root 121 | 2. filter metadata for specific `buildId` 122 | 3. extract the `source.entry` data for verification 123 | 2. Lookup the official-images submodule commit ref from the meta repository resolved dependency 124 | 1. Map the statement `subject` package name (e.g. `pkg:docker/hello-world`) to the library manifest file (e.g. [docker-library/official-images/library/hello-world](https://github.com/docker-library/official-images/blob/HEAD/library/hello-world)) 125 | 2. Compare the contents of the library manifest file (source of truth) to the contents of the build metadata (obtained from `builds.json`) 126 | 1. Verify that `GitRepo`, `GitCommit`, `GitFetch`, `Directory`, and `File` for the build match what is expected from the library manifest file 127 | > :warning: **Warning:** These values are often unique to the specific platform and tag of the image build and must be appropriately parsed and selected from the manifest 128 | 2. If `GitCommit` fails, calculate the [reproducibleGitChecksum](#doi-build-reproducible-git-checksum) and verify it against the value in the build metadata 129 | > :memo: **Note:** `reproducibleGitChecksum` represents the build context by cryptographic digest of the source repository's Dockerfile path contents. In this case, the build is considered valid because the build context is the same for different `GitCommit` values. 130 | 3. Verify all tags for subject references in the statement match expected tags for the artifact as defined in the library manifest file 131 | 132 | #### DOI build reproducible git checksum 133 | 134 | The source repository build context for each build can be identified by a digest of the git archive contents at the specified `GitRepo`, `GitCommit`, `GitFetch`, and `Directory` for a DOI build. This value is known as the `reproducibleGitChecksum`. 135 | 136 | To calculate the value of `reproducibleGitChecksum`: 137 | 138 | Prerequisites: 139 | 140 | 1. `git` version >= [2.40.0](https://git-scm.com/docs/git-archive/2.40.0) 141 | 1. tar scrubber to strip `uname` and `gname` headers (e.g. [tar-scrubber.go](./tar-scrubber.go)) 142 | 143 | Steps: 144 | 145 | 1. Clone the source repository at `GitRepo` from the library manifest file 146 | 2. Run `git archive` and scrub the tar output, using `GitCommit` and `Directory` from the library manifest file 147 | ```console 148 | git archive --format=tar --mtime='1970-01-01 00:00:00Z' : | go run tar-scrubber.go | sha256sum 149 | ``` 150 | 151 | Example: 152 | 153 | ```console 154 | git clone https://github.com/docker-library/hello-world.git 155 | ``` 156 | 157 | ```console 158 | git archive --format=tar --mtime='1970-01-01 00:00:00Z' 3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:amd64/hello-world | go run tar-scrubber.go | sha256sum 159 | 22266b0a36deee72428cffd00859ce991f1db101260999c40904ace7d634b788 160 | ``` 161 | 162 | Note that the output digest `22266b0a36deee72428cffd00859ce991f1db101260999c40904ace7d634b788` matches the value in [builds.json](https://github.com/docker-library/meta/blob/5171fea58d50da92eb4c3af3482a573a22197688/builds.json#L16977) 163 | 164 | ### Verification Summary Attestations 165 | 166 | The DOI build process generates [Verification Summary Attestations (VSA)](https://slsa.dev/spec/v1.0/verification_summary) to communicate that the image has been verified at the build level obtained by the builder that generated the artifact. 167 | 168 | The VSA generation process verifies expectations for DOI build provenance as outlined in [#steps-to-verify-doi-provenance](#steps-to-verify-doi-provenance) by use of attestation verification tooling and Rego policy stored in [Docker's TUF root](https://github.com/docker/tuf). 169 | 170 | Consumers of DOI need not implement the extensive verification process for provenance and instead can verify the VSA as a minimal summary of the verification process as performed by Docker. 171 | 172 | ## Build Level Requirements 173 | 174 | The table below describes how Docker Official Images (DOI) meet the [SLSA Build L3 (v1.0)](https://slsa.dev/spec/v1.0/) (Supply-chain Levels for Software Artifacts Build Level 3 spec version 1.0) requirements. 175 | 176 | | Level | Requirement | Implementation | 177 | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 178 | | Build L1: Provenance exists | Software producer follows a consistent build process so that others can form expectations about what a “correct” build looks like. | The whole [DOI build process](https://github.com/docker-library/official-images) is open, and there are strict rules that must be followed to be considered part of DOI. For example, dependencies should be pinned and digests checked when package managers are not used. For more details, see the docs on [creating a new DOI](https://github.com/docker-library/official-images#contributing-to-the-standard-library) | 179 | | | Provenance exists describing how the artifact was built, including the build platform, build process, and top-level inputs. | DOI GHA workflow builds generate a provenance attestation by the trusted control plane in accordance with the [official GitHub Actions buildType](https://actions.github.io/buildtypes/workflow/v1) | 180 | | | Software producer distributes provenance to consumers, preferably using a convention determined by the package ecosystem. | Provenance attestations are distributed as standard OCI artifacts, and conform to open [In-Toto](https://in-toto.io/) and [SLSA](https://slsa.dev/spec/v1.0/provenance) specifications | 181 | | Build L2: Hosted build platform | Build platform runs on dedicated infrastructure, not an individual’s workstation, and the provenance is tied to that infrastructure through a digital signature. | [GitHub Actions](https://github.com/docker-library/meta/actions) runs the build workloads, and workload identity is used to sign provenance. Build triggering (which does not affect the build output) is a process that [runs on Jenkins](https://doi-janky.infosiftr.net/) | 182 | | | Downstream verification of provenance includes validating the authenticity of the provenance. | A Verification Summary Attestation (VSA) is created from a version controlled and securely distributed policy that validates the authenticity of the build-time attestations | 183 | | Build L3: Hardened builds | Build platform implements strong controls to prevent runs from influencing one another, even within the same project. | Each build is performed on a clean node isolated from all other workloads. Where caching is used, it is verified e2e using a trusted channel to transmit cryptographic digests. | 184 | | | Build platform implements strong controls to prevent secret material used to sign the provenance from being accessible to the user-defined build steps. | Secret key material is held in Cloud KMS within HSMs and cannot be read by any process. Workload identity is used to authenticate with the KMS for the signing of attestations and is limited to trusted processes initiated by the control plane and isolated from the build environment. | 185 | --------------------------------------------------------------------------------