Skip to content

Commit

Permalink
Begin implementing Issuer interface for email and github identities (#…
Browse files Browse the repository at this point in the history
…1005)

* Begin implementing Issuer interface for email and github identities

Signed-off-by: Priya Wadhwa <priya@chainguard.dev>

* Add unit test

Signed-off-by: Priya Wadhwa <priya@chainguard.dev>

* Reuse existing extractIssuerURL function

Signed-off-by: Priya Wadhwa <priya@chainguard.dev>

---------

Signed-off-by: Priya Wadhwa <priya@chainguard.dev>
  • Loading branch information
priyawadhwa committed Mar 1, 2023
1 parent 09b59c9 commit 19adae0
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 39 deletions.
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.

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

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 {
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
}
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)
}

0 comments on commit 19adae0

Please sign in to comment.