WIP: android: avoid revocation check missing OCSP URI exceptions. #34
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is a draft for now, as there are some trade-offs to discuss here. Apologies for the length of this PR description. I did a lot of digging around to come up with this approach and I wanted to provide as much context as I could (both for my future self and for other maintainers).
Description
Revocation checking on Android has a few peculiarities that must be accounted for in order to maximize the value of the revocation checking that rustls-platform-verifier does given the platform constraints. See #13 for more information.
Most importantly, when we check a certificate chain preferring OCSP (default behaviour), Android's revocation checker implementation will throw a fatal exception if any certificates within the considered chain depth are missing an OCSP staple (which is true for all intermediates, only end-entity certs can have stapled OCSP) or are missing an authority information access extension with an OCSP access method and URI location (which is common for real-world intermediates like Let's Encrypt's R3 intermediate certificate).
In essence we have two decisions to make for any given validated certificate chain when it comes time to check revocation status:
We want our decisions to:
Unfortunately the existing APIs and PKI practices mean we have to make some compromises. Read on for more.
End entity vs Full Chain
Prior to this branch we made the determination of revocation checking depth exclusively by determining if the certificate chain was issued by a known system root trust anchor. If it was, we checked the whole certificate path (minus root trust anchor, since these can not be revoked by traditional means). If it wasn't, we checked only the end entity certificate:
rustls-platform-verifier/android/rustls-platform-verifier/src/main/java/org/rustls/platformverifier/CertificateVerifier.kt
Lines 300 to 307 in ce16ccc
In this branch we adjust this logic so that:
Checking only the end entity certificate when we lack OCSP revocation information for the rest of the chain is a trade-off: we can't specify OCSP vs CRL preferences per-certificate, just per-chain, so if we have a chain that has an OCSP URI or stapled OCSP response for the end-entity certificate, we prefer to check only the end-entity certificate revocation status rather than checking the whole chain when we know the chain is missing OCSP revocation information for an intermediate. The alternatives are to proceed with full chain verification, at which point an exception will be thrown from the missing OCSP information (current behaviour), or to prefer CRL validation for the full chain (which would ignore that we have enough information to check OCSP for the end-entity).
Prefer OCSP vs CRL only
Prior to this branch we always preferred OCSP (the default setting). In theory the Android revocation checker should fall back to checking CRLs if it encounters an error checking OCSP, but in practice this doesn't seem to work (TODO: Can we get a better answer for why?). Instead the first certificate that the revocation checker encounters without an OCSP staple or a suitable AIA OCSP URL results in a fatal exception of the form "certificate was revoked: java.security.cert.CertPathValidatorException: Certificate does not specify OCSP responder".
To try to meet our goals this branch changes the revocation logic so that:
"Does this cert have an AIA OCSP URL?"
As part of our new logic we need to make the determination about whether a certificate has an Authority Information Access extension that specifies an OCSP URI. Unfortunately Android/Java make this difficult :-(
The system provider's own OCSP implementation used by the revocation checker has all of the logic required to parse an AIA extension to find an OCSP URI, but it's locked away in platform internal only classes under the
sun.security.provider.*
namespace that we can not import. None of theX509Certificate
andCertificate
classes, or theX509Extension
interface provide access to the AIA extension.However
X509Certificate
implementsX509Extension
, which offers a method for getting a raw DER encoded extension value by OID. We can use this to access the raw DER encoding of the AIA extension.Our next challenge is doing something useful with the raw DER extension value. Unfortunately again the tools required for DER parsing are present, but locked away in internal-only classes that we can't use. Frequently folks will take a dependency on BouncyCastle to parse DER but this is an extremely heavy-weight cryptography library (and one I find particularly gross to work with...).
As a last resort we take a more extreme, but lightweight, approach: we have the DER encoding of the overall extension value, and we know the DER encoding of the OID that specifies a OCSP access method within that extension (
id-ad-ocsp
) so we can simply try to find the matching byte sequence for that OID in the raw DER extension. This is sufficient for our purposes as a heuristic for whether or not OCSP checking will fail from a missing OCSP URI. A maliciously crafted certificate could include an AIA extension designed to fool our heuristic into thinking there is an OCSP URI when there isn't, but this will only result in a revocation rejection when the provider OCSP checking code fails to find a real OCSP URI. It would also have to be a certificate issued by a known trust anchor since we do all of this work after verifying the chain signatures up to a trust anchor.If this approach is too gross I think the best alternative is to have Rust code do this parsing and provide the extension URI (if present) along with each certificate. Note that
webpki
andrustls
don't have existing code to parse/expose the AIA extension either, so this will require implementing that code somewhere, or taking a dependency on a large crate likex509-parser
.Let's Encrypt testcase
So far this branch includes one additional real world verification test, bundling a Let's Encrypt certificate chain of the form EE (AIA OCSP present) -> R3 intermediate (no AIA OCSP) -> ISRG Root X1 (no AIA OCSP) -> trust anchor.
Before the changes in this branch revocation checking would always fail for this chain, producing the "certificate was revoked: java.security.cert.CertPathValidatorException: Certificate does not specify OCSP responder" result observed in #13 due to the two intermediate certificates missing AIA OCSP URLs.
With the changes in this branch revocation checking succeeds:
Ultimately this translates into using the revocation checker options
[ONLY_END_ENTITY, SOFT_FAIL]
, and a positive "not revoked" determination.Other approaches
Unfortunately there's precious little information or example code in the wild for Android revocation checking. What I did find tended to either:
a) Ignore this problem, presumably encountering the exception we see when checking full chains when some certificates are missing OCSP information (e.g. this gist)
b) Tried to ignore this exception in particular, by matching the error message.
Neither approach seemed viable to me.
Todo