Add ECDSA support

pull/6/head
Jordan Liggitt 2016-09-27 01:44:33 -04:00
parent 987aef1f64
commit 6333d8fd86
No known key found for this signature in database
GPG Key ID: 24E7ADF9A3B42012
6 changed files with 207 additions and 60 deletions

View File

@ -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,

View File

@ -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.")

View File

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

View File

@ -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

View File

@ -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

View File

@ -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),