Skip to content

Commit c4aba0b

Browse files
feat: setup configuration for CRL certificate revocation
1 parent c0cb869 commit c4aba0b

File tree

10 files changed

+486
-8
lines changed

10 files changed

+486
-8
lines changed

gravitee-apim-gateway/gravitee-apim-gateway-services/gravitee-apim-gateway-services-debug/src/main/java/io/gravitee/gateway/debug/vertx/VertxDebugConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import io.gravitee.gateway.reactive.debug.vertx.DebugHttpProtocolVerticle;
1919
import io.gravitee.gateway.reactive.reactor.HttpRequestDispatcher;
20+
import io.gravitee.node.api.certificate.CRLLoaderFactoryRegistry;
2021
import io.gravitee.node.api.certificate.KeyStoreLoaderFactoryRegistry;
2122
import io.gravitee.node.api.certificate.KeyStoreLoaderOptions;
2223
import io.gravitee.node.api.certificate.TrustStoreLoaderOptions;
@@ -57,9 +58,10 @@ public VertxDebugHttpClientConfiguration debugHttpClientConfiguration(
5758
public VertxHttpServerFactory debugHttpServerFactory(
5859
Vertx vertx,
5960
KeyStoreLoaderFactoryRegistry<KeyStoreLoaderOptions> keyStoreLoaderFactoryRegistry,
60-
KeyStoreLoaderFactoryRegistry<TrustStoreLoaderOptions> truststoreLoaderFactoryRegistry
61+
KeyStoreLoaderFactoryRegistry<TrustStoreLoaderOptions> truststoreLoaderFactoryRegistry,
62+
CRLLoaderFactoryRegistry crlLoaderFactoryRegistry
6163
) {
62-
return new VertxHttpServerFactory(vertx, keyStoreLoaderFactoryRegistry, truststoreLoaderFactoryRegistry);
64+
return new VertxHttpServerFactory(vertx, keyStoreLoaderFactoryRegistry, truststoreLoaderFactoryRegistry, crlLoaderFactoryRegistry);
6365
}
6466

6567
@Bean("debugServer")

gravitee-apim-gateway/gravitee-apim-gateway-standalone/gravitee-apim-gateway-standalone-distribution/src/main/resources/config/gravitee.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ secrets:
109109
# path: ${gravitee.home}/security/truststore.jks
110110
# password: secret
111111
# watch: true # Watch for any updates on the keystore and reload it. Default is true.
112+
# crl:
113+
# path: # Path to the CRL file or folder. CRL checking is disabled if not set. Supports DER and PEM formats.
114+
# watch: true # Watch for any updates on the CRL and reload it. Default is true.
112115
# sni: false
113116
# openssl: false # Used to rely on OpenSSL Engine instead of default JDK SSL Engine
114117
# websocket:
@@ -158,6 +161,9 @@ secrets:
158161
# path: ${gravitee.home}/security/truststore.jks
159162
# password: secret
160163
# watch: true # Watch for any updates on the keystore/pem and reload it. Default is true.
164+
# crl:
165+
# path: # Path to the CRL file or folder. CRL checking is disabled if not set. Supports DER and PEM formats.
166+
# watch: true # Watch for any updates on the CRL and reload it. Default is true.
161167
# openssl: false # Used to rely on OpenSSL Engine instead of default JDK SSL Engine
162168
# haproxy: # Support for https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
163169
# proxyProtocol: false

gravitee-apim-gateway/gravitee-apim-gateway-tests-sdk/src/main/java/io/gravitee/apim/gateway/tests/sdk/utils/TLSUtils.java

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,45 @@
2020
import java.math.BigInteger;
2121
import java.nio.file.Files;
2222
import java.nio.file.Path;
23-
import java.security.*;
23+
import java.security.KeyPair;
24+
import java.security.KeyPairGenerator;
25+
import java.security.KeyStore;
26+
import java.security.KeyStoreException;
27+
import java.security.PrivateKey;
28+
import java.security.Security;
2429
import java.security.cert.Certificate;
30+
import java.security.cert.X509CRL;
2531
import java.security.cert.X509Certificate;
2632
import java.time.Instant;
2733
import java.time.temporal.ChronoUnit;
2834
import java.util.Collections;
2935
import java.util.Date;
36+
import java.util.concurrent.atomic.AtomicLong;
37+
import javax.security.auth.x500.X500Principal;
3038
import org.bouncycastle.asn1.x500.X500Name;
3139
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
40+
import org.bouncycastle.asn1.x509.BasicConstraints;
41+
import org.bouncycastle.asn1.x509.CRLReason;
42+
import org.bouncycastle.asn1.x509.Extension;
43+
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
44+
import org.bouncycastle.asn1.x509.KeyUsage;
3245
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
3346
import org.bouncycastle.cert.X509CertificateHolder;
47+
import org.bouncycastle.cert.X509v2CRLBuilder;
3448
import org.bouncycastle.cert.X509v3CertificateBuilder;
49+
import org.bouncycastle.cert.jcajce.JcaX509CRLConverter;
3550
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
51+
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
52+
import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder;
53+
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
3654
import org.bouncycastle.crypto.util.PrivateKeyFactory;
3755
import org.bouncycastle.jce.provider.BouncyCastleProvider;
3856
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
3957
import org.bouncycastle.operator.ContentSigner;
4058
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
4159
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
4260
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
61+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
4362

4463
/**
4564
* @author Benoit BORDIGONI (benoit.bordigoni at graviteesource.com)
@@ -49,6 +68,8 @@
4968
@SuppressWarnings("java:S112") // only used in tests
5069
public class TLSUtils {
5170

71+
private static final AtomicLong SERIAL_COUNTER = new AtomicLong(System.nanoTime());
72+
5273
static {
5374
Security.addProvider(new BouncyCastleProvider());
5475
}
@@ -168,7 +189,7 @@ public static KeyStore createKeyStore(String alias, Object data, char[] password
168189
}
169190

170191
/**
171-
* Happen data to an existing keystore.
192+
* Append data to an existing keystore.
172193
* @param keystore the keystore to happen
173194
* @param alias the alias used to add <code>data</code> to the keystore
174195
* @param data a {@link X509Pair} or {@link X509Cert} instance
@@ -212,4 +233,135 @@ private static void addEntry(KeyStore ks, String alias, Object data, char[] pass
212233
throw new IllegalArgumentException("%s cannot be added to a key store".formatted(data));
213234
}
214235
}
236+
237+
/**
238+
* Create a Certificate Authority certificate with proper CA extensions.
239+
* @param commonName the CA common name
240+
* @return a key pair with a CA certificate
241+
* @throws Exception when something wrong happened when generating the CA certificate
242+
*/
243+
public static X509Pair createCA(String commonName) throws Exception {
244+
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
245+
keyPairGenerator.initialize(2048);
246+
final KeyPair caKeyPair = keyPairGenerator.genKeyPair();
247+
248+
X500Principal subject = new X500Principal("C=FR, O=Gravitee, OU=IntegrationTests, CN=%s".formatted(commonName));
249+
250+
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
251+
subject,
252+
calculateSerialNumber(),
253+
Date.from(Instant.now().minus(1, ChronoUnit.DAYS)),
254+
Date.from(Instant.now().plus(365, ChronoUnit.DAYS)),
255+
subject,
256+
caKeyPair.getPublic()
257+
);
258+
259+
certBuilder
260+
.addExtension(Extension.basicConstraints, true, new BasicConstraints(true))
261+
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign));
262+
263+
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider("BC").build(caKeyPair.getPrivate());
264+
265+
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
266+
267+
X509Certificate certificate = converter.getCertificate(certBuilder.build(signer));
268+
269+
return new X509Pair(new X509Cert(certificate), new X509Key(caKeyPair.getPrivate()));
270+
}
271+
272+
/**
273+
* Create an End Entity certificate signed by a CA.
274+
* @param caKeyPair the CA key pair to sign the certificate
275+
* @param clientCN the client common name
276+
* @return a key pair with an end entity certificate signed by the CA
277+
* @throws Exception when something wrong happened when generating the certificate
278+
*/
279+
public static X509Pair createCASignedCertificate(X509Pair caKeyPair, String clientCN) throws Exception {
280+
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
281+
keyPairGenerator.initialize(2048);
282+
final KeyPair clientKeyPair = keyPairGenerator.genKeyPair();
283+
284+
X500Principal subject = new X500Principal("C=FR, O=Gravitee, OU=IntegrationTests, CN=%s".formatted(clientCN));
285+
286+
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
287+
caKeyPair.certificate().data().getSubjectX500Principal(),
288+
calculateSerialNumber(),
289+
Date.from(Instant.now().minus(1, ChronoUnit.DAYS)),
290+
Date.from(Instant.now().plus(365, ChronoUnit.DAYS)),
291+
subject,
292+
clientKeyPair.getPublic()
293+
);
294+
295+
certBuilder
296+
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
297+
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature));
298+
299+
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
300+
.setProvider("BC")
301+
.build(caKeyPair.privateKey().data());
302+
303+
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
304+
305+
X509Certificate certificate = converter.getCertificate(certBuilder.build(signer));
306+
307+
return new X509Pair(new X509Cert(certificate), new X509Key(clientKeyPair.getPrivate()));
308+
}
309+
310+
/**
311+
* Generate a Certificate Revocation List (CRL) with optional revoked certificates.
312+
* @param caKeyPair the CA key pair used to sign the CRL
313+
* @param revokedCerts optional array of certificates to mark as revoked
314+
* @return an X509CRL object
315+
* @throws Exception when something wrong happened when generating the CRL
316+
*/
317+
public static X509CRL generateCRL(X509Pair caKeyPair, X509Certificate... revokedCerts) throws Exception {
318+
X509v2CRLBuilder crlBuilder = new JcaX509v2CRLBuilder(
319+
caKeyPair.certificate().data().getSubjectX500Principal(),
320+
Date.from(Instant.now().minus(1, ChronoUnit.DAYS))
321+
);
322+
323+
crlBuilder.setNextUpdate(Date.from(Instant.now().plus(7, ChronoUnit.DAYS)));
324+
325+
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
326+
crlBuilder.addExtension(
327+
Extension.authorityKeyIdentifier,
328+
false,
329+
extUtils.createAuthorityKeyIdentifier(caKeyPair.certificate().data())
330+
);
331+
332+
if (revokedCerts.length > 0) {
333+
ExtensionsGenerator extGen = new ExtensionsGenerator();
334+
extGen.addExtension(Extension.reasonCode, false, CRLReason.lookup(CRLReason.privilegeWithdrawn));
335+
var extensions = extGen.generate();
336+
for (X509Certificate cert : revokedCerts) {
337+
crlBuilder.addCRLEntry(cert.getSerialNumber(), new Date(), extensions);
338+
}
339+
}
340+
341+
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
342+
.setProvider("BC")
343+
.build(caKeyPair.privateKey().data());
344+
345+
JcaX509CRLConverter converter = new JcaX509CRLConverter().setProvider("BC");
346+
347+
return converter.getCRL(crlBuilder.build(signer));
348+
}
349+
350+
/**
351+
* Write a CRL to a PEM file.
352+
* @param crl the CRL to write
353+
* @param path the file path
354+
* @throws Exception when something wrong happened when writing the CRL
355+
*/
356+
public static void writeCrlToPemFile(X509CRL crl, Path path) throws Exception {
357+
try (var writer = new StringWriter(); var pemWriter = new JcaPEMWriter(writer)) {
358+
pemWriter.writeObject(crl);
359+
pemWriter.flush();
360+
Files.writeString(path, writer.toString());
361+
}
362+
}
363+
364+
private static BigInteger calculateSerialNumber() {
365+
return BigInteger.valueOf(SERIAL_COUNTER.incrementAndGet());
366+
}
215367
}

