Skip to content

Commit 5419e38

Browse files
authored
Fix tag VSAs (#172)
* Fix tag VSAs They weren't including the verifiedLevels from the other VSAs. Also documenting how this works and updating the slsa-source-poc policy. Signed-off-by: Tom Hennen <[email protected]> * write verified levels when checking tag Signed-off-by: Tom Hennen <[email protected]> --------- Signed-off-by: Tom Hennen <[email protected]>
1 parent f87cef8 commit 5419e38

File tree

6 files changed

+172
-12
lines changed

6 files changed

+172
-12
lines changed

DESIGN.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -217,14 +217,8 @@ then we need to ensure the _repository_ adheres to tag hygiene requirements.
217217

218218
#### Tag Updates
219219

220-
TODO: Update the policy to either require tag_hygiene be set explicitly or
221-
make it implicit if the policy is Level 3+.
222-
223-
TODO: We should probably figure out if we want to issue tag prov or VSAs
224-
if Tag Hygiene isn't enabled.
225-
226220
This control also gets evaluated when tags are updated. When a tag is
227-
updated, if policy sets 'tag_hygiene', the tool will require the control
221+
updated, if the policy sets 'tag_hygiene', the tool will require the control
228222
is enabled. If so, it will create [Tag Provenance](#tag-provenance) for
229223
the tag and it will _copy_ the `verifiedLevels` from VSAs previously
230224
issued for the commit being tagged into a new VSA that includes this
@@ -365,10 +359,24 @@ This amounts to public declaration of SLSA adoption and allows backsliding to be
365359
}
366360
]
367361
}
368-
]
362+
],
363+
"protected_tag": {
364+
"tag_hygiene": true,
365+
"Since": "2025-02-25T17:27:49.445Z"
366+
}
369367
}
370368
```
371369

370+
### Protecting Tags
371+
372+
By default this tool will only issue VSAs for tags at SLSA_SOURCE_LEVEL_1 _unless_
373+
there is an explicit `protected_tag` setting in the policy. This serves as a
374+
declaration by the org that all tags are protected.
375+
376+
The tool does not yet support protecting only some tags. Adding support is
377+
tracked in
378+
[this issue](https://github.com/slsa-framework/slsa-source-poc/issues/129).
379+
372380
### Org Specified Properties
373381

374382
Policies also allow users to specify that the GitHub repo must have a rule requiring

policy/github.com/slsa-framework/slsa-source-poc/source-policy.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@
1313
}
1414
]
1515
}
16-
]
16+
],
17+
"protected_tag": {
18+
"tag_hygiene": true,
19+
"Since": "2025-02-25T17:27:49.445Z"
20+
}
1721
}

sourcetool/cmd/checktag.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package cmd
55

66
import (
77
"context"
8+
"fmt"
89
"log"
910
"os"
1011

@@ -94,6 +95,7 @@ func doCheckTag(args CheckTagArgs) {
9495
log.Printf("unsigned prov: %s\n", unsignedProv)
9596
log.Printf("unsigned vsa: %s\n", unsignedVsa)
9697
}
98+
fmt.Print(verifiedLevels)
9799
}
98100

99101
func init() {

sourcetool/pkg/policy/policy.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"os"
1010
"path/filepath"
11+
"slices"
1112
"strings"
1213
"time"
1314

@@ -409,11 +410,38 @@ func evaluateTagProv(tagPolicy *ProtectedTag, tagProvPred *attest.TagProvenanceP
409410
if err != nil {
410411
return slsa_types.SourceVerifiedLevels{}, fmt.Errorf("error computing tag immutability enforced: %w", err)
411412
}
412-
if len(computedControls) == 0 {
413+
if len(computedControls) == 0 || tagPolicy == nil {
413414
// If tag hygiene isn't enabled then we just return level 1.
414415
return slsa_types.SourceVerifiedLevels{slsa_types.ControlName(slsa_types.SlsaSourceLevel1)}, nil
415416
}
416-
return computedControls, nil
417+
418+
// We have multiple summaries with their own verifiedLevels.
419+
// There are probably duplicates. We need to return a single list.
420+
// We also need to remove duplicate SLSA Source Levels since we can
421+
// only include one. We'll include the highest.
422+
// There's probably a faster way to do this, or a library that could
423+
// be used, I don't think it would be very readable.
424+
verifiedLevels := slsa_types.SourceVerifiedLevels{}
425+
highestSlsaLevel := slsa_types.SlsaSourceLevel1
426+
for _, summary := range tagProvPred.VsaSummaries {
427+
for _, level := range summary.VerifiedLevels {
428+
verifiedLevels = append(verifiedLevels, level)
429+
if slsa_types.IsSlsaSourceLevel(level) &&
430+
slsa_types.IsLevelHigherOrEqualTo(slsa_types.SlsaSourceLevel(level), highestSlsaLevel) {
431+
highestSlsaLevel = slsa_types.SlsaSourceLevel(level)
432+
}
433+
}
434+
}
435+
// Sort (to keep order deterministic) and compact to remove dup
436+
slices.Sort(verifiedLevels)
437+
verifiedLevels = slices.Compact(verifiedLevels)
438+
439+
// Now delete anything that is a SLSA source level but isn't the highest one.
440+
verifiedLevels = slices.DeleteFunc(verifiedLevels, func(level slsa_types.ControlName) bool {
441+
return slsa_types.IsSlsaSourceLevel(level) && level != slsa_types.ControlName(highestSlsaLevel)
442+
})
443+
444+
return verifiedLevels, nil
417445
}
418446

419447
type PolicyEvaluator struct {

sourcetool/pkg/policy/policy_test.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,113 @@ func TestEvaluateSourceProv_Failure(t *testing.T) {
299299
}
300300
}
301301

302+
func createVsaSummary(ref string, verifiedLevels []slsa_types.ControlName) attest.VsaSummary {
303+
return attest.VsaSummary{
304+
SourceRefs: []string{ref},
305+
VerifiedLevels: verifiedLevels,
306+
}
307+
}
308+
309+
func TestEvaluateTagProv_Success(t *testing.T) {
310+
// Controls for mock provenance
311+
tagHygieneEarlier := slsa_types.Control{Name: slsa_types.TagHygiene, Since: earlierFixedTime}
312+
origL2ReviewedSummary := createVsaSummary("refs/heads/orig", []slsa_types.ControlName{
313+
slsa_types.ControlName(slsa_types.SlsaSourceLevel2), slsa_types.ReviewEnforced})
314+
mainL3Summary := createVsaSummary("refs/heads/main", []slsa_types.ControlName{
315+
slsa_types.ControlName(slsa_types.SlsaSourceLevel3)})
316+
317+
tests := []struct {
318+
name string
319+
protectedTagPolicy *ProtectedTag
320+
vsaSummaries []attest.VsaSummary
321+
expectedLevels slsa_types.SourceVerifiedLevels
322+
}{
323+
{
324+
name: "Policy has protected_tag setting, and enabled",
325+
protectedTagPolicy: &ProtectedTag{
326+
Since: fixedTime,
327+
TagHygiene: true,
328+
},
329+
vsaSummaries: []attest.VsaSummary{origL2ReviewedSummary},
330+
expectedLevels: slsa_types.SourceVerifiedLevels{slsa_types.ReviewEnforced, slsa_types.ControlName(slsa_types.SlsaSourceLevel2)},
331+
},
332+
{
333+
name: "Policy has protected_tag setting, and multiple summaries",
334+
protectedTagPolicy: &ProtectedTag{
335+
Since: fixedTime,
336+
TagHygiene: true,
337+
},
338+
vsaSummaries: []attest.VsaSummary{origL2ReviewedSummary, mainL3Summary},
339+
// The spec says we MUST NOT return multiple levels per track in a VSA...
340+
expectedLevels: slsa_types.SourceVerifiedLevels{
341+
slsa_types.ReviewEnforced, slsa_types.ControlName(slsa_types.SlsaSourceLevel3)},
342+
},
343+
{
344+
name: "Policy has protected_tag setting, and it's not enabled",
345+
protectedTagPolicy: &ProtectedTag{
346+
Since: fixedTime,
347+
TagHygiene: false,
348+
},
349+
vsaSummaries: []attest.VsaSummary{origL2ReviewedSummary},
350+
expectedLevels: slsa_types.SourceVerifiedLevels{slsa_types.ControlName(slsa_types.SlsaSourceLevel1)},
351+
},
352+
{
353+
name: "Policy has protected_tag setting, and it's earlier than the control",
354+
protectedTagPolicy: &ProtectedTag{
355+
Since: earlierFixedTime,
356+
TagHygiene: false,
357+
},
358+
vsaSummaries: []attest.VsaSummary{origL2ReviewedSummary},
359+
expectedLevels: slsa_types.SourceVerifiedLevels{slsa_types.ControlName(slsa_types.SlsaSourceLevel1)},
360+
},
361+
{
362+
name: "Policy has no protected_tag setting",
363+
protectedTagPolicy: nil,
364+
vsaSummaries: []attest.VsaSummary{origL2ReviewedSummary},
365+
expectedLevels: slsa_types.SourceVerifiedLevels{slsa_types.ControlName(slsa_types.SlsaSourceLevel1)},
366+
},
367+
}
368+
369+
for _, tt := range tests {
370+
t.Run(tt.name, func(t *testing.T) {
371+
// Valid Provenance Predicate (attest.SourceProvenancePred)
372+
tagProvPred := attest.TagProvenancePred{
373+
Controls: slsa_types.Controls{tagHygieneEarlier},
374+
VsaSummaries: tt.vsaSummaries,
375+
}
376+
377+
provenanceStatement := createStatementForTest(t, tagProvPred, attest.TagProvPredicateType)
378+
379+
pb := ProtectedBranch{
380+
Name: "main",
381+
TargetSlsaSourceLevel: slsa_types.SlsaSourceLevel2,
382+
RequireReview: true,
383+
Since: fixedTime,
384+
}
385+
rp := createTestPolicy(pb)
386+
rp.ProtectedTag = tt.protectedTagPolicy
387+
388+
expectedPolicyFilePath := createTempPolicyFile(t, rp)
389+
defer os.Remove(expectedPolicyFilePath)
390+
pe := &PolicyEvaluator{UseLocalPolicy: expectedPolicyFilePath}
391+
392+
ghConn := newTestGhBranchConnection("local", "local", "main")
393+
394+
verifiedLevels, policyPath, err := pe.EvaluateTagProv(context.Background(), ghConn, provenanceStatement)
395+
396+
if err != nil {
397+
t.Errorf("EvaluateTagProv() error = %v, want nil", err)
398+
}
399+
if policyPath != expectedPolicyFilePath {
400+
t.Errorf("EvaluateTagProv() policyPath = %q, want %q", policyPath, expectedPolicyFilePath)
401+
}
402+
if !slices.Equal(verifiedLevels, tt.expectedLevels) {
403+
t.Errorf("EvaluateTagProv() verifiedLevels = %v, want %v", verifiedLevels, tt.expectedLevels)
404+
}
405+
})
406+
}
407+
}
408+
302409
func TestEvaluateControl_Success(t *testing.T) {
303410
// Controls
304411
continuityEnforcedEarlier := slsa_types.Control{Name: slsa_types.ContinuityEnforced, Since: earlierFixedTime}
@@ -555,7 +662,7 @@ func assertProtectedBranchEquals(t *testing.T, got *ProtectedBranch, expected Pr
555662
if ignoreSince && actual.Since != (time.Time{}) { // Add note only if Since was ignored AND original got.Since was not zero
556663
errorMessage.WriteString(fmt.Sprintf("\n(Note: 'Since' field was ignored in comparison as requested. Original Expected.Since: %v, Original Got.Since: %v)", expected.Since, actual.Since))
557664
}
558-
t.Errorf(errorMessage.String())
665+
t.Error(errorMessage.String())
559666
}
560667
}
561668

sourcetool/pkg/slsa_types/slsa_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package slsa_types
22

33
import (
4+
"slices"
45
"time"
56
)
67

@@ -21,6 +22,16 @@ const (
2122
AllowedOrgPropPrefix = "ORG_SOURCE_"
2223
)
2324

25+
func IsSlsaSourceLevel(control ControlName) bool {
26+
return slices.Contains(
27+
[]ControlName{
28+
ControlName(SlsaSourceLevel1),
29+
ControlName(SlsaSourceLevel2),
30+
ControlName(SlsaSourceLevel3),
31+
ControlName(SlsaSourceLevel4)},
32+
control)
33+
}
34+
2435
func IsLevelHigherOrEqualTo(level1, level2 SlsaSourceLevel) bool {
2536
// There's probably some fancy stuff we can get in to, but...
2637
// it just so happens that these level strings should sort the way we want.

0 commit comments

Comments
 (0)