Browse Source

auth: Support all JWT algorithms

This change adds support to etcd for all of the JWT algorithms included
in the underlying JWT library.
Joe LeGasse 7 years ago
parent
commit
a6ddb51c8a

+ 1 - 0
Documentation/op-guide/configuration.md

@@ -372,6 +372,7 @@ Follow the instructions when using these flags.
 
 ### --auth-token
 + Specify a token type and token specific options, especially for JWT. Its format is "type,var1=val1,var2=val2,...". Possible type is 'simple' or 'jwt'. Possible variables are 'sign-method' for specifying a sign method of jwt (its possible values are 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', or 'PS512'), 'pub-key' for specifying a path to a public key for verifying jwt, 'priv-key' for specifying a path to a private key for signing jwt, and 'ttl' for specifying TTL of jwt tokens.
++ For asymmetric algorithms ('RS', 'PS', 'ES'), the public key is optional, as the private key contains enough information to both sign and verify tokens.
 + Example option of JWT: '--auth-token jwt,pub-key=app.rsa.pub,priv-key=app.rsa,sign-method=RS512,ttl=10m'
 + default: "simple"
 

+ 69 - 115
auth/jwt.go

@@ -16,8 +16,9 @@ package auth
 
 import (
 	"context"
+	"crypto/ecdsa"
 	"crypto/rsa"
-	"io/ioutil"
+	"errors"
 	"time"
 
 	jwt "github.com/dgrijalva/jwt-go"
@@ -26,10 +27,10 @@ import (
 
 type tokenJWT struct {
 	lg         *zap.Logger
-	signMethod string
-	signKey    *rsa.PrivateKey
-	verifyKey  *rsa.PublicKey
+	signMethod jwt.SigningMethod
+	key        interface{}
 	ttl        time.Duration
+	verifyOnly bool
 }
 
 func (t *tokenJWT) enable()                         {}
@@ -45,25 +46,20 @@ func (t *tokenJWT) info(ctx context.Context, token string, rev uint64) (*AuthInf
 	)
 
 	parsed, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
-		return t.verifyKey, nil
-	})
-
-	switch err.(type) {
-	case nil:
-		if !parsed.Valid {
-			if t.lg != nil {
-				t.lg.Warn("invalid JWT token", zap.String("token", token))
-			} else {
-				plog.Warningf("invalid jwt token: %s", token)
-			}
-			return nil, false
+		if token.Method.Alg() != t.signMethod.Alg() {
+			return nil, errors.New("invalid signing method")
 		}
+		switch k := t.key.(type) {
+		case *rsa.PrivateKey:
+			return &k.PublicKey, nil
+		case *ecdsa.PrivateKey:
+			return &k.PublicKey, nil
+		default:
+			return t.key, nil
+		}
+	})
 
-		claims := parsed.Claims.(jwt.MapClaims)
-
-		username = claims["username"].(string)
-		revision = uint64(claims["revision"].(float64))
-	default:
+	if err != nil {
 		if t.lg != nil {
 			t.lg.Warn(
 				"failed to parse a JWT token",
@@ -76,20 +72,37 @@ func (t *tokenJWT) info(ctx context.Context, token string, rev uint64) (*AuthInf
 		return nil, false
 	}
 
+	claims, ok := parsed.Claims.(jwt.MapClaims)
+	if !parsed.Valid || !ok {
+		if t.lg != nil {
+			t.lg.Warn("invalid JWT token", zap.String("token", token))
+		} else {
+			plog.Warningf("invalid jwt token: %s", token)
+		}
+		return nil, false
+	}
+
+	username = claims["username"].(string)
+	revision = uint64(claims["revision"].(float64))
+
 	return &AuthInfo{Username: username, Revision: revision}, true
 }
 
 func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64) (string, error) {
+	if t.verifyOnly {
+		return "", ErrVerifyOnly
+	}
+
 	// Future work: let a jwt token include permission information would be useful for
 	// permission checking in proxy side.
-	tk := jwt.NewWithClaims(jwt.GetSigningMethod(t.signMethod),
+	tk := jwt.NewWithClaims(t.signMethod,
 		jwt.MapClaims{
 			"username": username,
 			"revision": revision,
 			"exp":      time.Now().Add(t.ttl).Unix(),
 		})
 
-	token, err := tk.SignedString(t.signKey)
+	token, err := tk.SignedString(t.key)
 	if err != nil {
 		if t.lg != nil {
 			t.lg.Warn(
@@ -117,113 +130,54 @@ func (t *tokenJWT) assign(ctx context.Context, username string, revision uint64)
 	return token, err
 }
 
-func prepareOpts(lg *zap.Logger, opts map[string]string) (jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath string, ttl time.Duration, err error) {
-	for k, v := range opts {
-		switch k {
-		case "sign-method":
-			jwtSignMethod = v
-		case "pub-key":
-			jwtPubKeyPath = v
-		case "priv-key":
-			jwtPrivKeyPath = v
-		case "ttl":
-			ttl, err = time.ParseDuration(v)
-			if err != nil {
-				if lg != nil {
-					lg.Warn(
-						"failed to parse JWT TTL option",
-						zap.String("ttl-value", v),
-						zap.Error(err),
-					)
-				} else {
-					plog.Errorf("failed to parse ttl option (%s)", err)
-				}
-				return "", "", "", 0, ErrInvalidAuthOpts
-			}
-		default:
-			if lg != nil {
-				lg.Warn("unknown JWT token option", zap.String("option", k))
-			} else {
-				plog.Errorf("unknown token specific option: %s", k)
-			}
-			return "", "", "", 0, ErrInvalidAuthOpts
-		}
-	}
-	if len(jwtSignMethod) == 0 {
-		return "", "", "", 0, ErrInvalidAuthOpts
-	}
-	return jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, ttl, nil
-}
-
-func newTokenProviderJWT(lg *zap.Logger, opts map[string]string) (*tokenJWT, error) {
-	jwtSignMethod, jwtPubKeyPath, jwtPrivKeyPath, ttl, err := prepareOpts(lg, opts)
+func newTokenProviderJWT(lg *zap.Logger, optMap map[string]string) (*tokenJWT, error) {
+	var err error
+	var opts jwtOptions
+	err = opts.ParseWithDefaults(optMap)
 	if err != nil {
+		if lg != nil {
+			lg.Warn("problem loading JWT options", zap.Error(err))
+		} else {
+			plog.Errorf("problem loading JWT options: %s", err)
+		}
 		return nil, ErrInvalidAuthOpts
 	}
 
-	if ttl == 0 {
-		ttl = 5 * time.Minute
-	}
-
-	t := &tokenJWT{
-		lg:  lg,
-		ttl: ttl,
-	}
-
-	t.signMethod = jwtSignMethod
-
-	verifyBytes, err := ioutil.ReadFile(jwtPubKeyPath)
-	if err != nil {
-		if lg != nil {
-			lg.Warn(
-				"failed to read JWT public key",
-				zap.String("public-key-path", jwtPubKeyPath),
-				zap.Error(err),
-			)
-		} else {
-			plog.Errorf("failed to read public key (%s) for jwt: %s", jwtPubKeyPath, err)
+	var keys = make([]string, 0, len(optMap))
+	for k := range optMap {
+		if !knownOptions[k] {
+			keys = append(keys, k)
 		}
-		return nil, err
 	}
-	t.verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
-	if err != nil {
+	if len(keys) > 0 {
 		if lg != nil {
-			lg.Warn(
-				"failed to parse JWT public key",
-				zap.String("public-key-path", jwtPubKeyPath),
-				zap.Error(err),
-			)
+			lg.Warn("unknown JWT options", zap.Strings("keys", keys))
 		} else {
-			plog.Errorf("failed to parse public key (%s): %s", jwtPubKeyPath, err)
+			plog.Warningf("unknown JWT options: %v", keys)
 		}
-		return nil, err
 	}
 
-	signBytes, err := ioutil.ReadFile(jwtPrivKeyPath)
+	key, err := opts.Key()
 	if err != nil {
-		if lg != nil {
-			lg.Warn(
-				"failed to read JWT private key",
-				zap.String("private-key-path", jwtPrivKeyPath),
-				zap.Error(err),
-			)
-		} else {
-			plog.Errorf("failed to read private key (%s) for jwt: %s", jwtPrivKeyPath, err)
-		}
 		return nil, err
 	}
-	t.signKey, err = jwt.ParseRSAPrivateKeyFromPEM(signBytes)
-	if err != nil {
-		if lg != nil {
-			lg.Warn(
-				"failed to parse JWT private key",
-				zap.String("private-key-path", jwtPrivKeyPath),
-				zap.Error(err),
-			)
-		} else {
-			plog.Errorf("failed to parse private key (%s): %s", jwtPrivKeyPath, err)
+
+	t := &tokenJWT{
+		lg:         lg,
+		ttl:        opts.TTL,
+		signMethod: opts.SignMethod,
+		key:        key,
+	}
+
+	switch t.signMethod.(type) {
+	case *jwt.SigningMethodECDSA:
+		if _, ok := t.key.(*ecdsa.PublicKey); ok {
+			t.verifyOnly = true
+		}
+	case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
+		if _, ok := t.key.(*rsa.PublicKey); ok {
+			t.verifyOnly = true
 		}
-		return nil, err
 	}
 
 	return t, nil

+ 148 - 46
auth/jwt_test.go

@@ -23,80 +23,182 @@ import (
 )
 
 const (
-	jwtPubKey  = "../integration/fixtures/server.crt"
-	jwtPrivKey = "../integration/fixtures/server.key.insecure"
+	jwtRSAPubKey  = "../integration/fixtures/server.crt"
+	jwtRSAPrivKey = "../integration/fixtures/server.key.insecure"
+
+	jwtECPubKey  = "../integration/fixtures/server-ecdsa.crt"
+	jwtECPrivKey = "../integration/fixtures/server-ecdsa.key.insecure"
 )
 
 func TestJWTInfo(t *testing.T) {
-	opts := map[string]string{
-		"pub-key":     jwtPubKey,
-		"priv-key":    jwtPrivKey,
-		"sign-method": "RS256",
+	optsMap := map[string]map[string]string{
+		"RSA-priv": {
+			"priv-key":    jwtRSAPrivKey,
+			"sign-method": "RS256",
+			"ttl":         "1h",
+		},
+		"RSA": {
+			"pub-key":     jwtRSAPubKey,
+			"priv-key":    jwtRSAPrivKey,
+			"sign-method": "RS256",
+		},
+		"RSAPSS-priv": {
+			"priv-key":    jwtRSAPrivKey,
+			"sign-method": "PS256",
+		},
+		"RSAPSS": {
+			"pub-key":     jwtRSAPubKey,
+			"priv-key":    jwtRSAPrivKey,
+			"sign-method": "PS256",
+		},
+		"ECDSA-priv": {
+			"priv-key":    jwtECPrivKey,
+			"sign-method": "ES256",
+		},
+		"ECDSA": {
+			"pub-key":     jwtECPubKey,
+			"priv-key":    jwtECPrivKey,
+			"sign-method": "ES256",
+		},
+		"HMAC": {
+			"priv-key":    jwtECPrivKey, // any file, raw bytes used as shared secret
+			"sign-method": "HS256",
+		},
+	}
+
+	for k, opts := range optsMap {
+		t.Run(k, func(tt *testing.T) {
+			testJWTInfo(tt, opts)
+		})
 	}
-	jwt, err := newTokenProviderJWT(zap.NewExample(), opts)
+}
+
+func testJWTInfo(t *testing.T, opts map[string]string) {
+	lg := zap.NewNop()
+	jwt, err := newTokenProviderJWT(lg, opts)
 	if err != nil {
 		t.Fatal(err)
 	}
-	token, aerr := jwt.assign(context.TODO(), "abc", 123)
+
+	ctx := context.TODO()
+
+	token, aerr := jwt.assign(ctx, "abc", 123)
 	if aerr != nil {
-		t.Fatal(err)
+		t.Fatalf("%#v", aerr)
 	}
-	ai, ok := jwt.info(context.TODO(), token, 123)
+	ai, ok := jwt.info(ctx, token, 123)
 	if !ok {
 		t.Fatalf("failed to authenticate with token %s", token)
 	}
 	if ai.Revision != 123 {
 		t.Fatalf("expected revision 123, got %d", ai.Revision)
 	}
-	ai, ok = jwt.info(context.TODO(), "aaa", 120)
+	ai, ok = jwt.info(ctx, "aaa", 120)
 	if ok || ai != nil {
 		t.Fatalf("expected aaa to fail to authenticate, got %+v", ai)
 	}
-}
 
-func TestJWTBad(t *testing.T) {
-	opts := map[string]string{
-		"pub-key":     jwtPubKey,
-		"priv-key":    jwtPrivKey,
-		"sign-method": "RS256",
-	}
-	// private key instead of public key
-	opts["pub-key"] = jwtPrivKey
-	if _, err := newTokenProviderJWT(zap.NewExample(), opts); err == nil {
-		t.Fatalf("expected failure on missing public key")
-	}
-	opts["pub-key"] = jwtPubKey
+	// test verify-only provider
+	if opts["pub-key"] != "" && opts["priv-key"] != "" {
+		t.Run("verify-only", func(t *testing.T) {
+			newOpts := make(map[string]string, len(opts))
+			for k, v := range opts {
+				newOpts[k] = v
+			}
+			delete(newOpts, "priv-key")
+			verify, err := newTokenProviderJWT(lg, newOpts)
+			if err != nil {
+				t.Fatal(err)
+			}
 
-	// public key instead of private key
-	opts["priv-key"] = jwtPubKey
-	if _, err := newTokenProviderJWT(zap.NewExample(), opts); err == nil {
-		t.Fatalf("expected failure on missing public key")
-	}
-	opts["priv-key"] = jwtPrivKey
+			ai, ok := verify.info(ctx, token, 123)
+			if !ok {
+				t.Fatalf("failed to authenticate with token %s", token)
+			}
+			if ai.Revision != 123 {
+				t.Fatalf("expected revision 123, got %d", ai.Revision)
+			}
+			ai, ok = verify.info(ctx, "aaa", 120)
+			if ok || ai != nil {
+				t.Fatalf("expected aaa to fail to authenticate, got %+v", ai)
+			}
+
+			_, aerr := verify.assign(ctx, "abc", 123)
+			if aerr != ErrVerifyOnly {
+				t.Fatalf("unexpected error when attempting to sign with public key: %v", aerr)
+			}
 
-	// missing signing option
-	delete(opts, "sign-method")
-	if _, err := newTokenProviderJWT(zap.NewExample(), opts); err == nil {
-		t.Fatal("expected error on missing option")
+		})
 	}
-	opts["sign-method"] = "RS256"
+}
+
+func TestJWTBad(t *testing.T) {
 
-	// bad file for pubkey
-	opts["pub-key"] = "whatever"
-	if _, err := newTokenProviderJWT(zap.NewExample(), opts); err == nil {
-		t.Fatalf("expected failure on missing public key")
+	var badCases = map[string]map[string]string{
+		"no options": {},
+		"invalid method": {
+			"sign-method": "invalid",
+		},
+		"rsa no key": {
+			"sign-method": "RS256",
+		},
+		"invalid ttl": {
+			"sign-method": "RS256",
+			"ttl":         "forever",
+		},
+		"rsa invalid public key": {
+			"sign-method": "RS256",
+			"pub-key":     jwtRSAPrivKey,
+			"priv-key":    jwtRSAPrivKey,
+		},
+		"rsa invalid private key": {
+			"sign-method": "RS256",
+			"pub-key":     jwtRSAPubKey,
+			"priv-key":    jwtRSAPubKey,
+		},
+		"hmac no key": {
+			"sign-method": "HS256",
+		},
+		"hmac pub key": {
+			"sign-method": "HS256",
+			"pub-key":     jwtRSAPubKey,
+		},
+		"missing public key file": {
+			"sign-method": "HS256",
+			"pub-key":     "missing-file",
+		},
+		"missing private key file": {
+			"sign-method": "HS256",
+			"priv-key":    "missing-file",
+		},
+		"ecdsa no key": {
+			"sign-method": "ES256",
+		},
+		"ecdsa invalid public key": {
+			"sign-method": "ES256",
+			"pub-key":     jwtECPrivKey,
+			"priv-key":    jwtECPrivKey,
+		},
+		"ecdsa invalid private key": {
+			"sign-method": "ES256",
+			"pub-key":     jwtECPubKey,
+			"priv-key":    jwtECPubKey,
+		},
 	}
-	opts["pub-key"] = jwtPubKey
 
-	// bad file for private key
-	opts["priv-key"] = "whatever"
-	if _, err := newTokenProviderJWT(zap.NewExample(), opts); err == nil {
-		t.Fatalf("expeceted failure on missing private key")
+	lg := zap.NewNop()
+
+	for k, v := range badCases {
+		t.Run(k, func(t *testing.T) {
+			_, err := newTokenProviderJWT(lg, v)
+			if err == nil {
+				t.Errorf("expected error for options %v", v)
+			}
+		})
 	}
-	opts["priv-key"] = jwtPrivKey
 }
 
 // testJWTOpts is useful for passing to NewTokenProvider which requires a string.
 func testJWTOpts() string {
-	return fmt.Sprintf("%s,pub-key=%s,priv-key=%s,sign-method=RS256", tokenTypeJWT, jwtPubKey, jwtPrivKey)
+	return fmt.Sprintf("%s,pub-key=%s,priv-key=%s,sign-method=RS256", tokenTypeJWT, jwtRSAPubKey, jwtRSAPrivKey)
 }

+ 192 - 0
auth/options.go

@@ -0,0 +1,192 @@
+// Copyright 2018 The etcd 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 auth
+
+import (
+	"crypto/ecdsa"
+	"crypto/rsa"
+	"fmt"
+	"io/ioutil"
+	"time"
+
+	jwt "github.com/dgrijalva/jwt-go"
+)
+
+const (
+	optSignMethod = "sign-method"
+	optPublicKey  = "pub-key"
+	optPrivateKey = "priv-key"
+	optTTL        = "ttl"
+)
+
+var knownOptions = map[string]bool{
+	optSignMethod: true,
+	optPublicKey:  true,
+	optPrivateKey: true,
+	optTTL:        true,
+}
+
+var (
+	// DefaultTTL will be used when a 'ttl' is not specified
+	DefaultTTL = 5 * time.Minute
+)
+
+type jwtOptions struct {
+	SignMethod jwt.SigningMethod
+	PublicKey  []byte
+	PrivateKey []byte
+	TTL        time.Duration
+}
+
+// ParseWithDefaults will load options from the specified map or set defaults where appropriate
+func (opts *jwtOptions) ParseWithDefaults(optMap map[string]string) error {
+	if opts.TTL == 0 && optMap[optTTL] == "" {
+		opts.TTL = DefaultTTL
+	}
+
+	return opts.Parse(optMap)
+}
+
+// Parse will load options from the specified map
+func (opts *jwtOptions) Parse(optMap map[string]string) error {
+	var err error
+	if ttl := optMap[optTTL]; ttl != "" {
+		opts.TTL, err = time.ParseDuration(ttl)
+		if err != nil {
+			return err
+		}
+	}
+
+	if file := optMap[optPublicKey]; file != "" {
+		opts.PublicKey, err = ioutil.ReadFile(file)
+		if err != nil {
+			return err
+		}
+	}
+
+	if file := optMap[optPrivateKey]; file != "" {
+		opts.PrivateKey, err = ioutil.ReadFile(file)
+		if err != nil {
+			return err
+		}
+	}
+
+	// signing method is a required field
+	method := optMap[optSignMethod]
+	opts.SignMethod = jwt.GetSigningMethod(method)
+	if opts.SignMethod == nil {
+		return ErrInvalidAuthMethod
+	}
+
+	return nil
+}
+
+// Key will parse and return the appropriately typed key for the selected signature method
+func (opts *jwtOptions) Key() (interface{}, error) {
+	switch opts.SignMethod.(type) {
+	case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
+		return opts.rsaKey()
+	case *jwt.SigningMethodECDSA:
+		return opts.ecKey()
+	case *jwt.SigningMethodHMAC:
+		return opts.hmacKey()
+	default:
+		return nil, fmt.Errorf("unsupported signing method: %T", opts.SignMethod)
+	}
+}
+
+func (opts *jwtOptions) hmacKey() (interface{}, error) {
+	if len(opts.PrivateKey) == 0 {
+		return nil, ErrMissingKey
+	}
+	return opts.PrivateKey, nil
+}
+
+func (opts *jwtOptions) rsaKey() (interface{}, error) {
+	var (
+		priv *rsa.PrivateKey
+		pub  *rsa.PublicKey
+		err  error
+	)
+
+	if len(opts.PrivateKey) > 0 {
+		priv, err = jwt.ParseRSAPrivateKeyFromPEM(opts.PrivateKey)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if len(opts.PublicKey) > 0 {
+		pub, err = jwt.ParseRSAPublicKeyFromPEM(opts.PublicKey)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if priv == nil {
+		if pub == nil {
+			// Neither key given
+			return nil, ErrMissingKey
+		}
+		// Public key only, can verify tokens
+		return pub, nil
+	}
+
+	// both keys provided, make sure they match
+	if pub != nil && pub.E != priv.E && pub.N.Cmp(priv.N) != 0 {
+		return nil, ErrKeyMismatch
+	}
+
+	return priv, nil
+}
+
+func (opts *jwtOptions) ecKey() (interface{}, error) {
+	var (
+		priv *ecdsa.PrivateKey
+		pub  *ecdsa.PublicKey
+		err  error
+	)
+
+	if len(opts.PrivateKey) > 0 {
+		priv, err = jwt.ParseECPrivateKeyFromPEM(opts.PrivateKey)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if len(opts.PublicKey) > 0 {
+		pub, err = jwt.ParseECPublicKeyFromPEM(opts.PublicKey)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if priv == nil {
+		if pub == nil {
+			// Neither key given
+			return nil, ErrMissingKey
+		}
+		// Public key only, can verify tokens
+		return pub, nil
+	}
+
+	// both keys provided, make sure they match
+	if pub != nil && pub.Curve != priv.Curve &&
+		pub.X.Cmp(priv.X) != 0 && pub.Y.Cmp(priv.Y) != 0 {
+		return nil, ErrKeyMismatch
+	}
+
+	return priv, nil
+}

+ 4 - 0
auth/store.go

@@ -66,6 +66,10 @@ var (
 	ErrInvalidAuthToken     = errors.New("auth: invalid auth token")
 	ErrInvalidAuthOpts      = errors.New("auth: invalid auth options")
 	ErrInvalidAuthMgmt      = errors.New("auth: invalid auth management")
+	ErrInvalidAuthMethod    = errors.New("auth: invalid auth signature method")
+	ErrMissingKey           = errors.New("auth: missing key data")
+	ErrKeyMismatch          = errors.New("auth: public and private keys don't match")
+	ErrVerifyOnly           = errors.New("auth: token signing attempted with verify-only key")
 )
 
 const (

+ 9 - 0
integration/fixtures/gencerts.sh

@@ -25,6 +25,15 @@ cfssl gencert \
 mv server.pem server.crt
 mv server-key.pem server.key.insecure
 
+# generate DNS: localhost, IP: 127.0.0.1, CN: example.com certificates (ECDSA)
+cfssl gencert \
+  --ca ./ca.crt \
+  --ca-key ./ca-key.pem \
+  --config ./gencert.json \
+  ./server-ca-csr-ecdsa.json | cfssljson --bare ./server-ecdsa
+mv server-ecdsa.pem server-ecdsa.crt
+mv server-ecdsa-key.pem server-ecdsa.key.insecure
+
 # generate IP: 127.0.0.1, CN: example.com certificates
 cfssl gencert \
   --ca ./ca.crt \

+ 20 - 0
integration/fixtures/server-ca-csr-ecdsa.json

@@ -0,0 +1,20 @@
+{
+  "key": {
+    "algo": "ecdsa",
+    "size": 256
+  },
+  "names": [
+    {
+      "O": "etcd",
+      "OU": "etcd Security",
+      "L": "San Francisco",
+      "ST": "California",
+      "C": "USA"
+    }
+  ],
+  "CN": "example.com",
+  "hosts": [
+    "127.0.0.1",
+    "localhost"
+  ]
+}

+ 20 - 0
integration/fixtures/server-ecdsa.crt

@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDRzCCAi+gAwIBAgIUK5XUt/HZQ3IpLbDFI1EIU4jiAxIwDQYJKoZIhvcNAQEL
+BQAwbzEMMAoGA1UEBhMDVVNBMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH
+Ew1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKEwRldGNkMRYwFAYDVQQLEw1ldGNkIFNl
+Y3VyaXR5MQswCQYDVQQDEwJjYTAeFw0xODA2MTkxNjIwMDBaFw0yODA2MTYxNjIw
+MDBaMHgxDDAKBgNVBAYTA1VTQTETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UE
+BxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChMEZXRjZDEWMBQGA1UECxMNZXRjZCBT
+ZWN1cml0eTEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjO
+PQMBBwNCAARDiiEQNXiH6eYz5Tws31IeU/OZ0sf7gHIJNvbST/cpXtjo4oFGcu0t
+TY4+FAMk0ku07s/kX9r55TgKr1VljG31o4GcMIGZMA4GA1UdDwEB/wQEAwIFoDAd
+BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNV
+HQ4EFgQUzo0YV8GX/aN/WRsyygA8QVZaMQQwHwYDVR0jBBgwFoAURt/EV2KWh7I1
+N8NXXowk6J1QtvgwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3
+DQEBCwUAA4IBAQCbUYjMwKuHQjNEFTvx4jQB/LZTr1Mn53C1etR0qLd50v9TXVzb
+FeZoo0g4mXln0BrLVMLatw0CTlGBCw+yJQ+5iJB5z3bKEl4ADwzRFDxwCMXXG8lV
+wQOS/eaTBcAkzf/BWITLB1mIIp3kKZwXM6IW53yDkPFDpnExPY+ycoNp58U1JxOJ
+ySM3/zyr0Ac8qCNqAakT2WacJ+AdB7pgoupbVF2WKT6qYbF1yvYY8x/zr8ePHznS
+fvuO+80wYPbyw13s6rpNv4d0L1k7GDcXVs3lHC47hSNn7OBhf4Xkku101MtP3DhO
+gFqW7p7vigK20tZKy4NYF6+nW3xJmOlw3gJF
+-----END CERTIFICATE-----

+ 5 - 0
integration/fixtures/server-ecdsa.key.insecure

@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIIZcM3NsBY+ZjW2t+AqdvW1lqYhD5l4zT6xr/eBIoh1aoAoGCCqGSM49
+AwEHoUQDQgAEQ4ohEDV4h+nmM+U8LN9SHlPzmdLH+4ByCTb20k/3KV7Y6OKBRnLt
+LU2OPhQDJNJLtO7P5F/a+eU4Cq9VZYxt9Q==
+-----END EC PRIVATE KEY-----