mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
Merge pull request #9883 from joelegasse/jwt
auth: Support all JWT algorithms
This commit is contained in:
commit
54e2511b25
@ -41,6 +41,7 @@ See [code changes](https://github.com/coreos/etcd/compare/v3.3.0...v3.4.0) and [
|
||||
- Add [`snapshot`](https://github.com/coreos/etcd/pull/9118) package for easier snapshot workflow (see [`godoc.org/github.com/etcd/clientv3/snapshot`](https://godoc.org/github.com/coreos/etcd/clientv3/snapshot) for more).
|
||||
- Improve [functional tester](https://github.com/coreos/etcd/tree/master/functional) coverage: [proxy layer to run network fault tests in CI](https://github.com/coreos/etcd/pull/9081), [TLS is enabled both for server and client](https://github.com/coreos/etcd/pull/9534), [liveness mode](https://github.com/coreos/etcd/issues/9230), [shuffle test sequence](https://github.com/coreos/etcd/issues/9381), [membership reconfiguration failure cases](https://github.com/coreos/etcd/pull/9564), [disastrous quorum loss and snapshot recover from a seed member](https://github.com/coreos/etcd/pull/9565), [embedded etcd](https://github.com/coreos/etcd/pull/9572).
|
||||
- Improve [index compaction blocking](https://github.com/coreos/etcd/pull/9511) by using a copy on write clone to avoid holding the lock for the traversal of the entire index.
|
||||
- Update [JWT methods](https://github.com/coreos/etcd/pull/9883) to allow for use of any supported signature method/algorithm.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
192
auth/jwt.go
192
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
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
var keys = make([]string, 0, len(optMap))
|
||||
for k := range optMap {
|
||||
if !knownOptions[k] {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
if lg != nil {
|
||||
lg.Warn("unknown JWT options", zap.Strings("keys", keys))
|
||||
} else {
|
||||
plog.Warningf("unknown JWT options: %v", keys)
|
||||
}
|
||||
}
|
||||
|
||||
key, err := opts.Key()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &tokenJWT{
|
||||
lg: lg,
|
||||
ttl: ttl,
|
||||
lg: lg,
|
||||
ttl: opts.TTL,
|
||||
signMethod: opts.SignMethod,
|
||||
key: key,
|
||||
}
|
||||
|
||||
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)
|
||||
switch t.signMethod.(type) {
|
||||
case *jwt.SigningMethodECDSA:
|
||||
if _, ok := t.key.(*ecdsa.PublicKey); ok {
|
||||
t.verifyOnly = true
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
t.verifyKey, err = jwt.ParseRSAPublicKeyFromPEM(verifyBytes)
|
||||
if err != nil {
|
||||
if lg != nil {
|
||||
lg.Warn(
|
||||
"failed to parse JWT public key",
|
||||
zap.String("public-key-path", jwtPubKeyPath),
|
||||
zap.Error(err),
|
||||
)
|
||||
} else {
|
||||
plog.Errorf("failed to parse public key (%s): %s", jwtPubKeyPath, err)
|
||||
case *jwt.SigningMethodRSA, *jwt.SigningMethodRSAPSS:
|
||||
if _, ok := t.key.(*rsa.PublicKey); ok {
|
||||
t.verifyOnly = true
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signBytes, err := ioutil.ReadFile(jwtPrivKeyPath)
|
||||
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)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
|
194
auth/jwt_test.go
194
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",
|
||||
},
|
||||
}
|
||||
jwt, err := newTokenProviderJWT(zap.NewExample(), opts)
|
||||
|
||||
for k, opts := range optsMap {
|
||||
t.Run(k, func(tt *testing.T) {
|
||||
testJWTInfo(tt, 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// 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")
|
||||
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["priv-key"] = jwtPrivKey
|
||||
|
||||
// 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"
|
||||
lg := zap.NewNop()
|
||||
|
||||
// bad file for pubkey
|
||||
opts["pub-key"] = "whatever"
|
||||
if _, err := newTokenProviderJWT(zap.NewExample(), opts); err == nil {
|
||||
t.Fatalf("expected failure on missing public key")
|
||||
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["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")
|
||||
}
|
||||
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
auth/options.go
Normal file
192
auth/options.go
Normal file
@ -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
|
||||
}
|
@ -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 (
|
||||
|
@ -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
integration/fixtures/server-ca-csr-ecdsa.json
Normal file
20
integration/fixtures/server-ca-csr-ecdsa.json
Normal file
@ -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
integration/fixtures/server-ecdsa.crt
Normal file
20
integration/fixtures/server-ecdsa.crt
Normal file
@ -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
integration/fixtures/server-ecdsa.key.insecure
Normal file
5
integration/fixtures/server-ecdsa.key.insecure
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIIZcM3NsBY+ZjW2t+AqdvW1lqYhD5l4zT6xr/eBIoh1aoAoGCCqGSM49
|
||||
AwEHoUQDQgAEQ4ohEDV4h+nmM+U8LN9SHlPzmdLH+4ByCTb20k/3KV7Y6OKBRnLt
|
||||
LU2OPhQDJNJLtO7P5F/a+eU4Cq9VZYxt9Q==
|
||||
-----END EC PRIVATE KEY-----
|
Loading…
x
Reference in New Issue
Block a user