Skip to content

Commit

Permalink
Added support for account signing keys. (#962)
Browse files Browse the repository at this point in the history
* Added support for account signing keys. When account signing keys change
the validity of the client JWT and token imports need to be checked as well
as it is possible for the signing key used to sign the user or import
token to have been removed from the source account.
  • Loading branch information
aricart committed Apr 19, 2019
1 parent bc11c1c commit 84a7e28
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 67 deletions.
6 changes: 3 additions & 3 deletions go.mod
Expand Up @@ -2,9 +2,9 @@ module github.com/nats-io/gnatsd

require (
github.com/nats-io/go-nats v1.7.2
github.com/nats-io/jwt v0.1.0
github.com/nats-io/jwt v0.2.4
github.com/nats-io/nkeys v0.0.2
github.com/nats-io/nuid v1.0.1
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369
)
10 changes: 10 additions & 0 deletions go.sum
Expand Up @@ -4,6 +4,12 @@ github.com/nats-io/jwt v0.0.8 h1:oQsISWFvSmzKEs13h6X7p+8jQaXa9/X2fnBWoU2Zh4g=
github.com/nats-io/jwt v0.0.8/go.mod h1:mQxQ0uHQ9FhEVPIcTSKwx2lqZEpXWWcCgA7R6NrWvvY=
github.com/nats-io/jwt v0.1.0 h1:xO7kj8fyt0ECycBVG6WtOW+TnX8Aax4tI8i2fwspUro=
github.com/nats-io/jwt v0.1.0/go.mod h1:mQxQ0uHQ9FhEVPIcTSKwx2lqZEpXWWcCgA7R6NrWvvY=
github.com/nats-io/jwt v0.2.2 h1:tvCi/V+vJjSxOVmh5Zs3FN+n4jWDME4tH8BmGuZcnN0=
github.com/nats-io/jwt v0.2.2/go.mod h1:mQxQ0uHQ9FhEVPIcTSKwx2lqZEpXWWcCgA7R6NrWvvY=
github.com/nats-io/jwt v0.2.3-0.20190415222205-ef45966189e5 h1:hTRkgUpVZlCym185L0HG6tkO+EQxV4y9YOrsWx7K0wA=
github.com/nats-io/jwt v0.2.3-0.20190415222205-ef45966189e5/go.mod h1:mQxQ0uHQ9FhEVPIcTSKwx2lqZEpXWWcCgA7R6NrWvvY=
github.com/nats-io/jwt v0.2.4 h1:SZ27ZMqlP8US5QYFE+GPYtZZQa7PZUVZ6lNV3L7Z7EU=
github.com/nats-io/jwt v0.2.4/go.mod h1:mQxQ0uHQ9FhEVPIcTSKwx2lqZEpXWWcCgA7R6NrWvvY=
github.com/nats-io/nkeys v0.0.2 h1:+qM7QpgXnvDDixitZtQUBDY9w/s9mu1ghS+JIbsrx6M=
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
github.com/nats-io/nuid v0.0.0-20180712044959-3024a71c3cbe h1:2nFZc8mo/vXfkJX5mTrTUUhHt6mIHwDoamuqIs3U1jU=
Expand All @@ -17,8 +23,12 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/sys v0.0.0-20170627012538-f7928cfef4d0 h1:zBqTV7BZW0C9WnFZU5Izl5ZfxL5+qtufgtwXGFU4t7g=
golang.org/x/sys v0.0.0-20170627012538-f7928cfef4d0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 h1:1Fzlr8kkDLQwqMP8GxrhptBLqZG/EDpiATneiZHY998=
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369 h1:aBlRBZoCuZNRDClvfkDoklQqdLzBaA3uViASg2z2p24=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
91 changes: 84 additions & 7 deletions server/accounts.go
Expand Up @@ -51,10 +51,11 @@ type Account struct {
imports importMap
exports exportMap
limits
nae int32
pruning bool
expired bool
srv *Server // server this account is registered with (possibly nil)
nae int32
pruning bool
expired bool
signingKeys []string
srv *Server // server this account is registered with (possibly nil)
}

// Account based limits.
Expand Down Expand Up @@ -648,6 +649,9 @@ func (a *Account) checkActivation(acc *Account, claim *jwt.Import, expTimer bool
if vr.IsBlocking(true) {
return false
}
if !a.isIssuerClaimTrusted(act) {
return false
}
if act.Expires != 0 {
tn := time.Now().Unix()
if act.Expires <= tn {
Expand All @@ -660,9 +664,29 @@ func (a *Account) checkActivation(acc *Account, claim *jwt.Import, expTimer bool
})
}
}

return true
}

// Returns true if the activation claim is trusted. That is the issuer matches
// the account or is an entry in the signing keys.
func (a *Account) isIssuerClaimTrusted(claims *jwt.ActivationClaims) bool {
// if no issuer account, issuer is the account
if claims.IssuerAccount == "" {
return true
}
// get the referenced account
if a.srv != nil {
ia, err := a.srv.lookupAccount(claims.IssuerAccount)
if err != nil {
return false
}
return ia.hasIssuer(claims.Issuer)
}
// couldn't verify
return false
}

