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:
Ramon Petgrave
2024-07-10 21:25:16 -04:00
committed by GitHub
parent 1049da4841
commit 208ac12589
17 changed files with 1492 additions and 85 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View 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"}]}

View File

@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeGa6ZCZn0q6WpaUwJrSk+PPYEsca
3Xkk3UrxvbQtoZzTmq0zIYq+4QQl0YBedSyy+XcwAMaUWTouTrB05WhYtg==
-----END PUBLIC KEY-----

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View 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
}

View 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
}

View 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
}

View File

@@ -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

View File

@@ -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)
}