Skip to content

Commit 59bcaa4

Browse files
committed
Include raw subject in certificates
Sometimes identities have a different identifier that isn't captured by existing certificates, usually because the identifier isn't useful as a human friendly reference. For example - GCP accounts have an opaque numerical identifier. That said, subject is particularly useful to preserve since OIDC spec requires (issuer, subject) to be the canonical representation for an identity, and sometimes these opaque identifiers can be difficult to otherwise resolve. This change preserves the incoming subject as-is. While this may result in some duplicative data for some providers, this helps give a consistent reference point across OIDC issuers. Signed-off-by: Billy Lynch <billy@chainguard.dev>
1 parent b683519 commit 59bcaa4

File tree

15 files changed

+62
-15
lines changed

15 files changed

+62
-15
lines changed

docs/oid-info.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Nice-to-haves:
5252

5353
`1.3.6.1.4.1.57264.1.7` is formatted as a DER-encoded string in the SubjectAlternativeName extension, as per RFC 5280 4.2.1.6.
5454

55-
`1.3.6.1.4.1.57264.1.8` through `1.3.6.1.4.1.57264.1.22` are formatted as DER-encoded strings; the ASN.1 tag is
55+
`1.3.6.1.4.1.57264.1.8` through `1.3.6.1.4.1.57264.1.24` are formatted as DER-encoded strings; the ASN.1 tag is
5656
UTF8String (0x0C) and the tag class is universal.
5757

5858
## Directory
@@ -186,6 +186,10 @@ Source repository visibility at the time of signing the certificate. MAY be empt
186186

187187
Deployment target for a given job that maps to deployment protection rules. May be empty if no environment is defined. For example: `production` or `staging`.
188188

189+
### 1.3.6.1.4.1.57264.1.24 | Token Subject
190+
191+
The raw `sub` claim from the OIDC ID token that was presented at the time the code signing certificate was requested. This preserves the original token subject as-is, regardless of how the provider maps it to certificate SANs or other extensions. For example: `repo:sigstore/fulcio:ref:refs/heads/main` for GitHub Actions, or `project_path:mygroup/myproject:ref_type:branch:ref:main` for GitLab.
192+
189193
## 1.3.6.1.4.1.57264.2 | Policy OID for Sigstore Timestamp Authority
190194

191195
Not used by Fulcio. This specifies the policy OID for the [timestamp authority](https://github.com/sigstore/timestamp-authority)
@@ -215,6 +219,7 @@ that Sigstore operates.
215219
| server_url + repository + "/actions/runs/" + run_id + "/attempts/" + run_attempt | server_url + project_path + "/-/jobs/" + job_id | server_url + "/" organization_slug + "/" + pipeline_slug + "/builds/" + build_number + "#" + job_id | platform_url + "/build/" + workflow_id | platform_url + "/workflow/" + workflow-id + "/job/" + job-id | Run Invocation URI | An immutable identifier that can uniquely identify the build execution |
216220
| repository_visibility | project_visibility | N/A | N/A | N/A | Source Repository Visibility At Signing | Source repository visibility at the time of signing the certificate |
217221
| environment | environment | N/A | N/A | N/A | Deployment Environment | Deployment target for a workflow or job |
222+
| sub | sub | sub | sub | sub | Token Subject | The raw `sub` claim from the OIDC ID token |
218223

219224
[github-oidc-doc]: https://docs.github.com/en/actions/reference/security/oidc#oidc-token-claims
220225
[oid-link]: http://oid-info.com/get/1.3.6.1.4.1.57264

pkg/certificate/extensions.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ var (
3535
// Deprecated: Use OIDSourceRepositoryRef
3636
OIDGitHubWorkflowRef = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 6}
3737

38-
OIDOtherName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 7}
39-
OIDIssuerV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8}
38+
OIDOtherName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 7}
39+
OIDIssuerV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8}
40+
OIDTokenSubject = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 24}
4041

4142
// CI extensions
4243
OIDBuildSignerURI = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 9}
@@ -136,6 +137,9 @@ type Extensions struct {
136137

137138
// Deployment target for a workflow or job
138139
DeploymentEnvironment string `json:"DeploymentEnvironment,omitempty" yaml:"deployment-environment,omitempty"` // 1.3.6.1.4.1.57264.1.23
140+
141+
// Raw OIDC token subject (`sub` claim).
142+
Subject string `json:"Subject,omitempty" yaml:"subject,omitempty"` // 1.3.6.1.4.1.57264.1.24
139143
}
140144

