From dea9effa8dbb54d28beeeb2f5e3a2bd1dc155a83 Mon Sep 17 00:00:00 2001 From: Matthias Hanel Date: Tue, 26 Jan 2021 17:49:58 -0500 Subject: [PATCH] [added] support for StrictSigningKeyUsage and updated jwt library (#1845) This will cause the server to not trust accounts/user signed by an identity key The boot strapping system account will assume the account is issued by the operator. If this is not desirable, the system account can be provided right away as resolver_preload. [fixes] crash when the system account uses signing keys and an update changes that key set. Signed-off-by: Matthias Hanel --- go.mod | 2 +- go.sum | 27 +--- server/accounts.go | 40 ++++-- server/auth.go | 27 ++-- server/client.go | 2 +- server/jwt.go | 4 +- server/jwt_test.go | 118 ++++++++++++++++++ server/nkey.go | 2 +- server/server.go | 19 ++- .../nats-io/jwt/v2/account_claims.go | 2 +- .../nats-io/jwt/v2/operator_claims.go | 4 +- vendor/modules.txt | 2 +- 12 files changed, 188 insertions(+), 61 deletions(-) diff --git a/go.mod b/go.mod index 20a9eee8..ba5dcfad 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( github.com/klauspost/compress v1.11.7 github.com/minio/highwayhash v1.0.0 - github.com/nats-io/jwt/v2 v2.0.0-20210107222814-18c5cc45d263 + github.com/nats-io/jwt/v2 v2.0.0-20210125223648-1c24d462becc github.com/nats-io/nats.go v1.10.1-0.20210122204956-b8ea7fc17ea6 github.com/nats-io/nkeys v0.2.0 github.com/nats-io/nuid v1.0.1 diff --git a/go.sum b/go.sum index 496a8bcc..c9554443 100644 --- a/go.sum +++ b/go.sum @@ -3,49 +3,30 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU= -github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/jwt v0.3.3-0.20200519195258-f2bf5ce574c7 h1:RnGotxlghqR5D2KDAu4TyuLqyjuylOsJiAFhXvMvQIc= github.com/nats-io/jwt v0.3.3-0.20200519195258-f2bf5ce574c7/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M= github.com/nats-io/jwt v1.1.0 h1:+vOlgtM0ZsF46GbmUoadq0/2rChNS45gtxHEa3H1gqM= github.com/nats-io/jwt v1.1.0/go.mod h1:n3cvmLfBfnpV4JJRN7lRYCyZnw48ksGsbThGXEk4w9M= -github.com/nats-io/jwt v1.2.3-0.20210107222814-18c5cc45d263 h1:x3J+0KMQhbQE8iHxChJdJHNk7rJnCwXFX+WI0hfvYtE= -github.com/nats-io/jwt v1.2.3-0.20210107222814-18c5cc45d263/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= github.com/nats-io/jwt/v2 v2.0.0-20200916203241-1f8ce17dff02/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ= github.com/nats-io/jwt/v2 v2.0.0-20201015190852-e11ce317263c/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ= -github.com/nats-io/jwt/v2 v2.0.0-20210107222814-18c5cc45d263 h1:M2bkT/arzYFYPtRqQWN2m+LSLfcqFmPxlxvTxdB4aVE= -github.com/nats-io/jwt/v2 v2.0.0-20210107222814-18c5cc45d263/go.mod h1:PuO5FToRL31ecdFqVjc794vK0Bj0CwzveQEDvkb7MoQ= +github.com/nats-io/jwt/v2 v2.0.0-20210125223648-1c24d462becc h1:pu+s4XC+bYnI0iD2vDtOl83zjCYUau/q6c83pEvsGZc= +github.com/nats-io/jwt/v2 v2.0.0-20210125223648-1c24d462becc/go.mod h1:PuO5FToRL31ecdFqVjc794vK0Bj0CwzveQEDvkb7MoQ= github.com/nats-io/nats-server/v2 v2.1.8-0.20200524125952-51ebd92a9093/go.mod h1:rQnBf2Rv4P9adtAs/Ti6LfFmVtFG6HLhl/H7cVshcJU= github.com/nats-io/nats-server/v2 v2.1.8-0.20200601203034-f8d6dd992b71/go.mod h1:Nan/1L5Sa1JRW+Thm4HNYcIDcVRFc5zK9OpSZeI2kk4= github.com/nats-io/nats-server/v2 v2.1.8-0.20200929001935-7f44d075f7ad/go.mod h1:TkHpUIDETmTI7mrHN40D1pzxfzHZuGmtMbtb83TGVQw= github.com/nats-io/nats-server/v2 v2.1.8-0.20201129161730-ebe63db3e3ed/go.mod h1:XD0zHR/jTXdZvWaQfS5mQgsXj6x12kMjKLyAk/cOGgY= github.com/nats-io/nats.go v1.10.0/go.mod h1:AjGArbfyR50+afOUotNX2Xs5SYHf+CoOa5HH1eEl2HE= github.com/nats-io/nats.go v1.10.1-0.20200531124210-96f2130e4d55/go.mod h1:ARiFsjW9DVxk48WJbO3OSZ2DG8fjkMi7ecLmXoY/n9I= -github.com/nats-io/nats.go v1.10.1-0.20200606002146-fc6fed82929a h1:gzSKZOBlu/DpbuPbt34paXCOvA6+E+lVfU2BmomQ9HA= github.com/nats-io/nats.go v1.10.1-0.20200606002146-fc6fed82929a/go.mod h1:8eAIv96Mo9QW6Or40jUHejS7e4VwZ3VRYD6Sf0BTDp4= -github.com/nats-io/nats.go v1.10.1-0.20201021145452-94be476ad6e0 h1:s//xxvMgNRVJNTRXTRNvLlES0zvJoDLXfcmbK0YgN0E= github.com/nats-io/nats.go v1.10.1-0.20201021145452-94be476ad6e0/go.mod h1:VU2zERjp8xmF+Lw2NH4u2t5qWZxwc7jB3+7HVMWQXPI= -github.com/nats-io/nats.go v1.10.1-0.20201218032324-b4450fb04511 h1:VMkjinILzhvMPbG+MwRC9q5IElHdsOaB5aHIiCT8vd4= -github.com/nats-io/nats.go v1.10.1-0.20201218032324-b4450fb04511/go.mod h1:Sa3kLIonafChP5IF0b55i9uvGR10I3hPETFbi4+9kOI= -github.com/nats-io/nats.go v1.10.1-0.20210113000055-be60b8378d4f h1:HjROEVuamG+e0lCwjKs3yfTzDbbaogriW4HQQ2OQcr0= -github.com/nats-io/nats.go v1.10.1-0.20210113000055-be60b8378d4f/go.mod h1:Sa3kLIonafChP5IF0b55i9uvGR10I3hPETFbi4+9kOI= -github.com/nats-io/nats.go v1.10.1-0.20210113025029-acca6d69b4e3 h1:M7a1IwTIUnZ/ucEz+/oyOoQ11CJ/sDfybZYjdGMGjdE= -github.com/nats-io/nats.go v1.10.1-0.20210113025029-acca6d69b4e3/go.mod h1:Sa3kLIonafChP5IF0b55i9uvGR10I3hPETFbi4+9kOI= -github.com/nats-io/nats.go v1.10.1-0.20210114001154-0a6b5f686ab3 h1:2uYi4zZJ6zni7jwix8l1bA/srT3hjFwGN2iUvx0CWpA= -github.com/nats-io/nats.go v1.10.1-0.20210114001154-0a6b5f686ab3/go.mod h1:Sa3kLIonafChP5IF0b55i9uvGR10I3hPETFbi4+9kOI= -github.com/nats-io/nats.go v1.10.1-0.20210115180731-7fb8bacca613 h1:Zio4IMHHsFjtTeksjF4PySxFNcKwSH9urNIiIW7A/FQ= -github.com/nats-io/nats.go v1.10.1-0.20210115180731-7fb8bacca613/go.mod h1:Sa3kLIonafChP5IF0b55i9uvGR10I3hPETFbi4+9kOI= github.com/nats-io/nats.go v1.10.1-0.20210122204956-b8ea7fc17ea6 h1:cpS+9uyfHXvRG/Q+WcDd3KXRgPa9fo9tDbIeDHCxYAg= github.com/nats-io/nats.go v1.10.1-0.20210122204956-b8ea7fc17ea6/go.mod h1:Sa3kLIonafChP5IF0b55i9uvGR10I3hPETFbi4+9kOI= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= @@ -56,7 +37,6 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E= golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -64,19 +44,16 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= diff --git a/server/accounts.go b/server/accounts.go index 388580b9..73251227 100644 --- a/server/accounts.go +++ b/server/accounts.go @@ -2746,10 +2746,6 @@ func (a *Account) hasIssuer(issuer string) (jwt.Scope, bool) { // hasIssuerNoLock is the unlocked version of hasIssuer func (a *Account) hasIssuerNoLock(issuer string) (jwt.Scope, bool) { - // same issuer -- keep this for safety on the calling code - if a.Name == issuer { - return nil, true - } scope, ok := a.signingKeys[issuer] return scope, ok } @@ -2844,13 +2840,17 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim // update account signing keys a.signingKeys = nil - if len(ac.SigningKeys) > 0 { - a.signingKeys = make(map[string]jwt.Scope, len(ac.SigningKeys)) + _, strict := s.strictSigningKeyUsage[a.Issuer] + if len(ac.SigningKeys) > 0 || !strict { + a.signingKeys = make(map[string]jwt.Scope) } signersChanged := false for k, scope := range ac.SigningKeys { a.signingKeys[k] = scope } + if !strict { + a.signingKeys[a.Name] = nil + } if len(a.signingKeys) != len(old.signingKeys) { signersChanged = true } @@ -3150,6 +3150,10 @@ func (s *Server) updateAccountClaimsWithRefresh(a *Account, ac *jwt.AccountClaim if signersChanged { for _, c := range clients { c.mu.Lock() + if c.user == nil { + c.mu.Unlock() + continue + } sk := c.user.SigningKey c.mu.Unlock() if sk == _EMPTY_ { @@ -3506,19 +3510,21 @@ func handleDeleteRequest(store *DirJWTStore, s *Server, msg []byte, reply string } } -func getOperator(s *Server) (string, error) { +func getOperator(s *Server) (string, bool, error) { var op string + var strict bool if opts := s.getOpts(); opts != nil && len(opts.TrustedOperators) > 0 { op = opts.TrustedOperators[0].Subject + strict = opts.TrustedOperators[0].StrictSigningKeyUsage } if op == "" { - return "", fmt.Errorf("no operator found") + return "", false, fmt.Errorf("no operator found") } - return op, nil + return op, strict, nil } func (dr *DirAccResolver) Start(s *Server) error { - op, err := getOperator(s) + op, strict, err := getOperator(s) if err != nil { return err } @@ -3553,6 +3559,9 @@ func (dr *DirAccResolver) Start(s *Server) error { } else if claim.Subject != pubKey { err := errors.New("subject does not match jwt content") respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err) } else if err := dr.save(pubKey, string(msg)); err != nil { respondToUpdate(s, resp, pubKey, "jwt update resulted in error", err) } else { @@ -3565,6 +3574,9 @@ func (dr *DirAccResolver) Start(s *Server) error { if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, _ *client, subj, resp string, msg []byte) { if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil { respondToUpdate(s, resp, "n/a", "jwt update resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, claim.Subject, "jwt update resulted in error", err) } else if err := dr.save(claim.Subject, string(msg)); err != nil { respondToUpdate(s, resp, claim.Subject, "jwt update resulted in error", err) } else { @@ -3760,7 +3772,7 @@ func NewCacheDirAccResolver(path string, limit int64, ttl time.Duration, _ ...di } func (dr *CacheDirAccResolver) Start(s *Server) error { - op, err := getOperator(s) + op, strict, err := getOperator(s) if err != nil { return err } @@ -3794,6 +3806,9 @@ func (dr *CacheDirAccResolver) Start(s *Server) error { } else if claim.Subject != pubKey { err := errors.New("subject does not match jwt content") respondToUpdate(s, resp, pubKey, "jwt update cache resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, pubKey, "jwt update cache resulted in error", err) } else if _, ok := s.accounts.Load(pubKey); !ok { respondToUpdate(s, resp, pubKey, "jwt update cache skipped", nil) } else if err := dr.save(pubKey, string(msg)); err != nil { @@ -3808,6 +3823,9 @@ func (dr *CacheDirAccResolver) Start(s *Server) error { if _, err := s.sysSubscribe(accClaimsReqSubj, func(_ *subscription, _ *client, subj, resp string, msg []byte) { if claim, err := jwt.DecodeAccountClaims(string(msg)); err != nil { respondToUpdate(s, resp, "n/a", "jwt update cache resulted in error", err) + } else if claim.Issuer == op && strict { + err := errors.New("operator requires issuer to be a signing key") + respondToUpdate(s, resp, claim.Subject, "jwt update cache resulted in error", err) } else if _, ok := s.accounts.Load(claim.Subject); !ok { respondToUpdate(s, resp, claim.Subject, "jwt update cache skipped", nil) } else if err := dr.save(claim.Subject, string(msg)); err != nil { diff --git a/server/auth.go b/server/auth.go index 6265dfcd..8c520051 100644 --- a/server/auth.go +++ b/server/auth.go @@ -238,7 +238,7 @@ func (s *Server) configureAuthorization() { // This just checks and sets up the user map if we have multiple users. if opts.CustomClientAuthentication != nil { s.info.AuthRequired = true - } else if len(s.trustedKeys) > 0 { + } else if s.trustedKeys != nil { s.info.AuthRequired = true } else if opts.Nkeys != nil || opts.Users != nil { s.nkeys, s.users = s.buildNkeysAndUsersFromOptions(opts.Nkeys, opts.Users) @@ -560,21 +560,18 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo c.Debugf("Account JWT not signed by trusted operator") return false } - // this only executes IF there's an issuer on the Juc - otherwise the account is already vetted - if juc.IssuerAccount != _EMPTY_ { - if scope, ok := acc.hasIssuer(juc.Issuer); !ok { - c.Debugf("User JWT issuer is not known") + if scope, ok := acc.hasIssuer(juc.Issuer); !ok { + c.Debugf("User JWT issuer is not known") + return false + } else if scope != nil { + if err := scope.ValidateScopedSigner(juc); err != nil { + c.Debugf("User JWT is not valid: %v", err) return false - } else if scope != nil { - if err := scope.ValidateScopedSigner(juc); err != nil { - c.Debugf("User JWT is not valid: %v", err) - return false - } else if uSc, ok := scope.(*jwt.UserScope); !ok { - c.Debugf("User JWT is not valid") - return false - } else { - juc.UserPermissionLimits = uSc.Template - } + } else if uSc, ok := scope.(*jwt.UserScope); !ok { + c.Debugf("User JWT is not valid") + return false + } else { + juc.UserPermissionLimits = uSc.Template } } if acc.IsExpired() { diff --git a/server/client.go b/server/client.go index 2c09f55b..39527779 100644 --- a/server/client.go +++ b/server/client.go @@ -1787,7 +1787,7 @@ func (c *client) authViolation() { var hasTrustedNkeys, hasNkeys, hasUsers bool if s = c.srv; s != nil { s.mu.Lock() - hasTrustedNkeys = len(s.trustedKeys) > 0 + hasTrustedNkeys = s.trustedKeys != nil hasNkeys = s.nkeys != nil hasUsers = s.users != nil s.mu.Unlock() diff --git a/server/jwt.go b/server/jwt.go index f7e76ba3..49efd4ec 100644 --- a/server/jwt.go +++ b/server/jwt.go @@ -132,7 +132,9 @@ func validateTrustedOperators(o *Options) error { if o.TrustedKeys == nil { o.TrustedKeys = make([]string, 0, 4) } - o.TrustedKeys = append(o.TrustedKeys, opc.Issuer) + if !opc.StrictSigningKeyUsage { + o.TrustedKeys = append(o.TrustedKeys, opc.Subject) + } o.TrustedKeys = append(o.TrustedKeys, opc.SigningKeys...) } for _, key := range o.TrustedKeys { diff --git a/server/jwt_test.go b/server/jwt_test.go index 739fbcc0..8df61f0a 100644 --- a/server/jwt_test.go +++ b/server/jwt_test.go @@ -5144,3 +5144,121 @@ func TestJWScopedSigningKeys(t *testing.T) { }) require_Len(t, len(errChan), 0) } + +func TestJWTStrictSigningKeys(t *testing.T) { + newAccount := func(opKp nkeys.KeyPair) (nkeys.KeyPair, nkeys.KeyPair, string, *jwt.AccountClaims, string) { + accId, err := nkeys.CreateAccount() + require_NoError(t, err) + accIdPub, err := accId.PublicKey() + require_NoError(t, err) + + accSig, err := nkeys.CreateAccount() + require_NoError(t, err) + accSigPub, err := accSig.PublicKey() + require_NoError(t, err) + + aClaim := jwt.NewAccountClaims(accIdPub) + aClaim.SigningKeys.Add(accSigPub) + theJwt, err := aClaim.Encode(opKp) + require_NoError(t, err) + return accId, accSig, accIdPub, aClaim, theJwt + } + + opId, err := nkeys.CreateOperator() + require_NoError(t, err) + opIdPub, err := opId.PublicKey() + require_NoError(t, err) + + opSig, err := nkeys.CreateOperator() + require_NoError(t, err) + opSigPub, err := opSig.PublicKey() + require_NoError(t, err) + + aBadBadKp, aBadGoodKp, aBadPub, _, aBadJwt := newAccount(opId) + aGoodBadKp, aGoodGoodKp, aGoodPub, _, aGoodJwt := newAccount(opSig) + _, aSysKp, aSysPub, _, aSysJwt := newAccount(opSig) + + oClaim := jwt.NewOperatorClaims(opIdPub) + oClaim.StrictSigningKeyUsage = true + oClaim.SigningKeys.Add(opSigPub) + oClaim.SystemAccount = aSysPub + oJwt, err := oClaim.Encode(opId) + require_NoError(t, err) + + uBadBadCreds := newUserEx(t, aBadBadKp, false, aBadPub) + defer os.Remove(uBadBadCreds) + uBadGoodCreds := newUserEx(t, aBadGoodKp, false, aBadPub) + defer os.Remove(uBadGoodCreds) + uGoodBadCreds := newUserEx(t, aGoodBadKp, false, aGoodPub) + defer os.Remove(uGoodBadCreds) + uGoodGoodCreds := newUserEx(t, aGoodGoodKp, false, aGoodPub) + defer os.Remove(uGoodGoodCreds) + uSysCreds := newUserEx(t, aSysKp, false, aSysPub) + defer os.Remove(uSysCreds) + + connectTest := func(url string) { + for _, test := range []struct { + creds string + fail bool + }{ + {uBadBadCreds, true}, + {uBadGoodCreds, true}, + {uGoodBadCreds, true}, + {uGoodGoodCreds, false}, + } { + nc, err := nats.Connect(url, nats.UserCredentials(test.creds)) + nc.Close() + if test.fail { + require_Error(t, err) + } else { + require_NoError(t, err) + } + } + } + + t.Run("resolver", func(t *testing.T) { + dirSrv := createDir(t, "srv") + defer os.RemoveAll(dirSrv) + cf := createConfFile(t, []byte(fmt.Sprintf(` + port: -1 + operator = %s + resolver: { + type: full + dir: %s + } + resolver_preload = { + %s : "%s" + } + `, oJwt, dirSrv, aSysPub, aSysJwt))) + defer os.Remove(cf) + s, _ := RunServerWithConfig(cf) + defer s.Shutdown() + url := s.ClientURL() + if updateJwt(t, url, uSysCreds, aBadJwt, 1) != 0 { + t.Fatal("Expected negative response") + } + if updateJwt(t, url, uSysCreds, aGoodJwt, 1) != 1 { + t.Fatal("Expected positive response") + } + connectTest(url) + }) + + t.Run("mem-resolver", func(t *testing.T) { + dirSrv := createDir(t, "srv") + defer os.RemoveAll(dirSrv) + cf := createConfFile(t, []byte(fmt.Sprintf(` + port: -1 + operator = %s + resolver: MEMORY + resolver_preload = { + %s : "%s" + %s : "%s" + %s : "%s" + } + `, oJwt, aSysPub, aSysJwt, aBadPub, aBadJwt, aGoodPub, aGoodJwt))) + defer os.Remove(cf) + s, _ := RunServerWithConfig(cf) + defer s.Shutdown() + connectTest(s.ClientURL()) + }) +} diff --git a/server/nkey.go b/server/nkey.go index a792c6e0..87b20a4f 100644 --- a/server/nkey.go +++ b/server/nkey.go @@ -33,7 +33,7 @@ func (s *Server) NonceRequired() bool { // nonceRequired tells us if we should send a nonce. // Lock should be held on entry. func (s *Server) nonceRequired() bool { - return len(s.nkeys) > 0 || len(s.trustedKeys) > 0 + return len(s.nkeys) > 0 || s.trustedKeys != nil } // Generate a nonce for INFO challenge. diff --git a/server/server.go b/server/server.go index afe08ddb..60962674 100644 --- a/server/server.go +++ b/server/server.go @@ -194,6 +194,8 @@ type Server struct { // Trusted public operator keys. trustedKeys []string + // map of trusted keys to operator setting StrictSigningKeyUsage + strictSigningKeyUsage map[string]struct{} // We use this to minimize mem copies for requests to monitoring // endpoint /varz (when it comes from http). @@ -419,6 +421,8 @@ func NewServer(opts *Options) (*Server, error) { if a == nil { sac := NewAccount(s.opts.SystemAccount) sac.Issuer = opts.TrustedOperators[0].Issuer + sac.signingKeys = map[string]jwt.Scope{} + sac.signingKeys[s.opts.SystemAccount] = nil s.registerAccountNoLock(sac) } } @@ -748,7 +752,7 @@ func (s *Server) generateRouteInfoJSON() { func (s *Server) globalAccountOnly() bool { var hasOthers bool - if len(s.trustedKeys) > 0 { + if s.trustedKeys != nil { return false } @@ -798,7 +802,7 @@ func (s *Server) isTrustedIssuer(issuer string) bool { s.mu.Lock() defer s.mu.Unlock() // If we are not running in trusted mode and there is no issuer, that is ok. - if len(s.trustedKeys) == 0 && issuer == "" { + if s.trustedKeys == nil && issuer == "" { return true } for _, tk := range s.trustedKeys { @@ -812,6 +816,7 @@ func (s *Server) isTrustedIssuer(issuer string) bool { // processTrustedKeys will process binary stamped and // options-based trusted nkeys. Returns success. func (s *Server) processTrustedKeys() bool { + s.strictSigningKeyUsage = map[string]struct{}{} if trustedKeys != "" && !s.initStampedTrustedKeys() { return false } else if s.opts.TrustedKeys != nil { @@ -820,7 +825,15 @@ func (s *Server) processTrustedKeys() bool { return false } } - s.trustedKeys = s.opts.TrustedKeys + s.trustedKeys = append([]string(nil), s.opts.TrustedKeys...) + for _, claim := range s.opts.TrustedOperators { + if !claim.StrictSigningKeyUsage { + continue + } + for _, key := range claim.SigningKeys { + s.strictSigningKeyUsage[key] = struct{}{} + } + } } return true } diff --git a/vendor/github.com/nats-io/jwt/v2/account_claims.go b/vendor/github.com/nats-io/jwt/v2/account_claims.go index 2037c111..67169483 100644 --- a/vendor/github.com/nats-io/jwt/v2/account_claims.go +++ b/vendor/github.com/nats-io/jwt/v2/account_claims.go @@ -148,7 +148,7 @@ func NewAccountClaims(subject string) *AccountClaims { c.Limits = OperatorLimits{ NatsLimits{NoLimit, NoLimit, NoLimit}, AccountLimits{NoLimit, NoLimit, true, NoLimit, NoLimit}, - JetStreamLimits{NoLimit, NoLimit, NoLimit, NoLimit}} + JetStreamLimits{0, 0, 0, 0}} c.Subject = subject return c } diff --git a/vendor/github.com/nats-io/jwt/v2/operator_claims.go b/vendor/github.com/nats-io/jwt/v2/operator_claims.go index a41d9c04..61d474e5 100644 --- a/vendor/github.com/nats-io/jwt/v2/operator_claims.go +++ b/vendor/github.com/nats-io/jwt/v2/operator_claims.go @@ -43,6 +43,8 @@ type Operator struct { SystemAccount string `json:"system_account,omitempty"` // Min Server version AssertServerVersion string `json:"assert_server_version,omitempty"` + // Signing of subordinate objects will require signing keys + StrictSigningKeyUsage bool `json:"strict_signing_key_usage,omitempty"` GenericFields } @@ -174,7 +176,7 @@ func (oc *OperatorClaims) DidSign(op Claims) bool { } issuer := op.Claims().Issuer if issuer == oc.Subject { - return true + return !oc.StrictSigningKeyUsage } return oc.SigningKeys.Contains(issuer) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 60277f2c..c4e2429a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,7 +4,7 @@ github.com/klauspost/compress/s2 # github.com/minio/highwayhash v1.0.0 ## explicit github.com/minio/highwayhash -# github.com/nats-io/jwt/v2 v2.0.0-20210107222814-18c5cc45d263 +# github.com/nats-io/jwt/v2 v2.0.0-20210125223648-1c24d462becc ## explicit github.com/nats-io/jwt/v2 # github.com/nats-io/nats.go v1.10.1-0.20210122204956-b8ea7fc17ea6