mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-22 23:21:08 +02:00
* Validate OCSP response is signed by expected issuer and serial number matches request - There was a bug in the OCSP response signature logic, it properly verified but kept around the ocspRes object around so we ignored the errors found and passed the response object back up the stack. - Now extract the verification logic into a dedicated function, if it returns an error, blank the ocspRes response as we can't trust it. - Address an issue that the OCSP requests from multiple servers were clobbering each others responses as the index loop variable was not properly captured. - Add a missing validation that the response was for the serial number we requested * Add cl
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"
|
|
"io/ioutil"
|
|
"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 {
|
|
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) {
|
|
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])
|
|
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])
|
|
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-----
|
|
`
|