Skip to content

Commit 0a16568

Browse files
committed
NFC-128 Add revocation checking for intermediate CA certificates
1 parent ce5fdc6 commit 0a16568

4 files changed

Lines changed: 185 additions & 17 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,13 +343,13 @@ Starting from format version `web-eid:1.1`, the authentication token may additio
343343

344344
In addition to the fields described above, the `web-eid:1.1` format defines the following additional fields:
345345

346-
- `unverifiedIntermediateCertificates`: an array of base64-encoded DER intermediate CA certificates that make up the trust chain of the authentication certificate in `unverifiedCertificate`. Like the authentication certificate, these certificates are received from the client side and cannot be trusted; they are only used as candidate certificates when building the certification path, which must still terminate at a trusted certificate authority. The field is optional, but when present it must not be empty. When it is present, `unverifiedSigningCertificates` may be omitted.
346+
- `unverifiedIntermediateCertificates`: an array of base64-encoded DER intermediate CA certificates that make up the trust chain of the authentication certificate in `unverifiedCertificate`. Like the authentication certificate, these certificates are received from the client side and cannot be trusted; they are only used as candidate certificates when building the certification path, which must still terminate at a trusted certificate authority. Every intermediate selected for the path is revocation-checked with OCSP or CRLs and the token is rejected if its status cannot be determined. The field is optional, but when present it must not be empty. When it is present, `unverifiedSigningCertificates` may be omitted.
347347

348348
- `unverifiedSigningCertificates`: an array of the eID user's signing certificates, presented alongside the authentication certificate. For `web-eid:1.1` tokens this field is required unless `unverifiedIntermediateCertificates` is present, and when present it must not be empty. Each entry contains:
349349
350350
- `certificate`: the base64-encoded DER signing certificate. During validation it must have the same subject and issuer as the authentication certificate, be valid, contain the non-repudiation key usage bit and be signed by a trusted certificate authority.
351351
352-
- `intermediateCertificates`: an optional array of base64-encoded DER intermediate CA certificates that make up the trust chain of this signing certificate. When present it must not be empty and, as with `unverifiedIntermediateCertificates`, the certificates are only used as candidates when building the certification path to a trusted CA.
352+
- `intermediateCertificates`: an optional array of base64-encoded DER intermediate CA certificates that make up the trust chain of this signing certificate. When present it must not be empty and, as with `unverifiedIntermediateCertificates`, the certificates are only used as candidates when building the certification path to a trusted CA and every selected intermediate is revocation-checked.
353353
354354
- `supportedSignatureAlgorithms`: the signature algorithms that the signing certificate's key supports. Each entry has a `cryptoAlgorithm` (`ECC` or `RSA`), a `hashFunction` (one of `SHA-224`, `SHA-256`, `SHA-384`, `SHA-512`, `SHA3-224`, `SHA3-256`, `SHA3-384`, `SHA3-512`) and a `paddingScheme` (`NONE`, `PKCS1.5` or `PSS`).
355355

