diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategy.java index 99c91a1311..fbc93f54fa 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategy.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategy.java @@ -135,9 +135,9 @@ private static final class Rule { if (pins == null || pins.isEmpty()) { throw new IllegalArgumentException("At least one SPKI pin is required for " + pattern); } - this.pattern = norm; + this.pattern = stripRootDot(norm); this.wildcard = wc; - this.tail = wc ? norm.substring(1) : null; // ".example.com" + this.tail = wc ? this.pattern.substring(1) : null; // ".example.com" this.pins = Collections.unmodifiableSet(new HashSet<>(pins)); } @@ -180,7 +180,9 @@ protected void verifySession(final String hostname, final SSLSession sslSession) final String host; try { // Canonicalize host: IDNA (Punycode) + lowercase for consistent matching. - host = IDN.toASCII(hostname == null ? "" : hostname).toLowerCase(Locale.ROOT); + // The absolute form "api.example.com." names the same host and passes the + // built-in hostname check, so it must be matched against the same rules. + host = stripRootDot(IDN.toASCII(hostname == null ? "" : hostname).toLowerCase(Locale.ROOT)); } catch (final IllegalArgumentException e) { throw new SSLException("Invalid IDN host: " + hostname, e); } @@ -286,6 +288,10 @@ private static ByteArrayKey parsePin(final String s) { } + private static String stripRootDot(final String host) { + return host.length() > 1 && host.endsWith(".") ? host.substring(0, host.length() - 1) : host; + } + private List matchedRules(final String host) { final List out = new ArrayList<>(); for (int i = 0; i < rules.size(); i++) { diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategyTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategyTest.java index 422ac92e41..3a7d477bbd 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategyTest.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/ssl/SpkiPinningClientTlsStrategyTest.java @@ -221,6 +221,19 @@ void verifySessionInvalidIdnThrowsSslException() throws NoSuchAlgorithmException assertThrows(SSLException.class, () -> s.verifySession("\uDC00bad", session)); } + @Test + void verifySessionEnforcesPinsForTrailingDotHost() throws Exception { + final SpkiPinningClientTlsStrategy strategy = SpkiPinningClientTlsStrategy + .newBuilder(SSLContext.getDefault()) + .add("api.example.com", sha256Pin(new byte[]{8, 8, 8})) + .add("*.example.org", sha256Pin(new byte[]{8, 8, 8})) + .build(); + + final SSLSession session = new FakeSession(new X509WithKey(new byte[]{7, 7, 7})); + assertThrows(SSLException.class, () -> strategy.verifySession("api.example.com.", session)); + assertThrows(SSLException.class, () -> strategy.verifySession("svc.example.org.", session)); + } + @SuppressWarnings("deprecation") private static final class X509WithKey extends X509Certificate { private final PublicKey key;