...

Source file src/k8s.io/kubernetes/pkg/serviceaccount/openidmetadata.go

Documentation: k8s.io/kubernetes/pkg/serviceaccount

     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  }
   296  

View as plain text