open-vault/sdk/helper/ocsp/ocsp_test.go
Scott Miller b51b2a7027
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

531 lines
14 KiB
Go

// Copyright (c) 2017-2022 Snowflake Computing Inc. All rights reserved.
package ocsp
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"testing"
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-retryablehttp"
lru "github.com/hashicorp/golang-lru"
"golang.org/x/crypto/ocsp"
)
func TestOCSP(t *testing.T) {
targetURL := []string{
"https://sfcdev1.blob.core.windows.net/",
"https://sfctest0.snowflakecomputing.com/",
"https://s3-us-west-2.amazonaws.com/sfc-snowsql-updates/?prefix=1.1/windows_x86_64",
}
conf := VerifyConfig{
OcspFailureMode: FailOpenFalse,
}
c := New(testLogFactory, 10)
transports := []*http.Transport{
newInsecureOcspTransport(nil),
c.NewTransport(&conf),
}
for _, tgt := range targetURL {
c.ocspResponseCache, _ = lru.New2Q(10)
for _, tr := range transports {
c := &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}
req, err := http.NewRequest("GET", tgt, bytes.NewReader(nil))
if err != nil {
t.Fatalf("fail to create a request. err: %v", err)
}
res, err := c.Do(req)
if err != nil {
t.Fatalf("failed to GET contents. err: %v", err)
}
defer res.Body.Close()
_, err = ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("failed to read content body for %v", tgt)
}
}
}
}
/**
// Used for development, requires an active Vault with PKI setup
func TestMultiOCSP(t *testing.T) {
targetURL := []string{
"https://localhost:8200/v1/pki/ocsp",
"https://localhost:8200/v1/pki/ocsp",
"https://localhost:8200/v1/pki/ocsp",
}
b, _ := pem.Decode([]byte(vaultCert))
caCert, _ := x509.ParseCertificate(b.Bytes)
conf := VerifyConfig{
OcspFailureMode: FailOpenFalse,
QueryAllServers: true,
OcspServersOverride: targetURL,
ExtraCas: []*x509.Certificate{caCert},
}
c := New(testLogFactory, 10)
transports := []*http.Transport{
newInsecureOcspTransport(conf.ExtraCas),
c.NewTransport(&conf),
}
tgt := "https://localhost:8200/v1/pki/ca/pem"
c.ocspResponseCache, _ = lru.New2Q(10)
for _, tr := range transports {
c := &http.Client{
Transport: tr,
Timeout: 30 * time.Second,
}
req, err := http.NewRequest("GET", tgt, bytes.NewReader(nil))
if err != nil {
t.Fatalf("fail to create a request. err: %v", err)
}
res, err := c.Do(req)
if err != nil {
t.Fatalf("failed to GET contents. err: %v", err)
}
defer res.Body.Close()
_, err = ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("failed to read content body for %v", tgt)
}
}
}
*/
func TestUnitEncodeCertIDGood(t *testing.T) {
targetURLs := []string{
"faketestaccount.snowflakecomputing.com:443",
"s3-us-west-2.amazonaws.com:443",
"sfcdev1.blob.core.windows.net:443",
}
for _, tt := range targetURLs {
chainedCerts := getCert(tt)
for i := 0; i < len(chainedCerts)-1; i++ {
subject := chainedCerts[i]
issuer := chainedCerts[i+1]
ocspServers := subject.OCSPServer
if len(ocspServers) == 0 {
t.Fatalf("no OCSP server is found. cert: %v", subject.Subject)
}
ocspReq, err := ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{})
if err != nil {
t.Fatalf("failed to create OCSP request. err: %v", err)
}
var ost *ocspStatus
_, ost = extractCertIDKeyFromRequest(ocspReq)
if ost.err != nil {
t.Fatalf("failed to extract cert ID from the OCSP request. err: %v", ost.err)
}
// better hash. Not sure if the actual OCSP server accepts this, though.
ocspReq, err = ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{Hash: crypto.SHA512})
if err != nil {
t.Fatalf("failed to create OCSP request. err: %v", err)
}
_, ost = extractCertIDKeyFromRequest(ocspReq)
if ost.err != nil {
t.Fatalf("failed to extract cert ID from the OCSP request. err: %v", ost.err)
}
// tweaked request binary
ocspReq, err = ocsp.CreateRequest(subject, issuer, &ocsp.RequestOptions{Hash: crypto.SHA512})
if err != nil {
t.Fatalf("failed to create OCSP request. err: %v", err)
}
ocspReq[10] = 0 // random change
_, ost = extractCertIDKeyFromRequest(ocspReq)
if ost.err == nil {
t.Fatal("should have failed")
}
}
}
}
func TestUnitCheckOCSPResponseCache(t *testing.T) {
c := New(testLogFactory, 10)
dummyKey0 := certIDKey{
NameHash: "dummy0",
IssuerKeyHash: "dummy0",
SerialNumber: "dummy0",
}
dummyKey := certIDKey{
NameHash: "dummy1",
IssuerKeyHash: "dummy1",
SerialNumber: "dummy1",
}
currentTime := float64(time.Now().UTC().Unix())
c.ocspResponseCache.Add(dummyKey0, &ocspCachedResponse{time: currentTime})
subject := &x509.Certificate{}
issuer := &x509.Certificate{}
ost, err := c.checkOCSPResponseCache(&dummyKey, subject, issuer)
if err != nil {
t.Fatal(err)
}
if ost.code != ocspMissedCache {
t.Fatalf("should have failed. expected: %v, got: %v", ocspMissedCache, ost.code)
}
// old timestamp
c.ocspResponseCache.Add(dummyKey, &ocspCachedResponse{time: float64(1395054952)})
ost, err = c.checkOCSPResponseCache(&dummyKey, subject, issuer)
if err != nil {
t.Fatal(err)
}
if ost.code != ocspCacheExpired {
t.Fatalf("should have failed. expected: %v, got: %v", ocspCacheExpired, ost.code)
}
// invalid validity
c.ocspResponseCache.Add(dummyKey, &ocspCachedResponse{time: float64(currentTime - 1000)})
ost, err = c.checkOCSPResponseCache(&dummyKey, subject, nil)
if err == nil && isValidOCSPStatus(ost.code) {
t.Fatalf("should have failed.")
}
}
func TestUnitValidateOCSP(t *testing.T) {
ocspRes := &ocsp.Response{}
ost, err := validateOCSP(ocspRes)
if err == nil && isValidOCSPStatus(ost.code) {
t.Fatalf("should have failed.")
}
currentTime := time.Now()
ocspRes.ThisUpdate = currentTime.Add(-2 * time.Hour)
ocspRes.NextUpdate = currentTime.Add(2 * time.Hour)
ocspRes.Status = ocsp.Revoked
ost, err = validateOCSP(ocspRes)
if err != nil {
t.Fatal(err)
}
if ost.code != ocspStatusRevoked {
t.Fatalf("should have failed. expected: %v, got: %v", ocspStatusRevoked, ost.code)
}
ocspRes.Status = ocsp.Good
ost, err = validateOCSP(ocspRes)
if err != nil {
t.Fatal(err)
}
if ost.code != ocspStatusGood {
t.Fatalf("should have success. expected: %v, got: %v", ocspStatusGood, ost.code)
}
ocspRes.Status = ocsp.Unknown
ost, err = validateOCSP(ocspRes)
if err != nil {
t.Fatal(err)
}
if ost.code != ocspStatusUnknown {
t.Fatalf("should have failed. expected: %v, got: %v", ocspStatusUnknown, ost.code)
}
ocspRes.Status = ocsp.ServerFailed
ost, err = validateOCSP(ocspRes)
if err != nil {
t.Fatal(err)
}
if ost.code != ocspStatusOthers {
t.Fatalf("should have failed. expected: %v, got: %v", ocspStatusOthers, ost.code)
}
}
func TestUnitEncodeCertID(t *testing.T) {
var st *ocspStatus
_, st = extractCertIDKeyFromRequest([]byte{0x1, 0x2})
if st.code != ocspFailedDecomposeRequest {
t.Fatalf("failed to get OCSP status. expected: %v, got: %v", ocspFailedDecomposeRequest, st.code)
}
}
func getCert(addr string) []*x509.Certificate {
tcpConn, err := net.DialTimeout("tcp", addr, 40*time.Second)
if err != nil {
panic(err)
}
defer tcpConn.Close()
err = tcpConn.SetDeadline(time.Now().Add(10 * time.Second))
if err != nil {
panic(err)
}
config := tls.Config{InsecureSkipVerify: true, ServerName: addr}
conn := tls.Client(tcpConn, &config)
defer conn.Close()
err = conn.Handshake()
if err != nil {
panic(err)
}
state := conn.ConnectionState()
return state.PeerCertificates
}
func TestOCSPRetry(t *testing.T) {
c := New(testLogFactory, 10)
certs := getCert("s3-us-west-2.amazonaws.com:443")
dummyOCSPHost := &url.URL{
Scheme: "https",
Host: "dummyOCSPHost",
}
client := &fakeHTTPClient{
cnt: 3,
success: true,
body: []byte{1, 2, 3},
logger: hclog.New(hclog.DefaultOptions),
t: t,
}
res, b, st, err := c.retryOCSP(
context.TODO(),
client, fakeRequestFunc,
dummyOCSPHost,
make(map[string]string), []byte{0}, certs[len(certs)-1])
if err == nil {
fmt.Printf("should fail: %v, %v, %v\n", res, b, st)
}
client = &fakeHTTPClient{
cnt: 30,
success: true,
body: []byte{1, 2, 3},
logger: hclog.New(hclog.DefaultOptions),
t: t,
}
res, b, st, err = c.retryOCSP(
context.TODO(),
client, fakeRequestFunc,
dummyOCSPHost,
make(map[string]string), []byte{0}, certs[len(certs)-1])
if err == nil {
fmt.Printf("should fail: %v, %v, %v\n", res, b, st)
}
}
type tcCanEarlyExit struct {
results []*ocspStatus
resultLen int
retFailOpen *ocspStatus
retFailClosed *ocspStatus
}
func TestCanEarlyExitForOCSP(t *testing.T) {
testcases := []tcCanEarlyExit{
{ // 0
results: []*ocspStatus{
{
code: ocspStatusGood,
},
{
code: ocspStatusGood,
},
{
code: ocspStatusGood,
},
},
retFailOpen: nil,
retFailClosed: nil,
},
{ // 1
results: []*ocspStatus{
{
code: ocspStatusRevoked,
err: errors.New("revoked"),
},
{
code: ocspStatusGood,
},
{
code: ocspStatusGood,
},
},
retFailOpen: &ocspStatus{ocspStatusRevoked, errors.New("revoked")},
retFailClosed: &ocspStatus{ocspStatusRevoked, errors.New("revoked")},
},
{ // 2
results: []*ocspStatus{
{
code: ocspStatusUnknown,
err: errors.New("unknown"),
},
{
code: ocspStatusGood,
},
{
code: ocspStatusGood,
},
},
retFailOpen: nil,
retFailClosed: &ocspStatus{ocspStatusUnknown, errors.New("unknown")},
},
{ // 3: not taken as revoked if any invalid OCSP response (ocspInvalidValidity) is included.
results: []*ocspStatus{
{
code: ocspStatusRevoked,
err: errors.New("revoked"),
},
{
code: ocspInvalidValidity,
},
{
code: ocspStatusGood,
},
},
retFailOpen: nil,
retFailClosed: &ocspStatus{ocspStatusRevoked, errors.New("revoked")},
},
{ // 4: not taken as revoked if the number of results don't match the expected results.
results: []*ocspStatus{
{
code: ocspStatusRevoked,
err: errors.New("revoked"),
},
{
code: ocspStatusGood,
},
},
resultLen: 3,
retFailOpen: nil,
retFailClosed: &ocspStatus{ocspStatusRevoked, errors.New("revoked")},
},
}
c := New(testLogFactory, 10)
for idx, tt := range testcases {
expectedLen := len(tt.results)
if tt.resultLen > 0 {
expectedLen = tt.resultLen
}
r := c.canEarlyExitForOCSP(tt.results, expectedLen, &VerifyConfig{OcspFailureMode: FailOpenTrue})
if !(tt.retFailOpen == nil && r == nil) && !(tt.retFailOpen != nil && r != nil && tt.retFailOpen.code == r.code) {
t.Fatalf("%d: failed to match return. expected: %v, got: %v", idx, tt.retFailOpen, r)
}
r = c.canEarlyExitForOCSP(tt.results, expectedLen, &VerifyConfig{OcspFailureMode: FailOpenFalse})
if !(tt.retFailClosed == nil && r == nil) && !(tt.retFailClosed != nil && r != nil && tt.retFailClosed.code == r.code) {
t.Fatalf("%d: failed to match return. expected: %v, got: %v", idx, tt.retFailClosed, r)
}
}
}
var testLogger = hclog.New(hclog.DefaultOptions)
func testLogFactory() hclog.Logger {
return testLogger
}
type fakeHTTPClient struct {
cnt int // number of retry
success bool // return success after retry in cnt times
timeout bool // timeout
body []byte // return body
t *testing.T
logger hclog.Logger
redirected bool
}
func (c *fakeHTTPClient) Do(_ *retryablehttp.Request) (*http.Response, error) {
c.cnt--
if c.cnt < 0 {
c.cnt = 0
}
c.t.Log("fakeHTTPClient.cnt", c.cnt)
var retcode int
if !c.redirected {
c.redirected = true
c.cnt++
retcode = 405
} else if c.success && c.cnt == 1 {
retcode = 200
} else {
if c.timeout {
// simulate timeout
time.Sleep(time.Second * 1)
return nil, &fakeHTTPError{
err: "Whatever reason (Client.Timeout exceeded while awaiting headers)",
timeout: true,
}
}
retcode = 0
}
ret := &http.Response{
StatusCode: retcode,
Body: &fakeResponseBody{body: c.body},
}
return ret, nil
}
type fakeHTTPError struct {
err string
timeout bool
}
func (e *fakeHTTPError) Error() string { return e.err }
func (e *fakeHTTPError) Timeout() bool { return e.timeout }
func (e *fakeHTTPError) Temporary() bool { return true }
type fakeResponseBody struct {
body []byte
cnt int
}
func (b *fakeResponseBody) Read(p []byte) (n int, err error) {
if b.cnt == 0 {
copy(p, b.body)
b.cnt = 1
return len(b.body), nil
}
b.cnt = 0
return 0, io.EOF
}
func (b *fakeResponseBody) Close() error {
return nil
}
func fakeRequestFunc(_, _ string, _ interface{}) (*retryablehttp.Request, error) {
return nil, nil
}
const vaultCert = `-----BEGIN CERTIFICATE-----
MIIDuTCCAqGgAwIBAgIUA6VeVD1IB5rXcCZRAqPO4zr/GAMwDQYJKoZIhvcNAQEL
BQAwcjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlZBMREwDwYDVQQHDAhTb21lQ2l0
eTESMBAGA1UECgwJTXlDb21wYW55MRMwEQYDVQQLDApNeURpdmlzaW9uMRowGAYD
VQQDDBF3d3cuY29uaHVnZWNvLmNvbTAeFw0yMjA5MDcxOTA1MzdaFw0yNDA5MDYx
OTA1MzdaMHIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJWQTERMA8GA1UEBwwIU29t
ZUNpdHkxEjAQBgNVBAoMCU15Q29tcGFueTETMBEGA1UECwwKTXlEaXZpc2lvbjEa
MBgGA1UEAwwRd3d3LmNvbmh1Z2Vjby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDL9qzEXi4PIafSAqfcwcmjujFvbG1QZbI8swxnD+w8i4ufAQU5
LDmvMrGo3ZbhJ0mCihYmFxpjhRdP2raJQ9TysHlPXHtDRpr9ckWTKBz2oIfqVtJ2
qzteQkWCkDAO7kPqzgCFsMeoMZeONRkeGib0lEzQAbW/Rqnphg8zVVkyQ71DZ7Pc
d5WkC2E28kKcSramhWfVFpxG3hSIrLOX2esEXteLRzKxFPf+gi413JZFKYIWrebP
u5t0++MLNpuX322geoki4BWMjQsd47XILmxZ4aj33ScZvdrZESCnwP76hKIxg9mO
lMxrqSWKVV5jHZrElSEj9LYJgDO1Y6eItn7hAgMBAAGjRzBFMAsGA1UdDwQEAwIE
MDATBgNVHSUEDDAKBggrBgEFBQcDATAhBgNVHREEGjAYggtleGFtcGxlLmNvbYIJ
bG9jYWxob3N0MA0GCSqGSIb3DQEBCwUAA4IBAQA5dPdf5SdtMwe2uSspO/EuWqbM
497vMQBW1Ey8KRKasJjhvOVYMbe7De5YsnW4bn8u5pl0zQGF4hEtpmifAtVvziH/
K+ritQj9VVNbLLCbFcg+b0kfjt4yrDZ64vWvIeCgPjG1Kme8gdUUWgu9dOud5gdx
qg/tIFv4TRS/eIIymMlfd9owOD3Ig6S5fy4NaAJFAwXf8+3Rzuc+e7JSAPgAufjh
tOTWinxvoiOLuYwo9CyGgq4qKBFsrY0aE0gdA7oTQkpbEbo2EbqiWUl/PTCl1Y4Z
nSZ0n+4q9QC9RLrWwYTwh838d5RVLUst2mBKSA+vn7YkqmBJbdBC6nkd7n7H
-----END CERTIFICATE-----
`