mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
406 lines
14 KiB
Go
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(<, &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
|
|
}
|