Skip to content

Commit a183384

Browse files
authored
Merge pull request #3489 from st3penta/EC-1998
Use ValidateVSAAndComparePolicy for ec validate image VSA skip
2 parents 7d116df + 30712ca commit a183384

8 files changed

Lines changed: 201 additions & 108 deletions

File tree

cmd/validate/image.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
296296
}
297297
}
298298

299+
// Require --vsa-public-key when the VSA skip path is active
300+
if len(data.vsaUpload) > 0 && data.vsaPublicKey == "" && data.vsaExpiration > 0 {
301+
allErrors = errors.Join(allErrors, fmt.Errorf("--vsa-public-key required when --vsa-upload is set with --vsa-expiration > 0"))
302+
}
303+
299304
return
300305
},
301306

@@ -367,9 +372,20 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
367372
var out *output.Output
368373
var err error
369374
if data.vsaExpiration > 0 {
370-
vsaChecker := vsa.CreateVSACheckerFromUploadFlags(data.vsaUpload)
371-
if vsaChecker != nil {
372-
out, err = image.ValidateImageWithVSACheck(ctx, comp, data.spec, data.policy, evaluators, data.info, vsaChecker, data.vsaExpiration)
375+
retriever := vsa.CreateRetrieverFromUploadFlags(data.vsaUpload)
376+
if retriever != nil {
377+
vsaEffectiveTime := data.effectiveTime
378+
if vsaEffectiveTime == "attestation" {
379+
vsaEffectiveTime = policy.Now
380+
}
381+
vsaConfig := &vsa.VSAValidationConfig{
382+
Retriever: retriever,
383+
VSAExpiration: data.vsaExpiration,
384+
PublicKeyPath: data.vsaPublicKey,
385+
PolicySpec: data.policy.Spec(),
386+
EffectiveTime: vsaEffectiveTime,
387+
}
388+
out, err = image.ValidateImageWithVSACheck(ctx, comp, data.spec, data.policy, evaluators, data.info, vsaConfig)
373389
} else {
374390
// Fall back to normal validation if no VSA retriever is available
375391
out, err = validate(ctx, comp, data.spec, data.policy, evaluators, data.info)
@@ -583,6 +599,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
583599
cmd.Flags().BoolVar(&data.vsaEnabled, "vsa", false, "Generate a Verification Summary Attestation (VSA) for each validated image.")
584600
cmd.Flags().StringVar(&data.attestationFormat, "attestation-format", "dsse", "Attestation output format: dsse (signed envelope), predicate (raw JSON)")
585601
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).")
602+
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.")
586603
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")
587604
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)")
588605
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 {
667684
vsaEnabled bool
668685
attestationFormat string
669686
vsaSigningKey string
687+
vsaPublicKey string
670688
vsaUpload []string
671689
vsaExpiration time.Duration
672690
attestationOutputDir string

cmd/validate/image_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,7 @@ func TestValidateImageCommand_VSAUpload_Success(t *testing.T) {
14711471
"--vsa",
14721472
"--vsa-signing-key", "/tmp/vsa-key.pem",
14731473
"--vsa-upload", "local@/tmp/vsa-test",
1474+
"--vsa-public-key", "/tmp/vsa-pub.pem",
14741475
})
14751476

14761477
var out bytes.Buffer
@@ -1543,6 +1544,39 @@ func TestValidateImageCommand_VSAUpload_NoStorageBackends(t *testing.T) {
15431544
// Don't assert no error since VSA processing might fail, but upload logic should be reached
15441545
}
15451546

1547+
func TestValidateImageCommand_VSAPublicKeyRequired(t *testing.T) {
1548+
// --vsa-public-key is required when --vsa-upload is set
1549+
validateImageCmd := validateImageCmd(happyValidator())
1550+
cmd := setUpCobra(validateImageCmd)
1551+
1552+
fs := afero.NewMemMapFs()
1553+
ctx := utils.WithFS(context.Background(), fs)
1554+
1555+
client := fake.FakeClient{}
1556+
commonMockClient(&client)
1557+
ctx = oci.WithClient(ctx, &client)
1558+
cmd.SetContext(ctx)
1559+
1560+
cmd.SetArgs([]string{
1561+
"validate", "image",
1562+
"--image", "registry/image:tag",
1563+
"--policy", fmt.Sprintf(`{"publicKey": %s}`, utils.TestPublicKeyJSON),
1564+
"--vsa-upload", "local@/tmp/vsa-test",
1565+
// Missing --vsa-public-key
1566+
})
1567+
1568+
var out bytes.Buffer
1569+
cmd.SetOut(&out)
1570+
cmd.SilenceErrors = true
1571+
cmd.SilenceUsage = true
1572+
1573+
utils.SetTestRekorPublicKey(t)
1574+
1575+
err := cmd.Execute()
1576+
assert.Error(t, err)
1577+
assert.Contains(t, err.Error(), "--vsa-public-key required when --vsa-upload is set with --vsa-expiration > 0")
1578+
}
1579+
15461580
func TestValidateImageCommand_ShowWarningsFlag(t *testing.T) {
15471581
// Create a validator that returns warnings
15481582
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) {
16761710
"--attestation-format", "dsse",
16771711
"--vsa-signing-key", "/tmp/vsa-key.pem",
16781712
"--vsa-upload", "local@/tmp/vsa-test",
1713+
"--vsa-public-key", "/tmp/vsa-pub.pem",
16791714
})
16801715

