Skip to content

Commit 3a052e0

Browse files
committed
android: only check EE certificate revocation status
This commit updates the `CertificateVerifier.kt` logic used on Android to change our revocation checking preferences. Now, we only check the end entity certificate revocation status, preferring OCSP, and allowing soft failure. Checking the entire certificate chain's revocation status is infeasible with the platform options exposed to us: intermediates without complete authority information access (AIA) information will hard fail. Performing only EE revocation checking is preferable to more complicated logic that attempts to decide apriori whether the chain has enough information to support complete revocation checking or not. As a result of only checking EE status the logic and unit test for determining if a certificate is a known root (one installed in the system trust store) fall away.
1 parent 698744f commit 3a052e0

File tree

2 files changed

+12
-110
lines changed

2 files changed

+12
-110
lines changed

android/rustls-platform-verifier/src/androidTest/java/org/rustls/platformverifier/CertificateVerifierTests.kt

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import android.content.Context
44
import androidx.test.ext.junit.runners.AndroidJUnit4
55
import androidx.test.platform.app.InstrumentationRegistry
66
import org.junit.Assert.assertEquals
7-
import org.junit.Assert.assertTrue
87
import org.junit.BeforeClass
98
import org.junit.Test
109
import org.junit.runner.RunWith
@@ -51,22 +50,4 @@ class CertificateVerifierTests {
5150
val result = verifyMockRootUsage(context)
5251
assertEquals(FAILURE_MSG, SUCCESS_MARKER, result)
5352
}
54-
55-
// Note:
56-
//
57-
// - Full negative path (`CertificateVerifier`'s flow for unknown roots,
58-
// end-entity-only revocation check) already exercised via `runMockTestSuite`.
59-
//
60-
// - Full positive path (`CertificateVerifier`'s flow for known roots,
61-
// full-chain revocation check) already exercised via `runRealWorldTestSuite`.
62-
@Test
63-
fun runTestIsPublicRoot() {
64-
val rootCAs = CertificateVerifier.getSystemRootCAs()
65-
66-
// Positive - can ID known roots
67-
assertTrue(rootCAs.isNotEmpty())
68-
for (ca in rootCAs) {
69-
assertTrue(CertificateVerifier.isKnownRoot(ca))
70-
}
71-
}
7253
}

android/rustls-platform-verifier/src/main/java/org/rustls/platformverifier/CertificateVerifier.kt

