diff --git a/cmd/validate/image.go b/cmd/validate/image.go index c0d0105bb..6bbe7839c 100644 --- a/cmd/validate/image.go +++ b/cmd/validate/image.go @@ -296,6 +296,11 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { } } + // Require --vsa-public-key when the VSA skip path is active + if len(data.vsaUpload) > 0 && data.vsaPublicKey == "" && data.vsaExpiration > 0 { + allErrors = errors.Join(allErrors, fmt.Errorf("--vsa-public-key required when --vsa-upload is set with --vsa-expiration > 0")) + } + return }, @@ -367,9 +372,20 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { var out *output.Output var err error if data.vsaExpiration > 0 { - vsaChecker := vsa.CreateVSACheckerFromUploadFlags(data.vsaUpload) - if vsaChecker != nil { - out, err = image.ValidateImageWithVSACheck(ctx, comp, data.spec, data.policy, evaluators, data.info, vsaChecker, data.vsaExpiration) + retriever := vsa.CreateRetrieverFromUploadFlags(data.vsaUpload) + if retriever != nil { + vsaEffectiveTime := data.effectiveTime + if vsaEffectiveTime == "attestation" { + vsaEffectiveTime = policy.Now + } + vsaConfig := &vsa.VSAValidationConfig{ + Retriever: retriever, + VSAExpiration: data.vsaExpiration, + PublicKeyPath: data.vsaPublicKey, + PolicySpec: data.policy.Spec(), + EffectiveTime: vsaEffectiveTime, + } + out, err = image.ValidateImageWithVSACheck(ctx, comp, data.spec, data.policy, evaluators, data.info, vsaConfig) } else { // Fall back to normal validation if no VSA retriever is available out, err = validate(ctx, comp, data.spec, data.policy, evaluators, data.info) @@ -583,6 +599,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { cmd.Flags().BoolVar(&data.vsaEnabled, "vsa", false, "Generate a Verification Summary Attestation (VSA) for each validated image.") cmd.Flags().StringVar(&data.attestationFormat, "attestation-format", "dsse", "Attestation output format: dsse (signed envelope), predicate (raw JSON)") cmd.Flags().StringVar(&data.vsaSigningKey, "vsa-signing-key", "", "Path to the private key for signing the VSA. Supports file paths and Kubernetes secret references (k8s://namespace/secret-name/key-field).") + cmd.Flags().StringVar(&data.vsaPublicKey, "vsa-public-key", "", "Path to the public key for VSA signature verification. Required when --vsa-upload is set and --vsa-expiration is greater than 0.") cmd.Flags().StringSliceVar(&data.vsaUpload, "vsa-upload", nil, "Storage backends for VSA upload. Format: backend@url?param=value. Examples: rekor@https://rekor.sigstore.dev, local@./vsa-dir") cmd.Flags().DurationVar(&data.vsaExpiration, "vsa-expiration", data.vsaExpiration, "Expiration threshold for existing VSAs. If a valid VSA exists and is newer than this threshold, validation will be skipped. (default 168h)") cmd.Flags().StringVar(&data.attestationOutputDir, "attestation-output-dir", "", "Directory for attestation output files. Defaults to a temp directory under /tmp. Must be under /tmp or the current working directory.") @@ -667,6 +684,7 @@ type imageData struct { vsaEnabled bool attestationFormat string vsaSigningKey string + vsaPublicKey string vsaUpload []string vsaExpiration time.Duration attestationOutputDir string diff --git a/cmd/validate/image_test.go b/cmd/validate/image_test.go index 697d81c0d..e6d2fa165 100644 --- a/cmd/validate/image_test.go +++ b/cmd/validate/image_test.go @@ -1471,6 +1471,7 @@ func TestValidateImageCommand_VSAUpload_Success(t *testing.T) { "--vsa", "--vsa-signing-key", "/tmp/vsa-key.pem", "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -1543,6 +1544,39 @@ func TestValidateImageCommand_VSAUpload_NoStorageBackends(t *testing.T) { // Don't assert no error since VSA processing might fail, but upload logic should be reached } +func TestValidateImageCommand_VSAPublicKeyRequired(t *testing.T) { + // --vsa-public-key is required when --vsa-upload is set + validateImageCmd := validateImageCmd(happyValidator()) + cmd := setUpCobra(validateImageCmd) + + fs := afero.NewMemMapFs() + ctx := utils.WithFS(context.Background(), fs) + + client := fake.FakeClient{} + commonMockClient(&client) + ctx = oci.WithClient(ctx, &client) + cmd.SetContext(ctx) + + cmd.SetArgs([]string{ + "validate", "image", + "--image", "registry/image:tag", + "--policy", fmt.Sprintf(`{"publicKey": %s}`, utils.TestPublicKeyJSON), + "--vsa-upload", "local@/tmp/vsa-test", + // Missing --vsa-public-key + }) + + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + utils.SetTestRekorPublicKey(t) + + err := cmd.Execute() + assert.Error(t, err) + assert.Contains(t, err.Error(), "--vsa-public-key required when --vsa-upload is set with --vsa-expiration > 0") +} + func TestValidateImageCommand_ShowWarningsFlag(t *testing.T) { // Create a validator that returns warnings warningValidator := func(_ context.Context, component app.SnapshotComponent, _ *app.SnapshotSpec, _ policy.Policy, _ []evaluator.Evaluator, _ bool) (*output.Output, error) { @@ -1676,6 +1710,7 @@ func TestValidateImageCommand_VSAFormat_DSSE(t *testing.T) { "--attestation-format", "dsse", "--vsa-signing-key", "/tmp/vsa-key.pem", "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -1712,6 +1747,7 @@ func TestValidateImageCommand_VSAFormat_Predicate(t *testing.T) { "--vsa", "--attestation-format", "predicate", "--vsa-upload", "local@/tmp/vsa-predicates", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -1747,6 +1783,7 @@ func TestValidateImageCommand_VSAFormat_InvalidFormat(t *testing.T) { "--attestation-format", "invalid-format", "--vsa-signing-key", "/tmp/vsa-key.pem", "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -1784,6 +1821,7 @@ func TestValidateImageCommand_VSAFormat_DSSE_RequiresSigningKey(t *testing.T) { "--attestation-format", "dsse", // Missing --vsa-signing-key "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -1822,6 +1860,7 @@ func TestValidateImageCommand_VSAFormat_Predicate_WorksWithoutSigningKey(t *test "--attestation-format", "predicate", // No --vsa-signing-key provided "--vsa-upload", "local@/tmp/vsa-predicates", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -1940,6 +1979,7 @@ func TestGenerateVSAsDSSE_Errors(t *testing.T) { "--attestation-format", "dsse", "--vsa-signing-key", "/tmp/invalid-key.pem", "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -1974,6 +2014,7 @@ func TestGenerateVSAsDSSE_Errors(t *testing.T) { "--attestation-format", "dsse", "--vsa-signing-key", "/tmp/nonexistent-key.pem", "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -2025,6 +2066,7 @@ func TestGenerateVSAsDSSE_Errors(t *testing.T) { "--attestation-format", "dsse", "--vsa-signing-key", "/tmp/vsa-key.pem", "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -2060,6 +2102,7 @@ func TestGenerateVSAsPredicates_Errors(t *testing.T) { "--attestation-format", "predicate", "--attestation-output-dir", "/etc/invalid-dir", // Invalid directory outside /tmp and cwd "--vsa-upload", "local@/tmp/vsa-predicates", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -2096,6 +2139,7 @@ func TestGenerateVSAsPredicates_Errors(t *testing.T) { "--attestation-format", "predicate", "--attestation-output-dir", "/tmp/vsa-predicates", "--vsa-upload", "local@/tmp/vsa-predicates", + "--vsa-public-key", "/tmp/vsa-pub.pem", }) var out bytes.Buffer @@ -2175,6 +2219,7 @@ func TestVSAGeneration_WithOutputDir(t *testing.T) { "--attestation-format", tt.format, "--attestation-output-dir", tt.outputDir, "--vsa-upload", "local@/tmp/vsa-test", + "--vsa-public-key", "/tmp/vsa-pub.pem", } if tt.needsKey { diff --git a/docs/modules/ROOT/pages/ec_validate_image.adoc b/docs/modules/ROOT/pages/ec_validate_image.adoc index c50a2d524..bbb6c1ce3 100644 --- a/docs/modules/ROOT/pages/ec_validate_image.adoc +++ b/docs/modules/ROOT/pages/ec_validate_image.adoc @@ -162,6 +162,7 @@ JSON of the "spec" or a reference to a Kubernetes object [/] -s, --strict:: Return non-zero status on non-successful validation. Defaults to true. Use --strict=false to return a zero status code. (Default: true) --vsa:: Generate a Verification Summary Attestation (VSA) for each validated image. (Default: false) --vsa-expiration:: Expiration threshold for existing VSAs. If a valid VSA exists and is newer than this threshold, validation will be skipped. (default 168h) (Default: 168h0m0s) +--vsa-public-key:: Path to the public key for VSA signature verification. Required when --vsa-upload is set and --vsa-expiration is greater than 0. --vsa-signing-key:: Path to the private key for signing the VSA. Supports file paths and Kubernetes secret references (k8s://namespace/secret-name/key-field). --vsa-upload:: Storage backends for VSA upload. Format: backend@url?param=value. Examples: rekor@https://rekor.sigstore.dev, local@./vsa-dir (Default: []) --workers:: Number of workers to use for validation. Defaults to 5. (Default: 5) diff --git a/features/__snapshots__/validate_image.snap b/features/__snapshots__/validate_image.snap index 31c064735..b42c0b729 100644 --- a/features/__snapshots__/validate_image.snap +++ b/features/__snapshots__/validate_image.snap @@ -3691,1158 +3691,6 @@ Error: success criteria not met --- -[TestFeatures/many components and sources:stdout - 1] -{ - "success": true, - "snapshot": "acceptance/multitude", - "components": [ - { - "name": "component9", - "containerImage": "${REGISTRY}/multitude/image-9@sha256:${REGISTRY_multitude/image-9:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-9}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-9}" - } - ] - } - ] - }, - { - "name": "component8", - "containerImage": "${REGISTRY}/multitude/image-8@sha256:${REGISTRY_multitude/image-8:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-8}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-8}" - } - ] - } - ] - }, - { - "name": "component7", - "containerImage": "${REGISTRY}/multitude/image-7@sha256:${REGISTRY_multitude/image-7:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-7}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-7}" - } - ] - } - ] - }, - { - "name": "component6", - "containerImage": "${REGISTRY}/multitude/image-6@sha256:${REGISTRY_multitude/image-6:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-6}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-6}" - } - ] - } - ] - }, - { - "name": "component5", - "containerImage": "${REGISTRY}/multitude/image-5@sha256:${REGISTRY_multitude/image-5:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-5}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-5}" - } - ] - } - ] - }, - { - "name": "component4", - "containerImage": "${REGISTRY}/multitude/image-4@sha256:${REGISTRY_multitude/image-4:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-4}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-4}" - } - ] - } - ] - }, - { - "name": "component3", - "containerImage": "${REGISTRY}/multitude/image-3@sha256:${REGISTRY_multitude/image-3:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-3}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-3}" - } - ] - } - ] - }, - { - "name": "component2", - "containerImage": "${REGISTRY}/multitude/image-2@sha256:${REGISTRY_multitude/image-2:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-2}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-2}" - } - ] - } - ] - }, - { - "name": "component1", - "containerImage": "${REGISTRY}/multitude/image-1@sha256:${REGISTRY_multitude/image-1:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-1}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-1}" - } - ] - } - ] - }, - { - "name": "component0", - "containerImage": "${REGISTRY}/multitude/image-0@sha256:${REGISTRY_multitude/image-0:latest_DIGEST}", - "source": {}, - "successes": [ - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.attestation.syntax_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "builtin.image.signature_check" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - }, - { - "msg": "Pass", - "metadata": { - "code": "main.acceptor" - } - } - ], - "success": true, - "signatures": [ - { - "keyid": "", - "sig": "${IMAGE_SIGNATURE_multitude/image-0}" - } - ], - "attestations": [ - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://slsa.dev/provenance/v0.2", - "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", - "signatures": [ - { - "keyid": "", - "sig": "${ATTESTATION_SIGNATURE_multitude/image-0}" - } - ] - } - ] - } - ], - "key": "${known_PUBLIC_KEY_JSON}", - "policy": { - "sources": [ - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "key": "value" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "something": "here" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "key": "different" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "hello": "world" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "foo": "bar" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "peek": "poke" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "hide": "seek" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "hokus": "pokus" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "mr": "mxyzptlk" - } - }, - { - "policy": [ - "git::${GITHOST}/git/multitude-policy.git?ref=${LATEST_COMMIT}" - ], - "ruleData": { - "more": "data" - } - } - ], - "rekorUrl": "${REKOR}", - "publicKey": "${known_PUBLIC_KEY}" - }, - "ec-version": "${EC_VERSION}", - "effective-time": "${TIMESTAMP}" -} ---- - -[TestFeatures/many components and sources:stderr - 1] - ---- - [TestFeatures/Format options:stdout - 1] Success: false Result: FAILURE diff --git a/features/__snapshots__/vsa.snap b/features/__snapshots__/vsa.snap index f12606622..e8820aee3 100644 --- a/features/__snapshots__/vsa.snap +++ b/features/__snapshots__/vsa.snap @@ -47,7 +47,7 @@ --- [TestFeatures/VSA expiration flag functionality:stderr - 1] -time="${TIMESTAMP}" level=warning msg="Failed to check for existing VSA for image ${REGISTRY}/acceptance/vsa-expiration-image@sha256:${REGISTRY_acceptance/vsa-expiration-image:latest_DIGEST}: failed to retrieve VSA envelope: no entries found in Rekor for image digest: sha256:${REGISTRY_acceptance/vsa-expiration-image:latest_DIGEST}" +time="${TIMESTAMP}" level=warning msg="Failed to validate existing VSA for image ${REGISTRY}/acceptance/vsa-expiration-image@sha256:${REGISTRY_acceptance/vsa-expiration-image:latest_DIGEST}: failed to check existing VSA: failed to retrieve VSA envelope: no entries found in Rekor for image digest: sha256:${REGISTRY_acceptance/vsa-expiration-image:latest_DIGEST}" --- diff --git a/features/vsa.feature b/features/vsa.feature index 27df465bd..2f17d69f7 100644 --- a/features/vsa.feature +++ b/features/vsa.feature @@ -27,7 +27,7 @@ Feature: VSA generation and storage ] } """ - When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-test-image --policy acceptance/vsa-ec-policy --public-key ${vsa-test_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-test_PRIVATE_KEY} --vsa-upload local@${TMPDIR}/vsa-output --vsa-expiration 0 --output json" + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-test-image --policy acceptance/vsa-ec-policy --public-key ${vsa-test_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-test_PRIVATE_KEY} --vsa-upload local@${TMPDIR}/vsa-output --vsa-public-key ${vsa-test_PUBLIC_KEY} --vsa-expiration 0 --output json" Then the exit status should be 0 Then the output should match the snapshot And VSA envelope files should exist in "${TMPDIR}/vsa-output" @@ -52,7 +52,7 @@ Feature: VSA generation and storage } """ Given VSA upload to Rekor should be expected - When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-rekor-image --policy acceptance/vsa-rekor-ec-policy --public-key ${vsa-rekor_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-rekor_PRIVATE_KEY} --vsa-upload rekor@${REKOR} --vsa-expiration 0 --output json" + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-rekor-image --policy acceptance/vsa-rekor-ec-policy --public-key ${vsa-rekor_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-rekor_PRIVATE_KEY} --vsa-upload rekor@${REKOR} --vsa-public-key ${vsa-rekor_PUBLIC_KEY} --vsa-expiration 0 --output json" Then the exit status should be 0 Then the output should match the snapshot And VSA should be uploaded to Rekor successfully @@ -77,7 +77,7 @@ Feature: VSA generation and storage } """ Given VSA upload to Rekor should be expected - When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-multi-image --policy acceptance/vsa-multi-ec-policy --public-key ${vsa-multi_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-multi_PRIVATE_KEY} --vsa-upload local@${TMPDIR}/vsa-multi-output --vsa-upload rekor@${REKOR} --vsa-expiration 0 --output json" + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-multi-image --policy acceptance/vsa-multi-ec-policy --public-key ${vsa-multi_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-multi_PRIVATE_KEY} --vsa-upload local@${TMPDIR}/vsa-multi-output --vsa-upload rekor@${REKOR} --vsa-public-key ${vsa-multi_PUBLIC_KEY} --vsa-expiration 0 --output json" Then the exit status should be 0 Then the output should match the snapshot And VSA envelope files should exist in "${TMPDIR}/vsa-multi-output" @@ -102,7 +102,7 @@ Feature: VSA generation and storage ] } """ - When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-invalid-image --policy acceptance/vsa-invalid-ec-policy --public-key ${vsa-invalid_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-invalid_PRIVATE_KEY} --vsa-upload invalid-backend@somewhere --vsa-expiration 0 --output json" + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-invalid-image --policy acceptance/vsa-invalid-ec-policy --public-key ${vsa-invalid_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-invalid_PRIVATE_KEY} --vsa-upload invalid-backend@somewhere --vsa-public-key ${vsa-invalid_PUBLIC_KEY} --vsa-expiration 0 --output json" Then the exit status should be 0 Then the output should match the snapshot @@ -126,7 +126,7 @@ Feature: VSA generation and storage ] } """ - When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-expiration-image@sha256:${REGISTRY_acceptance/vsa-expiration-image:latest_DIGEST} --policy acceptance/vsa-expiration-ec-policy --public-key ${vsa-expiration_PUBLIC_KEY} --rekor-url ${REKOR} --vsa-expiration 1h --vsa-upload rekor@${REKOR} --output json" + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-expiration-image@sha256:${REGISTRY_acceptance/vsa-expiration-image:latest_DIGEST} --policy acceptance/vsa-expiration-ec-policy --public-key ${vsa-expiration_PUBLIC_KEY} --rekor-url ${REKOR} --vsa-expiration 1h --vsa-upload rekor@${REKOR} --vsa-public-key ${vsa-expiration_PUBLIC_KEY} --output json" Then the exit status should be 0 Then the output should match the snapshot @@ -153,7 +153,7 @@ Feature: VSA generation and storage Given VSA upload to Rekor should be expected # First, generate a VSA and upload it to Rekor Given VSA upload to Rekor should be expected - When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-existing-image@sha256:${REGISTRY_acceptance/vsa-existing-image:latest_DIGEST} --policy acceptance/vsa-existing-ec-policy --public-key ${vsa-existing_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-existing_PRIVATE_KEY} --vsa-upload rekor@${REKOR} --vsa-expiration 0 --output json" + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-existing-image@sha256:${REGISTRY_acceptance/vsa-existing-image:latest_DIGEST} --policy acceptance/vsa-existing-ec-policy --public-key ${vsa-existing_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-existing_PRIVATE_KEY} --vsa-upload rekor@${REKOR} --vsa-public-key ${vsa-existing_PUBLIC_KEY} --vsa-expiration 0 --output json" Then the exit status should be 0 And VSA should be uploaded to Rekor successfully @@ -206,6 +206,29 @@ Feature: VSA generation and storage } """ Given Rekor upload should fail - When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-upload-fail-image --policy acceptance/vsa-upload-fail-ec-policy --public-key ${vsa-upload-fail_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-upload-fail_PRIVATE_KEY} --vsa-upload rekor@${REKOR} --vsa-expiration 0 --output json" + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-upload-fail-image --policy acceptance/vsa-upload-fail-ec-policy --public-key ${vsa-upload-fail_PUBLIC_KEY} --rekor-url ${REKOR} --vsa --vsa-signing-key ${vsa-upload-fail_PRIVATE_KEY} --vsa-upload rekor@${REKOR} --vsa-public-key ${vsa-upload-fail_PUBLIC_KEY} --vsa-expiration 0 --output json" Then the exit status should be 0 And the log output should contain "[VSA] Failed to upload in-toto 0.0.2 entry" + + Scenario: Missing vsa-public-key with vsa-upload errors + Given a key pair named "vsa-pubkey" + Given an image named "acceptance/vsa-pubkey-image" + Given a valid image signature of "acceptance/vsa-pubkey-image" image signed by the "vsa-pubkey" key + Given a valid attestation of "acceptance/vsa-pubkey-image" signed by the "vsa-pubkey" key + Given a git repository named "vsa-pubkey-policy" with + | main.rego | examples/happy_day.rego | + Given policy configuration named "vsa-pubkey-ec-policy" with specification + """ + { + "sources": [ + { + "policy": [ + "git::https://${GITHOST}/git/vsa-pubkey-policy.git" + ] + } + ] + } + """ + When ec command is run with "validate image --image ${REGISTRY}/acceptance/vsa-pubkey-image --policy acceptance/vsa-pubkey-ec-policy --public-key ${vsa-pubkey_PUBLIC_KEY} --rekor-url ${REKOR} --vsa-upload local@${TMPDIR}/vsa-pubkey-output --output json" + Then the exit status should be 1 + And the log output should contain "--vsa-public-key required when --vsa-upload is set with --vsa-expiration > 0" diff --git a/internal/image/validate.go b/internal/image/validate.go index bce4747f7..c91ffbd9a 100644 --- a/internal/image/validate.go +++ b/internal/image/validate.go @@ -170,33 +170,35 @@ func ValidateImage(ctx context.Context, comp app.SnapshotComponent, snap *app.Sn return out, nil } -// ValidateImageWithVSACheck executes validation with VSA expiration checking. -// If a valid, unexpired VSA exists, validation is skipped. -func ValidateImageWithVSACheck(ctx context.Context, comp app.SnapshotComponent, snap *app.SnapshotSpec, p policy.Policy, evaluators []evaluator.Evaluator, detailed bool, vsaChecker *vsa.VSAChecker, vsaExpiration time.Duration) (*output.Output, error) { +// ValidateImageWithVSACheck executes validation with full VSA verification. +// If a valid VSA exists (signature verified, predicate passed, policy equivalent), +// validation is skipped. Otherwise, full image validation proceeds. +func ValidateImageWithVSACheck(ctx context.Context, comp app.SnapshotComponent, snap *app.SnapshotSpec, p policy.Policy, evaluators []evaluator.Evaluator, detailed bool, vsaConfig *vsa.VSAValidationConfig) (*output.Output, error) { if trace.IsEnabled() { region := trace.StartRegion(ctx, "ec:validate-image-with-vsa-check") defer region.End() - trace.Logf(ctx, "", "image=%q vsa-expiration=%v", comp.ContainerImage, vsaExpiration) + trace.Logf(ctx, "", "image=%q vsa-expiration=%v", comp.ContainerImage, vsaConfig.VSAExpiration) } - // Check for existing valid VSA - isValid, err := vsaChecker.IsValidVSA(ctx, comp.ContainerImage, vsaExpiration) + // Run full VSA validation: signature, predicate status, policy equivalence + result, err := vsa.ValidateVSAAndComparePolicy(ctx, comp.ContainerImage, vsaConfig) if err != nil { - log.Warnf("Failed to check for existing VSA for image %s: %v", comp.ContainerImage, err) - // Continue with validation on VSA lookup failure - } else if isValid { + log.Warnf("Failed to validate existing VSA for image %s: %v", comp.ContainerImage, err) + // Continue with full validation on VSA check failure + } else if result.Passed { log.WithFields(log.Fields{ - "image": comp.ContainerImage, - "expiration_threshold": vsaExpiration, + "image": comp.ContainerImage, + "signature_verified": result.SignatureVerified, + "predicate_outcome": result.PredicateOutcome, }).Info("Valid VSA found, skipping validation") // Return nil to indicate validation was skipped due to valid VSA return nil, nil } else { - log.Debugf("No valid VSA found for image %s, proceeding with validation", comp.ContainerImage) + log.Debugf("VSA validation did not pass for image %s: %s", comp.ContainerImage, result.Message) } - // Perform normal validation, if no valid VSA is found + // Perform normal validation when no valid VSA is found log.Debugf("Performing full validation for image %s", comp.ContainerImage) return ValidateImage(ctx, comp, snap, p, evaluators, detailed) } diff --git a/internal/image/validate_test.go b/internal/image/validate_test.go index e6df85841..d3081f630 100644 --- a/internal/image/validate_test.go +++ b/internal/image/validate_test.go @@ -28,6 +28,7 @@ import ( "testing" "time" + ecapi "github.com/conforma/crds/api/v1alpha1" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" @@ -426,48 +427,82 @@ func (e *mockEvaluatorWithCapture) CapabilitiesPath() string { return "" } -// createMockVSAChecker creates a mock VSA checker for testing -func createMockVSAChecker() *vsa.VSAChecker { - // Create a mock retriever that always returns "not found" - mockRetriever := &mockVSARetriever{} - return vsa.NewVSAChecker(mockRetriever) -} - // mockVSARetriever is a mock implementation of VSARetriever for testing -type mockVSARetriever struct{} +type mockVSARetriever struct { + envelope *ssldsse.Envelope + err error +} func (m *mockVSARetriever) RetrieveVSA(ctx context.Context, imageDigest string) (*ssldsse.Envelope, error) { - return nil, fmt.Errorf("no VSA found") + return m.envelope, m.err +} + +// createMockVSAConfig creates a VSAValidationConfig with a retriever that returns an error (no VSA found) +func createMockVSAConfig(expiration time.Duration) *vsa.VSAValidationConfig { + return &vsa.VSAValidationConfig{ + Retriever: &mockVSARetriever{err: fmt.Errorf("no VSA found")}, + VSAExpiration: expiration, + IgnoreSignatureVerification: true, + EffectiveTime: "now", + } +} + +// createPassingVSAEnvelope creates a DSSE envelope containing a VSA predicate with "passed" status +// and a recent timestamp so it won't be expired. +func createPassingVSAEnvelope() *ssldsse.Envelope { + payload := `{"_type":"https://in-toto.io/Statement/v0.1","subject":[{"name":"test-image","digest":{"sha256":"abc123"}}],"predicateType":"https://conforma.dev/verification_summary/v1","predicate":{"policy":{"sources":[{"name":"test-source","policy":["test-policy"]}]},"timestamp":"` + time.Now().Add(-1*time.Hour).Format(time.RFC3339) + `","status":"passed"}}` + return &ssldsse.Envelope{ + PayloadType: "application/vnd.in-toto+json", + Payload: base64.StdEncoding.EncodeToString([]byte(payload)), + Signatures: []ssldsse.Signature{{KeyID: "test", Sig: "test"}}, + } +} + +// createFailedVSAEnvelope creates a DSSE envelope containing a VSA predicate with "failed" status. +func createFailedVSAEnvelope() *ssldsse.Envelope { + payload := `{"_type":"https://in-toto.io/Statement/v0.1","subject":[{"name":"test-image","digest":{"sha256":"abc123"}}],"predicateType":"https://conforma.dev/verification_summary/v1","predicate":{"policy":{"sources":[{"name":"test-source","policy":["test-policy"]}]},"timestamp":"` + time.Now().Add(-1*time.Hour).Format(time.RFC3339) + `","status":"failed"}}` + return &ssldsse.Envelope{ + PayloadType: "application/vnd.in-toto+json", + Payload: base64.StdEncoding.EncodeToString([]byte(payload)), + Signatures: []ssldsse.Signature{{KeyID: "test", Sig: "test"}}, + } } func TestValidateImageWithVSACheck(t *testing.T) { tests := []struct { - name string - vsaExpiration time.Duration - vsaChecker *vsa.VSAChecker - expectVSACheck bool - expectSkip bool + name string + vsaConfig *vsa.VSAValidationConfig + expectSkip bool }{ { - name: "VSA checking disabled - zero expiration", - vsaExpiration: 0, - vsaChecker: createMockVSAChecker(), - expectVSACheck: false, - expectSkip: false, + name: "no VSA found - falls back to full validation", + vsaConfig: createMockVSAConfig(24 * time.Hour), + expectSkip: false, }, { - name: "VSA checking disabled - no checker", - vsaExpiration: 24 * time.Hour, - vsaChecker: createMockVSAChecker(), - expectVSACheck: false, - expectSkip: false, + name: "VSA passed - skip validation", + vsaConfig: &vsa.VSAValidationConfig{ + Retriever: &mockVSARetriever{envelope: createPassingVSAEnvelope()}, + VSAExpiration: 24 * time.Hour, + IgnoreSignatureVerification: true, + EffectiveTime: "now", + PolicySpec: ecapi.EnterpriseContractPolicySpec{ + Sources: []ecapi.Source{ + {Name: "test-source", Policy: []string{"test-policy"}}, + }, + }, + }, + expectSkip: true, }, { - name: "VSA checking enabled with checker", - vsaExpiration: 24 * time.Hour, - vsaChecker: createMockVSAChecker(), - expectVSACheck: true, - expectSkip: false, // Placeholder implementation returns "not found" + name: "VSA predicate failed - falls back to full validation", + vsaConfig: &vsa.VSAValidationConfig{ + Retriever: &mockVSARetriever{envelope: createFailedVSAEnvelope()}, + VSAExpiration: 24 * time.Hour, + IgnoreSignatureVerification: true, + EffectiveTime: "now", + }, + expectSkip: false, }, } @@ -476,56 +511,46 @@ func TestValidateImageWithVSACheck(t *testing.T) { fs := afero.NewMemMapFs() ctx := utils.WithFS(context.Background(), fs) - // Create a proper policy interface p, err := policy.NewOfflinePolicy(ctx, policy.Now) require.NoError(t, err) - // Create a test component comp := app.SnapshotComponent{ ContainerImage: "registry.example.com/test:latest", } - - // Create a mock snapshot spec snap := &app.SnapshotSpec{} - - // Create empty evaluators slice evaluators := []evaluator.Evaluator{} - // Call the function - it should work with basic setup - // The function handles VSA checking gracefully when image reference is a tag - _, err = ValidateImageWithVSACheck(ctx, comp, snap, p, evaluators, false, tt.vsaChecker, tt.vsaExpiration) + out, err := ValidateImageWithVSACheck(ctx, comp, snap, p, evaluators, false, tt.vsaConfig) - // The function should succeed even with minimal setup - // VSA checking will be skipped due to tag reference (not digest-based) - assert.NoError(t, err) + if tt.expectSkip { + // VSA validation passed, output is nil (skip) + assert.NoError(t, err) + assert.Nil(t, out) + } else { + // VSA validation failed or errored, falls back to full validation + assert.NoError(t, err) + } }) } } func TestValidateImageWithVSACheck_FlagCombinations(t *testing.T) { tests := []struct { - name string - vsaExpiration time.Duration - vsaChecker *vsa.VSAChecker - expectVSACheck bool + name string + vsaConfig *vsa.VSAValidationConfig }{ { - name: "VSA checking disabled - zero expiration", - vsaExpiration: 0, - vsaChecker: createMockVSAChecker(), - expectVSACheck: false, - }, - { - name: "VSA checking disabled - no checker", - vsaExpiration: 24 * time.Hour, - vsaChecker: createMockVSAChecker(), - expectVSACheck: false, + name: "retriever returns error - graceful fallback", + vsaConfig: createMockVSAConfig(24 * time.Hour), }, { - name: "VSA checking enabled with checker", - vsaExpiration: 24 * time.Hour, - vsaChecker: createMockVSAChecker(), - expectVSACheck: true, + name: "retriever returns nil envelope - no VSA found", + vsaConfig: &vsa.VSAValidationConfig{ + Retriever: &mockVSARetriever{envelope: nil, err: nil}, + VSAExpiration: 24 * time.Hour, + IgnoreSignatureVerification: true, + EffectiveTime: "now", + }, }, } @@ -534,40 +559,18 @@ func TestValidateImageWithVSACheck_FlagCombinations(t *testing.T) { fs := afero.NewMemMapFs() ctx := utils.WithFS(context.Background(), fs) - // Create a proper policy interface p, err := policy.NewOfflinePolicy(ctx, policy.Now) require.NoError(t, err) - // Create a test component with a tag reference (will cause VSA extraction to fail gracefully) comp := app.SnapshotComponent{ ContainerImage: "registry.example.com/test:latest", } - - // Create a mock snapshot spec snap := &app.SnapshotSpec{} - - // Create empty evaluators slice evaluators := []evaluator.Evaluator{} - // Call the function - // Note: This will either attempt VSA checking (and fail gracefully) or skip it entirely - // Either way, it will fall back to normal validation, which should complete without error - // for our minimal setup - output, err := ValidateImageWithVSACheck(ctx, comp, snap, p, evaluators, false, tt.vsaChecker, tt.vsaExpiration) - - // The function should return a non-nil output indicating normal validation proceeded - // The specific result depends on whether VSA checking was attempted - if tt.expectVSACheck { - // VSA checking was attempted but failed due to tag reference, then fell back to validation - // Validation should complete successfully with our minimal setup - assert.NoError(t, err) - assert.NotNil(t, output) - } else { - // VSA checking was skipped entirely, went straight to validation - // Validation should complete successfully with our minimal setup - assert.NoError(t, err) - assert.NotNil(t, output) - } + // All cases should fall back to full validation without error + _, err = ValidateImageWithVSACheck(ctx, comp, snap, p, evaluators, false, tt.vsaConfig) + assert.NoError(t, err) }) } } diff --git a/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml b/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml index 27ac239e2..019ecc9c1 100644 --- a/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml +++ b/tasks/verify-conforma-konflux-ta/0.1/verify-conforma-konflux-ta.yaml @@ -467,6 +467,7 @@ spec: cmd_args+=( --vsa-signing-key="${VSA_SIGNING_KEY}" --vsa-upload="${VSA_UPLOAD}" + --vsa-expiration=0 ) fi