mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
Ferry ocsp_ca_certificates over the OCSP ValidationConf (#28309)
* Ferry ocsp_ca_certificates over the OCSP ValidationConf * changelog * First check issuer, then check extraCAS * Use the correct cert when the signature validation from issuer succeeds * Validate via extraCas in the cert missing case as well * dedupe logic * remove CA test
This commit is contained in:
@@ -693,6 +693,15 @@ func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage,
|
|||||||
conf.QueryAllServers = conf.QueryAllServers || entry.OcspQueryAllServers
|
conf.QueryAllServers = conf.QueryAllServers || entry.OcspQueryAllServers
|
||||||
conf.OcspThisUpdateMaxAge = entry.OcspThisUpdateMaxAge
|
conf.OcspThisUpdateMaxAge = entry.OcspThisUpdateMaxAge
|
||||||
conf.OcspMaxRetries = entry.OcspMaxRetries
|
conf.OcspMaxRetries = entry.OcspMaxRetries
|
||||||
|
|
||||||
|
if len(entry.OcspCaCertificates) > 0 {
|
||||||
|
certs, err := certutil.ParseCertsPEM([]byte(entry.OcspCaCertificates))
|
||||||
|
if err != nil {
|
||||||
|
b.Logger().Error("failed to parse ocsp_ca_certificates", "name", name, "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conf.ExtraCas = certs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package cert
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
@@ -281,19 +282,6 @@ func TestCert_RoleResolve_RoleDoesNotExist(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCert_RoleResolveOCSP(t *testing.T) {
|
func TestCert_RoleResolveOCSP(t *testing.T) {
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
failOpen bool
|
|
||||||
certStatus int
|
|
||||||
errExpected bool
|
|
||||||
}{
|
|
||||||
{"failFalseGoodCert", false, ocsp.Good, false},
|
|
||||||
{"failFalseRevokedCert", false, ocsp.Revoked, true},
|
|
||||||
{"failFalseUnknownCert", false, ocsp.Unknown, true},
|
|
||||||
{"failTrueGoodCert", true, ocsp.Good, false},
|
|
||||||
{"failTrueRevokedCert", true, ocsp.Revoked, true},
|
|
||||||
{"failTrueUnknownCert", true, ocsp.Unknown, false},
|
|
||||||
}
|
|
||||||
certTemplate := &x509.Certificate{
|
certTemplate := &x509.Certificate{
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
CommonName: "example.com",
|
CommonName: "example.com",
|
||||||
@@ -332,15 +320,76 @@ func TestCert_RoleResolveOCSP(t *testing.T) {
|
|||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tempDir, connState2, err := generateTestCertAndConnState(t, certTemplate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error testing connection state: %v", err)
|
||||||
|
}
|
||||||
|
ca2, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer2 := parsePEM(ca2)
|
||||||
|
pkf2, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_key.pem"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
pk2, err := certutil.ParsePEMBundle(string(pkf2))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type caData struct {
|
||||||
|
privateKey crypto.Signer
|
||||||
|
caBytes []byte
|
||||||
|
caChain []*x509.Certificate
|
||||||
|
connState tls.ConnectionState
|
||||||
|
}
|
||||||
|
|
||||||
|
ca1Data := caData{
|
||||||
|
pk.PrivateKey,
|
||||||
|
ca,
|
||||||
|
issuer,
|
||||||
|
connState,
|
||||||
|
}
|
||||||
|
ca2Data := caData{
|
||||||
|
pk2.PrivateKey,
|
||||||
|
ca2,
|
||||||
|
issuer2,
|
||||||
|
connState2,
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
failOpen bool
|
||||||
|
certStatus int
|
||||||
|
errExpected bool
|
||||||
|
caData caData
|
||||||
|
ocspCaCerts string
|
||||||
|
}{
|
||||||
|
{name: "failFalseGoodCert", certStatus: ocsp.Good, caData: ca1Data},
|
||||||
|
{name: "failFalseRevokedCert", certStatus: ocsp.Revoked, errExpected: true, caData: ca1Data},
|
||||||
|
{name: "failFalseUnknownCert", certStatus: ocsp.Unknown, errExpected: true, caData: ca1Data},
|
||||||
|
{name: "failTrueGoodCert", failOpen: true, certStatus: ocsp.Good, caData: ca1Data},
|
||||||
|
{name: "failTrueRevokedCert", failOpen: true, certStatus: ocsp.Revoked, errExpected: true, caData: ca1Data},
|
||||||
|
{name: "failTrueUnknownCert", failOpen: true, certStatus: ocsp.Unknown, caData: ca1Data},
|
||||||
|
{name: "failFalseGoodCertExtraCas", certStatus: ocsp.Good, caData: ca2Data, ocspCaCerts: string(pkf2)},
|
||||||
|
{name: "failFalseRevokedCertExtraCas", certStatus: ocsp.Revoked, errExpected: true, caData: ca2Data, ocspCaCerts: string(pkf2)},
|
||||||
|
{name: "failFalseUnknownCertExtraCas", certStatus: ocsp.Unknown, errExpected: true, caData: ca2Data, ocspCaCerts: string(pkf2)},
|
||||||
|
{name: "failTrueGoodCertExtraCas", failOpen: true, certStatus: ocsp.Good, caData: ca2Data, ocspCaCerts: string(pkf2)},
|
||||||
|
{name: "failTrueRevokedCertExtraCas", failOpen: true, certStatus: ocsp.Revoked, errExpected: true, caData: ca2Data, ocspCaCerts: string(pkf2)},
|
||||||
|
{name: "failTrueUnknownCertExtraCas", failOpen: true, certStatus: ocsp.Unknown, caData: ca2Data, ocspCaCerts: string(pkf2)},
|
||||||
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
resp, err := ocsp.CreateResponse(issuer[0], issuer[0], ocsp.Response{
|
resp, err := ocsp.CreateResponse(c.caData.caChain[0], c.caData.caChain[0], ocsp.Response{
|
||||||
Status: c.certStatus,
|
Status: c.certStatus,
|
||||||
SerialNumber: certTemplate.SerialNumber,
|
SerialNumber: certTemplate.SerialNumber,
|
||||||
ProducedAt: time.Now(),
|
ProducedAt: time.Now(),
|
||||||
ThisUpdate: time.Now(),
|
ThisUpdate: time.Now(),
|
||||||
NextUpdate: time.Now().Add(time.Hour),
|
NextUpdate: time.Now().Add(time.Hour),
|
||||||
}, pk.PrivateKey)
|
}, c.caData.privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -351,18 +400,18 @@ func TestCert_RoleResolveOCSP(t *testing.T) {
|
|||||||
var resolveStep logicaltest.TestStep
|
var resolveStep logicaltest.TestStep
|
||||||
var loginStep logicaltest.TestStep
|
var loginStep logicaltest.TestStep
|
||||||
if c.errExpected {
|
if c.errExpected {
|
||||||
loginStep = testAccStepLoginWithNameInvalid(t, connState, "web")
|
loginStep = testAccStepLoginWithNameInvalid(t, c.caData.connState, "web")
|
||||||
resolveStep = testAccStepResolveRoleOCSPFail(t, connState, "web")
|
resolveStep = testAccStepResolveRoleOCSPFail(t, c.caData.connState, "web")
|
||||||
} else {
|
} else {
|
||||||
loginStep = testAccStepLoginWithName(t, connState, "web")
|
loginStep = testAccStepLoginWithName(t, c.caData.connState, "web")
|
||||||
resolveStep = testAccStepResolveRoleWithName(t, connState, "web")
|
resolveStep = testAccStepResolveRoleWithName(t, c.caData.connState, "web")
|
||||||
}
|
}
|
||||||
logicaltest.Test(t, logicaltest.TestCase{
|
logicaltest.Test(t, logicaltest.TestCase{
|
||||||
CredentialBackend: b,
|
CredentialBackend: b,
|
||||||
Steps: []logicaltest.TestStep{
|
Steps: []logicaltest.TestStep{
|
||||||
testAccStepCertWithExtraParams(t, "web", ca, "foo", allowed{dns: "example.com"}, false,
|
testAccStepCertWithExtraParams(t, "web", c.caData.caBytes, "foo", allowed{dns: "example.com"}, false,
|
||||||
map[string]interface{}{"ocsp_enabled": true, "ocsp_fail_open": c.failOpen}),
|
map[string]interface{}{"ocsp_enabled": true, "ocsp_fail_open": c.failOpen, "ocsp_ca_certificates": c.ocspCaCerts}),
|
||||||
testAccStepReadCertPolicy(t, "web", false, map[string]interface{}{"ocsp_enabled": true, "ocsp_fail_open": c.failOpen}),
|
testAccStepReadCertPolicy(t, "web", false, map[string]interface{}{"ocsp_enabled": true, "ocsp_fail_open": c.failOpen, "ocsp_ca_certificates": c.ocspCaCerts}),
|
||||||
loginStep,
|
loginStep,
|
||||||
resolveStep,
|
resolveStep,
|
||||||
},
|
},
|
||||||
|
|||||||
3
changelog/28309.txt
Normal file
3
changelog/28309.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:bug
|
||||||
|
auth/cert: ocsp_ca_certificates field was not honored when validating OCSP responses signed by a CA that did not issue the certificate.
|
||||||
|
```
|
||||||
@@ -321,6 +321,7 @@ func (c *Client) retryOCSP(
|
|||||||
reqBody []byte,
|
reqBody []byte,
|
||||||
subject,
|
subject,
|
||||||
issuer *x509.Certificate,
|
issuer *x509.Certificate,
|
||||||
|
extraCas []*x509.Certificate,
|
||||||
) (ocspRes *ocsp.Response, ocspResBytes []byte, ocspS *ocspStatus, retErr error) {
|
) (ocspRes *ocsp.Response, ocspResBytes []byte, ocspS *ocspStatus, retErr error) {
|
||||||
doRequest := func(request *retryablehttp.Request) (*http.Response, error) {
|
doRequest := func(request *retryablehttp.Request) (*http.Response, error) {
|
||||||
if request != nil {
|
if request != nil {
|
||||||
@@ -391,7 +392,7 @@ func (c *Client) retryOCSP(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateOCSPParsedResponse(ocspRes, subject, issuer); err != nil {
|
if err := validateOCSPParsedResponse(ocspRes, subject, issuer, extraCas); err != nil {
|
||||||
err = fmt.Errorf("error validating %v OCSP response: %w", method, err)
|
err = fmt.Errorf("error validating %v OCSP response: %w", method, err)
|
||||||
|
|
||||||
if IsOcspVerificationError(err) {
|
if IsOcspVerificationError(err) {
|
||||||
@@ -443,7 +444,7 @@ func IsOcspVerificationError(err error) bool {
|
|||||||
return errors.As(err, &errOcspIssuer)
|
return errors.As(err, &errOcspIssuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateOCSPParsedResponse(ocspRes *ocsp.Response, subject, issuer *x509.Certificate) error {
|
func validateOCSPParsedResponse(ocspRes *ocsp.Response, subject, issuer *x509.Certificate, extraCas []*x509.Certificate) error {
|
||||||
// Above, we use the unsafe issuer=nil parameter to ocsp.ParseResponse
|
// Above, we use the unsafe issuer=nil parameter to ocsp.ParseResponse
|
||||||
// because Go's library does the wrong thing.
|
// because Go's library does the wrong thing.
|
||||||
//
|
//
|
||||||
@@ -462,38 +463,59 @@ func validateOCSPParsedResponse(ocspRes *ocsp.Response, subject, issuer *x509.Ce
|
|||||||
//
|
//
|
||||||
// This addresses the !!unsafe!! behavior above.
|
// This addresses the !!unsafe!! behavior above.
|
||||||
if ocspRes.Certificate == nil {
|
if ocspRes.Certificate == nil {
|
||||||
|
// With no certificate, we need to validate that the response is signed by the issuer or an extra CA
|
||||||
if err := ocspRes.CheckSignatureFrom(issuer); err != nil {
|
if err := ocspRes.CheckSignatureFrom(issuer); err != nil {
|
||||||
|
if len(extraCas) > 0 {
|
||||||
|
// Perhaps it was signed by one of the extra configured OCSP CAs
|
||||||
|
matchedCA, overallErr := verifySignature(ocspRes, extraCas)
|
||||||
|
|
||||||
|
if overallErr != nil {
|
||||||
|
return &ErrOcspIssuerVerification{fmt.Errorf("error checking chain of trust %v failed: %w", issuer.Subject.String(), overallErr)}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validateSigner(matchedCA)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return &ErrOcspIssuerVerification{fmt.Errorf("error directly verifying signature: %w", err)}
|
return &ErrOcspIssuerVerification{fmt.Errorf("error directly verifying signature: %w", err)}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Because we have at least one certificate here, we know that
|
// Because we have at least one certificate here, we know that
|
||||||
// Go's ocsp library verified the signature from this certificate
|
// Go's ocsp library verified the signature from this certificate
|
||||||
// onto the response and it was valid. Now we need to know we trust
|
// onto the response and it was valid. Now we need to know we trust
|
||||||
// this certificate. There's two ways we can do this:
|
// this certificate. There are three ways we can do this:
|
||||||
//
|
//
|
||||||
// 1. Via confirming issuer == ocspRes.Certificate, or
|
// 1. Via confirming issuer == ocspRes.Certificate, or
|
||||||
// 2. Via confirming ocspRes.Certificate.CheckSignatureFrom(issuer).
|
// 2. Via confirming ocspRes.Certificate.CheckSignatureFrom(issuer).
|
||||||
|
// 3. Trusting extra configured OCSP CAs
|
||||||
if !bytes.Equal(issuer.Raw, ocspRes.Raw) {
|
if !bytes.Equal(issuer.Raw, ocspRes.Raw) {
|
||||||
// 1 must not hold, so 2 holds; verify the signature.
|
var overallErr error
|
||||||
|
var matchedCA *x509.Certificate
|
||||||
|
|
||||||
|
// Assumption 1 failed, try 2
|
||||||
if err := ocspRes.Certificate.CheckSignatureFrom(issuer); err != nil {
|
if err := ocspRes.Certificate.CheckSignatureFrom(issuer); err != nil {
|
||||||
return &ErrOcspIssuerVerification{fmt.Errorf("error checking chain of trust %v failed: %w", issuer.Subject.String(), err)}
|
// Assumption 2 failed, try 3
|
||||||
|
overallErr = multierror.Append(overallErr, err)
|
||||||
|
|
||||||
|
m, err := verifySignature(ocspRes, extraCas)
|
||||||
|
if err != nil {
|
||||||
|
overallErr = multierror.Append(overallErr, err)
|
||||||
|
} else {
|
||||||
|
matchedCA = m
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
matchedCA = ocspRes.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the OCSP responder certificate is still valid and
|
if overallErr != nil {
|
||||||
// contains the required EKU since it is a delegated OCSP
|
return &ErrOcspIssuerVerification{fmt.Errorf("error checking chain of trust %v failed: %w", issuer.Subject.String(), overallErr)}
|
||||||
// responder certificate.
|
|
||||||
if ocspRes.Certificate.NotAfter.Before(time.Now()) {
|
|
||||||
return &ErrOcspIssuerVerification{fmt.Errorf("error checking delegated OCSP responder OCSP response: certificate has expired")}
|
|
||||||
}
|
}
|
||||||
haveEKU := false
|
|
||||||
for _, ku := range ocspRes.Certificate.ExtKeyUsage {
|
err := validateSigner(matchedCA)
|
||||||
if ku == x509.ExtKeyUsageOCSPSigning {
|
if err != nil {
|
||||||
haveEKU = true
|
return err
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !haveEKU {
|
|
||||||
return &ErrOcspIssuerVerification{fmt.Errorf("error checking delegated OCSP responder: certificate lacks the OCSP Signing EKU")}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,6 +534,41 @@ func validateOCSPParsedResponse(ocspRes *ocsp.Response, subject, issuer *x509.Ce
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func verifySignature(res *ocsp.Response, extraCas []*x509.Certificate) (*x509.Certificate, error) {
|
||||||
|
var overallErr error
|
||||||
|
var matchedCA *x509.Certificate
|
||||||
|
for _, ca := range extraCas {
|
||||||
|
if err := res.CheckSignatureFrom(ca); err != nil {
|
||||||
|
overallErr = multierror.Append(overallErr, err)
|
||||||
|
} else {
|
||||||
|
matchedCA = ca
|
||||||
|
overallErr = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchedCA, overallErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSigner(matchedCA *x509.Certificate) error {
|
||||||
|
// Verify the OCSP responder certificate is still valid and
|
||||||
|
// contains the required EKU since it is a delegated OCSP
|
||||||
|
// responder certificate.
|
||||||
|
if matchedCA.NotAfter.Before(time.Now()) {
|
||||||
|
return &ErrOcspIssuerVerification{fmt.Errorf("error checking delegated OCSP responder OCSP response: certificate has expired")}
|
||||||
|
}
|
||||||
|
haveEKU := false
|
||||||
|
for _, ku := range matchedCA.ExtKeyUsage {
|
||||||
|
if ku == x509.ExtKeyUsageOCSPSigning {
|
||||||
|
haveEKU = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !haveEKU {
|
||||||
|
return &ErrOcspIssuerVerification{fmt.Errorf("error checking delegated OCSP responder: certificate lacks the OCSP Signing EKU")}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetRevocationStatus checks the certificate revocation status for subject using issuer certificate.
|
// 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) {
|
func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.Certificate, conf *VerifyConfig) (*ocspStatus, error) {
|
||||||
status, ocspReq, encodedCertID, err := c.validateWithCache(subject, issuer, conf)
|
status, ocspReq, encodedCertID, err := c.validateWithCache(subject, issuer, conf)
|
||||||
@@ -564,7 +621,7 @@ func (c *Client) GetRevocationStatus(ctx context.Context, subject, issuer *x509.
|
|||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
}
|
}
|
||||||
ocspRes, _, ocspS, err := c.retryOCSP(
|
ocspRes, _, ocspS, err := c.retryOCSP(
|
||||||
ctx, ocspClient, retryablehttp.NewRequest, u, headers, ocspReq, subject, issuer)
|
ctx, ocspClient, retryablehttp.NewRequest, u, headers, ocspReq, subject, issuer, conf.ExtraCas)
|
||||||
ocspResponses[i] = ocspRes
|
ocspResponses[i] = ocspRes
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors[i] = err
|
errors[i] = err
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ func TestOCSPRetry(t *testing.T) {
|
|||||||
context.TODO(),
|
context.TODO(),
|
||||||
client, fakeRequestFunc,
|
client, fakeRequestFunc,
|
||||||
dummyOCSPHost,
|
dummyOCSPHost,
|
||||||
make(map[string]string), []byte{0}, certs[0], certs[len(certs)-1])
|
make(map[string]string), []byte{0}, certs[0], certs[len(certs)-1], nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf("should fail: %v, %v, %v\n", res, b, st)
|
fmt.Printf("should fail: %v, %v, %v\n", res, b, st)
|
||||||
}
|
}
|
||||||
@@ -692,7 +692,7 @@ func TestOCSPRetry(t *testing.T) {
|
|||||||
context.TODO(),
|
context.TODO(),
|
||||||
client, fakeRequestFunc,
|
client, fakeRequestFunc,
|
||||||
dummyOCSPHost,
|
dummyOCSPHost,
|
||||||
make(map[string]string), []byte{0}, certs[0], certs[len(certs)-1])
|
make(map[string]string), []byte{0}, certs[0], certs[len(certs)-1], nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf("should fail: %v, %v, %v\n", res, b, st)
|
fmt.Printf("should fail: %v, %v, %v\n", res, b, st)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user