mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-22 15:11:07 +02:00
910 lines
26 KiB
Go
910 lines
26 KiB
Go
// Copyright (c) 2017-2022 Snowflake Computing Inc. All rights reserved.
|
|
|
|
package ocsp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
lru "github.com/hashicorp/golang-lru"
|
|
"github.com/stretchr/testify/require"
|
|
"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 {
|
|
ocspClient := retryablehttp.NewClient()
|
|
ocspClient.Logger = c.Logger()
|
|
ocspClient.RetryMax = conf.OcspMaxRetries
|
|
ocspClient.HTTPClient.Timeout = 30 * time.Second
|
|
ocspClient.HTTPClient.Transport = tr
|
|
req, err := retryablehttp.NewRequest("GET", tgt, bytes.NewReader(nil))
|
|
if err != nil {
|
|
t.Fatalf("fail to create a request. err: %v", err)
|
|
}
|
|
res, err := ocspClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("failed to GET contents. err: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
_, err = io.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 = io.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) {
|
|
conf := &VerifyConfig{OcspEnabled: true}
|
|
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, conf)
|
|
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, conf)
|
|
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, conf)
|
|
if err == nil && isValidOCSPStatus(ost.code) {
|
|
t.Fatalf("should have failed.")
|
|
}
|
|
}
|
|
|
|
// TestUnitValidOCSPResponse validates various combinations of acceptable OCSP responses
|
|
func TestUnitValidOCSPResponse(t *testing.T) {
|
|
rootCaKey, rootCa, leafCert := createCaLeafCerts(t)
|
|
|
|
type tests struct {
|
|
name string
|
|
ocspRes ocsp.Response
|
|
expectedStatus ocspStatusCode
|
|
}
|
|
|
|
now := time.Now()
|
|
ctx := context.Background()
|
|
|
|
tt := []tests{
|
|
{
|
|
name: "normal",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(30 * time.Minute),
|
|
Status: ocsp.Good,
|
|
},
|
|
expectedStatus: ocspStatusGood,
|
|
},
|
|
{
|
|
name: "no-next-update",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
Status: ocsp.Good,
|
|
},
|
|
expectedStatus: ocspStatusGood,
|
|
},
|
|
{
|
|
name: "revoked-update",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
Status: ocsp.Revoked,
|
|
},
|
|
expectedStatus: ocspStatusRevoked,
|
|
},
|
|
{
|
|
name: "revoked-update-with-next-update",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(1 * time.Hour),
|
|
Status: ocsp.Revoked,
|
|
},
|
|
expectedStatus: ocspStatusRevoked,
|
|
},
|
|
}
|
|
for _, tc := range tt {
|
|
for _, maxAge := range []time.Duration{time.Duration(0), time.Duration(2 * time.Hour)} {
|
|
t.Run(tc.name+"-max-age-"+maxAge.String(), func(t *testing.T) {
|
|
ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
response := buildOcspResponse(t, rootCa, rootCaKey, tc.ocspRes)
|
|
_, _ = w.Write(response)
|
|
})
|
|
ts := httptest.NewServer(ocspHandler)
|
|
defer ts.Close()
|
|
|
|
logFactory := func() hclog.Logger {
|
|
return hclog.NewNullLogger()
|
|
}
|
|
client := New(logFactory, 100)
|
|
config := &VerifyConfig{
|
|
OcspEnabled: true,
|
|
OcspServersOverride: []string{ts.URL},
|
|
OcspFailureMode: FailOpenFalse,
|
|
QueryAllServers: false,
|
|
OcspThisUpdateMaxAge: maxAge,
|
|
}
|
|
|
|
status, err := client.GetRevocationStatus(ctx, leafCert, rootCa, config)
|
|
require.NoError(t, err, "ocsp response should have been considered valid")
|
|
require.NoError(t, status.err, "ocsp status should not contain an error")
|
|
require.Equal(t, &ocspStatus{code: tc.expectedStatus}, status)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUnitBadOCSPResponses verifies that we fail properly on a bunch of different
|
|
// OCSP response conditions
|
|
func TestUnitBadOCSPResponses(t *testing.T) {
|
|
rootCaKey, rootCa, leafCert := createCaLeafCerts(t)
|
|
rootCaKey2, rootCa2, _ := createCaLeafCerts(t)
|
|
|
|
type tests struct {
|
|
name string
|
|
ocspRes ocsp.Response
|
|
maxAge time.Duration
|
|
ca *x509.Certificate
|
|
caKey *ecdsa.PrivateKey
|
|
errContains string
|
|
}
|
|
|
|
now := time.Now()
|
|
ctx := context.Background()
|
|
|
|
tt := []tests{
|
|
{
|
|
name: "bad-signing-issuer",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(30 * time.Minute),
|
|
Status: ocsp.Good,
|
|
},
|
|
ca: rootCa2,
|
|
caKey: rootCaKey2,
|
|
errContains: "error directly verifying signature",
|
|
},
|
|
{
|
|
name: "incorrect-serial-number",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: big.NewInt(1000),
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(30 * time.Minute),
|
|
Status: ocsp.Good,
|
|
},
|
|
ca: rootCa,
|
|
caKey: rootCaKey,
|
|
errContains: "did not match the leaf certificate serial number",
|
|
},
|
|
{
|
|
name: "expired-next-update",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(-30 * time.Minute),
|
|
Status: ocsp.Good,
|
|
},
|
|
errContains: "invalid validity",
|
|
},
|
|
{
|
|
name: "this-update-in-future",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(1 * time.Hour),
|
|
NextUpdate: now.Add(2 * time.Hour),
|
|
Status: ocsp.Good,
|
|
},
|
|
errContains: "invalid validity",
|
|
},
|
|
{
|
|
name: "next-update-before-this-update",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(-2 * time.Hour),
|
|
Status: ocsp.Good,
|
|
},
|
|
errContains: "invalid validity",
|
|
},
|
|
{
|
|
name: "missing-this-update",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
NextUpdate: now.Add(2 * time.Hour),
|
|
Status: ocsp.Good,
|
|
},
|
|
errContains: "invalid validity",
|
|
},
|
|
{
|
|
name: "unknown-status",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(30 * time.Minute),
|
|
Status: ocsp.Unknown,
|
|
},
|
|
errContains: "OCSP status unknown",
|
|
},
|
|
{
|
|
name: "over-max-age",
|
|
ocspRes: ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(30 * time.Minute),
|
|
Status: ocsp.Good,
|
|
},
|
|
maxAge: 10 * time.Minute,
|
|
errContains: "is greater than max age",
|
|
},
|
|
}
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
useCa := rootCa
|
|
useCaKey := rootCaKey
|
|
if tc.ca != nil {
|
|
useCa = tc.ca
|
|
}
|
|
if tc.caKey != nil {
|
|
useCaKey = tc.caKey
|
|
}
|
|
response := buildOcspResponse(t, useCa, useCaKey, tc.ocspRes)
|
|
_, _ = w.Write(response)
|
|
})
|
|
ts := httptest.NewServer(ocspHandler)
|
|
defer ts.Close()
|
|
|
|
logFactory := func() hclog.Logger {
|
|
return hclog.NewNullLogger()
|
|
}
|
|
client := New(logFactory, 100)
|
|
|
|
config := &VerifyConfig{
|
|
OcspEnabled: true,
|
|
OcspServersOverride: []string{ts.URL},
|
|
OcspFailureMode: FailOpenFalse,
|
|
QueryAllServers: false,
|
|
OcspThisUpdateMaxAge: tc.maxAge,
|
|
}
|
|
|
|
status, err := client.GetRevocationStatus(ctx, leafCert, rootCa, config)
|
|
if err == nil && status == nil || (status != nil && status.err == nil) {
|
|
t.Fatalf("expected an error got none")
|
|
}
|
|
if err != nil {
|
|
require.ErrorContains(t, err, tc.errContains,
|
|
"Expected error got response: %v, %v", status, err)
|
|
}
|
|
if status != nil && status.err != nil {
|
|
require.ErrorContains(t, status.err, tc.errContains,
|
|
"Expected error got response: %v, %v", status, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestUnitZeroNextUpdateAreNotCached verifies that we are not caching the responses
|
|
// with no NextUpdate field set as according to RFC6960 4.2.2.1
|
|
// "If nextUpdate is not set, the responder is indicating that newer
|
|
// revocation information is available all the time."
|
|
func TestUnitZeroNextUpdateAreNotCached(t *testing.T) {
|
|
rootCaKey, rootCa, leafCert := createCaLeafCerts(t)
|
|
numQueries := &atomic.Uint32{}
|
|
ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
numQueries.Add(1)
|
|
now := time.Now()
|
|
ocspRes := ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
Status: ocsp.Good,
|
|
}
|
|
response := buildOcspResponse(t, rootCa, rootCaKey, ocspRes)
|
|
_, _ = w.Write(response)
|
|
})
|
|
ts := httptest.NewServer(ocspHandler)
|
|
defer ts.Close()
|
|
|
|
logFactory := func() hclog.Logger {
|
|
return hclog.NewNullLogger()
|
|
}
|
|
client := New(logFactory, 100)
|
|
|
|
config := &VerifyConfig{
|
|
OcspEnabled: true,
|
|
OcspServersOverride: []string{ts.URL},
|
|
}
|
|
|
|
_, err := client.GetRevocationStatus(context.Background(), leafCert, rootCa, config)
|
|
require.NoError(t, err, "Failed fetching revocation status")
|
|
|
|
_, err = client.GetRevocationStatus(context.Background(), leafCert, rootCa, config)
|
|
require.NoError(t, err, "Failed fetching revocation status second time")
|
|
|
|
require.Equal(t, uint32(2), numQueries.Load())
|
|
}
|
|
|
|
// TestUnitResponsesAreCached verify that the OCSP responses are properly cached when
|
|
// querying for the same leaf certificates
|
|
func TestUnitResponsesAreCached(t *testing.T) {
|
|
rootCaKey, rootCa, leafCert := createCaLeafCerts(t)
|
|
numQueries := &atomic.Uint32{}
|
|
ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
numQueries.Add(1)
|
|
now := time.Now()
|
|
ocspRes := ocsp.Response{
|
|
SerialNumber: leafCert.SerialNumber,
|
|
ThisUpdate: now.Add(-1 * time.Hour),
|
|
NextUpdate: now.Add(1 * time.Hour),
|
|
Status: ocsp.Good,
|
|
}
|
|
response := buildOcspResponse(t, rootCa, rootCaKey, ocspRes)
|
|
_, _ = w.Write(response)
|
|
})
|
|
ts1 := httptest.NewServer(ocspHandler)
|
|
ts2 := httptest.NewServer(ocspHandler)
|
|
defer ts1.Close()
|
|
defer ts2.Close()
|
|
|
|
logFactory := func() hclog.Logger {
|
|
return hclog.NewNullLogger()
|
|
}
|
|
client := New(logFactory, 100)
|
|
|
|
config := &VerifyConfig{
|
|
OcspEnabled: true,
|
|
OcspServersOverride: []string{ts1.URL, ts2.URL},
|
|
QueryAllServers: true,
|
|
}
|
|
|
|
_, err := client.GetRevocationStatus(context.Background(), leafCert, rootCa, config)
|
|
require.NoError(t, err, "Failed fetching revocation status")
|
|
// Make sure that we queried both servers and not the cache
|
|
require.Equal(t, uint32(2), numQueries.Load())
|
|
|
|
// These query should be cached and not influence our counter
|
|
_, err = client.GetRevocationStatus(context.Background(), leafCert, rootCa, config)
|
|
require.NoError(t, err, "Failed fetching revocation status second time")
|
|
|
|
require.Equal(t, uint32(2), numQueries.Load())
|
|
}
|
|
|
|
func buildOcspResponse(t *testing.T, ca *x509.Certificate, caKey *ecdsa.PrivateKey, ocspRes ocsp.Response) []byte {
|
|
response, err := ocsp.CreateResponse(ca, ca, ocspRes, caKey)
|
|
if err != nil {
|
|
t.Fatalf("failed generating OCSP response: %v", err)
|
|
}
|
|
return response
|
|
}
|
|
|
|
func createCaLeafCerts(t *testing.T) (*ecdsa.PrivateKey, *x509.Certificate, *x509.Certificate) {
|
|
rootCaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err, "failed generated root key for CA")
|
|
|
|
// Validate we reject CSRs that contain CN that aren't in the original order
|
|
cr := &x509.Certificate{
|
|
Subject: pkix.Name{CommonName: "Root Cert"},
|
|
SerialNumber: big.NewInt(1),
|
|
IsCA: true,
|
|
BasicConstraintsValid: true,
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
NotBefore: time.Now().Add(-1 * time.Second),
|
|
NotAfter: time.Now().AddDate(1, 0, 0),
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},
|
|
}
|
|
rootCaBytes, err := x509.CreateCertificate(rand.Reader, cr, cr, &rootCaKey.PublicKey, rootCaKey)
|
|
require.NoError(t, err, "failed generating root ca")
|
|
|
|
rootCa, err := x509.ParseCertificate(rootCaBytes)
|
|
require.NoError(t, err, "failed parsing root ca")
|
|
|
|
leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err, "failed generated leaf key")
|
|
|
|
cr = &x509.Certificate{
|
|
Subject: pkix.Name{CommonName: "Leaf Cert"},
|
|
SerialNumber: big.NewInt(2),
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
NotBefore: time.Now().Add(-1 * time.Second),
|
|
NotAfter: time.Now().AddDate(1, 0, 0),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
}
|
|
leafCertBytes, err := x509.CreateCertificate(rand.Reader, cr, rootCa, &leafKey.PublicKey, rootCaKey)
|
|
require.NoError(t, err, "failed generating root ca")
|
|
|
|
leafCert, err := x509.ParseCertificate(leafCertBytes)
|
|
require.NoError(t, err, "failed parsing root ca")
|
|
return rootCaKey, rootCa, leafCert
|
|
}
|
|
|
|
func TestUnitValidateOCSP(t *testing.T) {
|
|
conf := &VerifyConfig{OcspEnabled: true}
|
|
ocspRes := &ocsp.Response{}
|
|
ost, err := validateOCSP(conf, 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(conf, 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(conf, 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(conf, 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(conf, 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[0], certs[len(certs)-1], nil)
|
|
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[0], certs[len(certs)-1], nil)
|
|
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-----
|
|
`
|