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

Begin implementing Issuer interface for email and github identities #1005

Merged
merged 3 commits into from
Mar 1, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 39 additions & 0 deletions pkg/identity/authorize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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.

priyawadhwa marked this conversation as resolved.
Show resolved Hide resolved
package identity

import (
"context"
"fmt"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/sigstore/fulcio/pkg/config"
)

// We do this to bypass needing actual OIDC tokens for unit testing.
var Authorize = actualAuthorize
priyawadhwa marked this conversation as resolved.
Show resolved Hide resolved

func actualAuthorize(ctx context.Context, token string) (*oidc.IDToken, error) {
issuer, err := extractIssuerURL(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)
}
42 changes: 42 additions & 0 deletions pkg/identity/email/issuer.go
Original file line number Diff line number Diff line change
@@ -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 {
priyawadhwa marked this conversation as resolved.
Show resolved Hide resolved
issuerURL string
}

func Issuer(issuerURL string) identity.Issuer {
return &emailIssuer{issuerURL: issuerURL}
}

func (e *emailIssuer) Authenticate(ctx context.Context, token string) (identity.Principal, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these implementations of Authenticate and Match are going to look the same, so I wonder if we can either implement this with generics, or implement this on the base Principal type. If the base Principal type included a PrincipalFromIDToken function, it could then implement the Issuer interface too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to include PrincipalFromIDToken in the interface since it's expected that Authenticate will always call it, and once this refactor is done we won't need to call PrincipalFromIDToken outside of a specific package. I think we can make it a private helper function once this refactor is done though, since it shouldn't need to be called outside of the package at that point!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see if there's some way to reduce common implementations, but I'm fine if it's in a later PR. Maybe just have Match be implemented on the base type? And Authenticate ends up calling a private principcalFromIDToken?

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
}
81 changes: 81 additions & 0 deletions pkg/identity/email/issuer_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
})
}
42 changes: 42 additions & 0 deletions pkg/identity/github/issuer.go
Original file line number Diff line number Diff line change
@@ -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
}
75 changes: 75 additions & 0 deletions pkg/identity/github/issuer_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
})
}
41 changes: 2 additions & 39 deletions pkg/server/grpc_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}