gravitee-apim-gateway/gravitee-apim-gateway-tests-sdk/src/test/java/io/gravitee/apim/gateway/tests/sdk/utils/TLSUtilsTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,77 @@ void should_convert_keystore_to_truststore() throws Exception {
9595
assertThat(trustStore.isKeyEntry("foo")).isFalse();
9696
assertThatCode(() -> trustStore.getEntry("foo", null)).doesNotThrowAnyException();
9797
}
98+
99+
@Test
100+
void should_create_CA_certificate() throws Exception {
101+
TLSUtils.X509Pair ca = TLSUtils.createCA("Test CA");
102+
assertThat(ca.certificate()).isNotNull();
103+
assertThat(ca.certificate().data()).isNotNull();
104+
assertThat(ca.certificate().data().getSubjectX500Principal().getName()).contains("CN=Test CA");
105+
assertThat(ca.privateKey()).isNotNull();
106+
assertThat(ca.privateKey().data()).isNotNull();
107+
assertThat(ca.privateKey().data().getAlgorithm()).isEqualTo("RSA");
108+
109+
assertThat(ca.certificate().data().getBasicConstraints()).isGreaterThanOrEqualTo(0); // is CA
110+
assertThat(ca.certificate().data().getKeyUsage()).isNotNull();
111+
assertThat(ca.certificate().data().getKeyUsage()[5]).isTrue(); // keyCertSign
112+
assertThat(ca.certificate().data().getKeyUsage()[6]).isTrue(); // cRLSign
113+
}
114+
115+
@Test
116+
void should_create_CA_signed_certificate() throws Exception {
117+
TLSUtils.X509Pair ca = TLSUtils.createCA("Test CA");
118+
TLSUtils.X509Pair client = TLSUtils.createCASignedCertificate(ca, "client");
119+
120+
assertThat(client.certificate()).isNotNull();
121+
assertThat(client.certificate().data()).isNotNull();
122+
assertThat(client.certificate().data().getSubjectX500Principal().getName()).contains("CN=client");
123+
assertThat(client.privateKey()).isNotNull();
124+
125+
assertThatCode(() -> client.certificate().data().verify(ca.certificate().data().getPublicKey())).doesNotThrowAnyException();
126+
127+
assertThat(client.certificate().data().getBasicConstraints()).isEqualTo(-1);
128+
}
129+
130+
@Test
131+
void should_generate_empty_CRL() throws Exception {
132+
TLSUtils.X509Pair ca = TLSUtils.createCA("Test CA");
133+
var crl = TLSUtils.generateCRL(ca);
134+
135+
assertThat(crl).isNotNull();
136+
assertThat(crl.getIssuerX500Principal()).isEqualTo(ca.certificate().data().getSubjectX500Principal());
137+
assertThat(crl.getRevokedCertificates()).isNullOrEmpty();
138+
139+
assertThatCode(() -> crl.verify(ca.certificate().data().getPublicKey())).doesNotThrowAnyException();
140+
}
141+
142+
@Test
143+
void should_generate_CRL_with_revoked_certificates() throws Exception {
144+
TLSUtils.X509Pair ca = TLSUtils.createCA("Test CA");
145+
TLSUtils.X509Pair client1 = TLSUtils.createCASignedCertificate(ca, "client1");
146+
TLSUtils.X509Pair client2 = TLSUtils.createCASignedCertificate(ca, "client2");
147+
148+
var crl = TLSUtils.generateCRL(ca, client1.certificate().data(), client2.certificate().data());
149+
150+
assertThat(crl).isNotNull();
151+
assertThat(crl.getRevokedCertificates()).hasSize(2);
152+
assertThat(crl.isRevoked(client1.certificate().data())).isTrue();
153+
assertThat(crl.isRevoked(client2.certificate().data())).isTrue();
154+
155+
// Verify CRL is signed by CA
156+
assertThatCode(() -> crl.verify(ca.certificate().data().getPublicKey())).doesNotThrowAnyException();
157+
}
158+
159+
@Test
160+
void should_write_CRL_to_PEM_file() throws Exception {
161+
TLSUtils.X509Pair ca = TLSUtils.createCA("Test CA");
162+
TLSUtils.X509Pair client = TLSUtils.createCASignedCertificate(ca, "revoked-client");
163+
var crl = TLSUtils.generateCRL(ca, client.certificate().data());
164+
165+
Path crlFile = Files.createTempFile("test-crl", ".pem");
166+
TLSUtils.writeCrlToPemFile(crl, crlFile);
167+
168+
assertThat(crlFile).exists();
169+
assertThat(crlFile).content().startsWith("-----BEGIN X509 CRL").endsWith("-----END X509 CRL-----\n");
170+
}
98171
}

0 commit comments

Comments
 (0)