diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..022e304 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + time: "08:00" + labels: + - "dependencies" + commit-message: + prefix: "feat" + include: "scope" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "08:00" + labels: + - "dependencies" + commit-message: + prefix: "chore" + include: "scope" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + time: "08:00" + labels: + - "dependencies" + commit-message: + prefix: "feat" + include: "scope" diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml new file mode 100644 index 0000000..3c90424 --- /dev/null +++ b/.github/release-drafter-config.yml @@ -0,0 +1,62 @@ +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" +categories: + - title: "🚀 Features" + labels: + - "feat" + - "feature" + - "enhancement" + - title: "🐛 Bug Fixes" + labels: + - "fix" + - "bugfix" + - "bug" + - title: "🧰 Maintenance" + labels: + - "chore" + +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - "major" + minor: + labels: + - "minor" + patch: + labels: + - "patch" + default: patch +template: | + ## Changes + + $CHANGES + +autolabeler: + - label: "chore" + files: + - "*.md" + branch: + - '/docs{0,1}\/.+/' + - '/tests{0,1}\/.+/' + - '/chore{0,1}\/.+/' + title: + - "/docs/i" + - "/test/i" + - "/chore/i" + - label: "bug" + branch: + - '/fix\/.+/' + - '/revert\/.+/' + title: + - "/fix/i" + - "/revert/i" + - label: "feature" + branch: + - '/feature\/.+/' + - '/feat\/.+/' + - '/add\/.+/' + title: + - "/feat/i" + - "/add/i" diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..02f2cba --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,22 @@ +name: Release Drafter +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize] +permissions: + contents: read +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter-config.yml + publish: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..17a5ee3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: test code +on: + push: + branches: + - main + pull_request: + workflow_dispatch: +jobs: + golang: + strategy: + matrix: + go-version: [1.21.x] + # temp disable windows tests see https://github.com/docker/image-signer-verifier/pull/154 + # os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Set git to use LF + run: git config --global core.autocrlf false + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Setup Testcontainers Cloud Client + uses: atomicjar/testcontainers-cloud-setup-action@v1 + with: + token: ${{ secrets.TC_CLOUD_TOKEN }} + - name: go test + run: go test ./... diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6f6b046 --- /dev/null +++ b/go.mod @@ -0,0 +1,82 @@ +module github.com/docker/attest + +go 1.22.1 + +require ( + github.com/google/go-containerregistry v0.19.1 + github.com/stretchr/testify v1.9.0 + github.com/testcontainers/testcontainers-go v0.30.0 + github.com/testcontainers/testcontainers-go/modules/registry v0.30.0 + github.com/theupdateframework/go-tuf/v2 v2.0.0-20240402164131-b2e024ad4752 +) + +replace github.com/theupdateframework/go-tuf/v2 => github.com/mrjoelkamp/go-tuf/v2 v2.0.1 // for https://github.com/theupdateframework/go-tuf/pull/632 + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/containerd v1.7.12 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v25.0.5+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.16.5 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sigstore/sigstore v1.8.3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect + github.com/yusufpapurcu/wmi v1.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/tools v0.13.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + google.golang.org/grpc v1.58.3 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3950aba --- /dev/null +++ b/go.sum @@ -0,0 +1,257 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= +github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= +github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e h1:RLTpX495BXToqxpM90Ws4hXEo4Wfh81jr9DX1n/4WOo= +github.com/letsencrypt/boulder v0.0.0-20230907030200-6d76a0f91e1e/go.mod h1:EAuqr9VFWxBi9nD5jc/EA2MT1RFty9288TF6zdtYoCU= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrjoelkamp/go-tuf/v2 v2.0.1 h1:nDJGPlrU05sirPlA16M1XJiGDqM0zMwguA4cVgCJ9YY= +github.com/mrjoelkamp/go-tuf/v2 v2.0.1/go.mod h1:LJo5jrV0LYV0jVSbCjPem6+0zrkPz8FnimzIECzsFDY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= +github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sigstore/sigstore v1.8.3 h1:G7LVXqL+ekgYtYdksBks9B38dPoIsbscjQJX/MGWkA4= +github.com/sigstore/sigstore v1.8.3/go.mod h1:mqbTEariiGA94cn6G3xnDiV6BD8eSLdL/eA7bvJ0fVs= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.30.0 h1:jmn/XS22q4YRrcMwWg0pAwlClzs/abopbsBzrepyc4E= +github.com/testcontainers/testcontainers-go v0.30.0/go.mod h1:K+kHNGiM5zjklKjgTtcrEetF3uhWbMUyqAQoyoh8Pf0= +github.com/testcontainers/testcontainers-go/modules/registry v0.30.0 h1:/GYaNnQ09Gvwv3GvhWYbzL2gQiqwzlqDyQZ175uVPC4= +github.com/testcontainers/testcontainers-go/modules/registry v0.30.0/go.mod h1:bu2AS7kGxJQgZ16qbb5SHKSuEVrriENPIpKugl0aCHA= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4= +golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= +google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/internal/embed/embedded-roots/1.root-dev.json b/internal/embed/embedded-roots/1.root-dev.json new file mode 100644 index 0000000..5e0adaa --- /dev/null +++ b/internal/embed/embedded-roots/1.root-dev.json @@ -0,0 +1,65 @@ +{ + "signatures": [ + { + "keyid": "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "sig": "3064023037bbb03c3472b140572a7d5a2895bd80e74435bbcb7053949731f81b104c6d05a0876590cd6a2e94d7ed619426a2f6fa02303adc8c9006fa5506fdd7ea87d2960074a537ad8bf2459f2863e806b47682cbb2f9b01b7502eaf5437a1a68fdaaeac114" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2034-04-02T17:00:22Z", + "keys": { + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61" + }, + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp384", + "x-tuf-on-ci-keyowner": "@mrjoelkamp" + } + }, + "roles": { + "root": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + }, + "targets": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } + }, + "spec_version": "1.0.31", + "version": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/embed/embedded-roots/1.root-staging.json b/internal/embed/embedded-roots/1.root-staging.json new file mode 100644 index 0000000..320afac --- /dev/null +++ b/internal/embed/embedded-roots/1.root-staging.json @@ -0,0 +1,79 @@ +{ + "signatures": [ + { + "keyid": "3da0404c531197e1e04622fb6ebcfe67ca462966c16115f856e8bba059b5f1de", + "sig": "30450221008b9de747c24c07586cddf0aa25ecb37dbe9ce4f8cf2d5316fd7a470d42c803db0220715270e40c79b0b4af9858db44b10cec1a2f14ca5c217b1f1f6835f3a1ff843c" + }, + { + "keyid": "e642e70171046d6d97efdea76792c373d863c55c054c4287c999c62c6011120f", + "sig": "3046022100c087742a7d10869163be844e4453566af461604cee99ab42560cef3136009cd4022100bedab954a32a693a9da63c2050a8cf4a2bd45e96daf586e489ad0fdd71ada2fd" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2024-11-15T17:56:20Z", + "keys": { + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61" + }, + "3da0404c531197e1e04622fb6ebcfe67ca462966c16115f856e8bba059b5f1de": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEERet/8hs3WHIXyOXNzhLpTOz6DBx\n7zzHnenJgV/TB0dRMAx6j9UVRvlEkh5OcYuktNeqnLpHce1rLjLjpiRPVg==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@jonnystoten" + }, + "e642e70171046d6d97efdea76792c373d863c55c054c4287c999c62c6011120f": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMtH9o0x/EHc/Rzoco4RyqmR7UwA\n0sHROw/79CMdbPh3/egmMxci3N+dJl6Re/cNkqR9eQy7joULS2K9Oxgxww==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@mrjoelkamp" + } + }, + "roles": { + "root": { + "keyids": [ + "3da0404c531197e1e04622fb6ebcfe67ca462966c16115f856e8bba059b5f1de", + "e642e70171046d6d97efdea76792c373d863c55c054c4287c999c62c6011120f" + ], + "threshold": 2 + }, + "snapshot": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 365, + "x-tuf-on-ci-signing-period": 60 + }, + "targets": { + "keyids": [ + "3da0404c531197e1e04622fb6ebcfe67ca462966c16115f856e8bba059b5f1de", + "e642e70171046d6d97efdea76792c373d863c55c054c4287c999c62c6011120f" + ], + "threshold": 2 + }, + "timestamp": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 2, + "x-tuf-on-ci-signing-period": 1 + } + }, + "spec_version": "1.0.31", + "version": 1, + "x-tuf-on-ci-expiry-period": 365, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/embed/root.go b/internal/embed/root.go new file mode 100644 index 0000000..67259ab --- /dev/null +++ b/internal/embed/root.go @@ -0,0 +1,13 @@ +package embed + +import ( + _ "embed" +) + +//go:embed embedded-roots/1.root-dev.json +var DevRoot []byte + +//go:embed embedded-roots/1.root-staging.json +var StagingRoot []byte + +var DefaultRoot = StagingRoot diff --git a/internal/test/test.go b/internal/test/test.go new file mode 100644 index 0000000..dadd5b8 --- /dev/null +++ b/internal/test/test.go @@ -0,0 +1,22 @@ +package test + +import ( + "os" + "testing" +) + +func CreateTempDir(t *testing.T, dir, pattern string) string { + // Create a temporary directory for output oci layout + tempDir, err := os.MkdirTemp(dir, pattern) + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + + // Register a cleanup function to delete the temp directory when the test exits + t.Cleanup(func() { + if err := os.RemoveAll(tempDir); err != nil { + t.Errorf("Failed to remove temp directory: %v", err) + } + }) + return tempDir +} diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/1fd0d9781f02486718fcbd7724db0e4c4ba47b649930cec22a3e7e6b6077ba38 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/1fd0d9781f02486718fcbd7724db0e4c4ba47b649930cec22a3e7e6b6077ba38 new file mode 100644 index 0000000..4458b1a --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/1fd0d9781f02486718fcbd7724db0e4c4ba47b649930cec22a3e7e6b6077ba38 @@ -0,0 +1 @@ +{"signatures":[{"keyid":"198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3","sig":"3044022039b56cd2e3597df74e57d200a652ba020cdc9a8cd050bd65b5f8e2640d50691d02205e073e4b6fc260acc64327a331e4440601af5b1cbff594ea91cf7b70d5828fb1"}],"signed":{"_type":"snapshot","expires":"2034-04-03T15:59:47Z","meta":{"targets.json":{"version":5},"test-role.json":{"version":3}},"spec_version":"1.0.31","version":6}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/4c1054844dba3241525cbd71ff9e58becca652fb1ce4a0e6ea55a01c4ec41950 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/4c1054844dba3241525cbd71ff9e58becca652fb1ce4a0e6ea55a01c4ec41950 new file mode 100644 index 0000000..66d05ac --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/4c1054844dba3241525cbd71ff9e58becca652fb1ce4a0e6ea55a01c4ec41950 @@ -0,0 +1 @@ +{"signatures":[{"keyid":"198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3","sig":"3045022011f2afa9b448fcbbac983c11fc3e264e95d5d7a9c9527b09d83a316ee762635f022100d05197a78ccc7a713ebdb0bccb44844f67a7c5208af8d346e201064b7ce11055"}],"signed":{"_type":"timestamp","expires":"2034-04-03T15:59:47Z","meta":{"snapshot.json":{"version":6}},"spec_version":"1.0.31","version":6}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/61a98e1e86ae279e59415d927e38beae430d7e6d2bd6207054179429ea9b6763 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/61a98e1e86ae279e59415d927e38beae430d7e6d2bd6207054179429ea9b6763 new file mode 100644 index 0000000..5e0adaa --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/61a98e1e86ae279e59415d927e38beae430d7e6d2bd6207054179429ea9b6763 @@ -0,0 +1,65 @@ +{ + "signatures": [ + { + "keyid": "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "sig": "3064023037bbb03c3472b140572a7d5a2895bd80e74435bbcb7053949731f81b104c6d05a0876590cd6a2e94d7ed619426a2f6fa02303adc8c9006fa5506fdd7ea87d2960074a537ad8bf2459f2863e806b47682cbb2f9b01b7502eaf5437a1a68fdaaeac114" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2034-04-02T17:00:22Z", + "keys": { + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61" + }, + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp384", + "x-tuf-on-ci-keyowner": "@mrjoelkamp" + } + }, + "roles": { + "root": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + }, + "targets": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } + }, + "spec_version": "1.0.31", + "version": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a152e59e7b58a3a1ec718192b07fbf630d2fe2d80a6f1a9dc09e1321cbd338a7 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a152e59e7b58a3a1ec718192b07fbf630d2fe2d80a6f1a9dc09e1321cbd338a7 new file mode 100644 index 0000000..2a162c9 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a152e59e7b58a3a1ec718192b07fbf630d2fe2d80a6f1a9dc09e1321cbd338a7 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":669,"digest":"sha256:ad4cacc170229608305ffccd8d09eeb59578fcb72ae394763cf7ef492175b1ee"},"layers":[{"mediaType":"application/vnd.tuf.metadata+json","size":2607,"digest":"sha256:a2e026ce65c198ee68a7ed2df6978ed0287bb38342f6ddb7bf934a456f1d6f87","annotations":{"tuf.io/filename":"2.root.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":2200,"digest":"sha256:61a98e1e86ae279e59415d927e38beae430d7e6d2bd6207054179429ea9b6763","annotations":{"tuf.io/filename":"1.root.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":410,"digest":"sha256:1fd0d9781f02486718fcbd7724db0e4c4ba47b649930cec22a3e7e6b6077ba38","annotations":{"tuf.io/filename":"6.snapshot.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":1683,"digest":"sha256:ea7713eb649ca1a33d79ebdccda9f7f066595b1b2c6e37e52dbfd250f5287260","annotations":{"tuf.io/filename":"5.targets.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":383,"digest":"sha256:4c1054844dba3241525cbd71ff9e58becca652fb1ce4a0e6ea55a01c4ec41950","annotations":{"tuf.io/filename":"timestamp.json"}}]} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a2e026ce65c198ee68a7ed2df6978ed0287bb38342f6ddb7bf934a456f1d6f87 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a2e026ce65c198ee68a7ed2df6978ed0287bb38342f6ddb7bf934a456f1d6f87 new file mode 100644 index 0000000..b75c99c --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a2e026ce65c198ee68a7ed2df6978ed0287bb38342f6ddb7bf934a456f1d6f87 @@ -0,0 +1 @@ +{"signatures":[{"keyid":"b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09","sig":"3066023100e99acc5f74777ebf40376b60f0216e8fe1829c1a49a5f6a6899126c15de1df7a56533baf493b2b53159c50843a289102023100b6a006b24da62ea0b743fbe38e1497ff485bf3a0833894985fc27a0305ad0693eeb968a7b52723ed3c49af8bef2027b6"},{"keyid":"81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664","sig":"30440220136debcc2f60dd1d63c9c2704f9b13c2cb2f5d2df58ea93f07f7c10f54f36742022059d7f8c6620e33506c6f1766394a32f86c9b008328f6398831ba7ebcf4ce0838"}],"signed":{"_type":"root","consistent_snapshot":true,"expires":"2034-04-03T08:45:50Z","keys":{"198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3":{"keytype":"ecdsa","keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n"},"scheme":"ecdsa-sha2-nistp256","x-tuf-on-ci-online-uri":"awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61"},"81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664":{"keytype":"ecdsa","keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWmhpAfB7Q53UNluMhpkDxXXup4E0\n2Hh4PSgHC1Yh6brGl6Akq9a4io55LtZTk5mnCTqxuB+rc5cI/yaNUeWEqQ==\n-----END PUBLIC KEY-----\n"},"scheme":"ecdsa-sha2-nistp256","x-tuf-on-ci-keyowner":"@kipz"},"b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09":{"keytype":"ecdsa","keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n"},"scheme":"ecdsa-sha2-nistp384","x-tuf-on-ci-keyowner":"@mrjoelkamp"}},"roles":{"root":{"keyids":["b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09","81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664"],"threshold":1},"snapshot":{"keyids":["198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3"],"threshold":1,"x-tuf-on-ci-expiry-period":3650,"x-tuf-on-ci-signing-period":60},"targets":{"keyids":["b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09","81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664"],"threshold":1},"timestamp":{"keyids":["198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3"],"threshold":1,"x-tuf-on-ci-expiry-period":3650,"x-tuf-on-ci-signing-period":60}},"spec_version":"1.0.31","version":2,"x-tuf-on-ci-expiry-period":3650,"x-tuf-on-ci-signing-period":60}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a744a2f1e62ae4ce410822b5e3f5508dbaf6a76768a9d23741828172bab1dc97 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a744a2f1e62ae4ce410822b5e3f5508dbaf6a76768a9d23741828172bab1dc97 new file mode 100644 index 0000000..32a2181 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/a744a2f1e62ae4ce410822b5e3f5508dbaf6a76768a9d23741828172bab1dc97 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":669,"digest":"sha256:c927b30f17fa8c64e3c20b8f92b7e348733f9c1281b5b7e6b6d669a8a74230a7"},"layers":[{"mediaType":"application/vnd.tuf.metadata+json","size":2200,"digest":"sha256:61a98e1e86ae279e59415d927e38beae430d7e6d2bd6207054179429ea9b6763","annotations":{"tuf.io/filename":"1.root.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":2607,"digest":"sha256:a2e026ce65c198ee68a7ed2df6978ed0287bb38342f6ddb7bf934a456f1d6f87","annotations":{"tuf.io/filename":"2.root.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":410,"digest":"sha256:1fd0d9781f02486718fcbd7724db0e4c4ba47b649930cec22a3e7e6b6077ba38","annotations":{"tuf.io/filename":"6.snapshot.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":1683,"digest":"sha256:ea7713eb649ca1a33d79ebdccda9f7f066595b1b2c6e37e52dbfd250f5287260","annotations":{"tuf.io/filename":"5.targets.json"}},{"mediaType":"application/vnd.tuf.metadata+json","size":383,"digest":"sha256:4c1054844dba3241525cbd71ff9e58becca652fb1ce4a0e6ea55a01c4ec41950","annotations":{"tuf.io/filename":"timestamp.json"}}]} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/ad4cacc170229608305ffccd8d09eeb59578fcb72ae394763cf7ef492175b1ee b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/ad4cacc170229608305ffccd8d09eeb59578fcb72ae394763cf7ef492175b1ee new file mode 100644 index 0000000..fbf0a18 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/ad4cacc170229608305ffccd8d09eeb59578fcb72ae394763cf7ef492175b1ee @@ -0,0 +1 @@ +{"architecture":"","created":"0001-01-01T00:00:00Z","history":[{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"}],"os":"","rootfs":{"type":"layers","diff_ids":["sha256:a2e026ce65c198ee68a7ed2df6978ed0287bb38342f6ddb7bf934a456f1d6f87","sha256:61a98e1e86ae279e59415d927e38beae430d7e6d2bd6207054179429ea9b6763","sha256:1fd0d9781f02486718fcbd7724db0e4c4ba47b649930cec22a3e7e6b6077ba38","sha256:ea7713eb649ca1a33d79ebdccda9f7f066595b1b2c6e37e52dbfd250f5287260","sha256:4c1054844dba3241525cbd71ff9e58becca652fb1ce4a0e6ea55a01c4ec41950"]},"config":{}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/c927b30f17fa8c64e3c20b8f92b7e348733f9c1281b5b7e6b6d669a8a74230a7 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/c927b30f17fa8c64e3c20b8f92b7e348733f9c1281b5b7e6b6d669a8a74230a7 new file mode 100644 index 0000000..590bd69 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/c927b30f17fa8c64e3c20b8f92b7e348733f9c1281b5b7e6b6d669a8a74230a7 @@ -0,0 +1 @@ +{"architecture":"","created":"0001-01-01T00:00:00Z","history":[{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"},{"created":"0001-01-01T00:00:00Z"}],"os":"","rootfs":{"type":"layers","diff_ids":["sha256:61a98e1e86ae279e59415d927e38beae430d7e6d2bd6207054179429ea9b6763","sha256:a2e026ce65c198ee68a7ed2df6978ed0287bb38342f6ddb7bf934a456f1d6f87","sha256:1fd0d9781f02486718fcbd7724db0e4c4ba47b649930cec22a3e7e6b6077ba38","sha256:ea7713eb649ca1a33d79ebdccda9f7f066595b1b2c6e37e52dbfd250f5287260","sha256:4c1054844dba3241525cbd71ff9e58becca652fb1ce4a0e6ea55a01c4ec41950"]},"config":{}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/ea7713eb649ca1a33d79ebdccda9f7f066595b1b2c6e37e52dbfd250f5287260 b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/ea7713eb649ca1a33d79ebdccda9f7f066595b1b2c6e37e52dbfd250f5287260 new file mode 100644 index 0000000..c716439 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/blobs/sha256/ea7713eb649ca1a33d79ebdccda9f7f066595b1b2c6e37e52dbfd250f5287260 @@ -0,0 +1 @@ +{"signatures":[{"keyid":"b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09","sig":""},{"keyid":"81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664","sig":"3046022100f892a496c9bd96082e3b06d5eae85429355876b8eb455aa04b53ab9051911d90022100a3e89c29b15bccfc2877278c0fb2d3b34500da6351e245ad0b3f8c0ae6b67eff"}],"signed":{"_type":"targets","delegations":{"keys":{"81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664":{"keytype":"ecdsa","keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWmhpAfB7Q53UNluMhpkDxXXup4E0\n2Hh4PSgHC1Yh6brGl6Akq9a4io55LtZTk5mnCTqxuB+rc5cI/yaNUeWEqQ==\n-----END PUBLIC KEY-----\n"},"scheme":"ecdsa-sha2-nistp256","x-tuf-on-ci-keyowner":"@kipz"},"b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09":{"keytype":"ecdsa","keyval":{"public":"-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n"},"scheme":"ecdsa-sha2-nistp384","x-tuf-on-ci-keyowner":"@mrjoelkamp"}},"roles":[{"keyids":["b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09","81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664"],"name":"test-role","paths":["test-role/*","test-role/*/*","test-role/*/*/*","test-role/*/*/*/*"],"terminating":true,"threshold":1}]},"expires":"2034-04-03T15:28:29Z","spec_version":"1.0.31","targets":{"test.txt":{"hashes":{"sha256":"02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b"},"length":31}},"version":5,"x-tuf-on-ci-expiry-period":3650,"x-tuf-on-ci-signing-period":60}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/index.json b/internal/test/testdata/test-repo-oci/metadata/index.json new file mode 100755 index 0000000..f72132f --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/index.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 1220, + "digest": "sha256:a744a2f1e62ae4ce410822b5e3f5508dbaf6a76768a9d23741828172bab1dc97" + } + ] +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/oci-layout b/internal/test/testdata/test-repo-oci/metadata/oci-layout new file mode 100755 index 0000000..224a869 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/2b2d4fba192ec164e05e6d90399c5cf4a45e4fe2ddebb9066c55aa2bcf0a73d3 b/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/2b2d4fba192ec164e05e6d90399c5cf4a45e4fe2ddebb9066c55aa2bcf0a73d3 new file mode 100644 index 0000000..a3d28e4 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/2b2d4fba192ec164e05e6d90399c5cf4a45e4fe2ddebb9066c55aa2bcf0a73d3 @@ -0,0 +1 @@ +{"signatures":[{"keyid":"b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09","sig":""},{"keyid":"81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664","sig":"3044022015b6ebe9d30895e3be20e707a6738e38460197d90cae3dc37527ddb7c437868602207f85f3d4e068bef4c51a749f5d166cc7fe2cb9483999ea197e72395081c3aa61"}],"signed":{"_type":"targets","expires":"2034-04-03T15:39:02Z","spec_version":"1.0.31","targets":{"test-role/dir1/dir2/dir3/myfile.txt":{"hashes":{"sha256":"ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1"},"length":10},"test-role/test.txt":{"hashes":{"sha256":"d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2"},"length":32}},"version":3,"x-tuf-on-ci-expiry-period":3650,"x-tuf-on-ci-signing-period":60}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/7c8d8f5dfca62068e3a4b18bb41cf85dad23ec9cdc7d7d2e10bc37b86ebffff5 b/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/7c8d8f5dfca62068e3a4b18bb41cf85dad23ec9cdc7d7d2e10bc37b86ebffff5 new file mode 100644 index 0000000..8b79d0a --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/7c8d8f5dfca62068e3a4b18bb41cf85dad23ec9cdc7d7d2e10bc37b86ebffff5 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":233,"digest":"sha256:9edf24c022c2cd6796e87f49ec6a6ea2fad3e7c939c32a8219aaa4726792457c"},"layers":[{"mediaType":"application/vnd.tuf.metadata+json","size":764,"digest":"sha256:2b2d4fba192ec164e05e6d90399c5cf4a45e4fe2ddebb9066c55aa2bcf0a73d3","annotations":{"tuf.io/filename":"3.test-role.json"}}]} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/9edf24c022c2cd6796e87f49ec6a6ea2fad3e7c939c32a8219aaa4726792457c b/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/9edf24c022c2cd6796e87f49ec6a6ea2fad3e7c939c32a8219aaa4726792457c new file mode 100644 index 0000000..dd48ffc --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/test-role/blobs/sha256/9edf24c022c2cd6796e87f49ec6a6ea2fad3e7c939c32a8219aaa4726792457c @@ -0,0 +1 @@ +{"architecture":"","created":"0001-01-01T00:00:00Z","history":[{"created":"0001-01-01T00:00:00Z"}],"os":"","rootfs":{"type":"layers","diff_ids":["sha256:2b2d4fba192ec164e05e6d90399c5cf4a45e4fe2ddebb9066c55aa2bcf0a73d3"]},"config":{}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/test-role/index.json b/internal/test/testdata/test-repo-oci/metadata/test-role/index.json new file mode 100755 index 0000000..4fe9454 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/test-role/index.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 444, + "digest": "sha256:7c8d8f5dfca62068e3a4b18bb41cf85dad23ec9cdc7d7d2e10bc37b86ebffff5" + } + ] +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/metadata/test-role/oci-layout b/internal/test/testdata/test-repo-oci/metadata/test-role/oci-layout new file mode 100755 index 0000000..224a869 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/metadata/test-role/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b new file mode 100644 index 0000000..c5c3e8d --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b @@ -0,0 +1 @@ +this is a top-level target file \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/cf0c754e6415fab25e2f59fb6b010dcf0c2369f7a59a45ff29c693c844163ca7 b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/cf0c754e6415fab25e2f59fb6b010dcf0c2369f7a59a45ff29c693c844163ca7 new file mode 100644 index 0000000..8a2d027 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/cf0c754e6415fab25e2f59fb6b010dcf0c2369f7a59a45ff29c693c844163ca7 @@ -0,0 +1 @@ +{"architecture":"","created":"0001-01-01T00:00:00Z","history":[{"created":"0001-01-01T00:00:00Z"}],"os":"","rootfs":{"type":"layers","diff_ids":["sha256:02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b"]},"config":{}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/cf70a3b91fd7dfaa30952dfa9f094809e6cd9bd7364942c7f067c747bc535f94 b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/cf70a3b91fd7dfaa30952dfa9f094809e6cd9bd7364942c7f067c747bc535f94 new file mode 100644 index 0000000..c3dee4b --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/blobs/sha256/cf70a3b91fd7dfaa30952dfa9f094809e6cd9bd7364942c7f067c747bc535f94 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":233,"digest":"sha256:cf0c754e6415fab25e2f59fb6b010dcf0c2369f7a59a45ff29c693c844163ca7"},"layers":[{"mediaType":"application/vnd.tuf.target","size":31,"digest":"sha256:02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b","annotations":{"tuf.io/filename":"02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt"}}]} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/index.json b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/index.json new file mode 100755 index 0000000..9a03dcf --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/index.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 493, + "digest": "sha256:cf70a3b91fd7dfaa30952dfa9f094809e6cd9bd7364942c7f067c747bc535f94" + } + ] +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/oci-layout b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/oci-layout new file mode 100755 index 0000000..224a869 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/0a4afcdad291941327b070ab4feaf052425fbf4ded864bc55c18cfefec8be6e2 b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/0a4afcdad291941327b070ab4feaf052425fbf4ded864bc55c18cfefec8be6e2 new file mode 100644 index 0000000..cdcc345 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/0a4afcdad291941327b070ab4feaf052425fbf4ded864bc55c18cfefec8be6e2 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":233,"digest":"sha256:d9941355ca037d7e878e04c1bc7cbf9c71a5d8035b6e27be0d9e5d9087599055"},"layers":[{"mediaType":"application/vnd.tuf.target","size":32,"digest":"sha256:d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2","annotations":{"tuf.io/filename":"d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2.test.txt"}}]} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/0b6b8fdb10421310b9aca2f1fb6ce51537baa243fb9fccca03f2ff3c15fb52f8 b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/0b6b8fdb10421310b9aca2f1fb6ce51537baa243fb9fccca03f2ff3c15fb52f8 new file mode 100644 index 0000000..adb249b --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/0b6b8fdb10421310b9aca2f1fb6ce51537baa243fb9fccca03f2ff3c15fb52f8 @@ -0,0 +1 @@ +{"architecture":"","created":"0001-01-01T00:00:00Z","history":[{"created":"0001-01-01T00:00:00Z"}],"os":"","rootfs":{"type":"layers","diff_ids":["sha256:ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1"]},"config":{}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/8d320e9d3f3663613df6e4fca1651604a6c0323011023145a140b38f02105b04 b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/8d320e9d3f3663613df6e4fca1651604a6c0323011023145a140b38f02105b04 new file mode 100644 index 0000000..3ddab28 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/8d320e9d3f3663613df6e4fca1651604a6c0323011023145a140b38f02105b04 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","size":233,"digest":"sha256:0b6b8fdb10421310b9aca2f1fb6ce51537baa243fb9fccca03f2ff3c15fb52f8"},"layers":[{"mediaType":"application/vnd.tuf.target","size":10,"digest":"sha256:ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1","annotations":{"tuf.io/filename":"ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1.myfile.txt"}}]} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2 b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2 new file mode 100644 index 0000000..df7d74c --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2 @@ -0,0 +1 @@ +this is a delegated targets file \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/d9941355ca037d7e878e04c1bc7cbf9c71a5d8035b6e27be0d9e5d9087599055 b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/d9941355ca037d7e878e04c1bc7cbf9c71a5d8035b6e27be0d9e5d9087599055 new file mode 100644 index 0000000..f87a003 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/d9941355ca037d7e878e04c1bc7cbf9c71a5d8035b6e27be0d9e5d9087599055 @@ -0,0 +1 @@ +{"architecture":"","created":"0001-01-01T00:00:00Z","history":[{"created":"0001-01-01T00:00:00Z"}],"os":"","rootfs":{"type":"layers","diff_ids":["sha256:d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2"]},"config":{}} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1 b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1 new file mode 100644 index 0000000..327dce1 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/blobs/sha256/ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1 @@ -0,0 +1 @@ +hello tuf diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/index.json b/internal/test/testdata/test-repo-oci/targets/test-role/index.json new file mode 100755 index 0000000..21fe556 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":495,"digest":"sha256:8d320e9d3f3663613df6e4fca1651604a6c0323011023145a140b38f02105b04","annotations":{"tuf.io/filename":"test-role/dir1/dir2/dir3/ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1.myfile.txt"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","size":493,"digest":"sha256:0a4afcdad291941327b070ab4feaf052425fbf4ded864bc55c18cfefec8be6e2","annotations":{"tuf.io/filename":"test-role/d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2.test.txt"}}]} \ No newline at end of file diff --git a/internal/test/testdata/test-repo-oci/targets/test-role/oci-layout b/internal/test/testdata/test-repo-oci/targets/test-role/oci-layout new file mode 100755 index 0000000..224a869 --- /dev/null +++ b/internal/test/testdata/test-repo-oci/targets/test-role/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo/metadata/1.root.json b/internal/test/testdata/test-repo/metadata/1.root.json new file mode 100644 index 0000000..5e0adaa --- /dev/null +++ b/internal/test/testdata/test-repo/metadata/1.root.json @@ -0,0 +1,65 @@ +{ + "signatures": [ + { + "keyid": "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "sig": "3064023037bbb03c3472b140572a7d5a2895bd80e74435bbcb7053949731f81b104c6d05a0876590cd6a2e94d7ed619426a2f6fa02303adc8c9006fa5506fdd7ea87d2960074a537ad8bf2459f2863e806b47682cbb2f9b01b7502eaf5437a1a68fdaaeac114" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2034-04-02T17:00:22Z", + "keys": { + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61" + }, + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp384", + "x-tuf-on-ci-keyowner": "@mrjoelkamp" + } + }, + "roles": { + "root": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + }, + "targets": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } + }, + "spec_version": "1.0.31", + "version": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo/metadata/2.root.json b/internal/test/testdata/test-repo/metadata/2.root.json new file mode 100644 index 0000000..3601eb2 --- /dev/null +++ b/internal/test/testdata/test-repo/metadata/2.root.json @@ -0,0 +1,79 @@ +{ + "signatures": [ + { + "keyid": "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "sig": "3066023100e99acc5f74777ebf40376b60f0216e8fe1829c1a49a5f6a6899126c15de1df7a56533baf493b2b53159c50843a289102023100b6a006b24da62ea0b743fbe38e1497ff485bf3a0833894985fc27a0305ad0693eeb968a7b52723ed3c49af8bef2027b6" + }, + { + "keyid": "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664", + "sig": "30440220136debcc2f60dd1d63c9c2704f9b13c2cb2f5d2df58ea93f07f7c10f54f36742022059d7f8c6620e33506c6f1766394a32f86c9b008328f6398831ba7ebcf4ce0838" + } + ], + "signed": { + "_type": "root", + "consistent_snapshot": true, + "expires": "2034-04-03T08:45:50Z", + "keys": { + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgDpP6O0sEt2R+l84WlfmqPBsFSby\nxJsJ6YmeUVgDk/wk9++8IAR6YBYewaKye56gMnIYjTFbyOI8WomA2NQFBw==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-online-uri": "awskms:arn:aws:kms:us-east-1:175142243308:key/fbd8dab6-5677-4b57-87e6-8369c45b3b61" + }, + "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWmhpAfB7Q53UNluMhpkDxXXup4E0\n2Hh4PSgHC1Yh6brGl6Akq9a4io55LtZTk5mnCTqxuB+rc5cI/yaNUeWEqQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@kipz" + }, + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp384", + "x-tuf-on-ci-keyowner": "@mrjoelkamp" + } + }, + "roles": { + "root": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664" + ], + "threshold": 1 + }, + "snapshot": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + }, + "targets": { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664" + ], + "threshold": 1 + }, + "timestamp": { + "keyids": [ + "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3" + ], + "threshold": 1, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } + }, + "spec_version": "1.0.31", + "version": 2, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo/metadata/3.test-role.json b/internal/test/testdata/test-repo/metadata/3.test-role.json new file mode 100644 index 0000000..db13fb0 --- /dev/null +++ b/internal/test/testdata/test-repo/metadata/3.test-role.json @@ -0,0 +1,34 @@ +{ + "signatures": [ + { + "keyid": "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "sig": "" + }, + { + "keyid": "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664", + "sig": "3044022015b6ebe9d30895e3be20e707a6738e38460197d90cae3dc37527ddb7c437868602207f85f3d4e068bef4c51a749f5d166cc7fe2cb9483999ea197e72395081c3aa61" + } + ], + "signed": { + "_type": "targets", + "expires": "2034-04-03T15:39:02Z", + "spec_version": "1.0.31", + "targets": { + "test-role/dir1/dir2/dir3/myfile.txt": { + "hashes": { + "sha256": "ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1" + }, + "length": 10 + }, + "test-role/test.txt": { + "hashes": { + "sha256": "d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2" + }, + "length": 32 + } + }, + "version": 3, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo/metadata/5.targets.json b/internal/test/testdata/test-repo/metadata/5.targets.json new file mode 100644 index 0000000..b177300 --- /dev/null +++ b/internal/test/testdata/test-repo/metadata/5.targets.json @@ -0,0 +1,65 @@ +{ + "signatures": [ + { + "keyid": "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "sig": "" + }, + { + "keyid": "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664", + "sig": "3046022100f892a496c9bd96082e3b06d5eae85429355876b8eb455aa04b53ab9051911d90022100a3e89c29b15bccfc2877278c0fb2d3b34500da6351e245ad0b3f8c0ae6b67eff" + } + ], + "signed": { + "_type": "targets", + "delegations": { + "keys": { + "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWmhpAfB7Q53UNluMhpkDxXXup4E0\n2Hh4PSgHC1Yh6brGl6Akq9a4io55LtZTk5mnCTqxuB+rc5cI/yaNUeWEqQ==\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp256", + "x-tuf-on-ci-keyowner": "@kipz" + }, + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09": { + "keytype": "ecdsa", + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3+asmp2GD6UijwWvMezwVG/BwFLuQa3o\nT6eRxFvkILGpVDbZ92ZYWidHl9LZ/eJUjhIjuVEkNVKoenw5KjKl8veP3MthZrQA\nSkYytOIwkidZo9Rk2dczbDcFSJvLGsmd\n-----END PUBLIC KEY-----\n" + }, + "scheme": "ecdsa-sha2-nistp384", + "x-tuf-on-ci-keyowner": "@mrjoelkamp" + } + }, + "roles": [ + { + "keyids": [ + "b7474a42f2588fa92ed4a2ebea6047a7b1b2f7351f1cfe0912732c0d0fb0fc09", + "81cf5a78d6ea2cd904256b9d814b340289b765e6f75ec4397e4ebb7586cab664" + ], + "name": "test-role", + "paths": [ + "test-role/*", + "test-role/*/*", + "test-role/*/*/*", + "test-role/*/*/*/*" + ], + "terminating": true, + "threshold": 1 + } + ] + }, + "expires": "2034-04-03T15:28:29Z", + "spec_version": "1.0.31", + "targets": { + "test.txt": { + "hashes": { + "sha256": "02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b" + }, + "length": 31 + } + }, + "version": 5, + "x-tuf-on-ci-expiry-period": 3650, + "x-tuf-on-ci-signing-period": 60 + } +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo/metadata/6.snapshot.json b/internal/test/testdata/test-repo/metadata/6.snapshot.json new file mode 100644 index 0000000..18421ed --- /dev/null +++ b/internal/test/testdata/test-repo/metadata/6.snapshot.json @@ -0,0 +1,22 @@ +{ + "signatures": [ + { + "keyid": "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3", + "sig": "3044022039b56cd2e3597df74e57d200a652ba020cdc9a8cd050bd65b5f8e2640d50691d02205e073e4b6fc260acc64327a331e4440601af5b1cbff594ea91cf7b70d5828fb1" + } + ], + "signed": { + "_type": "snapshot", + "expires": "2034-04-03T15:59:47Z", + "meta": { + "targets.json": { + "version": 5 + }, + "test-role.json": { + "version": 3 + } + }, + "spec_version": "1.0.31", + "version": 6 + } +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo/metadata/timestamp.json b/internal/test/testdata/test-repo/metadata/timestamp.json new file mode 100644 index 0000000..c8bbfc5 --- /dev/null +++ b/internal/test/testdata/test-repo/metadata/timestamp.json @@ -0,0 +1,19 @@ +{ + "signatures": [ + { + "keyid": "198f00ff96ea7cbfa7eac480cc9bfc43ce13bb434b901011ab777856533997d3", + "sig": "3045022011f2afa9b448fcbbac983c11fc3e264e95d5d7a9c9527b09d83a316ee762635f022100d05197a78ccc7a713ebdb0bccb44844f67a7c5208af8d346e201064b7ce11055" + } + ], + "signed": { + "_type": "timestamp", + "expires": "2034-04-03T15:59:47Z", + "meta": { + "snapshot.json": { + "version": 6 + } + }, + "spec_version": "1.0.31", + "version": 6 + } +} \ No newline at end of file diff --git a/internal/test/testdata/test-repo/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt b/internal/test/testdata/test-repo/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt new file mode 100644 index 0000000..c5c3e8d --- /dev/null +++ b/internal/test/testdata/test-repo/targets/02119a076ec3878c736c3a95e20794f5a8d5bce3d7ecc264681bb7334ca2e24b.test.txt @@ -0,0 +1 @@ +this is a top-level target file \ No newline at end of file diff --git a/internal/test/testdata/test-repo/targets/test-role/d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2.test.txt b/internal/test/testdata/test-repo/targets/test-role/d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2.test.txt new file mode 100644 index 0000000..df7d74c --- /dev/null +++ b/internal/test/testdata/test-repo/targets/test-role/d1bb6181284970ae43fbbc88b5e72f9a5942ebac20588aa0c4bf78ba621e1ee2.test.txt @@ -0,0 +1 @@ +this is a delegated targets file \ No newline at end of file diff --git a/internal/test/testdata/test-repo/targets/test-role/dir1/dir2/dir3/ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1.myfile.txt b/internal/test/testdata/test-repo/targets/test-role/dir1/dir2/dir3/ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1.myfile.txt new file mode 100644 index 0000000..327dce1 --- /dev/null +++ b/internal/test/testdata/test-repo/targets/test-role/dir1/dir2/dir3/ea230621c53e0bb858ea5526125414f8957fb29c08350528d50a162c620f36b1.myfile.txt @@ -0,0 +1 @@ +hello tuf diff --git a/internal/util/crypto.go b/internal/util/crypto.go new file mode 100644 index 0000000..7f03cdd --- /dev/null +++ b/internal/util/crypto.go @@ -0,0 +1,13 @@ +package util + +import ( + "crypto/sha256" + "encoding/hex" +) + +func HexHashBytes(input []byte) string { + s256 := sha256.New() + s256.Write(input) + hashSum := s256.Sum(nil) + return hex.EncodeToString(hashSum) +} diff --git a/pkg/mirror/metadata.go b/pkg/mirror/metadata.go new file mode 100644 index 0000000..aca4e8f --- /dev/null +++ b/pkg/mirror/metadata.go @@ -0,0 +1,196 @@ +package mirror + +import ( + "fmt" + "strconv" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +// ----------------- +// TUF root metadata +// ----------------- + +// GetMetadataManifest returns an image with TUF root metadata as layers +func (m *TufMirror) GetMetadataManifest(metadataURL string) (*v1.Image, error) { + metadata, err := m.getTufMetadataMirror(metadataURL) + if err != nil { + return nil, fmt.Errorf("failed to get metadata: %w", err) + } + manifest, err := m.buildMetadataManifest(metadata) + if err != nil { + return nil, fmt.Errorf("failed to build metadata manifest: %w", err) + } + return manifest, nil +} + +// getTufMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes +func (m *TufMirror) getTufMetadataMirror(metadataURL string) (*TufMetadata, error) { + trustedMetadata := m.TufClient.GetMetadata() + + rootMetadata := map[string][]byte{} + rootVersion := trustedMetadata.Root.Signed.Version + // get the previous versions of root metadata if any + if rootVersion != 1 { + var err error + rootMetadata, err = m.TufClient.GetPriorRoots(metadataURL) + if err != nil { + return nil, fmt.Errorf("failed to get prior root metadata: %w", err) + } + } + // get current root metadata + rootBytes, err := trustedMetadata.Root.ToBytes(false) + if err != nil { + return nil, fmt.Errorf("failed to get root metadata: %w", err) + } + rootMetadata[nameFromRole(metadata.ROOT, strconv.FormatInt(rootVersion, 10))] = rootBytes + + snapshotBytes, err := trustedMetadata.Snapshot.ToBytes(false) + if err != nil { + return nil, fmt.Errorf("failed to get snapshot metadata: %w", err) + } + targetsBytes, err := trustedMetadata.Targets[metadata.TARGETS].ToBytes(false) + if err != nil { + return nil, fmt.Errorf("failed to get targets metadata: %w", err) + } + timestampBytes, err := trustedMetadata.Timestamp.ToBytes(false) + if err != nil { + return nil, fmt.Errorf("failed to get timestamp metadata: %w", err) + } + + snapshotVersion := "" + targetsVersion := "" + if trustedMetadata.Root.Signed.ConsistentSnapshot { + snapshotVersion = strconv.FormatInt(trustedMetadata.Snapshot.Signed.Version, 10) + targetsVersion = strconv.FormatInt(trustedMetadata.Targets[metadata.TARGETS].Signed.Version, 10) + } + return &TufMetadata{ + Root: rootMetadata, + Snapshot: map[string][]byte{nameFromRole(metadata.SNAPSHOT, snapshotVersion): snapshotBytes}, + Targets: map[string][]byte{nameFromRole(metadata.TARGETS, targetsVersion): targetsBytes}, + Timestamp: timestampBytes, + }, nil +} + +// buildMetadataManifest returns an OCI image with TUF metadata as layers with annotations +func (m *TufMirror) buildMetadataManifest(metadata *TufMetadata) (*v1.Image, error) { + img := empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + for _, role := range TufRoles { + layers, err := m.makeRoleLayers(role, metadata) + if err != nil { + return nil, fmt.Errorf("failed to make role layer: %w", err) + } + img, err = mutate.Append(img, *layers...) + if err != nil { + return nil, fmt.Errorf("failed to append role layer to image: %w", err) + } + } + return &img, nil +} + +// makeRoleLayers returns a list of layers for a given TUF role +func (m *TufMirror) makeRoleLayers(role TufRole, tufMetadata *TufMetadata) (*[]mutate.Addendum, error) { + layers := new([]mutate.Addendum) + ann := map[string]string{tufFileAnnotation: ""} + switch role { + case metadata.ROOT: + layers = m.annotatedMetaLayers(tufMetadata.Root) + case metadata.SNAPSHOT: + layers = m.annotatedMetaLayers(tufMetadata.Snapshot) + case metadata.TARGETS: + layers = m.annotatedMetaLayers(tufMetadata.Targets) + case metadata.TIMESTAMP: + ann[tufFileAnnotation] = fmt.Sprintf("%s.json", role) + *layers = append(*layers, mutate.Addendum{Layer: static.NewLayer(tufMetadata.Timestamp, tufMetadataMediaType), Annotations: ann}) + default: + return nil, fmt.Errorf("unsupported TUF role: %s", role) + } + return layers, nil +} + +// annotatedMetaLayers returns a list of layers with annotations for each TUF metadata file +func (m *TufMirror) annotatedMetaLayers(meta map[string][]byte) *[]mutate.Addendum { + layers := new([]mutate.Addendum) + for name, data := range meta { + ann := map[string]string{tufFileAnnotation: name} + *layers = append(*layers, mutate.Addendum{Layer: static.NewLayer(data, tufMetadataMediaType), Annotations: ann}) + } + return layers +} + +// ------------------------------ +// TUF delegated targets metadata +// ------------------------------ + +// GetDelegatedMetadataMirrors returns a list of mirrors (image/tag pairs) for each delegated targets role metadata +func (m *TufMirror) GetDelegatedMetadataMirrors() ([]*MirrorImage, error) { + // get current delegated targets metadata + delegatedTargets, err := m.getDelegatedTargetsMetadata() + if err != nil { + return nil, fmt.Errorf("failed to get delegated targets metadata: %w", err) + } + mirror, err := m.buildDelegatedMetadataManifests(delegatedTargets) + if err != nil { + return nil, fmt.Errorf("failed to build delegated targets manifests: %w", err) + } + return mirror, nil +} + +// getDelegatedTargetsMetadata returns delegated targets metadata as a list of DelegatedTargetMetadata (role name and data) +func (m *TufMirror) getDelegatedTargetsMetadata() (*[]DelegatedTargetMetadata, error) { + delegatedTargets := new([]DelegatedTargetMetadata) + md := m.TufClient.GetMetadata() + for _, role := range md.Targets[metadata.TARGETS].Signed.Delegations.Roles { + roleMetadata, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS) + if err != nil { + return nil, fmt.Errorf("failed to get delegated role metadata: %w", err) + } + roleBytes, err := roleMetadata.ToBytes(false) + if err != nil { + return nil, fmt.Errorf("failed to get role %s metadata: %w", role.Name, err) + } + meta, ok := md.Snapshot.Signed.Meta[nameFromRole(role.Name, "")] + if !ok { + return nil, fmt.Errorf("failed to get role %s metadata: %w", role.Name, err) + } + // extract target metadata version in case of consistent snapshot naming + version := "" + if md.Root.Signed.ConsistentSnapshot { + version = strconv.FormatInt(meta.Version, 10) + } + *delegatedTargets = append(*delegatedTargets, DelegatedTargetMetadata{Name: role.Name, Version: version, Data: roleBytes}) + } + return delegatedTargets, nil +} + +// buildDelegatedMetadataManifests returns a list of mirrors (image/tag pairs) for each delegated target role metadata +func (m *TufMirror) buildDelegatedMetadataManifests(delegated *[]DelegatedTargetMetadata) ([]*MirrorImage, error) { + manifests := []*MirrorImage{} + for _, role := range *delegated { + img := empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + ann := map[string]string{tufFileAnnotation: nameFromRole(role.Name, role.Version)} + layer := mutate.Addendum{Layer: static.NewLayer(role.Data, tufMetadataMediaType), Annotations: ann} + img, err := mutate.Append(img, layer) + if err != nil { + return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err) + } + manifests = append(manifests, &MirrorImage{Image: &img, Tag: role.Name}) + } + return manifests, nil +} + +func nameFromRole(role, version string) string { + if version != "" { + return fmt.Sprintf("%s.%s.json", version, role) + } + return fmt.Sprintf("%s.json", role) +} diff --git a/pkg/mirror/metadata_test.go b/pkg/mirror/metadata_test.go new file mode 100644 index 0000000..2083a67 --- /dev/null +++ b/pkg/mirror/metadata_test.go @@ -0,0 +1,89 @@ +package mirror + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/docker/attest/internal/embed" + "github.com/docker/attest/internal/test" + "github.com/stretchr/testify/assert" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +func TestGetTufMetadataMirror(t *testing.T) { + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "internal", "test", "testdata", "test-repo")))) + defer server.Close() + + path := test.CreateTempDir(t, "", "tuf_temp") + m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets") + assert.Nil(t, err) + + tufMetadata, err := m.getTufMetadataMirror(server.URL + "/metadata") + assert.Nil(t, err) + + // check that all roles are not empty + assert.Greater(t, len(tufMetadata.Root), 0) + assert.Greater(t, len(tufMetadata.Snapshot), 0) + assert.Greater(t, len(tufMetadata.Targets), 0) + assert.Greater(t, len(tufMetadata.Timestamp), 0) +} + +func TestGetMetadataManifest(t *testing.T) { + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "internal", "test", "testdata", "test-repo")))) + defer server.Close() + + path := test.CreateTempDir(t, "", "tuf_temp") + m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets") + assert.Nil(t, err) + + img, err := m.GetMetadataManifest(server.URL + "/metadata") + assert.Nil(t, err) + assert.NotNil(t, img) + + image := *img + mf, err := image.RawManifest() + assert.Nil(t, err) + + type Annotations struct { + Annotations map[string]string `json:"annotations"` + } + type Layers struct { + Layers []Annotations `json:"layers"` + } + l := &Layers{} + err = json.Unmarshal(mf, l) + assert.Nil(t, err) + + // check that layers are annotated and use consistent snapshot naming + for _, layer := range l.Layers { + ann, ok := layer.Annotations[tufFileAnnotation] + assert.True(t, ok) + // check for consistent snapshot version + parts := strings.Split(ann, ".") + if parts[0] == metadata.TIMESTAMP { + continue + } + _, err := strconv.Atoi(parts[0]) + assert.Nil(t, err) + } +} + +func TestGetDelegatedMetadataMirrors(t *testing.T) { + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "internal", "test", "testdata", "test-repo")))) + defer server.Close() + + path := test.CreateTempDir(t, "", "tuf_temp") + m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets") + assert.Nil(t, err) + + delegations, err := m.GetDelegatedMetadataMirrors() + assert.Nil(t, err) + + assert.NotNil(t, delegations) + assert.Greater(t, len(delegations), 0) +} diff --git a/pkg/mirror/mirror.go b/pkg/mirror/mirror.go new file mode 100644 index 0000000..04508eb --- /dev/null +++ b/pkg/mirror/mirror.go @@ -0,0 +1,82 @@ +package mirror + +import ( + "fmt" + "log" + "os" + + "github.com/docker/attest/internal/embed" + "github.com/docker/attest/pkg/tuf" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func NewTufMirror(root []byte, tufPath, metadataURL, targetsURL string) (*TufMirror, error) { + if root == nil { + root = embed.DefaultRoot + } + tufClient, err := tuf.NewTufClient(root, tufPath, metadataURL, targetsURL) + if err != nil { + return nil, fmt.Errorf("failed to create TUF client: %w", err) + } + return &TufMirror{TufClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil +} + +func PushToRegistry(image any, imageName string) error { + // Parse the image name + ref, err := name.ParseReference(imageName) + if err != nil { + log.Fatalf("Failed to parse image name: %v", err) + } + // Get the authenticator from the default Docker keychain + auth, err := authn.DefaultKeychain.Resolve(ref.Context()) + if err != nil { + log.Fatalf("Failed to get authenticator: %v", err) + } + // Push the image to the registry + switch image := image.(type) { + case *v1.Image: + if err := remote.Write(ref, *image, remote.WithAuth(auth)); err != nil { + return fmt.Errorf("failed to push image %s: %w", imageName, err) + } + case *v1.ImageIndex: + if err := remote.WriteIndex(ref, *image, remote.WithAuth(auth)); err != nil { + return fmt.Errorf("failed to push image index %s: %w", imageName, err) + } + default: + return fmt.Errorf("unknown image type: %T", image) + } + return nil +} + +func SaveAsOCILayout(image any, path string) error { + // Save the image to the local filesystem + err := os.MkdirAll(path, os.FileMode(0744)) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + switch image := image.(type) { + case *v1.Image: + index := empty.Index + l, err := layout.Write(path, index) + if err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + err = l.AppendImage(*image) + if err != nil { + return fmt.Errorf("failed to append image to index: %w", err) + } + case *v1.ImageIndex: + _, err := layout.Write(path, *image) + if err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + default: + return fmt.Errorf("unknown image type: %T", image) + } + return nil +} diff --git a/pkg/mirror/targets.go b/pkg/mirror/targets.go new file mode 100644 index 0000000..b720e84 --- /dev/null +++ b/pkg/mirror/targets.go @@ -0,0 +1,109 @@ +package mirror + +import ( + "fmt" + "path/filepath" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +// GetTufTargetMirrors returns a list of top-level target files as MirrorImages (image with tag) +func (m *TufMirror) GetTufTargetMirrors() ([]*MirrorImage, error) { + targetMirrors := []*MirrorImage{} + md := m.TufClient.GetMetadata() + + // for each top-level target file, create an image with the target file as a layer + targets := md.Targets[metadata.TARGETS].Signed.Targets + for _, t := range targets { + // download target file + _, data, err := m.TufClient.DownloadTarget(t.Path, filepath.Join(m.tufPath, "download")) + if err != nil { + return nil, fmt.Errorf("failed to download target %s: %w", t.Path, err) + } + // create image with target file as layer + img := empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + // annotate layer + hash, ok := t.Hashes["sha256"] + if !ok { + return nil, fmt.Errorf("missing sha256 hash for target %s", t.Path) + } + name := strings.Join([]string{hash.String(), t.Path}, ".") + ann := map[string]string{tufFileAnnotation: name} + layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann} + img, err = mutate.Append(img, layer) + if err != nil { + return nil, fmt.Errorf("failed to append role layer to image: %w", err) + } + targetMirrors = append(targetMirrors, &MirrorImage{Image: &img, Tag: name}) + } + return targetMirrors, nil +} + +// GetDelegatedTargetMirrors returns a list of delegated target files as MirrorIndexes (image index with tag) +// each image in the index contains a delegated target file +func (m *TufMirror) GetDelegatedTargetMirrors() ([]*MirrorIndex, error) { + mirror := []*MirrorIndex{} + md := m.TufClient.GetMetadata() + + // for each delegated role, create an image index with target files as images + roles := md.Targets[metadata.TARGETS].Signed.Delegations.Roles + for _, role := range roles { + // create an image index + index := v1.ImageIndex(empty.Index) + + // get delegated targets metadata for role + roleMeta, err := m.TufClient.LoadDelegatedTargets(role.Name, metadata.TARGETS) + if err != nil { + return nil, fmt.Errorf("failed to load delegated targets metadata: %w", err) + } + + // for each target file, create an image with the target file as a layer + for _, target := range roleMeta.Signed.Targets { + // download target file + _, data, err := m.TufClient.DownloadTarget(target.Path, filepath.Join(m.tufPath, "download")) + if err != nil { + return nil, fmt.Errorf("failed to download target %s: %w", target.Path, err) + } + // create image with target file as layer + img := empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + // annotate layer + hash, ok := target.Hashes["sha256"] + if !ok { + return nil, fmt.Errorf("missing sha256 hash for target %s", target.Path) + } + filename := filepath.Base(target.Path) + subdir, ok := strings.CutSuffix(target.Path, "/"+filename) + if !ok { + return nil, fmt.Errorf("failed to find target subdirectory [%s] in path: %s", subdir, target.Path) + } + name := strings.Join([]string{hash.String(), filename}, ".") + ann := map[string]string{tufFileAnnotation: name} + layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann} + img, err = mutate.Append(img, layer) + if err != nil { + return nil, fmt.Errorf("failed to append role layer to image: %w", err) + } + // append image to index with annotation + index = mutate.AppendManifests(index, mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{ + Annotations: map[string]string{ + tufFileAnnotation: fmt.Sprintf("%s/%s", subdir, name), + }, + }, + }) + } + mirror = append(mirror, &MirrorIndex{Index: &index, Tag: role.Name}) + } + return mirror, nil +} diff --git a/pkg/mirror/targets_test.go b/pkg/mirror/targets_test.go new file mode 100644 index 0000000..5ab41df --- /dev/null +++ b/pkg/mirror/targets_test.go @@ -0,0 +1,103 @@ +package mirror + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/docker/attest/internal/embed" + "github.com/docker/attest/internal/test" + "github.com/stretchr/testify/assert" +) + +type Layer struct { + Annotations map[string]string `json:"annotations"` + Digest string `json:"digest"` +} +type Layers struct { + Layers []Layer `json:"layers"` +} + +func TestGetTufTargetsMirror(t *testing.T) { + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "internal", "test", "testdata", "test-repo")))) + defer server.Close() + + path := test.CreateTempDir(t, "", "tuf_temp") + m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets") + assert.Nil(t, err) + + targets, err := m.GetTufTargetMirrors() + assert.Nil(t, err) + assert.Greater(t, len(targets), 0) + + // check for image layer annotations + for _, target := range targets { + img := *target.Image + mf, err := img.RawManifest() + assert.Nil(t, err) + + // unmarshal manifest with annotations + l := &Layers{} + err = json.Unmarshal(mf, l) + assert.Nil(t, err) + + // check that layers are annotated + for _, layer := range l.Layers { + ann, ok := layer.Annotations[tufFileAnnotation] + assert.True(t, ok) + parts := strings.Split(ann, ".") + // .filename.json + assert.Equal(t, len(parts), 3) + } + } +} + +func TestTargetDelegationMetadata(t *testing.T) { + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "internal", "test", "testdata", "test-repo")))) + defer server.Close() + + path := test.CreateTempDir(t, "", "tuf_temp") + tm, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets") + assert.Nil(t, err) + + targets, err := tm.TufClient.LoadDelegatedTargets("test-role", "targets") + assert.Nil(t, err) + assert.Greater(t, len(targets.Signed.Targets), 0) +} + +func TestGetDelegatedTargetMirrors(t *testing.T) { + server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "internal", "test", "testdata", "test-repo")))) + defer server.Close() + + path := test.CreateTempDir(t, "", "tuf_temp") + m, err := NewTufMirror(embed.DevRoot, path, server.URL+"/metadata", server.URL+"/targets") + assert.Nil(t, err) + + mirrors, err := m.GetDelegatedTargetMirrors() + assert.Nil(t, err) + assert.Greater(t, len(mirrors), 0) + + // check for index image annotations + for _, mirror := range mirrors { + idx := *mirror.Index + mf, err := idx.RawManifest() + assert.Nil(t, err) + + // unmarshal manifest with annotations + l := &Layers{} + err = json.Unmarshal(mf, l) + assert.Nil(t, err) + + // check that layers are annotated + for _, layer := range l.Layers { + ann, ok := layer.Annotations[tufFileAnnotation] + assert.True(t, ok) + parts := strings.Split(ann, ".") + // /.filename.json + assert.Equal(t, len(parts), 3) + } + } +} diff --git a/pkg/mirror/types.go b/pkg/mirror/types.go new file mode 100644 index 0000000..c93cc5d --- /dev/null +++ b/pkg/mirror/types.go @@ -0,0 +1,49 @@ +package mirror + +import ( + "github.com/docker/attest/pkg/tuf" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +const ( + DefaultMetadataURL = "https://docker.github.io/tuf-staging/metadata" + DefaultTargetsURL = "https://docker.github.io/tuf-staging/targets" + tufMetadataMediaType = "application/vnd.tuf.metadata+json" + tufTargetMediaType = "application/vnd.tuf.target" + tufFileAnnotation = "tuf.io/filename" +) + +type TufRole string + +var TufRoles = []TufRole{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP} + +type TufMetadata struct { + Root map[string][]byte + Snapshot map[string][]byte + Targets map[string][]byte + Timestamp []byte +} + +type DelegatedTargetMetadata struct { + Name string + Version string + Data []byte +} + +type MirrorImage struct { + Image *v1.Image + Tag string +} + +type MirrorIndex struct { + Index *v1.ImageIndex + Tag string +} + +type TufMirror struct { + TufClient *tuf.TufClient + tufPath string + metadataURL string + targetsURL string +} diff --git a/pkg/tuf/mock.go b/pkg/tuf/mock.go new file mode 100644 index 0000000..aa9c433 --- /dev/null +++ b/pkg/tuf/mock.go @@ -0,0 +1,60 @@ +package tuf + +import ( + "io" + "os" + "path/filepath" +) + +type mockTufClient struct { + srcPath string + dstPath string +} + +func NewMockTufClient(srcPath string, dstPath string) *mockTufClient { + if srcPath == "" { + panic("srcPath must be set") + } + if dstPath == "" { + panic("dstPath must be set") + } + return &mockTufClient{ + srcPath: srcPath, + dstPath: dstPath, + } +} + +func (dc *mockTufClient) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) { + src, err := os.Open(filepath.Join(dc.srcPath, target)) + if err != nil { + return "", nil, err + } + defer src.Close() + + var dstFilePath string + if filePath == "" { + dstFilePath = filepath.Join(dc.dstPath, filepath.FromSlash(target)) + } else { + dstFilePath = filePath + } + + err = os.MkdirAll(filepath.Dir(dstFilePath), 0755) + if err != nil { + return "", nil, err + } + dst, err := os.Create(dstFilePath) + if err != nil { + return "", nil, err + } + defer dst.Close() + + // reading from tee will read from src and write to dst at the same time + tee := io.TeeReader(src, dst) + + b, err := io.ReadAll(tee) + if err != nil { + return "", nil, err + } + + return dstFilePath, b, nil +} diff --git a/pkg/tuf/registry.go b/pkg/tuf/registry.go new file mode 100644 index 0000000..153618b --- /dev/null +++ b/pkg/tuf/registry.go @@ -0,0 +1,306 @@ +package tuf + +import ( + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "path" + "strings" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/theupdateframework/go-tuf/v2/metadata" +) + +const ( + TufFileNameAnnotation = "tuf.io/filename" +) + +type TufRole string + +var TufRoles = []TufRole{metadata.ROOT, metadata.SNAPSHOT, metadata.TARGETS, metadata.TIMESTAMP} + +// RegistryFetcher implements Fetcher +type RegistryFetcher struct { + httpUserAgent string + metadataRepo string + metadataTag string + targetsRepo string + cache *ImageCache + timeout time.Duration +} + +type ImageCache struct { + cache map[string][]byte +} + +func NewImageCache() *ImageCache { + return &ImageCache{ + cache: make(map[string][]byte), + } +} + +// Get image from cache +func (c *ImageCache) Get(imgRef string) ([]byte, bool) { + img, found := c.cache[imgRef] + return img, found +} + +// Add image to cache +func (c *ImageCache) Put(imgRef string, img []byte) { + c.cache[imgRef] = img +} + +type Layer struct { + Annotations map[string]string `json:"annotations"` + Digest string `json:"digest"` +} +type Layers struct { + Layers []Layer `json:"layers"` + Manifests []Layer `json:"manifests"` + MediaType string `json:"mediaType"` +} + +func NewRegistryFetcher(metadataRepo, metadataTag, targetsRepo string) *RegistryFetcher { + return &RegistryFetcher{ + metadataRepo: metadataRepo, + metadataTag: metadataTag, + targetsRepo: targetsRepo, + cache: NewImageCache(), + } +} + +// DownloadFile downloads a file from an OCI registry, errors out if it failed, +// its length is larger than maxLength or the timeout is reached. +func (d *RegistryFetcher) DownloadFile(urlPath string, maxLength int64, timeout time.Duration) ([]byte, error) { + d.timeout = timeout + + imgRef, fileName, err := d.parseImgRef(urlPath) + if err != nil { + return nil, err + } + + // Get manifest for image or index + mf, err := d.getManifest(imgRef) + if err != nil { + return nil, err + } + + // Search image/index manifest for file + hash, err := d.findFileInManifest(mf, fileName) + if err != nil { + // TODO - refactor Fetcher interface for not found error handling? (requires go-tuf change) + return nil, &metadata.ErrDownloadHTTP{StatusCode: http.StatusNotFound} + } + + // Get file from layer + parts := strings.Split(imgRef, ":") + switch len(parts) { + // default host port + case 2: + return d.pullFileLayer(fmt.Sprintf("%s@%s", parts[0], *hash), maxLength) + // custom host port + case 3: + return d.pullFileLayer(fmt.Sprintf("%s:%s@%s", parts[0], parts[1], *hash), maxLength) + default: + return nil, fmt.Errorf("invalid image reference: %s", imgRef) + } +} + +// getManifest returns the manifest for an image or index +func (d *RegistryFetcher) getManifest(ref string) ([]byte, error) { + // Pull image manifest + var err error + var found bool + var mf []byte + // Check cache for manifest and only pull if not found + if mf, found = d.cache.Get(ref); !found { + mf, err = crane.Manifest(ref, + crane.WithUserAgent(d.httpUserAgent), + crane.WithTransport(transportWithTimeout(d.timeout)), + crane.WithAuth(authn.Anonymous), + crane.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return nil, err + } + // Cache the manifest + d.cache.Put(ref, mf) + } + return mf, nil +} + +// pullFileLayer pulls a layer for an image or index and returns its data +func (d *RegistryFetcher) pullFileLayer(ref string, maxLength int64) ([]byte, error) { + var data []byte + var found bool + // Check cache for layer and only pull if not found + if data, found = d.cache.Get(ref); !found { + layer, err := crane.PullLayer(ref, + crane.WithUserAgent(d.httpUserAgent), + crane.WithTransport(transportWithTimeout(d.timeout)), + crane.WithAuth(authn.Anonymous), + crane.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return nil, err + } + data, err = getDataFromLayer(layer, maxLength) + if err != nil { + return nil, err + } + // Cache the layer + d.cache.Put(ref, data) + } + return data, nil +} + +// getDataFromLayer returns the data from a layer in an image +func getDataFromLayer(fileLayer v1.Layer, maxLength int64) ([]byte, error) { + length, err := fileLayer.Size() + if err != nil { + return nil, err + } + // Error if the reported size is greater than what is expected. + if length > maxLength { + return nil, &metadata.ErrDownloadLengthMismatch{Msg: fmt.Sprintf("download failed, length %d is larger than expected %d", length, maxLength)} + } + content, err := fileLayer.Uncompressed() + if err != nil { + return nil, err + } + data, err := io.ReadAll(io.LimitReader(content, maxLength+1)) + if err != nil { + return nil, err + } + // Error if the reported size is greater than what is expected. + length = int64(len(data)) + if length > maxLength { + return nil, &metadata.ErrDownloadLengthMismatch{Msg: fmt.Sprintf("download failed, length %d is larger than expected %d", length, maxLength)} + } + return data, nil +} + +// parseImgRef maintains the Fetcher interface by parsing a URL path to an image reference and file name +func (d *RegistryFetcher) parseImgRef(urlPath string) (imgRef, fileName string, err error) { + // Check if repo is target or metadata + if strings.Contains(urlPath, d.targetsRepo) { + // determine if the target path contains subdirectories and set image name accordingly + // / -> image = :, layer = + // // -> index = : , image = -> layer = + target := strings.TrimPrefix(urlPath, d.targetsRepo+"/") + subdir, name, found := strings.Cut(target, "/") + if found { + return fmt.Sprintf("%s:%s", d.targetsRepo, subdir), fmt.Sprintf("%s/%s", subdir, name), nil + } + return fmt.Sprintf("%s:%s", d.targetsRepo, target), target, nil + } else if strings.Contains(urlPath, d.metadataRepo) { + // build the metadata image name + // determine if role is a delegated role and set the tag accordingly + fileName = path.Base(urlPath) + role := roleFromConsistentName(fileName) + // if the role is a delegated role use the role name as the tag for imgRef + if isDelegatedRole(role) { + return fmt.Sprintf("%s:%s", d.metadataRepo, role), fileName, nil + } + return fmt.Sprintf("%s:%s", d.metadataRepo, d.metadataTag), fileName, nil + } else { + return "", "", fmt.Errorf("urlPath: %s must be in metadata or targets repo", urlPath) + } +} + +// findFileInManifest searches the image or index manifest for a file with the given name and returns its digest +func (d *RegistryFetcher) findFileInManifest(mf []byte, name string) (*v1.Hash, error) { + var index bool + + // unmarshal manifest with annotations + l := &Layers{} + err := json.Unmarshal(mf, l) + if err != nil { + return nil, err + } + + // determine image or index manifest + var layers []Layer + if l.MediaType == string(types.OCIImageIndex) { + layers = l.Manifests + index = true + } else if l.MediaType == string(types.OCIManifestSchema1) { + layers = l.Layers + index = false + } else { + return nil, fmt.Errorf("invalid manifest media type: %s", l.MediaType) + } + + // find annotation with file name + var digest string + for _, layer := range layers { + if layer.Annotations[TufFileNameAnnotation] == name { + digest = layer.Digest + break + } + } + if digest == "" { + return nil, fmt.Errorf("file %s not found in image", name) + } + + // return layer digest as v1.Hash + hash := new(v1.Hash) + *hash, err = v1.NewHash(digest) + if err != nil { + return nil, err + } + + // if index manifest pull image to get file layer + if index { + mf, err := d.getManifest(fmt.Sprintf("%s@%s", d.targetsRepo, *hash)) + if err != nil { + return nil, err + } + parts := strings.Split(name, "/") + return d.findFileInManifest(mf, parts[len(parts)-1]) + } + return hash, nil +} + +// transportWithTimeout returns a http.RoundTripper with a specified timeout +func transportWithTimeout(timeout time.Duration) http.RoundTripper { + // transport is based on go-containerregistry remote.DefaultTransport + // with modifications to include a specified timeout + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: timeout, + KeepAlive: timeout, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConnsPerHost: 50, + } +} + +// isDelegatedRole returns true if the role is a delegated role +func isDelegatedRole(role string) bool { + for _, r := range TufRoles { + if role == string(r) { + return false // role is not a delegated role + } + } + return true // role is a delegated role +} + +// roleFromConsistentName returns the role name from a consistent snapshot file name +func roleFromConsistentName(filename string) string { + name := strings.TrimSuffix(filename, ".json") + role := strings.Split(name, ".") + if len(role) > 1 { + return role[1] + } + return role[0] +} diff --git a/pkg/tuf/registry_test.go b/pkg/tuf/registry_test.go new file mode 100644 index 0000000..662ba72 --- /dev/null +++ b/pkg/tuf/registry_test.go @@ -0,0 +1,446 @@ +package tuf + +import ( + "context" + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/docker/attest/internal/embed" + "github.com/docker/attest/internal/test" + "github.com/docker/attest/internal/util" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/registry" + "github.com/theupdateframework/go-tuf/v2/metadata" + "github.com/theupdateframework/go-tuf/v2/metadata/config" + "github.com/theupdateframework/go-tuf/v2/metadata/updater" +) + +const ( + tufTargetMediaType = "application/vnd.tuf.target" +) + +func TestRegistryFetcher(t *testing.T) { + // run local registry + registry, regAddr := RunTestRegistry(t) + defer func() { + if err := registry.Terminate(context.Background()); err != nil { + t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + } + }() + LoadRegistryTestData(t, regAddr, OciTufTestDataPath) + + metadataRepo := regAddr.Host + "/tuf-metadata" + metadataImgTag := "latest" + targetsRepo := regAddr.Host + "/tuf-targets" + targetFile := "test.txt" + delegatedRole := "test-role" + dir := test.CreateTempDir(t, "", "tuf_temp") + delegatedDir := test.CreateTempDir(t, dir, delegatedRole) + delegatedTargetFile := fmt.Sprintf("%s/%s", delegatedRole, targetFile) + + cfg, err := config.New(metadataRepo, embed.DevRoot) + assert.NoError(t, err) + + cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataImgTag, targetsRepo) + cfg.LocalMetadataDir = dir + cfg.LocalTargetsDir = dir + cfg.RemoteTargetsURL = targetsRepo + + // create a new Updater instance + up, err := updater.New(cfg) + assert.NoError(t, err) + + // refresh the metadata + err = up.Refresh() + assert.NoError(t, err) + + // download top-level target + targetInfo, err := up.GetTargetInfo(targetFile) + assert.NoError(t, err) + _, _, err = up.DownloadTarget(targetInfo, filepath.Join(dir, targetInfo.Path), "") + assert.NoError(t, err) + + // download delegated target + targetInfo, err = up.GetTargetInfo(delegatedTargetFile) + assert.NoError(t, err) + _, _, err = up.DownloadTarget(targetInfo, filepath.Join(delegatedDir, targetFile), "") + assert.NoError(t, err) +} + +func TestRoleFromConsistentName(t *testing.T) { + testCases := []struct { + name string + expected string + }{ + {"root.json", metadata.ROOT}, + {"1.root.json", metadata.ROOT}, + {"targets.json", metadata.TARGETS}, + {"63.targets.json", metadata.TARGETS}, + {"timestamp", metadata.TIMESTAMP}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, roleFromConsistentName(tc.name)) + }) + } +} + +func TestIsDelegatedRole(t *testing.T) { + testCases := []struct { + name string + expected bool + }{ + {metadata.ROOT, false}, + {metadata.TARGETS, false}, + {metadata.TIMESTAMP, false}, + {"doi", true}, + {"test", true}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, isDelegatedRole(tc.name)) + }) + } +} + +func TestFindFileInManifest(t *testing.T) { + // make test image manifest + file := "test.json" + data := []byte("test") + hash := v1.Hash{Algorithm: "sha256", Hex: util.HexHashBytes(data)} + img := empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + // add test layer + name := strings.Join([]string{hash.Hex, file}, ".") + ann := map[string]string{TufFileNameAnnotation: name} + layer := mutate.Addendum{Layer: static.NewLayer(data, tufTargetMediaType), Annotations: ann} + img, err := mutate.Append(img, layer) + assert.NoError(t, err) + image_manifest, err := img.RawManifest() + assert.NoError(t, err) + + // make test index manifest + idx := v1.ImageIndex(empty.Index) + assert.NoError(t, err) + // append image to index with annotation + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{ + Annotations: map[string]string{ + TufFileNameAnnotation: name, + }, + }, + }) + index_manifest, err := idx.RawManifest() + assert.NoError(t, err) + // cache image layer + targetsRepo := "test/tuf-targets" + d := &RegistryFetcher{ + cache: NewImageCache(), + targetsRepo: targetsRepo, + } + imgHash, err := img.Digest() + assert.NoError(t, err) + d.cache.Put(fmt.Sprintf("%s@%s", targetsRepo, imgHash.String()), image_manifest) + + testCases := []struct { + name string + manifest []byte + file string + expected string + }{ + {"consistent filename image", image_manifest, fmt.Sprintf("%s.%s", hash.Hex, file), hash.Hex}, + {"filename image", image_manifest, file, ""}, + {"consistent filename index", index_manifest, fmt.Sprintf("%s.%s", hash.Hex, file), hash.Hex}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + digest, err := d.findFileInManifest(tc.manifest, tc.file) + if tc.expected == "" { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expected, digest.Hex) + }) + } +} + +func TestParseImgRef(t *testing.T) { + metadataRepo := "test/tuf-metadata" + metadataTag := "latest" + targetsRepo := "test/tuf-targets" + delegatedRole := "test-role" + testCases := []struct { + name string + ref string + expectedRef string + expectedFile string + }{ + {"top-level metadata", fmt.Sprintf("%s/2.root.json", metadataRepo), fmt.Sprintf("%s:%s", metadataRepo, metadataTag), "2.root.json"}, + {"delegated metadata", fmt.Sprintf("%s/%s/5.test-role.json", metadataRepo, delegatedRole), fmt.Sprintf("%s:%s", metadataRepo, delegatedRole), "5.test-role.json"}, + {"top-level target", fmt.Sprintf("%s/policy.yaml", targetsRepo), fmt.Sprintf("%s:policy.yaml", targetsRepo), "policy.yaml"}, + {"delegated target", fmt.Sprintf("%s/%s/policy.yaml", targetsRepo, delegatedRole), fmt.Sprintf("%s:%s", targetsRepo, delegatedRole), fmt.Sprintf("%s/policy.yaml", delegatedRole)}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d := &RegistryFetcher{ + metadataRepo: metadataRepo, + metadataTag: "latest", + targetsRepo: targetsRepo, + } + imgRef, file, err := d.parseImgRef(tc.ref) + assert.NoError(t, err) + assert.Equal(t, tc.expectedRef, imgRef) + assert.Equal(t, tc.expectedFile, file) + }) + } +} + +func TestGetDataFromLayer(t *testing.T) { + data := []byte("test") + layer := static.NewLayer(data, tufTargetMediaType) + testCases := []struct { + name string + layer v1.Layer + max int64 + expected []byte + }{ + {"valid length", layer, int64(len(data)), data}, + {"invalid length", layer, int64(len(data) - 1), nil}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + data, err := getDataFromLayer(tc.layer, tc.max) + if tc.expected == nil { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expected, data) + }) + } +} + +func TestPullFileLayer(t *testing.T) { + // run local registry + registry, url := RunTestRegistry(t) + defer func() { + if err := registry.Terminate(context.Background()); err != nil { + t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + } + }() + + // make test layer + repo := "tuf-metadata" + data := []byte("test") + testLayer := static.NewLayer(data, tufTargetMediaType) + hash, err := testLayer.Digest() + assert.NoError(t, err) + layerRef := fmt.Sprintf("%s/%s@%s", url.Host, repo, hash.String()) + + // cache test layer + d := &RegistryFetcher{ + cache: NewImageCache(), + } + d.cache.Put(layerRef, data) + + // push uncached image layer to local registry + uncachedData := []byte("uncached") + uncachedTestLayer := static.NewLayer(uncachedData, tufTargetMediaType) + uncachedHash, err := uncachedTestLayer.Digest() + assert.NoError(t, err) + uncachedLayerRef := fmt.Sprintf("%s/%s@%s", url.Host, repo, uncachedHash.String()) + img := empty.Image + img, err = mutate.Append(img, mutate.Addendum{Layer: uncachedTestLayer}) + assert.NoError(t, err) + err = crane.Push(img, fmt.Sprintf("%s/%s", url.Host, fmt.Sprintf("%s:latest", repo))) + assert.NoError(t, err) + + testCases := []struct { + name string + ref string + maxLength int + expected []byte + }{ + {"cached layer", layerRef, len(data), data}, + {"uncached layer", uncachedLayerRef, len(uncachedData), uncachedData}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + layer, err := d.pullFileLayer(tc.ref, int64(tc.maxLength)) + if tc.expected == nil { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Greater(t, len(layer), 0) + }) + } +} + +func TestGetManifest(t *testing.T) { + // run local registry + registry, url := RunTestRegistry(t) + defer func() { + if err := registry.Terminate(context.Background()); err != nil { + t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + } + }() + + // make test manifest + repo := "tuf-metadata" + img := empty.Image + img = mutate.MediaType(img, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, types.OCIConfigJSON) + imgRef := fmt.Sprintf("%s/%s:latest", url.Host, repo) + + // cache test manifest + d := &RegistryFetcher{ + cache: NewImageCache(), + } + mf, err := img.RawManifest() + assert.NoError(t, err) + d.cache.Put(imgRef, mf) + + // push test image to local registry + unchachedImgRef := fmt.Sprintf("%s/%s:unchached", url.Host, repo) + err = crane.Push(img, unchachedImgRef) + assert.NoError(t, err) + + testCases := []struct { + name string + ref string + expected []byte + }{ + {"cached image manifest", imgRef, mf}, + {"uncached image manifest", unchachedImgRef, mf}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + manifest, err := d.getManifest(tc.ref) + assert.NoError(t, err) + assert.Equal(t, tc.expected, manifest) + }) + } +} + +// RunTestRegistry starts a registry testcontainer for TUF on OCI testdata +func RunTestRegistry(t *testing.T) (*registry.RegistryContainer, *url.URL) { + registryContainer, err := registry.RunContainer(context.Background(), testcontainers.WithImage("registry:2.8.3")) + if err != nil { + t.Fatalf("failed to start container: %s", err) + } + httpAddress, err := registryContainer.Address(context.Background()) + if err != nil { + t.Fatalf("failed to get container address: %s", err) + } + addr, err := url.Parse(httpAddress) + if err != nil { + t.Fatalf("failed to parse container address: %s", err) + } + if addr.Hostname() == "127.0.0.1" { + addr.Host = "localhost:" + addr.Port() + } + return registryContainer, addr +} + +// LoadRegistryTestData pushes TUF metadata and targets to an OCI registry +func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) { + // push tuf metadata and targets to local registry + METADATA_REPO := "tuf-metadata" + METADATA_TAG := "latest" + TARGETS_REPO := "tuf-targets" + DELEGATED_ROLE := "test-role" + + // push top-level metadata -> metadata:latest + err := LoadMetadata(filepath.Join(path, "metadata"), registry.Host, METADATA_REPO, METADATA_TAG) + if err != nil { + t.Fatal(err) + } + + // push delegated metadata -> metadata: + err = LoadMetadata(filepath.Join(path, "metadata", DELEGATED_ROLE), registry.Host, METADATA_REPO, DELEGATED_ROLE) + if err != nil { + t.Fatal(err) + } + + // push targets -> targets:..ext (image) or targets: 1 { + // delegated target + err = remote.WriteIndex(ref, tIdx, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + t.Fatal(err) + } + } else { + t.Fatal("no manifests found") + } + } +} + +// LoadMetadata loads TUF metadata from a local path and pushes to a registry +func LoadMetadata(path, host, repo, tag string) error { + mIdx, err := layout.ImageIndexFromPath(path) + if err != nil { + return err + } + ref, err := name.ParseReference(fmt.Sprintf("%s/%s:%s", host, repo, tag)) + if err != nil { + return err + } + mf, err := mIdx.IndexManifest() + if err != nil { + return err + } + img, err := mIdx.Image(mf.Manifests[0].Digest) + if err != nil { + return err + } + return remote.Write(ref, img, remote.WithAuthFromKeychain(authn.DefaultKeychain)) +} diff --git a/pkg/tuf/tuf.go b/pkg/tuf/tuf.go new file mode 100644 index 0000000..48d0711 --- /dev/null +++ b/pkg/tuf/tuf.go @@ -0,0 +1,216 @@ +package tuf + +import ( + "errors" + "fmt" + "io/fs" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/docker/attest/internal/util" + "github.com/theupdateframework/go-tuf/v2/metadata" + "github.com/theupdateframework/go-tuf/v2/metadata/config" + "github.com/theupdateframework/go-tuf/v2/metadata/fetcher" + "github.com/theupdateframework/go-tuf/v2/metadata/trustedmetadata" + "github.com/theupdateframework/go-tuf/v2/metadata/updater" +) + +type TufSource string + +const ( + HttpSource TufSource = "http" + OciSource TufSource = "oci" +) + +type TUFClient interface { + DownloadTarget(target, filePath string) (actualFilePath string, data []byte, err error) +} + +type TufClient struct { + updater *updater.Updater + cfg *config.UpdaterConfig +} + +// NewTufClient creates a new TUF client +func NewTufClient(initialRoot []byte, tufPath, metadataSource, targetsSource string) (*TufClient, error) { + var tufSource TufSource + if strings.HasPrefix(metadataSource, "https://") || strings.HasPrefix(metadataSource, "http://") { + tufSource = HttpSource + } else { + tufSource = OciSource + } + + tufRootDigest := util.HexHashBytes(initialRoot) + + // create a directory for each initial root.json + metadataPath := filepath.Join(tufPath, tufRootDigest) + err := os.MkdirAll(metadataPath, 0755) + if err != nil { + return nil, fmt.Errorf("failed to create directory '%s': %w", metadataPath, err) + } + rootFile := filepath.Join(metadataPath, "root.json") + var rootBytes []byte + rootBytes, err = os.ReadFile(rootFile) + + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("failed to read root.json: %w", err) + } + // write the root.json file to the metadata directory + err = os.WriteFile(rootFile, initialRoot, 0644) + if err != nil { + return nil, fmt.Errorf("Failed to write root.json %w", err) + } + rootBytes = initialRoot + } + + // create updater configuration + cfg, err := config.New(metadataSource, rootBytes) // default config + if err != nil { + return nil, fmt.Errorf("failed to create TUF updater configuration: %w", err) + } + cfg.LocalMetadataDir = metadataPath + cfg.LocalTargetsDir = filepath.Join(metadataPath, "download") + cfg.RemoteTargetsURL = targetsSource + + if tufSource == OciSource { + metadataRepo, metadataTag, found := strings.Cut(metadataSource, ":") + if !found { + fmt.Printf("metadata tag not found in URL, using latest\n") + metadataTag = "latest" + } + cfg.Fetcher = NewRegistryFetcher(metadataRepo, metadataTag, targetsSource) + } + + // create a new Updater instance + up, err := updater.New(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create TUF updater instance: %w", err) + } + + // try to build the top-level metadata + err = up.Refresh() + if err != nil { + return nil, fmt.Errorf("failed to refresh trusted metadata: %w", err) + } + + client := &TufClient{ + updater: up, + cfg: cfg, + } + return client, nil + +} + +// DownloadTarget downloads the target file using Updater. The Updater gets the target +// information, verifies if the target is already cached, and if it is not cached, +// downloads the target file. +func (t *TufClient) DownloadTarget(target string, filePath string) (actualFilePath string, data []byte, err error) { + // search if the desired target is available + targetInfo, err := t.updater.GetTargetInfo(target) + if err != nil { + return "", nil, err + } + + // target is available, so let's see if the target is already present locally + actualFilePath, data, err = t.updater.FindCachedTarget(targetInfo, filePath) + if err != nil { + return "", nil, fmt.Errorf("failed while finding a cached target: %w", err) + } + if data != nil { + return actualFilePath, data, err + } + + // target is not present locally, so let's try to download it + actualFilePath, data, err = t.updater.DownloadTarget(targetInfo, filePath, "") + if err != nil { + return "", nil, fmt.Errorf("failed to download target file %s - %w", target, err) + } + + return actualFilePath, data, err +} + +func (t *TufClient) GetMetadata() trustedmetadata.TrustedMetadata { + return t.updater.GetTrustedMetadataSet() +} + +func (t *TufClient) MaxRootLength() int64 { + return t.cfg.RootMaxLength +} + +func (t *TufClient) GetPriorRoots(metadataURL string) (map[string][]byte, error) { + rootMetadata := map[string][]byte{} + trustedMetadata := t.GetMetadata() + client := fetcher.DefaultFetcher{} + for i := 1; i < int(trustedMetadata.Root.Signed.Version); i++ { + meta, err := client.DownloadFile(metadataURL+fmt.Sprintf("/%d.root.json", i), t.MaxRootLength(), time.Second*15) + if err != nil { + return nil, fmt.Errorf("failed to download root metadata: %w", err) + } + rootMetadata[fmt.Sprintf("%d.root.json", i)] = meta + } + return rootMetadata, nil +} + +func (t *TufClient) SetRemoteTargetsURL(url string) { + t.cfg.RemoteTargetsURL = url +} + +// Derived from updater.loadTargets() in theupdateframework/go-tuf +func (t *TufClient) LoadDelegatedTargets(roleName, parentName string) (*metadata.Metadata[metadata.TargetsType], error) { + // extract the targets meta from the trusted snapshot metadata + meta := t.updater.GetTrustedMetadataSet() + metaInfo := meta.Snapshot.Signed.Meta[fmt.Sprintf("%s.json", roleName)] + // extract the length of the target metadata to be downloaded + length := metaInfo.Length + if length == 0 { + length = t.cfg.TargetsMaxLength + } + // extract which target metadata version should be downloaded in case of consistent snapshots + version := "" + if meta.Root.Signed.ConsistentSnapshot { + version = strconv.FormatInt(metaInfo.Version, 10) + } + // download targets metadata + data, err := t.downloadMetadata(roleName, length, version) + if err != nil { + return nil, err + } + // verify and load the new target metadata + delegatedTargets, err := meta.UpdateDelegatedTargets(data, roleName, parentName) + if err != nil { + return nil, err + } + return delegatedTargets, nil +} + +// downloadMetadata download a metadata file and return it as bytes +func (t *TufClient) downloadMetadata(roleName string, length int64, version string) ([]byte, error) { + urlPath := ensureTrailingSlash(t.cfg.RemoteMetadataURL) + // build urlPath + if version == "" { + urlPath = fmt.Sprintf("%s%s.json", urlPath, url.QueryEscape(roleName)) + } else { + urlPath = fmt.Sprintf("%s%s.%s.json", urlPath, version, url.QueryEscape(roleName)) + } + return t.cfg.Fetcher.DownloadFile(urlPath, length, time.Second*15) +} + +// ensureTrailingSlash ensures url ends with a slash +func ensureTrailingSlash(url string) string { + if updater.IsWindowsPath(url) { + slash := string(filepath.Separator) + if strings.HasSuffix(url, slash) { + return url + } + return url + slash + } + if strings.HasSuffix(url, "/") { + return url + } + return url + "/" +} diff --git a/pkg/tuf/tuf_test.go b/pkg/tuf/tuf_test.go new file mode 100644 index 0000000..dd3eba4 --- /dev/null +++ b/pkg/tuf/tuf_test.go @@ -0,0 +1,57 @@ +package tuf + +import ( + "context" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/docker/attest/internal/embed" + "github.com/docker/attest/internal/test" + "github.com/stretchr/testify/assert" +) + +var ( + HttpTufTestDataPath = filepath.Join("..", "..", "internal", "test", "testdata", "test-repo") + OciTufTestDataPath = filepath.Join("..", "..", "internal", "test", "testdata", "test-repo-oci") +) + +// NewTufClient creates a new TUF client +func TestRootInit(t *testing.T) { + tufPath := test.CreateTempDir(t, "", "tuf_temp") + + // Start a test HTTP server to serve data from ./testdata/test-repo/ paths + server := httptest.NewServer(http.FileServer(http.Dir(HttpTufTestDataPath))) + defer server.Close() + + // run local registry + registry, regAddr := RunTestRegistry(t) + defer func() { + if err := registry.Terminate(context.Background()); err != nil { + t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic + } + }() + LoadRegistryTestData(t, regAddr, OciTufTestDataPath) + + testCases := []struct { + name string + metadataSource string + targetsSource string + }{ + {"http", server.URL + "/metadata", server.URL + "/targets"}, + {"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"}, + } + + for _, tc := range testCases { + _, err := NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource) + assert.NoErrorf(t, err, "Failed to create TUF client: %v", err) + + // recreation should work with same root + _, err = NewTufClient(embed.DevRoot, tufPath, tc.metadataSource, tc.targetsSource) + assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err) + + _, err = NewTufClient([]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource) + assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err) + } +}