Skip to content

Commit

Permalink
Add API for fetching Fulcio configuration (#608)
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

Also fix the workflow's regex.

Signed-off-by: Hayden Blauzvern <[email protected]>
  • Loading branch information
haydentherapper authored Jun 1, 2022
1 parent 35b7117 commit 509c789
Show file tree
Hide file tree
Showing 12 changed files with 755 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ jobs:
- name: Upload Coverage Report
uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 # v3.0.0
- name: Ensure no files were modified as a result of the build
run: git update-index --refresh && git diff-index --quiet -I"^\/\/\s+protoc(-gen-go)?\s+v[0-9]+\.[0-9]+\.[0-9]+$" HEAD -- || git diff -I"^\/\/\s+protoc(-gen-go)?\s+v[0-9]+\.[0-9]+\.[0-9]+$" --exit-code
run: git update-index --refresh && git diff-index --quiet -I"^\/\/\s+(-\s+)?protoc(-gen-go)?\s+v[0-9]+\.[0-9]+\.[0-9]+$" HEAD -- || git diff -I"^\/\/\s+(-\s+)?protoc(-gen-go)?\s+v[0-9]+\.[0-9]+\.[0-9]+$" --exit-code
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;
}
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 ""
}
}
5 changes: 4 additions & 1 deletion pkg/config/config_network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ func TestLoad(t *testing.T) {
t.Fatal(err)
}

cfg, _ := Load(cfgPath)
cfg, err := Load(cfgPath)
if err != nil {
t.Fatal(err)
}
got, ok := cfg.GetIssuer("https://accounts.google.com")
if !ok {
t.Error("expected true, got false")
Expand Down
90 changes: 88 additions & 2 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ package config
import (
"fmt"
"net/url"
"reflect"
"testing"

"github.com/sigstore/fulcio/pkg/generated/protobuf"
)

var validCfg = `
{
"OIDCIssuers": {
"https://accounts.google.com": {
"IssuerURL": "https://accounts.google.com",
"ClientID": "foo"
"ClientID": "foo",
"Type": "email"
}
},
"MetaIssuers": {
"https://oidc.eks.*.amazonaws.com/id/*": {
"ClientID": "bar"
"ClientID": "bar",
"Type": "kubernetes"
}
}
}
Expand Down Expand Up @@ -299,6 +304,18 @@ func TestValidateConfig(t *testing.T) {
},
WantError: true,
},
"type without challenge claim is invalid": {
Config: &FulcioConfig{
OIDCIssuers: map[string]OIDCIssuer{
"https://issuer.example.com": {
IssuerURL: "htts://issuer.example.com",
ClientID: "sigstore",
Type: "invalid",
},
},
},
WantError: true,
},
"nil config isn't valid": {
Config: nil,
WantError: true,
Expand Down Expand Up @@ -451,3 +468,72 @@ func Test_validateAllowedDomain(t *testing.T) {
})
}
}

func Test_issuerToChallengeClaim(t *testing.T) {
if claim := issuerToChallengeClaim(IssuerTypeEmail); claim != "email" {
t.Fatalf("expected email subject claim for email issuer, got %s", claim)
}
if claim := issuerToChallengeClaim(IssuerTypeSpiffe); claim != "sub" {
t.Fatalf("expected sub subject claim for SPIFFE issuer, got %s", claim)
}
if claim := issuerToChallengeClaim(IssuerTypeUsername); claim != "sub" {
t.Fatalf("expected sub subject claim for username issuer, got %s", claim)
}
if claim := issuerToChallengeClaim(IssuerTypeURI); claim != "sub" {
t.Fatalf("expected sub subject claim for URI issuer, got %s", claim)
}
if claim := issuerToChallengeClaim(IssuerTypeGithubWorkflow); claim != "sub" {
t.Fatalf("expected sub subject claim for GitHub issuer, got %s", claim)
}
if claim := issuerToChallengeClaim(IssuerTypeKubernetes); claim != "sub" {
t.Fatalf("expected sub subject claim for K8S issuer, got %s", claim)
}
// unexpected issuer has empty claim
if claim := issuerToChallengeClaim("invalid"); claim != "" {
t.Fatalf("expected no claim for invalid issuer, got %s", claim)
}
}

func TestToIssuers(t *testing.T) {
config := &FulcioConfig{
OIDCIssuers: map[string]OIDCIssuer{
"example.com": {
IssuerURL: "example.com",
ClientID: "sigstore",
Type: IssuerTypeEmail,
},
},
MetaIssuers: map[string]OIDCIssuer{
"wildcard.*.example.com": {
ClientID: "sigstore",
Type: IssuerTypeKubernetes,
},
},
}

issuers := config.ToIssuers()
if len(issuers) != 2 {
t.Fatalf("unexpected number of issues, expected 2, got %v", len(issuers))
}

iss := &protobuf.OIDCIssuer{
Audience: "sigstore",
ChallengeClaim: "email",
Issuer: &protobuf.OIDCIssuer_IssuerUrl{
IssuerUrl: "example.com",
},
}
if !reflect.DeepEqual(issuers[0], iss) {
t.Fatalf("expected issuer %v, got %v", iss, issuers[0])
}
iss = &protobuf.OIDCIssuer{
Audience: "sigstore",
ChallengeClaim: "sub",
Issuer: &protobuf.OIDCIssuer_WildcardIssuerUrl{
WildcardIssuerUrl: "wildcard.*.example.com",
},
}
if !reflect.DeepEqual(issuers[1], iss) {
t.Fatalf("expected issuer %v, got %v", iss, issuers[1])
}
}
Loading

0 comments on commit 509c789

Please sign in to comment.