Skip to content

Commit

Permalink
Add URI OIDC type to support URI subjects (#455)
Browse files Browse the repository at this point in the history
* Add URI OIDC type to support URI subjects

Implementing the first part of #398, which adds support
for subjects in OIDC tokens that are URIs. The implementation
is very similar to SPIFFE-based tokens.

Tokens must conform to the following:
* The issuer of the token must partially match the domain in the
  configuration. This means that the scheme, top level domain, and
  second level domain must match. It is also expected that we validate
  that the requester who adds the configuration for the issuer has
  control over both the issuer and domain configuration fields (ACME).
* The domain of the configuration and hostname of the subject of the
  token must match exactly.

Slightly reworked the API test to test this issuer type. I'll
follow up in a later PR with some more refactoring around this
class, I think we can exercise the codepaths for all issuers.

Signed-off-by: Hayden Blauzvern <[email protected]>

* Style changes based on comments

Signed-off-by: Hayden Blauzvern <[email protected]>
  • Loading branch information
haydentherapper authored Mar 11, 2022
1 parent e88278c commit d640505
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 107 deletions.
236 changes: 137 additions & 99 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,127 +73,165 @@ func TestMissingRootFails(t *testing.T) {
}
}

func TestAPI(t *testing.T) {
signer, issuer := newOIDCIssuer(t)

subject := strings.ReplaceAll(issuer+"/foo/bar", "http", "spiffe")

// Create an OIDC token using this issuer's signer.
tok, err := jwt.Signed(signer).Claims(jwt.Claims{
Issuer: issuer,
IssuedAt: jwt.NewNumericDate(time.Now()),
Expiry: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
Subject: subject,
Audience: jwt.Audience{"sigstore"},
}).CompactSerialize()
if err != nil {
t.Fatalf("CompactSerialize() = %v", err)
}
// oidcTestContainer holds values needed for each API test invocation
type oidcTestContainer struct {
Signer jose.Signer
Issuer string
Subject string
}

// Create a FulcioConfig that supports this issuer.
// Tests API for SPIFFE and URI subject types
func TestAPIWithUriSubject(t *testing.T) {
spiffeSigner, spiffeIssuer := newOIDCIssuer(t)
uriSigner, uriIssuer := newOIDCIssuer(t)

// Create a FulcioConfig that supports these issuers.
cfg, err := config.Read([]byte(fmt.Sprintf(`{
"OIDCIssuers": {
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"Type": "spiffe"
},
%q: {
"IssuerURL": %q,
"ClientID": "sigstore",
"SubjectDomain": %q,
"Type": "uri"
}
}
}`, issuer, issuer)))
}`, spiffeIssuer, spiffeIssuer, uriIssuer, uriIssuer, uriIssuer)))
if err != nil {
t.Fatalf("config.Read() = %v", err)
}

// Stand up an ephemeral CA we can use for signing certificate requests.
eca, err := ephemeralca.NewEphemeralCA()
if err != nil {
t.Fatalf("ephemeralca.NewEphemeralCA() = %v", err)
}
spiffeSubject := strings.ReplaceAll(spiffeIssuer+"/foo/bar", "http", "spiffe")
uriSubject := uriIssuer + "/users/1"

ctlogServer := fakeCTLogServer(t)
if ctlogServer == nil {
t.Fatalf("Failed to create the fake ctlog server")
}
for _, c := range []oidcTestContainer{
{
Signer: spiffeSigner, Issuer: spiffeIssuer, Subject: spiffeSubject,
},
{
Signer: uriSigner, Issuer: uriIssuer, Subject: uriSubject,
}} {
// Create an OIDC token using this issuer's signer.
tok, err := jwt.Signed(c.Signer).Claims(jwt.Claims{
Issuer: c.Issuer,
IssuedAt: jwt.NewNumericDate(time.Now()),
Expiry: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
Subject: c.Subject,
Audience: jwt.Audience{"sigstore"},
}).CompactSerialize()
if err != nil {
t.Fatalf("CompactSerialize() = %v", err)
}

// Create a test HTTP server to host our API.
h := New(ctl.New(ctlogServer.URL), eca)
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// For each request, infuse context with our snapshot of the FulcioConfig.
ctx = config.With(ctx, cfg)
// Stand up an ephemeral CA we can use for signing certificate requests.
eca, err := ephemeralca.NewEphemeralCA()
if err != nil {
t.Fatalf("ephemeralca.NewEphemeralCA() = %v", err)
}

h.ServeHTTP(rw, r.WithContext(ctx))
}))
t.Cleanup(server.Close)
ctlogServer := fakeCTLogServer(t)
if ctlogServer == nil {
t.Fatalf("Failed to create the fake ctlog server")
}

// Create an API client that speaks to the API endpoint we created above.
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("url.Parse() = %v", err)
}
client := NewClient(u)
// Create a test HTTP server to host our API.
h := New(ctl.New(ctlogServer.URL), eca)
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// For each request, infuse context with our snapshot of the FulcioConfig.
ctx = config.With(ctx, cfg)

// Sign the subject with our keypair, and provide the public key
// for verification.
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey() = %v", err)
}
pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
t.Fatalf("x509.MarshalPKIXPublicKey() = %v", err)
}
hash := sha256.Sum256([]byte(subject))
proof, err := ecdsa.SignASN1(rand.Reader, priv, hash[:])
if err != nil {
t.Fatalf("SignASN1() = %v", err)
}
h.ServeHTTP(rw, r.WithContext(ctx))
}))
t.Cleanup(server.Close)

// Hit the API to have it sign our certificate.
resp, err := client.SigningCert(CertificateRequest{
PublicKey: Key{
Content: pubBytes,
},
SignedEmailAddress: proof,
}, tok)
if err != nil {
t.Fatalf("SigningCert() = %v", err)
}
// Create an API client that speaks to the API endpoint we created above.
u, err := url.Parse(server.URL)
if err != nil {
t.Fatalf("url.Parse() = %v", err)
}
client := NewClient(u)

if string(resp.SCT) == "" {
t.Error("Did not get SCT")
}
// Sign the subject with our keypair, and provide the public key
// for verification.
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("GenerateKey() = %v", err)
}
pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
if err != nil {
t.Fatalf("x509.MarshalPKIXPublicKey() = %v", err)
}
hash := sha256.Sum256([]byte(c.Subject))
proof, err := ecdsa.SignASN1(rand.Reader, priv, hash[:])
if err != nil {
t.Fatalf("SignASN1() = %v", err)
}

// Check that we get the CA root back as well.
root, err := client.RootCert()
if err != nil {
t.Fatal("Failed to get Root", err)
}
if root == nil {
t.Fatal("Got nil root back")
}
if len(root.ChainPEM) == 0 {
t.Fatal("Got back empty chain")
}
block, rest := pem.Decode(root.ChainPEM)
if block == nil {
t.Fatal("Did not find PEM data")
}
if len(rest) != 0 {
t.Fatal("Got more than bargained for, should only have one cert")
}
if block.Type != "CERTIFICATE" {
t.Fatalf("Unexpected root type, expected CERTIFICATE, got %s", block.Type)
}
rootCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("Failed to parse the received root cert: %v", err)
}
if !rootCert.Equal(eca.RootCA) {
t.Errorf("Root CA does not match, wanted %+v got %+v", eca.RootCA, rootCert)
// Hit the API to have it sign our certificate.
resp, err := client.SigningCert(CertificateRequest{
PublicKey: Key{
Content: pubBytes,
},
SignedEmailAddress: proof,
}, tok)
if err != nil {
t.Fatalf("SigningCert() = %v", err)
}

if string(resp.SCT) == "" {
t.Error("Did not get SCT")
}

// Check that we get the CA root back as well.
root, err := client.RootCert()
if err != nil {
t.Fatal("Failed to get Root", err)
}
if root == nil {
t.Fatal("Got nil root back")
}
if len(root.ChainPEM) == 0 {
t.Fatal("Got back empty chain")
}
block, rest := pem.Decode(root.ChainPEM)
if block == nil {
t.Fatal("Did not find PEM data")
}
if len(rest) != 0 {
t.Fatal("Got more than bargained for, should only have one cert")
}
if block.Type != "CERTIFICATE" {
t.Fatalf("Unexpected root type, expected CERTIFICATE, got %s", block.Type)
}
rootCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("Failed to parse the received root cert: %v", err)
}
if !rootCert.Equal(eca.RootCA) {
t.Errorf("Root CA does not match, wanted %+v got %+v", eca.RootCA, rootCert)
}
// Compare leaf certificate values
block, _ = pem.Decode(resp.CertPEM)
leafCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("Failed to parse the received leaf cert: %v", err)
}
if len(leafCert.URIs) != 1 {
t.Fatalf("Unexpected length of leaf certificate URIs, expected 1, got %d", len(leafCert.URIs))
}
uSubject, err := url.Parse(c.Subject)
if err != nil {
t.Fatalf("Failed to parse subject URI")
}
if *leafCert.URIs[0] != *uSubject {
t.Fatalf("Subjects do not match: Expected %v, got %v", uSubject, leafCert.URIs[0])
}
}
// TODO(mattmoor): What interesting checks can we perform on
// the other return values?
}

// Stand up a very simple OIDC endpoint.
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ func ExtractSubject(ctx context.Context, tok *oidc.IDToken, publicKey crypto.Pub
return challenges.GithubWorkflow(ctx, tok, publicKey, challenge)
case config.IssuerTypeKubernetes:
return challenges.Kubernetes(ctx, tok, publicKey, challenge)
case config.IssuerTypeURI:
return challenges.URI(ctx, tok, publicKey, challenge)
default:
return nil, fmt.Errorf("unsupported issuer: %s", iss.Type)
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/ca/x509ca/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func MakeX509(subject *challenges.ChallengeResult) (*x509.Certificate, error) {
return nil, ca.ValidationError(err)
}
cert.URIs = []*url.URL{k8sURI}
case challenges.URIValue:
subjectURI, err := url.Parse(subject.Value)
if err != nil {
return nil, ca.ValidationError(err)
}
cert.URIs = []*url.URL{subjectURI}
}
cert.ExtraExtensions = append(IssuerExtension(subject.Issuer), AdditionalExtensions(subject)...)
return cert, nil
Expand Down
Loading

0 comments on commit d640505

Please sign in to comment.