├── .github ├── dependabot.yml └── workflows │ ├── actions-lint.yml │ ├── ci.yml │ ├── release.yml │ └── update-pinned-deps.yml ├── .gitignore ├── LICENSE ├── README.md ├── action-constraints.txt ├── actions ├── create-signing-events │ └── action.yml ├── lint-requirements.txt ├── online-sign-targets │ └── action.yml ├── online-sign │ └── action.yml ├── signing-event │ └── action.yml ├── test-repository │ └── action.yml ├── update-issue │ └── action.yml └── upload-repository │ ├── action.yml │ └── index.css ├── build └── build-constraints.txt ├── docs ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CODEOWNERS ├── DELEGATION-MANUAL.md ├── DEVELOPMENT.md ├── ONLINE-SIGNING-SETUP.md ├── RELEASE.md ├── REPOSITORY-MAINTENANCE.md ├── SIGNER-MANUAL.md ├── SIGNER-SETUP.md ├── YUBIKEY-PIV-SETUP.md └── yubikey-manager.png ├── repo ├── LICENSE ├── README.md ├── pyproject.toml ├── test │ ├── __init__.py │ ├── test_ci_repository.py │ ├── test_repo1 │ │ ├── root.json │ │ ├── targets.json │ │ └── timestamp.json │ ├── test_repo2 │ │ ├── root.json │ │ └── timestamp.json │ └── test_repo3 │ │ ├── good │ │ └── metadata │ │ │ ├── myrole.json │ │ │ ├── oldrole.json │ │ │ ├── root.json │ │ │ ├── snapshot.json │ │ │ ├── targets.json │ │ │ └── timestamp.json │ │ └── src_targets │ │ ├── myrole │ │ ├── dir1 │ │ │ ├── dir2 │ │ │ │ ├── dir3 │ │ │ │ │ ├── dir4 │ │ │ │ │ │ └── file4.txt │ │ │ │ │ └── file3.txt │ │ │ │ └── file2.txt │ │ │ └── file1.txt │ │ └── file0.txt │ │ ├── oldrole │ │ ├── dir1 │ │ │ └── file1.txt │ │ └── file0.txt │ │ ├── other_dir │ │ └── otherfile.txt │ │ └── tfile1.txt └── tuf_on_ci │ ├── __init__.py │ ├── _repository.py │ ├── _version.py │ ├── build_repository.py │ ├── client.py │ ├── create_signing_events.py │ ├── online_sign.py │ └── signing_event.py ├── signer ├── LICENSE ├── README.md ├── create-config-file.sh ├── pyproject.toml ├── test │ ├── __init__.py │ ├── test_signer_repository.py │ └── test_user.py └── tuf_on_ci_sign │ ├── __init__.py │ ├── _common.py │ ├── _signer_repository.py │ ├── _user.py │ ├── delegate.py │ ├── import_repo.py │ └── sign.py ├── tests ├── README.md ├── e2e.sh ├── expected │ ├── basic │ │ └── metadata │ │ │ ├── 1.root.json │ │ │ ├── 1.snapshot.json │ │ │ ├── 1.targets.json │ │ │ └── timestamp.json │ ├── delegated │ │ └── metadata │ │ │ ├── 1.delegated.json │ │ │ ├── 1.root.json │ │ │ ├── 2.snapshot.json │ │ │ ├── 2.targets.json │ │ │ └── timestamp.json │ ├── multi-user-signing │ │ └── metadata │ │ │ ├── 1.root.json │ │ │ ├── 1.snapshot.json │ │ │ ├── 1.targets.json │ │ │ ├── 2.root.json │ │ │ └── timestamp.json │ ├── online-version-bump │ │ └── metadata │ │ │ ├── 1.root.json │ │ │ ├── 1.targets.json │ │ │ ├── 2.snapshot.json │ │ │ └── timestamp.json │ ├── root-key-rotation │ │ └── metadata │ │ │ ├── 1.root.json │ │ │ ├── 1.snapshot.json │ │ │ ├── 1.targets.json │ │ │ ├── 2.root.json │ │ │ └── timestamp.json │ ├── signing-event-creation │ │ └── metadata │ │ │ ├── 1.root.json │ │ │ ├── 2.root.json │ │ │ ├── 2.targets.json │ │ │ ├── 3.snapshot.json │ │ │ └── timestamp.json │ ├── target-file-changes │ │ ├── metadata │ │ │ ├── 1.root.json │ │ │ ├── 2.snapshot.json │ │ │ ├── 3.targets.json │ │ │ └── timestamp.json │ │ └── targets │ │ │ └── ac0fe9bf78cd278e66f787bcd02e035de2c4b4da41af783a1daa447c4734222d.file1.txt │ └── target-files-in-delegated-roles │ │ ├── metadata │ │ ├── 1.root.json │ │ ├── 1.targets.json │ │ ├── 2.delegated.json │ │ ├── 2.snapshot.json │ │ └── timestamp.json │ │ └── targets │ │ └── delegated │ │ ├── 1 │ │ └── 2 │ │ │ └── 3 │ │ │ └── 67ee5478eaadb034ba59944eb977797b49ca6aa8d3574587f36ebcbeeb65f70e.file2.txt │ │ └── ecdc5536f73bdae8816f0ea40726ef5e9b810d914493075903bb90623d97b1d8.file1.txt ├── online-test-key └── softhsm │ ├── README │ ├── tokens-user1 │ └── 6087c5cf-c6e9-8a71-a185-1322adf50f3f │ │ ├── 35fbb8af-9b92-0871-980b-8c3eabf1a1cb.lock │ │ ├── 35fbb8af-9b92-0871-980b-8c3eabf1a1cb.object │ │ ├── 9a14c021-cf89-2aff-1294-338c773c4c20.lock │ │ ├── 9a14c021-cf89-2aff-1294-338c773c4c20.object │ │ ├── generation │ │ ├── token.lock │ │ └── token.object │ └── tokens-user2 │ └── 3073b868-0942-0a4a-0bdc-bf54cea956eb │ ├── 1c2cd9d1-b5a3-479f-40bb-42d398bdaaa2.lock │ ├── 1c2cd9d1-b5a3-479f-40bb-42d398bdaaa2.object │ ├── 36f4e31a-e76a-3c7f-b437-ad7d0fc2a7e5.lock │ ├── 36f4e31a-e76a-3c7f-b437-ad7d0fc2a7e5.object │ ├── generation │ ├── token.lock │ └── token.object └── tox.ini /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "pip" 5 | directory: "/build" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | build-dependencies: 10 | # Critical build/release dependencies constrained in build/build-constraints.txt 11 | patterns: 12 | - "*" 13 | 14 | - package-ecosystem: "pip" 15 | directories: 16 | - "/signer" 17 | - "/repo" 18 | - "/actions" 19 | schedule: 20 | interval: "weekly" 21 | groups: 22 | pinned-test-dependencies: 23 | # Dependencies pinned to ensure test reproducibility 24 | patterns: 25 | - "mypy" 26 | - "ruff" 27 | - "zizmor" 28 | minimum-runtime-dependencies: 29 | # Runtime dependency ranges set in {signer,repo}/pyproject.toml 30 | patterns: 31 | - "*" 32 | 33 | - package-ecosystem: "github-actions" 34 | directories: 35 | - "/" 36 | - "/actions/create-signing-events/" 37 | - "/actions/online-sign/" 38 | - "/actions/signing-event/" 39 | - "/actions/test-repository/" 40 | - "/actions/update-issue/" 41 | - "/actions/upload-repository/" 42 | schedule: 43 | interval: "weekly" 44 | groups: 45 | actions-dependencies: 46 | patterns: 47 | - "*" 48 | -------------------------------------------------------------------------------- /.github/workflows/actions-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint actions & workflows 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | lint-actions-and-workflows: 13 | permissions: 14 | security-events: write 15 | contents: read 16 | actions: read 17 | runs-on: ubuntu-latest 18 | env: 19 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | with: 24 | persist-credentials: false 25 | 26 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: '3.13' 29 | cache: 'pip' 30 | cache-dependency-path: | 31 | actions/lint-requirements.txt 32 | 33 | - name: Install zizmor 34 | run: python -m pip install -r actions/lint-requirements.txt 35 | 36 | - name: Run zizmor 37 | run: zizmor --pedantic . 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Lint & test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | test: 12 | permissions: 13 | contents: read 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | toxenv: [lint-signer, test-signer] 18 | pyversion: ['3.9', '3.13'] 19 | os: [ubuntu-latest, macos-latest] 20 | # Only run repository on 3.13 (dependency pinning is easier with single version) 21 | include: 22 | - toxenv: lint-repo 23 | pyversion: '3.13' 24 | os: ubuntu-latest 25 | - toxenv: test-repo 26 | pyversion: '3.13' 27 | os: ubuntu-latest 28 | - toxenv: test-e2e 29 | pyversion: '3.13' 30 | os: ubuntu-latest 31 | runs-on: ${{ matrix.os }} 32 | env: 33 | TOXENV: ${{ matrix.toxenv }} 34 | 35 | steps: 36 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | with: 38 | persist-credentials: false 39 | 40 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 41 | with: 42 | python-version: ${{ matrix.pyversion }} 43 | cache: 'pip' 44 | cache-dependency-path: | 45 | signer/pyproject.toml 46 | repo/pyproject.toml 47 | action-constraints.txt 48 | build/build-constraints.txt 49 | 50 | - name: Install system dependencies for e2e test 51 | if: matrix.toxenv == 'test-e2e' 52 | run: | 53 | sudo apt-get install libfaketime softhsm2 54 | echo "PYKCS11LIB=/usr/lib/softhsm/libsofthsm2.so" >> $GITHUB_ENV 55 | 56 | - name: Install tox 57 | run: python -m pip install -c build/build-constraints.txt tox 58 | 59 | - name: ${{ matrix.toxenv }} 60 | run: tox 61 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | concurrency: release 3 | 4 | on: 5 | push: 6 | tags: 7 | - v* 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | build-signer: 13 | name: Build tuf-on-ci signer 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 17 | with: 18 | persist-credentials: false 19 | 20 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 21 | with: 22 | python-version: '3.13' 23 | 24 | - name: Install build dependencies 25 | run: python3 -m pip install -c build/build-constraints.txt build 26 | 27 | - name: Build release changelog, signer wheel & source tarball 28 | run: | 29 | PIP_CONSTRAINT=build/build-constraints.txt python3 -m build --sdist --wheel --outdir dist/ signer/ 30 | awk "/## $GITHUB_REF_NAME/{flag=1; next} /## v/{flag=0} flag" docs/CHANGELOG.md > changelog 31 | 32 | - name: Store build artifacts 33 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 34 | with: 35 | name: build-artifacts 36 | path: | 37 | dist 38 | changelog 39 | 40 | release-pypi: 41 | name: Release Signer on PyPI 42 | runs-on: ubuntu-latest 43 | needs: build-signer 44 | environment: release 45 | permissions: 46 | id-token: write # to authenticate as Trusted Publisher to pypi.org 47 | steps: 48 | - name: Fetch build artifacts 49 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 50 | with: 51 | name: build-artifacts 52 | 53 | - name: Publish binary wheel and source tarball on PyPI 54 | if: github.repository == 'theupdateframework/tuf-on-ci' 55 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 56 | 57 | release-gh: 58 | name: Release 59 | runs-on: ubuntu-latest 60 | needs: release-pypi 61 | permissions: 62 | contents: write # to modify GitHub releases 63 | steps: 64 | - name: Fetch build artifacts 65 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 66 | with: 67 | name: build-artifacts 68 | 69 | - name: Make a GitHub release 70 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 71 | with: 72 | script: | 73 | fs = require('fs') 74 | res = await github.rest.repos.createRelease({ 75 | owner: context.repo.owner, 76 | repo: context.repo.repo, 77 | name: process.env.GITHUB_REF_NAME, 78 | tag_name: process.env.GITHUB_REF, 79 | body: fs.readFileSync('changelog', 'utf8'), 80 | }) 81 | fs.readdirSync('dist/').forEach(file => { 82 | github.rest.repos.uploadReleaseAsset({ 83 | owner: context.repo.owner, 84 | repo: context.repo.repo, 85 | release_id: res.data.id, 86 | name: file, 87 | data: fs.readFileSync('dist/' + file), 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /.github/workflows/update-pinned-deps.yml: -------------------------------------------------------------------------------- 1 | name: Update pinned Python dependencies for the actions 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ['repo/pyproject.toml'] 7 | schedule: 8 | - cron: '21 9 * * 1' 9 | workflow_dispatch: 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | update-dependencies: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write # for pushing a branch 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: true # for pushing a new branch later 24 | 25 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 26 | with: 27 | python-version: '3.13' 28 | 29 | - name: Install pip-tools 30 | run: pip install -c build/build-constraints.txt pip-tools 31 | 32 | - name: Update action-constraints.txt 33 | id: update 34 | run: | 35 | pip-compile --strip-extras --upgrade --output-file action-constraints.txt repo/pyproject.toml 36 | if git diff --quiet; then 37 | echo "No dependency updates." 38 | echo "updated=false" >> $GITHUB_OUTPUT 39 | else 40 | echo "updated=true" >> $GITHUB_OUTPUT 41 | fi 42 | 43 | - name: Push branch 44 | id: push 45 | if: steps.update.outputs.updated == 'true' 46 | run: | 47 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 48 | git config user.name "github-actions[bot]" 49 | git add action-constraints.txt 50 | git commit -m "repo: Update pinned requirements" 51 | SHA=$(sha256sum action-constraints.txt) 52 | NAME="pin-requirements/${SHA:0:7}" 53 | if git ls-remote --exit-code origin $NAME; then 54 | echo "Branch $NAME exists, nothing to do." 55 | echo "pushed=false" >> $GITHUB_OUTPUT 56 | else 57 | git push origin HEAD:$NAME 58 | echo "Pushed branch $NAME." 59 | echo "pushed=true" >> $GITHUB_OUTPUT 60 | echo "branch=$NAME" >> $GITHUB_OUTPUT 61 | fi 62 | 63 | - name: Open pull request 64 | if: steps.push.outputs.pushed == 'true' 65 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 66 | env: 67 | BRANCH: ${{ steps.push.outputs.branch }} 68 | with: 69 | script: | 70 | await github.rest.pulls.create({ 71 | owner: context.repo.owner, 72 | repo: context.repo.repo, 73 | title: "actions: Update pinned requirements", 74 | body: "Note: close and reopen the PR to trigger CI.", 75 | head: process.env.BRANCH, 76 | base: "main", 77 | }) 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs backup files 2 | *~ 3 | __pycache__ 4 | .idea 5 | env 6 | **/.vscode 7 | dist/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | TUF-on-CI 4 | Copyright 2022-2023 repository-playground contributors 5 | Copyright 2023-2025 TUF-on-CI contributors 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TUF-on-CI: A TUF Repository and Signing Tool 2 | 3 | TUF-on-CI is a secure artifact delivery system that operates on a Continuous Integration 4 | platform. It contains a [TUF](https://theupdateframework.io) repository implementation and an 5 | easy-to-use local signing system that supports hardware keys (e.g. Yubikeys). 6 | 7 | TUF-on-CI can be used to publish a TUF repository that contains digitally signed metadata. 8 | Any TUF-compatible download client can use this repository to securely download 9 | the artifacts described in the repository. 10 | 11 | This system is highly secure against infrastructure compromise: Even a fully compromised 12 | repository hosting will not lead to compromised downloader clients. 13 | 14 | Supported features include: 15 | * Guided signing events for distributed signing 16 | * TUF delegations with signature thresholds 17 | * Signing with hardware keys and Sigstore 18 | * Automated online signing (Google Cloud, Azure, AWS, Sigstore) 19 | * No custom code required 20 | 21 | The optimal use case is TUF repositories with a low to moderate frequency of change, both for artifacts and keys. 22 | 23 | ## Documentation 24 | 25 | * [Signer Manual](docs/SIGNER-MANUAL.md) 26 | * [Repository Maintenance Manual](docs/REPOSITORY-MAINTENANCE.md) 27 | * [Developer notes](docs/DEVELOPMENT.md) 28 | 29 | ## Deployments 30 | 31 | ![logos](https://github.com/theupdateframework/tuf-on-ci/assets/31889/34eb2a5e-b9a2-41ad-b333-6a28590b17f3) 32 | 33 | * The [Sigstore project](https://www.sigstore.dev/) uses tuf-on-ci to manage their TUF repositories in 34 | [root-signing](https://github.com/sigstore/root-signing) and [root-signing-staging](https://github.com/sigstore/root-signing-staging). 35 | These repositories are used to deliver the Sigstore root of trust to all sigstore clients. 36 | * GitHub maintains a TUF repository for their 37 | [Artifact Attestations](https://github.blog/2024-05-02-introducing-artifact-attestations-now-in-public-beta/) 38 | with tuf-on-ci 39 | * There is also a [demo deployment](https://github.com/jku/tuf-demo/) for the TUF community 40 | 41 | ## Contact 42 | 43 | * We're on [Slack](https://cloud-native.slack.com/archives/C04SHK2DPK9) 44 | * Feel free to file issues if anything is unclear: this is a new project so docs are still lacking 45 | * Email sent to jkukkonen at google.com will be read eventually 46 | -------------------------------------------------------------------------------- /action-constraints.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=action-constraints.txt --strip-extras repo/pyproject.toml 6 | # 7 | annotated-types==0.7.0 8 | # via pydantic 9 | azure-core==1.34.0 10 | # via 11 | # azure-identity 12 | # azure-keyvault-keys 13 | azure-identity==1.23.0 14 | # via securesystemslib 15 | azure-keyvault-keys==4.10.0 16 | # via securesystemslib 17 | betterproto==2.0.0b6 18 | # via sigstore-protobuf-specs 19 | boto3==1.38.23 20 | # via securesystemslib 21 | botocore==1.38.23 22 | # via 23 | # boto3 24 | # s3transfer 25 | # securesystemslib 26 | cachetools==5.5.2 27 | # via google-auth 28 | certifi==2025.4.26 29 | # via requests 30 | cffi==1.17.1 31 | # via cryptography 32 | charset-normalizer==3.4.2 33 | # via requests 34 | click==8.2.1 35 | # via tuf-on-ci (repo/pyproject.toml) 36 | cryptography==44.0.3 37 | # via 38 | # azure-identity 39 | # azure-keyvault-keys 40 | # msal 41 | # pyjwt 42 | # pyopenssl 43 | # rfc3161-client 44 | # securesystemslib 45 | # sigstore 46 | dnspython==2.7.0 47 | # via email-validator 48 | email-validator==2.2.0 49 | # via pydantic 50 | google-api-core==2.24.2 51 | # via google-cloud-kms 52 | google-auth==2.40.2 53 | # via 54 | # google-api-core 55 | # google-cloud-kms 56 | google-cloud-kms==3.5.1 57 | # via securesystemslib 58 | googleapis-common-protos==1.70.0 59 | # via 60 | # google-api-core 61 | # grpc-google-iam-v1 62 | # grpcio-status 63 | grpc-google-iam-v1==0.14.2 64 | # via google-cloud-kms 65 | grpcio==1.72.0rc1 66 | # via 67 | # google-api-core 68 | # googleapis-common-protos 69 | # grpc-google-iam-v1 70 | # grpcio-status 71 | grpcio-status==1.72.0rc1 72 | # via google-api-core 73 | grpclib==0.4.8 74 | # via betterproto 75 | h2==4.2.0 76 | # via grpclib 77 | hpack==4.1.0 78 | # via h2 79 | hyperframe==6.1.0 80 | # via h2 81 | id==1.5.0 82 | # via sigstore 83 | idna==3.10 84 | # via 85 | # email-validator 86 | # requests 87 | isodate==0.7.2 88 | # via azure-keyvault-keys 89 | jmespath==1.0.1 90 | # via 91 | # boto3 92 | # botocore 93 | markdown-it-py==3.0.0 94 | # via rich 95 | mdurl==0.1.2 96 | # via markdown-it-py 97 | msal==1.32.3 98 | # via 99 | # azure-identity 100 | # msal-extensions 101 | msal-extensions==1.3.1 102 | # via azure-identity 103 | multidict==6.4.4 104 | # via grpclib 105 | platformdirs==4.3.8 106 | # via sigstore 107 | proto-plus==1.26.1 108 | # via 109 | # google-api-core 110 | # google-cloud-kms 111 | protobuf==6.31.0 112 | # via 113 | # google-api-core 114 | # google-cloud-kms 115 | # googleapis-common-protos 116 | # grpc-google-iam-v1 117 | # grpcio-status 118 | # proto-plus 119 | pyasn1==0.6.1 120 | # via 121 | # pyasn1-modules 122 | # rsa 123 | # sigstore 124 | pyasn1-modules==0.4.2 125 | # via google-auth 126 | pycparser==2.22 127 | # via cffi 128 | pydantic==2.11.5 129 | # via 130 | # sigstore 131 | # sigstore-rekor-types 132 | pydantic-core==2.33.2 133 | # via pydantic 134 | pygments==2.19.1 135 | # via rich 136 | pyjwt==2.10.1 137 | # via 138 | # msal 139 | # sigstore 140 | pyopenssl==25.1.0 141 | # via sigstore 142 | python-dateutil==2.9.0.post0 143 | # via 144 | # betterproto 145 | # botocore 146 | requests==2.32.3 147 | # via 148 | # azure-core 149 | # google-api-core 150 | # id 151 | # msal 152 | # sigstore 153 | rfc3161-client==1.0.2 154 | # via sigstore 155 | rfc8785==0.1.4 156 | # via sigstore 157 | rich==14.0.0 158 | # via sigstore 159 | rsa==4.9.1 160 | # via google-auth 161 | s3transfer==0.13.0 162 | # via boto3 163 | securesystemslib==1.3.0 164 | # via 165 | # tuf 166 | # tuf-on-ci (repo/pyproject.toml) 167 | sigstore==3.6.2 168 | # via securesystemslib 169 | sigstore-protobuf-specs==0.3.2 170 | # via sigstore 171 | sigstore-rekor-types==0.0.18 172 | # via sigstore 173 | six==1.17.0 174 | # via 175 | # azure-core 176 | # python-dateutil 177 | tuf==6.0.0 178 | # via 179 | # sigstore 180 | # tuf-on-ci (repo/pyproject.toml) 181 | typing-extensions==4.13.2 182 | # via 183 | # azure-core 184 | # azure-identity 185 | # azure-keyvault-keys 186 | # pydantic 187 | # pydantic-core 188 | # typing-inspection 189 | typing-inspection==0.4.1 190 | # via pydantic 191 | urllib3==2.4.0 192 | # via 193 | # botocore 194 | # requests 195 | # tuf 196 | -------------------------------------------------------------------------------- /actions/create-signing-events/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Create signing events' 2 | description: 'Create signing events for offline signed metadata that is about to expire' 3 | 4 | inputs: 5 | token: 6 | description: 'GitHub token' 7 | required: true 8 | 9 | runs: 10 | using: "composite" 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | with: 14 | token: ${{ inputs.token }} 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 18 | with: 19 | python-version: "3.13" 20 | 21 | - run: | 22 | echo "::group::Install tuf-on-ci" 23 | ROOT=$GITHUB_ACTION_PATH/../.. 24 | pip install -c $ROOT/action-constraints.txt $ROOT/repo/ 25 | echo "::endgroup::" 26 | shell: bash 27 | 28 | - name: Create signing event branches for expiring roles 29 | id: create-signing-events 30 | run: | 31 | events=$(tuf-on-ci-create-signing-events --push) 32 | echo events="$events" 33 | echo events="$events" >> $GITHUB_OUTPUT 34 | if [ -z "${events}" ]; then 35 | echo "Nothing to prepare" >> $GITHUB_STEP_SUMMARY 36 | else 37 | echo "Dispatching events for ${events}" >> $GITHUB_STEP_SUMMARY 38 | fi 39 | shell: bash 40 | 41 | - name: Dispatch signing event workflow 42 | # dispatch if using default token: otherwise create-signing-events step has already triggered push events 43 | if: inputs.token == github.token && steps.create-signing-events.outputs.events != '' 44 | env: 45 | EVENTS: ${{ steps.create-signing-events.outputs.events }} 46 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 47 | with: 48 | github-token: ${{ inputs.token }} 49 | script: | 50 | console.log('Dispatching events: ', process.env.EVENTS) 51 | process.env.EVENTS.trim().split(' ').forEach(event => { 52 | github.rest.actions.createWorkflowDispatch({ 53 | owner: context.repo.owner, 54 | repo: context.repo.repo, 55 | workflow_id: 'signing-event.yml', 56 | ref: event, 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /actions/lint-requirements.txt: -------------------------------------------------------------------------------- 1 | zizmor == 1.9.0 -------------------------------------------------------------------------------- /actions/online-sign-targets/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Online signing of targets' 2 | description: 'TUF-on-CI Online signer for targets' 3 | 4 | # This action is not called from any standard workflows. 5 | # To use this, create custom workflow that triggers this action when 6 | # needed. This is typically done for delegations, that would be 7 | # triggered based on a change to the delegation metadata file. 8 | 9 | inputs: 10 | token: 11 | description: 'GitHub token' 12 | required: true 13 | targets_to_sign: 14 | description: "whitespace separated list of targets role names that should be signed with KMS" 15 | required: false 16 | default: "" 17 | gcp_workload_identity_provider: 18 | description: "Google Cloud workload identity provider (required if GCP is used to sign targets roles)" 19 | required: false 20 | default: "" 21 | gcp_service_account: 22 | description: "Google Cloud service account name (required if GCP is used to sign targets roles)" 23 | required: false 24 | default: "" 25 | aws_region: 26 | description: "AWS region" 27 | required: false 28 | default: "" 29 | aws_role_to_assume: 30 | description: "AWS role to assume" 31 | required: false 32 | default: "" 33 | azure_client_id: 34 | description: "Azure SPN client id (required to use Azure to sign target roles)" 35 | required: false 36 | default: "" 37 | azure_tenant_id: 38 | description: "Azure SPN tenant id (required to use Azure to sign target roles)" 39 | required: false 40 | default: "" 41 | azure_subscription_id: 42 | description: "Azure SPN subscription id (required to use Azure to sign target roles)" 43 | required: false 44 | default: "" 45 | 46 | runs: 47 | using: "composite" 48 | steps: 49 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 50 | with: 51 | token: ${{ inputs.token }} 52 | fetch-depth: 0 53 | 54 | - name: Authenticate to Google Cloud 55 | if: inputs.gcp_workload_identity_provider != '' 56 | uses: google-github-actions/auth@71fee32a0bb7e97b4d33d548e7d957010649d8fa # v2.1.3 57 | with: 58 | token_format: access_token 59 | workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }} 60 | service_account: ${{ inputs.gcp_service_account }} 61 | 62 | - name: Authenticate to AWS 63 | if: inputs.aws_role_to_assume != '' 64 | uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 #v4.0.2 65 | with: 66 | aws-region: ${{ inputs.aws_region }} 67 | role-to-assume: ${{ inputs.aws_role_to_assume }} 68 | 69 | - name: Authenticate to Azure cloud 70 | if: inputs.azure_client_id != '' 71 | uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1 72 | with: 73 | client-id: ${{ inputs.azure_client_id }} 74 | tenant-id: ${{ inputs.azure_tenant_id }} 75 | subscription-id: ${{ inputs.azure_subscription_id }} 76 | 77 | - uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1 78 | with: 79 | python-version: "3.13" 80 | 81 | - run: | 82 | echo "::group::Install tuf-on-ci" 83 | ROOT=$GITHUB_ACTION_PATH/../.. 84 | pip install -c $ROOT/action-constraints.txt $ROOT/repo/ 85 | echo "::endgroup::" 86 | shell: bash 87 | 88 | - id: sign_targets 89 | if: inputs.targets_to_sign != '' 90 | env: 91 | TARGETS_TO_SIGN: ${{ inputs.targets_to_sign }} 92 | run: | 93 | if tuf-on-ci-online-sign-targets $TARGETS_TO_SIGN >> sign-output; then 94 | echo "targets_signed=true" >> $GITHUB_OUTPUT 95 | else 96 | echo "targets_signed=false" >> $GITHUB_OUTPUT 97 | fi 98 | cat sign-output 99 | cat sign-output >> output 100 | cat sign-output >> "$GITHUB_STEP_SUMMARY" 101 | shell: bash 102 | 103 | - name: Dispatch another signing event workflow 104 | # dispatch if using default token: otherwise update_targets step 105 | # has alreaddy triggered a push event 106 | if: inputs.token == github.token && steps.sign_targets.outputs.targets_signed == 'true' 107 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 108 | with: 109 | github-token: ${{ inputs.token }} 110 | script: | 111 | console.log('Dispatching another signing event workflow after a targets metadata update') 112 | github.rest.actions.createWorkflowDispatch({ 113 | owner: context.repo.owner, 114 | repo: context.repo.repo, 115 | workflow_id: 'signing-event.yml', 116 | ref: process.env.GITHUB_REF_NAME, 117 | }) 118 | -------------------------------------------------------------------------------- /actions/online-sign/action.yml: -------------------------------------------------------------------------------- 1 | name: "Online sign" 2 | description: "Creates a snapshot and timestamp if needed, moves publish branch if needed" 3 | 4 | inputs: 5 | token: 6 | description: 'GitHub token' 7 | required: true 8 | gcp_workload_identity_provider: 9 | description: "Google Cloud workload identity provider" 10 | required: false 11 | default: "" 12 | gcp_service_account: 13 | description: "Google Cloud service account name" 14 | required: false 15 | default: "" 16 | aws_region: 17 | description: "AWS region" 18 | required: false 19 | default: "" 20 | aws_role_to_assume: 21 | description: "AWS role to assume" 22 | required: false 23 | default: "" 24 | azure_client_id: 25 | description: "Azure SPN client id (required to use Azure to sign target roles)" 26 | required: false 27 | default: "" 28 | azure_tenant_id: 29 | description: "Azure SPN tenant id (required to use Azure to sign target roles)" 30 | required: false 31 | default: "" 32 | azure_subscription_id: 33 | description: "Azure SPN subscription id (required to use Azure to sign target roles)" 34 | required: false 35 | default: "" 36 | 37 | runs: 38 | using: "composite" 39 | steps: 40 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | with: 42 | token: ${{ inputs.token }} 43 | fetch-depth: 0 44 | 45 | - name: Authenticate to Google Cloud 46 | if: inputs.gcp_workload_identity_provider != '' 47 | uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10 48 | with: 49 | token_format: access_token 50 | workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }} 51 | service_account: ${{ inputs.gcp_service_account }} 52 | 53 | - name: Authenticate to AWS 54 | if: inputs.aws_role_to_assume != '' 55 | uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df #v4.2.1 56 | with: 57 | aws-region: ${{ inputs.aws_region }} 58 | role-to-assume: ${{ inputs.aws_role_to_assume }} 59 | 60 | - name: Authenticate to Azure cloud 61 | if: inputs.azure_client_id != '' 62 | uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0 63 | with: 64 | client-id: ${{ inputs.azure_client_id }} 65 | tenant-id: ${{ inputs.azure_tenant_id }} 66 | subscription-id: ${{ inputs.azure_subscription_id }} 67 | 68 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 69 | with: 70 | python-version: "3.13" 71 | 72 | - run: | 73 | echo "::group::Install tuf-on-ci" 74 | ROOT=$GITHUB_ACTION_PATH/../.. 75 | pip install -c $ROOT/action-constraints.txt $ROOT/repo/ 76 | echo "::endgroup::" 77 | shell: bash 78 | 79 | - id: online-sign 80 | run: | 81 | tuf-on-ci-online-sign --push 82 | 83 | # did we actually create a snapshot/timestamp commit? 84 | if [[ $GITHUB_SHA == $(git rev-parse HEAD) ]]; then 85 | echo "ONLINE_SIGNED=false" 86 | echo "ONLINE_SIGNED=false" >> "$GITHUB_ENV" 87 | echo "### Nothing to sign" >> "$GITHUB_STEP_SUMMARY" 88 | else 89 | echo "ONLINE_SIGNED=true" 90 | echo "ONLINE_SIGNED=true" >> "$GITHUB_ENV" 91 | fi 92 | shell: bash 93 | 94 | - id: move-publish-branch 95 | if: github.event_name != 'schedule' || env.ONLINE_SIGNED == 'true' 96 | run: | 97 | git show --oneline --no-patch HEAD 98 | git push origin HEAD:publish 99 | echo "rev=`git rev-parse HEAD`" >> $GITHUB_OUTPUT 100 | echo "### Online signing finished, will now publish" >> $GITHUB_STEP_SUMMARY 101 | shell: bash 102 | 103 | - id: dispatch-publish-workflow 104 | if: github.event_name != 'schedule' || env.ONLINE_SIGNED == 'true' 105 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 106 | env: 107 | REV: ${{ steps.move-publish-branch.outputs.rev }} 108 | with: 109 | github-token: ${{ inputs.token }} 110 | script: | 111 | console.log('Dispatching publish workflow') 112 | github.rest.actions.createWorkflowDispatch({ 113 | owner: context.repo.owner, 114 | repo: context.repo.repo, 115 | workflow_id: 'publish.yml', 116 | ref: 'publish', 117 | inputs: { 118 | ref: process.env.REV, 119 | }, 120 | }) 121 | -------------------------------------------------------------------------------- /actions/signing-event/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Signing event' 2 | description: 'TUF-on-CI Signing event management' 3 | 4 | # This action is called from signing-event workflow, which is dispatched in multiple ways 5 | # depending on the type of token provided in inputs. 6 | # 7 | # 1. create-signing-events action creates a new signing event branch 8 | # * When using a custom token this triggers push event handler 9 | # * When using the default GitHub token the action calls createWorkflowDispatch() 10 | # 2. A signer pushes artifact changes to a signing event branch 11 | # * This triggers push event handler 12 | # 3. This action (signing-event) makes a metadata change in update_targets step as a result of an artifact change 13 | # * When using a custom token this triggers push event handler 14 | # * When using the default GitHub token the action calls createWorkflowDispatch() 15 | # 16 | # Cases 1 & 3 lead to status step running. Case 2 leads to skipping status step, but 17 | # triggering case 3 immediately afterwards. 18 | 19 | inputs: 20 | token: 21 | description: 'GitHub token' 22 | required: true 23 | 24 | runs: 25 | using: "composite" 26 | steps: 27 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | token: ${{ inputs.token }} 30 | fetch-depth: 0 31 | 32 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 33 | with: 34 | python-version: "3.13" 35 | 36 | - run: | 37 | echo "::group::Install tuf-on-ci" 38 | ROOT=$GITHUB_ACTION_PATH/../.. 39 | pip install -c $ROOT/action-constraints.txt $ROOT/repo/ 40 | echo "::endgroup::" 41 | shell: bash 42 | 43 | - id: update_targets 44 | run: | 45 | if tuf-on-ci-update-targets >> update-output; then 46 | echo "targets_updated=true" >> $GITHUB_OUTPUT 47 | else 48 | echo "targets_updated=false" >> $GITHUB_OUTPUT 49 | fi 50 | cat update-output 51 | cat update-output >> output 52 | cat update-output >> "$GITHUB_STEP_SUMMARY" 53 | shell: bash 54 | 55 | - id: status 56 | if: steps.update_targets.outputs.targets_updated != 'true' 57 | run: | 58 | if tuf-on-ci-status >> status-output; then 59 | echo "status=success" >> $GITHUB_OUTPUT 60 | else 61 | echo "status=failure" >> $GITHUB_OUTPUT 62 | fi 63 | cat status-output 64 | cat status-output >> output 65 | cat status-output >> "$GITHUB_STEP_SUMMARY" 66 | shell: bash 67 | 68 | - id: update-pr 69 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 70 | env: 71 | STATUS: ${{ steps.status.outputs.status }} 72 | with: 73 | github-token: ${{ inputs.token }} 74 | script: | 75 | const fs = require('fs') 76 | const title = `Signing event: ${process.env.GITHUB_REF_NAME}` 77 | const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({ 78 | owner: context.repo.owner, 79 | repo: context.repo.repo, 80 | commit_sha: context.sha 81 | }) 82 | const open_prs = prs.data.filter(item => item.state === 'open') 83 | 84 | if (open_prs.length > 1) { 85 | core.setFailed("Found more than one open pull request with the current commit") 86 | } else if (open_prs.length == 0) { 87 | const response = await github.rest.pulls.create({ 88 | owner: context.repo.owner, 89 | repo: context.repo.repo, 90 | title: title, 91 | body: `Processing signing event ${process.env.GITHUB_REF_NAME}, please wait.`, 92 | draft: true, 93 | head: process.env.GITHUB_REF_NAME, 94 | base: "main", 95 | }) 96 | pr = response.data.number 97 | console.log(`Created pull request #${pr}`) 98 | } else { 99 | pr = open_prs[0].number 100 | console.log(`Found existing pull request #${pr}`) 101 | } 102 | 103 | message = fs.readFileSync('./output').toString() 104 | summary = "Signing event in progress" 105 | should_be_draft = true 106 | if (process.env.STATUS == 'success') { 107 | message += "### Signing event is successful\n\n" 108 | message += "Threshold of signatures has been reached: this signing event can be reviewed and merged." 109 | summary = "Signing event is successful" 110 | should_be_draft = false 111 | } 112 | 113 | // Pull request numbers are also valid issue numbers 114 | github.rest.issues.createComment({ 115 | owner: context.repo.owner, 116 | repo: context.repo.repo, 117 | issue_number: pr, 118 | body: message, 119 | }) 120 | 121 | // Following pile of GraphQL is here because draft state cannot 122 | // be modified through rest API at all! 123 | 124 | // First get the PR "Id" and current draft state... 125 | response = await github.graphql(`query { 126 | repository(owner: "${context.repo.owner}", name: "${context.repo.repo}") { 127 | pullRequest(number: ${pr}) { id, isDraft } 128 | } 129 | }`) 130 | pr_id = response.repository.pullRequest.id 131 | is_draft = response.repository.pullRequest.isDraft 132 | // Then modify PR if needed 133 | if (should_be_draft && !is_draft) { 134 | await github.graphql(`mutation SetPullRequestDraft { 135 | convertPullRequestToDraft(input: {pullRequestId: "${pr_id}"}) { 136 | pullRequest { isDraft } 137 | } 138 | }`) 139 | } else if (!should_be_draft && is_draft) { 140 | await github.graphql(`mutation SetPullRequestReady { 141 | markPullRequestReadyForReview(input: {pullRequestId: "${pr_id}"}) { 142 | pullRequest { isDraft } 143 | } 144 | }`) 145 | } 146 | 147 | await core.summary.addHeading(summary).write() 148 | 149 | - name: Dispatch another signing event workflow 150 | # dispatch if using default token: otherwise update_targets step 151 | # has alreaddy triggered a push event 152 | if: inputs.token == github.token && steps.update_targets.outputs.targets_updated == 'true' 153 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 154 | with: 155 | github-token: ${{ inputs.token }} 156 | script: | 157 | console.log('Dispatching another signing event workflow after a targets metadata update') 158 | github.rest.actions.createWorkflowDispatch({ 159 | owner: context.repo.owner, 160 | repo: context.repo.repo, 161 | workflow_id: 'signing-event.yml', 162 | ref: process.env.GITHUB_REF_NAME, 163 | }) 164 | -------------------------------------------------------------------------------- /actions/test-repository/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Test TUF-on-CI repository' 2 | description: 'Test a published TUF-on-CI repository with a client' 3 | 4 | inputs: 5 | metadata_url: 6 | description: | 7 | base metadata URL the client should use. The client will be initialized with 8 | `metadata_url/1.root.json` by default. However if there is a `root.json` file 9 | in the working directory, that will be used instead. 10 | default: '' 11 | artifact_url: 12 | description: 'Base artifact URL the client should use.' 13 | default: '' 14 | update_base_url: 15 | description: 'Optional metadata URL to use as previous repository state.' 16 | default: '' 17 | expected_artifact: 18 | description: | 19 | Optional artifact path that should be checked to exist in the repository. 20 | default: '' 21 | compare_source: 22 | description: | 23 | When true, client metadata is compared to current repository content. Set to 24 | false if action is not running in a tuf-on-ci repository. 25 | default: 'true' 26 | valid_days: 27 | description: | 28 | Number of days. The repository is checked to be valid at "now + N days". 29 | default: '0' 30 | offline_valid_days: 31 | description: | 32 | Number of days. Root and targets role validity is checked to be valid at 33 | "now + N days". This number can be larger than repository validity. 34 | default: '0' 35 | metadata_dir: 36 | description: | 37 | Optional directory name. The metadata client receives will be left here. 38 | Useful e.g. for deduplication purposes. 39 | default: '' 40 | 41 | runs: 42 | using: "composite" 43 | steps: 44 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 45 | if: inputs.compare_source == 'true' 46 | with: 47 | ref: "publish" 48 | path: "source/" 49 | 50 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 51 | with: 52 | python-version: "3.13" 53 | 54 | - run: | 55 | echo "::group::Install tuf-on-ci" 56 | ROOT=$GITHUB_ACTION_PATH/../.. 57 | pip install -c $ROOT/action-constraints.txt $ROOT/repo/ 58 | echo "::endgroup::" 59 | shell: bash 60 | 61 | - env: 62 | METADATA_URL: ${{ inputs.metadata_url }} 63 | ARTIFACT_URL: ${{ inputs.artifact_url }} 64 | UPDATE_BASE_URL: ${{ inputs.update_base_url }} 65 | EXPECTED_ARTIFACT: ${{ inputs.expected_artifact }} 66 | OWNER_REPO: ${{ github.repository }} 67 | COMPARE_SOURCE: ${{ inputs.compare_source }} 68 | VALID_DAYS: ${{ inputs.valid_days }} 69 | OFFLINE_VALID_DAYS: ${{ inputs.offline_valid_days }} 70 | METADATA_DIR: ${{ inputs.metadata_dir }} 71 | run: | 72 | # Run tuf-on-ci test client 73 | OWNER=${OWNER_REPO%/*} 74 | REPO=${OWNER_REPO#*/} 75 | 76 | # guess reasonable default urls 77 | if [ -z $METADATA_URL ]; then 78 | METADATA_URL="https://${OWNER}.github.io/${REPO}/metadata/" 79 | fi 80 | if [ -z $ARTIFACT_URL ]; then 81 | ARTIFACT_URL=${METADATA_URL%/metadata/}/targets/ 82 | fi 83 | 84 | if [ -z $UPDATE_BASE_URL ]; then 85 | UPDATE_BASE_URL_ARG="" 86 | else 87 | UPDATE_BASE_URL_ARG="--update-base-url $UPDATE_BASE_URL" 88 | fi 89 | 90 | if [ -z $EXPECTED_ARTIFACT ]; then 91 | ARTIFACT_ARG="" 92 | else 93 | ARTIFACT_ARG="--expected-artifact $EXPECTED_ARTIFACT" 94 | fi 95 | 96 | if [ "$COMPARE_SOURCE" = "true" ]; then 97 | COMPARE_SOURCE_ARG="--compare-source source/metadata" 98 | else 99 | COMPARE_SOURCE_ARG="" 100 | fi 101 | 102 | if [ $VALID_DAYS -eq 0 ]; then 103 | TIME_ARG="" 104 | else 105 | TIME=$(date -d "+$VALID_DAYS days" '+%s') 106 | TIME_ARG="--time $TIME" 107 | fi 108 | 109 | if [ $OFFLINE_VALID_DAYS -eq 0 ]; then 110 | OFFLINE_TIME_ARG="" 111 | else 112 | OFFLINE_TIME=$(date -d "+$OFFLINE_VALID_DAYS days" '+%s') 113 | OFFLINE_TIME_ARG="--offline-time $OFFLINE_TIME" 114 | fi 115 | 116 | if [ -z $METADATA_DIR ]; then 117 | METADATA_DIR_ARG="" 118 | else 119 | METADATA_DIR_ARG="--metadata-dir $METADATA_DIR" 120 | fi 121 | 122 | if [ -e root.json ]; then 123 | ROOT_ARG="--initial-root root.json" 124 | else 125 | ROOT_ARG="" 126 | fi 127 | 128 | echo "Testing repository at metadata-url $METADATA_URL, artifact-url $ARTIFACT_URL" 129 | tuf-on-ci-test-client \ 130 | --metadata-url "$METADATA_URL" \ 131 | --artifact-url "$ARTIFACT_URL" \ 132 | $UPDATE_BASE_URL_ARG \ 133 | $ROOT_ARG \ 134 | $ARTIFACT_ARG \ 135 | $COMPARE_SOURCE_ARG \ 136 | $TIME_ARG \ 137 | $OFFLINE_TIME_ARG \ 138 | $METADATA_DIR_ARG 139 | 140 | shell: bash 141 | -------------------------------------------------------------------------------- /actions/update-issue/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Update TUF-on-CI issue' 2 | description: 'Create, close or add a comment in a GitHub issue for a workflow failure' 3 | # * This action will open an issue per workflow if that workflow fails. 4 | # * If an issue is open for that workflow already, the action will add a comment. 5 | # * If an issue is open and the workflow succeeds, the action will close the issue. 6 | # * The issue is identified using a label that is the workflow name. 7 | # * Required permissions: 8 | # issues: write 9 | 10 | inputs: 11 | token: 12 | description: 'GitHub token' 13 | required: true 14 | 15 | success: 16 | description: '"true" if workflow is succeeding' 17 | required: true 18 | 19 | runs: 20 | using: "composite" 21 | steps: 22 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 23 | 24 | - name: Update issue 25 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 26 | env: 27 | SUCCESS: ${{ inputs.success }} 28 | with: 29 | github-token: ${{ inputs.token }} 30 | script: | 31 | const fs = require('fs') 32 | 33 | success = (process.env.SUCCESS == "true") 34 | 35 | // Find issue labeled with the forkflow name 36 | const issues = await github.rest.issues.listForRepo({ 37 | owner: context.repo.owner, 38 | repo: context.repo.repo, 39 | labels: [context.workflow], 40 | }) 41 | if (issues.data.length == 0) { 42 | issue_number = 0 43 | } else { 44 | issue_number = issues.data[0].number 45 | } 46 | 47 | if (success && !issue_number) { 48 | console.log("update-issue: Nothing to do (success, no issue open)") 49 | return 50 | } 51 | 52 | // Build comment body 53 | const run_url = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` 54 | if (success) { 55 | body = `### Workflow run succeeded for ${context.workflow}.\n` + 56 | `Successful run: ${run_url}\n\n` + 57 | `Closing issue based on this success.` 58 | } else { 59 | body = `### Workflow run failed for ${context.workflow}.\n` + 60 | `Failed run: ${run_url}\n\n` 61 | } 62 | 63 | // open, comment on, and close issue as needed 64 | if (!success && !issue_number) { 65 | console.log("update-issue: Opening a new issue on failure") 66 | try { 67 | body += fs.readFileSync(`.github/TUF_ON_CI_TEMPLATE/failure.md`).toString() 68 | } catch(err) { 69 | if (err.code != 'ENOENT') { 70 | console.log(err) 71 | } 72 | } 73 | 74 | await github.rest.issues.create({ 75 | owner: context.repo.owner, 76 | repo: context.repo.repo, 77 | title: `Failure in ${context.workflow}`, 78 | labels: [context.workflow], 79 | body: body, 80 | }) 81 | } 82 | if (issue_number) { 83 | console.log(`update-issue: Adding a comment (issue: ${issue_number})`) 84 | await github.rest.issues.createComment({ 85 | owner: context.repo.owner, 86 | repo: context.repo.repo, 87 | issue_number: issue_number, 88 | body: body, 89 | }) 90 | } 91 | if (success) { 92 | console.log(`update-issue: Closing issue on success (issue: ${issue_number})`) 93 | await github.rest.issues.update({ 94 | issue_number: issue_number, 95 | owner: context.repo.owner, 96 | repo: context.repo.repo, 97 | state: "closed", 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /actions/upload-repository/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Upload Repository artifacts' 2 | description: 'Build a publishable repository version and upload it as GitHub artifacts' 3 | 4 | inputs: 5 | gh_pages: 6 | description: 'Upload a GitHub Pages compatible single artifact' 7 | required: false 8 | default: false 9 | metadata_path: 10 | description: 'Relative published metadata path (only useful with gh_pages)' 11 | required: false 12 | default: "metadata" 13 | artifacts_path: 14 | description: 'relative published artifact path (only useful with gh_pages)' 15 | required: false 16 | default: "targets" 17 | ref: 18 | description: 'Ref to clone' 19 | required: false 20 | default: '' 21 | 22 | runs: 23 | using: "composite" 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | ref: ${{ inputs.ref }} 28 | 29 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 30 | with: 31 | python-version: "3.13" 32 | 33 | - run: | 34 | echo "::group::Install tuf-on-ci" 35 | ROOT=$GITHUB_ACTION_PATH/../.. 36 | pip install -c $ROOT/action-constraints.txt $ROOT/repo/ 37 | cp $GITHUB_ACTION_PATH/index.css . 38 | echo "::endgroup::" 39 | shell: bash 40 | 41 | - id: build-repository 42 | env: 43 | METADATA_PATH: ${{inputs.metadata_path}} 44 | ARTIFACTS_PATH: ${{inputs.artifacts_path}} 45 | run: | 46 | mkdir build 47 | tuf-on-ci-build-repository --metadata "build/$METADATA_PATH" --artifacts "build/$ARTIFACTS_PATH" 48 | 49 | find build -type f | xargs ls -lh 50 | shell: bash 51 | 52 | - name: Render repository state in HTML 53 | uses: docker://pandoc/core:3.5.0@sha256:771842ce6f661785e0e12931f82ea64046d70fa48ea5cff480492d79ab3b8ff1 54 | with: 55 | args: >- 56 | --metadata title="TUF Repository state" 57 | --variable title="" 58 | --standalone 59 | --embed-resources 60 | --css index.css 61 | --output build/${{inputs.metadata_path}}/index.html 62 | build/${{inputs.metadata_path}}/index.md 63 | 64 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 65 | if: inputs.gh_pages != 'true' 66 | with: 67 | name: metadata 68 | path: build/${{inputs.metadata_path}}/* 69 | 70 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 71 | if: inputs.gh_pages != 'true' 72 | with: 73 | name: artifacts 74 | path: build/${{inputs.artifacts_path}}/* 75 | 76 | - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 77 | if: inputs.gh_pages == 'true' 78 | with: 79 | path: build/ 80 | 81 | - id: status-summary 82 | shell: bash 83 | env: 84 | GH_PAGES: ${{inputs.gh_pages}} 85 | run: | 86 | if [ "$GH_PAGES" == "true" ]; then 87 | echo "Repository is uploaded and ready to be deployed to GitHub Pages" >> $GITHUB_STEP_SUMMARY 88 | else 89 | echo "Repository is uploaded to artifacts" >> $GITHUB_STEP_SUMMARY 90 | fi 91 | -------------------------------------------------------------------------------- /actions/upload-repository/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #24292e; 3 | line-height: 1.5; 4 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; 5 | font-size: 16px; 6 | line-height: 1.5; 7 | word-wrap: break-word; 8 | } 9 | 10 | a { 11 | background-color: transparent; 12 | color: #0366d6; 13 | text-decoration: none; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | table { 21 | margin-bottom: 16px; 22 | border-collapse: collapse; 23 | border-spacing: 0; 24 | display: block; 25 | overflow: auto; 26 | width: 100%; 27 | } 28 | 29 | table th { 30 | font-weight: 600; 31 | } 32 | 33 | table td, 34 | table th { 35 | border: 1px solid #dfe2e5; 36 | padding: 6px 13px; 37 | } 38 | 39 | table tr { 40 | background-color: #fff; 41 | border-top: 1px solid #c6cbd1; 42 | } 43 | 44 | table tr:nth-child(2n) { 45 | background-color: #f6f8fa; 46 | } 47 | -------------------------------------------------------------------------------- /build/build-constraints.txt: -------------------------------------------------------------------------------- 1 | # these packages are used as constraints during build 2 | build==1.2.2.post1 3 | hatchling==1.27.0 4 | 5 | # These packages are used used as constraints in workflows 6 | tox==4.26.0 7 | pip-tools==7.4.1 -------------------------------------------------------------------------------- /docs/CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## TUF-on-CI Code of Conduct 2 | 3 | TUF-on-CI follows the [CNCF Code of 4 | Conduct v1.3](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 5 | -------------------------------------------------------------------------------- /docs/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jku @kommendorkapten -------------------------------------------------------------------------------- /docs/DELEGATION-MANUAL.md: -------------------------------------------------------------------------------- 1 | # TUF-on-CI Online Delegations Manual 2 | 3 | > [!WARNING] 4 | > This functionality is still experimental. Changes in the API and 5 | > behaviour may happen in future releases. 6 | 7 | TUF-on-CI now supports "online" delegations, which refers to a 8 | delegator using an "online" key, such as a cloud KMS. 9 | 10 | The difference is that now there is an action that can be called to 11 | have TUF-on-CI automatically sign updated metadata files. 12 | 13 | The option to use an online signer is given when a role is 14 | modified. When configuring a role the cli now provides this choice: 15 | 16 | ``` 17 | Enter name of role to modify: targets 18 | Modifying delegation for targets 19 | 20 | Configuring role targets 21 | 1. Configure signers: [@-test-user-1], requiring 1 signatures 22 | 2. Configure expiry: Role expires in 365 days, re-signing starts 60 days before expiry 23 | Please choose an option or press enter to continue: 1 24 | Choose what keytype to use: 25 | 1. Configure offline signers: 26 | 2. Configure online signers 27 | ``` 28 | 29 | If online signers is selected, configuration similar to when 30 | configuring the online role (snapshot and timestamp) is required. 31 | 32 | ## Limitations 33 | 34 | This feature is still experimental, and also there are some known issues 35 | 36 | * **Dispatching of delegation workflows**: Due to GitHub's limitation 37 | on GitHub Action invocation based on changes made by the default 38 | GitHub Action token, automated signing may be required to be 39 | dispatched manually. 40 | 41 | A solution to this can be to use a different token, such as a PAT or 42 | an OAuth application. 43 | 44 | * **Number of signers**: Currently only a single online signer can be 45 | configured for a delegation. This also means that there can not be a 46 | combination of offline and online signers. As online keys are mostly 47 | used for automated signing, this limitation should not impose any 48 | practical problems. 49 | 50 | ## Use cases 51 | 52 | This could be used when a deployment wants to programatically add 53 | content (targets) to a TUF repository, and rely on automated 54 | signing. The operator of the repository would still need to provide 55 | some custom automation, such as how to modify the repository and push 56 | the changes, how to approve pull requests and so on. 57 | 58 | ## Operation 59 | 60 | To automate signing, the primary API is the [online sign targets 61 | action](actions/online-sign-targets/action.yml). In the default 62 | configuration, this action is not used and so must explicitly be 63 | called. 64 | 65 | The preferred method is to create a GitHub Actions job that is run 66 | when one or more metadata files for delegations are modified. This 67 | example shows how changes to `foo-delegation` can be signed 68 | automatically. This Example uses Azure KMS, but GCP and AWS KMSes are 69 | also supported. 70 | 71 | ```yml 72 | name: Foo Delegate Signing 73 | 74 | permissions: {} 75 | 76 | on: 77 | workflow_dispatch: 78 | push: 79 | branches: ['sign/**'] 80 | paths: 81 | - metadata/foo-delegate.json 82 | jobs: 83 | sign-and-push: 84 | name: TUF-on-CI Foo Delegate sign 85 | runs-on: ubuntu-latest 86 | permissions: 87 | contents: write # for making commits in signing event and for modifying draft state 88 | pull-requests: write # for modifying signing event pull requests 89 | actions: write # for dispatching another signing-event workflow 90 | steps: 91 | 92 | - name: Sign delegation 93 | uses: theupdateframework/tuf-on-ci/actions/online-sign-targets@main 94 | with: 95 | azure_client_id: secrets.AZURE_CLIENT_ID 96 | azure_tenant_id: secrets.AZURE_TENANT_ID 97 | azure_subscription_id: secrets.AZURE_SUBSCRIPTION_ID 98 | targets_to_sign: foo-delegate 99 | token: ${{ secrets.TUF_ON_CI_TOKEN || secrets.GITHUB_TOKEN }} 100 | ``` 101 | 102 | The steps to work with automated delegation signing would thus be: 103 | 104 | 1. A signing event ( e.g. `sign/update-foo-target`) branch is created 105 | and pushed 106 | 1. TUF-on-CI creates a signing event PR 107 | 1. TUF-on-CI detects changed targets and updates delegation metadata 108 | and commits the changes 109 | 1. Automated signing detects changes to delegation metadata and signs 110 | it, and commits the changes 111 | 1. TUF-on-CI moves signing event PR from draft to ready for review 112 | 113 | The PR can then be reviewed as normal and merged when ready as for any 114 | signing event. 115 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | ## Developer notes 2 | 3 | A development install can be made in any environment but venv is recommended: 4 | 5 | ```shell 6 | # Clone the project 7 | git clone https://github.com/theupdateframework/tuf-on-ci.git 8 | cd tuf-on-ci 9 | # Create virtual environment 10 | python3 -m venv .venv 11 | # Enter environment 12 | source .venv/bin/activate 13 | # install the signing and repository tools as editable 14 | pip install -e ./signer -e ./repo 15 | # install tox for a reproducible testing environment 16 | pip install tox 17 | ``` 18 | 19 | At this point `tuf-on-ci-sign` and other commands are available from the editable install (source code). 20 | 21 | ### Running tests and linters 22 | 23 | Tests and lints can be run with tox: 24 | 25 | ```shell 26 | # Run all lints 27 | tox -m lint 28 | 29 | # run all tests 30 | tox -m test 31 | ``` 32 | 33 | ### Trying things out without pushing changes to remote 34 | 35 | `tuf-on-ci-sign` and `tuf-on-ci-delegate` can be run with `--no-push` to prevent the push to 36 | the remote signing event branch: instead a local branch of the same name will be created 37 | (note that you are responsible for that being possible). This branch can be pushed 38 | manually after inspection and it will work as if the push was done by the tool itself. 39 | 40 | ### Debugging repository tools 41 | 42 | The same tools (`tuf-on-ci-status`, `tuf-on-ci-update-targets`) that run during the 43 | signing event automation can be run locally to inspect the current status of the signing 44 | event branch. Note that the repository tools only operate on current commit (unlike the 45 | signing tools that always checkout the remote branch). 46 | 47 | As an example, this would be the markdown output when an open invitation exists 48 | for a new user to become a root key holder: 49 | 50 | ```shell 51 | $ git fetch && git checkout sign/add-fakeuser 52 | ... 53 | $ tuf-on-ci-status 54 | ### Current signing event state 55 | Event [sign/add-fakeuser](../compare/sign/add-fakeuser) 56 | #### :x: root 57 | root delegations have open invites (@-fakeuser). 58 | Invitees can accept the invitations by running `tuf-on-ci-sign sign/add-fakeuser` 59 | $ 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/ONLINE-SIGNING-SETUP.md: -------------------------------------------------------------------------------- 1 | # Online Signing in TUF-on-CI 2 | 3 | When a TUF-on-CI repository is initialized, an online signing method is chosen. This 4 | choice can be changed later. The chosen method will be used by the repository to sign 5 | `timestamp` and `snapshot` roles automatically. 6 | 7 | Currently supported signing methods include 8 | * Sigstore (experimental) 9 | * Google Cloud KMS 10 | * Azure Key Vault 11 | * AWS KMS 12 | 13 | ## Configuration 14 | 15 | ### Sigstore 16 | 17 | Using sigstore as the online signing method requires no configuration but is 18 | currently experimental (and not supported by all TUF client libraries) 19 | 20 | ### Google Cloud KMS 21 | 22 | 1. Make sure Google Cloud Workload Identity Federation allows your GitHub repositorys OIDC identity to sign 23 | with a KMS key. 24 | 1. Define your authentication details as repository variables in _Settings->Secrets and variables->Actions->Variables_: 25 | ``` 26 | GCP_WORKLOAD_IDENTITY_PROVIDER: projects/843741030650/locations/global/workloadIdentityPools/git-repo-demo/providers/git-repo-demo 27 | GCP_SERVICE_ACCOUNT: git-repo-demo@python-tuf-kms.iam.gserviceaccount.com 28 | ``` 29 | 1. _(only needed for initial configuration)_ Prepare your local environment for accessing the cloud KMS: 30 | Use [gcloud](https://cloud.google.com/sdk/docs/install) and authenticate in the 31 | environment where you plan to run `tuf-on-ci-delegate` tool (you will need 32 | _roles/cloudkms.publicKeyViewer_ permission on KMS) 33 | 34 | ### Azure Key Vault 35 | 36 | 1. Make sure Azure allows this repository OIDC identity to sign with a Key Vault key. 37 | 1. Define `AZURE_CLIENT_ID`, `AZURE_TENANT_ID` and `AZURE_SUBSCRIPTION_ID` as repository 38 | secrets in _Settings->Secrets and variables->Actions->Secrets_ 39 | 1. Modify the online-sign workflow like this: 40 | ```yaml 41 | jobs: 42 | online-sign: 43 | runs-on: ubuntu-latest 44 | 45 | permissions: 46 | id-token: 'write' # for OIDC identity access 47 | contents: 'write' # for committing snapshot/timestamp changes 48 | actions: 'write' # for dispatching publish workflow 49 | 50 | steps: 51 | ... 52 | - name: Login to Azure 53 | uses: azure/login@v1 54 | with: 55 | client-id: ${{ secrets.AZURE_CLIENT_ID }} 56 | tenant-id: ${{ secrets.AZURE_TENANT_ID }} 57 | subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} 58 | ... 59 | - id: online-sign 60 | uses: theupdateframework/tuf-on-ci/actions/online-sign@main 61 | ``` 62 | 1. _(only needed for initial configuration)_ Prepare your local environment: Use [az 63 | login](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) 64 | and authenticate against the environment where the key vault 65 | exists. You will need to the role _"Key Vault Crypto User"_). 66 | 67 | ### AWS KMS 68 | 69 | 1. Make sure AWS IAM permissions allow your GitHub repositorys OIDC identity to sign with a KMS key. 70 | 1. Define your authentication details as repository variables in _Settings->Secrets and variables->Actions->Variables_: 71 | ``` 72 | AWS_ROLE_TO_ASSUME: arn:aws:iam::175142243308:role/tuf-testing-online-key 73 | AWS_REGION: us-east-1 74 | ``` 75 | 1. _(only needed for initial configuration)_ Prepare your local environment for accessing the cloud KMS: 76 | Use the [AWS CLI](https://aws.amazon.com/cli/) and authenticate in the 77 | environment where you plan to run `tuf-on-ci-delegate` tool (you will need permission to use 78 | the KMS key). 79 | -------------------------------------------------------------------------------- /docs/RELEASE.md: -------------------------------------------------------------------------------- 1 | ## Release process 2 | 3 | 1. Ensure `docs/CHANGELOG.md` contains a summary of notable changes since the 4 | prior release. Check that all required changes to workflows that 5 | call our actions are clearly documented. 6 | 2. Update version number in `signer/tuf_on_ci_sign/__init__.py` and `repo/tuf_on_ci/_version.py` 7 | 3. Create a PR with the updated CHANGELOG and version bumps. 8 | 4. Once the PR is merged, create a signed tag for the version number on the merge commit 9 | `git tag --sign vA.B.C -m "vA.B.C"` 10 | 6. Push the tag to GitHub `git push origin vA.B.C`. This triggers release workflow 11 | 7. [Review deployment](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments) 12 | on GitHub. On approval both the PyPI signer release and the GitHub release will be deployed 13 | 14 | 15 | After release, update tuf-on-ci-template actions to use the new release. 16 | -------------------------------------------------------------------------------- /docs/REPOSITORY-MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # TUF-on-CI Repository Maintenance Manual 2 | 3 | This page documents the initial setup of a TUF-on-CI repository as well as the 4 | ongoing maintenance. 5 | 6 | ## New Repository Setup 7 | 8 | 1. [Create new repository](https://github.com/new?template_name=tuf-on-ci-template&template_owner=theupdateframework) 9 | using the tuf-on-ci template: the created repository contains all the required workflows. 10 | 1. Configure the new repository: 11 | * set _Settings->Pages->Source_ to `GitHub Actions` 12 | * Change _Settings->Environments->github-pages_ deployment branch from `main` to 13 | `publish` 14 | * Check _Settings->Actions->General->Allow GitHub Actions to create and approve pull requests_ 15 | (not required if you are using a custom token, see below) 16 | 1. Clone the repository locally and [configure your local signing tool](SIGNER-SETUP.md) 17 | 1. Choose your online signing method and [configure it](ONLINE-SIGNING-SETUP.md): 18 | * Google Cloud KMS, Azure Key Vault, and AWS KMS are fully supported 19 | * Sigstore requires no configuration (but is experimental) 20 | 1. Run `tuf-on-ci-delegate sign/init` to configure the repository and to start the 21 | first signing event 22 | * The tool prompts for various repository details and finally prompts to 23 | sign and push the initial metadata to a signing event branch 24 | 1. When this initial signing event branch is merged, the repository generates the 25 | first snapshot and timestamp, and publishes the first repository version 26 | 27 | ## Modifying roles and creating new ones 28 | 29 | Modifying a role is needed when: 30 | * A new delegated role is created 31 | * A new signer is invited to a role 32 | * A signer is removed from a role 33 | * The required threshold of signatures is changed 34 | 35 | Roles are modified with `tuf-on-ci-delegate `. 36 | * The event name can be chosen freely (and will be used as a branch name). If the signing 37 | event does not exist yet, it will be created as a result. 38 | * The tool will prompt for new signers and other details, and then prompt to push changes 39 | to the repository. 40 | * The push triggers creation of a signing event pull request. The repository will report the 41 | status of the signing event in the pull request and will notify signers there. 42 | 43 | ### Examples 44 | 45 | TODO: Example: Creating a new delegated role 46 | 47 | TODO: Example: Removing a signer 48 | 49 |
50 | Example: Inviting a new root signer 51 | In this example the root signers list contains a single signer, but it is modified to contain 52 | two signers instead. The process is: 53 | 54 | * tuf-on-ci-delegate is used to modify signers 55 | * the new signer accepts the invitation and adds their keys to the delegating role's metadata 56 | * the signers of the delegating role must accept the new key by signing the new 57 | version of delegating metadata 58 | 59 | ```shell 60 | $ tuf-on-ci-delegate sign/add-fakeuser-2 root 61 | 62 | Remote branch not found: branching off from main 63 | Modifying delegation for root 64 | 65 | Configuring role root 66 | 1. Configure signers: [@-fakeuser-1], requiring 1 signatures 67 | 2. Configure expiry: Role expires in 365 days, re-signing starts 60 days before expiry 68 | Please choose an option or press enter to continue: 1 69 | Please enter list of root signers [@-fakeuser-1]: @-fakeuser-1,@-fakeuser-2 70 | Please enter root threshold [1]: 71 | 1. Configure signers: [@-fakeuser-1, @-fakeuser-2], requiring 1 signatures 72 | 2. Configure expiry: Role expires in 365 days, re-signing starts 60 days before expiry 73 | Please choose an option or press enter to continue: 74 | ... 75 | ``` 76 | 77 | Once finished the changes are pushed to the signing event branch 78 | which in the above example is `sign/add-fakueuser-2`. 79 | 80 | The repository automation runs the [signing 81 | automation](https://github.com/theupdateframework/tuf-on-ci-template/blob/main/.github/workflows/signing-event.yml) 82 | that creates PRs with comments documenting current signing event state 83 | and tags each signer. These comments (along with the PR commits) should 84 | provide signers with a clear view of what is happening in the signing 85 | event. 86 | 87 | To accept the invitation and become a signer, the invitee runs 88 | `tuf-on-ci-sign ` and provides information on what key to 89 | use. 90 | 91 | After this the delegating role signers (in this case root signers) accept 92 | the new key by signing the delegating metadata version. 93 |
94 | 95 | ## Configuration and modifying workflows 96 | 97 | tuf-on-ci workflows (with the exception of `publish`) are written in a way to minimize 98 | need to modify the workflows: It may be useful to consider the workflows part of the 99 | tuf-on-ci application. The intention with this is to make workflow upgrades easier: 100 | tuf-on-ci release notes will mention when workflows change and typically the suggested 101 | upgrade mechanism is to copy the modified workflows from tuf-on-ci-template. 102 | 103 | Supported ways to configure and modify tuf-on-ci workflows: 104 | * online signing is configured using signing method specific _Repository Variables_, 105 | see [ONLINE-SIGNING-SETUP.md](ONLINE-SIGNING-SETUP.md) for details 106 | * A custom GitHub token can be optionally configured with _Repository Secret_ 107 | `TUF_ON_CI_TOKEN`, see details below 108 | * Workflow failure messages can be configured with `.github/TUF_ON_CI_TEMPLATE/failure.md`: 109 | Contents of this file will be included in issues that are opened if workflows fail. This is 110 | useful to e.g. notify the maintenance team with @-mentions. 111 | * Signing pull request templates can be configured with 112 | `.github/PULL_REQUEST_TEMPLATE/signing_event.md`. Contents of this file will be included in 113 | the pull request message when non-maintainer signers contribute to signing events. This is 114 | useful to e.g. notify the maintenance team with @-mentions. 115 | * The `publish` workflow can be customized to publish to a destination that is not 116 | the default GitHub Pages 117 | 118 | ### Custom GitHub token 119 | 120 | tuf-on-ci uses GITHUB_TOKEN by default but supports using a custom fine-grained Github 121 | token. This allows the project to limit the default GITHUB_TOKEN permissions 122 | (in practice this means other workflows in the repository can operate with this lower 123 | permission default token while tuf-on-ci workflows still have higher permissions). 124 | 125 | The custom token needs the following repository permissions: 126 | * `Actions: write` to dispatch other workflows when needed 127 | * `Contents: write` to create online signing commits, and to create targets metadata 128 | change commits in signing event 129 | * `Issues: write` to create issues on workflow failures 130 | * `Pull requests: write` to create and modify signing event pull requests 131 | 132 | To use a custom token, define a _repository secret_ `TUF_ON_CI_TOKEN` with a fine grained 133 | token as the secrets value. No workflow changes are needed. Note that all automated comments 134 | in signing event pull requests will be seemingly made by the account that created the custom 135 | token: Creating the token on a "bot" account is sensible for this reason. 136 | 137 | When a custom token is used, some repository security settings can be tightened: 138 | * _Settings->Actions->General->Allow GitHub Actions to create and approve pull requests_ 139 | can be disabled 140 | * Custom token owner (bot) can be added to _Allow specified actors to bypass required 141 | pull requests_ list in GitHub branch protection settings, and _Settings->Branches-> 142 | main->Require a pull request before merging_ can then be enabled 143 | -------------------------------------------------------------------------------- /docs/SIGNER-MANUAL.md: -------------------------------------------------------------------------------- 1 | # TUF-on-CI Signer Manual 2 | 3 | The purpose of A TUF-on-CI repository is to secure artifact delivery to 4 | downloaders. This is accomplished by _signers_ digitally signing TUF metadata using 5 | the `tuf-on-ci-sign` tool. 6 | 7 | This page documents `tuf-on-ci-sign` usage. 8 | 9 | :exclamation: For installation and configuration, see [SIGNER-SETUP.md](SIGNER-SETUP.md) 10 | 11 | ### Terms 12 | 13 | _Signer_: A person who has agreed to verify the integrity of artifact hashes and other 14 | metadata of a _role_ by signing that role's metadata with their personal signing method 15 | (e.g. a Yubikey). 16 | 17 | _Signing event_: Collaboration of one or more signers to produce and sign a new version of 18 | a role's metadata. A signing event happens in a GitHub pull request. Signing event names 19 | start with "sign/". 20 | 21 | _Role_: A role manages a set of artifacts and (optionally) a set of delegations to other 22 | roles. A role has a set of _signers_ (defined by the delegating role): their signatures 23 | are needed when the role is changed. 24 | The default delegation structure includes only a `root` role and a `targets` 25 | role (delegated by root). The targets role can further delegate to other roles. 26 | 27 | ## Usage 28 | 29 | Metadata is signed in a _signing event_. The signing event process is: 30 | * A signing event pull request gets created by the repository. This happens as a 31 | response to either a timed event (like an expiry date approaching) or as a response to 32 | artifact changes. Either way, the signing event contains new metadata versions that 33 | need to be signed before they are considered valid. 34 | * The signing event directs _signers_ to sign the changes using `tuf-on-ci-sign`. By 35 | signing they confirm that the proposed changes are correct. The local signing tool 36 | makes a commit with the signature pushes the commit to the remote signing event branch. 37 | * If a signer does not have push permissions for the GitHub repository, their signature 38 | is added to the signing event via PR from their fork to the signing event branch. 39 | * Finally, a Pull Request to merge the signing event into main is created. 40 | 41 | Throughout the process, the repository updates the signing event pull request with status 42 | reports. These reports in the signing event pull request function as a notification 43 | mechanism but *signers should only ever fully trust their local signing tool*. 44 | 45 | The signing tool works in the repository (git clone) directory -- note that 46 | fetching, pushing or switching branches is not necessary: the tool will always use an 47 | up-to-date signing event branch and when the signer decides to sign, the signature is 48 | automatically pushed to the signing event branch. 49 | 50 | ### Accepting an invitation 51 | 52 | When a signing event pull request invites to become a signer: 53 | ```shell 54 | $ tuf-on-ci-sign 55 | ``` 56 | * The tool prompts to select a signing method and prompts to push the public key 57 | and signature to the repository 58 | * If push and pull remotes are different in signer configuration, signer creates a 59 | Pull Request _from their fork to the signing event branch_. 60 | 61 | ### Signing a change 62 | 63 | When a signing event pull request instructs to sign a change: 64 | ```shell 65 | $ tuf-on-ci-sign 66 | ``` 67 | * The tool describes the changes, prompts to sign and prompts to push the signature to 68 | the repository 69 | * If push and pull remotes are different in signer configuration, signer creates a 70 | Pull Request _from their fork to the signing event branch_. 71 | 72 | 73 | ### Modifying artifacts 74 | 75 | Artifacts are stored in git (in the `targets/` directory) and are modified using normal 76 | git tools: the signing tool is not used. Artifact modification commits should get pushed to a 77 | branch on the repository (with a branch name starting with "sign/"): this creates a signing 78 | event for the artifact change allowing signers to sign that change. 79 | 80 | The role where the artifact belongs to is chosen with pathname: 81 | * files in the targets directory are artifacts managed by top level role "targets" 82 | * NB: only files in the top level `targets` directory are owned by the "targets" role 83 | (so `targets/somefile` is owned by "targets", but `targets/somedir/otherfile` is not) 84 | * files in a subdirectory are artifacts of the role with the same name (so 85 | `targets/A/file.txt` is an artifact managed by role "A") 86 | * NB: Four levels of directories are supported below each role directory 87 | (so `targets/A/dir1/dir2/dir3/dir4/file.txt`) is owned by "A", but 88 | `targets/A/dir1/dir2/dir3/dir4/dir5/file.txt` is not 89 | 90 |
91 | Example 92 | 93 | Artifact changes are committed into a signing event branch using git: 94 | ```shell 95 | # Add a new artifact managed by top level role targets 96 | $ git fetch && git switch -c sign/add-a-target origin/main 97 | $ echo "artifact" > targets/file1.txtv 98 | $ git add targets/file1.txt 99 | $ git commit -m "New artifact file1.txt, managed by targets" 100 | 101 | # Pushing the branch starts a signing event: Repository will create a new metadata 102 | # version for the role and signers can then review and sign that version. 103 | $ git push origin sign/add-a-target 104 | ``` 105 | 106 | After the signing event is created, signers can follow instructions to sign the changes. 107 |
108 | -------------------------------------------------------------------------------- /docs/SIGNER-SETUP.md: -------------------------------------------------------------------------------- 1 | # TUF-on-CI Signer Installation and Configuration 2 | 3 | ### Requirements 4 | 5 | `tuf-on-ci-sign` can be used to sign with either a hardware key with PIV support (e.g. 6 | a Yubikey) or a Sigstore identity. 7 | 8 | #### Hardware signing requirements 9 | 10 | A hardware signing key must contain a _PIV Digital Signature private key_ to be used with TUF-on-CI. 11 | TUF-on-CI also needs access to a PKCS#11 module. 12 | 13 | 1. Generate a PIV signing key on your hardware key if you don't have one yet. 14 | For YubiKey owners, follow the [YubiKey setup instructions](YUBIKEY-PIV-SETUP.md). 15 | 16 | 1. Install a PKCS#11 module. TUF-on-CI has been tested with the Yubico ykcs11. Debian users can install it with 17 | ```shell 18 | $ apt install ykcs11 19 | ``` 20 | macOS users can install with 21 | ```shell 22 | $ brew install yubico-piv-tool 23 | ``` 24 | 25 | > **_NOTE:_** Windows WSL users may need to attach a USB hardware device using [usbipd-win](https://learn.microsoft.com/en-us/windows/wsl/connect-usb) 26 | 27 | #### Sigstore signing requirements 28 | 29 | :warning: Sigstore signing is an experimental feature and may not be compatible with all TUF client implementations. 30 | 31 | To use Sigstore as a signing method, you will need an account in one of the compatible 32 | identity providers (GitHub, Google or Microsoft). 33 | 34 | ### Signing tool installation 35 | 36 | ```shell 37 | pip install tuf-on-ci-sign 38 | ``` 39 | 40 | Note: macOS users may have to install swig in case the above wheel build fails 41 | 42 | ```shell 43 | $ brew install swig 44 | ``` 45 | 46 | ### Local configuration 47 | 48 | 1. `git clone` the repository you are a signer for 49 | 1. If you are not a GitHub maintainer of the repository, fork the repository on GitHub 50 | and add your fork as a remote in your local git clone 51 | 1. Create a local configuration file `.tuf-on-ci-sign.ini` in the repository directory 52 | (either manually or by running the `signer/create-config-file.sh` script included in 53 | TUF-on-CI sources): 54 | 55 | ``` 56 | [settings] 57 | # Path to PKCS#11 module (optional) 58 | # If not provided, tuf-on-ci-sign will probe some known install locations 59 | # pykcs11lib = /usr/lib/x86_64-linux-gnu/libykcs11.so 60 | 61 | # GitHub username 62 | user-name = @my-github-username 63 | 64 | # pull-remote: the git remote name of the TUF repository 65 | pull-remote = origin 66 | 67 | # push-remote: If you are allowed to push to the TUF repository, you can use the same value 68 | # as pull-remote. Otherwise use the remote name of your fork 69 | push-remote = origin 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/YUBIKEY-PIV-SETUP.md: -------------------------------------------------------------------------------- 1 | # YubiKey PIV Setup 2 | 3 | This guide walks through using the YubiKey Manager UI to configure PIV for TUF signing operations. 4 | 5 | > [!IMPORTANT] 6 | > This is a general setup guide for using YubiKey with TUF-on-CI. When provisioning YubiKeys for production TUF use, you may want to consider additional procedures around the procurement, distribution, and configuration of the devices. (e.g. serial number tracking, offline device configuration, YubiKey Manager CLI, hardware random number generators, etc.) 7 | 8 | ### Requirements 9 | 10 | - Download [YubiKey manager](https://yubico.com/support/download/yubikey-manager/): 11 | ![Yubikey manager UI](yubikey-manager.png) 12 | 13 | > [!TIP] 14 | > Use https://www.yubico.com/genuine/ to confirm that your YubiKey device is genuine 15 | 16 | ### Update PIV PIN Defaults 17 | 18 | A new YubiKey is configured with a default PIN, PUK (PIN unlock code), and Management Key. 19 | 20 | The default PIN codes must be updated with new values that you remember or store securely. 21 | 22 | PIN codes are used for signing operations and to unlock a device. 23 | 24 | #### Reset PIV to Defaults 25 | 26 | > [!CAUTION] 27 | > Performing this operation will destroy all existing PIV data 28 | 29 | 1. Navigate to the `Applications` > `PIV` menu in the YubiKey Manager UI 30 | 1. Under `Reset`, select `Reset PIV` 31 | 32 | #### Set PIN 33 | 34 | The PIV PIN is used to perform PIV operations such as signing and decrypting. 35 | 36 | 1. Navigate to the `Applications` > `PIV` menu in the YubiKey Manager UI 37 | 1. Under `PIN Management`, select `Configure PINs` 38 | 1. Under `PIN`, select `Change PIN` 39 | 1. Select the `Use default` checkbox for `Current PIN` 40 | 1. Enter an 8-digit PIN that only you know, or generate a random PIN that you can store securely 41 | 1. Select `Change PIN` 42 | 43 | #### Set PUK (PIN unlock code) 44 | 45 | The PUK PIN is used to unclock a device after a number of failed PIN entry attempts. 46 | 47 | 1. Navigate to the `Applications` > `PIV` menu in the YubiKey Manager UI 48 | 1. Under `PIN Management`, select `Configure PINs` 49 | 1. Under `PUK`, select `Change PUK` 50 | 1. Select the `Use default` checkbox for `Current PUK` 51 | 1. Enter an 8-digit PIN that only you know, or generate a random PIN that you can store securely 52 | 1. Select `Change PUK` 53 | 54 | #### Set Management Key 55 | 56 | The management key is used to perform many YubiKey management operations, such as generating a key pair. 57 | 58 | 1. Navigate to the `Applications` > `PIV` menu in the YubiKey Manager UI 59 | 2. Under `PIN Management`, select `Configure PINs` 60 | 3. Under `Management Key`, select `Change Management Key` 61 | 4. Select the algorithm, recommend selecting `AES256` 62 | > [!NOTE] 63 | > The option to use an AES key is only available for [YubiKeys with firmware 5.4.2 or newer](https://docs.yubico.com/hardware/yubikey/yk-tech-manual/yk5-overview.html#piv-management-key-aes) 64 | 5. Select `Generate` or generate a management key outside of the UI and populate the value here 65 | 6. Select `Protect with PIN`, if you want to store the management key on the device 66 | 7. Select `Finish` 67 | 68 | ### Generate Digital Signature Certificate 69 | 70 | 1. Navigate to the `Applications` > `PIV` menu in the YubiKey Manager UI 71 | 1. Under `Certificates`, select `Configure Certificates` 72 | 1. Select `Digital Signature` 73 | 1. Select `Generate`, or import an existing digital signature certificate 74 | 1. Select `Self-signed certificate` unless you are using a CA to issue the certificate via a CSR 75 | 1. Select the algorithm to be used for digital signatures 76 | 1. Populate the `Subject` field, this can be anything (recommend using your GitHub handle) 77 | 1. Select an `Expiration date`, this is the date the signing cert expires but the signing keys can still be used for TUF-on-CI after expiration 78 | 1. Select `Generate` 79 | 80 | After generating the digital signature certificate, continue the TUF-on-CI [signer setup process](SIGNER-SETUP.md). 81 | -------------------------------------------------------------------------------- /docs/yubikey-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/docs/yubikey-manager.png -------------------------------------------------------------------------------- /repo/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | TUF-on-CI 4 | Copyright 2022-2023 repository-playground contributors 5 | Copyright 2023-2025 TUF-on-CI contributors 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /repo/README.md: -------------------------------------------------------------------------------- 1 | ## CI tools for TUF-on-CI 2 | 3 | These commands are used by the GitHub actions in the [actions directory](../actions/). There should be no reason to install or use them elsewhere (except for debugging and testing). 4 | 5 | ### Installation 6 | 7 | Development install: `pip install -e .` 8 | 9 | ### Usage 10 | 11 | `tuf-on-ci-status [--push]`: Prints status of the signing event (aka current branch) based on the changes done in the signing event compared to the starting point of the event. Creates commits in the signing event branch, making the artifact hashes match current artifacts. If `--push` is used, the changes are pushed to signing event branch. Returns 0 if the signing event changes are correctly signed. 12 | 13 | `tuf-on-ci-online-sign [--push]`: Updates snapshot & timestamp based on current repository content. If `--push` is used, the changes are pushed to main branch. 14 | 15 | `tuf-on-ci-build-repository --metadata METADATA_DIR [--artifacts ARTIFACT_DIR]`: Creates a publishable versions of metadata and artifacts in given directories. 16 | 17 | `tuf-on-ci-create-signing-events [--push]`: Creates version bump commits for offline signed roles that are close to expiry. If `--push` is used, the changes are pushed to signing event branches (branch per role): the signing event names are printed on stdout. 18 | -------------------------------------------------------------------------------- /repo/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "tuf-on-ci" 7 | description = "TUF-on-CI repository tools, intended to be executed on a CI system" 8 | license = "MIT" 9 | license-files = [ "LICENSE" ] 10 | readme = "README.md" 11 | dependencies = [ 12 | "securesystemslib[awskms, azurekms, gcpkms, sigstore] ~= 1.2", 13 | "tuf >= 5.1,< 7.0", 14 | "click ~= 8.1", 15 | ] 16 | requires-python = ">=3.10" 17 | dynamic = ["version"] 18 | 19 | [project.urls] 20 | issues = "https://github.com/theupdateframework/tuf-on-ci/issues" 21 | source = "https://github.com/theupdateframework/tuf-on-ci/" 22 | 23 | [project.scripts] 24 | tuf-on-ci-build-repository = "tuf_on_ci:build_repository" 25 | tuf-on-ci-test-client = "tuf_on_ci:client" 26 | tuf-on-ci-create-signing-events = "tuf_on_ci:create_signing_events" 27 | tuf-on-ci-online-sign = "tuf_on_ci:online_sign" 28 | tuf-on-ci-online-sign-targets = "tuf_on_ci:online_sign_targets" 29 | tuf-on-ci-status = "tuf_on_ci:status" 30 | tuf-on-ci-update-targets = "tuf_on_ci:update_targets" 31 | 32 | [project.optional-dependencies] 33 | lint = [ 34 | "mypy == 1.16.0", 35 | "ruff == 0.11.12", 36 | ] 37 | 38 | [tool.hatch.version] 39 | path = "tuf_on_ci/_version.py" 40 | 41 | [[tool.mypy.overrides]] 42 | module = [ 43 | "securesystemslib.*", 44 | "sigstore.*", 45 | ] 46 | ignore_missing_imports = "True" 47 | 48 | [tool.ruff.lint] 49 | select = [ 50 | "ARG", # flake8-unused-arguments 51 | "B", # flake8-bugbear 52 | "BLE", # flake8-blind-except 53 | "C4", # flake8-comprehensions 54 | "E", # pycodestyle errors 55 | "F", # pyflakes 56 | "I", # isort 57 | "LOG", # flake8-logging 58 | "N", # pep8-naming 59 | "RET", # flake8-return 60 | "RUF", # ruff-specific rules 61 | "S", # flake8-bandit 62 | "SIM", # flake8-simplify 63 | "UP", # pyupgrade 64 | "W", # pycodestyle warnings 65 | ] 66 | ignore = [ 67 | "S101", # Use of `assert` detected 68 | "S603", # `subprocess` call: check for execution of untrusted input 69 | ] -------------------------------------------------------------------------------- /repo/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/repo/test/__init__.py -------------------------------------------------------------------------------- /repo/test/test_ci_repository.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import unittest 4 | from tempfile import TemporaryDirectory 5 | 6 | from tuf_on_ci._repository import CIRepository 7 | from tuf_on_ci.signing_event import _find_changed_target_roles 8 | 9 | 10 | class TestCIRepository(unittest.TestCase): 11 | def test_non_existing_repo(self): 12 | repo = CIRepository("no_such_file") 13 | self.assertRaises(ValueError, repo.open, "root") 14 | 15 | def test_signing_expiry_days_root(self): 16 | repo = CIRepository("test/test_repo1") 17 | 18 | signing_days, expiry_days = repo.signing_expiry_period("root") 19 | self.assertEqual(signing_days, 60) 20 | self.assertEqual(expiry_days, 365) 21 | 22 | def test_signing_expiry_days_targets(self): 23 | repo = CIRepository("test/test_repo1") 24 | 25 | signing_days, expiry_days = repo.signing_expiry_period("targets") 26 | self.assertEqual(signing_days, 40) 27 | self.assertEqual(expiry_days, 123) 28 | 29 | def test_signing_expiry_days_role(self): 30 | repo = CIRepository("test/test_repo2") 31 | 32 | signing_days, expiry_days = repo.signing_expiry_period("timestamp") 33 | self.assertEqual(signing_days, 6) 34 | self.assertEqual(expiry_days, 40) 35 | 36 | def test_default_signing_days(self): 37 | repo = CIRepository("test/test_repo1") 38 | 39 | signing_days, expiry_days = repo.signing_expiry_period("timestamp") 40 | self.assertEqual(signing_days, 2) 41 | self.assertEqual(expiry_days, 4) 42 | 43 | # def test_bump_expires_expired(self): 44 | # repo = CIRepository("test/test_repo1") 45 | # ver = repo.bump_expiring("timestamp") 46 | # self.assertEqual(ver, 2) 47 | 48 | def test_target_loading(self): 49 | repo_path = "test/test_repo3" 50 | good_meta = os.path.join(repo_path, "good/metadata") 51 | with TemporaryDirectory("_tuf_on_ci") as temp_dir: 52 | temp_meta = os.path.join(temp_dir, "metadata") 53 | temp_targets = os.path.join(temp_dir, "targets") 54 | os.makedirs(temp_targets) 55 | os.makedirs(temp_meta) 56 | src_targets = os.path.join(repo_path, "src_targets") 57 | shutil.copytree(good_meta, temp_meta, dirs_exist_ok=True) 58 | repo = CIRepository(temp_meta, good_meta) 59 | targets = repo.targets("targets") 60 | 61 | # no targets exist on disk yet, only in metadata 62 | self.assertIn("tfile1.txt", targets.targets) 63 | 64 | # updating removes them because they're not on disk 65 | repo.update_targets("targets") 66 | targets = repo.targets("targets") 67 | self.assertNotIn("tfile1.txt", targets.targets) 68 | 69 | # adding these files and updating adds them to targets 70 | shutil.copy(os.path.join(src_targets, "tfile1.txt"), temp_targets) 71 | repo.update_targets("targets") 72 | targets = repo.targets("targets") 73 | self.assertIn("tfile1.txt", targets.targets) 74 | self.assertEqual(len(targets.targets), 1) 75 | 76 | # targets does not support multiple levels right now 77 | shutil.copytree( 78 | os.path.join(src_targets, "other_dir"), 79 | os.path.join(temp_targets, "other_dir"), 80 | ) 81 | # on update, it should be in targets 82 | repo.update_targets("targets") 83 | targets = repo.targets("targets") 84 | self.assertNotIn("other_dir/otherfile.txt", targets.targets) 85 | self.assertEqual(len(targets.targets), 1) 86 | 87 | # nothing in myrole until some files are added 88 | repo.update_targets("myrole") 89 | targets = repo.targets("myrole") 90 | self.assertEqual(len(targets.targets), 0) 91 | 92 | shutil.copytree( 93 | os.path.join(src_targets, "myrole"), 94 | os.path.join(temp_targets, "myrole"), 95 | ) 96 | repo.update_targets("myrole") 97 | targets = repo.targets("myrole") 98 | self.assertEqual(len(targets.targets), 4) 99 | self.assertIn("myrole/file0.txt", targets.targets) 100 | self.assertIn("myrole/dir1/dir2/dir3/file3.txt", targets.targets) 101 | self.assertNotIn("myrole/dir1/dir2/dir3/dir4/file4.txt", targets.targets) 102 | 103 | # existing roles without deep paths are honored (deeper targets are ignored) 104 | shutil.copytree( 105 | os.path.join(src_targets, "oldrole"), 106 | os.path.join(temp_targets, "oldrole"), 107 | ) 108 | repo.update_targets("oldrole") 109 | targets = repo.targets("oldrole") 110 | self.assertEqual(len(targets.targets), 1) 111 | self.assertIn("oldrole/file0.txt", targets.targets) 112 | self.assertNotIn("oldrole/dir1/file1.txt", targets.targets) 113 | 114 | # changed roles are detected properly 115 | roles = _find_changed_target_roles(repo, temp_targets, "targets") 116 | self.assertSetEqual( 117 | roles, {"myrole", "targets", "oldrole"}, "unexpect roles found" 118 | ) 119 | 120 | 121 | if __name__ == "__main__": 122 | unittest.main() 123 | -------------------------------------------------------------------------------- /repo/test/test_repo1/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999": { 14 | "keytype": "ecdsa", 15 | "keyval": { 16 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 17 | }, 18 | "scheme": "ecdsa-sha2-nistp384", 19 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 20 | }, 21 | "fa47289": { 22 | "keytype": "ed25519", 23 | "keyval": { 24 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 25 | }, 26 | "scheme": "ed25519", 27 | "x-tuf-on-ci-online-uri": "envvar:LOCAL_TESTING_KEY" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "fa47289" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 365 43 | }, 44 | "targets": { 45 | "keyids": [ 46 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 47 | ], 48 | "threshold": 1 49 | }, 50 | "timestamp": { 51 | "keyids": [ 52 | "fa47289" 53 | ], 54 | "threshold": 1, 55 | "x-tuf-on-ci-expiry-period": 4 56 | } 57 | }, 58 | "spec_version": "1.0.31", 59 | "version": 1, 60 | "x-tuf-on-ci-expiry-period": 365, 61 | "x-tuf-on-ci-signing-period": 60 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /repo/test/test_repo1/targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 1, 14 | "x-tuf-on-ci-expiry-period": 123, 15 | "x-tuf-on-ci-signing-period": 40 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /repo/test/test_repo1/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "fa47289", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-04T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /repo/test/test_repo2/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999": { 14 | "keytype": "ecdsa", 15 | "keyval": { 16 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 17 | }, 18 | "scheme": "ecdsa-sha2-nistp384", 19 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 20 | }, 21 | "fa47289": { 22 | "keytype": "ed25519", 23 | "keyval": { 24 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 25 | }, 26 | "scheme": "ed25519", 27 | "x-tuf-on-ci-online-uri": "envvar:LOCAL_TESTING_KEY" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "fa47289" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 365 43 | }, 44 | "targets": { 45 | "keyids": [ 46 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 47 | ], 48 | "threshold": 1 49 | }, 50 | "timestamp": { 51 | "keyids": [ 52 | "fa47289" 53 | ], 54 | "threshold": 1, 55 | "x-tuf-on-ci-expiry-period": 40, 56 | "x-tuf-on-ci-signing-period": 6 57 | } 58 | }, 59 | "spec_version": "1.0.31", 60 | "version": 1, 61 | "x-tuf-on-ci-expiry-period": 365, 62 | "x-tuf-on-ci-signing-period": 60 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /repo/test/test_repo2/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "fa47289", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-04T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /repo/test/test_repo3/good/metadata/myrole.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "e642e70171046d6d97efdea76792c373d863c55c054c4287c999c62c6011120f", 5 | "sig": "" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2025-03-07T22:16:44Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 1, 14 | "x-tuf-on-ci-expiry-period": 365, 15 | "x-tuf-on-ci-signing-period": 60 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /repo/test/test_repo3/good/metadata/oldrole.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "e642e70171046d6d97efdea76792c373d863c55c054c4287c999c62c6011120f", 5 | "sig": "" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2025-03-07T22:16:44Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 1, 14 | "x-tuf-on-ci-expiry-period": 365, 15 | "x-tuf-on-ci-signing-period": 60 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /repo/test/test_repo3/good/metadata/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999": { 14 | "keytype": "ecdsa", 15 | "keyval": { 16 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 17 | }, 18 | "scheme": "ecdsa-sha2-nistp384", 19 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 20 | }, 21 | "fa47289": { 22 | "keytype": "ed25519", 23 | "keyval": { 24 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 25 | }, 26 | "scheme": "ed25519", 27 | "x-tuf-on-ci-online-uri": "envvar:LOCAL_TESTING_KEY" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "fa47289" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 365 43 | }, 44 | "targets": { 45 | "keyids": [ 46 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 47 | ], 48 | "threshold": 1 49 | }, 50 | "timestamp": { 51 | "keyids": [ 52 | "fa47289" 53 | ], 54 | "threshold": 1, 55 | "x-tuf-on-ci-expiry-period": 4 56 | } 57 | }, 58 | "spec_version": "1.0.31", 59 | "version": 1, 60 | "x-tuf-on-ci-expiry-period": 365, 61 | "x-tuf-on-ci-signing-period": 60 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /repo/test/test_repo3/good/metadata/snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3", 5 | "sig": "3045022056ab47c4a391473363f56f39379004f536e047a4bb1a2d7cae15782767655acd022100a4dc9d203a3b3e355fb2af2450dbe4f94d9097c14f00650ed37cb95623f1f122" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2025-03-08T15:24:43Z", 11 | "meta": { 12 | "myrole.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /repo/test/test_repo3/good/metadata/targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999", 5 | "sig": "" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "delegations": { 11 | "keys": { 12 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999": { 13 | "keytype": "ecdsa", 14 | "keyval": { 15 | "public": "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 16 | }, 17 | "scheme": "ecdsa-sha2-nistp256" 18 | } 19 | }, 20 | "roles": [ 21 | { 22 | "keyids": [ 23 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 24 | ], 25 | "name": "myrole", 26 | "paths": [ 27 | "myrole/*", 28 | "myrole/*/*", 29 | "myrole/*/*/*", 30 | "myrole/*/*/*/*" 31 | ], 32 | "terminating": true, 33 | "threshold": 1 34 | }, 35 | { 36 | "keyids": [ 37 | "95da323daa78f7b2557ae91e23be619ff932f9aec035abd4e40301405b363999" 38 | ], 39 | "name": "oldrole", 40 | "paths": [ 41 | "oldrole/*" 42 | ], 43 | "terminating": true, 44 | "threshold": 1 45 | } 46 | ] 47 | }, 48 | "expires": "2024-07-15T23:18:30Z", 49 | "spec_version": "1.0.31", 50 | "targets": { 51 | "tfile1.txt": { 52 | "hashes": { 53 | "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 54 | }, 55 | "length": 0 56 | }, 57 | "tfile2.txt": { 58 | "hashes": { 59 | "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 60 | }, 61 | "length": 0 62 | } 63 | }, 64 | "version": 2, 65 | "x-tuf-on-ci-expiry-period": 123, 66 | "x-tuf-on-ci-signing-period": 40 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /repo/test/test_repo3/good/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "fa47289", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-04T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/myrole/dir1/dir2/dir3/dir4/file4.txt: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/myrole/dir1/dir2/dir3/file3.txt: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/myrole/dir1/dir2/file2.txt: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/myrole/dir1/file1.txt: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/myrole/file0.txt: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/oldrole/dir1/file1.txt: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/oldrole/file0.txt: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/other_dir/otherfile.txt: -------------------------------------------------------------------------------- 1 | hello tuf 2 | -------------------------------------------------------------------------------- /repo/test/test_repo3/src_targets/tfile1.txt: -------------------------------------------------------------------------------- 1 | tfile1.txt 2 | -------------------------------------------------------------------------------- /repo/tuf_on_ci/__init__.py: -------------------------------------------------------------------------------- 1 | from tuf_on_ci._version import __version__ 2 | from tuf_on_ci.build_repository import build_repository 3 | from tuf_on_ci.client import client 4 | from tuf_on_ci.create_signing_events import create_signing_events 5 | from tuf_on_ci.online_sign import online_sign 6 | from tuf_on_ci.signing_event import online_sign_targets, status, update_targets 7 | 8 | __all__ = [ 9 | "__version__", 10 | "build_repository", 11 | "client", 12 | "create_signing_events", 13 | "online_sign", 14 | "online_sign_targets", 15 | "status", 16 | "update_targets", 17 | ] 18 | -------------------------------------------------------------------------------- /repo/tuf_on_ci/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.16.1" 2 | -------------------------------------------------------------------------------- /repo/tuf_on_ci/build_repository.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | """Command line tool to build a publishable TUF-on-CI repository""" 4 | 5 | import logging 6 | import os 7 | import subprocess 8 | from datetime import UTC, datetime, timedelta 9 | from urllib import parse 10 | 11 | import click 12 | from tuf.api.metadata import Root, Signed, Targets 13 | 14 | from tuf_on_ci._repository import CIRepository 15 | from tuf_on_ci._version import __version__ 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | def _git(cmd: list[str]) -> subprocess.CompletedProcess: 21 | cmd = [ 22 | "git", 23 | "-c", 24 | "user.name=tuf-on-ci", 25 | "-c", 26 | "user.email=41898282+github-actions[bot]@users.noreply.github.com", 27 | *cmd, 28 | ] 29 | proc = subprocess.run(cmd, check=True, capture_output=True, text=True) 30 | logger.debug("%s:\n%s", cmd, proc.stdout) 31 | return proc 32 | 33 | 34 | def build_description(repo: CIRepository) -> str: 35 | lines = [ 36 | "## TUF Repository state", 37 | "", 38 | "| Role | Signing starts | Expires | Signers |", 39 | "| - | - | - | - |", 40 | ] 41 | root = repo.root() 42 | targets = repo.targets() 43 | roles: list[tuple[Root | Targets, str]] = [ 44 | (root, "root"), 45 | (root, "timestamp"), 46 | (root, "snapshot"), 47 | (root, "targets"), 48 | ] 49 | if targets.delegations and targets.delegations.roles: 50 | for rolename in targets.delegations.roles: 51 | roles.append((targets, rolename)) 52 | 53 | for delegator, rolename in roles: 54 | role = delegator.get_delegated_role(rolename) 55 | keys = [delegator.get_key(keyid) for keyid in role.keyids] 56 | signers = [] 57 | for key in keys: 58 | owner = key.unrecognized_fields.get("x-tuf-on-ci-keyowner", "_online key_") 59 | signers.append(owner) 60 | 61 | delegate: Signed = repo.open(rolename).signed 62 | if rolename == "timestamp": 63 | json_link = f"{rolename}.json" 64 | else: 65 | json_link = f"{delegate.version}.{rolename}.json" 66 | expiry = delegate.expires 67 | signing_days, _ = repo.signing_expiry_period(rolename) 68 | signing = expiry - timedelta(days=signing_days) 69 | signing_date = signing.strftime("%Y-%m-%d") 70 | 71 | name_str = f'{rolename} (json)' 72 | threshold_str = f"{role.threshold} of {len(signers)}" 73 | signer_str = f"{', '.join(signers)} ({threshold_str} required)" 74 | 75 | lines.append(f"| {name_str} | {signing_date} | {expiry} UTC | {signer_str} |") 76 | 77 | now = datetime.now(UTC).isoformat(timespec="minutes") 78 | head = _git(["rev-parse", "HEAD"]).stdout.strip() 79 | 80 | url = parse.urlparse(_git(["config", "--get", "remote.origin.url"]).stdout.strip()) 81 | owner_project = url.path.removesuffix(".git") 82 | _, _, project = owner_project.rpartition("/") 83 | project_link = f"https://github.com{owner_project}" 84 | 85 | commit_link = f"[{head[:7]}]({project_link}/tree/{head})" 86 | tuf_on_ci_url = "https://github.com/theupdateframework/tuf-on-ci" 87 | 88 | lines.append(f"\n_Generated {now} from") 89 | lines.append(f"[{project}]({project_link}) commit {commit_link}") 90 | lines.append(f"by [TUF-on-CI]({tuf_on_ci_url}) v{__version__}._") 91 | 92 | return "\n".join(lines) 93 | 94 | 95 | @click.command() # type: ignore[arg-type] 96 | @click.option("-v", "--verbose", count=True, default=0) 97 | @click.option("--metadata", required=True) 98 | @click.option("--artifacts") 99 | def build_repository(verbose: int, metadata: str, artifacts: str | None) -> None: 100 | """Create publishable metadata and artifact directories""" 101 | logging.basicConfig(level=logging.WARNING - verbose * 10) 102 | 103 | repo = CIRepository("metadata") 104 | repo.build(metadata, artifacts) 105 | 106 | click.echo(f"Metadata published in {metadata}") 107 | if artifacts: 108 | click.echo(f"Artifacts published in {artifacts}") 109 | 110 | with open(os.path.join(metadata, "index.md"), "w") as f: 111 | f.write(build_description(repo)) 112 | -------------------------------------------------------------------------------- /repo/tuf_on_ci/client.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 - 2023, New York University and the TUF contributors 2 | # Copyright 2024 Google LLC 3 | 4 | """Command line testing client for a tuf-on-ci repository""" 5 | 6 | import logging 7 | import os 8 | import shutil 9 | import sys 10 | from datetime import UTC, datetime 11 | from filecmp import cmp 12 | from tempfile import TemporaryDirectory 13 | from urllib import request 14 | 15 | import click 16 | from tuf.api.exceptions import ExpiredMetadataError 17 | from tuf.api.metadata import Metadata 18 | from tuf.ngclient import Updater, UpdaterConfig 19 | 20 | 21 | def expiry_check(dir: str, role: str, timestamp: int): 22 | ref_time = datetime.fromtimestamp(timestamp, UTC) 23 | md = Metadata.from_file(os.path.join(dir, f"{role}.json")) 24 | expiry = md.signed.expires 25 | if ref_time > expiry: 26 | sys.exit(f"Error: {role} expires {expiry} (expected valid at {ref_time})") 27 | print(f"Role {role} is valid on {ref_time}: OK") 28 | 29 | 30 | @click.command() 31 | @click.option("-v", "--verbose", count=True, default=0) 32 | @click.option("-m", "--metadata-url", type=str, required=True) 33 | @click.option("-a", "--artifact-url", type=str, required=True) 34 | @click.option("-u", "--update-base-url", type=str) 35 | @click.option("-r", "--initial-root", type=str) 36 | @click.option("-e", "--expected-artifact", type=str) 37 | @click.option("-c", "--compare-source", type=str) 38 | @click.option("-t", "--time", type=int) 39 | @click.option("-o", "--offline-time", type=int) 40 | @click.option("-d", "--metadata-dir", type=str) 41 | def client( 42 | verbose: int, 43 | metadata_url: str, 44 | artifact_url: str, 45 | update_base_url: str | None, 46 | initial_root: str | None, 47 | expected_artifact: str | None, 48 | compare_source: str | None, 49 | time: int | None, 50 | offline_time: int | None, 51 | metadata_dir: str | None, 52 | ) -> None: 53 | """Test client for tuf-on-ci""" 54 | 55 | logging.basicConfig(level=logging.WARNING - verbose * 10) 56 | 57 | with TemporaryDirectory() as client_dir: 58 | if metadata_dir is None: 59 | metadata_dir = os.path.join(client_dir, "metadata") 60 | artifact_dir = os.path.join(client_dir, "artifacts") 61 | os.makedirs(metadata_dir, exist_ok=True) 62 | os.mkdir(artifact_dir) 63 | 64 | # Allow for a large number of root rotations, as metadata is 65 | # not cached during testing 66 | config = UpdaterConfig(max_root_rotations=256) 67 | 68 | # initialize client with --initial-root or from metadata_url 69 | if initial_root is not None: 70 | shutil.copy(initial_root, os.path.join(metadata_dir, "root.json")) 71 | else: 72 | root_url = f"{metadata_url}/1.root.json" 73 | try: 74 | request.urlretrieve(root_url, f"{metadata_dir}/root.json") # noqa: S310 75 | except OSError as e: 76 | sys.exit(f"Failed to download initial root {root_url}: {e}") 77 | 78 | if update_base_url is not None: 79 | # Update client to update_base_url before doing the actual update 80 | updater = Updater( 81 | metadata_dir, update_base_url, artifact_dir, artifact_url, config=config 82 | ) 83 | try: 84 | updater.refresh() 85 | print(f"Client metadata update from base url {update_base_url}: OK") 86 | except ExpiredMetadataError as e: 87 | print(f"WARNING: update base url has expired metadata: {e}") 88 | 89 | # Update client to metadata_url 90 | updater = Updater( 91 | metadata_dir, metadata_url, artifact_dir, artifact_url, config=config 92 | ) 93 | ref_time_string = "" 94 | if time is not None: 95 | # HACK: replace reference time with ours: initial root has been loaded 96 | # already but that is fine: the expiry check only happens during refresh 97 | ref_time = datetime.fromtimestamp(time, UTC) 98 | updater._trusted_set.reference_time = ref_time 99 | ref_time_string = f" at reference time {ref_time}" 100 | 101 | # Confirm we can get valid top level metadata from remote 102 | try: 103 | updater.refresh() 104 | except ExpiredMetadataError as e: 105 | sys.exit(f"Error: Update failed: {e}{ref_time_string}") 106 | print(f"Client metadata update from {metadata_url}{ref_time_string}: OK") 107 | 108 | if compare_source: 109 | # Compare received metadata versions with source metadata 110 | for f in ["root.json", "timestamp.json"]: 111 | client_file = os.path.join(metadata_dir, f) 112 | source_file = os.path.join(compare_source, f) 113 | if not cmp(source_file, client_file, shallow=False): 114 | sys.exit(f"Error: metadata does not match sources: {f} failed") 115 | print("Client metadata matches sources: OK") 116 | 117 | # Verify root and targets are valid at given reference time 118 | if offline_time is not None: 119 | expiry_check(metadata_dir, "root", offline_time) 120 | expiry_check(metadata_dir, "targets", offline_time) 121 | 122 | if expected_artifact: 123 | # Test expected artifact existence 124 | tinfo = updater.get_targetinfo(expected_artifact) 125 | if not tinfo: 126 | sys.exit("Error: Expected artifact '{expected_artifact}' not found") 127 | 128 | updater.download_target(tinfo) 129 | print(f"Expected artifact '{expected_artifact}': OK") 130 | -------------------------------------------------------------------------------- /repo/tuf_on_ci/create_signing_events.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | """Command line tool to create signing events for roles that are about to expire""" 4 | 5 | import logging 6 | import subprocess 7 | from glob import glob 8 | 9 | import click 10 | 11 | from tuf_on_ci._repository import CIRepository 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def _git(cmd: list[str]) -> subprocess.CompletedProcess: 17 | cmd = [ 18 | "git", 19 | "-c", 20 | "user.name=TUF-on-CI", 21 | "-c", 22 | "user.email=41898282+github-actions[bot]@users.noreply.github.com", 23 | *cmd, 24 | ] 25 | proc = subprocess.run(cmd, check=True, capture_output=True, text=True) 26 | logger.debug("%s:\n%s", cmd, proc.stdout) 27 | return proc 28 | 29 | 30 | @click.command() # type: ignore[arg-type] 31 | @click.option("-v", "--verbose", count=True, default=0) 32 | @click.option("--push/--no-push", default=False) 33 | def create_signing_events(verbose: int, push: bool) -> None: 34 | """Create new branches with version bump commits for expiring offline roles 35 | 36 | Note that these offline role versions will not be signed yet. 37 | If --push, the branches are pushed to origin. Otherwise local branches are 38 | created. 39 | """ 40 | logging.basicConfig(level=logging.WARNING - verbose * 10) 41 | 42 | repo = CIRepository("metadata") 43 | events = [] 44 | for filename in glob("*.json", root_dir="metadata"): 45 | if filename in ["timestamp.json", "snapshot.json"]: 46 | continue 47 | 48 | rolename = filename[: -len(".json")] 49 | version = repo.bump_expiring(rolename) 50 | if version is None: 51 | logger.debug("No version bump needed for %s", rolename) 52 | continue 53 | 54 | msg = f"Periodic version bump: {rolename} v{version}" 55 | event = f"sign/{rolename}-v{version}" 56 | ref = f"refs/remotes/origin/{event}" if push else f"refs/heads/{event}" 57 | files = [f"metadata/{rolename}.json"] 58 | if rolename == "root": 59 | files.append(f"metadata/root_history/{version}.root.json") 60 | 61 | _git(["add", "--", *files]) 62 | _git(["commit", "-m", msg, "--signoff"]) 63 | try: 64 | _git(["show-ref", "--quiet", "--verify", ref]) 65 | logger.debug("Signing event branch %s already exists", event) 66 | except subprocess.CalledProcessError: 67 | events.append(event) 68 | if push: 69 | _git(["push", "origin", f"HEAD:{event}"]) 70 | else: 71 | _git(["branch", event]) 72 | 73 | # get back to original HEAD (before we commited) 74 | _git(["reset", "--hard", "HEAD^"]) 75 | 76 | # print out list of created event branches 77 | click.echo(" ".join(events)) 78 | -------------------------------------------------------------------------------- /repo/tuf_on_ci/online_sign.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | """Command line online signing tool for TUF-on-CI""" 4 | 5 | import logging 6 | import subprocess 7 | 8 | import click 9 | 10 | from tuf_on_ci._repository import CIRepository 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | def _git(cmd: list[str]) -> subprocess.CompletedProcess: 16 | cmd = [ 17 | "git", 18 | "-c", 19 | "user.name=tuf-on-ci", 20 | "-c", 21 | "user.email=41898282+github-actions[bot]@users.noreply.github.com", 22 | *cmd, 23 | ] 24 | proc = subprocess.run(cmd, check=True, text=True) 25 | logger.debug("%s:\n%s", cmd, proc.stdout) 26 | return proc 27 | 28 | 29 | @click.command() # type: ignore[arg-type] 30 | @click.option("-v", "--verbose", count=True, default=0) 31 | @click.option("--push/--no-push", default=False) 32 | def online_sign(verbose: int, push: bool) -> None: 33 | """Update The TUF snapshot and timestamp if needed 34 | 35 | Create a commit with the snapshot and timestamp changes (if any). 36 | If --push, the commit is pushed to origin. 37 | 38 | A new snapshot will be created if 39 | * snapshot content has changed 40 | * or snapshot is in signing period 41 | * or snapshot is currently not correctly signed 42 | 43 | A new timestamp will be created if 44 | * A new snapshot was created 45 | * or timestamp is in signing period 46 | * or timestamp is currently not correctly signed 47 | """ 48 | 49 | logging.basicConfig(level=logging.WARNING - verbose * 10) 50 | repo = CIRepository("metadata") 51 | valid_s = repo.is_signed("snapshot") and not repo.is_in_signing_period("snapshot") 52 | snapshot_updated, _ = repo.do_snapshot(not valid_s) 53 | valid_t = repo.is_signed("timestamp") and not repo.is_in_signing_period("timestamp") 54 | timestamp_updated, _ = repo.do_timestamp(not valid_t) 55 | 56 | if timestamp_updated: 57 | roles = "snapshot & timestamp" if snapshot_updated else "timestamp" 58 | msg = f"Online sign ({roles})" 59 | 60 | click.echo(msg) 61 | _git(["add", "metadata/timestamp.json", "metadata/snapshot.json"]) 62 | _git(["commit", "-m", msg, "--signoff"]) 63 | if push: 64 | _git(["push", "origin", "HEAD"]) 65 | else: 66 | click.echo("Online signing not needed") 67 | -------------------------------------------------------------------------------- /signer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | TUF-on-CI 4 | Copyright 2022-2023 repository-playground contributors 5 | Copyright 2023-2025 TUF-on-CI contributors 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /signer/README.md: -------------------------------------------------------------------------------- 1 | ### TUF-on-CI Signing tools 2 | 3 | This package contains the signing tools for 4 | [TUF-on-CI](https://github.com/theupdateframework/tuf-on-ci). Please see 5 | TUF-on-CI README for usage. 6 | -------------------------------------------------------------------------------- /signer/create-config-file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Run this script in the folder where the TUF-on-CI repository resides to 4 | # initialize the config. 5 | 6 | set -u 7 | set -e 8 | 9 | OS=`uname -s` 10 | 11 | echo "Enter your GitHub handle, without '@', e.g. mona" 12 | read GITHUB_HANDLE 13 | 14 | case ${OS} in 15 | Darwin) 16 | if [ -f /opt/homebrew/lib/libykcs11.dylib ]; then 17 | YKSLIB=/opt/homebrew/lib/libykcs11.dylib 18 | elif [ -f /usr/local/lib/libykcs11.dylib ]; then 19 | YKSLIB=/usr/local/lib/libykcs11.dylib 20 | else 21 | # Unknown location 22 | YKSLIB=/ 23 | fi 24 | ;; 25 | Linux) 26 | YKSLIB=/usr/lib/x86_64-linux-gnu/libykcs11.so 27 | ;; 28 | *) 29 | echo Unsupported OS ${OS} 30 | exit 1 31 | ;; 32 | esac 33 | 34 | if [ ! -f ${YKSLIB} ]; then 35 | echo "Could not find a PKCS library at path ${YKSLIB}" 36 | echo "Please install a PKCS library, or enter a path where one is installed:" 37 | read YKSLIB 38 | echo "Using ${YKSLIB}. This can changed later via 'pykcs11lib' in file .tuf-on-ci-sign.ini" 39 | fi 40 | 41 | cat > .tuf-on-ci-sign.ini <= 24,< 26", 13 | "platformdirs ~= 4.2", 14 | "securesystemslib[awskms,azurekms,gcpkms,hsm,sigstore] ~= 1.2", 15 | "tuf >= 5.1,< 7.0", 16 | "click ~= 8.1", 17 | ] 18 | requires-python = ">=3.9" 19 | dynamic = ["version"] 20 | 21 | [project.urls] 22 | issues = "https://github.com/theupdateframework/tuf-on-ci/issues" 23 | source = "https://github.com/theupdateframework/tuf-on-ci/" 24 | 25 | [project.scripts] 26 | tuf-on-ci-delegate = "tuf_on_ci_sign:delegate" 27 | tuf-on-ci-import-repo = "tuf_on_ci_sign:import_repo" 28 | tuf-on-ci-sign = "tuf_on_ci_sign:sign" 29 | 30 | [project.optional-dependencies] 31 | lint = [ 32 | "mypy == 1.16.0", 33 | "ruff == 0.11.12", 34 | ] 35 | 36 | [tool.hatch.version] 37 | path = "tuf_on_ci_sign/__init__.py" 38 | 39 | [tool.mypy] 40 | python_version = "3.9" 41 | 42 | [[tool.mypy.overrides]] 43 | module = [ 44 | "securesystemslib.*", 45 | "PyKCS11.*", 46 | ] 47 | ignore_missing_imports = "True" 48 | 49 | [tool.ruff.lint] 50 | select = [ 51 | "ARG", # flake8-unused-arguments 52 | "B", # flake8-bugbear 53 | "BLE", # flake8-blind-except 54 | "C4", # flake8-comprehensions 55 | "E", # pycodestyle errors 56 | "F", # pyflakes 57 | "I", # isort 58 | "LOG", # flake8-logging 59 | "N", # pep8-naming 60 | "RET", # flake8-return 61 | "RUF", # ruff-specific rules 62 | "S", # flake8-bandit 63 | "SIM", # flake8-simplify 64 | "UP", # pyupgrade 65 | "W", # pycodestyle warnings 66 | ] 67 | ignore = [ 68 | "S101", # Use of `assert` detected 69 | "S603", # `subprocess` call: check for execution of untrusted input 70 | ] 71 | -------------------------------------------------------------------------------- /signer/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/signer/test/__init__.py -------------------------------------------------------------------------------- /signer/test/test_signer_repository.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from securesystemslib.signer import SSlibKey 4 | 5 | from tuf_on_ci_sign._signer_repository import ( 6 | SignerRepository, 7 | build_paths, 8 | set_key_field, 9 | ) 10 | 11 | 12 | class TestUser(unittest.TestCase): 13 | """Test delegate path generation""" 14 | 15 | def test_build_paths(self): 16 | paths = build_paths("myrole", SignerRepository.MAX_DEPTH) 17 | self.assertEqual( 18 | paths, ["myrole/*", "myrole/*/*", "myrole/*/*/*", "myrole/*/*/*/*"] 19 | ) 20 | 21 | def test_set_key_field(self): 22 | """Test that set_key_field() modifies the keyid as defined in specification""" 23 | key = SSlibKey("abcd", "ed25519", "ed25519", {"public": "abcde"}) 24 | expected_id = "3e5e819246b51532a5533efb5d7c3e18ca8e7a7f4d2267644c3e2298ac81de18" 25 | 26 | self.assertEqual(key.keyid, "abcd") 27 | set_key_field(key, "keyowner", "@testuser") 28 | self.assertEqual(key.keyid, expected_id) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /signer/test/test_user.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import unittest 4 | from tempfile import TemporaryDirectory 5 | 6 | import click 7 | from securesystemslib.signer import HSMSigner, SSlibKey 8 | 9 | from tuf_on_ci_sign import _user 10 | from tuf_on_ci_sign._user import User 11 | 12 | # Long lines are ok here 13 | # ruff: noqa: E501 14 | REQUIRED = """ 15 | [settings] 16 | user-name = @signer 17 | push-remote = origin 18 | pull-remote = myremote 19 | """ 20 | 21 | WITH_PYKCS11LIB = """ 22 | [settings] 23 | pykcs11lib = /usr/lib/x86_64-linux-gnu/libykcs11.so 24 | user-name = @signer 25 | push-remote = origin 26 | pull-remote = myremote 27 | """ 28 | 29 | MISSING_NAME = """ 30 | [settings] 31 | pykcs11lib = /usr/lib/x86_64-linux-gnu/libykcs11.so 32 | push-remote = origin 33 | pull-remote = myremote 34 | """ 35 | 36 | NAME_WITH_NO_PREFIX = """ 37 | [settings] 38 | pykcs11lib = /usr/lib/x86_64-linux-gnu/libykcs11.so 39 | user-name = signer 40 | push-remote = origin 41 | pull-remote = myremote 42 | """ 43 | 44 | REQUIRED_AND_SIGNING_KEYS = """ 45 | [settings] 46 | pykcs11lib = /usr/lib/x86_64-linux-gnu/libykcs11.so 47 | user-name = @signer 48 | push-remote = origin 49 | pull-remote = myremote 50 | 51 | [signing-keys] 52 | 762cb22caca65de5e9b7b6baecb84ca989d337280ce6914b6440aea95769ad93 = hsm:2?label=YubiKey+PIV+%2315835999 53 | 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b = file:keys/mykey?encrypted=false 54 | """ 55 | 56 | HSM_KEY = SSlibKey( 57 | "762cb22caca65de5e9b7b6baecb84ca989d337280ce6914b6440aea95769ad93", 58 | "ecdsa", 59 | "ecdsa-sha2-nistp256", 60 | { 61 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEohqIdE+yTl4OxpX8ZxNUPrg3SL9H\nBDnhZuceKkxy2oMhUOxhWweZeG3bfM1T4ZLnJimC6CAYVU5+F5jZCoftRw==\n-----END PUBLIC KEY-----\n" 62 | }, 63 | ) 64 | 65 | NONCONFIGURED_KEY = SSlibKey( 66 | "64eeece964e09c058ef8f9805daca546b01ba4719c80b6fe911b091a7c05124b", 67 | "ecdsa", 68 | "ecdsa-sha2-nistp256", 69 | { 70 | "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu+ebm3VUg6U2b0IIeR6NFZU7uxkL\nR1sVLxV8SEW7G+AMXMasEQf5daxfwVMP1kuEkhGs3mBYLkYXlWDh9BNSxg==\n-----END PUBLIC KEY-----\n" 71 | }, 72 | ) 73 | 74 | 75 | class TestUser(unittest.TestCase): 76 | """Test configuration management and signer caching""" 77 | 78 | def test_required(self): 79 | with TemporaryDirectory() as tempdir: 80 | inifile = os.path.join(tempdir, ".tuf-on-ci-sign.ini") 81 | with open(inifile, "w") as f: 82 | f.write(WITH_PYKCS11LIB) 83 | 84 | user = User(inifile) 85 | self.assertEqual(user.name, "@signer") 86 | self.assertEqual(user.pykcs11lib, "/usr/lib/x86_64-linux-gnu/libykcs11.so") 87 | self.assertEqual(user.push_remote, "origin") 88 | self.assertEqual(user.pull_remote, "myremote") 89 | 90 | with open(inifile, "w") as f: 91 | f.write(NAME_WITH_NO_PREFIX) 92 | 93 | user2 = User(inifile) 94 | self.assertEqual(user.name, user2.name) 95 | 96 | with open(inifile, "w") as f: 97 | f.write(MISSING_NAME) 98 | with self.assertRaises(click.ClickException): 99 | user = User(inifile) 100 | 101 | def test_pkcs_prober(self): 102 | with TemporaryDirectory() as tempdir: 103 | inifile = os.path.join(tempdir, ".tuf-on-ci-sign.ini") 104 | with open(inifile, "w") as f: 105 | f.write(REQUIRED) 106 | 107 | nonexistent_pkcs11lib = os.path.join(tempdir, "nonexistent-pkcs11lib") 108 | mock_pkcs11lib = os.path.join(tempdir, "mock-pkcs11lib") 109 | with open(mock_pkcs11lib, "w") as f: 110 | f.write("") 111 | 112 | # mock prober lookup locations so that a library is not found: 113 | _user.LIBYKCS11_LOCATIONS = {platform.system(): [nonexistent_pkcs11lib]} 114 | with self.assertRaises(click.ClickException): 115 | User(inifile) 116 | 117 | # mock prober lookup locations so that a library is found: 118 | _user.LIBYKCS11_LOCATIONS = { 119 | platform.system(): [nonexistent_pkcs11lib, mock_pkcs11lib] 120 | } 121 | user = User(inifile) 122 | self.assertEqual(user.pykcs11lib, mock_pkcs11lib) 123 | 124 | def test_signing_keys(self): 125 | with TemporaryDirectory() as tempdir: 126 | inifile = os.path.join(tempdir, ".tuf-on-ci-sign.ini") 127 | with open(inifile, "w") as f: 128 | f.write(REQUIRED_AND_SIGNING_KEYS) 129 | 130 | user = User(inifile) 131 | # We should get a signer for the configured HSM 132 | hsm_signer = user.get_signer(HSM_KEY) 133 | self.assertIsInstance(hsm_signer, HSMSigner) 134 | self.assertEqual( 135 | hsm_signer.token_filter, {"label": "YubiKey PIV #15835999"} 136 | ) 137 | self.assertEqual( 138 | hsm_signer.public_key.keyid, 139 | "762cb22caca65de5e9b7b6baecb84ca989d337280ce6914b6440aea95769ad93", 140 | ) 141 | 142 | # Cache the signer 143 | user.set_signer(HSM_KEY, hsm_signer) 144 | 145 | # If the signing key is not configured, we expect a generic HSM signer 146 | other_signer = user.get_signer(NONCONFIGURED_KEY) 147 | self.assertIsInstance(other_signer, HSMSigner) 148 | self.assertEqual(other_signer.token_filter, {}) 149 | self.assertEqual( 150 | other_signer.public_key.keyid, 151 | "64eeece964e09c058ef8f9805daca546b01ba4719c80b6fe911b091a7c05124b", 152 | ) 153 | 154 | # another lookup should return same instance 155 | second_hsm_signer = user.get_signer(HSM_KEY) 156 | self.assertIs(hsm_signer, second_hsm_signer) 157 | 158 | 159 | if __name__ == "__main__": 160 | unittest.main() 161 | -------------------------------------------------------------------------------- /signer/tuf_on_ci_sign/__init__.py: -------------------------------------------------------------------------------- 1 | from tuf_on_ci_sign.delegate import delegate 2 | from tuf_on_ci_sign.import_repo import import_repo 3 | from tuf_on_ci_sign.sign import sign 4 | 5 | __version__ = "0.16.1" 6 | 7 | __all__ = ["delegate", "import_repo", "sign"] 8 | -------------------------------------------------------------------------------- /signer/tuf_on_ci_sign/_common.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | """Common helper functions""" 4 | 5 | import json 6 | import logging 7 | import os 8 | import subprocess 9 | import webbrowser 10 | from collections.abc import Generator 11 | from contextlib import contextmanager 12 | from datetime import datetime, timedelta 13 | from tempfile import TemporaryDirectory 14 | from urllib import parse 15 | from urllib.request import Request, urlopen 16 | 17 | import click 18 | from packaging.version import Version 19 | from platformdirs import user_cache_dir 20 | from securesystemslib.signer import HSMSigner, Key, SigstoreSigner 21 | 22 | from tuf_on_ci_sign._signer_repository import SignerRepository 23 | from tuf_on_ci_sign._user import User 24 | 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | @contextmanager 29 | def signing_event(name: str, config: User) -> Generator[SignerRepository, None, None]: 30 | toplevel = git(["rev-parse", "--show-toplevel"]) 31 | 32 | # PyKCS11 (Yubikey support) needs the module path 33 | # TODO: if config is not set, complain/ask the user? 34 | if "PYKCS11LIB" not in os.environ: 35 | os.environ["PYKCS11LIB"] = config.pykcs11lib 36 | 37 | # first, make sure we're up-to-date 38 | git_expect(["fetch", config.pull_remote]) 39 | try: 40 | git(["checkout", f"{config.pull_remote}/{name}"]) 41 | except subprocess.CalledProcessError: 42 | click.echo("Remote branch not found: branching off from main") 43 | git_expect(["checkout", f"{config.pull_remote}/main"]) 44 | 45 | try: 46 | # checkout the base of this signing event in another directory 47 | with TemporaryDirectory() as temp_dir: 48 | base_sha = git_expect(["merge-base", f"{config.pull_remote}/main", "HEAD"]) 49 | event_sha = git_expect(["rev-parse", "HEAD"]) 50 | git_expect(["clone", "--quiet", toplevel, temp_dir]) 51 | git_expect(["-C", temp_dir, "checkout", "--quiet", base_sha]) 52 | base_metadata_dir = os.path.join(temp_dir, "metadata") 53 | metadata_dir = os.path.join(toplevel, "metadata") 54 | 55 | click.echo(bold_blue(f"Signing event {name} (commit {event_sha[:7]})")) 56 | yield SignerRepository(metadata_dir, base_metadata_dir, config) 57 | finally: 58 | # go back to original branch 59 | git_expect(["checkout", "-"]) 60 | 61 | 62 | def get_signing_key_input() -> Key: 63 | click.echo("\nConfiguring signing key") 64 | click.echo(" 1. Sigstore (OpenID Connect)") 65 | click.echo(" 2. Yubikey") 66 | choice = click.prompt( 67 | bold("Please choose the type of signing key you would like to use"), 68 | type=click.IntRange(1, 2), 69 | default=1, 70 | ) 71 | 72 | key: Key 73 | if choice == 1: 74 | click.echo(bold("Please authenticate with your Sigstore signing identity")) 75 | _, key = SigstoreSigner.import_via_auth() 76 | else: 77 | click.prompt( 78 | bold("Please insert your Yubikey and press enter"), 79 | default=True, 80 | show_default=False, 81 | ) 82 | try: 83 | _, key = HSMSigner.import_() 84 | except Exception as e: 85 | raise click.ClickException(f"Failed to read HW key: {e}") from e 86 | 87 | return key 88 | 89 | 90 | def git(cmd: list[str]) -> str: 91 | cmd = ["git", *cmd] 92 | proc = subprocess.run(cmd, capture_output=True, check=True, text=True) 93 | return proc.stdout.strip() 94 | 95 | 96 | def git_expect(cmd: list[str]) -> str: 97 | """Run git, expect success""" 98 | try: 99 | return git(cmd) 100 | except subprocess.CalledProcessError as e: 101 | print(f"git failure:\n{e.stderr}") 102 | print(f"\n{e.stdout}") 103 | raise 104 | 105 | 106 | def git_echo(cmd: list[str]): 107 | cmd = ["git", *cmd] 108 | subprocess.run(cmd, check=True, text=True) 109 | 110 | 111 | def bold(text: str) -> str: 112 | return click.style(text, bold=True) 113 | 114 | 115 | def bold_blue(text: str) -> str: 116 | return click.style(text, bold=True, fg="bright_blue") 117 | 118 | 119 | def application_update_reminder() -> None: 120 | from tuf_on_ci_sign import __version__ 121 | 122 | update_file = os.path.join(user_cache_dir("tuf-on-ci-sign"), "pypi_release_version") 123 | try: 124 | update_time = os.path.getmtime(update_file) 125 | except OSError: 126 | update_time = 0 127 | 128 | try: 129 | if datetime.fromtimestamp(update_time) + timedelta(days=1) > datetime.now(): 130 | # It's been less than a day since last pypi query 131 | with open(update_file) as f: 132 | max_version = Version(f.read()) 133 | else: 134 | # Find out newest release version from pypi 135 | request = Request("https://pypi.org/simple/tuf-on-ci-sign/") 136 | request.add_header("Accept", "application/vnd.pypi.simple.v1+json") 137 | with urlopen(request, timeout=5) as response: # noqa: S310 138 | data = json.load(response) 139 | 140 | max_version = Version("0") 141 | for ver_str in data["versions"]: 142 | ver = Version(ver_str) 143 | if not ver.is_devrelease and not ver.is_prerelease: 144 | max_version = max(max_version, ver) 145 | 146 | # store the current version number in cache 147 | os.makedirs(os.path.dirname(update_file), exist_ok=True) 148 | with open(update_file, "w") as f: 149 | f.write(str(max_version)) 150 | 151 | if max_version > Version(__version__): 152 | msg = bold( 153 | f"tuf-on-ci-sign {__version__} is outdated: New version " 154 | f"({max_version}) is available" 155 | ) 156 | print(msg) 157 | 158 | except Exception as e: # noqa: BLE001 159 | logger.warning(f"Failed to check current tuf-on-ci-sign version: {e}") 160 | 161 | 162 | def push_changes(user: User, event_name: str, title: str) -> None: 163 | """Push the event branch to users push remote""" 164 | branch = f"{user.push_remote}/{event_name}" 165 | msg = f"Press enter to push changes to {branch}" 166 | click.prompt(bold(msg), default=True, show_default=False) 167 | if user.push_remote == user.pull_remote: 168 | # maintainer flow: just push to signing event branch 169 | git_echo( 170 | [ 171 | "push", 172 | user.push_remote, 173 | f"HEAD:refs/heads/{event_name}", 174 | ] 175 | ) 176 | else: 177 | # non-maintainer flow: push to fork, make a PR. 178 | # NOTE: we force push: this is safe since any existing fork branches 179 | # have either been merged or are obsoleted by this push 180 | git_echo( 181 | [ 182 | "push", 183 | "--force", 184 | user.push_remote, 185 | f"HEAD:refs/heads/{event_name}", 186 | ] 187 | ) 188 | # Create PR from fork (push remote) to upstream (pull remote) 189 | upstream = get_repo_name(user.pull_remote) 190 | fork = get_repo_name(user.push_remote).replace("/", ":") 191 | query = parse.urlencode( 192 | { 193 | "quick_pull": 1, 194 | "title": title, 195 | "template": "signing_event.md", 196 | } 197 | ) 198 | pr_url = f"https://github.com/{upstream}/compare/{event_name}...{fork}:{event_name}?{query}" 199 | if webbrowser.open(pr_url): 200 | click.echo(bold("Please submit the pull request in your browser.")) 201 | else: 202 | click.echo(bold(f"Please submit the pull request:\n {pr_url}")) 203 | 204 | 205 | def get_repo_name(remote: str) -> str: 206 | """Return 'owner/repo' string for given GitHub remote""" 207 | url = parse.urlparse(git_expect(["config", "--get", f"remote.{remote}.url"])) 208 | owner_repo = url.path[: -len(".git")] 209 | # ssh-urls are relative URLs according to urllib: host is actually part of 210 | # path. We don't want the host part: 211 | _, _, owner_repo = owner_repo.rpartition(":") 212 | # http urls on the other hand are not relative: remove the leading / 213 | owner_repo = owner_repo.lstrip("/") 214 | 215 | # sanity check 216 | owner, slash, repo = owner_repo.partition("/") 217 | if not owner or slash != "/" or not repo: 218 | raise RuntimeError( 219 | "Failed to parse GitHub repository from git URL {url} for remote {remote}" 220 | ) 221 | 222 | return owner_repo 223 | -------------------------------------------------------------------------------- /signer/tuf_on_ci_sign/_user.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import platform 4 | import sys 5 | from configparser import ConfigParser 6 | 7 | import click 8 | from securesystemslib.signer import Key, Signer 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | # some known locations where we might find libykcs11. 13 | # These should all be _system_ locations (not user writable) 14 | LIBYKCS11_LOCATIONS = { 15 | "Linux": [ 16 | "/usr/lib/x86_64-linux-gnu/libykcs11.so", 17 | "/usr/lib64/libykcs11.so", 18 | "/usr/local/lib/libykcs11.so", 19 | ], 20 | "Darwin": ["/opt/homebrew/lib/libykcs11.dylib", "/usr/local/lib/libykcs11.dylib"], 21 | } 22 | 23 | 24 | def bold(text: str) -> str: 25 | return click.style(text, bold=True) 26 | 27 | 28 | class User: 29 | """Class that manages user configuration and manages the users signer cache""" 30 | 31 | def __init__(self, path: str): 32 | self._config_path = path 33 | 34 | self._config = ConfigParser(interpolation=None) 35 | self._config.read(path) 36 | 37 | # TODO: create config if missing, ask/confirm values from user 38 | if not self._config: 39 | raise click.ClickException(f"Settings file {path} not found") 40 | try: 41 | self.name = self._config["settings"]["user-name"].lower() 42 | if not self.name.startswith("@"): 43 | self.name = f"@{self.name}" 44 | self.push_remote = self._config["settings"]["push-remote"] 45 | self.pull_remote = self._config["settings"]["pull-remote"] 46 | except KeyError as e: 47 | raise click.ClickException( 48 | f"Failed to find required setting {e} in {path}" 49 | ) from e 50 | 51 | # signing key config is not required 52 | if "signing-keys" in self._config: 53 | self._signing_key_uris = dict(self._config.items("signing-keys")) 54 | else: 55 | self._signing_key_uris = {} 56 | 57 | # probe for pykcs11lib if it's not set 58 | try: 59 | self.pykcs11lib = self._config["settings"]["pykcs11lib"] 60 | except KeyError: 61 | for loc in LIBYKCS11_LOCATIONS.get(platform.system(), []): 62 | if os.path.exists(loc): 63 | self.pykcs11lib = loc 64 | logger.debug("Using probed YKCS11 location %s", self.pykcs11lib) 65 | break 66 | else: 67 | raise click.ClickException("Failed to find libykcs11") 68 | 69 | # signer cache gets populated as they are used the first time 70 | self._signers: dict[str, Signer] = {} 71 | 72 | def get_signer(self, key: Key) -> Signer: 73 | """Returns a Signer for the given public key 74 | 75 | The signer sources are (in order): 76 | * signers cached via set_signer() 77 | * any configured signer from 'signing-keys' config section 78 | * for sigstore type keys, a Signer is automatically created 79 | * for any remaining keys, HSM is assumed and a signer is created 80 | """ 81 | 82 | def get_secret(secret: str) -> str: 83 | msg = f"Enter {secret} to sign (provide touch/bio authentication if needed)" 84 | 85 | # special case for tests -- prompt() will lockup trying to hide STDIN: 86 | if not sys.stdin.isatty(): 87 | return sys.stdin.readline().rstrip() 88 | return click.prompt(bold(msg), hide_input=True) 89 | 90 | if key.keyid in self._signers: 91 | return self._signers[key.keyid] 92 | if key.keyid in self._signing_key_uris: 93 | # signer is not cached yet, but config exists 94 | uri = self._signing_key_uris[key.keyid] 95 | return Signer.from_priv_key_uri(uri, key, get_secret) 96 | if key.keytype == "sigstore-oidc": 97 | # signer is not cached, no configuration was found, type is sigstore 98 | return Signer.from_priv_key_uri("sigstore:?ambient=false", key, get_secret) 99 | # signer is not cached, no configuration was found: assume Yubikey 100 | return Signer.from_priv_key_uri("hsm:", key, get_secret) 101 | 102 | def set_signer(self, key: Key, signer: Signer) -> None: 103 | """Cache a signer for a keyid 104 | 105 | This should be called after a successful signing operation 106 | """ 107 | self._signers[key.keyid] = signer 108 | -------------------------------------------------------------------------------- /signer/tuf_on_ci_sign/import_repo.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | """tuf-on-ci-import: A command line import tool for TUF-on-CI signing events""" 4 | 5 | from __future__ import annotations 6 | 7 | import json 8 | import logging 9 | import os 10 | 11 | import click 12 | from tuf.api.metadata import Key, Role, Signed 13 | 14 | from tuf_on_ci_sign._common import ( 15 | bold, 16 | git_echo, 17 | git_expect, 18 | signing_event, 19 | ) 20 | from tuf_on_ci_sign._signer_repository import AbortEdit 21 | from tuf_on_ci_sign._user import User 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | EXPIRY_KEY = "x-tuf-on-ci-expiry-period" 26 | SIGNING_KEY = "x-tuf-on-ci-signing-period" 27 | ONLINE_URI_KEY = "x-tuf-on-ci-online-uri" 28 | KEYOWNER_KEY = "x-tuf-on-ci-keyowner" 29 | 30 | 31 | def _update_expiry(obj: Signed | Role, import_data: dict[str, int]): 32 | if EXPIRY_KEY in import_data and import_data[EXPIRY_KEY] != -1: 33 | expiry = import_data[EXPIRY_KEY] 34 | elif EXPIRY_KEY in obj.unrecognized_fields: 35 | expiry = obj.unrecognized_fields[EXPIRY_KEY] 36 | elif "x-playground-expiry-period" in obj.unrecognized_fields: 37 | expiry = obj.unrecognized_fields["x-playground-expiry-period"] 38 | else: 39 | # let user know this is needed 40 | import_data[EXPIRY_KEY] = -1 41 | return False 42 | 43 | # set the value 44 | obj.unrecognized_fields[EXPIRY_KEY] = expiry 45 | # unset legacy playground value 46 | obj.unrecognized_fields.pop("x-playground-expiry-period", None) 47 | 48 | return True 49 | 50 | 51 | def _update_signing(obj: Signed | Role, import_data: dict[str, int]): 52 | if SIGNING_KEY in import_data and import_data[SIGNING_KEY] != -1: 53 | signing = import_data[SIGNING_KEY] 54 | elif SIGNING_KEY in obj.unrecognized_fields: 55 | signing = obj.unrecognized_fields[SIGNING_KEY] 56 | elif "x-playground-signing-period" in obj.unrecognized_fields: 57 | signing = obj.unrecognized_fields["x-playground-signing-period"] 58 | elif "x-playground-expiry-period" in obj.unrecognized_fields: 59 | # signing-period was not required at some point 60 | signing = obj.unrecognized_fields["x-playground-expiry-period"] // 2 61 | else: 62 | # let user know this is needed 63 | import_data[SIGNING_KEY] = -1 64 | return False 65 | 66 | # set the value 67 | obj.unrecognized_fields[SIGNING_KEY] = signing 68 | # unset legacy playground value 69 | obj.unrecognized_fields.pop("x-playground-signing-period", None) 70 | 71 | return True 72 | 73 | 74 | def _update_keys(keys: dict[str, Key], import_data: dict[str, str]): 75 | success = True 76 | undefined = "UNDEFINED ONLINE_URI OR KEYOWNER" 77 | for key in keys.values(): 78 | if key.keyid in import_data and import_data[key.keyid] != undefined: 79 | value = import_data[key.keyid] 80 | elif ONLINE_URI_KEY in key.unrecognized_fields: 81 | value = key.unrecognized_fields[ONLINE_URI_KEY] 82 | elif KEYOWNER_KEY in key.unrecognized_fields: 83 | value = key.unrecognized_fields[KEYOWNER_KEY] 84 | elif "x-playground-online-uri" in key.unrecognized_fields: 85 | value = key.unrecognized_fields["x-playground-online-uri"] 86 | elif "x-playground-keyowner" in key.unrecognized_fields: 87 | value = key.unrecognized_fields["x-playground-keyowner"] 88 | else: 89 | # let user know this is needed 90 | import_data[key.keyid] = undefined 91 | success = False 92 | continue 93 | 94 | # set the value, unset legacy value 95 | if value.startswith("@"): 96 | key.unrecognized_fields[KEYOWNER_KEY] = value 97 | key.unrecognized_fields.pop("x-playground-keyowner", None) 98 | else: 99 | key.unrecognized_fields[ONLINE_URI_KEY] = value 100 | key.unrecognized_fields.pop("x-playground-online-uri", None) 101 | 102 | return success 103 | 104 | 105 | @click.command() # type: ignore[arg-type] 106 | @click.option("-v", "--verbose", count=True, default=0) 107 | @click.option("--push/--no-push", default=True) 108 | @click.argument("event-name", metavar="signing-event") 109 | @click.argument("import-file", required=False) 110 | def import_repo(verbose: int, push: bool, event_name: str, import_file: str | None): 111 | """Repository import tool for TUF-on-CI signing events. 112 | 113 | Works on both unmanaged repositories and legacy playground-repository managed 114 | repositories. 115 | 116 | \b 117 | tuf-on-ci-import-repo 118 | Creates a signing event with all of the import changes or, if there are missing 119 | custom metadata fields, prints out import file contents that can be filled. 120 | 121 | \b 122 | tuf-on-ci-import-repo 123 | Creates a signing event with all of the import changes using the import file 124 | to fill in missing custom metadata. 125 | """ 126 | logging.basicConfig(level=logging.WARNING - verbose * 10) 127 | 128 | toplevel = git_expect(["rev-parse", "--show-toplevel"]) 129 | settings_path = os.path.join(toplevel, ".tuf-on-ci-sign.ini") 130 | user_config = User(settings_path) 131 | 132 | if import_file: 133 | with open(import_file) as f: 134 | import_data = json.load(f) 135 | else: 136 | import_data = {} 137 | 138 | with signing_event(event_name, user_config) as repo: 139 | ok = True 140 | # handle root and all target files, in order of delegations 141 | roles = ["root", "targets"] 142 | for _, _, filenames in os.walk(f"{toplevel}/metadata"): 143 | for filename in filenames: 144 | if not filename.endswith(".json"): 145 | continue 146 | rolename = filename[: -len(".json")] 147 | if rolename in ["root", "timestamp", "snapshot", "targets"]: 148 | continue 149 | roles.append(rolename) 150 | 151 | for rolename in roles: 152 | if rolename not in import_data: 153 | import_data[rolename] = {} 154 | 155 | role_data = import_data[rolename] 156 | if rolename == "root": 157 | with repo.edit_root() as root: 158 | ok = _update_signing(root, role_data) and ok 159 | ok = _update_expiry(root, role_data) and ok 160 | 161 | for online_rolename in ["timestamp", "snapshot"]: 162 | role = root.get_delegated_role(online_rolename) 163 | ok = _update_signing(role, role_data) and ok 164 | ok = _update_expiry(role, role_data) and ok 165 | 166 | ok = _update_keys(root.keys, role_data) and ok 167 | if not ok: 168 | raise AbortEdit("Missing values") 169 | 170 | else: 171 | with repo.edit_targets(rolename) as targets: 172 | ok = _update_expiry(targets, role_data) and ok 173 | ok = _update_signing(targets, role_data) and ok 174 | 175 | if targets.delegations: 176 | ok = _update_keys(targets.delegations.keys, role_data) and ok 177 | if not ok: 178 | raise AbortEdit("Missing values") 179 | 180 | if not ok: 181 | print("Error: Undefined values found. please save this in a file,") 182 | print("fill in the values and use the file as import-file argument:\n") 183 | print(json.dumps(import_data, indent=2)) 184 | else: 185 | # we have updated keys defined in root/targets: make sure they are compliant 186 | repo.force_compliant_keyids("root") 187 | repo.force_compliant_keyids("targets") 188 | 189 | git_expect(["add", "metadata"]) 190 | git_expect( 191 | ["commit", "-m", f"Repo import by {user_config.name}", "--signoff"] 192 | ) 193 | 194 | if repo.unsigned: 195 | click.echo(f"Your signature is required for role(s) {repo.unsigned}.") 196 | 197 | for rolename in repo.unsigned: 198 | click.echo(repo.status(rolename)) 199 | repo.sign(rolename) 200 | 201 | git_expect(["add", "metadata/"]) 202 | git_expect( 203 | ["commit", "-m", f"Signed by {user_config.name}", "--signoff"] 204 | ) 205 | 206 | if push: 207 | branch = f"{user_config.push_remote}/{event_name}" 208 | msg = f"Press enter to push signature(s) to {branch}" 209 | click.prompt(bold(msg), default=True, show_default=False) 210 | git_echo( 211 | [ 212 | "push", 213 | "--progress", 214 | user_config.push_remote, 215 | f"HEAD:refs/heads/{event_name}", 216 | ] 217 | ) 218 | else: 219 | # TODO: maybe deal with existing branch? 220 | click.echo(f"Creating local branch {event_name}") 221 | git_expect(["branch", event_name]) 222 | -------------------------------------------------------------------------------- /signer/tuf_on_ci_sign/sign.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Google LLC 2 | 3 | """tuf-on-ci-sign: A command line signing tool for TUF-on-CI signing events""" 4 | 5 | import logging 6 | import os 7 | 8 | import click 9 | 10 | from tuf_on_ci_sign._common import ( 11 | application_update_reminder, 12 | get_signing_key_input, 13 | git_expect, 14 | push_changes, 15 | signing_event, 16 | ) 17 | from tuf_on_ci_sign._signer_repository import SignerState 18 | from tuf_on_ci_sign._user import User 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | @click.command() # type: ignore[arg-type] 24 | @click.version_option() 25 | @click.option("-v", "--verbose", count=True, default=0) 26 | @click.option("--push/--no-push", default=True) 27 | @click.argument("event-name", metavar="signing-event") 28 | def sign(verbose: int, push: bool, event_name: str): 29 | """Signing tool for TUF-on-CI signing events.""" 30 | logging.basicConfig(level=logging.WARNING - verbose * 10) 31 | 32 | application_update_reminder() 33 | 34 | toplevel = git_expect(["rev-parse", "--show-toplevel"]) 35 | settings_path = os.path.join(toplevel, ".tuf-on-ci-sign.ini") 36 | user_config = User(settings_path) 37 | 38 | with signing_event(event_name, user_config) as repo: 39 | if repo.state == SignerState.UNINITIALIZED: 40 | click.echo("No metadata repository found") 41 | change_status = None 42 | elif repo.state == SignerState.INVITED: 43 | click.echo( 44 | f"You have been invited to become a signer for role(s) {repo.invites}." 45 | ) 46 | key = get_signing_key_input() 47 | for rolename in repo.invites.copy(): 48 | # Modify the delegation 49 | role_config = repo.get_role_config(rolename) 50 | assert role_config 51 | repo.set_role_config(rolename, role_config, key) 52 | 53 | # Sign everything 54 | if repo.unsigned: 55 | click.echo(f"Your signature is requested for role(s) {repo.unsigned}.") 56 | for rolename in repo.unsigned: 57 | click.echo(repo.status(rolename)) 58 | repo.sign(rolename) 59 | change_status = f"{user_config.name} accepted invitation" 60 | elif repo.state == SignerState.SIGNATURE_NEEDED: 61 | click.echo(f"Your signature is requested for role(s) {repo.unsigned}.") 62 | for rolename in repo.unsigned: 63 | click.echo(repo.status(rolename)) 64 | repo.sign(rolename) 65 | change_status = f"Signature from {user_config.name}" 66 | elif repo.state == SignerState.NO_ACTION: 67 | change_status = None 68 | else: 69 | raise NotImplementedError 70 | 71 | if change_status: 72 | git_expect(["add", "metadata"]) 73 | git_expect(["commit", "-m", change_status, "--signoff"]) 74 | if push: 75 | push_changes(user_config, event_name, change_status) 76 | else: 77 | # TODO: maybe deal with existing branch? 78 | click.echo(f"Creating local branch {event_name}") 79 | git_expect(["branch", event_name]) 80 | else: 81 | click.echo("Nothing to do.") 82 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # End-to-end tests for TUF-on-CI 2 | 3 | TUF-on-CI is implemented on top of a CI system, git and includes quite a bit of 4 | user interaction (through both the CI system and the signing tools). This makes 5 | testing tricky. These tests are an attempt at defining a set of functionality that 6 | can be tested without 7 | * Github pull requests being created 8 | * git branches modified on github.com 9 | * sigstore or Google Cloud signing for online keys 10 | * a user signing with a Yubikey 11 | 12 | The rough layout of TUF-on-CI is: 13 | 1. Users run a set of python programs (see signer/) 14 | 2. **These programs modify metadata stored in git, commit the changes into git and push various branches to upstream** 15 | 3. Github workflows react to triggers (cron, push) and call GitHub actions defined in TUF-on-CI 16 | 4. The GitHub actions run a separate set of python programs (see repo/) 17 | 5. **These programs also modify metadata stored in git, commit changes and push various branches** 18 | 19 | The tests are designed to test steps 2 and 5 and emulate steps 1, 3 and 4. The purpose is to make 20 | refactoring and development of the python programs easier (because they have test coverage). In 21 | practice: 22 | * functions named `signer_*()` emulate user interactions with tools in signer/ (`tuf-on-ci-sign`, `tuf-on-ci-delegate`). 23 | * functions name `repo_*()` emulate GitHub workflows and actions using the tools in repo/ 24 | * The signer functions operate within one git repository, the repo functions in another: both of them 25 | push to and pull from the "upstream" git repository. In this test setup all of these git repositories are local 26 | * Yubikeys are simulated with SoftHSM2 27 | * Online signing is simulated with a hack that uses a environment variable private key instead of sigstore or GCP 28 | * simulated user input is handled by a bash array that is fed to STDIN of the signer tool 29 | * libfaketime is used to ensure the expiry times in the metadata are predictable 30 | 31 | The main thing being verified in the tests is the final "publishable" metadata repository -- the resulting git repository 32 | structure would be nice to verify as well but unfortunately the nondeterministic ECDSA signatures make that tricky. 33 | 34 | ## Requirements 35 | 36 | * libsofthsm2 (currently hardcoded "/usr/lib/softhsm/libsofthsm2.so") 37 | * libfaketime (currently hardcoded "/usr/lib/x86_64-linux-gnu/faketime/libfaketime.so.1") 38 | * Both signer and repo must be installed 39 | (`pip install -e ../signer/ && pip install -e ../repo/`) 40 | 41 | ## Issues 42 | 43 | * Hard to see what is happening in a test (`DEBUG_TESTS=1 ./e2e.sh` helps but is still not great) 44 | * The whole rig is a hack to get something running, not a real test setup. 45 | Could consider using https://github.com/bats-core/bats-core or similar 46 | if the core idea seems viable. 47 | -------------------------------------------------------------------------------- /tests/expected/basic/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 14 | "keytype": "ed25519", 15 | "keyval": { 16 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 17 | }, 18 | "scheme": "ed25519", 19 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 20 | }, 21 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 22 | "keytype": "ecdsa", 23 | "keyval": { 24 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 25 | }, 26 | "scheme": "ecdsa-sha2-nistp384", 27 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 365, 43 | "x-tuf-on-ci-signing-period": 60 44 | }, 45 | "targets": { 46 | "keyids": [ 47 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 48 | ], 49 | "threshold": 1 50 | }, 51 | "timestamp": { 52 | "keyids": [ 53 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 54 | ], 55 | "threshold": 1, 56 | "x-tuf-on-ci-expiry-period": 2, 57 | "x-tuf-on-ci-signing-period": 1 58 | } 59 | }, 60 | "spec_version": "1.0.31", 61 | "version": 1, 62 | "x-tuf-on-ci-expiry-period": 365, 63 | "x-tuf-on-ci-signing-period": 60 64 | } 65 | } -------------------------------------------------------------------------------- /tests/expected/basic/metadata/1.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "meta": { 12 | "targets.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/basic/metadata/1.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 1, 14 | "x-tuf-on-ci-expiry-period": 365, 15 | "x-tuf-on-ci-signing-period": 60 16 | } 17 | } -------------------------------------------------------------------------------- /tests/expected/basic/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-05T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/delegated/metadata/1.delegated.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 1, 14 | "x-tuf-on-ci-expiry-period": 365, 15 | "x-tuf-on-ci-signing-period": 60 16 | } 17 | } -------------------------------------------------------------------------------- /tests/expected/delegated/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 14 | "keytype": "ed25519", 15 | "keyval": { 16 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 17 | }, 18 | "scheme": "ed25519", 19 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 20 | }, 21 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 22 | "keytype": "ecdsa", 23 | "keyval": { 24 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 25 | }, 26 | "scheme": "ecdsa-sha2-nistp384", 27 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 365, 43 | "x-tuf-on-ci-signing-period": 60 44 | }, 45 | "targets": { 46 | "keyids": [ 47 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 48 | ], 49 | "threshold": 1 50 | }, 51 | "timestamp": { 52 | "keyids": [ 53 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 54 | ], 55 | "threshold": 1, 56 | "x-tuf-on-ci-expiry-period": 2, 57 | "x-tuf-on-ci-signing-period": 1 58 | } 59 | }, 60 | "spec_version": "1.0.31", 61 | "version": 1, 62 | "x-tuf-on-ci-expiry-period": 365, 63 | "x-tuf-on-ci-signing-period": 60 64 | } 65 | } -------------------------------------------------------------------------------- /tests/expected/delegated/metadata/2.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "meta": { 12 | "delegated.json": { 13 | "version": 1 14 | }, 15 | "targets.json": { 16 | "version": 2 17 | } 18 | }, 19 | "spec_version": "1.0.31", 20 | "version": 2 21 | } 22 | } -------------------------------------------------------------------------------- /tests/expected/delegated/metadata/2.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "delegations": { 11 | "keys": { 12 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 13 | "keytype": "ecdsa", 14 | "keyval": { 15 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 16 | }, 17 | "scheme": "ecdsa-sha2-nistp384", 18 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 19 | } 20 | }, 21 | "roles": [ 22 | { 23 | "keyids": [ 24 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 25 | ], 26 | "name": "delegated", 27 | "paths": [ 28 | "delegated/*", 29 | "delegated/*/*", 30 | "delegated/*/*/*", 31 | "delegated/*/*/*/*" 32 | ], 33 | "terminating": true, 34 | "threshold": 1 35 | } 36 | ] 37 | }, 38 | "expires": "2022-02-03T01:02:03Z", 39 | "spec_version": "1.0.31", 40 | "targets": {}, 41 | "version": 2, 42 | "x-tuf-on-ci-expiry-period": 365, 43 | "x-tuf-on-ci-signing-period": 60 44 | } 45 | } -------------------------------------------------------------------------------- /tests/expected/delegated/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-05T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 2 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 2 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/multi-user-signing/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46": { 14 | "keytype": "ecdsa", 15 | "keyval": { 16 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+jBdFHGUlKUilRL3/ReI6whKNbCZk8CL\nJFGVf5JypwD/2lr7d/6owMGuqd9ocCVVHw2GsuGRS7aCQfEvEsTgal6y2NC2gGpi\n1rsv2TGRzxKeZ7m0yX4h/vXlfJe7xys9\n-----END PUBLIC KEY-----\n" 17 | }, 18 | "scheme": "ecdsa-sha2-nistp384", 19 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user2" 20 | }, 21 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 22 | "keytype": "ed25519", 23 | "keyval": { 24 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 25 | }, 26 | "scheme": "ed25519", 27 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 28 | }, 29 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 30 | "keytype": "ecdsa", 31 | "keyval": { 32 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 33 | }, 34 | "scheme": "ecdsa-sha2-nistp384", 35 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 36 | } 37 | }, 38 | "roles": { 39 | "root": { 40 | "keyids": [ 41 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 42 | ], 43 | "threshold": 1 44 | }, 45 | "snapshot": { 46 | "keyids": [ 47 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 48 | ], 49 | "threshold": 1, 50 | "x-tuf-on-ci-expiry-period": 365, 51 | "x-tuf-on-ci-signing-period": 60 52 | }, 53 | "targets": { 54 | "keyids": [ 55 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 56 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46" 57 | ], 58 | "threshold": 2 59 | }, 60 | "timestamp": { 61 | "keyids": [ 62 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 63 | ], 64 | "threshold": 1, 65 | "x-tuf-on-ci-expiry-period": 2, 66 | "x-tuf-on-ci-signing-period": 1 67 | } 68 | }, 69 | "spec_version": "1.0.31", 70 | "version": 1, 71 | "x-tuf-on-ci-expiry-period": 365, 72 | "x-tuf-on-ci-signing-period": 60 73 | } 74 | } -------------------------------------------------------------------------------- /tests/expected/multi-user-signing/metadata/1.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "meta": { 12 | "targets.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/multi-user-signing/metadata/1.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | }, 7 | { 8 | "keyid": "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46", 9 | "sig": "XXX" 10 | } 11 | ], 12 | "signed": { 13 | "_type": "targets", 14 | "expires": "2022-02-03T01:02:03Z", 15 | "spec_version": "1.0.31", 16 | "targets": {}, 17 | "version": 1, 18 | "x-tuf-on-ci-expiry-period": 365, 19 | "x-tuf-on-ci-signing-period": 60 20 | } 21 | } -------------------------------------------------------------------------------- /tests/expected/multi-user-signing/metadata/2.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46": { 14 | "keytype": "ecdsa", 15 | "keyval": { 16 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+jBdFHGUlKUilRL3/ReI6whKNbCZk8CL\nJFGVf5JypwD/2lr7d/6owMGuqd9ocCVVHw2GsuGRS7aCQfEvEsTgal6y2NC2gGpi\n1rsv2TGRzxKeZ7m0yX4h/vXlfJe7xys9\n-----END PUBLIC KEY-----\n" 17 | }, 18 | "scheme": "ecdsa-sha2-nistp384", 19 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user2" 20 | }, 21 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 22 | "keytype": "ed25519", 23 | "keyval": { 24 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 25 | }, 26 | "scheme": "ed25519", 27 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 28 | }, 29 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 30 | "keytype": "ecdsa", 31 | "keyval": { 32 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 33 | }, 34 | "scheme": "ecdsa-sha2-nistp384", 35 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 36 | } 37 | }, 38 | "roles": { 39 | "root": { 40 | "keyids": [ 41 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 42 | ], 43 | "threshold": 1 44 | }, 45 | "snapshot": { 46 | "keyids": [ 47 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 48 | ], 49 | "threshold": 1, 50 | "x-tuf-on-ci-expiry-period": 365, 51 | "x-tuf-on-ci-signing-period": 60 52 | }, 53 | "targets": { 54 | "keyids": [ 55 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 56 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46" 57 | ], 58 | "threshold": 2 59 | }, 60 | "timestamp": { 61 | "keyids": [ 62 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 63 | ], 64 | "threshold": 1, 65 | "x-tuf-on-ci-expiry-period": 5, 66 | "x-tuf-on-ci-signing-period": 1 67 | } 68 | }, 69 | "spec_version": "1.0.31", 70 | "version": 2, 71 | "x-tuf-on-ci-expiry-period": 365, 72 | "x-tuf-on-ci-signing-period": 60 73 | } 74 | } -------------------------------------------------------------------------------- /tests/expected/multi-user-signing/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-05T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/online-version-bump/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 14 | "keytype": "ed25519", 15 | "keyval": { 16 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 17 | }, 18 | "scheme": "ed25519", 19 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 20 | }, 21 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 22 | "keytype": "ecdsa", 23 | "keyval": { 24 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 25 | }, 26 | "scheme": "ecdsa-sha2-nistp384", 27 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 10, 43 | "x-tuf-on-ci-signing-period": 4 44 | }, 45 | "targets": { 46 | "keyids": [ 47 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 48 | ], 49 | "threshold": 1 50 | }, 51 | "timestamp": { 52 | "keyids": [ 53 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 54 | ], 55 | "threshold": 1, 56 | "x-tuf-on-ci-expiry-period": 2, 57 | "x-tuf-on-ci-signing-period": 1 58 | } 59 | }, 60 | "spec_version": "1.0.31", 61 | "version": 1, 62 | "x-tuf-on-ci-expiry-period": 365, 63 | "x-tuf-on-ci-signing-period": 60 64 | } 65 | } -------------------------------------------------------------------------------- /tests/expected/online-version-bump/metadata/1.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 1, 14 | "x-tuf-on-ci-expiry-period": 365, 15 | "x-tuf-on-ci-signing-period": 60 16 | } 17 | } -------------------------------------------------------------------------------- /tests/expected/online-version-bump/metadata/2.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2021-02-24T01:02:03Z", 11 | "meta": { 12 | "targets.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 2 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/online-version-bump/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-18T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 2 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 3 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/root-key-rotation/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 14 | "keytype": "ed25519", 15 | "keyval": { 16 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 17 | }, 18 | "scheme": "ed25519", 19 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 20 | }, 21 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 22 | "keytype": "ecdsa", 23 | "keyval": { 24 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 25 | }, 26 | "scheme": "ecdsa-sha2-nistp384", 27 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 365, 43 | "x-tuf-on-ci-signing-period": 60 44 | }, 45 | "targets": { 46 | "keyids": [ 47 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 48 | ], 49 | "threshold": 1 50 | }, 51 | "timestamp": { 52 | "keyids": [ 53 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 54 | ], 55 | "threshold": 1, 56 | "x-tuf-on-ci-expiry-period": 2, 57 | "x-tuf-on-ci-signing-period": 1 58 | } 59 | }, 60 | "spec_version": "1.0.31", 61 | "version": 1, 62 | "x-tuf-on-ci-expiry-period": 365, 63 | "x-tuf-on-ci-signing-period": 60 64 | } 65 | } -------------------------------------------------------------------------------- /tests/expected/root-key-rotation/metadata/1.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "meta": { 12 | "targets.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/root-key-rotation/metadata/1.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 1, 14 | "x-tuf-on-ci-expiry-period": 365, 15 | "x-tuf-on-ci-signing-period": 60 16 | } 17 | } -------------------------------------------------------------------------------- /tests/expected/root-key-rotation/metadata/2.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | }, 7 | { 8 | "keyid": "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46", 9 | "sig": "XXX" 10 | } 11 | ], 12 | "signed": { 13 | "_type": "root", 14 | "consistent_snapshot": true, 15 | "expires": "2022-02-03T01:02:03Z", 16 | "keys": { 17 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46": { 18 | "keytype": "ecdsa", 19 | "keyval": { 20 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+jBdFHGUlKUilRL3/ReI6whKNbCZk8CL\nJFGVf5JypwD/2lr7d/6owMGuqd9ocCVVHw2GsuGRS7aCQfEvEsTgal6y2NC2gGpi\n1rsv2TGRzxKeZ7m0yX4h/vXlfJe7xys9\n-----END PUBLIC KEY-----\n" 21 | }, 22 | "scheme": "ecdsa-sha2-nistp384", 23 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user2" 24 | }, 25 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 26 | "keytype": "ed25519", 27 | "keyval": { 28 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 29 | }, 30 | "scheme": "ed25519", 31 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 32 | }, 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 34 | "keytype": "ecdsa", 35 | "keyval": { 36 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 37 | }, 38 | "scheme": "ecdsa-sha2-nistp384", 39 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 40 | } 41 | }, 42 | "roles": { 43 | "root": { 44 | "keyids": [ 45 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46" 46 | ], 47 | "threshold": 1 48 | }, 49 | "snapshot": { 50 | "keyids": [ 51 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 52 | ], 53 | "threshold": 1, 54 | "x-tuf-on-ci-expiry-period": 365, 55 | "x-tuf-on-ci-signing-period": 60 56 | }, 57 | "targets": { 58 | "keyids": [ 59 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 60 | ], 61 | "threshold": 1 62 | }, 63 | "timestamp": { 64 | "keyids": [ 65 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 66 | ], 67 | "threshold": 1, 68 | "x-tuf-on-ci-expiry-period": 2, 69 | "x-tuf-on-ci-signing-period": 1 70 | } 71 | }, 72 | "spec_version": "1.0.31", 73 | "version": 2, 74 | "x-tuf-on-ci-expiry-period": 365, 75 | "x-tuf-on-ci-signing-period": 60 76 | } 77 | } -------------------------------------------------------------------------------- /tests/expected/root-key-rotation/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-05T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 1 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 1 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/signing-event-creation/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2021-02-10T01:02:03Z", 12 | "keys": { 13 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 14 | "keytype": "ed25519", 15 | "keyval": { 16 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 17 | }, 18 | "scheme": "ed25519", 19 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 20 | }, 21 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 22 | "keytype": "ecdsa", 23 | "keyval": { 24 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 25 | }, 26 | "scheme": "ecdsa-sha2-nistp384", 27 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 7, 43 | "x-tuf-on-ci-signing-period": 6 44 | }, 45 | "targets": { 46 | "keyids": [ 47 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 48 | ], 49 | "threshold": 1 50 | }, 51 | "timestamp": { 52 | "keyids": [ 53 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 54 | ], 55 | "threshold": 1, 56 | "x-tuf-on-ci-expiry-period": 2, 57 | "x-tuf-on-ci-signing-period": 1 58 | } 59 | }, 60 | "spec_version": "1.0.31", 61 | "version": 1, 62 | "x-tuf-on-ci-expiry-period": 7, 63 | "x-tuf-on-ci-signing-period": 6 64 | } 65 | } -------------------------------------------------------------------------------- /tests/expected/signing-event-creation/metadata/2.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2021-02-12T01:02:03Z", 12 | "keys": { 13 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 14 | "keytype": "ed25519", 15 | "keyval": { 16 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 17 | }, 18 | "scheme": "ed25519", 19 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 20 | }, 21 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 22 | "keytype": "ecdsa", 23 | "keyval": { 24 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 25 | }, 26 | "scheme": "ecdsa-sha2-nistp384", 27 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 7, 43 | "x-tuf-on-ci-signing-period": 6 44 | }, 45 | "targets": { 46 | "keyids": [ 47 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 48 | ], 49 | "threshold": 1 50 | }, 51 | "timestamp": { 52 | "keyids": [ 53 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 54 | ], 55 | "threshold": 1, 56 | "x-tuf-on-ci-expiry-period": 2, 57 | "x-tuf-on-ci-signing-period": 1 58 | } 59 | }, 60 | "spec_version": "1.0.31", 61 | "version": 2, 62 | "x-tuf-on-ci-expiry-period": 7, 63 | "x-tuf-on-ci-signing-period": 6 64 | } 65 | } -------------------------------------------------------------------------------- /tests/expected/signing-event-creation/metadata/2.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2021-02-12T01:02:03Z", 11 | "spec_version": "1.0.31", 12 | "targets": {}, 13 | "version": 2, 14 | "x-tuf-on-ci-expiry-period": 7, 15 | "x-tuf-on-ci-signing-period": 6 16 | } 17 | } -------------------------------------------------------------------------------- /tests/expected/signing-event-creation/metadata/3.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2021-02-12T01:02:03Z", 11 | "meta": { 12 | "targets.json": { 13 | "version": 2 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 3 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/signing-event-creation/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-07T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 3 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 3 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/target-file-changes/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46": { 14 | "keytype": "ecdsa", 15 | "keyval": { 16 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+jBdFHGUlKUilRL3/ReI6whKNbCZk8CL\nJFGVf5JypwD/2lr7d/6owMGuqd9ocCVVHw2GsuGRS7aCQfEvEsTgal6y2NC2gGpi\n1rsv2TGRzxKeZ7m0yX4h/vXlfJe7xys9\n-----END PUBLIC KEY-----\n" 17 | }, 18 | "scheme": "ecdsa-sha2-nistp384", 19 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user2" 20 | }, 21 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 22 | "keytype": "ed25519", 23 | "keyval": { 24 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 25 | }, 26 | "scheme": "ed25519", 27 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 28 | }, 29 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 30 | "keytype": "ecdsa", 31 | "keyval": { 32 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 33 | }, 34 | "scheme": "ecdsa-sha2-nistp384", 35 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 36 | } 37 | }, 38 | "roles": { 39 | "root": { 40 | "keyids": [ 41 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 42 | ], 43 | "threshold": 1 44 | }, 45 | "snapshot": { 46 | "keyids": [ 47 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 48 | ], 49 | "threshold": 1, 50 | "x-tuf-on-ci-expiry-period": 365, 51 | "x-tuf-on-ci-signing-period": 60 52 | }, 53 | "targets": { 54 | "keyids": [ 55 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 56 | "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46" 57 | ], 58 | "threshold": 2 59 | }, 60 | "timestamp": { 61 | "keyids": [ 62 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 63 | ], 64 | "threshold": 1, 65 | "x-tuf-on-ci-expiry-period": 2, 66 | "x-tuf-on-ci-signing-period": 1 67 | } 68 | }, 69 | "spec_version": "1.0.31", 70 | "version": 1, 71 | "x-tuf-on-ci-expiry-period": 365, 72 | "x-tuf-on-ci-signing-period": 60 73 | } 74 | } -------------------------------------------------------------------------------- /tests/expected/target-file-changes/metadata/2.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "meta": { 12 | "targets.json": { 13 | "version": 3 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 2 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/target-file-changes/metadata/3.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | }, 7 | { 8 | "keyid": "75e88b2799fee265db824ad603f1c76fe14281d6a449dafdcc8e517036dccd46", 9 | "sig": "XXX" 10 | } 11 | ], 12 | "signed": { 13 | "_type": "targets", 14 | "expires": "2022-02-03T01:02:03Z", 15 | "spec_version": "1.0.31", 16 | "targets": { 17 | "file1.txt": { 18 | "hashes": { 19 | "sha256": "ac0fe9bf78cd278e66f787bcd02e035de2c4b4da41af783a1daa447c4734222d" 20 | }, 21 | "length": 15 22 | } 23 | }, 24 | "version": 3, 25 | "x-tuf-on-ci-expiry-period": 365, 26 | "x-tuf-on-ci-signing-period": 60 27 | } 28 | } -------------------------------------------------------------------------------- /tests/expected/target-file-changes/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-05T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 2 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 2 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/target-file-changes/targets/ac0fe9bf78cd278e66f787bcd02e035de2c4b4da41af783a1daa447c4734222d.file1.txt: -------------------------------------------------------------------------------- 1 | file1 modified 2 | -------------------------------------------------------------------------------- /tests/expected/target-files-in-delegated-roles/metadata/1.root.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "root", 10 | "consistent_snapshot": true, 11 | "expires": "2022-02-03T01:02:03Z", 12 | "keys": { 13 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34": { 14 | "keytype": "ed25519", 15 | "keyval": { 16 | "public": "fa472895c9756c2b9bcd1440bf867d0fa5c4edee79e9792fa9822be3dd6fcbb3" 17 | }, 18 | "scheme": "ed25519", 19 | "x-tuf-on-ci-online-uri": "file2:online-test-key" 20 | }, 21 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 22 | "keytype": "ecdsa", 23 | "keyval": { 24 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 25 | }, 26 | "scheme": "ecdsa-sha2-nistp384", 27 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 28 | } 29 | }, 30 | "roles": { 31 | "root": { 32 | "keyids": [ 33 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 34 | ], 35 | "threshold": 1 36 | }, 37 | "snapshot": { 38 | "keyids": [ 39 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 40 | ], 41 | "threshold": 1, 42 | "x-tuf-on-ci-expiry-period": 365, 43 | "x-tuf-on-ci-signing-period": 60 44 | }, 45 | "targets": { 46 | "keyids": [ 47 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 48 | ], 49 | "threshold": 1 50 | }, 51 | "timestamp": { 52 | "keyids": [ 53 | "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34" 54 | ], 55 | "threshold": 1, 56 | "x-tuf-on-ci-expiry-period": 2, 57 | "x-tuf-on-ci-signing-period": 1 58 | } 59 | }, 60 | "spec_version": "1.0.31", 61 | "version": 1, 62 | "x-tuf-on-ci-expiry-period": 365, 63 | "x-tuf-on-ci-signing-period": 60 64 | } 65 | } -------------------------------------------------------------------------------- /tests/expected/target-files-in-delegated-roles/metadata/1.targets.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "delegations": { 11 | "keys": { 12 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460": { 13 | "keytype": "ecdsa", 14 | "keyval": { 15 | "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEJ3pswWmx9Bx2VBcpqaooQFA7dQnhRafh\ntj942eg086x6EMHdfgdox9TbwGm7sU2sn/gyjyDr1ez8Ld2ORnyYJ8cAlegfTqNq\nE0eSrLrb+YpzQJxLwh6qWcSngF99Unft\n-----END PUBLIC KEY-----\n" 16 | }, 17 | "scheme": "ecdsa-sha2-nistp384", 18 | "x-tuf-on-ci-keyowner": "@tuf-on-ci-user1" 19 | } 20 | }, 21 | "roles": [ 22 | { 23 | "keyids": [ 24 | "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460" 25 | ], 26 | "name": "delegated", 27 | "paths": [ 28 | "delegated/*", 29 | "delegated/*/*", 30 | "delegated/*/*/*", 31 | "delegated/*/*/*/*" 32 | ], 33 | "terminating": true, 34 | "threshold": 1 35 | } 36 | ] 37 | }, 38 | "expires": "2022-02-03T01:02:03Z", 39 | "spec_version": "1.0.31", 40 | "targets": {}, 41 | "version": 1, 42 | "x-tuf-on-ci-expiry-period": 365, 43 | "x-tuf-on-ci-signing-period": 60 44 | } 45 | } -------------------------------------------------------------------------------- /tests/expected/target-files-in-delegated-roles/metadata/2.delegated.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "ddadf0c54d24c3429a36b7ad8434414fa35b80922497d2c99067261d38746460", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "targets", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "spec_version": "1.0.31", 12 | "targets": { 13 | "delegated/1/2/3/file2.txt": { 14 | "hashes": { 15 | "sha256": "67ee5478eaadb034ba59944eb977797b49ca6aa8d3574587f36ebcbeeb65f70e" 16 | }, 17 | "length": 6 18 | }, 19 | "delegated/file1.txt": { 20 | "hashes": { 21 | "sha256": "ecdc5536f73bdae8816f0ea40726ef5e9b810d914493075903bb90623d97b1d8" 22 | }, 23 | "length": 6 24 | } 25 | }, 26 | "version": 2, 27 | "x-tuf-on-ci-expiry-period": 365, 28 | "x-tuf-on-ci-signing-period": 60 29 | } 30 | } -------------------------------------------------------------------------------- /tests/expected/target-files-in-delegated-roles/metadata/2.snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "snapshot", 10 | "expires": "2022-02-03T01:02:03Z", 11 | "meta": { 12 | "delegated.json": { 13 | "version": 2 14 | }, 15 | "targets.json": { 16 | "version": 1 17 | } 18 | }, 19 | "spec_version": "1.0.31", 20 | "version": 2 21 | } 22 | } -------------------------------------------------------------------------------- /tests/expected/target-files-in-delegated-roles/metadata/timestamp.json: -------------------------------------------------------------------------------- 1 | { 2 | "signatures": [ 3 | { 4 | "keyid": "cda7a53138556e7c0d1737e4ba32868f3cf287e78ab9366c820115ce11383d34", 5 | "sig": "XXX" 6 | } 7 | ], 8 | "signed": { 9 | "_type": "timestamp", 10 | "expires": "2021-02-05T01:02:03Z", 11 | "meta": { 12 | "snapshot.json": { 13 | "version": 2 14 | } 15 | }, 16 | "spec_version": "1.0.31", 17 | "version": 2 18 | } 19 | } -------------------------------------------------------------------------------- /tests/expected/target-files-in-delegated-roles/targets/delegated/1/2/3/67ee5478eaadb034ba59944eb977797b49ca6aa8d3574587f36ebcbeeb65f70e.file2.txt: -------------------------------------------------------------------------------- 1 | file2 2 | -------------------------------------------------------------------------------- /tests/expected/target-files-in-delegated-roles/targets/delegated/ecdc5536f73bdae8816f0ea40726ef5e9b810d914493075903bb90623d97b1d8.file1.txt: -------------------------------------------------------------------------------- 1 | file1 2 | -------------------------------------------------------------------------------- /tests/online-test-key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEIB2aAkNI5BOJKu64zIRJMJwVL0gXcgDuYaAq5W9FDGSA 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/softhsm/README: -------------------------------------------------------------------------------- 1 | # How to create a new SoftHSM "environment" 2 | 3 | echo "directories.tokendir = tokens-user1/" > softhsm2.conf 4 | openssl genpkey -out eckey.pem -algorithm EC -pkeyopt ec_paramgen_curve:secp384r1 -pkeyopt ec_param_enc:named_curve 5 | SOFTHSM2_CONF="softhsm2.conf" softhsm2-util --init-token --slot 0 --label "mytoken" --so-pin 0000 --pin 0000 6 | SOFTHSM2_CONF="softhsm2.conf" softhsm2-util --import eckey.pem --token mytoken --label "mykey" --id 02 --pin 0000 7 | 8 | -------------------------------------------------------------------------------- /tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/35fbb8af-9b92-0871-980b-8c3eabf1a1cb.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/35fbb8af-9b92-0871-980b-8c3eabf1a1cb.lock -------------------------------------------------------------------------------- /tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/35fbb8af-9b92-0871-980b-8c3eabf1a1cb.object: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/35fbb8af-9b92-0871-980b-8c3eabf1a1cb.object -------------------------------------------------------------------------------- /tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/9a14c021-cf89-2aff-1294-338c773c4c20.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/9a14c021-cf89-2aff-1294-338c773c4c20.lock -------------------------------------------------------------------------------- /tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/9a14c021-cf89-2aff-1294-338c773c4c20.object: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/9a14c021-cf89-2aff-1294-338c773c4c20.object -------------------------------------------------------------------------------- /tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/generation: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/token.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/token.lock -------------------------------------------------------------------------------- /tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/token.object: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user1/6087c5cf-c6e9-8a71-a185-1322adf50f3f/token.object -------------------------------------------------------------------------------- /tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/1c2cd9d1-b5a3-479f-40bb-42d398bdaaa2.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/1c2cd9d1-b5a3-479f-40bb-42d398bdaaa2.lock -------------------------------------------------------------------------------- /tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/1c2cd9d1-b5a3-479f-40bb-42d398bdaaa2.object: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/1c2cd9d1-b5a3-479f-40bb-42d398bdaaa2.object -------------------------------------------------------------------------------- /tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/36f4e31a-e76a-3c7f-b437-ad7d0fc2a7e5.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/36f4e31a-e76a-3c7f-b437-ad7d0fc2a7e5.lock -------------------------------------------------------------------------------- /tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/36f4e31a-e76a-3c7f-b437-ad7d0fc2a7e5.object: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/36f4e31a-e76a-3c7f-b437-ad7d0fc2a7e5.object -------------------------------------------------------------------------------- /tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/generation: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/token.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/token.lock -------------------------------------------------------------------------------- /tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/token.object: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theupdateframework/tuf-on-ci/ca2861cbef417143dff49228a2b8f3f9653c1add/tests/softhsm/tokens-user2/3073b868-0942-0a4a-0bdc-bf54cea956eb/token.object -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = lint-signer, lint-repo, test-repo, test-signer, test-e2e 3 | 4 | [testenv:lint-signer] 5 | description = Signer Linting 6 | labels = lint 7 | deps = -e signer[lint] 8 | changedir = signer 9 | commands = 10 | ruff check . 11 | ruff format --check --diff . 12 | mypy . 13 | 14 | [testenv:lint-repo] 15 | description = Repository Linting 16 | labels = lint 17 | deps = 18 | -c action-constraints.txt 19 | -e repo[lint] 20 | changedir = repo 21 | commands = 22 | ruff check . 23 | ruff format --check --diff . 24 | mypy . 25 | 26 | [testenv:lint-actions] 27 | description = Actions Linting 28 | labels = lint 29 | deps = -r actions/lint-requirements.txt 30 | commands = 31 | zizmor --pedantic . 32 | 33 | [testenv:test-repo] 34 | description = Repository unit tests 35 | labels = test 36 | deps = 37 | -c action-constraints.txt 38 | -e repo 39 | changedir = repo 40 | commands = 41 | python -m unittest 42 | 43 | [testenv:test-signer] 44 | description = Signer unit tests 45 | labels = test 46 | deps = -e signer 47 | 48 | changedir = signer 49 | commands = 50 | python -m unittest 51 | 52 | [testenv:test-e2e] 53 | # See tests/README.md for the system dependencies 54 | description = End-to-end tests with mocked GitHub Actions 55 | labels = test 56 | deps = 57 | -c action-constraints.txt 58 | -e repo 59 | -e signer 60 | 61 | changedir = tests 62 | commands = 63 | ./e2e.sh 64 | --------------------------------------------------------------------------------