Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move github principal to its own package #599

Merged
merged 1 commit into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 2 additions & 103 deletions pkg/challenges/challenges.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/sigstore/fulcio/pkg/ca/x509ca"
"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/identity"
"github.com/sigstore/fulcio/pkg/identity/github"
"github.com/spiffe/go-spiffe/v2/spiffeid"

"github.com/coreos/go-oidc/v3/oidc"
Expand All @@ -41,23 +42,11 @@ type ChallengeType int
const (
EmailValue ChallengeType = iota
SpiffeValue
GithubWorkflowValue
KubernetesValue
URIValue
UsernameValue
)

type AdditionalInfo int

// Additional information that can be added as a cert extension.
const (
GithubWorkflowTrigger AdditionalInfo = iota
GithubWorkflowSha
GithubWorkflowName
GithubWorkflowRepository
GithubWorkflowRef
)

type ChallengeResult struct {
Issuer string
TypeVal ChallengeType
Expand All @@ -66,9 +55,6 @@ type ChallengeResult struct {
// the certificate issued.
Value string

// Extra information from the token that can be added to extensions.
AdditionalInfo map[AdditionalInfo]string

// subject or email from the id token. This must be the thing
// signed in the proof of possession!
subject string
Expand All @@ -88,12 +74,6 @@ func (cr *ChallengeResult) Embed(ctx context.Context, cert *x509.Certificate) er
return err
}
cert.URIs = []*url.URL{challengeURL}
case GithubWorkflowValue:
jobWorkflowURI, err := url.Parse(cr.Value)
if err != nil {
return err
}
cert.URIs = []*url.URL{jobWorkflowURI}
case KubernetesValue:
k8sURI, err := url.Parse(cr.Value)
if err != nil {
Expand All @@ -113,29 +93,6 @@ func (cr *ChallengeResult) Embed(ctx context.Context, cert *x509.Certificate) er
exts := x509ca.Extensions{
Issuer: cr.Issuer,
}
if cr.TypeVal == GithubWorkflowValue {
var ok bool
exts.GithubWorkflowTrigger, ok = cr.AdditionalInfo[GithubWorkflowTrigger]
if !ok {
return errors.New("github workflow missing trigger claim")
}
exts.GithubWorkflowSHA, ok = cr.AdditionalInfo[GithubWorkflowSha]
if !ok {
return errors.New("github workflow missing SHA claim")
}
exts.GithubWorkflowName, ok = cr.AdditionalInfo[GithubWorkflowName]
if !ok {
return errors.New("github workflow missing workflow name claim")
}
exts.GithubWorkflowRepository, ok = cr.AdditionalInfo[GithubWorkflowRepository]
if !ok {
return errors.New("github workflow missing repository claim")
}
exts.GithubWorkflowRef, ok = cr.AdditionalInfo[GithubWorkflowRef]
if !ok {
return errors.New("github workflow missing ref claim")
}
}

var err error
cert.ExtraExtensions, err = exts.Render()
Expand Down Expand Up @@ -228,25 +185,6 @@ func kubernetes(ctx context.Context, principal *oidc.IDToken) (identity.Principa
}, nil
}

func githubWorkflow(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
workflowRef, err := workflowFromIDToken(principal)
if err != nil {
return nil, err
}
additionalInfo, err := workflowInfoFromIDToken(principal)
if err != nil {
return nil, err
}

return &ChallengeResult{
Issuer: principal.Issuer,
TypeVal: GithubWorkflowValue,
Value: workflowRef,
AdditionalInfo: additionalInfo,
subject: principal.Subject,
}, nil
}

func uri(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
uriWithSubject := principal.Subject

Expand Down Expand Up @@ -335,45 +273,6 @@ func kubernetesToken(token *oidc.IDToken) (string, error) {
return "https://kubernetes.io/namespaces/" + claims.Kubernetes.Namespace + "/serviceaccounts/" + claims.Kubernetes.ServiceAccount.Name, nil
}

func workflowFromIDToken(token *oidc.IDToken) (string, error) {
// Extract custom claims
var claims struct {
JobWorkflowRef string `json:"job_workflow_ref"`
// 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 "", err
}

// We use this in URIs, so it has to be a URI.
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"`
Repository string `json:"repository"`
Workflow string `json:"workflow"`
Ref string `json:"ref"`
// 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,
GithubWorkflowName: claims.Workflow,
GithubWorkflowRepository: claims.Repository,
GithubWorkflowRef: claims.Ref}, nil
}

func PrincipalFromIDToken(ctx context.Context, tok *oidc.IDToken) (identity.Principal, error) {
iss, ok := config.FromContext(ctx).GetIssuer(tok.Issuer)
if !ok {
Expand All @@ -387,7 +286,7 @@ func PrincipalFromIDToken(ctx context.Context, tok *oidc.IDToken) (identity.Prin
case config.IssuerTypeSpiffe:
principal, err = spiffe(ctx, tok)
case config.IssuerTypeGithubWorkflow:
principal, err = githubWorkflow(ctx, tok)
principal, err = github.WorkflowPrincipalFromIDToken(ctx, tok)
case config.IssuerTypeKubernetes:
principal, err = kubernetes(ctx, tok)
case config.IssuerTypeURI:
Expand Down
38 changes: 0 additions & 38 deletions pkg/challenges/challenges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,44 +45,6 @@ func TestEmbedChallengeResult(t *testing.T) {
WantErr bool
WantFacts map[string]func(x509.Certificate) error
}{
`Github workflow challenge should have all Github workflow extensions and issuer set`: {
Challenge: ChallengeResult{
Issuer: `https://token.actions.githubusercontent.com`,
TypeVal: GithubWorkflowValue,
Value: `https://github.com/foo/bar/`,
AdditionalInfo: map[AdditionalInfo]string{
GithubWorkflowSha: "sha",
GithubWorkflowTrigger: "trigger",
GithubWorkflowName: "workflowname",
GithubWorkflowRepository: "repository",
GithubWorkflowRef: "ref",
},
},
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"),
},
},
`Github workflow value with bad URL fails`: {
Challenge: ChallengeResult{
Issuer: `https://token.actions.githubusercontent.com`,
TypeVal: GithubWorkflowValue,
Value: "\nbadurl",
AdditionalInfo: map[AdditionalInfo]string{
GithubWorkflowSha: "sha",
GithubWorkflowTrigger: "trigger",
GithubWorkflowName: "workflowname",
GithubWorkflowRepository: "repository",
GithubWorkflowRef: "ref",
},
},
WantErr: true,
},
`Email challenges should set issuer extension and email subject`: {
Challenge: ChallengeResult{
Issuer: `example.com`,
Expand Down
127 changes: 127 additions & 0 deletions pkg/identity/github/principal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2022 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package github

import (
"context"
"crypto/x509"
"errors"
"net/url"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/sigstore/fulcio/pkg/ca/x509ca"
"github.com/sigstore/fulcio/pkg/identity"
)

type workflowPrincipal struct {
// Subject matches the 'sub' claim from the OIDC ID token this is what is
// signed as proof of possession for Github workflow identities
subject string

// OIDC Issuer URL. Matches 'iss' claim from ID token. The real issuer URL is
// https://token.actions.githubcontent.com/.well-known/openid-configution
issuer string

// The full URL to the workflow. This will be the set as SubjectAlternativeName URI in
// the final certificate.
url string

// Commit SHA being built
sha string

// Event that triggered this workflow run. E.g "push", "tag" etc
trigger string

// Repository building built
repository string

// Workflow that is running
workflow string

// Git ref being built
ref 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"`
}
if err := token.Claims(&claims); err != nil {
return nil, err
}

if claims.JobWorkflowRef == "" {
return nil, errors.New("missing job_workflow_ref claim in ID token")
}
if claims.Sha == "" {
return nil, errors.New("missing sha claim in ID token")
}
if claims.Trigger == "" {
return nil, errors.New("missing event_name claim in ID token")
}
if claims.Repository == "" {
return nil, errors.New("missing repository claim in ID token")
}
if claims.Workflow == "" {
return nil, errors.New("missing workflow claim in ID token")
}
if claims.Ref == "" {
return nil, errors.New("missing ref 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,
}, nil
}

func (w workflowPrincipal) Name(ctx context.Context) string {
return w.subject
}

func (w workflowPrincipal) Embed(ctx context.Context, cert *x509.Certificate) error {
// Set workflow URL to SubjectAlternativeName on certificate
parsed, err := url.Parse(w.url)
if err != nil {
return err
}
cert.URIs = []*url.URL{parsed}

// Embed additional information into custom extensions
cert.ExtraExtensions, err = x509ca.Extensions{
Issuer: w.issuer,
GithubWorkflowTrigger: w.trigger,
GithubWorkflowSHA: w.sha,
GithubWorkflowName: w.workflow,
GithubWorkflowRepository: w.repository,
GithubWorkflowRef: w.ref,
}.Render()
if err != nil {
return err
}

return nil
}
Loading