Smallstep Certificates in Java: Comprehensive Certificate Management and Automation

Smallstep Certificates is a private certificate authority (CA) that automates certificate lifecycle management. It provides APIs for certificate issuance, renewal, and revocation with ACME protocol support.


Architecture Overview

Key Components:

  • Step CLI & CA: Certificate authority server and command-line interface
  • ACME Protocol: Automated Certificate Management Environment
  • OAuth OIDC: Identity integration for authentication
  • CRL & OCSP: Certificate revocation mechanisms
  • TLS/SSL: Secure communication protocols

Java Application → Smallstep CA → Certificate Management

  • Applications request certificates via Smallstep APIs
  • Smallstep handles certificate issuance, renewal, and revocation
  • Support for various certificate types and formats

Dependencies and Setup

Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <bouncycastle.version>1.75</bouncycastle.version> <apache-httpclient.version>5.2.1</apache-httpclient.version> <jackson.version>2.15.2</jackson.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Cryptography --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk18on</artifactId> <version>${bouncycastle.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>${apache-httpclient.version}</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <!-- SSL/TLS --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-ssl</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Utilities --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.13.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.13.0</version> </dependency> </dependencies>
Smallstep CA Setup (Docker)
# docker-compose.yml version: '3.8' services: step-ca: image: smallstep/step-ca:0.24.2 ports: - "9000:9000" environment: - STEP_CA_NAME=Smallstep CA - STEP_CA_ADDRESS=:9000 - STEP_CA_DNS=localhost,step-ca - STEP_CA_DEFAULT_PROVISIONER=admin - STEP_CA_DEFAULT_SSH=false volumes: - ./step-ca:/home/step - ./config/ca.json:/home/step/config/ca.json:ro command: | /bin/sh -c " if [ ! -f /home/step/ca.crt ]; then step ca init --name='Smallstep CA' --dns=localhost,step-ca --address=':9000' --provisioner=admin --password-file=/dev/null fi && step-ca /home/step/config/ca.json " networks: - step-network step-cli: image: smallstep/step-cli:0.24.2 volumes: - ./step-ca:/home/step working_dir: /home/step networks: - step-network networks: step-network: driver: bridge
CA Configuration
{ "root": "/home/step/ca.crt", "crt": "/home/step/certs/intermediate_ca.crt", "key": "/home/step/secrets/intermediate_ca_key", "address": ":9000", "dnsNames": ["localhost", "step-ca"], "logger": { "format": "text" }, "db": { "type": "badger", "dataSource": "/home/step/db" }, "authority": { "provisioners": [ { "type": "JWK", "name": "admin", "key": { "use": "sig", "kty": "EC", "kid": "admin", "crv": "P-256", "alg": "ES256", "x": "public-key-x", "y": "public-key-y" }, "encryptedKey": "encrypted-private-key" } ], "templates": { "ssh": { "template": { "type": "", "keyId": "{{.Subject}}", "principals": ["{{.Subject}}"], "extensions": { "permit-X11-forwarding": "", "permit-agent-forwarding": "", "permit-port-forwarding": "", "permit-pty": "", "permit-user-rc": "" } } } } }, "tls": { "cipherSuites": [ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" ], "minVersion": 1.2, "maxVersion": 1.3, "renegotiation": false } }

Configuration and Properties

1. Smallstep Configuration Properties
@ConfigurationProperties(prefix = "smallstep") @Data public class SmallstepProperties { private String caUrl = "https://localhost:9000"; private String caFingerprint; private String provisionerName = "admin"; private String provisionerPassword; private String rootCertificatePath; private AuthConfig auth = new AuthConfig(); private CertificateConfig certificate = new CertificateConfig(); private AcmeConfig acme = new AcmeConfig(); @Data public static class AuthConfig { private boolean enabled = true; private String type = "JWK"; private String jwkPath; private String oidcIssuer; private String oidcClientId; private String oidcClientSecret; } @Data public static class CertificateConfig { private String template; private List<String> dnsNames = new ArrayList<>(); private List<String> ipAddresses = new ArrayList<>(); private Duration validity = Duration.ofDays(90); private Duration renewalWindow = Duration.ofDays(30); private String keyType = "RSA"; private int keySize = 2048; private String keyAlgorithm = "SHA256withRSA"; } @Data public static class AcmeConfig { private boolean enabled = true; private String directoryUrl; private String accountKeyPath; private String contactEmail; } }
2. Application Configuration
# application.yaml smallstep: ca-url: "https://localhost:9000" ca-fingerprint: "${CA_FINGERPRINT:}" provisioner-name: "admin" provisioner-password: "${PROVISIONER_PASSWORD:}" root-certificate-path: "./certs/root_ca.crt" auth: enabled: true type: "JWK" jwk-path: "./config/provisioner.jwk" certificate: template: "server" dns-names: - "localhost" - "*.example.com" validity: "90d" renewal-window: "30d" key-type: "RSA" key-size: 2048 key-algorithm: "SHA256withRSA" acme: enabled: true directory-url: "https://localhost:9000/acme/acme/directory" account-key-path: "./acme-account.key" contact-email: "[email protected]" app: certificates: storage: type: "file" # file, database, memory path: "./certs" auto-renewal: enabled: true check-interval: "24h" threshold: "30d" monitoring: enabled: true metrics-prefix: "certificates" management: endpoints: web: exposure: include: health,info,metrics,certificates endpoint: health: show-details: always certificates: enabled: true logging: level: com.smallstep: DEBUG com.example.smallstep: DEBUG

Core Smallstep Client Implementation

1. Smallstep HTTP Client
@Component @Slf4j public class SmallstepHttpClient { private final SmallstepProperties properties; private final ObjectMapper objectMapper; private final CloseableHttpClient httpClient; public SmallstepHttpClient(SmallstepProperties properties, ObjectMapper objectMapper) { this.properties = properties; this.objectMapper = objectMapper; this.httpClient = createHttpClient(); } private CloseableHttpClient createHttpClient() { try { SSLContext sslContext = createSSLContext(); return HttpClients.custom() .setSSLContext(sslContext) .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() .setSslContext(sslContext) .build()) .build()) .setDefaultRequestConfig(RequestConfig.custom() .setConnectTimeout(30000) .setConnectionRequestTimeout(30000) .setSocketTimeout(60000) .build()) .build(); } catch (Exception e) { throw new SmallstepClientException("Failed to create HTTP client", e); } } private SSLContext createSSLContext() throws Exception { // Load root CA certificate for TLS verification if (properties.getRootCertificatePath() != null) { File rootCertFile = new File(properties.getRootCertificatePath()); if (rootCertFile.exists()) { CertificateFactory cf = CertificateFactory.getInstance("X.509"); Certificate rootCert; try (FileInputStream fis = new FileInputStream(rootCertFile)) { rootCert = cf.generateCertificate(fis); } KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry("root-ca", rootCert); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), new SecureRandom()); return sslContext; } } // Fallback to default SSL context return SSLContext.getDefault(); } public <T> T executeGet(String path, Class<T> responseType) { return executeRequest(HttpGet.METHOD_NAME, path, null, responseType); } public <T> T executePost(String path, Object requestBody, Class<T> responseType) { return executeRequest(HttpPost.METHOD_NAME, path, requestBody, responseType); } public <T> T executeRequest(String method, String path, Object requestBody, Class<T> responseType) { String url = properties.getCaUrl() + path; try { HttpRequestBase request = createRequest(method, url, requestBody); addAuthHeaders(request); log.debug("Executing {} request to: {}", method, url); try (CloseableHttpResponse response = httpClient.execute(request)) { int statusCode = response.getCode(); String responseBody = EntityUtils.toString(response.getEntity()); if (statusCode >= 200 && statusCode < 300) { if (responseType == String.class) { return responseType.cast(responseBody); } else { return objectMapper.readValue(responseBody, responseType); } } else { handleErrorResponse(statusCode, responseBody); return null; } } } catch (Exception e) { throw new SmallstepClientException("HTTP request failed: " + method + " " + url, e); } } private HttpRequestBase createRequest(String method, String url, Object requestBody) throws Exception { switch (method.toUpperCase()) { case HttpGet.METHOD_NAME: return new HttpGet(url); case HttpPost.METHOD_NAME: HttpPost post = new HttpPost(url); if (requestBody != null) { String jsonBody = objectMapper.writeValueAsString(requestBody); post.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON)); } return post; default: throw new IllegalArgumentException("Unsupported HTTP method: " + method); } } private void addAuthHeaders(HttpRequestBase request) { // Add JWT token or other authentication headers if (properties.getAuth().isEnabled()) { String token = generateAuthToken(); request.setHeader("Authorization", "Bearer " + token); } } private String generateAuthToken() { // Generate JWT token for Smallstep authentication // This would use the provisioner credentials try { return JwtTokenGenerator.generateToken(properties); } catch (Exception e) { throw new SmallstepAuthException("Failed to generate auth token", e); } } private void handleErrorResponse(int statusCode, String responseBody) { log.error("Smallstep API error: {} - {}", statusCode, responseBody); switch (statusCode) { case 400: throw new SmallstepBadRequestException("Bad request: " + responseBody); case 401: throw new SmallstepAuthException("Authentication failed"); case 403: throw new SmallstepAuthException("Authorization failed"); case 404: throw new SmallstepNotFoundException("Resource not found"); case 500: throw new SmallstepServerException("Server error: " + responseBody); default: throw new SmallstepClientException("HTTP error " + statusCode + ": " + responseBody); } } @PreDestroy public void close() { try { httpClient.close(); } catch (Exception e) { log.error("Failed to close HTTP client", e); } } }
2. JWT Token Generator
@Component @Slf4j public class JwtTokenGenerator { private static final String JWT_HEADER = "{\"alg\":\"ES256\",\"typ\":\"JWT\"}"; public static String generateToken(SmallstepProperties properties) throws Exception { String provisionerName = properties.getProvisionerName(); Instant now = Instant.now(); Instant expiry = now.plusSeconds(300); // 5 minutes // Create JWT claims Map<String, Object> claims = new HashMap<>(); claims.put("iss", provisionerName); claims.put("aud", properties.getCaUrl() + "/1.0/sign"); claims.put("sub", provisionerName); claims.put("nbf", now.getEpochSecond()); claims.put("iat", now.getEpochSecond()); claims.put("exp", expiry.getEpochSecond()); // Load provisioner private key PrivateKey privateKey = loadProvisionerPrivateKey(properties); // Sign JWT return signJwt(claims, privateKey); } private static PrivateKey loadProvisionerPrivateKey(SmallstepProperties properties) throws Exception { // Load private key from JWK file or other source String jwkPath = properties.getAuth().getJwkPath(); if (jwkPath != null) { File jwkFile = new File(jwkPath); if (jwkFile.exists()) { return loadPrivateKeyFromJwk(jwkFile); } } throw new SmallstepAuthException("Provisioner private key not found"); } private static PrivateKey loadPrivateKeyFromJwk(File jwkFile) throws Exception { ObjectMapper mapper = new ObjectMapper(); Map<String, Object> jwk = mapper.readValue(jwkFile, new TypeReference<Map<String, Object>>() {}); String kty = (String) jwk.get("kty"); if ("EC".equals(kty)) { return loadECPrivateKey(jwk); } else if ("RSA".equals(kty)) { return loadRSAPrivateKey(jwk); } else { throw new SmallstepAuthException("Unsupported key type: " + kty); } } private static PrivateKey loadECPrivateKey(Map<String, Object> jwk) throws Exception { String crv = (String) jwk.get("crv"); String d = (String) jwk.get("d"); // Private key parameter ECParameterSpec ecSpec = getECParameterSpec(crv); BigInteger privateKeyValue = base64UrlDecodeToBigInteger(d); KeyFactory keyFactory = KeyFactory.getInstance("EC"); ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(privateKeyValue, ecSpec); return keyFactory.generatePrivate(privateKeySpec); } private static ECParameterSpec getECParameterSpec(String crv) { switch (crv) { case "P-256": return new ECParameterSpec( new EllipticCurve( new ECFieldFp(new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16)), new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16), new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16) ), new ECPoint( new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16), new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16) ), new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16), 1 ); default: throw new SmallstepAuthException("Unsupported EC curve: " + crv); } } private static String signJwt(Map<String, Object> claims, PrivateKey privateKey) throws Exception { String headerBase64 = Base64.getUrlEncoder().withoutPadding() .encodeToString(JWT_HEADER.getBytes(StandardCharsets.UTF_8)); String claimsJson = new ObjectMapper().writeValueAsString(claims); String claimsBase64 = Base64.getUrlEncoder().withoutPadding() .encodeToString(claimsJson.getBytes(StandardCharsets.UTF_8)); String signingInput = headerBase64 + "." + claimsBase64; Signature signature = Signature.getInstance("SHA256withECDSA"); signature.initSign(privateKey); signature.update(signingInput.getBytes(StandardCharsets.UTF_8)); byte[] signatureBytes = signature.sign(); String signatureBase64 = Base64.getUrlEncoder().withoutPadding() .encodeToString(signatureBytes); return signingInput + "." + signatureBase64; } private static BigInteger base64UrlDecodeToBigInteger(String base64Url) { byte[] bytes = Base64.getUrlDecoder().decode(base64Url); return new BigInteger(1, bytes); } // RSA private key loading would be implemented similarly private static PrivateKey loadRSAPrivateKey(Map<String, Object> jwk) throws Exception { // Implementation for RSA private key loading throw new SmallstepAuthException("RSA private key loading not implemented"); } }

Certificate Service Implementation

1. Certificate Service
@Service @Slf4j public class CertificateService { private final SmallstepHttpClient httpClient; private final SmallstepProperties properties; private final KeyPairGenerator keyPairGenerator; private final CertificateStorageService storageService; public CertificateService(SmallstepHttpClient httpClient, SmallstepProperties properties, CertificateStorageService storageService) throws NoSuchAlgorithmException { this.httpClient = httpClient; this.properties = properties; this.storageService = storageService; this.keyPairGenerator = KeyPairGenerator.getInstance(properties.getCertificate().getKeyType()); this.keyPairGenerator.initialize(properties.getCertificate().getKeySize()); } /** * Request a new certificate */ public CertificateResponse requestCertificate(CertificateRequest request) { try { log.info("Requesting certificate for CN: {}", request.getCommonName()); // Generate key pair KeyPair keyPair = generateKeyPair(); // Create CSR String csr = createCertificateSigningRequest(keyPair, request); // Submit to Smallstep CA SignRequest signRequest = createSignRequest(csr, request); SignResponse signResponse = httpClient.executePost("/1.0/sign", signRequest, SignResponse.class); // Parse certificate chain X509Certificate certificate = parseCertificate(signResponse.getServerPem().getCert()); List<X509Certificate> chain = parseCertificateChain(signResponse.getServerPem().getCerts()); CertificateResponse response = CertificateResponse.builder() .certificate(certificate) .certificateChain(chain) .privateKey(keyPair.getPrivate()) .publicKey(keyPair.getPublic()) .csr(csr) .serialNumber(signResponse.getServerPem().getSerialNumber()) .validFrom(certificate.getNotBefore().toInstant()) .validTo(certificate.getNotAfter().toInstant()) .build(); // Store certificate storageService.storeCertificate(request.getCommonName(), response); log.info("Successfully issued certificate for CN: {} (serial: {})", request.getCommonName(), signResponse.getServerPem().getSerialNumber()); return response; } catch (Exception e) { log.error("Failed to request certificate for CN: {}", request.getCommonName(), e); throw new CertificateRequestException("Certificate request failed", e); } } /** * Renew an existing certificate */ public CertificateResponse renewCertificate(String commonName) { try { log.info("Renewing certificate for CN: {}", commonName); // Load existing certificate CertificateResponse existingCert = storageService.loadCertificate(commonName); if (existingCert == null) { throw new CertificateNotFoundException("Certificate not found: " + commonName); } // Create renewal request CertificateRequest renewalRequest = CertificateRequest.builder() .commonName(commonName) .dnsNames(List.of(commonName)) .keyType(properties.getCertificate().getKeyType()) .keySize(properties.getCertificate().getKeySize()) .validity(properties.getCertificate().getValidity()) .build(); return requestCertificate(renewalRequest); } catch (Exception e) { log.error("Failed to renew certificate for CN: {}", commonName, e); throw new CertificateRenewalException("Certificate renewal failed", e); } } /** * Revoke a certificate */ public void revokeCertificate(String serialNumber, RevocationReason reason) { try { log.info("Revoking certificate with serial: {} (reason: {})", serialNumber, reason); RevokeRequest revokeRequest = RevokeRequest.builder() .serial(serialNumber) .reason(reason.getCode()) .build(); httpClient.executePost("/1.0/revoke", revokeRequest, Void.class); // Remove from storage storageService.removeCertificate(serialNumber); log.info("Successfully revoked certificate with serial: {}", serialNumber); } catch (Exception e) { log.error("Failed to revoke certificate with serial: {}", serialNumber, e); throw new CertificateRevocationException("Certificate revocation failed", e); } } /** * Get certificate status */ public CertificateStatus getCertificateStatus(String serialNumber) { try { CertificateResponse cert = storageService.loadCertificateBySerial(serialNumber); if (cert == null) { return CertificateStatus.NOT_FOUND; } Instant now = Instant.now(); if (now.isBefore(cert.getValidFrom())) { return CertificateStatus.NOT_YET_VALID; } else if (now.isAfter(cert.getValidTo())) { return CertificateStatus.EXPIRED; } else { return CertificateStatus.VALID; } } catch (Exception e) { log.error("Failed to get certificate status for serial: {}", serialNumber, e); return CertificateStatus.UNKNOWN; } } /** * Check if certificate needs renewal */ public boolean needsRenewal(String commonName) { try { CertificateResponse cert = storageService.loadCertificate(commonName); if (cert == null) { return true; // No certificate exists } Instant renewalTime = cert.getValidTo() .minus(properties.getCertificate().getRenewalWindow()); return Instant.now().isAfter(renewalTime); } catch (Exception e) { log.error("Failed to check renewal for CN: {}", commonName, e); return true; } } private KeyPair generateKeyPair() { return keyPairGenerator.generateKeyPair(); } private String createCertificateSigningRequest(KeyPair keyPair, CertificateRequest request) throws Exception { StringWriter writer = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(writer); PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder( new X500Principal(buildDistinguishedName(request)), keyPair.getPublic() ); // Add extensions ExtensionsGenerator extensionsGenerator = new ExtensionsGenerator(); // Subject Alternative Names if (!request.getDnsNames().isEmpty() || !request.getIpAddresses().isEmpty()) { GeneralNames generalNames = new GeneralNames(); for (String dnsName : request.getDnsNames()) { generalNames.add(new GeneralName(GeneralName.dNSName, dnsName)); } for (String ipAddress : request.getIpAddresses()) { generalNames.add(new GeneralName(GeneralName.iPAddress, ipAddress)); } extensionsGenerator.addExtension(Extension.subjectAlternativeName, false, generalNames); } if (!extensionsGenerator.isEmpty()) { p10Builder.addAttribute( PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, extensionsGenerator.generate() ); } ContentSigner signer = new JcaContentSignerBuilder(properties.getCertificate().getKeyAlgorithm()) .build(keyPair.getPrivate()); PKCS10CertificationRequest csr = p10Builder.build(signer); pemWriter.writeObject(csr); pemWriter.close(); return writer.toString(); } private String buildDistinguishedName(CertificateRequest request) { List<String> components = new ArrayList<>(); if (request.getCommonName() != null) { components.add("CN=" + request.getCommonName()); } if (request.getOrganization() != null) { components.add("O=" + request.getOrganization()); } if (request.getOrganizationalUnit() != null) { components.add("OU=" + request.getOrganizationalUnit()); } if (request.getCountry() != null) { components.add("C=" + request.getCountry()); } if (request.getState() != null) { components.add("ST=" + request.getState()); } if (request.getLocality() != null) { components.add("L=" + request.getLocality()); } return String.join(",", components); } private SignRequest createSignRequest(String csr, CertificateRequest request) { return SignRequest.builder() .csr(csr) .ott(generateOneTimeToken()) .notBefore(Instant.now()) .notAfter(Instant.now().plus(request.getValidity())) .build(); } private String generateOneTimeToken() { // Generate OTT for certificate signing // This would typically come from Smallstep provisioner try { return JwtTokenGenerator.generateToken(properties); } catch (Exception e) { throw new SmallstepAuthException("Failed to generate OTT", e); } } private X509Certificate parseCertificate(String pem) throws Exception { CertificateFactory cf = CertificateFactory.getInstance("X.509"); try (ByteArrayInputStream bis = new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8))) { return (X509Certificate) cf.generateCertificate(bis); } } private List<X509Certificate> parseCertificateChain(String pemChain) throws Exception { List<X509Certificate> chain = new ArrayList<>(); CertificateFactory cf = CertificateFactory.getInstance("X.509"); try (ByteArrayInputStream bis = new ByteArrayInputStream(pemChain.getBytes(StandardCharsets.UTF_8))) { Collection<? extends Certificate> certificates = cf.generateCertificates(bis); for (Certificate cert : certificates) { chain.add((X509Certificate) cert); } } return chain; } }
2. Certificate Models
@Data @Builder @Jacksonized public class CertificateRequest { @NotBlank private String commonName; private List<String> dnsNames = new ArrayList<>(); private List<String> ipAddresses = new ArrayList<>(); private String organization; private String organizationalUnit; private String country; private String state; private String locality; private String emailAddress; @Builder.Default private String keyType = "RSA"; @Builder.Default private int keySize = 2048; @Builder.Default private Duration validity = Duration.ofDays(90); private Map<String, String> extensions; } @Data @Builder @Jacksonized public class CertificateResponse { private X509Certificate certificate; private List<X509Certificate> certificateChain; private PrivateKey privateKey; private PublicKey publicKey; private String csr; private String serialNumber; private Instant validFrom; private Instant validTo; private Instant issuedAt; public String getCertificatePem() { try { StringWriter writer = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(writer); pemWriter.writeObject(certificate); pemWriter.close(); return writer.toString(); } catch (Exception e) { throw new CertificateSerializationException("Failed to serialize certificate to PEM", e); } } public String getPrivateKeyPem() { try { StringWriter writer = new StringWriter(); JcaPEMWriter pemWriter = new JcaPEMWriter(writer); pemWriter.writeObject(privateKey); pemWriter.close(); return writer.toString(); } catch (Exception e) { throw new CertificateSerializationException("Failed to serialize private key to PEM", e); } } } @Data @Builder @Jacksonized public class SignRequest { private String csr; private String ott; private Instant notBefore; private Instant notAfter; } @Data @Builder @Jacksonized public class SignResponse { private ServerPem serverPem; private String caPem; private String certChainPem; @Data @Builder @Jacksonized public static class ServerPem { private String cert; private String certs; private String serialNumber; } } @Data @Builder @Jacksonized public class RevokeRequest { private String serial; private Integer reason; private String reasonString; } public enum RevocationReason { UNSPECIFIED(0), KEY_COMPROMISE(1), CA_COMPROMISE(2), AFFILIATION_CHANGED(3), SUPERSEDED(4), CESSATION_OF_OPERATION(5), CERTIFICATE_HOLD(6), REMOVE_FROM_CRL(8), PRIVILEGE_WITHDRAWN(9), AA_COMPROMISE(10); private final int code; RevocationReason(int code) { this.code = code; } public int getCode() { return code; } } public enum CertificateStatus { VALID, EXPIRED, REVOKED, NOT_FOUND, NOT_YET_VALID, UNKNOWN }
3. Certificate Storage Service
@Service @Slf4j public class CertificateStorageService { private final ObjectMapper objectMapper; private final String storagePath; public CertificateStorageService(ObjectMapper objectMapper, @Value("${app.certificates.storage.path:./certs}") String storagePath) { this.objectMapper = objectMapper; this.storagePath = storagePath; createStorageDirectory(); } public void storeCertificate(String commonName, CertificateResponse certificate) { try { String filename = commonName + ".json"; File file = new File(storagePath, filename); CertificateMetadata metadata = CertificateMetadata.fromResponse(commonName, certificate); String json = objectMapper.writeValueAsString(metadata); Files.writeString(file.toPath(), json, StandardCharsets.UTF_8); log.debug("Stored certificate for CN: {} at {}", commonName, file.getAbsolutePath()); } catch (Exception e) { throw new CertificateStorageException("Failed to store certificate", e); } } public CertificateResponse loadCertificate(String commonName) { try { String filename = commonName + ".json"; File file = new File(storagePath, filename); if (!file.exists()) { return null; } String json = Files.readString(file.toPath(), StandardCharsets.UTF_8); CertificateMetadata metadata = objectMapper.readValue(json, CertificateMetadata.class); return metadata.toResponse(); } catch (Exception e) { log.error("Failed to load certificate for CN: {}", commonName, e); return null; } } public CertificateResponse loadCertificateBySerial(String serialNumber) { try { File storageDir = new File(storagePath); File[] files = storageDir.listFiles((dir, name) -> name.endsWith(".json")); if (files != null) { for (File file : files) { String json = Files.readString(file.toPath(), StandardCharsets.UTF_8); CertificateMetadata metadata = objectMapper.readValue(json, CertificateMetadata.class); if (serialNumber.equals(metadata.getSerialNumber())) { return metadata.toResponse(); } } } return null; } catch (Exception e) { log.error("Failed to load certificate by serial: {}", serialNumber, e); return null; } } public void removeCertificate(String commonName) { try { String filename = commonName + ".json"; File file = new File(storagePath, filename); if (file.exists()) { Files.delete(file.toPath()); log.debug("Removed certificate for CN: {}", commonName); } } catch (Exception e) { log.error("Failed to remove certificate for CN: {}", commonName, e); } } public List<CertificateMetadata> listCertificates() { List<CertificateMetadata> certificates = new ArrayList<>(); try { File storageDir = new File(storagePath); File[] files = storageDir.listFiles((dir, name) -> name.endsWith(".json")); if (files != null) { for (File file : files) { try { String json = Files.readString(file.toPath(), StandardCharsets.UTF_8); CertificateMetadata metadata = objectMapper.readValue(json, CertificateMetadata.class); certificates.add(metadata); } catch (Exception e) { log.error("Failed to read certificate file: {}", file.getName(), e); } } } } catch (Exception e) { log.error("Failed to list certificates", e); } return certificates; } private void createStorageDirectory() { try { File dir = new File(storagePath); if (!dir.exists()) { Files.createDirectories(dir.toPath()); log.info("Created certificate storage directory: {}", dir.getAbsolutePath()); } } catch (Exception e) { throw new CertificateStorageException("Failed to create storage directory", e); } } } @Data @Builder @Jacksonized class CertificateMetadata { private String commonName; private String serialNumber; private Instant validFrom; private Instant validTo; private Instant issuedAt; private String certificatePem; private String privateKeyPem; private String csrPem; public static CertificateMetadata fromResponse(String commonName, CertificateResponse response) { return CertificateMetadata.builder() .commonName(commonName) .serialNumber(response.getSerialNumber()) .validFrom(response.getValidFrom()) .validTo(response.getValidTo()) .issuedAt(Instant.now()) .certificatePem(response.getCertificatePem()) .privateKeyPem(response.getPrivateKeyPem()) .csrPem(response.getCsr()) .build(); } public CertificateResponse toResponse() throws Exception { X509Certificate certificate = parseCertificate(certificatePem); PrivateKey privateKey = parsePrivateKey(privateKeyPem); PublicKey publicKey = certificate.getPublicKey(); return CertificateResponse.builder() .certificate(certificate) .certificateChain(List.of(certificate)) // Simplified .privateKey(privateKey) .publicKey(publicKey) .csr(csrPem) .serialNumber(serialNumber) .validFrom(validFrom) .validTo(validTo) .issuedAt(issuedAt) .build(); } private X509Certificate parseCertificate(String pem) throws Exception { CertificateFactory cf = CertificateFactory.getInstance("X.509"); try (ByteArrayInputStream bis = new ByteArrayInputStream(pem.getBytes(StandardCharsets.UTF_8))) { return (X509Certificate) cf.generateCertificate(bis); } } private PrivateKey parsePrivateKey(String pem) throws Exception { PEMParser parser = new PEMParser(new StringReader(pem)); Object object = parser.readObject(); parser.close(); if (object instanceof PrivateKeyInfo) { JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); return converter.getPrivateKey((PrivateKeyInfo) object); } else if (object instanceof org.bouncycastle.openssl.PKCS8Generator) { // Handle PKCS8 format return null; // Implementation would parse PKCS8 } else { throw new CertificateSerializationException("Unsupported private key format"); } } }

ACME Client Implementation

1. ACME Client Service
@Service @Slf4j public class AcmeClientService { private final SmallstepProperties properties; private final ObjectMapper objectMapper; private final CloseableHttpClient httpClient; public AcmeClientService(SmallstepProperties properties, ObjectMapper objectMapper) { this.properties = properties; this.objectMapper = objectMapper; this.httpClient = createHttpClient(); } /** * Request certificate via ACME protocol */ public CertificateResponse requestAcmeCertificate(AcmeCertificateRequest request) { try { log.info("Requesting ACME certificate for domains: {}", request.getDomains()); // ACME directory discovery AcmeDirectory directory = discoverAcmeDirectory(); // Create ACME account AcmeAccount account = createOrLoadAccount(directory); // Create order AcmeOrder order = createOrder(account, request.getDomains()); // Perform challenges performChallenges(order); // Finalize order CertificateResponse certificate = finalizeOrder(order, request); log.info("Successfully issued ACME certificate for domains: {}", request.getDomains()); return certificate; } catch (Exception e) { log.error("Failed to request ACME certificate for domains: {}", request.getDomains(), e); throw new AcmeException("ACME certificate request failed", e); } } private AcmeDirectory discoverAcmeDirectory() { String directoryUrl = properties.getAcme().getDirectoryUrl(); return httpClient.executeGet(directoryUrl, AcmeDirectory.class); } private AcmeAccount createOrLoadAccount(AcmeDirectory directory) { // Try to load existing account AcmeAccount existingAccount = loadAccount(); if (existingAccount != null) { return existingAccount; } // Create new account return createNewAccount(directory); } private AcmeAccount createNewAccount(AcmeDirectory directory) { AcmeAccountRequest accountRequest = AcmeAccountRequest.builder() .contact(List.of("mailto:" + properties.getAcme().getContactEmail())) .termsOfServiceAgreed(true) .build(); // Create account key pair if not exists KeyPair accountKeyPair = createOrLoadAccountKeyPair(); // Sign request with account key String signedRequest = signAcmeRequest(accountRequest, accountKeyPair, directory.getNewAccount()); // Submit account creation request AcmeAccount account = submitAcmeRequest(directory.getNewAccount(), signedRequest, AcmeAccount.class); // Save account saveAccount(account); return account; } private AcmeOrder createOrder(AcmeAccount account, List<String> domains) { AcmeOrderRequest orderRequest = AcmeOrderRequest.builder() .identifiers(domains.stream() .map(domain -> new Identifier("dns", domain)) .collect(Collectors.toList())) .build(); String signedRequest = signAcmeRequest(orderRequest, getAccountKeyPair(), account.getOrders()); return submitAcmeRequest(account.getOrders(), signedRequest, AcmeOrder.class); } private void performChallenges(AcmeOrder order) { for (String authUrl : order.getAuthorizations()) { AcmeAuthorization auth = fetchAuthorization(authUrl); for (AcmeChallenge challenge : auth.getChallenges()) { if ("http-01".equals(challenge.getType())) { performHttpChallenge(challenge); break; } else if ("dns-01".equals(challenge.getType())) { performDnsChallenge(challenge); break; } } } } private void performHttpChallenge(AcmeChallenge challenge) { // Implement HTTP-01 challenge // This would involve creating a file at the specified path log.info("Performing HTTP challenge: {}", challenge.getUrl()); // Notify ACME server that challenge is ready triggerChallengeValidation(challenge); // Wait for challenge validation waitForChallengeValidation(challenge); } private void performDnsChallenge(AcmeChallenge challenge) { // Implement DNS-01 challenge // This would involve creating a DNS TXT record log.info("Performing DNS challenge: {}", challenge.getUrl()); // Notify ACME server that challenge is ready triggerChallengeValidation(challenge); // Wait for challenge validation waitForChallengeValidation(challenge); } private CertificateResponse finalizeOrder(AcmeOrder order, AcmeCertificateRequest request) { // Generate key pair for certificate KeyPair keyPair = generateKeyPair(request); // Create CSR String csr = createCsr(keyPair, request); // Finalize order AcmeFinalizeRequest finalizeRequest = AcmeFinalizeRequest.builder() .csr(csr) .build(); String signedRequest = signAcmeRequest(finalizeRequest, getAccountKeyPair(), order.getFinalize()); AcmeOrder finalizedOrder = submitAcmeRequest(order.getFinalize(), signedRequest, AcmeOrder.class); // Download certificate return downloadCertificate(finalizedOrder.getCertificate(), keyPair); } // Helper methods for ACME protocol implementation private KeyPair createOrLoadAccountKeyPair() { // Implementation to create or load ACME account key pair return null; } private String signAcmeRequest(Object request, KeyPair keyPair, String url) { // Implementation for ACME request signing return null; } private <T> T submitAcmeRequest(String url, String signedRequest, Class<T> responseType) { // Implementation for ACME request submission return null; } private CloseableHttpClient createHttpClient() { // Similar to SmallstepHttpClient implementation return null; } } @Data @Builder @Jacksonized class AcmeCertificateRequest { @NotEmpty private List<String> domains; @Builder.Default private String keyType = "RSA"; @Builder.Default private int keySize = 2048; @Builder.Default private Duration validity = Duration.ofDays(90); private String organization; private String organizationalUnit; private String country; private String state; private String locality; } @Data @Builder @Jacksonized class AcmeDirectory { private String newNonce; private String newAccount; private String newOrder; private String revokeCert; private String keyChange; private Map<String, String> meta; } @Data @Builder @Jacksonized class AcmeAccount { private String status; private String contact; private String orders; private String initialIp; private String createdAt; private String key; } @Data @Builder @Jacksonized class AcmeAccountRequest { private List<String> contact; private Boolean termsOfServiceAgreed; private Boolean onlyReturnExisting; } @Data @Builder @Jacksonized class AcmeOrder { private String status; private String expires; private List<String> identifiers; private List<String> authorizations; private String finalize; private String certificate; } @Data @Builder @Jacksonized class AcmeOrderRequest { private List<Identifier> identifiers; private String notBefore; private String notAfter; } @Data @Builder @Jacksonized class Identifier { private String type; private String value; public Identifier(String type, String value) { this.type = type; this.value = value; } } @Data @Builder @Jacksonized class AcmeAuthorization { private String status; private String expires; private List<AcmeChallenge> challenges; } @Data @Builder @Jacksonized class AcmeChallenge { private String type; private String url; private String token; private String status; } @Data @Builder @Jacksonized class AcmeFinalizeRequest { private String csr; }

Certificate Monitoring and Auto-Renewal

1. Certificate Monitor Service
@Service @Slf4j public class CertificateMonitorService { private final CertificateService certificateService; private final CertificateStorageService storageService; private final SmallstepProperties properties; private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); public CertificateMonitorService(CertificateService certificateService, CertificateStorageService storageService, SmallstepProperties properties) { this.certificateService = certificateService; this.storageService = storageService; this.properties = properties; if (properties.getCertificate().getRenewalWindow() != null) { startMonitoring(); } } @PostConstruct public void startMonitoring() { Duration checkInterval = Duration.ofHours(24); // Check daily scheduler.scheduleAtFixedRate(this::checkCertificates, 0, checkInterval.toMinutes(), TimeUnit.MINUTES); log.info("Started certificate monitoring with {} interval", checkInterval); } /** * Check all certificates for renewal */ public void checkCertificates() { try { log.info("Starting certificate renewal check"); List<CertificateMetadata> certificates = storageService.listCertificates(); int renewedCount = 0; int failedCount = 0; for (CertificateMetadata cert : certificates) { if (certificateService.needsRenewal(cert.getCommonName())) { try { log.info("Certificate needs renewal: {}", cert.getCommonName()); certificateService.renewCertificate(cert.getCommonName()); renewedCount++; } catch (Exception e) { log.error("Failed to renew certificate: {}", cert.getCommonName(), e); failedCount++; } } } log.info("Certificate renewal check completed: {} renewed, {} failed", renewedCount, failedCount); } catch (Exception e) { log.error("Certificate renewal check failed", e); } } /** * Get certificate expiration summary */ public CertificateSummary getCertificateSummary() { List<CertificateMetadata> certificates = storageService.listCertificates(); Instant now = Instant.now(); long expired = certificates.stream() .filter(cert -> cert.getValidTo().isBefore(now)) .count(); long expiringSoon = certificates.stream() .filter(cert -> { Instant warningTime = cert.getValidTo() .minus(properties.getCertificate().getRenewalWindow()); return now.isAfter(warningTime) && cert.getValidTo().isAfter(now); }) .count(); long valid = certificates.stream() .filter(cert -> cert.getValidTo().isAfter(now)) .count() - expiringSoon; return CertificateSummary.builder() .total(certificates.size()) .valid(valid) .expiringSoon(expiringSoon) .expired(expired) .lastCheck(Instant.now()) .build(); } @PreDestroy public void shutdown() { scheduler.shutdown(); try { if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } } } @Data @Builder class CertificateSummary { private int total; private long valid; private long expiringSoon; private long expired; private Instant lastCheck; }

REST API Controllers

1. Certificate Management Controller
@RestController @RequestMapping("/api/certificates") @Slf4j @Validated public class CertificateController { private final CertificateService certificateService; private final AcmeClientService acmeClientService; private final CertificateMonitorService monitorService; public CertificateController(CertificateService certificateService, AcmeClientService acmeClientService, CertificateMonitorService monitorService) { this.certificateService = certificateService; this.acmeClientService = acmeClientService; this.monitorService = monitorService; } @PostMapping public ResponseEntity<ApiResponse<CertificateResponse>> requestCertificate( @Valid @RequestBody CertificateRequest request) { try { CertificateResponse response = certificateService.requestCertificate(request); return ResponseEntity.ok(ApiResponse.success(response)); } catch (Exception e) { log.error("Failed to request certificate for CN: {}", request.getCommonName(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("Certificate request failed: " + e.getMessage())); } } @PostMapping("/acme") public ResponseEntity<ApiResponse<CertificateResponse>> requestAcmeCertificate( @Valid @RequestBody AcmeCertificateRequest request) { try { CertificateResponse response = acmeClientService.requestAcmeCertificate(request); return ResponseEntity.ok(ApiResponse.success(response)); } catch (Exception e) { log.error("Failed to request ACME certificate for domains: {}", request.getDomains(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("ACME certificate request failed: " + e.getMessage())); } } @PostMapping("/{commonName}/renew") public ResponseEntity<ApiResponse<CertificateResponse>> renewCertificate( @PathVariable String commonName) { try { CertificateResponse response = certificateService.renewCertificate(commonName); return ResponseEntity.ok(ApiResponse.success(response)); } catch (Exception e) { log.error("Failed to renew certificate for CN: {}", commonName, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("Certificate renewal failed: " + e.getMessage())); } } @DeleteMapping("/{serialNumber}") public ResponseEntity<ApiResponse<String>> revokeCertificate( @PathVariable String serialNumber, @RequestParam(defaultValue = "UNSPECIFIED") RevocationReason reason) { try { certificateService.revokeCertificate(serialNumber, reason); return ResponseEntity.ok(ApiResponse.success("Certificate revoked successfully")); } catch (Exception e) { log.error("Failed to revoke certificate with serial: {}", serialNumber, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("Certificate revocation failed: " + e.getMessage())); } } @GetMapping("/{commonName}/status") public ResponseEntity<ApiResponse<CertificateStatus>> getCertificateStatus( @PathVariable String commonName) { try { // For simplicity, using common name instead of serial number CertificateStatus status = certificateService.getCertificateStatus(commonName); return ResponseEntity.ok(ApiResponse.success(status)); } catch (Exception e) { log.error("Failed to get certificate status for CN: {}", commonName, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("Failed to get certificate status: " + e.getMessage())); } } @GetMapping("/summary") public ResponseEntity<ApiResponse<CertificateSummary>> getCertificateSummary() { try { CertificateSummary summary = monitorService.getCertificateSummary(); return ResponseEntity.ok(ApiResponse.success(summary)); } catch (Exception e) { log.error("Failed to get certificate summary", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("Failed to get certificate summary: " + e.getMessage())); } } @PostMapping("/monitor/check") public ResponseEntity<ApiResponse<String>> triggerCertificateCheck() { try { monitorService.checkCertificates(); return ResponseEntity.ok(ApiResponse.success("Certificate check completed")); } catch (Exception e) { log.error("Failed to trigger certificate check", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("Certificate check failed: " + e.getMessage())); } } }
2. Health Check Controller
@RestController @RequestMapping("/api/health") @Slf4j public class HealthController { private final SmallstepHttpClient httpClient; private final CertificateMonitorService monitorService; public HealthController(SmallstepHttpClient httpClient, CertificateMonitorService monitorService) { this.httpClient = httpClient; this.monitorService = monitorService; } @GetMapping("/smallstep") public ResponseEntity<SmallstepHealth> checkSmallstepHealth() { SmallstepHealth health = new SmallstepHealth(); try { // Test Smallstep CA connectivity httpClient.executeGet("/health", String.class); health.setStatus(HealthStatus.UP); health.setMessage("Smallstep CA is accessible"); } catch (Exception e) { health.setStatus(HealthStatus.DOWN); health.setMessage("Smallstep CA is not accessible: " + e.getMessage()); log.error("Smallstep health check failed", e); } // Add certificate summary CertificateSummary summary = monitorService.getCertificateSummary(); health.setCertificateSummary(summary); HttpStatus status = health.getStatus() == HealthStatus.UP ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE; return ResponseEntity.status(status).body(health); } } @Data class SmallstepHealth { private HealthStatus status; private String message; private CertificateSummary certificateSummary; private Instant timestamp = Instant.now(); }

Custom Exceptions

public class SmallstepClientException extends RuntimeException { public SmallstepClientException(String message) { super(message); } public SmallstepClientException(String message, Throwable cause) { super(message, cause); } } public class SmallstepAuthException extends RuntimeException { public SmallstepAuthException(String message) { super(message); } public SmallstepAuthException(String message, Throwable cause) { super(message, cause); } } public class CertificateRequestException extends RuntimeException { public CertificateRequestException(String message) { super(message); } public CertificateRequestException(String message, Throwable cause) { super(message, cause); } } public class CertificateRenewalException extends RuntimeException { public CertificateRenewalException(String message) { super(message); } public CertificateRenewalException(String message, Throwable cause) { super(message, cause); } } public class CertificateRevocationException extends RuntimeException { public CertificateRevocationException(String message) { super(message); } public CertificateRevocationException(String message, Throwable cause) { super(message, cause); } } public class CertificateStorageException extends RuntimeException { public CertificateStorageException(String message) { super(message); } public CertificateStorageException(String message, Throwable cause) { super(message, cause); } } public class AcmeException extends RuntimeException { public AcmeException(String message) { super(message); } public AcmeException(String message, Throwable cause) { super(message, cause); } } @ControllerAdvice public class CertificateExceptionHandler { @ExceptionHandler(CertificateRequestException.class) public ResponseEntity<ApiResponse<?>> handleCertificateRequestException(CertificateRequestException e) { log.error("Certificate request failed", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("Certificate request failed: " + e.getMessage())); } @ExceptionHandler(SmallstepAuthException.class) public ResponseEntity<ApiResponse<?>> handleSmallstepAuthException(SmallstepAuthException e) { log.error("Smallstep authentication failed", e); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(ApiResponse.error("Authentication failed: " + e.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity<ApiResponse<?>> handleGenericException(Exception e) { log.error("Unexpected error in certificate management", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ApiResponse.error("An unexpected error occurred")); } }

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class) class CertificateServiceTest { @Mock private SmallstepHttpClient httpClient; @Mock private CertificateStorageService storageService; @InjectMocks private CertificateService certificateService; @Test void shouldRequestCertificate() throws Exception { // Given CertificateRequest request = CertificateRequest.builder() .commonName("test.example.com") .dnsNames(List.of("test.example.com")) .build(); SignResponse mockResponse = SignResponse.builder() .serverPem(SignResponse.ServerPem.builder() .cert("test-cert") .certs("test-chain") .serialNumber("12345") .build()) .build(); when(httpClient.executePost(anyString(), any(SignRequest.class), eq(SignResponse.class))) .thenReturn(mockResponse); // When CertificateResponse response = certificateService.requestCertificate(request); // Then assertThat(response).isNotNull(); assertThat(response.getSerialNumber()).isEqualTo("12345"); verify(storageService).storeCertificate(eq("test.example.com"), any(CertificateResponse.class)); } } @SpringBootTest @Testcontainers class SmallstepIntegrationTest { @Container private static final GenericContainer<?> stepCaContainer = new GenericContainer<>("smallstep/step-ca:0.24.2") .withExposedPorts(9000) .withCommand("step-ca --config /home/step/config/ca.json"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("smallstep.ca-url", () -> "https://localhost:" + stepCaContainer.getMappedPort(9000)); } @Autowired private CertificateService certificateService; @Test void shouldIssueCertificate() { // Integration test with real Smallstep CA // This would require proper CA setup and configuration } }

Best Practices

  1. Security: Store private keys securely, use HSM where possible
  2. Monitoring: Monitor certificate expiration and renewal status
  3. Backup: Regularly backup CA and certificate data
  4. Access Control: Implement proper authentication and authorization
  5. Audit Logging: Log all certificate operations
  6. Certificate Policies: Define and enforce certificate policies
// Example of secure key storage @Component @Slf4j public class SecureKeyStorageService { public void storePrivateKey(String keyId, PrivateKey privateKey, char[] password) throws Exception { // Encrypt private key before storage byte[] encryptedKey = encryptPrivateKey(privateKey, password); // Store encrypted key in secure storage // This could be a secure database, HSM, or encrypted file system log.info("Stored encrypted private key: {}", keyId); } public PrivateKey loadPrivateKey(String keyId, char[] password) throws Exception { // Load encrypted key from secure storage byte[] encryptedKey = loadEncryptedKey(keyId); // Decrypt private key return decryptPrivateKey(encryptedKey, password); } private byte[] encryptPrivateKey(PrivateKey privateKey, char[] password) throws Exception { // Implementation for private key encryption // Using AES-GCM or similar secure algorithm return new byte[0]; } private PrivateKey decryptPrivateKey(byte[] encryptedKey, char[] password) throws Exception { // Implementation for private key decryption return null; } }

Conclusion

Smallstep Certificates integration in Java provides:

  • Automated Certificate Management: Streamlined certificate lifecycle
  • ACME Protocol Support: Standard-based certificate automation
  • Private CA: Internal certificate authority with full control
  • Security: Strong cryptographic foundations and secure key management
  • Monitoring: Comprehensive certificate expiration and renewal tracking
  • Integration: Seamless integration with Spring applications and microservices

By implementing the patterns shown above, you can build robust certificate management solutions that automate the entire certificate lifecycle while maintaining security best practices and operational reliability.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper