-
Notifications
You must be signed in to change notification settings - Fork 44
/
builder.go
507 lines (452 loc) · 17.2 KB
/
builder.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
package gha
import (
"crypto/x509"
"encoding/asn1"
"fmt"
"net/url"
"strings"
fulcio "github.com/sigstore/fulcio/pkg/certificate"
serrors "github.com/slsa-framework/slsa-verifier/v2/errors"
"github.com/slsa-framework/slsa-verifier/v2/options"
ghacommon "github.com/slsa-framework/slsa-verifier/v2/verifiers/internal/gha/slsaprovenance/common"
"github.com/slsa-framework/slsa-verifier/v2/verifiers/utils"
)
var (
trustedBuilderRepository = "slsa-framework/slsa-github-generator"
e2eTestRepository = "slsa-framework/example-package"
jReleaserActionRepository = "jreleaser/release-action"
certOidcIssuer = "https://token.actions.githubusercontent.com"
githubCom = "github.com/"
httpsGithubCom = "https://" + githubCom
// This is used in cosign's CheckOpts for validating the certificate. We
// do specific builder verification after this.
certSubjectRegexp = httpsGithubCom + "*"
)
var defaultArtifactTrustedReusableWorkflows = map[string]bool{
ghacommon.GenericGeneratorBuilderID: true,
ghacommon.GoBuilderID: true,
ghacommon.ContainerBasedBuilderID: true,
}
var defaultContainerTrustedReusableWorkflows = map[string]bool{
ghacommon.ContainerGeneratorBuilderID: true,
}
var defaultBYOBReusableWorkflows = map[string]bool{
ghacommon.GenericDelegatorBuilderID: true,
ghacommon.GenericLowPermsDelegatorBuilderID: true,
}
var JReleaserRepository = httpsGithubCom + jReleaserActionRepository
// VerifyCertficateSourceRepository verifies the source repository.
func VerifyCertficateSourceRepository(id *WorkflowIdentity,
sourceRepo string,
) error {
// The caller repository in the x509 extension is not fully qualified. It only contains
// {org}/{repository}.
expectedSource := strings.TrimPrefix(sourceRepo, "git+https://")
expectedSource = strings.TrimPrefix(expectedSource, githubCom)
if id.SourceRepository != expectedSource {
return fmt.Errorf("%w: expected source '%s', got '%s'", serrors.ErrorMismatchSource,
expectedSource, id.SourceRepository)
}
return nil
}
// VerifyBuilderIdentity verifies the signing certificate information.
// Builder IDs are verified against an expected builder ID provided in the
// builerOpts, or against the set of defaultBuilders provided. The identiy
// in the certificate corresponds to a GitHub workflow's path.
func VerifyBuilderIdentity(id *WorkflowIdentity,
builderOpts *options.BuilderOpts,
defaultBuilders map[string]bool,
) (*utils.TrustedBuilderID, bool, error) {
// Issuer verification.
// NOTE: this is necessary before we do any further verification.
if id.Issuer != certOidcIssuer {
return nil, false, fmt.Errorf("%w: %q", serrors.ErrorInvalidOIDCIssuer, id.Issuer)
}
// cert URI is https://github.com/org/repo/path/to/workflow@ref
// Remove '@' from Path
workflowID := id.SubjectWorkflowName()
workflowTag := id.SubjectWorkflowRef()
if workflowID == "" || workflowTag == "" {
return nil, false, fmt.Errorf("%w: workflow uri: %q", serrors.ErrorMalformedURI, id.SubjectWorkflow.String())
}
// Verify trusted workflow.
builderID, byob, err := verifyTrustedBuilderID(workflowID, workflowTag,
builderOpts.ExpectedID, defaultBuilders)
if err != nil {
return nil, byob, err
}
// Verify the ref is a full semantic version tag.
if err := verifyTrustedBuilderRef(id, workflowTag); err != nil {
return nil, byob, err
}
return builderID, byob, nil
}
// Verifies the builder ID at path against an expected builderID.
// If an expected builderID is not provided, uses the defaultBuilders.
func verifyTrustedBuilderID(certBuilderID, certTag string, expectedBuilderID *string, defaultTrustedBuilders map[string]bool) (*utils.TrustedBuilderID, bool, error) {
var trustedBuilderID *utils.TrustedBuilderID
var err error
// WARNING: we don't validate the tag here, because we need to allow
// refs/heads/main for e2e tests. See verifyTrustedBuilderRef().
// No builder ID provided by user: use the default trusted workflows.
if expectedBuilderID == nil || *expectedBuilderID == "" {
if _, ok := defaultTrustedBuilders[certBuilderID]; !ok {
return nil, false, fmt.Errorf("%w: %s with builderID provided: %t", serrors.ErrorUntrustedReusableWorkflow, certBuilderID, expectedBuilderID != nil)
}
// Construct the builderID using the certificate's builder's name and tag.
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderID+"@"+certTag, true)
if err != nil {
return nil, false, err
}
// Check if:
// - the builder in the cert is a BYOB builder
// - the caller trusts the BYOB builder
// If both are true, we don't match the user-provided builder ID
// against the certificate. Instead that will be done by the caller.
//
// This return of the delegator builderID enables non-compulsory
// builderID feature for BYOB builders by setting byob flag to true.
return trustedBuilderID, isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders), nil
}
// Verify the builderID.
// We only accept IDs on github.com.
trustedBuilderID, err = utils.TrustedBuilderIDNew(certBuilderID+"@"+certTag, true)
if err != nil {
return nil, false, err
}
// Check if:
// - the builder in the cert is a BYOB builder
// - the caller trusts the BYOB builder
// If both are true, we don't match the user-provided builder ID
// against the certificate. Instead that will be done by the caller.
if isTrustedDelegatorBuilder(trustedBuilderID, defaultTrustedBuilders) {
return trustedBuilderID, true, nil
}
// Not a BYOB builder. BuilderID provided by user should match the certificate.
// Note: the certificate builderID has the form `name@refs/tags/v1.2.3`,
// so we pass `allowRef = true`.
if err := trustedBuilderID.MatchesLoose(*expectedBuilderID, true); err != nil {
return nil, false, fmt.Errorf("%w: %v", serrors.ErrorUntrustedReusableWorkflow, err)
}
return trustedBuilderID, false, nil
}
func isTrustedDelegatorBuilder(certBuilder *utils.TrustedBuilderID, trustedBuilders map[string]bool) bool {
for byobBuilder := range defaultBYOBReusableWorkflows {
// Check that the certificate builder is a BYOB workflow.
if err := certBuilder.MatchesLoose(byobBuilder, true); err == nil {
// We found a delegator workflow that matches the certificate identity.
// Check that the BYOB builder is trusted by the caller.
if _, ok := trustedBuilders[byobBuilder]; !ok {
return false
}
return true
}
}
return false
}
// Only allow `@refs/heads/main` for the builder and the e2e tests that need to work at HEAD.
// This lets us use the pre-build builder binary generated during release (release happen at main).
// For other projects, we only allow semantic versions that map to a release.
func verifyTrustedBuilderRef(id *WorkflowIdentity, ref string) error {
if (id.SourceRepository == trustedBuilderRepository ||
id.SourceRepository == e2eTestRepository) &&
options.TestingEnabled() {
// Allow verification on the main branch to support e2e tests.
if ref == "refs/heads/main" {
return nil
}
return utils.IsValidBuilderTag(ref, true)
}
return utils.IsValidBuilderTag(ref, false)
}
func getExtension(cert *x509.Certificate, oid asn1.ObjectIdentifier, encoded bool) (string, error) {
for _, ext := range cert.Extensions {
if !ext.Id.Equal(oid) {
continue
}
if !encoded {
return string(ext.Value), nil
}
// Decode first.
var decoded string
rest, err := asn1.Unmarshal(ext.Value, &decoded)
if err != nil {
return "", fmt.Errorf("%w", err)
}
if len(rest) != 0 {
return "", fmt.Errorf("decoding has rest for oid %v", oid)
}
return decoded, nil
}
return "", nil
}
type Hosted int
const (
HostedSelf Hosted = iota
HostedGitHub
)
// WorkflowIdentity is a identity captured from a Fulcio certificate.
// See https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md.
type WorkflowIdentity struct {
// The source repository
SourceRepository string
// The commit SHA where the workflow was triggered.
SourceSha1 string
// Ref of the source.
SourceRef *string
// ID of the source repository.
SourceID *string
// Source owner ID of repository.
SourceOwnerID *string
// Workflow path OIDC subject - ref of reuseable workflow or trigger workflow.
SubjectWorkflow *url.URL
// Subject commit sha1.
SubjectSha1 *string
// Hosted status of the subject.
SubjectHosted *Hosted
// BuildTrigger
BuildTrigger string
// Build config path, i.e. the trigger workflow.
BuildConfigPath *string
// Run ID
RunID *string
// Issuer
Issuer string
}
// SubjectWorkflowName returns the subject workflow without the git ref.
func (id *WorkflowIdentity) SubjectWorkflowName() string {
// NOTE: You should be able to copy a net.URL struct safely.
// See: https://github.com/golang/go/issues/38351
withoutRef := *id.SubjectWorkflow
withoutRef.Path = id.SubjectWorkflowPath()
return withoutRef.String()
}
// SubjectWorkflowPath returns the subject workflow without the server url.
func (id *WorkflowIdentity) SubjectWorkflowPath() string {
i := strings.LastIndex(id.SubjectWorkflow.Path, "@")
if i == -1 {
return id.SubjectWorkflow.Path
}
return id.SubjectWorkflow.Path[:i]
}
// SubjectWorkflowRef returns the ref for the subject workflow.
func (id *WorkflowIdentity) SubjectWorkflowRef() string {
i := strings.LastIndex(id.SubjectWorkflow.Path, "@")
if i == -1 {
return ""
}
return id.SubjectWorkflow.Path[i+1:]
}
func getHosted(cert *x509.Certificate) (*Hosted, error) {
runnerEnv, err := getExtension(cert, fulcio.OIDRunnerEnvironment, true)
if err != nil {
return nil, err
}
if runnerEnv == "github-hosted" {
r := HostedGitHub
return &r, nil
}
if runnerEnv == "self-hosted" {
r := HostedSelf
return &r, nil
}
return nil, nil
}
func validateClaimsEqual(deprecated, existing string) error {
if deprecated != "" && existing != "" && deprecated != existing {
return fmt.Errorf("%w: '%v' != '%v'", serrors.ErrorInvalidFormat, deprecated, existing)
}
if deprecated == "" && existing == "" {
return fmt.Errorf("%w: claims are empty", serrors.ErrorInvalidFormat)
}
return nil
}
func getAndValidateEqualClaims(cert *x509.Certificate, deprecatedOid, oid asn1.ObjectIdentifier) (string, error) {
deprecatedValue, err := getExtension(cert, deprecatedOid, false)
if err != nil {
return "", err
}
value, err := getExtension(cert, oid, true)
if err != nil {
return "", err
}
if err := validateClaimsEqual(deprecatedValue, value); err != nil {
return "", err
}
// New certificates.
if value != "" {
return value, nil
}
// Old certificates.
if deprecatedValue != "" {
return deprecatedValue, nil
}
// Both values are empty.
return "", fmt.Errorf("%w: empty fields %v and %v", serrors.ErrorInvalidCertificate,
deprecatedOid, oid)
}
// GetWorkflowFromCertificate gets the workflow identity from the Fulcio authenticated content.
// See https://github.com/sigstore/fulcio/blob/e763d76e3f7786b52db4b27ab87dc446da24895a/pkg/certificate/extensions.go.
// https://github.com/golangci/golangci-lint/issues/741#issuecomment-784171870.
//
//nolint:staticcheck // we want to disable SA1019 only to use deprecated methods but there is a bug in golangci-lint.
func GetWorkflowInfoFromCertificate(cert *x509.Certificate) (*WorkflowIdentity, error) {
if len(cert.URIs) == 0 {
return nil, fmt.Errorf("%w: missing URI information from certificate", serrors.ErrorInvalidFormat)
}
// 1.3.6.1.4.1.57264.1.2: DEPRECATED.
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726412--github-workflow-BuildTrigger-deprecated
// 1.3.6.1.4.1.57264.1.20 | Build Trigger
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264120--build-trigger
buildTrigger, err := getAndValidateEqualClaims(cert, fulcio.OIDGitHubWorkflowTrigger, fulcio.OIDBuildTrigger)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.3: DEPRECATED.
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726413--github-workflow-sha-deprecated
// 1.3.6.1.4.1.57264.1.13 | Source Repository Digest
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264113--source-repository-digest
sourceSha1, err := getAndValidateEqualClaims(cert, fulcio.OIDGitHubWorkflowSHA, fulcio.OIDSourceRepositoryDigest)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.19 | Build Config Digest
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264119--build-config-digest
buildConfigSha1, err := getExtension(cert, fulcio.OIDBuildConfigDigest, true)
if err != nil {
return nil, err
}
if err := validateClaimsEqual(sourceSha1, buildConfigSha1); err != nil {
return nil, err
}
// IssuerV1: 1.3.6.1.4.1.57264.1.1
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726411--issuer
// IssuerV2: 1.3.6.1.4.1.57264.1.8
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726418--issuer-v2
issuer, err := getAndValidateEqualClaims(cert, fulcio.OIDIssuer, fulcio.OIDIssuerV2)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.5: DEPRECATED.
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#1361415726415--github-workflow-repository-deprecated
deprecatedSourceRepository, err := getExtension(cert, fulcio.OIDGitHubWorkflowRepository, false)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.12 | Source Repository URI
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264112--source-repository-uri
sourceURI, err := getExtension(cert, fulcio.OIDSourceRepositoryURI, true)
if err != nil {
return nil, err
}
if deprecatedSourceRepository != "" && sourceURI != "" &&
"https://github.com/"+deprecatedSourceRepository != sourceURI {
return nil, fmt.Errorf("%w: '%v' != '%v'",
serrors.ErrorInvalidFormat, "https://github.com/"+deprecatedSourceRepository, sourceURI)
}
sourceRepository := strings.TrimPrefix(sourceURI, "https://github.com/")
// Handle old certifcates.
if sourceRepository == "" {
sourceRepository = deprecatedSourceRepository
}
// 1.3.6.1.4.1.57264.1.10 | Build Signer Digest
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264110--build-signer-digest
subjectSha1, err := getExtension(cert, fulcio.OIDBuildSignerDigest, true)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.11 | Runner Environment
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264111--runner-environment
subjectHosted, err := getHosted(cert)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.14 | Source Repository Ref
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264114--source-repository-ref
sourceRef, err := getExtension(cert, fulcio.OIDSourceRepositoryRef, true)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.15 | Source Repository Identifier
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264115--source-repository-identifier
sourceID, err := getExtension(cert, fulcio.OIDSourceRepositoryIdentifier, true)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.17 | Source Repository Owner Identifier
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264117--source-repository-owner-identifier
sourceOwnerID, err := getExtension(cert, fulcio.OIDSourceRepositoryOwnerIdentifier, true)
if err != nil {
return nil, err
}
// 1.3.6.1.4.1.57264.1.18 | Build Config URI
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264118--build-config-uri
var buildConfigPath string
buildConfigURI, err := getExtension(cert, fulcio.OIDBuildConfigURI, true)
if err != nil {
return nil, err
}
if buildConfigURI != "" {
parts := strings.Split(buildConfigURI, "@")
if len(parts) != 2 {
return nil, fmt.Errorf("%w: %v",
serrors.ErrorInvalidFormat, buildConfigURI)
}
prefix := fmt.Sprintf("https://github.com/%v/", sourceRepository)
if !strings.HasPrefix(parts[0], prefix) {
return nil, fmt.Errorf("%w: prefix: %v",
serrors.ErrorInvalidFormat, parts[0])
}
buildConfigPath = strings.TrimPrefix(parts[0], prefix)
}
// 1.3.6.1.4.1.57264.1.21 | Run Invocation URI
// https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md#13614157264121--run-invocation-uri
runURI, err := getExtension(cert, fulcio.OIDRunInvocationURI, true)
if err != nil {
return nil, err
}
runID := strings.TrimPrefix(runURI, fmt.Sprintf("https://github.com/%s/actions/runs/", sourceRepository))
// Subject path.
if !strings.HasPrefix(cert.URIs[0].Path, "/") {
return nil, fmt.Errorf("%w: %s", serrors.ErrorInvalidFormat, cert.URIs[0].Path)
}
subjectWorkflow := cert.URIs[0]
var pSubjectSha1, pSourceID, pSourceRef, pSourceOwnerID, pBuildConfigPath, pRunID *string
if subjectSha1 != "" {
pSubjectSha1 = &subjectSha1
}
if sourceID != "" {
pSourceID = &sourceID
}
if sourceRef != "" {
pSourceRef = &sourceRef
}
if sourceOwnerID != "" {
pSourceOwnerID = &sourceOwnerID
}
if buildConfigPath != "" {
pBuildConfigPath = &buildConfigPath
}
if runID != "" {
pRunID = &runID
}
return &WorkflowIdentity{
// Issuer.
Issuer: issuer,
// Subject
SubjectWorkflow: subjectWorkflow,
SubjectSha1: pSubjectSha1,
SubjectHosted: subjectHosted,
// Source.
SourceRepository: sourceRepository,
SourceSha1: sourceSha1,
SourceRef: pSourceRef,
SourceID: pSourceID,
SourceOwnerID: pSourceOwnerID,
// Build.
BuildTrigger: buildTrigger,
BuildConfigPath: pBuildConfigPath,
// Other.
RunID: pRunID,
}, nil
}