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