Skip to content

Commit b21e842

Browse files
authored
Add support to migrate to new actions repo (#446)
* Detect workflows on legacy action repos in status This commit adds a workflow scanner to the GitHub backend that reads every workflow file in the branch, whatever its name, and finds the uses: lines that call the SLSA actions. Expose it through the VcsBackend interface and the Tool as FindProvenanceWorkflows, returning the workflow path and the legacy repositories it still references. Signed-off-by: Adolfo García Veytia (Puerco) <puerco@carabiner.dev> * Update workflows on legacy repos from setup Signed-off-by: Adolfo García Veytia (Puerco) <puerco@carabiner.dev> * Accept the slsa/actions identity and verifier ID Match the new workflow identity by prefix using the signer library's identity matcher, keeping the source-actions and slsa-source-poc identities as exact alternates while repositories still carry attestations signed by them. Signed-off-by: Adolfo García Veytia (Puerco) <puerco@carabiner.dev> --------- Signed-off-by: Adolfo García Veytia (Puerco) <puerco@carabiner.dev>
1 parent 1edb63f commit b21e842

17 files changed

Lines changed: 1263 additions & 111 deletions

File tree

internal/cmd/setup.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,8 @@ this is required to reach SLSA source level 2+.
294294
295295
%s
296296
Opens a pull request in the repository to add the provenance generation workflow
297-
after every push.
297+
after every push. If the repository already has a workflow calling the SLSA
298+
actions from a deprecated location, the pull request updates it instead.
298299
299300
%s
300301
Opens a pull request on the SLSA policy repository to check in a SLSA Source

internal/cmd/status.go

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/slsa-framework/source-tool/pkg/policy"
1717
"github.com/slsa-framework/source-tool/pkg/slsa"
1818
"github.com/slsa-framework/source-tool/pkg/sourcetool"
19+
"github.com/slsa-framework/source-tool/pkg/sourcetool/models"
1920
)
2021

2122
var (
@@ -137,6 +138,12 @@ sourcetool status myorg/myrepo@mybranch
137138
return nil
138139
}
139140

