Files
vault/builtin/credential/cert/path_login_test.go
Scott Miller a61174815f Add cached OCSP client support to Cert Auth (#17093)
* 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>
2022-11-21 10:39:24 -06:00

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(), ":"))
}