Skip to content

Commit

Permalink
Final challenge result removal 🎉 (#626)
Browse files Browse the repository at this point in the history
* Move uri principal to package

Signed-off-by: Nathan Smith <[email protected]>

* Move username principal to package

Signed-off-by: Nathan Smith <[email protected]>
  • Loading branch information
nsmith5 authored Jun 1, 2022
1 parent 614d41e commit 35b7117
Show file tree
Hide file tree
Showing 6 changed files with 588 additions and 336 deletions.
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: "[email protected]",
},
WantErr: false,
WantFacts: map[string]func(x509.Certificate) error{
`Issuer is foo.example.com`: factIssuerIs(`foo.example.com`),
`SAN is [email protected]`: func(cert x509.Certificate) error {
if len(cert.EmailAddresses) != 1 {
return errors.New("no email SAN set")
}
if cert.EmailAddresses[0] != "[email protected]" {
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 := "[email protected]"
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 := "[email protected]"
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
Loading

0 comments on commit 35b7117

Please sign in to comment.