943 lines
31 KiB
Go
943 lines
31 KiB
Go
package va
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
"github.com/letsencrypt/boulder/bdns"
|
|
"github.com/letsencrypt/boulder/core"
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
"github.com/letsencrypt/boulder/probs"
|
|
"github.com/letsencrypt/boulder/test"
|
|
)
|
|
|
|
// acmeExtension returns the ACME TLS-ALPN-01 extension for the given key
|
|
// authorization. The OID can also be changed for the sake of testing.
|
|
func acmeExtension(oid asn1.ObjectIdentifier, keyAuthorization string) pkix.Extension {
|
|
shasum := sha256.Sum256([]byte(keyAuthorization))
|
|
encHash, _ := asn1.Marshal(shasum[:])
|
|
return pkix.Extension{
|
|
Id: oid,
|
|
Critical: true,
|
|
Value: encHash,
|
|
}
|
|
}
|
|
|
|
// testACMEExt is the ACME TLS-ALPN-01 extension with the default OID and
|
|
// key authorization used in most tests.
|
|
var testACMEExt = acmeExtension(IdPeAcmeIdentifier, expectedKeyAuthorization)
|
|
|
|
// testTLSCert returns a ready-to-use self-signed certificate with the given
|
|
// SANs and Extensions. It generates a new ECDSA key on each call.
|
|
func testTLSCert(names []string, ips []net.IP, extensions []pkix.Extension) *tls.Certificate {
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1337),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"tests"},
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().AddDate(0, 0, 1),
|
|
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
|
|
DNSNames: names,
|
|
IPAddresses: ips,
|
|
ExtraExtensions: extensions,
|
|
}
|
|
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
certBytes, _ := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
|
|
|
return &tls.Certificate{
|
|
Certificate: [][]byte{certBytes},
|
|
PrivateKey: key,
|
|
}
|
|
}
|
|
|
|
// testACMECert returns a certificate with the correctly-formed ACME TLS-ALPN-01
|
|
// extension with our default test values. Use acmeExtension and testCert if you
|
|
// need to customize the contents of that extension.
|
|
func testACMECert(names []string) *tls.Certificate {
|
|
return testTLSCert(names, nil, []pkix.Extension{testACMEExt})
|
|
}
|
|
|
|
// tlsalpn01SrvWithCert creates a test server which will present the given
|
|
// certificate when asked to do a tls-alpn-01 handshake.
|
|
func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion uint16, ipv6 bool) *httptest.Server {
|
|
t.Helper()
|
|
|
|
tlsConfig := &tls.Config{
|
|
Certificates: []tls.Certificate{},
|
|
ClientAuth: tls.NoClientCert,
|
|
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
return acmeCert, nil
|
|
},
|
|
NextProtos: []string{"http/1.1", ACMETLS1Protocol},
|
|
MinVersion: tlsVersion,
|
|
MaxVersion: tlsVersion,
|
|
}
|
|
|
|
hs := httptest.NewUnstartedServer(http.DefaultServeMux)
|
|
hs.TLS = tlsConfig
|
|
hs.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){
|
|
ACMETLS1Protocol: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
|
_ = conn.Close()
|
|
},
|
|
}
|
|
if ipv6 {
|
|
l, err := net.Listen("tcp", "[::1]:0")
|
|
if err != nil {
|
|
panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
|
|
}
|
|
hs.Listener = l
|
|
}
|
|
hs.StartTLS()
|
|
return hs
|
|
}
|
|
|
|
// testTLSALPN01Srv creates a test server with all default values, for tests
|
|
// that don't need to customize specific names or extensions in the certificate
|
|
// served by the TLS server.
|
|
func testTLSALPN01Srv(t *testing.T) *httptest.Server {
|
|
return tlsalpn01SrvWithCert(t, testACMECert([]string{"expected"}), 0, false)
|
|
}
|
|
|
|
func slowTLSSrv() *httptest.Server {
|
|
cert := testTLSCert([]string{"nomatter"}, nil, nil)
|
|
server := httptest.NewUnstartedServer(http.DefaultServeMux)
|
|
server.TLS = &tls.Config{
|
|
NextProtos: []string{"http/1.1", ACMETLS1Protocol},
|
|
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
time.Sleep(100 * time.Millisecond)
|
|
return cert, nil
|
|
},
|
|
}
|
|
server.StartTLS()
|
|
return server
|
|
}
|
|
|
|
func TestTLSALPNTimeoutAfterConnect(t *testing.T) {
|
|
hs := slowTLSSrv()
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
timeout := 50 * time.Millisecond
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
started := time.Now()
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("slow.server"), expectedKeyAuthorization)
|
|
if err == nil {
|
|
t.Fatalf("Validation should've failed")
|
|
}
|
|
// Check that the TLS connection doesn't return before a timeout, and times
|
|
// out after the expected time
|
|
took := time.Since(started)
|
|
// Check that the HTTP connection doesn't return too fast, and times
|
|
// out after the expected time
|
|
if took < timeout/2 {
|
|
t.Fatalf("TLSSNI returned before %s (%s) with %#v", timeout, took, err)
|
|
}
|
|
if took > 2*timeout {
|
|
t.Fatalf("TLSSNI didn't timeout after %s (took %s to return %#v)", timeout,
|
|
took, err)
|
|
}
|
|
if err == nil {
|
|
t.Fatalf("Connection should've timed out")
|
|
}
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
|
|
|
|
expected := "127.0.0.1: Timeout after connect (your server may be slow or overloaded)"
|
|
if prob.Detail != expected {
|
|
t.Errorf("Wrong error detail. Expected %q, got %q", expected, prob.Detail)
|
|
}
|
|
}
|
|
|
|
func TestTLSALPN01DialTimeout(t *testing.T) {
|
|
hs := slowTLSSrv()
|
|
va, _ := setup(hs, "", nil, dnsMockReturnsUnroutable{&bdns.MockClient{}})
|
|
started := time.Now()
|
|
|
|
timeout := 50 * time.Millisecond
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
// The only method I've found so far to trigger a connect timeout is to
|
|
// connect to an unrouteable IP address. This usually generates a connection
|
|
// timeout, but will rarely return "Network unreachable" instead. If we get
|
|
// that, just retry until we get something other than "Network unreachable".
|
|
var err error
|
|
for range 20 {
|
|
_, err = va.validateTLSALPN01(ctx, identifier.NewDNS("unroutable.invalid"), expectedKeyAuthorization)
|
|
if err != nil && strings.Contains(err.Error(), "Network unreachable") {
|
|
continue
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err == nil {
|
|
t.Fatalf("Validation should've failed")
|
|
}
|
|
// Check that the TLS connection doesn't return before a timeout, and times
|
|
// out after the expected time
|
|
took := time.Since(started)
|
|
// Check that the HTTP connection doesn't return too fast, and times
|
|
// out after the expected time
|
|
if took < timeout/2 {
|
|
t.Fatalf("TLSSNI returned before %s (%s) with %#v", timeout, took, err)
|
|
}
|
|
if took > 2*timeout {
|
|
t.Fatalf("TLSSNI didn't timeout after %s", timeout)
|
|
}
|
|
if err == nil {
|
|
t.Fatalf("Connection should've timed out")
|
|
}
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
|
|
expected := "198.51.100.1: Timeout during connect (likely firewall problem)"
|
|
if prob.Detail != expected {
|
|
t.Errorf("Wrong error detail. Expected %q, got %q", expected, prob.Detail)
|
|
}
|
|
}
|
|
|
|
func TestTLSALPN01Refused(t *testing.T) {
|
|
hs := testTLSALPN01Srv(t)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
// Take down validation server and check that validation fails.
|
|
hs.Close()
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
if err == nil {
|
|
t.Fatalf("Server's down; expected refusal. Where did we connect?")
|
|
}
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.ConnectionProblem)
|
|
expected := "127.0.0.1: Connection refused"
|
|
if prob.Detail != expected {
|
|
t.Errorf("Wrong error detail. Expected %q, got %q", expected, prob.Detail)
|
|
}
|
|
}
|
|
|
|
func TestTLSALPN01TalkingToHTTP(t *testing.T) {
|
|
hs := testTLSALPN01Srv(t)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
// Make the server only speak HTTP.
|
|
httpOnly := httpSrv(t, "", false)
|
|
va.tlsPort = getPort(httpOnly)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "TLS-SNI-01 validation passed when talking to a HTTP-only server")
|
|
prob := detailedError(err)
|
|
expected := "Server only speaks HTTP, not TLS"
|
|
if !strings.HasSuffix(prob.String(), expected) {
|
|
t.Errorf("Got wrong error detail. Expected %q, got %q", expected, prob)
|
|
}
|
|
}
|
|
|
|
func brokenTLSSrv() *httptest.Server {
|
|
server := httptest.NewUnstartedServer(http.DefaultServeMux)
|
|
server.TLS = &tls.Config{
|
|
GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
return nil, fmt.Errorf("Failing on purpose")
|
|
},
|
|
}
|
|
server.StartTLS()
|
|
return server
|
|
}
|
|
|
|
func TestTLSError(t *testing.T) {
|
|
hs := brokenTLSSrv()
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
if err == nil {
|
|
t.Fatalf("TLS validation should have failed: What cert was used?")
|
|
}
|
|
prob := detailedError(err)
|
|
if prob.Type != probs.TLSProblem {
|
|
t.Errorf("Wrong problem type: got %s, expected type %s",
|
|
prob, probs.TLSProblem)
|
|
}
|
|
}
|
|
|
|
func TestDNSError(t *testing.T) {
|
|
hs := brokenTLSSrv()
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("always.invalid"), expectedKeyAuthorization)
|
|
if err == nil {
|
|
t.Fatalf("TLS validation should have failed: what IP was used?")
|
|
}
|
|
prob := detailedError(err)
|
|
if prob.Type != probs.DNSProblem {
|
|
t.Errorf("Wrong problem type: got %s, expected type %s",
|
|
prob, probs.DNSProblem)
|
|
}
|
|
}
|
|
|
|
func TestCertNames(t *testing.T) {
|
|
uri, err := url.Parse("ftp://something.else:1234")
|
|
test.AssertNotError(t, err, "failed to parse fake URI")
|
|
|
|
// We duplicate names inside the fields corresponding to the SAN set
|
|
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: "hello.world",
|
|
},
|
|
DNSNames: []string{
|
|
"hello.world", "goodbye.world",
|
|
"hello.world", "goodbye.world",
|
|
"bonjour.le.monde", "au.revoir.le.monde",
|
|
"bonjour.le.monde", "au.revoir.le.monde",
|
|
},
|
|
EmailAddresses: []string{
|
|
"hello@world.gov", "hello@world.gov",
|
|
},
|
|
IPAddresses: []net.IP{
|
|
net.ParseIP("192.168.0.1"), net.ParseIP("192.168.0.1"),
|
|
net.ParseIP("2001:db8::68"), net.ParseIP("2001:db8::68"),
|
|
},
|
|
URIs: []*url.URL{
|
|
uri, uri,
|
|
},
|
|
}
|
|
|
|
// Round-trip the certificate through generation and parsing, to make sure
|
|
// certAltNames can handle "real" certificates and not just templates.
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
test.AssertNotError(t, err, "Error creating test key")
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
|
test.AssertNotError(t, err, "Error creating certificate")
|
|
|
|
cert, err := x509.ParseCertificate(certBytes)
|
|
test.AssertNotError(t, err, "Error parsing certificate")
|
|
|
|
// We expect only unique names, in sorted order.
|
|
expected := []string{
|
|
"192.168.0.1",
|
|
"2001:db8::68",
|
|
"au.revoir.le.monde",
|
|
"bonjour.le.monde",
|
|
"ftp://something.else:1234",
|
|
"goodbye.world",
|
|
"hello.world",
|
|
"hello@world.gov",
|
|
}
|
|
|
|
actual := certAltNames(cert)
|
|
test.AssertDeepEquals(t, actual, expected)
|
|
}
|
|
|
|
func TestTLSALPN01SuccessDNS(t *testing.T) {
|
|
hs := testTLSALPN01Srv(t)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
if err != nil {
|
|
t.Errorf("Validation failed: %v", err)
|
|
}
|
|
test.AssertMetricWithLabelsEquals(
|
|
t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1)
|
|
|
|
hs.Close()
|
|
}
|
|
|
|
func TestTLSALPN01SuccessIPv4(t *testing.T) {
|
|
cert := testTLSCert(nil, []net.IP{net.ParseIP("127.0.0.1")}, []pkix.Extension{testACMEExt})
|
|
hs := tlsalpn01SrvWithCert(t, cert, 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization)
|
|
if err != nil {
|
|
t.Errorf("Validation failed: %v", err)
|
|
}
|
|
test.AssertMetricWithLabelsEquals(
|
|
t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1)
|
|
|
|
hs.Close()
|
|
}
|
|
|
|
func TestTLSALPN01SuccessIPv6(t *testing.T) {
|
|
cert := testTLSCert(nil, []net.IP{net.ParseIP("::1")}, []pkix.Extension{testACMEExt})
|
|
hs := tlsalpn01SrvWithCert(t, cert, 0, true)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("::1")), expectedKeyAuthorization)
|
|
if err != nil {
|
|
t.Errorf("Validation failed: %v", err)
|
|
}
|
|
test.AssertMetricWithLabelsEquals(
|
|
t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1)
|
|
|
|
hs.Close()
|
|
}
|
|
|
|
func TestTLSALPN01ObsoleteFailure(t *testing.T) {
|
|
// NOTE: unfortunately another document claimed the OID we were using in
|
|
// draft-ietf-acme-tls-alpn-01 for their own extension and IANA chose to
|
|
// assign it early. Because of this we had to increment the
|
|
// id-pe-acmeIdentifier OID. We supported this obsolete OID for a long time,
|
|
// but no longer do so.
|
|
// As defined in https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.1
|
|
// id-pe OID + 30 (acmeIdentifier) + 1 (v1)
|
|
IdPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
|
|
|
|
cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifierV1Obsolete, expectedKeyAuthorization)})
|
|
hs := tlsalpn01SrvWithCert(t, cert, 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertNotNil(t, err, "expected validation to fail")
|
|
test.AssertContains(t, err.Error(), "Required extension OID 1.3.6.1.5.5.7.1.31 is not present")
|
|
}
|
|
|
|
func TestValidateTLSALPN01BadChallenge(t *testing.T) {
|
|
badKeyAuthorization := ka("bad token")
|
|
|
|
cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifier, badKeyAuthorization)})
|
|
hs := tlsalpn01SrvWithCert(t, cert, 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
if err == nil {
|
|
t.Fatalf("TLS ALPN validation should have failed.")
|
|
}
|
|
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
|
|
|
|
expectedDigest := sha256.Sum256([]byte(expectedKeyAuthorization))
|
|
badDigest := sha256.Sum256([]byte(badKeyAuthorization))
|
|
|
|
test.AssertContains(t, err.Error(), string(core.ChallengeTypeTLSALPN01))
|
|
test.AssertContains(t, err.Error(), hex.EncodeToString(expectedDigest[:]))
|
|
test.AssertContains(t, err.Error(), hex.EncodeToString(badDigest[:]))
|
|
}
|
|
|
|
func TestValidateTLSALPN01BrokenSrv(t *testing.T) {
|
|
hs := brokenTLSSrv()
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
if err == nil {
|
|
t.Fatalf("TLS ALPN validation should have failed.")
|
|
}
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.TLSProblem)
|
|
}
|
|
|
|
func TestValidateTLSALPN01UnawareSrv(t *testing.T) {
|
|
cert := testTLSCert([]string{"expected"}, nil, nil)
|
|
hs := httptest.NewUnstartedServer(http.DefaultServeMux)
|
|
hs.TLS = &tls.Config{
|
|
Certificates: []tls.Certificate{},
|
|
ClientAuth: tls.NoClientCert,
|
|
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
return cert, nil
|
|
},
|
|
NextProtos: []string{"http/1.1"}, // Doesn't list ACMETLS1Protocol
|
|
}
|
|
hs.StartTLS()
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
if err == nil {
|
|
t.Fatalf("TLS ALPN validation should have failed.")
|
|
}
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.TLSProblem)
|
|
}
|
|
|
|
// TestValidateTLSALPN01MalformedExtnValue tests that validating TLS-ALPN-01
|
|
// against a host that returns a certificate that contains an ASN.1 DER
|
|
// acmeValidation extension value that does not parse or is the wrong length
|
|
// will result in an Unauthorized problem
|
|
func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) {
|
|
wrongTypeDER, _ := asn1.Marshal("a string")
|
|
wrongLengthDER, _ := asn1.Marshal(make([]byte, 31))
|
|
badExtensions := []pkix.Extension{
|
|
{
|
|
Id: IdPeAcmeIdentifier,
|
|
Critical: true,
|
|
Value: wrongTypeDER,
|
|
},
|
|
{
|
|
Id: IdPeAcmeIdentifier,
|
|
Critical: true,
|
|
Value: wrongLengthDER,
|
|
},
|
|
}
|
|
|
|
for _, badExt := range badExtensions {
|
|
acmeCert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{badExt})
|
|
hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false)
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
hs.Close()
|
|
|
|
if err == nil {
|
|
t.Errorf("TLS ALPN validation should have failed for acmeValidation extension %+v.",
|
|
badExt)
|
|
continue
|
|
}
|
|
prob := detailedError(err)
|
|
test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
|
|
test.AssertContains(t, prob.Detail, string(core.ChallengeTypeTLSALPN01))
|
|
test.AssertContains(t, prob.Detail, "malformed acmeValidationV1 extension value")
|
|
}
|
|
}
|
|
|
|
func TestTLSALPN01TLSVersion(t *testing.T) {
|
|
cert := testACMECert([]string{"expected"})
|
|
|
|
for _, tc := range []struct {
|
|
version uint16
|
|
expectError bool
|
|
}{
|
|
{
|
|
version: tls.VersionTLS11,
|
|
expectError: true,
|
|
},
|
|
{
|
|
version: tls.VersionTLS12,
|
|
expectError: false,
|
|
},
|
|
{
|
|
version: tls.VersionTLS13,
|
|
expectError: false,
|
|
},
|
|
} {
|
|
// Create a server that only negotiates the given TLS version
|
|
hs := tlsalpn01SrvWithCert(t, cert, tc.version, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
if !tc.expectError {
|
|
if err != nil {
|
|
t.Errorf("expected success, got: %v", err)
|
|
}
|
|
// The correct TLS-ALPN-01 OID counter should have been incremented
|
|
test.AssertMetricWithLabelsEquals(
|
|
t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 1)
|
|
} else {
|
|
test.AssertNotNil(t, err, "expected validation error")
|
|
test.AssertContains(t, err.Error(), "protocol version not supported")
|
|
test.AssertMetricWithLabelsEquals(
|
|
t, va.metrics.tlsALPNOIDCounter, prometheus.Labels{"oid": IdPeAcmeIdentifier.String()}, 0)
|
|
}
|
|
|
|
hs.Close()
|
|
}
|
|
}
|
|
|
|
func TestTLSALPN01WrongName(t *testing.T) {
|
|
// Create a cert with a different name from what we're validating
|
|
hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"incorrect"}), 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "identifier does not match expected identifier")
|
|
}
|
|
|
|
func TestTLSALPN01WrongIPv4(t *testing.T) {
|
|
// Create a cert with a different IP address from what we're validating
|
|
cert := testTLSCert(nil, []net.IP{net.ParseIP("10.10.10.10")}, []pkix.Extension{testACMEExt})
|
|
hs := tlsalpn01SrvWithCert(t, cert, 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "identifier does not match expected identifier")
|
|
}
|
|
|
|
func TestTLSALPN01WrongIPv6(t *testing.T) {
|
|
// Create a cert with a different IP address from what we're validating
|
|
cert := testTLSCert(nil, []net.IP{net.ParseIP("::2")}, []pkix.Extension{testACMEExt})
|
|
hs := tlsalpn01SrvWithCert(t, cert, 0, true)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("::1")), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "identifier does not match expected identifier")
|
|
}
|
|
|
|
func TestTLSALPN01ExtraNames(t *testing.T) {
|
|
// Create a cert with two names when we only want to validate one.
|
|
hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"expected", "extra"}), 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "wrong number of identifiers")
|
|
}
|
|
|
|
func TestTLSALPN01WrongIdentType(t *testing.T) {
|
|
// Create a cert with an IP address encoded as a name.
|
|
hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"127.0.0.1"}), 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "wrong number of identifiers")
|
|
}
|
|
|
|
func TestTLSALPN01TooManyIdentTypes(t *testing.T) {
|
|
// Create a cert with both a name and an IP address when we only want to validate one.
|
|
hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, []net.IP{net.ParseIP("127.0.0.1")}, []pkix.Extension{testACMEExt}), 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "wrong number of identifiers")
|
|
|
|
_, err = va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "wrong number of identifiers")
|
|
}
|
|
|
|
func TestTLSALPN01NotSelfSigned(t *testing.T) {
|
|
// Create a normal-looking cert. We don't use testTLSCert because we need to
|
|
// control the issuer.
|
|
eeTemplate := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1337),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"tests"},
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().AddDate(0, 0, 1),
|
|
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
|
|
DNSNames: []string{"expected"},
|
|
IPAddresses: []net.IP{net.ParseIP("192.168.0.1")},
|
|
ExtraExtensions: []pkix.Extension{testACMEExt},
|
|
}
|
|
|
|
eeKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
test.AssertNotError(t, err, "creating test key")
|
|
|
|
issuerCert := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1234),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"testissuer"},
|
|
},
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
issuerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
test.AssertNotError(t, err, "creating test key")
|
|
|
|
// Test that a cert with mismatched subject and issuer fields is rejected,
|
|
// even though its signature is produced with the right (self-signed) key.
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, eeTemplate, issuerCert, eeKey.Public(), eeKey)
|
|
test.AssertNotError(t, err, "failed to create acme-tls/1 cert")
|
|
|
|
acmeCert := &tls.Certificate{
|
|
Certificate: [][]byte{certBytes},
|
|
PrivateKey: eeKey,
|
|
}
|
|
|
|
hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "not self-signed")
|
|
|
|
// Test that a cert whose signature was produced by some other key is rejected,
|
|
// even though its subject and issuer fields claim that it is self-signed.
|
|
certBytes, err = x509.CreateCertificate(rand.Reader, eeTemplate, eeTemplate, eeKey.Public(), issuerKey)
|
|
test.AssertNotError(t, err, "failed to create acme-tls/1 cert")
|
|
|
|
acmeCert = &tls.Certificate{
|
|
Certificate: [][]byte{certBytes},
|
|
PrivateKey: eeKey,
|
|
}
|
|
|
|
hs = tlsalpn01SrvWithCert(t, acmeCert, 0, false)
|
|
|
|
va, _ = setup(hs, "", nil, nil)
|
|
|
|
_, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "not self-signed")
|
|
}
|
|
|
|
func TestTLSALPN01ExtraIdentifiers(t *testing.T) {
|
|
// Create a cert with an extra non-dnsName identifier. We don't use testTLSCert
|
|
// because we need to set the IPAddresses field.
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1337),
|
|
Subject: pkix.Name{
|
|
Organization: []string{"tests"},
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().AddDate(0, 0, 1),
|
|
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
BasicConstraintsValid: true,
|
|
|
|
DNSNames: []string{"expected"},
|
|
IPAddresses: []net.IP{net.ParseIP("192.168.0.1")},
|
|
ExtraExtensions: []pkix.Extension{testACMEExt},
|
|
}
|
|
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
test.AssertNotError(t, err, "creating test key")
|
|
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
|
|
test.AssertNotError(t, err, "failed to create acme-tls/1 cert")
|
|
|
|
acmeCert := &tls.Certificate{
|
|
Certificate: [][]byte{certBytes},
|
|
PrivateKey: key,
|
|
}
|
|
|
|
hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
test.AssertContains(t, err.Error(), "Received certificate with unexpected identifiers")
|
|
}
|
|
|
|
func TestTLSALPN01ExtraSANs(t *testing.T) {
|
|
// Create a cert with multiple SAN extensions
|
|
sanValue, err := asn1.Marshal([]asn1.RawValue{
|
|
{Tag: 2, Class: 2, Bytes: []byte(`expected`)},
|
|
})
|
|
test.AssertNotError(t, err, "failed to marshal test SAN")
|
|
|
|
subjectAltName := pkix.Extension{
|
|
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
|
|
Critical: false,
|
|
Value: sanValue,
|
|
}
|
|
|
|
extensions := []pkix.Extension{testACMEExt, subjectAltName, subjectAltName}
|
|
hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, nil, extensions), 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
// In go >= 1.19, the TLS client library detects that the certificate has
|
|
// a duplicate extension and terminates the connection itself.
|
|
prob := detailedError(err)
|
|
test.AssertContains(t, prob.String(), "Error getting validation data")
|
|
}
|
|
|
|
func TestTLSALPN01ExtraAcmeExtensions(t *testing.T) {
|
|
// Create a cert with multiple SAN extensions
|
|
extensions := []pkix.Extension{testACMEExt, testACMEExt}
|
|
hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, nil, extensions), 0, false)
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
|
|
test.AssertError(t, err, "validation should have failed")
|
|
// In go >= 1.19, the TLS client library detects that the certificate has
|
|
// a duplicate extension and terminates the connection itself.
|
|
prob := detailedError(err)
|
|
test.AssertContains(t, prob.String(), "Error getting validation data")
|
|
}
|
|
|
|
func TestAcceptableExtensions(t *testing.T) {
|
|
requireAcmeAndSAN := []asn1.ObjectIdentifier{
|
|
IdPeAcmeIdentifier,
|
|
IdCeSubjectAltName,
|
|
}
|
|
|
|
sanValue, err := asn1.Marshal([]asn1.RawValue{
|
|
{Tag: 2, Class: 2, Bytes: []byte(`expected`)},
|
|
})
|
|
test.AssertNotError(t, err, "failed to marshal test SAN")
|
|
subjectAltName := pkix.Extension{
|
|
Id: asn1.ObjectIdentifier{2, 5, 29, 17},
|
|
Critical: false,
|
|
Value: sanValue,
|
|
}
|
|
|
|
acmeExtension := pkix.Extension{
|
|
Id: IdPeAcmeIdentifier,
|
|
Critical: true,
|
|
Value: []byte{},
|
|
}
|
|
|
|
weirdExt := pkix.Extension{
|
|
Id: asn1.ObjectIdentifier{99, 99, 99, 99},
|
|
Critical: false,
|
|
Value: []byte(`because I'm tacky`),
|
|
}
|
|
|
|
doubleAcmeExts := []pkix.Extension{subjectAltName, acmeExtension, acmeExtension}
|
|
err = checkAcceptableExtensions(doubleAcmeExts, requireAcmeAndSAN)
|
|
test.AssertError(t, err, "Two ACME extensions isn't okay")
|
|
|
|
doubleSANExts := []pkix.Extension{subjectAltName, subjectAltName, acmeExtension}
|
|
err = checkAcceptableExtensions(doubleSANExts, requireAcmeAndSAN)
|
|
test.AssertError(t, err, "Two SAN extensions isn't okay")
|
|
|
|
onlyUnexpectedExt := []pkix.Extension{weirdExt}
|
|
err = checkAcceptableExtensions(onlyUnexpectedExt, requireAcmeAndSAN)
|
|
test.AssertError(t, err, "Missing required extensions")
|
|
test.AssertContains(t, err.Error(), "Required extension OID 1.3.6.1.5.5.7.1.31 is not present")
|
|
|
|
okayExts := []pkix.Extension{acmeExtension, subjectAltName}
|
|
err = checkAcceptableExtensions(okayExts, requireAcmeAndSAN)
|
|
test.AssertNotError(t, err, "Correct type and number of extensions")
|
|
|
|
okayWithUnexpectedExt := []pkix.Extension{weirdExt, acmeExtension, subjectAltName}
|
|
err = checkAcceptableExtensions(okayWithUnexpectedExt, requireAcmeAndSAN)
|
|
test.AssertNotError(t, err, "Correct type and number of extensions")
|
|
}
|
|
|
|
func TestTLSALPN01BadIdentifier(t *testing.T) {
|
|
hs := httpSrv(t, expectedToken, false)
|
|
defer hs.Close()
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
_, err := va.validateTLSALPN01(ctx, identifier.ACMEIdentifier{Type: "smime", Value: "dobber@bad.horse"}, expectedKeyAuthorization)
|
|
test.AssertError(t, err, "Server accepted a hypothetical S/MIME identifier")
|
|
prob := detailedError(err)
|
|
test.AssertContains(t, prob.String(), "Identifier type for TLS-ALPN-01 challenge was not DNS or IP")
|
|
}
|
|
|
|
// TestTLSALPN01ServerName tests compliance with RFC 8737, Sec. 3 (step 3) & RFC
|
|
// 8738, Sec. 6.
|
|
func TestTLSALPN01ServerName(t *testing.T) {
|
|
testCases := []struct {
|
|
Name string
|
|
Ident identifier.ACMEIdentifier
|
|
CertNames []string
|
|
CertIPs []net.IP
|
|
IPv6 bool
|
|
want string
|
|
}{
|
|
{
|
|
Name: "DNS name",
|
|
Ident: identifier.NewDNS("example.com"),
|
|
CertNames: []string{"example.com"},
|
|
want: "example.com",
|
|
},
|
|
{
|
|
// RFC 8738, Sec. 6.
|
|
Name: "IPv4 address",
|
|
Ident: identifier.NewIP(netip.MustParseAddr("127.0.0.1")),
|
|
CertIPs: []net.IP{net.ParseIP("127.0.0.1")},
|
|
want: "1.0.0.127.in-addr.arpa",
|
|
},
|
|
{
|
|
// RFC 8738, Sec. 6.
|
|
Name: "IPv6 address",
|
|
Ident: identifier.NewIP(netip.MustParseAddr("::1")),
|
|
CertIPs: []net.IP{net.ParseIP("::1")},
|
|
IPv6: true,
|
|
want: "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500)
|
|
defer cancel()
|
|
|
|
tlsConfig := &tls.Config{
|
|
Certificates: []tls.Certificate{},
|
|
ClientAuth: tls.NoClientCert,
|
|
NextProtos: []string{"http/1.1", ACMETLS1Protocol},
|
|
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
got := clientHello.ServerName
|
|
if got != tc.want {
|
|
return nil, fmt.Errorf("Got host %#v, but want %#v", got, tc.want)
|
|
}
|
|
return testTLSCert(tc.CertNames, tc.CertIPs, []pkix.Extension{testACMEExt}), nil
|
|
},
|
|
}
|
|
|
|
hs := httptest.NewUnstartedServer(http.DefaultServeMux)
|
|
hs.TLS = tlsConfig
|
|
hs.Config.TLSNextProto = map[string]func(*http.Server, *tls.Conn, http.Handler){
|
|
ACMETLS1Protocol: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
|
|
_ = conn.Close()
|
|
},
|
|
}
|
|
if tc.IPv6 {
|
|
l, err := net.Listen("tcp", "[::1]:0")
|
|
if err != nil {
|
|
panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
|
|
}
|
|
hs.Listener = l
|
|
}
|
|
hs.StartTLS()
|
|
defer hs.Close()
|
|
|
|
va, _ := setup(hs, "", nil, nil)
|
|
|
|
// The actual test happens in the tlsConfig.GetCertificate function,
|
|
// which the validation will call and depend on for its success.
|
|
_, err := va.validateTLSALPN01(ctx, tc.Ident, expectedKeyAuthorization)
|
|
if err != nil {
|
|
t.Errorf("Validation failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|