Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions builtin/credential/cert/path_certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"time"

"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/tokenutil"
Expand Down Expand Up @@ -90,6 +91,11 @@ from the AuthorityInformationAccess extension on the certificate being inspected
Default: false,
Description: "If set to true, rather than accepting the first successful OCSP response, query all servers and consider the certificate valid only if all servers agree.",
},
"ocsp_this_update_max_age": {
Type: framework.TypeDurationSecond,
Default: 0,
Description: "If greater than 0, specifies the maximum age of an OCSP thisUpdate field to avoid accepting old responses without a nextUpdate field.",
},
"allowed_names": {
Type: framework.TypeCommaStringSlice,
Description: `A comma-separated list of names.
Expand Down Expand Up @@ -308,6 +314,7 @@ func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *fra
"ocsp_servers_override": cert.OcspServersOverride,
"ocsp_fail_open": cert.OcspFailOpen,
"ocsp_query_all_servers": cert.OcspQueryAllServers,
"ocsp_this_update_max_age": int64(cert.OcspThisUpdateMaxAge.Seconds()),
}
cert.PopulateTokenData(data)

Expand Down Expand Up @@ -366,6 +373,13 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
if ocspQueryAll, ok := d.GetOk("ocsp_query_all_servers"); ok {
cert.OcspQueryAllServers = ocspQueryAll.(bool)
}
if ocspThisUpdateMaxAge, ok := d.GetOk("ocsp_this_update_max_age"); ok {
maxAgeDuration, err := parseutil.ParseDurationSecond(ocspThisUpdateMaxAge)
if err != nil {
return nil, fmt.Errorf("failed to parse ocsp_this_update_max_age: %w", err)
}
cert.OcspThisUpdateMaxAge = maxAgeDuration
}
if displayNameRaw, ok := d.GetOk("display_name"); ok {
cert.DisplayName = displayNameRaw.(string)
}
Expand Down Expand Up @@ -511,11 +525,12 @@ type CertEntry struct {
AllowedMetadataExtensions []string
BoundCIDRs []*sockaddr.SockAddrMarshaler

OcspCaCertificates string
OcspEnabled bool
OcspServersOverride []string
OcspFailOpen bool
OcspQueryAllServers bool
OcspCaCertificates string
OcspEnabled bool
OcspServersOverride []string
OcspFailOpen bool
OcspQueryAllServers bool
OcspThisUpdateMaxAge time.Duration
}

const pathCertHelpSyn = `
Expand Down
3 changes: 2 additions & 1 deletion builtin/credential/cert/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"github.com/hashicorp/vault/sdk/helper/ocsp"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/logical"
glob "github.com/ryanuber/go-glob"
"github.com/ryanuber/go-glob"
)

// ParsedCert is a certificate that has been configured as trusted
Expand Down Expand Up @@ -662,6 +662,7 @@ func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage,
conf.OcspFailureMode = ocsp.FailOpenFalse
}
conf.QueryAllServers = conf.QueryAllServers || entry.OcspQueryAllServers
conf.OcspThisUpdateMaxAge = entry.OcspThisUpdateMaxAge
}
}

