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:
Daniel McCarney 2017-01-27 13:05:42 -05:00 committed by Jacob Hoffman-Andrews
parent 01e78fbd1b
commit 3fa950ac58
2 changed files with 114 additions and 5 deletions

View File

@ -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) {

View File

@ -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())