Skip to content

Commit c1ce0c8

Browse files
authored
identifier, policy, va: Remove/reject scope zone from IPv6 addresses (#8294)
Followup to #8293 Fixes #8292
1 parent 5b380ad commit c1ce0c8

File tree

6 files changed

+51
-2
lines changed

6 files changed

+51
-2
lines changed

identifier/identifier.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func NewIP(ip netip.Addr) ACMEIdentifier {
118118
// RFC 8738, Sec. 3: The identifier value MUST contain the textual form
119119
// of the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC
120120
// 5952, Sec. 4 for IPv6.
121-
Value: ip.String(),
121+
Value: ip.WithZone("").String(),
122122
}
123123
}
124124

identifier/identifier_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,39 @@ import (
1010
"testing"
1111
)
1212

13+
func TestNewIP(t *testing.T) {
14+
cases := []struct {
15+
name string
16+
ip netip.Addr
17+
want ACMEIdentifier
18+
}{
19+
{
20+
name: "IPv4 address",
21+
ip: netip.MustParseAddr("9.9.9.9"),
22+
want: ACMEIdentifier{Type: TypeIP, Value: "9.9.9.9"},
23+
},
24+
{
25+
name: "IPv6 address",
26+
ip: netip.MustParseAddr("fe80::cafe"),
27+
want: ACMEIdentifier{Type: TypeIP, Value: "fe80::cafe"},
28+
},
29+
{
30+
name: "IPv6 address with scope zone",
31+
ip: netip.MustParseAddr("fe80::cafe%lo"),
32+
want: ACMEIdentifier{Type: TypeIP, Value: "fe80::cafe"},
33+
},
34+
}
35+
for _, tc := range cases {
36+
t.Run(tc.name, func(t *testing.T) {
37+
t.Parallel()
38+
got := NewIP(tc.ip)
39+
if got != tc.want {
40+
t.Errorf("NewIP(%#v) = %#v, but want %#v", tc.ip, got, tc.want)
41+
}
42+
})
43+
}
44+
}
45+
1346
// TestFromX509 tests FromCert and FromCSR, which are fromX509's public
1447
// wrappers.
1548
func TestFromX509(t *testing.T) {

policy/pa.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ func ValidDomain(domain string) error {
326326
// ValidIP checks that an IP address:
327327
// - isn't empty
328328
// - is an IPv4 or IPv6 address
329+
// - doesn't contain a scope zone (RFC 4007)
329330
// - isn't in an IANA special-purpose address registry
330331
//
331332
// It does NOT ensure that the IP address is absent from any PA blocked lists.
@@ -340,7 +341,7 @@ func ValidIP(ip string) error {
340341
// 5952, Sec. 4 for IPv6.") ParseAddr() will accept a non-compliant but
341342
// otherwise valid string; String() will output a compliant string.
342343
parsedIP, err := netip.ParseAddr(ip)
343-
if err != nil || parsedIP.String() != ip {
344+
if err != nil || parsedIP.WithZone("").String() != ip {
344345
return errIPInvalid
345346
}
346347

@@ -477,6 +478,7 @@ func (pa *AuthorityImpl) WillingToIssue(idents identifier.ACMEIdentifiers) error
477478
//
478479
// For IP identifiers:
479480
// - MUST match the syntax of an IP address
481+
// - MUST NOT contain a scope zone (RFC 4007)
480482
// - MUST NOT be in an IANA special-purpose address registry
481483
//
482484
// If multiple identifiers are invalid, the error will contain suberrors

policy/pa_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ func TestWellFormedIdentifiers(t *testing.T) {
136136
{identifier.ACMEIdentifier{Type: "ip", Value: `1.1.168.192.in-addr.arpa`}, errIPInvalid}, // reverse DNS
137137

138138
// Unexpected IPv6 variants
139+
{identifier.ACMEIdentifier{Type: "ip", Value: `2602:80a:6000:abad:cafe::1%lo`}, errIPInvalid}, // scope zone (RFC 4007)
140+
{identifier.ACMEIdentifier{Type: "ip", Value: `2602:80a:6000:abad:cafe::1%`}, errIPInvalid}, // empty scope zone (RFC 4007)
139141
{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:deed:ffff`}, errIPInvalid}, // extra octet
140142
{identifier.ACMEIdentifier{Type: "ip", Value: `3fff:aaa:a:c0ff:ee:a:bad:mead`}, errIPInvalid}, // character out of range
141143
{identifier.ACMEIdentifier{Type: "ip", Value: `2001:db8::/32`}, errIPInvalid}, // with CIDR

va/http.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@ func (va *ValidationAuthorityImpl) extractRequestTarget(req *http.Request) (iden
338338

339339
reqIP, err := netip.ParseAddr(reqHost)
340340
if err == nil {
341+
// Reject IPv6 addresses with a scope zone (RFCs 4007 & 6874)
342+
if reqIP.Zone() != "" {
343+
return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target: contains scope zone")
344+
}
341345
err := va.isReservedIPFunc(reqIP)
342346
if err != nil {
343347
return identifier.ACMEIdentifier{}, 0, berrors.ConnectionFailureError("Invalid host in redirect target: %s", err)

va/http_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,14 @@ func TestExtractRequestTarget(t *testing.T) {
360360
ExpectedError: fmt.Errorf("Invalid host in redirect target: " +
361361
"IP address is in a reserved address block: [RFC9637]: Documentation"),
362362
},
363+
{
364+
Name: "bare IPv6, scope zone",
365+
Req: &http.Request{
366+
URL: mustURL("http://[::1%25lo]"),
367+
},
368+
ExpectedError: fmt.Errorf("Invalid host in redirect target: " +
369+
"contains scope zone"),
370+
},
363371
{
364372
Name: "valid HTTP redirect, explicit port",
365373
Req: &http.Request{

0 commit comments

Comments
 (0)