| /* |
| Copyright 2014 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 ( |
| "context" |
| "crypto/ecdsa" |
| "crypto/elliptic" |
| "crypto/rsa" |
| "encoding/base64" |
| "encoding/json" |
| "fmt" |
| "strings" |
| |
| jose "gopkg.in/square/go-jose.v2" |
| "gopkg.in/square/go-jose.v2/jwt" |
| |
| "k8s.io/api/core/v1" |
| utilerrors "k8s.io/apimachinery/pkg/util/errors" |
| "k8s.io/apiserver/pkg/authentication/authenticator" |
| ) |
| |
| // ServiceAccountTokenGetter defines functions to retrieve a named service account and secret |
| type ServiceAccountTokenGetter interface { |
| GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) |
| GetPod(namespace, name string) (*v1.Pod, error) |
| GetSecret(namespace, name string) (*v1.Secret, error) |
| } |
| |
| type TokenGenerator interface { |
| // GenerateToken generates a token which will identify the given |
| // ServiceAccount. privateClaims is an interface that will be |
| // serialized into the JWT payload JSON encoding at the root level of |
| // the payload object. Public claims take precedent over private |
| // claims i.e. if both claims and privateClaims have an "exp" field, |
| // the value in claims will be used. |
| GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error) |
| } |
| |
| // JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey. |
| // privateKey is a PEM-encoded byte array of a private RSA key. |
| // JWTTokenAuthenticator() |
| func JWTTokenGenerator(iss string, privateKey interface{}) (TokenGenerator, error) { |
| var alg jose.SignatureAlgorithm |
| switch pk := privateKey.(type) { |
| case *rsa.PrivateKey: |
| alg = jose.RS256 |
| case *ecdsa.PrivateKey: |
| switch pk.Curve { |
| case elliptic.P256(): |
| alg = jose.ES256 |
| case elliptic.P384(): |
| alg = jose.ES384 |
| case elliptic.P521(): |
| alg = jose.ES512 |
| default: |
| return nil, fmt.Errorf("unknown private key curve, must be 256, 384, or 521") |
| } |
| case jose.OpaqueSigner: |
| alg = jose.SignatureAlgorithm(pk.Public().Algorithm) |
| default: |
| return nil, fmt.Errorf("unknown private key type %T, must be *rsa.PrivateKey, *ecdsa.PrivateKey, or jose.OpaqueSigner", privateKey) |
| } |
| |
| signer, err := jose.NewSigner( |
| jose.SigningKey{ |
| Algorithm: alg, |
| Key: privateKey, |
| }, |
| nil, |
| ) |
| if err != nil { |
| return nil, err |
| } |
| return &jwtTokenGenerator{ |
| iss: iss, |
| signer: signer, |
| }, nil |
| } |
| |
| type jwtTokenGenerator struct { |
| iss string |
| signer jose.Signer |
| } |
| |
| func (j *jwtTokenGenerator) GenerateToken(claims *jwt.Claims, privateClaims interface{}) (string, error) { |
| // claims are applied in reverse precedence |
| return jwt.Signed(j.signer). |
| Claims(privateClaims). |
| Claims(claims). |
| Claims(&jwt.Claims{ |
| Issuer: j.iss, |
| }). |
| CompactSerialize() |
| } |
| |
| // JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator |
| // Token signatures are verified using each of the given public keys until one works (allowing key rotation) |
| // If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified with the provided ServiceAccountTokenGetter |
| func JWTTokenAuthenticator(iss string, keys []interface{}, implicitAuds authenticator.Audiences, validator Validator) authenticator.Token { |
| return &jwtTokenAuthenticator{ |
| iss: iss, |
| keys: keys, |
| implicitAuds: implicitAuds, |
| validator: validator, |
| } |
| } |
| |
| type jwtTokenAuthenticator struct { |
| iss string |
| keys []interface{} |
| validator Validator |
| implicitAuds authenticator.Audiences |
| } |
| |
| // Validator is called by the JWT token authenticator to apply domain specific |
| // validation to a token and extract user information. |
| type Validator interface { |
| // Validate validates a token and returns user information or an error. |
| // Validator can assume that the issuer and signature of a token are already |
| // verified when this function is called. |
| Validate(tokenData string, public *jwt.Claims, private interface{}) (*ServiceAccountInfo, error) |
| // NewPrivateClaims returns a struct that the authenticator should |
| // deserialize the JWT payload into. The authenticator may then pass this |
| // struct back to the Validator as the 'private' argument to a Validate() |
| // call. This struct should contain fields for any private claims that the |
| // Validator requires to validate the JWT. |
| NewPrivateClaims() interface{} |
| } |
| |
| func (j *jwtTokenAuthenticator) AuthenticateToken(ctx context.Context, tokenData string) (*authenticator.Response, bool, error) { |
| if !j.hasCorrectIssuer(tokenData) { |
| return nil, false, nil |
| } |
| |
| tok, err := jwt.ParseSigned(tokenData) |
| if err != nil { |
| return nil, false, nil |
| } |
| |
| public := &jwt.Claims{} |
| private := j.validator.NewPrivateClaims() |
| |
| var ( |
| found bool |
| errlist []error |
| ) |
| for _, key := range j.keys { |
| if err := tok.Claims(key, public, private); err != nil { |
| errlist = append(errlist, err) |
| continue |
| } |
| found = true |
| break |
| } |
| |
| if !found { |
| return nil, false, utilerrors.NewAggregate(errlist) |
| } |
| |
| tokenAudiences := authenticator.Audiences(public.Audience) |
| if len(tokenAudiences) == 0 { |
| // only apiserver audiences are allowed for legacy tokens |
| tokenAudiences = j.implicitAuds |
| } |
| |
| requestedAudiences, ok := authenticator.AudiencesFrom(ctx) |
| if !ok { |
| // default to apiserver audiences |
| requestedAudiences = j.implicitAuds |
| } |
| |
| auds := authenticator.Audiences(tokenAudiences).Intersect(requestedAudiences) |
| if len(auds) == 0 && len(j.implicitAuds) != 0 { |
| return nil, false, fmt.Errorf("token audiences %q is invalid for the target audiences %q", tokenAudiences, requestedAudiences) |
| } |
| |
| // If we get here, we have a token with a recognized signature and |
| // issuer string. |
| sa, err := j.validator.Validate(tokenData, public, private) |
| if err != nil { |
| return nil, false, err |
| } |
| |
| return &authenticator.Response{ |
| User: sa.UserInfo(), |
| Audiences: auds, |
| }, true, nil |
| } |
| |
| // hasCorrectIssuer returns true if tokenData is a valid JWT in compact |
| // serialization format and the "iss" claim matches the iss field of this token |
| // authenticator, and otherwise returns false. |
| // |
| // Note: go-jose currently does not allow access to unverified JWS payloads. |
| // See https://github.com/square/go-jose/issues/169 |
| func (j *jwtTokenAuthenticator) hasCorrectIssuer(tokenData string) bool { |
| parts := strings.Split(tokenData, ".") |
| if len(parts) != 3 { |
| return false |
| } |
| payload, err := base64.RawURLEncoding.DecodeString(parts[1]) |
| if err != nil { |
| return false |
| } |
| claims := struct { |
| // WARNING: this JWT is not verified. Do not trust these claims. |
| Issuer string `json:"iss"` |
| }{} |
| if err := json.Unmarshal(payload, &claims); err != nil { |
| return false |
| } |
| if claims.Issuer != j.iss { |
| return false |
| } |
| return true |
| |
| } |