From aa602a19ab69119f71674cc4cc21d1c528b7b655 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Thu, 16 Jan 2025 21:58:38 +0100 Subject: [PATCH] Decode Subject Alternative Names for IP, DNS, and binary data Supports DNS names as plain text, IPv4 and IPv6 addresses in binary form, and falls back to hex encoding for unknown types. --- .../http/ssl/DefaultHostnameVerifier.java | 34 ++- .../http/ssl/TestDefaultHostnameVerifier.java | 205 ++++++++++++++++++ 2 files changed, 238 insertions(+), 1 deletion(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java index 9b7418bf38..e7f3d3785e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/DefaultHostnameVerifier.java @@ -355,7 +355,14 @@ static List getSubjectAltNames(final X509Certificate cert, final in if (o instanceof String) { result.add(new SubjectName((String) o, type)); } else if (o instanceof byte[]) { - // TODO ASN.1 DER encoded form + final byte[] bytes = (byte[]) o; + if (type == SubjectName.IP) { + if (bytes.length == 4) { + result.add(new SubjectName(byteArrayToIp(bytes), type)); // IPv4 + } else if (bytes.length == 16) { + result.add(new SubjectName(byteArrayToIPv6(bytes), type)); // IPv6 + } + } } } } @@ -380,4 +387,29 @@ static String normaliseAddress(final String hostname) { return hostname; } } + + private static String byteArrayToIp(final byte[] bytes) { + if (bytes.length != 4) { + throw new IllegalArgumentException("Invalid byte array length for IPv4 address"); + } + return (bytes[0] & 0xFF) + "." + + (bytes[1] & 0xFF) + "." + + (bytes[2] & 0xFF) + "." + + (bytes[3] & 0xFF); + } + + private static String byteArrayToIPv6(final byte[] bytes) { + if (bytes.length != 16) { + throw new IllegalArgumentException("Invalid byte array length for IPv6 address"); + } + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i += 2) { + sb.append(String.format("%02x%02x", bytes[i], bytes[i + 1])); + if (i < bytes.length - 2) { + sb.append(":"); + } + } + return sb.toString(); + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java index 6ca2ac488d..38da6e2c78 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/TestDefaultHostnameVerifier.java @@ -31,12 +31,28 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.math.BigInteger; +import java.net.InetAddress; import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.List; +import java.util.Set; import javax.net.ssl.SSLException; @@ -548,4 +564,193 @@ void testMatchIdentity() { ); } + + @Test + void testSimulatedByteProperties() throws Exception { + // Simulated byte array for an IP address + final byte[] ipAsByteArray = {1, 1, 1, 1}; // 1.1.1.1 in byte form + + final List> entries = new ArrayList<>(); + final List entry = new ArrayList<>(); + entry.add(SubjectName.IP); + entry.add(ipAsByteArray); + entries.add(entry); + + // Mocking the certificate behavior + final X509Certificate mockCert = generateX509Certificate(entries); + + final List result = DefaultHostnameVerifier.getSubjectAltNames(mockCert, -1); + Assertions.assertEquals(1, result.size(), "Should have one SubjectAltName"); + + final SubjectName sn = result.get(0); + Assertions.assertEquals(SubjectName.IP, sn.getType(), "Should be an IP type"); + // Here, you'll need logic to convert byte array to string for assertion + Assertions.assertEquals("1.1.1.1", sn.getValue(), "IP address should match after conversion"); + } + + @Test + void testSimulatedBytePropertiesIPv6() throws Exception { + final byte[] ipv6AsByteArray = InetAddress.getByName("2001:db8:85a3::8a2e:370:7334").getAddress(); + // IPv6 2001:db8:85a3::8a2e:370:7334 + + final List> entries = new ArrayList<>(); + final List entry = new ArrayList<>(); + entry.add(SubjectName.IP); + entry.add(ipv6AsByteArray); + entries.add(entry); + + // Mocking the certificate behavior + final X509Certificate mockCert = generateX509Certificate(entries); + + final List result = DefaultHostnameVerifier.getSubjectAltNames(mockCert, -1); + Assertions.assertEquals(1, result.size(), "Should have one SubjectAltName"); + + final SubjectName sn = result.get(0); + Assertions.assertEquals(SubjectName.IP, sn.getType(), "Should be an IP type"); + // Here, you'll need logic to convert byte array to string for assertion + Assertions.assertEquals("2001:0db8:85a3:0000:0000:8a2e:0370:7334", sn.getValue(), "IP address should match after conversion"); + } + + + private X509Certificate generateX509Certificate(final List> entries) { + return new X509Certificate() { + + @Override + public boolean hasUnsupportedCriticalExtension() { + return false; + } + + @Override + public Set getCriticalExtensionOIDs() { + return null; + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return null; + } + + @Override + public byte[] getExtensionValue(final String oid) { + return new byte[0]; + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return new byte[0]; + } + + @Override + public void verify(final PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + public void verify(final PublicKey key, final String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException { + + } + + @Override + public String toString() { + return ""; + } + + @Override + public PublicKey getPublicKey() { + return null; + } + + @Override + public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { + + } + + @Override + public void checkValidity(final Date date) throws CertificateExpiredException, CertificateNotYetValidException { + + } + + @Override + public int getVersion() { + return 0; + } + + @Override + public BigInteger getSerialNumber() { + return null; + } + + @Override + public Principal getIssuerDN() { + return null; + } + + @Override + public Principal getSubjectDN() { + return null; + } + + @Override + public Date getNotBefore() { + return null; + } + + @Override + public Date getNotAfter() { + return null; + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return new byte[0]; + } + + @Override + public byte[] getSignature() { + return new byte[0]; + } + + @Override + public String getSigAlgName() { + return ""; + } + + @Override + public String getSigAlgOID() { + return ""; + } + + @Override + public byte[] getSigAlgParams() { + return new byte[0]; + } + + @Override + public boolean[] getIssuerUniqueID() { + return new boolean[0]; + } + + @Override + public boolean[] getSubjectUniqueID() { + return new boolean[0]; + } + + @Override + public boolean[] getKeyUsage() { + return new boolean[0]; + } + + @Override + public int getBasicConstraints() { + return 0; + } + + @Override + public Collection> getSubjectAlternativeNames() { + return entries; + } + }; + + } + } \ No newline at end of file