141+
// Look for provenance workflows that need to be updated
142+
workflows, err := srctool.FindProvenanceWorkflows(cmd.Context(), opts.GetBranch())
143+
if err != nil {
144+
return fmt.Errorf("checking provenance workflows: %w", err)
145+
}
146+
140147
title := fmt.Sprintf(
141148
"\nSLSA Source Status for %s/%s@%s", opts.owner, opts.repository,
142149
ghcontrol.BranchToFullRef(opts.branch),
@@ -197,8 +204,11 @@ sourcetool status myorg/myrepo@mybranch
197204

198205
fmt.Println(w("Current SLSA Source level: " + verifiedLevel))
199206
printLevelGap(toplevel, verifiedLevel, evalResult.Shortfall)
207+
printLegacyWorkflows(workflows)
200208
fmt.Println("")
201-
titled := false
209+
210+
// Collect the recommended actions from the controls
211+
actions := []*slsa.ControlRecommendedAction{}
202212
for _, status := range controls.Controls {
203213
if status.RecommendedAction == nil {
204214
continue
@@ -208,28 +218,25 @@ sourcetool status myorg/myrepo@mybranch
208218
if status.Name == slsa.PolicyAvailable && !slsa.IsLevelHigherOrEqualTo(toplevel, slsa.SlsaSourceLevel3) {
209219
continue
210220
}
221+
actions = append(actions, status.RecommendedAction)
222+
}
211223

212-
if !titled {
213-
fmt.Println(w2("✨ Recommended actions:"))
214-
titled = true
215-
}
216-
217-
fmt.Printf(" - %s\n", status.RecommendedAction.Message)
218-
if status.RecommendedAction.Command != "" {
219-
fmt.Printf(" > %s\n", status.RecommendedAction.Command)
224+
// ... from the provenance workflows
225+
for _, wf := range workflows {
226+
if wf.RecommendedAction != nil {
227+
actions = append(actions, wf.RecommendedAction)
220228
}
221-
fmt.Println()
222229
}
223230

231+
// ... and from the policy
224232
if policyNeedsUpdate {
225-
if !titled {
226-
fmt.Println(w2("✨ Recommended actions:"))
227-
}
228-
fmt.Println(" - Update the repository source policy")
229-
fmt.Printf(" > sourcetool policy create --update %s\n", opts.GetRepository().Path)
230-
fmt.Println()
233+
actions = append(actions, &slsa.ControlRecommendedAction{
234+
Message: "Update the repository source policy",
235+
Command: "sourcetool policy create --update " + opts.GetRepository().Path,
236+
})
231237
}
232238

239+
printRecommendedActions(actions)
233240
return nil
234241
},
235242
}
@@ -248,6 +255,35 @@ func firstSourceLevel(levels slsa.SourceVerifiedLevels) string {
248255
return string(slsa.SlsaSourceLevel0)
249256
}
250257

258+
// printRecommendedActions prints the list of recommended actions, if any
259+
func printRecommendedActions(actions []*slsa.ControlRecommendedAction) {
260+
if len(actions) == 0 {
261+
return
262+
}
263+
fmt.Println(w2("✨ Recommended actions:"))
264+
for _, action := range actions {
265+
fmt.Printf(" - %s\n", action.Message)
266+
if action.Command != "" {
267+
fmt.Printf(" > %s\n", action.Command)
268+
}
269+
fmt.Println()
270+
}
271+
}
272+
273+
// printLegacyWorkflows warns about provenance workflows still calling the
274+
// SLSA actions from a deprecated repository.
275+
func printLegacyWorkflows(workflows []*models.ProvenanceWorkflow) {
276+
for _, wf := range workflows {
277+
if !wf.IsLegacy() {
278+
continue
279+
}
280+
fmt.Printf(
281+
"%s The workflow %s calls the SLSA actions from the deprecated %s repository.\n",
282+
w2("⚠️ "), wf.Path, strings.Join(wf.LegacyActionsRepos, " and "),
283+
)
284+
}
285+
}
286+
251287
// printLevelGap explains when the policy-verified level is below the level the
252288
// active controls would otherwise support.
253289
func printLevelGap(eligible slsa.SlsaSourceLevel, verified string, shortfall *policy.PolicyShortfall) {

pkg/attest/provenance.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,9 @@ func (a *Attester) GetRevisionVSA(ctx context.Context, branch *models.Branch, re
207207
continue
208208
}
209209

210-
// Check the verifier ID matches
211-
if vsaPred.GetVerifier().GetId() != VsaVerifierId {
212-
Debugf("VSA verfier ID does not match %s", VsaVerifierId)
210+
// Check the verifier ID is one we accept
211+
if !IsAcceptedVsaVerifierId(vsaPred.GetVerifier().GetId()) {
212+
Debugf("VSA verifier ID %q is not one of %v", vsaPred.GetVerifier().GetId(), AcceptedVsaVerifierIds)
213213
continue
214214
}
215215

pkg/attest/verify.go

Lines changed: 111 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package attest
66
import (
77
"errors"
88
"fmt"
9+
"regexp"
910

1011
"github.com/carabiner-dev/attestation"
1112
"github.com/carabiner-dev/signer"
@@ -15,12 +16,23 @@ import (
1516
)
1617

1718
type VerificationOptions struct {
19+
// ExpectedIssuer is the OIDC issuer of the certificates signing the
20+
// attestations. It is required, no identity is accepted without it.
1821
ExpectedIssuer string
19-
ExpectedSan string
2022

21-
// AlternateSans lists additional signer identities accepted when
22-
// verifying attestations. It carries the pre-rename workflow identity
23-
// while repositories still have attestations signed with it.
23+
// ExpectedSan pins the signer identity to an exact subject alternative
24+
// name. When set, ExpectedSanPrefix is ignored.
25+
ExpectedSan string
26+
27+
// ExpectedSanPrefix accepts any signer identity starting with the
28+
// prefix. Users pin the provenance workflow to different tags and
29+
// digests, so the git reference ending its identity varies.
30+
ExpectedSanPrefix string
31+
32+
// AlternateSans lists additional signer identities accepted (exactly)
33+
// when verifying attestations. It carries the identities of the
34+
// workflows that signed attestations before the actions moved to their
35+
// current repository.
2436
//
2537
// See https://github.com/slsa-framework/source-tool/issues/255
2638
AlternateSans []string
@@ -30,24 +42,88 @@ const (
3042
// ExpectedIssuer is the OIDC issuer found in the sigstore bundles
3143
ExpectedIssuer = "https://token.actions.githubusercontent.com"
3244

33-
// Expected SAN is the expected identity of the workflow signing the
34-
// provenance and VSAs.
35-
ExpectedSan = "https://github.com/slsa-framework/source-actions/.github/workflows/compute_slsa_source.yml@refs/heads/main"
45+
// ExpectedSanPrefix is the prefix of the identity of the reusable workflow
46+
// signing the provenance and VSAs. The full identity ends with the git
47+
// reference the workflow was pinned to, which varies across users and
48+
// releases.
49+
ExpectedSanPrefix = "https://github.com/slsa-framework/actions/.github/workflows/compute_slsa_source.yml@"
3650

37-
// OldExpectedSan is the old singer identity before splitting out the actions to their own repo
38-
// this constant is part of a compatibility hack that should be reverted once the latests attestations
39-
// of the repos are signed with the new identity.
51+
// LegacySourceActionsSan is the identity of the workflow that signed
52+
// attestations while the actions lived in slsa-framework/source-actions.
53+
LegacySourceActionsSan = "https://github.com/slsa-framework/source-actions/.github/workflows/compute_slsa_source.yml@refs/heads/main"
54+
55+
// LegacyPocSan is the identity of the workflow that signed attestations
56+
// before the actions were split out of the slsa-source-poc repository.
4057
//
4158
// See https://github.com/slsa-framework/source-tool/issues/255
42-
OldExpectedSan = "https://github.com/slsa-framework/slsa-source-poc/.github/workflows/compute_slsa_source.yml@refs/heads/main"
59+
LegacyPocSan = "https://github.com/slsa-framework/slsa-source-poc/.github/workflows/compute_slsa_source.yml@refs/heads/main"
4360
)
4461

45-
// TODO: Update ExpectedSan to support regex so we can get the branches/tags we really think
46-
// folks should be using (they won't all run from main).
62+
// DefaultVerifierOptions accept attestations signed by the current provenance
63+
// workflow, whatever reference it is pinned to, and by the legacy workflows
64+
// while repositories still carry attestations signed by them.
4765
var DefaultVerifierOptions = VerificationOptions{
48-
ExpectedIssuer: ExpectedIssuer,
49-
ExpectedSan: ExpectedSan,
50-
AlternateSans: []string{OldExpectedSan},
66+
ExpectedIssuer: ExpectedIssuer,
67+
ExpectedSanPrefix: ExpectedSanPrefix,
68+
AlternateSans: []string{LegacySourceActionsSan, LegacyPocSan},
69+
}
70+
71+
// expectedIdentities returns the signer identities accepted by the options.
72+
// Without an issuer no identity is accepted.
73+
func (vo *VerificationOptions) expectedIdentities() []*sapi.Identity {
74+
if vo.ExpectedIssuer == "" {
75+
return nil
76+
}
77+
78+
ids := []*sapi.Identity{}
79+
switch {
80+
case vo.ExpectedSan != "":
81+
ids = append(ids, exactIdentity(vo.ExpectedIssuer, vo.ExpectedSan))
82+
case vo.ExpectedSanPrefix != "":
83+
ids = append(ids, &sapi.Identity{
84+
Sigstore: &sapi.IdentitySigstore{
85+
Issuer: vo.ExpectedIssuer,
86+
IdentityMatch: &sapi.StringMatcher{
87+
Kind: &sapi.StringMatcher_Prefix{Prefix: vo.ExpectedSanPrefix},
88+
},
89+
},
90+
})
91+
}
92+
93+
for _, san := range vo.AlternateSans {
94+
if san == "" {
95+
continue
96+
}
97+
ids = append(ids, exactIdentity(vo.ExpectedIssuer, san))
98+
}
99+
return ids
100+
}
101+
102+
// exactIdentity builds a sigstore identity matching the issuer and SAN exactly
103+
func exactIdentity(issuer, san string) *sapi.Identity {
104+
return &sapi.Identity{
105+
Sigstore: &sapi.IdentitySigstore{
106+
Issuer: issuer,
107+
Identity: san,
108+
},
109+
}
110+
}
111+
112+
// String describes the accepted identities for error messages
113+
func (vo *VerificationOptions) String() string {
114+
sans := []string{}
115+
switch {
116+
case vo.ExpectedSan != "":
117+
sans = append(sans, vo.ExpectedSan)
118+
case vo.ExpectedSanPrefix != "":
119+
sans = append(sans, vo.ExpectedSanPrefix+"*")
120+
}
121+
for _, san := range vo.AlternateSans {
122+
if san != "" {
123+
sans = append(sans, san)
124+
}
125+
}
126+
return fmt.Sprintf("issuer %q identities %q", vo.ExpectedIssuer, sans)
51127
}
52128

53129
type Verifier interface {
@@ -64,27 +140,32 @@ type BndVerifier struct {
64140
Options VerificationOptions
65141
}
66142

143+
// Verify checks a signed bundle, ensuring the signer matches the expected
144+
// identity. Note that this method does not accept the alternate identities,
145+
// only the expected SAN (or prefix) is checked.
67146
func (bv *BndVerifier) Verify(data string) (*verify.VerificationResult, error) {
68-
// TODO: There's more for us to do here... but what?
69-
// Maybe check to make sure it's from the identity we expect (the workflow?)
70147
verifier := signer.NewVerifier()
71148

149+
identityOpts := []options.VerificationOptFunc{
150+
options.WithExpectedIdentity(bv.Options.ExpectedIssuer, bv.Options.ExpectedSan),
151+
}
152+
if bv.Options.ExpectedSan == "" && bv.Options.ExpectedSanPrefix != "" {
153+
identityOpts = append(identityOpts, options.WithExpectedIdentityRegex(
154+
"", "^"+regexp.QuoteMeta(bv.Options.ExpectedSanPrefix),
155+
))
156+
}
157+
72158
// Verify the signed bundle
73-
vr, err := verifier.VerifyInlineBundle(
74-
[]byte(data),
75-
options.WithExpectedIdentity(
76-
bv.Options.ExpectedIssuer, bv.Options.ExpectedSan,
77-
),
78-
)
159+
vr, err := verifier.VerifyInlineBundle([]byte(data), identityOpts...)
79160
if err != nil {
80161
return nil, err
81162
}
82163
return vr, nil
83164
}
84165

85166
// VerifyEnvelope verifies the signature of an attestation envelope fetched
86-
// by the collector and checks that the signer matches the expected identity
87-
// (issuer + SAN) or one of the accepted alternate identities.
167+
// by the collector and checks that the signer matches one of the expected
168+
// identities.
88169
func (bv *BndVerifier) VerifyEnvelope(env attestation.Envelope) error {
89170
if env == nil {
90171
return errors.New("unable to verify, envelope is nil")
@@ -104,24 +185,15 @@ func (bv *BndVerifier) VerifyEnvelope(env attestation.Envelope) error {
104185
return errors.New("envelope carries no verified signature")
105186
}
106187

107-
// Check the signer identity against the expected SANs
108-
for _, san := range append([]string{bv.Options.ExpectedSan}, bv.Options.AlternateSans...) {
109-
if san == "" {
110-
continue
111-
}
112-
if verification.MatchesIdentity(&sapi.Identity{
113-
Sigstore: &sapi.IdentitySigstore{
114-
Issuer: bv.Options.ExpectedIssuer,
115-
Identity: san,
116-
},
117-
}) {
188+
// Check the signer identity against the expected identities
189+
for _, id := range bv.Options.expectedIdentities() {
190+
if verification.MatchesIdentity(id) {
118191
return nil
119192
}
120193
}
121194

122195
return fmt.Errorf(
123-
"envelope signer does not match the expected identity (issuer %q identity %q)",
124-
bv.Options.ExpectedIssuer, bv.Options.ExpectedSan,
196+
"envelope signer does not match any expected identity (%s)", bv.Options.String(),
125197
)
126198
}
127199

0 commit comments

Comments
 (0)