16811716
var out bytes.Buffer
@@ -1712,6 +1747,7 @@ func TestValidateImageCommand_VSAFormat_Predicate(t *testing.T) {
17121747
"--vsa",
17131748
"--attestation-format", "predicate",
17141749
"--vsa-upload", "local@/tmp/vsa-predicates",
1750+
"--vsa-public-key", "/tmp/vsa-pub.pem",
17151751
})
17161752

17171753
var out bytes.Buffer
@@ -1747,6 +1783,7 @@ func TestValidateImageCommand_VSAFormat_InvalidFormat(t *testing.T) {
17471783
"--attestation-format", "invalid-format",
17481784
"--vsa-signing-key", "/tmp/vsa-key.pem",
17491785
"--vsa-upload", "local@/tmp/vsa-test",
1786+
"--vsa-public-key", "/tmp/vsa-pub.pem",
17501787
})
17511788

17521789
var out bytes.Buffer
@@ -1784,6 +1821,7 @@ func TestValidateImageCommand_VSAFormat_DSSE_RequiresSigningKey(t *testing.T) {
17841821
"--attestation-format", "dsse",
17851822
// Missing --vsa-signing-key
17861823
"--vsa-upload", "local@/tmp/vsa-test",
1824+
"--vsa-public-key", "/tmp/vsa-pub.pem",
17871825
})
17881826

17891827
var out bytes.Buffer
@@ -1822,6 +1860,7 @@ func TestValidateImageCommand_VSAFormat_Predicate_WorksWithoutSigningKey(t *test
18221860
"--attestation-format", "predicate",
18231861
// No --vsa-signing-key provided
18241862
"--vsa-upload", "local@/tmp/vsa-predicates",
1863+
"--vsa-public-key", "/tmp/vsa-pub.pem",
18251864
})
18261865

18271866
var out bytes.Buffer
@@ -1940,6 +1979,7 @@ func TestGenerateVSAsDSSE_Errors(t *testing.T) {
19401979
"--attestation-format", "dsse",
19411980
"--vsa-signing-key", "/tmp/invalid-key.pem",
19421981
"--vsa-upload", "local@/tmp/vsa-test",
1982+
"--vsa-public-key", "/tmp/vsa-pub.pem",
19431983
})
19441984

19451985
var out bytes.Buffer
@@ -1974,6 +2014,7 @@ func TestGenerateVSAsDSSE_Errors(t *testing.T) {
19742014
"--attestation-format", "dsse",
19752015
"--vsa-signing-key", "/tmp/nonexistent-key.pem",
19762016
"--vsa-upload", "local@/tmp/vsa-test",
2017+
"--vsa-public-key", "/tmp/vsa-pub.pem",
19772018
})
19782019

19792020
var out bytes.Buffer
@@ -2025,6 +2066,7 @@ func TestGenerateVSAsDSSE_Errors(t *testing.T) {
20252066
"--attestation-format", "dsse",
20262067
"--vsa-signing-key", "/tmp/vsa-key.pem",
20272068
"--vsa-upload", "local@/tmp/vsa-test",
2069+
"--vsa-public-key", "/tmp/vsa-pub.pem",
20282070
})
20292071

20302072
var out bytes.Buffer
@@ -2060,6 +2102,7 @@ func TestGenerateVSAsPredicates_Errors(t *testing.T) {
20602102
"--attestation-format", "predicate",
20612103
"--attestation-output-dir", "/etc/invalid-dir", // Invalid directory outside /tmp and cwd
20622104
"--vsa-upload", "local@/tmp/vsa-predicates",
2105+
"--vsa-public-key", "/tmp/vsa-pub.pem",
20632106
})
20642107

20652108
var out bytes.Buffer
@@ -2096,6 +2139,7 @@ func TestGenerateVSAsPredicates_Errors(t *testing.T) {
20962139
"--attestation-format", "predicate",
20972140
"--attestation-output-dir", "/tmp/vsa-predicates",
20982141
"--vsa-upload", "local@/tmp/vsa-predicates",
2142+
"--vsa-public-key", "/tmp/vsa-pub.pem",
20992143
})
21002144

