Skip to content

Commit

Permalink
Add API for fetching Fulcio configuration
Browse files Browse the repository at this point in the history
This API provides the following:
* All OIDC issuers, including the meta/wildcard issuers
* The expected audience of the token
* The claim that must be signed for a proof of possession
* The SPIFFE trust domain, when the issuer is of type SPIFFE

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
haydentherapper committed May 24, 2022
1 parent 49429f0 commit 8de5b99
Show file tree
Hide file tree
Showing 10 changed files with 745 additions and 47 deletions.
36 changes: 36 additions & 0 deletions fulcio.proto
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ service CA {
get: "/api/v2/trustBundle"
};
}

/**
* Returns the configuration of supported OIDC issuers, including the required challenge for each issuer.
*/
rpc GetConfiguration (GetConfigurationRequest) returns (Configuration) {
option (google.api.http) = {
get: "/api/v2/configuration"
};
}
}

message CreateSigningCertificateRequest {
Expand Down Expand Up @@ -166,3 +175,30 @@ enum PublicKeyAlgorithm {
ECDSA = 2;
ED25519 = 3;
}

// This is created for forward compatibility in case we want to add fields in the future.
message GetConfigurationRequest {
}

// The configuration for the Fulcio instance.
message Configuration {
// The OIDC issuers supported by this Fulcio instance.
repeated OIDCIssuer issuers = 1;
}

// Metadata about an OIDC issuer.
message OIDCIssuer {
oneof issuer {
// The URL of the OIDC issuer.
string issuer_url = 1;
// The URL of wildcard OIDC issuer, e.g. "https://oidc.eks.*.amazonaws.com/id/*".
// When comparing the issuer, the wildcards will be replaced by "[-_a-zA-Z0-9]+".
string wildcard_issuer_url = 2;
}
// The expected audience of the OIDC token for the issuer.
string audience = 3;
// The OIDC claim that must be signed for a proof of possession challenge.
string challenge_claim = 4;
// The expected SPIFFE trust domain. Only present when the OIDC issuer issues tokens for SPIFFE identities.
string spiffe_trust_domain = 5;
}
16 changes: 16 additions & 0 deletions pkg/api/grpc_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"crypto"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -246,6 +247,21 @@ func (g *grpcCAServer) GetTrustBundle(ctx context.Context, _ *fulciogrpc.GetTrus
}, nil
}

func (g *grpcCAServer) GetConfiguration(ctx context.Context, _ *fulciogrpc.GetConfigurationRequest) (*fulciogrpc.Configuration, error) {
logger := log.ContextLogger(ctx)

cfg := config.FromContext(ctx)
if cfg == nil {
err := errors.New("configuration not loaded")
logger.Error(err)
return nil, handleFulcioGRPCError(ctx, codes.Internal, err, genericCAError)
}

return &fulciogrpc.Configuration{
Issuers: cfg.ToIssuers(),
}, nil
}

func extractIssuer(token string) (string, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
Expand Down
126 changes: 126 additions & 0 deletions pkg/api/grpc_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,132 @@ func TestGetTrustBundleSuccess(t *testing.T) {
}
}

// Tests GetConfiguration API
func TestGetConfiguration(t *testing.T) {
_, emailIssuer := newOIDCIssuer(t)
_, spiffeIssuer := newOIDCIssuer(t)
_, uriIssuer := newOIDCIssuer(t)
_, usernameIssuer := newOIDCIssuer(t)
_, k8sIssuer := newOIDCIssuer(t)
_, gitHubIssuer := newOIDCIssuer(t)

issuerDomain, err := url.Parse(usernameIssuer)
if err != nil {
t.Fatal("issuer URL could not be parsed", err)
}

cfg, err := config.Read([]byte(fmt.Sprintf(`{
"OIDCIssuers": {
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"Type": "spiffe",
"SPIFFETrustDomain": "example.com"
},
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"SubjectDomain": %q,
"Type": "uri"
},
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"Type": "email"
},
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"SubjectDomain": %q,
"Type": "username"
},
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"Type": "github-workflow"
}
},
"MetaIssuers": {
%q: {
"ClientID": "sigstore",
"Type": "kubernetes"
}
}
}`, spiffeIssuer, spiffeIssuer,
uriIssuer, uriIssuer, uriIssuer,
emailIssuer, emailIssuer,
usernameIssuer, usernameIssuer, issuerDomain.Hostname(),
gitHubIssuer, gitHubIssuer,
k8sIssuer)))
if err != nil {
t.Fatalf("config.Read() = %v", err)
}

ctClient, eca := createCA(cfg, t)
ctx := context.Background()
server, conn := setupGRPCForTest(ctx, t, cfg, ctClient, eca)
defer func() {
server.Stop()
conn.Close()
}()

client := protobuf.NewCAClient(conn)

config, err := client.GetConfiguration(ctx, &protobuf.GetConfigurationRequest{})
if err != nil {
t.Fatal("GetConfiguration failed", err)
}

if len(config.Issuers) != 6 {
t.Fatalf("expected 6 issuers, got %v", len(config.Issuers))
}

expectedIssuers := map[string]bool{
emailIssuer: true, spiffeIssuer: true, uriIssuer: true,
usernameIssuer: true, k8sIssuer: true, gitHubIssuer: true,
}
for _, iss := range config.Issuers {
var issURL string
if expectedIssuers[iss.GetIssuerUrl()] {
delete(expectedIssuers, iss.GetIssuerUrl())
issURL = iss.GetIssuerUrl()
} else if expectedIssuers[iss.GetWildcardIssuerUrl()] {
delete(expectedIssuers, iss.GetWildcardIssuerUrl())
issURL = iss.GetWildcardIssuerUrl()
} else {
t.Fatal("issuer missing from expected issuers")
}

if iss.Audience != "sigstore" {
t.Fatalf("expected audience to be sigstore, got %v", iss.Audience)
}

if issURL == emailIssuer {
if iss.ChallengeClaim != "email" {
t.Fatalf("expected email claim for email PoP challenge, got %v", iss.ChallengeClaim)
}
} else {
if iss.ChallengeClaim != "sub" {
t.Fatalf("expected sub claim for non-email PoP challenge, got %v", iss.ChallengeClaim)
}
}

if issURL == spiffeIssuer {
if iss.SpiffeTrustDomain != "example.com" {
t.Fatalf("expected SPIFFE trust domain example.com, got %v", iss.SpiffeTrustDomain)
}
} else {
if iss.SpiffeTrustDomain != "" {
t.Fatalf("expected no SPIFFE trust domain, got %v", iss.SpiffeTrustDomain)
}
}
}

if len(expectedIssuers) != 0 {
t.Fatal("not all issuers were found in configuration")
}
}

