Skip to content

Commit b2769e8

Browse files
committed
refactor of X.509 name constraints validator down to a single class.
fixed TODO on multi-valued RDNs in RFC3280CertPathUtilities.
1 parent ca79f33 commit b2769e8

6 files changed

Lines changed: 227 additions & 4138 deletions

File tree

core/src/main/java/org/bouncycastle/asn1/x509/PKIXNameConstraintValidator.java

Lines changed: 148 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

core/src/main/jdk1.4/org/bouncycastle/asn1/x509/PKIXNameConstraintValidator.java

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,29 +1698,47 @@ private static boolean isURIConstrained(String constraint, String uri)
16981698

16991699
private static String extractHostFromURL(String url)
17001700
{
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)
1701+
// RFC 3986 §3.2 authority structure:
1702+
// authority = [ userinfo "@" ] host [ ":" port ]
1703+
// The strip order is now: scheme -> "//" -> path/query fragment terminator -> userinfo (last '@') -> host
1704+
// with optional bracketed IPv6 / trailing ":port".
1705+
String sub = url;
1706+
int schemeEnd = sub.indexOf(':');
1707+
if (schemeEnd >= 0)
17071708
{
1708-
sub = sub.substring(slashesPos + 2);
1709+
sub = sub.substring(schemeEnd + 1);
17091710
}
1710-
// first remove port, e.g. https://test.com:21
1711-
int portColonPos = sub.lastIndexOf(':');
1712-
if (portColonPos != -1)
1711+
if (sub.startsWith("//"))
17131712
{
1714-
sub = sub.substring(0, portColonPos);
1713+
sub = sub.substring(2);
17151714
}
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)
1715+
for (int i = 0; i < sub.length(); ++i)
17221716
{
1723-
sub = sub.substring(0, slashPos);
1717+
char c = sub.charAt(i);
1718+
if (c == '/' || c == '?' || c == '#')
1719+
{
1720+
sub = sub.substring(0, i);
1721+
break;
1722+
}
1723+
}
1724+
int atPos = sub.lastIndexOf('@');
1725+
if (atPos >= 0)
1726+
{
1727+
sub = sub.substring(atPos + 1);
1728+
}
1729+
if (sub.startsWith("["))
1730+
{
1731+
int closeBracket = sub.indexOf(']');
1732+
if (closeBracket > 0)
1733+
{
1734+
return sub.substring(1, closeBracket);
1735+
}
1736+
return sub.substring(1);
1737+
}
1738+
int portColon = sub.lastIndexOf(':');
1739+
if (portColon >= 0)
1740+
{
1741+
sub = sub.substring(0, portColon);
17241742
}
17251743
return sub;
17261744
}

0 commit comments

Comments
 (0)