$OpenBSD: patch-ext_openssl_lib_openssl_ssl-internal_rb,v 1.2 2015/04/15 21:58:16 jeremy Exp $

Backport fix for regression introduced by fix for CVE-2013-4073 from
ruby svn r41805.

Fix overly permissive matching of hostnames, CVE-2015-1855.
Backport of upstream revision r50294.

--- ext/openssl/lib/openssl/ssl-internal.rb.orig	Thu Jun 27 04:22:26 2013
+++ ext/openssl/lib/openssl/ssl-internal.rb	Mon Apr 13 20:09:24 2015
@@ -90,14 +90,13 @@ module OpenSSL
       should_verify_common_name = true
       cert.extensions.each{|ext|
         next if ext.oid != "subjectAltName"
-        id, ostr = OpenSSL::ASN1.decode(ext.to_der).value
+        ostr = OpenSSL::ASN1.decode(ext.to_der).value.last
         sequence = OpenSSL::ASN1.decode(ostr.value)
         sequence.value.each{|san|
           case san.tag
           when 2 # dNSName in GeneralName (RFC5280)
             should_verify_common_name = false
-            reg = Regexp.escape(san.value).gsub(/\\\*/, "[^.]+")
-            return true if /\A#{reg}\z/i =~ hostname
+            return true if verify_hostname(hostname, san.value)
           when 7 # iPAddress in GeneralName (RFC5280)
             should_verify_common_name = false
             # follows GENERAL_NAME_print() in x509v3/v3_alt.c
@@ -112,8 +111,7 @@ module OpenSSL
       if should_verify_common_name
         cert.subject.to_a.each{|oid, value|
           if oid == "CN"
-            reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+")
-            return true if /\A#{reg}\z/i =~ hostname
+            return true if verify_hostname(hostname, value)
           end
         }
       end
@@ -121,11 +119,67 @@ module OpenSSL
     end
     module_function :verify_certificate_identity
 
+    def verify_hostname(hostname, san) # :nodoc:
+      # RFC 5280, IA5String is limited to the set of ASCII characters
+      return false unless san =~ /\A[\x00-\x7f]+\z/
+      return false unless hostname =~ /\A[\x00-\x7f]+\z/
+
+      # See RFC 6125, section 6.4.1
+      # Matching is case-insensitive.
+      san_parts = san.downcase.split(".")
+
+      # TODO: this behavior should probably be more strict
+      return san == hostname if san_parts.size < 2
+
+      # Matching is case-insensitive.
+      host_parts = hostname.downcase.split(".")
+
+      # RFC 6125, section 6.4.3, subitem 2.
+      # If the wildcard character is the only character of the left-most
+      # label in the presented identifier, the client SHOULD NOT compare
+      # against anything but the left-most label of the reference
+      # identifier (e.g., *.example.com would match foo.example.com but
+      # not bar.foo.example.com or example.com).
+      return false unless san_parts.size == host_parts.size
+
+      # RFC 6125, section 6.4.3, subitem 1.
+      # The client SHOULD NOT attempt to match a presented identifier in
+      # which the wildcard character comprises a label other than the
+      # left-most label (e.g., do not match bar.*.example.net).
+      return false unless verify_wildcard(host_parts.shift, san_parts.shift)
+
+      san_parts.join(".") == host_parts.join(".")
+    end
+    module_function :verify_hostname
+
+    def verify_wildcard(domain_component, san_component) # :nodoc:
+      parts = san_component.split("*", -1)
+
+      return false if parts.size > 2
+      return san_component == domain_component if parts.size == 1
+
+      # RFC 6125, section 6.4.3, subitem 3.
+      # The client SHOULD NOT attempt to match a presented identifier
+      # where the wildcard character is embedded within an A-label or
+      # U-label of an internationalized domain name.
+      return false if domain_component.start_with?("xn--") && san_component != "*"
+
+      parts[0].length + parts[1].length < domain_component.length &&
+      domain_component.start_with?(parts[0]) &&
+      domain_component.end_with?(parts[1])
+    end
+    module_function :verify_wildcard
+
     class SSLSocket
       include Buffering
       include SocketForwarder
       include Nonblock
 
+      ##
+      # Perform hostname verification after an SSL connection is established
+      #
+      # This method MUST be called after calling #connect to ensure that the
+      # hostname of a remote peer has been verified.
       def post_connection_check(hostname)
         unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname)
           raise SSLError, "hostname was not match with the server certificate"
