Skip to content

Commit eda36e9

Browse files
Support OCSP responses without NextUpdate field set (#25912) (#26023)
* Support OCSP responses without a NextUpdate value set - Validate that the ThisUpdate value is properly prior to our current time and if NextUpdate is set that, ThisUpdate is before NextUpdate. - If we don't have a value for NextUpdate just compare against ThisUpdate. * Add ocsp_this_update_max_ttl support to cert auth - Allow configuring a maximum TTL of the OCSP response based on the ThisUpdate time like OpenSSL does - Add test to validate that we don't cache OCSP responses with no NextUpdate * Add cl * Add missing ` in docs * Rename ocsp_this_update_max_ttl to ocsp_this_update_max_age * Missed a few TTL references * Fix error message Co-authored-by: Steven Clark <[email protected]>
1 parent 5b2a2e1 commit eda36e9

File tree

6 files changed

+319
-55
lines changed

6 files changed

+319
-55
lines changed

builtin/credential/cert/path_certs.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/hashicorp/go-secure-stdlib/parseutil"
1314
"github.com/hashicorp/go-sockaddr"
1415

1516
"github.com/hashicorp/vault/sdk/framework"
@@ -91,6 +92,11 @@ from the AuthorityInformationAccess extension on the certificate being inspected
9192
Default: false,
9293
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.",
9394
},
95+
"ocsp_this_update_max_age": {
96+
Type: framework.TypeDurationSecond,
97+
Default: 0,
98+
Description: "If greater than 0, specifies the maximum age of an OCSP thisUpdate field to avoid accepting old responses without a nextUpdate field.",
99+
},
94100
"allowed_names": {
95101
Type: framework.TypeCommaStringSlice,
96102
Description: `A comma-separated list of names.
@@ -309,6 +315,7 @@ func (b *backend) pathCertRead(ctx context.Context, req *logical.Request, d *fra
309315
"ocsp_servers_override": cert.OcspServersOverride,
310316
"ocsp_fail_open": cert.OcspFailOpen,
311317
"ocsp_query_all_servers": cert.OcspQueryAllServers,
318+
"ocsp_this_update_max_age": int64(cert.OcspThisUpdateMaxAge.Seconds()),
312319
}
313320
cert.PopulateTokenData(data)
314321

@@ -367,6 +374,13 @@ func (b *backend) pathCertWrite(ctx context.Context, req *logical.Request, d *fr
367374
if ocspQueryAll, ok := d.GetOk("ocsp_query_all_servers"); ok {
368375
cert.OcspQueryAllServers = ocspQueryAll.(bool)
369376
}
377+
if ocspThisUpdateMaxAge, ok := d.GetOk("ocsp_this_update_max_age"); ok {
378+
maxAgeDuration, err := parseutil.ParseDurationSecond(ocspThisUpdateMaxAge)
379+
if err != nil {
380+
return nil, fmt.Errorf("failed to parse ocsp_this_update_max_age: %w", err)
381+
}
382+
cert.OcspThisUpdateMaxAge = maxAgeDuration
383+
}
370384
if displayNameRaw, ok := d.GetOk("display_name"); ok {
371385
cert.DisplayName = displayNameRaw.(string)
372386
}
@@ -512,11 +526,12 @@ type CertEntry struct {
512526
AllowedMetadataExtensions []string
513527
BoundCIDRs []*sockaddr.SockAddrMarshaler
514528

515-
OcspCaCertificates string
516-
OcspEnabled bool
517-
OcspServersOverride []string
518-
OcspFailOpen bool
519-
OcspQueryAllServers bool
529+
OcspCaCertificates string
530+
OcspEnabled bool
531+
OcspServersOverride []string
532+
OcspFailOpen bool
533+
OcspQueryAllServers bool
534+
OcspThisUpdateMaxAge time.Duration
520535
}
521536

522537
const pathCertHelpSyn = `

builtin/credential/cert/path_login.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525

2626
"github.com/hashicorp/errwrap"
2727
"github.com/hashicorp/go-multierror"
28-
glob "github.com/ryanuber/go-glob"
28+
"github.com/ryanuber/go-glob"
2929
)
3030

3131
// ParsedCert is a certificate that has been configured as trusted
@@ -663,6 +663,7 @@ func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage,
663663
conf.OcspFailureMode = ocsp.FailOpenFalse
664664
}
665665
conf.QueryAllServers = conf.QueryAllServers || entry.OcspQueryAllServers
666+
conf.OcspThisUpdateMaxAge = entry.OcspThisUpdateMaxAge
666667
}
667668
}
668669

changelog/25912.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
auth/cert: Allow validation with OCSP responses with no NextUpdate time
3+
```

sdk/helper/ocsp/client.go

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,27 @@ func (c *Client) getHashAlgorithmFromOID(target pkix.AlgorithmIdentifier) crypto
162162
return crypto.SHA1
163163
}
164164

165-
// isInValidityRange checks the validity
166-
func isInValidityRange(currTime, nextUpdate time.Time) bool {
167-
return !nextUpdate.IsZero() && !currTime.After(nextUpdate)
165+
// isInValidityRange checks the validity times of the OCSP response making sure
166+
// that thisUpdate and nextUpdate values are bounded within currTime
167+
func isInValidityRange(currTime time.Time, ocspRes *ocsp.Response) bool {
168+
thisUpdate := ocspRes.ThisUpdate
169+
170+
// If the thisUpdate value in the OCSP response wasn't set or greater than current time fail this check
171+
if thisUpdate.IsZero() || thisUpdate.After(currTime) {
172+
return false
173+
}
174+
175+
nextUpdate := ocspRes.NextUpdate
176+
if nextUpdate.IsZero() {
177+
// We don't have a nextUpdate field set, assume we are okay.
178+
return true
179+
}
180+
181+
if currTime.After(nextUpdate) || thisUpdate.After(nextUpdate) {
182+
return false
183+
}
184+
185+
return true
168186
}
169187

170188
func extractCertIDKeyFromRequest(ocspReq []byte) (*certIDKey, *ocspStatus) {
@@ -209,7 +227,7 @@ func (c *Client) encodeCertIDKey(certIDKeyBase64 string) (*certIDKey, error) {
209227
}, nil
210228
}
211229

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

221-
status, err := c.extractOCSPCacheResponseValue(cacheValue, subject, issuer)
239+
status, err := c.extractOCSPCacheResponseValue(cacheValue, subject, issuer, config)
222240
if err != nil {
223241
return nil, err
224242
}
@@ -235,18 +253,25 @@ func (c *Client) deleteOCSPCache(encodedCertID *certIDKey) {
235253
c.ocspResponseCacheLock.Unlock()
236254
}
237255

238-
func validateOCSP(ocspRes *ocsp.Response) (*ocspStatus, error) {
256+
func validateOCSP(conf *VerifyConfig, ocspRes *ocsp.Response) (*ocspStatus, error) {
239257
curTime := time.Now()
240258

241259
if ocspRes == nil {
242260
return nil, errors.New("OCSP Response is nil")
243261
}
244-
if !isInValidityRange(curTime, ocspRes.NextUpdate) {
262+
if !isInValidityRange(curTime, ocspRes) {
245263
return &ocspStatus{
246264
code: ocspInvalidValidity,
247265
err: fmt.Errorf("invalid validity: producedAt: %v, thisUpdate: %v, nextUpdate: %v", ocspRes.ProducedAt, ocspRes.ThisUpdate, ocspRes.NextUpdate),
248266
}, nil
249267
}
268+
269+
if conf.OcspThisUpdateMaxAge > 0 && curTime.Sub(ocspRes.ThisUpdate) > conf.OcspThisUpdateMaxAge {
270+
return &ocspStatus{
271+
code: ocspInvalidValidity,
272+
err: fmt.Errorf("invalid validity: thisUpdate: %v is greater than max age: %s", ocspRes.ThisUpdate, conf.OcspThisUpdateMaxAge),
273+
}, nil
274+
}
250275
return returnOCSPStatus(ocspRes), nil
251276
}
252277

@@ -450,7 +475,7 @@ func (c *Client) retryOCSP(
450475

451476
// GetRevocationStatus checks the certificate revocation status for subject using issuer certificate.
452477
func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.Certificate, conf *VerifyConfig) (*ocspStatus, error) {
453-
status, ocspReq, encodedCertID, err := c.validateWithCache(subject, issuer)
478+
status, ocspReq, encodedCertID, err := c.validateWithCache(subject, issuer, conf)
454479
if err != nil {
455480
return nil, err
456481
}
@@ -510,7 +535,7 @@ func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.
510535
return nil
511536
}
512537

513-
ret, err := validateOCSP(ocspRes)
538+
ret, err := validateOCSP(conf, ocspRes)
514539
if err != nil {
515540
errors[i] = err
516541
return err
@@ -582,6 +607,12 @@ func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.
582607
if !isValidOCSPStatus(ret.code) {
583608
return ret, nil
584609
}
610+
611+
if ocspRes.NextUpdate.IsZero() {
612+
// We should not cache values with no NextUpdate values
613+
return ret, nil
614+
}
615+
585616
v := ocspCachedResponse{
586617
status: ret.code,
587618
time: float64(time.Now().UTC().Unix()),
@@ -602,11 +633,12 @@ func isValidOCSPStatus(status ocspStatusCode) bool {
602633
}
603634

604635
type VerifyConfig struct {
605-
OcspEnabled bool
606-
ExtraCas []*x509.Certificate
607-
OcspServersOverride []string
608-
OcspFailureMode FailOpenMode
609-
QueryAllServers bool
636+
OcspEnabled bool
637+
ExtraCas []*x509.Certificate
638+
OcspServersOverride []string
639+
OcspFailureMode FailOpenMode
640+
QueryAllServers bool
641+
OcspThisUpdateMaxAge time.Duration
610642
}
611643

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

695-
func (c *Client) validateWithCacheForAllCertificates(verifiedChains []*x509.Certificate) (bool, error) {
727+
func (c *Client) validateWithCacheForAllCertificates(verifiedChains []*x509.Certificate, config *VerifyConfig) (bool, error) {
696728
n := len(verifiedChains) - 1
697729
for j := 0; j < n; j++ {
698730
subject := verifiedChains[j]
699731
issuer := verifiedChains[j+1]
700-
status, _, _, err := c.validateWithCache(subject, issuer)
732+
status, _, _, err := c.validateWithCache(subject, issuer, config)
701733
if err != nil {
702734
return false, err
703735
}
@@ -708,7 +740,7 @@ func (c *Client) validateWithCacheForAllCertificates(verifiedChains []*x509.Cert
708740
return true, nil
709741
}
710742

711-
func (c *Client) validateWithCache(subject, issuer *x509.Certificate) (*ocspStatus, []byte, *certIDKey, error) {
743+
func (c *Client) validateWithCache(subject, issuer *x509.Certificate, config *VerifyConfig) (*ocspStatus, []byte, *certIDKey, error) {
712744
ocspReq, err := ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{})
713745
if err != nil {
714746
return nil, nil, nil, fmt.Errorf("failed to create OCSP request from the certificates: %v", err)
@@ -717,15 +749,15 @@ func (c *Client) validateWithCache(subject, issuer *x509.Certificate) (*ocspStat
717749
if ocspS.code != ocspSuccess {
718750
return nil, nil, nil, fmt.Errorf("failed to extract CertID from OCSP Request: %v", err)
719751
}
720-
status, err := c.checkOCSPResponseCache(encodedCertID, subject, issuer)
752+
status, err := c.checkOCSPResponseCache(encodedCertID, subject, issuer, config)
721753
if err != nil {
722754
return nil, nil, nil, err
723755
}
724756
return status, ocspReq, encodedCertID, nil
725757
}
726758

727759
func (c *Client) GetAllRevocationStatus(ctx context.Context, verifiedChains []*x509.Certificate, conf *VerifyConfig) ([]*ocspStatus, error) {
728-
_, err := c.validateWithCacheForAllCertificates(verifiedChains)
760+
_, err := c.validateWithCacheForAllCertificates(verifiedChains, conf)
729761
if err != nil {
730762
return nil, err
731763
}
@@ -750,11 +782,11 @@ func (c *Client) verifyPeerCertificateSerial(conf *VerifyConfig) func(_ [][]byte
750782
}
751783
}
752784

753-
func (c *Client) extractOCSPCacheResponseValueWithoutSubject(cacheValue ocspCachedResponse) (*ocspStatus, error) {
754-
return c.extractOCSPCacheResponseValue(&cacheValue, nil, nil)
785+
func (c *Client) extractOCSPCacheResponseValueWithoutSubject(cacheValue ocspCachedResponse, conf *VerifyConfig) (*ocspStatus, error) {
786+
return c.extractOCSPCacheResponseValue(&cacheValue, nil, nil, conf)
755787
}
756788

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

779811
sdkOcspStatus := internalStatusCodeToSDK(cacheValue.status)
780812

781-
return validateOCSP(&ocsp.Response{
813+
return validateOCSP(conf, &ocsp.Response{
782814
ProducedAt: time.Unix(int64(cacheValue.producedAt), 0).UTC(),
783815
ThisUpdate: time.Unix(int64(cacheValue.thisUpdate), 0).UTC(),
784816
NextUpdate: time.Unix(int64(cacheValue.nextUpdate), 0).UTC(),

0 commit comments

Comments
 (0)