Lines changed: 12 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -276,38 +276,23 @@ internal object CertificateVerifier {
276276
//
277277
// 2: Likely because of 1, Android requires all issued certificates to have some form of
278278
// revocation included in their authority information. This doesn't work universally as
279-
// internal CAs managed by companies aren't required to follow this (and generally don't),
280-
// so verifying those certificates would fail.
279+
// issuing certificates in use may omit authority access information (for example the
280+
// Let's Encrypt R3 Intermediate Certificate).
281281
//
282-
// Revocation checking has two factors:
283-
//
284-
// 1. Is the root CA known (installed in system trust store)?
285-
// 2. Did the server staple an OSCP response for it's own leaf certificate?
286-
//
287-
// Thus the below revocation logic handles four cases:
288-
//
289-
// 1. Known root + OSCP stapled -> Full-chain revocation, no extra network use
290-
// 2. Known root + no OSCP stapled -> Full-chain revocation, with extra network use
291-
// 3. Unknown root + OSCP stapled -> End-entity-only revocation, no extra network use
292-
// 4. Unknown root + no OSCP stapled -> End-entity-only revocation, with extra network use
282+
// Given these constraints, the best option is to only check revocation information
283+
// at the end-entity depth. We will prefer OCSP (to use stapled information if possible).
284+
// If there is no stapled OCSP response, Android may use the network to attempt to fetch
285+
// one. If OCSP checking fails, it may fall back to fetching CRLs. We allow "soft"
286+
// failures, for example transient network errors.
293287
val parameters = PKIXBuilderParameters(keystore, null)
294288

295289
val validator = CertPathValidator.getInstance("PKIX")
296290
val revocationChecker = validator.revocationChecker as PKIXRevocationChecker
297291

298-
// `PKIXRevocationChecker` checks the entire chain by default.
299-
// We allow it to fail if there are network issues.
300-
// If the chain's root is known, use this default setting for full-chain
301-
// revocation (excludes root itself, see below).
302-
// Else, only check revocation status for the end-entity.
303-
revocationChecker.options = if (isKnownRoot(validChain.last())) {
304-
EnumSet.of(PKIXRevocationChecker.Option.SOFT_FAIL)
305-
} else {
306-
EnumSet.of(
307-
PKIXRevocationChecker.Option.SOFT_FAIL,
308-
PKIXRevocationChecker.Option.ONLY_END_ENTITY
309-
)
310-
}
292+
revocationChecker.options = EnumSet.of(
293+
PKIXRevocationChecker.Option.SOFT_FAIL,
294+
PKIXRevocationChecker.Option.ONLY_END_ENTITY
295+
)
311296

312297
// Use the OCSP data `rustls` provided, if present.
313298
// Its expected that the server only sends revocation data for its own leaf certificate.
@@ -329,14 +314,8 @@ internal object CertificateVerifier {
329314
// - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker
330315
parameters.isRevocationEnabled = false
331316

332-
// Validate the revocation status of all non-root certificates in the chain.
317+
// Validate the revocation status of the end entity certificate.
333318
try {
334-
// `checkServerTrusted` always returns a trusted full chain. However, root CAs
335-
// don't have revocation properties so attempting to validate them as such fails.
336-
// To avoid this, always remove the root CA from the chain before validating its
337-
// revocation status. This is identical to the `CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT`
338-
// flag in the Win32 API.
339-
validChain.removeLast()
340319
validator.validate(certFactory.generateCertPath(validChain), parameters)
341320
} catch (e: CertPathValidatorException) {
342321
return VerificationResult(StatusCode.Revoked, e.toString())
@@ -383,62 +362,4 @@ internal object CertificateVerifier {
383362

384363
return String(hexChars)
385364
}
386-
387-
// Check if CA root is known or not.
388-
// Known means installed in root CA store, either a preset public CA or a custom one installed by an enterprise.
389-
// Function is public for testing only.
390-
//
391-
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=351
392-
fun isKnownRoot(root: X509Certificate): Boolean {
393-
// System keystore and cert directory must be non-null to perform checking
394-
systemKeystore?.let { loadedSystemKeystore ->
395-
systemCertificateDirectory?.let { loadedSystemCertificateDirectory ->
396-
397-
// Check the in-memory cache first
398-
val key = Pair(root.subjectX500Principal, root.publicKey)
399-
if (systemTrustAnchorCache.contains(key)) {
400-
return true
401-
}
402-
403-
// System trust anchors are stored under a hash of the principal.
404-
// In case of collisions, append number.
405-
val hash = hashPrincipal(root.subjectX500Principal)
406-
var i = 0
407-
while (true) {
408-
val alias = "$hash.$i"
409-
410-
if (!File(loadedSystemCertificateDirectory, alias).exists()) {
411-
break
412-
}
413-
414-
val anchor = loadedSystemKeystore.getCertificate("system:$alias")
415-
416-
// It's possible for `anchor` to be `null` if the user deleted a trust anchor.
417-
// Continue iterating as there may be further collisions after the deleted anchor.
418-
if (anchor == null) {
419-
continue
420-
// This should never happen
421-
} else if (anchor !is X509Certificate) {
422-
// SAFETY: This logs a unique identifier (hash value) only in cases where a file within the
423-
// system's root trust store is not a valid X509 certificate (extremely unlikely error).
424-
// The hash doesn't tell us any sensitive information about the invalid cert or reveal any of
425-
// its contents - it just lets us ID the bad file if a customer is having TLS failure issues.
426-
Log.e(TAG, "anchor is not a certificate, alias: $alias")
427-
continue
428-
// If subject and public key match, it's a system root.
429-
} else {
430-
if ((root.subjectX500Principal == anchor.subjectX500Principal) && (root.publicKey == anchor.publicKey)) {
431-
systemTrustAnchorCache.add(key)
432-
return true
433-
}
434-
}
435-
436-
i += 1
437-
}
438-
}
439-
}
440-
441-
// Not found in cache or store: non-public
442-
return false
443-
}
444365
}

0 commit comments

Comments
 (0)