Files
nats-server/server/ocsp_peer.go
Waldemar Quevedo 4109e420d2 Remove ocsp debug log on reload
Signed-off-by: Waldemar Quevedo <wally@nats.io>
2023-08-30 14:54:30 -07:00

406 lines
14 KiB
Go

// Copyright 2023 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"time"
"golang.org/x/crypto/ocsp"
"github.com/nats-io/nats-server/v2/server/certidp"
)
func parseOCSPPeer(v interface{}) (pcfg *certidp.OCSPPeerConfig, retError error) {
var lt token
defer convertPanicToError(&lt, &retError)
tk, v := unwrapValue(v, &lt)
cm, ok := v.(map[string]interface{})
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrIllegalPeerOptsConfig, v)}
}
pcfg = certidp.NewOCSPPeerConfig()
retError = nil
for mk, mv := range cm {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "verify":
verify, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.Verify = verify
case "allowed_clockskew":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.ClockSkew = at
}
case "ca_timeout":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.Timeout = at
}
case "cache_ttl_when_next_update_unset":
at := float64(0)
switch mv := mv.(type) {
case int64:
at = float64(mv)
case float64:
at = mv
case string:
d, err := time.ParseDuration(mv)
if err != nil {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, err)}
}
at = d.Seconds()
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldTypeConversion, "unexpected type")}
}
if at >= 0 {
pcfg.TTLUnsetNextUpdate = at
}
case "warn_only":
warnOnly, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.WarnOnly = warnOnly
case "unknown_is_good":
unknownIsGood, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.UnknownIsGood = unknownIsGood
case "allow_when_ca_unreachable":
allowWhenCAUnreachable, ok := mv.(bool)
if !ok {
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
pcfg.AllowWhenCAUnreachable = allowWhenCAUnreachable
default:
return nil, &configErr{tk, fmt.Sprintf(certidp.ErrParsingPeerOptFieldGeneric, mk)}
}
}
return pcfg, nil
}
func peerFromVerifiedChains(chains [][]*x509.Certificate) *x509.Certificate {
if len(chains) == 0 || len(chains[0]) == 0 {
return nil
}
return chains[0][0]
}
// plugTLSOCSPPeer will plug the TLS handshake lifecycle for client mTLS connections and Leaf connections
func (s *Server) plugTLSOCSPPeer(config *tlsConfigKind) (*tls.Config, bool, error) {
if config == nil || config.tlsConfig == nil {
return nil, false, errors.New(certidp.ErrUnableToPlugTLSEmptyConfig)
}
kind := config.kind
isSpoke := config.isLeafSpoke
tcOpts := config.tlsOpts
if tcOpts == nil || tcOpts.OCSPPeerConfig == nil || !tcOpts.OCSPPeerConfig.Verify {
return nil, false, nil
}
s.Debugf(certidp.DbgPlugTLSForKind, config.kind)
// 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
}