feat: Read notBefore/After and serial from x509 command
- Added notBefore/After to the DB entity - API changed to a command output oriented record instead of the certificate request object to separate concerns
This commit is contained in:
parent
cdd82443b0
commit
d725d7d249
6 changed files with 184 additions and 76 deletions
|
@ -6,6 +6,7 @@ import jakarta.validation.constraints.NotNull;
|
|||
import lombok.*;
|
||||
import org.hibernate.proxy.HibernateProxy;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
@ -32,8 +33,8 @@ public class Certificate {
|
|||
@Min(1)
|
||||
private int requestedKeyLength;
|
||||
|
||||
@Min(1)
|
||||
private int requestedValidityDays;
|
||||
private OffsetDateTime notBefore;
|
||||
private OffsetDateTime notAfter;
|
||||
|
||||
@NotNull
|
||||
private String subjectCommonName;
|
||||
|
|
|
@ -22,6 +22,12 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeFormatterBuilder;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.IsoFields;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -65,6 +71,21 @@ public class OpenSSLService {
|
|||
private static final Pattern FINGERPRINT_EXTRACTOR = Pattern.compile(
|
||||
"^(?<algo>[0-9a-zA-Z]+) (?i)Fingerprint(?-i)=(?<finger>[a-z:A-Z0-9]+)"
|
||||
);
|
||||
private final DateTimeFormatter OSSL_DATE_TIME = new DateTimeFormatterBuilder()
|
||||
.parseCaseInsensitive()
|
||||
.appendValue(ChronoField.YEAR, 4)
|
||||
.appendLiteral('-')
|
||||
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
|
||||
.appendLiteral('-')
|
||||
.appendValue(ChronoField.DAY_OF_MONTH, 2)
|
||||
.appendLiteral(' ')
|
||||
.appendValue(ChronoField.HOUR_OF_DAY, 2)
|
||||
.appendLiteral(':')
|
||||
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
|
||||
.appendLiteral(':')
|
||||
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
|
||||
.appendOffset("+HH:MM:ss","Z")
|
||||
.toFormatter();
|
||||
private static final String OSSL_ENV_KEY_PW = "KEY_PASS";
|
||||
private static final String OSSL_ARG_KEY_PW = "env:" + OSSL_ENV_KEY_PW;
|
||||
private final AtomicBoolean versionLogged = new AtomicBoolean(false);
|
||||
|
@ -560,7 +581,7 @@ public class OpenSSLService {
|
|||
|
||||
@NonNull
|
||||
@SneakyThrows
|
||||
public CertificateInfo getCertificateInfo(String pemContent) {
|
||||
public X509CertificateInfo getCertificateInfo(String pemContent) {
|
||||
StartedProcess infoProc = null;
|
||||
try (var input = new ByteArrayInputStream(pemContent.getBytes())) {
|
||||
infoProc = Commands.infoCommand(resolveOpenSSL()).redirectInput(input).start();
|
||||
|
@ -581,7 +602,7 @@ public class OpenSSLService {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
public CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException {
|
||||
public X509CertificateInfo getCertificateInfo(Path path) throws CommandLineOperationException {
|
||||
requireNonNull(path, "Certificate file must be provided to read the info.");
|
||||
try {
|
||||
return getCertificateInfo(Files.readString(path));
|
||||
|
@ -620,9 +641,12 @@ public class OpenSSLService {
|
|||
}
|
||||
}
|
||||
|
||||
private CertificateInfo getCertificateInfo(String[] lines) {
|
||||
var builder = CertificateInfo.builder();
|
||||
boolean hasIssuer = false;
|
||||
/**
|
||||
* Reads the OpenSSL x509 command output to provide the requested certificate information
|
||||
*/
|
||||
private X509CertificateInfo getCertificateInfo(String[] lines) {
|
||||
var builder = X509CertificateInfo.builder();
|
||||
List<CertificateInfoExtension> extensions = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
|
@ -634,9 +658,8 @@ public class OpenSSLService {
|
|||
subjectBuilder = readSubjectInfo(line, subjectBuilder);
|
||||
line = lines[++i];
|
||||
}
|
||||
builder = builder.subject(subjectBuilder);
|
||||
builder = builder.subject(subjectBuilder.build());
|
||||
} else if (line.startsWith("issuer=")) {
|
||||
hasIssuer = true;
|
||||
var issuerBuilder = CertificateInfoSubject.builder();
|
||||
|
||||
line = lines[++i];
|
||||
|
@ -645,14 +668,24 @@ public class OpenSSLService {
|
|||
line = lines[++i];
|
||||
}
|
||||
|
||||
builder = builder.issuer(issuerBuilder);
|
||||
builder = builder.issuer(issuerBuilder.build());
|
||||
} else if (line.startsWith("X509v3 Subject Alternative Name")) {
|
||||
String[] altNames = lines[++i].split(",");
|
||||
builder = builder.extension(CertificateInfoExtension.builder().alternativeDnsNames(altNames));
|
||||
extensions.add(CertificateInfoExtension.builder().alternativeDnsNames(altNames).build());
|
||||
} else if (line.startsWith("serial=")) {
|
||||
builder = builder.serial(line.substring("serial=".length()));
|
||||
} else if (line.startsWith("notBefore=")) {
|
||||
String notBeforeStr = line.substring("notBefore=".length());
|
||||
var notBefore = OffsetDateTime.parse(notBeforeStr, OSSL_DATE_TIME);
|
||||
builder = builder.notBefore(notBefore);
|
||||
} else if (line.startsWith("notAfter=")) {
|
||||
String notAfterStr = line.substring("notAfter=".length());
|
||||
var notAfter = OffsetDateTime.parse(notAfterStr, OSSL_DATE_TIME);
|
||||
builder = builder.notAfter(notAfter);
|
||||
}
|
||||
}
|
||||
|
||||
builder = builder.type(hasIssuer ? RequestType.NORMAL_CERTIFICATE : RequestType.STANDALONE_CERTIFICATE);
|
||||
builder = builder.extensions(extensions);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
@ -730,7 +763,8 @@ public class OpenSSLService {
|
|||
"-nameopt",
|
||||
"sep_multiline",
|
||||
"-nameopt",
|
||||
"lname"
|
||||
"lname",
|
||||
"-modulus"
|
||||
)
|
||||
);
|
||||
command.addAll(Arrays.asList(additArgs));
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package de.mlessmann.certassist.openssl;
|
||||
|
||||
import de.mlessmann.certassist.models.CertificateInfoExtension;
|
||||
import de.mlessmann.certassist.models.CertificateInfoSubject;
|
||||
import lombok.Builder;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Builder
|
||||
public record X509CertificateInfo(
|
||||
CertificateInfoSubject subject,
|
||||
@Nullable CertificateInfoSubject issuer,
|
||||
String serial,
|
||||
OffsetDateTime notBefore,
|
||||
OffsetDateTime notAfter,
|
||||
List<CertificateInfoExtension> extensions
|
||||
) {
|
||||
public X509CertificateInfo {
|
||||
Objects.requireNonNull(subject);
|
||||
Objects.requireNonNull(serial);
|
||||
Objects.requireNonNull(notBefore);
|
||||
Objects.requireNonNull(notAfter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CertificateInfoExtension> extensions() {
|
||||
if (extensions == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.unmodifiableList(extensions);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import de.mlessmann.certassist.repositories.CertificateRepository;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -55,7 +56,9 @@ public class CertificateCreationService {
|
|||
certificate.setSubjectCommonName(certificateInfo.getSubject().getCommonName());
|
||||
certificate.setTrustingAuthority(certificateInfo.getTrustingAuthority());
|
||||
certificate.setRequestedKeyLength(certificateInfo.getRequestedKeyLength());
|
||||
certificate.setRequestedValidityDays(certificateInfo.getRequestedValidityDays());
|
||||
certificate.setNotBefore(OffsetDateTime.now());
|
||||
certificate.setNotAfter(OffsetDateTime.now().plusDays(certificateInfo.getRequestedValidityDays()));
|
||||
|
||||
final CertificateInfoSubject subjectInfo = certificateInfo.getSubject();
|
||||
certificate.setSubjectEmailAddress(subjectInfo.getEmailAddress());
|
||||
certificate.setSubjectOrganization(subjectInfo.getOrganization());
|
||||
|
@ -74,6 +77,33 @@ public class CertificateCreationService {
|
|||
return certificate;
|
||||
}
|
||||
|
||||
private Certificate createEntityFromInfo(X509CertificateInfo info) {
|
||||
final Certificate certificate = new Certificate();
|
||||
certificate.setType(mapCertificateRequestType(info.issuer() != null ? CertificateInfo.RequestType.NORMAL_CERTIFICATE : CertificateInfo.RequestType.STANDALONE_CERTIFICATE));
|
||||
certificate.setSubjectCommonName(info.subject().getCommonName());
|
||||
certificate.setTrustingAuthority(info.issuer().getCommonName());
|
||||
certificate.setRequestedKeyLength(-1);
|
||||
certificate.setNotBefore(info.notBefore());
|
||||
certificate.setNotAfter(info.notAfter());
|
||||
|
||||
final CertificateInfoSubject subjectInfo = info.subject();
|
||||
certificate.setSubjectEmailAddress(subjectInfo.getEmailAddress());
|
||||
certificate.setSubjectOrganization(subjectInfo.getOrganization());
|
||||
certificate.setSubjectOrganizationalUnit(subjectInfo.getOrganizationalUnit());
|
||||
certificate.setSubjectCountry(subjectInfo.getCountry());
|
||||
certificate.setSubjectState(subjectInfo.getState());
|
||||
certificate.setSubjectLocality(subjectInfo.getLocality());
|
||||
|
||||
final CertificateInfoExtension extension = info.extensions().getFirst();
|
||||
if (extension != null) {
|
||||
final CertificateExtension certificateExtension = new CertificateExtension();
|
||||
certificateExtension.setIdentifier("alternativeNames");
|
||||
certificateExtension.setValue(String.join(",", extension.getAlternativeDnsNames()));
|
||||
certificate.setCertificateExtension(List.of(certificateExtension));
|
||||
}
|
||||
return certificate;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Certificate importCertificate(
|
||||
@NonNull Path certificate,
|
||||
|
@ -82,8 +112,7 @@ public class CertificateCreationService {
|
|||
) {
|
||||
try {
|
||||
String fingerprint = openSSLService.getCertificateFingerprint(certificate);
|
||||
var generatedRequest = openSSLService.getCertificateInfo(certificate);
|
||||
Certificate entity = createEntityFromRequest(generatedRequest);
|
||||
Certificate entity = createEntityFromInfo(openSSLService.getCertificateInfo(certificate));
|
||||
entity.setFingerprint(fingerprint);
|
||||
entity.setCert(Files.readAllBytes(certificate));
|
||||
if (keyFile != null) {
|
||||
|
@ -113,8 +142,7 @@ public class CertificateCreationService {
|
|||
int endIdx = endMatcher.end();
|
||||
String singleCert = pemContent.substring(startIdx, endIdx);
|
||||
String fingerprint = openSSLService.getCertificateFingerprint(singleCert);
|
||||
var generatedRequest = openSSLService.getCertificateInfo(singleCert);
|
||||
Certificate entity = createEntityFromRequest(generatedRequest);
|
||||
Certificate entity = createEntityFromInfo(openSSLService.getCertificateInfo(singleCert));
|
||||
entity.setFingerprint(fingerprint);
|
||||
entity.setCert(singleCert.getBytes());
|
||||
certsInBundle.put(fingerprint, entity);
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
package de.mlessmann.certassist;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import de.mlessmann.certassist.models.CertificateInfo;
|
||||
import de.mlessmann.certassist.models.CertificateInfo.RequestType;
|
||||
import de.mlessmann.certassist.models.CertificateInfoExtension;
|
||||
import de.mlessmann.certassist.models.CertificateInfoSubject;
|
||||
import de.mlessmann.certassist.openssl.*;
|
||||
import de.mlessmann.certassist.openssl.CertificatePasswordProvider;
|
||||
import de.mlessmann.certassist.openssl.CertificateProvider;
|
||||
import de.mlessmann.certassist.openssl.OpenSSLService;
|
||||
import de.mlessmann.certassist.service.ExecutableResolver;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@SpringBootTest
|
||||
class TestOpenSSLService {
|
||||
|
||||
|
@ -60,7 +63,7 @@ class TestOpenSSLService {
|
|||
assertThat(certificateCreator.verifyCertificate(cert.certificatePath(), cert.certificatePath()))
|
||||
.withFailMessage(ERR_VERIFY_FAILED)
|
||||
.isTrue();
|
||||
assertThat(certificateCreator.isKeyEncrypted(cert.certificateKeyPath()))
|
||||
assertThat(certificateCreator.isKeyEncrypted(requireNonNull(cert.certificateKeyPath())))
|
||||
.withFailMessage(ERR_NOT_ENCRYPTED)
|
||||
.isTrue();
|
||||
|
||||
|
@ -86,11 +89,11 @@ class TestOpenSSLService {
|
|||
try (var childCert = certificateCreator.createCertificate(childRequest)) {
|
||||
Path fullchain = childCert.fullchainPath();
|
||||
assertThat(
|
||||
certificateCreator.verifyCertificate(Objects.requireNonNull(fullchain), cert.certificatePath())
|
||||
certificateCreator.verifyCertificate(requireNonNull(fullchain), cert.certificatePath())
|
||||
)
|
||||
.withFailMessage(ERR_VERIFY_FAILED)
|
||||
.isTrue();
|
||||
assertThat(certificateCreator.isKeyEncrypted(childCert.certificateKeyPath()))
|
||||
assertThat(certificateCreator.isKeyEncrypted(requireNonNull(childCert.certificateKeyPath())))
|
||||
.withFailMessage(ERR_NOT_ENCRYPTED)
|
||||
.isTrue();
|
||||
}
|
||||
|
@ -105,20 +108,22 @@ class TestOpenSSLService {
|
|||
|
||||
var request = certificateCreator.getCertificateInfo(TEST_CERT_PATH.resolve("x509forImportCA.pem"));
|
||||
assertThat(request).isNotNull();
|
||||
assertThat(request.getSubject().getCommonName()).isEqualTo("test.home");
|
||||
assertThat(request.getSubject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.getSubject().getState()).isEqualTo("SH");
|
||||
assertThat(request.getSubject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.getSubject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
assertThat(request.getExtension()).isNull();
|
||||
assertThat(request.subject().getCommonName()).isEqualTo("test.home");
|
||||
assertThat(request.subject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.subject().getState()).isEqualTo("SH");
|
||||
assertThat(request.subject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.subject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
assertThat(request.notBefore()).isEqualTo("2024-11-22T18:57:40Z");
|
||||
assertThat(request.notAfter()).isEqualTo("2025-11-22T18:57:40Z");
|
||||
assertThat(request.extensions()).isEmpty();
|
||||
|
||||
request = certificateCreator.getCertificateInfo(TEST_CERT_PATH.resolve("x509forImport.pem"));
|
||||
assertThat(request).isNotNull();
|
||||
assertThat(request.getSubject().getCommonName()).isEqualTo("test.local");
|
||||
assertThat(request.getSubject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.getSubject().getState()).isEqualTo("SH");
|
||||
assertThat(request.getSubject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.getSubject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
assertThat(request.getExtension().getAlternativeDnsNames()).containsExactly("test2.local", "test3.local");
|
||||
assertThat(request.subject().getCommonName()).isEqualTo("test.local");
|
||||
assertThat(request.subject().getCountry()).isEqualTo("DE");
|
||||
assertThat(request.subject().getState()).isEqualTo("SH");
|
||||
assertThat(request.subject().getLocality()).isEqualTo("HH");
|
||||
assertThat(request.subject().getOrganization()).isEqualTo("Crazy-Cats");
|
||||
assertThat(request.extensions().getFirst().getAlternativeDnsNames()).containsExactly("test2.local", "test3.local");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import de.mlessmann.certassist.models.Certificate;
|
|||
import de.mlessmann.certassist.models.CertificateExtension;
|
||||
import de.mlessmann.certassist.models.CertificateType;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.StreamSupport;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
@ -33,7 +35,8 @@ class CertificateRepositoryTest {
|
|||
certificate.setSubjectCommonName("test-cn");
|
||||
certificate.setType(CertificateType.SIGNED_CERT);
|
||||
certificate.setRequestedKeyLength(1);
|
||||
certificate.setRequestedValidityDays(1);
|
||||
certificate.setNotBefore(OffsetDateTime.now());
|
||||
certificate.setNotAfter(OffsetDateTime.now().plusDays(1));
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue