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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Final challenge result removal 馃帀 #626

Merged
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
111 changes: 4 additions & 107 deletions pkg/challenges/challenges.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,71 +22,22 @@ import (
"crypto/x509"
"errors"
"fmt"
"net/url"
"strings"

"github.com/sigstore/fulcio/pkg/ca/x509ca"
"github.com/sigstore/fulcio/pkg/config"
"github.com/sigstore/fulcio/pkg/identity"
"github.com/sigstore/fulcio/pkg/identity/email"
"github.com/sigstore/fulcio/pkg/identity/github"
"github.com/sigstore/fulcio/pkg/identity/kubernetes"
"github.com/sigstore/fulcio/pkg/identity/spiffe"
"github.com/sigstore/fulcio/pkg/identity/uri"
"github.com/sigstore/fulcio/pkg/identity/username"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
)

type ChallengeType int

const (
URIValue ChallengeType = iota
UsernameValue
)

type ChallengeResult struct {
Issuer string
TypeVal ChallengeType

// Value configures what will be set for SubjectAlternativeName in
// the certificate issued.
Value string

// subject or email from the id token. This must be the thing
// signed in the proof of possession!
subject string
}

func (cr *ChallengeResult) Name(context.Context) string {
return cr.subject
}

func (cr *ChallengeResult) Embed(ctx context.Context, cert *x509.Certificate) error {
switch cr.TypeVal {
case URIValue:
subjectURI, err := url.Parse(cr.Value)
if err != nil {
return err
}
cert.URIs = []*url.URL{subjectURI}
case UsernameValue:
cert.EmailAddresses = []string{cr.Value}
}

exts := x509ca.Extensions{
Issuer: cr.Issuer,
}

var err error
cert.ExtraExtensions, err = exts.Render()
if err != nil {
return err
}

return nil
}

