va: Support IP address identifiers (#8020)

Add an `identifier` field to the `va.PerformValidationRequest` proto, which will soon replace its `dnsName` field.

Accept and prefer the `identifier` field in every VA function that uses this struct. Don't (yet) assume it will be present.

Throughout the VA, accept and handle the IP address identifier type. Handling is similar to DNS names, except that `getAddrs` is not called, and consider that:
- IPs are represented in a different field in the `x509.Certificate` struct.
- IPs must be presented as reverse DNS (`.arpa`) names in SNI for [TLS-ALPN-01 challenge requests](https://datatracker.ietf.org/doc/html/rfc8738#name-tls-with-application-layer-).
- IPv6 addresses are enclosed in square brackets when composing or parsing URLs.

For HTTP-01 challenges, accept redirects to bare IP addresses, which were previously rejected.

Fixes #2706
Part of #7311
This commit is contained in:
James Renken 2025-03-06 11:39:22 -08:00 committed by GitHub
parent 5822ba3c20
commit 3e6a8e2d25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 990 additions and 526 deletions

View File

@ -34,120 +34,71 @@ func parseCidr(network string, comment string) net.IPNet {
} }
var ( var (
// Private CIDRs to ignore // TODO(#8040): Rebuild these as structs that track the structure of IANA's
privateNetworks = []net.IPNet{ // CSV files, for better automated handling.
// RFC1918 //
// 10.0.0.0/8 // Private CIDRs to ignore. Sourced from:
{ // https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
IP: []byte{10, 0, 0, 0}, privateV4Networks = []net.IPNet{
Mask: []byte{255, 0, 0, 0}, parseCidr("0.0.0.0/8", "RFC 791, Section 3.2: This network"),
}, parseCidr("0.0.0.0/32", "RFC 1122, Section 3.2.1.3: This host on this network"),
// 172.16.0.0/12 parseCidr("10.0.0.0/8", "RFC 1918: Private-Use"),
{ parseCidr("100.64.0.0/10", "RFC 6598: Shared Address Space"),
IP: []byte{172, 16, 0, 0}, parseCidr("127.0.0.0/8", "RFC 1122, Section 3.2.1.3: Loopback"),
Mask: []byte{255, 240, 0, 0}, parseCidr("169.254.0.0/16", "RFC 3927: Link Local"),
}, parseCidr("172.16.0.0/12", "RFC 1918: Private-Use"),
// 192.168.0.0/16 parseCidr("192.0.0.0/24", "RFC 6890, Section 2.1: IETF Protocol Assignments"),
{ parseCidr("192.0.0.0/29", "RFC 7335: IPv4 Service Continuity Prefix"),
IP: []byte{192, 168, 0, 0}, parseCidr("192.0.0.8/32", "RFC 7600: IPv4 dummy address"),
Mask: []byte{255, 255, 0, 0}, parseCidr("192.0.0.9/32", "RFC 7723: Port Control Protocol Anycast"),
}, parseCidr("192.0.0.10/32", "RFC 8155: Traversal Using Relays around NAT Anycast"),
// RFC5735 parseCidr("192.0.0.170/32", "RFC 8880 & RFC 7050, Section 2.2: NAT64/DNS64 Discovery"),
// 127.0.0.0/8 parseCidr("192.0.0.171/32", "RFC 8880 & RFC 7050, Section 2.2: NAT64/DNS64 Discovery"),
{ parseCidr("192.0.2.0/24", "RFC 5737: Documentation (TEST-NET-1)"),
IP: []byte{127, 0, 0, 0}, parseCidr("192.31.196.0/24", "RFC 7535: AS112-v4"),
Mask: []byte{255, 0, 0, 0}, parseCidr("192.52.193.0/24", "RFC 7450: AMT"),
}, parseCidr("192.88.99.0/24", "RFC 7526: Deprecated (6to4 Relay Anycast)"),
// RFC1122 Section 3.2.1.3 parseCidr("192.168.0.0/16", "RFC 1918: Private-Use"),
// 0.0.0.0/8 parseCidr("192.175.48.0/24", "RFC 7534: Direct Delegation AS112 Service"),
{ parseCidr("198.18.0.0/15", "RFC 2544: Benchmarking"),
IP: []byte{0, 0, 0, 0}, parseCidr("198.51.100.0/24", "RFC 5737: Documentation (TEST-NET-2)"),
Mask: []byte{255, 0, 0, 0}, parseCidr("203.0.113.0/24", "RFC 5737: Documentation (TEST-NET-3)"),
}, parseCidr("240.0.0.0/4", "RFC1112, Section 4: Reserved"),
// RFC3927 parseCidr("255.255.255.255/32", "RFC 8190 & RFC 919, Section 7: Limited Broadcast"),
// 169.254.0.0/16 // 224.0.0.0/4 are multicast addresses as per RFC 3171. They are not
{ // present in the IANA registry.
IP: []byte{169, 254, 0, 0}, parseCidr("224.0.0.0/4", "RFC 3171: Multicast Addresses"),
Mask: []byte{255, 255, 0, 0},
},
// RFC 5736
// 192.0.0.0/24
{
IP: []byte{192, 0, 0, 0},
Mask: []byte{255, 255, 255, 0},
},
// RFC 5737
// 192.0.2.0/24
{
IP: []byte{192, 0, 2, 0},
Mask: []byte{255, 255, 255, 0},
},
// 198.51.100.0/24
{
IP: []byte{198, 51, 100, 0},
Mask: []byte{255, 255, 255, 0},
},
// 203.0.113.0/24
{
IP: []byte{203, 0, 113, 0},
Mask: []byte{255, 255, 255, 0},
},
// RFC 3068
// 192.88.99.0/24
{
IP: []byte{192, 88, 99, 0},
Mask: []byte{255, 255, 255, 0},
},
// RFC 2544, Errata 423
// 198.18.0.0/15
{
IP: []byte{198, 18, 0, 0},
Mask: []byte{255, 254, 0, 0},
},
// RFC 3171
// 224.0.0.0/4
{
IP: []byte{224, 0, 0, 0},
Mask: []byte{240, 0, 0, 0},
},
// RFC 1112
// 240.0.0.0/4
{
IP: []byte{240, 0, 0, 0},
Mask: []byte{240, 0, 0, 0},
},
// RFC 919 Section 7
// 255.255.255.255/32
{
IP: []byte{255, 255, 255, 255},
Mask: []byte{255, 255, 255, 255},
},
// RFC 6598
// 100.64.0.0/10
{
IP: []byte{100, 64, 0, 0},
Mask: []byte{255, 192, 0, 0},
},
} }
// Sourced from https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml // Sourced from:
// where Global, Source, or Destination is False // https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
privateV6Networks = []net.IPNet{ privateV6Networks = []net.IPNet{
parseCidr("::/128", "RFC 4291: Unspecified Address"), parseCidr("::/128", "RFC 4291: Unspecified Address"),
parseCidr("::1/128", "RFC 4291: Loopback Address"), parseCidr("::1/128", "RFC 4291: Loopback Address"),
parseCidr("::ffff:0:0/96", "RFC 4291: IPv4-mapped Address"), parseCidr("::ffff:0:0/96", "RFC 4291: IPv4-mapped Address"),
parseCidr("100::/64", "RFC 6666: Discard Address Block"), parseCidr("64:ff9b::/96", "RFC 6052: IPv4-IPv6 Translat."),
parseCidr("64:ff9b:1::/48", "RFC 8215: IPv4-IPv6 Translat."),
parseCidr("100::/64", "RFC 6666: Discard-Only Address Block"),
parseCidr("2001::/23", "RFC 2928: IETF Protocol Assignments"), parseCidr("2001::/23", "RFC 2928: IETF Protocol Assignments"),
parseCidr("2001:2::/48", "RFC 5180: Benchmarking"), parseCidr("2001::/32", "RFC 4380 & RFC 8190: TEREDO"),
parseCidr("2001:1::1/128", "RFC 7723: Port Control Protocol Anycast"),
parseCidr("2001:1::2/128", "RFC 8155: Traversal Using Relays around NAT Anycast"),
parseCidr("2001:1::3/128", "RFC-ietf-dnssd-srp-25: DNS-SD Service Registration Protocol Anycast"),
parseCidr("2001:2::/48", "RFC 5180 & RFC Errata 1752: Benchmarking"),
parseCidr("2001:3::/32", "RFC 7450: AMT"),
parseCidr("2001:4:112::/48", "RFC 7535: AS112-v6"),
parseCidr("2001:10::/28", "RFC 4843: Deprecated (previously ORCHID)"),
parseCidr("2001:20::/28", "RFC 7343: ORCHIDv2"),
parseCidr("2001:30::/28", "RFC 9374: Drone Remote ID Protocol Entity Tags (DETs) Prefix"),
parseCidr("2001:db8::/32", "RFC 3849: Documentation"), parseCidr("2001:db8::/32", "RFC 3849: Documentation"),
parseCidr("2001::/32", "RFC 4380: TEREDO"), parseCidr("2002::/16", "RFC 3056: 6to4"),
parseCidr("fc00::/7", "RFC 4193: Unique-Local"), parseCidr("2620:4f:8000::/48", "RFC 7534: Direct Delegation AS112 Service"),
parseCidr("fe80::/10", "RFC 4291: Section 2.5.6 Link-Scoped Unicast"), parseCidr("3fff::/20", "RFC 9637: Documentation"),
parseCidr("ff00::/8", "RFC 4291: Section 2.7"), parseCidr("5f00::/16", "RFC 9602: Segment Routing (SRv6) SIDs"),
// We disable validations to IPs under the 6to4 anycast prefix because parseCidr("fc00::/7", "RFC 4193 & RFC 8190: Unique-Local"),
// there's too much risk of a malicious actor advertising the prefix and parseCidr("fe80::/10", "RFC 4291: Link-Local Unicast"),
// answering validations for a 6to4 host they do not control. // ff00::/8 are multicast addresses as per RFC 4291, Sections 2.4 & 2.7.
// https://community.letsencrypt.org/t/problems-validating-ipv6-against-host-running-6to4/18312/9 // They are not present in the IANA registry.
parseCidr("2002::/16", "RFC 7526: 6to4 anycast prefix deprecated"), parseCidr("ff00::/8", "RFC 4291: Multicast Addresses"),
} }
) )
@ -480,7 +431,7 @@ func (dnsClient *impl) LookupTXT(ctx context.Context, hostname string) ([]string
} }
func isPrivateV4(ip net.IP) bool { func isPrivateV4(ip net.IP) bool {
for _, net := range privateNetworks { for _, net := range privateV4Networks {
if net.Contains(ip) { if net.Contains(ip) {
return true return true
} }
@ -729,3 +680,20 @@ func (d *dohExchanger) Exchange(query *dns.Msg, server string) (*dns.Msg, time.D
return response, d.clk.Since(start), nil return response, d.clk.Since(start), nil
} }
// IsReservedIP reports whether an IP address is part of a reserved range.
//
// TODO(#7311): Once we're fully ready to issue for IP address identifiers, dev
// environments should have a way to bypass this check for their own Private-Use
// IP addresses. Maybe plumb the DNSAllowLoopbackAddresses feature flag through
// to here.
//
// TODO(#8040): Move this and its dependencies into the policy package. As part
// of this, consider changing it to return an error and/or the description of
// the reserved network.
func IsReservedIP(ip net.IP) bool {
if ip.To4() == nil {
return isPrivateV6(ip)
}
return isPrivateV4(ip)
}

View File

@ -149,7 +149,8 @@ func main() {
logger, logger,
c.VA.AccountURIPrefixes, c.VA.AccountURIPrefixes,
va.PrimaryPerspective, va.PrimaryPerspective,
"") "",
bdns.IsReservedIP)
cmd.FailOnError(err, "Unable to create VA server") cmd.FailOnError(err, "Unable to create VA server")
start, err := bgrpc.NewServer(c.VA.GRPC, logger).Add( start, err := bgrpc.NewServer(c.VA.GRPC, logger).Add(

View File

@ -138,7 +138,8 @@ func main() {
logger, logger,
c.RVA.AccountURIPrefixes, c.RVA.AccountURIPrefixes,
c.RVA.Perspective, c.RVA.Perspective,
c.RVA.RIR) c.RVA.RIR,
bdns.IsReservedIP)
cmd.FailOnError(err, "Unable to create Remote-VA server") cmd.FailOnError(err, "Unable to create Remote-VA server")
start, err := bgrpc.NewServer(c.RVA.GRPC, logger).Add( start, err := bgrpc.NewServer(c.RVA.GRPC, logger).Add(

View File

@ -122,6 +122,8 @@ type ValidationRecord struct {
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
// Shared // Shared
//
// TODO(#7311): Replace DnsName with Identifier.
DnsName string `json:"hostname,omitempty"` DnsName string `json:"hostname,omitempty"`
Port string `json:"port,omitempty"` Port string `json:"port,omitempty"`
AddressesResolved []net.IP `json:"addressesResolved,omitempty"` AddressesResolved []net.IP `json:"addressesResolved,omitempty"`

View File

@ -39,6 +39,22 @@ func (i ACMEIdentifier) AsProto() *corepb.Identifier {
} }
} }
func FromProto(ident *corepb.Identifier) ACMEIdentifier {
return ACMEIdentifier{
Type: IdentifierType(ident.Type),
Value: ident.Value,
}
}
// FromProtoWithDefault can be removed after DnsNames are no longer used in
// RPCs. TODO(#8023)
func FromProtoWithDefault(ident *corepb.Identifier, name string) ACMEIdentifier {
if ident == nil {
return NewDNS(name)
}
return FromProto(ident)
}
// NewDNS is a convenience function for creating an ACMEIdentifier with Type // NewDNS is a convenience function for creating an ACMEIdentifier with Type
// "dns" for a given domain name. // "dns" for a given domain name.
func NewDNS(domain string) ACMEIdentifier { func NewDNS(domain string) ACMEIdentifier {
@ -52,7 +68,10 @@ func NewDNS(domain string) ACMEIdentifier {
// for a given IP address. // for a given IP address.
func NewIP(ip netip.Addr) ACMEIdentifier { func NewIP(ip netip.Addr) ACMEIdentifier {
return ACMEIdentifier{ return ACMEIdentifier{
Type: TypeIP, Type: TypeIP,
Value: ip.StringExpanded(), // RFC 8738, Sec. 3: The identifier value MUST contain the textual form
// of the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC
// 5952, Sec. 4 for IPv6.
Value: ip.String(),
} }
} }

View File

@ -41,7 +41,7 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal
logEvent := validationLogEvent{ logEvent := validationLogEvent{
AuthzID: req.AuthzID, AuthzID: req.AuthzID,
Requester: req.AccountURIID, Requester: req.AccountURIID,
Identifier: req.Domain, Identifier: identifier.NewDNS(req.Domain),
} }
challType := core.AcmeChallenge(req.ValidationMethod) challType := core.AcmeChallenge(req.ValidationMethod)

View File

@ -1122,18 +1122,18 @@ func TestMultiCAARechecking(t *testing.T) {
} }
func TestCAAFailure(t *testing.T) { func TestCAAFailure(t *testing.T) {
hs := httpSrv(t, expectedToken) hs := httpSrv(t, expectedToken, false)
defer hs.Close() defer hs.Close()
va, _ := setup(hs, "", nil, caaMockDNS{}) va, _ := setup(hs, "", nil, caaMockDNS{})
err := va.checkCAA(ctx, dnsi("reserved.com"), &caaParams{1, core.ChallengeTypeHTTP01}) err := va.checkCAA(ctx, identifier.NewDNS("reserved.com"), &caaParams{1, core.ChallengeTypeHTTP01})
if err == nil { if err == nil {
t.Fatalf("Expected CAA rejection for reserved.com, got success") t.Fatalf("Expected CAA rejection for reserved.com, got success")
} }
test.AssertErrorIs(t, err, berrors.CAA) test.AssertErrorIs(t, err, berrors.CAA)
err = va.checkCAA(ctx, dnsi("example.gonetld"), &caaParams{1, core.ChallengeTypeHTTP01}) err = va.checkCAA(ctx, identifier.NewDNS("example.gonetld"), &caaParams{1, core.ChallengeTypeHTTP01})
if err == nil { if err == nil {
t.Fatalf("Expected CAA rejection for gonetld, got success") t.Fatalf("Expected CAA rejection for gonetld, got success")
} }

View File

@ -51,7 +51,7 @@ func availableAddresses(allAddrs []net.IP) (v4 []net.IP, v6 []net.IP) {
func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { func (va *ValidationAuthorityImpl) validateDNS01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) {
if ident.Type != identifier.TypeDNS { if ident.Type != identifier.TypeDNS {
va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident) va.log.Infof("Identifier type for DNS challenge was not DNS: %s", ident)
return nil, berrors.MalformedError("Identifier type for DNS was not itself DNS") return nil, berrors.MalformedError("Identifier type for DNS challenge was not DNS")
} }
// Compute the digest of the key authorization file // Compute the digest of the key authorization file

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"net/netip"
"testing" "testing"
"time" "time"
@ -18,7 +19,7 @@ import (
func TestDNSValidationWrong(t *testing.T) { func TestDNSValidationWrong(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, err := va.validateDNS01(context.Background(), dnsi("wrong-dns01.com"), expectedKeyAuthorization) _, err := va.validateDNS01(context.Background(), identifier.NewDNS("wrong-dns01.com"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("Successful DNS validation with wrong TXT record") t.Fatalf("Successful DNS validation with wrong TXT record")
} }
@ -29,7 +30,7 @@ func TestDNSValidationWrong(t *testing.T) {
func TestDNSValidationWrongMany(t *testing.T) { func TestDNSValidationWrongMany(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, err := va.validateDNS01(context.Background(), dnsi("wrong-many-dns01.com"), expectedKeyAuthorization) _, err := va.validateDNS01(context.Background(), identifier.NewDNS("wrong-many-dns01.com"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("Successful DNS validation with wrong TXT record") t.Fatalf("Successful DNS validation with wrong TXT record")
} }
@ -40,7 +41,7 @@ func TestDNSValidationWrongMany(t *testing.T) {
func TestDNSValidationWrongLong(t *testing.T) { func TestDNSValidationWrongLong(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, err := va.validateDNS01(context.Background(), dnsi("long-dns01.com"), expectedKeyAuthorization) _, err := va.validateDNS01(context.Background(), identifier.NewDNS("long-dns01.com"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("Successful DNS validation with wrong TXT record") t.Fatalf("Successful DNS validation with wrong TXT record")
} }
@ -51,12 +52,21 @@ func TestDNSValidationWrongLong(t *testing.T) {
func TestDNSValidationFailure(t *testing.T) { func TestDNSValidationFailure(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, err := va.validateDNS01(ctx, dnsi("localhost"), expectedKeyAuthorization) _, err := va.validateDNS01(ctx, identifier.NewDNS("localhost"), expectedKeyAuthorization)
prob := detailedError(err) prob := detailedError(err)
test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem)
} }
func TestDNSValidationIP(t *testing.T) {
va, _ := setup(nil, "", nil, nil)
_, err := va.validateDNS01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization)
prob := detailedError(err)
test.AssertEquals(t, prob.Type, probs.MalformedProblem)
}
func TestDNSValidationInvalid(t *testing.T) { func TestDNSValidationInvalid(t *testing.T) {
var notDNS = identifier.ACMEIdentifier{ var notDNS = identifier.ACMEIdentifier{
Type: identifier.IdentifierType("iris"), Type: identifier.IdentifierType("iris"),
@ -74,7 +84,7 @@ func TestDNSValidationInvalid(t *testing.T) {
func TestDNSValidationServFail(t *testing.T) { func TestDNSValidationServFail(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, err := va.validateDNS01(ctx, dnsi("servfail.com"), expectedKeyAuthorization) _, err := va.validateDNS01(ctx, identifier.NewDNS("servfail.com"), expectedKeyAuthorization)
prob := detailedError(err) prob := detailedError(err)
test.AssertEquals(t, prob.Type, probs.DNSProblem) test.AssertEquals(t, prob.Type, probs.DNSProblem)
@ -94,7 +104,7 @@ func TestDNSValidationNoServer(t *testing.T) {
log, log,
nil) nil)
_, err = va.validateDNS01(ctx, dnsi("localhost"), expectedKeyAuthorization) _, err = va.validateDNS01(ctx, identifier.NewDNS("localhost"), expectedKeyAuthorization)
prob := detailedError(err) prob := detailedError(err)
test.AssertEquals(t, prob.Type, probs.DNSProblem) test.AssertEquals(t, prob.Type, probs.DNSProblem)
} }
@ -102,7 +112,7 @@ func TestDNSValidationNoServer(t *testing.T) {
func TestDNSValidationOK(t *testing.T) { func TestDNSValidationOK(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, prob := va.validateDNS01(ctx, dnsi("good-dns01.com"), expectedKeyAuthorization) _, prob := va.validateDNS01(ctx, identifier.NewDNS("good-dns01.com"), expectedKeyAuthorization)
test.Assert(t, prob == nil, "Should be valid.") test.Assert(t, prob == nil, "Should be valid.")
} }
@ -110,7 +120,7 @@ func TestDNSValidationOK(t *testing.T) {
func TestDNSValidationNoAuthorityOK(t *testing.T) { func TestDNSValidationNoAuthorityOK(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, prob := va.validateDNS01(ctx, dnsi("no-authority-dns01.com"), expectedKeyAuthorization) _, prob := va.validateDNS01(ctx, identifier.NewDNS("no-authority-dns01.com"), expectedKeyAuthorization)
test.Assert(t, prob == nil, "Should be valid.") test.Assert(t, prob == nil, "Should be valid.")
} }

View File

@ -8,6 +8,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@ -159,7 +160,7 @@ func httpTransport(df dialerFunc) *http.Transport {
// httpValidationTarget bundles all of the information needed to make an HTTP-01 // httpValidationTarget bundles all of the information needed to make an HTTP-01
// validation request against a target. // validation request against a target.
type httpValidationTarget struct { type httpValidationTarget struct {
// the hostname being validated // the host being validated
host string host string
// the port for the validation request // the port for the validation request
port int port int
@ -203,18 +204,28 @@ func (vt *httpValidationTarget) nextIP() error {
// lookups fail. // lookups fail.
func (va *ValidationAuthorityImpl) newHTTPValidationTarget( func (va *ValidationAuthorityImpl) newHTTPValidationTarget(
ctx context.Context, ctx context.Context,
host string, ident identifier.ACMEIdentifier,
port int, port int,
path string, path string,
query string) (*httpValidationTarget, error) { query string) (*httpValidationTarget, error) {
// Resolve IP addresses for the hostname var addrs []net.IP
addrs, resolvers, err := va.getAddrs(ctx, host) var resolvers bdns.ResolverAddrs
if err != nil { switch ident.Type {
return nil, err case identifier.TypeDNS:
// Resolve IP addresses for the identifier
dnsAddrs, dnsResolvers, err := va.getAddrs(ctx, ident.Value)
if err != nil {
return nil, err
}
addrs, resolvers = dnsAddrs, dnsResolvers
case identifier.TypeIP:
addrs = []net.IP{net.ParseIP(ident.Value)}
default:
return nil, fmt.Errorf("unknown identifier type: %s", ident.Type)
} }
target := &httpValidationTarget{ target := &httpValidationTarget{
host: host, host: ident.Value,
port: port, port: port,
path: path, path: path,
query: query, query: query,
@ -230,7 +241,7 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget(
if !hasV6Addrs && !hasV4Addrs { if !hasV6Addrs && !hasV4Addrs {
// If there are no v6 addrs and no v4addrs there was a bug with getAddrs or // If there are no v6 addrs and no v4addrs there was a bug with getAddrs or
// availableAddresses and we need to return an error. // availableAddresses and we need to return an error.
return nil, fmt.Errorf("host %q has no IPv4 or IPv6 addresses", host) return nil, fmt.Errorf("host %q has no IPv4 or IPv6 addresses", ident.Value)
} else if !hasV6Addrs && hasV4Addrs { } else if !hasV6Addrs && hasV4Addrs {
// If there are no v6 addrs and there are v4 addrs then use the first v4 // If there are no v6 addrs and there are v4 addrs then use the first v4
// address. There's no fallback address. // address. There's no fallback address.
@ -250,45 +261,44 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget(
return target, nil return target, nil
} }
// extractRequestTarget extracts the hostname and port specified in the provided // extractRequestTarget extracts the host and port specified in the provided
// HTTP redirect request. If the request's URL's protocol schema is not HTTP or // HTTP redirect request. If the request's URL's protocol schema is not HTTP or
// HTTPS an error is returned. If an explicit port is specified in the request's // HTTPS an error is returned. If an explicit port is specified in the request's
// URL and it isn't the VA's HTTP or HTTPS port, an error is returned. If the // URL and it isn't the VA's HTTP or HTTPS port, an error is returned.
// request's URL's Host is a bare IPv4 or IPv6 address and not a domain name an func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (identifier.ACMEIdentifier, int, error) {
// error is returned.
func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (string, int, error) {
// A nil request is certainly not a valid redirect and has no port to extract. // A nil request is certainly not a valid redirect and has no port to extract.
if req == nil { if req == nil {
return "", 0, fmt.Errorf("redirect HTTP request was nil") return identifier.ACMEIdentifier{}, 0, fmt.Errorf("redirect HTTP request was nil")
} }
reqScheme := req.URL.Scheme reqScheme := req.URL.Scheme
// The redirect request must use HTTP or HTTPs protocol schemes regardless of the port.. // The redirect request must use HTTP or HTTPs protocol schemes regardless of the port..
if reqScheme != "http" && reqScheme != "https" { if reqScheme != "http" && reqScheme != "https" {
return "", 0, berrors.ConnectionFailureError( return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError(
"Invalid protocol scheme in redirect target. "+ "Invalid protocol scheme in redirect target. "+
`Only "http" and "https" protocol schemes are supported, not %q`, reqScheme) `Only "http" and "https" protocol schemes are supported, not %q`, reqScheme)
} }
// Try and split an explicit port number from the request URL host. If there is // Try to parse an explicit port number from the request URL host. If there
// one we need to make sure its a valid port. If there isn't one we need to // is one, we need to make sure its a valid port. If there isn't one we need
// pick the port based on the reqScheme default port. // to pick the port based on the reqScheme default port.
reqHost := req.URL.Host reqHost := req.URL.Hostname()
var reqPort int var reqPort int
if h, p, err := net.SplitHostPort(reqHost); err == nil { if req.URL.Port() != "" {
reqHost = h parsedPort, err := strconv.Atoi(req.URL.Port())
reqPort, err = strconv.Atoi(p)
if err != nil { if err != nil {
return "", 0, err return identifier.ACMEIdentifier{}, 0, err
} }
// The explicit port must match the VA's configured HTTP or HTTPS port. // The explicit port must match the VA's configured HTTP or HTTPS port.
if reqPort != va.httpPort && reqPort != va.httpsPort { if parsedPort != va.httpPort && parsedPort != va.httpsPort {
return "", 0, berrors.ConnectionFailureError( return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError(
"Invalid port in redirect target. Only ports %d and %d are supported, not %d", "Invalid port in redirect target. Only ports %d and %d are supported, not %d",
va.httpPort, va.httpsPort, reqPort) va.httpPort, va.httpsPort, parsedPort)
} }
reqPort = parsedPort
} else if reqScheme == "http" { } else if reqScheme == "http" {
reqPort = va.httpPort reqPort = va.httpPort
} else if reqScheme == "https" { } else if reqScheme == "https" {
@ -296,17 +306,11 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (stri
} else { } else {
// This shouldn't happen but defensively return an internal server error in // This shouldn't happen but defensively return an internal server error in
// case it does. // case it does.
return "", 0, fmt.Errorf("unable to determine redirect HTTP request port") return identifier.ACMEIdentifier{}, 0, fmt.Errorf("unable to determine redirect HTTP request port")
} }
if reqHost == "" { if reqHost == "" {
return "", 0, berrors.ConnectionFailureError("Invalid empty hostname in redirect target") return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid empty host in redirect target")
}
// Check that the request host isn't a bare IP address. We only follow
// redirects to hostnames.
if net.ParseIP(reqHost) != nil {
return "", 0, berrors.ConnectionFailureError("Invalid host in redirect target %q. Only domain names are supported, not IP addresses", reqHost)
} }
// Often folks will misconfigure their webserver to send an HTTP redirect // Often folks will misconfigure their webserver to send an HTTP redirect
@ -319,17 +323,25 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (stri
// This happens frequently enough we want to return a distinct error message // This happens frequently enough we want to return a distinct error message
// for this case by detecting the reqHost ending in ".well-known". // for this case by detecting the reqHost ending in ".well-known".
if strings.HasSuffix(reqHost, ".well-known") { if strings.HasSuffix(reqHost, ".well-known") {
return "", 0, berrors.ConnectionFailureError( return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError(
"Invalid host in redirect target %q. Check webserver config for missing '/' in redirect target.", "Invalid host in redirect target %q. Check webserver config for missing '/' in redirect target.",
reqHost, reqHost,
) )
} }
if _, err := iana.ExtractSuffix(reqHost); err != nil { reqIP, err := netip.ParseAddr(reqHost)
return "", 0, berrors.ConnectionFailureError("Invalid hostname in redirect target, must end in IANA registered TLD") if err == nil {
if va.isReservedIPFunc(reqIP.AsSlice()) {
return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target, must not be a reserved IP address")
}
return identifier.NewIP(reqIP), reqPort, nil
} }
return reqHost, reqPort, nil if _, err := iana.ExtractSuffix(reqHost); err != nil {
return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target, must end in IANA registered TLD")
}
return identifier.NewDNS(reqHost), reqPort, nil
} }
// setupHTTPValidation sets up a preresolvedDialer and a validation record for // setupHTTPValidation sets up a preresolvedDialer and a validation record for
@ -371,6 +383,8 @@ func (va *ValidationAuthorityImpl) setupHTTPValidation(
"host %q has no IP addresses remaining to use", "host %q has no IP addresses remaining to use",
target.host) target.host)
} }
// TODO(#8041): This could be a good place for a backstop check for reserved IP
// addresses.
record.AddressUsed = targetIP record.AddressUsed = targetIP
dialer := &preresolvedDialer{ dialer := &preresolvedDialer{
@ -403,14 +417,27 @@ func fallbackErr(err error) bool {
// a non-nil error and potentially some ValidationRecords are returned. // a non-nil error and potentially some ValidationRecords are returned.
func (va *ValidationAuthorityImpl) processHTTPValidation( func (va *ValidationAuthorityImpl) processHTTPValidation(
ctx context.Context, ctx context.Context,
host string, ident identifier.ACMEIdentifier,
path string) ([]byte, []core.ValidationRecord, error) { path string) ([]byte, []core.ValidationRecord, error) {
// Create a target for the host, port and path with no query parameters // Create a target for the host, port and path with no query parameters
target, err := va.newHTTPValidationTarget(ctx, host, va.httpPort, path, "") target, err := va.newHTTPValidationTarget(ctx, ident, va.httpPort, path, "")
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// When constructing a URL, bare IPv6 addresses must be enclosed in square
// brackets. Otherwise, a colon may be interpreted as a port separator.
host := ident.Value
if ident.Type == identifier.TypeIP {
netipHost, err := netip.ParseAddr(host)
if err != nil {
return nil, nil, fmt.Errorf("couldn't parse IP address from identifier")
}
if !netipHost.Is4() {
host = "[" + host + "]"
}
}
// Create an initial GET Request // Create an initial GET Request
initialURL := url.URL{ initialURL := url.URL{
Scheme: "http", Scheme: "http",
@ -626,14 +653,14 @@ func (va *ValidationAuthorityImpl) processHTTPValidation(
} }
func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, ident identifier.ACMEIdentifier, token string, keyAuthorization string) ([]core.ValidationRecord, error) { func (va *ValidationAuthorityImpl) validateHTTP01(ctx context.Context, ident identifier.ACMEIdentifier, token string, keyAuthorization string) ([]core.ValidationRecord, error) {
if ident.Type != identifier.TypeDNS { if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP {
va.log.Infof("Got non-DNS identifier for HTTP validation: %s", ident) va.log.Info(fmt.Sprintf("Identifier type for HTTP-01 challenge was not DNS or IP: %s", ident))
return nil, berrors.MalformedError("Identifier type for HTTP validation was not DNS") return nil, berrors.MalformedError("Identifier type for HTTP-01 challenge was not DNS or IP")
} }
// Perform the fetch // Perform the fetch
path := fmt.Sprintf(".well-known/acme-challenge/%s", token) path := fmt.Sprintf(".well-known/acme-challenge/%s", token)
body, validationRecords, err := va.processHTTPValidation(ctx, ident.Value, "/"+path) body, validationRecords, err := va.processHTTPValidation(ctx, ident, "/"+path)
if err != nil { if err != nil {
return validationRecords, err return validationRecords, err
} }

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,9 @@ type IsCAAValidRequest struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
// TODO: Accept an identifier instead of a domain (purely for consistency,
// because only DNS identifiers support CAA checks).
//
// NOTE: Domain may be a name with a wildcard prefix (e.g. `*.example.com`) // NOTE: Domain may be a name with a wildcard prefix (e.g. `*.example.com`)
Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"`
ValidationMethod string `protobuf:"bytes,2,opt,name=validationMethod,proto3" json:"validationMethod,omitempty"` ValidationMethod string `protobuf:"bytes,2,opt,name=validationMethod,proto3" json:"validationMethod,omitempty"`
@ -162,10 +165,13 @@ type PerformValidationRequest struct {
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
DnsName string `protobuf:"bytes,1,opt,name=dnsName,proto3" json:"dnsName,omitempty"` // Next unused field number: 6
Challenge *proto.Challenge `protobuf:"bytes,2,opt,name=challenge,proto3" json:"challenge,omitempty"` // TODO(#8023): dnsNames are being deprecated in favour of identifiers.
Authz *AuthzMeta `protobuf:"bytes,3,opt,name=authz,proto3" json:"authz,omitempty"` DnsName string `protobuf:"bytes,1,opt,name=dnsName,proto3" json:"dnsName,omitempty"`
ExpectedKeyAuthorization string `protobuf:"bytes,4,opt,name=expectedKeyAuthorization,proto3" json:"expectedKeyAuthorization,omitempty"` Identifier *proto.Identifier `protobuf:"bytes,5,opt,name=identifier,proto3" json:"identifier,omitempty"`
Challenge *proto.Challenge `protobuf:"bytes,2,opt,name=challenge,proto3" json:"challenge,omitempty"`
Authz *AuthzMeta `protobuf:"bytes,3,opt,name=authz,proto3" json:"authz,omitempty"`
ExpectedKeyAuthorization string `protobuf:"bytes,4,opt,name=expectedKeyAuthorization,proto3" json:"expectedKeyAuthorization,omitempty"`
} }
func (x *PerformValidationRequest) Reset() { func (x *PerformValidationRequest) Reset() {
@ -207,6 +213,13 @@ func (x *PerformValidationRequest) GetDnsName() string {
return "" return ""
} }
func (x *PerformValidationRequest) GetIdentifier() *proto.Identifier {
if x != nil {
return x.Identifier
}
return nil
}
func (x *PerformValidationRequest) GetChallenge() *proto.Challenge { func (x *PerformValidationRequest) GetChallenge() *proto.Challenge {
if x != nil { if x != nil {
return x.Challenge return x.Challenge
@ -376,45 +389,48 @@ var file_va_proto_rawDesc = []byte{
0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69,
0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65,
0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x22, 0xc4, 0x01, 0x0a, 0x18, 0x50, 0x65, 0x72, 0x66, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x22, 0xf6, 0x01, 0x0a, 0x18, 0x50, 0x65, 0x72, 0x66,
0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x30,
0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01,
0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69,
0x67, 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72,
0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, 0x12, 0x2d, 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20,
0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x61, 0x75, 0x74, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c,
0x68, 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x65, 0x6e, 0x67, 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12,
0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x23, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d,
0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, 0x2e, 0x76, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x61,
0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x31, 0x75, 0x74, 0x68, 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64,
0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64,
0x65, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x44, 0x22, 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x31, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a,
0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a,
0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65,
0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, 0x67, 0x49, 0x44, 0x22, 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69,
0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f,
0x6c, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65,
0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72,
0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72,
0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f,
0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c,
0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x43, 0x0a, 0x02, 0x73, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65,
0x56, 0x41, 0x12, 0x3d, 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, 0x1c, 0x2e, 0x76, 0x61, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x0b, 0x70, 0x65, 0x72, 0x73, 0x70, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03,
0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x43,
0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x0a, 0x02, 0x56, 0x41, 0x12, 0x3d, 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, 0x1c, 0x2e,
0x00, 0x32, 0x3f, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x38, 0x0a, 0x05, 0x44, 0x6f, 0x43, 0x41, 0x76, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61,
0x41, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61,
0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c,
0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x74, 0x22, 0x00, 0x32, 0x3f, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x38, 0x0a, 0x05, 0x44, 0x6f,
0x22, 0x00, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x43, 0x41, 0x41, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61,
0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62, 0x6f, 0x75, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e,
0x6c, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x72, 0x6f, 0x74, 0x6f, 0x33, 0x73, 0x65, 0x22, 0x00, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x6c, 0x65, 0x74, 0x73, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2f, 0x62,
0x6f, 0x75, 0x6c, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x61, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
var ( var (
@ -437,24 +453,26 @@ var file_va_proto_goTypes = []interface{}{
(*AuthzMeta)(nil), // 3: va.AuthzMeta (*AuthzMeta)(nil), // 3: va.AuthzMeta
(*ValidationResult)(nil), // 4: va.ValidationResult (*ValidationResult)(nil), // 4: va.ValidationResult
(*proto.ProblemDetails)(nil), // 5: core.ProblemDetails (*proto.ProblemDetails)(nil), // 5: core.ProblemDetails
(*proto.Challenge)(nil), // 6: core.Challenge (*proto.Identifier)(nil), // 6: core.Identifier
(*proto.ValidationRecord)(nil), // 7: core.ValidationRecord (*proto.Challenge)(nil), // 7: core.Challenge
(*proto.ValidationRecord)(nil), // 8: core.ValidationRecord
} }
var file_va_proto_depIdxs = []int32{ var file_va_proto_depIdxs = []int32{
5, // 0: va.IsCAAValidResponse.problem:type_name -> core.ProblemDetails 5, // 0: va.IsCAAValidResponse.problem:type_name -> core.ProblemDetails
6, // 1: va.PerformValidationRequest.challenge:type_name -> core.Challenge 6, // 1: va.PerformValidationRequest.identifier:type_name -> core.Identifier
3, // 2: va.PerformValidationRequest.authz:type_name -> va.AuthzMeta 7, // 2: va.PerformValidationRequest.challenge:type_name -> core.Challenge
7, // 3: va.ValidationResult.records:type_name -> core.ValidationRecord 3, // 3: va.PerformValidationRequest.authz:type_name -> va.AuthzMeta
5, // 4: va.ValidationResult.problem:type_name -> core.ProblemDetails 8, // 4: va.ValidationResult.records:type_name -> core.ValidationRecord
2, // 5: va.VA.DoDCV:input_type -> va.PerformValidationRequest 5, // 5: va.ValidationResult.problem:type_name -> core.ProblemDetails
0, // 6: va.CAA.DoCAA:input_type -> va.IsCAAValidRequest 2, // 6: va.VA.DoDCV:input_type -> va.PerformValidationRequest
4, // 7: va.VA.DoDCV:output_type -> va.ValidationResult 0, // 7: va.CAA.DoCAA:input_type -> va.IsCAAValidRequest
1, // 8: va.CAA.DoCAA:output_type -> va.IsCAAValidResponse 4, // 8: va.VA.DoDCV:output_type -> va.ValidationResult
7, // [7:9] is the sub-list for method output_type 1, // 9: va.CAA.DoCAA:output_type -> va.IsCAAValidResponse
5, // [5:7] is the sub-list for method input_type 8, // [8:10] is the sub-list for method output_type
5, // [5:5] is the sub-list for extension type_name 6, // [6:8] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension extendee 6, // [6:6] is the sub-list for extension type_name
0, // [0:5] is the sub-list for field type_name 6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
} }
func init() { file_va_proto_init() } func init() { file_va_proto_init() }

View File

@ -14,6 +14,9 @@ service CAA {
} }
message IsCAAValidRequest { message IsCAAValidRequest {
// TODO: Accept an identifier instead of a domain (purely for consistency,
// because only DNS identifiers support CAA checks).
//
// NOTE: Domain may be a name with a wildcard prefix (e.g. `*.example.com`) // NOTE: Domain may be a name with a wildcard prefix (e.g. `*.example.com`)
string domain = 1; string domain = 1;
string validationMethod = 2; string validationMethod = 2;
@ -29,7 +32,10 @@ message IsCAAValidResponse {
} }
message PerformValidationRequest { message PerformValidationRequest {
// Next unused field number: 6
// TODO(#8023): dnsNames are being deprecated in favour of identifiers.
string dnsName = 1; string dnsName = 1;
core.Identifier identifier = 5;
core.Challenge challenge = 2; core.Challenge challenge = 2;
AuthzMeta authz = 3; AuthzMeta authz = 3;
string expectedKeyAuthorization = 4; string expectedKeyAuthorization = 4;

View File

@ -13,9 +13,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"net/netip"
"strconv" "strconv"
"strings" "strings"
"github.com/miekg/dns"
"github.com/letsencrypt/boulder/core" "github.com/letsencrypt/boulder/core"
berrors "github.com/letsencrypt/boulder/errors" berrors "github.com/letsencrypt/boulder/errors"
"github.com/letsencrypt/boulder/identifier" "github.com/letsencrypt/boulder/identifier"
@ -58,28 +61,38 @@ func certAltNames(cert *x509.Certificate) []string {
func (va *ValidationAuthorityImpl) tryGetChallengeCert( func (va *ValidationAuthorityImpl) tryGetChallengeCert(
ctx context.Context, ctx context.Context,
identifier identifier.ACMEIdentifier, ident identifier.ACMEIdentifier,
tlsConfig *tls.Config,
) (*x509.Certificate, *tls.ConnectionState, core.ValidationRecord, error) { ) (*x509.Certificate, *tls.ConnectionState, core.ValidationRecord, error) {
allAddrs, resolvers, err := va.getAddrs(ctx, identifier.Value)
validationRecord := core.ValidationRecord{ validationRecord := core.ValidationRecord{
DnsName: identifier.Value, DnsName: ident.Value,
AddressesResolved: allAddrs, Port: strconv.Itoa(va.tlsPort),
Port: strconv.Itoa(va.tlsPort),
ResolverAddrs: resolvers,
} }
if err != nil {
return nil, nil, validationRecord, err var addrs []net.IP
switch ident.Type {
case identifier.TypeDNS:
// Resolve IP addresses for the identifier
dnsAddrs, dnsResolvers, err := va.getAddrs(ctx, ident.Value)
if err != nil {
return nil, nil, validationRecord, err
}
addrs, validationRecord.ResolverAddrs = dnsAddrs, dnsResolvers
validationRecord.AddressesResolved = addrs
case identifier.TypeIP:
addrs = []net.IP{net.ParseIP(ident.Value)}
default:
// This should never happen. The calling function should check the
// identifier type.
return nil, nil, validationRecord, fmt.Errorf("unknown identifier type: %s", ident.Type)
} }
// Split the available addresses into v4 and v6 addresses // Split the available addresses into v4 and v6 addresses
v4, v6 := availableAddresses(allAddrs) v4, v6 := availableAddresses(addrs)
addresses := append(v4, v6...) addresses := append(v4, v6...)
// This shouldn't happen, but be defensive about it anyway // This shouldn't happen, but be defensive about it anyway
if len(addresses) < 1 { if len(addresses) < 1 {
return nil, nil, validationRecord, berrors.MalformedError("no IP addresses found for %q", identifier.Value) return nil, nil, validationRecord, berrors.MalformedError("no IP addresses found for %q", ident.Value)
} }
// If there is at least one IPv6 address then try it first // If there is at least one IPv6 address then try it first
@ -87,7 +100,7 @@ func (va *ValidationAuthorityImpl) tryGetChallengeCert(
address := net.JoinHostPort(v6[0].String(), validationRecord.Port) address := net.JoinHostPort(v6[0].String(), validationRecord.Port)
validationRecord.AddressUsed = v6[0] validationRecord.AddressUsed = v6[0]
cert, cs, err := va.getChallengeCert(ctx, address, identifier, tlsConfig) cert, cs, err := va.getChallengeCert(ctx, address, ident)
// If there is no problem, return immediately // If there is no problem, return immediately
if err == nil { if err == nil {
@ -114,27 +127,50 @@ func (va *ValidationAuthorityImpl) tryGetChallengeCert(
// talking to the first IPv6 address, try the first IPv4 address // talking to the first IPv6 address, try the first IPv4 address
validationRecord.AddressUsed = v4[0] validationRecord.AddressUsed = v4[0]
address := net.JoinHostPort(v4[0].String(), validationRecord.Port) address := net.JoinHostPort(v4[0].String(), validationRecord.Port)
cert, cs, err := va.getChallengeCert(ctx, address, identifier, tlsConfig) cert, cs, err := va.getChallengeCert(ctx, address, ident)
return cert, cs, validationRecord, err return cert, cs, validationRecord, err
} }
func (va *ValidationAuthorityImpl) getChallengeCert( func (va *ValidationAuthorityImpl) getChallengeCert(
ctx context.Context, ctx context.Context,
hostPort string, hostPort string,
identifier identifier.ACMEIdentifier, ident identifier.ACMEIdentifier,
config *tls.Config,
) (*x509.Certificate, *tls.ConnectionState, error) { ) (*x509.Certificate, *tls.ConnectionState, error) {
va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, identifier, hostPort, config.ServerName)) var serverName string
// We expect a self-signed challenge certificate, do not verify it here. switch ident.Type {
config.InsecureSkipVerify = true case identifier.TypeDNS:
serverName = ident.Value
case identifier.TypeIP:
reverseIP, err := dns.ReverseAddr(ident.Value)
if err != nil {
va.log.Infof("%s Failed to parse IP address %s.", core.ChallengeTypeTLSALPN01, ident.Value)
return nil, nil, fmt.Errorf("failed to parse IP address")
}
serverName = reverseIP
default:
// This should never happen. The calling function should check the
// identifier type.
va.log.Infof("%s Unknown identifier type '%s' for %s.", core.ChallengeTypeTLSALPN01, ident.Type, ident.Value)
return nil, nil, fmt.Errorf("unknown identifier type: %s", ident.Type)
}
va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, ident, hostPort, serverName))
dialCtx, cancel := context.WithTimeout(ctx, va.singleDialTimeout) dialCtx, cancel := context.WithTimeout(ctx, va.singleDialTimeout)
defer cancel() defer cancel()
dialer := &tls.Dialer{Config: config} dialer := &tls.Dialer{Config: &tls.Config{
MinVersion: tls.VersionTLS12,
NextProtos: []string{ACMETLS1Protocol},
ServerName: serverName,
// We expect a self-signed challenge certificate, do not verify it here.
InsecureSkipVerify: true,
}}
// TODO(#8041): This could be a good place for a backstop check for reserved IP
// addresses.
conn, err := dialer.DialContext(dialCtx, "tcp", hostPort) conn, err := dialer.DialContext(dialCtx, "tcp", hostPort)
if err != nil { if err != nil {
va.log.Infof("%s connection failure for %s. err=[%#v] errStr=[%s]", core.ChallengeTypeTLSALPN01, identifier, err, err) va.log.Infof("%s connection failure for %s. err=[%#v] errStr=[%s]", core.ChallengeTypeTLSALPN01, ident, err, err)
host, _, splitErr := net.SplitHostPort(hostPort) host, _, splitErr := net.SplitHostPort(hostPort)
if splitErr == nil && net.ParseIP(host) != nil { if splitErr == nil && net.ParseIP(host) != nil {
// Wrap the validation error and the IP of the remote host in an // Wrap the validation error and the IP of the remote host in an
@ -150,36 +186,69 @@ func (va *ValidationAuthorityImpl) getChallengeCert(
cs := conn.(*tls.Conn).ConnectionState() cs := conn.(*tls.Conn).ConnectionState()
certs := cs.PeerCertificates certs := cs.PeerCertificates
if len(certs) == 0 { if len(certs) == 0 {
va.log.Infof("%s challenge for %s resulted in no certificates", core.ChallengeTypeTLSALPN01, identifier.Value) va.log.Infof("%s challenge for %s resulted in no certificates", core.ChallengeTypeTLSALPN01, ident.Value)
return nil, nil, berrors.UnauthorizedError("No certs presented for %s challenge", core.ChallengeTypeTLSALPN01) return nil, nil, berrors.UnauthorizedError("No certs presented for %s challenge", core.ChallengeTypeTLSALPN01)
} }
for i, cert := range certs { for i, cert := range certs {
va.log.AuditInfof("%s challenge for %s received certificate (%d of %d): cert=[%s]", va.log.AuditInfof("%s challenge for %s received certificate (%d of %d): cert=[%s]",
core.ChallengeTypeTLSALPN01, identifier.Value, i+1, len(certs), hex.EncodeToString(cert.Raw)) core.ChallengeTypeTLSALPN01, ident.Value, i+1, len(certs), hex.EncodeToString(cert.Raw))
} }
return certs[0], &cs, nil return certs[0], &cs, nil
} }
func checkExpectedSAN(cert *x509.Certificate, name identifier.ACMEIdentifier) error { func checkExpectedSAN(cert *x509.Certificate, ident identifier.ACMEIdentifier) error {
if len(cert.DNSNames) != 1 { var expectedSANBytes []byte
return errors.New("wrong number of dNSNames") switch ident.Type {
case identifier.TypeDNS:
if len(cert.DNSNames) != 1 || len(cert.IPAddresses) != 0 {
return errors.New("wrong number of identifiers")
}
if !strings.EqualFold(cert.DNSNames[0], ident.Value) {
return errors.New("identifier does not match expected identifier")
}
bytes, err := asn1.Marshal([]asn1.RawValue{
{Tag: 2, Class: 2, Bytes: []byte(ident.Value)},
})
if err != nil {
return fmt.Errorf("composing SAN extension: %w", err)
}
expectedSANBytes = bytes
case identifier.TypeIP:
if len(cert.IPAddresses) != 1 || len(cert.DNSNames) != 0 {
return errors.New("wrong number of identifiers")
}
if !cert.IPAddresses[0].Equal(net.ParseIP(ident.Value)) {
return errors.New("identifier does not match expected identifier")
}
netipAddr, err := netip.ParseAddr(ident.Value)
if err != nil {
return fmt.Errorf("parsing IP address identifier: %w", err)
}
netipBytes, err := netipAddr.MarshalBinary()
if err != nil {
return fmt.Errorf("marshalling IP address identifier: %w", err)
}
bytes, err := asn1.Marshal([]asn1.RawValue{
{Tag: 7, Class: 2, Bytes: netipBytes},
})
if err != nil {
return fmt.Errorf("composing SAN extension: %w", err)
}
expectedSANBytes = bytes
default:
// This should never happen. The calling function should check the
// identifier type.
return fmt.Errorf("unknown identifier type: %s", ident.Type)
} }
for _, ext := range cert.Extensions { for _, ext := range cert.Extensions {
if IdCeSubjectAltName.Equal(ext.Id) { if IdCeSubjectAltName.Equal(ext.Id) {
expectedSANs, err := asn1.Marshal([]asn1.RawValue{ if !bytes.Equal(ext.Value, expectedSANBytes) {
{Tag: 2, Class: 2, Bytes: []byte(cert.DNSNames[0])},
})
if err != nil || !bytes.Equal(expectedSANs, ext.Value) {
return errors.New("SAN extension does not match expected bytes") return errors.New("SAN extension does not match expected bytes")
} }
} }
} }
if !strings.EqualFold(cert.DNSNames[0], name.Value) {
return errors.New("dNSName does not match expected identifier")
}
return nil return nil
} }
@ -205,23 +274,19 @@ func checkAcceptableExtensions(exts []pkix.Extension, requiredOIDs []asn1.Object
return nil return nil
} }
func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identifier identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) {
if identifier.Type != "dns" { if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP {
va.log.Info(fmt.Sprintf("Identifier type for TLS-ALPN-01 was not DNS: %s", identifier)) va.log.Info(fmt.Sprintf("Identifier type for TLS-ALPN-01 challenge was not DNS or IP: %s", ident))
return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 was not DNS") return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 challenge was not DNS or IP")
} }
cert, cs, tvr, problem := va.tryGetChallengeCert(ctx, identifier, &tls.Config{ cert, cs, tvr, err := va.tryGetChallengeCert(ctx, ident)
MinVersion: tls.VersionTLS12,
NextProtos: []string{ACMETLS1Protocol},
ServerName: identifier.Value,
})
// Copy the single validationRecord into the slice that we have to return, and // Copy the single validationRecord into the slice that we have to return, and
// get a reference to it so we can modify it if we have to. // get a reference to it so we can modify it if we have to.
validationRecords := []core.ValidationRecord{tvr} validationRecords := []core.ValidationRecord{tvr}
validationRecord := &validationRecords[0] validationRecord := &validationRecords[0]
if problem != nil { if err != nil {
return validationRecords, problem return validationRecords, err
} }
if cs.NegotiatedProtocol != ACMETLS1Protocol { if cs.NegotiatedProtocol != ACMETLS1Protocol {
@ -237,11 +302,11 @@ func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identi
return berrors.UnauthorizedError( return berrors.UnauthorizedError(
"Incorrect validation certificate for %s challenge. "+ "Incorrect validation certificate for %s challenge. "+
"Requested %s from %s. %s", "Requested %s from %s. %s",
core.ChallengeTypeTLSALPN01, identifier.Value, hostPort, msg) core.ChallengeTypeTLSALPN01, ident.Value, hostPort, msg)
} }
// The certificate must be self-signed. // The certificate must be self-signed.
err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature) err = cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature)
if err != nil || !bytes.Equal(cert.RawSubject, cert.RawIssuer) { if err != nil || !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
return validationRecords, badCertErr( return validationRecords, badCertErr(
"Received certificate which is not self-signed.") "Received certificate which is not self-signed.")
@ -259,8 +324,8 @@ func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identi
} }
// The certificate returned must have a subjectAltName extension containing // The certificate returned must have a subjectAltName extension containing
// only the dNSName being validated and no other entries. // only the identifier being validated and no other entries.
err = checkExpectedSAN(cert, identifier) err = checkExpectedSAN(cert, ident)
if err != nil { if err != nil {
names := strings.Join(certAltNames(cert), ", ") names := strings.Join(certAltNames(cert), ", ")
return validationRecords, badCertErr( return validationRecords, badCertErr(

View File

@ -11,6 +11,7 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1" "encoding/asn1"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
@ -49,7 +50,7 @@ var testACMEExt = acmeExtension(IdPeAcmeIdentifier, expectedKeyAuthorization)
// testTLSCert returns a ready-to-use self-signed certificate with the given // testTLSCert returns a ready-to-use self-signed certificate with the given
// SANs and Extensions. It generates a new ECDSA key on each call. // SANs and Extensions. It generates a new ECDSA key on each call.
func testTLSCert(names []string, extensions []pkix.Extension) *tls.Certificate { func testTLSCert(names []string, ips []net.IP, extensions []pkix.Extension) *tls.Certificate {
template := &x509.Certificate{ template := &x509.Certificate{
SerialNumber: big.NewInt(1337), SerialNumber: big.NewInt(1337),
Subject: pkix.Name{ Subject: pkix.Name{
@ -63,6 +64,7 @@ func testTLSCert(names []string, extensions []pkix.Extension) *tls.Certificate {
BasicConstraintsValid: true, BasicConstraintsValid: true,
DNSNames: names, DNSNames: names,
IPAddresses: ips,
ExtraExtensions: extensions, ExtraExtensions: extensions,
} }
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@ -77,19 +79,24 @@ func testTLSCert(names []string, extensions []pkix.Extension) *tls.Certificate {
// testACMECert returns a certificate with the correctly-formed ACME TLS-ALPN-01 // testACMECert returns a certificate with the correctly-formed ACME TLS-ALPN-01
// extension with our default test values. Use acmeExtension and testCert if you // extension with our default test values. Use acmeExtension and testCert if you
// need to customize the contents of that extension. // need to customize the contents of that extension.
func testACMECert(names ...string) *tls.Certificate { func testACMECert(names []string) *tls.Certificate {
return testTLSCert(names, []pkix.Extension{testACMEExt}) return testTLSCert(names, nil, []pkix.Extension{testACMEExt})
} }
// tlsalpn01SrvWithCert creates a test server which will present the given // tlsalpn01SrvWithCert creates a test server which will present the given
// certificate when asked to do a tls-alpn-01 handshake. // certificate when asked to do a tls-alpn-01 handshake.
func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion uint16) *httptest.Server { func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion uint16, ipv6 bool) *httptest.Server {
t.Helper() t.Helper()
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
Certificates: []tls.Certificate{}, Certificates: []tls.Certificate{},
ClientAuth: tls.NoClientCert, ClientAuth: tls.NoClientCert,
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
// This is a backstop test for RFC 8738, Section 6. Go's
// tls.hostnameInSNI already does the right thing.
if net.ParseIP(clientHello.ServerName) != nil {
return nil, errors.New("TLS client used a bare IP address for SNI")
}
return acmeCert, nil return acmeCert, nil
}, },
NextProtos: []string{"http/1.1", ACMETLS1Protocol}, NextProtos: []string{"http/1.1", ACMETLS1Protocol},
@ -104,6 +111,13 @@ func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion ui
_ = conn.Close() _ = 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() hs.StartTLS()
return hs return hs
} }
@ -112,24 +126,11 @@ func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion ui
// that don't need to customize specific names or extensions in the certificate // that don't need to customize specific names or extensions in the certificate
// served by the TLS server. // served by the TLS server.
func testTLSALPN01Srv(t *testing.T) *httptest.Server { func testTLSALPN01Srv(t *testing.T) *httptest.Server {
return tlsalpn01SrvWithCert(t, testACMECert("expected"), 0) return tlsalpn01SrvWithCert(t, testACMECert([]string{"expected"}), 0, false)
}
func TestTLSALPN01FailIP(t *testing.T) {
hs := testTLSALPN01Srv(t)
va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedKeyAuthorization)
if err == nil {
t.Fatalf("IdentifierType IP shouldn't have worked.")
}
prob := detailedError(err)
test.AssertEquals(t, prob.Type, probs.MalformedProblem)
} }
func slowTLSSrv() *httptest.Server { func slowTLSSrv() *httptest.Server {
cert := testTLSCert([]string{"nomatter"}, nil) cert := testTLSCert([]string{"nomatter"}, nil, nil)
server := httptest.NewUnstartedServer(http.DefaultServeMux) server := httptest.NewUnstartedServer(http.DefaultServeMux)
server.TLS = &tls.Config{ server.TLS = &tls.Config{
NextProtos: []string{"http/1.1", ACMETLS1Protocol}, NextProtos: []string{"http/1.1", ACMETLS1Protocol},
@ -151,7 +152,7 @@ func TestTLSALPNTimeoutAfterConnect(t *testing.T) {
defer cancel() defer cancel()
started := time.Now() started := time.Now()
_, err := va.validateTLSALPN01(ctx, dnsi("slow.server"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("slow.server"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("Validation should've failed") t.Fatalf("Validation should've failed")
} }
@ -194,7 +195,7 @@ func TestTLSALPN01DialTimeout(t *testing.T) {
// that, just retry until we get something other than "Network unreachable". // that, just retry until we get something other than "Network unreachable".
var err error var err error
for range 20 { for range 20 {
_, err = va.validateTLSALPN01(ctx, dnsi("unroutable.invalid"), expectedKeyAuthorization) _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("unroutable.invalid"), expectedKeyAuthorization)
if err != nil && strings.Contains(err.Error(), "Network unreachable") { if err != nil && strings.Contains(err.Error(), "Network unreachable") {
continue continue
} else { } else {
@ -234,7 +235,7 @@ func TestTLSALPN01Refused(t *testing.T) {
// Take down validation server and check that validation fails. // Take down validation server and check that validation fails.
hs.Close() hs.Close()
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("Server's down; expected refusal. Where did we connect?") t.Fatalf("Server's down; expected refusal. Where did we connect?")
} }
@ -252,10 +253,10 @@ func TestTLSALPN01TalkingToHTTP(t *testing.T) {
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
// Make the server only speak HTTP. // Make the server only speak HTTP.
httpOnly := httpSrv(t, "") httpOnly := httpSrv(t, "", false)
va.tlsPort = getPort(httpOnly) va.tlsPort = getPort(httpOnly)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "TLS-SNI-01 validation passed when talking to a HTTP-only server") test.AssertError(t, err, "TLS-SNI-01 validation passed when talking to a HTTP-only server")
prob := detailedError(err) prob := detailedError(err)
expected := "Server only speaks HTTP, not TLS" expected := "Server only speaks HTTP, not TLS"
@ -280,7 +281,7 @@ func TestTLSError(t *testing.T) {
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("TLS validation should have failed: What cert was used?") t.Fatalf("TLS validation should have failed: What cert was used?")
} }
@ -296,7 +297,7 @@ func TestDNSError(t *testing.T) {
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("always.invalid"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("always.invalid"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("TLS validation should have failed: what IP was used?") t.Fatalf("TLS validation should have failed: what IP was used?")
} }
@ -368,12 +369,44 @@ func TestCertNames(t *testing.T) {
test.AssertDeepEquals(t, actual, expected) test.AssertDeepEquals(t, actual, expected)
} }
func TestTLSALPN01Success(t *testing.T) { func TestTLSALPN01SuccessDNS(t *testing.T) {
hs := testTLSALPN01Srv(t) hs := testTLSALPN01Srv(t)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, 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 { if err != nil {
t.Errorf("Validation failed: %v", err) t.Errorf("Validation failed: %v", err)
} }
@ -393,12 +426,12 @@ func TestTLSALPN01ObsoleteFailure(t *testing.T) {
// id-pe OID + 30 (acmeIdentifier) + 1 (v1) // id-pe OID + 30 (acmeIdentifier) + 1 (v1)
IdPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} IdPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1}
cert := testTLSCert([]string{"expected"}, []pkix.Extension{acmeExtension(IdPeAcmeIdentifierV1Obsolete, expectedKeyAuthorization)}) cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifierV1Obsolete, expectedKeyAuthorization)})
hs := tlsalpn01SrvWithCert(t, cert, 0) hs := tlsalpn01SrvWithCert(t, cert, 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertNotNil(t, err, "expected validation to fail") 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") test.AssertContains(t, err.Error(), "Required extension OID 1.3.6.1.5.5.7.1.31 is not present")
} }
@ -406,12 +439,12 @@ func TestTLSALPN01ObsoleteFailure(t *testing.T) {
func TestValidateTLSALPN01BadChallenge(t *testing.T) { func TestValidateTLSALPN01BadChallenge(t *testing.T) {
badKeyAuthorization := ka("bad token") badKeyAuthorization := ka("bad token")
cert := testTLSCert([]string{"expected"}, []pkix.Extension{acmeExtension(IdPeAcmeIdentifier, badKeyAuthorization)}) cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifier, badKeyAuthorization)})
hs := tlsalpn01SrvWithCert(t, cert, 0) hs := tlsalpn01SrvWithCert(t, cert, 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("TLS ALPN validation should have failed.") t.Fatalf("TLS ALPN validation should have failed.")
} }
@ -432,7 +465,7 @@ func TestValidateTLSALPN01BrokenSrv(t *testing.T) {
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("TLS ALPN validation should have failed.") t.Fatalf("TLS ALPN validation should have failed.")
} }
@ -441,7 +474,7 @@ func TestValidateTLSALPN01BrokenSrv(t *testing.T) {
} }
func TestValidateTLSALPN01UnawareSrv(t *testing.T) { func TestValidateTLSALPN01UnawareSrv(t *testing.T) {
cert := testTLSCert([]string{"expected"}, nil) cert := testTLSCert([]string{"expected"}, nil, nil)
hs := httptest.NewUnstartedServer(http.DefaultServeMux) hs := httptest.NewUnstartedServer(http.DefaultServeMux)
hs.TLS = &tls.Config{ hs.TLS = &tls.Config{
Certificates: []tls.Certificate{}, Certificates: []tls.Certificate{},
@ -455,7 +488,7 @@ func TestValidateTLSALPN01UnawareSrv(t *testing.T) {
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
if err == nil { if err == nil {
t.Fatalf("TLS ALPN validation should have failed.") t.Fatalf("TLS ALPN validation should have failed.")
} }
@ -484,11 +517,11 @@ func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) {
} }
for _, badExt := range badExtensions { for _, badExt := range badExtensions {
acmeCert := testTLSCert([]string{"expected"}, []pkix.Extension{badExt}) acmeCert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{badExt})
hs := tlsalpn01SrvWithCert(t, acmeCert, 0) hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
hs.Close() hs.Close()
if err == nil { if err == nil {
@ -504,7 +537,7 @@ func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) {
} }
func TestTLSALPN01TLSVersion(t *testing.T) { func TestTLSALPN01TLSVersion(t *testing.T) {
cert := testACMECert("expected") cert := testACMECert([]string{"expected"})
for _, tc := range []struct { for _, tc := range []struct {
version uint16 version uint16
@ -524,11 +557,11 @@ func TestTLSALPN01TLSVersion(t *testing.T) {
}, },
} { } {
// Create a server that only negotiates the given TLS version // Create a server that only negotiates the given TLS version
hs := tlsalpn01SrvWithCert(t, cert, tc.version) hs := tlsalpn01SrvWithCert(t, cert, tc.version, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
if !tc.expectError { if !tc.expectError {
if err != nil { if err != nil {
t.Errorf("expected success, got: %v", err) t.Errorf("expected success, got: %v", err)
@ -549,24 +582,74 @@ func TestTLSALPN01TLSVersion(t *testing.T) {
func TestTLSALPN01WrongName(t *testing.T) { func TestTLSALPN01WrongName(t *testing.T) {
// Create a cert with a different name from what we're validating // Create a cert with a different name from what we're validating
hs := tlsalpn01SrvWithCert(t, testACMECert("incorrect"), 0) hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"incorrect"}), 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "validation should have failed") test.AssertError(t, err, "validation should have failed")
test.AssertContains(t, err.Error(), "dNSName does not match expected identifier") 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) { func TestTLSALPN01ExtraNames(t *testing.T) {
// Create a cert with two names when we only want to validate one. // Create a cert with two names when we only want to validate one.
hs := tlsalpn01SrvWithCert(t, testACMECert("expected", "extra"), 0) hs := tlsalpn01SrvWithCert(t, testACMECert([]string{"expected", "extra"}), 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "validation should have failed") test.AssertError(t, err, "validation should have failed")
test.AssertContains(t, err.Error(), "wrong number of dNSNames") 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) { func TestTLSALPN01NotSelfSigned(t *testing.T) {
@ -614,11 +697,11 @@ func TestTLSALPN01NotSelfSigned(t *testing.T) {
PrivateKey: eeKey, PrivateKey: eeKey,
} }
hs := tlsalpn01SrvWithCert(t, acmeCert, 0) hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "validation should have failed") test.AssertError(t, err, "validation should have failed")
test.AssertContains(t, err.Error(), "not self-signed") test.AssertContains(t, err.Error(), "not self-signed")
@ -632,11 +715,11 @@ func TestTLSALPN01NotSelfSigned(t *testing.T) {
PrivateKey: eeKey, PrivateKey: eeKey,
} }
hs = tlsalpn01SrvWithCert(t, acmeCert, 0) hs = tlsalpn01SrvWithCert(t, acmeCert, 0, false)
va, _ = setup(hs, "", nil, nil) va, _ = setup(hs, "", nil, nil)
_, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "validation should have failed") test.AssertError(t, err, "validation should have failed")
test.AssertContains(t, err.Error(), "not self-signed") test.AssertContains(t, err.Error(), "not self-signed")
} }
@ -671,11 +754,11 @@ func TestTLSALPN01ExtraIdentifiers(t *testing.T) {
PrivateKey: key, PrivateKey: key,
} }
hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12) hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "validation should have failed") test.AssertError(t, err, "validation should have failed")
test.AssertContains(t, err.Error(), "Received certificate with unexpected identifiers") test.AssertContains(t, err.Error(), "Received certificate with unexpected identifiers")
} }
@ -694,11 +777,11 @@ func TestTLSALPN01ExtraSANs(t *testing.T) {
} }
extensions := []pkix.Extension{testACMEExt, subjectAltName, subjectAltName} extensions := []pkix.Extension{testACMEExt, subjectAltName, subjectAltName}
hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, extensions), 0) hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, nil, extensions), 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "validation should have failed") test.AssertError(t, err, "validation should have failed")
// In go >= 1.19, the TLS client library detects that the certificate has // In go >= 1.19, the TLS client library detects that the certificate has
// a duplicate extension and terminates the connection itself. // a duplicate extension and terminates the connection itself.
@ -709,11 +792,11 @@ func TestTLSALPN01ExtraSANs(t *testing.T) {
func TestTLSALPN01ExtraAcmeExtensions(t *testing.T) { func TestTLSALPN01ExtraAcmeExtensions(t *testing.T) {
// Create a cert with multiple SAN extensions // Create a cert with multiple SAN extensions
extensions := []pkix.Extension{testACMEExt, testACMEExt} extensions := []pkix.Extension{testACMEExt, testACMEExt}
hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, extensions), 0) hs := tlsalpn01SrvWithCert(t, testTLSCert([]string{"expected"}, nil, extensions), 0, false)
va, _ := setup(hs, "", nil, nil) va, _ := setup(hs, "", nil, nil)
_, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization)
test.AssertError(t, err, "validation should have failed") test.AssertError(t, err, "validation should have failed")
// In go >= 1.19, the TLS client library detects that the certificate has // In go >= 1.19, the TLS client library detects that the certificate has
// a duplicate extension and terminates the connection itself. // a duplicate extension and terminates the connection itself.
@ -770,3 +853,15 @@ func TestAcceptableExtensions(t *testing.T) {
err = checkAcceptableExtensions(okayWithUnexpectedExt, requireAcmeAndSAN) err = checkAcceptableExtensions(okayWithUnexpectedExt, requireAcmeAndSAN)
test.AssertNotError(t, err, "Correct type and number of extensions") 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.Error(), "Identifier type for TLS-ALPN-01 challenge was not DNS or IP")
}

View File

@ -216,6 +216,7 @@ type ValidationAuthorityImpl struct {
singleDialTimeout time.Duration singleDialTimeout time.Duration
perspective string perspective string
rir string rir string
isReservedIPFunc func(ip net.IP) bool
metrics *vaMetrics metrics *vaMetrics
} }
@ -235,6 +236,7 @@ func NewValidationAuthorityImpl(
accountURIPrefixes []string, accountURIPrefixes []string,
perspective string, perspective string,
rir string, rir string,
reservedIPChecker func(ip net.IP) bool,
) (*ValidationAuthorityImpl, error) { ) (*ValidationAuthorityImpl, error) {
if len(accountURIPrefixes) == 0 { if len(accountURIPrefixes) == 0 {
@ -271,6 +273,7 @@ func NewValidationAuthorityImpl(
singleDialTimeout: 10 * time.Second, singleDialTimeout: 10 * time.Second,
perspective: perspective, perspective: perspective,
rir: rir, rir: rir,
isReservedIPFunc: reservedIPChecker,
} }
return va, nil return va, nil
@ -410,13 +413,12 @@ func (va *ValidationAuthorityImpl) validateChallenge(
token string, token string,
keyAuthorization string, keyAuthorization string,
) ([]core.ValidationRecord, error) { ) ([]core.ValidationRecord, error) {
// Strip a (potential) leading wildcard token from the identifier.
ident.Value = strings.TrimPrefix(ident.Value, "*.")
switch kind { switch kind {
case core.ChallengeTypeHTTP01: case core.ChallengeTypeHTTP01:
return va.validateHTTP01(ctx, ident, token, keyAuthorization) return va.validateHTTP01(ctx, ident, token, keyAuthorization)
case core.ChallengeTypeDNS01: case core.ChallengeTypeDNS01:
// Strip a (potential) leading wildcard token from the identifier.
ident.Value = strings.TrimPrefix(ident.Value, "*.")
return va.validateDNS01(ctx, ident, keyAuthorization) return va.validateDNS01(ctx, ident, keyAuthorization)
case core.ChallengeTypeTLSALPN01: case core.ChallengeTypeTLSALPN01:
return va.validateTLSALPN01(ctx, ident, keyAuthorization) return va.validateTLSALPN01(ctx, ident, keyAuthorization)
@ -640,7 +642,7 @@ func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op rem
type validationLogEvent struct { type validationLogEvent struct {
AuthzID string AuthzID string
Requester int64 Requester int64
Identifier string Identifier identifier.ACMEIdentifier
Challenge core.Challenge Challenge core.Challenge
Error string `json:",omitempty"` Error string `json:",omitempty"`
InternalError string `json:",omitempty"` InternalError string `json:",omitempty"`
@ -659,7 +661,14 @@ type validationLogEvent struct {
// implements the DCV portion of Multi-Perspective Issuance Corroboration as // implements the DCV portion of Multi-Perspective Issuance Corroboration as
// defined in BRs Sections 3.2.2.9 and 5.4.1. // defined in BRs Sections 3.2.2.9 and 5.4.1.
func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) { func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformValidationRequest) (*vapb.ValidationResult, error) {
if core.IsAnyNilOrZero(req, req.DnsName, req.Challenge, req.Authz, req.ExpectedKeyAuthorization) { // TODO(#8023): Once DnsNames are no longer used in RPCs, use req.Identifier
// directly instead of setting ident.
if req.Identifier != nil && req.DnsName != "" {
return nil, errors.New("both Identifier and DNSName are set")
}
ident := identifier.FromProtoWithDefault(req.Identifier, req.DnsName)
if core.IsAnyNilOrZero(req, ident, req.Challenge, req.Authz, req.ExpectedKeyAuthorization) {
return nil, berrors.InternalServerError("Incomplete validation request") return nil, berrors.InternalServerError("Incomplete validation request")
} }
@ -683,7 +692,7 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV
logEvent := validationLogEvent{ logEvent := validationLogEvent{
AuthzID: req.Authz.Id, AuthzID: req.Authz.Id,
Requester: req.Authz.RegID, Requester: req.Authz.RegID,
Identifier: req.DnsName, Identifier: ident,
Challenge: chall, Challenge: chall,
} }
defer func() { defer func() {
@ -717,7 +726,7 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV
// was successful or not, and cannot themselves fail. // was successful or not, and cannot themselves fail.
records, err := va.validateChallenge( records, err := va.validateChallenge(
ctx, ctx,
identifier.NewDNS(req.DnsName), ident,
chall.Type, chall.Type,
chall.Token, chall.Token,
req.ExpectedKeyAuthorization, req.ExpectedKeyAuthorization,

View File

@ -10,6 +10,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip"
"os" "os"
"strings" "strings"
"sync" "sync"
@ -69,11 +70,6 @@ var expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0"
var expectedThumbprint = "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI" var expectedThumbprint = "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI"
var expectedKeyAuthorization = ka(expectedToken) var expectedKeyAuthorization = ka(expectedToken)
// Return an ACME DNS identifier for the given hostname
func dnsi(hostname string) identifier.ACMEIdentifier {
return identifier.NewDNS(hostname)
}
var ctx context.Context var ctx context.Context
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -86,9 +82,9 @@ func TestMain(m *testing.M) {
var accountURIPrefixes = []string{"http://boulder.service.consul:4000/acme/reg/"} var accountURIPrefixes = []string{"http://boulder.service.consul:4000/acme/reg/"}
func createValidationRequest(domain string, challengeType core.AcmeChallenge) *vapb.PerformValidationRequest { func createValidationRequest(ident identifier.ACMEIdentifier, challengeType core.AcmeChallenge) *vapb.PerformValidationRequest {
return &vapb.PerformValidationRequest{ return &vapb.PerformValidationRequest{
DnsName: domain, Identifier: ident.AsProto(),
Challenge: &corepb.Challenge{ Challenge: &corepb.Challenge{
Type: string(challengeType), Type: string(challengeType),
Status: string(core.StatusPending), Status: string(core.StatusPending),
@ -103,6 +99,21 @@ func createValidationRequest(domain string, challengeType core.AcmeChallenge) *v
} }
} }
// isNonLoopbackReservedIP is a mock reserved IP checker that permits loopback
// networks.
func isNonLoopbackReservedIP(ip net.IP) bool {
loopbackV4 := netip.MustParsePrefix("127.0.0.0/8")
loopbackV6 := netip.MustParsePrefix("::1/128")
netIPAddr, ok := netip.AddrFromSlice(ip)
if !ok {
panic(fmt.Sprintf("error parsing IP (%s)", ip))
}
if loopbackV4.Contains(netIPAddr) || loopbackV6.Contains(netIPAddr) {
return false
}
return bdns.IsReservedIP(ip)
}
// setup returns an in-memory VA and a mock logger. The default resolver client // setup returns an in-memory VA and a mock logger. The default resolver client
// is MockClient{}, but can be overridden. // is MockClient{}, but can be overridden.
// //
@ -136,6 +147,7 @@ func setup(srv *httptest.Server, userAgent string, remoteVAs []RemoteVA, mockDNS
accountURIPrefixes, accountURIPrefixes,
perspective, perspective,
"", "",
isNonLoopbackReservedIP,
) )
if err != nil { if err != nil {
panic(fmt.Sprintf("Failed to create validation authority: %v", err)) panic(fmt.Sprintf("Failed to create validation authority: %v", err))
@ -321,6 +333,7 @@ func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) {
accountURIPrefixes, accountURIPrefixes,
"example perspective", "example perspective",
"", "",
isNonLoopbackReservedIP,
) )
test.AssertError(t, err, "NewValidationAuthorityImpl allowed duplicate remote perspectives") test.AssertError(t, err, "NewValidationAuthorityImpl allowed duplicate remote perspectives")
test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"") test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"")
@ -343,7 +356,7 @@ func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) {
remoteVAs = append(remoteVAs, mismatched1, mismatched2) remoteVAs = append(remoteVAs, mismatched1, mismatched2)
va, mockLog := setup(nil, "", remoteVAs, nil) va, mockLog := setup(nil, "", remoteVAs, nil)
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01)
res, _ := va.DoDCV(context.Background(), req) res, _ := va.DoDCV(context.Background(), req)
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
@ -366,7 +379,7 @@ func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) {
remoteVAs = append(remoteVAs, mismatched1, mismatched2) remoteVAs = append(remoteVAs, mismatched1, mismatched2)
va, mockLog := setup(nil, "", remoteVAs, nil) va, mockLog := setup(nil, "", remoteVAs, nil)
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01)
res, _ := va.DoDCV(context.Background(), req) res, _ := va.DoDCV(context.Background(), req)
test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives")
test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2)
@ -375,7 +388,7 @@ func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) {
func TestValidateMalformedChallenge(t *testing.T) { func TestValidateMalformedChallenge(t *testing.T) {
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
_, err := va.validateChallenge(ctx, dnsi("example.com"), "fake-type-01", expectedToken, expectedKeyAuthorization) _, err := va.validateChallenge(ctx, identifier.NewDNS("example.com"), "fake-type-01", expectedToken, expectedKeyAuthorization)
prob := detailedError(err) prob := detailedError(err)
test.AssertEquals(t, prob.Type, probs.MalformedProblem) test.AssertEquals(t, prob.Type, probs.MalformedProblem)
@ -385,7 +398,7 @@ func TestPerformValidationInvalid(t *testing.T) {
t.Parallel() t.Parallel()
va, _ := setup(nil, "", nil, nil) va, _ := setup(nil, "", nil, nil)
req := createValidationRequest("foo.com", core.ChallengeTypeDNS01) req := createValidationRequest(identifier.NewDNS("foo.com"), core.ChallengeTypeDNS01)
res, _ := va.DoDCV(context.Background(), req) res, _ := va.DoDCV(context.Background(), req)
test.Assert(t, res.Problem != nil, "validation succeeded") test.Assert(t, res.Problem != nil, "validation succeeded")
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
@ -404,7 +417,7 @@ func TestInternalErrorLogged(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel() defer cancel()
req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01) req := createValidationRequest(identifier.NewDNS("nonexistent.com"), core.ChallengeTypeHTTP01)
_, err := va.DoDCV(ctx, req) _, err := va.DoDCV(ctx, req)
test.AssertNotError(t, err, "failed validation should not be an error") test.AssertNotError(t, err, "failed validation should not be an error")
matchingLogs := mockLog.GetAllMatching( matchingLogs := mockLog.GetAllMatching(
@ -418,7 +431,7 @@ func TestPerformValidationValid(t *testing.T) {
va, mockLog := setup(nil, "", nil, nil) va, mockLog := setup(nil, "", nil, nil)
// create a challenge with well known token // create a challenge with well known token
req := createValidationRequest("good-dns01.com", core.ChallengeTypeDNS01) req := createValidationRequest(identifier.NewDNS("good-dns01.com"), core.ChallengeTypeDNS01)
res, _ := va.DoDCV(context.Background(), req) res, _ := va.DoDCV(context.Background(), req)
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem))
test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{
@ -432,7 +445,8 @@ func TestPerformValidationValid(t *testing.T) {
if len(resultLog) != 1 { if len(resultLog) != 1 {
t.Fatalf("Wrong number of matching lines for 'Validation result'") t.Fatalf("Wrong number of matching lines for 'Validation result'")
} }
if !strings.Contains(resultLog[0], `"Identifier":"good-dns01.com"`) {
if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"good-dns01.com"}`) {
t.Error("PerformValidation didn't log validation identifier.") t.Error("PerformValidation didn't log validation identifier.")
} }
} }
@ -445,7 +459,7 @@ func TestPerformValidationWildcard(t *testing.T) {
va, mockLog := setup(nil, "", nil, nil) va, mockLog := setup(nil, "", nil, nil)
// create a challenge with well known token // create a challenge with well known token
req := createValidationRequest("*.good-dns01.com", core.ChallengeTypeDNS01) req := createValidationRequest(identifier.NewDNS("*.good-dns01.com"), core.ChallengeTypeDNS01)
// perform a validation for a wildcard name // perform a validation for a wildcard name
res, _ := va.DoDCV(context.Background(), req) res, _ := va.DoDCV(context.Background(), req)
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem))
@ -462,7 +476,7 @@ func TestPerformValidationWildcard(t *testing.T) {
} }
// We expect that the top level Identifier reflect the wildcard name // We expect that the top level Identifier reflect the wildcard name
if !strings.Contains(resultLog[0], `"Identifier":"*.good-dns01.com"`) { if !strings.Contains(resultLog[0], `"Identifier":{"type":"dns","value":"*.good-dns01.com"}`) {
t.Errorf("PerformValidation didn't log correct validation identifier.") t.Errorf("PerformValidation didn't log correct validation identifier.")
} }
// We expect that the ValidationRecord contain the correct non-wildcard // We expect that the ValidationRecord contain the correct non-wildcard
@ -476,7 +490,7 @@ func TestMultiVA(t *testing.T) {
t.Parallel() t.Parallel()
// Create a new challenge to use for the httpSrv // Create a new challenge to use for the httpSrv
req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) req := createValidationRequest(identifier.NewDNS("localhost"), core.ChallengeTypeHTTP01)
brokenVA := RemoteClients{ brokenVA := RemoteClients{
VAClient: brokenRemoteVA{}, VAClient: brokenRemoteVA{},
@ -724,7 +738,7 @@ func TestMultiVAEarlyReturn(t *testing.T) {
// Perform all validations // Perform all validations
start := time.Now() start := time.Now()
req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) req := createValidationRequest(identifier.NewDNS("localhost"), core.ChallengeTypeHTTP01)
res, _ := localVA.DoDCV(ctx, req) res, _ := localVA.DoDCV(ctx, req)
// It should always fail // It should always fail
@ -763,13 +777,12 @@ func TestMultiVAPolicy(t *testing.T) {
localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
// Perform validation for a domain not in the disabledDomains list // Perform validation for a domain not in the disabledDomains list
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) req := createValidationRequest(identifier.NewDNS("letsencrypt.org"), core.ChallengeTypeHTTP01)
res, _ := localVA.DoDCV(ctx, req) res, _ := localVA.DoDCV(ctx, req)
// It should fail // It should fail
if res.Problem == nil { if res.Problem == nil {
t.Error("expected prob from PerformValidation, got nil") t.Error("expected prob from PerformValidation, got nil")
} }
} }
func TestMultiVALogging(t *testing.T) { func TestMultiVALogging(t *testing.T) {
@ -785,7 +798,7 @@ func TestMultiVALogging(t *testing.T) {
defer ms.Close() defer ms.Close()
va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) va, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil)
req := createValidationRequest("letsencrypt.org", core.ChallengeTypeHTTP01) req := createValidationRequest(identifier.NewDNS("letsencrypt.org"), core.ChallengeTypeHTTP01)
res, err := va.DoDCV(ctx, req) res, err := va.DoDCV(ctx, req)
test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem)) test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem))
test.AssertNotError(t, err, "performing validation") test.AssertNotError(t, err, "performing validation")
@ -842,3 +855,59 @@ func TestDetailedError(t *testing.T) {
} }
} }
} }
// TestPerformValidationDnsName modifies the PerformValidationRequest to test
// backward compatibility during the transition to using an Identifier instead
// of a DnsName.
//
// TODO(#8023): Remove this after the transition is over.
func TestPerformValidationDnsName(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
identDomain string
transmogrifier func(*vapb.PerformValidationRequest)
expectErr bool
expectErrString string
expectLog string
}{
{
name: "Both Identifier and DnsName",
identDomain: "good-dns01.com",
transmogrifier: func(req *vapb.PerformValidationRequest) {
req.DnsName = "good-dns02.com"
},
expectErr: true,
expectErrString: "both Identifier and DNSName are set",
expectLog: `"Identifier":{"type":"dns","value":"good-dns01.com"}`,
},
{
name: "No Identifier",
identDomain: "good-dns01.com",
transmogrifier: func(req *vapb.PerformValidationRequest) {
req.DnsName = "good-dns02.com"
req.Identifier = nil
},
expectLog: `"Identifier":{"type":"dns","value":"good-dns02.com"}`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
va, _ := setup(nil, "", nil, nil)
// create a challenge with well known token
req := createValidationRequest(identifier.NewDNS(tc.identDomain), core.ChallengeTypeDNS01)
tc.transmogrifier(req)
res, err := va.DoDCV(context.Background(), req)
if tc.expectErr {
test.AssertDeepEquals(t, err, errors.New(tc.expectErrString))
} else {
test.AssertNotNil(t, res.GetProblem(), fmt.Sprintf("validation failed: %#v", res.Problem))
}
})
}
}