From 9844f97320262d3df66ded6c7cbbc7878f0cc6da Mon Sep 17 00:00:00 2001 From: Hayden Blauzvern Date: Tue, 21 Mar 2023 03:38:37 +0000 Subject: [PATCH] Implement standardized CI extensions for GitHub This adds the set of standardized extensions and creates the mapping for GitHub Actions. All extension values are DER-encoded strings. This also creates a duplicated issuer extension to match the encoding that was used for the new extensions. OIDs 1.1 through 1.6 will be deprecated but still present in the certificates until a future major version of Fulcio. Updated the OID numbers so that the issuer is the first of the new OIDs. A future refactor will be ideal when implementing the extensions for other CI platforms. Signed-off-by: Hayden Blauzvern --- docs/oid-info.md | 45 ++- pkg/certificate/extensions.go | 291 +++++++++++++- pkg/certificate/extensions_test.go | 104 ++++- pkg/identity/github/issuer_test.go | 29 +- pkg/identity/github/principal.go | 139 +++++-- pkg/identity/github/principal_test.go | 534 +++++++++++++++++++++----- pkg/server/grpc_server_test.go | 168 +++++--- 7 files changed, 1100 insertions(+), 210 deletions(-) diff --git a/docs/oid-info.md b/docs/oid-info.md index a72e4aa95..defecfda8 100644 --- a/docs/oid-info.md +++ b/docs/oid-info.md @@ -45,6 +45,15 @@ Nice-to-haves: - Fully qualified URL: Complete URL with protocol. - `Digest`: Output of a cryptographic hash function, e.g. git commit SHA +## Extension values + +`1.3.6.1.4.1.57264.1.1` through `1.3.6.1.4.1.57264.1.6` are formatted as raw strings without any DER encoding. + +`1.3.6.1.4.1.57264.1.7` is formatted as a raw string, as per RFC 5280 4.2.1.6. + +`1.3.6.1.4.1.57264.1.8` through `1.3.6.1.4.1.57264.1.21` are formatted as DER-encoded strings; the ASN.1 tag is +UTF8String (0x0C) and the tag class is universal. + ## Directory Note that all values begin from the root OID 1.3.6.1.4.1.57264 [registered by Sigstore][oid-link]. @@ -97,60 +106,70 @@ the git ref that the workflow run was based upon. This specifies the username identity in the OtherName Subject Alternative Name, as defined by [RFC5280 4.2.1.6](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6). -### 1.3.6.1.4.1.57264.1.8 | Build Signer URI +### 1.3.6.1.4.1.57264.1.8 | Issuer (V2) + +This contains the `iss` claim from the OIDC Identity Token that was +presented at the time the code signing certificate was requested to be created. +This claim is the URI of the OIDC Identity Provider that digitally signed the +identity token. For example: `https://oidc-issuer.com`. + +The difference between this extension and `1.3.6.1.4.1.57264.1.1` is that the extension value +is formatted to the RFC 5280 specification. + +### 1.3.6.1.4.1.57264.1.9 | Build Signer URI Reference to specific build instructions that are responsible for signing. SHOULD be fully qualified. MAY be the same as Build Config URI. Build Signer URI is also included in the Subject Alternative Name. For example a reusable workflow ref in GitHub Actions or a Circle CI Orb name/version. For example: `https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.4.0`. -### 1.3.6.1.4.1.57264.1.9 | Build Signer Digest +### 1.3.6.1.4.1.57264.1.10 | Build Signer Digest Immutable reference to the specific version of the build instructions that is responsible for signing. For example: `abc123` git commit SHA. -### 1.3.6.1.4.1.57264.1.10 | Runner Environment +### 1.3.6.1.4.1.57264.1.11 | Runner Environment Runner Environment specifying whether the build took place in platform-hosted cloud infrastructure or customer/self-hosted infrastructure. For example: `[platform]-hosted` and `self-hosted`. -### 1.3.6.1.4.1.57264.1.11 | Source Repository URI +### 1.3.6.1.4.1.57264.1.12 | Source Repository URI Source repository URL that the build was based on. SHOULD be fully qualified. For example: `https://example.com/owner/repository`. -### 1.3.6.1.4.1.57264.1.12 | Source Repository Digest +### 1.3.6.1.4.1.57264.1.13 | Source Repository Digest Immutable reference to a specific version of the source code that the build was based upon. For example: `abc123` git commit SHA. -### 1.3.6.1.4.1.57264.1.13 | Source Repository Ref +### 1.3.6.1.4.1.57264.1.14 | Source Repository Ref Source Repository Ref that the build run was based upon. For example: `refs/head/main` git branch or tag. -### 1.3.6.1.4.1.57264.1.14 | Source Repository Identifier +### 1.3.6.1.4.1.57264.1.15 | Source Repository Identifier Immutable identifier for the source repository the workflow was based upon. MAY be empty if the Source Repository URI is immutable. For example: `1234` if using a primary key. -### 1.3.6.1.4.1.57264.1.15 | Source Repository Owner URI +### 1.3.6.1.4.1.57264.1.16 | Source Repository Owner URI Source repository owner URL of the owner of the source repository that the build was based on. SHOULD be fully qualified. MAY be empty if there is no Source Repository Owner. For example: `https://example.com/owner` -### 1.3.6.1.4.1.57264.1.16 | Source Repository Owner Identifier +### 1.3.6.1.4.1.57264.1.17 | Source Repository Owner Identifier Immutable identifier for the owner of the source repository that the workflow was based upon. MAY be empty if there is no Source Repository Owner or Source Repository Owner URI is immutable. For example: `5678` if using a primary key. -### 1.3.6.1.4.1.57264.1.17 | Build Config URI +### 1.3.6.1.4.1.57264.1.18 | Build Config URI Build Config URL to the top-level/initiating build instructions. SHOULD be fully qualified. For example: `https://example.com/owner/repository/build-config.yml`. -### 1.3.6.1.4.1.57264.1.18 | Build Config Digest +### 1.3.6.1.4.1.57264.1.19 | Build Config Digest Immutable reference to the specific version of the top-level/initiating build instructions. For example: `abc123` git commit SHA. -### 1.3.6.1.4.1.57264.1.19 | Build Trigger +### 1.3.6.1.4.1.57264.1.20 | Build Trigger Event or action that initiated the build. For example: `push`. -### 1.3.6.1.4.1.57264.1.20 | Run Invocation URI +### 1.3.6.1.4.1.57264.1.21 | Run Invocation URI Run Invocation URL to uniquely identify the build execution. SHOULD be fully qualified. For example: `https://github.com/example/repository/actions/runs/1536140711/attempts/1`. diff --git a/pkg/certificate/extensions.go b/pkg/certificate/extensions.go index b58a7bc93..54e55ef1b 100644 --- a/pkg/certificate/extensions.go +++ b/pkg/certificate/extensions.go @@ -18,16 +18,36 @@ import ( "crypto/x509/pkix" "encoding/asn1" "errors" + "fmt" ) var ( + // BEGIN: Deprecated OIDIssuer = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1} OIDGitHubWorkflowTrigger = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2} OIDGitHubWorkflowSHA = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3} OIDGitHubWorkflowName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4} OIDGitHubWorkflowRepository = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5} OIDGitHubWorkflowRef = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6} - OIDOtherName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 7} + // END: Deprecated + + OIDOtherName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 7} + OIDIssuerV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8} + + // CI extensions + OIDBuildSignerURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 9} + OIDBuildSignerDigest = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 10} + OIDRunnerEnvironment = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 11} + OIDSourceRepositoryURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 12} + OIDSourceRepositoryDigest = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 13} + OIDSourceRepositoryRef = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 14} + OIDSourceRepositoryIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 15} + OIDSourceRepositoryOwnerURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 16} + OIDSourceRepositoryOwnerIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 17} + OIDBuildConfigURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 18} + OIDBuildConfigDigest = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 19} + OIDBuildTrigger = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 20} + OIDRunInvocationURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 21} ) // Extensions contains all custom x509 extensions defined by Fulcio @@ -39,33 +59,79 @@ type Extensions struct { // a federated login like Dex it should match the issuer URL of the // upstream issuer. The issuer is not set the extensions are invalid and // will fail to render. - Issuer string // OID 1.3.6.1.4.1.57264.1.1 + Issuer string // OID 1.3.6.1.4.1.57264.1.8 and 1.3.6.1.4.1.57264.1.1 (Deprecated) + // Deprecated // Triggering event of the Github Workflow. Matches the `event_name` claim of ID // tokens from Github Actions GithubWorkflowTrigger string // OID 1.3.6.1.4.1.57264.1.2 + // Deprecated // SHA of git commit being built in Github Actions. Matches the `sha` claim of ID // tokens from Github Actions GithubWorkflowSHA string // OID 1.3.6.1.4.1.57264.1.3 + // Deprecated // Name of Github Actions Workflow. Matches the `workflow` claim of the ID // tokens from Github Actions GithubWorkflowName string // OID 1.3.6.1.4.1.57264.1.4 + // Deprecated // Repository of the Github Actions Workflow. Matches the `repository` claim of the ID // tokens from Github Actions GithubWorkflowRepository string // OID 1.3.6.1.4.1.57264.1.5 + // Deprecated // Git Ref of the Github Actions Workflow. Matches the `ref` claim of the ID tokens // from Github Actions GithubWorkflowRef string // 1.3.6.1.4.1.57264.1.6 + + // Reference to specific build instructions that are responsible for signing. + BuildSignerURI string // 1.3.6.1.4.1.57264.1.9 + + // Immutable reference to the specific version of the build instructions that is responsible for signing. + BuildSignerDigest string // 1.3.6.1.4.1.57264.1.10 + + // Specifies whether the build took place in platform-hosted cloud infrastructure or customer/self-hosted infrastructure. + RunnerEnvironment string // 1.3.6.1.4.1.57264.1.11 + + // Source repository URL that the build was based on. + SourceRepositoryURI string // 1.3.6.1.4.1.57264.1.12 + + // Immutable reference to a specific version of the source code that the build was based upon. + SourceRepositoryDigest string // 1.3.6.1.4.1.57264.1.13 + + // Source Repository Ref that the build run was based upon. + SourceRepositoryRef string // 1.3.6.1.4.1.57264.1.14 + + // Immutable identifier for the source repository the workflow was based upon. + SourceRepositoryIdentifier string // 1.3.6.1.4.1.57264.1.15 + + // Source repository owner URL of the owner of the source repository that the build was based on. + SourceRepositoryOwnerURI string // 1.3.6.1.4.1.57264.1.16 + + // Immutable identifier for the owner of the source repository that the workflow was based upon. + SourceRepositoryOwnerIdentifier string // 1.3.6.1.4.1.57264.1.17 + + // Build Config URL to the top-level/initiating build instructions. + BuildConfigURI string // 1.3.6.1.4.1.57264.1.18 + + // Immutable reference to the specific version of the top-level/initiating build instructions. + BuildConfigDigest string // 1.3.6.1.4.1.57264.1.19 + + // Event or action that initiated the build. + BuildTrigger string // 1.3.6.1.4.1.57264.1.20 + + // Run Invocation URL to uniquely identify the build execution. + RunInvocationURI string // 1.3.6.1.4.1.57264.1.21 } func (e Extensions) Render() ([]pkix.Extension, error) { var exts []pkix.Extension + // BEGIN: Deprecated if e.Issuer != "" { + // deprecated issuer extension due to incorrect encoding exts = append(exts, pkix.Extension{ Id: OIDIssuer, Value: []byte(e.Issuer), @@ -103,14 +169,163 @@ func (e Extensions) Render() ([]pkix.Extension, error) { Value: []byte(e.GithubWorkflowRef), }) } + // END: Deprecated + + // duplicate issuer with correct RFC 5280 encoding + if e.Issuer != "" { + // construct DER encoding of issuer string + val, err := asn1.MarshalWithParams(e.Issuer, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDIssuerV2, + Value: val, + }) + } else { + return nil, errors.New("extensions must have a non-empty issuer url") + } + + if e.BuildSignerURI != "" { + val, err := asn1.MarshalWithParams(e.BuildSignerURI, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDBuildSignerURI, + Value: val, + }) + } + if e.BuildSignerDigest != "" { + val, err := asn1.MarshalWithParams(e.BuildSignerDigest, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDBuildSignerDigest, + Value: val, + }) + } + if e.RunnerEnvironment != "" { + val, err := asn1.MarshalWithParams(e.RunnerEnvironment, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDRunnerEnvironment, + Value: val, + }) + } + if e.SourceRepositoryURI != "" { + val, err := asn1.MarshalWithParams(e.SourceRepositoryURI, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDSourceRepositoryURI, + Value: val, + }) + } + if e.SourceRepositoryDigest != "" { + val, err := asn1.MarshalWithParams(e.SourceRepositoryDigest, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDSourceRepositoryDigest, + Value: val, + }) + } + if e.SourceRepositoryRef != "" { + val, err := asn1.MarshalWithParams(e.SourceRepositoryRef, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDSourceRepositoryRef, + Value: val, + }) + } + if e.SourceRepositoryIdentifier != "" { + val, err := asn1.MarshalWithParams(e.SourceRepositoryIdentifier, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDSourceRepositoryIdentifier, + Value: val, + }) + } + if e.SourceRepositoryOwnerURI != "" { + val, err := asn1.MarshalWithParams(e.SourceRepositoryOwnerURI, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDSourceRepositoryOwnerURI, + Value: val, + }) + } + if e.SourceRepositoryOwnerIdentifier != "" { + val, err := asn1.MarshalWithParams(e.SourceRepositoryOwnerIdentifier, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDSourceRepositoryOwnerIdentifier, + Value: val, + }) + } + if e.BuildConfigURI != "" { + val, err := asn1.MarshalWithParams(e.BuildConfigURI, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDBuildConfigURI, + Value: val, + }) + } + if e.BuildConfigDigest != "" { + val, err := asn1.MarshalWithParams(e.BuildConfigDigest, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDBuildConfigDigest, + Value: val, + }) + } + if e.BuildTrigger != "" { + val, err := asn1.MarshalWithParams(e.BuildTrigger, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDBuildTrigger, + Value: val, + }) + } + if e.RunInvocationURI != "" { + val, err := asn1.MarshalWithParams(e.RunInvocationURI, "utf8") + if err != nil { + return nil, err + } + exts = append(exts, pkix.Extension{ + Id: OIDRunInvocationURI, + Value: val, + }) + } + return exts, nil } -func ParseExtensions(ext []pkix.Extension) (Extensions, error) { +func parseExtensions(ext []pkix.Extension) (Extensions, error) { out := Extensions{} for _, e := range ext { switch { + // BEGIN: Deprecated case e.Id.Equal(OIDIssuer): out.Issuer = string(e.Value) case e.Id.Equal(OIDGitHubWorkflowTrigger): @@ -123,6 +338,63 @@ func ParseExtensions(ext []pkix.Extension) (Extensions, error) { out.GithubWorkflowRepository = string(e.Value) case e.Id.Equal(OIDGitHubWorkflowRef): out.GithubWorkflowRef = string(e.Value) + // END: Deprecated + case e.Id.Equal(OIDIssuerV2): + if err := parseDERString(e.Value, &out.Issuer); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDBuildSignerURI): + if err := parseDERString(e.Value, &out.BuildSignerURI); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDBuildSignerDigest): + if err := parseDERString(e.Value, &out.BuildSignerDigest); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDRunnerEnvironment): + if err := parseDERString(e.Value, &out.RunnerEnvironment); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDSourceRepositoryURI): + if err := parseDERString(e.Value, &out.SourceRepositoryURI); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDSourceRepositoryDigest): + if err := parseDERString(e.Value, &out.SourceRepositoryDigest); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDSourceRepositoryRef): + if err := parseDERString(e.Value, &out.SourceRepositoryRef); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDSourceRepositoryIdentifier): + if err := parseDERString(e.Value, &out.SourceRepositoryIdentifier); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDSourceRepositoryOwnerURI): + if err := parseDERString(e.Value, &out.SourceRepositoryOwnerURI); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDSourceRepositoryOwnerIdentifier): + if err := parseDERString(e.Value, &out.SourceRepositoryOwnerIdentifier); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDBuildConfigURI): + if err := parseDERString(e.Value, &out.BuildConfigURI); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDBuildConfigDigest): + if err := parseDERString(e.Value, &out.BuildConfigDigest); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDBuildTrigger): + if err := parseDERString(e.Value, &out.BuildTrigger); err != nil { + return Extensions{}, err + } + case e.Id.Equal(OIDRunInvocationURI): + if err := parseDERString(e.Value, &out.RunInvocationURI); err != nil { + return Extensions{}, err + } } } @@ -130,3 +402,16 @@ func ParseExtensions(ext []pkix.Extension) (Extensions, error) { // more complex parsing of fields in a backwards compatible way if needed. return out, nil } + +// parseDERString decodes a DER-encoded string and puts the value in parsedVal. +// Rerturns an error if the unmarshalling fails or if there are trailing bytes in the encoding. +func parseDERString(val []byte, parsedVal *string) error { + rest, err := asn1.Unmarshal(val, parsedVal) + if err != nil { + return fmt.Errorf("unexpected error unmarshalling DER-encoded string: %v", err) + } + if len(rest) != 0 { + return errors.New("unexpected trailing bytes in DER-encoded string") + } + return nil +} diff --git a/pkg/certificate/extensions_test.go b/pkg/certificate/extensions_test.go index 1ae5e9252..223ebc47a 100644 --- a/pkg/certificate/extensions_test.go +++ b/pkg/certificate/extensions_test.go @@ -16,6 +16,7 @@ package certificate import ( "crypto/x509/pkix" + "encoding/asn1" "testing" @@ -36,37 +37,106 @@ func TestExtensions(t *testing.T) { }, `complete extensions list should create all extensions with correct OIDs`: { Extensions: Extensions{ - Issuer: `1`, // OID 1.3.6.1.4.1.57264.1.1 - GithubWorkflowTrigger: `2`, // OID 1.3.6.1.4.1.57264.1.2 - GithubWorkflowSHA: `3`, // OID 1.3.6.1.4.1.57264.1.3 - GithubWorkflowName: `4`, // OID 1.3.6.1.4.1.57264.1.4 - GithubWorkflowRepository: `5`, // OID 1.3.6.1.4.1.57264.1.5 - GithubWorkflowRef: `6`, // 1.3.6.1.4.1.57264.1.6 + Issuer: "issuer", // OID 1.3.6.1.4.1.57264.1.1 and 1.3.6.1.4.1.57264.1.8 + GithubWorkflowTrigger: "2", // OID 1.3.6.1.4.1.57264.1.2 + GithubWorkflowSHA: "3", // OID 1.3.6.1.4.1.57264.1.3 + GithubWorkflowName: "4", // OID 1.3.6.1.4.1.57264.1.4 + GithubWorkflowRepository: "5", // OID 1.3.6.1.4.1.57264.1.5 + GithubWorkflowRef: "6", // 1.3.6.1.4.1.57264.1.6 + BuildSignerURI: "9", // 1.3.6.1.4.1.57264.1.9 + BuildSignerDigest: "10", // 1.3.6.1.4.1.57264.1.10 + RunnerEnvironment: "11", // 1.3.6.1.4.1.57264.1.11 + SourceRepositoryURI: "12", // 1.3.6.1.4.1.57264.1.12 + SourceRepositoryDigest: "13", // 1.3.6.1.4.1.57264.1.13 + SourceRepositoryRef: "14", // 1.3.6.1.4.1.57264.1.14 + SourceRepositoryIdentifier: "15", // 1.3.6.1.4.1.57264.1.15 + SourceRepositoryOwnerURI: "16", // 1.3.6.1.4.1.57264.1.16 + SourceRepositoryOwnerIdentifier: "17", // 1.3.6.1.4.1.57264.1.17 + BuildConfigURI: "18", // 1.3.6.1.4.1.57264.1.18 + BuildConfigDigest: "19", // 1.3.6.1.4.1.57264.1.19 + BuildTrigger: "20", // 1.3.6.1.4.1.57264.1.20 + RunInvocationURI: "21", // 1.3.6.1.4.1.57264.1.21 }, Expect: []pkix.Extension{ { Id: OIDIssuer, - Value: []byte(`1`), + Value: []byte("issuer"), }, { Id: OIDGitHubWorkflowTrigger, - Value: []byte(`2`), + Value: []byte("2"), }, { Id: OIDGitHubWorkflowSHA, - Value: []byte(`3`), + Value: []byte("3"), }, { Id: OIDGitHubWorkflowName, - Value: []byte(`4`), + Value: []byte("4"), }, { Id: OIDGitHubWorkflowRepository, - Value: []byte(`5`), + Value: []byte("5"), }, { Id: OIDGitHubWorkflowRef, - Value: []byte(`6`), + Value: []byte("6"), + }, + { + Id: OIDIssuerV2, + Value: marshalDERString(t, "issuer"), + }, + { + Id: OIDBuildSignerURI, + Value: marshalDERString(t, "9"), + }, + { + Id: OIDBuildSignerDigest, + Value: marshalDERString(t, "10"), + }, + { + Id: OIDRunnerEnvironment, + Value: marshalDERString(t, "11"), + }, + { + Id: OIDSourceRepositoryURI, + Value: marshalDERString(t, "12"), + }, + { + Id: OIDSourceRepositoryDigest, + Value: marshalDERString(t, "13"), + }, + { + Id: OIDSourceRepositoryRef, + Value: marshalDERString(t, "14"), + }, + { + Id: OIDSourceRepositoryIdentifier, + Value: marshalDERString(t, "15"), + }, + { + Id: OIDSourceRepositoryOwnerURI, + Value: marshalDERString(t, "16"), + }, + { + Id: OIDSourceRepositoryOwnerIdentifier, + Value: marshalDERString(t, "17"), + }, + { + Id: OIDBuildConfigURI, + Value: marshalDERString(t, "18"), + }, + { + Id: OIDBuildConfigDigest, + Value: marshalDERString(t, "19"), + }, + { + Id: OIDBuildTrigger, + Value: marshalDERString(t, "20"), + }, + { + Id: OIDRunInvocationURI, + Value: marshalDERString(t, "21"), }, }, WantErr: false, @@ -87,7 +157,7 @@ func TestExtensions(t *testing.T) { t.Errorf("Render: %s", diff) } - parse, err := ParseExtensions(render) + parse, err := parseExtensions(render) if err != nil { t.Fatalf("ParseExtensions: err = %v", err) } @@ -97,3 +167,11 @@ func TestExtensions(t *testing.T) { }) } } + +func marshalDERString(t *testing.T, val string) []byte { + derString, err := asn1.MarshalWithParams(val, "utf8") + if err != nil { + t.Fatalf("error marshalling string %v", err) + } + return derString +} diff --git a/pkg/identity/github/issuer_test.go b/pkg/identity/github/issuer_test.go index 9ddf1035c..8d952fa09 100644 --- a/pkg/identity/github/issuer_test.go +++ b/pkg/identity/github/issuer_test.go @@ -44,16 +44,25 @@ func TestIssuer(t *testing.T) { Subject: "repo:sigstore/fulcio:ref:refs/heads/main", } claims, err := json.Marshal(map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "ref": "refs/heads/main", - "repository": "sigstore/fulcio", - "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }) if err != nil { t.Fatal(err) diff --git a/pkg/identity/github/principal.go b/pkg/identity/github/principal.go index 9f1201ad3..e019bd3b0 100644 --- a/pkg/identity/github/principal.go +++ b/pkg/identity/github/principal.go @@ -34,33 +34,73 @@ type workflowPrincipal struct { // https://token.actions.githubusercontent.com/.well-known/openid-configution issuer string - // the final certificate. + // URL of issuer url string // Commit SHA being built sha string - // Event that triggered this workflow run. E.g "push", "tag" etc - trigger string + // Event that triggered this workflow run. E.g "push", "tag" + eventName string - // Repository building built + // Name of repository being built repository string - // Workflow that is running + // Deprecated + // Name of workflow that is running (mutable) workflow string // Git ref being built ref string + + // Specific build instructions (i.e. reusable workflow) + jobWorkflowRef string + + // Commit SHA to specific build instructions + jobWorkflowSha string + + // Whether the build took place in cloud or self-hosted infrastructure + runnerEnvironment string + + // ID to the source repo + repositoryID string + + // Owner of the source repo (mutable) + repositoryOwner string + + // ID of the source repo + repositoryOwnerID string + + // Ref of top-level workflow that is running + workflowRef string + + // Commit SHA of top-level workflow that is running + workflowSha string + + // ID of workflow run + runID string + + // Attempt number of workflow run + runAttempt string } func WorkflowPrincipalFromIDToken(ctx context.Context, token *oidc.IDToken) (identity.Principal, error) { var claims struct { - JobWorkflowRef string `json:"job_workflow_ref"` - Sha string `json:"sha"` - Trigger string `json:"event_name"` - Repository string `json:"repository"` - Workflow string `json:"workflow"` - Ref string `json:"ref"` + JobWorkflowRef string `json:"job_workflow_ref"` + Sha string `json:"sha"` + EventName string `json:"event_name"` + Repository string `json:"repository"` + Workflow string `json:"workflow"` + Ref string `json:"ref"` + JobWorkflowSha string `json:"job_workflow_sha"` + RunnerEnvironment string `json:"runner_environment"` + RepositoryID string `json:"repository_id"` + RepositoryOwner string `json:"repository_owner"` + RepositoryOwnerID string `json:"repository_owner_id"` + WorkflowRef string `json:"workflow_ref"` + WorkflowSha string `json:"workflow_sha"` + RunID string `json:"run_id"` + RunAttempt string `json:"run_attempt"` } if err := token.Claims(&claims); err != nil { return nil, err @@ -72,7 +112,7 @@ func WorkflowPrincipalFromIDToken(ctx context.Context, token *oidc.IDToken) (ide if claims.Sha == "" { return nil, errors.New("missing sha claim in ID token") } - if claims.Trigger == "" { + if claims.EventName == "" { return nil, errors.New("missing event_name claim in ID token") } if claims.Repository == "" { @@ -84,16 +124,53 @@ func WorkflowPrincipalFromIDToken(ctx context.Context, token *oidc.IDToken) (ide if claims.Ref == "" { return nil, errors.New("missing ref claim in ID token") } + if claims.JobWorkflowSha == "" { + return nil, errors.New("missing job_workflow_sha claim in ID token") + } + if claims.RunnerEnvironment == "" { + return nil, errors.New("missing runner_environment claim in ID token") + } + if claims.RepositoryID == "" { + return nil, errors.New("missing repository_id claim in ID token") + } + if claims.RepositoryOwner == "" { + return nil, errors.New("missing repository_owner claim in ID token") + } + if claims.RepositoryOwnerID == "" { + return nil, errors.New("missing repository_owner_id claim in ID token") + } + if claims.WorkflowRef == "" { + return nil, errors.New("missing workflow_ref claim in ID token") + } + if claims.WorkflowSha == "" { + return nil, errors.New("missing workflow_sha claim in ID token") + } + if claims.RunID == "" { + return nil, errors.New("missing run_id claim in ID token") + } + if claims.RunAttempt == "" { + return nil, errors.New("missing run_attempt claim in ID token") + } return &workflowPrincipal{ - subject: token.Subject, - issuer: token.Issuer, - url: `https://github.com/` + claims.JobWorkflowRef, - sha: claims.Sha, - trigger: claims.Trigger, - repository: claims.Repository, - workflow: claims.Workflow, - ref: claims.Ref, + subject: token.Subject, + issuer: token.Issuer, + url: `https://github.com/`, + sha: claims.Sha, + eventName: claims.EventName, + repository: claims.Repository, + workflow: claims.Workflow, + ref: claims.Ref, + jobWorkflowRef: claims.JobWorkflowRef, + jobWorkflowSha: claims.JobWorkflowSha, + runnerEnvironment: claims.RunnerEnvironment, + repositoryID: claims.RepositoryID, + repositoryOwner: claims.RepositoryOwner, + repositoryOwnerID: claims.RepositoryOwnerID, + workflowRef: claims.WorkflowRef, + workflowSha: claims.WorkflowSha, + runID: claims.RunID, + runAttempt: claims.RunAttempt, }, nil } @@ -103,7 +180,7 @@ func (w workflowPrincipal) Name(ctx context.Context) string { func (w workflowPrincipal) Embed(ctx context.Context, cert *x509.Certificate) error { // Set workflow URL to SubjectAlternativeName on certificate - parsed, err := url.Parse(w.url) + parsed, err := url.Parse(w.url + w.jobWorkflowRef) if err != nil { return err } @@ -111,12 +188,28 @@ func (w workflowPrincipal) Embed(ctx context.Context, cert *x509.Certificate) er // Embed additional information into custom extensions cert.ExtraExtensions, err = certificate.Extensions{ - Issuer: w.issuer, - GithubWorkflowTrigger: w.trigger, + Issuer: w.issuer, + // BEGIN: Deprecated + GithubWorkflowTrigger: w.eventName, GithubWorkflowSHA: w.sha, GithubWorkflowName: w.workflow, GithubWorkflowRepository: w.repository, GithubWorkflowRef: w.ref, + // END: Deprecated + + BuildSignerURI: w.url + w.jobWorkflowRef, + BuildSignerDigest: w.jobWorkflowSha, + RunnerEnvironment: w.runnerEnvironment, + SourceRepositoryURI: w.url + w.repository, + SourceRepositoryDigest: w.sha, + SourceRepositoryRef: w.ref, + SourceRepositoryIdentifier: w.repositoryID, + SourceRepositoryOwnerURI: w.url + w.repositoryOwner, + SourceRepositoryOwnerIdentifier: w.repositoryOwnerID, + BuildConfigURI: w.url + w.workflowRef, + BuildConfigDigest: w.workflowSha, + BuildTrigger: w.eventName, + RunInvocationURI: w.url + w.repository + "/actions/runs/" + w.runID + "/attempts/" + w.runAttempt, }.Render() if err != nil { return err diff --git a/pkg/identity/github/principal_test.go b/pkg/identity/github/principal_test.go index 3dbabcea3..6ec3d6ff1 100644 --- a/pkg/identity/github/principal_test.go +++ b/pkg/identity/github/principal_test.go @@ -40,119 +40,408 @@ func TestWorkflowPrincipalFromIDToken(t *testing.T) { }{ `Valid token authenticates with correct claims`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "ref": "refs/heads/main", - "repository": "sigstore/fulcio", - "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, ExpectPrincipal: workflowPrincipal{ - issuer: "https://token.actions.githubusercontent.com", - subject: "repo:sigstore/fulcio:ref:refs/heads/main", - url: "https://github.com/sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - trigger: "push", - repository: "sigstore/fulcio", - workflow: "foo", - ref: "refs/heads/main", + issuer: "https://token.actions.githubusercontent.com", + subject: "repo:sigstore/fulcio:ref:refs/heads/main", + url: "https://github.com/", + jobWorkflowRef: "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + eventName: "push", + repository: "sigstore/fulcio", + workflow: "foo", + ref: "refs/heads/main", + jobWorkflowSha: "example-sha", + runnerEnvironment: "cloud-hosted", + repositoryID: "12345", + repositoryOwner: "username", + repositoryOwnerID: "345", + workflowRef: "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + workflowSha: "example-sha-other", + runID: "42", + runAttempt: "1", }, WantErr: false, }, `Token missing job_workflow_ref claim should be rejected`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "ref": "refs/heads/main", - "repository": "sigstore/fulcio", - "sha": "bf96275471e83ff04ce5c8eb515c04a75d43f854", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, WantErr: true, ErrContains: "job_workflow_ref", }, `Token missing sha should be rejected`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "ref": "refs/heads/main", - "repository": "sigstore/fulcio", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, WantErr: true, ErrContains: "sha", }, `Token missing event_name claim should be rejected`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "ref": "refs/heads/main", - "repository": "sigstore/fulcio", - "sha": "bf96275471e83ff04ce5c8eb515c04a75d43f854", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, WantErr: true, ErrContains: "event_name", }, `Token missing repository claim should be rejected`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "ref": "refs/heads/main", - "sha": "bf96275471e83ff04ce5c8eb515c04a75d43f854", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, WantErr: true, ErrContains: "repository", }, `Token missing workflow claim should be rejected`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "ref": "refs/heads/main", - "repository": "sigstore/fulcio", - "sha": "bf96275471e83ff04ce5c8eb515c04a75d43f854", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, WantErr: true, ErrContains: "workflow", }, `Token missing ref claim should be rejected`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "repository": "sigstore/fulcio", - "sha": "bf96275471e83ff04ce5c8eb515c04a75d43f854", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, WantErr: true, ErrContains: "ref", }, + `Token missing job_workflow_sha claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "job_workflow_sha", + }, + `Token missing runner_environment claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "runner_environment", + }, + `Token missing repository_id claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "repository_id", + }, + `Token missing repository_owner claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "repository_owner", + }, + `Token missing repository_owner_id claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "repository_owner_id", + }, + `Token missing workflow_ref claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "workflow_ref", + }, + `Token missing workflow_sha claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + }, + WantErr: true, + ErrContains: "workflow_sha", + }, + `Token missing run_id claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "run_id", + }, + `Token missing run_attempt claim should be rejected`: { + Claims: map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", + }, + WantErr: true, + ErrContains: "run_attempt", + }, } for name, test := range tests { @@ -209,16 +498,25 @@ func TestName(t *testing.T) { }{ `Valid token authenticates with correct claims`: { Claims: map[string]interface{}{ - "aud": "sigstore", - "event_name": "push", - "exp": 0, - "iss": "https://token.actions.githubusercontent.com", - "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", - "ref": "refs/heads/main", - "repository": "sigstore/fulcio", - "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "sub": "repo:sigstore/fulcio:ref:refs/heads/main", - "workflow": "foo", + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "job_workflow_sha": "example-sha", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "repository_id": "12345", + "repository_owner": "username", + "repository_owner_id": "345", + "run_attempt": "1", + "run_id": "42", + "runner_environment": "cloud-hosted", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + "workflow_ref": "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + "workflow_sha": "example-sha-other", }, ExpectName: "repo:sigstore/fulcio:ref:refs/heads/main", }, @@ -258,23 +556,47 @@ func TestEmbed(t *testing.T) { }{ `Github workflow challenge should have all Github workflow extensions and issuer set`: { Principal: &workflowPrincipal{ - issuer: "https://token.actions.githubusercontent.com", - subject: "doesntmatter", - url: `https://github.com/foo/bar/`, - sha: "sha", - trigger: "trigger", - workflow: "workflowname", - repository: "repository", - ref: "ref", + issuer: "https://token.actions.githubusercontent.com", + subject: "doesntmatter", + url: `https://github.com/`, + sha: "sha", + eventName: "trigger", + workflow: "workflowname", + repository: "repository", + ref: "ref", + jobWorkflowRef: "jobWorkflowRef", + jobWorkflowSha: "jobWorkflowSha", + runnerEnvironment: "runnerEnv", + repositoryID: "repoID", + repositoryOwner: "repoOwner", + repositoryOwnerID: "repoOwnerID", + workflowRef: "workflowRef", + workflowSha: "workflowSHA", + runID: "runID", + runAttempt: "runAttempt", }, WantErr: false, WantFacts: map[string]func(x509.Certificate) error{ - `Certifificate should have correct issuer`: factIssuerIs(`https://token.actions.githubusercontent.com`), - `Certificate has correct trigger extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}, "trigger"), - `Certificate has correct SHA extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}, "sha"), - `Certificate has correct workflow extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}, "workflowname"), - `Certificate has correct repository extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5}, "repository"), - `Certificate has correct ref extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}, "ref"), + `Certifificate should have correct issuer`: factDeprecatedExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, "https://token.actions.githubusercontent.com"), + `Certificate has correct trigger extension`: factDeprecatedExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}, "trigger"), + `Certificate has correct SHA extension`: factDeprecatedExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}, "sha"), + `Certificate has correct workflow extension`: factDeprecatedExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}, "workflowname"), + `Certificate has correct repository extension`: factDeprecatedExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5}, "repository"), + `Certificate has correct ref extension`: factDeprecatedExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}, "ref"), + `Certificate has correct issuer (v2) extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8}, "https://token.actions.githubusercontent.com"), + `Certificate has correct builder signer URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 9}, "https://github.com/jobWorkflowRef"), + `Certificate has correct builder signer digest extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 10}, "jobWorkflowSha"), + `Certificate has correct runner environment extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 11}, "runnerEnv"), + `Certificate has correct source repo URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 12}, "https://github.com/repository"), + `Certificate has correct source repo digest extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 13}, "sha"), + `Certificate has correct source repo ref extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 14}, "ref"), + `Certificate has correct source repo ID extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 15}, "repoID"), + `Certificate has correct source repo owner URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 16}, "https://github.com/repoOwner"), + `Certificate has correct source repo owner ID extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 17}, "repoOwnerID"), + `Certificate has correct build config URI extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 18}, "https://github.com/workflowRef"), + `Certificate has correct build config digest extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 19}, "workflowSHA"), + `Certificate has correct build trigger extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 20}, "trigger"), + `Certificate has correct run invocation ID extension`: factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 21}, "https://github.com/repository/actions/runs/runID/attempts/runAttempt"), }, }, `Github workflow value with bad URL fails`: { @@ -282,7 +604,7 @@ func TestEmbed(t *testing.T) { subject: "doesntmatter", url: "\nbadurl", sha: "sha", - trigger: "trigger", + eventName: "trigger", workflow: "workflowname", repository: "repository", ref: "ref", @@ -314,11 +636,23 @@ func TestEmbed(t *testing.T) { } } -func factIssuerIs(issuer string) func(x509.Certificate) error { - return factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, issuer) +func factExtensionIs(oid asn1.ObjectIdentifier, value string) func(x509.Certificate) error { + return func(cert x509.Certificate) error { + for _, ext := range cert.ExtraExtensions { + if ext.Id.Equal(oid) { + var strVal string + _, _ = asn1.Unmarshal(ext.Value, &strVal) + if value != strVal { + return fmt.Errorf("expected oid %v to be %s, but got %s", oid, value, strVal) + } + return nil + } + } + return errors.New("extension not set") + } } -func factExtensionIs(oid asn1.ObjectIdentifier, value string) func(x509.Certificate) error { +func factDeprecatedExtensionIs(oid asn1.ObjectIdentifier, value string) func(x509.Certificate) error { return func(cert x509.Certificate) error { for _, ext := range cert.ExtraExtensions { if ext.Id.Equal(oid) { diff --git a/pkg/server/grpc_server_test.go b/pkg/server/grpc_server_test.go index c5e05807e..3da933123 100644 --- a/pkg/server/grpc_server_test.go +++ b/pkg/server/grpc_server_test.go @@ -780,12 +780,21 @@ func TestAPIWithBuildkite(t *testing.T) { // githubClaims holds the additional JWT claims for GitHub OIDC tokens type githubClaims struct { - JobWorkflowRef string `json:"job_workflow_ref"` - Sha string `json:"sha"` - Trigger string `json:"event_name"` - Repository string `json:"repository"` - Workflow string `json:"workflow"` - Ref string `json:"ref"` + JobWorkflowRef string `json:"job_workflow_ref"` + Sha string `json:"sha"` + EventName string `json:"event_name"` + Repository string `json:"repository"` + Workflow string `json:"workflow"` + Ref string `json:"ref"` + JobWorkflowSha string `json:"job_workflow_sha"` + RunnerEnvironment string `json:"runner_environment"` + RepositoryID string `json:"repository_id"` + RepositoryOwner string `json:"repository_owner"` + RepositoryOwnerID string `json:"repository_owner_id"` + WorkflowRef string `json:"workflow_ref"` + WorkflowSha string `json:"workflow_sha"` + RunID string `json:"run_id"` + RunAttempt string `json:"run_attempt"` } // Tests API for GitHub subject types @@ -807,12 +816,21 @@ func TestAPIWithGitHub(t *testing.T) { } claims := githubClaims{ - JobWorkflowRef: "job/workflow/ref", - Sha: "sha", - Trigger: "trigger", - Repository: "sigstore/fulcio", - Workflow: "workflow", - Ref: "refs/heads/main", + JobWorkflowRef: "job/workflow/ref", + Sha: "sha", + EventName: "trigger", + Repository: "sigstore/fulcio", + Workflow: "workflow", + Ref: "refs/heads/main", + JobWorkflowSha: "example-sha", + RunnerEnvironment: "cloud-hosted", + RepositoryID: "12345", + RepositoryOwner: "username", + RepositoryOwnerID: "345", + WorkflowRef: "sigstore/other/.github/workflows/foo.yaml@refs/heads/main", + WorkflowSha: "example-sha-other", + RunID: "42", + RunAttempt: "1", } githubSubject := fmt.Sprintf("repo:%s:ref:%s", claims.Repository, claims.Ref) @@ -875,41 +893,62 @@ func TestAPIWithGitHub(t *testing.T) { t.Fatalf("URIs do not match: Expected %v, got %v", githubURI, leafCert.URIs[0]) } // Verify custom OID values - triggerExt, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}) - if !found { - t.Fatal("expected trigger in custom OID") - } - if string(triggerExt.Value) != claims.Trigger { - t.Fatalf("unexpected trigger, expected %s, got %s", claims.Trigger, string(triggerExt.Value)) - } - shaExt, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}) - if !found { - t.Fatal("expected sha in custom OID") - } - if string(shaExt.Value) != claims.Sha { - t.Fatalf("unexpected sha, expected %s, got %s", claims.Sha, string(shaExt.Value)) - } - workflowExt, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 4}) - if !found { - t.Fatal("expected workflow name in custom OID") - } - if string(workflowExt.Value) != claims.Workflow { - t.Fatalf("unexpected workflow name, expected %s, got %s", claims.Workflow, string(workflowExt.Value)) - } - repoExt, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 5}) - if !found { - t.Fatal("expected repo in custom OID") - } - if string(repoExt.Value) != claims.Repository { - t.Fatalf("unexpected repo, expected %s, got %s", claims.Repository, string(repoExt.Value)) - } - refExt, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}) - if !found { - t.Fatal("expected ref in custom OID") + deprecatedExpectedExts := map[int]string{ + 2: claims.EventName, + 3: claims.Sha, + 4: claims.Workflow, + 5: claims.Repository, + 6: claims.Ref, + } + for o, value := range deprecatedExpectedExts { + ext, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, o}) + if !found { + t.Fatalf("expected extension in custom OID 1.3.6.1.4.1.57264.1.%d", o) + } + if string(ext.Value) != value { + t.Fatalf("unexpected extension value, expected %s, got %s", value, ext.Value) + } } - if string(refExt.Value) != claims.Ref { - t.Fatalf("unexpected ref, expected %s, got %s", claims.Ref, string(refExt.Value)) + url := "https://github.com/" + expectedExts := map[int]string{ + 9: url + claims.JobWorkflowRef, + 10: claims.JobWorkflowSha, + 11: claims.RunnerEnvironment, + 12: url + claims.Repository, + 13: claims.Sha, + 14: claims.Ref, + 15: claims.RepositoryID, + 16: url + claims.RepositoryOwner, + 17: claims.RepositoryOwnerID, + 18: url + claims.WorkflowRef, + 19: claims.WorkflowSha, + 20: claims.EventName, + 21: url + claims.Repository + "/actions/runs/" + claims.RunID + "/attempts/" + claims.RunAttempt, + } + for o, value := range expectedExts { + ext, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, o}) + if !found { + t.Fatalf("expected extension in custom OID 1.3.6.1.4.1.57264.1.%d", o) + } + var extValue string + rest, err := asn1.Unmarshal(ext.Value, &extValue) + if err != nil { + t.Fatalf("error unmarshalling extension: :%v", err) + } + if len(rest) != 0 { + t.Fatal("error unmarshalling extension, rest is not 0") + } + if string(extValue) != value { + t.Fatalf("unexpected extension value, expected %s, got %s", value, extValue) + } } + // buildSignerURIExt, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 9}) + // if !found { + // t.Fatal("expected ref in custom OID") + // } + // if string(buildSignerURIExt.Value) != claims.Ref { + // t.Fatalf("unexpected build signer URI, expected %s, got %s", claims.Ref, string(buildSignerURIExt.Value)) + // } } // Tests API with issuer claim in different field in the OIDC token @@ -1621,13 +1660,46 @@ func verifyResponse(resp *protobuf.SigningCertificate, eca *ephemeralca.Ephemera if leafCert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning { t.Fatalf("unexpected key usage, expected %v, got %v", x509.ExtKeyUsageCodeSigning, leafCert.ExtKeyUsage[0]) } - // Check issuer in custom OID + // Check issuer in custom OIDs issuerExt, found := findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}) if !found { - t.Fatal("expected issuer in custom OID") + t.Fatal("expected issuer in custom OID 1.3.6.1.4.1.57264.1.1") } if string(issuerExt.Value) != issuer { - t.Fatalf("unexpected issuer, expected %s, got %s", issuer, string(issuerExt.Value)) + t.Fatalf("unexpected issuer for 1.1, expected %s, got %s", issuer, string(issuerExt.Value)) + } + issuerExt, found = findCustomExtension(leafCert, asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8}) + if !found { + t.Fatal("expected issuer in custom OID 1.3.6.1.4.1.57264.1.8") + } + // verify ASN.1 encoding is correct + var raw asn1.RawValue + rest, err = asn1.Unmarshal(issuerExt.Value, &raw) + if err != nil { + t.Fatalf("unexpected error unmarshalling issuer to RawValue: %v", err) + } + if len(rest) != 0 { + t.Fatalf("unexpected trailing bytes in issuer") + } + // Universal class + if raw.Class != 0 { + t.Fatalf("expected ASN.1 issuer class to be 0, got %d", raw.Class) + } + // UTF8String + if raw.Tag != 12 { + t.Fatalf("expected ASN.1 issuer tag to be 12, got %d", raw.Tag) + } + // verify issuer unmarshals properly + var issuerVal string + rest, err = asn1.Unmarshal(issuerExt.Value, &issuerVal) + if err != nil { + t.Fatalf("unexpected error unmarshalling issuer: %v", err) + } + if len(rest) != 0 { + t.Fatalf("unexpected trailing bytes in issuer") + } + if string(issuerVal) != issuer { + t.Fatalf("unexpected issuer 1.3.6.1.4.1.57264.1.8, expected %s, got %s", issuer, string(issuerExt.Value)) } return leafCert