Creating TLS Certificates and Requests

Applications often need two versions of the same identity workflow. A test setup creates its own certificate authority and issues server and client certificates immediately, while an enterprise setup creates a private key and sends a certificate signing request to the organization’s CA. The profile-driven builder supports both paths with the same subject and extension configuration.

Choosing a Key Profile

SigningPrivateKey::generate() defaults to ECDSA P-256, which is a compact and broadly interoperable choice for new TLS identities. Select P-384 when the surrounding system requires its higher security strength, or an RSA profile when compatibility with RSA-only software matters.

using namespace el::cryptology;

auto defaultKey = SigningPrivateKey::generate();
auto p384Key = SigningPrivateKey::generate(SigningKeyProfile::EcdsaP384);
auto rsaKey = SigningPrivateKey::generate(SigningKeyProfile::Rsa3072);

The RSA choices fix the public exponent and modulus size. The API intentionally has no controls for primes, exponent, curve parameters, signature padding, or digest selection.

Creating a Test Certificate Hierarchy

A self-signed certificate is available only from the certificate-authority profile. The following root permits one subordinate CA level; the intermediate keeps the default path length of zero and can therefore issue leaves but no further CA.

using namespace el::cryptology;
using namespace el::text::literals;

auto rootKey = SigningPrivateKey::generate();
auto root = X509CertificateBuilder::certificateAuthority("Example Test Root"_el)
                .setOrganization("Example Application"_el)
                .setCaPathLength(1)
                .createSelfSignedCertificate(rootKey);

auto intermediateKey = SigningPrivateKey::generate();
auto intermediate = X509CertificateBuilder::certificateAuthority("Example Test Issuing CA"_el)
                        .setOrganization("Example Application"_el)
                        .createCertificate(intermediateKey, root, rootKey);

For a server, add every DNS name and literal IP address through which clients connect. The common name is descriptive but does not replace a SAN during service-identity validation.

auto serverKey = SigningPrivateKey::generate(SigningKeyProfile::EcdsaP384);
auto serverBuilder = X509CertificateBuilder::tlsServer("api.example.test"_el);
serverBuilder.setOrganization("Example Application"_el)
    .addDnsName("api.example.test"_el)
    .addDnsName("localhost"_el)
    .addIpAddress(el::network::IpAddress::fromStringOrThrow("127.0.0.1"_el));
auto server = serverBuilder.createCertificate(serverKey, intermediate, intermediateKey);

auto clientKey = SigningPrivateKey::generate();
auto client = X509CertificateBuilder::tlsClient("integration-client"_el)
                  .setOrganization("Example Application"_el)
                  .createCertificate(clientKey, intermediate, intermediateKey);

Use tlsServerAndClient() only when the same identity really serves both roles. It requires a SAN like the server profile and includes both TLS purposes.

Writing the Setup Safely

File output infers PEM or DER from an artifact-specific suffix. Private-key files are created with user-only access, and no write operation replaces an existing file. This makes accidental key replacement a visible setup error.

rootKey.writeToFile(outputDirectory / "root-ca.key"_el);
root.writeToFile(outputDirectory / "root-ca.crt"_el);
intermediateKey.writeToFile(outputDirectory / "issuing-ca.key"_el);
intermediate.writeToFile(outputDirectory / "issuing-ca.crt"_el);
serverKey.writeToFile(outputDirectory / "server.key"_el);
server.writeToFile(outputDirectory / "server.crt"_el);
clientKey.writeToFile(outputDirectory / "client.key"_el);
client.writeToFile(outputDirectory / "client.crt"_el);

.key and .crt select PEM in this example. Use .p8 and .cer for DER, or pass PemDerFormat explicitly when a naming convention does not use a recognized suffix.

Protecting a Private Key with a Password

Unencrypted keys are convenient for unattended test processes whose directory is already protected. When the key must be protected at rest, write EncryptedPrivateKeyInfo instead.

auto password = el::String{"correct horse battery staple"_el};
password.markAsSensitive();
serverKey.writeEncryptedToFile(outputDirectory / "server-encrypted.key"_el, password);

auto loadedKey = SigningPrivateKey::fromEncryptedFileOrThrow(
    outputDirectory / "server-encrypted.key"_el, password);

An empty password is rejected. Keep password input marked sensitive and erase it as soon as the application no longer needs it. The encrypted-key reader deliberately gives the same failure diagnostic for a wrong password, invalid padding, or malformed plaintext.

Creating an Enterprise Signing Request

For enterprise enrollment, keep the private key inside the application and send only the request. The request contains the subject public key plus one extensionRequest attribute carrying the profile’s SAN, Key Usage, Extended Key Usage, and Basic Constraints request.

auto enterpriseKey = SigningPrivateKey::generate(SigningKeyProfile::Rsa3072);
auto requestBuilder = X509CertificateBuilder::tlsServer("api.corp.example"_el);
requestBuilder.setCountry("CH"_el)
    .setOrganization("Example AG"_el)
    .setOrganizationalUnit("Platform Services"_el)
    .addDnsName("api.corp.example"_el);
auto request = requestBuilder.createSigningRequest(enterpriseKey);

enterpriseKey.writeEncryptedToFile(outputDirectory / "api.key"_el, password);
request.writeToFile(outputDirectory / "api.csr"_el);

The enterprise CA decides the final issuer, serial, validity, and extensions. After receiving the certificate, parse it and verify that its public key matches enterpriseKey.publicKey() before installing the identity.

Validity and Reissuance

Default validity begins five minutes in the past to tolerate modest clock differences. CA certificates last ten calendar years and leaf certificates last 397 days unless the builder specifies exact times or a lifetime. Issuer-signed defaults are clipped to the issuer range; explicit ranges outside that range are rejected rather than silently changed.

Use fromCertificate() when renewing an existing identity. It copies supported subject and SAN data while generating a new serial, validity, key identifiers, issuer, and signature. Unknown noncritical extensions are retained, but an unknown critical extension stops reissuance so its semantics cannot be lost accidentally.