gemini-browser

A text-based gemini browser
git clone git://git.laack.co/gemini-browser.git
Log | Files | Refs | README

verify_hostname.go (6145B)


      1 // Source: https://git.sr.ht/~adnano/go-gemini/tree/f6b0443a6262d17f90b4e75cf5ae37577db7f897/vendor.go
      2 // No code changes were made.
      3 
      4 // Hostname verification code from the crypto/x509 package.
      5 // Modified to allow Common Names in the short term, until new certificates
      6 // can be issued with SANs.
      7 
      8 // Copyright 2011 The Go Authors. All rights reserved.
      9 // Use of this source code is governed by a BSD-style
     10 // license that can be found in the LICENSE-GO file.
     11 
     12 package gemini
     13 
     14 import (
     15 	"crypto/x509"
     16 	"net"
     17 	"strings"
     18 	"unicode/utf8"
     19 )
     20 
     21 var oidExtensionSubjectAltName = []int{2, 5, 29, 17}
     22 
     23 func hasSANExtension(c *x509.Certificate) bool {
     24 	for _, e := range c.Extensions {
     25 		if e.Id.Equal(oidExtensionSubjectAltName) {
     26 			return true
     27 		}
     28 	}
     29 	return false
     30 }
     31 
     32 func validHostnamePattern(host string) bool { return validHostname(host, true) }
     33 func validHostnameInput(host string) bool   { return validHostname(host, false) }
     34 
     35 // validHostname reports whether host is a valid hostname that can be matched or
     36 // matched against according to RFC 6125 2.2, with some leniency to accommodate
     37 // legacy values.
     38 func validHostname(host string, isPattern bool) bool {
     39 	if !isPattern {
     40 		host = strings.TrimSuffix(host, ".")
     41 	}
     42 	if len(host) == 0 {
     43 		return false
     44 	}
     45 
     46 	for i, part := range strings.Split(host, ".") {
     47 		if part == "" {
     48 			// Empty label.
     49 			return false
     50 		}
     51 		if isPattern && i == 0 && part == "*" {
     52 			// Only allow full left-most wildcards, as those are the only ones
     53 			// we match, and matching literal '*' characters is probably never
     54 			// the expected behavior.
     55 			continue
     56 		}
     57 		for j, c := range part {
     58 			if 'a' <= c && c <= 'z' {
     59 				continue
     60 			}
     61 			if '0' <= c && c <= '9' {
     62 				continue
     63 			}
     64 			if 'A' <= c && c <= 'Z' {
     65 				continue
     66 			}
     67 			if c == '-' && j != 0 {
     68 				continue
     69 			}
     70 			if c == '_' {
     71 				// Not a valid character in hostnames, but commonly
     72 				// found in deployments outside the WebPKI.
     73 				continue
     74 			}
     75 			return false
     76 		}
     77 	}
     78 
     79 	return true
     80 }
     81 
     82 // commonNameAsHostname reports whether the Common Name field should be
     83 // considered the hostname that the certificate is valid for. This is a legacy
     84 // behavior, disabled by default or if the Subject Alt Name extension is present.
     85 //
     86 // It applies the strict validHostname check to the Common Name field, so that
     87 // certificates without SANs can still be validated against CAs with name
     88 // constraints if there is no risk the CN would be matched as a hostname.
     89 // See NameConstraintsWithoutSANs and issue 24151.
     90 func commonNameAsHostname(c *x509.Certificate) bool {
     91 	return !hasSANExtension(c) && validHostnamePattern(c.Subject.CommonName)
     92 }
     93 
     94 func matchExactly(hostA, hostB string) bool {
     95 	if hostA == "" || hostA == "." || hostB == "" || hostB == "." {
     96 		return false
     97 	}
     98 	return toLowerCaseASCII(hostA) == toLowerCaseASCII(hostB)
     99 }
    100 
    101 func matchHostnames(pattern, host string) bool {
    102 	pattern = toLowerCaseASCII(pattern)
    103 	host = toLowerCaseASCII(strings.TrimSuffix(host, "."))
    104 
    105 	if len(pattern) == 0 || len(host) == 0 {
    106 		return false
    107 	}
    108 
    109 	patternParts := strings.Split(pattern, ".")
    110 	hostParts := strings.Split(host, ".")
    111 
    112 	if len(patternParts) != len(hostParts) {
    113 		return false
    114 	}
    115 
    116 	for i, patternPart := range patternParts {
    117 		if i == 0 && patternPart == "*" {
    118 			continue
    119 		}
    120 		if patternPart != hostParts[i] {
    121 			return false
    122 		}
    123 	}
    124 
    125 	return true
    126 }
    127 
    128 // toLowerCaseASCII returns a lower-case version of in. See RFC 6125 6.4.1. We use
    129 // an explicitly ASCII function to avoid any sharp corners resulting from
    130 // performing Unicode operations on DNS labels.
    131 func toLowerCaseASCII(in string) string {
    132 	// If the string is already lower-case then there's nothing to do.
    133 	isAlreadyLowerCase := true
    134 	for _, c := range in {
    135 		if c == utf8.RuneError {
    136 			// If we get a UTF-8 error then there might be
    137 			// upper-case ASCII bytes in the invalid sequence.
    138 			isAlreadyLowerCase = false
    139 			break
    140 		}
    141 		if 'A' <= c && c <= 'Z' {
    142 			isAlreadyLowerCase = false
    143 			break
    144 		}
    145 	}
    146 
    147 	if isAlreadyLowerCase {
    148 		return in
    149 	}
    150 
    151 	out := []byte(in)
    152 	for i, c := range out {
    153 		if 'A' <= c && c <= 'Z' {
    154 			out[i] += 'a' - 'A'
    155 		}
    156 	}
    157 	return string(out)
    158 }
    159 
    160 // verifyHostname returns nil if c is a valid certificate for the named host.
    161 // Otherwise it returns an error describing the mismatch.
    162 //
    163 // IP addresses can be optionally enclosed in square brackets and are checked
    164 // against the IPAddresses field. Other names are checked case insensitively
    165 // against the DNSNames field. If the names are valid hostnames, the certificate
    166 // fields can have a wildcard as the left-most label.
    167 //
    168 // The legacy Common Name field is ignored unless it's a valid hostname, the
    169 // certificate doesn't have any Subject Alternative Names, and the GODEBUG
    170 // environment variable is set to "x509ignoreCN=0". Support for Common Name is
    171 // deprecated will be entirely removed in the future.
    172 func verifyHostname(c *x509.Certificate, h string) error {
    173 	// IP addresses may be written in [ ].
    174 	candidateIP := h
    175 	if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' {
    176 		candidateIP = h[1 : len(h)-1]
    177 	}
    178 	if ip := net.ParseIP(candidateIP); ip != nil {
    179 		// We only match IP addresses against IP SANs.
    180 		// See RFC 6125, Appendix B.2.
    181 		for _, candidate := range c.IPAddresses {
    182 			if ip.Equal(candidate) {
    183 				return nil
    184 			}
    185 		}
    186 		return x509.HostnameError{c, candidateIP}
    187 	}
    188 
    189 	names := c.DNSNames
    190 	if commonNameAsHostname(c) {
    191 		names = []string{c.Subject.CommonName}
    192 	}
    193 
    194 	candidateName := toLowerCaseASCII(h) // Save allocations inside the loop.
    195 	validCandidateName := validHostnameInput(candidateName)
    196 
    197 	for _, match := range names {
    198 		// Ideally, we'd only match valid hostnames according to RFC 6125 like
    199 		// browsers (more or less) do, but in practice Go is used in a wider
    200 		// array of contexts and can't even assume DNS resolution. Instead,
    201 		// always allow perfect matches, and only apply wildcard and trailing
    202 		// dot processing to valid hostnames.
    203 		if validCandidateName && validHostnamePattern(match) {
    204 			if matchHostnames(match, candidateName) {
    205 				return nil
    206 			}
    207 		} else {
    208 			if matchExactly(match, candidateName) {
    209 				return nil
    210 			}
    211 		}
    212 	}
    213 
    214 	return x509.HostnameError{c, h}
    215 }