backport of commit f079b7b0a4de28f1230a270fc35ea5a787ad96d2 (#21060)
Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
parent
1f8c665eb3
commit
d8979b449c
|
@ -415,6 +415,22 @@ func (ace *ACMEChallengeEngine) _verifyChallenge(sc *storageContext, id string,
|
|||
err = fmt.Errorf("%w: error validating dns-01 challenge %v: %s", ErrIncorrectResponse, id, err.Error())
|
||||
return ace._verifyChallengeRetry(sc, cv, authzPath, authz, challenge, err, id)
|
||||
}
|
||||
case ACMEALPNChallenge:
|
||||
if authz.Identifier.Type != ACMEDNSIdentifier {
|
||||
err = fmt.Errorf("unsupported identifier type for authorization %v/%v in challenge %v: %v", cv.Account, cv.Authorization, id, authz.Identifier.Type)
|
||||
return ace._verifyChallengeCleanup(sc, err, id)
|
||||
}
|
||||
|
||||
if authz.Wildcard {
|
||||
err = fmt.Errorf("unable to validate wildcard authorization %v/%v in challenge %v via tls-alpn-01 challenge", cv.Account, cv.Authorization, id)
|
||||
return ace._verifyChallengeCleanup(sc, err, id)
|
||||
}
|
||||
|
||||
valid, err = ValidateTLSALPN01Challenge(authz.Identifier.Value, cv.Token, cv.Thumbprint, config)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: error validating tls-alpn-01 challenge %v: %s", ErrIncorrectResponse, id, err.Error())
|
||||
return ace._verifyChallengeRetry(sc, cv, authzPath, authz, challenge, err, id)
|
||||
}
|
||||
default:
|
||||
err = fmt.Errorf("unsupported ACME challenge type %v for challenge %v", cv.ChallengeType, id)
|
||||
return ace._verifyChallengeCleanup(sc, err, id)
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -12,7 +16,21 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const DNSChallengePrefix = "_acme-challenge."
|
||||
const (
|
||||
DNSChallengePrefix = "_acme-challenge."
|
||||
ALPNProtocol = "acme-tls/1"
|
||||
)
|
||||
|
||||
// While this should be a constant, there's no way to do a low-level test of
|
||||
// ValidateTLSALPN01Challenge without spinning up a complicated Docker
|
||||
// instance to build a custom responder. Because we already have a local
|
||||
// toolchain, it is far easier to drive this through Go tests with a custom
|
||||
// (high) port, rather than requiring permission to bind to port 443 (root-run
|
||||
// tests are even worse).
|
||||
var ALPNPort = "443"
|
||||
|
||||
// OID of the acmeIdentifier X.509 Certificate Extension.
|
||||
var OIDACMEIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
|
||||
|
||||
// ValidateKeyAuthorization validates that the given keyAuthz from a challenge
|
||||
// matches our expectation, returning (true, nil) if so, or (false, err) if
|
||||
|
@ -67,15 +85,28 @@ func buildResolver(config *acmeConfigEntry) (*net.Resolver, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func buildDialerConfig(config *acmeConfigEntry) (*net.Dialer, error) {
|
||||
resolver, err := buildResolver(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build resolver: %w", err)
|
||||
}
|
||||
|
||||
return &net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: -1 * time.Second,
|
||||
Resolver: resolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validates a given ACME http-01 challenge against the specified domain,
|
||||
// per RFC 8555.
|
||||
//
|
||||
// We attempt to be defensive here against timeouts, extra redirects, &c.
|
||||
func ValidateHTTP01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) {
|
||||
path := "http://" + domain + "/.well-known/acme-challenge/" + token
|
||||
resolver, err := buildResolver(config)
|
||||
dialer, err := buildDialerConfig(config)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to build resolver: %w", err)
|
||||
return false, fmt.Errorf("failed to build dialer: %w", err)
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
|
@ -90,11 +121,7 @@ func ValidateHTTP01Challenge(domain string, token string, thumbprint string, con
|
|||
|
||||
// We'd rather timeout and re-attempt validation later than hang
|
||||
// too many validators waiting for slow hosts.
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 10 * time.Second,
|
||||
KeepAlive: -1 * time.Second,
|
||||
Resolver: resolver,
|
||||
}).DialContext,
|
||||
DialContext: dialer.DialContext,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
|
@ -188,3 +215,255 @@ func ValidateDNS01Challenge(domain string, token string, thumbprint string, conf
|
|||
|
||||
return false, fmt.Errorf("dns-01: challenge failed against %v records", len(results))
|
||||
}
|
||||
|
||||
func ValidateTLSALPN01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) {
|
||||
// This RFC is defined in RFC 8737 Automated Certificate Management
|
||||
// Environment (ACME) TLS Application‑Layer Protocol Negotiation
|
||||
// (ALPN) Challenge Extension.
|
||||
//
|
||||
// This is conceptually similar to ValidateHTTP01Challenge, but
|
||||
// uses a TLS connection on port 443 with the specified ALPN
|
||||
// protocol.
|
||||
|
||||
cfg := &tls.Config{
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge, the name of the negotiated
|
||||
// protocol is "acme-tls/1".
|
||||
NextProtos: []string{ALPNProtocol},
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > ... and an SNI extension containing only the domain name
|
||||
// > being validated during the TLS handshake.
|
||||
//
|
||||
// According to the Go docs, setting this option (even though
|
||||
// InsecureSkipVerify=true is also specified), allows us to
|
||||
// set the SNI extension to this value.
|
||||
ServerName: domain,
|
||||
|
||||
VerifyConnection: func(connState tls.ConnectionState) error {
|
||||
// We initiated a fresh connection with no session tickets;
|
||||
// even if we did have a session ticket, we do not wish to
|
||||
// use it. Verify that the server has not inadvertently
|
||||
// reused connections between validation attempts or something.
|
||||
if connState.DidResume {
|
||||
return fmt.Errorf("server under test incorrectly reported that handshake was resumed when no session cache was provided; refusing to continue")
|
||||
}
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > The ACME server verifies that during the TLS handshake the
|
||||
// > application-layer protocol "acme-tls/1" was successfully
|
||||
// > negotiated (and that the ALPN extension contained only the
|
||||
// > value "acme-tls/1").
|
||||
if connState.NegotiatedProtocol != ALPNProtocol {
|
||||
return fmt.Errorf("server under test negotiated unexpected ALPN protocol %v", connState.NegotiatedProtocol)
|
||||
}
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > and that the certificate returned
|
||||
//
|
||||
// Because this certificate MUST be self-signed (per earlier
|
||||
// statement in RFC 8737 Section 3), there is no point in sending
|
||||
// more than one certificate, and so we will err early here if
|
||||
// we got more than one.
|
||||
if len(connState.PeerCertificates) > 1 {
|
||||
return fmt.Errorf("server under test returned multiple (%v) certificates when we expected only one", len(connState.PeerCertificates))
|
||||
}
|
||||
cert := connState.PeerCertificates[0]
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > The client prepares for validation by constructing a
|
||||
// > self-signed certificate that MUST contain an acmeIdentifier
|
||||
// > extension and a subjectAlternativeName extension [RFC5280].
|
||||
//
|
||||
// Verify that this is a self-signed certificate that isn't signed
|
||||
// by another certificate (i.e., with the same key material but
|
||||
// different issuer).
|
||||
if err := cert.CheckSignatureFrom(cert); err != nil {
|
||||
return fmt.Errorf("server under test returned a non-self-signed certificate: %w", err)
|
||||
}
|
||||
if !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
|
||||
return fmt.Errorf("server under test returned a non-self-signed certificate: invalid subject (%v) <-> issuer (%v) match", cert.Subject.String(), cert.Issuer.String())
|
||||
}
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > The subjectAlternativeName extension MUST contain a single
|
||||
// > dNSName entry where the value is the domain name being
|
||||
// > validated.
|
||||
//
|
||||
// TODO: this does not validate that there are not other SANs
|
||||
// with unknown (to Go) OIDs.
|
||||
if len(cert.DNSNames) != 1 || len(cert.EmailAddresses) > 0 || len(cert.IPAddresses) > 0 || len(cert.URIs) > 0 {
|
||||
return fmt.Errorf("server under test returned a certificate with incorrect SANs")
|
||||
}
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > The comparison of dNSNames MUST be case insensitive
|
||||
// > [RFC4343]. Note that as ACME doesn't support Unicode
|
||||
// > identifiers, all dNSNames MUST be encoded using the rules
|
||||
// > of [RFC3492].
|
||||
if !strings.EqualFold(cert.DNSNames[0], domain) {
|
||||
return fmt.Errorf("server under test returned a certificate with unexpected identifier: %v", cert.DNSNames[0])
|
||||
}
|
||||
|
||||
// Per above, verify that the acmeIdentifier extension is present
|
||||
// exactly once and has the correct value.
|
||||
var foundACMEId bool
|
||||
for _, ext := range cert.Extensions {
|
||||
if !ext.Id.Equal(OIDACMEIdentifier) {
|
||||
continue
|
||||
}
|
||||
|
||||
// There must be only a single ACME extension.
|
||||
if foundACMEId {
|
||||
return fmt.Errorf("server under test returned a certificate with multiple acmeIdentifier extensions")
|
||||
}
|
||||
foundACMEId = true
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > a critical acmeIdentifier extension
|
||||
if !ext.Critical {
|
||||
return fmt.Errorf("server under test returned a certificate with an acmeIdentifier extension marked non-Critical")
|
||||
}
|
||||
|
||||
keyAuthz := string(ext.Value)
|
||||
ok, err := ValidateSHA256KeyAuthorization(keyAuthz, token, thumbprint)
|
||||
if !ok || err != nil {
|
||||
return fmt.Errorf("server under test returned a certificate with an invalid key authorization (%w)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > The ACME server verifies that ... the certificate returned
|
||||
// > contains: ... a critical acmeIdentifier extension containing
|
||||
// > the expected SHA-256 digest computed in step 1.
|
||||
if !foundACMEId {
|
||||
return fmt.Errorf("server under test returned a certificate without the required acmeIdentifier extension")
|
||||
}
|
||||
|
||||
// Remove the handled critical extension and validate that we
|
||||
// have no additional critical extensions left unhandled.
|
||||
var index int = -1
|
||||
for oidIndex, oid := range cert.UnhandledCriticalExtensions {
|
||||
if oid.Equal(OIDACMEIdentifier) {
|
||||
index = oidIndex
|
||||
break
|
||||
}
|
||||
}
|
||||
if index != -1 {
|
||||
// Unlike the foundACMEId case, this is not a failure; if Go
|
||||
// updates to "understand" this critical extension, we do not
|
||||
// wish to fail.
|
||||
cert.UnhandledCriticalExtensions = append(cert.UnhandledCriticalExtensions[0:index], cert.UnhandledCriticalExtensions[index+1:]...)
|
||||
}
|
||||
if len(cert.UnhandledCriticalExtensions) > 0 {
|
||||
return fmt.Errorf("server under test returned a certificate with additional unknown critical extensions (%v)", cert.UnhandledCriticalExtensions)
|
||||
}
|
||||
|
||||
// All good!
|
||||
return nil
|
||||
},
|
||||
|
||||
// We never want to resume a connection; do not provide session
|
||||
// cache storage.
|
||||
ClientSessionCache: nil,
|
||||
|
||||
// Do not trust any system trusted certificates; we're going to be
|
||||
// manually validating the chain, so specifying a non-empty pool
|
||||
// here could only cause additional, unnecessary work.
|
||||
RootCAs: x509.NewCertPool(),
|
||||
|
||||
// Do not bother validating the client's chain; we know it should be
|
||||
// self-signed. This also disables hostname verification, but we do
|
||||
// this verification as part of VerifyConnection(...) ourselves.
|
||||
//
|
||||
// Per Go docs, this option is only safe in conjunction with
|
||||
// VerifyConnection which we define above.
|
||||
InsecureSkipVerify: true,
|
||||
|
||||
// RFC 8737 Section 4. acme-tls/1 Protocol Definition:
|
||||
//
|
||||
// > ACME servers that implement "acme-tls/1" MUST only negotiate
|
||||
// > TLS 1.2 [RFC5246] or higher when connecting to clients for
|
||||
// > validation.
|
||||
MinVersion: tls.VersionTLS12,
|
||||
|
||||
// While RFC 8737 does not place restrictions around allowed cipher
|
||||
// suites, we wish to restrict ourselves to secure defaults. Specify
|
||||
// the Intermediate guideline from Mozilla's TLS config generator to
|
||||
// disable obviously weak ciphers.
|
||||
//
|
||||
// See also: https://ssl-config.mozilla.org/#server=go&version=1.14.4&config=intermediate&guideline=5.7
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
}
|
||||
|
||||
// Build a dialer using our custom DNS resolver, to ensure domains get
|
||||
// resolved according to configuration.
|
||||
dialer, err := buildDialerConfig(config)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to build dialer: %w", err)
|
||||
}
|
||||
|
||||
// Per RFC 8737 Section 3. TLS with Application-Layer Protocol
|
||||
// Negotiation (TLS ALPN) Challenge:
|
||||
//
|
||||
// > 2. The ACME server resolves the domain name being validated and
|
||||
// > chooses one of the IP addresses returned for validation (the
|
||||
// > server MAY validate against multiple addresses if more than
|
||||
// > one is returned).
|
||||
// > 3. The ACME server initiates a TLS connection to the chosen IP
|
||||
// > address. This connection MUST use TCP port 443.
|
||||
address := fmt.Sprintf("%v:"+ALPNPort, domain)
|
||||
conn, err := dialer.Dial("tcp", address)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tls-alpn-01: failed to dial host: %w", err)
|
||||
}
|
||||
|
||||
// Initiate the connection to the remote peer.
|
||||
client := tls.Client(conn, cfg)
|
||||
|
||||
// We intentionally swallow this error as it isn't useful to the
|
||||
// underlying protocol we perform here. Notably, per RFC 8737
|
||||
// Section 4. acme-tls/1 Protocol Definition:
|
||||
//
|
||||
// > Once the handshake is completed, the client MUST NOT exchange
|
||||
// > any further data with the server and MUST immediately close the
|
||||
// > connection. ... Because of this, an ACME server MAY choose to
|
||||
// > withhold authorization if either the certificate signature is
|
||||
// > invalid or the handshake doesn't fully complete.
|
||||
defer client.Close()
|
||||
|
||||
// We wish to put time bounds on the total time the handshake can
|
||||
// stall for, so build a connection context here.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// See note above about why we can allow Handshake to complete
|
||||
// successfully.
|
||||
if err := client.HandshakeContext(ctx); err != nil {
|
||||
return false, fmt.Errorf("tls-alpn-01: failed to perform handshake: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
@ -10,6 +20,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/builtin/logical/pki/dnstest"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type keyAuthorizationTestCase struct {
|
||||
|
@ -190,7 +202,7 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) {
|
|||
func TestAcmeValidateDNS01Challenge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
host := "alsdkjfasldkj.com"
|
||||
host := "dadgarcorp.com"
|
||||
resolver := dnstest.SetupResolver(t, host)
|
||||
defer resolver.Cleanup()
|
||||
|
||||
|
@ -219,3 +231,473 @@ func TestAcmeValidateDNS01Challenge(t *testing.T) {
|
|||
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{}
|
||||
|
||||
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
|
||||
t.Logf("[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
|
||||
t.Logf("[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() {
|
||||
t.Logf("[alpn-server] starting accept...")
|
||||
connRaw, err := ln.Accept()
|
||||
require.NoError(t, err, "failed to accept TLS connection")
|
||||
|
||||
t.Logf("[alpn-server] got connection...")
|
||||
conn := tls.Server(connRaw.(*tls.Conn), tlsCfg)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer func() {
|
||||
t.Logf("[alpn-server] defer context cancel executing")
|
||||
cancel()
|
||||
}()
|
||||
|
||||
t.Logf("[alpn-server] starting handshake...")
|
||||
if err := conn.HandshakeContext(ctx); err != nil {
|
||||
t.Logf("[alpn-server] got non-fatal error while handshaking connection: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("[alpn-server] closing connection...")
|
||||
if err := conn.Close(); err != nil {
|
||||
t.Logf("[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 {
|
||||
t.Logf("using keyAuthorizationTestCase [tc=%d] as alpnTestCase [tc=%d]...", index, len(alpnTestCases))
|
||||
// Properly encode the authorization.
|
||||
checksum := sha256.Sum256([]byte(tc.keyAuthz))
|
||||
authz := base64.RawURLEncoding.EncodeToString(checksum[:])
|
||||
|
||||
// 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: []byte(authz),
|
||||
},
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
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 := base64.RawURLEncoding.EncodeToString(checksum[:])
|
||||
|
||||
// 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: []byte(authz),
|
||||
},
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
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 := base64.RawURLEncoding.EncodeToString(checksum[:])
|
||||
|
||||
// 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: []byte(authz),
|
||||
},
|
||||
},
|
||||
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: "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 := base64.RawURLEncoding.EncodeToString(checksum[:])
|
||||
|
||||
// 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: []byte(authz),
|
||||
},
|
||||
},
|
||||
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: "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 := base64.RawURLEncoding.EncodeToString(checksum[:])
|
||||
|
||||
// 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: []byte(authz),
|
||||
},
|
||||
},
|
||||
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: "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 := base64.RawURLEncoding.EncodeToString(checksum[:])
|
||||
|
||||
// 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: []byte(authz),
|
||||
},
|
||||
},
|
||||
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: "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 {
|
||||
t.Logf("\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 {
|
||||
t.Logf("[tc=%d/name=%s] got expected failure: err=%v", index, tc.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -799,7 +799,7 @@ func generateAuthorization(acct *acmeAccount, identifier *ACMEIdentifier) (*ACME
|
|||
// Certain challenges have certain restrictions: DNS challenges cannot
|
||||
// be used to validate IP addresses, and only DNS challenges can be used
|
||||
// to validate wildcards.
|
||||
allowedChallenges := []ACMEChallengeType{ACMEHTTPChallenge, ACMEDNSChallenge}
|
||||
allowedChallenges := []ACMEChallengeType{ACMEHTTPChallenge, ACMEDNSChallenge, ACMEALPNChallenge}
|
||||
if identifier.Type == ACMEIPIdentifier {
|
||||
allowedChallenges = []ACMEChallengeType{ACMEHTTPChallenge}
|
||||
} else if identifier.IsWildcard {
|
||||
|
|
|
@ -184,7 +184,7 @@ func TestAcmeBasicWorkflow(t *testing.T) {
|
|||
require.False(t, domainAuth.Wildcard, "should not be a wildcard")
|
||||
require.True(t, domainAuth.Expires.IsZero(), "authorization should only have expiry set on valid status")
|
||||
|
||||
require.Len(t, domainAuth.Challenges, 2, "expected two challenges")
|
||||
require.Len(t, domainAuth.Challenges, 3, "expected three challenges")
|
||||
require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status)
|
||||
require.True(t, domainAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge")
|
||||
require.Equal(t, "http-01", domainAuth.Challenges[0].Type)
|
||||
|
@ -193,6 +193,10 @@ func TestAcmeBasicWorkflow(t *testing.T) {
|
|||
require.True(t, domainAuth.Challenges[1].Validated.IsZero(), "validated time should be 0 on challenge")
|
||||
require.Equal(t, "dns-01", domainAuth.Challenges[1].Type)
|
||||
require.NotEmpty(t, domainAuth.Challenges[1].Token, "missing challenge token")
|
||||
require.Equal(t, acme.StatusPending, domainAuth.Challenges[2].Status)
|
||||
require.True(t, domainAuth.Challenges[2].Validated.IsZero(), "validated time should be 0 on challenge")
|
||||
require.Equal(t, "tls-alpn-01", domainAuth.Challenges[2].Type)
|
||||
require.NotEmpty(t, domainAuth.Challenges[2].Token, "missing challenge token")
|
||||
|
||||
// Test the values for the wildcard authentication
|
||||
require.Equal(t, acme.StatusPending, wildcardAuth.Status)
|
||||
|
@ -201,7 +205,7 @@ func TestAcmeBasicWorkflow(t *testing.T) {
|
|||
require.True(t, wildcardAuth.Wildcard, "should be a wildcard")
|
||||
require.True(t, wildcardAuth.Expires.IsZero(), "authorization should only have expiry set on valid status")
|
||||
|
||||
require.Len(t, wildcardAuth.Challenges, 1, "expected two challenges")
|
||||
require.Len(t, wildcardAuth.Challenges, 1, "expected one challenge")
|
||||
require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status)
|
||||
require.True(t, wildcardAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge")
|
||||
require.Equal(t, "dns-01", wildcardAuth.Challenges[0].Type)
|
||||
|
@ -1080,7 +1084,7 @@ func TestAcmeValidationError(t *testing.T) {
|
|||
authorizations = append(authorizations, auth)
|
||||
}
|
||||
require.Len(t, authorizations, 1, "expected a certain number of authorizations")
|
||||
require.Len(t, authorizations[0].Challenges, 2, "expected a certain number of challenges associated with authorization")
|
||||
require.Len(t, authorizations[0].Challenges, 3, "expected a certain number of challenges associated with authorization")
|
||||
|
||||
acceptedAuth, err := acmeClient.Accept(testCtx, authorizations[0].Challenges[0])
|
||||
require.NoError(t, err, "Should have been allowed to accept challenge 1")
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
secrets/pki: Support TLS-ALPN-01 challenge type in ACME for DNS certificate identifiers.
|
||||
```
|
|
@ -165,8 +165,9 @@ identifiers.
|
|||
|
||||
Vault supports the following ACME challenge types presently:
|
||||
|
||||
- `http-01`, supporting both `dns` and `ip` identifiers,
|
||||
- `http-01`, supporting both `dns` and `ip` identifiers.
|
||||
- `dns-01`, supporting `dns` identifiers including wildcards.
|
||||
- `tls-alpn-01`, supporting only non-wildcard `dns` identifiers.
|
||||
|
||||
A custom DNS resolver used by the server for looking up DNS names for use
|
||||
with both mechanisms can be added via the [ACME configuration](#set-acme-configuration).
|
||||
|
|
Loading…
Reference in New Issue