diff --git a/src/main/java/de/mlessmann/certassist/models/Certificate.java b/src/main/java/de/mlessmann/certassist/models/Certificate.java index ff50266..cc9dfa2 100644 --- a/src/main/java/de/mlessmann/certassist/models/Certificate.java +++ b/src/main/java/de/mlessmann/certassist/models/Certificate.java @@ -46,10 +46,16 @@ public class Certificate { private List certificateExtension = new ArrayList<>(); @Lob + @Column(nullable = false) private byte[] cert = new byte[0]; @Lob + @Column(nullable = false) private byte[] privateKey = new byte[0]; + @Lob + @Column + private byte[] fullchain; + private String fingerprint; } diff --git a/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java b/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java index 737fd86..1e7e394 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java +++ b/src/main/java/de/mlessmann/certassist/openssl/CertificateUsage.java @@ -1,6 +1,7 @@ package de.mlessmann.certassist.openssl; import java.nio.file.Path; +import org.springframework.lang.Nullable; /** * Instance of a certificate that is temporarily stored on disk to be available for use in command line calls. @@ -8,8 +9,30 @@ import java.nio.file.Path; * @implSpec The files should be removed from disk when the instance is closed, UNLESS the provided paths are the permanent storage location for the certificate files. */ public interface CertificateUsage extends AutoCloseable { + /** + * Returns the path to the certificate file (on disk, potentially temporary depending on the storage implementation). + */ Path certificatePath(); + + /** + * Returns the path to the private key file (on disk, potentially temporary depending on the storage implementation). + * This file should also be encrypted. + * @see CertificatePasswordProvider + */ Path certificateKeyPath(); + + /** + * Returns the path to the fullchain file (on disk, potentially temporary depending on the storage implementation). + * This should contain the entire certification chain from (inclusive) the certificate to the root authority (inclusive). + * @implSpec This method may return {@code null} if the certificate is self-signed. + */ + @Nullable + Path fullchainPath(); + + /** + * String representation of the certificate's fingerprint. + * In case of OpenSSL, this should be in the form of: {@code SHA1;::...} + */ String fingerprint(); @Override diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java index 85025d7..4358c2f 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateCreator.java @@ -89,7 +89,7 @@ public class OpenSSLCertificateCreator { Path certificate = createCertificate(request, keyFile, tmpDir.resolve("certificate.crt"), certPassword); String fingerprint = getCertificateFingerprint(certificate); passwordProvider.setPasswordFor(fingerprint, certPassword); - return new OpenSSLCertificateResult(tmpDir, certificate, keyFile, fingerprint); + return new OpenSSLCertificateResult(tmpDir, certificate, keyFile, certificate, fingerprint); } try (var certAuthority = certificateProvider.requestCertificateUsage(request.getTrustingAuthority())) { @@ -103,7 +103,16 @@ public class OpenSSLCertificateCreator { ); String fingerprint = getCertificateFingerprint(signedCert); passwordProvider.setPasswordFor(fingerprint, certPassword); - return new OpenSSLCertificateResult(tmpDir, signedCert, keyFile, fingerprint); + + Path fullchain = tmpDir.resolve("fullchain.pem"); + try { + Files.write(fullchain, Files.readAllBytes(certAuthority.certificatePath()), StandardOpenOption.CREATE); + Files.write(fullchain, Files.readAllBytes(signedCert), StandardOpenOption.APPEND); + } catch (IOException e) { + throw new CommandLineOperationException("Failed to create fullchain file.", e); + } + + return new OpenSSLCertificateResult(tmpDir, signedCert, keyFile, fullchain, fingerprint); } } diff --git a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java index 37cc359..bbca3f5 100644 --- a/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java +++ b/src/main/java/de/mlessmann/certassist/openssl/OpenSSLCertificateResult.java @@ -16,6 +16,7 @@ public class OpenSSLCertificateResult implements CertificateUsage { private final Path tmpDir; private final Path certificatePath; private final Path privateKeyPath; + private final Path fullchainPath; private final String certificateFingerPrint; @Override @@ -28,6 +29,11 @@ public class OpenSSLCertificateResult implements CertificateUsage { return privateKeyPath; } + @Override + public Path fullchainPath() { + return fullchainPath; + } + @Override public String fingerprint() { return certificateFingerPrint; diff --git a/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java b/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java index 8e511aa..e6d5cb7 100644 --- a/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java +++ b/src/main/java/de/mlessmann/certassist/service/CertificateProviderImpl.java @@ -2,6 +2,8 @@ package de.mlessmann.certassist.service; import de.mlessmann.certassist.Constants; import de.mlessmann.certassist.DeleteRecursiveFileVisitor; +import de.mlessmann.certassist.models.Certificate; +import de.mlessmann.certassist.models.CertificateType; import de.mlessmann.certassist.openssl.CertificateProvider; import de.mlessmann.certassist.openssl.CertificateUsage; import de.mlessmann.certassist.repositories.CertificateRepository; @@ -27,15 +29,22 @@ public class CertificateProviderImpl implements CertificateProvider { @Override public CertificateUsage requestCertificateUsage(String fingerprint) { - var certFromDB = certificateRepository.findByFingerprintIs(fingerprint); + Certificate certFromDB = certificateRepository.findByFingerprintIs(fingerprint); if (certFromDB == null) { throw new IllegalArgumentException("Unknown fingerprint"); } + boolean selfSigned = + certFromDB.getType() == CertificateType.ROOT_CA || certFromDB.getType() == CertificateType.STANDALONE_CERT; + try { Path tempDirectory = Files.createTempDirectory(Constants.CERTASSIST_TMP_PREFIX); Files.write(tempDirectory.resolve("key.pem"), certFromDB.getPrivateKey(), CREATE_TRUNCATE); Files.write(tempDirectory.resolve("cert.pem"), certFromDB.getCert(), CREATE_TRUNCATE); + if (!selfSigned) { + Files.write(tempDirectory.resolve("fullchain.pem"), certFromDB.getFullchain(), CREATE_TRUNCATE); + } + return new ExtractedCert(tempDirectory, certFromDB.getFingerprint()); } catch (IOException e) { // TODO: Better exception definitions @@ -55,6 +64,15 @@ public class CertificateProviderImpl implements CertificateProvider { return this.tempDir.resolve("cert.pem"); } + @Override + public Path fullchainPath() { + Path fcFile = this.tempDir.resolve("fullchain.pem"); + if (Files.exists(fcFile)) { + return fcFile; + } + return null; + } + @Override public void close() { try { diff --git a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java index eb076ad..38bfd45 100644 --- a/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java +++ b/src/test/java/de/mlessmann/certassist/TestOpenSSLCertificateCreator.java @@ -5,9 +5,8 @@ import static org.mockito.Mockito.*; import de.mlessmann.certassist.openssl.*; import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardOpenOption; +import java.util.Objects; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -59,11 +58,11 @@ class TestOpenSSLCertificateCreator { when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert); try (var childCert = certificateCreator.createCertificate(childRequest)) { System.out.println("Child certificate created: " + childCert); - Path fullchain = childCert.certificatePath().getParent().resolve("fullchain.pem"); - Files.write(fullchain, Files.readAllBytes(childCert.certificatePath()), StandardOpenOption.CREATE); - Files.write(fullchain, Files.readAllBytes(cert.certificatePath()), StandardOpenOption.APPEND); - - assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), fullchain)).isEqualTo(true); + Path fullchain = childCert.fullchainPath(); + assertThat( + certificateCreator.verifyCertificate(cert.certificatePath(), Objects.requireNonNull(fullchain)) + ) + .isEqualTo(true); } } }