src/main/java/eu/webeid/security/certificate/CertificateValidator.java

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,20 @@
3030
import java.security.GeneralSecurityException;
3131
import java.security.InvalidAlgorithmParameterException;
3232
import java.security.NoSuchAlgorithmException;
33+
import java.security.cert.CertPath;
3334
import java.security.cert.CertPathBuilder;
3435
import java.security.cert.CertPathBuilderException;
36+
import java.security.cert.CertPathValidator;
37+
import java.security.cert.CertPathValidatorException;
3538
import java.security.cert.CertStore;
3639
import java.security.cert.Certificate;
40+
import java.security.cert.CertificateException;
41+
import java.security.cert.CertificateFactory;
3742
import java.security.cert.CollectionCertStoreParameters;
3843
import java.security.cert.PKIXBuilderParameters;
3944
import java.security.cert.PKIXCertPathBuilderResult;
45+
import java.security.cert.PKIXParameters;
46+
import java.security.cert.PKIXRevocationChecker;
4047
import java.security.cert.TrustAnchor;
4148
import java.security.cert.X509CertSelector;
4249
import java.security.cert.X509Certificate;
@@ -89,20 +96,64 @@ public static X509Certificate validateIsSignedByTrustedCA(X509Certificate certif
8996
final CertPathBuilder certPathBuilder = CertPathBuilder.getInstance(CertPathBuilder.getDefaultType());
9097
final PKIXCertPathBuilderResult result = (PKIXCertPathBuilderResult) certPathBuilder.build(pkixBuilderParameters);
9198

99+
validateIntermediateCertificatesNotRevoked(
100+
result,
101+
trustedCACertificateAnchors,
102+
trustedCACertificateCertStore,
103+
additionalIntermediateCertificates,
104+
now
105+
);
106+
92107
final X509Certificate trustedCACert = result.getTrustAnchor().getTrustedCert();
93108

94109
// Verify that the trusted CA cert is presently valid before returning the result.
95110
certificateIsValidOnDate(trustedCACert, now, "Trusted CA");
96111

97112
return getIssuerCertificate(result, trustedCACert);
98113

99-
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) {
114+
} catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | CertificateException e) {
100115
throw new JceException(e);
101-
} catch (CertPathBuilderException e) {
116+
} catch (CertPathBuilderException | CertPathValidatorException e) {
102117
throw new CertificateNotTrustedException(certificate, e);
103118
}
104119
}
105120