21012145
var out bytes.Buffer
@@ -2175,6 +2219,7 @@ func TestVSAGeneration_WithOutputDir(t *testing.T) {
21752219
"--attestation-format", tt.format,
21762220
"--attestation-output-dir", tt.outputDir,
21772221
"--vsa-upload", "local@/tmp/vsa-test",
2222+
"--vsa-public-key", "/tmp/vsa-pub.pem",
21782223
}
21792224

21802225
if tt.needsKey {

docs/modules/ROOT/pages/ec_validate_image.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ JSON of the "spec" or a reference to a Kubernetes object [<namespace>/]<name>
162162
-s, --strict:: Return non-zero status on non-successful validation. Defaults to true. Use --strict=false to return a zero status code. (Default: true)
163163
--vsa:: Generate a Verification Summary Attestation (VSA) for each validated image. (Default: false)
164164
--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)
165+
--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.
165166
--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).
166167
--vsa-upload:: Storage backends for VSA upload. Format: backend@url?param=value. Examples: rekor@https://rekor.sigstore.dev, local@./vsa-dir (Default: [])
167168
--workers:: Number of workers to use for validation. Defaults to 5. (Default: 5)

features/__snapshots__/vsa.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
---
4848

4949
[TestFeatures/VSA expiration flag functionality:stderr - 1]
50-
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}"
50+
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}"
5151

5252
---
5353

features/vsa.feature

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Feature: VSA generation and storage
2727
]
2828
}
2929
"""
30-
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"
30+
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"
3131
Then the exit status should be 0
3232
Then the output should match the snapshot
3333
And VSA envelope files should exist in "${TMPDIR}/vsa-output"
@@ -52,7 +52,7 @@ Feature: VSA generation and storage
5252
}
5353
"""
5454
Given VSA upload to Rekor should be expected
55-
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"
55+
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"
5656
Then the exit status should be 0
5757
Then the output should match the snapshot
5858
And VSA should be uploaded to Rekor successfully
@@ -77,7 +77,7 @@ Feature: VSA generation and storage
7777
}
7878
"""
7979
Given VSA upload to Rekor should be expected
80-
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"
80+
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"
8181
Then the exit status should be 0
8282
Then the output should match the snapshot
8383
And VSA envelope files should exist in "${TMPDIR}/vsa-multi-output"
@@ -102,7 +102,7 @@ Feature: VSA generation and storage
102102
]
103103
}
104104
"""
105-
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"
105+
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"
106106
Then the exit status should be 0
107107
Then the output should match the snapshot
108108

@@ -126,7 +126,7 @@ Feature: VSA generation and storage
126126
]
127127
}
128128
"""
129-
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"
129+
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"
130130
Then the exit status should be 0
131131
Then the output should match the snapshot
132132

@@ -153,7 +153,7 @@ Feature: VSA generation and storage
153153
Given VSA upload to Rekor should be expected
154154
# First, generate a VSA and upload it to Rekor
155155
Given VSA upload to Rekor should be expected
156-
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"
156+
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"
157157
Then the exit status should be 0
158158
And VSA should be uploaded to Rekor successfully
159159

@@ -206,6 +206,29 @@ Feature: VSA generation and storage
206206
}
207207
"""
208208
Given Rekor upload should fail
209-
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"
209+
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"
210210
Then the exit status should be 0
211211
And the log output should contain "[VSA] Failed to upload in-toto 0.0.2 entry"
212+
213+
Scenario: Missing vsa-public-key with vsa-upload errors
214+
Given a key pair named "vsa-pubkey"
215+
Given an image named "acceptance/vsa-pubkey-image"
216+
Given a valid image signature of "acceptance/vsa-pubkey-image" image signed by the "vsa-pubkey" key
217+
Given a valid attestation of "acceptance/vsa-pubkey-image" signed by the "vsa-pubkey" key
218+
Given a git repository named "vsa-pubkey-policy" with
219+
| main.rego | examples/happy_day.rego |
220+
Given policy configuration named "vsa-pubkey-ec-policy" with specification
221+
"""
222+
{
223+
"sources": [
224+
{
225+
"policy": [
226+
"git::https://${GITHOST}/git/vsa-pubkey-policy.git"
227+
]
228+
}
229+
]
230+
}
231+
"""
232+
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"
233+
Then the exit status should be 1
234+
And the log output should contain "--vsa-public-key required when --vsa-upload is set with --vsa-expiration > 0"

0 commit comments

Comments
 (0)