From 587eb8ede00fb0601676b603ce59414f85d2d3ae Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Wed, 10 Jul 2024 12:30:28 -0700 Subject: [PATCH] Surface the right `Name()` from our principal. The cosign logic for interacting with Fulcio treats identity tokens as *largely* opaque, and most of the logic for how issuers and subjects and whatnot is handled happens server-side. However, for the "proof of possession" `cosign` has some logic (from `sigstore/sigstore`) that fumbles with `email` and `sub` claims in ways that have (until now) been compatible with Fulcio principals. The Chainguard provider is the first provider that optionally includes an `email` claim, but we always want the subject we use to be our opaque identifier string (from `sub`). This creates a tear in the fulcio/cosign continuum, and so we must surface what `cosign` is signing as `Name()` even though that isn't necessarily what we embed in the certificate. The only correct way to implement `Name()` today is to match what this function does, and current implementations happen to align, but unfortunately because of how this abstraction is formulated it is challenging to actually change how we confirm the proof of possession to use this directly in place of the principal itself. Fixes: https://github.com/sigstore/cosign/issues/3777 Signed-off-by: Matt Moore --- pkg/identity/chainguard/principal.go | 14 ++++++++++- pkg/identity/chainguard/principal_test.go | 29 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) 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",