// Returns true if `a` and `b` stream imports are the same. Note that the
// check is done with the account's name, not the pointer. This is used
// during config reload where we are comparing current and new config
Expand Down Expand Up @@ -793,6 +817,23 @@ func (a *Account) checkExpiration(claims *jwt.ClaimsData) {
a.expired = false
}

// hasIssuer returns true if the issuer matches the account
// issuer or it is a signing key for the account.
func (a *Account) hasIssuer(issuer string) bool {
a.mu.RLock()
defer a.mu.RUnlock()
// same issuer
if a.Issuer == issuer {
return true
}
for i := 0; i < len(a.signingKeys); i++ {
if a.signingKeys[i] == issuer {
return true
}
}
return false
}

// Placeholder for signaling token auth required.
var tokenAuthReq = []*Account{}

Expand Down Expand Up @@ -831,13 +872,34 @@ func (s *Server) updateAccountClaims(a *Account, ac *jwt.AccountClaims) {
s.Debugf("Updating account claims: %s", a.Name)
a.checkExpiration(ac.Claims())

a.mu.Lock()
// Clone to update, only select certain fields.
old := &Account{Name: a.Name, imports: a.imports, exports: a.exports, limits: a.limits}
old := &Account{Name: a.Name, imports: a.imports, exports: a.exports, limits: a.limits, signingKeys: a.signingKeys}

// Reset exports and imports here.
a.exports = exportMap{}
a.imports = importMap{}

// update account signing keys
a.signingKeys = nil
signersChanged := false
if len(ac.SigningKeys) > 0 {
// insure copy the new keys and sort
a.signingKeys = append(a.signingKeys, ac.SigningKeys...)
sort.Strings(a.signingKeys)
}
if len(a.signingKeys) != len(old.signingKeys) {
signersChanged = true
} else {
for i := 0; i < len(old.signingKeys); i++ {
if a.signingKeys[i] != old.signingKeys[i] {
signersChanged = true
break
}
}
}
a.mu.Unlock()

gatherClients := func() []*client {
a.mu.RLock()
clients := make([]*client, 0, len(a.clients))
Expand Down Expand Up @@ -891,7 +953,7 @@ func (s *Server) updateAccountClaims(a *Account, ac *jwt.AccountClaims) {
}
}
// Now check if stream exports have changed.
if !a.checkStreamExportsEqual(old) {
if !a.checkStreamExportsEqual(old) || signersChanged {
clients := make([]*client, 0, 16)
// We need to check all accounts that have an import claim from this account.
awcsti := map[string]struct{}{}
Expand All @@ -915,7 +977,7 @@ func (s *Server) updateAccountClaims(a *Account, ac *jwt.AccountClaims) {
}
}
// Now check if service exports have changed.
if !a.checkServiceExportsEqual(old) {
if !a.checkServiceExportsEqual(old) || signersChanged {
for _, acc := range s.accounts {
acc.mu.Lock()
for _, im := range acc.imports.services {
Expand Down Expand Up @@ -949,6 +1011,18 @@ func (s *Server) updateAccountClaims(a *Account, ac *jwt.AccountClaims) {
c.applyAccountLimits()
c.mu.Unlock()
}

// Check if the signing keys changed, might have to evict
if signersChanged {
for _, c := range clients {
c.mu.Lock()
sk := c.user.SigningKey
c.mu.Unlock()
if sk != "" && !a.hasIssuer(sk) {
c.closeConnection(AuthenticationViolation)
}
}
}
}

// Helper to build an internal account structure from a jwt.AccountClaims.
Expand All @@ -962,6 +1036,9 @@ func (s *Server) buildInternalAccount(ac *jwt.AccountClaims) *Account {
// Helper to build internal NKeyUser.
func buildInternalNkeyUser(uc *jwt.UserClaims, acc *Account) *NkeyUser {
nu := &NkeyUser{Nkey: uc.Subject, Account: acc}
if uc.IssuerAccount != "" {
nu.SigningKey = uc.Issuer
}

// Now check for permissions.
var p *Permissions
Expand Down
11 changes: 10 additions & 1 deletion server/auth.go
Expand Up @@ -47,6 +47,7 @@ type NkeyUser struct {
Nkey string `json:"user"`
Permissions *Permissions `json:"permissions,omitempty"`
Account *Account `json:"account,omitempty"`
SigningKey string `json:"signing_key,omitempty"`
}

// User is for multiple accounts/users.
Expand Down Expand Up @@ -348,14 +349,22 @@ func (s *Server) isClientAuthorized(c *client) bool {
// If we have a jwt and a userClaim, make sure we have the Account, etc associated.
// We need to look up the account. This will use an account resolver if one is present.
if juc != nil {
if acc, _ = s.LookupAccount(juc.Issuer); acc == nil {
issuer := juc.Issuer
if juc.IssuerAccount != "" {
issuer = juc.IssuerAccount
}
if acc, _ = s.LookupAccount(issuer); acc == nil {
c.Debugf("Account JWT can not be found")
return false
}
if !s.isTrustedIssuer(acc.Issuer) {
c.Debugf("Account JWT not signed by trusted operator")
return false
}
if juc.IssuerAccount != "" && !acc.hasIssuer(juc.Issuer) {
c.Debugf("User JWT issuer is not known")
return false
}
if acc.IsExpired() {
c.Debugf("Account JWT has expired")
return false
Expand Down

0 comments on commit 84a7e28

Please sign in to comment.