feat: Store fullchain certificate information alongside certs

This commit is contained in:
Magnus Leßmann (@MarkL4YG) 2024-11-22 10:43:02 +01:00
parent e888ea57c1
commit c7f05f1337
6 changed files with 71 additions and 10 deletions

View file

@ -46,10 +46,16 @@ public class Certificate {
private List<CertificateExtension> certificateExtension = new ArrayList<>(); private List<CertificateExtension> certificateExtension = new ArrayList<>();
@Lob @Lob
@Column(nullable = false)
private byte[] cert = new byte[0]; private byte[] cert = new byte[0];
@Lob @Lob
@Column(nullable = false)
private byte[] privateKey = new byte[0]; private byte[] privateKey = new byte[0];
@Lob
@Column
private byte[] fullchain;
private String fingerprint; private String fingerprint;
} }

View file

@ -1,6 +1,7 @@
package de.mlessmann.certassist.openssl; package de.mlessmann.certassist.openssl;
import java.nio.file.Path; 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. * 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. * @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 { public interface CertificateUsage extends AutoCloseable {
/**
* Returns the path to the certificate file (on disk, potentially temporary depending on the storage implementation).
*/
Path certificatePath(); 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(); 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;<HEX>:<HEX>:...}
*/
String fingerprint(); String fingerprint();
@Override @Override

View file

@ -89,7 +89,7 @@ public class OpenSSLCertificateCreator {
Path certificate = createCertificate(request, keyFile, tmpDir.resolve("certificate.crt"), certPassword); Path certificate = createCertificate(request, keyFile, tmpDir.resolve("certificate.crt"), certPassword);
String fingerprint = getCertificateFingerprint(certificate); String fingerprint = getCertificateFingerprint(certificate);
passwordProvider.setPasswordFor(fingerprint, certPassword); 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())) { try (var certAuthority = certificateProvider.requestCertificateUsage(request.getTrustingAuthority())) {
@ -103,7 +103,16 @@ public class OpenSSLCertificateCreator {
); );
String fingerprint = getCertificateFingerprint(signedCert); String fingerprint = getCertificateFingerprint(signedCert);
passwordProvider.setPasswordFor(fingerprint, certPassword); 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);
} }
} }

View file

@ -16,6 +16,7 @@ public class OpenSSLCertificateResult implements CertificateUsage {
private final Path tmpDir; private final Path tmpDir;
private final Path certificatePath; private final Path certificatePath;
private final Path privateKeyPath; private final Path privateKeyPath;
private final Path fullchainPath;
private final String certificateFingerPrint; private final String certificateFingerPrint;
@Override @Override
@ -28,6 +29,11 @@ public class OpenSSLCertificateResult implements CertificateUsage {
return privateKeyPath; return privateKeyPath;
} }
@Override
public Path fullchainPath() {
return fullchainPath;
}
@Override @Override
public String fingerprint() { public String fingerprint() {
return certificateFingerPrint; return certificateFingerPrint;

View file

@ -2,6 +2,8 @@ package de.mlessmann.certassist.service;
import de.mlessmann.certassist.Constants; import de.mlessmann.certassist.Constants;
import de.mlessmann.certassist.DeleteRecursiveFileVisitor; 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.CertificateProvider;
import de.mlessmann.certassist.openssl.CertificateUsage; import de.mlessmann.certassist.openssl.CertificateUsage;
import de.mlessmann.certassist.repositories.CertificateRepository; import de.mlessmann.certassist.repositories.CertificateRepository;
@ -27,15 +29,22 @@ public class CertificateProviderImpl implements CertificateProvider {
@Override @Override
public CertificateUsage requestCertificateUsage(String fingerprint) { public CertificateUsage requestCertificateUsage(String fingerprint) {
var certFromDB = certificateRepository.findByFingerprintIs(fingerprint); Certificate certFromDB = certificateRepository.findByFingerprintIs(fingerprint);
if (certFromDB == null) { if (certFromDB == null) {
throw new IllegalArgumentException("Unknown fingerprint"); throw new IllegalArgumentException("Unknown fingerprint");
} }
boolean selfSigned =
certFromDB.getType() == CertificateType.ROOT_CA || certFromDB.getType() == CertificateType.STANDALONE_CERT;
try { try {
Path tempDirectory = Files.createTempDirectory(Constants.CERTASSIST_TMP_PREFIX); Path tempDirectory = Files.createTempDirectory(Constants.CERTASSIST_TMP_PREFIX);
Files.write(tempDirectory.resolve("key.pem"), certFromDB.getPrivateKey(), CREATE_TRUNCATE); Files.write(tempDirectory.resolve("key.pem"), certFromDB.getPrivateKey(), CREATE_TRUNCATE);
Files.write(tempDirectory.resolve("cert.pem"), certFromDB.getCert(), 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()); return new ExtractedCert(tempDirectory, certFromDB.getFingerprint());
} catch (IOException e) { } catch (IOException e) {
// TODO: Better exception definitions // TODO: Better exception definitions
@ -55,6 +64,15 @@ public class CertificateProviderImpl implements CertificateProvider {
return this.tempDir.resolve("cert.pem"); 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 @Override
public void close() { public void close() {
try { try {

View file

@ -5,9 +5,8 @@ import static org.mockito.Mockito.*;
import de.mlessmann.certassist.openssl.*; import de.mlessmann.certassist.openssl.*;
import de.mlessmann.certassist.openssl.CertificateRequest.RequestType; import de.mlessmann.certassist.openssl.CertificateRequest.RequestType;
import java.nio.file.Files;
import java.nio.file.Path; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -59,11 +58,11 @@ class TestOpenSSLCertificateCreator {
when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert); when(certificateProvider.requestCertificateUsage(cert.fingerprint())).thenReturn(spiedCert);
try (var childCert = certificateCreator.createCertificate(childRequest)) { try (var childCert = certificateCreator.createCertificate(childRequest)) {
System.out.println("Child certificate created: " + childCert); System.out.println("Child certificate created: " + childCert);
Path fullchain = childCert.certificatePath().getParent().resolve("fullchain.pem"); Path fullchain = childCert.fullchainPath();
Files.write(fullchain, Files.readAllBytes(childCert.certificatePath()), StandardOpenOption.CREATE); assertThat(
Files.write(fullchain, Files.readAllBytes(cert.certificatePath()), StandardOpenOption.APPEND); certificateCreator.verifyCertificate(cert.certificatePath(), Objects.requireNonNull(fullchain))
)
assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), fullchain)).isEqualTo(true); .isEqualTo(true);
} }
} }
} }