// CheckSignature verifies a challenge, a signature over the subject or email
// of an OIDC token
func CheckSignature(pub crypto.PublicKey, proof []byte, subject string) error {
Expand All @@ -98,60 +49,6 @@ func CheckSignature(pub crypto.PublicKey, proof []byte, subject string) error {
return verifier.VerifySignature(bytes.NewReader(proof), strings.NewReader(subject))
}

func uri(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
uriWithSubject := principal.Subject

cfg, ok := config.FromContext(ctx).GetIssuer(principal.Issuer)
if !ok {
return nil, errors.New("invalid configuration for OIDC ID Token issuer")
}

// The subject hostname must exactly match the subject domain from the configuration
uSubject, err := url.Parse(uriWithSubject)
if err != nil {
return nil, err
}
uDomain, err := url.Parse(cfg.SubjectDomain)
if err != nil {
return nil, err
}
if uSubject.Scheme != uDomain.Scheme {
return nil, fmt.Errorf("subject URI scheme (%s) must match expected domain URI scheme (%s)", uSubject.Scheme, uDomain.Scheme)
}
if uSubject.Hostname() != uDomain.Hostname() {
return nil, fmt.Errorf("subject hostname (%s) must match expected domain (%s)", uSubject.Hostname(), uDomain.Hostname())
}

return &ChallengeResult{
Issuer: principal.Issuer,
TypeVal: URIValue,
Value: uriWithSubject,
subject: uriWithSubject,
}, nil
}

func username(ctx context.Context, principal *oidc.IDToken) (identity.Principal, error) {
username := principal.Subject

if strings.Contains(username, "@") {
return nil, errors.New("username cannot contain @ character")
}

cfg, ok := config.FromContext(ctx).GetIssuer(principal.Issuer)
if !ok {
return nil, errors.New("invalid configuration for OIDC ID Token issuer")
}

emailSubject := fmt.Sprintf("%s@%s", username, cfg.SubjectDomain)

return &ChallengeResult{
Issuer: principal.Issuer,
TypeVal: UsernameValue,
Value: emailSubject,
subject: username,
}, nil
}

func PrincipalFromIDToken(ctx context.Context, tok *oidc.IDToken) (identity.Principal, error) {
iss, ok := config.FromContext(ctx).GetIssuer(tok.Issuer)
if !ok {
Expand All @@ -169,9 +66,9 @@ func PrincipalFromIDToken(ctx context.Context, tok *oidc.IDToken) (identity.Prin
case config.IssuerTypeKubernetes:
principal, err = kubernetes.PrincipalFromIDToken(ctx, tok)
case config.IssuerTypeURI:
principal, err = uri(ctx, tok)
principal, err = uri.PrincipalFromIDToken(ctx, tok)
case config.IssuerTypeUsername:
principal, err = username(ctx, tok)
principal, err = username.PrincipalFromIDToken(ctx, tok)
default:
return nil, fmt.Errorf("unsupported issuer: %s", iss.Type)
}
Expand Down
229 changes: 0 additions & 229 deletions pkg/challenges/challenges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,246 +16,17 @@
package challenges

import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"errors"
"fmt"
"net/url"
"testing"

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

func TestEmbedChallengeResult(t *testing.T) {
tests := map[string]struct {
Challenge ChallengeResult
WantErr bool
WantFacts map[string]func(x509.Certificate) error
}{
`Good URI value`: {
Challenge: ChallengeResult{
Issuer: `foo.example.com`,
TypeVal: URIValue,
Value: "https://foo.example.com",
},
WantErr: false,
WantFacts: map[string]func(x509.Certificate) error{
`Issuer is foo.example.com`: factIssuerIs(`foo.example.com`),
`SAN is https://foo.example.com`: func(cert x509.Certificate) error {
WantURI, err := url.Parse("https://foo.example.com")
if err != nil {
return err
}
if len(cert.URIs) != 1 {
return errors.New("no URI SAN set")
}
if diff := cmp.Diff(cert.URIs[0], WantURI); diff != "" {
return errors.New(diff)
}
return nil
},
},
},
`Bad URI value fails`: {
Challenge: ChallengeResult{
Issuer: `foo.example.com`,
TypeVal: URIValue,
Value: "\nnoooooo",
},
WantErr: true,
},
`Good username value`: {
Challenge: ChallengeResult{
Issuer: `foo.example.com`,
TypeVal: UsernameValue,
Value: "name@foo.example.com",
},
WantErr: false,
WantFacts: map[string]func(x509.Certificate) error{
`Issuer is foo.example.com`: factIssuerIs(`foo.example.com`),
`SAN is name@foo.example.com`: func(cert x509.Certificate) error {
if len(cert.EmailAddresses) != 1 {
return errors.New("no email SAN set")
}
if cert.EmailAddresses[0] != "name@foo.example.com" {
return errors.New("wrong email")
}
return nil
},
},
},
`No issuer should fail to render extensions`: {
Challenge: ChallengeResult{
Issuer: ``,
TypeVal: URIValue,
Value: "https://foo.example.com/foo/bar",
},
WantErr: true,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
var cert x509.Certificate
err := test.Challenge.Embed(context.TODO(), &cert)
if err != nil {
if !test.WantErr {
t.Error(err)
}
return
} else if test.WantErr {
t.Error("expected error")
}
for factName, fact := range test.WantFacts {
t.Run(factName, func(t *testing.T) {
if err := fact(cert); err != nil {
t.Error(err)
}
})
}
})
}
}

func factIssuerIs(issuer string) func(x509.Certificate) error {
return factExtensionIs(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, issuer)
}

func factExtensionIs(oid asn1.ObjectIdentifier, value string) func(x509.Certificate) error {
return func(cert x509.Certificate) error {
for _, ext := range cert.ExtraExtensions {
if ext.Id.Equal(oid) {
if !bytes.Equal(ext.Value, []byte(value)) {
return fmt.Errorf("expected oid %v to be %s, but got %s", oid, value, ext.Value)
}
return nil
}
}
return errors.New("extension not set")
}
}

func TestURI(t *testing.T) {
cfg := &config.FulcioConfig{
OIDCIssuers: map[string]config.OIDCIssuer{
"https://accounts.example.com": {
IssuerURL: "https://accounts.example.com",
ClientID: "sigstore",
SubjectDomain: "https://example.com",
Type: config.IssuerTypeURI,
},
},
}
ctx := config.With(context.Background(), cfg)
subject := "https://example.com/users/1"
issuer := "https://accounts.example.com"
token := &oidc.IDToken{Subject: subject, Issuer: issuer}

principal, err := uri(ctx, token)
if err != nil {
t.Errorf("Expected test success, got %v", err)
}
if principal.Name(ctx) != token.Subject {
t.Errorf("Expected subject %v, got %v", token.Subject, principal.Name(ctx))
}
raw, ok := principal.(*ChallengeResult)
if !ok {
t.Fatal("expected principal to be a ChallengeResult")
}
if raw.Issuer != issuer {
t.Errorf("Expected issuer %s, got %s", issuer, raw.Issuer)
}
if raw.Value != subject {
t.Errorf("Expected subject value %s, got %s", subject, raw.Value)
}
if raw.TypeVal != URIValue {
t.Errorf("Expected type %v, got %v", URIValue, raw.TypeVal)
}
if raw.subject != token.Subject {
t.Errorf("Expected subject %v, got %v", token.Subject, raw.subject)
}
}

func TestUsername(t *testing.T) {
cfg := &config.FulcioConfig{
OIDCIssuers: map[string]config.OIDCIssuer{
"https://accounts.example.com": {
IssuerURL: "https://accounts.example.com",
ClientID: "sigstore",
SubjectDomain: "example.com",
Type: config.IssuerTypeUsername,
},
},
}
ctx := config.With(context.Background(), cfg)
usernameVal := "foobar"
usernameWithEmail := "foobar@example.com"
issuer := "https://accounts.example.com"
token := &oidc.IDToken{Subject: usernameVal, Issuer: issuer}

principal, err := username(ctx, token)
if err != nil {
t.Errorf("Expected test success, got %v", err)
}
if principal.Name(ctx) != token.Subject {
t.Errorf("Expected subject %s, got %s", token.Subject, principal.Name(ctx))
}
raw, ok := principal.(*ChallengeResult)
if !ok {
t.Fatal("expected principal to be a ChallengeResult")
}

if raw.Issuer != issuer {
t.Errorf("Expected issuer %s, got %s", issuer, raw.Issuer)
}
if raw.Value != usernameWithEmail {
t.Errorf("Expected subject value %s, got %s", usernameWithEmail, raw.Value)
}
if raw.TypeVal != UsernameValue {
t.Errorf("Expected type %v, got %v", UsernameValue, raw.TypeVal)
}
if raw.subject != token.Subject {
t.Errorf("Expected subject %s, got %s", token.Subject, raw.subject)
}
}

func TestUsernameInvalidChar(t *testing.T) {
cfg := &config.FulcioConfig{
OIDCIssuers: map[string]config.OIDCIssuer{
"https://accounts.example.com": {
IssuerURL: "https://accounts.example.com",
ClientID: "sigstore",
SubjectDomain: "example.com",
Type: config.IssuerTypeUsername,
},
},
}
ctx := config.With(context.Background(), cfg)
usernameVal := "foobar@example.com"
issuer := "https://accounts.example.com"
token := &oidc.IDToken{Subject: usernameVal, Issuer: issuer}

_, err := username(ctx, token)
if err == nil {
t.Errorf("expected test failure, got no error")
}
msg := "username cannot contain @ character"
if err.Error() != msg {
t.Errorf("unexpected test failure message, got %s, expected %s", err.Error(), msg)
}
}

func failErr(t *testing.T, err error) {
if err != nil {
t.Fatal(err)
Expand Down