Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,11 @@ func isInternalIP(addr string) bool {
func hostOnly(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
// May be a bare IPv6 address in brackets without a port (e.g. "[::1]").
// net.SplitHostPort requires a port when brackets are present, so strip them.
if len(hostport) > 1 && hostport[0] == '[' && hostport[len(hostport)-1] == ']' {
return hostport[1 : len(hostport)-1]
}
return hostport // OK; probably had no port to begin with
}
return host
Expand All @@ -665,6 +670,8 @@ func hostOnly(hostport string) string {
// It uses DNS wildcard matching logic and is case-insensitive.
// https://tools.ietf.org/html/rfc2818#section-3.1
func MatchWildcard(subject, wildcard string) bool {
// Strip brackets from IPv6 addresses (e.g. "[::1]" from HTTP Host headers).
subject = hostOnly(subject)
subject, wildcard = strings.ToLower(subject), strings.ToLower(wildcard)
if subject == wildcard {
return true
Expand Down
35 changes: 35 additions & 0 deletions certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,37 @@ func TestSubjectQualifiesForPublicCert(t *testing.T) {
}
}

func TestHostOnly(t *testing.T) {
for i, test := range []struct {
input string
expect string
}{
// hostname without port
{"example.com", "example.com"},
// hostname with port
{"example.com:443", "example.com"},
// IPv4 without port
{"1.2.3.4", "1.2.3.4"},
// IPv4 with port
{"1.2.3.4:80", "1.2.3.4"},
// IPv6 without port and without brackets
{"::1", "::1"},
// IPv6 with port (brackets required by RFC 7230)
{"[::1]:80", "::1"},
// IPv6 without port but with brackets (Go's HTTP server format for host-only)
{"[::1]", "::1"},
// full IPv6 without port but with brackets
{"[2001:db8::1]", "2001:db8::1"},
// full IPv6 with port
{"[2001:db8::1]:8080", "2001:db8::1"},
} {
actual := hostOnly(test.input)
if actual != test.expect {
t.Errorf("Test %d: hostOnly(%q) = %q, want %q", i, test.input, actual, test.expect)
}
}
}

func TestMatchWildcard(t *testing.T) {
for i, test := range []struct {
subject, wildcard string
Expand Down Expand Up @@ -217,6 +248,10 @@ func TestMatchWildcard(t *testing.T) {
{"1.2.3.4.5.6", "*.*.*.*.*.*", true},
{"0.1.2.3.4.5.6", "*.*.*.*.*.*", false},
{"1.2.3.4", "1.2.3.*", false}, // https://tools.ietf.org/html/rfc2818#section-3.1
// Bracketed IPv6 subjects (from HTTP Host headers) must match bare IPv6 wildcards.
{"[::1]", "::1", true},
{"[2001:db8::1]", "2001:db8::1", true},
{"[::1]", "::2", false},
} {
actual := MatchWildcard(test.subject, test.wildcard)
if actual != test.expect {
Expand Down
2 changes: 2 additions & 0 deletions solvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,8 @@ func dialTCPSocket(addr string) error {
// GetACMEChallenge returns an active ACME challenge for the given identifier,
// or false if no active challenge for that identifier is known.
func GetACMEChallenge(identifier string) (Challenge, bool) {
// Strip brackets from IPv6 addresses (e.g. "[::1]" from HTTP Host headers).
identifier = hostOnly(identifier)
activeChallengesMu.Lock()
chalData, ok := activeChallenges[identifier]
activeChallengesMu.Unlock()
Expand Down
22 changes: 22 additions & 0 deletions solvers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,25 @@ func Test_challengeKey(t *testing.T) {
})
}
}

func TestGetACMEChallenge_IPv6Brackets(t *testing.T) {
// Store a challenge under a bare IPv6 identifier (as CertMagic does internally).
bare := "::1"
activeChallengesMu.Lock()
activeChallenges[bare] = Challenge{}
activeChallengesMu.Unlock()
defer func() {
activeChallengesMu.Lock()
delete(activeChallenges, bare)
activeChallengesMu.Unlock()
}()

// Lookup with bracketed IPv6 (as received from Go's HTTP server via r.Host).
if _, ok := GetACMEChallenge("[::1]"); !ok {
t.Error("GetACMEChallenge(\"[::1]\") should find challenge stored under \"::1\"")
}
// Lookup with bare IPv6 should still work.
if _, ok := GetACMEChallenge("::1"); !ok {
t.Error("GetACMEChallenge(\"::1\") should find challenge stored under \"::1\"")
}
}
Loading