I came across a weird issue of a java client not being able to connect to a webserver because a certificate was not valid or somehow untrusted even though the validity of the certificate is fine (at least that's what I thought):
CertificateException - java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
After some google search, it says this was a bug in some JAVA versions and happens if the intermediate has a longer validity time than the root ca. Is a certificate chain like the following valid?
Issuer: O = Digital Signature Trust Co., CN = DST Root CA X3
Validity
Not Before: Jan 20 19:14:03 2021 GMT
Not After : Sep 30 18:14:03 2024 GMT
Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X1
Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1
Validity
Not Before: Sep 4 00:00:00 2020 GMT
Not After : Sep 15 16:00:00 2025 GMT // longer than the root ca
Subject: C = US, O = Let's Encrypt, CN = R3
Issuer: C = US, O = Let's Encrypt, CN = R3
Validity
Not Before: Aug 16 14:00:00 2023 GMT
Not After : Nov 14 14:00:00 2023 GMT
Meta: not necessarily programming or development, but too long for comment, and we already have PLENTY on this topic.
Yes this is permitted according to RFC5280; it requires that each cert's validity period include the time being validated (i.e. 'now' for a TLS handshake) but not that the periods for different certs relate in a particular way. And yes there was a bug in some Java versions that wrongly rejected this case (child period not 'nested' within parent), but that was in 2012 (I know because I still have some dummy certs I use in dev testing that I had to reissue then because of this bug); I very much doubt you'll encounter it now.
But the DST X3 root (which you don't show; the root generally is not sent in the TLS handshake, but provided from the client's truststore) expired in Sep. 2021; many other implementations (not Java) require the root to be unexpired, so they immediately began rejecting this (LetsEncrypt) chain, and we have LOTS of Qs and As from that time on several Stacks including SO. And because it expired most implementations (also) removed it from their truststores including Java which (obviously!) prevents this specific chain from validating. LetsEncrypt obtained the unusual past-expiration bridge cert from DST/Identrust and continues to use it to support old Android devices which (1) pre-date LE/ISRG's creation and can't add its root because they are no longer updated and (2) ignore the DST X3 expiration (because Java); see https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/ . But they plan to finally drop this next year, see https://letsencrypt.org/2023/07/10/cross-sign-expiration.html .
On the other hand most implementations, including Java, DO have the root cert for ISRG X1 (as opposed to the bridge cert using DST X3) in their truststores since about 2017, and a TLS client is always permitted to ignore the chain sent by the server except for the leaf/EE cert, and build a different chain, in this case one reaching the valid (and unexpired) ISRG root -- and Java, or more specifically JSSE's default TrustManager
, DOES do this, so it should validate a TLS server using an LE cert and presenting the still-default 'compatibility' LE chain as you show, and all my Java apps do so for me. (You may notice, by the way, that StackExchange itself, including SO, uses LE certs, with this chain at the moment.)
Your exact exception is also surprising. For a server using an invalid cert/chain, JSSE normally fails with an ultimate cause of ... SunCertPathBuilderException: unable to find valid certification path to requested target
not CertPathValidatorException: Path does not chain with any of the trust anchors
; these are semantically similar but definitely not the same. It looks like you are modifying the standard trust logic somehow, and likely that modification is causing the error. In which case to be helped you should show a minimal reproducible example of the problem.