diff options
Diffstat (limited to 'vendor/k8s.io/kubernetes/pkg/serviceaccount/jwt.go')
-rw-r--r-- | vendor/k8s.io/kubernetes/pkg/serviceaccount/jwt.go | 314 |
1 files changed, 314 insertions, 0 deletions
diff --git a/vendor/k8s.io/kubernetes/pkg/serviceaccount/jwt.go b/vendor/k8s.io/kubernetes/pkg/serviceaccount/jwt.go new file mode 100644 index 000000000..38c063253 --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/serviceaccount/jwt.go @@ -0,0 +1,314 @@ +/* +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 ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + + "k8s.io/apiserver/pkg/authentication/authenticator" + apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/util/cert" + "k8s.io/kubernetes/pkg/api/v1" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/golang/glog" +) + +const ( + Issuer = "kubernetes/serviceaccount" + + SubjectClaim = "sub" + IssuerClaim = "iss" + ServiceAccountNameClaim = "kubernetes.io/serviceaccount/service-account.name" + ServiceAccountUIDClaim = "kubernetes.io/serviceaccount/service-account.uid" + SecretNameClaim = "kubernetes.io/serviceaccount/secret.name" + NamespaceClaim = "kubernetes.io/serviceaccount/namespace" +) + +// ServiceAccountTokenGetter defines functions to retrieve a named service account and secret +type ServiceAccountTokenGetter interface { + GetServiceAccount(namespace, name string) (*v1.ServiceAccount, error) + GetSecret(namespace, name string) (*v1.Secret, error) +} + +type TokenGenerator interface { + // GenerateToken generates a token which will identify the given ServiceAccount. + // The returned token will be stored in the given (and yet-unpersisted) Secret. + GenerateToken(serviceAccount v1.ServiceAccount, secret v1.Secret) (string, error) +} + +// ReadPrivateKey is a helper function for reading a private key from a PEM-encoded file +func ReadPrivateKey(file string) (interface{}, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + key, err := cert.ParsePrivateKeyPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading private key file %s: %v", file, err) + } + return key, nil +} + +// ReadPublicKeys is a helper function for reading an array of rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded file. +// Reads public keys from both public and private key files. +func ReadPublicKeys(file string) ([]interface{}, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + keys, err := ReadPublicKeysFromPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading public key file %s: %v", file, err) + } + return keys, nil +} + +// ReadPublicKeysFromPEM is a helper function for reading an array of rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded byte array. +// Reads public keys from both public and private key files. +func ReadPublicKeysFromPEM(data []byte) ([]interface{}, error) { + var block *pem.Block + keys := []interface{}{} + for { + // read the next block + block, data = pem.Decode(data) + if block == nil { + break + } + + // get PEM bytes for just this block + blockData := pem.EncodeToMemory(block) + if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(blockData); err == nil { + keys = append(keys, &privateKey.PublicKey) + continue + } + if publicKey, err := jwt.ParseRSAPublicKeyFromPEM(blockData); err == nil { + keys = append(keys, publicKey) + continue + } + + if privateKey, err := jwt.ParseECPrivateKeyFromPEM(blockData); err == nil { + keys = append(keys, &privateKey.PublicKey) + continue + } + if publicKey, err := jwt.ParseECPublicKeyFromPEM(blockData); err == nil { + keys = append(keys, publicKey) + continue + } + + // tolerate non-key PEM blocks for backwards compatibility + // originally, only the first PEM block was parsed and expected to be a key block + } + + if len(keys) == 0 { + return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA key") + } + return keys, nil +} + +// 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(privateKey interface{}) TokenGenerator { + return &jwtTokenGenerator{privateKey} +} + +type jwtTokenGenerator struct { + privateKey interface{} +} + +func (j *jwtTokenGenerator) GenerateToken(serviceAccount v1.ServiceAccount, secret v1.Secret) (string, error) { + var method jwt.SigningMethod + switch privateKey := j.privateKey.(type) { + case *rsa.PrivateKey: + method = jwt.SigningMethodRS256 + case *ecdsa.PrivateKey: + switch privateKey.Curve { + case elliptic.P256(): + method = jwt.SigningMethodES256 + case elliptic.P384(): + method = jwt.SigningMethodES384 + case elliptic.P521(): + method = jwt.SigningMethodES512 + default: + return "", fmt.Errorf("unknown private key curve, must be 256, 384, or 521") + } + default: + return "", fmt.Errorf("unknown private key type %T, must be *rsa.PrivateKey or *ecdsa.PrivateKey", j.privateKey) + } + + token := jwt.New(method) + + claims, _ := token.Claims.(jwt.MapClaims) + + // Identify the issuer + claims[IssuerClaim] = Issuer + + // Username + claims[SubjectClaim] = apiserverserviceaccount.MakeUsername(serviceAccount.Namespace, serviceAccount.Name) + + // Persist enough structured info for the authenticator to be able to look up the service account and secret + claims[NamespaceClaim] = serviceAccount.Namespace + claims[ServiceAccountNameClaim] = serviceAccount.Name + claims[ServiceAccountUIDClaim] = serviceAccount.UID + claims[SecretNameClaim] = secret.Name + + // Sign and get the complete encoded token as a string + return token.SignedString(j.privateKey) +} + +// 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(keys []interface{}, lookup bool, getter ServiceAccountTokenGetter) authenticator.Token { + return &jwtTokenAuthenticator{keys, lookup, getter} +} + +type jwtTokenAuthenticator struct { + keys []interface{} + lookup bool + getter ServiceAccountTokenGetter +} + +var errMismatchedSigningMethod = errors.New("invalid signing method") + +func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) { + var validationError error + + for i, key := range j.keys { + // Attempt to verify with each key until we find one that works + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + switch token.Method.(type) { + case *jwt.SigningMethodRSA: + if _, ok := key.(*rsa.PublicKey); ok { + return key, nil + } + return nil, errMismatchedSigningMethod + case *jwt.SigningMethodECDSA: + if _, ok := key.(*ecdsa.PublicKey); ok { + return key, nil + } + return nil, errMismatchedSigningMethod + default: + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + }) + + if err != nil { + switch err := err.(type) { + case *jwt.ValidationError: + if (err.Errors & jwt.ValidationErrorMalformed) != 0 { + // Not a JWT, no point in continuing + return nil, false, nil + } + + if (err.Errors & jwt.ValidationErrorSignatureInvalid) != 0 { + // Signature error, perhaps one of the other keys will verify the signature + // If not, we want to return this error + glog.V(4).Infof("Signature error (key %d): %v", i, err) + validationError = err + continue + } + + // This key doesn't apply to the given signature type + // Perhaps one of the other keys will verify the signature + // If not, we want to return this error + if err.Inner == errMismatchedSigningMethod { + glog.V(4).Infof("Mismatched key type (key %d): %v", i, err) + validationError = err + continue + } + } + + // Other errors should just return as errors + return nil, false, err + } + + // If we get here, we have a token with a recognized signature + + claims, _ := parsedToken.Claims.(jwt.MapClaims) + + // Make sure we issued the token + iss, _ := claims[IssuerClaim].(string) + if iss != Issuer { + return nil, false, nil + } + + // Make sure the claims we need exist + sub, _ := claims[SubjectClaim].(string) + if len(sub) == 0 { + return nil, false, errors.New("sub claim is missing") + } + namespace, _ := claims[NamespaceClaim].(string) + if len(namespace) == 0 { + return nil, false, errors.New("namespace claim is missing") + } + secretName, _ := claims[SecretNameClaim].(string) + if len(namespace) == 0 { + return nil, false, errors.New("secretName claim is missing") + } + serviceAccountName, _ := claims[ServiceAccountNameClaim].(string) + if len(serviceAccountName) == 0 { + return nil, false, errors.New("serviceAccountName claim is missing") + } + serviceAccountUID, _ := claims[ServiceAccountUIDClaim].(string) + if len(serviceAccountUID) == 0 { + return nil, false, errors.New("serviceAccountUID claim is missing") + } + + subjectNamespace, subjectName, err := apiserverserviceaccount.SplitUsername(sub) + if err != nil || subjectNamespace != namespace || subjectName != serviceAccountName { + return nil, false, errors.New("sub claim is invalid") + } + + if j.lookup { + // Make sure token hasn't been invalidated by deletion of the secret + secret, err := j.getter.GetSecret(namespace, secretName) + if err != nil { + glog.V(4).Infof("Could not retrieve token %s/%s for service account %s/%s: %v", namespace, secretName, namespace, serviceAccountName, err) + return nil, false, errors.New("Token has been invalidated") + } + if bytes.Compare(secret.Data[v1.ServiceAccountTokenKey], []byte(token)) != 0 { + glog.V(4).Infof("Token contents no longer matches %s/%s for service account %s/%s", namespace, secretName, namespace, serviceAccountName) + return nil, false, errors.New("Token does not match server's copy") + } + + // Make sure service account still exists (name and UID) + serviceAccount, err := j.getter.GetServiceAccount(namespace, serviceAccountName) + if err != nil { + glog.V(4).Infof("Could not retrieve service account %s/%s: %v", namespace, serviceAccountName, err) + return nil, false, err + } + if string(serviceAccount.UID) != serviceAccountUID { + glog.V(4).Infof("Service account UID no longer matches %s/%s: %q != %q", namespace, serviceAccountName, string(serviceAccount.UID), serviceAccountUID) + return nil, false, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID) + } + } + + return UserInfo(namespace, serviceAccountName, serviceAccountUID), true, nil + } + + return nil, false, validationError +} |