Skip to content

Commit d640505

Browse files
Add URI OIDC type to support URI subjects (#455)
* 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]>
1 parent e88278c commit d640505

File tree

6 files changed

+372
-107
lines changed

6 files changed

+372
-107
lines changed

pkg/api/api_test.go

Lines changed: 137 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -73,127 +73,165 @@ func TestMissingRootFails(t *testing.T) {
7373
}
7474
}
7575

76-
func TestAPI(t *testing.T) {
77-
signer, issuer := newOIDCIssuer(t)
78-
79-
subject := strings.ReplaceAll(issuer+"/foo/bar", "http", "spiffe")
80-
81-
// Create an OIDC token using this issuer's signer.
82-
tok, err := jwt.Signed(signer).Claims(jwt.Claims{
83-
Issuer: issuer,
84-
IssuedAt: jwt.NewNumericDate(time.Now()),
85-
Expiry: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
86-
Subject: subject,
87-
Audience: jwt.Audience{"sigstore"},
88-
}).CompactSerialize()
89-
if err != nil {
90-
t.Fatalf("CompactSerialize() = %v", err)
91-
}
76+
// oidcTestContainer holds values needed for each API test invocation
77+
type oidcTestContainer struct {
78+
Signer jose.Signer
79+
Issuer string
80+
Subject string
81+
}
9282

93-
// Create a FulcioConfig that supports this issuer.
83+
// Tests API for SPIFFE and URI subject types
84+
func TestAPIWithUriSubject(t *testing.T) {
85+
spiffeSigner, spiffeIssuer := newOIDCIssuer(t)
86+
uriSigner, uriIssuer := newOIDCIssuer(t)
87+
88+
// Create a FulcioConfig that supports these issuers.
9489
cfg, err := config.Read([]byte(fmt.Sprintf(`{
9590
"OIDCIssuers": {
9691
%q: {
9792
"IssuerURL": %q,
9893
"ClientID": "sigstore",
9994
"Type": "spiffe"
95+
},
96+
%q: {
97+
"IssuerURL": %q,
98+
"ClientID": "sigstore",
99+
"SubjectDomain": %q,
100+
"Type": "uri"
100101
}
101102
}
102-
}`, issuer, issuer)))
103+
}`, spiffeIssuer, spiffeIssuer, uriIssuer, uriIssuer, uriIssuer)))
103104
if err != nil {
104105
t.Fatalf("config.Read() = %v", err)
105106
}
106107

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

113-
ctlogServer := fakeCTLogServer(t)
114-
if ctlogServer == nil {
115-
t.Fatalf("Failed to create the fake ctlog server")
116-
}
111+
for _, c := range []oidcTestContainer{
112+
{
113+
Signer: spiffeSigner, Issuer: spiffeIssuer, Subject: spiffeSubject,
114+
},
115+
{
116+
Signer: uriSigner, Issuer: uriIssuer, Subject: uriSubject,
117+
}} {
118+
// Create an OIDC token using this issuer's signer.
119+
tok, err := jwt.Signed(c.Signer).Claims(jwt.Claims{
120+
Issuer: c.Issuer,
121+
IssuedAt: jwt.NewNumericDate(time.Now()),
122+
Expiry: jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
123+
Subject: c.Subject,
124+
Audience: jwt.Audience{"sigstore"},
125+
}).CompactSerialize()
126+
if err != nil {
127+
t.Fatalf("CompactSerialize() = %v", err)
128+
}
117129

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

125-
h.ServeHTTP(rw, r.WithContext(ctx))
126-
}))
127-
t.Cleanup(server.Close)
136+
ctlogServer := fakeCTLogServer(t)
137+
if ctlogServer == nil {
138+
t.Fatalf("Failed to create the fake ctlog server")
139+
}
128140

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

136-
// Sign the subject with our keypair, and provide the public key
137-
// for verification.
138-
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
139-
if err != nil {
140-
t.Fatalf("GenerateKey() = %v", err)
141-
}
142-
pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
143-
if err != nil {
144-
t.Fatalf("x509.MarshalPKIXPublicKey() = %v", err)
145-
}
146-
hash := sha256.Sum256([]byte(subject))
147-
proof, err := ecdsa.SignASN1(rand.Reader, priv, hash[:])
148-
if err != nil {
149-
t.Fatalf("SignASN1() = %v", err)
150-
}
148+
h.ServeHTTP(rw, r.WithContext(ctx))
149+
}))
150+
t.Cleanup(server.Close)
151151

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

163-
if string(resp.SCT) == "" {
164-
t.Error("Did not get SCT")
165-
}
159+
// Sign the subject with our keypair, and provide the public key
160+
// for verification.
161+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
162+
if err != nil {
163+
t.Fatalf("GenerateKey() = %v", err)
164+
}
165+
pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
166+
if err != nil {
167+
t.Fatalf("x509.MarshalPKIXPublicKey() = %v", err)
168+
}
169+
hash := sha256.Sum256([]byte(c.Subject))
170+
proof, err := ecdsa.SignASN1(rand.Reader, priv, hash[:])
171+
if err != nil {
172+
t.Fatalf("SignASN1() = %v", err)
173+
}
166174

167-
// Check that we get the CA root back as well.
168-
root, err := client.RootCert()
169-
if err != nil {
170-
t.Fatal("Failed to get Root", err)
171-
}
172-
if root == nil {
173-
t.Fatal("Got nil root back")
174-
}
175-
if len(root.ChainPEM) == 0 {
176-
t.Fatal("Got back empty chain")
177-
}
178-
block, rest := pem.Decode(root.ChainPEM)
179-
if block == nil {
180-
t.Fatal("Did not find PEM data")
181-
}
182-
if len(rest) != 0 {
183-
t.Fatal("Got more than bargained for, should only have one cert")
184-
}
185-
if block.Type != "CERTIFICATE" {
186-
t.Fatalf("Unexpected root type, expected CERTIFICATE, got %s", block.Type)
187-
}
188-
rootCert, err := x509.ParseCertificate(block.Bytes)
189-
if err != nil {
190-
t.Fatalf("Failed to parse the received root cert: %v", err)
191-
}
192-
if !rootCert.Equal(eca.RootCA) {
193-
t.Errorf("Root CA does not match, wanted %+v got %+v", eca.RootCA, rootCert)
175+
// Hit the API to have it sign our certificate.
176+
resp, err := client.SigningCert(CertificateRequest{
177+
PublicKey: Key{
178+
Content: pubBytes,
179+
},
180+
SignedEmailAddress: proof,
181+
}, tok)
182+
if err != nil {
183+
t.Fatalf("SigningCert() = %v", err)
184+
}
185+
186+
if string(resp.SCT) == "" {
187+
t.Error("Did not get SCT")
188+
}
189+
190+
// Check that we get the CA root back as well.
191+
root, err := client.RootCert()
192+
if err != nil {
193+
t.Fatal("Failed to get Root", err)
194+
}
195+
if root == nil {
196+
t.Fatal("Got nil root back")
197+
}
198+
if len(root.ChainPEM) == 0 {
199+
t.Fatal("Got back empty chain")
200+
}
201+
block, rest := pem.Decode(root.ChainPEM)
202+
if block == nil {
203+
t.Fatal("Did not find PEM data")
204+
}
205+
if len(rest) != 0 {
206+
t.Fatal("Got more than bargained for, should only have one cert")
207+
}
208+
if block.Type != "CERTIFICATE" {
209+
t.Fatalf("Unexpected root type, expected CERTIFICATE, got %s", block.Type)
210+
}
211+
rootCert, err := x509.ParseCertificate(block.Bytes)
212+
if err != nil {
213+
t.Fatalf("Failed to parse the received root cert: %v", err)
214+
}
215+
if !rootCert.Equal(eca.RootCA) {
216+
t.Errorf("Root CA does not match, wanted %+v got %+v", eca.RootCA, rootCert)
217+
}
218+
// Compare leaf certificate values
219+
block, _ = pem.Decode(resp.CertPEM)
220+
leafCert, err := x509.ParseCertificate(block.Bytes)
221+
if err != nil {
222+
t.Fatalf("Failed to parse the received leaf cert: %v", err)
223+
}
224+
if len(leafCert.URIs) != 1 {
225+
t.Fatalf("Unexpected length of leaf certificate URIs, expected 1, got %d", len(leafCert.URIs))
226+
}
227+
uSubject, err := url.Parse(c.Subject)
228+
if err != nil {
229+
t.Fatalf("Failed to parse subject URI")
230+
}
231+
if *leafCert.URIs[0] != *uSubject {
232+
t.Fatalf("Subjects do not match: Expected %v, got %v", uSubject, leafCert.URIs[0])
233+
}
194234
}
195-
// TODO(mattmoor): What interesting checks can we perform on
196-
// the other return values?
197235
}
198236

199237
// Stand up a very simple OIDC endpoint.

pkg/api/ca.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ func ExtractSubject(ctx context.Context, tok *oidc.IDToken, publicKey crypto.Pub
269269
return challenges.GithubWorkflow(ctx, tok, publicKey, challenge)
270270
case config.IssuerTypeKubernetes:
271271
return challenges.Kubernetes(ctx, tok, publicKey, challenge)
272+
case config.IssuerTypeURI:
273+
return challenges.URI(ctx, tok, publicKey, challenge)
272274
default:
273275
return nil, fmt.Errorf("unsupported issuer: %s", iss.Type)
274276
}

pkg/ca/x509ca/common.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ func MakeX509(subject *challenges.ChallengeResult) (*x509.Certificate, error) {
7979
return nil, ca.ValidationError(err)
8080
}
8181
cert.URIs = []*url.URL{k8sURI}
82+
case challenges.URIValue:
83+
subjectURI, err := url.Parse(subject.Value)
84+
if err != nil {
85+
return nil, ca.ValidationError(err)
86+
}
87+
cert.URIs = []*url.URL{subjectURI}
8288
}
8389
cert.ExtraExtensions = append(IssuerExtension(subject.Issuer), AdditionalExtensions(subject)...)
8490
return cert, nil

0 commit comments

Comments
 (0)