mirror of
https://github.com/slsa-framework/slsa-verifier.git
synced 2026-02-14 09:39:54 +00:00
feat: vsa support (#777)
Fixes #542
Adds support for VSAs.
## Testing process
- added some unit an end-to-end tests
- manually invoking
```
go run ./cli/slsa-verifier/ verify-vsa \
--subject-digest gce_image_id:8970095005306000053 \
--attestation-path
./cli/slsa-verifier/testdata/vsa/gce/v1/gke-gce-pre.bcid-vsa.jsonl \
--verifier-id
https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1 \
--resource-uri
gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre
\
--verified-level BCID_L1 \
--verified-level SLSA_BUILD_LEVEL_2 \
--public-key-path
./cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem \
--public-key-id keystore://76574:prod:vsa_signing_public_key \
--print-attestation
{"_type":"https://in-toto.io/Statement/v1","predicateType":"https://slsa.dev/verification_summary/v1","predicate":{"timeVerified":"2024-06-12T07:24:34.351608Z","verifier":{"id":"https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"},"verificationResult":"PASSED","verifiedLevels":["BCID_L1","SLSA_BUILD_LEVEL_2"],"resourceUri":"gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre","policy":{"uri":"googlefile:/google_src/files/642513192/depot/google3/production/security/bcid/software/gce_image/gke/vm_images.sw_policy.textproto"}},"subject":[{"name":"_","digest":{"gce_image_id":"8970095005306000053"}}]}
Verifying VSA: PASSED
PASSED: SLSA verification passed
```
TODOS:
- open issue on the in_toto attestations repo about the incorrect json
[fields](36c1129542/go/predicates/vsa/v1/vsa.pb.go (L26-L40))
for vsa 1.0
---------
Signed-off-by: Ramon Petgrave <ramon.petgrave64@gmail.com>
This commit is contained in:
66
README.md
66
README.md
@@ -37,6 +37,10 @@
|
||||
- [Verification for Google Cloud Build](#verification-for-google-cloud-build)
|
||||
- [Artifacts](#artifacts-1)
|
||||
- [Containers](#containers-1)
|
||||
- [Verification Summary Attestations (VSA)](#verification-summary-attestations-vsa)
|
||||
- [Caveats](#caveats)
|
||||
- [Sigstore](#sigstore)
|
||||
- [Subject Resource Descriptors](#subject-resource-descriptors)
|
||||
- [Known Issues](#known-issues)
|
||||
- [tuf: invalid key](#tuf-invalid-key)
|
||||
- [panic: assignment to entry in nil map](#panic-assignment-to-entry-in-nil-map)
|
||||
@@ -481,6 +485,68 @@ The verified in-toto statement may be written to stdout with the
|
||||
|
||||
Note that `--source-uri` supports GitHub repository URIs like `github.com/$OWNER/$REPO` when the build was enabled with a Cloud Build [GitHub trigger](https://cloud.google.com/build/docs/automating-builds/github/build-repos-from-github). Otherwise, the build provenance will contain the name of the Cloud Storage bucket used to host the source files, usually of the form `gs://[PROJECT_ID]_cloudbuild/source` (see [Running build](https://cloud.google.com/build/docs/running-builds/submit-build-via-cli-api#running_builds)). We recommend using GitHub triggers in order to preserve the source provenance and valiate that the source came from an expected, version-controlled repository. You _may_ match on the fully-qualified tar like `gs://[PROJECT_ID]_cloudbuild/source/1665165360.279777-955d1904741e4bbeb3461080299e929a.tgz`.
|
||||
|
||||
## Verification Summary Attestations (VSA)
|
||||
|
||||
We have support for [verifying](https://slsa.dev/spec/v1.1/verification_summary#how-to-verify) VSAs.
|
||||
Rather than passing in filepaths as arguments, we allow passing in mulitple `--subject-digest` cli options, to
|
||||
accomodate subjects that are not simple-files.
|
||||
|
||||
|
||||
The verify-vsa command
|
||||
|
||||
```shell
|
||||
$ slsa-verifier verify-vsa --help
|
||||
Verifies SLSA VSAs for the given subject-digests
|
||||
|
||||
Usage:
|
||||
slsa-verifier verify-vsa [flags] subject-digest [subject-digest...]
|
||||
|
||||
Flags:
|
||||
--attestation-path string path to a file containing the attestation
|
||||
-h, --help help for verify-vsa
|
||||
--print-attestation [optional] print the contents of attestation to stdout
|
||||
--public-key-id string [optional] the ID of the public key, defaults to the SHA256 digest of the base64-encoded public key
|
||||
--public-key-path string path to a public key file
|
||||
--resource-uri string the resource URI to be verified
|
||||
--subject-digest stringArray the digests to be verified. Pass multiple digests by repeating the flag. e.g. --subject-digest <digest type>:<digest value> --subject-digest <digest type>:<digest value>
|
||||
--verified-level stringArray [optional] the levels of verification to be performed. Pass multiple digests by repeating the flag, e.g., --verified-level SLSA_BUILD_LEVEL_2 --verified-level FEDRAMP_LOW'
|
||||
--verifier-id string the unique verifier ID who created the attestation
|
||||
```
|
||||
|
||||
To verify VSAs, invoke like this
|
||||
|
||||
```shell
|
||||
$ slsa-verifier verify-vsa \
|
||||
--subject-digest gce_image_id:8970095005306000053 \
|
||||
--attestation-path ./cli/slsa-verifier/testdata/vsa/gce/v1/gke-gce-pre.bcid-vsa.jsonl \
|
||||
--verifier-id https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1 \
|
||||
--resource-uri gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre \
|
||||
--verified-level BCID_L1 \
|
||||
--verified-level SLSA_BUILD_LEVEL_2 \
|
||||
--public-key-path ./cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem \
|
||||
--public-key-id keystore://76574:prod:vsa_signing_public_key \
|
||||
--print-attestation
|
||||
```
|
||||
|
||||
For multiple subjects, use:
|
||||
|
||||
```
|
||||
--subject-digest sha256:abc123
|
||||
--subject-digest sha256:xyz456
|
||||
```
|
||||
|
||||
### Caveats
|
||||
|
||||
#### Sigstore
|
||||
|
||||
This support does not work yet with VSAs wrapped in Sigstore bundles, only with simple DSSE envelopes.
|
||||
With that, we allow the user to pass in the public key.
|
||||
Note that if the DSSE Envelope `signatures` specifies a `keyid` that is not a unpadded base64 encoded sha256 hash the key, like `sha256:abc123...` (not a well-known identifier, e.g, `my-kms:prod-vsa-key`), then you must supply the `--public-key-id` cli option.
|
||||
|
||||
#### Subject Resource Descriptors
|
||||
|
||||
According to slsa.dev's [VSA schema](https://slsa.dev/spec/v1.1/verification_summary#schema), we only support the Subject's `Name` and `Digest`, not the full in_toto [Statement](https://pkg.go.dev/github.com/in-toto/attestation/go/v1#Statement)'s [ResourceDescriptor](https://github.com/in-toto/attestation/blob/main/spec/v1/resource_descriptor.md).
|
||||
|
||||
## Known Issues
|
||||
|
||||
### tuf: invalid key
|
||||
|
||||
@@ -37,6 +37,7 @@ For more information on SLSA, visit https://slsa.dev`,
|
||||
c.AddCommand(verifyArtifactCmd())
|
||||
c.AddCommand(verifyImageCmd())
|
||||
c.AddCommand(verifyNpmPackageCmd())
|
||||
c.AddCommand(verifyVSACmd())
|
||||
// We print our own errors and usage in the check function.
|
||||
c.SilenceErrors = true
|
||||
return c
|
||||
|
||||
@@ -1515,88 +1515,88 @@ func Test_runVerifyNpmPackage(t *testing.T) {
|
||||
name: "valid npm CLI builder",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder short runner name",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: PointerTo("https://github.com/actions/runner"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: pointerTo("https://github.com/actions/runner"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder no builder",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
err: serrors.ErrorInvalidBuilderID,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch builder",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: PointerTo("https://github.com/actions/runner2"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: pointerTo("https://github.com/actions/runner2"),
|
||||
err: serrors.ErrorNotSupported,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder no package name",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder no package version",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch source",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggleS",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch package version",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgVersion: PointerTo("1.0.4"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgVersion: pointerTo("1.0.4"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorMismatchPackageVersion,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch package name",
|
||||
artifact: "supreme-googles-cli-v02-tag.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggleS"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggleS"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorMismatchPackageName,
|
||||
},
|
||||
{
|
||||
name: "invalid signature provenance npm CLI",
|
||||
artifact: "supreme-googles-cli-v02-tag-invalidsigprov.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorInvalidSignature,
|
||||
},
|
||||
{
|
||||
name: "invalid signature provenance npm CLI",
|
||||
artifact: "supreme-googles-cli-v02-tag-invalidsigpub.tgz",
|
||||
source: "github.com/trishankatdatadog/supreme-goggles",
|
||||
pkgName: PointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@trishankatdatadog/supreme-goggles"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorInvalidSignature,
|
||||
},
|
||||
// npm CLI with main branch.
|
||||
@@ -1604,86 +1604,86 @@ func Test_runVerifyNpmPackage(t *testing.T) {
|
||||
name: "valid npm CLI builder",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.3"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgVersion: pointerTo("1.0.3"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder short runner name",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.3"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/actions/runner"),
|
||||
pkgVersion: pointerTo("1.0.3"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/actions/runner"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder no builder",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.3"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
pkgVersion: pointerTo("1.0.3"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
err: serrors.ErrorInvalidBuilderID,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch builder",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.3"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/actions/runner2"),
|
||||
pkgVersion: pointerTo("1.0.3"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/actions/runner2"),
|
||||
err: serrors.ErrorNotSupported,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder no package name",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
pkgVersion: PointerTo("1.0.3"),
|
||||
pkgVersion: pointerTo("1.0.3"),
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder no package version",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch source",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test2",
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch package version",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.4"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgVersion: pointerTo("1.0.4"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorMismatchPackageVersion,
|
||||
},
|
||||
{
|
||||
name: "valid npm CLI builder mismatch package name",
|
||||
artifact: "provenance-npm-test-cli-v02-prega.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test2"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test2"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorMismatchPackageName,
|
||||
},
|
||||
{
|
||||
name: "invalid signature provenance npm CLI",
|
||||
artifact: "provenance-npm-test-cli-v02-prega-invalidsigprov.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorInvalidSignature,
|
||||
},
|
||||
{
|
||||
name: "invalid signature publish npm CLI",
|
||||
artifact: "provenance-npm-test-cli-v02-prega-invalidsigpub.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/actions/runner/github-hosted"),
|
||||
err: serrors.ErrorInvalidSignature,
|
||||
},
|
||||
// OSSF builder.
|
||||
@@ -1691,84 +1691,84 @@ func Test_runVerifyNpmPackage(t *testing.T) {
|
||||
name: "valid npm OSSF builder",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
},
|
||||
{
|
||||
name: "valid npm OSSF builder no builder",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
err: serrors.ErrorInvalidBuilderID,
|
||||
},
|
||||
{
|
||||
name: "valid npm OSSF builder mismatch builder",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa.yml"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa.yml"),
|
||||
err: serrors.ErrorMismatchBuilderID,
|
||||
},
|
||||
{
|
||||
name: "valid npm OSSF builder no package name",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
},
|
||||
{
|
||||
name: "valid npm OSSF builder no package version",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
},
|
||||
{
|
||||
name: "valid npm OSSF builder mismatch package name",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test2"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test2"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
err: serrors.ErrorMismatchPackageName,
|
||||
},
|
||||
{
|
||||
name: "valid npm OSSF builder mismatch package version",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.6"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgVersion: pointerTo("1.0.6"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
err: serrors.ErrorMismatchPackageVersion,
|
||||
},
|
||||
{
|
||||
name: "valid npm OSSF builder mismatch mismatch source",
|
||||
artifact: "provenance-npm-test-ossf.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test2",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
err: serrors.ErrorMismatchSource,
|
||||
},
|
||||
{
|
||||
name: "invalid signature provenance npm OSSF builder",
|
||||
artifact: "provenance-npm-test-ossf-invalidsigprov.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
err: serrors.ErrorInvalidSignature,
|
||||
},
|
||||
{
|
||||
name: "invalid signature publish npm OSSF builder",
|
||||
artifact: "provenance-npm-test-ossf-invalidsigpub.tgz",
|
||||
source: "github.com/laurentsimon/provenance-npm-test",
|
||||
pkgVersion: PointerTo("1.0.5"),
|
||||
pkgName: PointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: PointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
pkgVersion: pointerTo("1.0.5"),
|
||||
pkgName: pointerTo("@laurentsimon/provenance-npm-test"),
|
||||
builderID: pointerTo("https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_nodejs_slsa3.yml"),
|
||||
err: serrors.ErrorInvalidSignature,
|
||||
},
|
||||
}
|
||||
@@ -1795,6 +1795,74 @@ func Test_runVerifyNpmPackage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func PointerTo[K any](object K) *K {
|
||||
// Test_runVerifyVSA tests the CLI inputes of verify-vsa. More extensive tests are in
|
||||
// slsa-verifier/verifiers/internal/vsa/verifier_test.go
|
||||
func Test_runVerifyVSA(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
attestationPath *string
|
||||
subjectDigests *[]string
|
||||
verifierID *string
|
||||
resourceURI *string
|
||||
verifiedLevels *[]string
|
||||
publicKeyPath *string
|
||||
publicKeyID *string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "success: gke",
|
||||
attestationPath: pointerTo("gce/v1/gke-gce-pre.bcid-vsa.jsonl"),
|
||||
subjectDigests: pointerTo([]string{"gce_image_id:8970095005306000053"}),
|
||||
verifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"),
|
||||
resourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"),
|
||||
verifiedLevels: pointerTo([]string{"BCID_L1", "SLSA_BUILD_LEVEL_2"}),
|
||||
publicKeyPath: pointerTo("gce/v1/vsa_signing_public_key.pem"),
|
||||
publicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"),
|
||||
},
|
||||
{
|
||||
name: "fail: gke, empty public key id",
|
||||
attestationPath: pointerTo("gce/v1/gke-gce-pre.bcid-vsa.jsonl"),
|
||||
publicKeyPath: pointerTo("gce/v1/vsa_signing_public_key.pem"),
|
||||
publicKeyID: pointerTo(""),
|
||||
err: serrors.ErrorNoValidSignature,
|
||||
},
|
||||
{
|
||||
name: "fail: gke, wrong key id",
|
||||
attestationPath: pointerTo("gce/v1/gke-gce-pre.bcid-vsa.jsonl"),
|
||||
publicKeyPath: pointerTo("gce/v1/vsa_signing_public_key.pem"),
|
||||
publicKeyID: pointerTo("my_key_id"),
|
||||
err: serrors.ErrorNoValidSignature,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt // Re-initializing variable so it is not changed while executing the closure below
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
attestationPath := filepath.Clean(filepath.Join(TEST_DIR, "vsa", *tt.attestationPath))
|
||||
publicKeyPath := filepath.Clean(filepath.Join(TEST_DIR, "vsa", *tt.publicKeyPath))
|
||||
|
||||
cmd := verify.VerifyVSACommand{
|
||||
AttestationPath: &attestationPath,
|
||||
SubjectDigests: tt.subjectDigests,
|
||||
VerifierID: tt.verifierID,
|
||||
ResourceURI: tt.resourceURI,
|
||||
VerifiedLevels: tt.verifiedLevels,
|
||||
PublicKeyPath: &publicKeyPath,
|
||||
PublicKeyID: tt.publicKeyID,
|
||||
}
|
||||
|
||||
err := cmd.Exec(context.Background())
|
||||
if diff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Fatalf("unexpected error (-want +got): \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func pointerTo[K any](object K) *K {
|
||||
return &object
|
||||
}
|
||||
|
||||
1
cli/slsa-verifier/testdata/vsa/gce/v1/gke-gce-pre.bcid-vsa.jsonl
vendored
Normal file
1
cli/slsa-verifier/testdata/vsa/gce/v1/gke-gce-pre.bcid-vsa.jsonl
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJwcmVkaWNhdGVUeXBlIjoiaHR0cHM6Ly9zbHNhLmRldi92ZXJpZmljYXRpb25fc3VtbWFyeS92MSIsInByZWRpY2F0ZSI6eyJ0aW1lVmVyaWZpZWQiOiIyMDI0LTA2LTEyVDA3OjI0OjM0LjM1MTYwOFoiLCJ2ZXJpZmllciI6eyJpZCI6Imh0dHBzOi8vYmNpZC5jb3JwLmdvb2dsZS5jb20vdmVyaWZpZXIvYmNpZF9wYWNrYWdlX2VuZm9yY2VyL3YwLjEifSwidmVyaWZpY2F0aW9uUmVzdWx0IjoiUEFTU0VEIiwidmVyaWZpZWRMZXZlbHMiOlsiQkNJRF9MMSIsIlNMU0FfQlVJTERfTEVWRUxfMiJdLCJyZXNvdXJjZVVyaSI6ImdjZV9pbWFnZTovL2drZS1ub2RlLWltYWdlczpna2UtMTI2MTUtZ2tlMTQxODAwMC1jb3MtMTAxLTE3MTYyLTQ2My0yOS1jLWNncHYxLXByZSIsInBvbGljeSI6eyJ1cmkiOiJnb29nbGVmaWxlOi9nb29nbGVfc3JjL2ZpbGVzLzY0MjUxMzE5Mi9kZXBvdC9nb29nbGUzL3Byb2R1Y3Rpb24vc2VjdXJpdHkvYmNpZC9zb2Z0d2FyZS9nY2VfaW1hZ2UvZ2tlL3ZtX2ltYWdlcy5zd19wb2xpY3kudGV4dHByb3RvIn19LCJzdWJqZWN0IjpbeyJuYW1lIjoiXyIsImRpZ2VzdCI6eyJnY2VfaW1hZ2VfaWQiOiI4OTcwMDk1MDA1MzA2MDAwMDUzIn19XX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"sig":"bmIy2gfnQt6oYpd0WbpQMtZcMRtmntDmyki+Be+2Z9qkboMVbi2RQAD1b5AWbBs7iAP8NZVJOI4R/4jOVYB/FA==","keyid":"keystore://76574:prod:vsa_signing_public_key"}]}
|
||||
4
cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem
vendored
Normal file
4
cli/slsa-verifier/testdata/vsa/gce/v1/vsa_signing_public_key.pem
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca
|
||||
3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg==
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SUCCESS = "PASSED: Verified SLSA provenance"
|
||||
SUCCESS = "PASSED: SLSA verification passed"
|
||||
FAILURE = "FAILED: SLSA verification failed"
|
||||
)
|
||||
|
||||
@@ -184,3 +184,34 @@ func verifyNpmPackageCmd() *cobra.Command {
|
||||
o.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func verifyVSACmd() *cobra.Command {
|
||||
o := &verify.VerifyVSAOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "verify-vsa [flags] subject-digest [subject-digest...]",
|
||||
Args: cobra.NoArgs,
|
||||
Short: "Verifies SLSA VSAs for the given subject-digests",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
v := verify.VerifyVSACommand{
|
||||
SubjectDigests: &o.SubjectDigests,
|
||||
AttestationPath: &o.AttestationPath,
|
||||
VerifierID: &o.VerifierID,
|
||||
ResourceURI: &o.ResourceURI,
|
||||
VerifiedLevels: &o.VerifiedLevels,
|
||||
PrintAttestation: o.PrintAttestation,
|
||||
PublicKeyPath: &o.PublicKeyPath,
|
||||
PublicKeyID: &o.PublicKeyID,
|
||||
}
|
||||
if err := v.Exec(cmd.Context()); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s: %v\n", FAILURE, err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", SUCCESS)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
o.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -127,6 +127,54 @@ func (o *VerifyNpmOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.MarkFlagsMutuallyExclusive("source-versioned-tag", "source-tag")
|
||||
}
|
||||
|
||||
// VerifyVSAOptions is the top-level options for the `verifyVSA` command.
|
||||
type VerifyVSAOptions struct {
|
||||
SubjectDigests []string
|
||||
AttestationPath string
|
||||
VerifierID string
|
||||
ResourceURI string
|
||||
VerifiedLevels []string
|
||||
PublicKeyPath string
|
||||
PublicKeyID string
|
||||
PrintAttestation bool
|
||||
}
|
||||
|
||||
var _ Interface = (*VerifyVSAOptions)(nil)
|
||||
|
||||
// AddFlags implements Interface.
|
||||
func (o *VerifyVSAOptions) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringArrayVar(&o.SubjectDigests, "subject-digest", []string{},
|
||||
"the digests to be verified. Pass multiple digests by repeating the flag. e.g. --subject-digest <digest type>:<digest value> --subject-digest <digest type>:<digest value>")
|
||||
|
||||
cmd.Flags().StringVar(&o.AttestationPath, "attestation-path", "",
|
||||
"path to a file containing the attestation")
|
||||
|
||||
cmd.Flags().StringVar(&o.VerifierID, "verifier-id", "",
|
||||
"the unique verifier ID who created the attestation")
|
||||
|
||||
cmd.Flags().StringVar(&o.ResourceURI, "resource-uri", "",
|
||||
"the resource URI to be verified")
|
||||
|
||||
cmd.Flags().StringArrayVar(&o.VerifiedLevels, "verified-level", []string{},
|
||||
"[optional] the levels of verification to be performed. Pass multiple digests by repeating the flag, e.g., --verified-level SLSA_BUILD_LEVEL_2 --verified-level FEDRAMP_LOW'")
|
||||
|
||||
cmd.Flags().BoolVar(&o.PrintAttestation, "print-attestation", false,
|
||||
"[optional] print the contents of attestation to stdout")
|
||||
|
||||
cmd.Flags().StringVar(&o.PublicKeyPath, "public-key-path", "",
|
||||
"path to a public key file")
|
||||
|
||||
cmd.Flags().StringVar(&o.PublicKeyID, "public-key-id", "",
|
||||
"[optional] the ID of the public key, defaults to the SHA256 digest of the base64-encoded public key")
|
||||
|
||||
cmd.MarkFlagRequired("subject-digests")
|
||||
cmd.MarkFlagRequired("attestation-path")
|
||||
cmd.MarkFlagRequired("verifier-id")
|
||||
cmd.MarkFlagRequired("resource-uri")
|
||||
cmd.MarkFlagRequired("public-key-path")
|
||||
// public-key-id" and "public-key-signing-hash-algo" are optional since they have useful defaults
|
||||
}
|
||||
|
||||
type workflowInputs struct {
|
||||
kv map[string]string
|
||||
}
|
||||
|
||||
117
cli/slsa-verifier/verify/verify_vsa.go
Normal file
117
cli/slsa-verifier/verify/verify_vsa.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2022 SLSA Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package verify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/options"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/verifiers"
|
||||
)
|
||||
|
||||
// VerifyVSACommand contains the parameters for the verify-vsa command.
|
||||
type VerifyVSACommand struct {
|
||||
SubjectDigests *[]string
|
||||
AttestationPath *string
|
||||
VerifierID *string
|
||||
ResourceURI *string
|
||||
VerifiedLevels *[]string
|
||||
PrintAttestation bool
|
||||
PublicKeyPath *string
|
||||
PublicKeyID *string
|
||||
}
|
||||
|
||||
// Exec executes the verifiers.VerifyVSA.
|
||||
func (c *VerifyVSACommand) Exec(ctx context.Context) error {
|
||||
vsaOpts := &options.VSAOpts{
|
||||
ExpectedDigests: c.SubjectDigests,
|
||||
ExpectedVerifierID: c.VerifierID,
|
||||
ExpectedResourceURI: c.ResourceURI,
|
||||
ExpectedVerifiedLevels: c.VerifiedLevels,
|
||||
}
|
||||
pubKeyBytes, err := os.ReadFile(*c.PublicKeyPath)
|
||||
if err != nil {
|
||||
printFailed(err)
|
||||
return err
|
||||
}
|
||||
pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %w", serrors.ErrorInvalidPublicKey, err)
|
||||
printFailed(err)
|
||||
return err
|
||||
}
|
||||
hashAlgo := determineSignatureHashAlgo(pubKey)
|
||||
VerificationOpts := &options.VerificationOpts{
|
||||
PublicKey: pubKey,
|
||||
PublicKeyID: c.PublicKeyID,
|
||||
PublicKeyHashAlgo: hashAlgo,
|
||||
}
|
||||
attestation, err := os.ReadFile(*c.AttestationPath)
|
||||
if err != nil {
|
||||
printFailed(err)
|
||||
return err
|
||||
}
|
||||
vsaBytes, err := verifiers.VerifyVSA(ctx, attestation, vsaOpts, VerificationOpts)
|
||||
if err != nil {
|
||||
printFailed(err)
|
||||
return err
|
||||
}
|
||||
if c.PrintAttestation {
|
||||
fmt.Fprintf(os.Stdout, "%s\n", string(vsaBytes))
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Verifying VSA: PASSED\n\n")
|
||||
// verfiers.VerifyVSA already checks if the producerID matches
|
||||
return nil
|
||||
}
|
||||
|
||||
// printFailed prints the error message to stderr.
|
||||
func printFailed(err error) {
|
||||
fmt.Fprintf(os.Stderr, "Verifying VSA: FAILED: %v\n\n", err)
|
||||
}
|
||||
|
||||
// determineSignatureHashAlgo determines the hash algorithm used to compute the digest to be signed, based on the public key.
|
||||
// some well-known defaults can be determined, otherwise the it returns crypto.SHA256.
|
||||
func determineSignatureHashAlgo(pubKey crypto.PublicKey) crypto.Hash {
|
||||
var h crypto.Hash
|
||||
switch pk := pubKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
h = crypto.SHA256
|
||||
case *ecdsa.PublicKey:
|
||||
switch pk.Curve {
|
||||
case elliptic.P256():
|
||||
h = crypto.SHA256
|
||||
case elliptic.P384():
|
||||
h = crypto.SHA384
|
||||
case elliptic.P521():
|
||||
h = crypto.SHA512
|
||||
default:
|
||||
h = crypto.SHA256
|
||||
}
|
||||
case ed25519.PublicKey:
|
||||
h = crypto.SHA512
|
||||
default:
|
||||
h = crypto.SHA256
|
||||
}
|
||||
return h
|
||||
}
|
||||
@@ -44,4 +44,11 @@ var (
|
||||
ErrorInvalidHash = errors.New("invalid hash")
|
||||
ErrorNotPresent = errors.New("not present")
|
||||
ErrorInvalidPublicKey = errors.New("invalid public key")
|
||||
ErrorInvalidVerificationResult = errors.New("verificationResult is not PASSED")
|
||||
ErrorMismatchVerifiedLevels = errors.New("verified levels do not match")
|
||||
ErrorMissingSubjectDigest = errors.New("missing subject digest")
|
||||
ErrorEmptyRequiredField = errors.New("empty value in required field")
|
||||
ErrorMismatchResourceURI = errors.New("resource URI does not match")
|
||||
ErrorMismatchVerifierID = errors.New("verifier ID does not match")
|
||||
ErrorInvalidSLSALevel = errors.New("invalid SLSA level")
|
||||
)
|
||||
|
||||
3
go.mod
3
go.mod
@@ -18,6 +18,7 @@ require (
|
||||
require (
|
||||
github.com/google/go-containerregistry v0.19.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/in-toto/attestation v1.1.0
|
||||
github.com/sigstore/cosign/v2 v2.2.4
|
||||
github.com/sigstore/sigstore-go v0.2.0
|
||||
github.com/slsa-framework/slsa-github-generator v1.9.0
|
||||
@@ -115,7 +116,7 @@ require (
|
||||
golang.org/x/term v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/grpc v1.62.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0
|
||||
google.golang.org/protobuf v1.34.1
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.120.1 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -322,6 +322,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/in-toto/attestation v1.1.0 h1:oRWzfmZPDSctChD0VaQV7MJrywKOzyNrtpENQFq//2Q=
|
||||
github.com/in-toto/attestation v1.1.0/go.mod h1:DB59ytd3z7cIHgXxwpSX2SABrU6WJUKg/grpdgHVgVs=
|
||||
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
|
||||
github.com/in-toto/in-toto-golang v0.9.0/go.mod h1:xsBVrVsHNsB61++S6Dy2vWosKhuA3lUTQd+eF9HdeMo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -640,8 +642,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
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=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/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=
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package options
|
||||
|
||||
import "crypto"
|
||||
|
||||
// ProvenanceOpts are the options for checking provenance information.
|
||||
type ProvenanceOpts struct {
|
||||
// ExpectedBranch is the expected branch (github_ref or github_base_ref) in
|
||||
@@ -28,12 +30,38 @@ type ProvenanceOpts struct {
|
||||
|
||||
ExpectedPackageVersion *string
|
||||
|
||||
// ExpectedProvenanceRepository is the provenance repository that is passed from user and not verified
|
||||
// ExpectedProvenanceRepository is the provenance repository that is passed from user.
|
||||
ExpectedProvenanceRepository *string
|
||||
}
|
||||
|
||||
// BuildOpts are the options for checking the builder.
|
||||
type BuilderOpts struct {
|
||||
// ExpectedBuilderID is the builderID passed in from the user to be verified
|
||||
// ExpectedBuilderID is the builderID passed in from the user.
|
||||
ExpectedID *string
|
||||
}
|
||||
|
||||
// VSAOpts are the options for checking the VSA.
|
||||
type VSAOpts struct {
|
||||
// ExpectedDigests are the digests expected to be in the VSA.
|
||||
ExpectedDigests *[]string
|
||||
|
||||
// ExpectedVerifierID is the verifier ID that is passed from user.
|
||||
ExpectedVerifierID *string
|
||||
|
||||
// ExpectedResourceURI is the resource URI that is passed from user.
|
||||
ExpectedResourceURI *string
|
||||
|
||||
// ExpectedVerifiedLevels is the levels of verification that are passed from user.
|
||||
ExpectedVerifiedLevels *[]string
|
||||
}
|
||||
|
||||
type VerificationOpts struct {
|
||||
// PublicKey is the public key used to verify the signature on the Envelope.
|
||||
PublicKey crypto.PublicKey
|
||||
|
||||
// PublicKeyID is the ID of the public key.
|
||||
PublicKeyID *string
|
||||
|
||||
// PublicKeyHashAlgo is the hash algorithm used to compute digest that was signed.
|
||||
PublicKeyHashAlgo crypto.Hash
|
||||
}
|
||||
|
||||
58
verifiers/internal/vsa/v1.0/vsa.go
Normal file
58
verifiers/internal/vsa/v1.0/vsa.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package vsa10
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
intotoGolang "github.com/in-toto/in-toto-golang/in_toto"
|
||||
intotoCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
|
||||
)
|
||||
|
||||
const PredicateType = "https://slsa.dev/verification_summary/v1"
|
||||
|
||||
// VSA is a struct that represents a VSA statement.
|
||||
// spec: https://slsa.dev/spec/v1.0/verification_summary.
|
||||
// Idealy, we use "github.com/in-toto/attestation/go/predicates/vsa/v1"'s VerfificationSummary,
|
||||
// but it currently does not correctly implement some fields according to spec, such as VerifiedLevels.
|
||||
type VSA struct {
|
||||
intotoGolang.StatementHeader
|
||||
// Predicate is the VSA predicate.
|
||||
Predicate Predicate `json:"predicate"`
|
||||
}
|
||||
|
||||
// Predicate is the VSA predicate.
|
||||
type Predicate struct {
|
||||
Verifier Verifier `json:"verifier"`
|
||||
TimeVerified time.Time `json:"timeVerified"`
|
||||
ResourceURI string `json:"resourceUri"`
|
||||
Policy intotoCommon.ProvenanceMaterial `json:"policy"`
|
||||
InputAttestations []intotoCommon.ProvenanceMaterial `json:"inputAttestations"`
|
||||
VerificationResult string `json:"verificationResult"`
|
||||
VerifiedLevels []string `json:"verifiedLevels"`
|
||||
DependecyLevels map[string]int `json:"dependencyLevels"`
|
||||
SlsaVersion string `json:"slsaVersion"`
|
||||
}
|
||||
|
||||
// Verifier is the VSA verifier.
|
||||
type Verifier struct {
|
||||
ID string `json:"id"`
|
||||
Version map[string]string `json:"version"`
|
||||
}
|
||||
|
||||
// VSAFromStatement creates a VSA from a statement.
|
||||
func VSAFromStatement(statement *intotoGolang.Statement) (*VSA, error) {
|
||||
if statement.PredicateType != PredicateType {
|
||||
return nil, fmt.Errorf("%w: expected predicate type %q, got %q", serrors.ErrorInvalidDssePayload, PredicateType, statement.PredicateType)
|
||||
}
|
||||
vsaBytes, err := json.Marshal(statement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err)
|
||||
}
|
||||
var vsa VSA
|
||||
if err := json.Unmarshal(vsaBytes, &vsa); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidDssePayload, err)
|
||||
}
|
||||
return &vsa, nil
|
||||
}
|
||||
276
verifiers/internal/vsa/verifier.go
Normal file
276
verifiers/internal/vsa/verifier.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package vsa
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
sigstoreSignature "github.com/sigstore/sigstore/pkg/signature"
|
||||
sigstoreDSSE "github.com/sigstore/sigstore/pkg/signature/dsse"
|
||||
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/options"
|
||||
vsa10 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/vsa/v1.0"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
|
||||
)
|
||||
|
||||
// VerifyVSA verifies the VSA attestation. It returns the attestation base64-decoded from the envelope, and the trusted attester ID.
|
||||
// We don't return a TrustedBuilderID. Instead, the user can user can parse the builderID separately, perhaps with
|
||||
// https://pkg.go.dev/golang.org/x/mod/semver.
|
||||
func VerifyVSA(ctx context.Context,
|
||||
attestation []byte,
|
||||
vsaOpts *options.VSAOpts,
|
||||
verificationOpts *options.VerificationOpts,
|
||||
) ([]byte, error) {
|
||||
// following steps in https://slsa.dev/spec/v1.1/verification_summary#how-to-verify
|
||||
envelope, err := utils.EnvelopeFromBytes(attestation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. verify the envelope signature,
|
||||
// 4. match the verfier with the public key: implicit because we accept a user-provided public key.
|
||||
// 3. parse the VSA, verifying the predicateType.
|
||||
vsa, err := extractSignedVSA(ctx, envelope, verificationOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. match the subject digests,
|
||||
// 4. match the verifier ID,
|
||||
// 5. match the expected resourceURI,
|
||||
// 6. confirm the slsaResult is PASSED,
|
||||
// 7. match the verifiedLevels,
|
||||
// no other fields are checked.
|
||||
err = matchExpectedValues(vsa, vsaOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vsaBytes, err := envelope.DecodeB64Payload()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err)
|
||||
}
|
||||
return vsaBytes, nil
|
||||
}
|
||||
|
||||
// extractSignedVSA verifies the envelope signature and type and extracts the VSA from the envelope.
|
||||
func extractSignedVSA(ctx context.Context, envelope *dsse.Envelope, verificationOpts *options.VerificationOpts) (*vsa10.VSA, error) {
|
||||
// 1. verify the envelope signature,
|
||||
// 4. match the verfier with the public key: implicit because we accept a user-provided public key.
|
||||
err := verifyEnvelopeSignature(ctx, envelope, verificationOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statement, err := utils.StatementFromEnvelope(envelope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 3. parse the VSA, verifying the predicateType.
|
||||
vsa, err := vsa10.VSAFromStatement(statement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return vsa, nil
|
||||
}
|
||||
|
||||
// verifyEnvelopeSignature verifies the signature of the envelope.
|
||||
func verifyEnvelopeSignature(ctx context.Context, envelope *dsse.Envelope, verificationOpts *options.VerificationOpts) error {
|
||||
signatureVerifier, err := sigstoreSignature.LoadVerifier(verificationOpts.PublicKey, verificationOpts.PublicKeyHashAlgo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: loading sigstore DSSE envolope verifier: %w", serrors.ErrorInvalidPublicKey, err)
|
||||
}
|
||||
envelopeVerifier, err := dsse.NewEnvelopeVerifier(&sigstoreDSSE.VerifierAdapter{
|
||||
SignatureVerifier: signatureVerifier,
|
||||
Pub: verificationOpts.PublicKey,
|
||||
PubKeyID: *verificationOpts.PublicKeyID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: creating sigstore DSSE envelope verifier: %w", serrors.ErrorInvalidPublicKey, err)
|
||||
}
|
||||
_, err = envelopeVerifier.Verify(ctx, envelope)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: verifying envelope: %w", serrors.ErrorNoValidSignature, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchExpectedValues checks if the expected values are present in the VSA.
|
||||
func matchExpectedValues(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error {
|
||||
// 2. match the expected subject digests
|
||||
if err := matchExepectedSubjectDigests(vsa, vsaOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
// 4. match the verifier ID
|
||||
if err := matchVerifierID(vsa, vsaOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
// 5. match the expected resourceURI
|
||||
if err := matchResourceURI(vsa, vsaOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
// 6. confirm the verificationResult is Passed
|
||||
if err := confirmVerificationResult(vsa); err != nil {
|
||||
return err
|
||||
}
|
||||
// 7. match the verifiedLevels
|
||||
if err := matchVerifiedLevels(vsa, vsaOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchExepectedSubjectDigests checks if the expected subject digests are present in the VSA.
|
||||
func matchExepectedSubjectDigests(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error {
|
||||
if len(*vsaOpts.ExpectedDigests) == 0 {
|
||||
return fmt.Errorf("%w: no subject digests provided", serrors.ErrorEmptyRequiredField)
|
||||
}
|
||||
// collect all digests from the VSA, so we can efficiently search, e.g.:
|
||||
// {
|
||||
// "sha256": {
|
||||
// "abc": true,
|
||||
// "def": true,
|
||||
// },
|
||||
// "gce_image_id": {
|
||||
// "123": true,
|
||||
// "456": true,
|
||||
// }
|
||||
// }
|
||||
allVSASubjectDigests := make(map[string]map[string]bool)
|
||||
for _, subject := range vsa.Subject {
|
||||
for digestType, digestValue := range subject.Digest {
|
||||
if _, ok := allVSASubjectDigests[digestType]; !ok {
|
||||
allVSASubjectDigests[digestType] = make(map[string]bool)
|
||||
}
|
||||
allVSASubjectDigests[digestType][digestValue] = true
|
||||
}
|
||||
}
|
||||
if len(allVSASubjectDigests) == 0 {
|
||||
return fmt.Errorf("%w: no subject digests found in the VSA", serrors.ErrorInvalidDssePayload)
|
||||
}
|
||||
// search for the expected digests in the VSA
|
||||
for _, expectedDigest := range *vsaOpts.ExpectedDigests {
|
||||
parts := strings.SplitN(expectedDigest, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("%w: expected digest %s is not in the format <digest type>:<digest value>", serrors.ErrorInvalidDssePayload, expectedDigest)
|
||||
}
|
||||
digestType := parts[0]
|
||||
digestValue := parts[1]
|
||||
if _, ok := allVSASubjectDigests[digestType]; !ok {
|
||||
return fmt.Errorf("%w: expected digest not found: %s", serrors.ErrorMissingSubjectDigest, expectedDigest)
|
||||
}
|
||||
if _, ok := allVSASubjectDigests[digestType][digestValue]; !ok {
|
||||
return fmt.Errorf("%w: expected digest not found: %s", serrors.ErrorMissingSubjectDigest, expectedDigest)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchVerifierID checks if the verifier ID in the VSA matches the expected value.
|
||||
func matchVerifierID(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error {
|
||||
if vsa.Predicate.Verifier.ID == "" {
|
||||
return fmt.Errorf("%w: no verifierID found in the VSA", serrors.ErrorEmptyRequiredField)
|
||||
}
|
||||
if *vsaOpts.ExpectedVerifierID != vsa.Predicate.Verifier.ID {
|
||||
return fmt.Errorf("%w: verifier ID mismatch: wanted %s, got %s", serrors.ErrorMismatchVerifierID, *vsaOpts.ExpectedVerifierID, vsa.Predicate.Verifier.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchResourceURI checks if the resource URI in the VSA matches the expected value.
|
||||
func matchResourceURI(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error {
|
||||
if vsa.Predicate.ResourceURI == "" {
|
||||
return fmt.Errorf("%w: no resourceURI provided", serrors.ErrorEmptyRequiredField)
|
||||
}
|
||||
if *vsaOpts.ExpectedResourceURI != vsa.Predicate.ResourceURI {
|
||||
return fmt.Errorf("%w: resource URI mismatch: wanted %s, got %s", serrors.ErrorMismatchResourceURI, *vsaOpts.ExpectedResourceURI, vsa.Predicate.ResourceURI)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// confirmVerificationResult checks that the policy verification result is "PASSED".
|
||||
func confirmVerificationResult(vsa *vsa10.VSA) error {
|
||||
if vsa.Predicate.VerificationResult != "PASSED" {
|
||||
return fmt.Errorf("%w: verification result is not Passed: %s", serrors.ErrorInvalidVerificationResult, vsa.Predicate.VerificationResult)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchVerifiedLevels checks if the verified levels in the VSA match the expected values.
|
||||
func matchVerifiedLevels(vsa *vsa10.VSA, vsaOpts *options.VSAOpts) error {
|
||||
// check for SLSA track levels
|
||||
wantedSLSALevels, err := extractSLSALevels(vsaOpts.ExpectedVerifiedLevels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gotSLSALevels, err := extractSLSALevels(&vsa.Predicate.VerifiedLevels)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for track, expectedMinLSLSALevel := range wantedSLSALevels {
|
||||
if vsaLevel, exists := gotSLSALevels[track]; !exists {
|
||||
return fmt.Errorf("%w: expected SLSA level not found: %s", serrors.ErrorMismatchVerifiedLevels, track)
|
||||
} else if vsaLevel < expectedMinLSLSALevel {
|
||||
return fmt.Errorf("%w: expected SLSA level %s to be at least %d, got %d", serrors.ErrorMismatchVerifiedLevels, track, expectedMinLSLSALevel, vsaLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// check for non-SLSA track levels
|
||||
nonSLSAVSALevels := make(map[string]bool)
|
||||
for _, level := range vsa.Predicate.VerifiedLevels {
|
||||
if isSLSATRACKLevel(level) {
|
||||
continue
|
||||
}
|
||||
nonSLSAVSALevels[level] = true
|
||||
}
|
||||
for _, expectedLevel := range *vsaOpts.ExpectedVerifiedLevels {
|
||||
if isSLSATRACKLevel(expectedLevel) {
|
||||
continue
|
||||
}
|
||||
if _, ok := nonSLSAVSALevels[expectedLevel]; !ok {
|
||||
return fmt.Errorf("%w: expected verified level not found: %s", serrors.ErrorMismatchVerifiedLevels, expectedLevel)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSLSATRACKLevel checks if the level is an SLSA track level.
|
||||
// SLSA track levels are of the form SLSA_<track>_LEVEL_<level>, e.g., SLSA_BUILD_LEVEL_2.
|
||||
func isSLSATRACKLevel(level string) bool {
|
||||
return strings.HasPrefix(level, "SLSA_")
|
||||
}
|
||||
|
||||
// extractSLSALevels extracts the SLSA levels from the verified levels.
|
||||
// It returns a map of track to the highest level found, e.g.,
|
||||
// SLSA_BUILD_LEVEL_2, SLSA_SOURCE_LEVEL_3 ->
|
||||
//
|
||||
// {
|
||||
// "BUILD": 2,
|
||||
// "SOURCE": 3,
|
||||
// }
|
||||
func extractSLSALevels(trackLevels *[]string) (map[string]int, error) {
|
||||
vsaSLSATrackLadder := make(map[string]int)
|
||||
for _, trackLevel := range *trackLevels {
|
||||
if !strings.HasPrefix(trackLevel, "SLSA_") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trackLevel, "_", 4)
|
||||
if len(parts) != 4 {
|
||||
return nil, fmt.Errorf("%w: invalid SLSA level: %s", serrors.ErrorInvalidSLSALevel, trackLevel)
|
||||
}
|
||||
if parts[2] != "LEVEL" {
|
||||
return nil, fmt.Errorf("%w: invalid SLSA level: %s", serrors.ErrorInvalidSLSALevel, trackLevel)
|
||||
}
|
||||
track := parts[1]
|
||||
level, err := strconv.Atoi(parts[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid SLSA level: %s", serrors.ErrorInvalidSLSALevel, trackLevel)
|
||||
}
|
||||
if currentLevel, exists := vsaSLSATrackLadder[track]; exists {
|
||||
vsaSLSATrackLadder[track] = max(currentLevel, level)
|
||||
} else {
|
||||
vsaSLSATrackLadder[track] = level
|
||||
}
|
||||
}
|
||||
return vsaSLSATrackLadder, nil
|
||||
}
|
||||
682
verifiers/internal/vsa/verifier_test.go
Normal file
682
verifiers/internal/vsa/verifier_test.go
Normal file
@@ -0,0 +1,682 @@
|
||||
package vsa
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
intotoAttestations "github.com/in-toto/attestation/go/v1"
|
||||
intotoGolang "github.com/in-toto/in-toto-golang/in_toto"
|
||||
intotoCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
|
||||
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/options"
|
||||
vsa10 "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/vsa/v1.0"
|
||||
)
|
||||
|
||||
func Test_extractSignedVSA(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Parallel()
|
||||
|
||||
goodAttestationString := `
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"predicateType": "https://slsa.dev/verification_summary/v1",
|
||||
"predicate": {
|
||||
"timeVerified": "2024-06-12T07:24:34.351608Z",
|
||||
"verifier": {
|
||||
"id": "https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"
|
||||
},
|
||||
"verificationResult": "PASSED",
|
||||
"verifiedLevels": [
|
||||
"BCID_L1",
|
||||
"SLSA_BUILD_LEVEL_2"
|
||||
],
|
||||
"resourceUri": "gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre",
|
||||
"policy": {
|
||||
"uri": "googlefile:/google_src/files/642513192/depot/google3/production/security/bcid/software/gce_image/gke/vm_images.sw_policy.textproto"
|
||||
}
|
||||
},
|
||||
"subject": [
|
||||
{
|
||||
"name": "_",
|
||||
"digest": {
|
||||
"gce_image_id": "8970095005306000053"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
goodEnvelope := &dsse.Envelope{
|
||||
PayloadType: intotoGolang.PayloadType,
|
||||
Payload: mustEncodeAttestationString(goodAttestationString),
|
||||
Signatures: []dsse.Signature{
|
||||
{
|
||||
KeyID: "keystore://76574:prod:vsa_signing_public_key",
|
||||
Sig: "bmIy2gfnQt6oYpd0WbpQMtZcMRtmntDmyki+Be+2Z9qkboMVbi2RQAD1b5AWbBs7iAP8NZVJOI4R/4jOVYB/FA==",
|
||||
},
|
||||
},
|
||||
}
|
||||
goodVSAOpts := &options.VerificationOpts{
|
||||
PublicKey: mustPublicKeyFromBytes([]byte(`-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca
|
||||
3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg==
|
||||
-----END PUBLIC KEY-----`)),
|
||||
PublicKeyID: pointerTo("keystore://76574:prod:vsa_signing_public_key"),
|
||||
PublicKeyHashAlgo: crypto.SHA256,
|
||||
}
|
||||
goodVSA := &vsa10.VSA{
|
||||
StatementHeader: intotoGolang.StatementHeader{
|
||||
Type: intotoAttestations.StatementTypeUri,
|
||||
PredicateType: vsa10.PredicateType,
|
||||
Subject: []intotoGolang.Subject{
|
||||
{
|
||||
Name: "_",
|
||||
Digest: map[string]string{
|
||||
"gce_image_id": "8970095005306000053",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Predicate: vsa10.Predicate{
|
||||
TimeVerified: time.Date(2024, 6, 12, 7, 24, 34, 351608000, time.UTC),
|
||||
Verifier: vsa10.Verifier{
|
||||
ID: "https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1",
|
||||
},
|
||||
ResourceURI: "gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre",
|
||||
Policy: intotoCommon.ProvenanceMaterial{
|
||||
URI: "googlefile:/google_src/files/642513192/depot/google3/production/security/bcid/software/gce_image/gke/vm_images.sw_policy.textproto",
|
||||
},
|
||||
VerificationResult: "PASSED",
|
||||
VerifiedLevels: []string{"BCID_L1", "SLSA_BUILD_LEVEL_2"},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
envelope *dsse.Envelope
|
||||
opts *options.VerificationOpts
|
||||
expectedVSA *vsa10.VSA
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
envelope: goodEnvelope,
|
||||
opts: goodVSAOpts,
|
||||
expectedVSA: goodVSA,
|
||||
},
|
||||
{
|
||||
name: "success: sha256 key id in envelope",
|
||||
envelope: &dsse.Envelope{
|
||||
PayloadType: goodEnvelope.PayloadType,
|
||||
Payload: goodEnvelope.Payload,
|
||||
Signatures: []dsse.Signature{
|
||||
{
|
||||
KeyID: "SHA256:Zphi7kubaI7RnOrkqPgkRdVhF5a2JOFB4gor/Zajiiw",
|
||||
Sig: goodEnvelope.Signatures[0].Sig,
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: &options.VerificationOpts{
|
||||
PublicKey: goodVSAOpts.PublicKey,
|
||||
PublicKeyID: pointerTo(""),
|
||||
PublicKeyHashAlgo: crypto.SHA256,
|
||||
},
|
||||
expectedVSA: goodVSA,
|
||||
},
|
||||
{
|
||||
name: "success: no key ids",
|
||||
envelope: &dsse.Envelope{
|
||||
PayloadType: goodEnvelope.PayloadType,
|
||||
Payload: goodEnvelope.Payload,
|
||||
Signatures: []dsse.Signature{
|
||||
{
|
||||
KeyID: "",
|
||||
Sig: goodEnvelope.Signatures[0].Sig,
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: &options.VerificationOpts{
|
||||
PublicKey: goodVSAOpts.PublicKey,
|
||||
PublicKeyID: pointerTo(""),
|
||||
PublicKeyHashAlgo: crypto.SHA256,
|
||||
},
|
||||
expectedVSA: goodVSA,
|
||||
},
|
||||
{
|
||||
name: "success: keyid only in opts",
|
||||
envelope: &dsse.Envelope{
|
||||
PayloadType: goodEnvelope.PayloadType,
|
||||
Payload: goodEnvelope.Payload,
|
||||
Signatures: []dsse.Signature{
|
||||
{
|
||||
KeyID: "",
|
||||
Sig: goodEnvelope.Signatures[0].Sig,
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: &options.VerificationOpts{
|
||||
PublicKey: goodVSAOpts.PublicKey,
|
||||
PublicKeyID: pointerTo("SHA256:Zphi7kubaI7RnOrkqPgkRdVhF5a2JOFB4gor/Zajiiw"),
|
||||
PublicKeyHashAlgo: crypto.SHA256,
|
||||
},
|
||||
expectedVSA: goodVSA,
|
||||
},
|
||||
{
|
||||
name: "failure: empty signatures",
|
||||
envelope: &dsse.Envelope{
|
||||
PayloadType: goodEnvelope.PayloadType,
|
||||
Payload: goodEnvelope.Payload,
|
||||
Signatures: []dsse.Signature{},
|
||||
},
|
||||
opts: goodVSAOpts,
|
||||
expectedVSA: nil,
|
||||
err: dsse.ErrNoSignature,
|
||||
},
|
||||
{
|
||||
name: "failure: mismatch signature",
|
||||
envelope: &dsse.Envelope{
|
||||
PayloadType: goodEnvelope.PayloadType,
|
||||
Payload: mustEncodeAttestationString("{}"),
|
||||
Signatures: goodEnvelope.Signatures,
|
||||
},
|
||||
opts: goodVSAOpts,
|
||||
expectedVSA: nil,
|
||||
err: serrors.ErrorNoValidSignature,
|
||||
},
|
||||
{
|
||||
name: "failure: misatch keyID",
|
||||
envelope: goodEnvelope,
|
||||
opts: &options.VerificationOpts{
|
||||
PublicKey: goodVSAOpts.PublicKey,
|
||||
PublicKeyID: pointerTo("keystore://76574:prod:another_key_id"),
|
||||
PublicKeyHashAlgo: crypto.SHA256,
|
||||
},
|
||||
expectedVSA: nil,
|
||||
err: serrors.ErrorNoValidSignature,
|
||||
},
|
||||
{
|
||||
name: "failure: missing needed keyID",
|
||||
envelope: goodEnvelope,
|
||||
opts: &options.VerificationOpts{
|
||||
PublicKey: goodVSAOpts.PublicKey,
|
||||
PublicKeyID: pointerTo(""),
|
||||
PublicKeyHashAlgo: crypto.SHA256,
|
||||
},
|
||||
expectedVSA: nil,
|
||||
err: serrors.ErrorNoValidSignature,
|
||||
},
|
||||
{
|
||||
name: "failure: incorrect algorithm",
|
||||
envelope: goodEnvelope,
|
||||
opts: &options.VerificationOpts{
|
||||
PublicKey: goodVSAOpts.PublicKey,
|
||||
PublicKeyID: pointerTo(""),
|
||||
PublicKeyHashAlgo: crypto.SHA512,
|
||||
},
|
||||
expectedVSA: nil,
|
||||
err: serrors.ErrorNoValidSignature,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vsa, err := extractSignedVSA(ctx, tc.envelope, tc.opts)
|
||||
|
||||
if diff := cmp.Diff(tc.expectedVSA, vsa, cmpopts.EquateComparable()); diff != "" {
|
||||
t.Errorf("unexpected VSA (-want +got): \n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.err, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("unexpected error (-want +got): \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_matchExpectedValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
goodVSA := &vsa10.VSA{
|
||||
StatementHeader: intotoGolang.StatementHeader{
|
||||
PredicateType: vsa10.PredicateType,
|
||||
Subject: []intotoGolang.Subject{
|
||||
{
|
||||
Digest: map[string]string{
|
||||
"gce_image_id": "8970095005306000053",
|
||||
"sha256": "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Predicate: vsa10.Predicate{
|
||||
Verifier: vsa10.Verifier{
|
||||
ID: "https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1",
|
||||
},
|
||||
ResourceURI: "gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre",
|
||||
VerificationResult: "PASSED",
|
||||
VerifiedLevels: []string{"BCID_L1", "SLSA_BUILD_LEVEL_2"},
|
||||
},
|
||||
}
|
||||
goodVSAOpts := &options.VSAOpts{
|
||||
ExpectedDigests: &[]string{"gce_image_id:8970095005306000053", "sha256:abc"},
|
||||
ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.1"),
|
||||
ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-12615-gke1418000-cos-101-17162-463-29-c-cgpv1-pre"),
|
||||
ExpectedVerifiedLevels: &[]string{"BCID_L1", "SLSA_BUILD_LEVEL_2"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vsa *vsa10.VSA
|
||||
opts *options.VSAOpts
|
||||
err error
|
||||
}{
|
||||
// success cases
|
||||
{
|
||||
name: "success",
|
||||
vsa: goodVSA,
|
||||
opts: goodVSAOpts,
|
||||
},
|
||||
{
|
||||
name: "success: empty verifiedLevels",
|
||||
vsa: &vsa10.VSA{
|
||||
StatementHeader: goodVSA.StatementHeader,
|
||||
Predicate: vsa10.Predicate{
|
||||
Verifier: goodVSA.Predicate.Verifier,
|
||||
ResourceURI: goodVSA.Predicate.ResourceURI,
|
||||
VerificationResult: goodVSA.Predicate.VerificationResult,
|
||||
VerifiedLevels: []string{},
|
||||
},
|
||||
},
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: goodVSAOpts.ExpectedDigests,
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: &[]string{},
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success: unspecified verifiedLevels",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: goodVSAOpts.ExpectedDigests,
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: &[]string{},
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success: expected lower SLSA level",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: goodVSAOpts.ExpectedDigests,
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_1"},
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
},
|
||||
// failure cases
|
||||
{
|
||||
name: "expected higher SLSA level",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: goodVSAOpts.ExpectedDigests,
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_3"},
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
err: serrors.ErrorMismatchVerifiedLevels,
|
||||
},
|
||||
{
|
||||
name: "failure empty digests",
|
||||
vsa: &vsa10.VSA{
|
||||
StatementHeader: intotoGolang.StatementHeader{
|
||||
PredicateType: vsa10.PredicateType,
|
||||
Subject: []intotoGolang.Subject{
|
||||
{
|
||||
Digest: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
Predicate: goodVSA.Predicate,
|
||||
},
|
||||
opts: goodVSAOpts,
|
||||
err: serrors.ErrorInvalidDssePayload,
|
||||
},
|
||||
{
|
||||
name: "failure: no supplied digests",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: &[]string{},
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels,
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
err: serrors.ErrorEmptyRequiredField,
|
||||
},
|
||||
{
|
||||
name: "failure: missing digest",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: &[]string{"zeit:geist"},
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels,
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
err: serrors.ErrorMissingSubjectDigest,
|
||||
},
|
||||
{
|
||||
name: "failure: empty verifierID",
|
||||
vsa: &vsa10.VSA{
|
||||
StatementHeader: goodVSA.StatementHeader,
|
||||
Predicate: vsa10.Predicate{
|
||||
Verifier: vsa10.Verifier{},
|
||||
ResourceURI: goodVSA.Predicate.ResourceURI,
|
||||
VerificationResult: goodVSA.Predicate.VerificationResult,
|
||||
VerifiedLevels: goodVSA.Predicate.VerifiedLevels,
|
||||
},
|
||||
},
|
||||
opts: goodVSAOpts,
|
||||
err: serrors.ErrorEmptyRequiredField,
|
||||
},
|
||||
{
|
||||
name: "failure: mismatch verifierID",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: goodVSAOpts.ExpectedDigests,
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels,
|
||||
ExpectedVerifierID: pointerTo("https://bcid.corp.google.com/verifier/bcid_package_enforcer/v0.2"),
|
||||
},
|
||||
err: serrors.ErrorMismatchVerifierID,
|
||||
},
|
||||
{
|
||||
name: "failure: empty resourceURI",
|
||||
vsa: &vsa10.VSA{
|
||||
StatementHeader: goodVSA.StatementHeader,
|
||||
Predicate: vsa10.Predicate{
|
||||
Verifier: goodVSA.Predicate.Verifier,
|
||||
ResourceURI: "",
|
||||
VerificationResult: goodVSA.Predicate.VerificationResult,
|
||||
VerifiedLevels: goodVSA.Predicate.VerifiedLevels,
|
||||
},
|
||||
},
|
||||
opts: goodVSAOpts,
|
||||
err: serrors.ErrorEmptyRequiredField,
|
||||
},
|
||||
{
|
||||
name: "failure: mismatch resourceURI",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: goodVSAOpts.ExpectedDigests,
|
||||
ExpectedResourceURI: pointerTo("gce_image://gke-node-images:gke-126GGG"),
|
||||
ExpectedVerifiedLevels: goodVSAOpts.ExpectedVerifiedLevels,
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
err: serrors.ErrorMismatchResourceURI,
|
||||
},
|
||||
{
|
||||
name: "failure: empty verificationResult",
|
||||
vsa: &vsa10.VSA{
|
||||
StatementHeader: goodVSA.StatementHeader,
|
||||
Predicate: vsa10.Predicate{
|
||||
Verifier: goodVSA.Predicate.Verifier,
|
||||
ResourceURI: goodVSA.Predicate.ResourceURI,
|
||||
VerificationResult: "",
|
||||
VerifiedLevels: goodVSA.Predicate.VerifiedLevels,
|
||||
},
|
||||
},
|
||||
opts: goodVSAOpts,
|
||||
err: serrors.ErrorInvalidVerificationResult,
|
||||
},
|
||||
{
|
||||
name: "failure: wrong verificationResult",
|
||||
vsa: &vsa10.VSA{
|
||||
StatementHeader: goodVSA.StatementHeader,
|
||||
Predicate: vsa10.Predicate{
|
||||
Verifier: goodVSA.Predicate.Verifier,
|
||||
ResourceURI: goodVSA.Predicate.ResourceURI,
|
||||
VerificationResult: "FAILED",
|
||||
VerifiedLevels: goodVSA.Predicate.VerifiedLevels,
|
||||
},
|
||||
},
|
||||
opts: goodVSAOpts,
|
||||
err: serrors.ErrorInvalidVerificationResult,
|
||||
},
|
||||
{
|
||||
name: "failure: missing verifiedLevels",
|
||||
vsa: goodVSA,
|
||||
opts: &options.VSAOpts{
|
||||
ExpectedDigests: goodVSAOpts.ExpectedDigests,
|
||||
ExpectedResourceURI: goodVSAOpts.ExpectedResourceURI,
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_3"},
|
||||
ExpectedVerifierID: goodVSAOpts.ExpectedVerifierID,
|
||||
},
|
||||
err: serrors.ErrorMismatchVerifiedLevels,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := matchExpectedValues(tc.vsa, tc.opts)
|
||||
if diff := cmp.Diff(tc.err, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("unexpected error (-want +got): \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_matchVerifiedLevels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
vsa *vsa10.VSA
|
||||
vsaOpts *options.VSAOpts
|
||||
err error
|
||||
}{
|
||||
// success cases
|
||||
{
|
||||
name: "success: equal levels",
|
||||
vsa: &vsa10.VSA{
|
||||
Predicate: vsa10.Predicate{
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_1", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
},
|
||||
vsaOpts: &options.VSAOpts{
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_1", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success: expected lower SLSA level",
|
||||
vsa: &vsa10.VSA{
|
||||
Predicate: vsa10.Predicate{
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_1", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
},
|
||||
vsaOpts: &options.VSAOpts{
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_0", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success: unspecified verifiedLevels",
|
||||
vsa: &vsa10.VSA{
|
||||
Predicate: vsa10.Predicate{
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_1", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
},
|
||||
vsaOpts: &options.VSAOpts{
|
||||
ExpectedVerifiedLevels: &[]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success: no SLSA levels",
|
||||
vsa: &vsa10.VSA{
|
||||
Predicate: vsa10.Predicate{
|
||||
VerifiedLevels: []string{"BCID_L1"},
|
||||
},
|
||||
},
|
||||
vsaOpts: &options.VSAOpts{
|
||||
ExpectedVerifiedLevels: &[]string{},
|
||||
},
|
||||
},
|
||||
// failure cases
|
||||
{
|
||||
name: "failure: expected higher SLSA level",
|
||||
vsa: &vsa10.VSA{
|
||||
Predicate: vsa10.Predicate{
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_1", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
},
|
||||
vsaOpts: &options.VSAOpts{
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_2", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
err: serrors.ErrorMismatchVerifiedLevels,
|
||||
},
|
||||
{
|
||||
name: "failure: missing a expected SLSA track",
|
||||
vsa: &vsa10.VSA{
|
||||
Predicate: vsa10.Predicate{
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
},
|
||||
vsaOpts: &options.VSAOpts{
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_2", "SLSA_SOURCE_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
err: serrors.ErrorMismatchVerifiedLevels,
|
||||
},
|
||||
{
|
||||
name: "failure: missing a expected non-SLSA track",
|
||||
vsa: &vsa10.VSA{
|
||||
Predicate: vsa10.Predicate{
|
||||
VerifiedLevels: []string{"SLSA_BUILD_LEVEL_2"},
|
||||
},
|
||||
},
|
||||
vsaOpts: &options.VSAOpts{
|
||||
ExpectedVerifiedLevels: &[]string{"SLSA_BUILD_LEVEL_2", "BCID_L1"},
|
||||
},
|
||||
err: serrors.ErrorMismatchVerifiedLevels,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := matchVerifiedLevels(tc.vsa, tc.vsaOpts)
|
||||
|
||||
if diff := cmp.Diff(tc.err, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("unexpected error (-want +got): \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_extractSLSALevels(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
levels *[]string
|
||||
want map[string]int
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
levels: &[]string{
|
||||
"SLSA_BUILD_LEVEL_1",
|
||||
"SLSA_SOURCE_LEVEL_2",
|
||||
},
|
||||
want: map[string]int{
|
||||
"BUILD": 1,
|
||||
"SOURCE": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success: empty",
|
||||
levels: &[]string{},
|
||||
want: map[string]int{},
|
||||
},
|
||||
{
|
||||
name: "failure: invalid level number",
|
||||
levels: &[]string{
|
||||
"SLSA_BUILD_LEVEL_X",
|
||||
},
|
||||
err: serrors.ErrorInvalidSLSALevel,
|
||||
},
|
||||
{
|
||||
name: "failure: invalid level text",
|
||||
levels: &[]string{
|
||||
"SLSA_BUILD_L_1",
|
||||
},
|
||||
err: serrors.ErrorInvalidSLSALevel,
|
||||
},
|
||||
{
|
||||
name: "failure: no level number",
|
||||
levels: &[]string{
|
||||
"SLSA_BUILD_LEVEL_",
|
||||
},
|
||||
err: serrors.ErrorInvalidSLSALevel,
|
||||
},
|
||||
{
|
||||
name: "failure: no last underscore",
|
||||
levels: &[]string{
|
||||
"SLSA_BUILD_LEVEL",
|
||||
},
|
||||
err: serrors.ErrorInvalidSLSALevel,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := extractSLSALevels(tc.levels)
|
||||
|
||||
if diff := cmp.Diff(tc.want, got, cmpopts.EquateComparable()); diff != "" {
|
||||
t.Errorf("unexpected VSA (-want +got): \n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.err, err, cmpopts.EquateErrors()); diff != "" {
|
||||
t.Errorf("unexpected error (-want +got): \n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustEncodeAttestationString(attestationString string) string {
|
||||
dst := &bytes.Buffer{}
|
||||
if err := json.Compact(dst, []byte(attestationString)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(dst.Bytes())
|
||||
}
|
||||
|
||||
func mustPublicKeyFromBytes(pubKeyBytes []byte) crypto.PublicKey {
|
||||
pubKey, err := cryptoutils.UnmarshalPEMToPublicKey(pubKeyBytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pubKey
|
||||
}
|
||||
|
||||
func pointerTo[K any](object K) *K {
|
||||
return &object
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
intotoAttestations "github.com/in-toto/attestation/go/v1"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
|
||||
@@ -42,13 +43,17 @@ func PayloadFromEnvelope(env *dsselib.Envelope) ([]byte, error) {
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// StatementFromBytes parses the provided byte slice as a JSON payload and returns an intoto.Statement.
|
||||
// Ideally, we use the "V1" Statement in https://pkg.go.dev/github.com/in-toto/attestation/go/v1#pkg-constants,
|
||||
// but it parses json fields in snake case, while the official spec uses camel case
|
||||
// https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/statement.md.
|
||||
func StatementFromBytes(payload []byte) (*intoto.Statement, error) {
|
||||
var statement intoto.Statement
|
||||
if err := json.Unmarshal(payload, &statement); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", serrors.ErrorInvalidDssePayload, err)
|
||||
}
|
||||
|
||||
if statement.Type != intoto.StatementInTotoV01 {
|
||||
if statement.Type != intoto.StatementInTotoV01 && statement.Type != intotoAttestations.StatementTypeUri {
|
||||
return nil, fmt.Errorf("%w: invalid statement type: %q", serrors.ErrorInvalidDssePayload, statement.Type)
|
||||
}
|
||||
return &statement, nil
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/slsa-framework/slsa-verifier/v2/register"
|
||||
_ "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gcb"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/vsa"
|
||||
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
|
||||
)
|
||||
|
||||
@@ -74,3 +75,14 @@ func VerifyNpmPackage(ctx context.Context,
|
||||
return verifier.VerifyNpmPackage(ctx, attestations, tarballHash,
|
||||
provenanceOpts, builderOpts)
|
||||
}
|
||||
|
||||
// VerifyVSA verifies the VSA attestation. It returns the attestation base64-decoded from the envelope.
|
||||
// We don't return a TrustedBuilderID. Instead, the user can user can parse the builderID separately, perhaps with
|
||||
// https://pkg.go.dev/golang.org/x/mod/semver
|
||||
func VerifyVSA(ctx context.Context,
|
||||
attestation []byte,
|
||||
vsaOpts *options.VSAOpts,
|
||||
verificationOpts *options.VerificationOpts,
|
||||
) ([]byte, error) {
|
||||
return vsa.VerifyVSA(ctx, attestation, vsaOpts, verificationOpts)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user