diff --git a/pkg/identity/chainguard/principal.go b/pkg/identity/chainguard/principal.go index d2940c667..194b89924 100644 --- a/pkg/identity/chainguard/principal.go +++ b/pkg/identity/chainguard/principal.go @@ -22,11 +22,13 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/sigstore/fulcio/pkg/certificate" "github.com/sigstore/fulcio/pkg/identity" + "github.com/sigstore/sigstore/pkg/oauthflow" ) type workflowPrincipal struct { issuer string subject string + name string actor map[string]string servicePrincipal string @@ -35,7 +37,7 @@ type workflowPrincipal struct { var _ identity.Principal = (*workflowPrincipal)(nil) func (w workflowPrincipal) Name(_ context.Context) string { - return w.subject + return w.name } func PrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.Principal, error) { @@ -50,9 +52,19 @@ func PrincipalFromIDToken(_ context.Context, token *oidc.IDToken) (identity.Prin return nil, err } + // This is the exact function that cosign uses to extract the "subject" + // (misnomer) from the token in order to establish "proof of possession". + // We MUST use this to implement Name() or tokens that embed an email claim + // will fail to sign because of this divergent logic. + name, err := oauthflow.SubjectFromToken(token) + if err != nil { + return nil, err + } + return &workflowPrincipal{ issuer: token.Issuer, subject: token.Subject, + name: name, actor: claims.Actor, servicePrincipal: claims.Internal.ServicePrincipal, }, nil diff --git a/pkg/identity/chainguard/principal_test.go b/pkg/identity/chainguard/principal_test.go index db995db17..cd666aab0 100644 --- a/pkg/identity/chainguard/principal_test.go +++ b/pkg/identity/chainguard/principal_test.go @@ -60,6 +60,7 @@ func TestJobPrincipalFromIDToken(t *testing.T) { ExpectPrincipal: workflowPrincipal{ issuer: "https://issuer.enforce.dev", subject: id.String(), + name: id.String(), actor: map[string]string{ "iss": "https://iss.example.com/", "sub": fmt.Sprintf("catalog-syncer:%s", group.String()), @@ -85,6 +86,34 @@ func TestJobPrincipalFromIDToken(t *testing.T) { ExpectPrincipal: workflowPrincipal{ issuer: "https://issuer.enforce.dev", subject: group.String(), + name: group.String(), + actor: map[string]string{ + "iss": "https://auth.chainguard.dev/", + "sub": "google-oauth2|1234567890", + "aud": "fdsaldfkjhasldf", + }, + }, + WantErr: false, + }, + `Human SSO token (with email)`: { + Claims: map[string]interface{}{ + "iss": "https://issuer.enforce.dev", + "sub": group.String(), + "email": "jane@doe.dev", + "email_verified": true, + // Actor claims track the identity that was used to assume the + // Chainguard identity. In this case, it is the Catalog Syncer + // service principal. + "act": map[string]string{ + "iss": "https://auth.chainguard.dev/", + "sub": "google-oauth2|1234567890", + "aud": "fdsaldfkjhasldf", + }, + }, + ExpectPrincipal: workflowPrincipal{ + issuer: "https://issuer.enforce.dev", + subject: group.String(), + name: "jane@doe.dev", actor: map[string]string{ "iss": "https://auth.chainguard.dev/", "sub": "google-oauth2|1234567890",