121+
private static void validateIntermediateCertificatesNotRevoked(
122+
PKIXCertPathBuilderResult pathBuilderResult,
123+
Set<TrustAnchor> trustedCACertificateAnchors,
124+
CertStore trustedCACertificateCertStore,
125+
List<X509Certificate> additionalIntermediateCertificates,
126+
Date now
127+
) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertificateException,
128+
CertPathValidatorException {
129+
final List<? extends Certificate> certificatePath = pathBuilderResult.getCertPath().getCertificates();
130+
if (certificatePath.size() <= 1) {
131+
return; // leaf chains directly to a trust anchor; no non-anchor intermediate to validate
132+
}
133+
134+
// The existing application OCSP flow checks the end-entity certificate. Validate only the CA suffix here,
135+
// excluding both the end entity at index 0 and the trust anchor, which is not part of the built path.
136+
final CertPath intermediateCertificatePath = CertificateFactory.getInstance("X.509")
137+
.generateCertPath(certificatePath.subList(1, certificatePath.size()));
138+
final PKIXParameters revocationCheckingParameters = new PKIXParameters(trustedCACertificateAnchors);
139+
revocationCheckingParameters.setDate(now);
140+
// An explicitly added checker is active regardless of this flag and avoids installing a second default checker.
141+
revocationCheckingParameters.setRevocationEnabled(false);
142+
revocationCheckingParameters.addCertStore(trustedCACertificateCertStore);
143+
if (additionalIntermediateCertificates != null && !additionalIntermediateCertificates.isEmpty()) {
144+
revocationCheckingParameters.addCertStore(CertStore.getInstance(
145+
"Collection", new CollectionCertStoreParameters(additionalIntermediateCertificates)));
146+
}
147+
148+
final CertPathValidator certPathValidator = CertPathValidator.getInstance(CertPathValidator.getDefaultType());
149+
final PKIXRevocationChecker revocationChecker =
150+
(PKIXRevocationChecker) certPathValidator.getRevocationChecker();
151+
// The default checker prefers OCSP and falls back to CRLs. SOFT_FAIL is deliberately not enabled: a token-
152+
// supplied intermediate whose revocation status cannot be established must not become part of a trusted path.
153+
revocationCheckingParameters.addCertPathChecker(revocationChecker);
154+
certPathValidator.validate(intermediateCertificatePath, revocationCheckingParameters);
155+
}
156+
106157
private static X509Certificate getIssuerCertificate(PKIXCertPathBuilderResult path, X509Certificate trustedCACert) {
107158
// The built path is ordered from the subject towards the anchor and excludes the anchor, so index 1 (when
108159
// present) is the subject's direct issuer; otherwise the subject was issued directly by the trust anchor.

src/test/java/eu/webeid/security/certificate/CertificateValidatorTest.java

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@
2222

2323
package eu.webeid.security.certificate;
2424

25+
import eu.webeid.security.exceptions.CertificateNotTrustedException;
2526
import org.bouncycastle.asn1.x500.X500Name;
2627
import org.bouncycastle.asn1.x509.BasicConstraints;
2728
import org.bouncycastle.asn1.x509.Extension;
2829
import org.bouncycastle.asn1.x509.KeyUsage;
30+
import org.bouncycastle.cert.X509v2CRLBuilder;
31+
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter;
2932
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
3033
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
3134
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
@@ -39,15 +42,19 @@
3942
import java.security.KeyPairGenerator;
4043
import java.security.PrivateKey;
4144
import java.security.PublicKey;
45+
import java.security.cert.CertPathValidatorException;
4246
import java.security.cert.CertStore;
47+
import java.security.cert.CollectionCertStoreParameters;
4348
import java.security.cert.TrustAnchor;
49+
import java.security.cert.X509CRL;
4450
import java.security.cert.X509Certificate;
4551
import java.util.Collections;
4652
import java.util.Date;
4753
import java.util.List;
4854
import java.util.Set;
4955

5056
import static org.assertj.core.api.Assertions.assertThat;
57+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5158

5259
class CertificateValidatorTest {
5360

@@ -60,6 +67,10 @@ class CertificateValidatorTest {
6067
private static X509Certificate intermediateCertificateB; // signed by C
6168
private static X509Certificate intermediateCertificateA; // signed by B, direct issuer of the leaf
6269
private static X509Certificate leafCertificate; // signed by A
70+
private static X509CRL rootCrl;
71+
private static X509CRL intermediateCCrl;
72+
private static X509CRL intermediateBCrl;
73+
private static X509CRL intermediateBRevokingACrl;
6374

6475
@BeforeAll
6576
static void setUp() throws Exception {
@@ -86,15 +97,21 @@ static void setUp() throws Exception {
8697
intermediateBName, intermediateBKeyPair.getPrivate(), intermediateBKeyPair.getPublic(), true, BigInteger.valueOf(4));
8798
leafCertificate = generateCertificate(leafName, leafKeyPair.getPublic(),
8899
intermediateAName, intermediateAKeyPair.getPrivate(), intermediateAKeyPair.getPublic(), false, BigInteger.valueOf(5));
100+
101+
rootCrl = generateCrl(rootName, rootKeyPair.getPrivate());
102+
intermediateCCrl = generateCrl(intermediateCName, intermediateCKeyPair.getPrivate());
103+
intermediateBCrl = generateCrl(intermediateBName, intermediateBKeyPair.getPrivate());
104+
intermediateBRevokingACrl = generateCrl(
105+
intermediateBName, intermediateBKeyPair.getPrivate(), intermediateCertificateA.getSerialNumber());
89106
}
90107

91108
@Test
92109
void whenChainHasTokenSuppliedIntermediate_thenReturnsDirectIssuerNotTrustAnchor() throws Exception {
93110
final Set<TrustAnchor> anchors = Collections.singleton(new TrustAnchor(rootCertificate, null));
94-
final CertStore emptyStore = CertificateValidator.buildCertStoreFromCertificates(Collections.emptyList());
111+
final CertStore revocationStore = buildCrlStore(rootCrl, intermediateCCrl, intermediateBCrl);
95112

96113
final X509Certificate issuer = CertificateValidator.validateIsSignedByTrustedCA(
97-
leafCertificate, anchors, emptyStore,
114+
leafCertificate, anchors, revocationStore,
98115
List.of(intermediateCertificateA, intermediateCertificateB, intermediateCertificateC), NOW);
99116

100117
// The leaf is issued by intermediate A, whose chain (A -> B -> C) leads to the root trust anchor. The issuer
@@ -119,15 +136,48 @@ void whenChainHasMultipleTokenSuppliedIntermediatesAndGrandparentIsPinned_thenVa
119136
// Token supplies the full A -> B -> C intermediate chain; the top (C) is configured as the trust anchor.
120137
// The path builds leaf -> A -> B -> C, and the issuer returned for OCSP is the direct issuer (A).
121138
final Set<TrustAnchor> anchors = Collections.singleton(new TrustAnchor(intermediateCertificateC, null));
122-
final CertStore emptyStore = CertificateValidator.buildCertStoreFromCertificates(Collections.emptyList());
139+
final CertStore revocationStore = buildCrlStore(intermediateCCrl, intermediateBCrl);
123140

124141
final X509Certificate issuer = CertificateValidator.validateIsSignedByTrustedCA(
125-
leafCertificate, anchors, emptyStore,
142+
leafCertificate, anchors, revocationStore,
126143
List.of(intermediateCertificateA, intermediateCertificateB, intermediateCertificateC), NOW);
127144

128145
assertThat(issuer).isEqualTo(intermediateCertificateA);
129146
}
130147

148+
@Test
149+
void whenTokenSuppliedIntermediateIsRevoked_thenRejectsCertificateChain() throws Exception {
150+
final Set<TrustAnchor> anchors = Collections.singleton(new TrustAnchor(rootCertificate, null));
151+
final CertStore revocationStore = buildCrlStore(
152+
rootCrl, intermediateCCrl, intermediateBRevokingACrl);
153+
154+
assertThat(intermediateBRevokingACrl.isRevoked(intermediateCertificateA)).isTrue();
155+
156+
assertThatThrownBy(() -> CertificateValidator.validateIsSignedByTrustedCA(
157+
leafCertificate, anchors, revocationStore,
158+
List.of(intermediateCertificateA, intermediateCertificateB, intermediateCertificateC), NOW))
159+
.isInstanceOf(CertificateNotTrustedException.class)
160+
.satisfies(exception -> {
161+
assertThat(exception.getCause()).isInstanceOf(CertPathValidatorException.class);
162+
final CertPathValidatorException validationException =
163+
(CertPathValidatorException) exception.getCause();
164+
assertThat(validationException.getReason())
165+
.isEqualTo(CertPathValidatorException.BasicReason.REVOKED);
166+
});
167+
}
168+
169+
@Test
170+
void whenTokenSuppliedIntermediateRevocationStatusIsUnknown_thenRejectsCertificateChain() throws Exception {
171+
final Set<TrustAnchor> anchors = Collections.singleton(new TrustAnchor(rootCertificate, null));
172+
final CertStore emptyStore = CertificateValidator.buildCertStoreFromCertificates(Collections.emptyList());
173+
174+
assertThatThrownBy(() -> CertificateValidator.validateIsSignedByTrustedCA(
175+
leafCertificate, anchors, emptyStore,
176+
List.of(intermediateCertificateA, intermediateCertificateB, intermediateCertificateC), NOW))
177+
.isInstanceOf(CertificateNotTrustedException.class)
178+
.hasCauseInstanceOf(CertPathValidatorException.class);
179+
}
180+
131181
private static KeyPair generateKeyPair() throws Exception {
132182
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
133183
keyPairGenerator.initialize(2048);
@@ -149,4 +199,19 @@ private static X509Certificate generateCertificate(X500Name subject, PublicKey s
149199
final ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(issuerPrivateKey);
150200
return new JcaX509CertificateConverter().getCertificate(builder.build(signer));
151201
}
202+
203+
private static X509CRL generateCrl(X500Name issuer, PrivateKey issuerPrivateKey,
204+
BigInteger... revokedCertificateSerials) throws Exception {
205+
final X509v2CRLBuilder builder = new X509v2CRLBuilder(issuer, NOT_BEFORE);
206+
builder.setNextUpdate(NOT_AFTER);
207+
for (final BigInteger serial : revokedCertificateSerials) {
208+
builder.addCRLEntry(serial, NOT_BEFORE, 0);
209+
}
210+
final ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(issuerPrivateKey);
211+
return new JcaX509CRLConverter().getCRL(builder.build(signer));
212+
}
213+
214+
private static CertStore buildCrlStore(X509CRL... crls) throws Exception {
215+
return CertStore.getInstance("Collection", new CollectionCertStoreParameters(List.of(crls)));
216+
}
152217
}

0 commit comments

Comments
 (0)