mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
* wip * Add cached OCSP client support to Cert Auth * ->pointer * Code cleanup * Fix unit tests * Use an LRU cache, and only persist up to 1000 of the most recently used values to stay under the storage entry limit * Fix caching, add fail open mode parameter to cert auth roles * reduce logging * Add the retry client and GET then POST logic * Drop persisted cache, make cache size configurable, allow for parallel testing of multiple servers * dead code * Update builtin/credential/cert/path_certs.go Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com> * Hook invalidate to reinit the ocsp cache size * locking * Conditionally init the ocsp client * Remove cache size config from cert configs, it's a backend global * Add field * Remove strangely complex validity logic * Address more feedback * Rework error returning logic * More edge cases * MORE edge cases * Add a test matrix with a builtin responder * changelog * Use an atomic for configUpdated * Actually use ocsp_enabled, and bind to a random port for testing * Update builtin/credential/cert/path_login.go Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com> * Refactor unit tests * Add status to cache * Make some functions private * Rename for testing, and attribute * Up to date gofumpt * remove hash from key, and disable the vault dependent unit test * Comment out TestMultiOCSP * imports * more imports * Address semgrep results * Attempt to pass some sort of logging to test_responder * fix overzealous search&replace Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
359 lines
10 KiB
Go
359 lines
10 KiB
Go
package cert
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
mathrand "math/rand"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
|
|
"golang.org/x/crypto/ocsp"
|
|
|
|
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
|
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
var ocspPort int
|
|
|
|
var source InMemorySource
|
|
|
|
type testLogger struct{}
|
|
|
|
func (t *testLogger) Log(args ...any) {
|
|
fmt.Printf("%v", args)
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
source = make(InMemorySource)
|
|
|
|
listener, err := net.Listen("tcp", ":0")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
ocspPort = listener.Addr().(*net.TCPAddr).Port
|
|
srv := &http.Server{
|
|
Addr: "localhost:0",
|
|
Handler: NewResponder(&testLogger{}, source, nil),
|
|
}
|
|
go func() {
|
|
srv.Serve(listener)
|
|
}()
|
|
defer srv.Shutdown(context.Background())
|
|
m.Run()
|
|
}
|
|
|
|
func TestCert_RoleResolve(t *testing.T) {
|
|
certTemplate := &x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "example.com",
|
|
},
|
|
DNSNames: []string{"example.com"},
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
},
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
|
SerialNumber: big.NewInt(mathrand.Int63()),
|
|
NotBefore: time.Now().Add(-30 * time.Second),
|
|
NotAfter: time.Now().Add(262980 * time.Hour),
|
|
}
|
|
|
|
tempDir, connState, err := generateTestCertAndConnState(t, certTemplate)
|
|
if tempDir != "" {
|
|
defer os.RemoveAll(tempDir)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("error testing connection state: %v", err)
|
|
}
|
|
ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
CredentialBackend: testFactory(t),
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false),
|
|
testAccStepLoginWithName(t, connState, "web"),
|
|
testAccStepResolveRoleWithName(t, connState, "web"),
|
|
},
|
|
})
|
|
}
|
|
|
|
func testAccStepResolveRoleWithName(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.ResolveRoleOperation,
|
|
Path: "login",
|
|
Unauthenticated: true,
|
|
ConnState: &connState,
|
|
Check: func(resp *logical.Response) error {
|
|
if resp.Data["role"] != certName {
|
|
t.Fatalf("Role was not as expected. Expected %s, received %s", certName, resp.Data["role"])
|
|
}
|
|
return nil
|
|
},
|
|
Data: map[string]interface{}{
|
|
"name": certName,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestCert_RoleResolveWithoutProvidingCertName(t *testing.T) {
|
|
certTemplate := &x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "example.com",
|
|
},
|
|
DNSNames: []string{"example.com"},
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
},
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
|
SerialNumber: big.NewInt(mathrand.Int63()),
|
|
NotBefore: time.Now().Add(-30 * time.Second),
|
|
NotAfter: time.Now().Add(262980 * time.Hour),
|
|
}
|
|
|
|
tempDir, connState, err := generateTestCertAndConnState(t, certTemplate)
|
|
if tempDir != "" {
|
|
defer os.RemoveAll(tempDir)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("error testing connection state: %v", err)
|
|
}
|
|
ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
CredentialBackend: testFactory(t),
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false),
|
|
testAccStepLoginWithName(t, connState, "web"),
|
|
testAccStepResolveRoleWithEmptyDataMap(t, connState, "web"),
|
|
},
|
|
})
|
|
}
|
|
|
|
func testAccStepResolveRoleWithEmptyDataMap(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.ResolveRoleOperation,
|
|
Path: "login",
|
|
Unauthenticated: true,
|
|
ConnState: &connState,
|
|
Check: func(resp *logical.Response) error {
|
|
if resp.Data["role"] != certName {
|
|
t.Fatalf("Role was not as expected. Expected %s, received %s", certName, resp.Data["role"])
|
|
}
|
|
return nil
|
|
},
|
|
Data: map[string]interface{}{},
|
|
}
|
|
}
|
|
|
|
func testAccStepResolveRoleExpectRoleResolutionToFail(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.ResolveRoleOperation,
|
|
Path: "login",
|
|
Unauthenticated: true,
|
|
ConnState: &connState,
|
|
ErrorOk: true,
|
|
Check: func(resp *logical.Response) error {
|
|
if resp == nil && !resp.IsError() {
|
|
t.Fatalf("Response was not an error: resp:%#v", resp)
|
|
}
|
|
|
|
errString, ok := resp.Data["error"].(string)
|
|
if !ok {
|
|
t.Fatal("Error not part of response.")
|
|
}
|
|
|
|
if !strings.Contains(errString, "invalid certificate") {
|
|
t.Fatalf("Error was not due to invalid role name. Error: %s", errString)
|
|
}
|
|
return nil
|
|
},
|
|
Data: map[string]interface{}{
|
|
"name": certName,
|
|
},
|
|
}
|
|
}
|
|
|
|
func testAccStepResolveRoleOCSPFail(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.ResolveRoleOperation,
|
|
Path: "login",
|
|
Unauthenticated: true,
|
|
ConnState: &connState,
|
|
ErrorOk: true,
|
|
Check: func(resp *logical.Response) error {
|
|
if resp == nil || !resp.IsError() {
|
|
t.Fatalf("Response was not an error: resp:%#v", resp)
|
|
}
|
|
|
|
errString, ok := resp.Data["error"].(string)
|
|
if !ok {
|
|
t.Fatal("Error not part of response.")
|
|
}
|
|
|
|
if !strings.Contains(errString, "no chain matching") {
|
|
t.Fatalf("Error was not due to OCSP failure. Error: %s", errString)
|
|
}
|
|
return nil
|
|
},
|
|
Data: map[string]interface{}{
|
|
"name": certName,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestCert_RoleResolve_RoleDoesNotExist(t *testing.T) {
|
|
certTemplate := &x509.Certificate{
|
|
Subject: pkix.Name{
|
|
CommonName: "example.com",
|
|
},
|
|
DNSNames: []string{"example.com"},
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
},
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
|
SerialNumber: big.NewInt(mathrand.Int63()),
|
|
NotBefore: time.Now().Add(-30 * time.Second),
|
|
NotAfter: time.Now().Add(262980 * time.Hour),
|
|
}
|
|
|
|
tempDir, connState, err := generateTestCertAndConnState(t, certTemplate)
|
|
if tempDir != "" {
|
|
defer os.RemoveAll(tempDir)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("error testing connection state: %v", err)
|
|
}
|
|
ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
CredentialBackend: testFactory(t),
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false),
|
|
testAccStepLoginWithName(t, connState, "web"),
|
|
testAccStepResolveRoleExpectRoleResolutionToFail(t, connState, "notweb"),
|
|
},
|
|
})
|
|
}
|
|
|
|
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{
|
|
Subject: pkix.Name{
|
|
CommonName: "example.com",
|
|
},
|
|
DNSNames: []string{"example.com"},
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
},
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
|
|
SerialNumber: big.NewInt(mathrand.Int63()),
|
|
NotBefore: time.Now().Add(-30 * time.Second),
|
|
NotAfter: time.Now().Add(262980 * time.Hour),
|
|
OCSPServer: []string{fmt.Sprintf("http://localhost:%d", ocspPort)},
|
|
}
|
|
tempDir, connState, err := generateTestCertAndConnState(t, certTemplate)
|
|
if tempDir != "" {
|
|
defer os.RemoveAll(tempDir)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("error testing connection state: %v", err)
|
|
}
|
|
ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
issuer := parsePEM(ca)
|
|
pkf, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_key.pem"))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
pk, err := certutil.ParsePEMBundle(string(pkf))
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
resp, err := ocsp.CreateResponse(issuer[0], issuer[0], ocsp.Response{
|
|
Status: c.certStatus,
|
|
SerialNumber: certTemplate.SerialNumber,
|
|
ProducedAt: time.Now(),
|
|
ThisUpdate: time.Now(),
|
|
NextUpdate: time.Now().Add(time.Hour),
|
|
}, pk.PrivateKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
source[certTemplate.SerialNumber.String()] = resp
|
|
|
|
b := testFactory(t)
|
|
b.(*backend).ocspClient.ClearCache()
|
|
var resolveStep logicaltest.TestStep
|
|
var loginStep logicaltest.TestStep
|
|
if c.errExpected {
|
|
loginStep = testAccStepLoginWithNameInvalid(t, connState, "web")
|
|
resolveStep = testAccStepResolveRoleOCSPFail(t, connState, "web")
|
|
} else {
|
|
loginStep = testAccStepLoginWithName(t, connState, "web")
|
|
resolveStep = testAccStepResolveRoleWithName(t, connState, "web")
|
|
}
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
CredentialBackend: b,
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepCertWithExtraParams(t, "web", ca, "foo", allowed{dns: "example.com"}, false,
|
|
map[string]interface{}{"ocsp_enabled": true, "ocsp_fail_open": c.failOpen}),
|
|
loginStep,
|
|
resolveStep,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func serialFromBigInt(serial *big.Int) string {
|
|
return strings.TrimSpace(certutil.GetHexFormatted(serial.Bytes(), ":"))
|
|
}
|