From 2e3a961e635c13e191c7d6be3a4e193e5152ec95 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Wed, 15 Feb 2023 12:53:01 -0800 Subject: [PATCH 1/3] Begin implementing Issuer interface for email and github identities Signed-off-by: Priya Wadhwa --- pkg/identity/authorize.go | 61 +++++++++++++++++++++++++++++++++++ pkg/identity/email/issuer.go | 42 ++++++++++++++++++++++++ pkg/identity/github/issuer.go | 42 ++++++++++++++++++++++++ pkg/server/grpc_server.go | 41 ++--------------------- 4 files changed, 147 insertions(+), 39 deletions(-) create mode 100644 pkg/identity/authorize.go create mode 100644 pkg/identity/email/issuer.go create mode 100644 pkg/identity/github/issuer.go diff --git a/pkg/identity/authorize.go b/pkg/identity/authorize.go new file mode 100644 index 000000000..1d71d2c0e --- /dev/null +++ b/pkg/identity/authorize.go @@ -0,0 +1,61 @@ +// Copyright 2023 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 identity + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/sigstore/fulcio/pkg/config" +) + +func extractIssuer(token string) (string, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return "", fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) + } + raw, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", fmt.Errorf("oidc: malformed jwt payload: %w", err) + } + var payload struct { + Issuer string `json:"iss"` + } + + if err := json.Unmarshal(raw, &payload); err != nil { + return "", fmt.Errorf("oidc: failed to unmarshal claims: %w", err) + } + return payload.Issuer, nil +} + +// We do this to bypass needing actual OIDC tokens for unit testing. +var Authorize = actualAuthorize + +func actualAuthorize(ctx context.Context, token string) (*oidc.IDToken, error) { + issuer, err := extractIssuer(token) + if err != nil { + return nil, err + } + + verifier, ok := config.FromContext(ctx).GetVerifier(issuer) + if !ok { + return nil, fmt.Errorf("unsupported issuer: %s", issuer) + } + return verifier.Verify(ctx, token) +} diff --git a/pkg/identity/email/issuer.go b/pkg/identity/email/issuer.go new file mode 100644 index 000000000..c793ff18b --- /dev/null +++ b/pkg/identity/email/issuer.go @@ -0,0 +1,42 @@ +// 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 email + +import ( + "context" + + "github.com/sigstore/fulcio/pkg/identity" +) + +type emailIssuer struct { + issuerURL string +} + +func Issuer(issuerURL string) identity.Issuer { + return &emailIssuer{issuerURL: issuerURL} +} + +func (e *emailIssuer) Authenticate(ctx context.Context, token string) (identity.Principal, error) { + idtoken, err := identity.Authorize(ctx, token) + if err != nil { + return nil, err + } + return PrincipalFromIDToken(ctx, idtoken) +} + +// Match checks if this issuer can authenticate tokens from a given issuer URL +func (e *emailIssuer) Match(ctx context.Context, url string) bool { + return url == e.issuerURL +} diff --git a/pkg/identity/github/issuer.go b/pkg/identity/github/issuer.go new file mode 100644 index 000000000..a0e35e72a --- /dev/null +++ b/pkg/identity/github/issuer.go @@ -0,0 +1,42 @@ +// Copyright 2023 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" + + "github.com/sigstore/fulcio/pkg/identity" +) + +type githubIssuer struct { + issuerURL string +} + +func Issuer(issuerURL string) identity.Issuer { + return &githubIssuer{issuerURL: issuerURL} +} + +func (e *githubIssuer) Authenticate(ctx context.Context, token string) (identity.Principal, error) { + idtoken, err := identity.Authorize(ctx, token) + if err != nil { + return nil, err + } + return WorkflowPrincipalFromIDToken(ctx, idtoken) +} + +// Match checks if this issuer can authenticate tokens from a given issuer URL +func (e *githubIssuer) Match(ctx context.Context, url string) bool { + return url == e.issuerURL +} diff --git a/pkg/server/grpc_server.go b/pkg/server/grpc_server.go index e8dfd5204..87ef78a2a 100644 --- a/pkg/server/grpc_server.go +++ b/pkg/server/grpc_server.go @@ -18,19 +18,17 @@ package server import ( "context" "crypto" - "encoding/base64" "encoding/json" "errors" "fmt" - "strings" - "github.com/coreos/go-oidc/v3/oidc" ctclient "github.com/google/certificate-transparency-go/client" certauth "github.com/sigstore/fulcio/pkg/ca" "github.com/sigstore/fulcio/pkg/challenges" "github.com/sigstore/fulcio/pkg/config" "github.com/sigstore/fulcio/pkg/ctl" fulciogrpc "github.com/sigstore/fulcio/pkg/generated/protobuf" + "github.com/sigstore/fulcio/pkg/identity" "github.com/sigstore/fulcio/pkg/log" "github.com/sigstore/sigstore/pkg/cryptoutils" "google.golang.org/grpc/codes" @@ -72,7 +70,7 @@ func (g *grpcCAServer) CreateSigningCertificate(ctx context.Context, request *fu } // Authenticate OIDC ID token by checking signature - idtoken, err := authorize(ctx, token) + idtoken, err := identity.Authorize(ctx, token) if err != nil { return nil, handleFulcioGRPCError(ctx, codes.Unauthenticated, err, invalidCredentials) } @@ -269,38 +267,3 @@ func (g *grpcCAServer) GetConfiguration(ctx context.Context, _ *fulciogrpc.GetCo Issuers: cfg.ToIssuers(), }, nil } - -func extractIssuer(token string) (string, error) { - parts := strings.Split(token, ".") - if len(parts) != 3 { - return "", fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) - } - raw, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return "", fmt.Errorf("oidc: malformed jwt payload: %w", err) - } - var payload struct { - Issuer string `json:"iss"` - } - - if err := json.Unmarshal(raw, &payload); err != nil { - return "", fmt.Errorf("oidc: failed to unmarshal claims: %w", err) - } - return payload.Issuer, nil -} - -// We do this to bypass needing actual OIDC tokens for unit testing. -var authorize = actualAuthorize - -func actualAuthorize(ctx context.Context, token string) (*oidc.IDToken, error) { - issuer, err := extractIssuer(token) - if err != nil { - return nil, err - } - - verifier, ok := config.FromContext(ctx).GetVerifier(issuer) - if !ok { - return nil, fmt.Errorf("unsupported issuer: %s", issuer) - } - return verifier.Verify(ctx, token) -} From 36ed58778e338eb9263bd587c11e69011d3e9920 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Wed, 1 Mar 2023 12:58:31 -0500 Subject: [PATCH 2/3] Add unit test Signed-off-by: Priya Wadhwa --- pkg/identity/email/issuer_test.go | 81 ++++++++++++++++++++++++++++++ pkg/identity/github/issuer_test.go | 75 +++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 pkg/identity/email/issuer_test.go create mode 100644 pkg/identity/github/issuer_test.go diff --git a/pkg/identity/email/issuer_test.go b/pkg/identity/email/issuer_test.go new file mode 100644 index 000000000..4d6f0ea63 --- /dev/null +++ b/pkg/identity/email/issuer_test.go @@ -0,0 +1,81 @@ +// Copyright 2023 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 email + +import ( + "context" + "encoding/json" + "testing" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/sigstore/fulcio/pkg/config" + "github.com/sigstore/fulcio/pkg/identity" +) + +func TestIssuer(t *testing.T) { + ctx := context.Background() + url := "test-issuer-url" + issuer := Issuer(url) + + // test the Match function + t.Run("match", func(t *testing.T) { + if matches := issuer.Match(ctx, url); !matches { + t.Fatal("expected url to match but it doesn't") + } + if matches := issuer.Match(ctx, "some-other-url"); matches { + t.Fatal("expected match to fail but it didn't") + } + }) + + t.Run("authenticate", func(t *testing.T) { + token := &oidc.IDToken{ + Issuer: "https://iss.example.com", + Subject: "subject", + } + claims, err := json.Marshal(map[string]interface{}{ + "aud": "sigstore", + "iss": "https://iss.example.com", + "sub": "doesntmatter", + "email": "alice@example.com", + "email_verified": true, + }) + if err != nil { + t.Fatal(err) + } + withClaims(token, claims) + + ctx := config.With(context.Background(), &config.FulcioConfig{ + OIDCIssuers: map[string]config.OIDCIssuer{ + "https://iss.example.com": { + IssuerURL: "https://iss.example.com", + Type: config.IssuerTypeEmail, + ClientID: "sigstore", + }, + }, + }) + + identity.Authorize = func(_ context.Context, _ string) (*oidc.IDToken, error) { + return token, nil + } + principal, err := issuer.Authenticate(ctx, "token") + if err != nil { + t.Fatal(err) + } + + if principal.Name(ctx) != "alice@example.com" { + t.Fatalf("got unexpected name %s", principal.Name(ctx)) + } + }) +} diff --git a/pkg/identity/github/issuer_test.go b/pkg/identity/github/issuer_test.go new file mode 100644 index 000000000..9ddf1035c --- /dev/null +++ b/pkg/identity/github/issuer_test.go @@ -0,0 +1,75 @@ +// Copyright 2023 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" + "encoding/json" + "testing" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/sigstore/fulcio/pkg/identity" +) + +func TestIssuer(t *testing.T) { + ctx := context.Background() + url := "test-issuer-url" + issuer := Issuer(url) + + // test the Match function + t.Run("match", func(t *testing.T) { + if matches := issuer.Match(ctx, url); !matches { + t.Fatal("expected url to match but it doesn't") + } + if matches := issuer.Match(ctx, "some-other-url"); matches { + t.Fatal("expected match to fail but it didn't") + } + }) + + t.Run("authenticate", func(t *testing.T) { + token := &oidc.IDToken{ + Issuer: "https://iss.example.com", + Subject: "repo:sigstore/fulcio:ref:refs/heads/main", + } + claims, err := json.Marshal(map[string]interface{}{ + "aud": "sigstore", + "event_name": "push", + "exp": 0, + "iss": "https://token.actions.githubusercontent.com", + "job_workflow_ref": "sigstore/fulcio/.github/workflows/foo.yaml@refs/heads/main", + "ref": "refs/heads/main", + "repository": "sigstore/fulcio", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sub": "repo:sigstore/fulcio:ref:refs/heads/main", + "workflow": "foo", + }) + if err != nil { + t.Fatal(err) + } + withClaims(token, claims) + + identity.Authorize = func(_ context.Context, _ string) (*oidc.IDToken, error) { + return token, nil + } + principal, err := issuer.Authenticate(ctx, "token") + if err != nil { + t.Fatal(err) + } + + if principal.Name(ctx) != "repo:sigstore/fulcio:ref:refs/heads/main" { + t.Fatalf("got unexpected name %s", principal.Name(ctx)) + } + }) +} From 5dcf8b6f0028df9dcf6744ede02b0211facdf312 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Wed, 1 Mar 2023 15:35:22 -0500 Subject: [PATCH 3/3] Reuse existing extractIssuerURL function Signed-off-by: Priya Wadhwa --- pkg/identity/authorize.go | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/pkg/identity/authorize.go b/pkg/identity/authorize.go index 1d71d2c0e..8bfbaeb8e 100644 --- a/pkg/identity/authorize.go +++ b/pkg/identity/authorize.go @@ -16,39 +16,17 @@ package identity import ( "context" - "encoding/base64" - "encoding/json" "fmt" - "strings" "github.com/coreos/go-oidc/v3/oidc" "github.com/sigstore/fulcio/pkg/config" ) -func extractIssuer(token string) (string, error) { - parts := strings.Split(token, ".") - if len(parts) != 3 { - return "", fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) - } - raw, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return "", fmt.Errorf("oidc: malformed jwt payload: %w", err) - } - var payload struct { - Issuer string `json:"iss"` - } - - if err := json.Unmarshal(raw, &payload); err != nil { - return "", fmt.Errorf("oidc: failed to unmarshal claims: %w", err) - } - return payload.Issuer, nil -} - // We do this to bypass needing actual OIDC tokens for unit testing. var Authorize = actualAuthorize func actualAuthorize(ctx context.Context, token string) (*oidc.IDToken, error) { - issuer, err := extractIssuer(token) + issuer, err := extractIssuerURL(token) if err != nil { return nil, err }