Skip to content

Commit cdc4eb2

Browse files
u5surfmholt
andauthored
fix: Normalization IPv6 addresses for ACME challenge (#376)
* subsumed caddyserver/caddy#7619 Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
1 parent 2cf7e08 commit cdc4eb2

4 files changed

Lines changed: 66 additions & 0 deletions

File tree

certificates.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,11 @@ func isInternalIP(addr string) bool {
651651
func hostOnly(hostport string) string {
652652
host, _, err := net.SplitHostPort(hostport)
653653
if err != nil {
654+
// May be a bare IPv6 address in brackets without a port (e.g. "[::1]").
655+
// net.SplitHostPort requires a port when brackets are present, so strip them.
656+
if len(hostport) > 1 && hostport[0] == '[' && hostport[len(hostport)-1] == ']' {
657+
return hostport[1 : len(hostport)-1]
658+
}
654659
return hostport // OK; probably had no port to begin with
655660
}
656661
return host
@@ -665,6 +670,8 @@ func hostOnly(hostport string) string {
665670
// It uses DNS wildcard matching logic and is case-insensitive.
666671
// https://tools.ietf.org/html/rfc2818#section-3.1
667672
func MatchWildcard(subject, wildcard string) bool {
673+
// Strip brackets from IPv6 addresses (e.g. "[::1]" from HTTP Host headers).
674+
subject = hostOnly(subject)
668675
subject, wildcard = strings.ToLower(subject), strings.ToLower(wildcard)
669676
if subject == wildcard {
670677
return true

certificates_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,37 @@ func TestSubjectQualifiesForPublicCert(t *testing.T) {
189189
}
190190
}
191191

192+
func TestHostOnly(t *testing.T) {
193+
for i, test := range []struct {
194+
input string
195+
expect string
196+
}{
197+
// hostname without port
198+
{"example.com", "example.com"},
199+
// hostname with port
200+
{"example.com:443", "example.com"},
201+
// IPv4 without port
202+
{"1.2.3.4", "1.2.3.4"},
203+
// IPv4 with port
204+
{"1.2.3.4:80", "1.2.3.4"},
205+
// IPv6 without port and without brackets
206+
{"::1", "::1"},
207+
// IPv6 with port (brackets required by RFC 7230)
208+
{"[::1]:80", "::1"},
209+
// IPv6 without port but with brackets (Go's HTTP server format for host-only)
210+
{"[::1]", "::1"},
211+
// full IPv6 without port but with brackets
212+
{"[2001:db8::1]", "2001:db8::1"},
213+
// full IPv6 with port
214+
{"[2001:db8::1]:8080", "2001:db8::1"},
215+
} {
216+
actual := hostOnly(test.input)
217+
if actual != test.expect {
218+
t.Errorf("Test %d: hostOnly(%q) = %q, want %q", i, test.input, actual, test.expect)
219+
}
220+
}
221+
}
222+
192223
func TestMatchWildcard(t *testing.T) {
193224
for i, test := range []struct {
194225
subject, wildcard string
@@ -217,6 +248,10 @@ func TestMatchWildcard(t *testing.T) {
217248
{"1.2.3.4.5.6", "*.*.*.*.*.*", true},
218249
{"0.1.2.3.4.5.6", "*.*.*.*.*.*", false},
219250
{"1.2.3.4", "1.2.3.*", false}, // https://tools.ietf.org/html/rfc2818#section-3.1
251+
// Bracketed IPv6 subjects (from HTTP Host headers) must match bare IPv6 wildcards.
252+
{"[::1]", "::1", true},
253+
{"[2001:db8::1]", "2001:db8::1", true},
254+
{"[::1]", "::2", false},
220255
} {
221256
actual := MatchWildcard(test.subject, test.wildcard)
222257
if actual != test.expect {

solvers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,8 @@ func dialTCPSocket(addr string) error {
768768
// GetACMEChallenge returns an active ACME challenge for the given identifier,
769769
// or false if no active challenge for that identifier is known.
770770
func GetACMEChallenge(identifier string) (Challenge, bool) {
771+
// Strip brackets from IPv6 addresses (e.g. "[::1]" from HTTP Host headers).
772+
identifier = hostOnly(identifier)
771773
activeChallengesMu.Lock()
772774
chalData, ok := activeChallenges[identifier]
773775
activeChallengesMu.Unlock()

solvers_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,25 @@ func Test_challengeKey(t *testing.T) {
155155
})
156156
}
157157
}
158+
159+
func TestGetACMEChallenge_IPv6Brackets(t *testing.T) {
160+
// Store a challenge under a bare IPv6 identifier (as CertMagic does internally).
161+
bare := "::1"
162+
activeChallengesMu.Lock()
163+
activeChallenges[bare] = Challenge{}
164+
activeChallengesMu.Unlock()
165+
defer func() {
166+
activeChallengesMu.Lock()
167+
delete(activeChallenges, bare)
168+
activeChallengesMu.Unlock()
169+
}()
170+
171+
// Lookup with bracketed IPv6 (as received from Go's HTTP server via r.Host).
172+
if _, ok := GetACMEChallenge("[::1]"); !ok {
173+
t.Error("GetACMEChallenge(\"[::1]\") should find challenge stored under \"::1\"")
174+
}
175+
// Lookup with bare IPv6 should still work.
176+
if _, ok := GetACMEChallenge("::1"); !ok {
177+
t.Error("GetACMEChallenge(\"::1\") should find challenge stored under \"::1\"")
178+
}
179+
}

0 commit comments

Comments
 (0)