react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +6 -5
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/ios/Model/NitroTypeStubs.swift +18 -2
- package/ios/Tests/QueueManagerTests.swift +4 -4
- package/package.json +3 -2
- package/src/nitro-stub-drift.test.ts +237 -0
package/android/build.gradle
CHANGED
|
@@ -62,12 +62,13 @@ android {
|
|
|
62
62
|
ndkVersion getExtOrDefault("ndkVersion")
|
|
63
63
|
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
65
|
+
// Deliberately left off (the AGP default). Turning it on is global: every un-mocked
|
|
66
|
+
// android.* call in every unit test silently returns 0/false/null instead of throwing,
|
|
67
|
+
// so a test can pass while the code under test never ran. Tests that need real framework
|
|
68
|
+
// behaviour run under Robolectric instead — see BrowserManagerLayerResolutionTest, whose
|
|
69
|
+
// android.util.LruCache.evictAll() is what this flag used to paper over.
|
|
69
70
|
testOptions {
|
|
70
|
-
unitTests.returnDefaultValues =
|
|
71
|
+
unitTests.returnDefaultValues = false
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
defaultConfig {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.audiobrowser.tls
|
|
2
2
|
|
|
3
3
|
import java.security.cert.X509Certificate
|
|
4
|
+
import javax.security.auth.x500.X500Principal
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Fills in missing intermediate CA certificates by following the Authority Information Access (AIA)
|
|
@@ -14,6 +15,18 @@ import java.security.cert.X509Certificate
|
|
|
14
15
|
* found".
|
|
15
16
|
*/
|
|
16
17
|
object AiaCertChaser {
|
|
18
|
+
/** How many distinct CA-Issuers URLs from one certificate are worth trying. */
|
|
19
|
+
const val MAX_URLS_PER_CERT = 3
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wall-clock ceiling on one chain-completion attempt, across all of its fetches. The deadline it
|
|
23
|
+
* implies is handed to every fetch, whose connects, handshakes, reads and redirects are all
|
|
24
|
+
* clipped to the time remaining — so the whole walk ends at the deadline. The one thing it cannot
|
|
25
|
+
* cut short is a DNS lookup already in flight (Java exposes no timeout for resolution), so the
|
|
26
|
+
* true bound is this budget plus at most one lookup's duration.
|
|
27
|
+
*/
|
|
28
|
+
const val MAX_CHASE_MILLIS = 20_000L
|
|
29
|
+
|
|
17
30
|
/** Authority Information Access extension. */
|
|
18
31
|
private const val AIA_OID = "1.3.6.1.5.5.7.1.1"
|
|
19
32
|
|
|
@@ -69,8 +82,9 @@ object AiaCertChaser {
|
|
|
69
82
|
|
|
70
83
|
/**
|
|
71
84
|
* Walks up from the presented chain, fetching each missing issuer via its predecessor's AIA "CA
|
|
72
|
-
* Issuers" pointer, until it reaches a
|
|
73
|
-
* or [maxIntermediates] additions.
|
|
85
|
+
* Issuers" pointer, until it reaches a certificate issued by one of [anchorSubjects], a
|
|
86
|
+
* self-signed certificate, an issuer that cannot be fetched, or [maxIntermediates] additions.
|
|
87
|
+
* Returns the (possibly extended) chain.
|
|
74
88
|
*
|
|
75
89
|
* Only ever *adds* certificates — the caller is still responsible for validating the completed
|
|
76
90
|
* chain against the system trust anchors, so this can never cause an untrusted chain to be
|
|
@@ -78,25 +92,52 @@ object AiaCertChaser {
|
|
|
78
92
|
* selected (defends against an AIA pointer to an unrelated certificate, and picks the right one
|
|
79
93
|
* out of a multi-certificate PKCS#7 bundle).
|
|
80
94
|
*
|
|
95
|
+
* @param anchorSubjects the subjects of the caller's trust anchors. A chain whose top is already
|
|
96
|
+
* issued by one of them is not missing anything, so nothing is fetched — chasing could not help
|
|
97
|
+
* and the fetch would be a blocking round trip to a host named by a certificate that just
|
|
98
|
+
* failed validation. This is what keeps an expired-but-complete chain, a hostname mismatch or
|
|
99
|
+
* any other non-path failure from triggering a chase, and it stops the genuine
|
|
100
|
+
* missing-intermediate case one hop early, where the chain reaches a root the caller already
|
|
101
|
+
* trusts. Empty means "unknown", and the walk runs to one of the other stopping conditions.
|
|
102
|
+
* @param maxUrlsPerCert how many of a certificate's distinct CA-Issuers URLs are tried before
|
|
103
|
+
* giving up on it. The extension is authored by the same server that failed validation and
|
|
104
|
+
* nothing limits how many URLs it may list — a leaf can carry hundreds — while each one costs a
|
|
105
|
+
* blocking connect on the handshake thread. Real CAs publish one. Duplicates are collapsed
|
|
106
|
+
* before the cap, so the cap buys distinct hosts, not repeat visits to one dead one.
|
|
107
|
+
* @param budgetMs wall-clock ceiling on the whole walk. It is checked before each fetch, and the
|
|
108
|
+
* deadline it implies is passed to [fetch], which is expected to clip its own connects,
|
|
109
|
+
* handshakes, reads and redirects to the time remaining — so the budget bounds the fetches
|
|
110
|
+
* themselves, not just how many of them start.
|
|
81
111
|
* @param fetch resolves a CA-Issuers URL to its candidate certificates (e.g. every certificate in
|
|
82
|
-
* a PKCS#7 bundle)
|
|
112
|
+
* a PKCS#7 bundle), giving up no later than `deadlineNanos` (absolute, [nowNanos] origin);
|
|
113
|
+
* empty if nothing could be retrieved.
|
|
83
114
|
*/
|
|
84
115
|
fun completeChain(
|
|
85
116
|
presented: List<X509Certificate>,
|
|
117
|
+
anchorSubjects: Set<X500Principal> = emptySet(),
|
|
86
118
|
maxIntermediates: Int = 5,
|
|
87
|
-
|
|
119
|
+
maxUrlsPerCert: Int = MAX_URLS_PER_CERT,
|
|
120
|
+
budgetMs: Long = MAX_CHASE_MILLIS,
|
|
121
|
+
nowNanos: () -> Long = System::nanoTime,
|
|
122
|
+
fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
|
|
88
123
|
): List<X509Certificate> {
|
|
89
124
|
if (presented.isEmpty()) return presented
|
|
90
125
|
val chain = presented.toMutableList()
|
|
91
126
|
val seen = HashSet(chain)
|
|
127
|
+
val deadline = nowNanos() + budgetMs * 1_000_000
|
|
92
128
|
var added = 0
|
|
93
129
|
while (added < maxIntermediates) {
|
|
94
130
|
val last = chain.last()
|
|
95
131
|
if (last.subjectX500Principal == last.issuerX500Principal) break // self-signed root
|
|
132
|
+
if (last.issuerX500Principal in anchorSubjects) break // already reaches a trust anchor
|
|
96
133
|
val issuer =
|
|
97
134
|
extractCaIssuerUrls(last)
|
|
98
135
|
.asSequence()
|
|
99
|
-
.
|
|
136
|
+
.distinct()
|
|
137
|
+
.take(maxUrlsPerCert)
|
|
138
|
+
// Subtraction, not `>`: overflow-safe across nanoTime's arbitrary origin.
|
|
139
|
+
.takeWhile { nowNanos() - deadline < 0 }
|
|
140
|
+
.flatMap { url -> runCatching { fetch(url, deadline) }.getOrElse { emptyList() } }
|
|
100
141
|
.firstOrNull { it.subjectX500Principal == last.issuerX500Principal } ?: break
|
|
101
142
|
if (!seen.add(issuer)) break // cycle guard
|
|
102
143
|
chain.add(issuer)
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
package com.audiobrowser.tls
|
|
2
2
|
|
|
3
|
+
import android.os.Build
|
|
4
|
+
import androidx.annotation.RequiresApi
|
|
5
|
+
import java.lang.reflect.InvocationTargetException
|
|
6
|
+
import java.lang.reflect.Method
|
|
3
7
|
import java.net.Socket
|
|
4
8
|
import java.security.cert.CertificateException
|
|
5
9
|
import java.security.cert.X509Certificate
|
|
6
10
|
import javax.net.ssl.SSLEngine
|
|
7
11
|
import javax.net.ssl.X509ExtendedTrustManager
|
|
8
12
|
import javax.net.ssl.X509TrustManager
|
|
13
|
+
import javax.security.auth.x500.X500Principal
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* The [AiaChasingTrustManager] behaviour, as an [X509ExtendedTrustManager].
|
|
@@ -18,9 +23,8 @@ import javax.net.ssl.X509TrustManager
|
|
|
18
23
|
* > checkServerTrusted(X509Certificate[], String, String) is used
|
|
19
24
|
*
|
|
20
25
|
* Wrapping it in a plain [X509TrustManager] hides those overloads, so conscrypt falls through to
|
|
21
|
-
* the two-argument form and *every* HTTPS connection in the process fails
|
|
22
|
-
*
|
|
23
|
-
* overload is also where endpoint identification happens for a raw `SSLSocket` configured with
|
|
26
|
+
* the two-argument form and *every* HTTPS connection in the process fails. The socket overload is
|
|
27
|
+
* also where endpoint identification happens for a raw `SSLSocket` configured with
|
|
24
28
|
* `endpointIdentificationAlgorithm = "HTTPS"`; skipping it skips hostname verification.
|
|
25
29
|
*
|
|
26
30
|
* [X509ExtendedTrustManager] is API 24+, hence the split from [AiaChasingTrustManager] — see
|
|
@@ -28,25 +32,49 @@ import javax.net.ssl.X509TrustManager
|
|
|
28
32
|
* themselves API 24+, so the plain variant on 23 loses nothing.
|
|
29
33
|
*
|
|
30
34
|
* @param delegate the wrapped trust manager, normally the platform default.
|
|
31
|
-
* @param fetch resolves a CA-Issuers URL to its candidate certificates
|
|
35
|
+
* @param fetch resolves a CA-Issuers URL to its candidate certificates, giving up no later than
|
|
36
|
+
* `deadlineNanos`.
|
|
32
37
|
*/
|
|
38
|
+
@RequiresApi(Build.VERSION_CODES.N)
|
|
33
39
|
class AiaChasingExtendedTrustManager(
|
|
34
40
|
private val delegate: X509TrustManager,
|
|
35
|
-
private val fetch: (url: String) -> List<X509Certificate>,
|
|
41
|
+
private val fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
|
|
36
42
|
) : X509ExtendedTrustManager() {
|
|
37
43
|
|
|
38
44
|
/** The delegate's own extended overloads, when it has them. */
|
|
39
45
|
private val extended = delegate as? X509ExtendedTrustManager
|
|
40
46
|
|
|
47
|
+
private val anchorSubjects: Set<X500Principal> by lazy { delegate.anchorSubjects() }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The delegate's Android-specific hostname-aware check, if it has one.
|
|
51
|
+
*
|
|
52
|
+
* `android.net.http.X509TrustManagerExtensions` — how an HTTP client reaches per-host certificate
|
|
53
|
+
* pinning — locates this method reflectively and refuses a trust manager that lacks it. It is not
|
|
54
|
+
* part of [X509ExtendedTrustManager], and `RootTrustManager` is not a public type, so it is
|
|
55
|
+
* forwarded reflectively rather than overridden.
|
|
56
|
+
*/
|
|
57
|
+
private val hostAwareCheck: Method? by lazy {
|
|
58
|
+
runCatching {
|
|
59
|
+
delegate.javaClass.getMethod(
|
|
60
|
+
"checkServerTrusted",
|
|
61
|
+
Array<X509Certificate>::class.java,
|
|
62
|
+
String::class.java,
|
|
63
|
+
String::class.java,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
.getOrNull()
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
42
|
-
withAiaRetry(chain) { delegate.checkServerTrusted(it, authType) }
|
|
70
|
+
withAiaRetry(chain, anchorSubjects, fetch) { delegate.checkServerTrusted(it, authType) }
|
|
43
71
|
|
|
44
72
|
override fun checkServerTrusted(
|
|
45
73
|
chain: Array<X509Certificate>,
|
|
46
74
|
authType: String,
|
|
47
75
|
socket: Socket?,
|
|
48
76
|
) =
|
|
49
|
-
withAiaRetry(chain) {
|
|
77
|
+
withAiaRetry(chain, anchorSubjects, fetch) {
|
|
50
78
|
// A delegate without the extended overloads cannot be hostname-aware in the first place,
|
|
51
79
|
// so falling back to its two-argument form loses nothing it could have offered.
|
|
52
80
|
extended?.checkServerTrusted(it, authType, socket)
|
|
@@ -58,11 +86,61 @@ class AiaChasingExtendedTrustManager(
|
|
|
58
86
|
authType: String,
|
|
59
87
|
engine: SSLEngine?,
|
|
60
88
|
) =
|
|
61
|
-
withAiaRetry(chain) {
|
|
89
|
+
withAiaRetry(chain, anchorSubjects, fetch) {
|
|
62
90
|
extended?.checkServerTrusted(it, authType, engine)
|
|
63
91
|
?: delegate.checkServerTrusted(it, authType)
|
|
64
92
|
}
|
|
65
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The signature `X509TrustManagerExtensions` looks for: validates against [host] and returns the
|
|
96
|
+
* chain it accepted. Kept in step with the other overloads, AIA retry included, so installing
|
|
97
|
+
* this wrapper does not cost a consumer its certificate pinning.
|
|
98
|
+
*/
|
|
99
|
+
@Suppress("unused") // called reflectively
|
|
100
|
+
fun checkServerTrusted(
|
|
101
|
+
chain: Array<X509Certificate>,
|
|
102
|
+
authType: String,
|
|
103
|
+
host: String,
|
|
104
|
+
): List<X509Certificate> {
|
|
105
|
+
var accepted: List<X509Certificate> = emptyList()
|
|
106
|
+
val method = hostAwareCheck
|
|
107
|
+
withAiaRetry(chain, anchorSubjects, fetch) { checked ->
|
|
108
|
+
// Whichever chain passed is the one to report — reporting the originally presented chain
|
|
109
|
+
// would hand a pinning consumer a chain missing the intermediate that made it validate.
|
|
110
|
+
// `ifEmpty` guards a delegate that reports nothing on success; the checked chain is what
|
|
111
|
+
// it accepted, so that is the answer.
|
|
112
|
+
accepted =
|
|
113
|
+
if (method != null)
|
|
114
|
+
invokeHostAware(method, checked, authType, host).ifEmpty { checked.toList() }
|
|
115
|
+
else {
|
|
116
|
+
// Same routing as the other overloads: an extended delegate may refuse its
|
|
117
|
+
// two-argument form outright (the domain-config case), so prefer its socket overload.
|
|
118
|
+
extended?.checkServerTrusted(checked, authType, null as Socket?)
|
|
119
|
+
?: delegate.checkServerTrusted(checked, authType)
|
|
120
|
+
checked.toList()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return accepted
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Suppress("UNCHECKED_CAST")
|
|
127
|
+
private fun invokeHostAware(
|
|
128
|
+
method: Method,
|
|
129
|
+
chain: Array<X509Certificate>,
|
|
130
|
+
authType: String,
|
|
131
|
+
host: String,
|
|
132
|
+
): List<X509Certificate> =
|
|
133
|
+
try {
|
|
134
|
+
method.invoke(delegate, chain, authType, host) as List<X509Certificate>
|
|
135
|
+
} catch (e: InvocationTargetException) {
|
|
136
|
+
// Unwrap, so a rejection still reaches withAiaRetry as the CertificateException it is.
|
|
137
|
+
when (val cause = e.targetException) {
|
|
138
|
+
is CertificateException -> throw cause
|
|
139
|
+
is RuntimeException -> throw cause
|
|
140
|
+
else -> throw CertificateException(cause)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
66
144
|
// Client certificates are the app's own; there is nothing to chase.
|
|
67
145
|
|
|
68
146
|
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
@@ -87,23 +165,4 @@ class AiaChasingExtendedTrustManager(
|
|
|
87
165
|
}
|
|
88
166
|
|
|
89
167
|
override fun getAcceptedIssuers(): Array<X509Certificate> = delegate.acceptedIssuers
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Runs [check], and on rejection retries it once against the AIA-completed chain. The completed
|
|
93
|
-
* chain goes through the same [check], so trust is never weakened: whatever the delegate would
|
|
94
|
-
* have refused, it still refuses.
|
|
95
|
-
*/
|
|
96
|
-
private inline fun withAiaRetry(
|
|
97
|
-
chain: Array<X509Certificate>,
|
|
98
|
-
check: (Array<X509Certificate>) -> Unit,
|
|
99
|
-
) {
|
|
100
|
-
try {
|
|
101
|
-
check(chain)
|
|
102
|
-
} catch (original: CertificateException) {
|
|
103
|
-
val completed = AiaCertChaser.completeChain(chain.toList(), fetch = fetch)
|
|
104
|
-
// Nothing could be added — the original failure stands (don't re-check an unchanged chain).
|
|
105
|
-
if (completed.size == chain.size) throw original
|
|
106
|
-
check(completed.toTypedArray())
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
168
|
}
|
|
@@ -3,6 +3,7 @@ package com.audiobrowser.tls
|
|
|
3
3
|
import java.security.cert.CertificateException
|
|
4
4
|
import java.security.cert.X509Certificate
|
|
5
5
|
import javax.net.ssl.X509TrustManager
|
|
6
|
+
import javax.security.auth.x500.X500Principal
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* An [X509TrustManager] that retries validation after filling in missing intermediate certificates
|
|
@@ -10,28 +11,71 @@ import javax.net.ssl.X509TrustManager
|
|
|
10
11
|
*
|
|
11
12
|
* Wraps a [delegate] (typically the platform default trust manager). On the happy path the delegate
|
|
12
13
|
* accepts the server-presented chain on the first try and this adds nothing. Only when the delegate
|
|
13
|
-
* rejects the chain
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* rejects the chain *and* the top of the chain is not already issued by one of the delegate's
|
|
15
|
+
* anchors does it attempt AIA chasing. That gate is an issuer-name test, not a diagnosis: it rules
|
|
16
|
+
* out the common non-path failures (an expired leaf, a hostname mismatch on a complete chain), but
|
|
17
|
+
* a chain topping at a root the device does not carry is still chased whatever its real problem
|
|
18
|
+
* was. Either way the *completed* chain is still validated by the same delegate against the system
|
|
19
|
+
* trust anchors, so trust can never be weakened: an untrusted root still fails. See
|
|
20
|
+
* [AiaCertChaser].
|
|
21
|
+
*
|
|
22
|
+
* This is the API 23 variant. On 24+ [AiaChasingExtendedTrustManager] is used instead, and must be:
|
|
23
|
+
* see its documentation for why a plain [X509TrustManager] is not a safe wrapper there.
|
|
16
24
|
*/
|
|
17
25
|
class AiaChasingTrustManager(
|
|
18
26
|
private val delegate: X509TrustManager,
|
|
19
|
-
private val fetch: (url: String) -> List<X509Certificate>,
|
|
27
|
+
private val fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
|
|
20
28
|
) : X509TrustManager {
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
val completed = AiaCertChaser.completeChain(chain.toList(), fetch = fetch)
|
|
27
|
-
// Nothing could be added — the original failure stands (don't re-check an unchanged chain).
|
|
28
|
-
if (completed.size == chain.size) throw original
|
|
29
|
-
delegate.checkServerTrusted(completed.toTypedArray(), authType)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
30
|
+
private val anchorSubjects: Set<X500Principal> by lazy { delegate.anchorSubjects() }
|
|
31
|
+
|
|
32
|
+
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
33
|
+
withAiaRetry(chain, anchorSubjects, fetch) { delegate.checkServerTrusted(it, authType) }
|
|
32
34
|
|
|
33
35
|
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) =
|
|
34
36
|
delegate.checkClientTrusted(chain, authType)
|
|
35
37
|
|
|
36
38
|
override fun getAcceptedIssuers(): Array<X509Certificate> = delegate.acceptedIssuers
|
|
37
39
|
}
|
|
40
|
+
|
|
41
|
+
/** The subjects of a trust manager's accepted issuers; empty if it will not say. */
|
|
42
|
+
internal fun X509TrustManager.anchorSubjects(): Set<X500Principal> =
|
|
43
|
+
runCatching { acceptedIssuers.mapTo(HashSet()) { it.subjectX500Principal } }
|
|
44
|
+
.getOrElse { emptySet() }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runs [check], and on rejection retries it once against the AIA-completed chain.
|
|
48
|
+
*
|
|
49
|
+
* Nothing is fetched when the presented chain already reaches one of the caller's anchors — see
|
|
50
|
+
* [AiaCertChaser.completeChain]'s `anchorSubjects`. Without that test *every* failed handshake in
|
|
51
|
+
* the process, whatever the cause, would make a blocking outbound request to a host named by the
|
|
52
|
+
* certificate that just failed: an expired certificate, a hostname mismatch and a
|
|
53
|
+
* certificate-transparency refusal all arrive here as a [CertificateException] from a delegate that
|
|
54
|
+
* is perfectly happy with the path. (The test is on issuer names, so a chain topping at an unknown
|
|
55
|
+
* root is still chased even when its real failure was something else.)
|
|
56
|
+
*
|
|
57
|
+
* The completed chain goes through the same [check], so trust is never weakened: whatever the
|
|
58
|
+
* delegate would have refused, it still refuses.
|
|
59
|
+
*/
|
|
60
|
+
internal inline fun withAiaRetry(
|
|
61
|
+
chain: Array<X509Certificate>,
|
|
62
|
+
anchorSubjects: Set<X500Principal>,
|
|
63
|
+
noinline fetch: (url: String, deadlineNanos: Long) -> List<X509Certificate>,
|
|
64
|
+
check: (Array<X509Certificate>) -> Unit,
|
|
65
|
+
) {
|
|
66
|
+
try {
|
|
67
|
+
check(chain)
|
|
68
|
+
} catch (original: CertificateException) {
|
|
69
|
+
val completed = AiaCertChaser.completeChain(chain.toList(), anchorSubjects, fetch = fetch)
|
|
70
|
+
// Nothing could be added — the original failure stands (don't re-check an unchanged chain).
|
|
71
|
+
if (completed.size == chain.size) throw original
|
|
72
|
+
try {
|
|
73
|
+
check(completed.toTypedArray())
|
|
74
|
+
} catch (retried: CertificateException) {
|
|
75
|
+
// The chase can change which failure the caller sees (a missing intermediate can turn out
|
|
76
|
+
// to hide an expiry). Keep the first one attached rather than losing it.
|
|
77
|
+
retried.addSuppressed(original)
|
|
78
|
+
throw retried
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -5,6 +5,7 @@ import java.security.KeyStore
|
|
|
5
5
|
import javax.net.ssl.SSLContext
|
|
6
6
|
import javax.net.ssl.SSLSocketFactory
|
|
7
7
|
import javax.net.ssl.TrustManagerFactory
|
|
8
|
+
import javax.net.ssl.X509ExtendedTrustManager
|
|
8
9
|
import javax.net.ssl.X509TrustManager
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -12,11 +13,24 @@ import javax.net.ssl.X509TrustManager
|
|
|
12
13
|
* following the leaf's Authority Information Access (AIA) "CA Issuers" pointer — the behaviour
|
|
13
14
|
* Apple's Secure Transport has by default but Android's lacks.
|
|
14
15
|
*
|
|
15
|
-
* The returned components
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* The returned components wrap the platform default trust manager and only ever *add* a missing
|
|
17
|
+
* intermediate before re-validating against the same system trust anchors, so they cannot weaken
|
|
18
|
+
* trust: whatever the platform would have refused, it still refuses.
|
|
19
|
+
*
|
|
20
|
+
* What they are not is side-effect-free. Completing a chain means fetching the missing intermediate
|
|
21
|
+
* from a URL named by the certificate that just failed to validate — so a rejected handshake can
|
|
22
|
+
* make an outbound request to a host, port and path of that server's choosing, over cleartext, not
|
|
23
|
+
* subject to the app's `NetworkSecurityPolicy`. That is inherent to AIA chasing and is what
|
|
24
|
+
* browsers and Apple's Secure Transport do too, but it is a real consequence of installing this,
|
|
25
|
+
* and it is bounded rather than absent: see [CachingCertificateFetcher] for the per-fetch limits
|
|
26
|
+
* and [AiaCertChaser.MAX_CHASE_MILLIS] for the wall-clock ceiling on the whole chase. The fetch is
|
|
27
|
+
* skipped when the chain already reaches one of the system anchors — an issuer-name test, so it
|
|
28
|
+
* spares the common expired-leaf and hostname-mismatch failures, but a chain topping at a root the
|
|
29
|
+
* device does not carry is still chased whatever its real problem was.
|
|
30
|
+
*
|
|
31
|
+
* Installing them as a process default (e.g.
|
|
32
|
+
* `HttpsURLConnection.setDefaultSSLSocketFactory(AiaTls.socketFactory())`) applies that to every
|
|
33
|
+
* connection in the process, and is the caller's decision.
|
|
20
34
|
*/
|
|
21
35
|
object AiaTls {
|
|
22
36
|
|
|
@@ -38,7 +52,7 @@ object AiaTls {
|
|
|
38
52
|
*/
|
|
39
53
|
fun trustManager(): X509TrustManager {
|
|
40
54
|
val fetcher = CachingCertificateFetcher()
|
|
41
|
-
val fetch = { url: String -> fetcher.fetch(url) }
|
|
55
|
+
val fetch = { url: String, deadlineNanos: Long -> fetcher.fetch(url, deadlineNanos) }
|
|
42
56
|
val system = systemTrustManager()
|
|
43
57
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
44
58
|
AiaChasingExtendedTrustManager(system, fetch)
|