blob: 11c2cf2793912e0be6abeb9a8964b825b6a43ee9 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault/builtin/logical/pki/dnstest"
"github.com/stretchr/testify/require"
)
type keyAuthorizationTestCase struct {
keyAuthz string
token string
thumbprint string
shouldFail bool
}
var keyAuthorizationTestCases = []keyAuthorizationTestCase{
{
// Entirely empty
"",
"non-empty-token",
"non-empty-thumbprint",
true,
},
{
// Both empty
".",
"non-empty-token",
"non-empty-thumbprint",
true,
},
{
// Not equal
"non-.non-",
"non-empty-token",
"non-empty-thumbprint",
true,
},
{
// Empty thumbprint
"non-.",
"non-empty-token",
"non-empty-thumbprint",
true,
},
{
// Empty token
".non-",
"non-empty-token",
"non-empty-thumbprint",
true,
},
{
// Wrong order
"non-empty-thumbprint.non-empty-token",
"non-empty-token",
"non-empty-thumbprint",
true,
},
{
// Too many pieces
"one.two.three",
"non-empty-token",
"non-empty-thumbprint",
true,
},
{
// Valid
"non-empty-token.non-empty-thumbprint",
"non-empty-token",
"non-empty-thumbprint",
false,
},
}
func TestAcmeValidateKeyAuthorization(t *testing.T) {
t.Parallel()
for index, tc := range keyAuthorizationTestCases {
isValid, err := ValidateKeyAuthorization(tc.keyAuthz, tc.token, tc.thumbprint)
if !isValid && err == nil {
t.Fatalf("[%d] expected failure to give reason via err (%v / %v)", index, isValid, err)
}
expectedValid := !tc.shouldFail
if expectedValid != isValid {
t.Fatalf("[%d] got ret=%v, expected ret=%v (shouldFail=%v)", index, isValid, expectedValid, tc.shouldFail)
}
}
}
func TestAcmeValidateHTTP01Challenge(t *testing.T) {
t.Parallel()
for index, tc := range keyAuthorizationTestCases {
validFunc := func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(tc.keyAuthz))
}
withPadding := func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(" " + tc.keyAuthz + " "))
}
withRedirect := func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/.well-known/") {
http.Redirect(w, r, "/my-http-01-challenge-response", 301)
return
}
w.Write([]byte(tc.keyAuthz))
}
withSleep := func(w http.ResponseWriter, r *http.Request) {
// Long enough to ensure any excessively short timeouts are hit,
// not long enough to trigger a failure (hopefully).
time.Sleep(5 * time.Second)
w.Write([]byte(tc.keyAuthz))
}
validHandlers := []http.HandlerFunc{
http.HandlerFunc(validFunc), http.HandlerFunc(withPadding),
http.HandlerFunc(withRedirect), http.HandlerFunc(withSleep),
}
for handlerIndex, handler := range validHandlers {
func() {
ts := httptest.NewServer(handler)
defer ts.Close()
host := ts.URL[7:]
isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{})
if !isValid && err == nil {
t.Fatalf("[tc=%d/handler=%d] expected failure to give reason via err (%v / %v)", index, handlerIndex, isValid, err)
}
expectedValid := !tc.shouldFail
if expectedValid != isValid {
t.Fatalf("[tc=%d/handler=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, handlerIndex, isValid, err, expectedValid, tc.shouldFail)
}
}()
}
}
// Negative test cases for various HTTP-specific scenarios.
redirectLoop := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/my-http-01-challenge-response", 301)
}
publicRedirect := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "http://hashicorp.com/", 301)
}
noData := func(w http.ResponseWriter, r *http.Request) {}
noContent := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
notFound := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}
simulateHang := func(w http.ResponseWriter, r *http.Request) {
time.Sleep(30 * time.Second)
w.Write([]byte("my-token.my-thumbprint"))
}
tooLarge := func(w http.ResponseWriter, r *http.Request) {
for i := 0; i < 512; i++ {
w.Write([]byte("my-token.my-thumbprint\n"))
}
}
validHandlers := []http.HandlerFunc{
http.HandlerFunc(redirectLoop), http.HandlerFunc(publicRedirect),
http.HandlerFunc(noData), http.HandlerFunc(noContent),
http.HandlerFunc(notFound), http.HandlerFunc(simulateHang),
http.HandlerFunc(tooLarge),
}
for handlerIndex, handler := range validHandlers {
func() {
ts := httptest.NewServer(handler)
defer ts.Close()
host := ts.URL[7:]
isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", &acmeConfigEntry{})
if isValid || err == nil {
t.Fatalf("[handler=%d] expected failure validating challenge (%v / %v)", handlerIndex, isValid, err)
}
}()
}
}
func TestAcmeValidateDNS01Challenge(t *testing.T) {
t.Parallel()
host := "dadgarcorp.com"
resolver := dnstest.SetupResolver(t, host)
defer resolver.Cleanup()
t.Logf("DNS Server Address: %v", resolver.GetLocalAddr())
config := &acmeConfigEntry{
DNSResolver: resolver.GetLocalAddr(),
}
for index, tc := range keyAuthorizationTestCases {
checksum := sha256.Sum256([]byte(tc.keyAuthz))
authz := base64.RawURLEncoding.EncodeToString(checksum[:])
resolver.AddRecord(DNSChallengePrefix+host, "TXT", authz)
resolver.PushConfig()
isValid, err := ValidateDNS01Challenge(host, tc.token, tc.thumbprint, config)
if !isValid && err == nil {
t.Fatalf("[tc=%d] expected failure to give reason via err (%v / %v)", index, isValid, err)
}
expectedValid := !tc.shouldFail
if expectedValid != isValid {
t.Fatalf("[tc=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, isValid, err, expectedValid, tc.shouldFail)
}
resolver.RemoveAllRecords()
}
}
func TestAcmeValidateTLSALPN01Challenge(t *testing.T) {
// This test is not parallel because we modify ALPNPort to use a custom
// non-standard port _just for testing purposes_.
host := "localhost"
config := &acmeConfigEntry{}
log := hclog.L()
returnedProtocols := []string{ALPNProtocol}
var certificates []*x509.Certificate
var privateKey crypto.PrivateKey
tlsCfg := &tls.Config{}
tlsCfg.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) {
var retCfg tls.Config = *tlsCfg
retCfg.NextProtos = returnedProtocols
log.Info(fmt.Sprintf("[alpn-server] returned protocol: %v", returnedProtocols))
return &retCfg, nil
}
tlsCfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
var ret tls.Certificate
for index, cert := range certificates {
ret.Certificate = append(ret.Certificate, cert.Raw)
if index == 0 {
ret.Leaf = cert
}
}
ret.PrivateKey = privateKey
log.Info(fmt.Sprintf("[alpn-server] returned certificates: %v", ret))
return &ret, nil
}
ln, err := tls.Listen("tcp", host+":0", tlsCfg)
require.NoError(t, err, "failed to listen with TLS config")
doOneAccept := func() {
log.Info("[alpn-server] starting accept...")
connRaw, err := ln.Accept()
require.NoError(t, err, "failed to accept TLS connection")
log.Info("[alpn-server] got connection...")
conn := tls.Server(connRaw.(*tls.Conn), tlsCfg)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer func() {
log.Info("[alpn-server] canceling listener connection...")
cancel()
}()
log.Info("[alpn-server] starting handshake...")
if err := conn.HandshakeContext(ctx); err != nil {
log.Info("[alpn-server] got non-fatal error while handshaking connection: %v", err)
}
log.Info("[alpn-server] closing connection...")
if err := conn.Close(); err != nil {
log.Info("[alpn-server] got non-fatal error while closing connection: %v", err)
}
}
ALPNPort = strings.Split(ln.Addr().String(), ":")[1]
type alpnTestCase struct {
name string
certificates []*x509.Certificate
privateKey crypto.PrivateKey
protocols []string
token string
thumbprint string
shouldFail bool
}
var alpnTestCases []alpnTestCase
// Add all of our keyAuthorizationTestCases into alpnTestCases
for index, tc := range keyAuthorizationTestCases {
log.Info(fmt.Sprintf("using keyAuthorizationTestCase [tc=%d] as alpnTestCase [tc=%d]...", index, len(alpnTestCases)))
// Properly encode the authorization.
checksum := sha256.Sum256([]byte(tc.keyAuthz))
authz, err := asn1.Marshal(checksum[:])
require.NoError(t, err, "failed asn.1 marshalling authz")
// Build a self-signed certificate.
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: host,
},
Issuer: pkix.Name{
CommonName: host,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: key.Public(),
SerialNumber: big.NewInt(1),
DNSNames: []string{host},
ExtraExtensions: []pkix.Extension{
{
Id: OIDACMEIdentifier,
Critical: true,
Value: authz,
},
},
BasicConstraintsValid: true,
IsCA: false,
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
require.NoError(t, err, "failed to create certificate")
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err, "failed to parse newly generated certificate")
newTc := alpnTestCase{
name: fmt.Sprintf("keyAuthorizationTestCase[%d]", index),
certificates: []*x509.Certificate{cert},
privateKey: key,
protocols: []string{ALPNProtocol},
token: tc.token,
thumbprint: tc.thumbprint,
shouldFail: tc.shouldFail,
}
alpnTestCases = append(alpnTestCases, newTc)
}
{
// Test case: Longer chain
// Build a self-signed certificate.
rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating root private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: "Root CA",
},
Issuer: pkix.Name{
CommonName: "Root CA",
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: rootKey.Public(),
SerialNumber: big.NewInt(1),
BasicConstraintsValid: true,
IsCA: true,
}
rootCertBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootKey.Public(), rootKey)
require.NoError(t, err, "failed to create root certificate")
rootCert, err := x509.ParseCertificate(rootCertBytes)
require.NoError(t, err, "failed to parse newly generated root certificate")
// Compute our authorization.
checksum := sha256.Sum256([]byte("valid.valid"))
authz, err := asn1.Marshal(checksum[:])
require.NoError(t, err, "failed to marshal authz with asn.1 ")
// Build a leaf certificate which _could_ pass validation
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating leaf private key")
tmpl = &x509.Certificate{
Subject: pkix.Name{
CommonName: host,
},
Issuer: pkix.Name{
CommonName: "Root CA",
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: key.Public(),
SerialNumber: big.NewInt(2),
DNSNames: []string{host},
ExtraExtensions: []pkix.Extension{
{
Id: OIDACMEIdentifier,
Critical: true,
Value: authz,
},
},
BasicConstraintsValid: true,
IsCA: false,
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, rootCert, key.Public(), rootKey)
require.NoError(t, err, "failed to create leaf certificate")
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err, "failed to parse newly generated leaf certificate")
newTc := alpnTestCase{
name: "longer chain with valid leaf",
certificates: []*x509.Certificate{cert, rootCert},
privateKey: key,
protocols: []string{ALPNProtocol},
token: "valid",
thumbprint: "valid",
shouldFail: true,
}
alpnTestCases = append(alpnTestCases, newTc)
}
{
// Test case: cert without DNSSan
// Compute our authorization.
checksum := sha256.Sum256([]byte("valid.valid"))
authz, err := asn1.Marshal(checksum[:])
require.NoError(t, err, "failed to marshal authz with asn.1 ")
// Build a leaf certificate without a DNSSan
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating leaf private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: host,
},
Issuer: pkix.Name{
CommonName: host,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: key.Public(),
SerialNumber: big.NewInt(2),
// NO DNSNames
ExtraExtensions: []pkix.Extension{
{
Id: OIDACMEIdentifier,
Critical: true,
Value: authz,
},
},
BasicConstraintsValid: true,
IsCA: false,
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
require.NoError(t, err, "failed to create leaf certificate")
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err, "failed to parse newly generated leaf certificate")
newTc := alpnTestCase{
name: "valid keyauthz without valid dnsname",
certificates: []*x509.Certificate{cert},
privateKey: key,
protocols: []string{ALPNProtocol},
token: "valid",
thumbprint: "valid",
shouldFail: true,
}
alpnTestCases = append(alpnTestCases, newTc)
}
{
// Test case: cert without matching DNSSan
// Compute our authorization.
checksum := sha256.Sum256([]byte("valid.valid"))
authz, err := asn1.Marshal(checksum[:])
require.NoError(t, err, "failed to marshal authz with asn.1 ")
// Build a leaf certificate which fails validation due to bad DNSName
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating leaf private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: host,
},
Issuer: pkix.Name{
CommonName: host,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: key.Public(),
SerialNumber: big.NewInt(2),
DNSNames: []string{host + ".dadgarcorp.com" /* not matching host! */},
ExtraExtensions: []pkix.Extension{
{
Id: OIDACMEIdentifier,
Critical: true,
Value: authz,
},
},
BasicConstraintsValid: true,
IsCA: false,
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
require.NoError(t, err, "failed to create leaf certificate")
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err, "failed to parse newly generated leaf certificate")
newTc := alpnTestCase{
name: "valid keyauthz without matching dnsname",
certificates: []*x509.Certificate{cert},
privateKey: key,
protocols: []string{ALPNProtocol},
token: "valid",
thumbprint: "valid",
shouldFail: true,
}
alpnTestCases = append(alpnTestCases, newTc)
}
{
// Test case: cert with additional SAN
// Compute our authorization.
checksum := sha256.Sum256([]byte("valid.valid"))
authz, err := asn1.Marshal(checksum[:])
require.NoError(t, err, "failed to marshal authz with asn.1 ")
// Build a leaf certificate which has an invalid additional SAN
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating leaf private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: host,
},
Issuer: pkix.Name{
CommonName: host,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: key.Public(),
SerialNumber: big.NewInt(2),
DNSNames: []string{host},
EmailAddresses: []string{"webmaster@" + host}, /* unexpected */
ExtraExtensions: []pkix.Extension{
{
Id: OIDACMEIdentifier,
Critical: true,
Value: authz,
},
},
BasicConstraintsValid: true,
IsCA: false,
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
require.NoError(t, err, "failed to create leaf certificate")
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err, "failed to parse newly generated leaf certificate")
newTc := alpnTestCase{
name: "valid keyauthz with additional email SANs",
certificates: []*x509.Certificate{cert},
privateKey: key,
protocols: []string{ALPNProtocol},
token: "valid",
thumbprint: "valid",
shouldFail: true,
}
alpnTestCases = append(alpnTestCases, newTc)
}
{
// Test case: cert without CN
// Compute our authorization.
checksum := sha256.Sum256([]byte("valid.valid"))
authz, err := asn1.Marshal(checksum[:])
require.NoError(t, err, "failed to marshal authz with asn.1 ")
// Build a leaf certificate which should pass validation
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating leaf private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{},
Issuer: pkix.Name{},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: key.Public(),
SerialNumber: big.NewInt(2),
DNSNames: []string{host},
ExtraExtensions: []pkix.Extension{
{
Id: OIDACMEIdentifier,
Critical: true,
Value: authz,
},
},
BasicConstraintsValid: true,
IsCA: false,
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
require.NoError(t, err, "failed to create leaf certificate")
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err, "failed to parse newly generated leaf certificate")
newTc := alpnTestCase{
name: "valid certificate; no Subject/Issuer (missing CN)",
certificates: []*x509.Certificate{cert},
privateKey: key,
protocols: []string{ALPNProtocol},
token: "valid",
thumbprint: "valid",
shouldFail: false,
}
alpnTestCases = append(alpnTestCases, newTc)
}
{
// Test case: cert without the extension
// Build a leaf certificate which should fail validation
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating leaf private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{},
Issuer: pkix.Name{},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: key.Public(),
SerialNumber: big.NewInt(1),
DNSNames: []string{host},
BasicConstraintsValid: true,
IsCA: true,
}
certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
require.NoError(t, err, "failed to create leaf certificate")
cert, err := x509.ParseCertificate(certBytes)
require.NoError(t, err, "failed to parse newly generated leaf certificate")
newTc := alpnTestCase{
name: "missing required acmeIdentifier extension",
certificates: []*x509.Certificate{cert},
privateKey: key,
protocols: []string{ALPNProtocol},
token: "valid",
thumbprint: "valid",
shouldFail: true,
}
alpnTestCases = append(alpnTestCases, newTc)
}
{
// Test case: root without a leaf
// Build a self-signed certificate.
rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generating root private key")
tmpl := &x509.Certificate{
Subject: pkix.Name{
CommonName: "Root CA",
},
Issuer: pkix.Name{
CommonName: "Root CA",
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
PublicKey: rootKey.Public(),
SerialNumber: big.NewInt(1),
BasicConstraintsValid: true,
IsCA: true,
}
rootCertBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootKey.Public(), rootKey)
require.NoError(t, err, "failed to create root certificate")
rootCert, err := x509.ParseCertificate(rootCertBytes)
require.NoError(t, err, "failed to parse newly generated root certificate")
newTc := alpnTestCase{
name: "root without leaf",
certificates: []*x509.Certificate{rootCert},
privateKey: rootKey,
protocols: []string{ALPNProtocol},
token: "valid",
thumbprint: "valid",
shouldFail: true,
}
alpnTestCases = append(alpnTestCases, newTc)
}
for index, tc := range alpnTestCases {
log.Info(fmt.Sprintf("\n\n[tc=%d/name=%s] starting validation", index, tc.name))
certificates = tc.certificates
privateKey = tc.privateKey
returnedProtocols = tc.protocols
// Attempt to validate the challenge.
go doOneAccept()
isValid, err := ValidateTLSALPN01Challenge(host, tc.token, tc.thumbprint, config)
if !isValid && err == nil {
t.Fatalf("[tc=%d/name=%s] expected failure to give reason via err (%v / %v)", index, tc.name, isValid, err)
}
expectedValid := !tc.shouldFail
if expectedValid != isValid {
t.Fatalf("[tc=%d/name=%s] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, tc.name, isValid, err, expectedValid, tc.shouldFail)
} else if err != nil {
log.Info(fmt.Sprintf("[tc=%d/name=%s] got expected failure: err=%v", index, tc.name, err))
}
}
}