diff --git a/LICENSE b/LICENSE index fef17d1..71ab8af 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,51 @@ -Copyright (c) 2020 Adnan Maolood +go-gemini is available under the terms of the MIT license: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Copyright (c) 2020 Adnan Maolood -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Portions of this program were taken from Go: + + Copyright (c) 2009 The Go Authors. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/client.go b/client.go index d507960..bbdbf24 100644 --- a/client.go +++ b/client.go @@ -210,7 +210,8 @@ func (c *Client) Send(req *Request) (*Response, error) { return err } // Check that the certificate is valid for the hostname - if err := cert.VerifyHostname(req.Hostname()); err != nil { + // Use our own implementation of verifyHostname + if err := verifyHostname(cert, req.Hostname()); err != nil { return err } // Check that the client trusts the certificate diff --git a/verify.go b/verify.go new file mode 100644 index 0000000..7abef8f --- /dev/null +++ b/verify.go @@ -0,0 +1,227 @@ +package gemini + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "net" + "strings" + "unicode/utf8" +) + +// Hostname verification code from the crypto/x509 package. +// Modified to allow Common Names in the short term, until new certificates +// can be issued with SANs. + +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +var ( + oidExtensionSubjectAltName = []int{2, 5, 29, 17} +) + +// oidNotInExtensions reports whether an extension with the given oid exists in +// extensions. +func oidInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) bool { + for _, e := range extensions { + if e.Id.Equal(oid) { + return true + } + } + return false +} + +func hasSANExtension(c *x509.Certificate) bool { + return oidInExtensions(oidExtensionSubjectAltName, c.Extensions) +} + +// ignoreCN disables interpreting Common Name as a hostname. See issue 24151. +// NOTE: This is set to false so that certificates with common names will still +// be supported. +var ignoreCN = false + +func validHostnamePattern(host string) bool { return validHostname(host, true) } +func validHostnameInput(host string) bool { return validHostname(host, false) } + +// validHostname reports whether host is a valid hostname that can be matched or +// matched against according to RFC 6125 2.2, with some leniency to accommodate +// legacy values. +func validHostname(host string, isPattern bool) bool { + if !isPattern { + host = strings.TrimSuffix(host, ".") + } + if len(host) == 0 { + return false + } + + for i, part := range strings.Split(host, ".") { + if part == "" { + // Empty label. + return false + } + if isPattern && i == 0 && part == "*" { + // Only allow full left-most wildcards, as those are the only ones + // we match, and matching literal '*' characters is probably never + // the expected behavior. + continue + } + for j, c := range part { + if 'a' <= c && c <= 'z' { + continue + } + if '0' <= c && c <= '9' { + continue + } + if 'A' <= c && c <= 'Z' { + continue + } + if c == '-' && j != 0 { + continue + } + if c == '_' { + // Not a valid character in hostnames, but commonly + // found in deployments outside the WebPKI. + continue + } + return false + } + } + + return true +} + +// commonNameAsHostname reports whether the Common Name field should be +// considered the hostname that the certificate is valid for. This is a legacy +// behavior, disabled by default or if the Subject Alt Name extension is present. +// +// It applies the strict validHostname check to the Common Name field, so that +// certificates without SANs can still be validated against CAs with name +// constraints if there is no risk the CN would be matched as a hostname. +// See NameConstraintsWithoutSANs and issue 24151. +func commonNameAsHostname(c *x509.Certificate) bool { + return !ignoreCN && !hasSANExtension(c) && validHostnamePattern(c.Subject.CommonName) +} + +func matchExactly(hostA, hostB string) bool { + if hostA == "" || hostA == "." || hostB == "" || hostB == "." { + return false + } + return toLowerCaseASCII(hostA) == toLowerCaseASCII(hostB) +} + +func matchHostnames(pattern, host string) bool { + pattern = toLowerCaseASCII(pattern) + host = toLowerCaseASCII(strings.TrimSuffix(host, ".")) + + if len(pattern) == 0 || len(host) == 0 { + return false + } + + patternParts := strings.Split(pattern, ".") + hostParts := strings.Split(host, ".") + + if len(patternParts) != len(hostParts) { + return false + } + + for i, patternPart := range patternParts { + if i == 0 && patternPart == "*" { + continue + } + if patternPart != hostParts[i] { + return false + } + } + + return true +} + +// toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use +// an explicitly ASCII function to avoid any sharp corners resulting from +// performing Unicode operations on DNS labels. +func toLowerCaseASCII(in string) string { + // If the string is already lower-case then there's nothing to do. + isAlreadyLowerCase := true + for _, c := range in { + if c == utf8.RuneError { + // If we get a UTF-8 error then there might be + // upper-case ASCII bytes in the invalid sequence. + isAlreadyLowerCase = false + break + } + if 'A' <= c && c <= 'Z' { + isAlreadyLowerCase = false + break + } + } + + if isAlreadyLowerCase { + return in + } + + out := []byte(in) + for i, c := range out { + if 'A' <= c && c <= 'Z' { + out[i] += 'a' - 'A' + } + } + return string(out) +} + +// verifyHostname returns nil if c is a valid certificate for the named host. +// Otherwise it returns an error describing the mismatch. +// +// IP addresses can be optionally enclosed in square brackets and are checked +// against the IPAddresses field. Other names are checked case insensitively +// against the DNSNames field. If the names are valid hostnames, the certificate +// fields can have a wildcard as the left-most label. +// +// The legacy Common Name field is ignored unless it's a valid hostname, the +// certificate doesn't have any Subject Alternative Names, and the GODEBUG +// environment variable is set to "x509ignoreCN=0". Support for Common Name is +// deprecated will be entirely removed in the future. +func verifyHostname(c *x509.Certificate, h string) error { + // IP addresses may be written in [ ]. + candidateIP := h + if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' { + candidateIP = h[1 : len(h)-1] + } + if ip := net.ParseIP(candidateIP); ip != nil { + // We only match IP addresses against IP SANs. + // See RFC 6125, Appendix B.2. + for _, candidate := range c.IPAddresses { + if ip.Equal(candidate) { + return nil + } + } + return x509.HostnameError{c, candidateIP} + } + + names := c.DNSNames + if commonNameAsHostname(c) { + names = []string{c.Subject.CommonName} + } + + candidateName := toLowerCaseASCII(h) // Save allocations inside the loop. + validCandidateName := validHostnameInput(candidateName) + + for _, match := range names { + // Ideally, we'd only match valid hostnames according to RFC 6125 like + // browsers (more or less) do, but in practice Go is used in a wider + // array of contexts and can't even assume DNS resolution. Instead, + // always allow perfect matches, and only apply wildcard and trailing + // dot processing to valid hostnames. + if validCandidateName && validHostnamePattern(match) { + if matchHostnames(match, candidateName) { + return nil + } + } else { + if matchExactly(match, candidateName) { + return nil + } + } + } + + return x509.HostnameError{c, h} +}