Temporarily allow common names in certificates
This commit is contained in:
parent
e01d59f8f6
commit
c4af352e87
64
LICENSE
64
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
|
Copyright (c) 2020 Adnan Maolood
|
||||||
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 above copyright notice and this permission notice shall be included in all
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
copies or substantial portions of the Software.
|
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
|
The above copyright notice and this permission notice shall be included in all
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
copies or substantial portions of the Software.
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
SOFTWARE.
|
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.
|
||||||
|
@ -210,7 +210,8 @@ func (c *Client) Send(req *Request) (*Response, error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Check that the certificate is valid for the hostname
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
// Check that the client trusts the certificate
|
// Check that the client trusts the certificate
|
||||||
|
227
verify.go
Normal file
227
verify.go
Normal file
@ -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}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user