141145
func (e Extensions) Render() ([]pkix.Extension, error) {
@@ -348,6 +352,16 @@ func (e Extensions) Render() ([]pkix.Extension, error) {
348352
Value: val,
349353
})
350354
}
355+
if e.Subject != "" {
356+
val, err := asn1.MarshalWithParams(e.Subject, "utf8")
357+
if err != nil {
358+
return nil, err
359+
}
360+
exts = append(exts, pkix.Extension{
361+
Id: OIDTokenSubject,
362+
Value: val,
363+
})
364+
}
351365

352366
return exts, nil
353367
}
@@ -435,6 +449,10 @@ func ParseExtensions(ext []pkix.Extension) (Extensions, error) {
435449
if err := ParseDERString(e.Value, &out.DeploymentEnvironment); err != nil {
436450
return Extensions{}, err
437451
}
452+
case e.Id.Equal(OIDTokenSubject):
453+
if err := ParseDERString(e.Value, &out.Subject); err != nil {
454+
return Extensions{}, err
455+
}
438456
}
439457
}
440458

pkg/certificate/extensions_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func TestExtensions(t *testing.T) {
5858
RunInvocationURI: "21", // 1.3.6.1.4.1.57264.1.21
5959
SourceRepositoryVisibilityAtSigning: "22", // 1.3.6.1.4.1.57264.1.22
6060
DeploymentEnvironment: "23", // 1.3.6.1.4.1.57264.1.23
61+
Subject: "24", // 1.3.6.1.4.1.57264.1.24
6162
},
6263
Expect: []pkix.Extension{
6364
{
@@ -148,6 +149,10 @@ func TestExtensions(t *testing.T) {
148149
Id: OIDDeploymentEnvironment,
149150
Value: marshalDERString(t, "23"),
150151
},
152+
{
153+
Id: OIDTokenSubject,
154+
Value: marshalDERString(t, "24"),
155+
},
151156
},
152157
WantErr: false,
153158
},

pkg/identity/buildkite/principal.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ func (p jobPrincipal) Embed(_ context.Context, cert *x509.Certificate) error {
8080

8181
// Embed additional information into custom extensions
8282
cert.ExtraExtensions, err = certificate.Extensions{
83-
Issuer: p.issuer,
83+
Issuer: p.issuer,
84+
Subject: p.subject,
8485
}.Render()
8586
if err != nil {
8687
return err

pkg/identity/chainguard/principal.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ func (w workflowPrincipal) Embed(_ context.Context, cert *x509.Certificate) erro
8080
cert.URIs = []*url.URL{baseURL.JoinPath(w.subject)}
8181

8282
cert.ExtraExtensions, err = certificate.Extensions{
83-
Issuer: w.issuer,
83+
Issuer: w.issuer,
84+
Subject: w.subject,
8485

8586
// TODO(mattmoor): Embed more of the Chainguard token structure via OIDs.
8687
}.Render()

pkg/identity/ciprovider/principal.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func (principal ciPrincipal) Embed(_ context.Context, cert *x509.Certificate) er
168168
s := v.Field(i).String() // value of each field, e.g the template string
169169
// We check the field name to avoid applying the template for the Issuer.
170170
// The Issuer field should always come from the token issuer.
171-
if strings.TrimSpace(s) == "" || vType.Field(i).Name == "Issuer" {
171+
if strings.TrimSpace(s) == "" || vType.Field(i).Name == "Issuer" || vType.Field(i).Name == "Subject" {
172172
continue
173173
}
174174
extValue, err := applyTemplateOrReplace(s, claims, defaults,
@@ -182,9 +182,10 @@ func (principal ciPrincipal) Embed(_ context.Context, cert *x509.Certificate) er
182182
v.Field(i).SetString(extValue)
183183
}
184184

185-
// Guarantee that the extension issuer is set to the token issuer,
186-
// regardless of whether this field has been set before
185+
// Guarantee that the extension issuer and subject are set to the token values,
186+
// regardless of whether these fields have been set before
187187
claimsTemplates.Issuer = principal.Token.Issuer
188+
claimsTemplates.Subject = principal.Token.Subject
188189
// Embed additional information into custom extensions
189190
cert.ExtraExtensions, err = claimsTemplates.Render()
190191
if err != nil {

pkg/identity/codefresh/principal.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ func (w workflowPrincipal) Embed(_ context.Context, cert *x509.Certificate) erro
150150
cert.URIs = []*url.URL{baseURL.JoinPath(w.accountName, fmt.Sprintf("%s:%s/%s", w.pipelineName, w.accountID, w.pipelineID))}
151151

152152
cert.ExtraExtensions, err = certificate.Extensions{
153-
Issuer: w.issuer,
153+
Issuer: w.issuer,
154+
Subject: w.subject,
154155
// URL of the build in Codefresh.
155156
// The workflow url is used for build signer in Codefresh because for public builds unauthenticated users only have access to the workflow, not the pipeline definition.
156157
// Also, the workflow contains the definition of the pipeline that was used at the time of the build, making it ideal to be used as the signer url.

pkg/identity/email/principal.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
type principal struct {
3232
address string
3333
issuer string
34+
subject string
3435
}
3536

3637
func PrincipalFromIDToken(ctx context.Context, token *oidc.IDToken) (identity.Principal, error) {
@@ -61,6 +62,7 @@ func PrincipalFromIDToken(ctx context.Context, token *oidc.IDToken) (identity.Pr
6162
return principal{
6263
issuer: issuer,
6364
address: emailAddress,
65+
subject: token.Subject,
6466
}, nil
6567
}
6668

@@ -73,7 +75,8 @@ func (p principal) Embed(_ context.Context, cert *x509.Certificate) error {
7375

7476
var err error
7577
cert.ExtraExtensions, err = certificate.Extensions{
76-
Issuer: p.issuer,
78+
Issuer: p.issuer,
79+
Subject: p.subject,
7780
}.Render()
7881
if err != nil {
7982
return err

pkg/identity/email/principal_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func TestPrincipalFromIDToken(t *testing.T) {
5757
ExpectedPrincipal: principal{
5858
issuer: "https://iss.example.com",
5959
address: "alice@example.com",
60+
subject: "doesntmatter",
6061
},
6162
WantErr: false,
6263
},
@@ -84,6 +85,7 @@ func TestPrincipalFromIDToken(t *testing.T) {
8485
ExpectedPrincipal: principal{
8586
issuer: "https://example.com",
8687
address: "alice@example.com",
88+
subject: "doesntmatter",
8789
},
8890
WantErr: false,
8991
},
@@ -111,6 +113,7 @@ func TestPrincipalFromIDToken(t *testing.T) {
111113
ExpectedPrincipal: principal{
112114
issuer: "https://example.com",
113115
address: "alice@example.com",
116+
subject: "doesntmatter",
114117
},
115118
WantErr: false,
116119
},
@@ -173,6 +176,7 @@ func TestPrincipalFromIDToken(t *testing.T) {
173176
ExpectedPrincipal: principal{
174177
issuer: "https://internal.example.com",
175178
address: "alice@example.com",
179+
subject: "doesntmatter",
176180
},
177181
WantErr: false,
178182
},
@@ -197,6 +201,7 @@ func TestPrincipalFromIDToken(t *testing.T) {
197201
ExpectedPrincipal: principal{
198202
issuer: "https://internal.example.com",
199203
address: "alice@example.com",
204+
subject: "doesntmatter",
200205
},
201206
WantErr: false,
202207
},
@@ -221,6 +226,7 @@ func TestPrincipalFromIDToken(t *testing.T) {
221226
ExpectedPrincipal: principal{
222227
issuer: "https://internal.example.com",
223228
address: "alice@example.com",
229+
subject: "doesntmatter",
224230
},
225231
WantErr: false,
226232
},

pkg/identity/github/principal.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ func (w workflowPrincipal) Embed(_ context.Context, cert *x509.Certificate) erro
199199

200200
// Embed additional information into custom extensions
201201
cert.ExtraExtensions, err = certificate.Extensions{
202-
Issuer: w.issuer,
202+
Issuer: w.issuer,
203+
Subject: w.subject,
203204
// BEGIN: Deprecated
204205
GithubWorkflowTrigger: w.eventName,
205206
GithubWorkflowSHA: w.sha,

0 commit comments

Comments
 (0)