Improve VA TLS-SNI-01 challenge failure error. (#2527)
Previous to this PR the VA's validateTLSWithZName function would return an error message containing the SAN names of the leaf certificate when the validation failed. This commit updates that message to include the Subject Common Name of the leaf cert in addition to the SANs. The names are deduplicated to prevent listing a Subj CN twice if its also a SAN. This will help debug cases where a cert with no SANs is returned by the server. In addition, the number of certificates in the chain received from the server is included in the message. This will hopefully further help users identify misconfiguration since a TLS SNI 01 challenge response should have a chain length of 1. Resolves #2468
This commit is contained in:
parent
01e78fbd1b
commit
3fa950ac58
28
va/va.go
28
va/va.go
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
@ -290,6 +291,19 @@ func (va *ValidationAuthorityImpl) fetchHTTP(ctx context.Context, identifier cor
|
|||
return body, validationRecords, nil
|
||||
}
|
||||
|
||||
// certNames collects up all of a certificate's subject names (Subject CN and
|
||||
// Subject Alternate Names) and reduces them to a unique, sorted set, typically for an
|
||||
// error message
|
||||
func certNames(cert *x509.Certificate) []string {
|
||||
var names []string
|
||||
if cert.Subject.CommonName != "" {
|
||||
names = append(names, cert.Subject.CommonName)
|
||||
}
|
||||
names = append(names, cert.DNSNames...)
|
||||
names = core.UniqueLowerNames(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) validateTLSWithZName(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge, zName string) ([]core.ValidationRecord, *probs.ProblemDetails) {
|
||||
addr, allAddrs, problem := va.getAddr(ctx, identifier.Value)
|
||||
validationRecords := []core.ValidationRecord{
|
||||
|
@ -333,17 +347,21 @@ func (va *ValidationAuthorityImpl) validateTLSWithZName(ctx context.Context, ide
|
|||
va.log.AuditInfo(fmt.Sprintf("TLS-SNI-01 challenge for %s received certificate (%d of %d): cert=[%s]",
|
||||
identifier.Value, i+1, len(certs), hex.EncodeToString(cert.Raw)))
|
||||
}
|
||||
for _, name := range certs[0].DNSNames {
|
||||
leafCert := certs[0]
|
||||
for _, name := range leafCert.DNSNames {
|
||||
if subtle.ConstantTimeCompare([]byte(name), []byte(zName)) == 1 {
|
||||
return validationRecords, nil
|
||||
}
|
||||
}
|
||||
|
||||
names := certNames(leafCert)
|
||||
errText := fmt.Sprintf(
|
||||
"Incorrect validation certificate for TLS-SNI-01 challenge. "+
|
||||
"Requested %s from %s. Received %d certificate(s), "+
|
||||
"first certificate had names %q",
|
||||
zName, hostPort, len(certs), strings.Join(names, ", "))
|
||||
va.log.Info(fmt.Sprintf("Remote host failed to give TLS-01 challenge name. host: %s", identifier))
|
||||
return validationRecords, probs.Unauthorized(
|
||||
fmt.Sprintf("Incorrect validation certificate for TLS-SNI-01 challenge. "+
|
||||
"Requested %s from %s. Received certificate containing '%s'",
|
||||
zName, hostPort, strings.Join(certs[0].DNSNames, ", ")))
|
||||
return validationRecords, probs.Unauthorized(errText)
|
||||
}
|
||||
|
||||
func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, identifier core.AcmeIdentifier, challenge core.Challenge) ([]core.ValidationRecord, *probs.ProblemDetails) {
|
||||
|
|
|
@ -535,6 +535,97 @@ func TestTLSError(t *testing.T) {
|
|||
test.AssertEquals(t, prob.Type, probs.TLSProblem)
|
||||
}
|
||||
|
||||
// misconfiguredTLSSrv is a TLS HTTP test server that returns a certificate
|
||||
// chain with more than one cert, none of which will solve a TLS SNI challenge
|
||||
func misconfiguredTLSSrv() *httptest.Server {
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1337),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(0, 0, 1),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
|
||||
Subject: pkix.Name{
|
||||
CommonName: "hello.world",
|
||||
},
|
||||
DNSNames: []string{"goodbye.world", "hello.world"},
|
||||
}
|
||||
|
||||
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
|
||||
cert := &tls.Certificate{
|
||||
Certificate: [][]byte{certBytes, certBytes},
|
||||
PrivateKey: &TheKey,
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(http.DefaultServeMux)
|
||||
server.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
}
|
||||
server.StartTLS()
|
||||
return server
|
||||
}
|
||||
|
||||
func TestCertNames(t *testing.T) {
|
||||
// We duplicate names inside the SAN set
|
||||
names := []string{
|
||||
"hello.world", "goodbye.world",
|
||||
"hello.world", "goodbye.world",
|
||||
"bonjour.le.monde", "au.revoir.le.monde",
|
||||
"bonjour.le.monde", "au.revoir.le.monde",
|
||||
}
|
||||
// We expect only unique names, in sorted order
|
||||
expected := []string{
|
||||
"au.revoir.le.monde", "bonjour.le.monde",
|
||||
"goodbye.world", "hello.world",
|
||||
}
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1337),
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(0, 0, 1),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
|
||||
Subject: pkix.Name{
|
||||
// We also duplicate a name from the SANs as the CN
|
||||
CommonName: names[0],
|
||||
},
|
||||
DNSNames: names,
|
||||
}
|
||||
|
||||
// Create the certificate, check that certNames provides the expected result
|
||||
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, &TheKey.PublicKey, &TheKey)
|
||||
cert, _ := x509.ParseCertificate(certBytes)
|
||||
actual := certNames(cert)
|
||||
test.AssertDeepEquals(t, actual, expected)
|
||||
}
|
||||
|
||||
// TestSNIErrInvalidChain sets up a TLS server with two certificates, neither of
|
||||
// which validate the SNI challenge.
|
||||
func TestSNIErrInvalidChain(t *testing.T) {
|
||||
chall := createChallenge(core.ChallengeTypeTLSSNI01)
|
||||
hs := misconfiguredTLSSrv()
|
||||
|
||||
port, err := getPort(hs)
|
||||
test.AssertNotError(t, err, "failed to get test server port")
|
||||
va, _, _ := setup()
|
||||
va.tlsPort = port
|
||||
|
||||
// Validate the SNI challenge with the test server, expecting it to fail
|
||||
_, prob := va.validateTLSSNI01(ctx, ident, chall)
|
||||
if prob == nil {
|
||||
t.Fatalf("TLS validation should have failed")
|
||||
}
|
||||
|
||||
// We expect that the error message will say 2 certificates were received, and
|
||||
// we expect the error to contain a deduplicated list of domain names from the
|
||||
// subject CN and SANs of the leaf cert
|
||||
expected := "Received 2 certificate(s), first certificate had names \"goodbye.world, hello.world\""
|
||||
test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
|
||||
test.AssertContains(t, prob.Detail, expected)
|
||||
}
|
||||
|
||||
func TestValidateHTTP(t *testing.T) {
|
||||
chall := core.HTTPChallenge01()
|
||||
setChallengeToken(&chall, core.NewToken())
|
||||
|
|
Loading…
Reference in New Issue