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(&lt, &retError)
   577  	tk, v := unwrapValue(v, &lt)
   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, &lt)
   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  }