Files
mesh-controller/vendor/github.com/nats-io/nats-server/v2/server/ocsp_responsecache.go
T
jochen be92762969 Give every test a bus of its own, at the release the mesh runs (hq ADR 0237)
The live tests reached one shared bus and assert, read and remove the mesh's own objects by
their fixed names, so packages run in parallel deleted what each other read and the suite
passed only one package at a time; a red suite read as noise. internal/testbus starts a server
per test, linked in at the nats-server release go.mod pins, and a test holds that pin to the
catalogue's bus image and to the facts snapshot's bus when there is one, so the tests never run
a bus the mesh does not. The waiter test read a timing (the most connections held at one look)
and now reads the state it means (the fewest held across the wait). make check runs the packages
in parallel under the race detector, with a timeout.
2026-10-06 21:17:02 +02:00

637 lines
16 KiB
Go

// Copyright 2023-2024 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 any) (pcfg *OCSPResponseCacheConfig, retError error) {
var lt token
defer convertPanicToError(&lt, &retError)
tk, v := unwrapValue(v, &lt)
cm, ok := v.(map[string]any)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalCacheOptsConfig, v)}
}
pcfg = NewOCSPResponseCacheConfig()
retError = nil
for mk, mv := range cm {
// Again, unwrap token value if line check is required.
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "type":
cache, ok := mv.(string)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
cacheType, exists := OCSPResponseCacheTypeMap[strings.ToLower(cache)]
if !exists {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrUnknownCacheType, cache)}
}
pcfg.Type = cacheType
case "local_store":
store, ok := mv.(string)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
pcfg.LocalStore = store
case "preserve_revoked":
preserve, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
pcfg.PreserveRevoked = preserve
case "save_interval":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldTypeConversion, "unexpected type")}
}
si := time.Duration(at) * time.Second
if si < OCSPResponseCacheMinimumSaveInterval {
si = OCSPResponseCacheMinimumSaveInterval
}
pcfg.SaveInterval = si.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)}
}
}
return pcfg, nil
}