Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<Rule> matchedRules(final String host) {
final List<Rule> out = new ArrayList<>();
for (int i = 0; i < rules.size(); i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading