diff --git a/config/config.jsn b/config/config.jsn index 2b386c2e4..7600554a2 100644 --- a/config/config.jsn +++ b/config/config.jsn @@ -20,6 +20,11 @@ "IssuerURL": "https://oidc.dlorenc.dev", "ClientID": "sigstore", "Type": "spiffe" - } + }, + "https://token.actions.githubusercontent.com": { + "IssuerURL": "https://token.actions.githubusercontent.com", + "ClientID": "sigstore", + "Type": "github-workflow" + } } } diff --git a/pkg/ca/googlecabeta/googleca.go b/pkg/ca/googlecabeta/googleca.go index 368890f3f..88b19839e 100644 --- a/pkg/ca/googlecabeta/googleca.go +++ b/pkg/ca/googlecabeta/googleca.go @@ -146,6 +146,30 @@ func githubWorkflowSubject(id string) *privatecapb.CertificateConfig_SubjectConf } } +func AdditionalExtensions(subject *challenges.ChallengeResult) []*privatecapb.X509Extension { + res := []*privatecapb.X509Extension{} + if subject.TypeVal == challenges.GithubWorkflowValue { + if trigger, ok := subject.AdditionalInfo[challenges.GithubWorkflowTrigger]; ok { + res = append(res, &privatecapb.X509Extension{ + ObjectId: &privatecapb.ObjectId{ + ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 57264, 1, 3}, + }, + Value: []byte(trigger), + }) + } + + if sha, ok := subject.AdditionalInfo[challenges.GithubWorkflowSha]; ok { + res = append(res, &privatecapb.X509Extension{ + ObjectId: &privatecapb.ObjectId{ + ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 57264, 1, 2}, + }, + Value: []byte(sha), + }) + } + } + return res +} + func KubernetesSubject(id string) *privatecapb.CertificateConfig_SubjectConfig { return &privatecapb.CertificateConfig_SubjectConfig{ SubjectAltName: &privatecapb.SubjectAltNames{ @@ -184,7 +208,7 @@ func (c *CertAuthorityService) CreateCertificate(ctx context.Context, subj *chal return nil, ca.ValidationError(err) } - extensions := IssuerExtension(subj.Issuer) + extensions := append(IssuerExtension(subj.Issuer), AdditionalExtensions(subj)...) req, err := Req(c.parent, privca, pubKeyBytes, extensions) if err != nil { diff --git a/pkg/ca/x509ca/x509ca.go b/pkg/ca/x509ca/x509ca.go index d0decb42a..79c74f396 100644 --- a/pkg/ca/x509ca/x509ca.go +++ b/pkg/ca/x509ca/x509ca.go @@ -124,7 +124,7 @@ func (x *X509CA) CreateCertificateWithCA(certauth *X509CA, subject *challenges.C } cert.URIs = []*url.URL{k8sURI} } - cert.ExtraExtensions = IssuerExtension(subject.Issuer) + cert.ExtraExtensions = append(IssuerExtension(subject.Issuer), AdditionalExtensions(subject)...) finalCertBytes, err := x509.CreateCertificate(rand.Reader, cert, certauth.RootCA, subject.PublicKey, certauth.PrivKey) if err != nil { @@ -134,6 +134,26 @@ func (x *X509CA) CreateCertificateWithCA(certauth *X509CA, subject *challenges.C return ca.CreateCSCFromDER(subject, finalCertBytes, nil) } +func AdditionalExtensions(subject *challenges.ChallengeResult) []pkix.Extension { + res := []pkix.Extension{} + if subject.TypeVal == challenges.GithubWorkflowValue { + if trigger, ok := subject.AdditionalInfo[challenges.GithubWorkflowTrigger]; ok { + res = append(res, pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 2}, + Value: []byte(trigger), + }) + } + + if sha, ok := subject.AdditionalInfo[challenges.GithubWorkflowSha]; ok { + res = append(res, pkix.Extension{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 3}, + Value: []byte(sha), + }) + } + } + return res +} + func IssuerExtension(issuer string) []pkix.Extension { if issuer == "" { return nil diff --git a/pkg/challenges/challenges.go b/pkg/challenges/challenges.go index db2c36d2f..9e142edee 100644 --- a/pkg/challenges/challenges.go +++ b/pkg/challenges/challenges.go @@ -40,11 +40,21 @@ const ( KubernetesValue ) +type AdditionalInfo int + +// Additional information that can be added as a cert extension. +const ( + GithubWorkflowTrigger AdditionalInfo = iota + GithubWorkflowSha +) + type ChallengeResult struct { Issuer string TypeVal ChallengeType PublicKey crypto.PublicKey Value string + // Extra information from the token that can be added to extensions. + AdditionalInfo map[AdditionalInfo]string } func CheckSignature(pub crypto.PublicKey, proof []byte, email string) error { @@ -165,6 +175,10 @@ func GithubWorkflow(ctx context.Context, principal *oidc.IDToken, pubKey crypto. if err != nil { return nil, err } + additionalInfo, err := workflowInfoFromIDToken(principal) + if err != nil { + return nil, err + } // Check the proof if err := CheckSignature(pubKey, challenge, principal.Subject); err != nil { @@ -184,10 +198,11 @@ func GithubWorkflow(ctx context.Context, principal *oidc.IDToken, pubKey crypto. // Now issue cert! return &ChallengeResult{ - Issuer: issuer, - PublicKey: pubKey, - TypeVal: GithubWorkflowValue, - Value: workflowRef, + Issuer: issuer, + PublicKey: pubKey, + TypeVal: GithubWorkflowValue, + Value: workflowRef, + AdditionalInfo: additionalInfo, }, nil } @@ -240,6 +255,24 @@ func workflowFromIDToken(token *oidc.IDToken) (string, error) { return "https://github.com/" + claims.JobWorkflowRef, nil } +func workflowInfoFromIDToken(token *oidc.IDToken) (map[AdditionalInfo]string, error) { + // Extract custom claims + var claims struct { + Sha string `json:"sha"` + Trigger string `json:"event_name"` + // The other fields that are present here seem to depend on the type + // of workflow trigger that initiated the action. + } + if err := token.Claims(&claims); err != nil { + return nil, err + } + + // We use this in URIs, so it has to be a URI. + return map[AdditionalInfo]string{ + GithubWorkflowSha: claims.Sha, + GithubWorkflowTrigger: claims.Trigger}, nil +} + func isSpiffeIDAllowed(host, spiffeID string) bool { // Strip spiffe:// name := strings.TrimPrefix(spiffeID, "spiffe://") @@ -251,5 +284,4 @@ func isSpiffeIDAllowed(host, spiffeID string) bool { return true } return strings.Contains(spiffeDomain, "."+host) - }