diff --git a/cmd/kube-apiserver/app/options/options.go b/cmd/kube-apiserver/app/options/options.go index 41dead7354..35b0dc705f 100644 --- a/cmd/kube-apiserver/app/options/options.go +++ b/cmd/kube-apiserver/app/options/options.go @@ -71,7 +71,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) { "Amount of time to retain events. Default is 1h.") fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, ""+ - "File containing PEM-encoded x509 RSA private or public key, used to verify "+ + "File containing PEM-encoded x509 RSA or ECDSA private or public key, used to verify "+ "ServiceAccount tokens. If unspecified, --tls-private-key-file is used.") fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, diff --git a/cmd/kube-controller-manager/app/options/options.go b/cmd/kube-controller-manager/app/options/options.go index 78b388a52a..2bf8542c1e 100644 --- a/cmd/kube-controller-manager/app/options/options.go +++ b/cmd/kube-controller-manager/app/options/options.go @@ -158,7 +158,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) { "Amount of time which we allow starting Node to be unresponsive before marking it unhealthy.") fs.DurationVar(&s.NodeMonitorPeriod.Duration, "node-monitor-period", s.NodeMonitorPeriod.Duration, "The period for syncing NodeStatus in NodeController.") - fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA key used to sign service account tokens.") + fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA or ECDSA key used to sign service account tokens.") fs.StringVar(&s.ClusterSigningCertFile, "cluster-signing-cert-file", s.ClusterSigningCertFile, "Filename containing a PEM-encoded X509 CA certificate used to issue cluster-scoped certificates") fs.StringVar(&s.ClusterSigningKeyFile, "cluster-signing-key-file", s.ClusterSigningKeyFile, "Filename containing a PEM-encoded RSA or ECDSA private key used to sign cluster-scoped certificates") fs.StringVar(&s.ApproveAllKubeletCSRsForGroup, "insecure-experimental-approve-all-kubelet-csrs-for-group", s.ApproveAllKubeletCSRsForGroup, "The group for which the controller-manager will auto approve all CSRs for kubelet client certificates.") diff --git a/pkg/apiserver/authenticator/authn.go b/pkg/apiserver/authenticator/authn.go index 1a73830887..d83db6c0d5 100644 --- a/pkg/apiserver/authenticator/authn.go +++ b/pkg/apiserver/authenticator/authn.go @@ -17,7 +17,6 @@ limitations under the License. package authenticator import ( - "crypto/rsa" "time" "k8s.io/kubernetes/pkg/auth/authenticator" @@ -183,7 +182,7 @@ func newServiceAccountAuthenticator(keyfile string, lookup bool, serviceAccountG return nil, err } - tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{publicKey}, lookup, serviceAccountGetter) + tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]interface{}{publicKey}, lookup, serviceAccountGetter) return bearertoken.New(tokenAuthenticator), nil } diff --git a/pkg/serviceaccount/jwt.go b/pkg/serviceaccount/jwt.go index 84792a88a5..363f53270f 100644 --- a/pkg/serviceaccount/jwt.go +++ b/pkg/serviceaccount/jwt.go @@ -18,6 +18,8 @@ package serviceaccount import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rsa" "errors" "fmt" @@ -54,43 +56,95 @@ type TokenGenerator interface { GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) } -// ReadPrivateKey is a helper function for reading an rsa.PrivateKey from a PEM-encoded file -func ReadPrivateKey(file string) (*rsa.PrivateKey, 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 } - return jwt.ParseRSAPrivateKeyFromPEM(data) + key, err := ReadPrivateKeyFromPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading private key file %s: %v", file, err) + } + return key, nil } -// ReadPublicKey is a helper function for reading an rsa.PublicKey from a PEM-encoded file -// Reads public keys from both public and private key files -func ReadPublicKey(file string) (*rsa.PublicKey, error) { +// ReadPrivateKeyFromPEM is a helper function for reading a private key from a PEM-encoded file +func ReadPrivateKeyFromPEM(data []byte) (interface{}, error) { + if key, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil { + return key, nil + } + if key, err := jwt.ParseECPrivateKeyFromPEM(data); err == nil { + return key, nil + } + return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA private key") +} + +// ReadPublicKey is a helper function for reading an rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded file. +// Reads public keys from both public and private key files. +func ReadPublicKey(file string) (interface{}, error) { data, err := ioutil.ReadFile(file) if err != nil { return nil, err } + key, err := ReadPublicKeyFromPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading public key file %s: %v", file, err) + } + return key, nil +} +// ReadPublicKeyFromPEM is a helper function for reading an rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded byte array. +// Reads public keys from both public and private key files. +func ReadPublicKeyFromPEM(data []byte) (interface{}, error) { if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil { return &privateKey.PublicKey, nil } + if publicKey, err := jwt.ParseRSAPublicKeyFromPEM(data); err == nil { + return publicKey, nil + } - return jwt.ParseRSAPublicKeyFromPEM(data) + if privateKey, err := jwt.ParseECPrivateKeyFromPEM(data); err == nil { + return &privateKey.PublicKey, nil + } + if publicKey, err := jwt.ParseECPublicKeyFromPEM(data); err == nil { + return publicKey, nil + } + return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA key") } // 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(key *rsa.PrivateKey) TokenGenerator { - return &jwtTokenGenerator{key} +func JWTTokenGenerator(privateKey interface{}) TokenGenerator { + return &jwtTokenGenerator{privateKey} } type jwtTokenGenerator struct { - key *rsa.PrivateKey + privateKey interface{} } func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) { - token := jwt.New(jwt.SigningMethodRS256) + 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) @@ -107,32 +161,44 @@ func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, sec claims[SecretNameClaim] = secret.Name // Sign and get the complete encoded token as a string - return token.SignedString(j.key) + 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 []*rsa.PublicKey, lookup bool, getter ServiceAccountTokenGetter) authenticator.Token { +func JWTTokenAuthenticator(keys []interface{}, lookup bool, getter ServiceAccountTokenGetter) authenticator.Token { return &jwtTokenAuthenticator{keys, lookup, getter} } type jwtTokenAuthenticator struct { - keys []*rsa.PublicKey + 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) { - if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + 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"]) } - return key, nil }) if err != nil { @@ -150,6 +216,15 @@ func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool 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 diff --git a/pkg/serviceaccount/jwt_test.go b/pkg/serviceaccount/jwt_test.go index 1ade162009..f1287c7b75 100644 --- a/pkg/serviceaccount/jwt_test.go +++ b/pkg/serviceaccount/jwt_test.go @@ -17,14 +17,11 @@ limitations under the License. package serviceaccount_test import ( - "crypto/rsa" "io/ioutil" "os" "reflect" "testing" - "github.com/dgrijalva/jwt-go" - "k8s.io/kubernetes/pkg/api" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" @@ -42,7 +39,7 @@ gPZm7ZsipmfbZK2Tkhnpsa4QxDg7zHJPMsB5kxRXW0cQipXcC3baDyN9KBApNXa0 PwIDAQAB -----END PUBLIC KEY-----` -const publicKey = `-----BEGIN PUBLIC KEY----- +const rsaPublicKey = `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG @@ -53,7 +50,7 @@ WwIDAQAB -----END PUBLIC KEY----- ` -const privateKey = `-----BEGIN RSA PRIVATE KEY----- +const rsaPrivateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA249XwEo9k4tM8fMxV7zxOhcrP+WvXn917koM5Qr2ZXs4vo26 e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecIzshKuv1gKIxbbLQMOuK1eA/4HALy EkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0T @@ -82,16 +79,28 @@ X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ -----END RSA PRIVATE KEY----- ` -func getPrivateKey(data string) *rsa.PrivateKey { - key, _ := jwt.ParseRSAPrivateKeyFromPEM([]byte(data)) +// openssl ecparam -name prime256v1 -genkey -noout -out ecdsa256.pem +const ecdsaPrivateKey = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEZmTmUhuanLjPA2CLquXivuwBDHTt5XYwgIr/kA1LtRoAoGCCqGSM49 +AwEHoUQDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp/C/ASqiIGUeeKQtX0 +/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== +-----END EC PRIVATE KEY-----` + +// openssl ec -in ecdsa256.pem -pubout -out ecdsa256pub.pem +const ecdsaPublicKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEH6cuzP8XuD5wal6wf9M6xDljTOPL +X2i8uIp/C/ASqiIGUeeKQtX0/IR3qCXyThP/dbCiHrF3v1cuhBOHY8CLVg== +-----END PUBLIC KEY-----` + +func getPrivateKey(data string) interface{} { + key, _ := serviceaccount.ReadPrivateKeyFromPEM([]byte(data)) return key } -func getPublicKey(data string) *rsa.PublicKey { - key, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(data)) +func getPublicKey(data string) interface{} { + key, _ := serviceaccount.ReadPublicKeyFromPEM([]byte(data)) return key } - func TestReadPrivateKey(t *testing.T) { f, err := ioutil.TempFile("", "") if err != nil { @@ -99,12 +108,18 @@ func TestReadPrivateKey(t *testing.T) { } defer os.Remove(f.Name()) - if err := ioutil.WriteFile(f.Name(), []byte(privateKey), os.FileMode(0600)); err != nil { + if err := ioutil.WriteFile(f.Name(), []byte(rsaPrivateKey), os.FileMode(0600)); err != nil { t.Fatalf("error writing private key to tmpfile: %v", err) } - if _, err := serviceaccount.ReadPrivateKey(f.Name()); err != nil { - t.Fatalf("error reading private key: %v", err) + t.Fatalf("error reading private RSA key: %v", err) + } + + if err := ioutil.WriteFile(f.Name(), []byte(ecdsaPrivateKey), os.FileMode(0600)); err != nil { + t.Fatalf("error writing private key to tmpfile: %v", err) + } + if _, err := serviceaccount.ReadPrivateKey(f.Name()); err != nil { + t.Fatalf("error reading private ECDSA key: %v", err) } } @@ -115,12 +130,18 @@ func TestReadPublicKey(t *testing.T) { } defer os.Remove(f.Name()) - if err := ioutil.WriteFile(f.Name(), []byte(publicKey), os.FileMode(0600)); err != nil { + if err := ioutil.WriteFile(f.Name(), []byte(rsaPublicKey), os.FileMode(0600)); err != nil { t.Fatalf("error writing public key to tmpfile: %v", err) } - if _, err := serviceaccount.ReadPublicKey(f.Name()); err != nil { - t.Fatalf("error reading public key: %v", err) + t.Fatalf("error reading RSA public key: %v", err) + } + + if err := ioutil.WriteFile(f.Name(), []byte(ecdsaPublicKey), os.FileMode(0600)); err != nil { + t.Fatalf("error writing public key to tmpfile: %v", err) + } + if _, err := serviceaccount.ReadPublicKey(f.Name()); err != nil { + t.Fatalf("error reading ECDSA public key: %v", err) } } @@ -136,31 +157,49 @@ func TestTokenGenerateAndValidate(t *testing.T) { Namespace: "test", }, } - secret := &api.Secret{ + rsaSecret := &api.Secret{ ObjectMeta: api.ObjectMeta{ - Name: "my-secret", + Name: "my-rsa-secret", + Namespace: "test", + }, + } + ecdsaSecret := &api.Secret{ + ObjectMeta: api.ObjectMeta{ + Name: "my-ecdsa-secret", Namespace: "test", }, } - // Generate the token - generator := serviceaccount.JWTTokenGenerator(getPrivateKey(privateKey)) - token, err := generator.GenerateToken(*serviceAccount, *secret) + // Generate the RSA token + rsaGenerator := serviceaccount.JWTTokenGenerator(getPrivateKey(rsaPrivateKey)) + rsaToken, err := rsaGenerator.GenerateToken(*serviceAccount, *rsaSecret) if err != nil { t.Fatalf("error generating token: %v", err) } - if len(token) == 0 { + if len(rsaToken) == 0 { t.Fatalf("no token generated") } + rsaSecret.Data = map[string][]byte{ + "token": []byte(rsaToken), + } - // "Save" the token - secret.Data = map[string][]byte{ - "token": []byte(token), + // Generate the ECDSA token + ecdsaGenerator := serviceaccount.JWTTokenGenerator(getPrivateKey(ecdsaPrivateKey)) + ecdsaToken, err := ecdsaGenerator.GenerateToken(*serviceAccount, *ecdsaSecret) + if err != nil { + t.Fatalf("error generating token: %v", err) + } + if len(ecdsaToken) == 0 { + t.Fatalf("no token generated") + } + ecdsaSecret.Data = map[string][]byte{ + "token": []byte(ecdsaToken), } testCases := map[string]struct { Client clientset.Interface - Keys []*rsa.PublicKey + Keys []interface{} + Token string ExpectedErr bool ExpectedOK bool @@ -169,29 +208,60 @@ func TestTokenGenerateAndValidate(t *testing.T) { ExpectedGroups []string }{ "no keys": { + Token: rsaToken, Client: nil, - Keys: []*rsa.PublicKey{}, + Keys: []interface{}{}, ExpectedErr: false, ExpectedOK: false, }, - "invalid keys": { + "invalid keys (rsa)": { + Token: rsaToken, Client: nil, - Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey)}, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(ecdsaPublicKey)}, ExpectedErr: true, ExpectedOK: false, }, - "valid key": { + "invalid keys (ecdsa)": { + Token: ecdsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(rsaPublicKey)}, + ExpectedErr: true, + ExpectedOK: false, + }, + "valid key (rsa)": { + Token: rsaToken, Client: nil, - Keys: []*rsa.PublicKey{getPublicKey(publicKey)}, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, ExpectedErr: false, ExpectedOK: true, ExpectedUserName: expectedUserName, ExpectedUserUID: expectedUserUID, ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, }, - "rotated keys": { + "valid key (ecdsa)": { + Token: ecdsaToken, Client: nil, - Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey), getPublicKey(publicKey)}, + Keys: []interface{}{getPublicKey(ecdsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "rotated keys (rsa)": { + Token: rsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(ecdsaPublicKey), getPublicKey(rsaPublicKey)}, + ExpectedErr: false, + ExpectedOK: true, + ExpectedUserName: expectedUserName, + ExpectedUserUID: expectedUserUID, + ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, + }, + "rotated keys (ecdsa)": { + Token: ecdsaToken, + Client: nil, + Keys: []interface{}{getPublicKey(otherPublicKey), getPublicKey(rsaPublicKey), getPublicKey(ecdsaPublicKey)}, ExpectedErr: false, ExpectedOK: true, ExpectedUserName: expectedUserName, @@ -199,8 +269,9 @@ func TestTokenGenerateAndValidate(t *testing.T) { ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, }, "valid lookup": { - Client: fake.NewSimpleClientset(serviceAccount, secret), - Keys: []*rsa.PublicKey{getPublicKey(publicKey)}, + Token: rsaToken, + Client: fake.NewSimpleClientset(serviceAccount, rsaSecret, ecdsaSecret), + Keys: []interface{}{getPublicKey(rsaPublicKey)}, ExpectedErr: false, ExpectedOK: true, ExpectedUserName: expectedUserName, @@ -208,14 +279,16 @@ func TestTokenGenerateAndValidate(t *testing.T) { ExpectedGroups: []string{"system:serviceaccounts", "system:serviceaccounts:test"}, }, "invalid secret lookup": { + Token: rsaToken, Client: fake.NewSimpleClientset(serviceAccount), - Keys: []*rsa.PublicKey{getPublicKey(publicKey)}, + Keys: []interface{}{getPublicKey(rsaPublicKey)}, ExpectedErr: true, ExpectedOK: false, }, "invalid serviceaccount lookup": { - Client: fake.NewSimpleClientset(secret), - Keys: []*rsa.PublicKey{getPublicKey(publicKey)}, + Token: rsaToken, + Client: fake.NewSimpleClientset(rsaSecret, ecdsaSecret), + Keys: []interface{}{getPublicKey(rsaPublicKey)}, ExpectedErr: true, ExpectedOK: false, }, @@ -231,7 +304,7 @@ func TestTokenGenerateAndValidate(t *testing.T) { continue } - user, ok, err := authenticator.AuthenticateToken(token) + user, ok, err := authenticator.AuthenticateToken(tc.Token) if (err != nil) != tc.ExpectedErr { t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err) continue diff --git a/test/integration/serviceaccount/service_account_test.go b/test/integration/serviceaccount/service_account_test.go index 9121b97699..b4efc6e641 100644 --- a/test/integration/serviceaccount/service_account_test.go +++ b/test/integration/serviceaccount/service_account_test.go @@ -361,7 +361,7 @@ func startServiceAccountTestServer(t *testing.T) (*clientset.Clientset, restclie }) serviceAccountKey, _ := rsa.GenerateKey(rand.Reader, 2048) serviceAccountTokenGetter := serviceaccountcontroller.NewGetterFromClient(rootClientset) - serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{&serviceAccountKey.PublicKey}, true, serviceAccountTokenGetter) + serviceAccountTokenAuth := serviceaccount.JWTTokenAuthenticator([]interface{}{&serviceAccountKey.PublicKey}, true, serviceAccountTokenGetter) authenticator := union.New( bearertoken.New(rootTokenAuth), bearertoken.New(serviceAccountTokenAuth),