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())
|
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)
|
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:
|
default:
|
||||||
err = fmt.Errorf("unsupported ACME challenge type %v for challenge %v", cv.ChallengeType, id)
|
err = fmt.Errorf("unsupported ACME challenge type %v for challenge %v", cv.ChallengeType, id)
|
||||||
return ace._verifyChallengeCleanup(sc, err, id)
|
return ace._verifyChallengeCleanup(sc, err, id)
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package pki
|
package pki
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -12,7 +16,21 @@ import (
|
||||||
"time"
|
"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
|
// ValidateKeyAuthorization validates that the given keyAuthz from a challenge
|
||||||
// matches our expectation, returning (true, nil) if so, or (false, err) if
|
// matches our expectation, returning (true, nil) if so, or (false, err) if
|
||||||
|
@ -67,15 +85,28 @@ func buildResolver(config *acmeConfigEntry) (*net.Resolver, error) {
|
||||||
}, nil
|
}, 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,
|
// Validates a given ACME http-01 challenge against the specified domain,
|
||||||
// per RFC 8555.
|
// per RFC 8555.
|
||||||
//
|
//
|
||||||
// We attempt to be defensive here against timeouts, extra redirects, &c.
|
// We attempt to be defensive here against timeouts, extra redirects, &c.
|
||||||
func ValidateHTTP01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) {
|
func ValidateHTTP01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) {
|
||||||
path := "http://" + domain + "/.well-known/acme-challenge/" + token
|
path := "http://" + domain + "/.well-known/acme-challenge/" + token
|
||||||
resolver, err := buildResolver(config)
|
dialer, err := buildDialerConfig(config)
|
||||||
if err != nil {
|
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{
|
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
|
// We'd rather timeout and re-attempt validation later than hang
|
||||||
// too many validators waiting for slow hosts.
|
// too many validators waiting for slow hosts.
|
||||||
DialContext: (&net.Dialer{
|
DialContext: dialer.DialContext,
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
KeepAlive: -1 * time.Second,
|
|
||||||
Resolver: resolver,
|
|
||||||
}).DialContext,
|
|
||||||
ResponseHeaderTimeout: 10 * time.Second,
|
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))
|
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
|
package pki
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -10,6 +20,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/builtin/logical/pki/dnstest"
|
"github.com/hashicorp/vault/builtin/logical/pki/dnstest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type keyAuthorizationTestCase struct {
|
type keyAuthorizationTestCase struct {
|
||||||
|
@ -190,7 +202,7 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) {
|
||||||
func TestAcmeValidateDNS01Challenge(t *testing.T) {
|
func TestAcmeValidateDNS01Challenge(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
host := "alsdkjfasldkj.com"
|
host := "dadgarcorp.com"
|
||||||
resolver := dnstest.SetupResolver(t, host)
|
resolver := dnstest.SetupResolver(t, host)
|
||||||
defer resolver.Cleanup()
|
defer resolver.Cleanup()
|
||||||
|
|
||||||
|
@ -219,3 +231,473 @@ func TestAcmeValidateDNS01Challenge(t *testing.T) {
|
||||||
resolver.RemoveAllRecords()
|
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
|
// Certain challenges have certain restrictions: DNS challenges cannot
|
||||||
// be used to validate IP addresses, and only DNS challenges can be used
|
// be used to validate IP addresses, and only DNS challenges can be used
|
||||||
// to validate wildcards.
|
// to validate wildcards.
|
||||||
allowedChallenges := []ACMEChallengeType{ACMEHTTPChallenge, ACMEDNSChallenge}
|
allowedChallenges := []ACMEChallengeType{ACMEHTTPChallenge, ACMEDNSChallenge, ACMEALPNChallenge}
|
||||||
if identifier.Type == ACMEIPIdentifier {
|
if identifier.Type == ACMEIPIdentifier {
|
||||||
allowedChallenges = []ACMEChallengeType{ACMEHTTPChallenge}
|
allowedChallenges = []ACMEChallengeType{ACMEHTTPChallenge}
|
||||||
} else if identifier.IsWildcard {
|
} else if identifier.IsWildcard {
|
||||||
|
|
|
@ -184,7 +184,7 @@ func TestAcmeBasicWorkflow(t *testing.T) {
|
||||||
require.False(t, domainAuth.Wildcard, "should not be a wildcard")
|
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.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.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status)
|
||||||
require.True(t, domainAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge")
|
require.True(t, domainAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge")
|
||||||
require.Equal(t, "http-01", domainAuth.Challenges[0].Type)
|
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.True(t, domainAuth.Challenges[1].Validated.IsZero(), "validated time should be 0 on challenge")
|
||||||
require.Equal(t, "dns-01", domainAuth.Challenges[1].Type)
|
require.Equal(t, "dns-01", domainAuth.Challenges[1].Type)
|
||||||
require.NotEmpty(t, domainAuth.Challenges[1].Token, "missing challenge token")
|
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
|
// Test the values for the wildcard authentication
|
||||||
require.Equal(t, acme.StatusPending, wildcardAuth.Status)
|
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.Wildcard, "should be a wildcard")
|
||||||
require.True(t, wildcardAuth.Expires.IsZero(), "authorization should only have expiry set on valid status")
|
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.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status)
|
||||||
require.True(t, wildcardAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge")
|
require.True(t, wildcardAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge")
|
||||||
require.Equal(t, "dns-01", wildcardAuth.Challenges[0].Type)
|
require.Equal(t, "dns-01", wildcardAuth.Challenges[0].Type)
|
||||||
|
@ -1080,7 +1084,7 @@ func TestAcmeValidationError(t *testing.T) {
|
||||||
authorizations = append(authorizations, auth)
|
authorizations = append(authorizations, auth)
|
||||||
}
|
}
|
||||||
require.Len(t, authorizations, 1, "expected a certain number of authorizations")
|
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])
|
acceptedAuth, err := acmeClient.Accept(testCtx, authorizations[0].Challenges[0])
|
||||||
require.NoError(t, err, "Should have been allowed to accept challenge 1")
|
require.NoError(t, err, "Should have been allowed to accept challenge 1")
|
||||||
|
|
3
changelog/20943.txt
Normal file
3
changelog/20943.txt
Normal file
|
@ -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:
|
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.
|
- `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
|
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).
|
with both mechanisms can be added via the [ACME configuration](#set-acme-configuration).
|
||||||
|
|
Loading…
Reference in a new issue