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:
committed by
Ralf Haferkamp
parent
0aafeccb93
commit
6d55325a41
+2
-3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+320
-335
File diff suppressed because it is too large
Load Diff
+7
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]"
|
||||
)
|
||||
+83
@@ -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
@@ -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)
|
||||
}
|
||||
+46
@@ -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{}
|
||||
+827
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(<, &retError)
|
||||
tk, v := unwrapValue(v, <)
|
||||
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, <)
|
||||
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
@@ -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(<, &retError)
|
||||
tk, v := unwrapValue(v, <)
|
||||
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, <)
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user