Bump github.com/nats-io/nats-server/v2 from 2.9.19 to 2.9.21

Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.9.19 to 2.9.21.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/.goreleaser.yml)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.9.19...v2.9.21)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2023-08-30 13:11:47 +02:00
committed by Ralf Haferkamp
parent 0aafeccb93
commit 6d55325a41
61 changed files with 5460 additions and 861 deletions
+2 -3
View File
@@ -90,9 +90,8 @@ type advancedState struct {
ii uint16 // position of last match, intended to overflow to reset.
// input window: unprocessed data is window[index:windowEnd]
index int
estBitsPerByte int
hashMatch [maxMatchLength + minMatchLength]uint32
index int
hashMatch [maxMatchLength + minMatchLength]uint32
// Input hash chains
// hashHead[hashValue] contains the largest inputIndex with the specified hash value
-5
View File
@@ -34,11 +34,6 @@ const (
// Should preferably be a multiple of 6, since
// we accumulate 6 bytes between writes to the buffer.
bufferFlushSize = 246
// bufferSize is the actual output byte buffer size.
// It must have additional headroom for a flush
// which can contain up to 8 bytes.
bufferSize = bufferFlushSize + 8
)
// Minimum length code that emits bits.
-19
View File
@@ -42,25 +42,6 @@ func quickSortByFreq(data []literalNode, a, b, maxDepth int) {
}
}
// siftDownByFreq implements the heap property on data[lo, hi).
// first is an offset into the array where the root of the heap lies.
func siftDownByFreq(data []literalNode, lo, hi, first int) {
root := lo
for {
child := 2*root + 1
if child >= hi {
break
}
if child+1 < hi && (data[first+child].freq == data[first+child+1].freq && data[first+child].literal < data[first+child+1].literal || data[first+child].freq < data[first+child+1].freq) {
child++
}
if data[first+root].freq == data[first+child].freq && data[first+root].literal > data[first+child].literal || data[first+root].freq > data[first+child].freq {
return
}
data[first+root], data[first+child] = data[first+child], data[first+root]
root = child
}
}
func doPivotByFreq(data []literalNode, lo, hi int) (midlo, midhi int) {
m := int(uint(lo+hi) >> 1) // Written like this to avoid integer overflow.
if hi-lo > 40 {
-1
View File
@@ -742,7 +742,6 @@ searchDict:
x := load64(src, s-2)
m2Hash := hash6(x, tableBits)
currHash := hash6(x>>8, tableBits)
candidate = int(table[currHash])
table[m2Hash] = uint32(s - 2)
table[currHash] = uint32(s - 1)
cv = load64(src, s)
+25 -19
View File
@@ -157,7 +157,6 @@ func encodeBlockBetterGo(dst, src []byte) (d int) {
index0 := base + 1
index1 := s - 2
cv = load64(src, s)
for index0 < index1 {
cv0 := load64(src, index0)
cv1 := load64(src, index1)
@@ -269,18 +268,21 @@ func encodeBlockBetterGo(dst, src []byte) (d int) {
lTable[hash7(cv0, lTableBits)] = uint32(index0)
sTable[hash4(cv0>>8, sTableBits)] = uint32(index0 + 1)
// lTable could be postponed, but very minor difference.
lTable[hash7(cv1, lTableBits)] = uint32(index1)
sTable[hash4(cv1>>8, sTableBits)] = uint32(index1 + 1)
index0 += 1
index1 -= 1
cv = load64(src, s)
// index every second long in between.
for index0 < index1 {
// Index large values sparsely in between.
// We do two starting from different offsets for speed.
index2 := (index0 + index1 + 1) >> 1
for index2 < index1 {
lTable[hash7(load64(src, index0), lTableBits)] = uint32(index0)
lTable[hash7(load64(src, index1), lTableBits)] = uint32(index1)
lTable[hash7(load64(src, index2), lTableBits)] = uint32(index2)
index0 += 2
index1 -= 2
index2 += 2
}
}
@@ -459,12 +461,14 @@ func encodeBlockBetterSnappyGo(dst, src []byte) (d int) {
index1 -= 1
cv = load64(src, s)
// index every second long in between.
for index0 < index1 {
// Index large values sparsely in between.
// We do two starting from different offsets for speed.
index2 := (index0 + index1 + 1) >> 1
for index2 < index1 {
lTable[hash7(load64(src, index0), lTableBits)] = uint32(index0)
lTable[hash7(load64(src, index1), lTableBits)] = uint32(index1)
lTable[hash7(load64(src, index2), lTableBits)] = uint32(index2)
index0 += 2
index1 -= 2
index2 += 2
}
}
@@ -599,7 +603,6 @@ searchDict:
if s >= sLimit {
break searchDict
}
cv = load64(src, s)
// Index in-between
index0 := base + 1
index1 := s - 2
@@ -865,12 +868,14 @@ searchDict:
index1 -= 1
cv = load64(src, s)
// index every second long in between.
for index0 < index1 {
// Index large values sparsely in between.
// We do two starting from different offsets for speed.
index2 := (index0 + index1 + 1) >> 1
for index2 < index1 {
lTable[hash7(load64(src, index0), lTableBits)] = uint32(index0)
lTable[hash7(load64(src, index1), lTableBits)] = uint32(index1)
lTable[hash7(load64(src, index2), lTableBits)] = uint32(index2)
index0 += 2
index1 -= 2
index2 += 2
}
}
@@ -961,7 +966,6 @@ searchDict:
index0 := base + 1
index1 := s - 2
cv = load64(src, s)
for index0 < index1 {
cv0 := load64(src, index0)
cv1 := load64(src, index1)
@@ -1079,12 +1083,14 @@ searchDict:
index1 -= 1
cv = load64(src, s)
// index every second long in between.
for index0 < index1 {
// Index large values sparsely in between.
// We do two starting from different offsets for speed.
index2 := (index0 + index1 + 1) >> 1
for index2 < index1 {
lTable[hash7(load64(src, index0), lTableBits)] = uint32(index0)
lTable[hash7(load64(src, index1), lTableBits)] = uint32(index1)
lTable[hash7(load64(src, index2), lTableBits)] = uint32(index2)
index0 += 2
index1 -= 2
index2 += 2
}
}
File diff suppressed because it is too large Load Diff
+7
View File
@@ -147,6 +147,13 @@ type Reader struct {
ignoreCRC bool
}
// GetBufferCapacity returns the capacity of the internal buffer.
// This might be useful to know when reusing the same reader in combination
// with the lazy buffer option.
func (r *Reader) GetBufferCapacity() int {
return cap(r.buf)
}
// ensureBufferSize will ensure that the buffer can take at least n bytes.
// If false is returned the buffer exceeds maximum allowed size.
func (r *Reader) ensureBufferSize(n int) bool {
+1 -1
View File
@@ -771,7 +771,7 @@ func (w *Writer) closeIndex(idx bool) ([]byte, error) {
}
var index []byte
if w.err(nil) == nil && w.writer != nil {
if w.err(err) == nil && w.writer != nil {
// Create index.
if idx {
compSize := int64(-1)
+3 -1
View File
@@ -146,7 +146,9 @@ func parse(data, fp string, pedantic bool) (p *parser, err error) {
return nil, err
}
}
if len(p.mapping) == 0 {
return nil, fmt.Errorf("config has no values or is empty")
}
return p, nil
}
+28 -7
View File
@@ -38,13 +38,37 @@ type Logger struct {
fl *fileLogger
}
// NewStdLogger creates a logger with output directed to Stderr
func NewStdLogger(time, debug, trace, colors, pid bool) *Logger {
type LogOption interface {
isLoggerOption()
}
// LogUTC controls whether timestamps in the log output should be UTC or local time.
type LogUTC bool
func (l LogUTC) isLoggerOption() {}
func logFlags(time bool, opts ...LogOption) int {
flags := 0
if time {
flags = log.LstdFlags | log.Lmicroseconds
}
for _, opt := range opts {
switch v := opt.(type) {
case LogUTC:
if time && bool(v) {
flags |= log.LUTC
}
}
}
return flags
}
// NewStdLogger creates a logger with output directed to Stderr
func NewStdLogger(time, debug, trace, colors, pid bool, opts ...LogOption) *Logger {
flags := logFlags(time, opts...)
pre := ""
if pid {
pre = pidPrefix()
@@ -66,11 +90,8 @@ func NewStdLogger(time, debug, trace, colors, pid bool) *Logger {
}
// NewFileLogger creates a logger with output directed to a file
func NewFileLogger(filename string, time, debug, trace, pid bool) *Logger {
flags := 0
if time {
flags = log.LstdFlags | log.Lmicroseconds
}
func NewFileLogger(filename string, time, debug, trace, pid bool, opts ...LogOption) *Logger {
flags := logFlags(time, opts...)
pre := ""
if pid {
+297
View File
@@ -0,0 +1,297 @@
// Copyright 2023 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 certidp
import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
const (
DefaultAllowedClockSkew = 30 * time.Second
DefaultOCSPResponderTimeout = 2 * time.Second
DefaultTTLUnsetNextUpdate = 1 * time.Hour
)
type StatusAssertion int
var (
StatusAssertionStrToVal = map[string]StatusAssertion{
"good": ocsp.Good,
"revoked": ocsp.Revoked,
"unknown": ocsp.Unknown,
}
StatusAssertionValToStr = map[StatusAssertion]string{
ocsp.Good: "good",
ocsp.Revoked: "revoked",
ocsp.Unknown: "unknown",
}
StatusAssertionIntToVal = map[int]StatusAssertion{
0: ocsp.Good,
1: ocsp.Revoked,
2: ocsp.Unknown,
}
)
func GetStatusAssertionStr(sa int) string {
return StatusAssertionValToStr[StatusAssertionIntToVal[sa]]
}
func (sa StatusAssertion) MarshalJSON() ([]byte, error) {
str, ok := StatusAssertionValToStr[sa]
if !ok {
// set unknown as fallback
str = StatusAssertionValToStr[ocsp.Unknown]
}
return json.Marshal(str)
}
func (sa *StatusAssertion) UnmarshalJSON(in []byte) error {
v, ok := StatusAssertionStrToVal[strings.ReplaceAll(string(in), "\"", "")]
if !ok {
// set unknown as fallback
v = StatusAssertionStrToVal["unknown"]
}
*sa = v
return nil
}
type ChainLink struct {
Leaf *x509.Certificate
Issuer *x509.Certificate
OCSPWebEndpoints *[]*url.URL
}
// OCSPPeerConfig holds the parsed OCSP peer configuration section of TLS configuration
type OCSPPeerConfig struct {
Verify bool
Timeout float64
ClockSkew float64
WarnOnly bool
UnknownIsGood bool
AllowWhenCAUnreachable bool
TTLUnsetNextUpdate float64
}
func NewOCSPPeerConfig() *OCSPPeerConfig {
return &OCSPPeerConfig{
Verify: false,
Timeout: DefaultOCSPResponderTimeout.Seconds(),
ClockSkew: DefaultAllowedClockSkew.Seconds(),
WarnOnly: false,
UnknownIsGood: false,
AllowWhenCAUnreachable: false,
TTLUnsetNextUpdate: DefaultTTLUnsetNextUpdate.Seconds(),
}
}
// Log is a neutral method of passing server loggers to plugins
type Log struct {
Debugf func(format string, v ...interface{})
Noticef func(format string, v ...interface{})
Warnf func(format string, v ...interface{})
Errorf func(format string, v ...interface{})
Tracef func(format string, v ...interface{})
}
type CertInfo struct {
Subject string `json:"subject,omitempty"`
Issuer string `json:"issuer,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Raw []byte `json:"raw,omitempty"`
}
var OCSPPeerUsage = `
For client, leaf spoke (remotes), and leaf hub connections, you may enable OCSP peer validation:
tls {
...
# mTLS must be enabled (with exception of Leaf remotes)
verify: true
...
# short form enables peer verify and takes option defaults
ocsp_peer: true
# long form includes settable options
ocsp_peer {
# Enable OCSP peer validation (default false)
verify: true
# OCSP responder timeout in seconds (may be fractional, default 2 seconds)
ca_timeout: 2
# Allowed skew between server and OCSP responder time in seconds (may be fractional, default 30 seconds)
allowed_clockskew: 30
# Warn-only and never reject connections (default false)
warn_only: false
# Treat response Unknown status as valid certificate (default false)
unknown_is_good: false
# Warn-only if no CA response can be obtained and no cached revocation exists (default false)
allow_when_ca_unreachable: false
# If response NextUpdate unset by CA, set a default cache TTL in seconds from ThisUpdate (default 1 hour)
cache_ttl_when_next_update_unset: 3600
}
...
}
Note: OCSP validation for route and gateway connections is enabled using the 'ocsp' configuration option.
`
// GenerateFingerprint returns a base64-encoded SHA256 hash of the raw certificate
func GenerateFingerprint(cert *x509.Certificate) string {
data := sha256.Sum256(cert.Raw)
return base64.StdEncoding.EncodeToString(data[:])
}
func getWebEndpoints(uris []string) []*url.URL {
var urls []*url.URL
for _, uri := range uris {
endpoint, err := url.ParseRequestURI(uri)
if err != nil {
// skip invalid URLs
continue
}
if endpoint.Scheme != "http" && endpoint.Scheme != "https" {
// skip non-web URLs
continue
}
urls = append(urls, endpoint)
}
return urls
}
// GetSubjectDNForm returns RDN sequence concatenation of the certificate's subject to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetSubjectDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Subject.ToRDNSequence()), "+")
}
// GetIssuerDNForm returns RDN sequence concatenation of the certificate's issuer to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetIssuerDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Issuer.ToRDNSequence()), "+")
}
// CertOCSPEligible checks if the certificate's issuer has populated AIA with OCSP responder endpoint(s)
// and is thus eligible for OCSP validation
func CertOCSPEligible(link *ChainLink) bool {
if link == nil || link.Leaf.Raw == nil || len(link.Leaf.Raw) == 0 {
return false
}
if link.Leaf.OCSPServer == nil || len(link.Leaf.OCSPServer) == 0 {
return false
}
urls := getWebEndpoints(link.Leaf.OCSPServer)
if len(urls) == 0 {
return false
}
link.OCSPWebEndpoints = &urls
return true
}
// GetLeafIssuerCert returns the issuer certificate of the leaf (positional) certificate in the chain
func GetLeafIssuerCert(chain []*x509.Certificate, leafPos int) *x509.Certificate {
if len(chain) == 0 || leafPos < 0 {
return nil
}
// self-signed certificate or too-big leafPos
if leafPos >= len(chain)-1 {
return nil
}
// returns pointer to issuer cert or nil
return (chain)[leafPos+1]
}
// OCSPResponseCurrent checks if the OCSP response is current (i.e. not expired and not future effective)
func OCSPResponseCurrent(ocspr *ocsp.Response, opts *OCSPPeerConfig, log *Log) bool {
skew := time.Duration(opts.ClockSkew * float64(time.Second))
if skew < 0*time.Second {
skew = DefaultAllowedClockSkew
}
now := time.Now().UTC()
// Typical effectivity check based on CA response ThisUpdate and NextUpdate semantics
if !ocspr.NextUpdate.IsZero() && ocspr.NextUpdate.Before(now.Add(-1*skew)) {
t := ocspr.NextUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseExpired, t, nt, skew)
return false
}
// CA responder can assert NextUpdate unset, in which case use config option to set a default cache TTL
if ocspr.NextUpdate.IsZero() {
ttl := time.Duration(opts.TTLUnsetNextUpdate * float64(time.Second))
if ttl < 0*time.Second {
ttl = DefaultTTLUnsetNextUpdate
}
expiryTime := ocspr.ThisUpdate.Add(ttl)
if expiryTime.Before(now.Add(-1 * skew)) {
t := expiryTime.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseTTLExpired, t, nt, skew)
return false
}
}
if ocspr.ThisUpdate.After(now.Add(skew)) {
t := ocspr.ThisUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseFutureDated, t, nt, skew)
return false
}
return true
}
// ValidDelegationCheck checks if the CA OCSP Response was signed by a valid CA Issuer delegate as per (RFC 6960, section 4.2.2.2)
// If a valid delegate or direct-signed by CA Issuer, true returned.
func ValidDelegationCheck(iss *x509.Certificate, ocspr *ocsp.Response) bool {
// This call assumes prior successful parse and signature validation of the OCSP response
// The Go OCSP library (as of x/crypto/ocsp v0.9) will detect and perform a 1-level delegate signature check but does not
// implement the additional criteria for delegation specified in RFC 6960, section 4.2.2.2.
if iss == nil || ocspr == nil {
return false
}
// not a delegation, no-op
if ocspr.Certificate == nil {
return true
}
// delegate is self-same with CA Issuer, not a delegation although response issued in that form
if ocspr.Certificate.Equal(iss) {
return true
}
// we need to verify CA Issuer stamped id-kp-OCSPSigning on delegate
delegatedSigner := false
for _, keyUseExt := range ocspr.Certificate.ExtKeyUsage {
if keyUseExt == x509.ExtKeyUsageOCSPSigning {
delegatedSigner = true
break
}
}
return delegatedSigner
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2023 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 certidp
var (
// Returned errors
ErrIllegalPeerOptsConfig = "expected map to define OCSP peer options, got [%T]"
ErrIllegalCacheOptsConfig = "expected map to define OCSP peer cache options, got [%T]"
ErrParsingPeerOptFieldGeneric = "error parsing tls peer config, unknown field [%q]"
ErrParsingPeerOptFieldTypeConversion = "error parsing tls peer config, conversion error: %s"
ErrParsingCacheOptFieldTypeConversion = "error parsing OCSP peer cache config, conversion error: %s"
ErrUnableToPlugTLSEmptyConfig = "unable to plug TLS verify connection, config is nil"
ErrMTLSRequired = "OCSP peer verification for client connections requires TLS verify (mTLS) to be enabled"
ErrUnableToPlugTLSClient = "unable to register client OCSP verification"
ErrUnableToPlugTLSServer = "unable to register server OCSP verification"
ErrCannotWriteCompressed = "error writing to compression writer: %w"
ErrCannotReadCompressed = "error reading compression reader: %w"
ErrTruncatedWrite = "short write on body (%d != %d)"
ErrCannotCloseWriter = "error closing compression writer: %w"
ErrParsingCacheOptFieldGeneric = "error parsing OCSP peer cache config, unknown field [%q]"
ErrUnknownCacheType = "error parsing OCSP peer cache config, unknown type [%s]"
ErrInvalidChainlink = "invalid chain link"
ErrBadResponderHTTPStatus = "bad OCSP responder http status: [%d]"
ErrNoAvailOCSPServers = "no available OCSP servers"
ErrFailedWithAllRequests = "exhausted OCSP responders: %w"
// Direct logged errors
ErrLoadCacheFail = "Unable to load OCSP peer cache: %s"
ErrSaveCacheFail = "Unable to save OCSP peer cache: %s"
ErrBadCacheTypeConfig = "Unimplemented OCSP peer cache type [%v]"
ErrResponseCompressFail = "Unable to compress OCSP response for key [%s]: %s"
ErrResponseDecompressFail = "Unable to decompress OCSP response for key [%s]: %s"
ErrPeerEmptyNoEvent = "Peer certificate is nil, cannot send OCSP peer reject event"
ErrPeerEmptyAutoReject = "Peer certificate is nil, rejecting OCSP peer"
// Debug information
DbgPlugTLSForKind = "Plugging TLS OCSP peer for [%s]"
DbgNumServerChains = "Peer OCSP enabled: %d TLS server chain(s) will be evaluated"
DbgNumClientChains = "Peer OCSP enabled: %d TLS client chain(s) will be evaluated"
DbgLinksInChain = "Chain [%d]: %d total link(s)"
DbgSelfSignedValid = "Chain [%d] is self-signed, thus peer is valid"
DbgValidNonOCSPChain = "Chain [%d] has no OCSP eligible links, thus peer is valid"
DbgChainIsOCSPEligible = "Chain [%d] has %d OCSP eligible link(s)"
DbgChainIsOCSPValid = "Chain [%d] is OCSP valid for all eligible links, thus peer is valid"
DbgNoOCSPValidChains = "No OCSP valid chains, thus peer is invalid"
DbgCheckingCacheForCert = "Checking OCSP peer cache for [%s], key [%s]"
DbgCurrentResponseCached = "Cached OCSP response is current, status [%s]"
DbgExpiredResponseCached = "Cached OCSP response is expired, status [%s]"
DbgOCSPValidPeerLink = "OCSP verify pass for [%s]"
DbgCachingResponse = "Caching OCSP response for [%s], key [%s]"
DbgAchievedCompression = "OCSP response compression ratio: [%f]"
DbgCacheHit = "OCSP peer cache hit for key [%s]"
DbgCacheMiss = "OCSP peer cache miss for key [%s]"
DbgPreservedRevocation = "Revoked OCSP response for key [%s] preserved by cache policy"
DbgDeletingCacheResponse = "Deleting OCSP peer cached response for key [%s]"
DbgStartingCache = "Starting OCSP peer cache"
DbgStoppingCache = "Stopping OCSP peer cache"
DbgLoadingCache = "Loading OCSP peer cache [%s]"
DbgNoCacheFound = "No OCSP peer cache found, starting with empty cache"
DbgSavingCache = "Saving OCSP peer cache [%s]"
DbgCacheSaved = "Saved OCSP peer cache successfully (%d bytes)"
DbgMakingCARequest = "Trying OCSP responder url [%s]"
DbgResponseExpired = "OCSP response NextUpdate [%s] is before now [%s] with clockskew [%s]"
DbgResponseTTLExpired = "OCSP response cache expiry [%s] is before now [%s] with clockskew [%s]"
DbgResponseFutureDated = "OCSP response ThisUpdate [%s] is before now [%s] with clockskew [%s]"
DbgCacheSaveTimerExpired = "OCSP peer cache save timer expired"
DbgCacheDirtySave = "OCSP peer cache is dirty, saving"
// Returned to peer as TLS reject reason
MsgTLSClientRejectConnection = "client not OCSP valid"
MsgTLSServerRejectConnection = "server not OCSP valid"
// Expected runtime errors (direct logged)
ErrCAResponderCalloutFail = "Attempt to obtain OCSP response from CA responder for [%s] failed: %s"
ErrNewCAResponseNotCurrent = "New OCSP CA response obtained for [%s] but not current"
ErrCAResponseParseFailed = "Could not parse OCSP CA response for [%s]: %s"
ErrOCSPInvalidPeerLink = "OCSP verify fail for [%s] with CA status [%s]"
// Policy override warnings (direct logged)
MsgAllowWhenCAUnreachableOccurred = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; no cached revocation so allowing"
MsgAllowWhenCAUnreachableOccurredCachedRevoke = "Failed to obtain OCSP CA response for [%s] but AllowWhenCAUnreachable set; cached revocation exists so rejecting"
MsgAllowWarnOnlyOccurred = "OCSP verify fail for [%s] but WarnOnly is true so allowing"
// Info (direct logged)
MsgCacheOnline = "OCSP peer cache online, type [%s]"
MsgCacheOffline = "OCSP peer cache offline, type [%s]"
// OCSP cert invalid reasons (debug and event reasons)
MsgFailedOCSPResponseFetch = "Failed OCSP response fetch"
MsgOCSPResponseNotEffective = "OCSP response not in effectivity window"
MsgFailedOCSPResponseParse = "Failed OCSP response parse"
MsgOCSPResponseInvalidStatus = "Invalid OCSP response status: %s"
MsgOCSPResponseDelegationInvalid = "Invalid OCSP response delegation: %s"
MsgCachedOCSPResponseInvalid = "Invalid cached OCSP response for [%s] with fingerprint [%s]"
)
@@ -0,0 +1,83 @@
// Copyright 2023 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 certidp
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"
"time"
"golang.org/x/crypto/ocsp"
)
func FetchOCSPResponse(link *ChainLink, opts *OCSPPeerConfig, log *Log) ([]byte, error) {
if link == nil || link.Leaf == nil || link.Issuer == nil || opts == nil || log == nil {
return nil, fmt.Errorf(ErrInvalidChainlink)
}
timeout := time.Duration(opts.Timeout * float64(time.Second))
if timeout <= 0*time.Second {
timeout = DefaultOCSPResponderTimeout
}
getRequestBytes := func(u string, hc *http.Client) ([]byte, error) {
resp, err := hc.Get(u)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(ErrBadResponderHTTPStatus, resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
// Request documentation:
// https://tools.ietf.org/html/rfc6960#appendix-A.1
reqDER, err := ocsp.CreateRequest(link.Leaf, link.Issuer, nil)
if err != nil {
return nil, err
}
reqEnc := base64.StdEncoding.EncodeToString(reqDER)
responders := *link.OCSPWebEndpoints
if len(responders) == 0 {
return nil, fmt.Errorf(ErrNoAvailOCSPServers)
}
var raw []byte
hc := &http.Client{
Timeout: timeout,
}
for _, u := range responders {
url := u.String()
log.Debugf(DbgMakingCARequest, url)
url = strings.TrimSuffix(url, "/")
raw, err = getRequestBytes(fmt.Sprintf("%s/%s", url, reqEnc), hc)
if err == nil {
break
}
}
if err != nil {
return nil, fmt.Errorf(ErrFailedWithAllRequests, err)
}
return raw, nil
}
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2022-2023 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 certstore
import (
"crypto"
"crypto/x509"
"io"
"runtime"
"strings"
)
type StoreType int
const MATCHBYEMPTY = 0
const STOREEMPTY = 0
const (
windowsCurrentUser StoreType = iota + 1
windowsLocalMachine
)
var StoreMap = map[string]StoreType{
"windowscurrentuser": windowsCurrentUser,
"windowslocalmachine": windowsLocalMachine,
}
var StoreOSMap = map[StoreType]string{
windowsCurrentUser: "windows",
windowsLocalMachine: "windows",
}
type MatchByType int
const (
matchByIssuer MatchByType = iota + 1
matchBySubject
)
var MatchByMap = map[string]MatchByType{
"issuer": matchByIssuer,
"subject": matchBySubject,
}
var Usage = `
In place of cert_file and key_file you may use the windows certificate store:
tls {
cert_store: "WindowsCurrentUser"
cert_match_by: "Subject"
cert_match: "MyServer123"
}
`
func ParseCertStore(certStore string) (StoreType, error) {
certStoreType, exists := StoreMap[strings.ToLower(certStore)]
if !exists {
return 0, ErrBadCertStore
}
validOS, exists := StoreOSMap[certStoreType]
if !exists || validOS != runtime.GOOS {
return 0, ErrOSNotCompatCertStore
}
return certStoreType, nil
}
func ParseCertMatchBy(certMatchBy string) (MatchByType, error) {
certMatchByType, exists := MatchByMap[strings.ToLower(certMatchBy)]
if !exists {
return 0, ErrBadMatchByType
}
return certMatchByType, nil
}
func GetLeafIssuer(leaf *x509.Certificate, vOpts x509.VerifyOptions) (issuer *x509.Certificate) {
chains, err := leaf.Verify(vOpts)
if err != nil || len(chains) == 0 {
issuer = nil
} else {
issuer = chains[0][1]
}
return
}
// credential provides access to a public key and is a crypto.Signer.
type credential interface {
// Public returns the public key corresponding to the leaf certificate.
Public() crypto.PublicKey
// Sign signs digest with the private key.
Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error)
}
@@ -0,0 +1,46 @@
// Copyright 2022-2023 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.
//go:build !windows
package certstore
import (
"crypto"
"crypto/tls"
"io"
)
var _ = MATCHBYEMPTY
// otherKey implements crypto.Signer and crypto.Decrypter to satisfy linter on platforms that don't implement certstore
type otherKey struct{}
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, config *tls.Config) error {
_, _, _, _ = certStore, certMatchBy, certMatch, config
return ErrOSNotCompatCertStore
}
// Public always returns nil public key since this is a stub on non-supported platform
func (k otherKey) Public() crypto.PublicKey {
return nil
}
// Sign always returns a nil signature since this is a stub on non-supported platform
func (k otherKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) {
_, _, _ = rand, digest, opts
return nil, nil
}
// Verify interface conformance.
var _ credential = &otherKey{}
@@ -0,0 +1,827 @@
// Copyright 2022-2023 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.
//
// Adapted, updated, and enhanced from CertToStore, https://github.com/google/certtostore/releases/tag/v1.0.2
// Apache License, Version 2.0, Copyright 2017 Google Inc.
package certstore
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/binary"
"fmt"
"io"
"math/big"
"reflect"
"sync"
"syscall"
"unicode/utf16"
"unsafe"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
"golang.org/x/sys/windows"
)
const (
// wincrypt.h constants
winAcquireCached = 0x1 // CRYPT_ACQUIRE_CACHE_FLAG
winAcquireSilent = 0x40 // CRYPT_ACQUIRE_SILENT_FLAG
winAcquireOnlyNCryptKey = 0x40000 // CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG
winEncodingX509ASN = 1 // X509_ASN_ENCODING
winEncodingPKCS7 = 65536 // PKCS_7_ASN_ENCODING
winCertStoreProvSystem = 10 // CERT_STORE_PROV_SYSTEM
winCertStoreCurrentUser = uint32(winCertStoreCurrentUserID << winCompareShift) // CERT_SYSTEM_STORE_CURRENT_USER
winCertStoreLocalMachine = uint32(winCertStoreLocalMachineID << winCompareShift) // CERT_SYSTEM_STORE_LOCAL_MACHINE
winCertStoreCurrentUserID = 1 // CERT_SYSTEM_STORE_CURRENT_USER_ID
winCertStoreLocalMachineID = 2 // CERT_SYSTEM_STORE_LOCAL_MACHINE_ID
winInfoIssuerFlag = 4 // CERT_INFO_ISSUER_FLAG
winInfoSubjectFlag = 7 // CERT_INFO_SUBJECT_FLAG
winCompareNameStrW = 8 // CERT_COMPARE_NAME_STR_A
winCompareShift = 16 // CERT_COMPARE_SHIFT
// Reference https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
winFindIssuerStr = winCompareNameStrW<<winCompareShift | winInfoIssuerFlag // CERT_FIND_ISSUER_STR_W
winFindSubjectStr = winCompareNameStrW<<winCompareShift | winInfoSubjectFlag // CERT_FIND_SUBJECT_STR_W
winNcryptKeySpec = 0xFFFFFFFF // CERT_NCRYPT_KEY_SPEC
winBCryptPadPKCS1 uintptr = 0x2
winBCryptPadPSS uintptr = 0x8 // Modern TLS 1.2+
winBCryptPadPSSSalt uint32 = 32 // default 20, 32 optimal for typical SHA256 hash
winRSA1Magic = 0x31415352 // "RSA1" BCRYPT_RSAPUBLIC_MAGIC
winECS1Magic = 0x31534345 // "ECS1" BCRYPT_ECDSA_PUBLIC_P256_MAGIC
winECS3Magic = 0x33534345 // "ECS3" BCRYPT_ECDSA_PUBLIC_P384_MAGIC
winECS5Magic = 0x35534345 // "ECS5" BCRYPT_ECDSA_PUBLIC_P521_MAGIC
winECK1Magic = 0x314B4345 // "ECK1" BCRYPT_ECDH_PUBLIC_P256_MAGIC
winECK3Magic = 0x334B4345 // "ECK3" BCRYPT_ECDH_PUBLIC_P384_MAGIC
winECK5Magic = 0x354B4345 // "ECK5" BCRYPT_ECDH_PUBLIC_P521_MAGIC
winCryptENotFound = 0x80092004 // CRYPT_E_NOT_FOUND
providerMSSoftware = "Microsoft Software Key Storage Provider"
)
var (
winBCryptRSAPublicBlob = winWide("RSAPUBLICBLOB")
winBCryptECCPublicBlob = winWide("ECCPUBLICBLOB")
winNCryptAlgorithmGroupProperty = winWide("Algorithm Group") // NCRYPT_ALGORITHM_GROUP_PROPERTY
winNCryptUniqueNameProperty = winWide("Unique Name") // NCRYPT_UNIQUE_NAME_PROPERTY
winNCryptECCCurveNameProperty = winWide("ECCCurveName") // NCRYPT_ECC_CURVE_NAME_PROPERTY
winCurveIDs = map[uint32]elliptic.Curve{
winECS1Magic: elliptic.P256(), // BCRYPT_ECDSA_PUBLIC_P256_MAGIC
winECS3Magic: elliptic.P384(), // BCRYPT_ECDSA_PUBLIC_P384_MAGIC
winECS5Magic: elliptic.P521(), // BCRYPT_ECDSA_PUBLIC_P521_MAGIC
winECK1Magic: elliptic.P256(), // BCRYPT_ECDH_PUBLIC_P256_MAGIC
winECK3Magic: elliptic.P384(), // BCRYPT_ECDH_PUBLIC_P384_MAGIC
winECK5Magic: elliptic.P521(), // BCRYPT_ECDH_PUBLIC_P521_MAGIC
}
winCurveNames = map[string]elliptic.Curve{
"nistP256": elliptic.P256(), // BCRYPT_ECC_CURVE_NISTP256
"nistP384": elliptic.P384(), // BCRYPT_ECC_CURVE_NISTP384
"nistP521": elliptic.P521(), // BCRYPT_ECC_CURVE_NISTP521
}
winAlgIDs = map[crypto.Hash]*uint16{
crypto.SHA1: winWide("SHA1"), // BCRYPT_SHA1_ALGORITHM
crypto.SHA256: winWide("SHA256"), // BCRYPT_SHA256_ALGORITHM
crypto.SHA384: winWide("SHA384"), // BCRYPT_SHA384_ALGORITHM
crypto.SHA512: winWide("SHA512"), // BCRYPT_SHA512_ALGORITHM
}
// MY is well-known system store on Windows that holds personal certificates
winMyStore = winWide("MY")
// These DLLs must be available on all Windows hosts
winCrypt32 = windows.MustLoadDLL("crypt32.dll")
winNCrypt = windows.MustLoadDLL("ncrypt.dll")
winCertFindCertificateInStore = winCrypt32.MustFindProc("CertFindCertificateInStore")
winCryptAcquireCertificatePrivateKey = winCrypt32.MustFindProc("CryptAcquireCertificatePrivateKey")
winNCryptExportKey = winNCrypt.MustFindProc("NCryptExportKey")
winNCryptOpenStorageProvider = winNCrypt.MustFindProc("NCryptOpenStorageProvider")
winNCryptGetProperty = winNCrypt.MustFindProc("NCryptGetProperty")
winNCryptSignHash = winNCrypt.MustFindProc("NCryptSignHash")
winFnGetProperty = winGetProperty
)
type winPKCS1PaddingInfo struct {
pszAlgID *uint16
}
type winPSSPaddingInfo struct {
pszAlgID *uint16
cbSalt uint32
}
// TLSConfig fulfills the same function as reading cert and key pair from pem files but
// sources the Windows certificate store instead
func TLSConfig(certStore StoreType, certMatchBy MatchByType, certMatch string, config *tls.Config) error {
var (
leaf *x509.Certificate
leafCtx *windows.CertContext
pk *winKey
vOpts = x509.VerifyOptions{}
chains [][]*x509.Certificate
chain []*x509.Certificate
rawChain [][]byte
)
// By StoreType, open a store
if certStore == windowsCurrentUser || certStore == windowsLocalMachine {
var scope uint32
cs, err := winOpenCertStore(providerMSSoftware)
if err != nil || cs == nil {
return err
}
if certStore == windowsCurrentUser {
scope = winCertStoreCurrentUser
}
if certStore == windowsLocalMachine {
scope = winCertStoreLocalMachine
}
// certByIssuer or certBySubject
if certMatchBy == matchBySubject || certMatchBy == MATCHBYEMPTY {
leaf, leafCtx, err = cs.certBySubject(certMatch, scope)
} else if certMatchBy == matchByIssuer {
leaf, leafCtx, err = cs.certByIssuer(certMatch, scope)
} else {
return ErrBadMatchByType
}
if err != nil {
// pass through error from cert search
return err
}
if leaf == nil || leafCtx == nil {
return ErrFailedCertSearch
}
pk, err = cs.certKey(leafCtx)
if err != nil {
return err
}
if pk == nil {
return ErrNoPrivateKeyStoreRef
}
} else {
return ErrBadCertStore
}
// Get intermediates in the cert store for the found leaf IFF there is a full chain of trust in the store
// otherwise just use leaf as the final chain.
//
// Using std lib Verify as a reliable way to get valid chains out of the win store for the leaf; however,
// using empty options since server TLS stanza could be TLS role as server identity or client identity.
chains, err := leaf.Verify(vOpts)
if err != nil || len(chains) == 0 {
chains = append(chains, []*x509.Certificate{leaf})
}
// We have at least one verified chain so pop the first chain and remove the self-signed CA cert (if present)
// from the end of the chain
chain = chains[0]
if len(chain) > 1 {
chain = chain[:len(chain)-1]
}
// For tls.Certificate.Certificate need a [][]byte from []*x509.Certificate
// Approximate capacity for efficiency
rawChain = make([][]byte, 0, len(chain))
for _, link := range chain {
rawChain = append(rawChain, link.Raw)
}
tlsCert := tls.Certificate{
Certificate: rawChain,
PrivateKey: pk,
Leaf: leaf,
}
config.Certificates = []tls.Certificate{tlsCert}
// note: pk is a windows pointer (not freed by Go) but needs to live the life of the server for Signing.
// The cert context (leafCtx) windows pointer must not be freed underneath the pk so also life of the server.
return nil
}
// winWide returns a pointer to uint16 representing the equivalent
// to a Windows LPCWSTR.
func winWide(s string) *uint16 {
w := utf16.Encode([]rune(s))
w = append(w, 0)
return &w[0]
}
// winOpenProvider gets a provider handle for subsequent calls
func winOpenProvider(provider string) (uintptr, error) {
var hProv uintptr
pname := winWide(provider)
// Open the provider, the last parameter is not used
r, _, err := winNCryptOpenStorageProvider.Call(uintptr(unsafe.Pointer(&hProv)), uintptr(unsafe.Pointer(pname)), 0)
if r == 0 {
return hProv, nil
}
return hProv, fmt.Errorf("NCryptOpenStorageProvider returned %X: %v", r, err)
}
// winFindCert wraps the CertFindCertificateInStore library call. Note that any cert context passed
// into prev will be freed. If no certificate was found, nil will be returned.
func winFindCert(store windows.Handle, enc, findFlags, findType uint32, para *uint16, prev *windows.CertContext) (*windows.CertContext, error) {
h, _, err := winCertFindCertificateInStore.Call(
uintptr(store),
uintptr(enc),
uintptr(findFlags),
uintptr(findType),
uintptr(unsafe.Pointer(para)),
uintptr(unsafe.Pointer(prev)),
)
if h == 0 {
// Actual error, or simply not found?
if errno, ok := err.(syscall.Errno); ok && errno == winCryptENotFound {
return nil, ErrFailedCertSearch
}
return nil, ErrFailedCertSearch
}
// nolint:govet
return (*windows.CertContext)(unsafe.Pointer(h)), nil
}
// winCertStore is a store implementation for the Windows Certificate Store
type winCertStore struct {
Prov uintptr
ProvName string
stores map[string]*winStoreHandle
mu sync.Mutex
}
// winOpenCertStore creates a winCertStore
func winOpenCertStore(provider string) (*winCertStore, error) {
cngProv, err := winOpenProvider(provider)
if err != nil {
// pass through error from winOpenProvider
return nil, err
}
wcs := &winCertStore{
Prov: cngProv,
ProvName: provider,
stores: make(map[string]*winStoreHandle),
}
return wcs, nil
}
// winCertContextToX509 creates an x509.Certificate from a Windows cert context.
func winCertContextToX509(ctx *windows.CertContext) (*x509.Certificate, error) {
var der []byte
slice := (*reflect.SliceHeader)(unsafe.Pointer(&der))
slice.Data = uintptr(unsafe.Pointer(ctx.EncodedCert))
slice.Len = int(ctx.Length)
slice.Cap = int(ctx.Length)
return x509.ParseCertificate(der)
}
// certByIssuer matches and returns the first certificate found by passed issuer.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_ISSUER_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certByIssuer(issuer string, storeType uint32) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindIssuerStr, issuer, winMyStore, storeType)
}
// certBySubject matches and returns the first certificate found by passed subject field.
// CertContext pointer returned allows subsequent key operations like Sign. Caller specifies
// current user's personal certs or local machine's personal certs using storeType.
// See CERT_FIND_SUBJECT_STR description at https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certfindcertificateinstore
func (w *winCertStore) certBySubject(subject string, storeType uint32) (*x509.Certificate, *windows.CertContext, error) {
return w.certSearch(winFindSubjectStr, subject, winMyStore, storeType)
}
// certSearch is a helper function to lookup certificates based on search type and match value.
// store is used to specify which store to perform the lookup in (system or user).
func (w *winCertStore) certSearch(searchType uint32, matchValue string, searchRoot *uint16, store uint32) (*x509.Certificate, *windows.CertContext, error) {
// store handle to "MY" store
h, err := w.storeHandle(store, searchRoot)
if err != nil {
return nil, nil, err
}
var prev *windows.CertContext
var cert *x509.Certificate
i, err := windows.UTF16PtrFromString(matchValue)
if err != nil {
return nil, nil, ErrFailedCertSearch
}
// pass 0 as the third parameter because it is not used
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa376064(v=vs.85).aspx
nc, err := winFindCert(h, winEncodingX509ASN|winEncodingPKCS7, 0, searchType, i, prev)
if err != nil {
return nil, nil, err
}
if nc != nil {
// certificate found
prev = nc
// Extract the DER-encoded certificate from the cert context
xc, err := winCertContextToX509(nc)
if err == nil {
cert = xc
} else {
return nil, nil, ErrFailedX509Extract
}
} else {
return nil, nil, ErrFailedCertSearch
}
if cert == nil {
return nil, nil, ErrFailedX509Extract
}
return cert, prev, nil
}
type winStoreHandle struct {
handle *windows.Handle
}
func winNewStoreHandle(provider uint32, store *uint16) (*winStoreHandle, error) {
var s winStoreHandle
if s.handle != nil {
return &s, nil
}
st, err := windows.CertOpenStore(
winCertStoreProvSystem,
0,
0,
provider,
uintptr(unsafe.Pointer(store)))
if err != nil {
return nil, ErrBadCryptoStoreProvider
}
s.handle = &st
return &s, nil
}
// winKey implements crypto.Signer and crypto.Decrypter for key based operations.
type winKey struct {
handle uintptr
pub crypto.PublicKey
Container string
AlgorithmGroup string
}
// Public exports a public key to implement crypto.Signer
func (k winKey) Public() crypto.PublicKey {
return k.pub
}
// Sign returns the signature of a hash to implement crypto.Signer
func (k winKey) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
switch k.AlgorithmGroup {
case "ECDSA", "ECDH":
return winSignECDSA(k.handle, digest)
case "RSA":
hf := opts.HashFunc()
algID, ok := winAlgIDs[hf]
if !ok {
return nil, ErrBadRSAHashAlgorithm
}
switch opts.(type) {
case *rsa.PSSOptions:
return winSignRSAPSSPadding(k.handle, digest, algID)
default:
return winSignRSAPKCS1Padding(k.handle, digest, algID)
}
default:
return nil, ErrBadSigningAlgorithm
}
}
func winSignECDSA(kh uintptr, digest []byte) ([]byte, error) {
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
0,
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrStoreECDSASigningError
}
// Obtain the signature data
buf := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
0,
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrStoreECDSASigningError
}
if len(buf) != int(size) {
return nil, ErrStoreECDSASigningError
}
return winPackECDSASigValue(bytes.NewReader(buf[:size]), len(digest))
}
func winPackECDSASigValue(r io.Reader, digestLength int) ([]byte, error) {
sigR := make([]byte, digestLength)
if _, err := io.ReadFull(r, sigR); err != nil {
return nil, ErrStoreECDSASigningError
}
sigS := make([]byte, digestLength)
if _, err := io.ReadFull(r, sigS); err != nil {
return nil, ErrStoreECDSASigningError
}
var b cryptobyte.Builder
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
b.AddASN1BigInt(new(big.Int).SetBytes(sigR))
b.AddASN1BigInt(new(big.Int).SetBytes(sigS))
})
return b.Bytes()
}
func winSignRSAPKCS1Padding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
// PKCS#1 v1.5 padding for some TLS 1.2
padInfo := winPKCS1PaddingInfo{pszAlgID: algID}
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
winBCryptPadPKCS1)
if r != 0 {
return nil, ErrStoreRSASigningError
}
// Obtain the signature data
sig := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&sig[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
winBCryptPadPKCS1)
if r != 0 {
return nil, ErrStoreRSASigningError
}
return sig[:size], nil
}
func winSignRSAPSSPadding(kh uintptr, digest []byte, algID *uint16) ([]byte, error) {
// PSS padding for TLS 1.3 and some TLS 1.2
padInfo := winPSSPaddingInfo{pszAlgID: algID, cbSalt: winBCryptPadPSSSalt}
var size uint32
// Obtain the size of the signature
r, _, _ := winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
0,
0,
uintptr(unsafe.Pointer(&size)),
winBCryptPadPSS)
if r != 0 {
return nil, ErrStoreRSASigningError
}
// Obtain the signature data
sig := make([]byte, size)
r, _, _ = winNCryptSignHash.Call(
kh,
uintptr(unsafe.Pointer(&padInfo)),
uintptr(unsafe.Pointer(&digest[0])),
uintptr(len(digest)),
uintptr(unsafe.Pointer(&sig[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
winBCryptPadPSS)
if r != 0 {
return nil, ErrStoreRSASigningError
}
return sig[:size], nil
}
// certKey wraps CryptAcquireCertificatePrivateKey. It obtains the CNG private
// key of a known certificate and returns a pointer to a winKey which implements
// both crypto.Signer. When a nil cert context is passed
// a nil key is intentionally returned, to model the expected behavior of a
// non-existent cert having no private key.
// https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptacquirecertificateprivatekey
func (w *winCertStore) certKey(cert *windows.CertContext) (*winKey, error) {
// Return early if a nil cert was passed.
if cert == nil {
return nil, nil
}
var (
kh uintptr
spec uint32
mustFree int
)
r, _, _ := winCryptAcquireCertificatePrivateKey.Call(
uintptr(unsafe.Pointer(cert)),
winAcquireCached|winAcquireSilent|winAcquireOnlyNCryptKey,
0, // Reserved, must be null.
uintptr(unsafe.Pointer(&kh)),
uintptr(unsafe.Pointer(&spec)),
uintptr(unsafe.Pointer(&mustFree)),
)
// If the function succeeds, the return value is nonzero (TRUE).
if r == 0 {
return nil, ErrNoPrivateKeyStoreRef
}
if mustFree != 0 {
return nil, ErrNoPrivateKeyStoreRef
}
if spec != winNcryptKeySpec {
return nil, ErrNoPrivateKeyStoreRef
}
return winKeyMetadata(kh)
}
func winKeyMetadata(kh uintptr) (*winKey, error) {
// uc is used to populate the unique container name attribute of the private key
uc, err := winGetPropertyStr(kh, winNCryptUniqueNameProperty)
if err != nil {
// unable to determine key unique name
return nil, ErrExtractingPrivateKeyMetadata
}
alg, err := winGetPropertyStr(kh, winNCryptAlgorithmGroupProperty)
if err != nil {
// unable to determine key algorithm
return nil, ErrExtractingPrivateKeyMetadata
}
var pub crypto.PublicKey
switch alg {
case "ECDSA", "ECDH":
buf, err := winExport(kh, winBCryptECCPublicBlob)
if err != nil {
// failed to export ECC public key
return nil, ErrExtractingECCPublicKey
}
pub, err = unmarshalECC(buf, kh)
if err != nil {
return nil, ErrExtractingECCPublicKey
}
case "RSA":
buf, err := winExport(kh, winBCryptRSAPublicBlob)
if err != nil {
return nil, ErrExtractingRSAPublicKey
}
pub, err = winUnmarshalRSA(buf)
if err != nil {
return nil, ErrExtractingRSAPublicKey
}
default:
return nil, ErrBadPublicKeyAlgorithm
}
return &winKey{handle: kh, pub: pub, Container: uc, AlgorithmGroup: alg}, nil
}
func winGetProperty(kh uintptr, property *uint16) ([]byte, error) {
var strSize uint32
r, _, _ := winNCryptGetProperty.Call(
kh,
uintptr(unsafe.Pointer(property)),
0,
0,
uintptr(unsafe.Pointer(&strSize)),
0,
0)
if r != 0 {
return nil, ErrExtractPropertyFromKey
}
buf := make([]byte, strSize)
r, _, _ = winNCryptGetProperty.Call(
kh,
uintptr(unsafe.Pointer(property)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(strSize),
uintptr(unsafe.Pointer(&strSize)),
0,
0)
if r != 0 {
return nil, ErrExtractPropertyFromKey
}
return buf, nil
}
func winGetPropertyStr(kh uintptr, property *uint16) (string, error) {
buf, err := winFnGetProperty(kh, property)
if err != nil {
return "", ErrExtractPropertyFromKey
}
uc := bytes.ReplaceAll(buf, []byte{0x00}, []byte(""))
return string(uc), nil
}
func winExport(kh uintptr, blobType *uint16) ([]byte, error) {
var size uint32
// When obtaining the size of a public key, most parameters are not required
r, _, _ := winNCryptExportKey.Call(
kh,
0,
uintptr(unsafe.Pointer(blobType)),
0,
0,
0,
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrExtractingPublicKey
}
// Place the exported key in buf now that we know the size required
buf := make([]byte, size)
r, _, _ = winNCryptExportKey.Call(
kh,
0,
uintptr(unsafe.Pointer(blobType)),
0,
uintptr(unsafe.Pointer(&buf[0])),
uintptr(size),
uintptr(unsafe.Pointer(&size)),
0)
if r != 0 {
return nil, ErrExtractingPublicKey
}
return buf, nil
}
func unmarshalECC(buf []byte, kh uintptr) (*ecdsa.PublicKey, error) {
// BCRYPT_ECCKEY_BLOB from bcrypt.h
header := struct {
Magic uint32
Key uint32
}{}
r := bytes.NewReader(buf)
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
return nil, ErrExtractingECCPublicKey
}
curve, ok := winCurveIDs[header.Magic]
if !ok {
// Fix for b/185945636, where despite specifying the curve, nCrypt returns
// an incorrect response with BCRYPT_ECDSA_PUBLIC_GENERIC_MAGIC.
var err error
curve, err = winCurveName(kh)
if err != nil {
// unsupported header magic or cannot match the curve by name
return nil, err
}
}
keyX := make([]byte, header.Key)
if n, err := r.Read(keyX); n != int(header.Key) || err != nil {
// failed to read key X
return nil, ErrExtractingECCPublicKey
}
keyY := make([]byte, header.Key)
if n, err := r.Read(keyY); n != int(header.Key) || err != nil {
// failed to read key Y
return nil, ErrExtractingECCPublicKey
}
pub := &ecdsa.PublicKey{
Curve: curve,
X: new(big.Int).SetBytes(keyX),
Y: new(big.Int).SetBytes(keyY),
}
return pub, nil
}
// winCurveName reads the curve name property and returns the corresponding curve.
func winCurveName(kh uintptr) (elliptic.Curve, error) {
cn, err := winGetPropertyStr(kh, winNCryptECCCurveNameProperty)
if err != nil {
// unable to determine the curve property name
return nil, ErrExtractPropertyFromKey
}
curve, ok := winCurveNames[cn]
if !ok {
// unknown curve name
return nil, ErrBadECCCurveName
}
return curve, nil
}
func winUnmarshalRSA(buf []byte) (*rsa.PublicKey, error) {
// BCRYPT_RSA_BLOB from bcrypt.h
header := struct {
Magic uint32
BitLength uint32
PublicExpSize uint32
ModulusSize uint32
UnusedPrime1 uint32
UnusedPrime2 uint32
}{}
r := bytes.NewReader(buf)
if err := binary.Read(r, binary.LittleEndian, &header); err != nil {
return nil, ErrExtractingRSAPublicKey
}
if header.Magic != winRSA1Magic {
// invalid header magic
return nil, ErrExtractingRSAPublicKey
}
if header.PublicExpSize > 8 {
// unsupported public exponent size
return nil, ErrExtractingRSAPublicKey
}
exp := make([]byte, 8)
if n, err := r.Read(exp[8-header.PublicExpSize:]); n != int(header.PublicExpSize) || err != nil {
// failed to read public exponent
return nil, ErrExtractingRSAPublicKey
}
mod := make([]byte, header.ModulusSize)
if n, err := r.Read(mod); n != int(header.ModulusSize) || err != nil {
// failed to read modulus
return nil, ErrExtractingRSAPublicKey
}
pub := &rsa.PublicKey{
N: new(big.Int).SetBytes(mod),
E: int(binary.BigEndian.Uint64(exp)),
}
return pub, nil
}
// storeHandle returns a handle to a given cert store, opening the handle as needed.
func (w *winCertStore) storeHandle(provider uint32, store *uint16) (windows.Handle, error) {
w.mu.Lock()
defer w.mu.Unlock()
key := fmt.Sprintf("%d%s", provider, windows.UTF16PtrToString(store))
var err error
if w.stores[key] == nil {
w.stores[key], err = winNewStoreHandle(provider, store)
if err != nil {
return 0, ErrBadCryptoStoreProvider
}
}
return *w.stores[key].handle, nil
}
// Verify interface conformance.
var _ credential = &winKey{}
+73
View File
@@ -0,0 +1,73 @@
package certstore
import (
"errors"
)
var (
// ErrBadCryptoStoreProvider represents inablity to establish link with a certificate store
ErrBadCryptoStoreProvider = errors.New("unable to open certificate store or store not available")
// ErrBadRSAHashAlgorithm represents a bad or unsupported RSA hash algorithm
ErrBadRSAHashAlgorithm = errors.New("unsupported RSA hash algorithm")
// ErrBadSigningAlgorithm represents a bad or unsupported signing algorithm
ErrBadSigningAlgorithm = errors.New("unsupported signing algorithm")
// ErrStoreRSASigningError represents an error returned from store during RSA signature
ErrStoreRSASigningError = errors.New("unable to obtain RSA signature from store")
// ErrStoreECDSASigningError represents an error returned from store during ECDSA signature
ErrStoreECDSASigningError = errors.New("unable to obtain ECDSA signature from store")
// ErrNoPrivateKeyStoreRef represents an error getting a handle to a private key in store
ErrNoPrivateKeyStoreRef = errors.New("unable to obtain private key handle from store")
// ErrExtractingPrivateKeyMetadata represents a family of errors extracting metadata about the private key in store
ErrExtractingPrivateKeyMetadata = errors.New("unable to extract private key metadata")
// ErrExtractingECCPublicKey represents an error exporting ECC-type public key from store
ErrExtractingECCPublicKey = errors.New("unable to extract ECC public key from store")
// ErrExtractingRSAPublicKey represents an error exporting RSA-type public key from store
ErrExtractingRSAPublicKey = errors.New("unable to extract RSA public key from store")
// ErrExtractingPublicKey represents a general error exporting public key from store
ErrExtractingPublicKey = errors.New("unable to extract public key from store")
// ErrBadPublicKeyAlgorithm represents a bad or unsupported public key algorithm
ErrBadPublicKeyAlgorithm = errors.New("unsupported public key algorithm")
// ErrExtractPropertyFromKey represents a general failure to extract a metadata property field
ErrExtractPropertyFromKey = errors.New("unable to extract property from key")
// ErrBadECCCurveName represents an ECC signature curve name that is bad or unsupported
ErrBadECCCurveName = errors.New("unsupported ECC curve name")
// ErrFailedCertSearch represents not able to find certificate in store
ErrFailedCertSearch = errors.New("unable to find certificate in store")
// ErrFailedX509Extract represents not being able to extract x509 certificate from found cert in store
ErrFailedX509Extract = errors.New("unable to extract x509 from certificate")
// ErrBadMatchByType represents unknown CERT_MATCH_BY passed
ErrBadMatchByType = errors.New("cert match by type not implemented")
// ErrBadCertStore represents unknown CERT_STORE passed
ErrBadCertStore = errors.New("cert store type not implemented")
// ErrConflictCertFileAndStore represents ambiguous configuration of both file and store
ErrConflictCertFileAndStore = errors.New("'cert_file' and 'cert_store' may not both be configured")
// ErrBadCertStoreField represents malformed cert_store option
ErrBadCertStoreField = errors.New("expected 'cert_store' to be a valid non-empty string")
// ErrBadCertMatchByField represents malformed cert_match_by option
ErrBadCertMatchByField = errors.New("expected 'cert_match_by' to be a valid non-empty string")
// ErrBadCertMatchField represents malformed cert_match option
ErrBadCertMatchField = errors.New("expected 'cert_match' to be a valid non-empty string")
// ErrOSNotCompatCertStore represents cert_store passed that exists but is not valid on current OS
ErrOSNotCompatCertStore = errors.New("cert_store not compatible with current operating system")
)
+44 -40
View File
@@ -789,15 +789,16 @@ func (c *client) subsAtLimit() bool {
}
func minLimit(value *int32, limit int32) bool {
if *value != jwt.NoLimit {
v := atomic.LoadInt32(value)
if v != jwt.NoLimit {
if limit != jwt.NoLimit {
if limit < *value {
*value = limit
if limit < v {
atomic.StoreInt32(value, limit)
return true
}
}
} else if limit != jwt.NoLimit {
*value = limit
atomic.StoreInt32(value, limit)
return true
}
return false
@@ -810,7 +811,7 @@ func (c *client) applyAccountLimits() {
if c.acc == nil || (c.kind != CLIENT && c.kind != LEAF) {
return
}
c.mpay = jwt.NoLimit
atomic.StoreInt32(&c.mpay, jwt.NoLimit)
c.msubs = jwt.NoLimit
if c.opts.JWT != _EMPTY_ { // user jwt implies account
if uc, _ := jwt.DecodeUserClaims(c.opts.JWT); uc != nil {
@@ -2170,7 +2171,7 @@ func (c *client) generateClientInfoJSON(info Info) []byte {
if c.srv != nil { // Otherwise lame duck info can panic
c.srv.websocket.mu.RLock()
info.TLSAvailable = c.srv.websocket.tls
if c.srv.websocket.server != nil {
if c.srv.websocket.tls && c.srv.websocket.server != nil {
if tc := c.srv.websocket.server.TLSConfig; tc != nil {
info.TLSRequired = !tc.InsecureSkipVerify
}
@@ -3127,20 +3128,14 @@ var needFlush = struct{}{}
// deliverMsg will deliver a message to a matching subscription and its underlying client.
// We process all connection/client types. mh is the part that will be protocol/client specific.
func (c *client) deliverMsg(prodIsMQTT bool, sub *subscription, acc *Account, subject, reply, mh, msg []byte, gwrply bool) bool {
// Check sub client and check echo
if sub.client == nil || c == sub.client && !sub.client.echo {
// Check sub client and check echo. Only do this if not a service import.
if sub.client == nil || (c == sub.client && !sub.client.echo && !sub.si) {
return false
}
client := sub.client
client.mu.Lock()
// Check echo
if c == client && !client.echo {
client.mu.Unlock()
return false
}
// Check if we have a subscribe deny clause. This will trigger us to check the subject
// for a match against the denied subjects.
if client.mperms != nil && client.checkDenySub(string(subject)) {
@@ -3582,15 +3577,21 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
}
// Mostly under testing scenarios.
c.mu.Lock()
if c.srv == nil || c.acc == nil {
c.mu.Unlock()
return false, false
}
acc := c.acc
genidAddr := &acc.sl.genid
// Check pub permissions
if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) && !c.pubAllowed(string(c.pa.subject)) {
if c.perms != nil && (c.perms.pub.allow != nil || c.perms.pub.deny != nil) && !c.pubAllowedFullCheck(string(c.pa.subject), true, true) {
c.mu.Unlock()
c.pubPermissionViolation(c.pa.subject)
return false, true
}
c.mu.Unlock()
// Now check for reserved replies. These are used for service imports.
if c.kind == CLIENT && len(c.pa.reply) > 0 && isReservedReply(c.pa.reply) {
@@ -3611,10 +3612,10 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
// performance impact reported in our bench)
var isGWRouted bool
if c.kind != CLIENT {
if atomic.LoadInt32(&c.acc.gwReplyMapping.check) > 0 {
c.acc.mu.RLock()
c.pa.subject, isGWRouted = c.acc.gwReplyMapping.get(c.pa.subject)
c.acc.mu.RUnlock()
if atomic.LoadInt32(&acc.gwReplyMapping.check) > 0 {
acc.mu.RLock()
c.pa.subject, isGWRouted = acc.gwReplyMapping.get(c.pa.subject)
acc.mu.RUnlock()
}
} else if atomic.LoadInt32(&c.gwReplyMapping.check) > 0 {
c.mu.Lock()
@@ -3657,7 +3658,7 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
var r *SublistResult
var ok bool
genid := atomic.LoadUint64(&c.acc.sl.genid)
genid := atomic.LoadUint64(genidAddr)
if genid == c.in.genid && c.in.results != nil {
r, ok = c.in.results[string(c.pa.subject)]
} else {
@@ -3668,15 +3669,17 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
// Go back to the sublist data structure.
if !ok {
r = c.acc.sl.Match(string(c.pa.subject))
c.in.results[string(c.pa.subject)] = r
// Prune the results cache. Keeps us from unbounded growth. Random delete.
if len(c.in.results) > maxResultCacheSize {
n := 0
for subject := range c.in.results {
delete(c.in.results, subject)
if n++; n > pruneSize {
break
r = acc.sl.Match(string(c.pa.subject))
if len(r.psubs)+len(r.qsubs) > 0 {
c.in.results[string(c.pa.subject)] = r
// Prune the results cache. Keeps us from unbounded growth. Random delete.
if len(c.in.results) > maxResultCacheSize {
n := 0
for subject := range c.in.results {
delete(c.in.results, subject)
if n++; n > pruneSize {
break
}
}
}
}
@@ -3699,7 +3702,7 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
atomic.LoadInt64(&c.srv.gateway.totalQSubs) > 0 {
flag |= pmrCollectQueueNames
}
didDeliver, qnames = c.processMsgResults(c.acc, r, msg, c.pa.deliver, c.pa.subject, c.pa.reply, flag)
didDeliver, qnames = c.processMsgResults(acc, r, msg, c.pa.deliver, c.pa.subject, c.pa.reply, flag)
}
// Now deal with gateways
@@ -3709,7 +3712,7 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
reply = append(reply, '@')
reply = append(reply, c.pa.deliver...)
}
didDeliver = c.sendMsgToGateways(c.acc, msg, c.pa.subject, reply, qnames) || didDeliver
didDeliver = c.sendMsgToGateways(acc, msg, c.pa.subject, reply, qnames) || didDeliver
}
// Check to see if we did not deliver to anyone and the client has a reply subject set
@@ -3915,6 +3918,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
checkJS = true
}
}
siAcc := si.acc
acc.mu.RUnlock()
// We have a special case where JetStream pulls in all service imports through one export.
@@ -3945,7 +3949,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
}
} else if !isResponse && si.latency != nil && tracking {
// Check to see if this was a bad request with no reply and we were supposed to be tracking.
si.acc.sendBadRequestTrackingLatency(si, c, headers)
siAcc.sendBadRequestTrackingLatency(si, c, headers)
}
// Send tracking info here if we are tracking this response.
@@ -3973,7 +3977,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
// Now check to see if this account has mappings that could affect the service import.
// Can't use non-locked trick like in processInboundClientMsg, so just call into selectMappedSubject
// so we only lock once.
nsubj, changed := si.acc.selectMappedSubject(to)
nsubj, changed := siAcc.selectMappedSubject(to)
if changed {
c.pa.mapped = []byte(to)
to = nsubj
@@ -3990,7 +3994,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
// Place our client info for the request in the original message.
// This will survive going across routes, etc.
if !isResponse {
isSysImport := si.acc == c.srv.SystemAccount()
isSysImport := siAcc == c.srv.SystemAccount()
var ci *ClientInfo
if hadPrevSi && c.pa.hdr >= 0 {
var cis ClientInfo
@@ -4031,11 +4035,11 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
c.pa.reply = nrr
if changed && c.isMqtt() && c.pa.hdr > 0 {
c.srv.mqttStoreQoS1MsgForAccountOnNewSubject(c.pa.hdr, msg, si.acc.GetName(), to)
c.srv.mqttStoreQoS1MsgForAccountOnNewSubject(c.pa.hdr, msg, siAcc.GetName(), to)
}
// FIXME(dlc) - Do L1 cache trick like normal client?
rr := si.acc.sl.Match(to)
rr := siAcc.sl.Match(to)
// If we are a route or gateway or leafnode and this message is flipped to a queue subscriber we
// need to handle that since the processMsgResults will want a queue filter.
@@ -4060,10 +4064,10 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
if c.srv.gateway.enabled {
flags |= pmrCollectQueueNames
var queues [][]byte
didDeliver, queues = c.processMsgResults(si.acc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
didDeliver = c.sendMsgToGateways(si.acc, msg, []byte(to), nrr, queues) || didDeliver
didDeliver, queues = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
didDeliver = c.sendMsgToGateways(siAcc, msg, []byte(to), nrr, queues) || didDeliver
} else {
didDeliver, _ = c.processMsgResults(si.acc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
didDeliver, _ = c.processMsgResults(siAcc, rr, msg, c.pa.deliver, []byte(to), nrr, flags)
}
// Restore to original values.
@@ -4096,7 +4100,7 @@ func (c *client) processServiceImport(si *serviceImport, acc *Account, msg []byt
} else {
// This is a main import and since we could not even deliver to the exporting account
// go ahead and remove the respServiceImport we created above.
si.acc.removeRespServiceImport(rsi, reason)
siAcc.removeRespServiceImport(rsi, reason)
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ var (
const (
// VERSION is the current version for the server.
VERSION = "2.9.19"
VERSION = "2.9.21"
// PROTO is the currently supported protocol.
// 0 was the original
+1 -1
View File
@@ -3506,7 +3506,7 @@ func (o *consumer) loopAndGatherMsgs(qch chan struct{}) {
if err == ErrStoreEOF {
o.checkNumPendingOnEOF()
}
if err == ErrStoreMsgNotFound || err == ErrStoreEOF || err == errMaxAckPending || err == errPartialCache {
if err == ErrStoreMsgNotFound || err == errDeletedMsg || err == ErrStoreEOF || err == errMaxAckPending || err == errPartialCache {
goto waitForMsgs
} else {
s.Errorf("Received an error looking up message for consumer: %v", err)
+111 -26
View File
@@ -17,6 +17,7 @@ import (
"bytes"
"compress/gzip"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
@@ -30,7 +31,9 @@ import (
"time"
"github.com/klauspost/compress/s2"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nats-server/v2/server/certidp"
"github.com/nats-io/nats-server/v2/server/pse"
)
@@ -78,6 +81,9 @@ const (
accReqTokens = 5
accReqAccIndex = 3
ocspPeerRejectEventSubj = "$SYS.SERVER.%s.OCSP.PEER.CONN.REJECT"
ocspPeerChainlinkInvalidEventSubj = "$SYS.SERVER.%s.OCSP.PEER.LINK.INVALID"
)
// FIXME(dlc) - make configurable.
@@ -151,6 +157,34 @@ type DisconnectEventMsg struct {
// DisconnectEventMsgType is the schema type for DisconnectEventMsg
const DisconnectEventMsgType = "io.nats.server.advisory.v1.client_disconnect"
// OCSPPeerRejectEventMsg is sent when a peer TLS handshake is ultimately rejected due to OCSP invalidation.
// A "peer" can be an inbound client connection or a leaf connection to a remote server. Peer in event payload
// is always the peer's (TLS) leaf cert, which may or may be the invalid cert (See also OCSPPeerChainlinkInvalidEventMsg)
type OCSPPeerRejectEventMsg struct {
TypedEvent
Kind string `json:"kind"`
Peer certidp.CertInfo `json:"peer"`
Server ServerInfo `json:"server"`
Reason string `json:"reason"`
}
// OCSPPeerRejectEventMsgType is the schema type for OCSPPeerRejectEventMsg
const OCSPPeerRejectEventMsgType = "io.nats.server.advisory.v1.ocsp_peer_reject"
// OCSPPeerChainlinkInvalidEventMsg is sent when a certificate (link) in a valid TLS chain is found to be OCSP invalid
// during a peer TLS handshake. A "peer" can be an inbound client connection or a leaf connection to a remote server.
// Peer and Link may be the same if the invalid cert was the peer's leaf cert
type OCSPPeerChainlinkInvalidEventMsg struct {
TypedEvent
Link certidp.CertInfo `json:"link"`
Peer certidp.CertInfo `json:"peer"`
Server ServerInfo `json:"server"`
Reason string `json:"reason"`
}
// OCSPPeerChainlinkInvalidEventMsgType is the schema type for OCSPPeerChainlinkInvalidEventMsg
const OCSPPeerChainlinkInvalidEventMsgType = "io.nats.server.advisory.v1.ocsp_peer_link_invalid"
// AccountNumConns is an event that will be sent from a server that is tracking
// a given account when the number of connections changes. It will also HB
// updates in the absence of any changes.
@@ -843,35 +877,15 @@ func getHash(name string) string {
return getHashSize(name, sysHashLen)
}
var nameToHashSize8 = sync.Map{}
var nameToHashSize6 = sync.Map{}
// Computes a hash for the given `name`. The result will be `size` characters long.
func getHashSize(name string, size int) string {
compute := func() string {
sha := sha256.New()
sha.Write([]byte(name))
b := sha.Sum(nil)
for i := 0; i < size; i++ {
b[i] = digits[int(b[i]%base)]
}
return string(b[:size])
sha := sha256.New()
sha.Write([]byte(name))
b := sha.Sum(nil)
for i := 0; i < size; i++ {
b[i] = digits[int(b[i]%base)]
}
var m *sync.Map
switch size {
case 8:
m = &nameToHashSize8
case 6:
m = &nameToHashSize6
default:
return compute()
}
if v, ok := m.Load(name); ok {
return v.(string)
}
h := compute()
m.Store(name, h)
return h
return string(b[:size])
}
// Returns the node name for this server which is a hash of the server name.
@@ -2488,3 +2502,74 @@ func (s *Server) wrapChk(f func()) func() {
s.mu.Unlock()
}
}
// sendOCSPPeerRejectEvent sends a system level event to system account when a peer connection is
// rejected due to OCSP invalid status of its trust chain(s).
func (s *Server) sendOCSPPeerRejectEvent(kind string, peer *x509.Certificate, reason string) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.eventsEnabled() {
return
}
if peer == nil {
s.Errorf(certidp.ErrPeerEmptyNoEvent)
return
}
eid := s.nextEventID()
now := time.Now().UTC()
m := OCSPPeerRejectEventMsg{
TypedEvent: TypedEvent{
Type: OCSPPeerRejectEventMsgType,
ID: eid,
Time: now,
},
Kind: kind,
Peer: certidp.CertInfo{
Subject: certidp.GetSubjectDNForm(peer),
Issuer: certidp.GetIssuerDNForm(peer),
Fingerprint: certidp.GenerateFingerprint(peer),
Raw: peer.Raw,
},
Reason: reason,
}
subj := fmt.Sprintf(ocspPeerRejectEventSubj, s.info.ID)
s.sendInternalMsg(subj, _EMPTY_, &m.Server, &m)
}
// sendOCSPPeerChainlinkInvalidEvent sends a system level event to system account when a link in a peer's trust chain
// is OCSP invalid.
func (s *Server) sendOCSPPeerChainlinkInvalidEvent(peer *x509.Certificate, link *x509.Certificate, reason string) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.eventsEnabled() {
return
}
if peer == nil || link == nil {
s.Errorf(certidp.ErrPeerEmptyNoEvent)
return
}
eid := s.nextEventID()
now := time.Now().UTC()
m := OCSPPeerChainlinkInvalidEventMsg{
TypedEvent: TypedEvent{
Type: OCSPPeerChainlinkInvalidEventMsgType,
ID: eid,
Time: now,
},
Link: certidp.CertInfo{
Subject: certidp.GetSubjectDNForm(link),
Issuer: certidp.GetIssuerDNForm(link),
Fingerprint: certidp.GenerateFingerprint(link),
Raw: link.Raw,
},
Peer: certidp.CertInfo{
Subject: certidp.GetSubjectDNForm(peer),
Issuer: certidp.GetIssuerDNForm(peer),
Fingerprint: certidp.GenerateFingerprint(peer),
Raw: peer.Raw,
},
Reason: reason,
}
subj := fmt.Sprintf(ocspPeerChainlinkInvalidEventSubj, s.info.ID)
s.sendInternalMsg(subj, _EMPTY_, &m.Server, &m)
}
+134 -43
View File
@@ -275,6 +275,8 @@ const (
wiThresh = int64(30 * time.Second)
// Time threshold to write index info for non FIFO cases
winfThresh = int64(2 * time.Second)
// Checksum size for hash for msg records.
recordHashSize = 8
)
func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
@@ -349,6 +351,14 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim
return nil, fmt.Errorf("could not create hash: %v", err)
}
keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
// Make sure we do not have an encrypted store underneath of us but no main key.
if fs.prf == nil {
if _, err := os.Stat(keyFile); err == nil {
return nil, errNoMainKey
}
}
// Recover our message state.
if err := fs.recoverMsgs(); err != nil {
return nil, err
@@ -366,7 +376,6 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim
// If we expect to be encrypted check that what we are restoring is not plaintext.
// This can happen on snapshot restores or conversions.
if fs.prf != nil {
keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) {
if err := fs.writeStreamMeta(); err != nil {
return nil, err
@@ -964,6 +973,10 @@ func (mb *msgBlock) rebuildState() (*LostStreamData, error) {
func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
startLastSeq := mb.last.seq
// Remove the .fss file and clear any cache we have set.
mb.clearCacheAndOffset()
mb.removePerSubjectInfoLocked()
buf, err := mb.loadBlock(nil)
if err != nil || len(buf) == 0 {
var ld *LostStreamData
@@ -989,9 +1002,6 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
mb.last.seq, mb.last.ts = 0, 0
firstNeedsSet := true
// Remove the .fss file from disk.
mb.removePerSubjectInfoLocked()
// Check if we need to decrypt.
if mb.bek != nil && len(buf) > 0 {
// Recreate to reset counter.
@@ -1063,12 +1073,7 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
rl &^= hbit
dlen := int(rl) - msgHdrSize
// Do some quick sanity checks here.
if dlen < 0 || int(slen) > (dlen-8) || dlen > int(rl) || rl > rlBadThresh {
truncate(index)
return gatherLost(lbuf - index), errBadMsg
}
if index+rl > lbuf {
if dlen < 0 || int(slen) > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
truncate(index)
return gatherLost(lbuf - index), errBadMsg
}
@@ -1084,15 +1089,17 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
addToDmap(seq)
}
index += rl
mb.last.seq = seq
mb.last.ts = ts
if seq >= mb.first.seq {
mb.last.seq = seq
mb.last.ts = ts
}
continue
}
// This is for when we have index info that adjusts for deleted messages
// at the head. So the first.seq will be already set here. If this is larger
// replace what we have with this seq.
if firstNeedsSet && seq > mb.first.seq {
if firstNeedsSet && seq >= mb.first.seq {
firstNeedsSet, mb.first.seq, mb.first.ts = false, seq, ts
}
@@ -1112,12 +1119,12 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
hh.Write(hdr[4:20])
hh.Write(data[:slen])
if hasHeaders {
hh.Write(data[slen+4 : dlen-8])
hh.Write(data[slen+4 : dlen-recordHashSize])
} else {
hh.Write(data[slen : dlen-8])
hh.Write(data[slen : dlen-recordHashSize])
}
checksum := hh.Sum(nil)
if !bytes.Equal(checksum, data[len(data)-8:]) {
if !bytes.Equal(checksum, data[len(data)-recordHashSize:]) {
truncate(index)
return gatherLost(lbuf - index), errBadMsg
}
@@ -1158,6 +1165,11 @@ func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, error) {
mb.last.seq = mb.first.seq - 1
}
// Update our fss file if needed.
if len(mb.fss) > 0 {
mb.writePerSubjectInfo()
}
return nil, nil
}
@@ -1868,12 +1880,24 @@ func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool)
seqStart, _ = fs.selectMsgBlockWithIndex(sseq)
}
tsa := [32]string{}
fsa := [32]string{}
var tsa, fsa [32]string
fts := tokenizeSubjectIntoSlice(fsa[:0], filter)
isAll := filter == _EMPTY_ || filter == fwcs
wc := subjectHasWildcard(filter)
// See if filter was provided but its the only subject.
if !isAll && !wc && len(fs.psim) == 1 && fs.psim[filter] != nil {
isAll = true
}
// If we are isAll and have no deleted we can do a simpler calculation.
if isAll && (fs.state.LastSeq-fs.state.FirstSeq+1) == fs.state.Msgs {
if sseq == 0 {
return fs.state.Msgs, validThrough
}
return fs.state.LastSeq - sseq + 1, validThrough
}
isMatch := func(subj string) bool {
if isAll {
return true
@@ -1900,6 +1924,7 @@ func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool)
var t uint64
if isAll && sseq <= mb.first.seq {
if lastPerSubject {
mb.ensurePerSubjectInfoLoaded()
for subj := range mb.fss {
if !seen[subj] {
total++
@@ -2023,16 +2048,20 @@ func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool)
mb.mu.Lock()
// Check if we should include all of this block in adjusting. If so work with metadata.
if sseq > mb.last.seq {
// We need to adjust for all matches in this block.
// We will scan fss state vs messages themselves.
// Make sure we have fss loaded.
mb.ensurePerSubjectInfoLoaded()
for subj, ss := range mb.fss {
if isMatch(subj) {
if lastPerSubject {
adjust++
} else {
adjust += ss.Msgs
if isAll && !lastPerSubject {
adjust += mb.msgs
} else {
// We need to adjust for all matches in this block.
// We will scan fss state vs messages themselves.
// Make sure we have fss loaded.
mb.ensurePerSubjectInfoLoaded()
for subj, ss := range mb.fss {
if isMatch(subj) {
if lastPerSubject {
adjust++
} else {
adjust += ss.Msgs
}
}
}
}
@@ -2574,14 +2603,42 @@ func (fs *fileStore) enforceMsgPerSubjectLimit() {
fs.scb = nil
defer func() { fs.scb = cb }()
var numMsgs uint64
// collect all that are not correct.
needAttention := make(map[string]*psi)
for subj, psi := range fs.psim {
numMsgs += psi.total
if psi.total > maxMsgsPer {
needAttention[subj] = psi
}
}
// We had an issue with a use case where psim (and hence fss) were correct but idx was not and was not properly being caught.
// So do a quick sanity check here. If we detect a skew do a rebuild then re-check.
if numMsgs != fs.state.Msgs {
// Clear any global subject state.
fs.psim = make(map[string]*psi)
for _, mb := range fs.blks {
mb.removeIndexFile()
ld, err := mb.rebuildState()
mb.writeIndexInfo()
if err != nil && ld != nil {
fs.addLostData(ld)
}
fs.populateGlobalPerSubjectInfo(mb)
}
// Rebuild fs state too.
fs.rebuildStateLocked(nil)
// Need to redo blocks that need attention.
needAttention = make(map[string]*psi)
for subj, psi := range fs.psim {
if psi.total > maxMsgsPer {
needAttention[subj] = psi
}
}
}
// Collect all the msgBlks we alter.
blks := make(map[*msgBlock]struct{})
@@ -3026,8 +3083,7 @@ func (mb *msgBlock) compact() {
return
}
// Close cache and index file and wipe delete map, then rebuild.
mb.clearCacheAndOffset()
// Remove index file and wipe delete map, then rebuild.
mb.removeIndexFileLocked()
mb.deleteDmap()
mb.rebuildStateLocked()
@@ -3053,6 +3109,11 @@ func (mb *msgBlock) slotInfo(slot int) (uint32, uint32, bool, error) {
bi := mb.cache.idx[slot]
ri, hashChecked := (bi &^ hbit), (bi&hbit) != 0
// If this is a deleted slot return here.
if bi == dbit {
return 0, 0, false, errDeletedMsg
}
// Determine record length
var rl uint32
if len(mb.cache.idx) > slot+1 {
@@ -3998,7 +4059,7 @@ func (fs *fileStore) selectMsgBlockForStart(minTime time.Time) *msgBlock {
func (mb *msgBlock) indexCacheBuf(buf []byte) error {
var le = binary.LittleEndian
var fseq uint64
var fseq, pseq uint64
var idx []uint32
var index uint32
@@ -4031,7 +4092,7 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error {
dlen := int(rl) - msgHdrSize
// Do some quick sanity checks here.
if dlen < 0 || int(slen) > dlen || dlen > int(rl) || index+rl > lbuf || rl > 32*1024*1024 {
if dlen < 0 || int(slen) > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
// This means something is off.
// TODO(dlc) - Add into bad list?
return errCorruptState
@@ -4039,15 +4100,31 @@ func (mb *msgBlock) indexCacheBuf(buf []byte) error {
// Clear erase bit.
seq = seq &^ ebit
// Adjust if we guessed wrong.
if seq != 0 && seq < fseq {
fseq = seq
}
// We defer checksum checks to individual msg cache lookups to amortorize costs and
// not introduce latency for first message from a newly loaded block.
idx = append(idx, index)
mb.cache.lrl = uint32(rl)
index += mb.cache.lrl
if seq >= mb.first.seq {
// Track that we do not have holes.
// Not expected but did see it in the field.
if pseq > 0 && seq != pseq+1 {
if mb.dmap == nil {
mb.dmap = make(map[uint64]struct{})
}
for dseq := pseq + 1; dseq < seq; dseq++ {
idx = append(idx, dbit)
mb.dmap[dseq] = struct{}{}
}
}
pseq = seq
idx = append(idx, index)
mb.cache.lrl = uint32(rl)
// Adjust if we guessed wrong.
if seq != 0 && seq < fseq {
fseq = seq
}
}
index += rl
}
mb.cache.buf = buf
mb.cache.idx = idx
@@ -4373,6 +4450,7 @@ var (
errMsgBlkTooBig = errors.New("message block size exceeded int capacity")
errUnknownCipher = errors.New("unknown cipher")
errDIOStalled = errors.New("IO is stalled")
errNoMainKey = errors.New("encrypted store encountered with no main key")
)
// Used for marking messages that have had their checksums checked.
@@ -4382,6 +4460,9 @@ const hbit = 1 << 31
// Used for marking erased messages sequences.
const ebit = 1 << 63
// Used to mark a bad index as deleted.
const dbit = 1 << 30
// Will do a lookup from cache.
// Lock should be held.
func (mb *msgBlock) cacheLookup(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
@@ -4392,6 +4473,7 @@ func (mb *msgBlock) cacheLookup(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
// If we have a delete map check it.
if mb.dmap != nil {
if _, ok := mb.dmap[seq]; ok {
mb.llts = time.Now().UnixNano()
return nil, errDeletedMsg
}
}
@@ -4534,9 +4616,9 @@ func (mb *msgBlock) msgFromBuf(buf []byte, sm *StoreMsg, hh hash.Hash64) (*Store
hh.Write(hdr[4:20])
hh.Write(data[:slen])
if hasHeaders {
hh.Write(data[slen+4 : dlen-8])
hh.Write(data[slen+4 : dlen-recordHashSize])
} else {
hh.Write(data[slen : dlen-8])
hh.Write(data[slen : dlen-recordHashSize])
}
if !bytes.Equal(hh.Sum(nil), data[len(data)-8:]) {
return nil, errBadMsg
@@ -5339,13 +5421,13 @@ func (fs *fileStore) Compact(seq uint64) (uint64, error) {
var purged, bytes uint64
// We have to delete interior messages.
fs.mu.Lock()
// Same as purge all.
if lseq := fs.state.LastSeq; seq > lseq {
fs.mu.Unlock()
return fs.purge(seq)
}
// We have to delete interior messages.
smb := fs.selectMsgBlock(seq)
if smb == nil {
fs.mu.Unlock()
@@ -5852,6 +5934,8 @@ func (mb *msgBlock) recalculateFirstForSubj(subj string, startSeq uint64, ss *Si
if startSlot >= len(mb.cache.idx) {
ss.First = ss.Last
return
} else if startSlot < 0 {
startSlot = 0
}
var le = binary.LittleEndian
@@ -6261,6 +6345,9 @@ func (fs *fileStore) Stop() error {
fs.cancelSyncTimer()
fs.cancelAgeChk()
// We should update the upper usage layer on a stop.
cb, bytes := fs.scb, int64(fs.state.Bytes)
var _cfs [256]ConsumerStore
cfs := append(_cfs[:0], fs.cfs...)
fs.cfs = nil
@@ -6270,6 +6357,10 @@ func (fs *fileStore) Stop() error {
o.Stop()
}
if bytes > 0 && cb != nil {
cb(0, -bytes, 0, _EMPTY_)
}
return nil
}
+1 -1
View File
@@ -2953,7 +2953,7 @@ func (c *client) processInboundGatewayMsg(msg []byte) {
// Check if this is a service reply subject (_R_)
noInterest := len(r.psubs) == 0
checkNoInterest := true
if acc.imports.services != nil {
if acc.NumServiceImports() > 0 {
if isServiceReply(c.pa.subject) {
checkNoInterest = false
} else {
+1 -1
View File
@@ -2734,7 +2734,7 @@ func canonicalName(name string) string {
}
// To throttle the out of resources errors.
func (s *Server) resourcesExeededError() {
func (s *Server) resourcesExceededError() {
var didAlert bool
s.rerrMu.Lock()
+8 -8
View File
@@ -1,4 +1,4 @@
// Copyright 2020-2022 The NATS Authors
// Copyright 2020-2023 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
@@ -2656,7 +2656,7 @@ func (s *Server) jsLeaderAccountPurgeRequest(sub *subscription, c *client, _ *Ac
}
// Request to have the meta leader stepdown.
// These will only be received the meta leaders, so less checking needed.
// These will only be received by the meta leader, so less checking needed.
func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
if c == nil || !s.JetStreamEnabled() {
return
@@ -3360,7 +3360,7 @@ func (s *Server) processStreamRestore(ci *ClientInfo, acc *Account, cfg *StreamC
// TODO(dlc) - We could check apriori and cancel initial request if we know it won't fit.
total += len(msg)
if js.wouldExceedLimits(FileStorage, total) {
s.resourcesExeededError()
s.resourcesExceededError()
resultCh <- result{NewJSInsufficientResourcesError(), reply}
return
}
@@ -3766,11 +3766,11 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun
} else {
streamName = streamNameFromSubject(subject)
consumerName = consumerNameFromSubject(subject)
}
// New has optional filtered subject as part of main subject..
if n > 7 {
tokens := strings.Split(subject, tsep)
filteredSubject = strings.Join(tokens[6:], tsep)
// New has optional filtered subject as part of main subject..
if n > 6 {
tokens := strings.Split(subject, tsep)
filteredSubject = strings.Join(tokens[6:], tsep)
}
}
}
+22 -15
View File
@@ -759,6 +759,7 @@ func (js *jetStream) setupMetaGroup() error {
s.Errorf("Error creating filestore: %v", err)
return err
}
// Register our server.
fs.registerServer(s)
@@ -2290,9 +2291,13 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps
case isLeader = <-lch:
if isLeader {
if sendSnapshot && mset != nil && n != nil {
n.SendSnapshot(mset.stateSnapshot())
sendSnapshot = false
if mset != nil && n != nil {
// Send a snapshot if being asked or if we are tracking
// a failed state so that followers sync.
if clfs := mset.clearCLFS(); clfs > 0 || sendSnapshot {
n.SendSnapshot(mset.stateSnapshot())
sendSnapshot = false
}
}
if isRestore {
acc, _ := s.LookupAccount(sa.Client.serviceAccount())
@@ -2713,15 +2718,14 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco
// Grab last sequence and CLFS.
last, clfs := mset.lastSeqAndCLFS()
// We can skip if we know this is less than what we already have.
if lseq-clfs < last {
s.Debugf("Apply stream entries for '%s > %s' skipping message with sequence %d with last of %d",
mset.account(), mset.name(), lseq+1-clfs, last)
// Check for any preAcks in case we are interest based.
mset.mu.Lock()
seq := lseq + 1 - mset.clfs
mset.clearAllPreAcks(seq)
// Check for any preAcks in case we are interest based.
mset.clearAllPreAcks(lseq + 1 - mset.clfs)
mset.mu.Unlock()
continue
}
@@ -2807,12 +2811,15 @@ func (js *jetStream) applyStreamEntries(mset *stream, ce *CommittedEntry, isReco
}
panic(err.Error())
}
// Ignore if we are recovering and we have already processed.
if isRecovering && (sp.Request == nil || sp.Request.Sequence == 0) {
// If no explicit request, fill in with leader stamped last sequence to protect ourselves on replay during server start.
if sp.Request == nil || sp.Request.Sequence == 0 {
purgeSeq := sp.LastSeq + 1
if sp.Request == nil {
sp.Request = &JSApiStreamPurgeRequest{Sequence: sp.LastSeq}
} else {
sp.Request.Sequence = sp.LastSeq
sp.Request = &JSApiStreamPurgeRequest{Sequence: purgeSeq}
} else if sp.Request.Keep == 0 {
sp.Request.Sequence = purgeSeq
} else if isRecovering {
continue
}
}
@@ -6523,7 +6530,7 @@ LOOP:
})
}
resp.Total = len(resp.Consumers)
resp.Total = ocnt
resp.Limit = JSApiListLimit
resp.Offset = offset
resp.Missing = missingNames
@@ -7222,7 +7229,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [
// Check here pre-emptively if we have exceeded this server limits.
if js.limitsExceeded(stype) {
s.resourcesExeededError()
s.resourcesExceededError()
if canRespond {
b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: NewJSInsufficientResourcesError()})
outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, b, nil, 0))
@@ -7768,7 +7775,7 @@ RETRY:
} else if err == NewJSInsufficientResourcesError() {
notifyLeaderStopCatchup(mrec, err)
if mset.js.limitsExceeded(mset.cfg.Storage) {
s.resourcesExeededError()
s.resourcesExceededError()
} else {
s.Warnf("Catchup for stream '%s > %s' errored, account resources exceeded: %v", mset.account(), mset.name(), err)
}
+5 -2
View File
@@ -2101,8 +2101,11 @@ func (c *client) processLeafSub(argo []byte) (err error) {
spoke := c.isSpokeLeafNode()
c.mu.Unlock()
if err := c.addShadowSubscriptions(acc, sub); err != nil {
c.Errorf(err.Error())
// Only add in shadow subs if a new sub or qsub.
if osub == nil {
if err := c.addShadowSubscriptions(acc, sub); err != nil {
c.Errorf(err.Error())
}
}
// If we are not solicited, treat leaf node subscriptions similar to a
+7 -4
View File
@@ -66,7 +66,7 @@ func (s *Server) ConfigureLogger() {
}
if opts.LogFile != "" {
log = srvlog.NewFileLogger(opts.LogFile, opts.Logtime, opts.Debug, opts.Trace, true)
log = srvlog.NewFileLogger(opts.LogFile, opts.Logtime, opts.Debug, opts.Trace, true, srvlog.LogUTC(opts.LogtimeUTC))
if opts.LogSizeLimit > 0 {
if l, ok := log.(*srvlog.Logger); ok {
l.SetSizeLimit(opts.LogSizeLimit)
@@ -84,7 +84,7 @@ func (s *Server) ConfigureLogger() {
if err != nil || (stat.Mode()&os.ModeCharDevice) == 0 {
colors = false
}
log = srvlog.NewStdLogger(opts.Logtime, opts.Debug, opts.Trace, colors, true)
log = srvlog.NewStdLogger(opts.Logtime, opts.Debug, opts.Trace, colors, true, srvlog.LogUTC(opts.LogtimeUTC))
}
s.SetLoggerV2(log, opts.Debug, opts.Trace, opts.TraceVerbose)
@@ -154,8 +154,11 @@ func (s *Server) ReOpenLogFile() {
if opts.LogFile == "" {
s.Noticef("File log re-open ignored, not a file logger")
} else {
fileLog := srvlog.NewFileLogger(opts.LogFile,
opts.Logtime, opts.Debug, opts.Trace, true)
fileLog := srvlog.NewFileLogger(
opts.LogFile, opts.Logtime,
opts.Debug, opts.Trace, true,
srvlog.LogUTC(opts.LogtimeUTC),
)
s.SetLogger(fileLog, opts.Debug, opts.Trace)
if opts.LogSizeLimit > 0 {
fileLog.SetSizeLimit(opts.LogSizeLimit)
+2 -1
View File
@@ -1144,11 +1144,12 @@ func memStoreMsgSize(subj string, hdr, msg []byte) uint64 {
// Delete is same as Stop for memory store.
func (ms *memStore) Delete() error {
ms.Purge()
return ms.Stop()
}
func (ms *memStore) Stop() error {
// These can't come back, so stop is same as Delete.
ms.Purge()
ms.mu.Lock()
if ms.ageChk != nil {
ms.ageChk.Stop()
+111 -67
View File
@@ -1154,6 +1154,7 @@ type Varz struct {
AuthRequired bool `json:"auth_required,omitempty"`
TLSRequired bool `json:"tls_required,omitempty"`
TLSVerify bool `json:"tls_verify,omitempty"`
TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"`
IP string `json:"ip,omitempty"`
ClientConnectURLs []string `json:"connect_urls,omitempty"`
WSConnectURLs []string `json:"ws_connect_urls,omitempty"`
@@ -1202,6 +1203,7 @@ type Varz struct {
TrustedOperatorsClaim []*jwt.OperatorClaims `json:"trusted_operators_claim,omitempty"`
SystemAccount string `json:"system_account,omitempty"`
PinnedAccountFail uint64 `json:"pinned_account_fails,omitempty"`
OCSPResponseCache OCSPResponseCacheVarz `json:"ocsp_peer_cache,omitempty"`
}
// JetStreamVarz contains basic runtime information about jetstream
@@ -1247,13 +1249,14 @@ type RemoteGatewayOptsVarz struct {
// LeafNodeOptsVarz contains monitoring leaf node information
type LeafNodeOptsVarz struct {
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSRequired bool `json:"tls_required,omitempty"`
TLSVerify bool `json:"tls_verify,omitempty"`
Remotes []RemoteLeafOptsVarz `json:"remotes,omitempty"`
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSRequired bool `json:"tls_required,omitempty"`
TLSVerify bool `json:"tls_verify,omitempty"`
Remotes []RemoteLeafOptsVarz `json:"remotes,omitempty"`
TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"`
}
// DenyRules Contains lists of subjects not allowed to be imported/exported
@@ -1264,41 +1267,55 @@ type DenyRules struct {
// RemoteLeafOptsVarz contains monitoring remote leaf node information
type RemoteLeafOptsVarz struct {
LocalAccount string `json:"local_account,omitempty"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
URLs []string `json:"urls,omitempty"`
Deny *DenyRules `json:"deny,omitempty"`
LocalAccount string `json:"local_account,omitempty"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
URLs []string `json:"urls,omitempty"`
Deny *DenyRules `json:"deny,omitempty"`
TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"`
}
// MQTTOptsVarz contains monitoring MQTT information
type MQTTOptsVarz struct {
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
NoAuthUser string `json:"no_auth_user,omitempty"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
TLSMap bool `json:"tls_map,omitempty"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSPinnedCerts []string `json:"tls_pinned_certs,omitempty"`
JsDomain string `json:"js_domain,omitempty"`
AckWait time.Duration `json:"ack_wait,omitempty"`
MaxAckPending uint16 `json:"max_ack_pending,omitempty"`
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
NoAuthUser string `json:"no_auth_user,omitempty"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
TLSMap bool `json:"tls_map,omitempty"`
TLSTimeout float64 `json:"tls_timeout,omitempty"`
TLSPinnedCerts []string `json:"tls_pinned_certs,omitempty"`
JsDomain string `json:"js_domain,omitempty"`
AckWait time.Duration `json:"ack_wait,omitempty"`
MaxAckPending uint16 `json:"max_ack_pending,omitempty"`
TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"`
}
// WebsocketOptsVarz contains monitoring websocket information
type WebsocketOptsVarz struct {
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
Advertise string `json:"advertise,omitempty"`
NoAuthUser string `json:"no_auth_user,omitempty"`
JWTCookie string `json:"jwt_cookie,omitempty"`
HandshakeTimeout time.Duration `json:"handshake_timeout,omitempty"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
NoTLS bool `json:"no_tls,omitempty"`
TLSMap bool `json:"tls_map,omitempty"`
TLSPinnedCerts []string `json:"tls_pinned_certs,omitempty"`
SameOrigin bool `json:"same_origin,omitempty"`
AllowedOrigins []string `json:"allowed_origins,omitempty"`
Compression bool `json:"compression,omitempty"`
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
Advertise string `json:"advertise,omitempty"`
NoAuthUser string `json:"no_auth_user,omitempty"`
JWTCookie string `json:"jwt_cookie,omitempty"`
HandshakeTimeout time.Duration `json:"handshake_timeout,omitempty"`
AuthTimeout float64 `json:"auth_timeout,omitempty"`
NoTLS bool `json:"no_tls,omitempty"`
TLSMap bool `json:"tls_map,omitempty"`
TLSPinnedCerts []string `json:"tls_pinned_certs,omitempty"`
SameOrigin bool `json:"same_origin,omitempty"`
AllowedOrigins []string `json:"allowed_origins,omitempty"`
Compression bool `json:"compression,omitempty"`
TLSOCSPPeerVerify bool `json:"tls_ocsp_peer_verify,omitempty"`
}
// OCSPResponseCacheVarz contains OCSP response cache information
type OCSPResponseCacheVarz struct {
Type string `json:"cache_type,omitempty"`
Hits int64 `json:"cache_hits,omitempty"`
Misses int64 `json:"cache_misses,omitempty"`
Responses int64 `json:"cached_responses,omitempty"`
Revokes int64 `json:"cached_revoked_responses,omitempty"`
Goods int64 `json:"cached_good_responses,omitempty"`
Unknowns int64 `json:"cached_unknown_responses,omitempty"`
}
// VarzOptions are the options passed to Varz().
@@ -1452,6 +1469,9 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz {
gatewayTlsReq := gw.TLSConfig != nil
leafTlsReq := ln.TLSConfig != nil
leafTlsVerify := leafTlsReq && ln.TLSConfig.ClientAuth == tls.RequireAndVerifyClientCert
leafTlsOCSPPeerVerify := s.ocspPeerVerify && leafTlsReq && ln.tlsConfigOpts.OCSPPeerConfig != nil && ln.tlsConfigOpts.OCSPPeerConfig.Verify
mqttTlsOCSPPeerVerify := s.ocspPeerVerify && mqtt.TLSConfig != nil && mqtt.tlsConfigOpts.OCSPPeerConfig != nil && mqtt.tlsConfigOpts.OCSPPeerConfig.Verify
wsTlsOCSPPeerVerify := s.ocspPeerVerify && ws.TLSConfig != nil && ws.tlsConfigOpts.OCSPPeerConfig != nil && ws.tlsConfigOpts.OCSPPeerConfig.Verify
varz := &Varz{
ID: info.ID,
Version: info.Version,
@@ -1489,38 +1509,41 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz {
RejectUnknown: gw.RejectUnknown,
},
LeafNode: LeafNodeOptsVarz{
Host: ln.Host,
Port: ln.Port,
AuthTimeout: ln.AuthTimeout,
TLSTimeout: ln.TLSTimeout,
TLSRequired: leafTlsReq,
TLSVerify: leafTlsVerify,
Remotes: []RemoteLeafOptsVarz{},
Host: ln.Host,
Port: ln.Port,
AuthTimeout: ln.AuthTimeout,
TLSTimeout: ln.TLSTimeout,
TLSRequired: leafTlsReq,
TLSVerify: leafTlsVerify,
TLSOCSPPeerVerify: leafTlsOCSPPeerVerify,
Remotes: []RemoteLeafOptsVarz{},
},
MQTT: MQTTOptsVarz{
Host: mqtt.Host,
Port: mqtt.Port,
NoAuthUser: mqtt.NoAuthUser,
AuthTimeout: mqtt.AuthTimeout,
TLSMap: mqtt.TLSMap,
TLSTimeout: mqtt.TLSTimeout,
JsDomain: mqtt.JsDomain,
AckWait: mqtt.AckWait,
MaxAckPending: mqtt.MaxAckPending,
Host: mqtt.Host,
Port: mqtt.Port,
NoAuthUser: mqtt.NoAuthUser,
AuthTimeout: mqtt.AuthTimeout,
TLSMap: mqtt.TLSMap,
TLSTimeout: mqtt.TLSTimeout,
JsDomain: mqtt.JsDomain,
AckWait: mqtt.AckWait,
MaxAckPending: mqtt.MaxAckPending,
TLSOCSPPeerVerify: mqttTlsOCSPPeerVerify,
},
Websocket: WebsocketOptsVarz{
Host: ws.Host,
Port: ws.Port,
Advertise: ws.Advertise,
NoAuthUser: ws.NoAuthUser,
JWTCookie: ws.JWTCookie,
AuthTimeout: ws.AuthTimeout,
NoTLS: ws.NoTLS,
TLSMap: ws.TLSMap,
SameOrigin: ws.SameOrigin,
AllowedOrigins: copyStrings(ws.AllowedOrigins),
Compression: ws.Compression,
HandshakeTimeout: ws.HandshakeTimeout,
Host: ws.Host,
Port: ws.Port,
Advertise: ws.Advertise,
NoAuthUser: ws.NoAuthUser,
JWTCookie: ws.JWTCookie,
AuthTimeout: ws.AuthTimeout,
NoTLS: ws.NoTLS,
TLSMap: ws.TLSMap,
SameOrigin: ws.SameOrigin,
AllowedOrigins: copyStrings(ws.AllowedOrigins),
Compression: ws.Compression,
HandshakeTimeout: ws.HandshakeTimeout,
TLSOCSPPeerVerify: wsTlsOCSPPeerVerify,
},
Start: s.start.UTC(),
MaxSubs: opts.MaxSubs,
@@ -1553,11 +1576,14 @@ func (s *Server) createVarz(pcpu float64, rss int64) *Varz {
Exports: r.DenyExports,
}
}
remoteTlsOCSPPeerVerify := s.ocspPeerVerify && r.tlsConfigOpts != nil && r.tlsConfigOpts.OCSPPeerConfig != nil && r.tlsConfigOpts.OCSPPeerConfig.Verify
rlna[i] = RemoteLeafOptsVarz{
LocalAccount: r.LocalAccount,
URLs: urlsToStrings(r.URLs),
TLSTimeout: r.TLSTimeout,
Deny: deny,
LocalAccount: r.LocalAccount,
URLs: urlsToStrings(r.URLs),
TLSTimeout: r.TLSTimeout,
Deny: deny,
TLSOCSPPeerVerify: remoteTlsOCSPPeerVerify,
}
}
varz.LeafNode.Remotes = rlna
@@ -1611,6 +1637,8 @@ func (s *Server) updateVarzConfigReloadableFields(v *Varz) {
}
v.MQTT.TLSPinnedCerts = getPinnedCertsAsSlice(opts.MQTT.TLSPinnedCerts)
v.Websocket.TLSPinnedCerts = getPinnedCertsAsSlice(opts.Websocket.TLSPinnedCerts)
v.TLSOCSPPeerVerify = s.ocspPeerVerify && v.TLSRequired && s.opts.tlsConfigOpts != nil && s.opts.tlsConfigOpts.OCSPPeerConfig != nil && s.opts.tlsConfigOpts.OCSPPeerConfig.Verify
}
func getPinnedCertsAsSlice(certs PinnedCertSet) []string {
@@ -1702,6 +1730,21 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64
}
}
gw.RUnlock()
if s.ocsprc != nil && s.ocsprc.Type() != "none" {
stats := s.ocsprc.Stats()
if stats != nil {
v.OCSPResponseCache = OCSPResponseCacheVarz{
s.ocsprc.Type(),
stats.Hits,
stats.Misses,
stats.Responses,
stats.Revokes,
stats.Goods,
stats.Unknowns,
}
}
}
}
// HandleVarz will process HTTP requests for server information.
@@ -3147,7 +3190,8 @@ func (s *Server) healthz(opts *HealthzOptions) *HealthStatus {
for acc, asa := range cc.streams {
nasa := make(map[string]*streamAssignment)
for stream, sa := range asa {
if sa.Group.isMember(ourID) {
// If we are a member and we are not being restored, select for check.
if sa.Group.isMember(ourID) && sa.Restore == nil {
csa := sa.copyGroup()
csa.consumers = make(map[string]*consumerAssignment)
for consumer, ca := range sa.consumers {
+3 -3
View File
@@ -1,4 +1,4 @@
// Copyright 2020-2021 The NATS Authors
// Copyright 2020-2023 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
@@ -3407,8 +3407,8 @@ func mqttSubscribeTrace(pi uint16, filters []*mqttFilter) string {
// message and this is the callback for a QoS1 subscription because in
// that case, it will be handled by the other callback. This avoid getting
// duplicate deliveries.
func mqttDeliverMsgCbQos0(sub *subscription, pc *client, _ *Account, subject, _ string, rmsg []byte) {
if pc.kind == JETSTREAM {
func mqttDeliverMsgCbQos0(sub *subscription, pc *client, _ *Account, subject, reply string, rmsg []byte) {
if pc.kind == JETSTREAM && len(reply) > 0 && strings.HasPrefix(reply, jsAckPre) {
return
}
+188 -83
View File
@@ -30,6 +30,9 @@ import (
"time"
"golang.org/x/crypto/ocsp"
"github.com/nats-io/nats-server/v2/server/certidp"
"github.com/nats-io/nats-server/v2/server/certstore"
)
const (
@@ -389,7 +392,7 @@ func (srv *Server) NewOCSPMonitor(config *tlsConfigKind) (*tls.Config, *OCSPMoni
}
// TODO: Add OCSP 'responder_cert' option in case CA cert not available.
issuers, err := getOCSPIssuer(caFile, cert.Certificate)
issuer, err := getOCSPIssuer(caFile, cert.Certificate)
if err != nil {
return nil, nil, err
}
@@ -402,7 +405,7 @@ func (srv *Server) NewOCSPMonitor(config *tlsConfigKind) (*tls.Config, *OCSPMoni
certFile: certFile,
stopCh: make(chan struct{}, 1),
Leaf: cert.Leaf,
Issuer: issuers[len(issuers)-1],
Issuer: issuer,
}
// Get the certificate status from the memory, then remote OCSP responder.
@@ -448,21 +451,20 @@ func (srv *Server) NewOCSPMonitor(config *tlsConfigKind) (*tls.Config, *OCSPMoni
}
chain := s.VerifiedChains[0]
leaf := chain[0]
parent := issuers[len(issuers)-1]
peerLeaf := chain[0]
peerIssuer := certidp.GetLeafIssuerCert(chain, 0)
if peerIssuer == nil {
return fmt.Errorf("failed to get issuer certificate for %s peer", kind)
}
resp, err := ocsp.ParseResponseForCert(oresp, leaf, parent)
// Response signature of issuer or issuer delegate is checked in the library parse
resp, err := ocsp.ParseResponseForCert(oresp, peerLeaf, peerIssuer)
if err != nil {
return fmt.Errorf("failed to parse OCSP response from %s peer: %w", kind, err)
}
if resp.Certificate == nil {
if err := resp.CheckSignatureFrom(parent); err != nil {
return fmt.Errorf("OCSP staple not issued by issuer: %w", err)
}
} else {
if err := resp.Certificate.CheckSignatureFrom(parent); err != nil {
return fmt.Errorf("OCSP staple's signer not signed by issuer: %w", err)
}
// If signer was issuer delegate double-check issuer delegate authorization
if resp.Certificate != nil {
ok := false
for _, eku := range resp.Certificate.ExtKeyUsage {
if eku == x509.ExtKeyUsageOCSPSigning {
@@ -474,6 +476,14 @@ func (srv *Server) NewOCSPMonitor(config *tlsConfigKind) (*tls.Config, *OCSPMoni
return fmt.Errorf("OCSP staple's signer missing authorization by CA to act as OCSP signer")
}
}
// Check that the OCSP response is effective, take defaults for clockskew and default validity
peerOpts := certidp.OCSPPeerConfig{ClockSkew: -1, TTLUnsetNextUpdate: -1}
sLog := certidp.Log{Debugf: srv.Debugf}
if !certidp.OCSPResponseCurrent(resp, &peerOpts, &sLog) {
return fmt.Errorf("OCSP staple from %s peer not current", kind)
}
if resp.Status != ocsp.Good {
return fmt.Errorf("bad status for OCSP Staple from %s peer: %s", kind, ocspStatusString(resp.Status))
}
@@ -520,10 +530,11 @@ func (s *Server) setupOCSPStapleStoreDir() error {
}
type tlsConfigKind struct {
tlsConfig *tls.Config
tlsOpts *TLSConfigOpts
kind string
apply func(*tls.Config)
tlsConfig *tls.Config
tlsOpts *TLSConfigOpts
kind string
isLeafSpoke bool
apply func(*tls.Config)
}
func (s *Server) configureOCSP() []*tlsConfigKind {
@@ -541,6 +552,26 @@ func (s *Server) configureOCSP() []*tlsConfigKind {
}
configs = append(configs, o)
}
if config := sopts.Websocket.TLSConfig; config != nil {
opts := sopts.Websocket.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[CLIENT],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.Websocket.TLSConfig = tc },
}
configs = append(configs, o)
}
if config := sopts.MQTT.TLSConfig; config != nil {
opts := sopts.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[CLIENT],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) { sopts.MQTT.TLSConfig = tc },
}
configs = append(configs, o)
}
if config := sopts.Cluster.TLSConfig; config != nil {
opts := sopts.Cluster.tlsConfigOpts
o := &tlsConfigKind{
@@ -557,16 +588,7 @@ func (s *Server) configureOCSP() []*tlsConfigKind {
kind: kindStringMap[LEAF],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) {
// RequireAndVerifyClientCert is used to tell a client that it
// should send the client cert to the server.
if opts.Verify {
tc.ClientAuth = tls.RequireAndVerifyClientCert
}
// We're a leaf hub server, so we must not set this.
tc.GetClientCertificate = nil
sopts.LeafNode.TLSConfig = tc
},
apply: func(tc *tls.Config) { sopts.LeafNode.TLSConfig = tc },
}
configs = append(configs, o)
}
@@ -576,14 +598,11 @@ func (s *Server) configureOCSP() []*tlsConfigKind {
// in the apply func callback below.
r, opts := remote, remote.tlsConfigOpts
o := &tlsConfigKind{
kind: kindStringMap[LEAF],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) {
// We're a leaf client, so we must not set this.
tc.GetCertificate = nil
r.TLSConfig = tc
},
kind: kindStringMap[LEAF],
tlsConfig: config,
tlsOpts: opts,
isLeafSpoke: true,
apply: func(tc *tls.Config) { r.TLSConfig = tc },
}
configs = append(configs, o)
}
@@ -605,9 +624,7 @@ func (s *Server) configureOCSP() []*tlsConfigKind {
kind: kindStringMap[GATEWAY],
tlsConfig: config,
tlsOpts: opts,
apply: func(tc *tls.Config) {
gw.TLSConfig = tc
},
apply: func(tc *tls.Config) { gw.TLSConfig = tc },
}
configs = append(configs, o)
}
@@ -619,16 +636,33 @@ func (s *Server) enableOCSP() error {
configs := s.configureOCSP()
for _, config := range configs {
tc, mon, err := s.NewOCSPMonitor(config)
if err != nil {
return err
}
// Check if an OCSP stapling monitor is required for this certificate.
if mon != nil {
s.ocsps = append(s.ocsps, mon)
// Override the TLS config with one that follows OCSP.
config.apply(tc)
// We do not staple Leaf Hub and Leaf Spokes, use ocsp_peer
if config.kind != kindStringMap[LEAF] {
// OCSP Stapling feature, will also enable tls server peer check for gateway and route peers
tc, mon, err := s.NewOCSPMonitor(config)
if err != nil {
return err
}
// Check if an OCSP stapling monitor is required for this certificate.
if mon != nil {
s.ocsps = append(s.ocsps, mon)
// Override the TLS config with one that follows OCSP stapling
config.apply(tc)
}
}
// OCSP peer check (client mTLS, leaf mTLS, leaf remote TLS)
if config.kind == kindStringMap[CLIENT] || config.kind == kindStringMap[LEAF] {
tc, plugged, err := s.plugTLSOCSPPeer(config)
if err != nil {
return err
}
if plugged && tc != nil {
s.ocspPeerVerify = true
config.apply(tc)
}
}
}
@@ -670,17 +704,39 @@ func (s *Server) reloadOCSP() error {
// Restart the monitors under the new configuration.
ocspm := make([]*OCSPMonitor, 0)
for _, config := range configs {
tc, mon, err := s.NewOCSPMonitor(config)
if err != nil {
return err
}
// Check if an OCSP stapling monitor is required for this certificate.
if mon != nil {
ocspm = append(ocspm, mon)
// Apply latest TLS configuration.
config.apply(tc)
// Reset server's ocspPeerVerify flag to re-detect at least one plugged OCSP peer
s.mu.Lock()
s.ocspPeerVerify = false
s.mu.Unlock()
s.stopOCSPResponseCache()
for _, config := range configs {
// We do not staple Leaf Hub and Leaf Spokes, use ocsp_peer
if config.kind != kindStringMap[LEAF] {
tc, mon, err := s.NewOCSPMonitor(config)
if err != nil {
return err
}
// Check if an OCSP stapling monitor is required for this certificate.
if mon != nil {
ocspm = append(ocspm, mon)
// Apply latest TLS configuration.
config.apply(tc)
}
}
// OCSP peer check (client mTLS, leaf mTLS, leaf remote TLS)
if config.kind == kindStringMap[CLIENT] || config.kind == kindStringMap[LEAF] {
tc, plugged, err := s.plugTLSOCSPPeer(config)
if err != nil {
return err
}
if plugged && tc != nil {
s.ocspPeerVerify = true
config.apply(tc)
}
}
}
@@ -692,6 +748,11 @@ func (s *Server) reloadOCSP() error {
// Dispatch all goroutines once again.
s.startOCSPMonitoring()
// Init and restart OCSP responder cache
s.stopOCSPResponseCache()
s.initOCSPResponseCache()
s.startOCSPResponseCache()
return nil
}
@@ -782,37 +843,81 @@ func parseCertPEM(name string) ([]*x509.Certificate, error) {
return x509.ParseCertificates(pemBytes)
}
// getOCSPIssuer returns a CA cert from the given path. If the path is empty,
// then this checks a given cert chain. If both are empty, then it returns an
// error.
func getOCSPIssuer(issuerCert string, chain [][]byte) ([]*x509.Certificate, error) {
var issuers []*x509.Certificate
var err error
switch {
case len(chain) == 1 && issuerCert == _EMPTY_:
err = fmt.Errorf("ocsp ca required in chain or configuration")
case issuerCert != _EMPTY_:
issuers, err = parseCertPEM(issuerCert)
case len(chain) > 1 && issuerCert == _EMPTY_:
issuers, err = x509.ParseCertificates(chain[1])
default:
err = fmt.Errorf("invalid ocsp ca configuration")
}
if err != nil {
return nil, err
// getOCSPIssuerLocally determines a leaf's issuer from locally configured certificates
func getOCSPIssuerLocally(trustedCAs []*x509.Certificate, certBundle []*x509.Certificate) (*x509.Certificate, error) {
var vOpts x509.VerifyOptions
var leaf *x509.Certificate
trustedCAPool := x509.NewCertPool()
// Require Leaf as first cert in bundle
if len(certBundle) > 0 {
leaf = certBundle[0]
} else {
return nil, fmt.Errorf("invalid ocsp ca configuration")
}
if len(issuers) == 0 {
return nil, fmt.Errorf("no issuers found")
}
for _, issuer := range issuers {
if !issuer.IsCA {
return nil, fmt.Errorf("%s invalid ca basic constraints: is not ca", issuer.Subject)
// Allow Issuer to be configured as second cert in bundle
if len(certBundle) > 1 {
// The operator may have misconfigured the cert bundle
issuerCandidate := certBundle[1]
err := issuerCandidate.CheckSignature(leaf.SignatureAlgorithm, leaf.RawTBSCertificate, leaf.Signature)
if err != nil {
return nil, fmt.Errorf("invalid issuer configuration: %w", err)
} else {
return issuerCandidate, nil
}
}
return issuers, nil
// Operator did not provide the Leaf Issuer in cert bundle second position
// so we will attempt to create at least one ordered verified chain from the
// trusted CA pool.
// Specify CA trust store to validator; if unset, system trust store used
if len(trustedCAs) > 0 {
for _, ca := range trustedCAs {
trustedCAPool.AddCert(ca)
}
vOpts.Roots = trustedCAPool
}
return certstore.GetLeafIssuer(leaf, vOpts), nil
}
// getOCSPIssuer determines an issuer certificate from the cert (bundle) or the file-based CA trust store
func getOCSPIssuer(caFile string, chain [][]byte) (*x509.Certificate, error) {
var issuer *x509.Certificate
var trustedCAs []*x509.Certificate
var certBundle []*x509.Certificate
var err error
// FIXME(tgb): extend if pluggable CA store provider added to NATS (i.e. other than PEM file)
// Non-system default CA trust store passed
if caFile != _EMPTY_ {
trustedCAs, err = parseCertPEM(caFile)
if err != nil {
return nil, fmt.Errorf("failed to parse ca_file: %v", err)
}
}
// Specify bundled intermediate CA store
for _, certBytes := range chain {
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse cert: %v", err)
}
certBundle = append(certBundle, cert)
}
issuer, err = getOCSPIssuerLocally(trustedCAs, certBundle)
if err != nil || issuer == nil {
return nil, fmt.Errorf("no issuers found")
}
if !issuer.IsCA {
return nil, fmt.Errorf("%s invalid ca basic constraints: is not ca", issuer.Subject)
}
return issuer, nil
}
func ocspStatusString(n int) string {
+405
View File
@@ -0,0 +1,405 @@
// Copyright 2023 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 (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/crypto/ocsp"
"github.com/nats-io/nats-server/v2/server/certidp"
)
func parseOCSPPeer(v interface{}) (pcfg *certidp.OCSPPeerConfig, retError error) {
var lt token
defer convertPanicToError(&lt, &retError)
tk, v := unwrapValue(v, &lt)
cm, ok := v.(map[string]interface{})
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalPeerOptsConfig, v)}
}
pcfg = certidp.NewOCSPPeerConfig()
retError = nil
for mk, mv := range cm {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "verify":
verify, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.Verify = verify
case "allowed_clockskew":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.ClockSkew = at
}
case "ca_timeout":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.Timeout = at
}
case "cache_ttl_when_next_update_unset":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.TTLUnsetNextUpdate = at
}
case "warn_only":
warnOnly, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.WarnOnly = warnOnly
case "unknown_is_good":
unknownIsGood, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.UnknownIsGood = unknownIsGood
case "allow_when_ca_unreachable":
allowWhenCAUnreachable, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.AllowWhenCAUnreachable = allowWhenCAUnreachable
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
}
return pcfg, nil
}
func peerFromVerifiedChains(chains [][]*x509.Certificate) *x509.Certificate {
if len(chains) == 0 || len(chains[0]) == 0 {
return nil
}
return chains[0][0]
}
// plugTLSOCSPPeer will plug the TLS handshake lifecycle for client mTLS connections and Leaf connections
func (s *Server) plugTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, error) {
if config == nil || config.tlsConfig == nil {
return nil, false, errors.New(certidp.ErrUnableToPlugTLSEmptyConfig)
}
s.Debugf(certidp.DbgPlugTLSForKind, config.kind)
kind := config.kind
isSpoke := config.isLeafSpoke
tcOpts := config.tlsOpts
if tcOpts == nil || tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
return nil, false, nil
}
// peer is a tls client
if kind == kindStringMap[CLIENT] || (kind == kindStringMap[LEAF] && !isSpoke) {
if !tcOpts.Verify {
return nil, false, errors.New(certidp.ErrMTLSRequired)
}
return s.plugClientTLSOCSPPeer(config)
}
// peer is a tls server
if kind == kindStringMap[LEAF] && isSpoke {
return s.plugServerTLSOCSPPeer(config)
}
return nil, false, nil
}
func (s *Server) plugClientTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, error) {
if config == nil || config.tlsConfig == nil || config.tlsOpts == nil {
return nil, false, errors.New(certidp.ErrUnableToPlugTLSClient)
}
tc := config.tlsConfig
tcOpts := config.tlsOpts
kind := config.kind
if tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
return tc, false, nil
}
tc.VerifyConnection = func(cs tls.ConnectionState) error {
if !s.tlsClientOCSPValid(cs.VerifiedChains, tcOpts.OCSPPeerConfig) {
s.sendOCSPPeerRejectEvent(kind, peerFromVerifiedChains(cs.VerifiedChains), certidp.MsgTLSClientRejectConnection)
return errors.New(certidp.MsgTLSClientRejectConnection)
}
return nil
}
return tc, true, nil
}
func (s *Server) plugServerTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, error) {
if config == nil || config.tlsConfig == nil || config.tlsOpts == nil {
return nil, false, errors.New(certidp.ErrUnableToPlugTLSServer)
}
tc := config.tlsConfig
tcOpts := config.tlsOpts
kind := config.kind
if tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
return tc, false, nil
}
tc.VerifyConnection = func(cs tls.ConnectionState) error {
if !s.tlsServerOCSPValid(cs.VerifiedChains, tcOpts.OCSPPeerConfig) {
s.sendOCSPPeerRejectEvent(kind, peerFromVerifiedChains(cs.VerifiedChains), certidp.MsgTLSServerRejectConnection)
return errors.New(certidp.MsgTLSServerRejectConnection)
}
return nil
}
return tc, true, nil
}
// tlsServerOCSPValid evaluates verified chains (post successful TLS handshake) against OCSP
// eligibility. A verified chain is considered OCSP Valid if either none of the links are
// OCSP eligible, or current "good" responses from the CA can be obtained for each eligible link.
// Upon first OCSP Valid chain found, the Server is deemed OCSP Valid. If none of the chains are
// OCSP Valid, the Server is deemed OCSP Invalid. A verified self-signed certificate (chain length 1)
// is also considered OCSP Valid.
func (s *Server) tlsServerOCSPValid(chains [][]*x509.Certificate, opts *certidp.OCSPPeerConfig) bool {
s.Debugf(certidp.DbgNumServerChains, len(chains))
return s.peerOCSPValid(chains, opts)
}
// tlsClientOCSPValid evaluates verified chains (post successful TLS handshake) against OCSP
// eligibility. A verified chain is considered OCSP Valid if either none of the links are
// OCSP eligible, or current "good" responses from the CA can be obtained for each eligible link.
// Upon first OCSP Valid chain found, the Client is deemed OCSP Valid. If none of the chains are
// OCSP Valid, the Client is deemed OCSP Invalid. A verified self-signed certificate (chain length 1)
// is also considered OCSP Valid.
func (s *Server) tlsClientOCSPValid(chains [][]*x509.Certificate, opts *certidp.OCSPPeerConfig) bool {
s.Debugf(certidp.DbgNumClientChains, len(chains))
return s.peerOCSPValid(chains, opts)
}
func (s *Server) peerOCSPValid(chains [][]*x509.Certificate, opts *certidp.OCSPPeerConfig) bool {
peer := peerFromVerifiedChains(chains)
if peer == nil {
s.Errorf(certidp.ErrPeerEmptyAutoReject)
return false
}
for ci, chain := range chains {
s.Debugf(certidp.DbgLinksInChain, ci, len(chain))
// Self-signed certificate is Client OCSP Valid (no CA)
if len(chain) == 1 {
s.Debugf(certidp.DbgSelfSignedValid, ci)
return true
}
// Check if any of the links in the chain are OCSP eligible
chainEligible := false
var eligibleLinks []*certidp.ChainLink
// Iterate over links skipping the root cert which is not OCSP eligible (self == issuer)
for linkPos := 0; linkPos < len(chain)-1; linkPos++ {
cert := chain[linkPos]
link := &certidp.ChainLink{
Leaf: cert,
}
if certidp.CertOCSPEligible(link) {
chainEligible = true
issuerCert := certidp.GetLeafIssuerCert(chain, linkPos)
if issuerCert == nil {
// unexpected chain condition, reject Client as OCSP Invalid
return false
}
link.Issuer = issuerCert
eligibleLinks = append(eligibleLinks, link)
}
}
// A trust-store verified chain that is not OCSP eligible is always OCSP Valid
if !chainEligible {
s.Debugf(certidp.DbgValidNonOCSPChain, ci)
return true
}
s.Debugf(certidp.DbgChainIsOCSPEligible, ci, len(eligibleLinks))
// Chain has at least one OCSP eligible link, so check each eligible link;
// any link with a !good OCSP response chain OCSP Invalid
chainValid := true
for _, link := range eligibleLinks {
// if option selected, good could reflect either ocsp.Good or ocsp.Unknown
if badReason, good := s.certOCSPGood(link, opts); !good {
s.Debugf(badReason)
s.sendOCSPPeerChainlinkInvalidEvent(peer, link.Leaf, badReason)
chainValid = false
break
}
}
if chainValid {
s.Debugf(certidp.DbgChainIsOCSPValid, ci)
return true
}
}
// If we are here, all chains had OCSP eligible links, but none of the chains achieved OCSP valid
s.Debugf(certidp.DbgNoOCSPValidChains)
return false
}
func (s *Server) certOCSPGood(link *certidp.ChainLink, opts *certidp.OCSPPeerConfig) (string, bool) {
if link == nil || link.Leaf == nil || link.Issuer == nil || link.OCSPWebEndpoints == nil || len(*link.OCSPWebEndpoints) < 1 {
return "Empty chainlink found", false
}
var err error
sLogs := &certidp.Log{
Debugf: s.Debugf,
Noticef: s.Noticef,
Warnf: s.Warnf,
Errorf: s.Errorf,
Tracef: s.Tracef,
}
fingerprint := certidp.GenerateFingerprint(link.Leaf)
// Used for debug/operator only, not match
subj := certidp.GetSubjectDNForm(link.Leaf)
var rawResp []byte
var ocspr *ocsp.Response
var useCachedResp bool
var rc = s.ocsprc
var cachedRevocation bool
// Check our cache before calling out to the CA OCSP responder
s.Debugf(certidp.DbgCheckingCacheForCert, subj, fingerprint)
if rawResp = rc.Get(fingerprint, sLogs); len(rawResp) > 0 {
// Signature validation of CA's OCSP response occurs in ParseResponse
ocspr, err = ocsp.ParseResponse(rawResp, link.Issuer)
if err == nil && ocspr != nil {
// Check if OCSP Response delegation present and if so is valid
if !certidp.ValidDelegationCheck(link.Issuer, ocspr) {
// Invalid delegation was already in cache, purge it and don't use it
s.Debugf(certidp.MsgCachedOCSPResponseInvalid, subj)
rc.Delete(fingerprint, true, sLogs)
goto AFTERCACHE
}
if certidp.OCSPResponseCurrent(ocspr, opts, sLogs) {
s.Debugf(certidp.DbgCurrentResponseCached, certidp.GetStatusAssertionStr(ocspr.Status))
useCachedResp = true
} else {
// Cached response is not current, delete it and tidy runtime stats to reflect a miss;
// if preserve_revoked is enabled, the cache will not delete the cached response
s.Debugf(certidp.DbgExpiredResponseCached, certidp.GetStatusAssertionStr(ocspr.Status))
rc.Delete(fingerprint, true, sLogs)
}
// Regardless of currency, record a cached revocation found in case AllowWhenCAUnreachable is set
if ocspr.Status == ocsp.Revoked {
cachedRevocation = true
}
} else {
// Bogus cached assertion, purge it and don't use it
s.Debugf(certidp.MsgCachedOCSPResponseInvalid, subj, fingerprint)
rc.Delete(fingerprint, true, sLogs)
goto AFTERCACHE
}
}
AFTERCACHE:
if !useCachedResp {
// CA OCSP responder callout needed
rawResp, err = certidp.FetchOCSPResponse(link, opts, sLogs)
if err != nil || rawResp == nil || len(rawResp) == 0 {
s.Warnf(certidp.ErrCAResponderCalloutFail, subj, err)
if opts.WarnOnly {
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
if opts.AllowWhenCAUnreachable && !cachedRevocation {
// Link has no cached history of revocation, so allow it to pass
s.Warnf(certidp.MsgAllowWhenCAUnreachableOccurred, subj)
return _EMPTY_, true
} else if opts.AllowWhenCAUnreachable {
// Link has cached but expired revocation so reject when CA is unreachable
s.Warnf(certidp.MsgAllowWhenCAUnreachableOccurredCachedRevoke, subj)
}
return certidp.MsgFailedOCSPResponseFetch, false
}
// Signature validation of CA's OCSP response occurs in ParseResponse
ocspr, err = ocsp.ParseResponse(rawResp, link.Issuer)
if err == nil && ocspr != nil {
// Check if OCSP Response delegation present and if so is valid
if !certidp.ValidDelegationCheck(link.Issuer, ocspr) {
s.Warnf(certidp.MsgOCSPResponseDelegationInvalid, subj)
if opts.WarnOnly {
// Can't use bogus assertion, but warn-only set so allow link to pass
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return fmt.Sprintf(certidp.MsgOCSPResponseDelegationInvalid, subj), false
}
if !certidp.OCSPResponseCurrent(ocspr, opts, sLogs) {
s.Warnf(certidp.ErrNewCAResponseNotCurrent, subj)
if opts.WarnOnly {
// Can't use non-effective assertion, but warn-only set so allow link to pass
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return certidp.MsgOCSPResponseNotEffective, false
}
} else {
s.Errorf(certidp.ErrCAResponseParseFailed, subj, err)
if opts.WarnOnly {
// Can't use bogus assertion, but warn-only set so allow link to pass
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return certidp.MsgFailedOCSPResponseParse, false
}
// cache the valid fetched CA OCSP Response
rc.Put(fingerprint, ocspr, subj, sLogs)
}
// Whether through valid cache response available or newly fetched valid response, now check the status
if ocspr.Status == ocsp.Revoked || (ocspr.Status == ocsp.Unknown && !opts.UnknownIsGood) {
s.Warnf(certidp.ErrOCSPInvalidPeerLink, subj, certidp.GetStatusAssertionStr(ocspr.Status))
if opts.WarnOnly {
s.Warnf(certidp.MsgAllowWarnOnlyOccurred, subj)
return _EMPTY_, true
}
return fmt.Sprintf(certidp.MsgOCSPResponseInvalidStatus, certidp.GetStatusAssertionStr(ocspr.Status)), false
}
s.Debugf(certidp.DbgOCSPValidPeerLink, subj)
return _EMPTY_, true
}
+636
View File
@@ -0,0 +1,636 @@
// Copyright 2023 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 (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/klauspost/compress/s2"
"golang.org/x/crypto/ocsp"
"github.com/nats-io/nats-server/v2/server/certidp"
)
const (
OCSPResponseCacheDefaultDir = "_rc_"
OCSPResponseCacheDefaultFilename = "cache.json"
OCSPResponseCacheDefaultTempFilePrefix = "ocsprc-*"
OCSPResponseCacheMinimumSaveInterval = 1 * time.Second
OCSPResponseCacheDefaultSaveInterval = 5 * time.Minute
)
type OCSPResponseCacheType int
const (
NONE OCSPResponseCacheType = iota + 1
LOCAL
)
var OCSPResponseCacheTypeMap = map[string]OCSPResponseCacheType{
"none": NONE,
"local": LOCAL,
}
type OCSPResponseCacheConfig struct {
Type OCSPResponseCacheType
LocalStore string
PreserveRevoked bool
SaveInterval float64
}
func NewOCSPResponseCacheConfig() *OCSPResponseCacheConfig {
return &OCSPResponseCacheConfig{
Type: LOCAL,
LocalStore: OCSPResponseCacheDefaultDir,
PreserveRevoked: false,
SaveInterval: OCSPResponseCacheDefaultSaveInterval.Seconds(),
}
}
type OCSPResponseCacheStats struct {
Responses int64 `json:"size"`
Hits int64 `json:"hits"`
Misses int64 `json:"misses"`
Revokes int64 `json:"revokes"`
Goods int64 `json:"goods"`
Unknowns int64 `json:"unknowns"`
}
type OCSPResponseCacheItem struct {
Subject string `json:"subject,omitempty"`
CachedAt time.Time `json:"cached_at"`
RespStatus certidp.StatusAssertion `json:"resp_status"`
RespExpires time.Time `json:"resp_expires,omitempty"`
Resp []byte `json:"resp"`
}
type OCSPResponseCache interface {
Put(key string, resp *ocsp.Response, subj string, log *certidp.Log)
Get(key string, log *certidp.Log) []byte
Delete(key string, miss bool, log *certidp.Log)
Type() string
Start(s *Server)
Stop(s *Server)
Online() bool
Config() *OCSPResponseCacheConfig
Stats() *OCSPResponseCacheStats
}
// NoOpCache is a no-op implementation of OCSPResponseCache
type NoOpCache struct {
config *OCSPResponseCacheConfig
stats *OCSPResponseCacheStats
online bool
mu *sync.RWMutex
}
func (c *NoOpCache) Put(_ string, _ *ocsp.Response, _ string, _ *certidp.Log) {}
func (c *NoOpCache) Get(_ string, _ *certidp.Log) []byte {
return nil
}
func (c *NoOpCache) Delete(_ string, _ bool, _ *certidp.Log) {}
func (c *NoOpCache) Start(_ *Server) {
c.mu.Lock()
defer c.mu.Unlock()
c.stats = &OCSPResponseCacheStats{}
c.online = true
}
func (c *NoOpCache) Stop(_ *Server) {
c.mu.Lock()
defer c.mu.Unlock()
c.online = false
}
func (c *NoOpCache) Online() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.online
}
func (c *NoOpCache) Type() string {
c.mu.RLock()
defer c.mu.RUnlock()
return "none"
}
func (c *NoOpCache) Config() *OCSPResponseCacheConfig {
c.mu.RLock()
defer c.mu.RUnlock()
return c.config
}
func (c *NoOpCache) Stats() *OCSPResponseCacheStats {
c.mu.RLock()
defer c.mu.RUnlock()
return c.stats
}
// LocalCache is a local file implementation of OCSPResponseCache
type LocalCache struct {
config *OCSPResponseCacheConfig
stats *OCSPResponseCacheStats
online bool
cache map[string]OCSPResponseCacheItem
mu *sync.RWMutex
saveInterval time.Duration
dirty bool
timer *time.Timer
}
// Put captures a CA OCSP response to the OCSP peer cache indexed by response fingerprint (a hash)
func (c *LocalCache) Put(key string, caResp *ocsp.Response, subj string, log *certidp.Log) {
c.mu.RLock()
if !c.online || caResp == nil || key == "" {
c.mu.RUnlock()
return
}
c.mu.RUnlock()
log.Debugf(certidp.DbgCachingResponse, subj, key)
rawC, err := c.Compress(caResp.Raw)
if err != nil {
log.Errorf(certidp.ErrResponseCompressFail, key, err)
return
}
log.Debugf(certidp.DbgAchievedCompression, float64(len(rawC))/float64(len(caResp.Raw)))
c.mu.Lock()
defer c.mu.Unlock()
// check if we are replacing and do stats
item, ok := c.cache[key]
if ok {
c.adjustStats(-1, item.RespStatus)
}
item = OCSPResponseCacheItem{
Subject: subj,
CachedAt: time.Now().UTC().Round(time.Second),
RespStatus: certidp.StatusAssertionIntToVal[caResp.Status],
RespExpires: caResp.NextUpdate,
Resp: rawC,
}
c.cache[key] = item
c.adjustStats(1, item.RespStatus)
c.dirty = true
}
// Get returns a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash)
func (c *LocalCache) Get(key string, log *certidp.Log) []byte {
c.mu.RLock()
defer c.mu.RUnlock()
if !c.online || key == "" {
return nil
}
val, ok := c.cache[key]
if ok {
atomic.AddInt64(&c.stats.Hits, 1)
log.Debugf(certidp.DbgCacheHit, key)
} else {
atomic.AddInt64(&c.stats.Misses, 1)
log.Debugf(certidp.DbgCacheMiss, key)
return nil
}
resp, err := c.Decompress(val.Resp)
if err != nil {
log.Errorf(certidp.ErrResponseDecompressFail, key, err)
return nil
}
return resp
}
func (c *LocalCache) adjustStatsHitToMiss() {
atomic.AddInt64(&c.stats.Misses, 1)
atomic.AddInt64(&c.stats.Hits, -1)
}
func (c *LocalCache) adjustStats(delta int64, rs certidp.StatusAssertion) {
if delta == 0 {
return
}
atomic.AddInt64(&c.stats.Responses, delta)
switch rs {
case ocsp.Good:
atomic.AddInt64(&c.stats.Goods, delta)
case ocsp.Revoked:
atomic.AddInt64(&c.stats.Revokes, delta)
case ocsp.Unknown:
atomic.AddInt64(&c.stats.Unknowns, delta)
}
}
// Delete removes a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash)
func (c *LocalCache) Delete(key string, wasMiss bool, log *certidp.Log) {
c.mu.Lock()
defer c.mu.Unlock()
if !c.online || key == "" || c.config == nil {
return
}
item, ok := c.cache[key]
if !ok {
return
}
if item.RespStatus == ocsp.Revoked && c.config.PreserveRevoked {
log.Debugf(certidp.DbgPreservedRevocation, key)
if wasMiss {
c.adjustStatsHitToMiss()
}
return
}
log.Debugf(certidp.DbgDeletingCacheResponse, key)
delete(c.cache, key)
c.adjustStats(-1, item.RespStatus)
if wasMiss {
c.adjustStatsHitToMiss()
}
c.dirty = true
}
// Start initializes the configured OCSP peer cache, loads a saved cache from disk (if present), and initializes runtime statistics
func (c *LocalCache) Start(s *Server) {
s.Debugf(certidp.DbgStartingCache)
c.loadCache(s)
c.initStats()
c.mu.Lock()
c.online = true
c.mu.Unlock()
}
func (c *LocalCache) Stop(s *Server) {
c.mu.Lock()
s.Debugf(certidp.DbgStoppingCache)
c.online = false
c.timer.Stop()
c.mu.Unlock()
c.saveCache(s)
}
func (c *LocalCache) Online() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.online
}
func (c *LocalCache) Type() string {
c.mu.RLock()
defer c.mu.RUnlock()
return "local"
}
func (c *LocalCache) Config() *OCSPResponseCacheConfig {
c.mu.RLock()
defer c.mu.RUnlock()
return c.config
}
func (c *LocalCache) Stats() *OCSPResponseCacheStats {
c.mu.RLock()
defer c.mu.RUnlock()
if c.stats == nil {
return nil
}
stats := OCSPResponseCacheStats{
Responses: c.stats.Responses,
Hits: c.stats.Hits,
Misses: c.stats.Misses,
Revokes: c.stats.Revokes,
Goods: c.stats.Goods,
Unknowns: c.stats.Unknowns,
}
return &stats
}
func (c *LocalCache) initStats() {
c.mu.Lock()
defer c.mu.Unlock()
c.stats = &OCSPResponseCacheStats{}
c.stats.Hits = 0
c.stats.Misses = 0
c.stats.Responses = int64(len(c.cache))
for _, resp := range c.cache {
switch resp.RespStatus {
case ocsp.Good:
c.stats.Goods++
case ocsp.Revoked:
c.stats.Revokes++
case ocsp.Unknown:
c.stats.Unknowns++
}
}
}
func (c *LocalCache) Compress(buf []byte) ([]byte, error) {
bodyLen := int64(len(buf))
var output bytes.Buffer
writer := s2.NewWriter(&output)
input := bytes.NewReader(buf[:bodyLen])
if n, err := io.CopyN(writer, input, bodyLen); err != nil {
return nil, fmt.Errorf(certidp.ErrCannotWriteCompressed, err)
} else if n != bodyLen {
return nil, fmt.Errorf(certidp.ErrTruncatedWrite, n, bodyLen)
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf(certidp.ErrCannotCloseWriter, err)
}
return output.Bytes(), nil
}
func (c *LocalCache) Decompress(buf []byte) ([]byte, error) {
bodyLen := int64(len(buf))
input := bytes.NewReader(buf[:bodyLen])
reader := io.NopCloser(s2.NewReader(input))
output, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf(certidp.ErrCannotReadCompressed, err)
}
return output, reader.Close()
}
func (c *LocalCache) loadCache(s *Server) {
d := s.opts.OCSPCacheConfig.LocalStore
if d == _EMPTY_ {
d = OCSPResponseCacheDefaultDir
}
f := OCSPResponseCacheDefaultFilename
store, err := filepath.Abs(path.Join(d, f))
if err != nil {
s.Errorf(certidp.ErrLoadCacheFail, err)
return
}
s.Debugf(certidp.DbgLoadingCache, store)
c.mu.Lock()
defer c.mu.Unlock()
c.cache = make(map[string]OCSPResponseCacheItem)
dat, err := os.ReadFile(store)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
s.Debugf(certidp.DbgNoCacheFound)
} else {
s.Warnf(certidp.ErrLoadCacheFail, err)
}
return
}
err = json.Unmarshal(dat, &c.cache)
if err != nil {
// make sure clean cache
c.cache = make(map[string]OCSPResponseCacheItem)
s.Warnf(certidp.ErrLoadCacheFail, err)
c.dirty = true
return
}
c.dirty = false
}
func (c *LocalCache) saveCache(s *Server) {
c.mu.RLock()
dirty := c.dirty
c.mu.RUnlock()
if !dirty {
return
}
s.Debugf(certidp.DbgCacheDirtySave)
var d string
if c.config.LocalStore != _EMPTY_ {
d = c.config.LocalStore
} else {
d = OCSPResponseCacheDefaultDir
}
f := OCSPResponseCacheDefaultFilename
store, err := filepath.Abs(path.Join(d, f))
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
s.Debugf(certidp.DbgSavingCache, store)
if _, err := os.Stat(d); os.IsNotExist(err) {
err = os.Mkdir(d, defaultDirPerms)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
}
tmp, err := os.CreateTemp(d, OCSPResponseCacheDefaultTempFilePrefix)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
defer func() {
tmp.Close()
os.Remove(tmp.Name())
}() // clean up any temp files
// RW lock here because we're going to snapshot the cache to disk and mark as clean if successful
c.mu.Lock()
defer c.mu.Unlock()
dat, err := json.MarshalIndent(c.cache, "", " ")
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
cacheSize, err := tmp.Write(dat)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
err = tmp.Sync()
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
err = tmp.Close()
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
// do the final swap and overwrite any old saved peer cache
err = os.Rename(tmp.Name(), store)
if err != nil {
s.Errorf(certidp.ErrSaveCacheFail, err)
return
}
c.dirty = false
s.Debugf(certidp.DbgCacheSaved, cacheSize)
}
var OCSPResponseCacheUsage = `
You may enable OCSP peer response cacheing at server configuration root level:
(If no TLS blocks are configured with OCSP peer verification, ocsp_cache is ignored.)
...
# short form enables with defaults
ocsp_cache: true
# if false or undefined and one or more TLS blocks are configured with OCSP peer verification, "none" is implied
# long form includes settable options
ocsp_cache {
# Cache type <none, local> (default local)
type: local
# Cache file directory for local-type cache (default _rc_ in current working directory)
local_store: "_rc_"
# Ignore cache deletes if cached OCSP response is Revoked status (default false)
preserve_revoked: false
# For local store, interval to save in-memory cache to disk in seconds (default 300 seconds, minimum 1 second)
save_interval: 300
}
...
Note: Cache of server's own OCSP response (staple) is enabled using the 'ocsp' configuration option.
`
func (s *Server) initOCSPResponseCache() {
// No mTLS OCSP or Leaf OCSP enablements, so no need to init cache
s.mu.RLock()
if !s.ocspPeerVerify {
s.mu.RUnlock()
return
}
s.mu.RUnlock()
so := s.getOpts()
if so.OCSPCacheConfig == nil {
so.OCSPCacheConfig = NewOCSPResponseCacheConfig()
}
var cc = so.OCSPCacheConfig
s.mu.Lock()
defer s.mu.Unlock()
switch cc.Type {
case NONE:
s.ocsprc = &NoOpCache{config: cc, online: true, mu: &sync.RWMutex{}}
case LOCAL:
c := &LocalCache{
config: cc,
online: false,
cache: make(map[string]OCSPResponseCacheItem),
mu: &sync.RWMutex{},
dirty: false,
}
c.saveInterval = time.Duration(cc.SaveInterval) * time.Second
c.timer = time.AfterFunc(c.saveInterval, func() {
s.Debugf(certidp.DbgCacheSaveTimerExpired)
c.saveCache(s)
c.timer.Reset(c.saveInterval)
})
s.ocsprc = c
default:
s.Fatalf(certidp.ErrBadCacheTypeConfig, cc.Type)
}
}
func (s *Server) startOCSPResponseCache() {
// No mTLS OCSP or Leaf OCSP enablements, so no need to start cache
s.mu.RLock()
if !s.ocspPeerVerify || s.ocsprc == nil {
s.mu.RUnlock()
return
}
s.mu.RUnlock()
// Could be heavier operation depending on cache implementation
s.ocsprc.Start(s)
if s.ocsprc.Online() {
s.Noticef(certidp.MsgCacheOnline, s.ocsprc.Type())
} else {
s.Noticef(certidp.MsgCacheOffline, s.ocsprc.Type())
}
}
func (s *Server) stopOCSPResponseCache() {
s.mu.RLock()
if s.ocsprc == nil {
s.mu.RUnlock()
return
}
s.mu.RUnlock()
s.ocsprc.Stop(s)
}
func parseOCSPResponseCache(v interface{}) (pcfg *OCSPResponseCacheConfig, retError error) {
var lt token
defer convertPanicToError(&lt, &retError)
tk, v := unwrapValue(v, &lt)
cm, ok := v.(map[string]interface{})
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalCacheOptsConfig, v)}
}
pcfg = NewOCSPResponseCacheConfig()
retError = nil
for mk, mv := range cm {
// Again, unwrap token value if line check is required.
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "type":
cache, ok := mv.(string)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
cacheType, exists := OCSPResponseCacheTypeMap[strings.ToLower(cache)]
if !exists {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrUnknownCacheType, cache)}
}
pcfg.Type = cacheType
case "local_store":
store, ok := mv.(string)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
pcfg.LocalStore = store
case "preserve_revoked":
preserve, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
pcfg.PreserveRevoked = preserve
case "save_interval":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldTypeConversion, "unexpected type")}
}
si := time.Duration(at) * time.Second
if si < OCSPResponseCacheMinimumSaveInterval {
si = OCSPResponseCacheMinimumSaveInterval
}
pcfg.SaveInterval = si.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
}
return pcfg, nil
}
+134 -17
View File
@@ -1,4 +1,4 @@
// Copyright 2012-2022 The NATS Authors
// Copyright 2012-2023 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
@@ -34,9 +34,10 @@ import (
"time"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nkeys"
"github.com/nats-io/nats-server/v2/conf"
"github.com/nats-io/nats-server/v2/server/certidp"
"github.com/nats-io/nats-server/v2/server/certstore"
"github.com/nats-io/nkeys"
)
var allowUnknownTopLevelField = int32(0)
@@ -53,7 +54,7 @@ func NoErrOnUnknownFields(noError bool) {
atomic.StoreInt32(&allowUnknownTopLevelField, val)
}
// Set of lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo
// PinnedCertSet is a set of lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo
type PinnedCertSet map[string]struct{}
// ClusterOpts are options for clusters.
@@ -221,6 +222,7 @@ type Options struct {
NoHeaderSupport bool `json:"-"`
DisableShortFirstPing bool `json:"-"`
Logtime bool `json:"-"`
LogtimeUTC bool `json:"-"`
MaxConn int `json:"max_connections"`
MaxSubs int `json:"max_subscriptions,omitempty"`
MaxSubTokens uint8 `json:"-"`
@@ -340,6 +342,9 @@ type Options struct {
// JetStream
maxMemSet bool
maxStoreSet bool
// OCSP Cache config enables next-gen cache for OCSP features
OCSPCacheConfig *OCSPResponseCacheConfig
}
// WebsocketOpts are options for websocket
@@ -403,6 +408,9 @@ type WebsocketOpts struct {
// and write the response back to the client. This include the
// time needed for the TLS Handshake.
HandshakeTimeout time.Duration
// Snapshot of configured TLS options.
tlsConfigOpts *TLSConfigOpts
}
// MQTTOpts are options for MQTT
@@ -483,6 +491,9 @@ type MQTTOpts struct {
// subscription ending with "#" will use 2 times the MaxAckPending value.
// Note that changes to this option is applied only to new subscriptions.
MaxAckPending uint16
// Snapshot of configured TLS options.
tlsConfigOpts *TLSConfigOpts
}
type netResolver interface {
@@ -574,6 +585,10 @@ type TLSConfigOpts struct {
Ciphers []uint16
CurvePreferences []tls.CurveID
PinnedCerts PinnedCertSet
CertStore certstore.StoreType
CertMatchBy certstore.MatchByType
CertMatch string
OCSPPeerConfig *certidp.OCSPPeerConfig
}
// OCSPConfig represents the options of OCSP stapling options.
@@ -786,6 +801,9 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
case "logtime":
o.Logtime = v.(bool)
trackExplicitVal(o, &o.inConfig, "Logtime", o.Logtime)
case "logtime_utc":
o.LogtimeUTC = v.(bool)
trackExplicitVal(o, &o.inConfig, "LogtimeUTC", o.LogtimeUTC)
case "mappings", "maps":
gacc := NewAccount(globalAccountName)
o.Accounts = append(o.Accounts, gacc)
@@ -1179,17 +1197,22 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
*errors = append(*errors, &configErr{tk, err.Error()})
return
}
if dir == "" {
*errors = append(*errors, &configErr{tk, "dir has no value and needs to point to a directory"})
return
}
if info, _ := os.Stat(dir); info != nil && (!info.IsDir() || info.Mode().Perm()&(1<<(uint(7))) == 0) {
*errors = append(*errors, &configErr{tk, "dir needs to point to an accessible directory"})
return
checkDir := func() {
if dir == _EMPTY_ {
*errors = append(*errors, &configErr{tk, "dir has no value and needs to point to a directory"})
return
}
if info, _ := os.Stat(dir); info != nil && (!info.IsDir() || info.Mode().Perm()&(1<<(uint(7))) == 0) {
*errors = append(*errors, &configErr{tk, "dir needs to point to an accessible directory"})
return
}
}
var res AccountResolver
switch strings.ToUpper(dirType) {
case "CACHE":
checkDir()
if sync != 0 {
*errors = append(*errors, &configErr{tk, "CACHE does not accept sync"})
}
@@ -1201,6 +1224,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
}
res, err = NewCacheDirAccResolver(dir, limit, ttl, opts...)
case "FULL":
checkDir()
if ttl != 0 {
*errors = append(*errors, &configErr{tk, "FULL does not accept ttl"})
}
@@ -1216,6 +1240,8 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
}
}
res, err = NewDirAccResolver(dir, limit, sync, delete, opts...)
case "MEM", "MEMORY":
res = &MemAccResolver{}
}
if err != nil {
*errors = append(*errors, &configErr{tk, err.Error()})
@@ -1393,6 +1419,34 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error
m[kk] = v.(string)
}
o.JsAccDefaultDomain = m
case "ocsp_cache":
var err error
switch vv := v.(type) {
case bool:
pc := NewOCSPResponseCacheConfig()
if vv {
// Set enabled
pc.Type = LOCAL
o.OCSPCacheConfig = pc
} else {
// Set disabled (none cache)
pc.Type = NONE
o.OCSPCacheConfig = pc
}
case map[string]interface{}:
pc, err := parseOCSPResponseCache(v)
if err != nil {
*errors = append(*errors, err)
return
}
o.OCSPCacheConfig = pc
default:
err = &configErr{tk, fmt.Sprintf("error parsing tags: unsupported type %T", v)}
}
if err != nil {
*errors = append(*errors, err)
return
}
default:
if au := atomic.LoadInt32(&allowUnknownTopLevelField); au == 0 && !tk.IsUsedVariable() {
err := &unknownConfigFieldErr{
@@ -3847,6 +3901,11 @@ func PrintTLSHelpAndDie() {
for k := range curvePreferenceMap {
fmt.Printf(" %s\n", k)
}
if runtime.GOOS == "windows" {
fmt.Printf("%s\n", certstore.Usage)
}
fmt.Printf("%s", certidp.OCSPPeerUsage)
fmt.Printf("%s", OCSPResponseCacheUsage)
os.Exit(0)
}
@@ -4004,6 +4063,54 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error)
}
tc.PinnedCerts = wl
}
case "cert_store":
certStore, ok := mv.(string)
if !ok || certStore == _EMPTY_ {
return nil, &configErr{tk, certstore.ErrBadCertStoreField.Error()}
}
certStoreType, err := certstore.ParseCertStore(certStore)
if err != nil {
return nil, &configErr{tk, err.Error()}
}
tc.CertStore = certStoreType
case "cert_match_by":
certMatchBy, ok := mv.(string)
if !ok || certMatchBy == _EMPTY_ {
return nil, &configErr{tk, certstore.ErrBadCertMatchByField.Error()}
}
certMatchByType, err := certstore.ParseCertMatchBy(certMatchBy)
if err != nil {
return nil, &configErr{tk, err.Error()}
}
tc.CertMatchBy = certMatchByType
case "cert_match":
certMatch, ok := mv.(string)
if !ok || certMatch == _EMPTY_ {
return nil, &configErr{tk, certstore.ErrBadCertMatchField.Error()}
}
tc.CertMatch = certMatch
case "ocsp_peer":
switch vv := mv.(type) {
case bool:
pc := certidp.NewOCSPPeerConfig()
if vv {
// Set enabled
pc.Verify = true
tc.OCSPPeerConfig = pc
} else {
// Set disabled
pc.Verify = false
tc.OCSPPeerConfig = pc
}
case map[string]interface{}:
pc, err := parseOCSPPeer(mv)
if err != nil {
return nil, &configErr{tk, err.Error()}
}
tc.OCSPPeerConfig = pc
default:
return nil, &configErr{tk, fmt.Sprintf("error parsing ocsp peer config: unsupported type %T", v)}
}
default:
return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, unknown field [%q]", mk)}
}
@@ -4134,6 +4241,7 @@ func parseWebsocket(v interface{}, o *Options, errors *[]error, warnings *[]erro
}
o.Websocket.TLSMap = tc.Map
o.Websocket.TLSPinnedCerts = tc.PinnedCerts
o.Websocket.tlsConfigOpts = tc
case "same_origin":
o.Websocket.SameOrigin = mv.(bool)
case "allowed_origins", "allowed_origin", "allow_origins", "allow_origin", "origins", "origin":
@@ -4224,6 +4332,7 @@ func parseMQTT(v interface{}, o *Options, errors *[]error, warnings *[]error) er
o.MQTT.TLSTimeout = tc.Timeout
o.MQTT.TLSMap = tc.Map
o.MQTT.TLSPinnedCerts = tc.PinnedCerts
o.MQTT.tlsConfigOpts = tc
case "authorization", "authentication":
auth := parseSimpleAuth(tk, errors, warnings)
o.MQTT.Username = auth.user
@@ -4290,11 +4399,13 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
}
switch {
case tc.CertFile != "" && tc.KeyFile == "":
case tc.CertFile != _EMPTY_ && tc.CertStore != certstore.STOREEMPTY:
return nil, certstore.ErrConflictCertFileAndStore
case tc.CertFile != _EMPTY_ && tc.KeyFile == _EMPTY_:
return nil, fmt.Errorf("missing 'key_file' in TLS configuration")
case tc.CertFile == "" && tc.KeyFile != "":
case tc.CertFile == _EMPTY_ && tc.KeyFile != _EMPTY_:
return nil, fmt.Errorf("missing 'cert_file' in TLS configuration")
case tc.CertFile != "" && tc.KeyFile != "":
case tc.CertFile != _EMPTY_ && tc.KeyFile != _EMPTY_:
// Now load in cert and private key
cert, err := tls.LoadX509KeyPair(tc.CertFile, tc.KeyFile)
if err != nil {
@@ -4305,6 +4416,11 @@ func GenTLSConfig(tc *TLSConfigOpts) (*tls.Config, error) {
return nil, fmt.Errorf("error parsing certificate: %v", err)
}
config.Certificates = []tls.Certificate{cert}
case tc.CertStore != certstore.STOREEMPTY:
err := certstore.TLSConfig(tc.CertStore, tc.CertMatchBy, tc.CertMatch, &config)
if err != nil {
return nil, err
}
}
// Require client certificates as needed
@@ -4690,9 +4806,10 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp,
fs.BoolVar(&dbgAndTrcAndVerboseTrc, "DVV", false, "Enable Debug and Verbose Trace logging. (Traces system account as well)")
fs.BoolVar(&opts.Logtime, "T", true, "Timestamp log entries.")
fs.BoolVar(&opts.Logtime, "logtime", true, "Timestamp log entries.")
fs.StringVar(&opts.Username, "user", "", "Username required for connection.")
fs.StringVar(&opts.Password, "pass", "", "Password required for connection.")
fs.StringVar(&opts.Authorization, "auth", "", "Authorization token required for connection.")
fs.BoolVar(&opts.LogtimeUTC, "logtime_utc", false, "Timestamps in UTC instead of local timezone.")
fs.StringVar(&opts.Username, "user", _EMPTY_, "Username required for connection.")
fs.StringVar(&opts.Password, "pass", _EMPTY_, "Password required for connection.")
fs.StringVar(&opts.Authorization, "auth", _EMPTY_, "Authorization token required for connection.")
fs.IntVar(&opts.HTTPPort, "m", 0, "HTTP Port for /varz, /connz endpoints.")
fs.IntVar(&opts.HTTPPort, "http_port", 0, "HTTP Port for /varz, /connz endpoints.")
fs.IntVar(&opts.HTTPSPort, "ms", 0, "HTTPS Port for /varz, /connz endpoints.")
+33 -7
View File
@@ -162,6 +162,17 @@ func (l *logtimeOption) Apply(server *Server) {
server.Noticef("Reloaded: logtime = %v", l.newValue)
}
// logtimeUTCOption implements the option interface for the `logtime_utc` setting.
type logtimeUTCOption struct {
loggingOption
newValue bool
}
// Apply is a no-op because logging will be reloaded after options are applied.
func (l *logtimeUTCOption) Apply(server *Server) {
server.Noticef("Reloaded: logtime_utc = %v", l.newValue)
}
// logfileOption implements the option interface for the `log_file` setting.
type logfileOption struct {
loggingOption
@@ -609,7 +620,7 @@ func (jso jetStreamOption) IsStatszChange() bool {
}
type ocspOption struct {
noopOption
tlsOption
newValue *OCSPConfig
}
@@ -617,6 +628,15 @@ func (a *ocspOption) Apply(s *Server) {
s.Noticef("Reloaded: OCSP")
}
type ocspResponseCacheOption struct {
tlsOption
newValue *OCSPResponseCacheConfig
}
func (a *ocspResponseCacheOption) Apply(s *Server) {
s.Noticef("Reloaded OCSP peer cache")
}
// connectErrorReports implements the option interface for the `connect_error_reports`
// setting.
type connectErrorReports struct {
@@ -940,7 +960,7 @@ func imposeOrder(value interface{}) error {
sort.Strings(value.AllowedOrigins)
case string, bool, uint8, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet,
*URLAccResolver, *MemAccResolver, *DirAccResolver, *CacheDirAccResolver, Authentication, MQTTOpts, jwt.TagList,
*OCSPConfig, map[string]string, JSLimitOpts, StoreCipher:
*OCSPConfig, map[string]string, JSLimitOpts, StoreCipher, *OCSPResponseCacheConfig:
// explicitly skipped types
default:
// this will fail during unit tests
@@ -1009,6 +1029,8 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
diffOpts = append(diffOpts, &debugOption{newValue: newValue.(bool)})
case "logtime":
diffOpts = append(diffOpts, &logtimeOption{newValue: newValue.(bool)})
case "logtimeutc":
diffOpts = append(diffOpts, &logtimeUTCOption{newValue: newValue.(bool)})
case "logfile":
diffOpts = append(diffOpts, &logfileOption{newValue: newValue.(string)})
case "syslog":
@@ -1264,8 +1286,8 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
// Similar to gateways
tmpOld := oldValue.(WebsocketOpts)
tmpNew := newValue.(WebsocketOpts)
tmpOld.TLSConfig = nil
tmpNew.TLSConfig = nil
tmpOld.TLSConfig, tmpOld.tlsConfigOpts = nil, nil
tmpNew.TLSConfig, tmpNew.tlsConfigOpts = nil, nil
// If there is really a change prevents reload.
if !reflect.DeepEqual(tmpOld, tmpNew) {
// See TODO(ik) note below about printing old/new values.
@@ -1284,9 +1306,9 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
// we only fail reload if some that we don't support are changed.
tmpOld := oldValue.(MQTTOpts)
tmpNew := newValue.(MQTTOpts)
tmpOld.TLSConfig, tmpOld.AckWait, tmpOld.MaxAckPending, tmpOld.StreamReplicas, tmpOld.ConsumerReplicas, tmpOld.ConsumerMemoryStorage = nil, 0, 0, 0, 0, false
tmpOld.TLSConfig, tmpOld.tlsConfigOpts, tmpOld.AckWait, tmpOld.MaxAckPending, tmpOld.StreamReplicas, tmpOld.ConsumerReplicas, tmpOld.ConsumerMemoryStorage = nil, nil, 0, 0, 0, 0, false
tmpOld.ConsumerInactiveThreshold = 0
tmpNew.TLSConfig, tmpNew.AckWait, tmpNew.MaxAckPending, tmpNew.StreamReplicas, tmpNew.ConsumerReplicas, tmpNew.ConsumerMemoryStorage = nil, 0, 0, 0, 0, false
tmpNew.TLSConfig, tmpNew.tlsConfigOpts, tmpNew.AckWait, tmpNew.MaxAckPending, tmpNew.StreamReplicas, tmpNew.ConsumerReplicas, tmpNew.ConsumerMemoryStorage = nil, nil, 0, 0, 0, 0, false
tmpNew.ConsumerInactiveThreshold = 0
if !reflect.DeepEqual(tmpOld, tmpNew) {
@@ -1339,6 +1361,8 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) {
}
case "ocspconfig":
diffOpts = append(diffOpts, &ocspOption{newValue: newValue.(*OCSPConfig)})
case "ocspcacheconfig":
diffOpts = append(diffOpts, &ocspResponseCacheOption{newValue: newValue.(*OCSPResponseCacheConfig)})
default:
// TODO(ik): Implement String() on those options to have a nice print.
// %v is difficult to figure what's what, %+v print private fields and
@@ -1476,10 +1500,12 @@ func (s *Server) applyOptions(ctx *reloadContext, opts []option) {
s.updateRemoteLeafNodesTLSConfig(newOpts)
}
// This will fire if TLS enabled at root (NATS listener) -or- if ocsp or ocsp_cache
// appear in the config.
if reloadTLS {
// Restart OCSP monitoring.
if err := s.reloadOCSP(); err != nil {
s.Warnf("Can't restart OCSP Stapling: %v", err)
s.Warnf("Can't restart OCSP features: %v", err)
}
}
+67 -16
View File
@@ -246,6 +246,12 @@ type Server struct {
// OCSP monitoring
ocsps []*OCSPMonitor
// OCSP peer verification (at least one TLS block)
ocspPeerVerify bool
// OCSP response cache
ocsprc OCSPResponseCache
// exporting account name the importer experienced issues with
incompleteAccExporterMap sync.Map
@@ -453,8 +459,8 @@ func NewServer(opts *Options) (*Server, error) {
// Ensure that non-exported options (used in tests) are properly set.
s.setLeafNodeNonExportedOptions()
// Setup OCSP Stapling. This will abort server from starting if there
// are no valid staples and OCSP policy is set to Always or MustStaple.
// Setup OCSP Stapling and OCSP Peer. This will abort server from starting if there
// are no valid staples and OCSP Stapling policy is set to Always or MustStaple.
if err := s.enableOCSP(); err != nil {
return nil, err
}
@@ -753,6 +759,12 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
opts := s.getOpts()
// We need to track service imports since we can not swap them out (unsub and re-sub)
// until the proper server struct accounts have been swapped in properly. Doing it in
// place could lead to data loss or server panic since account under new si has no real
// account and hence no sublist, so will panic on inbound message.
siMap := make(map[*Account][][]byte)
// Check opts and walk through them. We need to copy them here
// so that we do not keep a real one sitting in the options.
for _, acc := range opts.Accounts {
@@ -773,12 +785,16 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
// Collect the sids for the service imports since we are going to
// replace with new ones.
var sids [][]byte
c := a.ic
for _, si := range a.imports.services {
if c != nil && si.sid != nil {
if si.sid != nil {
sids = append(sids, si.sid)
}
}
// Setup to process later if needed.
if len(sids) > 0 || len(acc.imports.services) > 0 {
siMap[a] = sids
}
// Now reset all export/imports fields since they are going to be
// filled in shallowCopy()
a.imports.streams, a.imports.services = nil, nil
@@ -787,14 +803,6 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
// and pass `a` (our existing account) to get it updated.
acc.shallowCopy(a)
a.mu.Unlock()
// Need to release the lock for this.
s.mu.Unlock()
for _, sid := range sids {
c.processUnsub(sid)
}
// Add subscriptions for existing service imports.
a.addAllServiceImportSubs()
s.mu.Lock()
create = false
}
}
@@ -862,6 +870,7 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
for _, si := range acc.imports.services {
if v, ok := s.accounts.Load(si.acc.Name); ok {
si.acc = v.(*Account)
// It is possible to allow for latency tracking inside your
// own account, so lock only when not the same account.
if si.acc == acc {
@@ -889,6 +898,19 @@ func (s *Server) configureAccounts(reloading bool) (map[string]struct{}, error)
return true
})
// Check if we need to process service imports pending from above.
// This processing needs to be after we swap in the real accounts above.
for acc, sids := range siMap {
c := acc.ic
for _, sid := range sids {
c.processUnsub(sid)
}
acc.addAllServiceImportSubs()
s.mu.Unlock()
s.registerSystemImports(acc)
s.mu.Lock()
}
// Set the system account if it was configured.
// Otherwise create a default one.
if opts.SystemAccount != _EMPTY_ {
@@ -1888,9 +1910,13 @@ func (s *Server) Start() {
}
}
// Start OCSP Stapling monitoring for TLS certificates if enabled.
// Start OCSP Stapling monitoring for TLS certificates if enabled. Hook TLS handshake for
// OCSP check on peers (LEAF and CLIENT kind) if enabled.
s.startOCSPMonitoring()
// Configure OCSP Response Cache for peer OCSP checks if enabled.
s.initOCSPResponseCache()
// Start up gateway if needed. Do this before starting the routes, because
// we want to resolve the gateway host:port so that this information can
// be sent to other routes.
@@ -1957,6 +1983,9 @@ func (s *Server) Start() {
if !opts.DontListen {
s.AcceptLoop(clientListenReady)
}
// Bring OSCP Response cache online after accept loop started in anticipation of NATS-enabled cache types
s.startOCSPResponseCache()
}
// Shutdown will shutdown the server instance by kicking out the AcceptLoop
@@ -2117,6 +2146,12 @@ func (s *Server) Shutdown() {
}
s.Noticef("Server Exiting..")
// Stop OCSP Response Cache
if s.ocsprc != nil {
s.ocsprc.Stop(s)
}
// Close logger if applicable. It allows tests on Windows
// to be able to do proper cleanup (delete log file).
s.logging.RLock()
@@ -2217,7 +2252,7 @@ func (s *Server) AcceptLoop(clr chan struct{}) {
func (s *Server) InProcessConn() (net.Conn, error) {
pl, pr := net.Pipe()
if !s.startGoRoutine(func() {
s.createClient(pl)
s.createClientInProcess(pl)
s.grWG.Done()
}) {
pl.Close()
@@ -2572,6 +2607,14 @@ func (c *tlsMixConn) Read(b []byte) (int, error) {
}
func (s *Server) createClient(conn net.Conn) *client {
return s.createClientEx(conn, false)
}
func (s *Server) createClientInProcess(conn net.Conn) *client {
return s.createClientEx(conn, true)
}
func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client {
// Snapshot server options.
opts := s.getOpts()
@@ -2609,6 +2652,13 @@ func (s *Server) createClient(conn net.Conn) *client {
info.AuthRequired = false
}
// Check to see if this is an in-process connection with tls_required.
// If so, set as not required, but available.
if inProcess && info.TLSRequired {
info.TLSRequired = false
info.TLSAvailable = true
}
s.totalClients++
s.mu.Unlock()
@@ -2670,8 +2720,9 @@ func (s *Server) createClient(conn net.Conn) *client {
var pre []byte
// If we have both TLS and non-TLS allowed we need to see which
// one the client wants.
if !isClosed && opts.TLSConfig != nil && opts.AllowNonTLS {
// one the client wants. We'll always allow this for in-process
// connections.
if !isClosed && opts.TLSConfig != nil && (inProcess || opts.AllowNonTLS) {
pre = make([]byte, 4)
c.nc.SetReadDeadline(time.Now().Add(secondsToDuration(opts.TLSTimeout)))
n, _ := io.ReadFull(c.nc, pre[:])
+13 -5
View File
@@ -838,6 +838,14 @@ func (mset *stream) lastSeqAndCLFS() (uint64, uint64) {
return mset.lseq, mset.clfs
}
func (mset *stream) clearCLFS() uint64 {
mset.mu.Lock()
defer mset.mu.Unlock()
clfs := mset.clfs
mset.clfs, mset.clseq = 0, 0
return clfs
}
func (mset *stream) lastSeq() uint64 {
mset.mu.RLock()
lseq := mset.lseq
@@ -2093,7 +2101,7 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
var err error
if node != nil {
if js.limitsExceeded(stype) {
s.resourcesExeededError()
s.resourcesExceededError()
err = ApiErrors[JSInsufficientResourcesErr]
} else {
err = node.Propose(encodeStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts))
@@ -3364,9 +3372,9 @@ func (mset *stream) setupStore(fsCfg *FileStoreConfig) error {
// Register our server.
fs.registerServer(s)
}
mset.mu.Unlock()
// This will fire the callback but we do not require the lock since md will be 0 here.
mset.store.RegisterStorageUpdates(mset.storeUpdates)
mset.mu.Unlock()
return nil
}
@@ -3838,7 +3846,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
}
// Expected last sequence per subject.
// If we are clustered we have prechecked seq > 0.
if seq, exists := getExpectedLastSeqPerSubject(hdr); exists && (!isClustered || seq == 0) {
if seq, exists := getExpectedLastSeqPerSubject(hdr); exists {
// TODO(dlc) - We could make a new store func that does this all in one.
var smv StoreMsg
var fseq uint64
@@ -3952,7 +3960,7 @@ func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte,
// Check to see if we have exceeded our limits.
if js.limitsExceeded(stype) {
s.resourcesExeededError()
s.resourcesExceededError()
mset.clfs++
mset.mu.Unlock()
if canRespond {
+1 -1
View File
@@ -48,7 +48,7 @@ const (
// cacheMax is used to bound limit the frontend cache
slCacheMax = 1024
// If we run a sweeper we will drain to this count.
slCacheSweep = 512
slCacheSweep = 256
// plistMin is our lower bounds to create a fast plist for Match.
plistMin = 256
)
+14
View File
@@ -25,3 +25,17 @@ script:
- if [[ "$TRAVIS_GO_VERSION" =~ 1.20 ]]; then ./scripts/cov.sh TRAVIS; else go test -modfile=go_test.mod -race -v -p=1 ./... --failfast -vet=off; fi
after_success:
- if [[ "$TRAVIS_GO_VERSION" =~ 1.20 ]]; then $HOME/gopath/bin/goveralls -coverprofile=acc.out -service travis-ci; fi
jobs:
include:
- name: "Go: 1.20.x (nats-server@dev)"
go: "1.20.x"
before_script:
- go get -modfile go_test.mod github.com/nats-io/nats-server/v2@dev
- name: "Go: 1.20.x (nats-server@main)"
go: "1.20.x"
before_script:
- go get -modfile go_test.mod github.com/nats-io/nats-server/v2@main
allow_failures:
- name: "Go: 1.20.x (nats-server@dev)"
- name: "Go: 1.20.x (nats-server@main)"
+28 -72
View File
@@ -29,7 +29,7 @@ When using or transitioning to Go modules support:
```bash
# Go client latest or explicit version
go get github.com/nats-io/nats.go/@latest
go get github.com/nats-io/nats.go/@v1.27.0
go get github.com/nats-io/nats.go/@v1.28.0
# For latest NATS Server, add /v2 at the end
go get github.com/nats-io/nats-server/v2
@@ -90,91 +90,47 @@ nc.Drain()
nc.Close()
```
## JetStream Basic Usage
## JetStream
> __NOTE__
>
> We encourage you to try out a new, simplified version on JetStream API.
> The new API is currently in preview and is available under `jetstream` package.
>
> You can find more information on the new API [here](https://github.com/nats-io/nats.go/blob/main/jetstream/README.md)
JetStream is the built-in NATS persistence system. `nats.go` provides a built-in
API enabling both managing JetStream assets as well as publishing/consuming
persistent messages.
### Basic usage
```go
import "github.com/nats-io/nats.go"
// Connect to NATS
// connect to nats server
nc, _ := nats.Connect(nats.DefaultURL)
// Create JetStream Context
js, _ := nc.JetStream(nats.PublishAsyncMaxPending(256))
// create jetstream context from nats connection
js, _ := jetstream.New(nc)
// Simple Stream Publisher
js.Publish("ORDERS.scratch", []byte("hello"))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Simple Async Stream Publisher
for i := 0; i < 500; i++ {
js.PublishAsync("ORDERS.scratch", []byte("hello"))
}
select {
case <-js.PublishAsyncComplete():
case <-time.After(5 * time.Second):
fmt.Println("Did not resolve in time")
}
// get existing stream handle
stream, _ := js.Stream(ctx, "foo")
// Simple Async Ephemeral Consumer
js.Subscribe("ORDERS.*", func(m *nats.Msg) {
fmt.Printf("Received a JetStream message: %s\n", string(m.Data))
// retrieve consumer handle from a stream
cons, _ := stream.Consumer(ctx, "cons")
// consume messages from the consumer in callback
cc, _ := cons.Consume(func(msg jetstream.Msg) {
fmt.Println("Received jetstream message: ", string(msg.Data()))
msg.Ack()
})
// Simple Sync Durable Consumer (optional SubOpts at the end)
sub, err := js.SubscribeSync("ORDERS.*", nats.Durable("MONITOR"), nats.MaxDeliver(3))
m, err := sub.NextMsg(timeout)
// Simple Pull Consumer
sub, err := js.PullSubscribe("ORDERS.*", "MONITOR")
msgs, err := sub.Fetch(10)
// Unsubscribe
sub.Unsubscribe()
// Drain
sub.Drain()
defer cc.Stop()
```
## JetStream Basic Management
To find more information on `nats.go` JetStream API, visit
[`jetstream/README.md`](jetstream/README.md)
```go
import "github.com/nats-io/nats.go"
> The current JetStream API replaces the [legacy JetStream API](legacy_jetstream.md)
// Connect to NATS
nc, _ := nats.Connect(nats.DefaultURL)
## Service API
// Create JetStream Context
js, _ := nc.JetStream()
// Create a Stream
js.AddStream(&nats.StreamConfig{
Name: "ORDERS",
Subjects: []string{"ORDERS.*"},
})
// Update a Stream
js.UpdateStream(&nats.StreamConfig{
Name: "ORDERS",
MaxBytes: 8,
})
// Create a Consumer
js.AddConsumer("ORDERS", &nats.ConsumerConfig{
Durable: "MONITOR",
})
// Delete Consumer
js.DeleteConsumer("ORDERS", "MONITOR")
// Delete Stream
js.DeleteStream("ORDERS")
```
The service API (`micro`) allows you to [easily build NATS services](micro/README.md) The
services API is currently in beta release.
## Encoded Connections
+1 -1
View File
@@ -217,7 +217,7 @@ func (nc *Conn) FlushWithContext(ctx context.Context) error {
// RequestWithContext will create an Inbox and perform a Request
// using the provided cancellation context with the Inbox reply
// for the data v. A response will be decoded into the vPtr last parameter.
func (c *EncodedConn) RequestWithContext(ctx context.Context, subject string, v interface{}, vPtr interface{}) error {
func (c *EncodedConn) RequestWithContext(ctx context.Context, subject string, v any, vPtr any) error {
if ctx == nil {
return ErrInvalidContext
}
+7 -7
View File
@@ -26,8 +26,8 @@ import (
// Encoder interface is for all register encoders
type Encoder interface {
Encode(subject string, v interface{}) ([]byte, error)
Decode(subject string, data []byte, vPtr interface{}) error
Encode(subject string, v any) ([]byte, error)
Decode(subject string, data []byte, vPtr any) error
}
var encMap map[string]Encoder
@@ -88,7 +88,7 @@ func EncoderForType(encType string) Encoder {
// Publish publishes the data argument to the given subject. The data argument
// will be encoded using the associated encoder.
func (c *EncodedConn) Publish(subject string, v interface{}) error {
func (c *EncodedConn) Publish(subject string, v any) error {
b, err := c.Enc.Encode(subject, v)
if err != nil {
return err
@@ -99,7 +99,7 @@ func (c *EncodedConn) Publish(subject string, v interface{}) error {
// PublishRequest will perform a Publish() expecting a response on the
// reply subject. Use Request() for automatically waiting for a response
// inline.
func (c *EncodedConn) PublishRequest(subject, reply string, v interface{}) error {
func (c *EncodedConn) PublishRequest(subject, reply string, v any) error {
b, err := c.Enc.Encode(subject, v)
if err != nil {
return err
@@ -110,7 +110,7 @@ func (c *EncodedConn) PublishRequest(subject, reply string, v interface{}) error
// Request will create an Inbox and perform a Request() call
// with the Inbox reply for the data v. A response will be
// decoded into the vPtr Response.
func (c *EncodedConn) Request(subject string, v interface{}, vPtr interface{}, timeout time.Duration) error {
func (c *EncodedConn) Request(subject string, v any, vPtr any, timeout time.Duration) error {
b, err := c.Enc.Encode(subject, v)
if err != nil {
return err
@@ -129,7 +129,7 @@ func (c *EncodedConn) Request(subject string, v interface{}, vPtr interface{}, t
}
// Handler is a specific callback used for Subscribe. It is generalized to
// an interface{}, but we will discover its format and arguments at runtime
// an any, but we will discover its format and arguments at runtime
// and perform the correct callback, including demarshaling encoded data
// back into the appropriate struct based on the signature of the Handler.
//
@@ -150,7 +150,7 @@ func (c *EncodedConn) Request(subject string, v interface{}, vPtr interface{}, t
// and demarshal it into the given struct, e.g. person.
// There are also variants where the callback wants either the subject, or the
// subject and the reply subject.
type Handler interface{}
type Handler any
// Dissect the cb Handler's signature
func argInfo(cb Handler) (reflect.Type, int) {
+2 -2
View File
@@ -35,7 +35,7 @@ var falseB = []byte("false")
var nilB = []byte("")
// Encode
func (je *DefaultEncoder) Encode(subject string, v interface{}) ([]byte, error) {
func (je *DefaultEncoder) Encode(subject string, v any) ([]byte, error) {
switch arg := v.(type) {
case string:
bytes := *(*[]byte)(unsafe.Pointer(&arg))
@@ -58,7 +58,7 @@ func (je *DefaultEncoder) Encode(subject string, v interface{}) ([]byte, error)
}
// Decode
func (je *DefaultEncoder) Decode(subject string, data []byte, vPtr interface{}) error {
func (je *DefaultEncoder) Decode(subject string, data []byte, vPtr any) error {
// Figure out what it's pointing to...
sData := *(*string)(unsafe.Pointer(&data))
switch arg := vPtr.(type) {
+2 -2
View File
@@ -28,7 +28,7 @@ type GobEncoder struct {
// FIXME(dlc) - This could probably be more efficient.
// Encode
func (ge *GobEncoder) Encode(subject string, v interface{}) ([]byte, error) {
func (ge *GobEncoder) Encode(subject string, v any) ([]byte, error) {
b := new(bytes.Buffer)
enc := gob.NewEncoder(b)
if err := enc.Encode(v); err != nil {
@@ -38,7 +38,7 @@ func (ge *GobEncoder) Encode(subject string, v interface{}) ([]byte, error) {
}
// Decode
func (ge *GobEncoder) Decode(subject string, data []byte, vPtr interface{}) (err error) {
func (ge *GobEncoder) Decode(subject string, data []byte, vPtr any) (err error) {
dec := gob.NewDecoder(bytes.NewBuffer(data))
err = dec.Decode(vPtr)
return
+2 -2
View File
@@ -26,7 +26,7 @@ type JsonEncoder struct {
}
// Encode
func (je *JsonEncoder) Encode(subject string, v interface{}) ([]byte, error) {
func (je *JsonEncoder) Encode(subject string, v any) ([]byte, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
@@ -35,7 +35,7 @@ func (je *JsonEncoder) Encode(subject string, v interface{}) ([]byte, error) {
}
// Decode
func (je *JsonEncoder) Decode(subject string, data []byte, vPtr interface{}) (err error) {
func (je *JsonEncoder) Decode(subject string, data []byte, vPtr any) (err error) {
switch arg := vPtr.(type) {
case *string:
// If they want a string and it is a JSON string, strip quotes
+3 -3
View File
@@ -5,7 +5,7 @@ go 1.19
require (
github.com/golang/protobuf v1.4.2
github.com/klauspost/compress v1.16.5
github.com/nats-io/nats-server/v2 v2.9.16
github.com/nats-io/nats-server/v2 v2.9.19
github.com/nats-io/nkeys v0.4.4
github.com/nats-io/nuid v1.0.1
go.uber.org/goleak v1.2.1
@@ -16,7 +16,7 @@ require (
require (
github.com/minio/highwayhash v1.0.2 // indirect
github.com/nats-io/jwt/v2 v2.4.1 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/time v0.3.0 // indirect
)
+6 -6
View File
@@ -16,8 +16,8 @@ github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
github.com/nats-io/jwt/v2 v2.4.1 h1:Y35W1dgbbz2SQUYDPCaclXcuqleVmpbRa7646Jf2EX4=
github.com/nats-io/jwt/v2 v2.4.1/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI=
github.com/nats-io/nats-server/v2 v2.9.16 h1:SuNe6AyCcVy0g5326wtyU8TdqYmcPqzTjhkHojAjprc=
github.com/nats-io/nats-server/v2 v2.9.16/go.mod h1:z1cc5Q+kqJkz9mLUdlcSsdYnId4pyImHjNgoh6zxSC0=
github.com/nats-io/nats-server/v2 v2.9.19 h1:OF9jSKZGo425C/FcVVIvNgpd36CUe7aVTTXEZRJk6kA=
github.com/nats-io/nats-server/v2 v2.9.19/go.mod h1:aTb/xtLCGKhfTFLxP591CMWfkdgBmcUUSkiSOe5A3gw=
github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA=
github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -26,11 +26,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+38 -8
View File
@@ -227,13 +227,14 @@ type js struct {
opts *jsOpts
// For async publish context.
mu sync.RWMutex
rpre string
rsub *Subscription
pafs map[string]*pubAckFuture
stc chan struct{}
dch chan struct{}
rr *rand.Rand
mu sync.RWMutex
rpre string
rsub *Subscription
pafs map[string]*pubAckFuture
stc chan struct{}
dch chan struct{}
rr *rand.Rand
connStatusCh chan (Status)
}
type jsOpts struct {
@@ -666,6 +667,10 @@ func (js *js) newAsyncReply() string {
js.rsub = sub
js.rr = rand.New(rand.NewSource(time.Now().UnixNano()))
}
if js.connStatusCh == nil {
js.connStatusCh = js.nc.StatusChanged(RECONNECTING, CLOSED)
go js.resetPendingAcksOnReconnect()
}
var sb strings.Builder
sb.WriteString(js.rpre)
rn := js.rr.Int63()
@@ -679,12 +684,34 @@ func (js *js) newAsyncReply() string {
return sb.String()
}
func (js *js) resetPendingAcksOnReconnect() {
js.mu.Lock()
connStatusCh := js.connStatusCh
js.mu.Unlock()
for {
newStatus, ok := <-connStatusCh
if !ok || newStatus == CLOSED {
return
}
js.mu.Lock()
for _, paf := range js.pafs {
paf.err = ErrDisconnected
}
js.pafs = nil
js.mu.Unlock()
}
}
func (js *js) cleanupReplySub() {
js.mu.Lock()
if js.rsub != nil {
js.rsub.Unsubscribe()
js.rsub = nil
}
if js.connStatusCh != nil {
close(js.connStatusCh)
js.connStatusCh = nil
}
js.mu.Unlock()
}
@@ -1352,7 +1379,7 @@ func processConsInfo(info *ConsumerInfo, userCfg *ConsumerConfig, isPullMode boo
}
func checkConfig(s, u *ConsumerConfig) error {
makeErr := func(fieldName string, usrVal, srvVal interface{}) error {
makeErr := func(fieldName string, usrVal, srvVal any) error {
return fmt.Errorf("configuration requests %s to be %v, but consumer's value is %v", fieldName, usrVal, srvVal)
}
@@ -1991,6 +2018,9 @@ func (sub *Subscription) resetOrderedConsumer(sseq uint64) {
cfg.DeliverSubject = newDeliver
cfg.DeliverPolicy = DeliverByStartSequencePolicy
cfg.OptStartSeq = sseq
// In case the consumer was created with a start time, we need to clear it
// since we are now using a start sequence.
cfg.OptStartTime = nil
js := jsi.js
sub.mu.Unlock()
+16 -7
View File
@@ -361,20 +361,29 @@ func (js *js) upsertConsumer(stream, consumerName string, cfg *ConsumerConfig, o
var ccSubj string
if consumerName == _EMPTY_ {
// if consumer name is empty, use the legacy ephemeral endpoint
// if consumer name is empty (neither Durable nor Name is set), use the legacy ephemeral endpoint
ccSubj = fmt.Sprintf(apiLegacyConsumerCreateT, stream)
} else if err := checkConsumerName(consumerName); err != nil {
return nil, err
} else if !js.nc.serverMinVersion(2, 9, 0) || (cfg.Durable != "" && js.opts.featureFlags.useDurableConsumerCreate) {
// if server version is lower than 2.9.0 or user set the useDurableConsumerCreate flag, use the legacy DURABLE.CREATE endpoint
ccSubj = fmt.Sprintf(apiDurableCreateT, stream, consumerName)
} else {
// if above server version 2.9.0, use the endpoints with consumer name
if cfg.FilterSubject == _EMPTY_ || cfg.FilterSubject == ">" {
} else if js.nc.serverMinVersion(2, 9, 0) {
if cfg.Durable != "" && js.opts.featureFlags.useDurableConsumerCreate {
// if user set the useDurableConsumerCreate flag, use the legacy DURABLE.CREATE endpoint
ccSubj = fmt.Sprintf(apiDurableCreateT, stream, consumerName)
} else if cfg.FilterSubject == _EMPTY_ || cfg.FilterSubject == ">" {
// if filter subject is empty or ">", use the endpoint without filter subject
ccSubj = fmt.Sprintf(apiConsumerCreateT, stream, consumerName)
} else {
// if filter subject is not empty, use the endpoint with filter subject
ccSubj = fmt.Sprintf(apiConsumerCreateWithFilterSubjectT, stream, consumerName, cfg.FilterSubject)
}
} else {
if cfg.Durable != "" {
// if Durable is set, use the DURABLE.CREATE endpoint
ccSubj = fmt.Sprintf(apiDurableCreateT, stream, consumerName)
} else {
// if Durable is not set, use the legacy ephemeral endpoint
ccSubj = fmt.Sprintf(apiLegacyConsumerCreateT, stream)
}
}
resp, err := js.apiRequestWithContext(o.ctx, js.apiSubj(ccSubj), req)
+83
View File
@@ -0,0 +1,83 @@
# Legacy JetStream API
This is a documentation for the legacy JetStream API. A README for the current
API can be found [here](jetstream/README.md)
## JetStream Basic Usage
```go
import "github.com/nats-io/nats.go"
// Connect to NATS
nc, _ := nats.Connect(nats.DefaultURL)
// Create JetStream Context
js, _ := nc.JetStream(nats.PublishAsyncMaxPending(256))
// Simple Stream Publisher
js.Publish("ORDERS.scratch", []byte("hello"))
// Simple Async Stream Publisher
for i := 0; i < 500; i++ {
js.PublishAsync("ORDERS.scratch", []byte("hello"))
}
select {
case <-js.PublishAsyncComplete():
case <-time.After(5 * time.Second):
fmt.Println("Did not resolve in time")
}
// Simple Async Ephemeral Consumer
js.Subscribe("ORDERS.*", func(m *nats.Msg) {
fmt.Printf("Received a JetStream message: %s\n", string(m.Data))
})
// Simple Sync Durable Consumer (optional SubOpts at the end)
sub, err := js.SubscribeSync("ORDERS.*", nats.Durable("MONITOR"), nats.MaxDeliver(3))
m, err := sub.NextMsg(timeout)
// Simple Pull Consumer
sub, err := js.PullSubscribe("ORDERS.*", "MONITOR")
msgs, err := sub.Fetch(10)
// Unsubscribe
sub.Unsubscribe()
// Drain
sub.Drain()
```
## JetStream Basic Management
```go
import "github.com/nats-io/nats.go"
// Connect to NATS
nc, _ := nats.Connect(nats.DefaultURL)
// Create JetStream Context
js, _ := nc.JetStream()
// Create a Stream
js.AddStream(&nats.StreamConfig{
Name: "ORDERS",
Subjects: []string{"ORDERS.*"},
})
// Update a Stream
js.UpdateStream(&nats.StreamConfig{
Name: "ORDERS",
MaxBytes: 8,
})
// Create a Consumer
js.AddConsumer("ORDERS", &nats.ConsumerConfig{
Durable: "MONITOR",
})
// Delete Consumer
js.DeleteConsumer("ORDERS", "MONITOR")
// Delete Stream
js.DeleteStream("ORDERS")
```
+2 -2
View File
@@ -47,7 +47,7 @@ import (
// Default Constants
const (
Version = "1.27.0"
Version = "1.28.0"
DefaultURL = "nats://127.0.0.1:4222"
DefaultPort = 4222
DefaultMaxReconnect = 60
@@ -5471,7 +5471,7 @@ func (nc *Conn) StatusChanged(statuses ...Status) chan Status {
if len(statuses) == 0 {
statuses = []Status{CONNECTED, RECONNECTING, DISCONNECTED, CLOSED}
}
ch := make(chan Status)
ch := make(chan Status, 10)
for _, s := range statuses {
nc.registerStatusChangeListener(s, ch)
}
+4 -4
View File
@@ -23,7 +23,7 @@ import (
// Data will be encoded and decoded via the EncodedConn and its associated encoders.
// BindSendChan binds a channel for send operations to NATS.
func (c *EncodedConn) BindSendChan(subject string, channel interface{}) error {
func (c *EncodedConn) BindSendChan(subject string, channel any) error {
chVal := reflect.ValueOf(channel)
if chVal.Kind() != reflect.Chan {
return ErrChanArg
@@ -61,17 +61,17 @@ func chPublish(c *EncodedConn, chVal reflect.Value, subject string) {
}
// BindRecvChan binds a channel for receive operations from NATS.
func (c *EncodedConn) BindRecvChan(subject string, channel interface{}) (*Subscription, error) {
func (c *EncodedConn) BindRecvChan(subject string, channel any) (*Subscription, error) {
return c.bindRecvChan(subject, _EMPTY_, channel)
}
// BindRecvQueueChan binds a channel for queue-based receive operations from NATS.
func (c *EncodedConn) BindRecvQueueChan(subject, queue string, channel interface{}) (*Subscription, error) {
func (c *EncodedConn) BindRecvQueueChan(subject, queue string, channel any) (*Subscription, error) {
return c.bindRecvChan(subject, queue, channel)
}
// Internal function to bind receive operations for a channel.
func (c *EncodedConn) bindRecvChan(subject, queue string, channel interface{}) (*Subscription, error) {
func (c *EncodedConn) bindRecvChan(subject, queue string, channel any) (*Subscription, error) {
chVal := reflect.ValueOf(channel)
if chVal.Kind() != reflect.Chan {
return nil, ErrChanArg
+1
View File
@@ -622,6 +622,7 @@ func (obs *obs) Get(name string, opts ...GetObjectOpt) (ObjectResult, error) {
result.digest = sha256.New()
processChunk := func(m *Msg) {
var err error
if ctx != nil {
select {
case <-ctx.Done():