diff --git a/bdns/dns.go b/bdns/dns.go index c43847bdf..ff8623d3d 100644 --- a/bdns/dns.go +++ b/bdns/dns.go @@ -34,120 +34,71 @@ func parseCidr(network string, comment string) net.IPNet { } var ( - // Private CIDRs to ignore - privateNetworks = []net.IPNet{ - // RFC1918 - // 10.0.0.0/8 - { - IP: []byte{10, 0, 0, 0}, - Mask: []byte{255, 0, 0, 0}, - }, - // 172.16.0.0/12 - { - IP: []byte{172, 16, 0, 0}, - Mask: []byte{255, 240, 0, 0}, - }, - // 192.168.0.0/16 - { - IP: []byte{192, 168, 0, 0}, - Mask: []byte{255, 255, 0, 0}, - }, - // RFC5735 - // 127.0.0.0/8 - { - IP: []byte{127, 0, 0, 0}, - Mask: []byte{255, 0, 0, 0}, - }, - // RFC1122 Section 3.2.1.3 - // 0.0.0.0/8 - { - IP: []byte{0, 0, 0, 0}, - Mask: []byte{255, 0, 0, 0}, - }, - // RFC3927 - // 169.254.0.0/16 - { - IP: []byte{169, 254, 0, 0}, - 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}, - }, + // TODO(#8040): Rebuild these as structs that track the structure of IANA's + // CSV files, for better automated handling. + // + // Private CIDRs to ignore. Sourced from: + // https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + privateV4Networks = []net.IPNet{ + 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"), + parseCidr("10.0.0.0/8", "RFC 1918: Private-Use"), + parseCidr("100.64.0.0/10", "RFC 6598: Shared Address Space"), + parseCidr("127.0.0.0/8", "RFC 1122, Section 3.2.1.3: Loopback"), + parseCidr("169.254.0.0/16", "RFC 3927: Link Local"), + parseCidr("172.16.0.0/12", "RFC 1918: Private-Use"), + 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"), + parseCidr("192.0.0.8/32", "RFC 7600: IPv4 dummy address"), + 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"), + parseCidr("192.0.0.170/32", "RFC 8880 & RFC 7050, Section 2.2: NAT64/DNS64 Discovery"), + 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)"), + parseCidr("192.31.196.0/24", "RFC 7535: AS112-v4"), + parseCidr("192.52.193.0/24", "RFC 7450: AMT"), + parseCidr("192.88.99.0/24", "RFC 7526: Deprecated (6to4 Relay Anycast)"), + parseCidr("192.168.0.0/16", "RFC 1918: Private-Use"), + parseCidr("192.175.48.0/24", "RFC 7534: Direct Delegation AS112 Service"), + parseCidr("198.18.0.0/15", "RFC 2544: Benchmarking"), + parseCidr("198.51.100.0/24", "RFC 5737: Documentation (TEST-NET-2)"), + parseCidr("203.0.113.0/24", "RFC 5737: Documentation (TEST-NET-3)"), + parseCidr("240.0.0.0/4", "RFC1112, Section 4: Reserved"), + parseCidr("255.255.255.255/32", "RFC 8190 & RFC 919, Section 7: Limited Broadcast"), + // 224.0.0.0/4 are multicast addresses as per RFC 3171. They are not + // present in the IANA registry. + parseCidr("224.0.0.0/4", "RFC 3171: Multicast Addresses"), } - // Sourced from https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml - // where Global, Source, or Destination is False + // Sourced from: + // https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml privateV6Networks = []net.IPNet{ parseCidr("::/128", "RFC 4291: Unspecified Address"), parseCidr("::1/128", "RFC 4291: Loopback 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: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::/32", "RFC 4380: TEREDO"), - parseCidr("fc00::/7", "RFC 4193: Unique-Local"), - parseCidr("fe80::/10", "RFC 4291: Section 2.5.6 Link-Scoped Unicast"), - parseCidr("ff00::/8", "RFC 4291: Section 2.7"), - // We disable validations to IPs under the 6to4 anycast prefix because - // there's too much risk of a malicious actor advertising the prefix and - // answering validations for a 6to4 host they do not control. - // https://community.letsencrypt.org/t/problems-validating-ipv6-against-host-running-6to4/18312/9 - parseCidr("2002::/16", "RFC 7526: 6to4 anycast prefix deprecated"), + parseCidr("2002::/16", "RFC 3056: 6to4"), + parseCidr("2620:4f:8000::/48", "RFC 7534: Direct Delegation AS112 Service"), + parseCidr("3fff::/20", "RFC 9637: Documentation"), + parseCidr("5f00::/16", "RFC 9602: Segment Routing (SRv6) SIDs"), + parseCidr("fc00::/7", "RFC 4193 & RFC 8190: Unique-Local"), + parseCidr("fe80::/10", "RFC 4291: Link-Local Unicast"), + // ff00::/8 are multicast addresses as per RFC 4291, Sections 2.4 & 2.7. + // They are not present in the IANA registry. + 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 { - for _, net := range privateNetworks { + for _, net := range privateV4Networks { if net.Contains(ip) { 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 } + +// 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) +} diff --git a/cmd/boulder-va/main.go b/cmd/boulder-va/main.go index 04da53c2d..e18989222 100644 --- a/cmd/boulder-va/main.go +++ b/cmd/boulder-va/main.go @@ -149,7 +149,8 @@ func main() { logger, c.VA.AccountURIPrefixes, va.PrimaryPerspective, - "") + "", + bdns.IsReservedIP) cmd.FailOnError(err, "Unable to create VA server") start, err := bgrpc.NewServer(c.VA.GRPC, logger).Add( diff --git a/cmd/remoteva/main.go b/cmd/remoteva/main.go index bf236ddc6..0dc71028f 100644 --- a/cmd/remoteva/main.go +++ b/cmd/remoteva/main.go @@ -138,7 +138,8 @@ func main() { logger, c.RVA.AccountURIPrefixes, c.RVA.Perspective, - c.RVA.RIR) + c.RVA.RIR, + bdns.IsReservedIP) cmd.FailOnError(err, "Unable to create Remote-VA server") start, err := bgrpc.NewServer(c.RVA.GRPC, logger).Add( diff --git a/core/objects.go b/core/objects.go index a17bb68ee..8a6948482 100644 --- a/core/objects.go +++ b/core/objects.go @@ -122,6 +122,8 @@ type ValidationRecord struct { URL string `json:"url,omitempty"` // Shared + // + // TODO(#7311): Replace DnsName with Identifier. DnsName string `json:"hostname,omitempty"` Port string `json:"port,omitempty"` AddressesResolved []net.IP `json:"addressesResolved,omitempty"` diff --git a/identifier/identifier.go b/identifier/identifier.go index 1b48d1c5a..dc4ae8917 100644 --- a/identifier/identifier.go +++ b/identifier/identifier.go @@ -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 // "dns" for a given domain name. func NewDNS(domain string) ACMEIdentifier { @@ -52,7 +68,10 @@ func NewDNS(domain string) ACMEIdentifier { // for a given IP address. func NewIP(ip netip.Addr) ACMEIdentifier { return ACMEIdentifier{ - Type: TypeIP, - Value: ip.StringExpanded(), + Type: TypeIP, + // 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(), } } diff --git a/va/caa.go b/va/caa.go index 5ea8f2543..893268225 100644 --- a/va/caa.go +++ b/va/caa.go @@ -41,7 +41,7 @@ func (va *ValidationAuthorityImpl) DoCAA(ctx context.Context, req *vapb.IsCAAVal logEvent := validationLogEvent{ AuthzID: req.AuthzID, Requester: req.AccountURIID, - Identifier: req.Domain, + Identifier: identifier.NewDNS(req.Domain), } challType := core.AcmeChallenge(req.ValidationMethod) diff --git a/va/caa_test.go b/va/caa_test.go index c766e88d9..a0e79c6ba 100644 --- a/va/caa_test.go +++ b/va/caa_test.go @@ -1122,18 +1122,18 @@ func TestMultiCAARechecking(t *testing.T) { } func TestCAAFailure(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() 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 { t.Fatalf("Expected CAA rejection for reserved.com, got success") } 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 { t.Fatalf("Expected CAA rejection for gonetld, got success") } diff --git a/va/dns.go b/va/dns.go index 273f1afb7..7445d4fdd 100644 --- a/va/dns.go +++ b/va/dns.go @@ -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) { if ident.Type != identifier.TypeDNS { 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 diff --git a/va/dns_test.go b/va/dns_test.go index 812e1252c..804dc1b66 100644 --- a/va/dns_test.go +++ b/va/dns_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net" + "net/netip" "testing" "time" @@ -18,7 +19,7 @@ import ( func TestDNSValidationWrong(t *testing.T) { 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 { t.Fatalf("Successful DNS validation with wrong TXT record") } @@ -29,7 +30,7 @@ func TestDNSValidationWrong(t *testing.T) { func TestDNSValidationWrongMany(t *testing.T) { 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 { t.Fatalf("Successful DNS validation with wrong TXT record") } @@ -40,7 +41,7 @@ func TestDNSValidationWrongMany(t *testing.T) { func TestDNSValidationWrongLong(t *testing.T) { 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 { t.Fatalf("Successful DNS validation with wrong TXT record") } @@ -51,12 +52,21 @@ func TestDNSValidationWrongLong(t *testing.T) { func TestDNSValidationFailure(t *testing.T) { va, _ := setup(nil, "", nil, nil) - _, err := va.validateDNS01(ctx, dnsi("localhost"), expectedKeyAuthorization) + _, err := va.validateDNS01(ctx, identifier.NewDNS("localhost"), expectedKeyAuthorization) prob := detailedError(err) 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) { var notDNS = identifier.ACMEIdentifier{ Type: identifier.IdentifierType("iris"), @@ -74,7 +84,7 @@ func TestDNSValidationInvalid(t *testing.T) { func TestDNSValidationServFail(t *testing.T) { 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) test.AssertEquals(t, prob.Type, probs.DNSProblem) @@ -94,7 +104,7 @@ func TestDNSValidationNoServer(t *testing.T) { log, nil) - _, err = va.validateDNS01(ctx, dnsi("localhost"), expectedKeyAuthorization) + _, err = va.validateDNS01(ctx, identifier.NewDNS("localhost"), expectedKeyAuthorization) prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.DNSProblem) } @@ -102,7 +112,7 @@ func TestDNSValidationNoServer(t *testing.T) { func TestDNSValidationOK(t *testing.T) { 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.") } @@ -110,7 +120,7 @@ func TestDNSValidationOK(t *testing.T) { func TestDNSValidationNoAuthorityOK(t *testing.T) { 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.") } diff --git a/va/http.go b/va/http.go index 446a2b23c..04b119ca2 100644 --- a/va/http.go +++ b/va/http.go @@ -8,6 +8,7 @@ import ( "io" "net" "net/http" + "net/netip" "net/url" "strconv" "strings" @@ -159,7 +160,7 @@ func httpTransport(df dialerFunc) *http.Transport { // httpValidationTarget bundles all of the information needed to make an HTTP-01 // validation request against a target. type httpValidationTarget struct { - // the hostname being validated + // the host being validated host string // the port for the validation request port int @@ -203,18 +204,28 @@ func (vt *httpValidationTarget) nextIP() error { // lookups fail. func (va *ValidationAuthorityImpl) newHTTPValidationTarget( ctx context.Context, - host string, + ident identifier.ACMEIdentifier, port int, path string, query string) (*httpValidationTarget, error) { - // Resolve IP addresses for the hostname - addrs, resolvers, err := va.getAddrs(ctx, host) - if err != nil { - return nil, err + var addrs []net.IP + var resolvers bdns.ResolverAddrs + 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, 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{ - host: host, + host: ident.Value, port: port, path: path, query: query, @@ -230,7 +241,7 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget( if !hasV6Addrs && !hasV4Addrs { // If there are no v6 addrs and no v4addrs there was a bug with getAddrs or // 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 { // If there are no v6 addrs and there are v4 addrs then use the first v4 // address. There's no fallback address. @@ -250,45 +261,44 @@ func (va *ValidationAuthorityImpl) newHTTPValidationTarget( 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 // 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 -// request's URL's Host is a bare IPv4 or IPv6 address and not a domain name an -// error is returned. -func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (string, int, error) { +// URL and it isn't the VA's HTTP or HTTPS port, an error is returned. +func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (identifier.ACMEIdentifier, int, error) { // A nil request is certainly not a valid redirect and has no port to extract. 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 // The redirect request must use HTTP or HTTPs protocol schemes regardless of the port.. if reqScheme != "http" && reqScheme != "https" { - return "", 0, berrors.ConnectionFailureError( + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError( "Invalid protocol scheme in redirect target. "+ `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 - // one we need to make sure its a valid port. If there isn't one we need to - // pick the port based on the reqScheme default port. - reqHost := req.URL.Host + // Try to parse an explicit port number from the request URL host. If there + // is one, we need to make sure its a valid port. If there isn't one we need + // to pick the port based on the reqScheme default port. + reqHost := req.URL.Hostname() var reqPort int - if h, p, err := net.SplitHostPort(reqHost); err == nil { - reqHost = h - reqPort, err = strconv.Atoi(p) + if req.URL.Port() != "" { + parsedPort, err := strconv.Atoi(req.URL.Port()) if err != nil { - return "", 0, err + return identifier.ACMEIdentifier{}, 0, err } // The explicit port must match the VA's configured HTTP or HTTPS port. - if reqPort != va.httpPort && reqPort != va.httpsPort { - return "", 0, berrors.ConnectionFailureError( + if parsedPort != va.httpPort && parsedPort != va.httpsPort { + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError( "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" { reqPort = va.httpPort } else if reqScheme == "https" { @@ -296,17 +306,11 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (stri } else { // This shouldn't happen but defensively return an internal server error in // 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 == "" { - return "", 0, berrors.ConnectionFailureError("Invalid empty hostname 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) + return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid empty host in redirect target") } // 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 // for this case by detecting the reqHost ending in ".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.", reqHost, ) } - if _, err := iana.ExtractSuffix(reqHost); err != nil { - return "", 0, berrors.ConnectionFailureError("Invalid hostname in redirect target, must end in IANA registered TLD") + reqIP, err := netip.ParseAddr(reqHost) + 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 @@ -371,6 +383,8 @@ func (va *ValidationAuthorityImpl) setupHTTPValidation( "host %q has no IP addresses remaining to use", target.host) } + // TODO(#8041): This could be a good place for a backstop check for reserved IP + // addresses. record.AddressUsed = targetIP dialer := &preresolvedDialer{ @@ -403,14 +417,27 @@ func fallbackErr(err error) bool { // a non-nil error and potentially some ValidationRecords are returned. func (va *ValidationAuthorityImpl) processHTTPValidation( ctx context.Context, - host string, + ident identifier.ACMEIdentifier, path string) ([]byte, []core.ValidationRecord, error) { // 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 { 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 initialURL := url.URL{ 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) { - if ident.Type != identifier.TypeDNS { - va.log.Infof("Got non-DNS identifier for HTTP validation: %s", ident) - return nil, berrors.MalformedError("Identifier type for HTTP validation was not DNS") + if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP { + 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-01 challenge was not DNS or IP") } // Perform the fetch 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 { return validationRecords, err } diff --git a/va/http_test.go b/va/http_test.go index 70488d7fa..63c11379c 100644 --- a/va/http_test.go +++ b/va/http_test.go @@ -89,7 +89,7 @@ func TestDialerTimeout(t *testing.T) { var took time.Duration for range 20 { started := time.Now() - _, _, err = va.processHTTPValidation(ctx, "unroutable.invalid", "/.well-known/acme-challenge/whatever") + _, _, err = va.processHTTPValidation(ctx, identifier.NewDNS("unroutable.invalid"), "/.well-known/acme-challenge/whatever") took = time.Since(started) if err != nil && strings.Contains(err.Error(), "network is unreachable") { continue @@ -134,31 +134,41 @@ func TestHTTPValidationTarget(t *testing.T) { // hostnames used in this test. testCases := []struct { Name string - Host string + Ident identifier.ACMEIdentifier ExpectedError error ExpectedIPs []string }{ { - Name: "No IPs for host", - Host: "always.invalid", + Name: "No IPs for DNS identifier", + Ident: identifier.NewDNS("always.invalid"), ExpectedError: berrors.DNSError("No valid IP addresses found for always.invalid"), }, { - Name: "Only IPv4 addrs for host", - Host: "some.example.com", + Name: "Only IPv4 addrs for DNS identifier", + Ident: identifier.NewDNS("some.example.com"), ExpectedIPs: []string{"127.0.0.1"}, }, { - Name: "Only IPv6 addrs for host", - Host: "ipv6.localhost", + Name: "Only IPv6 addrs for DNS identifier", + Ident: identifier.NewDNS("ipv6.localhost"), ExpectedIPs: []string{"::1"}, }, { - Name: "Both IPv6 and IPv4 addrs for host", - Host: "ipv4.and.ipv6.localhost", + Name: "Both IPv6 and IPv4 addrs for DNS identifier", + Ident: identifier.NewDNS("ipv4.and.ipv6.localhost"), // In this case we expect 1 IPv6 address first, and then 1 IPv4 address ExpectedIPs: []string{"::1", "127.0.0.1"}, }, + { + Name: "IPv4 IP address identifier", + Ident: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedIPs: []string{"127.0.0.1"}, + }, + { + Name: "IPv6 IP address identifier", + Ident: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedIPs: []string{"::1"}, + }, } const ( @@ -172,7 +182,7 @@ func TestHTTPValidationTarget(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { target, err := va.newHTTPValidationTarget( context.Background(), - tc.Host, + tc.Ident, examplePort, examplePath, exampleQuery) @@ -211,7 +221,7 @@ func TestExtractRequestTarget(t *testing.T) { Name string Req *http.Request ExpectedError error - ExpectedHost string + ExpectedIdent identifier.ACMEIdentifier ExpectedPort int }{ { @@ -236,11 +246,11 @@ func TestExtractRequestTarget(t *testing.T) { "and 443 are supported, not 9999"), }, { - Name: "invalid empty hostname", + Name: "invalid empty host", Req: &http.Request{ URL: mustURL("https:///who/needs/a/hostname?not=me"), }, - ExpectedError: errors.New("Invalid empty hostname in redirect target"), + ExpectedError: errors.New("Invalid empty host in redirect target"), }, { Name: "invalid .well-known hostname", @@ -254,47 +264,133 @@ func TestExtractRequestTarget(t *testing.T) { Req: &http.Request{ URL: mustURL("https://my.tld.is.cpu/pretty/cool/right?yeah=Ithoughtsotoo"), }, - ExpectedError: errors.New("Invalid hostname in redirect target, must end in IANA registered TLD"), + ExpectedError: errors.New("Invalid host in redirect target, must end in IANA registered TLD"), }, { - Name: "bare IP", + Name: "malformed wildcard-ish IPv4 address", Req: &http.Request{ - URL: mustURL("https://10.10.10.10"), + URL: mustURL("https://10.10.10.*"), }, - ExpectedError: fmt.Errorf(`Invalid host in redirect target "10.10.10.10". ` + - "Only domain names are supported, not IP addresses"), + ExpectedError: errors.New("Invalid host in redirect target, must end in IANA registered TLD"), + }, + { + Name: "malformed too-long IPv6 address", + Req: &http.Request{ + URL: mustURL("https://[a:b:c:d:e:f:b:a:d]"), + }, + ExpectedError: errors.New("Invalid host in redirect target, must end in IANA registered TLD"), + }, + { + Name: "bare IPv4, implicit port", + Req: &http.Request{ + URL: mustURL("http://127.0.0.1"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv4, explicit valid port", + Req: &http.Request{ + URL: mustURL("http://127.0.0.1:80"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv4, explicit invalid port", + Req: &http.Request{ + URL: mustURL("http://127.0.0.1:9999"), + }, + ExpectedError: fmt.Errorf("Invalid port in redirect target. Only ports 80 " + + "and 443 are supported, not 9999"), + }, + { + Name: "bare IPv4, HTTPS", + Req: &http.Request{ + URL: mustURL("https://127.0.0.1"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("127.0.0.1")), + ExpectedPort: 443, + }, + { + Name: "bare IPv4, reserved IP address", + Req: &http.Request{ + URL: mustURL("http://10.10.10.10"), + }, + ExpectedError: fmt.Errorf("Invalid host in redirect target, " + + "must not be a reserved IP address"), + }, + { + Name: "bare IPv6, implicit port", + Req: &http.Request{ + URL: mustURL("http://[::1]"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv6, explicit valid port", + Req: &http.Request{ + URL: mustURL("http://[::1]:80"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedPort: 80, + }, + { + Name: "bare IPv6, explicit invalid port", + Req: &http.Request{ + URL: mustURL("http://[::1]:9999"), + }, + ExpectedError: fmt.Errorf("Invalid port in redirect target. Only ports 80 " + + "and 443 are supported, not 9999"), + }, + { + Name: "bare IPv6, HTTPS", + Req: &http.Request{ + URL: mustURL("https://[::1]"), + }, + ExpectedIdent: identifier.NewIP(netip.MustParseAddr("::1")), + ExpectedPort: 443, + }, + { + Name: "bare IPv6, reserved IP address", + Req: &http.Request{ + URL: mustURL("http://[3fff:aaa:aaaa:aaaa:abad:0ff1:cec0:ffee]"), + }, + ExpectedError: fmt.Errorf("Invalid host in redirect target, " + + "must not be a reserved IP address"), }, { Name: "valid HTTP redirect, explicit port", Req: &http.Request{ URL: mustURL("http://cpu.letsencrypt.org:80"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 80, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 80, }, { Name: "valid HTTP redirect, implicit port", Req: &http.Request{ URL: mustURL("http://cpu.letsencrypt.org"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 80, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 80, }, { Name: "valid HTTPS redirect, explicit port", Req: &http.Request{ URL: mustURL("https://cpu.letsencrypt.org:443/hello.world"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 443, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 443, }, { Name: "valid HTTPS redirect, implicit port", Req: &http.Request{ URL: mustURL("https://cpu.letsencrypt.org/hello.world"), }, - ExpectedHost: "cpu.letsencrypt.org", - ExpectedPort: 443, + ExpectedIdent: identifier.NewDNS("cpu.letsencrypt.org"), + ExpectedPort: 443, }, } @@ -309,7 +405,7 @@ func TestExtractRequestTarget(t *testing.T) { } else if err == nil && tc.ExpectedError != nil { t.Errorf("Expected err %v, got nil", tc.ExpectedError) } else { - test.AssertEquals(t, host, tc.ExpectedHost) + test.AssertEquals(t, host, tc.ExpectedIdent) test.AssertEquals(t, port, tc.ExpectedPort) } }) @@ -322,7 +418,7 @@ func TestExtractRequestTarget(t *testing.T) { func TestHTTPValidationDNSError(t *testing.T) { va, mockLog := setup(nil, "", nil, nil) - _, _, prob := va.processHTTPValidation(ctx, "always.error", "/.well-known/acme-challenge/whatever") + _, _, prob := va.processHTTPValidation(ctx, identifier.NewDNS("always.error"), "/.well-known/acme-challenge/whatever") test.AssertError(t, prob, "Expected validation fetch to fail") matchingLines := mockLog.GetAllMatching(`read udp: some net error`) if len(matchingLines) != 1 { @@ -338,7 +434,7 @@ func TestHTTPValidationDNSError(t *testing.T) { func TestHTTPValidationDNSIdMismatchError(t *testing.T) { va, mockLog := setup(nil, "", nil, nil) - _, _, prob := va.processHTTPValidation(ctx, "id.mismatch", "/.well-known/acme-challenge/whatever") + _, _, prob := va.processHTTPValidation(ctx, identifier.NewDNS("id.mismatch"), "/.well-known/acme-challenge/whatever") test.AssertError(t, prob, "Expected validation fetch to fail") matchingLines := mockLog.GetAllMatching(`logDNSError ID mismatch`) if len(matchingLines) != 1 { @@ -380,7 +476,7 @@ func TestSetupHTTPValidation(t *testing.T) { mustTarget := func(t *testing.T, host string, port int, path string) *httpValidationTarget { target, err := va.newHTTPValidationTarget( context.Background(), - host, + identifier.NewDNS(host), port, path, "") @@ -487,11 +583,19 @@ func TestSetupHTTPValidation(t *testing.T) { } // A more concise version of httpSrv() that supports http.go tests -func httpTestSrv(t *testing.T) *httptest.Server { +func httpTestSrv(t *testing.T, ipv6 bool) *httptest.Server { t.Helper() mux := http.NewServeMux() server := httptest.NewUnstartedServer(mux) + if ipv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + server.Listener = l + } + server.Start() httpPort := getPort(server) @@ -556,11 +660,20 @@ func httpTestSrv(t *testing.T) *httptest.Server { }) // A path that always redirects to a URL with a bare IP address - mux.HandleFunc("/redir-bad-host", func(resp http.ResponseWriter, req *http.Request) { + mux.HandleFunc("/redir-bare-ipv4", func(resp http.ResponseWriter, req *http.Request) { http.Redirect( resp, req, - "https://127.0.0.1", + "http://127.0.0.1/ok", + http.StatusMovedPermanently, + ) + }) + + mux.HandleFunc("/redir-bare-ipv6", func(resp http.ResponseWriter, req *http.Request) { + http.Redirect( + resp, + req, + "http://[::1]/ok", http.StatusMovedPermanently, ) }) @@ -739,16 +852,20 @@ func TestFallbackErr(t *testing.T) { } func TestFetchHTTP(t *testing.T) { - // Create a test server - testSrv := httpTestSrv(t) - defer testSrv.Close() + // Create test servers + testSrvIPv4 := httpTestSrv(t, false) + defer testSrvIPv4.Close() + testSrvIPv6 := httpTestSrv(t, true) + defer testSrvIPv6.Close() - // Setup a VA. By providing the testSrv to setup the VA will use the testSrv's + // Setup VAs. By providing the testSrv to setup the VA will use the testSrv's // randomly assigned port as its HTTP port. - va, _ := setup(testSrv, "", nil, nil) + vaIPv4, _ := setup(testSrvIPv4, "", nil, nil) + vaIPv6, _ := setup(testSrvIPv6, "", nil, nil) // We need to know the randomly assigned HTTP port for testcases as well - httpPort := getPort(testSrv) + httpPortIPv4 := getPort(testSrvIPv4) + httpPortIPv6 := getPort(testSrvIPv6) // For the looped test case we expect one validation record per redirect // until boulder detects that a url has been used twice indicating a @@ -762,12 +879,12 @@ func TestFetchHTTP(t *testing.T) { // The first request will not have a port # in the URL. url := "http://example.com/loop" if i != 0 { - url = fmt.Sprintf("http://example.com:%d/loop", httpPort) + url = fmt.Sprintf("http://example.com:%d/loop", httpPortIPv4) } expectedLoopRecords = append(expectedLoopRecords, core.ValidationRecord{ DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: url, AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -783,12 +900,12 @@ func TestFetchHTTP(t *testing.T) { // The first request will not have a port # in the URL. url := "http://example.com/max-redirect/0" if i != 0 { - url = fmt.Sprintf("http://example.com:%d/max-redirect/%d", httpPort, i) + url = fmt.Sprintf("http://example.com:%d/max-redirect/%d", httpPortIPv4, i) } expectedTooManyRedirRecords = append(expectedTooManyRedirRecords, core.ValidationRecord{ DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: url, AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -803,16 +920,17 @@ func TestFetchHTTP(t *testing.T) { testCases := []struct { Name string - Host string + IPv6 bool + Ident identifier.ACMEIdentifier Path string ExpectedBody string ExpectedRecords []core.ValidationRecord ExpectedProblem *probs.ProblemDetails }{ { - Name: "No IPs for host", - Host: "always.invalid", - Path: "/.well-known/whatever", + Name: "No IPs for host", + Ident: identifier.NewDNS("always.invalid"), + Path: "/.well-known/whatever", ExpectedProblem: probs.DNS( "No valid IP addresses found for always.invalid"), // There are no validation records in this case because the base record @@ -820,16 +938,16 @@ func TestFetchHTTP(t *testing.T) { ExpectedRecords: nil, }, { - Name: "Timeout for host with standard ACME allowed port", - Host: "example.com", - Path: "/timeout", + Name: "Timeout for host with standard ACME allowed port", + Ident: identifier.NewDNS("example.com"), + Path: "/timeout", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching http://example.com/timeout: " + "Timeout after connect (your server may be slow or overloaded)"), ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/timeout", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -838,43 +956,25 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "Connecting to bad port", - Host: "example.com:" + strconv.Itoa(httpPort), - Path: "/timeout", - ExpectedProblem: probs.Connection( - "127.0.0.1: Fetching http://example.com:" + strconv.Itoa(httpPort) + "/timeout: " + - "Error getting validation data"), - ExpectedRecords: []core.ValidationRecord{ - { - DnsName: "example.com:" + strconv.Itoa(httpPort), - Port: strconv.Itoa(httpPort), - URL: "http://example.com:" + strconv.Itoa(httpPort) + "/timeout", - AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, - AddressUsed: net.ParseIP("127.0.0.1"), - ResolverAddrs: []string{"MockClient"}, - }, - }, - }, - { - Name: "Redirect loop", - Host: "example.com", - Path: "/loop", + Name: "Redirect loop", + Ident: identifier.NewDNS("example.com"), + Path: "/loop", ExpectedProblem: probs.Connection(fmt.Sprintf( - "127.0.0.1: Fetching http://example.com:%d/loop: Redirect loop detected", httpPort)), + "127.0.0.1: Fetching http://example.com:%d/loop: Redirect loop detected", httpPortIPv4)), ExpectedRecords: expectedLoopRecords, }, { - Name: "Too many redirects", - Host: "example.com", - Path: "/max-redirect/0", + Name: "Too many redirects", + Ident: identifier.NewDNS("example.com"), + Path: "/max-redirect/0", ExpectedProblem: probs.Connection(fmt.Sprintf( - "127.0.0.1: Fetching http://example.com:%d/max-redirect/12: Too many redirects", httpPort)), + "127.0.0.1: Fetching http://example.com:%d/max-redirect/12: Too many redirects", httpPortIPv4)), ExpectedRecords: expectedTooManyRedirRecords, }, { - Name: "Redirect to bad protocol", - Host: "example.com", - Path: "/redir-bad-proto", + Name: "Redirect to bad protocol", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-bad-proto", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching gopher://example.com: Invalid protocol scheme in " + `redirect target. Only "http" and "https" protocol schemes ` + @@ -882,7 +982,7 @@ func TestFetchHTTP(t *testing.T) { ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-bad-proto", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -891,16 +991,16 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "Redirect to bad port", - Host: "example.com", - Path: "/redir-bad-port", + Name: "Redirect to bad port", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-bad-port", ExpectedProblem: probs.Connection(fmt.Sprintf( "127.0.0.1: Fetching https://example.com:1987: Invalid port in redirect target. "+ - "Only ports %d and 443 are supported, not 1987", httpPort)), + "Only ports %d and 443 are supported, not 1987", httpPortIPv4)), ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-bad-port", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -909,33 +1009,61 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "Redirect to bad host (bare IP address)", - Host: "example.com", - Path: "/redir-bad-host", - ExpectedProblem: probs.Connection( - "127.0.0.1: Fetching https://127.0.0.1: Invalid host in redirect target " + - `"127.0.0.1". Only domain names are supported, not IP addresses`), + Name: "Redirect to bare IPv4 address", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-bare-ipv4", + ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), - URL: "http://example.com/redir-bad-host", + Port: strconv.Itoa(httpPortIPv4), + URL: "http://example.com/redir-bare-ipv4", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), ResolverAddrs: []string{"MockClient"}, }, + { + DnsName: "127.0.0.1", + Port: strconv.Itoa(httpPortIPv4), + URL: "http://127.0.0.1/ok", + AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, + AddressUsed: net.ParseIP("127.0.0.1"), + }, + }, + }, { + Name: "Redirect to bare IPv6 address", + IPv6: true, + Ident: identifier.NewDNS("ipv6.localhost"), + Path: "/redir-bare-ipv6", + ExpectedBody: "ok", + ExpectedRecords: []core.ValidationRecord{ + { + DnsName: "ipv6.localhost", + Port: strconv.Itoa(httpPortIPv6), + URL: "http://ipv6.localhost/redir-bare-ipv6", + AddressesResolved: []net.IP{net.ParseIP("::1")}, + AddressUsed: net.ParseIP("::1"), + ResolverAddrs: []string{"MockClient"}, + }, + { + DnsName: "::1", + Port: strconv.Itoa(httpPortIPv6), + URL: "http://[::1]/ok", + AddressesResolved: []net.IP{net.ParseIP("::1")}, + AddressUsed: net.ParseIP("::1"), + }, }, }, { - Name: "Redirect to long path", - Host: "example.com", - Path: "/redir-path-too-long", + Name: "Redirect to long path", + Ident: identifier.NewDNS("example.com"), + Path: "/redir-path-too-long", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching https://example.com/this-is-too-long-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789: Redirect target too long"), ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-path-too-long", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -944,15 +1072,15 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "Wrong HTTP status code", - Host: "example.com", - Path: "/bad-status-code", + Name: "Wrong HTTP status code", + Ident: identifier.NewDNS("example.com"), + Path: "/bad-status-code", ExpectedProblem: probs.Unauthorized( "127.0.0.1: Invalid response from http://example.com/bad-status-code: 410"), ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/bad-status-code", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -961,15 +1089,15 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "HTTP status code 303 redirect", - Host: "example.com", - Path: "/303-see-other", + Name: "HTTP status code 303 redirect", + Ident: identifier.NewDNS("example.com"), + Path: "/303-see-other", ExpectedProblem: probs.Connection( "127.0.0.1: Fetching http://example.org/303-see-other: received disallowed redirect status code"), ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/303-see-other", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -978,16 +1106,16 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "Response too large", - Host: "example.com", - Path: "/resp-too-big", + Name: "Response too large", + Ident: identifier.NewDNS("example.com"), + Path: "/resp-too-big", ExpectedProblem: probs.Unauthorized(fmt.Sprintf( "127.0.0.1: Invalid response from http://example.com/resp-too-big: %q", expectedTruncatedResp.String(), )), ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/resp-too-big", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -996,15 +1124,15 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "Broken IPv6 only", - Host: "ipv6.localhost", - Path: "/ok", + Name: "Broken IPv6 only", + Ident: identifier.NewDNS("ipv6.localhost"), + Path: "/ok", ExpectedProblem: probs.Connection( "::1: Fetching http://ipv6.localhost/ok: Connection refused"), ExpectedRecords: []core.ValidationRecord{ { DnsName: "ipv6.localhost", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://ipv6.localhost/ok", AddressesResolved: []net.IP{net.ParseIP("::1")}, AddressUsed: net.ParseIP("::1"), @@ -1014,13 +1142,13 @@ func TestFetchHTTP(t *testing.T) { }, { Name: "Dual homed w/ broken IPv6, working IPv4", - Host: "ipv4.and.ipv6.localhost", + Ident: identifier.NewDNS("ipv4.and.ipv6.localhost"), Path: "/ok", ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { DnsName: "ipv4.and.ipv6.localhost", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://ipv4.and.ipv6.localhost/ok", AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")}, // The first validation record should have used the IPv6 addr @@ -1029,7 +1157,7 @@ func TestFetchHTTP(t *testing.T) { }, { DnsName: "ipv4.and.ipv6.localhost", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://ipv4.and.ipv6.localhost/ok", AddressesResolved: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")}, // The second validation record should have used the IPv4 addr as a fallback @@ -1040,13 +1168,13 @@ func TestFetchHTTP(t *testing.T) { }, { Name: "Working IPv4 only", - Host: "example.com", + Ident: identifier.NewDNS("example.com"), Path: "/ok", ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/ok", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -1056,13 +1184,13 @@ func TestFetchHTTP(t *testing.T) { }, { Name: "Redirect to uppercase Public Suffix", - Host: "example.com", + Ident: identifier.NewDNS("example.com"), Path: "/redir-uppercase-publicsuffix", ExpectedBody: "ok", ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/redir-uppercase-publicsuffix", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -1070,7 +1198,7 @@ func TestFetchHTTP(t *testing.T) { }, { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/ok", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -1079,9 +1207,9 @@ func TestFetchHTTP(t *testing.T) { }, }, { - Name: "Reflected response body containing printf verbs", - Host: "example.com", - Path: "/printf-verbs", + Name: "Reflected response body containing printf verbs", + Ident: identifier.NewDNS("example.com"), + Path: "/printf-verbs", ExpectedProblem: &probs.ProblemDetails{ Type: probs.UnauthorizedProblem, Detail: fmt.Sprintf("127.0.0.1: Invalid response from http://example.com/printf-verbs: %q", @@ -1091,7 +1219,7 @@ func TestFetchHTTP(t *testing.T) { ExpectedRecords: []core.ValidationRecord{ { DnsName: "example.com", - Port: strconv.Itoa(httpPort), + Port: strconv.Itoa(httpPortIPv4), URL: "http://example.com/printf-verbs", AddressesResolved: []net.IP{net.ParseIP("127.0.0.1")}, AddressUsed: net.ParseIP("127.0.0.1"), @@ -1105,7 +1233,14 @@ func TestFetchHTTP(t *testing.T) { t.Run(tc.Name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) defer cancel() - body, records, err := va.processHTTPValidation(ctx, tc.Host, tc.Path) + var body []byte + var records []core.ValidationRecord + var err error + if tc.IPv6 { + body, records, err = vaIPv6.processHTTPValidation(ctx, tc.Ident, tc.Path) + } else { + body, records, err = vaIPv4.processHTTPValidation(ctx, tc.Ident, tc.Path) + } if tc.ExpectedProblem == nil { test.AssertNotError(t, err, "expected nil prob") } else { @@ -1138,7 +1273,7 @@ const pathLooper = "looper" const pathValid = "valid" const rejectUserAgent = "rejectMe" -func httpSrv(t *testing.T, token string) *httptest.Server { +func httpSrv(t *testing.T, token string, ipv6 bool) *httptest.Server { m := http.NewServeMux() server := httptest.NewUnstartedServer(m) @@ -1179,7 +1314,7 @@ func httpSrv(t *testing.T, token string) *httptest.Server { port := getPort(server) http.Redirect(w, r, fmt.Sprintf("http://other.valid.com:%d/path", port), http.StatusFound) } else if strings.HasSuffix(r.URL.Path, pathReLookupInvalid) { - t.Logf("HTTPSRV: Got a redirect req to an invalid hostname\n") + t.Logf("HTTPSRV: Got a redirect req to an invalid host\n") http.Redirect(w, r, "http://invalid.invalid/path", http.StatusFound) } else if strings.HasSuffix(r.URL.Path, pathRedirectToFailingURL) { t.Logf("HTTPSRV: Redirecting to a URL that will fail\n") @@ -1208,12 +1343,20 @@ func httpSrv(t *testing.T, token string) *httptest.Server { } }) + if ipv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + server.Listener = l + } + server.Start() return server } func TestHTTPBadPort(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() va, _ := setup(hs, "", nil, nil) @@ -1224,7 +1367,7 @@ func TestHTTPBadPort(t *testing.T) { badPort := 40000 + mrand.IntN(25000) va.httpPort = badPort - _, err := va.validateHTTP01(ctx, dnsi("localhost"), expectedToken, expectedKeyAuthorization) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), expectedToken, expectedKeyAuthorization) if err == nil { t.Fatalf("Server's down; expected refusal. Where did we connect?") } @@ -1235,6 +1378,23 @@ func TestHTTPBadPort(t *testing.T) { } } +func TestHTTPBadIdentifier(t *testing.T) { + hs := httpSrv(t, expectedToken, false) + defer hs.Close() + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateHTTP01(ctx, identifier.ACMEIdentifier{Type: "smime", Value: "dobber@bad.horse"}, expectedToken, expectedKeyAuthorization) + if err == nil { + t.Fatalf("Server accepted a hypothetical S/MIME identifier") + } + prob := detailedError(err) + test.AssertEquals(t, prob.Type, probs.MalformedProblem) + if !strings.Contains(prob.Detail, "Identifier type for HTTP-01 challenge was not DNS or IP") { + t.Errorf("Expected an identifier type error, got %q", prob.Detail) + } +} + func TestHTTPKeyAuthorizationFileMismatch(t *testing.T) { m := http.NewServeMux() hs := httptest.NewUnstartedServer(m) @@ -1244,7 +1404,7 @@ func TestHTTPKeyAuthorizationFileMismatch(t *testing.T) { hs.Start() va, _ := setup(hs, "", nil, nil) - _, err := va.validateHTTP01(ctx, dnsi("localhost.com"), expectedToken, expectedKeyAuthorization) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), expectedToken, expectedKeyAuthorization) if err == nil { t.Fatalf("Expected validation to fail when file mismatched.") @@ -1256,19 +1416,26 @@ func TestHTTPKeyAuthorizationFileMismatch(t *testing.T) { } func TestHTTP(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() va, log := setup(hs, "", nil, nil) - _, err := va.validateHTTP01(ctx, dnsi("localhost.com"), expectedToken, expectedKeyAuthorization) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), expectedToken, expectedKeyAuthorization) if err != nil { - t.Errorf("Unexpected failure in HTTP validation: %s", err) + t.Errorf("Unexpected failure in HTTP validation for DNS: %s", err) } test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), path404, ka(path404)) + _, err = va.validateHTTP01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), expectedToken, expectedKeyAuthorization) + if err != nil { + t.Errorf("Unexpected failure in HTTP validation for IPv4: %s", err) + } + test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) + + log.Clear() + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), path404, ka(path404)) if err == nil { t.Fatalf("Should have found a 404 for the challenge.") } @@ -1278,7 +1445,7 @@ func TestHTTP(t *testing.T) { log.Clear() // The "wrong token" will actually be the expectedToken. It's wrong // because it doesn't match pathWrongToken. - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathWrongToken, ka(pathWrongToken)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathWrongToken, ka(pathWrongToken)) if err == nil { t.Fatalf("Should have found the wrong token value.") } @@ -1287,7 +1454,7 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathMoved, ka(pathMoved)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathMoved, ka(pathMoved)) if err != nil { t.Fatalf("Failed to follow http.StatusMovedPermanently redirect") } @@ -1296,7 +1463,7 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, len(matchedValidRedirect), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathFound, ka(pathFound)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathFound, ka(pathFound)) if err != nil { t.Fatalf("Failed to follow http.StatusFound redirect") } @@ -1305,12 +1472,6 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, len(matchedValidRedirect), 1) test.AssertEquals(t, len(matchedMovedRedirect), 1) - _, err = va.validateHTTP01(ctx, identifier.NewIP(netip.MustParseAddr("127.0.0.1")), pathFound, ka(pathFound)) - if err == nil { - t.Fatalf("IdentifierType IP shouldn't have worked.") - } - test.AssertErrorIs(t, err, berrors.Malformed) - _, err = va.validateHTTP01(ctx, identifier.NewDNS("always.invalid"), pathFound, ka(pathFound)) if err == nil { t.Fatalf("Domain name is invalid.") @@ -1319,8 +1480,21 @@ func TestHTTP(t *testing.T) { test.AssertEquals(t, prob.Type, probs.DNSProblem) } +func TestHTTPIPv6(t *testing.T) { + hs := httpSrv(t, expectedToken, true) + defer hs.Close() + + va, log := setup(hs, "", nil, nil) + + _, err := va.validateHTTP01(ctx, identifier.NewIP(netip.MustParseAddr("::1")), expectedToken, expectedKeyAuthorization) + if err != nil { + t.Errorf("Unexpected failure in HTTP validation for IPv6: %s", err) + } + test.AssertEquals(t, len(log.GetAllMatching(`\[AUDIT\] `)), 1) +} + func TestHTTPTimeout(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() va, _ := setup(hs, "", nil, nil) @@ -1329,7 +1503,7 @@ func TestHTTPTimeout(t *testing.T) { timeout := 250 * time.Millisecond ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - _, err := va.validateHTTP01(ctx, dnsi("localhost"), pathWaitLong, ka(pathWaitLong)) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), pathWaitLong, ka(pathWaitLong)) if err == nil { t.Fatalf("Connection should've timed out") } @@ -1349,11 +1523,11 @@ func TestHTTPTimeout(t *testing.T) { } func TestHTTPRedirectLookup(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() va, log := setup(hs, "", nil, nil) - _, err := va.validateHTTP01(ctx, dnsi("localhost.com"), pathMoved, ka(pathMoved)) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathMoved, ka(pathMoved)) if err != nil { t.Fatalf("Unexpected failure in redirect (%s): %s", pathMoved, err) } @@ -1363,7 +1537,7 @@ func TestHTTPRedirectLookup(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 2) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathFound, ka(pathFound)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathFound, ka(pathFound)) if err != nil { t.Fatalf("Unexpected failure in redirect (%s): %s", pathFound, err) } @@ -1373,14 +1547,14 @@ func TestHTTPRedirectLookup(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 3) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathReLookupInvalid, ka(pathReLookupInvalid)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathReLookupInvalid, ka(pathReLookupInvalid)) test.AssertError(t, err, "error for pathReLookupInvalid should not be nil") test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for localhost.com: \[127.0.0.1\]`)), 1) prob := detailedError(err) - test.AssertDeepEquals(t, prob, probs.Connection(`127.0.0.1: Fetching http://invalid.invalid/path: Invalid hostname in redirect target, must end in IANA registered TLD`)) + test.AssertDeepEquals(t, prob, probs.Connection(`127.0.0.1: Fetching http://invalid.invalid/path: Invalid host in redirect target, must end in IANA registered TLD`)) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathReLookup, ka(pathReLookup)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathReLookup, ka(pathReLookup)) if err != nil { t.Fatalf("Unexpected error in redirect (%s): %s", pathReLookup, err) } @@ -1390,7 +1564,7 @@ func TestHTTPRedirectLookup(t *testing.T) { test.AssertEquals(t, len(log.GetAllMatching(`Resolved addresses for other.valid.com: \[127.0.0.1\]`)), 1) log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathRedirectInvalidPort, ka(pathRedirectInvalidPort)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathRedirectInvalidPort, ka(pathRedirectInvalidPort)) test.AssertNotNil(t, err, "error for pathRedirectInvalidPort should not be nil") prob = detailedError(err) test.AssertEquals(t, prob.Detail, fmt.Sprintf( @@ -1401,7 +1575,7 @@ func TestHTTPRedirectLookup(t *testing.T) { // HTTP 500 errors. The test case is ensuring that the connection error // is referencing the redirected to host, instead of the original host. log.Clear() - _, err = va.validateHTTP01(ctx, dnsi("localhost.com"), pathRedirectToFailingURL, ka(pathRedirectToFailingURL)) + _, err = va.validateHTTP01(ctx, identifier.NewDNS("localhost.com"), pathRedirectToFailingURL, ka(pathRedirectToFailingURL)) test.AssertNotNil(t, err, "err should not be nil") prob = detailedError(err) test.AssertDeepEquals(t, prob, @@ -1411,28 +1585,28 @@ func TestHTTPRedirectLookup(t *testing.T) { } func TestHTTPRedirectLoop(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() va, _ := setup(hs, "", nil, nil) - _, prob := va.validateHTTP01(ctx, dnsi("localhost"), "looper", ka("looper")) + _, prob := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), "looper", ka("looper")) if prob == nil { t.Fatalf("Challenge should have failed for looper") } } func TestHTTPRedirectUserAgent(t *testing.T) { - hs := httpSrv(t, expectedToken) + hs := httpSrv(t, expectedToken, false) defer hs.Close() va, _ := setup(hs, "", nil, nil) va.userAgent = rejectUserAgent - _, prob := va.validateHTTP01(ctx, dnsi("localhost"), pathMoved, ka(pathMoved)) + _, prob := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), pathMoved, ka(pathMoved)) if prob == nil { t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathMoved) } - _, prob = va.validateHTTP01(ctx, dnsi("localhost"), pathFound, ka(pathFound)) + _, prob = va.validateHTTP01(ctx, identifier.NewDNS("localhost"), pathFound, ka(pathFound)) if prob == nil { t.Fatalf("Challenge with rejectUserAgent should have failed (%s).", pathFound) } @@ -1457,23 +1631,23 @@ func getPort(hs *httptest.Server) int { func TestValidateHTTP(t *testing.T) { token := core.NewToken() - hs := httpSrv(t, token) + hs := httpSrv(t, token, false) defer hs.Close() va, _ := setup(hs, "", nil, nil) - _, prob := va.validateHTTP01(ctx, dnsi("localhost"), token, ka(token)) + _, prob := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), token, ka(token)) test.Assert(t, prob == nil, "validation failed") } func TestLimitedReader(t *testing.T) { token := core.NewToken() - hs := httpSrv(t, "012345\xff67890123456789012345678901234567890123456789012345678901234567890123456789") + hs := httpSrv(t, "012345\xff67890123456789012345678901234567890123456789012345678901234567890123456789", false) va, _ := setup(hs, "", nil, nil) defer hs.Close() - _, err := va.validateHTTP01(ctx, dnsi("localhost"), token, ka(token)) + _, err := va.validateHTTP01(ctx, identifier.NewDNS("localhost"), token, ka(token)) prob := detailedError(err) test.AssertEquals(t, prob.Type, probs.UnauthorizedProblem) diff --git a/va/proto/va.pb.go b/va/proto/va.pb.go index 70ed9c5d4..e5eba6584 100644 --- a/va/proto/va.pb.go +++ b/va/proto/va.pb.go @@ -26,6 +26,9 @@ type IsCAAValidRequest struct { sizeCache protoimpl.SizeCache 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`) Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` ValidationMethod string `protobuf:"bytes,2,opt,name=validationMethod,proto3" json:"validationMethod,omitempty"` @@ -162,10 +165,13 @@ type PerformValidationRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - DnsName string `protobuf:"bytes,1,opt,name=dnsName,proto3" json:"dnsName,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"` + // Next unused field number: 6 + // TODO(#8023): dnsNames are being deprecated in favour of identifiers. + DnsName string `protobuf:"bytes,1,opt,name=dnsName,proto3" json:"dnsName,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() { @@ -207,6 +213,13 @@ func (x *PerformValidationRequest) GetDnsName() string { return "" } +func (x *PerformValidationRequest) GetIdentifier() *proto.Identifier { + if x != nil { + return x.Identifier + } + return nil +} + func (x *PerformValidationRequest) GetChallenge() *proto.Challenge { if x != nil { 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, 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, - 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, 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, - 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, - 0x67, 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, 0x23, 0x0a, - 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x76, - 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x61, 0x75, 0x74, - 0x68, 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4b, 0x65, - 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x31, - 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x72, - 0x65, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, 0x67, 0x49, - 0x44, 0x22, 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x52, - 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x62, - 0x6c, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, 0x72, 0x65, - 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, - 0x07, 0x70, 0x72, 0x6f, 0x62, 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, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x69, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x43, 0x0a, 0x02, - 0x56, 0x41, 0x12, 0x3d, 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, 0x1c, 0x2e, 0x76, 0x61, - 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, 0x2e, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, - 0x00, 0x32, 0x3f, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x38, 0x0a, 0x05, 0x44, 0x6f, 0x43, 0x41, - 0x41, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, - 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 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, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x30, + 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, + 0x12, 0x2d, 0x0a, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6c, 0x6c, + 0x65, 0x6e, 0x67, 0x65, 0x52, 0x09, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x12, + 0x23, 0x0a, 0x05, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x76, 0x61, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x05, 0x61, + 0x75, 0x74, 0x68, 0x7a, 0x12, 0x3a, 0x0a, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x22, 0x31, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x7a, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x72, 0x65, 0x67, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x72, 0x65, + 0x67, 0x49, 0x44, 0x22, 0xa8, 0x01, 0x0a, 0x10, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x63, 0x6f, + 0x72, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x6f, 0x72, + 0x64, 0x52, 0x07, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x70, 0x72, + 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x63, 0x6f, + 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, + 0x73, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x62, 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, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x10, 0x0a, 0x03, + 0x72, 0x69, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x69, 0x72, 0x32, 0x43, + 0x0a, 0x02, 0x56, 0x41, 0x12, 0x3d, 0x0a, 0x05, 0x44, 0x6f, 0x44, 0x43, 0x56, 0x12, 0x1c, 0x2e, + 0x76, 0x61, 0x2e, 0x50, 0x65, 0x72, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x76, 0x61, + 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x22, 0x00, 0x32, 0x3f, 0x0a, 0x03, 0x43, 0x41, 0x41, 0x12, 0x38, 0x0a, 0x05, 0x44, 0x6f, + 0x43, 0x41, 0x41, 0x12, 0x15, 0x2e, 0x76, 0x61, 0x2e, 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x61, 0x2e, + 0x49, 0x73, 0x43, 0x41, 0x41, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 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 ( @@ -437,24 +453,26 @@ var file_va_proto_goTypes = []interface{}{ (*AuthzMeta)(nil), // 3: va.AuthzMeta (*ValidationResult)(nil), // 4: va.ValidationResult (*proto.ProblemDetails)(nil), // 5: core.ProblemDetails - (*proto.Challenge)(nil), // 6: core.Challenge - (*proto.ValidationRecord)(nil), // 7: core.ValidationRecord + (*proto.Identifier)(nil), // 6: core.Identifier + (*proto.Challenge)(nil), // 7: core.Challenge + (*proto.ValidationRecord)(nil), // 8: core.ValidationRecord } var file_va_proto_depIdxs = []int32{ 5, // 0: va.IsCAAValidResponse.problem:type_name -> core.ProblemDetails - 6, // 1: va.PerformValidationRequest.challenge:type_name -> core.Challenge - 3, // 2: va.PerformValidationRequest.authz:type_name -> va.AuthzMeta - 7, // 3: va.ValidationResult.records:type_name -> core.ValidationRecord - 5, // 4: va.ValidationResult.problem:type_name -> core.ProblemDetails - 2, // 5: va.VA.DoDCV:input_type -> va.PerformValidationRequest - 0, // 6: va.CAA.DoCAA:input_type -> va.IsCAAValidRequest - 4, // 7: va.VA.DoDCV:output_type -> va.ValidationResult - 1, // 8: va.CAA.DoCAA:output_type -> va.IsCAAValidResponse - 7, // [7:9] is the sub-list for method output_type - 5, // [5:7] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 6, // 1: va.PerformValidationRequest.identifier:type_name -> core.Identifier + 7, // 2: va.PerformValidationRequest.challenge:type_name -> core.Challenge + 3, // 3: va.PerformValidationRequest.authz:type_name -> va.AuthzMeta + 8, // 4: va.ValidationResult.records:type_name -> core.ValidationRecord + 5, // 5: va.ValidationResult.problem:type_name -> core.ProblemDetails + 2, // 6: va.VA.DoDCV:input_type -> va.PerformValidationRequest + 0, // 7: va.CAA.DoCAA:input_type -> va.IsCAAValidRequest + 4, // 8: va.VA.DoDCV:output_type -> va.ValidationResult + 1, // 9: va.CAA.DoCAA:output_type -> va.IsCAAValidResponse + 8, // [8:10] is the sub-list for method output_type + 6, // [6:8] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension 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() } diff --git a/va/proto/va.proto b/va/proto/va.proto index d5e2019b7..2b384bd93 100644 --- a/va/proto/va.proto +++ b/va/proto/va.proto @@ -14,6 +14,9 @@ service CAA { } 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`) string domain = 1; string validationMethod = 2; @@ -29,7 +32,10 @@ message IsCAAValidResponse { } message PerformValidationRequest { + // Next unused field number: 6 + // TODO(#8023): dnsNames are being deprecated in favour of identifiers. string dnsName = 1; + core.Identifier identifier = 5; core.Challenge challenge = 2; AuthzMeta authz = 3; string expectedKeyAuthorization = 4; diff --git a/va/tlsalpn.go b/va/tlsalpn.go index 034f7e097..6684d8850 100644 --- a/va/tlsalpn.go +++ b/va/tlsalpn.go @@ -13,9 +13,12 @@ import ( "errors" "fmt" "net" + "net/netip" "strconv" "strings" + "github.com/miekg/dns" + "github.com/letsencrypt/boulder/core" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/identifier" @@ -58,28 +61,38 @@ func certAltNames(cert *x509.Certificate) []string { func (va *ValidationAuthorityImpl) tryGetChallengeCert( ctx context.Context, - identifier identifier.ACMEIdentifier, - tlsConfig *tls.Config, + ident identifier.ACMEIdentifier, ) (*x509.Certificate, *tls.ConnectionState, core.ValidationRecord, error) { - - allAddrs, resolvers, err := va.getAddrs(ctx, identifier.Value) validationRecord := core.ValidationRecord{ - DnsName: identifier.Value, - AddressesResolved: allAddrs, - Port: strconv.Itoa(va.tlsPort), - ResolverAddrs: resolvers, + DnsName: ident.Value, + Port: strconv.Itoa(va.tlsPort), } - 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 - v4, v6 := availableAddresses(allAddrs) + v4, v6 := availableAddresses(addrs) addresses := append(v4, v6...) // This shouldn't happen, but be defensive about it anyway 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 @@ -87,7 +100,7 @@ func (va *ValidationAuthorityImpl) tryGetChallengeCert( address := net.JoinHostPort(v6[0].String(), validationRecord.Port) 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 err == nil { @@ -114,27 +127,50 @@ func (va *ValidationAuthorityImpl) tryGetChallengeCert( // talking to the first IPv6 address, try the first IPv4 address validationRecord.AddressUsed = v4[0] 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 } func (va *ValidationAuthorityImpl) getChallengeCert( ctx context.Context, hostPort string, - identifier identifier.ACMEIdentifier, - config *tls.Config, + ident identifier.ACMEIdentifier, ) (*x509.Certificate, *tls.ConnectionState, error) { - va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, identifier, hostPort, config.ServerName)) - // We expect a self-signed challenge certificate, do not verify it here. - config.InsecureSkipVerify = true + var serverName string + switch ident.Type { + 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) 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) 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) if splitErr == nil && net.ParseIP(host) != nil { // 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() certs := cs.PeerCertificates 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) } for i, cert := range certs { 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 } -func checkExpectedSAN(cert *x509.Certificate, name identifier.ACMEIdentifier) error { - if len(cert.DNSNames) != 1 { - return errors.New("wrong number of dNSNames") +func checkExpectedSAN(cert *x509.Certificate, ident identifier.ACMEIdentifier) error { + var expectedSANBytes []byte + 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 { if IdCeSubjectAltName.Equal(ext.Id) { - expectedSANs, err := asn1.Marshal([]asn1.RawValue{ - {Tag: 2, Class: 2, Bytes: []byte(cert.DNSNames[0])}, - }) - if err != nil || !bytes.Equal(expectedSANs, ext.Value) { + if !bytes.Equal(ext.Value, expectedSANBytes) { 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 } @@ -205,23 +274,19 @@ func checkAcceptableExtensions(exts []pkix.Extension, requiredOIDs []asn1.Object return nil } -func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identifier identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { - if identifier.Type != "dns" { - va.log.Info(fmt.Sprintf("Identifier type for TLS-ALPN-01 was not DNS: %s", identifier)) - return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 was not DNS") +func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) { + if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP { + 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 challenge was not DNS or IP") } - cert, cs, tvr, problem := va.tryGetChallengeCert(ctx, identifier, &tls.Config{ - MinVersion: tls.VersionTLS12, - NextProtos: []string{ACMETLS1Protocol}, - ServerName: identifier.Value, - }) + cert, cs, tvr, err := va.tryGetChallengeCert(ctx, ident) // 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. validationRecords := []core.ValidationRecord{tvr} validationRecord := &validationRecords[0] - if problem != nil { - return validationRecords, problem + if err != nil { + return validationRecords, err } if cs.NegotiatedProtocol != ACMETLS1Protocol { @@ -237,11 +302,11 @@ func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, identi return berrors.UnauthorizedError( "Incorrect validation certificate for %s challenge. "+ "Requested %s from %s. %s", - core.ChallengeTypeTLSALPN01, identifier.Value, hostPort, msg) + core.ChallengeTypeTLSALPN01, ident.Value, hostPort, msg) } // 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) { return validationRecords, badCertErr( "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 - // only the dNSName being validated and no other entries. - err = checkExpectedSAN(cert, identifier) + // only the identifier being validated and no other entries. + err = checkExpectedSAN(cert, ident) if err != nil { names := strings.Join(certAltNames(cert), ", ") return validationRecords, badCertErr( diff --git a/va/tlsalpn_test.go b/va/tlsalpn_test.go index 35a3a4719..5ed5d8927 100644 --- a/va/tlsalpn_test.go +++ b/va/tlsalpn_test.go @@ -11,6 +11,7 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/hex" + "errors" "fmt" "math/big" "net" @@ -49,7 +50,7 @@ var testACMEExt = acmeExtension(IdPeAcmeIdentifier, expectedKeyAuthorization) // testTLSCert returns a ready-to-use self-signed certificate with the given // SANs and Extensions. It generates a new ECDSA key on each call. -func testTLSCert(names []string, extensions []pkix.Extension) *tls.Certificate { +func testTLSCert(names []string, ips []net.IP, extensions []pkix.Extension) *tls.Certificate { template := &x509.Certificate{ SerialNumber: big.NewInt(1337), Subject: pkix.Name{ @@ -63,6 +64,7 @@ func testTLSCert(names []string, extensions []pkix.Extension) *tls.Certificate { BasicConstraintsValid: true, DNSNames: names, + IPAddresses: ips, ExtraExtensions: extensions, } 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 // extension with our default test values. Use acmeExtension and testCert if you // need to customize the contents of that extension. -func testACMECert(names ...string) *tls.Certificate { - return testTLSCert(names, []pkix.Extension{testACMEExt}) +func testACMECert(names []string) *tls.Certificate { + return testTLSCert(names, nil, []pkix.Extension{testACMEExt}) } // tlsalpn01SrvWithCert creates a test server which will present the given // certificate when asked to do a tls-alpn-01 handshake. -func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion uint16) *httptest.Server { +func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion uint16, ipv6 bool) *httptest.Server { t.Helper() tlsConfig := &tls.Config{ Certificates: []tls.Certificate{}, ClientAuth: tls.NoClientCert, GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { + // 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 }, NextProtos: []string{"http/1.1", ACMETLS1Protocol}, @@ -104,6 +111,13 @@ func tlsalpn01SrvWithCert(t *testing.T, acmeCert *tls.Certificate, tlsVersion ui _ = conn.Close() }, } + if ipv6 { + l, err := net.Listen("tcp", "[::1]:0") + if err != nil { + panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err)) + } + hs.Listener = l + } hs.StartTLS() return hs } @@ -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 // served by the TLS server. func testTLSALPN01Srv(t *testing.T) *httptest.Server { - return tlsalpn01SrvWithCert(t, testACMECert("expected"), 0) -} - -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) + return tlsalpn01SrvWithCert(t, testACMECert([]string{"expected"}), 0, false) } func slowTLSSrv() *httptest.Server { - cert := testTLSCert([]string{"nomatter"}, nil) + cert := testTLSCert([]string{"nomatter"}, nil, nil) server := httptest.NewUnstartedServer(http.DefaultServeMux) server.TLS = &tls.Config{ NextProtos: []string{"http/1.1", ACMETLS1Protocol}, @@ -151,7 +152,7 @@ func TestTLSALPNTimeoutAfterConnect(t *testing.T) { defer cancel() started := time.Now() - _, err := va.validateTLSALPN01(ctx, dnsi("slow.server"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("slow.server"), expectedKeyAuthorization) if err == nil { 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". var err error 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") { continue } else { @@ -234,7 +235,7 @@ func TestTLSALPN01Refused(t *testing.T) { // Take down validation server and check that validation fails. hs.Close() - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { 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) // Make the server only speak HTTP. - httpOnly := httpSrv(t, "") + httpOnly := httpSrv(t, "", false) 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") prob := detailedError(err) expected := "Server only speaks HTTP, not TLS" @@ -280,7 +281,7 @@ func TestTLSError(t *testing.T) { va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { 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) - _, err := va.validateTLSALPN01(ctx, dnsi("always.invalid"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("always.invalid"), expectedKeyAuthorization) if err == nil { 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) } -func TestTLSALPN01Success(t *testing.T) { +func TestTLSALPN01SuccessDNS(t *testing.T) { hs := testTLSALPN01Srv(t) 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 { t.Errorf("Validation failed: %v", err) } @@ -393,12 +426,12 @@ func TestTLSALPN01ObsoleteFailure(t *testing.T) { // id-pe OID + 30 (acmeIdentifier) + 1 (v1) IdPeAcmeIdentifierV1Obsolete := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 30, 1} - cert := testTLSCert([]string{"expected"}, []pkix.Extension{acmeExtension(IdPeAcmeIdentifierV1Obsolete, expectedKeyAuthorization)}) - hs := tlsalpn01SrvWithCert(t, cert, 0) + cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifierV1Obsolete, expectedKeyAuthorization)}) + hs := tlsalpn01SrvWithCert(t, cert, 0, false) va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertNotNil(t, err, "expected validation to fail") test.AssertContains(t, err.Error(), "Required extension OID 1.3.6.1.5.5.7.1.31 is not present") } @@ -406,12 +439,12 @@ func TestTLSALPN01ObsoleteFailure(t *testing.T) { func TestValidateTLSALPN01BadChallenge(t *testing.T) { badKeyAuthorization := ka("bad token") - cert := testTLSCert([]string{"expected"}, []pkix.Extension{acmeExtension(IdPeAcmeIdentifier, badKeyAuthorization)}) - hs := tlsalpn01SrvWithCert(t, cert, 0) + cert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{acmeExtension(IdPeAcmeIdentifier, badKeyAuthorization)}) + hs := tlsalpn01SrvWithCert(t, cert, 0, false) va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS ALPN validation should have failed.") } @@ -432,7 +465,7 @@ func TestValidateTLSALPN01BrokenSrv(t *testing.T) { va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS ALPN validation should have failed.") } @@ -441,7 +474,7 @@ func TestValidateTLSALPN01BrokenSrv(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.TLS = &tls.Config{ Certificates: []tls.Certificate{}, @@ -455,7 +488,7 @@ func TestValidateTLSALPN01UnawareSrv(t *testing.T) { va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if err == nil { t.Fatalf("TLS ALPN validation should have failed.") } @@ -484,11 +517,11 @@ func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) { } for _, badExt := range badExtensions { - acmeCert := testTLSCert([]string{"expected"}, []pkix.Extension{badExt}) - hs := tlsalpn01SrvWithCert(t, acmeCert, 0) + acmeCert := testTLSCert([]string{"expected"}, nil, []pkix.Extension{badExt}) + hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false) va, _ := setup(hs, "", nil, nil) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) hs.Close() if err == nil { @@ -504,7 +537,7 @@ func TestValidateTLSALPN01MalformedExtnValue(t *testing.T) { } func TestTLSALPN01TLSVersion(t *testing.T) { - cert := testACMECert("expected") + cert := testACMECert([]string{"expected"}) for _, tc := range []struct { version uint16 @@ -524,11 +557,11 @@ func TestTLSALPN01TLSVersion(t *testing.T) { }, } { // 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) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) if !tc.expectError { if err != nil { t.Errorf("expected success, got: %v", err) @@ -549,24 +582,74 @@ func TestTLSALPN01TLSVersion(t *testing.T) { func TestTLSALPN01WrongName(t *testing.T) { // 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) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) 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) { // 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) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) 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) { @@ -614,11 +697,11 @@ func TestTLSALPN01NotSelfSigned(t *testing.T) { PrivateKey: eeKey, } - hs := tlsalpn01SrvWithCert(t, acmeCert, 0) + hs := tlsalpn01SrvWithCert(t, acmeCert, 0, false) 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.AssertContains(t, err.Error(), "not self-signed") @@ -632,11 +715,11 @@ func TestTLSALPN01NotSelfSigned(t *testing.T) { PrivateKey: eeKey, } - hs = tlsalpn01SrvWithCert(t, acmeCert, 0) + hs = tlsalpn01SrvWithCert(t, acmeCert, 0, false) 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.AssertContains(t, err.Error(), "not self-signed") } @@ -671,11 +754,11 @@ func TestTLSALPN01ExtraIdentifiers(t *testing.T) { PrivateKey: key, } - hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12) + hs := tlsalpn01SrvWithCert(t, acmeCert, tls.VersionTLS12, false) 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.AssertContains(t, err.Error(), "Received certificate with unexpected identifiers") } @@ -694,11 +777,11 @@ func TestTLSALPN01ExtraSANs(t *testing.T) { } 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) - _, err = va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err = va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertError(t, err, "validation should have failed") // In go >= 1.19, the TLS client library detects that the certificate has // a duplicate extension and terminates the connection itself. @@ -709,11 +792,11 @@ func TestTLSALPN01ExtraSANs(t *testing.T) { func TestTLSALPN01ExtraAcmeExtensions(t *testing.T) { // Create a cert with multiple SAN extensions 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) - _, err := va.validateTLSALPN01(ctx, dnsi("expected"), expectedKeyAuthorization) + _, err := va.validateTLSALPN01(ctx, identifier.NewDNS("expected"), expectedKeyAuthorization) test.AssertError(t, err, "validation should have failed") // In go >= 1.19, the TLS client library detects that the certificate has // a duplicate extension and terminates the connection itself. @@ -770,3 +853,15 @@ func TestAcceptableExtensions(t *testing.T) { err = checkAcceptableExtensions(okayWithUnexpectedExt, requireAcmeAndSAN) test.AssertNotError(t, err, "Correct type and number of extensions") } + +func TestTLSALPN01BadIdentifier(t *testing.T) { + hs := httpSrv(t, expectedToken, false) + defer hs.Close() + + va, _ := setup(hs, "", nil, nil) + + _, err := va.validateTLSALPN01(ctx, identifier.ACMEIdentifier{Type: "smime", Value: "dobber@bad.horse"}, expectedKeyAuthorization) + test.AssertError(t, err, "Server accepted a hypothetical S/MIME identifier") + prob := detailedError(err) + test.AssertContains(t, prob.Error(), "Identifier type for TLS-ALPN-01 challenge was not DNS or IP") +} diff --git a/va/va.go b/va/va.go index c93baed6f..fcd35a5db 100644 --- a/va/va.go +++ b/va/va.go @@ -216,6 +216,7 @@ type ValidationAuthorityImpl struct { singleDialTimeout time.Duration perspective string rir string + isReservedIPFunc func(ip net.IP) bool metrics *vaMetrics } @@ -235,6 +236,7 @@ func NewValidationAuthorityImpl( accountURIPrefixes []string, perspective string, rir string, + reservedIPChecker func(ip net.IP) bool, ) (*ValidationAuthorityImpl, error) { if len(accountURIPrefixes) == 0 { @@ -271,6 +273,7 @@ func NewValidationAuthorityImpl( singleDialTimeout: 10 * time.Second, perspective: perspective, rir: rir, + isReservedIPFunc: reservedIPChecker, } return va, nil @@ -410,13 +413,12 @@ func (va *ValidationAuthorityImpl) validateChallenge( token string, keyAuthorization string, ) ([]core.ValidationRecord, error) { - // Strip a (potential) leading wildcard token from the identifier. - ident.Value = strings.TrimPrefix(ident.Value, "*.") - switch kind { case core.ChallengeTypeHTTP01: return va.validateHTTP01(ctx, ident, token, keyAuthorization) case core.ChallengeTypeDNS01: + // Strip a (potential) leading wildcard token from the identifier. + ident.Value = strings.TrimPrefix(ident.Value, "*.") return va.validateDNS01(ctx, ident, keyAuthorization) case core.ChallengeTypeTLSALPN01: return va.validateTLSALPN01(ctx, ident, keyAuthorization) @@ -640,7 +642,7 @@ func (va *ValidationAuthorityImpl) doRemoteOperation(ctx context.Context, op rem type validationLogEvent struct { AuthzID string Requester int64 - Identifier string + Identifier identifier.ACMEIdentifier Challenge core.Challenge Error string `json:",omitempty"` InternalError string `json:",omitempty"` @@ -659,7 +661,14 @@ type validationLogEvent struct { // implements the DCV portion of Multi-Perspective Issuance Corroboration as // 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) { - 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") } @@ -683,7 +692,7 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV logEvent := validationLogEvent{ AuthzID: req.Authz.Id, Requester: req.Authz.RegID, - Identifier: req.DnsName, + Identifier: ident, Challenge: chall, } defer func() { @@ -717,7 +726,7 @@ func (va *ValidationAuthorityImpl) DoDCV(ctx context.Context, req *vapb.PerformV // was successful or not, and cannot themselves fail. records, err := va.validateChallenge( ctx, - identifier.NewDNS(req.DnsName), + ident, chall.Type, chall.Token, req.ExpectedKeyAuthorization, diff --git a/va/va_test.go b/va/va_test.go index d94e394de..27ebffcc8 100644 --- a/va/va_test.go +++ b/va/va_test.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "net/http/httptest" + "net/netip" "os" "strings" "sync" @@ -69,11 +70,6 @@ var expectedToken = "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" var expectedThumbprint = "9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI" 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 func TestMain(m *testing.M) { @@ -86,9 +82,9 @@ func TestMain(m *testing.M) { 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{ - DnsName: domain, + Identifier: ident.AsProto(), Challenge: &corepb.Challenge{ Type: string(challengeType), 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 // is MockClient{}, but can be overridden. // @@ -136,6 +147,7 @@ func setup(srv *httptest.Server, userAgent string, remoteVAs []RemoteVA, mockDNS accountURIPrefixes, perspective, "", + isNonLoopbackReservedIP, ) if err != nil { panic(fmt.Sprintf("Failed to create validation authority: %v", err)) @@ -321,6 +333,7 @@ func TestNewValidationAuthorityImplWithDuplicateRemotes(t *testing.T) { accountURIPrefixes, "example perspective", "", + isNonLoopbackReservedIP, ) test.AssertError(t, err, "NewValidationAuthorityImpl allowed duplicate remote perspectives") test.AssertContains(t, err.Error(), "duplicate remote VA perspective \"dadaist\"") @@ -343,7 +356,7 @@ func TestPerformValidationWithMismatchedRemoteVAPerspectives(t *testing.T) { remoteVAs = append(remoteVAs, mismatched1, mismatched2) 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) test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) @@ -366,7 +379,7 @@ func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) { remoteVAs = append(remoteVAs, mismatched1, mismatched2) 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) test.AssertNotNil(t, res.GetProblem(), "validation succeeded with mismatched remote VA perspectives") test.AssertEquals(t, len(mockLog.GetAllMatching("Expected perspective")), 2) @@ -375,7 +388,7 @@ func TestPerformValidationWithMismatchedRemoteVARIRs(t *testing.T) { func TestValidateMalformedChallenge(t *testing.T) { 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) test.AssertEquals(t, prob.Type, probs.MalformedProblem) @@ -385,7 +398,7 @@ func TestPerformValidationInvalid(t *testing.T) { t.Parallel() 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) test.Assert(t, res.Problem != nil, "validation succeeded") 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) defer cancel() - req := createValidationRequest("nonexistent.com", core.ChallengeTypeHTTP01) + req := createValidationRequest(identifier.NewDNS("nonexistent.com"), core.ChallengeTypeHTTP01) _, err := va.DoDCV(ctx, req) test.AssertNotError(t, err, "failed validation should not be an error") matchingLogs := mockLog.GetAllMatching( @@ -418,7 +431,7 @@ func TestPerformValidationValid(t *testing.T) { va, mockLog := setup(nil, "", nil, nil) // 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) test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed: %#v", res.Problem)) test.AssertMetricWithLabelsEquals(t, va.metrics.validationLatency, prometheus.Labels{ @@ -432,7 +445,8 @@ func TestPerformValidationValid(t *testing.T) { if len(resultLog) != 1 { 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.") } } @@ -445,7 +459,7 @@ func TestPerformValidationWildcard(t *testing.T) { va, mockLog := setup(nil, "", nil, nil) // 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 res, _ := va.DoDCV(context.Background(), req) 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 - 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.") } // We expect that the ValidationRecord contain the correct non-wildcard @@ -476,7 +490,7 @@ func TestMultiVA(t *testing.T) { t.Parallel() // Create a new challenge to use for the httpSrv - req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) + req := createValidationRequest(identifier.NewDNS("localhost"), core.ChallengeTypeHTTP01) brokenVA := RemoteClients{ VAClient: brokenRemoteVA{}, @@ -724,7 +738,7 @@ func TestMultiVAEarlyReturn(t *testing.T) { // Perform all validations start := time.Now() - req := createValidationRequest("localhost", core.ChallengeTypeHTTP01) + req := createValidationRequest(identifier.NewDNS("localhost"), core.ChallengeTypeHTTP01) res, _ := localVA.DoDCV(ctx, req) // It should always fail @@ -763,13 +777,12 @@ func TestMultiVAPolicy(t *testing.T) { localVA, _ := setupWithRemotes(ms.Server, pass, remoteConfs, nil) // 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) // It should fail if res.Problem == nil { t.Error("expected prob from PerformValidation, got nil") } - } func TestMultiVALogging(t *testing.T) { @@ -785,7 +798,7 @@ func TestMultiVALogging(t *testing.T) { defer ms.Close() 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) test.Assert(t, res.Problem == nil, fmt.Sprintf("validation failed with: %#v", res.Problem)) 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)) + } + }) + } +}