-
Notifications
You must be signed in to change notification settings - Fork 38.8k
/
openidmetadata.go
295 lines (256 loc) · 9.37 KB
/
openidmetadata.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package serviceaccount
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/json"
"fmt"
"net/url"
jose "gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
)
const (
// OpenIDConfigPath is the URL path at which the API server serves
// an OIDC Provider Configuration Information document, corresponding
// to the Kubernetes Service Account key issuer.
// https://openid.net/specs/openid-connect-discovery-1_0.html
OpenIDConfigPath = "/.well-known/openid-configuration"
// JWKSPath is the URL path at which the API server serves a JWKS
// containing the public keys that may be used to sign Kubernetes
// Service Account keys.
JWKSPath = "/openid/v1/jwks"
)
// OpenIDMetadata contains the pre-rendered responses for OIDC discovery endpoints.
type OpenIDMetadata struct {
ConfigJSON []byte
PublicKeysetJSON []byte
}
// NewOpenIDMetadata returns the pre-rendered JSON responses for the OIDC discovery
// endpoints, or an error if they could not be constructed. Callers should note
// that this function may perform additional validation on inputs that is not
// backwards-compatible with all command-line validation. The recommendation is
// to log the error and skip installing the OIDC discovery endpoints.
func NewOpenIDMetadata(issuerURL, jwksURI, defaultExternalAddress string, pubKeys []interface{}) (*OpenIDMetadata, error) {
if issuerURL == "" {
return nil, fmt.Errorf("empty issuer URL")
}
if jwksURI == "" && defaultExternalAddress == "" {
return nil, fmt.Errorf("either the JWKS URI or the default external address, or both, must be set")
}
if len(pubKeys) == 0 {
return nil, fmt.Errorf("no keys provided for validating keyset")
}
// Ensure the issuer URL meets the OIDC spec (this is the additional
// validation the doc comment warns about).
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
iss, err := url.Parse(issuerURL)
if err != nil {
return nil, err
}
if iss.Scheme != "https" {
return nil, fmt.Errorf("issuer URL must use https scheme, got: %s", issuerURL)
}
if iss.RawQuery != "" {
return nil, fmt.Errorf("issuer URL may not include a query, got: %s", issuerURL)
}
if iss.Fragment != "" {
return nil, fmt.Errorf("issuer URL may not include a fragment, got: %s", issuerURL)
}
// Either use the provided JWKS URI or default to ExternalAddress plus
// the JWKS path.
if jwksURI == "" {
const msg = "attempted to build jwks_uri from external " +
"address %s, but could not construct a valid URL. Error: %v"
if defaultExternalAddress == "" {
return nil, fmt.Errorf(msg, defaultExternalAddress,
fmt.Errorf("empty address"))
}
u := &url.URL{
Scheme: "https",
Host: defaultExternalAddress,
Path: JWKSPath,
}
jwksURI = u.String()
// TODO(mtaufen): I think we can probably expect ExternalAddress is
// at most just host + port and skip the sanity check, but want to be
// careful until that is confirmed.
// Sanity check that the jwksURI we produced is the valid URL we expect.
// This is just in case ExternalAddress came in as something weird,
// like a scheme + host + port, instead of just host + port.
parsed, err := url.Parse(jwksURI)
if err != nil {
return nil, fmt.Errorf(msg, defaultExternalAddress, err)
} else if u.Scheme != parsed.Scheme ||
u.Host != parsed.Host ||
u.Path != parsed.Path {
return nil, fmt.Errorf(msg, defaultExternalAddress,
fmt.Errorf("got %v, expected %v", parsed, u))
}
} else {
// Double-check that jwksURI is an https URL
if u, err := url.Parse(jwksURI); err != nil {
return nil, err
} else if u.Scheme != "https" {
return nil, fmt.Errorf("jwksURI requires https scheme, parsed as: %v", u.String())
}
}
configJSON, err := openIDConfigJSON(issuerURL, jwksURI, pubKeys)
if err != nil {
return nil, fmt.Errorf("could not marshal issuer discovery JSON, error: %v", err)
}
keysetJSON, err := openIDKeysetJSON(pubKeys)
if err != nil {
return nil, fmt.Errorf("could not marshal issuer keys JSON, error: %v", err)
}
return &OpenIDMetadata{
ConfigJSON: configJSON,
PublicKeysetJSON: keysetJSON,
}, nil
}
// openIDMetadata provides a minimal subset of OIDC provider metadata:
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
type openIDMetadata struct {
Issuer string `json:"issuer"` // REQUIRED in OIDC; meaningful to relying parties.
// TODO(mtaufen): Since our goal is compatibility for relying parties that
// need to validate ID tokens, but do not need to initiate login flows,
// and since we aren't sure what to put in authorization_endpoint yet,
// we will omit this field until someone files a bug.
// AuthzEndpoint string `json:"authorization_endpoint"` // REQUIRED in OIDC; but useless to relying parties.
JWKSURI string `json:"jwks_uri"` // REQUIRED in OIDC; meaningful to relying parties.
ResponseTypes []string `json:"response_types_supported"` // REQUIRED in OIDC
SubjectTypes []string `json:"subject_types_supported"` // REQUIRED in OIDC
SigningAlgs []string `json:"id_token_signing_alg_values_supported"` // REQUIRED in OIDC
}
// openIDConfigJSON returns the JSON OIDC Discovery Doc for the service
// account issuer.
func openIDConfigJSON(iss, jwksURI string, keys []interface{}) ([]byte, error) {
keyset, errs := publicJWKSFromKeys(keys)
if errs != nil {
return nil, errs
}
metadata := openIDMetadata{
Issuer: iss,
JWKSURI: jwksURI,
ResponseTypes: []string{"id_token"}, // Kubernetes only produces ID tokens
SubjectTypes: []string{"public"}, // https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
SigningAlgs: getAlgs(keyset), // REQUIRED by OIDC
}
metadataJSON, err := json.Marshal(metadata)
if err != nil {
return nil, fmt.Errorf("failed to marshal service account issuer metadata: %v", err)
}
return metadataJSON, nil
}
// openIDKeysetJSON returns the JSON Web Key Set for the service account
// issuer's keys.
func openIDKeysetJSON(keys []interface{}) ([]byte, error) {
keyset, errs := publicJWKSFromKeys(keys)
if errs != nil {
return nil, errs
}
keysetJSON, err := json.Marshal(keyset)
if err != nil {
return nil, fmt.Errorf("failed to marshal service account issuer JWKS: %v", err)
}
return keysetJSON, nil
}
func getAlgs(keys *jose.JSONWebKeySet) []string {
algs := sets.NewString()
for _, k := range keys.Keys {
algs.Insert(k.Algorithm)
}
// Note: List returns a sorted slice.
return algs.List()
}
type publicKeyGetter interface {
Public() crypto.PublicKey
}
// publicJWKSFromKeys constructs a JSONWebKeySet from a list of keys. The key
// set will only contain the public keys associated with the input keys.
func publicJWKSFromKeys(in []interface{}) (*jose.JSONWebKeySet, errors.Aggregate) {
// Decode keys into a JWKS.
var keys jose.JSONWebKeySet
var errs []error
for i, key := range in {
var pubkey *jose.JSONWebKey
var err error
switch k := key.(type) {
case publicKeyGetter:
// This is a private key. Get its public key
pubkey, err = jwkFromPublicKey(k.Public())
default:
pubkey, err = jwkFromPublicKey(k)
}
if err != nil {
errs = append(errs, fmt.Errorf("error constructing JWK for key #%d: %v", i, err))
continue
}
if !pubkey.Valid() {
errs = append(errs, fmt.Errorf("key #%d not valid", i))
continue
}
keys.Keys = append(keys.Keys, *pubkey)
}
if len(errs) != 0 {
return nil, errors.NewAggregate(errs)
}
return &keys, nil
}
func jwkFromPublicKey(publicKey crypto.PublicKey) (*jose.JSONWebKey, error) {
alg, err := algorithmFromPublicKey(publicKey)
if err != nil {
return nil, err
}
keyID, err := keyIDFromPublicKey(publicKey)
if err != nil {
return nil, err
}
jwk := &jose.JSONWebKey{
Algorithm: string(alg),
Key: publicKey,
KeyID: keyID,
Use: "sig",
}
if !jwk.IsPublic() {
return nil, fmt.Errorf("JWK was not a public key! JWK: %v", jwk)
}
return jwk, nil
}
func algorithmFromPublicKey(publicKey crypto.PublicKey) (jose.SignatureAlgorithm, error) {
switch pk := publicKey.(type) {
case *rsa.PublicKey:
// IMPORTANT: If this function is updated to support additional key sizes,
// signerFromRSAPrivateKey in serviceaccount/jwt.go must also be
// updated to support the same key sizes. Today we only support RS256.
return jose.RS256, nil
case *ecdsa.PublicKey:
switch pk.Curve {
case elliptic.P256():
return jose.ES256, nil
case elliptic.P384():
return jose.ES384, nil
case elliptic.P521():
return jose.ES512, nil
default:
return "", fmt.Errorf("unknown private key curve, must be 256, 384, or 521")
}
case jose.OpaqueSigner:
return jose.SignatureAlgorithm(pk.Public().Algorithm), nil
default:
return "", fmt.Errorf("unknown public key type, must be *rsa.PublicKey, *ecdsa.PublicKey, or jose.OpaqueSigner")
}
}