@@ -890,15 +890,25 @@ private static boolean isIPConstrained(Set constraints, byte[] ip)
890890 */
891891 private static boolean isIPConstrained (byte [] constraint , byte [] ip )
892892 {
893- int ipLength = ip .length ;
893+ // Normalise IPv4-mapped IPv6 (::ffff:0:0/96 per RFC 4291 sec. 2.5.5.2)
894+ // to IPv4 on BOTH sides before the length-equality pre-filter, so a
895+ // SAN that encodes the same IPv4 address using the 16-byte IPv4-
896+ // mapped IPv6 form doesn't escape an 8-byte IPv4 constraint via
897+ // the address-family-length mismatch. RFC 4291 makes the two forms
898+ // equivalent for host identification, so the normalisation is also
899+ // semantics-preserving in the permitted-subtree direction.
900+ byte [] normIp = normalizeIPv4MappedIPv6Address (ip );
901+ byte [] normConstraint = normalizeIPv4MappedIPv6Constraint (constraint );
894902
895- if (ipLength != (constraint .length / 2 ))
903+ int ipLength = normIp .length ;
904+
905+ if (ipLength != (normConstraint .length / 2 ))
896906 {
897907 return false ;
898908 }
899909
900910 byte [] subnetMask = new byte [ipLength ];
901- System .arraycopy (constraint , ipLength , subnetMask , 0 , ipLength );
911+ System .arraycopy (normConstraint , ipLength , subnetMask , 0 , ipLength );
902912
903913 byte [] permittedSubnetAddress = new byte [ipLength ];
904914
@@ -907,13 +917,84 @@ private static boolean isIPConstrained(byte[] constraint, byte[] ip)
907917 // the resulting IP address by applying the subnet mask
908918 for (int i = 0 ; i < ipLength ; i ++)
909919 {
910- permittedSubnetAddress [i ] = (byte )(constraint [i ] & subnetMask [i ]);
911- ipSubnetAddress [i ] = (byte )(ip [i ] & subnetMask [i ]);
920+ permittedSubnetAddress [i ] = (byte )(normConstraint [i ] & subnetMask [i ]);
921+ ipSubnetAddress [i ] = (byte )(normIp [i ] & subnetMask [i ]);
912922 }
913923
914924 return Arrays .areEqual (permittedSubnetAddress , ipSubnetAddress );
915925 }
916926
927+ /**
928+ * If {@code ip} is a 16-byte IPv4-mapped IPv6 address (RFC 4291
929+ * sec. 2.5.5.2: leading 80 bits zero, next 16 bits all-ones, trailing
930+ * 32 bits the IPv4 address), return the 4-byte IPv4 form; otherwise
931+ * return {@code ip} unchanged.
932+ */
933+ private static byte [] normalizeIPv4MappedIPv6Address (byte [] ip )
934+ {
935+ if (!isIPv4MappedIPv6Address (ip ))
936+ {
937+ return ip ;
938+ }
939+ byte [] ipv4 = new byte [4 ];
940+ System .arraycopy (ip , 12 , ipv4 , 0 , 4 );
941+ return ipv4 ;
942+ }
943+
944+ /**
945+ * A Name-Constraints iPAddress constraint is encoded as
946+ * {@code IP || subnet-mask}. If both halves are in IPv4-mapped IPv6
947+ * form (the IP half matches the {@code ::ffff:0:0/96} prefix and the
948+ * mask half is all-ones across the first 96 bits), reduce to the
949+ * 8-byte (4-byte IPv4 || 4-byte mask) form. Otherwise return the
950+ * constraint unchanged. The mask check matters: a mask narrower than
951+ * /96 means the constraint is really an IPv6 range that happens to
952+ * start at an IPv4-mapped address, and collapsing it to IPv4 would
953+ * change which addresses match.
954+ */
955+ private static byte [] normalizeIPv4MappedIPv6Constraint (byte [] constraint )
956+ {
957+ if (constraint .length != 32 )
958+ {
959+ return constraint ;
960+ }
961+ byte [] ipHalf = new byte [16 ];
962+ byte [] maskHalf = new byte [16 ];
963+ System .arraycopy (constraint , 0 , ipHalf , 0 , 16 );
964+ System .arraycopy (constraint , 16 , maskHalf , 0 , 16 );
965+ if (!isIPv4MappedIPv6Address (ipHalf ))
966+ {
967+ return constraint ;
968+ }
969+ for (int i = 0 ; i < 12 ; i ++)
970+ {
971+ if (maskHalf [i ] != (byte )0xff )
972+ {
973+ return constraint ;
974+ }
975+ }
976+ byte [] result = new byte [8 ];
977+ System .arraycopy (ipHalf , 12 , result , 0 , 4 );
978+ System .arraycopy (maskHalf , 12 , result , 4 , 4 );
979+ return result ;
980+ }
981+
982+ private static boolean isIPv4MappedIPv6Address (byte [] ip )
983+ {
984+ if (ip == null || ip .length != 16 )
985+ {
986+ return false ;
987+ }
988+ for (int i = 0 ; i < 10 ; i ++)
989+ {
990+ if (ip [i ] != 0 )
991+ {
992+ return false ;
993+ }
994+ }
995+ return ip [10 ] == (byte )0xff && ip [11 ] == (byte )0xff ;
996+ }
997+
917998 private static boolean isOtherNameConstrained (Set constraints , OtherName otherName )
918999 {
9191000 Iterator it = constraints .iterator ();
@@ -983,6 +1064,10 @@ private static boolean withinDomain(String testDomain, String domain)
9831064 {
9841065 domain = domain .substring (1 );
9851066 }
1067+ // Strip the RFC 1034 root-label trailing dot so the per-label
1068+ // compare doesn't see a phantom empty label.
1069+ testDomain = stripTrailingDot (testDomain );
1070+ domain = stripTrailingDot (domain );
9861071
9871072 String [] domainParts = Strings .split (domain , '.' );
9881073 String [] testDomainParts = Strings .split (testDomain , '.' );
@@ -1046,7 +1131,28 @@ private static boolean isDNSConstrained(Set constraints, String dns)
10461131
10471132 private static boolean isDNSConstrained (String constraint , String dns )
10481133 {
1049- return dns .equalsIgnoreCase (constraint ) || withinDomain (dns , constraint );
1134+ // RFC 1034 sec. 3.1 allows a trailing dot to denote the root label of
1135+ // a fully-qualified domain name. A dNSName SAN such as
1136+ // "foo.example.com." (legal IA5String per RFC 5280 sec. 4.2.1.6) used
1137+ // to escape Name-Constraint matching because withinDomain split it
1138+ // to ["foo", "example", "com", ""], misaligning the per-label
1139+ // compare against a "example.com" constraint and returning "not
1140+ // constrained" — bypassing the excluded subtree. Normalise away
1141+ // at most one trailing dot on both sides before comparing.
1142+ String normDns = stripTrailingDot (dns );
1143+ String normConstraint = stripTrailingDot (constraint );
1144+ return normDns .equalsIgnoreCase (normConstraint ) || withinDomain (normDns , normConstraint );
1145+ }
1146+
1147+ private static String stripTrailingDot (String s )
1148+ {
1149+ // length > 1 so a single bare "." (theoretically the empty-label
1150+ // root) is preserved rather than reduced to "".
1151+ if (s != null && s .length () > 1 && s .charAt (s .length () - 1 ) == '.' )
1152+ {
1153+ return s .substring (0 , s .length () - 1 );
1154+ }
1155+ return s ;
10501156 }
10511157
10521158 /**
@@ -1698,29 +1804,47 @@ private static boolean isURIConstrained(String constraint, String uri)
16981804
16991805 private static String extractHostFromURL (String url )
17001806 {
1701- // see RFC 1738
1702- // remove ':' after protocol, e.g. https:
1703- String sub = url .substring (url .indexOf (':' ) + 1 );
1704- // extract host from Common Internet Scheme Syntax, e.g. https://
1705- int slashesPos = sub .indexOf ("//" );
1706- if (slashesPos != -1 )
1807+ // RFC 3986 §3.2 authority structure:
1808+ // authority = [ userinfo "@" ] host [ ":" port ]
1809+ // The strip order is: scheme → "//" → path/query/fragment terminator → userinfo (last '@') → host
1810+ // with optional bracketed IPv6 / trailing ":port".
1811+ String sub = url ;
1812+ int schemeEnd = sub .indexOf (':' );
1813+ if (schemeEnd >= 0 )
1814+ {
1815+ sub = sub .substring (schemeEnd + 1 );
1816+ }
1817+ if (sub .startsWith ("//" ))
1818+ {
1819+ sub = sub .substring (2 );
1820+ }
1821+ for (int i = 0 ; i < sub .length (); ++i )
1822+ {
1823+ char c = sub .charAt (i );
1824+ if (c == '/' || c == '?' || c == '#' )
1825+ {
1826+ sub = sub .substring (0 , i );
1827+ break ;
1828+ }
1829+ }
1830+ int atPos = sub .lastIndexOf ('@' );
1831+ if (atPos >= 0 )
17071832 {
1708- sub = sub .substring (slashesPos + 2 );
1833+ sub = sub .substring (atPos + 1 );
17091834 }
1710- // first remove port, e.g. https://test.com:21
1711- int portColonPos = sub .lastIndexOf (':' );
1712- if (portColonPos != -1 )
1835+ if (sub .startsWith ("[" ))
17131836 {
1714- sub = sub .substring (0 , portColonPos );
1837+ int closeBracket = sub .indexOf (']' );
1838+ if (closeBracket > 0 )
1839+ {
1840+ return sub .substring (1 , closeBracket );
1841+ }
1842+ return sub .substring (1 );
17151843 }
1716- // remove user and password, e.g. https://john:password@test.com
1717- sub = sub .substring (sub .indexOf (':' ) + 1 );
1718- sub = sub .substring (sub .indexOf ('@' ) + 1 );
1719- // remove local parts, e.g. https://test.com/bla
1720- int slashPos = sub .indexOf ('/' );
1721- if (slashPos != -1 )
1844+ int portColon = sub .lastIndexOf (':' );
1845+ if (portColon >= 0 )
17221846 {
1723- sub = sub .substring (0 , slashPos );
1847+ sub = sub .substring (0 , portColon );
17241848 }
17251849 return sub ;
17261850 }
0 commit comments