diff --git a/api/http/api_useradm.go b/api/http/api_useradm.go index 7efb8f95..c8293bf9 100644 --- a/api/http/api_useradm.go +++ b/api/http/api_useradm.go @@ -76,7 +76,7 @@ var ( type UserAdmApiHandlers struct { userAdm useradm.App db store.DataStore - jwth *jwt.JWTHandlerRS256 + jwth jwt.Handler config Config } @@ -89,7 +89,7 @@ type Config struct { func NewUserAdmApiHandlers( userAdm useradm.App, db store.DataStore, - jwth *jwt.JWTHandlerRS256, + jwth jwt.Handler, config Config, ) ApiHandler { return &UserAdmApiHandlers{ diff --git a/api/http/api_useradm_test.go b/api/http/api_useradm_test.go index b302365d..1435dbfe 100644 --- a/api/http/api_useradm_test.go +++ b/api/http/api_useradm_test.go @@ -16,11 +16,14 @@ package http import ( "bytes" "context" + "crypto/x509" "encoding/json" + "encoding/pem" "fmt" "io/ioutil" "net/http" "net/url" + "os" "strings" "testing" "time" @@ -41,7 +44,6 @@ import ( "github.com/mendersoftware/useradm/authz" mauthz "github.com/mendersoftware/useradm/authz/mocks" "github.com/mendersoftware/useradm/jwt" - "github.com/mendersoftware/useradm/keys" "github.com/mendersoftware/useradm/model" "github.com/mendersoftware/useradm/store" mstore "github.com/mendersoftware/useradm/store/mocks" @@ -856,11 +858,16 @@ func TestUpdateUser(t *testing.T) { func makeMockApiHandler(t *testing.T, uadm useradm.App, db store.DataStore) http.Handler { // JWT handler - privkey, err := keys.LoadRSAPrivate("../../crypto/private.pem") - if !assert.NoError(t, err) { + data, err := os.ReadFile("../../crypto/private.pem") + if err != nil { t.Fatalf("faied to load private key: %v", err) } - jwth := jwt.NewJWTHandlerRS256(privkey, nil) + + block, _ := pem.Decode(data) + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + assert.NoError(t, err) + + jwth := jwt.NewJWTHandlerRS256(key) // API handler handlers := NewUserAdmApiHandlers(uadm, db, jwth, Config{}) diff --git a/authz/authz.go b/authz/authz.go index e4ddddf0..7abe82cb 100644 --- a/authz/authz.go +++ b/authz/authz.go @@ -1,16 +1,16 @@ -// 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. -// You may obtain a copy of the License at +// 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 +// 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. +// 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 authz import ( @@ -30,6 +30,7 @@ var ( // Authorizer defines the interface for checking the permissions of a given user(token) vs an action // on a resource. +// //go:generate ../utils/mockgen.sh type Authorizer interface { // Authorize checks if the given user (identified by token) has permissions to an action on a diff --git a/authz/middleware.go b/authz/middleware.go index 1696667b..a0b03223 100644 --- a/authz/middleware.go +++ b/authz/middleware.go @@ -1,16 +1,16 @@ -// 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. -// You may obtain a copy of the License at +// 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 +// 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. +// 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 authz import ( @@ -33,9 +33,10 @@ const ( // It retrieves the token + requested resource and action, and delegates the authz check to an // Authorizer. type AuthzMiddleware struct { - Authz Authorizer - ResFunc ResourceActionExtractor - JWTHandler jwt.Handler + Authz Authorizer + ResFunc ResourceActionExtractor + JWTHandler jwt.Handler + JWTFallbackHandler jwt.Handler } // Action combines info about the requested resourd + http method. @@ -61,6 +62,9 @@ func (mw *AuthzMiddleware) MiddlewareFunc(h rest.HandlerFunc) rest.HandlerFunc { // parse token, insert into env token, err := mw.JWTHandler.FromJWT(tokstr) + if err != nil && mw.JWTFallbackHandler != nil { + token, err = mw.JWTFallbackHandler.FromJWT(tokstr) + } if err != nil { rest_utils.RestErrWithLog(w, r, l, ErrAuthzTokenInvalid, http.StatusUnauthorized) return diff --git a/authz/middleware_test.go b/authz/middleware_test.go index cbed75d5..f706da9f 100644 --- a/authz/middleware_test.go +++ b/authz/middleware_test.go @@ -18,8 +18,9 @@ import ( "crypto/rsa" "crypto/x509" "encoding/pem" - "io/ioutil" + "io" "net/http" + "os" "testing" "time" @@ -304,7 +305,7 @@ func TestAuthzMiddleware(t *testing.T) { api := rest.NewApi() api.Use( &requestlog.RequestLogMiddleware{ - BaseLogger: &logrus.Logger{Out: ioutil.Discard}, + BaseLogger: &logrus.Logger{Out: io.Discard}, }, &requestid.RequestIdMiddleware{}, ) @@ -330,7 +331,7 @@ func TestAuthzMiddleware(t *testing.T) { //finish setting up the middleware privkey := loadPrivKey("../crypto/private.pem", t) - jwth := jwt.NewJWTHandlerRS256(privkey, nil) + jwth := jwt.NewJWTHandlerRS256(privkey) mw := AuthzMiddleware{ Authz: a, ResFunc: resfunc, @@ -365,7 +366,7 @@ func restError(status string) map[string]interface{} { } func loadPrivKey(path string, t *testing.T) *rsa.PrivateKey { - pem_data, err := ioutil.ReadFile(path) + pem_data, err := os.ReadFile(path) if err != nil { t.FailNow() } diff --git a/config/config.go b/config/config.go index 816a5aaf..a5c3c432 100644 --- a/config/config.go +++ b/config/config.go @@ -25,8 +25,8 @@ const ( SettingMiddleware = "middleware" SettingMiddlewareDefault = "prod" - SettingPrivKeyPath = "server_priv_key_path" - SettingPrivKeyPathDefault = "/etc/useradm/rsa/private.pem" + SettingServerPrivKeyPath = "server_priv_key_path" + SettingServerPrivKeyPathDefault = "/etc/useradm/rsa/private.pem" SettingServerFallbackPrivKeyPath = "server_fallback_priv_key_path" SettingServerFallbackPrivKeyPathDefault = "" @@ -72,7 +72,7 @@ var ( ConfigDefaults = []config.Default{ {Key: SettingListen, Value: SettingListenDefault}, {Key: SettingMiddleware, Value: SettingMiddlewareDefault}, - {Key: SettingPrivKeyPath, Value: SettingPrivKeyPathDefault}, + {Key: SettingServerPrivKeyPath, Value: SettingServerPrivKeyPathDefault}, {Key: SettingServerFallbackPrivKeyPath, Value: SettingServerFallbackPrivKeyPathDefault}, {Key: SettingJWTIssuer, Value: SettingJWTIssuerDefault}, {Key: SettingJWTExpirationTimeout, Value: SettingJWTExpirationTimeoutDefault}, diff --git a/jwt/jwt.go b/jwt/jwt.go index 7fbe96e0..7117e405 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,77 +28,47 @@ var ( ErrTokenInvalid = errors.New("jwt: token invalid") ) -// JWTHandler jwt generator/verifier +const ( + pemHeaderPKCS1 = "RSA PRIVATE KEY" + pemHeaderPKCS8 = "PRIVATE KEY" +) + +// Handler jwt generator/verifier // //go:generate ../utils/mockgen.sh type Handler interface { ToJWT(t *Token) (string, error) - // FromJWT parses the token and does basic validity checks (Claims.Valid(). + // FromJWT parses the token and does basic validity checks (Claims.Valid()). // returns: // ErrTokenExpired when the token is valid but expired // ErrTokenInvalid when the token is invalid (malformed, missing required claims, etc.) FromJWT(string) (*Token, 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) { - 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 - } + 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") } - } - - // our Claims return Mender-specific validation errors - // go-jwt will wrap them in a generic ValidationError - unwrap and return directly - if err != nil { - err, ok := err.(*jwtgo.ValidationError) - if ok && err.Inner != nil { - return nil, err.Inner + 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") + } + switch v := key.(type) { + case *rsa.PrivateKey: + return NewJWTHandlerRS256(v), nil + case ed25519.PrivateKey: + return NewJWTHandlerEd25519(&v), nil } - return nil, err - } - - token := Token{} - - if claims, ok := jwttoken.Claims.(*Claims); ok && jwttoken.Valid { - token.Claims = *claims - return &token, nil } - return nil, ErrTokenInvalid + 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 00000000..29d683f2 --- /dev/null +++ b/jwt/jwt_ed25519.go @@ -0,0 +1,62 @@ +// 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) { + 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 + }, + ) + + if err == nil { + token := Token{} + if claims, ok := jwttoken.Claims.(*Claims); ok && jwttoken.Valid { + token.Claims = *claims + return &token, nil + } + } + + return nil, ErrTokenInvalid +} diff --git a/jwt/jwt_ed25519_test.go b/jwt/jwt_ed25519_test.go new file mode 100644 index 00000000..457f2807 --- /dev/null +++ b/jwt/jwt_ed25519_test.go @@ -0,0 +1,252 @@ +// 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-" + + "FCw", + + 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 - bad claims": { + privKey: key, + + inToken: "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJqdG" + + "kiOiJiOTQ3NTMzNi1kZGU2LTU0OTctODA0NC01MWFhOW" + + "RkYzAyZjgiLCJzdWIiOiJiY2E5NWFkYi1iNWYxLTU2NG" + + "YtOTZhNy02MzU1YzUyZDFmYTciLCJhdWQiOiJNZW5kZX" + + "IiLCJzY3AiOiJtZW5kZXIuKiIsImV4cCI6NDE0NzQ4Mz" + + "Y0NywiaWF0IjoxMjM0NTY3LCJuYmYiOjEyMzQ1Njc4LC" + + "JtZW5kZXIudHJpYWwiOmZhbHNlfQ.T4PVYJvRSusq7MZ" + + "5XaOo6mLW9GDKdqdWO8NUZOZZ-KJ69d1UDbKWFSs9PPx" + + "cNwS5a0j8iiTA6m6-YW0nEvLWAg", + + outErr: ErrTokenInvalid, + }, + "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", + + 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 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 00000000..1830a0e0 --- /dev/null +++ b/jwt/jwt_rsa.go @@ -0,0 +1,62 @@ +// 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) { + 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 + }, + ) + + if err == nil { + token := Token{} + if claims, ok := jwttoken.Claims.(*Claims); ok && jwttoken.Valid { + token.Claims = *claims + return &token, nil + } + } + + return nil, ErrTokenInvalid +} diff --git a/jwt/jwt_rsa_test.go b/jwt/jwt_rsa_test.go new file mode 100644 index 00000000..a6d4f16d --- /dev/null +++ b/jwt/jwt_rsa_test.go @@ -0,0 +1,266 @@ +// 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 - 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: ErrTokenInvalid, + }, + "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", + + 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 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 bd2a3095..8fd60c58 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -14,299 +14,44 @@ package jwt import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "io/ioutil" "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 := loadPrivKey("../crypto/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 - }{ - "ok": { - privKey: loadPrivKey("../crypto/private.pem", t), - claims: Claims{ - Issuer: "Mender", - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: &Time{ - Time: time.Now().Add(time.Hour), - }, - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - jwtHandler := NewJWTHandlerRS256(tc.privKey, nil) - - raw, err := jwtHandler.ToJWT(&Token{ - Claims: tc.claims, - }) - assert.NoError(t, err) - - _ = parseGeneratedTokenRS256(t, string(raw), tc.privKey) - }) - } -} - -func TestJWTHandlerRS256FromJWT(t *testing.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: loadPrivKey("../crypto/private.pem", t), - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH" + - "AiOjQ0ODE4OTM5MDAsImlzcyI6Ik1lbmRlciIsImF1ZC" + - "I6Ik1lbmRlciIsInN1YiI6ImJjYTk1YWRiLWI1ZjEtNT" + - "Y0Zi05NmE3LTYzNTVjNTJkMWZhNyIsInNjcCI6Im1lbm" + - "Rlci4qIiwiaWF0IjoxMjM0NTY3LCJqdGkiOiJiOTQ3NT" + - "MzNi1kZGU2LTU0OTctODA0NC01MWFhOWRkYzAyZjgifQ" + - ".xkL2V6nzPsJaLUezrZg-lSCqH5yrG0ee-79TuaDC7u9" + - "ty3btT1VhoGdgEmrGUkLRdOAxnY_KI9rNHAkxzuTj8ef" + - "p6hss8PKC6DHM_Ke_cZH0xRt2V0QjhhZT5QkGFjb60me" + - "iY5oMQdhXY1rtaFuAvMvPMSZ0Rs4Twy3tuWvws9sekIY" + - "GWyVV-EGOtheI8_lGXlPSUXc5_0aUJuUNoKyIDFK4Chp" + - "eYxjyL20U0GPtGPAEKQQkCBqlliBsu1Rdww3a7ephIIs" + - "Fu6A8BWJpT5hGpiQlKK2hu2MZ9wh94wbcZXJRtlE_BWz" + - "NLKjV0L1oiaeWKuMGOTQ4TYgKeifWRCm_nw", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: &Time{ - Time: time.Unix(4481893900, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Audience: "Mender", - Issuer: "Mender", - Scope: "mender.*", - }, - }, - }, - "ok (some claims)": { - privKey: loadPrivKey("../crypto/private.pem", t), - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdW" + - "IiOiJiY2E5NWFkYi1iNWYxLTU2NGYtOTZhNy02MzU1Yz" + - "UyZDFmYTciLCJqdGkiOiJiOTQ3NTMzNi1kZGU2LTU0OT" + - "ctODA0NC01MWFhOWRkYzAyZjgiLCJleHAiOjQ0ODE4OT" + - "M5MDAsImlzcyI6Ik1lbmRlciIsImF1ZCI6Ik1lbmRlci" + - "IsInNjcCI6Im1lbmRlci51c2Vycy5pbml0aWFsLmNyZW" + - "F0ZSIsImlhdCI6MTIzNDU2N30.rzvPALb8-p8PUblS1Q" + - "LdgWuhVXrZw_kv0xl_qY3OhbKaV1aN2sB8kEea5jdLX0" + - "ukrKqXD2v9rmqcGNi3pvXqy2zj1EJslHxtSx1BCxzLCB" + - "l5pu3MhFwTjSlhkyOSL_TTlexcWvw3WCFCnj7D1irwym" + - "idZPPTvYrq7Zw5WTb_3VcCzf8xrPaXNlaHhIBH265RMW" + - "_s-9W8R20aFeMHLCNYvsF358sJAXrLI2_NhQlIW_PHy9" + - "08Tx8F8-GxXqK2vMxa1XPHi_Wr9HScsfz0_6dNtaq8AS" + - "TCaIibmnTUGe2UYg4xeO66bjKQbsuDXZP_ChOLNzYNmw" + - "mNfRHRHDT-nnsOWg", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: &Time{ - Time: time.Unix(4481893900, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Issuer: "Mender", - Audience: "Mender", - Scope: "mender.users.initial.create", - }, - }, + "ok, pkcs1, rsa": { + privateKeyPath: "./testdata/rsa.pem", }, - "ok (some claims w. tenant_token)": { - privKey: loadPrivKey("../crypto/private.pem", t), - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdW" + - "IiOiJiY2E5NWFkYi1iNWYxLTU2NGYtOTZhNy02MzU1Yz" + - "UyZDFmYTciLCJqdGkiOiJiOTQ3NTMzNi1kZGU2LTU0OT" + - "ctODA0NC01MWFhOWRkYzAyZjgiLCJleHAiOjQ0ODE4OT" + - "M5MDAsImlzcyI6Ik1lbmRlciIsInNjcCI6Im1lbmRlci" + - "4qIiwiaWF0IjoxMjM0NTY3LCJtZW5kZXIudGVuYW50Ij" + - "oiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIn0.GHw6EC" + - "1kfeAuB7UkwqwZ6yt25US7lhWXhCG6HYTvXhY1MSaPrL" + - "0QMQWnAwWYbM7T9o8CEBhKUumJCZ1JiRiC8cKwd9SytL" + - "UWxuxE4f2qGSyhMpku1yPXQ-mq6s58wrbAf1s0cEU0vT" + - "gygncp7fnfKcCpg9A3kYZaDnTmqgA63sXlaiSSnPHd-y" + - "MO5duFb8xqZeoRzkJrKiI2Bh5pMDPNIJkGkGyF37w_8i" + - "vjKiGB5ph_vm0LyeVjlzEGU7nri5qARE7oJqN1lICgXX" + - "MzKZXxUB6h-v2vnkIJC0uZR35ddXhXUrpnRWwHn2xSdz" + - "5QAKAgnr12OlK1fPWrn2xy0cK2Mw", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: &Time{ - Time: time.Unix(4481893900, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Issuer: "Mender", - Scope: "mender.*", - Tenant: "000000000000000000000000", - }, - }, + "ok, pkcs8, rsa": { + privateKeyPath: "./testdata/rsa_pkcs8.pem", }, - "ok (fallback not used)": { - privKey: loadPrivKey("../crypto/private.pem", t), - fallbackPrivKey: loadPrivKey("../crypto/private_alternative.pem", t), - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH" + - "AiOjQ0ODE4OTM5MDAsImlzcyI6Ik1lbmRlciIsImF1ZC" + - "I6Ik1lbmRlciIsInN1YiI6ImJjYTk1YWRiLWI1ZjEtNT" + - "Y0Zi05NmE3LTYzNTVjNTJkMWZhNyIsInNjcCI6Im1lbm" + - "Rlci4qIiwiaWF0IjoxMjM0NTY3LCJqdGkiOiJiOTQ3NT" + - "MzNi1kZGU2LTU0OTctODA0NC01MWFhOWRkYzAyZjgifQ" + - ".xkL2V6nzPsJaLUezrZg-lSCqH5yrG0ee-79TuaDC7u9" + - "ty3btT1VhoGdgEmrGUkLRdOAxnY_KI9rNHAkxzuTj8ef" + - "p6hss8PKC6DHM_Ke_cZH0xRt2V0QjhhZT5QkGFjb60me" + - "iY5oMQdhXY1rtaFuAvMvPMSZ0Rs4Twy3tuWvws9sekIY" + - "GWyVV-EGOtheI8_lGXlPSUXc5_0aUJuUNoKyIDFK4Chp" + - "eYxjyL20U0GPtGPAEKQQkCBqlliBsu1Rdww3a7ephIIs" + - "Fu6A8BWJpT5hGpiQlKK2hu2MZ9wh94wbcZXJRtlE_BWz" + - "NLKjV0L1oiaeWKuMGOTQ4TYgKeifWRCm_nw", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: &Time{ - Time: time.Unix(4481893900, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Audience: "Mender", - Issuer: "Mender", - Scope: "mender.*", - }, - }, + "ok, pkcs8, ed25519": { + privateKeyPath: "./testdata/ed25519.pem", }, - "ok (fallback used)": { - privKey: loadPrivKey("../crypto/private_alternative.pem", t), - fallbackPrivKey: loadPrivKey("../crypto/private.pem", t), - - inToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH" + - "AiOjQ0ODE4OTM5MDAsImlzcyI6Ik1lbmRlciIsImF1ZC" + - "I6Ik1lbmRlciIsInN1YiI6ImJjYTk1YWRiLWI1ZjEtNT" + - "Y0Zi05NmE3LTYzNTVjNTJkMWZhNyIsInNjcCI6Im1lbm" + - "Rlci4qIiwiaWF0IjoxMjM0NTY3LCJqdGkiOiJiOTQ3NT" + - "MzNi1kZGU2LTU0OTctODA0NC01MWFhOWRkYzAyZjgifQ" + - ".xkL2V6nzPsJaLUezrZg-lSCqH5yrG0ee-79TuaDC7u9" + - "ty3btT1VhoGdgEmrGUkLRdOAxnY_KI9rNHAkxzuTj8ef" + - "p6hss8PKC6DHM_Ke_cZH0xRt2V0QjhhZT5QkGFjb60me" + - "iY5oMQdhXY1rtaFuAvMvPMSZ0Rs4Twy3tuWvws9sekIY" + - "GWyVV-EGOtheI8_lGXlPSUXc5_0aUJuUNoKyIDFK4Chp" + - "eYxjyL20U0GPtGPAEKQQkCBqlliBsu1Rdww3a7ephIIs" + - "Fu6A8BWJpT5hGpiQlKK2hu2MZ9wh94wbcZXJRtlE_BWz" + - "NLKjV0L1oiaeWKuMGOTQ4TYgKeifWRCm_nw", - - outToken: Token{ - Claims: Claims{ - ID: oid.NewUUIDv5("someid"), - Subject: oid.NewUUIDv5("foo"), - ExpiresAt: &Time{ - Time: time.Unix(4481893900, 0), - }, - IssuedAt: Time{ - Time: time.Unix(1234567, 0), - }, - Audience: "Mender", - Issuer: "Mender", - 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: loadPrivKey("../crypto/private.pem", t), - - inToken: "1234123412341234", - - outToken: Token{}, - outErr: errors.New("token contains an invalid number of segments"), + "unknown priate key type": { + privateKeyPath: "./testdata/dsa.pem", + err: errors.New("unsupported server private key type"), }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { - 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.Claims, (*token).Claims) - assert.NotEmpty(t, token.ID) + _, err := NewJWTHandler(tc.privateKeyPath) + if tc.err != nil { + assert.EqualError(t, err, tc.err.Error()) } else { - assert.EqualError(t, tc.outErr, err.Error()) + assert.NoError(t, err) } }) } } - -func loadPrivKey(path string, t *testing.T) *rsa.PrivateKey { - pem_data, err := ioutil.ReadFile(path) - if err != nil { - t.FailNow() - } - - block, _ := pem.Decode(pem_data) - - if block == nil || - block.Type != "RSA PRIVATE KEY" { - t.FailNow() - } - - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - t.FailNow() - } - - 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/mocks/Handler.go b/jwt/mocks/Handler.go index 993246a7..77f78953 100644 --- a/jwt/mocks/Handler.go +++ b/jwt/mocks/Handler.go @@ -1,16 +1,16 @@ -// 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. -// You may obtain a copy of the License at +// 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 +// 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. +// 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. // Code generated by mockery v2.2.2. DO NOT EDIT. diff --git a/jwt/testdata/dsa.pem b/jwt/testdata/dsa.pem new file mode 100644 index 00000000..363dd17c --- /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 00000000..98b23e9b --- /dev/null +++ b/jwt/testdata/ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIG1ZSPHHBCWnpD1hZAsEMxMemPK26E6EbxxXgsA5M/9H +-----END PRIVATE KEY----- diff --git a/jwt/testdata/rsa.pem b/jwt/testdata/rsa.pem new file mode 100644 index 00000000..f6f71280 --- /dev/null +++ b/jwt/testdata/rsa.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAzTjC+bJ79qZagrgn2SLtDbHNFfFLORYH/VnoeMZxfkzYYdHi +4Y6/soSY15M+baKnRKFv/9UDJKVUTzy1cTinWSV3R0/I0Oo2bLzWG045akaTf2FP +DQHk9kCHaRl0ZvZw5u+lpRdrb0C80AuCrRi2cAJzrzME2NL3kc9YJNGUhgVYBgdc +vNh9PZPaVhCus6IDgZAFTiimt9bQmPyaMpUwcYTxa8ALgq38/PtHQNE0OZZ9td49 +Ro4Wo729uwnkf//Z22ksd08hlyfhxcl5ivXqDgLd72LXtYiWCYMMQwPgtLyQy0x3 +GZmR2JvrRH8peyK7cytSl3UrIibcZUuwYzftWwIDAQABAoIBAQDEoq2XaWCD/fNd +qyxrTp7K72Fts+z6vtRa+enYx3P5q5HtcJkuvIOLfqegb3JyxFX3WaQiAq1nUI7O ++YF1Ae6/aTm394eYPcJ2QB36veIfpY4wkEGZWgZTuZLFrmEtQtb9QAR8gksrp2ED +CvRj/PjZrE/CQGVViBc0+/IeHT5thjt4FtOy8l1NE/0UTb4SR5nrCMFJDMiGQ8e2 +8dqRLOb/j00ZZNTF4p+RUZbWwX1yypV/wOwtpjaMCRknXCPpNO4c/TG8Ul92yqrK +IwcoHKJhhdd0137ryZrqzVswTgI1lBNcGRedM+/8MMobyFoOBl6Q4IbkxTZjoX67 +C5uRHVZpAoGBAPErtDJleY03ICJyHXOpqSjmcJZe9eJlKcPNt5eq/3Jd2HWGtV2P +ciEiY6E3vAGGGuEKtiSh/hY9f6tHNFVzrbHWGJlfLdknfw+Qx7KGLRtmTsfnim0e +/Y0GTWHGdlnuodJjEtlT1k/lXFhvzYEbtIsYEKVDlbp5Tq95pAQGRsxlAoGBANnX +LjPJvnio9m9RocaFY5x2GmObGT1uVeWqmrFuOb/vGIs8nWWx4LFH0PR1as8jK2mu +6vH228FMXmVbBZki/g9GcZsInyAMDCQLcOmnfRiEY8H5KDIFmqRprYXOJO5ajN9z +3tpAAIbE+EtXsdNVBU7M0zI1VVjwAkAN++WIVda/AoGBALSHhJIdB8o/s5xVU9qa +6/ej2C+X0fOwyny5525vIFzaBwii1+y7TjGjbnTmJaP2YPSIciQl5u97BbCO0owM +1b7DhxJ3/vgI6sIaHIJ0khtqkBpwJlzcz+vbBd3lE/7p9NRqOVfZvBl+lYvV1T2K +IbHlR8COQOyfldkg8zfYAvNJAoGAXr32azGn9GRP4bRYLrYez5KSAZYYER6mnx9m +7hopZa+ANjZjojINriy01U0n6fS60djwd9HMW7JyL5S5x5NveYdCq5HBWzfCI/8R +2Z0ti/cwR9GsSK0lR7JqdJJmf3/EWv7TAorpb7PE5Ue7oFUO3Om6RNDrUKX55I/w +aTC1XJMCgYEAyFHy8flWXXI9oKzgmfB3LcTKWjgAeZUXAgFEMFXgKIEgijmu6QnM +u0yq35T2vx1RjOecO8AdU3w8GvHeseKFZAFDCbMZgx58+keZud/PXWEleRhEapBS +MF5HiJ2vfmFUegj4Enr8u488+WOfomiouoZcx77gr2i6ACf+QDelHfU= +-----END RSA PRIVATE KEY----- diff --git a/jwt/testdata/rsa_pkcs8.pem b/jwt/testdata/rsa_pkcs8.pem new file mode 100644 index 00000000..ba0d407a --- /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/jwt/token.go b/jwt/token.go index fe981685..a0d1cbe5 100644 --- a/jwt/token.go +++ b/jwt/token.go @@ -1,19 +1,29 @@ -// 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. -// You may obtain a copy of the License at +// 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 +// 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. +// 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 "time" +import ( + "time" + + "github.com/pkg/errors" +) + +// SignFunc will sign and encode token. +type SignFunc func(token *Token) (string, error) + +// UnpackFunc will decode token +type UnpackFunc func(s string) (*Token, error) // Token wrapper type Token struct { @@ -23,3 +33,30 @@ type Token struct { // TokenName holds the name of the token TokenName *string `json:"name,omitempty" bson:"name,omitempty"` } + +// MarshalJWT marshals Token into JWT comaptible format. `sign` provides means +// for generating a signed JWT token. +func (t *Token) MarshalJWT(sign SignFunc) ([]byte, error) { + if sign == nil { + panic("no signature helper") + } + + signed, err := sign(t) + if err != nil { + return nil, errors.Wrapf(err, "failed to sign token") + } + return []byte(signed), nil +} + +// UnmarshalJWT unmarshals raw JWT data into Token. UnpackFunc does the +// actual heavy-lifting of parsing and deserializing base64'ed JWT. Returns an +// error if `unpack` failed, however if `unpack` returns a token `t` will be +// updated as well (may happen if token is valid wrt. to structure & signature, +// but expired). +func (t *Token) UnmarshalJWT(raw []byte, unpack UnpackFunc) error { + tok, err := unpack(string(raw)) + if tok != nil { + *t = *tok + } + return err +} diff --git a/jwt/token_test.go b/jwt/token_test.go new file mode 100644 index 00000000..6024810d --- /dev/null +++ b/jwt/token_test.go @@ -0,0 +1,89 @@ +// 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 ( + "testing" + "time" + + "github.com/mendersoftware/go-lib-micro/mongo/oid" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestTokenMarshal(t *testing.T) { + tok := &Token{ + Claims: Claims{ + ID: oid.NewUUIDv5("foo"), + Subject: oid.NewUUIDv5("valid-subject"), + ExpiresAt: &Time{ + Time: time.Now().Add(time.Hour), + }, + }, + } + + res, err := tok.MarshalJWT(func(toSign *Token) (string, error) { + assert.Equal(t, tok, toSign) + return "signed", nil + }) + assert.Equal(t, []byte("signed"), res) + assert.NoError(t, err) + + res, err = tok.MarshalJWT(func(toSign *Token) (string, error) { + assert.Equal(t, tok, toSign) + return "", errors.New("failed") + }) + assert.Empty(t, res) + assert.Error(t, err) +} + +func TestTokenUnmarshal(t *testing.T) { + tokin := []byte("some-fake-jwt") + tok := &Token{ + Claims: Claims{ + ID: oid.NewUUIDv5("foo"), + Subject: oid.NewUUIDv5("valid-subject"), + ExpiresAt: &Time{ + Time: time.Now().Add(time.Hour), + }, + }, + } + + unTok := &Token{} + + err := unTok.UnmarshalJWT(tokin, func(toUnpack string) (*Token, error) { + assert.Equal(t, string(tokin), toUnpack) + return tok, nil + }) + assert.Equal(t, unTok, tok) + assert.NoError(t, err) + + unTok = &Token{} + + err = unTok.UnmarshalJWT(tokin, func(toUnpack string) (*Token, error) { + assert.Equal(t, string(tokin), toUnpack) + return nil, errors.New("failed") + }) + assert.Error(t, err) + + unTok = &Token{} + // make sure that the token is updated if UnpackFunc returns both + // a token & an error + err = unTok.UnmarshalJWT(tokin, func(toUnpack string) (*Token, error) { + assert.Equal(t, string(tokin), toUnpack) + return tok, errors.New("failed") + }) + assert.Equal(t, unTok, tok) + assert.Error(t, err) +} diff --git a/keys/key.go b/keys/key.go deleted file mode 100644 index a96d24be..00000000 --- a/keys/key.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2022 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 67c4c6e2..00000000 --- a/keys/key_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2022 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/middleware.go b/middleware.go index 17dafcb8..54547903 100644 --- a/middleware.go +++ b/middleware.go @@ -1,16 +1,16 @@ // Copyright 2021 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 +// 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 +// 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. +// 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 main import ( @@ -80,6 +80,7 @@ func SetupMiddleware( mwtype string, authorizer authz.Authorizer, jwth jwt.Handler, + jwthFallback jwt.Handler, ) error { l := log.New(log.Ctx{}) @@ -98,9 +99,10 @@ func SetupMiddleware( api.Use(commonStack...) authzmw := &authz.AuthzMiddleware{ - Authz: authorizer, - ResFunc: api_http.ExtractResourceAction, - JWTHandler: jwth, + Authz: authorizer, + ResFunc: api_http.ExtractResourceAction, + JWTHandler: jwth, + JWTFallbackHandler: jwthFallback, } //force authz only on verification endpoint diff --git a/middleware_test.go b/middleware_test.go index b323e7d8..dd40b181 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -1,16 +1,16 @@ -// Copyright 2017 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. -// You may obtain a copy of the License at +// 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 +// 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. +// 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 main import ( @@ -33,7 +33,7 @@ func TestSetupMiddleware(t *testing.T) { for _, td := range tdata { api := rest.NewApi() - err := SetupMiddleware(api, td.mwtype, nil, nil) + err := SetupMiddleware(api, td.mwtype, nil, nil, nil) if err != nil && !td.experr { t.Errorf("dod not expect error: %s", err) } else if err == nil && td.experr { diff --git a/server.go b/server.go index bec80fc0..fd148777 100644 --- a/server.go +++ b/server.go @@ -14,7 +14,6 @@ package main import ( - "crypto/rsa" "net/http" "github.com/ant0ine/go-json-rest/rest" @@ -27,14 +26,14 @@ import ( "github.com/mendersoftware/useradm/client/tenant" . "github.com/mendersoftware/useradm/config" "github.com/mendersoftware/useradm/jwt" - "github.com/mendersoftware/useradm/keys" "github.com/mendersoftware/useradm/store/mongo" useradm "github.com/mendersoftware/useradm/user" ) -func SetupAPI(stacktype string, authz authz.Authorizer, jwth jwt.Handler) (*rest.Api, error) { +func SetupAPI(stacktype string, authz authz.Authorizer, jwth jwt.Handler, + jwthFallback jwt.Handler) (*rest.Api, error) { api := rest.NewApi() - if err := SetupMiddleware(api, stacktype, authz, jwth); err != nil { + if err := SetupMiddleware(api, stacktype, authz, jwth, jwthFallback); err != nil { return nil, errors.Wrap(err, "failed to setup middleware") } @@ -51,29 +50,27 @@ func RunServer(c config.Reader) error { l := log.New(log.Ctx{}) - privKey, err := keys.LoadRSAPrivate(c.GetString(SettingPrivKeyPath)) - if err != nil { - return errors.Wrap(err, "failed to read rsa private key") + authz := &SimpleAuthz{} + jwtHandler, err := jwt.NewJWTHandler( + c.GetString(SettingServerPrivKeyPath), + ) + var jwtFallbackHandler jwt.Handler + fallback := c.GetString(SettingServerFallbackPrivKeyPath) + if err == nil && fallback != "" { + jwtFallbackHandler, err = jwt.NewJWTHandler( + fallback, + ) } - - fallbackPrivKeyPath := c.GetString(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") - } + if err != nil { + return err } - authz := &SimpleAuthz{} - jwth := jwt.NewJWTHandlerRS256(privKey, fallbackPrivKey) - db, err := mongo.GetDataStoreMongo(dataStoreMongoConfigFromAppConfig(c)) if err != nil { return errors.Wrap(err, "database connection failed") } - ua := useradm.NewUserAdm(jwth, db, + ua := useradm.NewUserAdm(jwtHandler, db, useradm.Config{ Issuer: c.GetString(SettingJWTIssuer), ExpirationTimeSeconds: int64(c.GetInt(SettingJWTExpirationTimeout)), @@ -92,12 +89,12 @@ func RunServer(c config.Reader) error { ua = ua.WithTenantVerification(tc) } - useradmapi := api_http.NewUserAdmApiHandlers(ua, db, jwth, + useradmapi := api_http.NewUserAdmApiHandlers(ua, db, jwtHandler, api_http.Config{ TokenMaxExpSeconds: c.GetInt(SettingTokenMaxExpirationSeconds), }) - api, err := SetupAPI(c.GetString(SettingMiddleware), authz, jwth) + api, err := SetupAPI(c.GetString(SettingMiddleware), authz, jwtHandler, jwtFallbackHandler) if err != nil { return errors.Wrap(err, "API setup failed") } diff --git a/server_test.go b/server_test.go index 3028556d..0673f6b7 100644 --- a/server_test.go +++ b/server_test.go @@ -1,16 +1,16 @@ -// Copyright 2017 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. -// You may obtain a copy of the License at +// 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 +// 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. +// 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 main import ( @@ -21,11 +21,11 @@ import ( func TestSetupApi(t *testing.T) { // expecting an error - api, err := SetupAPI("foo", nil, nil) + api, err := SetupAPI("foo", nil, nil, nil) assert.Nil(t, api) assert.Error(t, err) - api, err = SetupAPI(EnvDev, nil, nil) + api, err = SetupAPI(EnvDev, nil, nil, nil) assert.NotNil(t, api) assert.Nil(t, err) }