openidmetadata.go 9.4 KB

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