diff --git a/config/config.go b/config/config.go index dc2e09eac..e057e2a30 100644 --- a/config/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -// Copyright 2022 Northern.tech AS +// Copyright 2023 Northern.tech AS // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/devauth/devauth.go b/devauth/devauth.go index 169652bec..b110c92af 100644 --- a/devauth/devauth.go +++ b/devauth/devauth.go @@ -119,6 +119,7 @@ type DevAuth struct { cOrch orchestrator.ClientRunner cTenant tenant.ClientRunner jwt jwt.Handler + jwtFallback jwt.Handler verifyTenant bool config Config cache cache.Cache @@ -1212,6 +1213,21 @@ func verifyTenantClaim(ctx context.Context, verifyTenant bool, tenant string) er return nil } +func (d *DevAuth) validateJWTToken(ctx context.Context, jti oid.ObjectID, raw string) error { + err := d.jwt.Validate(raw) + if err != nil && d.jwtFallback != nil { + err = d.jwtFallback.Validate(raw) + } + if err == jwt.ErrTokenExpired && jti.String() != "" { + log.FromContext(ctx).Errorf("Token %s expired: %v", jti.String(), err) + return d.handleExpiredToken(ctx, jti) + } else if err != nil { + log.FromContext(ctx).Errorf("Token %s invalid: %v", jti.String(), err) + return jwt.ErrTokenInvalid + } + return nil +} + func (d *DevAuth) VerifyToken(ctx context.Context, raw string) error { l := log.FromContext(ctx) @@ -1263,13 +1279,9 @@ func (d *DevAuth) VerifyToken(ctx context.Context, raw string) error { } // perform JWT signature and claims validation - if err := d.jwt.Validate(raw); err != nil { - if err == jwt.ErrTokenExpired && jti.String() != "" { - l.Errorf("Token %s expired: %v", jti.String(), err) - return d.handleExpiredToken(ctx, jti) - } - l.Errorf("Token %s invalid: %v", jti.String(), err) - return jwt.ErrTokenInvalid + err = d.validateJWTToken(ctx, jti, raw) + if err != nil { + return err } // cache check was a MISS, hit the db for verification @@ -1500,6 +1512,11 @@ func (d *DevAuth) GetTenantLimit( return d.GetLimit(ctx, name) } +func (d *DevAuth) WithJWTFallbackHandler(handler jwt.Handler) *DevAuth { + d.jwtFallback = handler + return d +} + // WithTenantVerification will force verification of tenant token with tenant // administrator when processing device authentication requests. Returns an // updated devauth. diff --git a/devauth/devauth_test.go b/devauth/devauth_test.go index c0cc3cce2..bb777266c 100644 --- a/devauth/devauth_test.go +++ b/devauth/devauth_test.go @@ -1809,8 +1809,9 @@ func TestDevAuthVerifyToken(t *testing.T) { tokenValidateErr error tokenOtherError error - jwToken *jwt.Token - validateErr error + jwToken *jwt.Token + validateErr error + fallbackValidateErr error getToken bool getTokenErr error @@ -1823,8 +1824,9 @@ func TestDevAuthVerifyToken(t *testing.T) { updateDeviceErr error - tenantVerify bool - willUpdateDevice bool + tenantVerify bool + willUpdateDevice bool + jwtHandlerFallback bool }{ { tokenString: "expired", @@ -1951,6 +1953,51 @@ func TestDevAuthVerifyToken(t *testing.T) { tenantVerify: true, }, + { + tokenString: "with fallback", + jwToken: &jwt.Token{ + Claims: jwt.Claims{ + ID: oid.NewUUIDv5("good"), + Subject: oid.NewUUIDv5("bar"), + ExpiresAt: jwt.Time{ + Time: time.Now().Add(time.Hour), + }, + Issuer: "Tester", + Device: true, + }, + }, + validateErr: jwt.ErrTokenInvalid, + getToken: true, + auth: &model.AuthSet{ + Id: oid.NewUUIDv5("good").String(), + Status: model.DevStatusAccepted, + DeviceId: oid.NewUUIDv5("bar").String(), + }, + dev: &model.Device{ + Id: oid.NewUUIDv5("bar").String(), + Decommissioning: false, + }, + willUpdateDevice: true, + jwtHandlerFallback: true, + }, + { + tokenString: "failed-validation-with-fallback", + tokenValidateErr: jwt.ErrTokenInvalid, + jwToken: &jwt.Token{ + Claims: jwt.Claims{ + ID: oid.NewUUIDv5("good"), + Subject: oid.NewUUIDv5("bar"), + ExpiresAt: jwt.Time{ + Time: time.Now().Add(time.Hour), + }, + Issuer: "Tester", + Device: true, + }, + }, + validateErr: jwt.ErrTokenInvalid, + fallbackValidateErr: jwt.ErrTokenInvalid, + jwtHandlerFallback: true, + }, } for i := range testCases { @@ -1962,6 +2009,12 @@ func TestDevAuthVerifyToken(t *testing.T) { ja := &mjwt.Handler{} devauth := NewDevAuth(db, nil, ja, Config{}) + if tc.jwtHandlerFallback { + jaFallback := &mjwt.Handler{} + jaFallback.On("Validate", tc.tokenString).Return(tc.fallbackValidateErr) + + devauth = devauth.WithJWTFallbackHandler(jaFallback) + } if tc.tenantVerify { // ok to pass nil tenantadm client here devauth = devauth.WithTenantVerification(nil) @@ -1973,8 +2026,8 @@ func TestDevAuthVerifyToken(t *testing.T) { return tc.jwToken }, tc.tokenParseErr) - if tc.tokenParseErr == nil && tc.jwToken != nil && - tc.tokenOtherError == nil && tc.tokenString != "missing-tenant-claim" { + if tc.tokenParseErr == nil && tc.jwToken != nil && tc.tokenOtherError == nil && + tc.tokenString != "missing-tenant-claim" { ja.On("Validate", tc.tokenString).Return(tc.validateErr) } diff --git a/jwt/jwt.go b/jwt/jwt.go index 4ca936c52..f5340c629 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -14,9 +14,12 @@ package jwt import ( + "crypto/ed25519" "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" - jwtgo "github.com/golang-jwt/jwt/v4" "github.com/pkg/errors" ) @@ -25,6 +28,11 @@ var ( ErrTokenInvalid = errors.New("jwt: token invalid") ) +const ( + pemHeaderPKCS1 = "RSA PRIVATE KEY" + pemHeaderPKCS8 = "PRIVATE KEY" +) + // Handler jwt generator/verifier // //go:generate ../utils/mockgen.sh @@ -41,76 +49,30 @@ type Handler interface { Validate(string) error } -// JWTHandlerRS256 is an RS256-specific JWTHandler -type JWTHandlerRS256 struct { - privKey *rsa.PrivateKey - fallbackPrivKey *rsa.PrivateKey -} - -func NewJWTHandlerRS256(privKey *rsa.PrivateKey, fallbackPrivKey *rsa.PrivateKey) *JWTHandlerRS256 { - return &JWTHandlerRS256{ - privKey: privKey, - fallbackPrivKey: fallbackPrivKey, +func NewJWTHandler(privateKeyPath string) (Handler, error) { + priv, err := os.ReadFile(privateKeyPath) + block, _ := pem.Decode(priv) + if block == nil { + return nil, errors.Wrap(err, "failed to read private key") } -} - -func (j *JWTHandlerRS256) ToJWT(token *Token) (string, error) { - //generate - jt := jwtgo.NewWithClaims(jwtgo.SigningMethodRS256, &token.Claims) - - //sign - data, err := jt.SignedString(j.privKey) - return data, err -} - -func (j *JWTHandlerRS256) FromJWT(tokstr string) (*Token, error) { - parser := jwtgo.NewParser(jwtgo.WithoutClaimsValidation()) - jwttoken, _, err := parser.ParseUnverified(tokstr, &Claims{}) - if err == nil { - token := Token{} - if claims, ok := jwttoken.Claims.(*Claims); ok { - token.Claims = *claims - return &token, nil + switch block.Type { + case pemHeaderPKCS1: + privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "failed to read rsa private key") } - } - - return nil, ErrTokenInvalid -} - -func (j *JWTHandlerRS256) Validate(tokstr string) error { - var err error - var jwttoken *jwtgo.Token - for _, privKey := range []*rsa.PrivateKey{ - j.privKey, - j.fallbackPrivKey, - } { - if privKey != nil { - jwttoken, err = jwtgo.ParseWithClaims(tokstr, &Claims{}, - func(token *jwtgo.Token) (interface{}, error) { - if _, ok := token.Method.(*jwtgo.SigningMethodRSA); !ok { - return nil, errors.New("unexpected signing method: " + token.Method.Alg()) - } - return &privKey.PublicKey, nil - }, - ) - if jwttoken != nil && err == nil { - break - } + return NewJWTHandlerRS256(privKey), nil + case pemHeaderPKCS8: + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, errors.Wrap(err, "failed to read private key") } - } - - // our Claims return Mender-specific validation errors - // go-jwt will wrap them in a generic ValidationError - unwrap and return directly - if jwttoken != nil && !jwttoken.Valid { - return ErrTokenInvalid - } else if err != nil { - err, ok := err.(*jwtgo.ValidationError) - if ok && err.Inner != nil { - return err.Inner - } else { - return err + switch v := key.(type) { + case *rsa.PrivateKey: + return NewJWTHandlerRS256(v), nil + case ed25519.PrivateKey: + return NewJWTHandlerEd25519(&v), nil } } - - return nil + return nil, errors.Errorf("unsupported server private key type") } diff --git a/jwt/jwt_ed25519.go b/jwt/jwt_ed25519.go new file mode 100644 index 000000000..9c4b029c9 --- /dev/null +++ b/jwt/jwt_ed25519.go @@ -0,0 +1,81 @@ +// Copyright 2023 Northern.tech AS +// +// 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 jwt + +import ( + "crypto/ed25519" + + "github.com/golang-jwt/jwt/v4" + "github.com/pkg/errors" +) + +// JWTHandlerEd25519 is an Ed25519-specific JWTHandler +type JWTHandlerEd25519 struct { + privKey *ed25519.PrivateKey +} + +func NewJWTHandlerEd25519(privKey *ed25519.PrivateKey) *JWTHandlerEd25519 { + return &JWTHandlerEd25519{ + privKey: privKey, + } +} + +func (j *JWTHandlerEd25519) ToJWT(token *Token) (string, error) { + //generate + jt := jwt.NewWithClaims(jwt.SigningMethodEdDSA, &token.Claims) + + //sign + data, err := jt.SignedString(j.privKey) + return data, err +} + +func (j *JWTHandlerEd25519) FromJWT(tokstr string) (*Token, error) { + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) + jwttoken, _, err := parser.ParseUnverified(tokstr, &Claims{}) + if err == nil { + token := Token{} + if claims, ok := jwttoken.Claims.(*Claims); ok { + token.Claims = *claims + return &token, nil + } + } + + return nil, ErrTokenInvalid +} + +func (j *JWTHandlerEd25519) Validate(tokstr string) error { + jwttoken, err := jwt.ParseWithClaims(tokstr, &Claims{}, + func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok { + return nil, errors.New("unexpected signing method: " + token.Method.Alg()) + } + return j.privKey.Public(), nil + }, + ) + + // our Claims return Mender-specific validation errors + // go-jwt will wrap them in a generic ValidationError - unwrap and return directly + if jwttoken != nil && !jwttoken.Valid { + return ErrTokenInvalid + } else if err != nil { + err, ok := err.(*jwt.ValidationError) + if ok && err.Inner != nil { + return err.Inner + } else { + return err + } + } + + return nil +} diff --git a/jwt/jwt_ed25519_test.go b/jwt/jwt_ed25519_test.go new file mode 100644 index 000000000..07a7428e8 --- /dev/null +++ b/jwt/jwt_ed25519_test.go @@ -0,0 +1,313 @@ +// Copyright 2023 Northern.tech AS +// +// 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 jwt + +import ( + "crypto/ed25519" + "crypto/x509" + "encoding/pem" + "os" + "testing" + "time" + + jwtgo "github.com/golang-jwt/jwt/v4" + "github.com/mendersoftware/go-lib-micro/mongo/oid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewJWTHandlerEd25519(t *testing.T) { + privKey := loadEd25519PrivKey("./testdata/ed25519.pem", t) + jwtHandler := NewJWTHandlerEd25519(privKey) + + assert.NotNil(t, jwtHandler) +} + +func TestJWTHandlerEd25519GenerateToken(t *testing.T) { + testCases := map[string]struct { + privKey *ed25519.PrivateKey + claims Claims + expiresInSec int64 + }{ + "ok": { + privKey: loadEd25519PrivKey("./testdata/ed25519.pem", t), + claims: Claims{ + Issuer: "Mender", + Subject: oid.NewUUIDv5("foo"), + ExpiresAt: Time{ + Time: time.Now().Add(time.Hour), + }, + }, + expiresInSec: 3600, + }, + "ok, with tenant": { + privKey: loadEd25519PrivKey("./testdata/ed25519.pem", t), + claims: Claims{ + Issuer: "Mender", + Subject: oid.NewUUIDv5("foo"), + ExpiresAt: Time{ + Time: time.Now().Add(time.Hour), + }, + Tenant: "foobar", + }, + expiresInSec: 3600, + }, + } + + for name, tc := range testCases { + t.Logf("test case: %s", name) + jwtHandler := NewJWTHandlerEd25519(tc.privKey) + + raw, err := jwtHandler.ToJWT(&Token{ + Claims: tc.claims, + }) + assert.NoError(t, err) + + parsed := parseGeneratedTokenEd25519(t, string(raw), tc.privKey) + if assert.NotNil(t, parsed) { + mc := parsed.Claims.(jwtgo.MapClaims) + assert.Equal(t, tc.claims.Issuer, mc["iss"]) + assert.Equal(t, tc.claims.Subject.String(), mc["sub"]) + if tc.claims.Tenant != "" { + assert.Equal(t, tc.claims.Tenant, mc["mender.tenant"]) + } else { + assert.Nil(t, mc["mender.tenant"]) + } + } + } +} + +func TestJWTHandlerEd25519FromJWT(t *testing.T) { + + key := loadEd25519PrivKey("./testdata/ed25519.pem", t) + + testCases := map[string]struct { + privKey *ed25519.PrivateKey + + inToken string + + outToken Token + outErr error + }{ + "ok (all claims)": { + privKey: key, + + inToken: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdG" + + "kiOiJiOTQ3NTMzNi1kZGU2LTU0OTctODA0NC01MWFhOW" + + "RkYzAyZjgiLCJzdWIiOiJiY2E5NWFkYi1iNWYxLTU2NG" + + "YtOTZhNy02MzU1YzUyZDFmYTciLCJhdWQiOiJNZW5kZX" + + "IiLCJzY3AiOiJtZW5kZXIuKiIsImlzcyI6Ik1lbmRlci" + + "IsImV4cCI6NDE0NzQ4MzY0NywiaWF0IjoxMjM0NTY3LC" + + "JuYmYiOjEyMzQ1Njc4LCJtZW5kZXIudHJpYWwiOmZhbH" + + "NlfQ.eOnpurEYseItJXycyjOyfTO-RI_MCSF1e79HG63" + + "HzVoR2xLzrA044hQ_pUneqG1V30h67EhWZY1wspqBay-" + + "Cw", + + outToken: Token{ + Claims: Claims{ + ID: oid.NewUUIDv5("someid"), + Subject: oid.NewUUIDv5("foo"), + Audience: "Mender", + ExpiresAt: Time{ + Time: time.Unix(4147483647, 0), + }, + IssuedAt: Time{ + Time: time.Unix(1234567, 0), + }, + Issuer: "Mender", + NotBefore: Time{ + Time: time.Unix(12345678, 0), + }, + Scope: "mender.*", + }, + }, + }, + "ok (some claims)": { + privKey: key, + + inToken: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdG" + + "kiOiJiOTQ3NTMzNi1kZGU2LTU0OTctODA0NC01MWFhOW" + + "RkYzAyZjgiLCJzdWIiOiJiY2E5NWFkYi1iNWYxLTU2NG" + + "YtOTZhNy02MzU1YzUyZDFmYTciLCJzY3AiOiJtZW5kZX" + + "IudXNlcnMuaW5pdGlhbC5jcmVhdGUiLCJpc3MiOiJNZW" + + "5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImlhdCI6MTIzND" + + "U2NywibmJmIjoxMjM0NTY3OCwibWVuZGVyLnRyaWFsIj" + + "pmYWxzZX0.M2TiIXKt5vVYVznlzACUkD_PQCnfhedg3r" + + "LpLAge3wI9Xq22t2KL0nc2c8GhQWXVV40M73zwf5p8rn" + + "42PdGvCg", + + outToken: Token{ + Claims: Claims{ + ID: oid.NewUUIDv5("someid"), + Subject: oid.NewUUIDv5("foo"), + ExpiresAt: Time{ + Time: time.Unix(4147483647, 0), + }, + IssuedAt: Time{ + Time: time.Unix(1234567, 0), + }, + NotBefore: Time{ + Time: time.Unix(12345678, 0), + }, + Issuer: "Mender", + Scope: "mender.users.initial.create", + }, + }, + }, + "error - token invalid": { + privKey: key, + + inToken: "1234123412341234", + + outToken: Token{}, + outErr: ErrTokenInvalid, + }, + } + + for name, tc := range testCases { + t.Logf("test case: %s", name) + jwtHandler := NewJWTHandlerEd25519(tc.privKey) + + token, err := jwtHandler.FromJWT(tc.inToken) + if tc.outErr == nil { + assert.NoError(t, err) + assert.Equal(t, tc.outToken, *token) + } else { + assert.EqualError(t, tc.outErr, err.Error()) + } + } +} + +func TestJWTHandlerEd25519Validate(t *testing.T) { + key := loadEd25519PrivKey("./testdata/ed25519.pem", t) + + testCases := map[string]struct { + privKey *ed25519.PrivateKey + + inToken string + + outErr error + }{ + "ok (all claims)": { + privKey: key, + + inToken: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdG" + + "kiOiJiOTQ3NTMzNi1kZGU2LTU0OTctODA0NC01MWFhOW" + + "RkYzAyZjgiLCJzdWIiOiJiY2E5NWFkYi1iNWYxLTU2NG" + + "YtOTZhNy02MzU1YzUyZDFmYTciLCJhdWQiOiJNZW5kZX" + + "IiLCJzY3AiOiJtZW5kZXIuKiIsImlzcyI6Ik1lbmRlci" + + "IsImV4cCI6NDE0NzQ4MzY0NywiaWF0IjoxMjM0NTY3LC" + + "JuYmYiOjEyMzQ1Njc4LCJtZW5kZXIudHJpYWwiOmZhbH" + + "NlfQ.eOnpurEYseItJXycyjOyfTO-RI_MCSF1e79HG63" + + "HzVoR2xLzrA044hQ_pUneqG1V30h67EhWZY1wspqBay-" + + "FCw", + }, + "ok (some claims)": { + privKey: key, + + inToken: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdG" + + "kiOiJiOTQ3NTMzNi1kZGU2LTU0OTctODA0NC01MWFhOW" + + "RkYzAyZjgiLCJzdWIiOiJiY2E5NWFkYi1iNWYxLTU2NG" + + "YtOTZhNy02MzU1YzUyZDFmYTciLCJzY3AiOiJtZW5kZX" + + "IudXNlcnMuaW5pdGlhbC5jcmVhdGUiLCJpc3MiOiJNZW" + + "5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImlhdCI6MTIzND" + + "U2NywibmJmIjotNjIxMzU1OTY4MDAsIm1lbmRlci50cm" + + "lhbCI6ZmFsc2V9.C2-Ws5A_Pm9H7v_TCVDXCgREWBeYZ" + + "HCRuZmTFxBuv7TJhfGvGusu-SB6bZ3N9OPJYwfConmq1" + + "YprlfFpAAgmDQ", + }, + "error - bad claims": { + privKey: key, + + inToken: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdG" + + "kiOiJiOTQ3NTMzNi1kZGU2LTU0OTctODA0NC01MWFhOW" + + "RkYzAyZjgiLCJzdWIiOiJiY2E5NWFkYi1iNWYxLTU2NG" + + "YtOTZhNy02MzU1YzUyZDFmYTciLCJhdWQiOiJNZW5kZX" + + "IiLCJzY3AiOiJtZW5kZXIuKiIsImV4cCI6NDE0NzQ4Mz" + + "Y0NywiaWF0IjoxMjM0NTY3LCJuYmYiOjEyMzQ1Njc4LC" + + "JtZW5kZXIudHJpYWwiOmZhbHNlfQ.T4PVYJvRSusq7MZ" + + "5XaOo6mLW9GDKdqdWO8NUZOZZ-KJ69d1UDbKWFSs9PPx" + + "cNwS5a0j8iiTA6m6-YW0nEvLWAg", + + outErr: errors.New("jwt: token invalid"), + }, + "error - bad signature": { + privKey: key, + + inToken: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdG" + + "kiOiJiOTQ3NTMzNi1kZGU2LTU0OTctODA0NC01MWFhOW" + + "RkYzAyZjgiLCJzdWIiOiJiY2E5NWFkYi1iNWYxLTU2NG" + + "YtOTZhNy02MzU1YzUyZDFmYTciLCJhdWQiOiJNZW5kZX" + + "IiLCJzY3AiOiJtZW5kZXIuKiIsImlzcyI6Ik1lbmRlci" + + "IsImV4cCI6NDE0NzQ4MzY0NywiaWF0IjoxMjM0NTY3LC" + + "JuYmYiOjEyMzQ1Njc4LCJtZW5kZXIudHJpYWwiOmZhbH" + + "NlfQ.eOnpurEYseItJXycyjOyfTO-RI_MCSF1e79HG63" + + "HzVoR2xLzrA044hQ_pUneqG1V30h67EhWZY1wspqBay-" + + "XXX", + + outErr: ErrTokenInvalid, + }, + "error - token invalid": { + privKey: key, + + inToken: "1234123412341234", + + outErr: errors.New("token contains an invalid number of segments"), + }, + } + + for name, tc := range testCases { + t.Logf("test case: %s", name) + jwtHandler := NewJWTHandlerEd25519(tc.privKey) + + err := jwtHandler.Validate(tc.inToken) + if tc.outErr == nil { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.EqualError(t, tc.outErr, err.Error()) + } + } +} + +func loadEd25519PrivKey(path string, t *testing.T) *ed25519.PrivateKey { + pemData, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to load key: %v", err) + } + + block, _ := pem.Decode(pemData) + assert.Equal(t, block.Type, pemHeaderPKCS8) + + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + assert.NoError(t, err) + + retKey := key.(ed25519.PrivateKey) + return &retKey +} + +func parseGeneratedTokenEd25519(t *testing.T, token string, key *ed25519.PrivateKey) *jwtgo.Token { + tokenParsed, err := jwtgo.Parse(token, func(token *jwtgo.Token) (interface{}, error) { + if _, ok := token.Method.(*jwtgo.SigningMethodEd25519); !ok { + return nil, errors.New("Unexpected signing method: " + token.Method.Alg()) + } + return key.Public(), nil + }) + + if err != nil { + t.Fatalf("can't parse token: %s", err.Error()) + } + + return tokenParsed +} diff --git a/jwt/jwt_rsa.go b/jwt/jwt_rsa.go new file mode 100644 index 000000000..073880a9a --- /dev/null +++ b/jwt/jwt_rsa.go @@ -0,0 +1,81 @@ +// Copyright 2023 Northern.tech AS +// +// 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 jwt + +import ( + "crypto/rsa" + + "github.com/golang-jwt/jwt/v4" + "github.com/pkg/errors" +) + +// JWTHandlerRS256 is an RS256-specific JWTHandler +type JWTHandlerRS256 struct { + privKey *rsa.PrivateKey +} + +func NewJWTHandlerRS256(privKey *rsa.PrivateKey) *JWTHandlerRS256 { + return &JWTHandlerRS256{ + privKey: privKey, + } +} + +func (j *JWTHandlerRS256) ToJWT(token *Token) (string, error) { + //generate + jt := jwt.NewWithClaims(jwt.SigningMethodRS256, &token.Claims) + + //sign + data, err := jt.SignedString(j.privKey) + return data, err +} + +func (j *JWTHandlerRS256) FromJWT(tokstr string) (*Token, error) { + parser := jwt.NewParser(jwt.WithoutClaimsValidation()) + jwttoken, _, err := parser.ParseUnverified(tokstr, &Claims{}) + if err == nil { + token := Token{} + if claims, ok := jwttoken.Claims.(*Claims); ok { + token.Claims = *claims + return &token, nil + } + } + + return nil, ErrTokenInvalid +} + +func (j *JWTHandlerRS256) Validate(tokstr string) error { + jwttoken, err := jwt.ParseWithClaims(tokstr, &Claims{}, + func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, errors.New("unexpected signing method: " + token.Method.Alg()) + } + return &j.privKey.PublicKey, nil + }, + ) + + // our Claims return Mender-specific validation errors + // go-jwt will wrap them in a generic ValidationError - unwrap and return directly + if jwttoken != nil && !jwttoken.Valid { + return ErrTokenInvalid + } else if err != nil { + err, ok := err.(*jwt.ValidationError) + if ok && err.Inner != nil { + return err.Inner + } else { + return err + } + } + + return nil +} diff --git a/jwt/jwt_rsa_test.go b/jwt/jwt_rsa_test.go new file mode 100644 index 000000000..d2aedf529 --- /dev/null +++ b/jwt/jwt_rsa_test.go @@ -0,0 +1,337 @@ +// Copyright 2023 Northern.tech AS +// +// 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 jwt + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "testing" + "time" + + jwtgo "github.com/golang-jwt/jwt/v4" + "github.com/mendersoftware/go-lib-micro/mongo/oid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestNewJWTHandlerRS256(t *testing.T) { + privKey := loadRSAPrivKey("./testdata/rsa.pem", t) + jwtHandler := NewJWTHandlerRS256(privKey) + + assert.NotNil(t, jwtHandler) +} + +func TestJWTHandlerRS256GenerateToken(t *testing.T) { + testCases := map[string]struct { + privKey *rsa.PrivateKey + claims Claims + expiresInSec int64 + }{ + "ok": { + privKey: loadRSAPrivKey("./testdata/rsa.pem", t), + claims: Claims{ + Issuer: "Mender", + Subject: oid.NewUUIDv5("foo"), + ExpiresAt: Time{ + Time: time.Now().Add(time.Hour), + }, + }, + expiresInSec: 3600, + }, + "ok, with tenant": { + privKey: loadRSAPrivKey("./testdata/rsa.pem", t), + claims: Claims{ + Issuer: "Mender", + Subject: oid.NewUUIDv5("foo"), + ExpiresAt: Time{ + Time: time.Now().Add(time.Hour), + }, + Tenant: "foobar", + }, + expiresInSec: 3600, + }, + } + + for name, tc := range testCases { + t.Logf("test case: %s", name) + jwtHandler := NewJWTHandlerRS256(tc.privKey) + + raw, err := jwtHandler.ToJWT(&Token{ + Claims: tc.claims, + }) + assert.NoError(t, err) + + parsed := parseGeneratedTokenRS256(t, string(raw), tc.privKey) + if assert.NotNil(t, parsed) { + mc := parsed.Claims.(jwtgo.MapClaims) + assert.Equal(t, tc.claims.Issuer, mc["iss"]) + assert.Equal(t, tc.claims.Subject.String(), mc["sub"]) + if tc.claims.Tenant != "" { + assert.Equal(t, tc.claims.Tenant, mc["mender.tenant"]) + } else { + assert.Nil(t, mc["mender.tenant"]) + } + } + } +} + +func TestJWTHandlerRS256FromJWT(t *testing.T) { + + key := loadRSAPrivKey("./testdata/rsa.pem", t) + + testCases := map[string]struct { + privKey *rsa.PrivateKey + fallbackPrivKey *rsa.PrivateKey + + inToken string + + outToken Token + outErr error + }{ + "ok (all claims)": { + privKey: key, + + inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + + "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + + "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + + "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + + "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + + "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + + "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + + "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + + "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + + "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + + "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + + "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + + "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + + "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + + "6Wol2S-I8ag", + + outToken: Token{ + Claims: Claims{ + ID: oid.NewUUIDv5("someid"), + Subject: oid.NewUUIDv5("foo"), + Audience: "Mender", + ExpiresAt: Time{ + Time: time.Unix(4147483647, 0), + }, + IssuedAt: Time{ + Time: time.Unix(1234567, 0), + }, + Issuer: "Mender", + NotBefore: Time{ + Time: time.Unix(12345678, 0), + }, + Scope: "mender.*", + }, + }, + }, + "ok (some claims)": { + privKey: key, + + inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH" + + "AiOjQxNDc0ODM2NDcsImp0aSI6ImI5NDc1MzM2LWRkZT" + + "YtNTQ5Ny04MDQ0LTUxYWE5ZGRjMDJmOCIsImlhdCI6MT" + + "IzNDU2NywiaXNzIjoiTWVuZGVyIiwic3ViIjoiYmNhOT" + + "VhZGItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Ii" + + "wic2NwIjoibWVuZGVyLnVzZXJzLmluaXRpYWwuY3JlYX" + + "RlIn0.qzW1QfnvfB384DfOyX6LC4jsTSVEWwsyb-vSeA" + + "ebfHdJquX2BfQ6_1ZGtqyCC7mOhMrXeJv1gmprpkOxKw" + + "hPBexS-U1gOc_aO7Oi7uPl1HQRhMw9SM2QamOOVGmLi5" + + "1uVg9ZEQhvnN7s-w4girnmGyhnPWV58CorJtW4t1Dgyr" + + "6fG_v8wtrGt-rMb7uMLmEQMjIqcUBa6mlU1sVBEPTeGb" + + "KvR6kSJ727UW91y7krTcQUdNN4rv2CfG7ETlPsrUgMvr" + + "GUPqoq_ygbLX3kDZveVzTE2CQdI7PpAO14UZQxRBfff5" + + "ewyW4P0ulYRj0mPF5NmsHwbADoAjILoA5uSWW9Dg", + + outToken: Token{ + Claims: Claims{ + ID: oid.NewUUIDv5("someid"), + Subject: oid.NewUUIDv5("foo"), + ExpiresAt: Time{ + Time: time.Unix(4147483647, 0), + }, + IssuedAt: Time{ + Time: time.Unix(1234567, 0), + }, + Issuer: "Mender", + Scope: "mender.users.initial.create", + }, + }, + }, + "error - token invalid": { + privKey: key, + + inToken: "1234123412341234", + + outToken: Token{}, + outErr: ErrTokenInvalid, + }, + } + + for name, tc := range testCases { + t.Logf("test case: %s", name) + jwtHandler := NewJWTHandlerRS256(tc.privKey) + + token, err := jwtHandler.FromJWT(tc.inToken) + if tc.outErr == nil { + assert.NoError(t, err) + assert.Equal(t, tc.outToken, *token) + } else { + assert.EqualError(t, tc.outErr, err.Error()) + } + } +} + +func TestJWTHandlerRS256Validate(t *testing.T) { + key := loadRSAPrivKey("./testdata/rsa.pem", t) + + testCases := map[string]struct { + privKey *rsa.PrivateKey + + inToken string + + outErr error + }{ + "ok (all claims)": { + privKey: key, + + inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + + "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + + "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + + "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + + "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + + "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + + "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + + "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + + "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + + "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + + "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + + "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + + "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + + "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + + "6Wol2S-I8ag", + }, + "ok (some claims)": { + privKey: key, + + inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH" + + "AiOjQxNDc0ODM2NDcsImp0aSI6ImI5NDc1MzM2LWRkZT" + + "YtNTQ5Ny04MDQ0LTUxYWE5ZGRjMDJmOCIsImlhdCI6MT" + + "IzNDU2NywiaXNzIjoiTWVuZGVyIiwic3ViIjoiYmNhOT" + + "VhZGItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Ii" + + "wic2NwIjoibWVuZGVyLnVzZXJzLmluaXRpYWwuY3JlYX" + + "RlIn0.qzW1QfnvfB384DfOyX6LC4jsTSVEWwsyb-vSeA" + + "ebfHdJquX2BfQ6_1ZGtqyCC7mOhMrXeJv1gmprpkOxKw" + + "hPBexS-U1gOc_aO7Oi7uPl1HQRhMw9SM2QamOOVGmLi5" + + "1uVg9ZEQhvnN7s-w4girnmGyhnPWV58CorJtW4t1Dgyr" + + "6fG_v8wtrGt-rMb7uMLmEQMjIqcUBa6mlU1sVBEPTeGb" + + "KvR6kSJ727UW91y7krTcQUdNN4rv2CfG7ETlPsrUgMvr" + + "GUPqoq_ygbLX3kDZveVzTE2CQdI7PpAO14UZQxRBfff5" + + "ewyW4P0ulYRj0mPF5NmsHwbADoAjILoA5uSWW9Dg", + }, + + "error - bad claims": { + privKey: key, + + inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGk" + + "iOm51bGwsInN1YiI6ImJjYTk1YWRiLWI1ZjEtNTY0Zi0" + + "5NmE3LTYzNTVjNTJkMWZhNyIsImV4cCI6MTY5NjQxNjU" + + "2NCwiaWF0IjotNjIxMzU1OTY4MDAsIm5iZiI6LTYyMTM" + + "1NTk2ODAwLCJtZW5kZXIudHJpYWwiOmZhbHNlfQ.LNRO" + + "1CzYVkOqU_-ikNva-MvFyvZTVLbR8irmecbPKPij-6cv" + + "h_DymOdbtupRCpABq2XFfLGAz68AOqGhc0Utp_AL-EY7" + + "kSH-QbPVdlFvnZO_T-gPHxOY2wNoZqnyusr-cpiRR413" + + "lySS5t5ZPsghFtlCSFHITdZ11sin79C1JJxd3cUnhjXj" + + "P-wL7YJmsfFR9KfSL4AEPtpDsQ98gPhcnqPRCBuLSFcU" + + "d3_w-pbc7PkbM0A_nO2jrwCJCaHvjMMvL9FHIZ2-xfUW" + + "qDB13KkPo0BrVwHLvhykLlCuhshNaNugzH0Tb4djrM__" + + "NCKofdozu3DowLLjesXp7oIYWRAKUQ", + + outErr: errors.New("jwt: token invalid"), + }, + "error - bad signature": { + privKey: key, + + inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + + "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + + "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + + "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + + "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + + "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + + "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + + "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + + "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + + "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + + "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + + "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + + "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + + "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + + "6Wol2S-I8XX", + outErr: ErrTokenInvalid, + }, + "error - token invalid": { + privKey: key, + + inToken: "1234123412341234", + + outErr: errors.New("token contains an invalid number of segments"), + }, + } + + for name, tc := range testCases { + t.Logf("test case: %s", name) + jwtHandler := NewJWTHandlerRS256(tc.privKey) + + err := jwtHandler.Validate(tc.inToken) + if tc.outErr == nil { + assert.NoError(t, err) + } else { + assert.Error(t, err) + assert.EqualError(t, tc.outErr, err.Error()) + } + } +} + +func loadRSAPrivKey(path string, t *testing.T) *rsa.PrivateKey { + pemData, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to load key: %v", err) + } + + block, _ := pem.Decode(pemData) + assert.Equal(t, block.Type, pemHeaderPKCS1) + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + assert.NoError(t, err) + + return key +} + +func parseGeneratedTokenRS256(t *testing.T, token string, key *rsa.PrivateKey) *jwtgo.Token { + tokenParsed, err := jwtgo.Parse(token, func(token *jwtgo.Token) (interface{}, error) { + if _, ok := token.Method.(*jwtgo.SigningMethodRSA); !ok { + return nil, errors.New("Unexpected signing method: " + token.Method.Alg()) + } + return &key.PublicKey, nil + }) + + if err != nil { + t.Fatalf("can't parse token: %s", err.Error()) + } + + return tokenParsed +} diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index c87622908..8fd60c58c 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -14,440 +14,44 @@ package jwt import ( - "crypto/rsa" "testing" - "time" - jwtgo "github.com/golang-jwt/jwt/v4" "github.com/pkg/errors" "github.com/stretchr/testify/assert" - - "github.com/mendersoftware/deviceauth/keys" - "github.com/mendersoftware/go-lib-micro/mongo/oid" ) -func TestNewJWTHandlerRS256(t *testing.T) { - privKey := loadPrivKey("./testdata/private.pem", t) - jwtHandler := NewJWTHandlerRS256(privKey, nil) - - assert.NotNil(t, jwtHandler) -} - -func TestJWTHandlerRS256GenerateToken(t *testing.T) { - testCases := map[string]struct { - privKey *rsa.PrivateKey - claims Claims - expiresInSec int64 - }{ - "ok": { - privKey: loadPrivKey("./testdata/private.pem", t), - claims: Claims{ - Issuer: "Mender", - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: Time{ - Time: time.Now().Add(time.Hour), - }, - }, - expiresInSec: 3600, - }, - "ok, with tenant": { - privKey: loadPrivKey("./testdata/private.pem", t), - claims: Claims{ - Issuer: "Mender", - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: Time{ - Time: time.Now().Add(time.Hour), - }, - Tenant: "foobar", - }, - expiresInSec: 3600, - }, - } - - for name, tc := range testCases { - t.Logf("test case: %s", name) - jwtHandler := NewJWTHandlerRS256(tc.privKey, nil) - - raw, err := jwtHandler.ToJWT(&Token{ - Claims: tc.claims, - }) - assert.NoError(t, err) - - parsed := parseGeneratedTokenRS256(t, string(raw), tc.privKey) - if assert.NotNil(t, parsed) { - mc := parsed.Claims.(jwtgo.MapClaims) - assert.Equal(t, tc.claims.Issuer, mc["iss"]) - assert.Equal(t, tc.claims.Subject.String(), mc["sub"]) - if tc.claims.Tenant != "" { - assert.Equal(t, tc.claims.Tenant, mc["mender.tenant"]) - } else { - assert.Nil(t, mc["mender.tenant"]) - } - } - } -} - -func TestJWTHandlerRS256FromJWT(t *testing.T) { - - key := loadPrivKey("./testdata/private.pem", t) - keyAlternative := loadPrivKey("./testdata/private_alternative.pem", t) - +func TestNewJWTHandler(t *testing.T) { testCases := map[string]struct { - privKey *rsa.PrivateKey - fallbackPrivKey *rsa.PrivateKey - - inToken string - - outToken Token - outErr error + privateKeyPath string + err error }{ - "ok (all claims)": { - privKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + - "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + - "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + - "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + - "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + - "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + - "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + - "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + - "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + - "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + - "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + - "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + - "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + - "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + - "6Wol2S-I8ag", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - Audience: "Mender", - ExpiresAt: Time{ - Time: time.Unix(4147483647, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Issuer: "Mender", - NotBefore: Time{ - Time: time.Unix(12345678, 0), - }, - Scope: "mender.*", - }, - }, + "ok, pkcs1, rsa": { + privateKeyPath: "./testdata/rsa.pem", }, - "ok (some claims)": { - privKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH" + - "AiOjQxNDc0ODM2NDcsImp0aSI6ImI5NDc1MzM2LWRkZT" + - "YtNTQ5Ny04MDQ0LTUxYWE5ZGRjMDJmOCIsImlhdCI6MT" + - "IzNDU2NywiaXNzIjoiTWVuZGVyIiwic3ViIjoiYmNhOT" + - "VhZGItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Ii" + - "wic2NwIjoibWVuZGVyLnVzZXJzLmluaXRpYWwuY3JlYX" + - "RlIn0.qzW1QfnvfB384DfOyX6LC4jsTSVEWwsyb-vSeA" + - "ebfHdJquX2BfQ6_1ZGtqyCC7mOhMrXeJv1gmprpkOxKw" + - "hPBexS-U1gOc_aO7Oi7uPl1HQRhMw9SM2QamOOVGmLi5" + - "1uVg9ZEQhvnN7s-w4girnmGyhnPWV58CorJtW4t1Dgyr" + - "6fG_v8wtrGt-rMb7uMLmEQMjIqcUBa6mlU1sVBEPTeGb" + - "KvR6kSJ727UW91y7krTcQUdNN4rv2CfG7ETlPsrUgMvr" + - "GUPqoq_ygbLX3kDZveVzTE2CQdI7PpAO14UZQxRBfff5" + - "ewyW4P0ulYRj0mPF5NmsHwbADoAjILoA5uSWW9Dg", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: Time{ - Time: time.Unix(4147483647, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Issuer: "Mender", - Scope: "mender.users.initial.create", - }, - }, + "ok, pkcs8, rsa": { + privateKeyPath: "./testdata/rsa_pkcs8.pem", }, - "ok (fallback not used)": { - privKey: key, - fallbackPrivKey: keyAlternative, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + - "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + - "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + - "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + - "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + - "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + - "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + - "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + - "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + - "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + - "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + - "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + - "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + - "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + - "6Wol2S-I8ag", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - Audience: "Mender", - ExpiresAt: Time{ - Time: time.Unix(4147483647, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Issuer: "Mender", - NotBefore: Time{ - Time: time.Unix(12345678, 0), - }, - Scope: "mender.*", - }, - }, + "ok, pkcs8, ed25519": { + privateKeyPath: "./testdata/ed25519.pem", }, - "ok (fallback used)": { - privKey: keyAlternative, - fallbackPrivKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + - "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + - "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + - "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + - "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + - "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + - "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + - "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + - "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + - "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + - "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + - "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + - "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + - "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + - "6Wol2S-I8ag", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - Audience: "Mender", - ExpiresAt: Time{ - Time: time.Unix(4147483647, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Issuer: "Mender", - NotBefore: Time{ - Time: time.Unix(12345678, 0), - }, - Scope: "mender.*", - }, - }, + "ko": { + privateKeyPath: "./testdata/doesnotexist.pem", + err: errors.New("failed to read private key: open ./testdata/doesnotexist.pem: no such file or directory"), }, - "error - token invalid": { - privKey: key, - - inToken: "1234123412341234", - - outToken: Token{}, - outErr: ErrTokenInvalid, + "unknown priate key type": { + privateKeyPath: "./testdata/dsa.pem", + err: errors.New("unsupported server private key type"), }, } for name, tc := range testCases { - t.Logf("test case: %s", name) - jwtHandler := NewJWTHandlerRS256(tc.privKey, tc.fallbackPrivKey) - - token, err := jwtHandler.FromJWT(tc.inToken) - if tc.outErr == nil { - assert.NoError(t, err) - assert.Equal(t, tc.outToken, *token) - } else { - assert.EqualError(t, tc.outErr, err.Error()) - } - } -} - -func TestJWTHandlerRS256Validate(t *testing.T) { - - key := loadPrivKey("./testdata/private.pem", t) - keyAlternative := loadPrivKey("./testdata/private_alternative.pem", t) - - testCases := map[string]struct { - privKey *rsa.PrivateKey - fallbackPrivKey *rsa.PrivateKey - - inToken string - - outErr error - }{ - "ok (all claims)": { - privKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + - "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + - "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + - "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + - "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + - "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + - "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + - "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + - "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + - "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + - "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + - "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + - "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + - "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + - "6Wol2S-I8ag", - }, - "ok (some claims)": { - privKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH" + - "AiOjQxNDc0ODM2NDcsImp0aSI6ImI5NDc1MzM2LWRkZT" + - "YtNTQ5Ny04MDQ0LTUxYWE5ZGRjMDJmOCIsImlhdCI6MT" + - "IzNDU2NywiaXNzIjoiTWVuZGVyIiwic3ViIjoiYmNhOT" + - "VhZGItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Ii" + - "wic2NwIjoibWVuZGVyLnVzZXJzLmluaXRpYWwuY3JlYX" + - "RlIn0.qzW1QfnvfB384DfOyX6LC4jsTSVEWwsyb-vSeA" + - "ebfHdJquX2BfQ6_1ZGtqyCC7mOhMrXeJv1gmprpkOxKw" + - "hPBexS-U1gOc_aO7Oi7uPl1HQRhMw9SM2QamOOVGmLi5" + - "1uVg9ZEQhvnN7s-w4girnmGyhnPWV58CorJtW4t1Dgyr" + - "6fG_v8wtrGt-rMb7uMLmEQMjIqcUBa6mlU1sVBEPTeGb" + - "KvR6kSJ727UW91y7krTcQUdNN4rv2CfG7ETlPsrUgMvr" + - "GUPqoq_ygbLX3kDZveVzTE2CQdI7PpAO14UZQxRBfff5" + - "ewyW4P0ulYRj0mPF5NmsHwbADoAjILoA5uSWW9Dg", - }, - "ok (fallback not used)": { - privKey: key, - fallbackPrivKey: keyAlternative, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + - "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + - "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + - "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + - "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + - "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + - "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + - "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + - "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + - "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + - "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + - "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + - "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + - "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + - "6Wol2S-I8ag", - }, - "ok (fallback used)": { - privKey: keyAlternative, - fallbackPrivKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + - "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + - "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + - "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + - "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + - "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + - "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + - "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + - "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + - "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + - "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + - "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + - "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + - "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + - "6Wol2S-I8ag", - }, - "error - bad claims": { - privKey: keyAlternative, - fallbackPrivKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGk" + - "iOm51bGwsInN1YiI6ImJjYTk1YWRiLWI1ZjEtNTY0Zi0" + - "5NmE3LTYzNTVjNTJkMWZhNyIsImV4cCI6MTY5NjQxNjU" + - "2NCwiaWF0IjotNjIxMzU1OTY4MDAsIm5iZiI6LTYyMTM" + - "1NTk2ODAwLCJtZW5kZXIudHJpYWwiOmZhbHNlfQ.LNRO" + - "1CzYVkOqU_-ikNva-MvFyvZTVLbR8irmecbPKPij-6cv" + - "h_DymOdbtupRCpABq2XFfLGAz68AOqGhc0Utp_AL-EY7" + - "kSH-QbPVdlFvnZO_T-gPHxOY2wNoZqnyusr-cpiRR413" + - "lySS5t5ZPsghFtlCSFHITdZ11sin79C1JJxd3cUnhjXj" + - "P-wL7YJmsfFR9KfSL4AEPtpDsQ98gPhcnqPRCBuLSFcU" + - "d3_w-pbc7PkbM0A_nO2jrwCJCaHvjMMvL9FHIZ2-xfUW" + - "qDB13KkPo0BrVwHLvhykLlCuhshNaNugzH0Tb4djrM__" + - "NCKofdozu3DowLLjesXp7oIYWRAKUQ", - - outErr: errors.New("jwt: token invalid"), - }, - "error - bad signature": { - privKey: keyAlternative, - fallbackPrivKey: key, - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdW" + - "QiOiJNZW5kZXIiLCJleHAiOjQxNDc0ODM2NDcsImp0aS" + - "I6ImI5NDc1MzM2LWRkZTYtNTQ5Ny04MDQ0LTUxYWE5ZG" + - "RjMDJmOCIsImlhdCI6MTIzNDU2NywiaXNzIjoiTWVuZG" + - "VyIiwibmJmIjoxMjM0NTY3OCwic3ViIjoiYmNhOTVhZG" + - "ItYjVmMS01NjRmLTk2YTctNjM1NWM1MmQxZmE3Iiwic2" + - "NwIjoibWVuZGVyLioifQ.bEvw5q8Ohf_3DOw77EDeOTq" + - "99_JKUDz1YhCpJ5NaKPtMGmTksZIDoc6vk_lFyrPWzXm" + - "lmbiCB8bEYI2-QGe2XwTnCkWm8YPxTFJw3UriZLt-5Pw" + - "cEBDPG8FqTMtFaRjcbH-E7W7m_KT_Tm6fm93Vvqv_z6a" + - "JiCOL7e16sLC0DQCJ2nZ4OleztNDkP4rCOgtBuSbhOaR" + - "E_zhSsLf2Dj4Dlt5DVqDd8kqUBmA9-Sn9m5BeCUs023_" + - "W4FWOH4NJpqyxjO0jXGoncvZu0AYPqHSbJ9J6Oucvc4y" + - "lpbrCHN4diQ39s2egWzRbrSORsr-IL3hb1PZTINzLlQE" + - "6Wol2S-I8XX", - outErr: ErrTokenInvalid, - }, - "error - token invalid": { - privKey: key, - - inToken: "1234123412341234", - - outErr: errors.New("token contains an invalid number of segments"), - }, - } - - for name, tc := range testCases { - t.Logf("test case: %s", name) - jwtHandler := NewJWTHandlerRS256(tc.privKey, tc.fallbackPrivKey) - - err := jwtHandler.Validate(tc.inToken) - if tc.outErr == nil { - assert.NoError(t, err) - } else { - assert.Error(t, err) - assert.EqualError(t, tc.outErr, err.Error()) - } - } -} - -func loadPrivKey(path string, t *testing.T) *rsa.PrivateKey { - key, err := keys.LoadRSAPrivate(path) - if err != nil { - t.Fatalf("failed to load key: %v", err) - } - - return key -} - -func parseGeneratedTokenRS256(t *testing.T, token string, key *rsa.PrivateKey) *jwtgo.Token { - tokenParsed, err := jwtgo.Parse(token, func(token *jwtgo.Token) (interface{}, error) { - if _, ok := token.Method.(*jwtgo.SigningMethodRSA); !ok { - return nil, errors.New("Unexpected signing method: " + token.Method.Alg()) - } - return &key.PublicKey, nil - }) - - if err != nil { - t.Fatalf("can't parse token: %s", err.Error()) + t.Run(name, func(t *testing.T) { + _, err := NewJWTHandler(tc.privateKeyPath) + if tc.err != nil { + assert.EqualError(t, err, tc.err.Error()) + } else { + assert.NoError(t, err) + } + }) } - - return tokenParsed } diff --git a/jwt/testdata/dsa.pem b/jwt/testdata/dsa.pem new file mode 100644 index 000000000..363dd17ce --- /dev/null +++ b/jwt/testdata/dsa.pem @@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBvAIBAAKBgQDaqdgwD3YvYwgbWzs8RQQOm8RmPztSYMUrcM7KQtdJ111sTZ/x +VAq84frCt/TEupAN5hUFkC+bpJ/diZixQgPvLKo6FVtBKy97HSpuZT8n2pUYZ9/4 +sBTR5YQtP9qExXUYO/yR+fZ+RE9w0TbSAtHW2YZHKnoowJAHdoEGMbaChQIVAK/q +iXNHCha4xHnIdD2jT0OUs03fAoGBAMnCeTgO09r2GquRAQmGFAT/6IGMhux7KOC8 +QrW7jDaqAYLiuA45E3Ira584RF2rg0VhewxcdEMbqNzqCeSKk9OAmwXpJ1J8vCUR +dRojGz0DYZHJbcspoGtZF1IF6Z3BoaggRcLX6/KYLbnzFZnBXV/+//gRTbm/V2ie +BzCWE/qEAoGBANbrGxzVTTdTD8MaVtlOpjU3RqoGFHmFCd4lv0PIt2mjFsXO3Dt/ +6BMtJVREtb74WF0SUGmnpy6FTYoDb05j2LhH1IvCSkFT5hUK0WtAJ3NidJ6ARxxD +z2QITWI1FTr1K9NbZdR6DoTxeKfV6wWbuLywlwoWYmLe6oAmq21Oft4XAhRcKcLk +r2R/Rn1uchUL8ru0B2OVkg== +-----END DSA PRIVATE KEY----- diff --git a/jwt/testdata/ed25519.pem b/jwt/testdata/ed25519.pem new file mode 100644 index 000000000..98b23e9bc --- /dev/null +++ b/jwt/testdata/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIG1ZSPHHBCWnpD1hZAsEMxMemPK26E6EbxxXgsA5M/9H +-----END PRIVATE KEY----- diff --git a/jwt/testdata/private_alternative.pem b/jwt/testdata/private_alternative.pem deleted file mode 100644 index 9536217e5..000000000 --- a/jwt/testdata/private_alternative.pem +++ /dev/null @@ -1,39 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIG5AIBAAKCAYEA4YHYk50VfCzoW+9soo4cW4dM3WXT8Pz9FtAd9+CwF1BL4hjb -Ql6hdEFU2Y7Pr8RNBcH5p/N+G+m2rExBLVV7/p3UhTf4nIzLlp0Lo69e4oAiOi/o -xWcjgYQqcnJtDjt+FTftkhcd20k115Qif922Hs7Wf+VFzCdQhH/qJXnpXz5TKsOh -wINzkAwPGrq9YNBKTdKDUgk2NzMT8aMrfbHLPEAEfm5e76hoQQoH0VfAD1jkeH7Y -xLiDjGQMmOLlwt3slZDF0b+e5rJq5VEO2nKRFiN2XWY48y+RVeid7aqH0H3af9Ac -DPNBoQcR+k/HIAWXp6T42fqpuip3kq+uJaLUaqlnpLk22sczlyOQHoH0og0yFESU -LlGSK9UPOemal6dpRJ8fgBapVZVlpfv17eoachHMyYqrwhN0dWNCr/h5CH2VBq6r -bvVu5Km2DfXYJLXbeaxQWR6+73SWvnXxHNedX2o4PRsgVvd5vFAPP93Xq3prgMvn -fY1Zogd95xUBnHilAgMBAAECggGAWd3J9kGCX2LyEjkevMUPTfZkTlKXHJFlNqMn -XjH/WrsYEnk/X3qgstiaPLzpzSzYWPQNTr39QphnwM/+pVdMu5IgMBq3C4QcWeAp -1Eg98tuHSFLxW0GX57RIyiUJisNwonoQRcjKkCnjuA6DlRtAyZQ57OvsXchJERR8 -tgSYWOlYcVFW84YvFneHyWojsCIxikPbK2UaYvMK/p+85DFnQZJteAj58l3Vdh3O -McHbewI8wjwinHq/icIRNxgs4fRX5VxwclpZou4Uj298b4k2e0dgKDrFvw+LemXx -mEX8QMYBJWSY9F21i8Aj9U3snxyPZIqPWHDL0/89V2epVZsV8OY8AUiYZGqxU/Lz -ubzMV+hQ2fgjd3GFgPbRIRphefv69U61HS3CSMbIxo+v6TiEJrvb+xb97s2bzH4I -/vwrxnw4IkUCHZsDhVjEua9g2ZD0meieeEJvCXo9sYfidESzIXGmOBOzYKWj59Tx -vGFhmqceaif3IN9quemfDVEhS/C9AoHBAPJMGsIicIwoYhD8SH9Q5mazduvFJL/d -3DyU7FuYl8ZmaDCNISA9ksiO1cny6ggfjPK1vM3cqnWzyMBOC3LSOh4oQlrvSGJo -4WTBNdT0ilQLAiklGYDAJWwcOpZQbKakbu3jw/2cyewRgP5dlBGra2CsM3UrJ3T4 -2YKQxmn2NCqNPiMNvShaK4yrrcBXiylrJta8BtcCfq2RTmDULGCxGKQdUGHGfmTK -7m0OPtngf4v5Fzu4vuSzqhm4Qqhs8N6SiwKBwQDuQqkhzHxgDPwo/a9XL0qZ0Mnr -NGTh+pxoe+5cWWOvsdHYo8qj376Sx7uIYF+d9Zo/NmEAQ96ebRVYSX7jlfEio2KY -Tp18TtsgBa/10XXTM7qk3KyLi5fVfTu32ClmR2CKuaXUc6nZOnryNSwbWKLSn912 -ztahbPTGAkaCUbeI3e9V6MamYYme78oTS5DzFGGLvqzfbUCP5kpMipE0QN9NFqAR -u5Q6wgofsvQYIX6L97AuT45Vhki0rhEJqjS+d48CgcEAykmFkYzlP+67KQknkbu+ -EAJHB8I400LsC1weFL7KACchV1+MR3mAoZa2oHsArrWAnGXkck6HteracMr3ve4L -/X+JHphla1u6yb6RLfDXyCDEtgs2Roqk86SBuVr9ywa/YjblO0h6QG/ArRz9cN8X -r8c00t6re3oDRoGOVYrqtJflR6jBwv29HB1823d7RKIj8+VZJIr9rfUBZc/eGP/h -3apnoKnyfj36XuwkxwwqEHdu0TqFjTT+j6iJZkpFstzPAoHAdZkLV7ZRMStXjnfg -Al1CKfdGokPDA1KpvlxlAsnQDRjsZUw6qXW230WcByziJNnjXw3dBL968qEOpvvn -mDcaxKj18gzjaEaXmw+TkPXofO0MU3Dj7SBiOapNPml/zY6vVOlasyi/jYtywXeD -JKA0BVeIfKqMfzKDAfR/jrT8rJ42EiYJXnfH8F2UVL+4dHH7z3i2ZLTM5/mXdH30 -O2FFlgvzx6zY9cNuMcUm+34Ussr293Sm4QmdeyhrGmUbEWN7AoHBAIz5NkMpGkra -qBKC5gXcIXgMdxMel/uMThfsp+NPsd94gLo1zfKDY9C1a2vzK4oL6SQp20RkbOQc -EN63DB+7cCqZKk0FdctQRI7yaggmbRtpknzWWA/OQDhzyrSXSdaQ3q9VZrakEbuL -m5WEfuaP01vX5ilzeMQQjStGJPYCzBsbAfZPMnBji5/JzKSS3gFo4+D+szyKZVWk -LbLWagvQr6YEAVWuDGpCphSGuaPM3+yo/LwUv9ZSB3984QCvtMlJ0w== ------END RSA PRIVATE KEY----- diff --git a/jwt/testdata/private.pem b/jwt/testdata/rsa.pem similarity index 100% rename from jwt/testdata/private.pem rename to jwt/testdata/rsa.pem diff --git a/jwt/testdata/rsa_pkcs8.pem b/jwt/testdata/rsa_pkcs8.pem new file mode 100644 index 000000000..ba0d407a4 --- /dev/null +++ b/jwt/testdata/rsa_pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCc8AY24055iFqf +ozlNDkK9BvpPw2McT/lJWJia2vhy7CnXMKZDEuQEmma0WFbpxLcUsD+uh/18OCjw +k3kl1eT11tuF7n0zKebq0OD4tbLWSh5Zww7cziDErCgkL+DRoCeHjwwKdOMaxpPs +BPe6V1oR537Ay4Bgze0nleFm0BPBsNxtpn/AG9BPVjZthK0A8lLYKOE0aOsg35gt +NVQYZjnE+2O9WVBxblFPYu+1E7NQ0iqFi9jH7ood+mU6FWoHoxF7XBkSLAIiYhVt +oy3I+3aueW2etr01Jd5YdcsKCmL3UnrxhFnZF8dbWxoGy0dmGIQQVJANH4hp+NXo +mrQD2/LVAgMBAAECggEAF7A6O+g5FOqiRTThxhIIPFiUiVwkdmZS6kGLfVpehJhF +o4PmILs1/ETjCkdITS5idSa6YgmIV0rx0Qhtkaq0ze/RwGhUcRfa0WhYgOoKNcLH +zIQ/FtCtTQpwX6/zZyjdtp4+sshcbFL7inVggDnFsGypKwg8l0AWEzLSLE7toH9p +LP47HAJ9WWFyZ3BzibUjLEiMlK/w8UTDAv5IUT3dkSEe6ztTAwbZoZ3r+CJyhDUA +U7VvJQwhh/vXTBiOw75PKFykz1Bp2P5ZuGW21DVYhRQcyq9NegPhTepHt/cSQysC +y51J9mIFrp2M4oTBpMw694N9Ozk3kVZRoeiUzl4m8QKBgQDb69bc2I8qO6OTrHK7 +/Xw/9Wghts/R+GdnSLpzIeZI1zUQvR1LvJDRxOfY+kFw+94tADOa66vPBYTn2F4E +Mo0CZNIEnkdyUvUy0MFoefbNBhHAHo9arilTq1DbCG81Di+GP7iRyo+3y01stTqz +TsPGuvk2ywcNALd1ZBUMgODUpQKBgQC2rwYaKTHr0Tu3EMfZ1bw4smEuUtbSis9v +K3iS8AMr1e3QaDqQGARiME+iz3zcijYXKVARVhj8ebiu8qKdPNbJqDq+bRFmLhkT +10XTT6XRPCnOBcotk9HTrezIj4Fb+1XSV/SXJdRN/eqB9pp6unzJZkuxLRIGNFex +HIdlRqvecQKBgAx60qOPqngkEEFGDPC8Drv2aiVXoW1x4jRLPUFhUBccF0fO44Wz +uqgcu2dltCb8M/xrwYHuE77YulUJwzQLxlK3c++NJ9LGAGIU1JTgLvAtgv5a/ZmQ +vomf9COp092341yD6y5ix0sPv2IG2sDoHFX/sDq6xLipLL/9oPAntBp9AoGAMo0W +HDEgDkg0xQCQvNenIO1DdQUZSuN8aR/XWpmt1vh4uT3OTsdGl0EVGFFgFMruEtSs +wk9X1K1+DHM5ylbmfKDfuIgH04WYDOR5/vJASTjjvI3fl2MbIf8z0X/cZO6UngMW +vKiMKhTESrhJoQJvu29iLKHzJeJgDbN+R+kZcBECgYBfMy025YhOJArPtmzFrHuN +IwVasR+3iuDTr7WMIR130vAvY12PaYR5+NbMt1mNkV9O+MZtKCdCvphCtf/kEpXv +v9cqQKUxh/YrI0nvysc7GoQWtZKYip9xa0kjVyhLeZ8Xs7M1CXok4rXcDxCQPPbz +ugFPykXGUqka4p+VEoEKgw== +-----END PRIVATE KEY----- diff --git a/keys/key.go b/keys/key.go deleted file mode 100644 index 28e29acf0..000000000 --- a/keys/key.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2023 Northern.tech AS -// -// 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 keys - -import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "io/ioutil" - "os" - - "github.com/pkg/errors" -) - -const ( - ErrMsgPrivKeyReadFailed = "failed to read server private key file" - ErrMsgPrivKeyNotPEMEncoded = "server private key not PEM-encoded" - - blockTypePKCS1 = "RSA PRIVATE KEY" - blockTypePKCS8 = "PRIVATE KEY" -) - -func LoadRSAPrivate(privKeyPath string) (*rsa.PrivateKey, error) { - var ( - err error - pemData []byte - rsaKey *rsa.PrivateKey - ) - // read key from file - pemData, err = ioutil.ReadFile(privKeyPath) - if err != nil { - return nil, errors.Wrap(err, ErrMsgPrivKeyReadFailed) - } - // decode pem key - block, _ := pem.Decode(pemData) - if block == nil { - return nil, &os.PathError{ - Err: errors.New(ErrMsgPrivKeyNotPEMEncoded), - Op: "PEMDecode", - Path: privKeyPath, - } - } - - switch block.Type { - case blockTypePKCS1: - rsaKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) - case blockTypePKCS8: - var ( - key interface{} - ok bool - ) - key, err = x509.ParsePKCS8PrivateKey(block.Bytes) - if rsaKey, ok = key.(*rsa.PrivateKey); !ok || rsaKey == nil { - err = errors.New("key type not supported") - } - default: - err = errors.Errorf("invalid PEM block header: %s", block.Type) - } - if err != nil { - err = &os.PathError{ - Err: err, - Op: "LoadRSAPrivate", - Path: privKeyPath, - } - } - return rsaKey, err -} diff --git a/keys/key_test.go b/keys/key_test.go deleted file mode 100644 index 12c6c206d..000000000 --- a/keys/key_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2023 Northern.tech AS -// -// 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 keys - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLoadRsaPrivateKey(t *testing.T) { - t.Parallel() - - testCases := []struct { - PrivateKey string - Error string - }{ - { - PrivateKey: PrivateKeyRSAPKCS1, - Error: "", - }, - { - PrivateKey: PrivateKeyRSA, - Error: "", - }, - { - PrivateKey: PrivateKeyECDSAP521, - Error: "key type not supported", - }, - { - PrivateKey: "randomGarbage", - Error: ErrMsgPrivKeyNotPEMEncoded, - }, - { - PrivateKey: `-----BEGIN PRIVATE KEY----- -randomPKCS8Garba ------END PRIVATE KEY-----`, - Error: "LoadRSAPrivate", - }, - { - PrivateKey: PublicKeyECDSAP521, - Error: "invalid PEM block header: PUBLIC KEY", - }, - } - - for i := range testCases { - tc := testCases[i] - t.Run(fmt.Sprintf("tc %d", i), func(t *testing.T) { - t.Parallel() - - fd, err := os.CreateTemp(t.TempDir(), "private*.pem") - if err != nil { - panic(err) - } - _, err = fd.Write([]byte(tc.PrivateKey)) - fd.Close() - if err != nil { - panic(err) - } - - key, err := LoadRSAPrivate(fd.Name()) - if tc.Error != "" { - assert.ErrorContains(t, err, tc.Error) - } else { - assert.NoError(t, err) - assert.NotNil(t, key) - } - }) - } - - t.Run("error/file not exist", func(t *testing.T) { - path := filepath.Join(t.TempDir(), "not-exist.pem") - _, err := LoadRSAPrivate(path) - var expected *os.PathError - assert.ErrorAs(t, err, &expected) - }) -} - -const ( - PrivateKeyECDSAP521 = `-----BEGIN PRIVATE KEY----- -MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIBb5dG63AsYEDyDzz2 -8NxEY/K2X4zqpQ2RkCbwn3vsXHsFDWQMQjT6+hFs1aPoHquYcYXi4q9TJwHwcXzp -4J6J/uGhgYkDgYYABABjebTZZu6l6Orhb6NKwQ1YsIsgTg5BFJXuBRnApl0cm7hq -4lP9yH3qsW+okIs+r3YktApw45js5T0JWEqhGX021QGaZtw2ezL7PROkWV5A/ihc -VmpBmV0lMDAStu+Vlj9g5oM8TphpTXF24VXDk8O8+Swwq+Sp1mpRjWI9AizzBPMq -bQ== ------END PRIVATE KEY-----` - PublicKeyECDSAP521 = `-----BEGIN PUBLIC KEY----- -MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAY3m02Wbupejq4W+jSsENWLCLIE4O -QRSV7gUZwKZdHJu4auJT/ch96rFvqJCLPq92JLQKcOOY7OU9CVhKoRl9NtUBmmbc -Nnsy+z0TpFleQP4oXFZqQZldJTAwErbvlZY/YOaDPE6YaU1xduFVw5PDvPksMKvk -qdZqUY1iPQIs8wTzKm0= ------END PUBLIC KEY-----` - PrivateKeyRSA = `-----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDvaEqOq1uOTrSj -uAVIBKwQ7gspal8nPL4mKEuw2rO0upV7PSFKrANv7qyWy6ZzxoxbkdyqZFSQH3wS -z6uJEado0BfH8lF2U3W/+LRj5dPyVs2DTjLNRqCC7klhk5s5jtosEcnYechPHWPm -ggM5iJe7sni0JxE88KUwHVDjEydzbvRXTQSp2ccX6fAyMAWpNQr7AWfy4rHoWsfc -APIW/2ai6ufs+PXbNurjaxoZMxawaR5QM7kVhNFlfOSVq7TxRmfkZZDvVSGc4as2 -g+clnlQsnM6C1UfercGwvkGfIIueUtN9SLZIgpuVTXLNswLOBjvOx/ESzFohmBUp -FgCnd+zxAgMBAAECggEAYuXhTtiI5NuskalWPS746bF8WOqBTlMwddDVm8Rs0i71 -y0gwdYljjhy5nT2ZkGAn4Tf7QURbDoKDHb4+LUxmrMyx1j5K2qeVj+0sj8wEZyrm -kOR/5f7UFeJb2/w+9mMFy4i5qjx8u/n3J+TzchP0ImySolE1NMhwZNTncjaaaHtT -w2HInbdnfuMX8AKX5OSsYl6upE99JU9Vd53JZophu02wf8hdeobWbn4akxl4YGlO -3ZXYfE4L9+wpvB/weV1Eof7zn0+a4cCjilSySWEnOiBb/ZUg+mEdJA/eMR0rHmex -KbqZbzrk6AWxPmn4FOpD2PspFAHGrJ6lIoaCdOZ/dQKBgQD+wCIFDJ3tFjhAONoG -A8RNeYbsicx2gWRdwnlMKcqdGgvgWoyd1FQReB0+1MOp1csihmAsaOCYo6vmhDvq -WFEpJhCxiztXsYkfilB6xbQ2LhvTsXaPlJBA6SMDJ2XgA+NFCE9iYNjeurTp1/aj -LyETbU7RFkO06TcuJiofljDkcwKBgQDwlOS0B0DaDRekinD/BH7O22J5MDyUtxjO -xv8J00LfvXhxuDhEhTbT4mMJgTyCk0jgsFEpRbpMdqjWH6R+NbE1tekFpazkgYd9 -z4HMLmic17XKOXWnledOaAkQB6USINp4GWLdGjiAeB3ELmmev91ZcfbU0d68LOo5 -ODMLaD50CwKBgBeJWttKkiDAh8vvNL2PhYh+7OdXx+s/Ay3idOCDj/O531UIKKvA -XVAL3+/ZKoa7ePwknCgePHn9zTkMCJkbNcxuduZgbcgpX/jpB4yATakf03RYlhKn -8Df/EjwNXM04rrvHC8aUGhVh/KsKSABFr3GjDMAmpXTGg1GhNw0aDERfAoGBANYa -a/6bheeIR0YzvqP1iDTnoRdhCkj/OaCsEETaMmWT5SCvZcP1GfovOxw2W3eJRA5S -W6hzWXy7DT6iIm3/spmuLpbL/rXNYJtilIz1sDwE7M/vmvltutBYXdhaNVmQy1ye -mxFSSH5sZ3E0LOMOtRrpBVYZADRPdJM/pI2+U/ZJAoGBANVYEY7eJDSwnGcJFvf4 -C4RrvNsyrQOLOF5+u5HAhWxrsRZXM+0I7Dqvid+/4yOVXY3Sdr6x0wOzjFMO1nxZ -vAodwAZdy5kWtFJjBOKruBAfkxxQ3dqTiofsm0Jp+h4SEnj/DsSLe3la2/BGXkhm -BWwgHX6PoC7FAdbJ3tILBxD2 ------END PRIVATE KEY-----` - PrivateKeyRSAPKCS1 = `-----BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEA2Bkw7DIJg8+7hQPHSWp1YditDQTn/aF5Uc8yNlzkcCyi4lpY -hc7rKQkzPKRqopNRGhSPKpjRxt4OQz4ZRsAlDjQ5HpbknEZKOkWLbf8M2mG4u2wF -a4mdDZo3+rXwPyYffSlTbmovbg0MKcbV8Pqb6YeW/nF1zX4FSNcSvVY1CDHRdnp2 -P16clWA4vUKSzOX6mxAuYjfuqE6SKtBDDQMtAWXqEvY1LYTBh0P3ny6VsHWhaecP -AmMJNyOkvXTHrrNI2cUSh+++ilbuTqTEi77Cw7jLPVcJr+IodcCER1oZ+9oyliLD -pdYNOlM26WGM2Ul48KH4ToAGRvJhfQjkla5/owIDAQABAoIBADCYqanULtOXmaH2 -EZDvAeq5IWF2Iv2knHXLVI1pIm4fe5nPm2yr9bJKwVz31Isu+eQVj4SSzUodkbOJ -eYGxoCOrltTMNij2naaxEQPxgWBy7WoohqeCUPFIJyKYW6i32Aj7jCmec4AaKwwS -DPaeRQWlWk1qEoXduy6AP1SY2GA4+f8StG3+xMOWUlo3H1qIWUKwAcQnW5q2XWiF -0q2Nayuz/D17xG8IPVx/T3ZFA1t7P8Y5gjM0LHwf8GWu0KcAZaK2myrF67O3zhQe -7QSmMjiyKNymE/rUuLH+apx6HBCkpuvHVjbqG4strt2mUYsvmp6peyMPupqIm4Kl -xnq1CeECgYEA8hAzk7XD+ZwxxTCOvvq0tJqncaLQeW3HZMKo4nqNhnrjN21NW8Gg -xQ6wh+9uH4WHLHRLp0HNP/z9jLmFcHQygebcHDTQrwLGgAAy+nnxIh40cNWdR4B/ -dJA6GxWkapgQw84byDXnJROYJRede4+ITlR0KOfPzERXTHfo5jXM0JsCgYEA5IpK -TE8dl/1DDaXCWzWW0orKslKl49/tIzZrV8JLiGvGehCOCAdgLOLMsE96iUQka9T2 -us2120ltVjTuo95RulqJHcP5Hb6Z4jxK7ho5V28AzMpWJc3Sto8kS1Y+abAiGNyN -4NJqeDI+P2ATW8eQOJIsCrpT28Fyb3Ylov/nKZkCgYAKnX9FiQERHzJnjVuVMHVg -Pi/9ocA2swO9fXPeirVOInF4asirr3AXdC91pqBTrY1h+6+dpBsWJUgRNcmORuo4 -HCGm8wH7ysldr6SMq3BRqLVwBU4iZpYwTGrf6TEOo6CIla9ONl7ul09iwQhc9Mxr -cvStHo1UTeLuLYv/HHjg5QKBgE2X8k/kUKzo7Ro2HD3xfOqw+s7+ppouzgm1kU5z -hkekJ/gLpN1u+6Vhv5Ng+L6gJymBXd/gtgzk6j1prVhvxBncYU982RjTPNYGGH6s -4qkf5Aqj7Anbzt3yzaTSfFBP39PHFlituD5k+KN10DzKDdpXLqLZzlz/WgYj+/VS -oz6JAoGAUaG6yncXpdmZmDfGhxwQtfZ5saJoj/KooKrnwerxoItDsUYLeIGmwDgV -yLMn8dwhm2xg8cjYsdR2uxEiBhGtA0VH6PnAnOU5Bnw4haOWiZ6D4zh7PAGh5jAW -j/ZC1+dPF08FixsQbPxEtTYZe9/9cvhbB0iVg5ir6X2Y7EfW+LY= ------END RSA PRIVATE KEY-----` -) diff --git a/server.go b/server.go index 97161dab3..11a8d5c19 100644 --- a/server.go +++ b/server.go @@ -1,4 +1,4 @@ -// Copyright 2021 Northern.tech AS +// Copyright 2023 Northern.tech AS // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ package main import ( - "crypto/rsa" "net/http" "time" @@ -31,7 +30,6 @@ import ( dconfig "github.com/mendersoftware/deviceauth/config" "github.com/mendersoftware/deviceauth/devauth" "github.com/mendersoftware/deviceauth/jwt" - "github.com/mendersoftware/deviceauth/keys" "github.com/mendersoftware/deviceauth/store/mongo" ) @@ -55,20 +53,6 @@ func RunServer(c config.Reader) error { l := log.New(log.Ctx{}) - privKey, err := keys.LoadRSAPrivate(c.GetString(dconfig.SettingServerPrivKeyPath)) - if err != nil { - return errors.Wrap(err, "failed to read rsa private key") - } - - fallbackPrivKeyPath := c.GetString(dconfig.SettingServerFallbackPrivKeyPath) - var fallbackPrivKey *rsa.PrivateKey - if fallbackPrivKeyPath != "" { - fallbackPrivKey, err = keys.LoadRSAPrivate(fallbackPrivKeyPath) - if err != nil { - return errors.Wrap(err, "failed to read fallback rsa private key") - } - } - db, err := mongo.NewDataStoreMongo( mongo.DataStoreMongoConfig{ ConnectionString: c.GetString(dconfig.SettingDb), @@ -83,7 +67,19 @@ func RunServer(c config.Reader) error { return errors.Wrap(err, "database connection failed") } - jwtHandler := jwt.NewJWTHandlerRS256(privKey, fallbackPrivKey) + jwtHandler, err := jwt.NewJWTHandler( + c.GetString(dconfig.SettingServerPrivKeyPath), + ) + var jwtFallbackHandler jwt.Handler + fallback := c.GetString(dconfig.SettingServerFallbackPrivKeyPath) + if err == nil && fallback != "" { + jwtFallbackHandler, err = jwt.NewJWTHandler( + fallback, + ) + } + if err != nil { + return err + } orchClientConf := orchestrator.Config{ OrchestratorAddr: c.GetString(dconfig.SettingOrchestratorAddr), @@ -104,6 +100,10 @@ func RunServer(c config.Reader) error { tenantadmAddr != "", }) + if jwtFallbackHandler != nil { + devauth = devauth.WithJWTFallbackHandler(jwtFallbackHandler) + } + if tenantadmAddr != "" { tc := tenant.NewClient(tenant.Config{ TenantAdmAddr: tenantadmAddr,