github.com/nats-io/nats-server/v2@v2.11.0-preview.2/server/ocsp_responsecache.go (about) 1 // Copyright 2023 The NATS Authors 2 // Licensed under the Apache License, Version 2.0 (the "License"); 3 // you may not use this file except in compliance with the License. 4 // You may obtain a copy of the License at 5 // 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package server 15 16 import ( 17 "bytes" 18 "encoding/json" 19 "errors" 20 "fmt" 21 "io" 22 "os" 23 "path" 24 "path/filepath" 25 "strings" 26 "sync" 27 "sync/atomic" 28 "time" 29 30 "github.com/klauspost/compress/s2" 31 "golang.org/x/crypto/ocsp" 32 33 "github.com/nats-io/nats-server/v2/server/certidp" 34 ) 35 36 const ( 37 OCSPResponseCacheDefaultDir = "_rc_" 38 OCSPResponseCacheDefaultFilename = "cache.json" 39 OCSPResponseCacheDefaultTempFilePrefix = "ocsprc-*" 40 OCSPResponseCacheMinimumSaveInterval = 1 * time.Second 41 OCSPResponseCacheDefaultSaveInterval = 5 * time.Minute 42 ) 43 44 type OCSPResponseCacheType int 45 46 const ( 47 NONE OCSPResponseCacheType = iota + 1 48 LOCAL 49 ) 50 51 var OCSPResponseCacheTypeMap = map[string]OCSPResponseCacheType{ 52 "none": NONE, 53 "local": LOCAL, 54 } 55 56 type OCSPResponseCacheConfig struct { 57 Type OCSPResponseCacheType 58 LocalStore string 59 PreserveRevoked bool 60 SaveInterval float64 61 } 62 63 func NewOCSPResponseCacheConfig() *OCSPResponseCacheConfig { 64 return &OCSPResponseCacheConfig{ 65 Type: LOCAL, 66 LocalStore: OCSPResponseCacheDefaultDir, 67 PreserveRevoked: false, 68 SaveInterval: OCSPResponseCacheDefaultSaveInterval.Seconds(), 69 } 70 } 71 72 type OCSPResponseCacheStats struct { 73 Responses int64 `json:"size"` 74 Hits int64 `json:"hits"` 75 Misses int64 `json:"misses"` 76 Revokes int64 `json:"revokes"` 77 Goods int64 `json:"goods"` 78 Unknowns int64 `json:"unknowns"` 79 } 80 81 type OCSPResponseCacheItem struct { 82 Subject string `json:"subject,omitempty"` 83 CachedAt time.Time `json:"cached_at"` 84 RespStatus certidp.StatusAssertion `json:"resp_status"` 85 RespExpires time.Time `json:"resp_expires,omitempty"` 86 Resp []byte `json:"resp"` 87 } 88 89 type OCSPResponseCache interface { 90 Put(key string, resp *ocsp.Response, subj string, log *certidp.Log) 91 Get(key string, log *certidp.Log) []byte 92 Delete(key string, miss bool, log *certidp.Log) 93 Type() string 94 Start(s *Server) 95 Stop(s *Server) 96 Online() bool 97 Config() *OCSPResponseCacheConfig 98 Stats() *OCSPResponseCacheStats 99 } 100 101 // NoOpCache is a no-op implementation of OCSPResponseCache 102 type NoOpCache struct { 103 config *OCSPResponseCacheConfig 104 stats *OCSPResponseCacheStats 105 online bool 106 mu *sync.RWMutex 107 } 108 109 func (c *NoOpCache) Put(_ string, _ *ocsp.Response, _ string, _ *certidp.Log) {} 110 111 func (c *NoOpCache) Get(_ string, _ *certidp.Log) []byte { 112 return nil 113 } 114 115 func (c *NoOpCache) Delete(_ string, _ bool, _ *certidp.Log) {} 116 117 func (c *NoOpCache) Start(_ *Server) { 118 c.mu.Lock() 119 defer c.mu.Unlock() 120 c.stats = &OCSPResponseCacheStats{} 121 c.online = true 122 } 123 124 func (c *NoOpCache) Stop(_ *Server) { 125 c.mu.Lock() 126 defer c.mu.Unlock() 127 c.online = false 128 } 129 130 func (c *NoOpCache) Online() bool { 131 c.mu.RLock() 132 defer c.mu.RUnlock() 133 return c.online 134 } 135 136 func (c *NoOpCache) Type() string { 137 c.mu.RLock() 138 defer c.mu.RUnlock() 139 return "none" 140 } 141 142 func (c *NoOpCache) Config() *OCSPResponseCacheConfig { 143 c.mu.RLock() 144 defer c.mu.RUnlock() 145 return c.config 146 } 147 148 func (c *NoOpCache) Stats() *OCSPResponseCacheStats { 149 c.mu.RLock() 150 defer c.mu.RUnlock() 151 return c.stats 152 } 153 154 // LocalCache is a local file implementation of OCSPResponseCache 155 type LocalCache struct { 156 config *OCSPResponseCacheConfig 157 stats *OCSPResponseCacheStats 158 online bool 159 cache map[string]OCSPResponseCacheItem 160 mu *sync.RWMutex 161 saveInterval time.Duration 162 dirty bool 163 timer *time.Timer 164 } 165 166 // Put captures a CA OCSP response to the OCSP peer cache indexed by response fingerprint (a hash) 167 func (c *LocalCache) Put(key string, caResp *ocsp.Response, subj string, log *certidp.Log) { 168 c.mu.RLock() 169 if !c.online || caResp == nil || key == "" { 170 c.mu.RUnlock() 171 return 172 } 173 c.mu.RUnlock() 174 log.Debugf(certidp.DbgCachingResponse, subj, key) 175 rawC, err := c.Compress(caResp.Raw) 176 if err != nil { 177 log.Errorf(certidp.ErrResponseCompressFail, key, err) 178 return 179 } 180 log.Debugf(certidp.DbgAchievedCompression, float64(len(rawC))/float64(len(caResp.Raw))) 181 c.mu.Lock() 182 defer c.mu.Unlock() 183 // check if we are replacing and do stats 184 item, ok := c.cache[key] 185 if ok { 186 c.adjustStats(-1, item.RespStatus) 187 } 188 item = OCSPResponseCacheItem{ 189 Subject: subj, 190 CachedAt: time.Now().UTC().Round(time.Second), 191 RespStatus: certidp.StatusAssertionIntToVal[caResp.Status], 192 RespExpires: caResp.NextUpdate, 193 Resp: rawC, 194 } 195 c.cache[key] = item 196 c.adjustStats(1, item.RespStatus) 197 c.dirty = true 198 } 199 200 // Get returns a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash) 201 func (c *LocalCache) Get(key string, log *certidp.Log) []byte { 202 c.mu.RLock() 203 defer c.mu.RUnlock() 204 if !c.online || key == "" { 205 return nil 206 } 207 val, ok := c.cache[key] 208 if ok { 209 atomic.AddInt64(&c.stats.Hits, 1) 210 log.Debugf(certidp.DbgCacheHit, key) 211 } else { 212 atomic.AddInt64(&c.stats.Misses, 1) 213 log.Debugf(certidp.DbgCacheMiss, key) 214 return nil 215 } 216 resp, err := c.Decompress(val.Resp) 217 if err != nil { 218 log.Errorf(certidp.ErrResponseDecompressFail, key, err) 219 return nil 220 } 221 return resp 222 } 223 224 func (c *LocalCache) adjustStatsHitToMiss() { 225 atomic.AddInt64(&c.stats.Misses, 1) 226 atomic.AddInt64(&c.stats.Hits, -1) 227 } 228 229 func (c *LocalCache) adjustStats(delta int64, rs certidp.StatusAssertion) { 230 if delta == 0 { 231 return 232 } 233 atomic.AddInt64(&c.stats.Responses, delta) 234 switch rs { 235 case ocsp.Good: 236 atomic.AddInt64(&c.stats.Goods, delta) 237 case ocsp.Revoked: 238 atomic.AddInt64(&c.stats.Revokes, delta) 239 case ocsp.Unknown: 240 atomic.AddInt64(&c.stats.Unknowns, delta) 241 } 242 } 243 244 // Delete removes a CA OCSP response from the OCSP peer cache matching the response fingerprint (a hash) 245 func (c *LocalCache) Delete(key string, wasMiss bool, log *certidp.Log) { 246 c.mu.Lock() 247 defer c.mu.Unlock() 248 if !c.online || key == "" || c.config == nil { 249 return 250 } 251 item, ok := c.cache[key] 252 if !ok { 253 return 254 } 255 if item.RespStatus == ocsp.Revoked && c.config.PreserveRevoked { 256 log.Debugf(certidp.DbgPreservedRevocation, key) 257 if wasMiss { 258 c.adjustStatsHitToMiss() 259 } 260 return 261 } 262 log.Debugf(certidp.DbgDeletingCacheResponse, key) 263 delete(c.cache, key) 264 c.adjustStats(-1, item.RespStatus) 265 if wasMiss { 266 c.adjustStatsHitToMiss() 267 } 268 c.dirty = true 269 } 270 271 // Start initializes the configured OCSP peer cache, loads a saved cache from disk (if present), and initializes runtime statistics 272 func (c *LocalCache) Start(s *Server) { 273 s.Debugf(certidp.DbgStartingCache) 274 c.loadCache(s) 275 c.initStats() 276 c.mu.Lock() 277 c.online = true 278 c.mu.Unlock() 279 } 280 281 func (c *LocalCache) Stop(s *Server) { 282 c.mu.Lock() 283 s.Debugf(certidp.DbgStoppingCache) 284 c.online = false 285 c.timer.Stop() 286 c.mu.Unlock() 287 c.saveCache(s) 288 } 289 290 func (c *LocalCache) Online() bool { 291 c.mu.RLock() 292 defer c.mu.RUnlock() 293 return c.online 294 } 295 296 func (c *LocalCache) Type() string { 297 c.mu.RLock() 298 defer c.mu.RUnlock() 299 return "local" 300 } 301 302 func (c *LocalCache) Config() *OCSPResponseCacheConfig { 303 c.mu.RLock() 304 defer c.mu.RUnlock() 305 return c.config 306 } 307 308 func (c *LocalCache) Stats() *OCSPResponseCacheStats { 309 c.mu.RLock() 310 defer c.mu.RUnlock() 311 if c.stats == nil { 312 return nil 313 } 314 stats := OCSPResponseCacheStats{ 315 Responses: c.stats.Responses, 316 Hits: c.stats.Hits, 317 Misses: c.stats.Misses, 318 Revokes: c.stats.Revokes, 319 Goods: c.stats.Goods, 320 Unknowns: c.stats.Unknowns, 321 } 322 return &stats 323 } 324 325 func (c *LocalCache) initStats() { 326 c.mu.Lock() 327 defer c.mu.Unlock() 328 c.stats = &OCSPResponseCacheStats{} 329 c.stats.Hits = 0 330 c.stats.Misses = 0 331 c.stats.Responses = int64(len(c.cache)) 332 for _, resp := range c.cache { 333 switch resp.RespStatus { 334 case ocsp.Good: 335 c.stats.Goods++ 336 case ocsp.Revoked: 337 c.stats.Revokes++ 338 case ocsp.Unknown: 339 c.stats.Unknowns++ 340 } 341 } 342 } 343 344 func (c *LocalCache) Compress(buf []byte) ([]byte, error) { 345 bodyLen := int64(len(buf)) 346 var output bytes.Buffer 347 writer := s2.NewWriter(&output) 348 input := bytes.NewReader(buf[:bodyLen]) 349 if n, err := io.CopyN(writer, input, bodyLen); err != nil { 350 return nil, fmt.Errorf(certidp.ErrCannotWriteCompressed, err) 351 } else if n != bodyLen { 352 return nil, fmt.Errorf(certidp.ErrTruncatedWrite, n, bodyLen) 353 } 354 if err := writer.Close(); err != nil { 355 return nil, fmt.Errorf(certidp.ErrCannotCloseWriter, err) 356 } 357 return output.Bytes(), nil 358 } 359 360 func (c *LocalCache) Decompress(buf []byte) ([]byte, error) { 361 bodyLen := int64(len(buf)) 362 input := bytes.NewReader(buf[:bodyLen]) 363 reader := io.NopCloser(s2.NewReader(input)) 364 output, err := io.ReadAll(reader) 365 if err != nil { 366 return nil, fmt.Errorf(certidp.ErrCannotReadCompressed, err) 367 } 368 return output, reader.Close() 369 } 370 371 func (c *LocalCache) loadCache(s *Server) { 372 d := s.opts.OCSPCacheConfig.LocalStore 373 if d == _EMPTY_ { 374 d = OCSPResponseCacheDefaultDir 375 } 376 f := OCSPResponseCacheDefaultFilename 377 store, err := filepath.Abs(path.Join(d, f)) 378 if err != nil { 379 s.Errorf(certidp.ErrLoadCacheFail, err) 380 return 381 } 382 s.Debugf(certidp.DbgLoadingCache, store) 383 c.mu.Lock() 384 defer c.mu.Unlock() 385 c.cache = make(map[string]OCSPResponseCacheItem) 386 dat, err := os.ReadFile(store) 387 if err != nil { 388 if errors.Is(err, os.ErrNotExist) { 389 s.Debugf(certidp.DbgNoCacheFound) 390 } else { 391 s.Warnf(certidp.ErrLoadCacheFail, err) 392 } 393 return 394 } 395 err = json.Unmarshal(dat, &c.cache) 396 if err != nil { 397 // make sure clean cache 398 c.cache = make(map[string]OCSPResponseCacheItem) 399 s.Warnf(certidp.ErrLoadCacheFail, err) 400 c.dirty = true 401 return 402 } 403 c.dirty = false 404 } 405 406 func (c *LocalCache) saveCache(s *Server) { 407 c.mu.RLock() 408 dirty := c.dirty 409 c.mu.RUnlock() 410 if !dirty { 411 return 412 } 413 s.Debugf(certidp.DbgCacheDirtySave) 414 var d string 415 if c.config.LocalStore != _EMPTY_ { 416 d = c.config.LocalStore 417 } else { 418 d = OCSPResponseCacheDefaultDir 419 } 420 f := OCSPResponseCacheDefaultFilename 421 store, err := filepath.Abs(path.Join(d, f)) 422 if err != nil { 423 s.Errorf(certidp.ErrSaveCacheFail, err) 424 return 425 } 426 s.Debugf(certidp.DbgSavingCache, store) 427 if _, err := os.Stat(d); os.IsNotExist(err) { 428 err = os.Mkdir(d, defaultDirPerms) 429 if err != nil { 430 s.Errorf(certidp.ErrSaveCacheFail, err) 431 return 432 } 433 } 434 tmp, err := os.CreateTemp(d, OCSPResponseCacheDefaultTempFilePrefix) 435 if err != nil { 436 s.Errorf(certidp.ErrSaveCacheFail, err) 437 return 438 } 439 defer func() { 440 tmp.Close() 441 os.Remove(tmp.Name()) 442 }() // clean up any temp files 443 444 // RW lock here because we're going to snapshot the cache to disk and mark as clean if successful 445 c.mu.Lock() 446 defer c.mu.Unlock() 447 dat, err := json.MarshalIndent(c.cache, "", " ") 448 if err != nil { 449 s.Errorf(certidp.ErrSaveCacheFail, err) 450 return 451 } 452 cacheSize, err := tmp.Write(dat) 453 if err != nil { 454 s.Errorf(certidp.ErrSaveCacheFail, err) 455 return 456 } 457 err = tmp.Sync() 458 if err != nil { 459 s.Errorf(certidp.ErrSaveCacheFail, err) 460 return 461 } 462 err = tmp.Close() 463 if err != nil { 464 s.Errorf(certidp.ErrSaveCacheFail, err) 465 return 466 } 467 // do the final swap and overwrite any old saved peer cache 468 err = os.Rename(tmp.Name(), store) 469 if err != nil { 470 s.Errorf(certidp.ErrSaveCacheFail, err) 471 return 472 } 473 c.dirty = false 474 s.Debugf(certidp.DbgCacheSaved, cacheSize) 475 } 476 477 var OCSPResponseCacheUsage = ` 478 You may enable OCSP peer response cacheing at server configuration root level: 479 480 (If no TLS blocks are configured with OCSP peer verification, ocsp_cache is ignored.) 481 482 ... 483 # short form enables with defaults 484 ocsp_cache: true 485 486 # if false or undefined and one or more TLS blocks are configured with OCSP peer verification, "none" is implied 487 488 # long form includes settable options 489 ocsp_cache { 490 491 # Cache type <none, local> (default local) 492 type: local 493 494 # Cache file directory for local-type cache (default _rc_ in current working directory) 495 local_store: "_rc_" 496 497 # Ignore cache deletes if cached OCSP response is Revoked status (default false) 498 preserve_revoked: false 499 500 # For local store, interval to save in-memory cache to disk in seconds (default 300 seconds, minimum 1 second) 501 save_interval: 300 502 } 503 ... 504 505 Note: Cache of server's own OCSP response (staple) is enabled using the 'ocsp' configuration option. 506 ` 507 508 func (s *Server) initOCSPResponseCache() { 509 // No mTLS OCSP or Leaf OCSP enablements, so no need to init cache 510 s.mu.RLock() 511 if !s.ocspPeerVerify { 512 s.mu.RUnlock() 513 return 514 } 515 s.mu.RUnlock() 516 so := s.getOpts() 517 if so.OCSPCacheConfig == nil { 518 so.OCSPCacheConfig = NewOCSPResponseCacheConfig() 519 } 520 var cc = so.OCSPCacheConfig 521 s.mu.Lock() 522 defer s.mu.Unlock() 523 switch cc.Type { 524 case NONE: 525 s.ocsprc = &NoOpCache{config: cc, online: true, mu: &sync.RWMutex{}} 526 case LOCAL: 527 c := &LocalCache{ 528 config: cc, 529 online: false, 530 cache: make(map[string]OCSPResponseCacheItem), 531 mu: &sync.RWMutex{}, 532 dirty: false, 533 } 534 c.saveInterval = time.Duration(cc.SaveInterval) * time.Second 535 c.timer = time.AfterFunc(c.saveInterval, func() { 536 s.Debugf(certidp.DbgCacheSaveTimerExpired) 537 c.saveCache(s) 538 c.timer.Reset(c.saveInterval) 539 }) 540 s.ocsprc = c 541 default: 542 s.Fatalf(certidp.ErrBadCacheTypeConfig, cc.Type) 543 } 544 } 545 546 func (s *Server) startOCSPResponseCache() { 547 // No mTLS OCSP or Leaf OCSP enablements, so no need to start cache 548 s.mu.RLock() 549 if !s.ocspPeerVerify || s.ocsprc == nil { 550 s.mu.RUnlock() 551 return 552 } 553 s.mu.RUnlock() 554 555 // Could be heavier operation depending on cache implementation 556 s.ocsprc.Start(s) 557 if s.ocsprc.Online() { 558 s.Noticef(certidp.MsgCacheOnline, s.ocsprc.Type()) 559 } else { 560 s.Noticef(certidp.MsgCacheOffline, s.ocsprc.Type()) 561 } 562 } 563 564 func (s *Server) stopOCSPResponseCache() { 565 s.mu.RLock() 566 if s.ocsprc == nil { 567 s.mu.RUnlock() 568 return 569 } 570 s.mu.RUnlock() 571 s.ocsprc.Stop(s) 572 } 573 574 func parseOCSPResponseCache(v any) (pcfg *OCSPResponseCacheConfig, retError error) { 575 var lt token 576 defer convertPanicToError(<, &retError) 577 tk, v := unwrapValue(v, <) 578 cm, ok := v.(map[string]any) 579 if !ok { 580 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalCacheOptsConfig, v)} 581 } 582 pcfg = NewOCSPResponseCacheConfig() 583 retError = nil 584 for mk, mv := range cm { 585 // Again, unwrap token value if line check is required. 586 tk, mv = unwrapValue(mv, <) 587 switch strings.ToLower(mk) { 588 case "type": 589 cache, ok := mv.(string) 590 if !ok { 591 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)} 592 } 593 cacheType, exists := OCSPResponseCacheTypeMap[strings.ToLower(cache)] 594 if !exists { 595 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrUnknownCacheType, cache)} 596 } 597 pcfg.Type = cacheType 598 case "local_store": 599 store, ok := mv.(string) 600 if !ok { 601 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)} 602 } 603 pcfg.LocalStore = store 604 case "preserve_revoked": 605 preserve, ok := mv.(bool) 606 if !ok { 607 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)} 608 } 609 pcfg.PreserveRevoked = preserve 610 case "save_interval": 611 at := float64(0) 612 switch mv := mv.(type) { 613 case int64: 614 at = float64(mv) 615 case float64: 616 at = mv 617 case string: 618 d, err := time.ParseDuration(mv) 619 if err != nil { 620 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)} 621 } 622 at = d.Seconds() 623 default: 624 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldTypeConversion, "unexpected type")} 625 } 626 si := time.Duration(at) * time.Second 627 if si < OCSPResponseCacheMinimumSaveInterval { 628 si = OCSPResponseCacheMinimumSaveInterval 629 } 630 pcfg.SaveInterval = si.Seconds() 631 default: 632 return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingCacheOptFieldGeneric, mk)} 633 } 634 } 635 return pcfg, nil 636 }