Expand Down
3 changes: 3 additions & 0 deletions changelog/25912.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
auth/cert: Allow validation with OCSP responses with no NextUpdate time
```
78 changes: 55 additions & 23 deletions sdk/helper/ocsp/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,27 @@ func (c *Client) getHashAlgorithmFromOID(target pkix.AlgorithmIdentifier) crypto
return crypto.SHA1
}

// isInValidityRange checks the validity
func isInValidityRange(currTime, nextUpdate time.Time) bool {
return !nextUpdate.IsZero() && !currTime.After(nextUpdate)
// isInValidityRange checks the validity times of the OCSP response making sure
// that thisUpdate and nextUpdate values are bounded within currTime
func isInValidityRange(currTime time.Time, ocspRes *ocsp.Response) bool {
thisUpdate := ocspRes.ThisUpdate

// If the thisUpdate value in the OCSP response wasn't set or greater than current time fail this check
if thisUpdate.IsZero() || thisUpdate.After(currTime) {
return false
}

nextUpdate := ocspRes.NextUpdate
if nextUpdate.IsZero() {
// We don't have a nextUpdate field set, assume we are okay.
return true
}

if currTime.After(nextUpdate) || thisUpdate.After(nextUpdate) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when would thisUpdate > nextUpdate? Or is this just defensive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup completely defensive, it should never be greater.

return false
}

return true
}

func extractCertIDKeyFromRequest(ocspReq []byte) (*certIDKey, *ocspStatus) {
Expand Down Expand Up @@ -209,7 +227,7 @@ func (c *Client) encodeCertIDKey(certIDKeyBase64 string) (*certIDKey, error) {
}, nil
}

func (c *Client) checkOCSPResponseCache(encodedCertID *certIDKey, subject, issuer *x509.Certificate) (*ocspStatus, error) {
func (c *Client) checkOCSPResponseCache(encodedCertID *certIDKey, subject, issuer *x509.Certificate, config *VerifyConfig) (*ocspStatus, error) {
c.ocspResponseCacheLock.RLock()
var cacheValue *ocspCachedResponse
v, ok := c.ocspResponseCache.Get(*encodedCertID)
Expand All @@ -218,7 +236,7 @@ func (c *Client) checkOCSPResponseCache(encodedCertID *certIDKey, subject, issue
}
c.ocspResponseCacheLock.RUnlock()

status, err := c.extractOCSPCacheResponseValue(cacheValue, subject, issuer)
status, err := c.extractOCSPCacheResponseValue(cacheValue, subject, issuer, config)
if err != nil {
return nil, err
}
Expand All @@ -235,18 +253,25 @@ func (c *Client) deleteOCSPCache(encodedCertID *certIDKey) {
c.ocspResponseCacheLock.Unlock()
}

func validateOCSP(ocspRes *ocsp.Response) (*ocspStatus, error) {
func validateOCSP(conf *VerifyConfig, ocspRes *ocsp.Response) (*ocspStatus, error) {
curTime := time.Now()

if ocspRes == nil {
return nil, errors.New("OCSP Response is nil")
}
if !isInValidityRange(curTime, ocspRes.NextUpdate) {
if !isInValidityRange(curTime, ocspRes) {
return &ocspStatus{
code: ocspInvalidValidity,
err: fmt.Errorf("invalid validity: producedAt: %v, thisUpdate: %v, nextUpdate: %v", ocspRes.ProducedAt, ocspRes.ThisUpdate, ocspRes.NextUpdate),
}, nil
}

if conf.OcspThisUpdateMaxAge > 0 && curTime.Sub(ocspRes.ThisUpdate) > conf.OcspThisUpdateMaxAge {
return &ocspStatus{
code: ocspInvalidValidity,
err: fmt.Errorf("invalid validity: thisUpdate: %v is greater than max age: %s", ocspRes.ThisUpdate, conf.OcspThisUpdateMaxAge),
}, nil
}
return returnOCSPStatus(ocspRes), nil
}

Expand Down Expand Up @@ -450,7 +475,7 @@ func (c *Client) retryOCSP(

// GetRevocationStatus checks the certificate revocation status for subject using issuer certificate.
func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.Certificate, conf *VerifyConfig) (*ocspStatus, error) {
status, ocspReq, encodedCertID, err := c.validateWithCache(subject, issuer)
status, ocspReq, encodedCertID, err := c.validateWithCache(subject, issuer, conf)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -510,7 +535,7 @@ func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.
return nil
}

ret, err := validateOCSP(ocspRes)
ret, err := validateOCSP(conf, ocspRes)
if err != nil {
errors[i] = err
return err
Expand Down Expand Up @@ -582,6 +607,12 @@ func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.
if !isValidOCSPStatus(ret.code) {
return ret, nil
}

if ocspRes.NextUpdate.IsZero() {
// We should not cache values with no NextUpdate values
return ret, nil
}

v := ocspCachedResponse{
status: ret.code,
time: float64(time.Now().UTC().Unix()),
Expand All @@ -602,11 +633,12 @@ func isValidOCSPStatus(status ocspStatusCode) bool {
}

type VerifyConfig struct {
OcspEnabled bool
ExtraCas []*x509.Certificate
OcspServersOverride []string
OcspFailureMode FailOpenMode
QueryAllServers bool
OcspEnabled bool
ExtraCas []*x509.Certificate
OcspServersOverride []string
OcspFailureMode FailOpenMode
QueryAllServers bool
OcspThisUpdateMaxAge time.Duration
}

// VerifyLeafCertificate verifies just the subject against it's direct issuer
Expand Down Expand Up @@ -692,12 +724,12 @@ func (c *Client) canEarlyExitForOCSP(results []*ocspStatus, chainSize int, conf
return nil
}

func (c *Client) validateWithCacheForAllCertificates(verifiedChains []*x509.Certificate) (bool, error) {
func (c *Client) validateWithCacheForAllCertificates(verifiedChains []*x509.Certificate, config *VerifyConfig) (bool, error) {
n := len(verifiedChains) - 1
for j := 0; j < n; j++ {
subject := verifiedChains[j]
issuer := verifiedChains[j+1]
status, _, _, err := c.validateWithCache(subject, issuer)
status, _, _, err := c.validateWithCache(subject, issuer, config)
if err != nil {
return false, err
}
Expand All @@ -708,7 +740,7 @@ func (c *Client) validateWithCacheForAllCertificates(verifiedChains []*x509.Cert
return true, nil
}

func (c *Client) validateWithCache(subject, issuer *x509.Certificate) (*ocspStatus, []byte, *certIDKey, error) {
func (c *Client) validateWithCache(subject, issuer *x509.Certificate, config *VerifyConfig) (*ocspStatus, []byte, *certIDKey, error) {
ocspReq, err := ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{})
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to create OCSP request from the certificates: %v", err)
Expand All @@ -717,15 +749,15 @@ func (c *Client) validateWithCache(subject, issuer *x509.Certificate) (*ocspStat
if ocspS.code != ocspSuccess {
return nil, nil, nil, fmt.Errorf("failed to extract CertID from OCSP Request: %v", err)
}
status, err := c.checkOCSPResponseCache(encodedCertID, subject, issuer)
status, err := c.checkOCSPResponseCache(encodedCertID, subject, issuer, config)
if err != nil {
return nil, nil, nil, err
}
return status, ocspReq, encodedCertID, nil
}

func (c *Client) GetAllRevocationStatus(ctx context.Context, verifiedChains []*x509.Certificate, conf *VerifyConfig) ([]*ocspStatus, error) {
_, err := c.validateWithCacheForAllCertificates(verifiedChains)
_, err := c.validateWithCacheForAllCertificates(verifiedChains, conf)
if err != nil {
return nil, err
}
Expand All @@ -750,11 +782,11 @@ func (c *Client) verifyPeerCertificateSerial(conf *VerifyConfig) func(_ [][]byte
}
}

func (c *Client) extractOCSPCacheResponseValueWithoutSubject(cacheValue ocspCachedResponse) (*ocspStatus, error) {
return c.extractOCSPCacheResponseValue(&cacheValue, nil, nil)
func (c *Client) extractOCSPCacheResponseValueWithoutSubject(cacheValue ocspCachedResponse, conf *VerifyConfig) (*ocspStatus, error) {
return c.extractOCSPCacheResponseValue(&cacheValue, nil, nil, conf)
}

func (c *Client) extractOCSPCacheResponseValue(cacheValue *ocspCachedResponse, subject, issuer *x509.Certificate) (*ocspStatus, error) {
func (c *Client) extractOCSPCacheResponseValue(cacheValue *ocspCachedResponse, subject, issuer *x509.Certificate, conf *VerifyConfig) (*ocspStatus, error) {
subjectName := "Unknown"
if subject != nil {
subjectName = subject.Subject.CommonName
Expand All @@ -778,7 +810,7 @@ func (c *Client) extractOCSPCacheResponseValue(cacheValue *ocspCachedResponse, s

sdkOcspStatus := internalStatusCodeToSDK(cacheValue.status)

return validateOCSP(&ocsp.Response{
return validateOCSP(conf, &ocsp.Response{
ProducedAt: time.Unix(int64(cacheValue.producedAt), 0).UTC(),
ThisUpdate: time.Unix(int64(cacheValue.thisUpdate), 0).UTC(),
NextUpdate: time.Unix(int64(cacheValue.nextUpdate), 0).UTC(),
Expand Down
Loading