// oidcTestContainer holds values needed for each API test invocation
type oidcTestContainer struct {
Signer jose.Signer
Expand Down
55 changes: 55 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

"github.com/coreos/go-oidc/v3/oidc"
lru "github.com/hashicorp/golang-lru"
fulciogrpc "github.com/sigstore/fulcio/pkg/generated/protobuf"
"github.com/sigstore/fulcio/pkg/log"
"github.com/spiffe/go-spiffe/v2/spiffeid"
)
Expand Down Expand Up @@ -158,6 +159,33 @@ func (fc *FulcioConfig) GetVerifier(issuerURL string) (*oidc.IDTokenVerifier, bo
return verifier, true
}

// ToIssuers returns a proto representation of the OIDC issuer configuration.
func (fc *FulcioConfig) ToIssuers() []*fulciogrpc.OIDCIssuer {
var issuers []*fulciogrpc.OIDCIssuer

for _, cfgIss := range fc.OIDCIssuers {
issuer := &fulciogrpc.OIDCIssuer{
Issuer: &fulciogrpc.OIDCIssuer_IssuerUrl{IssuerUrl: cfgIss.IssuerURL},
Audience: cfgIss.ClientID,
SpiffeTrustDomain: cfgIss.SPIFFETrustDomain,
ChallengeClaim: issuerToChallengeClaim(cfgIss.Type),
}
issuers = append(issuers, issuer)
}

for metaIss, cfgIss := range fc.MetaIssuers {
issuer := &fulciogrpc.OIDCIssuer{
Issuer: &fulciogrpc.OIDCIssuer_WildcardIssuerUrl{WildcardIssuerUrl: metaIss},
Audience: cfgIss.ClientID,
SpiffeTrustDomain: cfgIss.SPIFFETrustDomain,
ChallengeClaim: issuerToChallengeClaim(cfgIss.Type),
}
issuers = append(issuers, issuer)
}

return issuers
}

func (fc *FulcioConfig) prepare() error {
fc.verifiers = make(map[string]*oidc.IDTokenVerifier, len(fc.OIDCIssuers))
for _, iss := range fc.OIDCIssuers {
Expand Down Expand Up @@ -274,6 +302,10 @@ func validateConfig(conf *FulcioConfig) error {
return err
}
}

if issuerToChallengeClaim(issuer.Type) == "" {
return errors.New("issuer missing challenge claim")
}
}

for _, metaIssuer := range conf.MetaIssuers {
Expand All @@ -282,6 +314,10 @@ func validateConfig(conf *FulcioConfig) error {
// to trust domains so we fail early and reject this configuration.
return errors.New("SPIFFE meta issuers not supported")
}

if issuerToChallengeClaim(metaIssuer.Type) == "" {
return errors.New("issuer missing challenge claim")
}
}

return nil
Expand Down Expand Up @@ -421,3 +457,22 @@ func validateAllowedDomain(subjectHostname, issuerHostname string) error {
}
return fmt.Errorf("hostname top-level and second-level domains do not match: %s, %s", subjectHostname, issuerHostname)
}

func issuerToChallengeClaim(issType IssuerType) string {
switch issType {
case IssuerTypeEmail:
return "email"
case IssuerTypeGithubWorkflow:
return "sub"
case IssuerTypeKubernetes:
return "sub"
case IssuerTypeSpiffe:
return "sub"
case IssuerTypeURI:
return "sub"
case IssuerTypeUsername:
return "sub"
default:
return ""
}
}

0 comments on commit 8de5b99

Please sign in to comment.