Skip to content

Commit

Permalink
OCSP backports and adds (#4362)
Browse files Browse the repository at this point in the history
This PR backports the OCSP Peer feature option (as in 2.10 train) and
includes two fixes for the existing OCSP Staple feature.

OCSP Staple: 

1. Fixed and clarified how NATS Server determines its own Issuer CA when
obtaining and validating an OCSP Response for subsequent staple
2. Eliminated problematic assumption that all node peers are issued by
same CA when NATS Server validates ROUTE and GATEWAY peer nodes
3. Added OCSP Response effectivity checks on ROUTE and GATEWAY
peer-presented staple

Note for #3: Allowed host clock skew between node peers set at
30-seconds. If the OCSP Response contains an empty assertion for
NextUpdate, NATS Server will default to 1-hour validity (after
ThisUpdate). It is recommended that CA OCSP Responder should assert
NextUpdate.
  • Loading branch information
derekcollison authored Aug 3, 2023
2 parents 5577d18 + ac43a8d commit 9de5e3e
Show file tree
Hide file tree
Showing 62 changed files with 10,750 additions and 187 deletions.
297 changes: 297 additions & 0 deletions server/certidp/certidp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// 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 certidp

import (
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"

"golang.org/x/crypto/ocsp"
)

const (
DefaultAllowedClockSkew = 30 * time.Second
DefaultOCSPResponderTimeout = 2 * time.Second
DefaultTTLUnsetNextUpdate = 1 * time.Hour
)

type StatusAssertion int

var (
StatusAssertionStrToVal = map[string]StatusAssertion{
"good": ocsp.Good,
"revoked": ocsp.Revoked,
"unknown": ocsp.Unknown,
}
StatusAssertionValToStr = map[StatusAssertion]string{
ocsp.Good: "good",
ocsp.Revoked: "revoked",
ocsp.Unknown: "unknown",
}
StatusAssertionIntToVal = map[int]StatusAssertion{
0: ocsp.Good,
1: ocsp.Revoked,
2: ocsp.Unknown,
}
)

func GetStatusAssertionStr(sa int) string {
return StatusAssertionValToStr[StatusAssertionIntToVal[sa]]
}

func (sa StatusAssertion) MarshalJSON() ([]byte, error) {
str, ok := StatusAssertionValToStr[sa]
if !ok {
// set unknown as fallback
str = StatusAssertionValToStr[ocsp.Unknown]
}
return json.Marshal(str)
}

func (sa *StatusAssertion) UnmarshalJSON(in []byte) error {
v, ok := StatusAssertionStrToVal[strings.ReplaceAll(string(in), "\"", "")]
if !ok {
// set unknown as fallback
v = StatusAssertionStrToVal["unknown"]
}
*sa = v
return nil
}

type ChainLink struct {
Leaf *x509.Certificate
Issuer *x509.Certificate
OCSPWebEndpoints *[]*url.URL
}

// OCSPPeerConfig holds the parsed OCSP peer configuration section of TLS configuration
type OCSPPeerConfig struct {
Verify bool
Timeout float64
ClockSkew float64
WarnOnly bool
UnknownIsGood bool
AllowWhenCAUnreachable bool
TTLUnsetNextUpdate float64
}

func NewOCSPPeerConfig() *OCSPPeerConfig {
return &OCSPPeerConfig{
Verify: false,
Timeout: DefaultOCSPResponderTimeout.Seconds(),
ClockSkew: DefaultAllowedClockSkew.Seconds(),
WarnOnly: false,
UnknownIsGood: false,
AllowWhenCAUnreachable: false,
TTLUnsetNextUpdate: DefaultTTLUnsetNextUpdate.Seconds(),
}
}

// Log is a neutral method of passing server loggers to plugins
type Log struct {
Debugf func(format string, v ...interface{})
Noticef func(format string, v ...interface{})
Warnf func(format string, v ...interface{})
Errorf func(format string, v ...interface{})
Tracef func(format string, v ...interface{})
}

type CertInfo struct {
Subject string `json:"subject,omitempty"`
Issuer string `json:"issuer,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Raw []byte `json:"raw,omitempty"`
}

var OCSPPeerUsage = `
For client, leaf spoke (remotes), and leaf hub connections, you may enable OCSP peer validation:
tls {
...
# mTLS must be enabled (with exception of Leaf remotes)
verify: true
...
# short form enables peer verify and takes option defaults
ocsp_peer: true
# long form includes settable options
ocsp_peer {
# Enable OCSP peer validation (default false)
verify: true
# OCSP responder timeout in seconds (may be fractional, default 2 seconds)
ca_timeout: 2
# Allowed skew between server and OCSP responder time in seconds (may be fractional, default 30 seconds)
allowed_clockskew: 30
# Warn-only and never reject connections (default false)
warn_only: false
# Treat response Unknown status as valid certificate (default false)
unknown_is_good: false
# Warn-only if no CA response can be obtained and no cached revocation exists (default false)
allow_when_ca_unreachable: false
# If response NextUpdate unset by CA, set a default cache TTL in seconds from ThisUpdate (default 1 hour)
cache_ttl_when_next_update_unset: 3600
}
...
}
Note: OCSP validation for route and gateway connections is enabled using the 'ocsp' configuration option.
`

// GenerateFingerprint returns a base64-encoded SHA256 hash of the raw certificate
func GenerateFingerprint(cert *x509.Certificate) string {
data := sha256.Sum256(cert.Raw)
return base64.StdEncoding.EncodeToString(data[:])
}

func getWebEndpoints(uris []string) []*url.URL {
var urls []*url.URL
for _, uri := range uris {
endpoint, err := url.ParseRequestURI(uri)
if err != nil {
// skip invalid URLs
continue
}
if endpoint.Scheme != "http" && endpoint.Scheme != "https" {
// skip non-web URLs
continue
}
urls = append(urls, endpoint)
}
return urls
}

// GetSubjectDNForm returns RDN sequence concatenation of the certificate's subject to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetSubjectDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Subject.ToRDNSequence()), "+")
}

// GetIssuerDNForm returns RDN sequence concatenation of the certificate's issuer to be
// used in logs, events, etc. Should never be used for reliable cache matching or other crypto purposes.
func GetIssuerDNForm(cert *x509.Certificate) string {
if cert == nil {
return ""
}
return strings.TrimSuffix(fmt.Sprintf("%s+", cert.Issuer.ToRDNSequence()), "+")
}

// CertOCSPEligible checks if the certificate's issuer has populated AIA with OCSP responder endpoint(s)
// and is thus eligible for OCSP validation
func CertOCSPEligible(link *ChainLink) bool {
if link == nil || link.Leaf.Raw == nil || len(link.Leaf.Raw) == 0 {
return false
}
if link.Leaf.OCSPServer == nil || len(link.Leaf.OCSPServer) == 0 {
return false
}
urls := getWebEndpoints(link.Leaf.OCSPServer)
if len(urls) == 0 {
return false
}
link.OCSPWebEndpoints = &urls
return true
}

// GetLeafIssuerCert returns the issuer certificate of the leaf (positional) certificate in the chain
func GetLeafIssuerCert(chain []*x509.Certificate, leafPos int) *x509.Certificate {
if len(chain) == 0 || leafPos < 0 {
return nil
}
// self-signed certificate or too-big leafPos
if leafPos >= len(chain)-1 {
return nil
}
// returns pointer to issuer cert or nil
return (chain)[leafPos+1]
}

// OCSPResponseCurrent checks if the OCSP response is current (i.e. not expired and not future effective)
func OCSPResponseCurrent(ocspr *ocsp.Response, opts *OCSPPeerConfig, log *Log) bool {
skew := time.Duration(opts.ClockSkew * float64(time.Second))
if skew < 0*time.Second {
skew = DefaultAllowedClockSkew
}
now := time.Now().UTC()
// Typical effectivity check based on CA response ThisUpdate and NextUpdate semantics
if !ocspr.NextUpdate.IsZero() && ocspr.NextUpdate.Before(now.Add(-1*skew)) {
t := ocspr.NextUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseExpired, t, nt, skew)
return false
}
// CA responder can assert NextUpdate unset, in which case use config option to set a default cache TTL
if ocspr.NextUpdate.IsZero() {
ttl := time.Duration(opts.TTLUnsetNextUpdate * float64(time.Second))
if ttl < 0*time.Second {
ttl = DefaultTTLUnsetNextUpdate
}
expiryTime := ocspr.ThisUpdate.Add(ttl)
if expiryTime.Before(now.Add(-1 * skew)) {
t := expiryTime.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseTTLExpired, t, nt, skew)
return false
}
}
if ocspr.ThisUpdate.After(now.Add(skew)) {
t := ocspr.ThisUpdate.Format(time.RFC3339Nano)
nt := now.Format(time.RFC3339Nano)
log.Debugf(DbgResponseFutureDated, t, nt, skew)
return false
}
return true
}

// ValidDelegationCheck checks if the CA OCSP Response was signed by a valid CA Issuer delegate as per (RFC 6960, section 4.2.2.2)
// If a valid delegate or direct-signed by CA Issuer, true returned.
func ValidDelegationCheck(iss *x509.Certificate, ocspr *ocsp.Response) bool {
// This call assumes prior successful parse and signature validation of the OCSP response
// The Go OCSP library (as of x/crypto/ocsp v0.9) will detect and perform a 1-level delegate signature check but does not
// implement the additional criteria for delegation specified in RFC 6960, section 4.2.2.2.
if iss == nil || ocspr == nil {
return false
}
// not a delegation, no-op
if ocspr.Certificate == nil {
return true
}
// delegate is self-same with CA Issuer, not a delegation although response issued in that form
if ocspr.Certificate.Equal(iss) {
return true
}
// we need to verify CA Issuer stamped id-kp-OCSPSigning on delegate
delegatedSigner := false
for _, keyUseExt := range ocspr.Certificate.ExtKeyUsage {
if keyUseExt == x509.ExtKeyUsageOCSPSigning {
delegatedSigner = true
break
}
}
return delegatedSigner
}
Loading

0 comments on commit 9de5e3e

Please sign in to comment.