Files
nats-server/server/jwt.go
Matthias Hanel dea9effa8d [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 <mh@synadia.com>
2021-01-26 17:49:58 -05:00

212 lines
6.3 KiB
Go

// Copyright 2018-2019 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"io/ioutil"
"net"
"regexp"
"strings"
"time"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
)
var nscDecoratedRe = regexp.MustCompile(`\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}[\n]*))`)
// All JWTs once encoded start with this
const jwtPrefix = "eyJ"
// ReadOperatorJWT will read a jwt file for an operator claim. This can be a decorated file.
func ReadOperatorJWT(jwtfile string) (*jwt.OperatorClaims, error) {
contents, err := ioutil.ReadFile(jwtfile)
if err != nil {
// Check to see if the JWT has been inlined.
if !strings.HasPrefix(jwtfile, jwtPrefix) {
return nil, err
}
// We may have an inline jwt here.
contents = []byte(jwtfile)
}
defer wipeSlice(contents)
var claim string
items := nscDecoratedRe.FindAllSubmatch(contents, -1)
if len(items) == 0 {
claim = string(contents)
} else {
// First result should be the JWT.
// We copy here so that if the file contained a seed file too we wipe appropriately.
raw := items[0][1]
tmp := make([]byte, len(raw))
copy(tmp, raw)
claim = string(tmp)
}
opc, err := jwt.DecodeOperatorClaims(claim)
if err != nil {
return nil, err
}
return opc, nil
}
// Just wipe slice with 'x', for clearing contents of nkey seed file.
func wipeSlice(buf []byte) {
for i := range buf {
buf[i] = 'x'
}
}
// validateTrustedOperators will check that we do not have conflicts with
// assigned trusted keys and trusted operators. If operators are defined we
// will expand the trusted keys in options.
func validateTrustedOperators(o *Options) error {
if len(o.TrustedOperators) == 0 {
return nil
}
if o.AllowNewAccounts {
return fmt.Errorf("operators do not allow dynamic creation of new accounts")
}
if o.AccountResolver == nil {
return fmt.Errorf("operators require an account resolver to be configured")
}
if len(o.Accounts) > 0 {
return fmt.Errorf("operators do not allow Accounts to be configured directly")
}
if len(o.Users) > 0 || len(o.Nkeys) > 0 {
return fmt.Errorf("operators do not allow users to be configured directly")
}
if len(o.TrustedOperators) > 0 && len(o.TrustedKeys) > 0 {
return fmt.Errorf("conflicting options for 'TrustedKeys' and 'TrustedOperators'")
}
if o.SystemAccount != "" {
foundSys := false
foundNonEmpty := false
for _, op := range o.TrustedOperators {
if op.SystemAccount != "" {
foundNonEmpty = true
}
if op.SystemAccount == o.SystemAccount {
foundSys = true
break
}
}
if foundNonEmpty && !foundSys {
return fmt.Errorf("system_account in config and operator JWT must be identical")
}
}
srvMajor, srvMinor, srvUpdate, _ := jwt.ParseServerVersion(strings.Split(VERSION, "-")[0])
for _, opc := range o.TrustedOperators {
if major, minor, update, err := jwt.ParseServerVersion(opc.AssertServerVersion); err != nil {
return fmt.Errorf("operator %s expects version %s got error instead: %s",
opc.Subject, opc.AssertServerVersion, err)
} else if major > srvMajor {
return fmt.Errorf("operator %s expected major version %d > server major version %d",
opc.Subject, major, srvMajor)
} else if srvMajor > major {
} else if minor > srvMinor {
return fmt.Errorf("operator %s expected minor version %d > server minor version %d",
opc.Subject, minor, srvMinor)
} else if srvMinor > minor {
} else if update > srvUpdate {
return fmt.Errorf("operator %s expected update version %d > server update version %d",
opc.Subject, update, srvUpdate)
}
}
// If we have operators, fill in the trusted keys.
// FIXME(dlc) - We had TrustedKeys before TrustedOperators. The jwt.OperatorClaims
// has a DidSign(). Use that longer term. For now we can expand in place.
for _, opc := range o.TrustedOperators {
if o.TrustedKeys == nil {
o.TrustedKeys = make([]string, 0, 4)
}
if !opc.StrictSigningKeyUsage {
o.TrustedKeys = append(o.TrustedKeys, opc.Subject)
}
o.TrustedKeys = append(o.TrustedKeys, opc.SigningKeys...)
}
for _, key := range o.TrustedKeys {
if !nkeys.IsValidPublicOperatorKey(key) {
return fmt.Errorf("trusted Keys %q are required to be a valid public operator nkey", key)
}
}
return nil
}
func validateSrc(claims *jwt.UserClaims, host string) bool {
if claims == nil {
return false
} else if len(claims.Src) == 0 {
return true
} else if host == "" {
return false
}
ip := net.ParseIP(host)
if ip == nil {
return false
}
for _, cidr := range claims.Src {
if _, net, err := net.ParseCIDR(cidr); err != nil {
return false // should not happen as this jwt is invalid
} else if net.Contains(ip) {
return true
}
}
return false
}
func validateTimes(claims *jwt.UserClaims) (bool, time.Duration) {
if claims == nil {
return false, time.Duration(0)
} else if len(claims.Times) == 0 {
return true, time.Duration(0)
}
now := time.Now()
loc := time.Local
if claims.Locale != "" {
var err error
if loc, err = time.LoadLocation(claims.Locale); err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
now = now.In(loc)
}
for _, timeRange := range claims.Times {
y, m, d := now.Date()
m = m - 1
d = d - 1
start, err := time.ParseInLocation("15:04:05", timeRange.Start, loc)
if err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
end, err := time.ParseInLocation("15:04:05", timeRange.End, loc)
if err != nil {
return false, time.Duration(0) // parsing not expected to fail at this point
}
if start.After(end) {
start = start.AddDate(y, int(m), d)
d++ // the intent is to be the next day
} else {
start = start.AddDate(y, int(m), d)
}
if start.Before(now) {
end = end.AddDate(y, int(m), d)
if end.After(now) {
return true, end.Sub(now)
}
}
}
return false, time.Duration(0)
}