DIY Certificate Authority using Quarkus on GraalVM
We built our home automation using OpenHAB on a Raspberry Pi 3. It has an awesome web interface, but it’s missing a SSL server certificate for added security. I could just use something off-the-shelve but since I want to try Quarkus and GraalVM I’ll be implementing my own Certificate Authority (CA). Just for the fun of it.
Warning: if you’re new to PKI you should not be implementing one yourself. If you’re familiar with the topic, you probably still shouldn’t. Instead, use battle tested software and trusted CAs for your PKI management. This post is intended for educational purposes only; don’t blame me if your DIY CA gets hacked.
Tooling
We’ll wire Quarkus, GraalVM and Bouncy Castle to build our CA:
Quarkus
Quarkus is a microservice framework like Micronaut but comes from a Java EE background (sponsored by Red Hat), where Micronaut feels more like Spring. I consider it to be very bleeding edge (current release is 0.16.1) but it’s being developed very actively. Since migrating to a microservice framework is probably worth it, I believe a little competition for Micronaut is certainly beneficial for the ecosystem.
GraalVM
GraalVM has been slowly gaining attention as a polyglot VM bundled with an interesting tool called SubstrateVM (SVM). Using SVM you can build a native image of your Java application (and other languages supported by Graal), turning it into an ordinary binary. This binary boots much faster and allows you to run it without having a JVM installed.
Building a Java application
Building and running a regular Java application involves javac compiling source code into JVM byte code, which is translated and executed to machine instructions at runtime:
Building a native image
When building a native image, things are a little different. Translation from JVM byte code to executable machine instructions happens at compile-time:
When creating a native image all dependencies are linked statically into your program, alongside with all features the JVM normally provides (e.g. memory management, type system, standard libraries, etc.).
Bouncy Castle
We’ll be using Bouncy Castle for creating and signing X.509 certificates since that’s not supported by JCE. It would also be possible to sign certificates using stuff inside the sun.security.x509 packages but since that’s considered bad practice since forever, we’ll just use Bouncy Castle.
Building the CA
Now that we’ve determined what stuff we’ll be using, let’s get building. Please see reinkrul/quarkus-ca on GitHub for the completed solution.
1. Create a new project
Use the Quarkus Maven Plugin to generate a new project with a JAX-RS resource.
$ mvn io.quarkus:quarkus-maven-plugin:0.16.0:create \
-DprojectGroupId=nl.reinkrul \
-DprojectArtifactId=quarkus-ca \
-DclassName="nl.reinkrul.quarkusca.IssueCertificateResource" \
-Dpath="/certificate/issue"
2. Start the development environment
Now start the hot-reload development environment:
$ mvn compile quarkus:dev
When started, navigate to (http://localhost:8080/certificate/issue). It should return ‘hello’, indicating our Quarkus application is running. You can now hot-reload your changes by refreshing your browser. Code changes, adding/removing classes or static files, it all works. And pretty fast too!
3. Generating key pairs and signing certificates
Normally, the party requesting a certificate from the CA will generate a key pair and create a CSR (Certificate Signing Request), which is a Proof of Possession for the private key. To keep things simple, our CA will be generating the RSA keys in question. Also, we’ll be using 2048 bit RSA keys for development speed, but today 4096 bit RSA keys (or elliptic curves) are recommended.
KeyPair generateKeyPair(int keySize) throws ... {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(keySize);
return generator.generateKeyPair();
}
With the key pair generated, we can now sign a certificate with it:
X509Certificate sign(X500Name endEntity, KeyPair endEntityKey, int validityInDays, BigInteger serial,
Iterable<Extension> extensions) throws ... {
Date validFrom = new Date(System.currentTimeMillis());
Date validUntil = new Date(validFrom.getTime() + (validityInDays * MILLIS_PER_DAY));
ContentSigner signer = new JcaContentSignerBuilder(ALGORITHM).build(caKey);
X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(caName, serial, validFrom, validUntil, endEntity, endEntityKey.getPublic());
for (Extension extension : extensions) {
certificateBuilder.addExtension(extension);
}
return new JcaX509CertificateConverter().getCertificate(certificateBuilder.build(signer));
}
Parameter endEntity defines the name of the subject which we issue the certificate to. For me, living in the Netherlands, it could be as following: CN=Rein Krul,C=NL. Serial is the certificate’s serial number, which is a unique number assigned to the certificate by the CA. To keep things simple (again) we’ll be using the Unix epoch in milliseconds. Don’t do this in production, because 1. it can lead to clashes when two certificates are issued at the exact same moment and 2. it makes the serial predictable.
Variables validFrom and validUntil define the period in which the certificate can be used.
MILLIS_PER_DAY = 1000 * 60 * 60 * 24 (doh), ALGORITHM = SHA256withRSA (meaning: when signing we’ll be hashing using SHA-256, then encrypting that hash using RSA), caName is the name of our CA (e.g. CN=Quarkus CA,C=NL) and caKey is our CA’s key (doh).
4. Self-signing CA certificate
A PKI is typically a hierarchy of a root CA issuing certificates to sub CAs (these might be resellers) who finally issue certificates to end entities (like servers). To keep things simple (again) we’ll just have a root CA issuing certificates to end entities (only servers, for now). Since there’s nobody else to issue our root CA certificate, we’ll be issuing it to ourselves a.k.a. self signing. Also, CA certificates require specific extensions which we’ll be adding (marked being a CA certificate, certificate signing as key usage):
X509Certificate signCaCertificate(X500Name endEntity, KeyPair endEntityKey, int validityInDays,
BigInteger serial) throws ... {
List<Extension> extensions = Arrays.asList(
new Extension(Extension.basicConstraints, true, new BasicConstraints(true).getEncoded()),
new Extension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign).getEncoded())
);
return sign(endEntity, endEntityKey, validityInDays, serial, extensions);
}
5. Signing server certificates
Issuing (signing) a server certificate doesn’t differ much from signing a CA certificate. The common name should be the domain name (e.g. CN=reinkrul.nl), it should not be marked as a CA and its usage should indicate digital signing, key encipherment (required for HTTPS key exchange) and server authentication:
X509Certificate signServerCertificate(X500Name endEntity, KeyPair endEntityKey, int validityInDays,
BigInteger serial) throws ... {
List<Extension> extensions = Arrays.asList(
new Extension(Extension.basicConstraints, true, new BasicConstraints(false).getEncoded()),
new Extension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment).getEncoded()),
new Extension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth).getEncoded())
);
return sign(endEntity, endEntityKey, validityInDays, serial, extensions);
}
6. Building the native image
The final step is to build a native image by packaging the application using native profile:
$ ./mvnw package -Pnative
The native image starts quite a bit faster than the JAR (almost 10 times!). Running the JAR version:
$ java -jar target/quarkus-ca-1.0-SNAPSHOT-runner.jar
...
INFO [io.quarkus] (main) Quarkus 0.16.0 started in 1.260s. Listening on: http://[::]:8080
Running the native image:
$ target/quarkus-ca-1.0-SNAPSHOT-runner
...
INFO [io.quarkus] (main) Quarkus 0.16.0 started in 0.177s. Listening on: http://[::]:8080
Final Thoughts
Quarkus is becoming a framework to consider:
- Developing web based services awesome; hit refresh and all changes are compiled and your application is reloaded instantly.
- Note: adding Maven dependencies requires restarting the development server.
- It is bleeding edge, I came across missing or incomplete features. For instance, sending basic authentication challenges or client certificate authentication. That’s probably why it’s still on version 0.16.
On GraalVM:
- Only Java 8 is supported so no goodies from Java 9/10/11.
- Limited JCA (what we’re using for everything cryptography related) support is enabled by default, to improve native image size: unless you enable ‘native SSL’, cryptographic operations aren’t supported. This is done by setting quarkus.ssl.native = true in your application.properties configuration file. For more information on JCA in native images see JCA Security Services on SubstrateVM.
- Even with the guide above I couldn’t get Bouncy Castle to work for RSA operations (key generation and signing) so I switched back to default JCE. I’m still using BouncyCastle’s classes for certificate generation though.
- Quarkus only supports GraalVM RC16 instead of the latest stable release 19.0.0, so don’t make the mistake of downloading the latest version (kinda sad, but they’re working on it).
Running on a Pi
I promised to run the result on a Pi, but I failed at that part. The Raspberry Pi 3 is powered by an ARM processor which SubstrateVM simply doesn’t support yet. So for now I have to stick to running the JAR on my Pi which actually works fine, too.