fix(quic): PSL subset, JCA ChaCha20-Poly1305, truthful ECN reporting
Round 13 — three architectural follow-ups + a closeout note.
* JdkCertificateValidator: ship a hand-picked Public Suffix List
subset covering the high-volume multi-label effective-TLDs
(multi-tenant ccTLDs and major hosting platforms). Pre-fix the
dot-count heuristic accepted `*.co.uk`, `*.s3.amazonaws.com`,
`*.github.io`, etc. — wildcards spanning these would impersonate
every co-tenant. The new MULTI_LABEL_PUBLIC_SUFFIXES set adds a
layer above the dot-count check; combined with the WebPKI / CT
ecosystem already requiring CAs to consult the full PSL when
issuing, this closes the practical attack surfaces. Full
~9000-entry PSL data shipping is still deferred (data-shipping
ask, doc'd); a domain not in the subset that's also a multi-label
ETLD remains a gap.
* JcaChaCha20Poly1305Aead: new JCA-backed implementation mirroring
JcaAesGcmAead's shape (cached Cipher + SecretKeySpec, range
overloads via Cipher.doFinal(input, off, len, output, outOff),
recent-nonce history for legitimate IV reuse on the
Initial-padding rebuild path). bestChaCha20Poly1305Aead(key)
expect/actual factory tries the JCA path first (Java 11+ /
Android API 28+) and falls back to the pure-Kotlin
ChaCha20Poly1305Aead singleton if the algorithm isn't available
(older Android, headless GraalVM native-image without the
standard providers). PacketProtectionBuilder routes the
ChaCha20-Poly1305 cipher suite through the factory instead of
the singleton. On supporting platforms this gives the same
outbound-allocation savings as round 8's AES-GCM range overload.
* QuicConnectionWriter: stop emitting fake ECN counts on ACK
frames. Pre-fix every 1-RTT ACK carried `AckEcnCounts(0, 0, 0)`
— claiming to track ECN while actually never reading inbound TOS
bits. RFC 9000 §13.4.2: "An endpoint that uses ECN MUST report
accurate ECN counts." Hardcoded zeros could be flagged as a
PROTOCOL_VIOLATION by strict peers cross-validating against
outbound packet counts; aioquic / picoquic / quic-go tolerate
it but other stacks may not. With ecnCounts = null we honestly
advertise "this endpoint isn't reporting ECN", peer skips its
own ECN-driven congestion logic for our direction. We still
mark outbound ECT(0) (other peers' tracking benefits from the
path-quality signal); RFC 9000 §13.4 allows the asymmetry.
* MutableSharedFlow migration for QuicStream.incoming declined as
obviated. The audit's suggestion was a workaround for the
cancel-coupling specifically (collector cancel → channel cancel
→ INTERNAL_ERROR) — round 11's `flow { for (c in
incomingChannel) emit(c) }` wrapper solved that. Switching to
MutableSharedFlow would change the semantics from "each byte to
exactly one consumer" (correct for stream bytes) to fan-out
(every emission to all collectors), which is wrong for QUIC
stream data.
All 269 :quic:jvmTest tests pass. BUILD SUCCESSFUL in 40s.
https://claude.ai/code/session_01AhGvbMV8uPRse3TmAGaddM
This commit is contained in:
+12
-3
@@ -73,9 +73,18 @@ fun packetProtectionFromSecret(
|
||||
// which avoids `Cipher.getInstance` per-packet (audit-1, audit-3 finding).
|
||||
val aead: Aead =
|
||||
when (cipherSuite) {
|
||||
TlsConstants.CIPHER_TLS_AES_128_GCM_SHA256 -> bestAes128GcmAead(keys.key)
|
||||
TlsConstants.CIPHER_TLS_CHACHA20_POLY1305_SHA256 -> com.vitorpamplona.quic.crypto.ChaCha20Poly1305Aead
|
||||
else -> error("unreachable")
|
||||
TlsConstants.CIPHER_TLS_AES_128_GCM_SHA256 -> {
|
||||
bestAes128GcmAead(keys.key)
|
||||
}
|
||||
|
||||
TlsConstants.CIPHER_TLS_CHACHA20_POLY1305_SHA256 -> {
|
||||
com.vitorpamplona.quic.crypto
|
||||
.bestChaCha20Poly1305Aead(keys.key)
|
||||
}
|
||||
|
||||
else -> {
|
||||
error("unreachable")
|
||||
}
|
||||
}
|
||||
return PacketProtection(aead, keys.key, keys.iv, hp, keys.hp)
|
||||
}
|
||||
|
||||
+17
-25
@@ -622,31 +622,23 @@ private fun buildApplicationPacket(
|
||||
// Skip ACK building when we're about to emit a 0-RTT packet.
|
||||
if (use1Rtt) {
|
||||
state.ackTracker.buildAckFrame(nowMillis, conn.config.ackDelayExponent.toInt())?.let { plainAck ->
|
||||
// RFC 9000 §13.4.2: an endpoint that USES ECN on outbound
|
||||
// packets (we set ECT(0) on every datagram via the socket's
|
||||
// IP_TOS option) MUST report ECN counts in 1-RTT ACK frames so
|
||||
// the peer can detect path congestion. We don't currently
|
||||
// read inbound TOS bits — JDK's DatagramChannel doesn't expose
|
||||
// them without JNI — so the counts are all-zero. The interop
|
||||
// runner's `ecn` testcase only checks for the field's presence
|
||||
// (`hasattr(p["quic"], "ack.ect0_count")`); strict peers that
|
||||
// cross-validate counts would reject this, but aioquic /
|
||||
// picoquic / quic-go all tolerate it. Initial / Handshake-space
|
||||
// ACKs stay plain (ecnCounts=null) — the spec allows ECN counts
|
||||
// there too, but interop implementations don't always handle
|
||||
// them and we'd gain nothing.
|
||||
val ackWithEcn =
|
||||
AckFrame(
|
||||
largestAcknowledged = plainAck.largestAcknowledged,
|
||||
ackDelay = plainAck.ackDelay,
|
||||
firstAckRange = plainAck.firstAckRange,
|
||||
additionalRanges = plainAck.additionalRanges,
|
||||
ecnCounts =
|
||||
com.vitorpamplona.quic.frame
|
||||
.AckEcnCounts(0L, 0L, 0L),
|
||||
)
|
||||
frames += ackWithEcn
|
||||
tokens += RecoveryToken.Ack(level = EncryptionLevel.APPLICATION, largestAcked = ackWithEcn.largestAcknowledged)
|
||||
// RFC 9000 §13.4.2: an endpoint that USES ECN MUST report
|
||||
// accurate ECN counts. Pre-fix we hardcoded
|
||||
// `AckEcnCounts(0, 0, 0)` while still marking outbound
|
||||
// datagrams with ECT(0) — a strict peer cross-validating
|
||||
// counts could treat the all-zero report as a
|
||||
// PROTOCOL_VIOLATION, since we claim to be using ECN but
|
||||
// never accumulate the counts. We don't read inbound TOS
|
||||
// (JDK's DatagramChannel doesn't expose it without JNI),
|
||||
// so honest reporting means: don't claim to track ECN at
|
||||
// all — emit ACK with `ecnCounts = null`. The peer reads
|
||||
// that as "this endpoint isn't reporting ECN" and skips
|
||||
// its own ECN-driven congestion logic for our direction.
|
||||
// We still mark outbound ECT(0) (other peers' tracking
|
||||
// benefits from the path-quality signal); the asymmetry
|
||||
// is allowed by §13.4.
|
||||
frames += plainAck
|
||||
tokens += RecoveryToken.Ack(level = EncryptionLevel.APPLICATION, largestAcked = plainAck.largestAcknowledged)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,3 +34,13 @@ expect val PlatformChaCha20Block: ChaCha20BlockEncrypt
|
||||
* stateless singleton) — correct, just not the fast path.
|
||||
*/
|
||||
expect fun bestAes128GcmAead(key: ByteArray): Aead
|
||||
|
||||
/**
|
||||
* Build the platform's preferred ChaCha20-Poly1305 AEAD for a fixed [key].
|
||||
* JVM 11+ / Android API 28+ provide a JCA `ChaCha20-Poly1305` cipher
|
||||
* that supports range-based `Cipher.doFinal(input, off, len, output, off)`,
|
||||
* unlocking the same allocation-elision wins as [bestAes128GcmAead].
|
||||
* Older platforms fall back to the pure-Kotlin [ChaCha20Poly1305Aead]
|
||||
* singleton — correct, just slower and without range overloads.
|
||||
*/
|
||||
expect fun bestChaCha20Poly1305Aead(key: ByteArray): Aead
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quic.crypto
|
||||
|
||||
import java.security.GeneralSecurityException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
/**
|
||||
* ChaCha20-Poly1305 AEAD via the JCA `ChaCha20-Poly1305` cipher
|
||||
* (Java 11+ / Android API 28+). Mirrors [JcaAesGcmAead]'s shape: caches
|
||||
* the [Cipher] + [SecretKeySpec] across calls and exposes the range
|
||||
* overloads ([sealRange] / [openRange] / [sealInto]) that pass
|
||||
* offsets straight to the JCA cipher's `doFinal(input, inOff, inLen,
|
||||
* output, outOff)` form, eliminating the per-packet slice
|
||||
* allocations the pure-Kotlin [ChaCha20Poly1305Aead] requires.
|
||||
*
|
||||
* Single-thread per direction (one PacketProtection per side, one
|
||||
* direction per side). Synchronization on a private monitor is
|
||||
* defence-in-depth — a future caller (test harness, key-update path)
|
||||
* sharing the instance across coroutines would otherwise corrupt
|
||||
* the cached `Cipher` state silently.
|
||||
*/
|
||||
class JcaChaCha20Poly1305Aead(
|
||||
key: ByteArray,
|
||||
) : Aead() {
|
||||
override val keyLength = 32
|
||||
override val nonceLength = 12
|
||||
override val tagLength = 16
|
||||
|
||||
private val keySpec = SecretKeySpec(key, "ChaCha20")
|
||||
private val encryptCipher: Cipher = Cipher.getInstance("ChaCha20-Poly1305")
|
||||
private val decryptCipher: Cipher = Cipher.getInstance("ChaCha20-Poly1305")
|
||||
|
||||
/**
|
||||
* Last-N nonces successfully consumed by [seal] / [sealRange] /
|
||||
* [sealInto]. JCA's ChaCha20-Poly1305 (like AES-GCM) refuses to
|
||||
* encrypt under a (key, nonce) pair already used by THIS cipher
|
||||
* instance — even when the reuse is legitimate (Initial-padding
|
||||
* rebuild). When we detect a reuse against this history we fall
|
||||
* back to a fresh `Cipher.getInstance` to avoid fighting the
|
||||
* provider's safety check.
|
||||
*/
|
||||
private val recentEncryptNonces = ArrayDeque<ByteArray>()
|
||||
|
||||
override fun seal(
|
||||
key: ByteArray,
|
||||
nonce: ByteArray,
|
||||
aad: ByteArray,
|
||||
plaintext: ByteArray,
|
||||
): ByteArray =
|
||||
synchronized(this) {
|
||||
sealCommon(nonce, aad, 0, aad.size, plaintext, 0, plaintext.size, output = null, outputOffset = 0)
|
||||
.first
|
||||
}
|
||||
|
||||
override fun sealRange(
|
||||
key: ByteArray,
|
||||
nonce: ByteArray,
|
||||
aad: ByteArray,
|
||||
aadOffset: Int,
|
||||
aadLength: Int,
|
||||
plaintext: ByteArray,
|
||||
plaintextOffset: Int,
|
||||
plaintextLength: Int,
|
||||
): ByteArray =
|
||||
synchronized(this) {
|
||||
sealCommon(nonce, aad, aadOffset, aadLength, plaintext, plaintextOffset, plaintextLength, output = null, outputOffset = 0)
|
||||
.first
|
||||
}
|
||||
|
||||
override fun sealInto(
|
||||
key: ByteArray,
|
||||
nonce: ByteArray,
|
||||
aad: ByteArray,
|
||||
aadOffset: Int,
|
||||
aadLength: Int,
|
||||
plaintext: ByteArray,
|
||||
plaintextOffset: Int,
|
||||
plaintextLength: Int,
|
||||
output: ByteArray,
|
||||
outputOffset: Int,
|
||||
): Int =
|
||||
synchronized(this) {
|
||||
sealCommon(nonce, aad, aadOffset, aadLength, plaintext, plaintextOffset, plaintextLength, output, outputOffset)
|
||||
.second
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared seal path for all three entry points. Returns
|
||||
* `(freshOrEmpty, bytesWritten)`:
|
||||
* - When [output] is null we allocate the result internally,
|
||||
* return it as `freshOrEmpty`, and `bytesWritten` is its size.
|
||||
* - When [output] is non-null we write into it at [outputOffset],
|
||||
* return an empty array as `freshOrEmpty` (caller ignores), and
|
||||
* `bytesWritten` is the number of bytes written.
|
||||
*/
|
||||
private fun sealCommon(
|
||||
nonce: ByteArray,
|
||||
aad: ByteArray,
|
||||
aadOffset: Int,
|
||||
aadLength: Int,
|
||||
plaintext: ByteArray,
|
||||
plaintextOffset: Int,
|
||||
plaintextLength: Int,
|
||||
output: ByteArray?,
|
||||
outputOffset: Int,
|
||||
): Pair<ByteArray, Int> {
|
||||
val reuse = recentEncryptNonces.any { it.contentEquals(nonce) }
|
||||
val cipher: Cipher
|
||||
if (reuse) {
|
||||
cipher = Cipher.getInstance("ChaCha20-Poly1305")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(nonce))
|
||||
} else {
|
||||
cipher = encryptCipher
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(nonce))
|
||||
} catch (t: Throwable) {
|
||||
if (recentEncryptNonces.lastOrNull()?.contentEquals(nonce) == true) {
|
||||
recentEncryptNonces.removeLast()
|
||||
}
|
||||
throw t
|
||||
}
|
||||
}
|
||||
try {
|
||||
cipher.updateAAD(aad, aadOffset, aadLength)
|
||||
return if (output != null) {
|
||||
val written = cipher.doFinal(plaintext, plaintextOffset, plaintextLength, output, outputOffset)
|
||||
if (!reuse) rememberEncryptNonce(nonce)
|
||||
EMPTY_BYTES to written
|
||||
} else {
|
||||
val out = cipher.doFinal(plaintext, plaintextOffset, plaintextLength)
|
||||
if (!reuse) rememberEncryptNonce(nonce)
|
||||
out to out.size
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
if (!reuse && recentEncryptNonces.lastOrNull()?.contentEquals(nonce) == true) {
|
||||
recentEncryptNonces.removeLast()
|
||||
}
|
||||
throw t
|
||||
}
|
||||
}
|
||||
|
||||
private fun rememberEncryptNonce(nonce: ByteArray) {
|
||||
recentEncryptNonces.addLast(nonce)
|
||||
while (recentEncryptNonces.size > NONCE_HISTORY_LIMIT) {
|
||||
recentEncryptNonces.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
override fun open(
|
||||
key: ByteArray,
|
||||
nonce: ByteArray,
|
||||
aad: ByteArray,
|
||||
ciphertext: ByteArray,
|
||||
): ByteArray? =
|
||||
synchronized(this) {
|
||||
try {
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(nonce))
|
||||
decryptCipher.updateAAD(aad)
|
||||
decryptCipher.doFinal(ciphertext)
|
||||
} catch (_: GeneralSecurityException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun openRange(
|
||||
key: ByteArray,
|
||||
nonce: ByteArray,
|
||||
aad: ByteArray,
|
||||
aadOffset: Int,
|
||||
aadLength: Int,
|
||||
ciphertext: ByteArray,
|
||||
ciphertextOffset: Int,
|
||||
ciphertextLength: Int,
|
||||
): ByteArray? =
|
||||
synchronized(this) {
|
||||
try {
|
||||
decryptCipher.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(nonce))
|
||||
decryptCipher.updateAAD(aad, aadOffset, aadLength)
|
||||
decryptCipher.doFinal(ciphertext, ciphertextOffset, ciphertextLength)
|
||||
} catch (_: GeneralSecurityException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val NONCE_HISTORY_LIMIT = 8
|
||||
private val EMPTY_BYTES = ByteArray(0)
|
||||
}
|
||||
}
|
||||
@@ -45,3 +45,24 @@ actual val PlatformChaCha20Block: ChaCha20BlockEncrypt =
|
||||
}
|
||||
|
||||
actual fun bestAes128GcmAead(key: ByteArray): Aead = JcaAesGcmAead(key)
|
||||
|
||||
/**
|
||||
* Try the JCA `ChaCha20-Poly1305` cipher first (Java 11+ /
|
||||
* Android API 28+) — gives us range-overload + offset-based doFinal,
|
||||
* which lets [com.vitorpamplona.quic.packet.LongHeaderPacket.build]
|
||||
* and [com.vitorpamplona.quic.packet.ShortHeaderPacket.build] write
|
||||
* ciphertext+tag in-place without intermediate allocations on the
|
||||
* outbound hot path.
|
||||
*
|
||||
* Falls back to the pure-Kotlin [ChaCha20Poly1305Aead] singleton if
|
||||
* the JCA provider doesn't ship the algorithm — older Android
|
||||
* versions, headless containers without the standard provider set,
|
||||
* GraalVM native-image unsubsetted, etc. The fallback is correct
|
||||
* (same algorithm) but slower and skips the range overloads.
|
||||
*/
|
||||
actual fun bestChaCha20Poly1305Aead(key: ByteArray): Aead =
|
||||
try {
|
||||
JcaChaCha20Poly1305Aead(key)
|
||||
} catch (_: java.security.NoSuchAlgorithmException) {
|
||||
ChaCha20Poly1305Aead
|
||||
}
|
||||
|
||||
@@ -247,28 +247,242 @@ class JdkCertificateValidator(
|
||||
if (!lhost.endsWith(suffix)) return false
|
||||
val prefix = lhost.substring(0, lhost.length - suffix.length)
|
||||
if (prefix.isEmpty() || '.' in prefix) return false
|
||||
// RFC 6125 §6.4.3 — disallow wildcards in the public-suffix label.
|
||||
// RFC 6125 §6.4.3 — disallow wildcards in the public-suffix
|
||||
// label. The full Mozilla PSL is ~9000 entries; we ship a
|
||||
// hand-picked subset covering the common multi-label
|
||||
// effective-TLDs that come up in the wild for cert
|
||||
// mis-issuance scenarios (multi-tenant ccTLDs, hosting
|
||||
// platforms). Two layers of defence:
|
||||
// 1. The dot-count heuristic (≥ 2 dots in the suffix)
|
||||
// catches the obvious `*.com` / `*.net` patterns.
|
||||
// 2. The denylist below catches `*.co.uk`,
|
||||
// `*.s3.amazonaws.com`, `*.github.io`, etc. — the
|
||||
// multi-label tldsets where the dot-count alone would
|
||||
// let a rogue wildcard impersonate an entire tenant
|
||||
// pool.
|
||||
//
|
||||
// KNOWN GAP: this heuristic counts dots in the suffix (≥ 2 → OK)
|
||||
// and DOES NOT consult the actual public-suffix list. So it
|
||||
// accepts `*.co.uk`, `*.github.io`, `*.s3.amazonaws.com`, etc.
|
||||
// — multi-tenant TLDs whose effective TLD is multi-label. A
|
||||
// CA that mis-issues such a wildcard could impersonate any
|
||||
// co-tenant. The mitigation is partial: the WebPKI ecosystem
|
||||
// already requires CAs to consult the PSL when issuing, so a
|
||||
// rogue cert is unlikely to make it past CT logging — but if
|
||||
// one does, our validation accepts it.
|
||||
//
|
||||
// Full fix requires shipping PSL data; deferred until the cost
|
||||
// is justified by a higher-risk deployment. Production callers
|
||||
// should rely on the OS / NetworkSecurityConfig pinning layer
|
||||
// for sensitive endpoints rather than QUIC's built-in
|
||||
// hostname check alone.
|
||||
// This is intentionally conservative — false positives
|
||||
// (rejecting a legitimate but unusual wildcard) are recoverable
|
||||
// by the application using OS-level pinning, while false
|
||||
// negatives (accepting a rogue wildcard) silently break
|
||||
// hostname authentication. The full PSL would close the
|
||||
// remaining gap; until then, this catches the high-volume
|
||||
// attack surfaces.
|
||||
val suffixWithoutLeadingDot = suffix.removePrefix(".")
|
||||
if (suffixWithoutLeadingDot in MULTI_LABEL_PUBLIC_SUFFIXES) return false
|
||||
val suffixDots = suffix.count { it == '.' }
|
||||
return suffixDots >= 2
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Hand-picked subset of the Mozilla Public Suffix List covering
|
||||
* common multi-label effective-TLDs. Wildcards spanning these
|
||||
* suffixes (e.g. `*.co.uk`, `*.s3.amazonaws.com`) MUST be
|
||||
* rejected per RFC 6125 §6.4.3 — a rogue cert that captured
|
||||
* such a wildcard would impersonate every co-tenant.
|
||||
*
|
||||
* Strict subset of the full PSL — we don't ship the ~9000-entry
|
||||
* data file; entries here cover the high-frequency attack
|
||||
* surfaces (multi-tenant ccTLDs, major hosting platforms). The
|
||||
* full PSL would close the remaining gap; for now, callers of
|
||||
* sensitive endpoints should still layer OS-level pinning.
|
||||
*
|
||||
* Sources:
|
||||
* - Top multi-label ccTLDs from publicsuffix.org/list/
|
||||
* (uk / au / nz / jp / kr / br / mx / in / ar / il / etc.)
|
||||
* - Major hosting platforms whose tenant subdomains all share
|
||||
* one cert root (s3.amazonaws.com, github.io, herokuapp.com,
|
||||
* vercel.app, netlify.app, web.app, blogspot.com,
|
||||
* appspot.com, pages.dev, workers.dev, etc.)
|
||||
*/
|
||||
private val MULTI_LABEL_PUBLIC_SUFFIXES: Set<String> =
|
||||
setOf(
|
||||
// UK
|
||||
"co.uk",
|
||||
"org.uk",
|
||||
"ac.uk",
|
||||
"gov.uk",
|
||||
"ltd.uk",
|
||||
"plc.uk",
|
||||
"me.uk",
|
||||
"net.uk",
|
||||
"sch.uk",
|
||||
"nhs.uk",
|
||||
"police.uk",
|
||||
// AU
|
||||
"com.au",
|
||||
"net.au",
|
||||
"org.au",
|
||||
"edu.au",
|
||||
"gov.au",
|
||||
"asn.au",
|
||||
"id.au",
|
||||
// NZ
|
||||
"co.nz",
|
||||
"net.nz",
|
||||
"org.nz",
|
||||
"ac.nz",
|
||||
"govt.nz",
|
||||
"school.nz",
|
||||
// JP
|
||||
"co.jp",
|
||||
"ne.jp",
|
||||
"or.jp",
|
||||
"ac.jp",
|
||||
"ad.jp",
|
||||
"ed.jp",
|
||||
"go.jp",
|
||||
"gr.jp",
|
||||
"lg.jp",
|
||||
// KR
|
||||
"co.kr",
|
||||
"ne.kr",
|
||||
"or.kr",
|
||||
"re.kr",
|
||||
"ac.kr",
|
||||
"go.kr",
|
||||
"mil.kr",
|
||||
"sc.kr",
|
||||
// BR
|
||||
"com.br",
|
||||
"net.br",
|
||||
"org.br",
|
||||
"edu.br",
|
||||
"gov.br",
|
||||
"mil.br",
|
||||
// MX
|
||||
"com.mx",
|
||||
"net.mx",
|
||||
"org.mx",
|
||||
"edu.mx",
|
||||
"gob.mx",
|
||||
// IN
|
||||
"co.in",
|
||||
"net.in",
|
||||
"org.in",
|
||||
"edu.in",
|
||||
"gov.in",
|
||||
"ac.in",
|
||||
"res.in",
|
||||
// AR
|
||||
"com.ar",
|
||||
"net.ar",
|
||||
"org.ar",
|
||||
"edu.ar",
|
||||
"gov.ar",
|
||||
"gob.ar",
|
||||
"mil.ar",
|
||||
// IL
|
||||
"co.il",
|
||||
"net.il",
|
||||
"org.il",
|
||||
"ac.il",
|
||||
"gov.il",
|
||||
"muni.il",
|
||||
"k12.il",
|
||||
// ZA
|
||||
"co.za",
|
||||
"net.za",
|
||||
"org.za",
|
||||
"ac.za",
|
||||
"gov.za",
|
||||
"edu.za",
|
||||
"law.za",
|
||||
// CN
|
||||
"com.cn",
|
||||
"net.cn",
|
||||
"org.cn",
|
||||
"edu.cn",
|
||||
"gov.cn",
|
||||
"ac.cn",
|
||||
"mil.cn",
|
||||
// TR
|
||||
"com.tr",
|
||||
"net.tr",
|
||||
"org.tr",
|
||||
"edu.tr",
|
||||
"gov.tr",
|
||||
"biz.tr",
|
||||
"info.tr",
|
||||
// RU
|
||||
"com.ru",
|
||||
"net.ru",
|
||||
"org.ru",
|
||||
"pp.ru",
|
||||
"msk.ru",
|
||||
"spb.ru",
|
||||
// PL
|
||||
"com.pl",
|
||||
"net.pl",
|
||||
"org.pl",
|
||||
"edu.pl",
|
||||
"gov.pl",
|
||||
"mil.pl",
|
||||
// ES
|
||||
"com.es",
|
||||
"nom.es",
|
||||
"org.es",
|
||||
"gob.es",
|
||||
"edu.es",
|
||||
// HK / SG / TW / MY
|
||||
"com.hk",
|
||||
"net.hk",
|
||||
"org.hk",
|
||||
"edu.hk",
|
||||
"gov.hk",
|
||||
"idv.hk",
|
||||
"com.sg",
|
||||
"net.sg",
|
||||
"org.sg",
|
||||
"edu.sg",
|
||||
"gov.sg",
|
||||
"per.sg",
|
||||
"com.tw",
|
||||
"net.tw",
|
||||
"org.tw",
|
||||
"edu.tw",
|
||||
"gov.tw",
|
||||
"idv.tw",
|
||||
"com.my",
|
||||
"net.my",
|
||||
"org.my",
|
||||
"edu.my",
|
||||
"gov.my",
|
||||
"mil.my",
|
||||
// Major hosting platforms — all tenants share root cert paths.
|
||||
"github.io",
|
||||
"github.com",
|
||||
"s3.amazonaws.com",
|
||||
"compute.amazonaws.com",
|
||||
"blogspot.com",
|
||||
"blogspot.co.uk",
|
||||
"blogspot.de",
|
||||
"blogspot.fr",
|
||||
"appspot.com",
|
||||
"googleapis.com",
|
||||
"googleusercontent.com",
|
||||
"herokuapp.com",
|
||||
"herokussl.com",
|
||||
"vercel.app",
|
||||
"now.sh",
|
||||
"netlify.app",
|
||||
"netlify.com",
|
||||
"web.app",
|
||||
"firebaseapp.com",
|
||||
"pages.dev",
|
||||
"workers.dev",
|
||||
"azurewebsites.net",
|
||||
"cloudapp.net",
|
||||
"trafficmanager.net",
|
||||
"fastly.net",
|
||||
"fastlylb.net",
|
||||
"cloudfront.net",
|
||||
"ngrok.io",
|
||||
"ngrok.app",
|
||||
"execute-api.us-east-1.amazonaws.com",
|
||||
)
|
||||
|
||||
private fun defaultTrustManager(): X509TrustManager {
|
||||
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
tmf.init(null as KeyStore?)
|
||||
|
||||
Reference in New Issue
Block a user