feat: support password-protected private keys
Encrypted PKCS#8 private keys are decrypted during staging using the provided password. The decrypted key is stored for Traefik (which needs cleartext PEM). Unencrypted keys continue to work without a password. - CertificateManager.stage() accepts optional keyPassword - DockerCertificateManager handles EncryptedPrivateKeyInfo decryption - UI: password field in upload form (vendor CertificatesPage) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,13 +42,13 @@ class CertificateServiceTest {
|
||||
var info = new CertificateInfo("CN=test", "CN=test", Instant.now(),
|
||||
Instant.now().plusSeconds(86400), false, true, "AA:BB");
|
||||
when(certManager.isAvailable()).thenReturn(true);
|
||||
when(certManager.stage(any(), any(), any()))
|
||||
when(certManager.stage(any(), any(), any(), any()))
|
||||
.thenReturn(CertValidationResult.ok(info));
|
||||
when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
|
||||
.thenReturn(Optional.empty());
|
||||
when(certRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = service.stage("cert".getBytes(), "key".getBytes(), null, UUID.randomUUID());
|
||||
var result = service.stage("cert".getBytes(), "key".getBytes(), null, null, UUID.randomUUID());
|
||||
|
||||
assertThat(result.valid()).isTrue();
|
||||
var captor = ArgumentCaptor.forClass(CertificateEntity.class);
|
||||
@@ -63,10 +63,10 @@ class CertificateServiceTest {
|
||||
when(certManager.isAvailable()).thenReturn(true);
|
||||
when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
|
||||
.thenReturn(Optional.of(existing));
|
||||
when(certManager.stage(any(), any(), any()))
|
||||
when(certManager.stage(any(), any(), any(), any()))
|
||||
.thenReturn(CertValidationResult.fail(List.of("bad cert")));
|
||||
|
||||
service.stage("cert".getBytes(), "key".getBytes(), null, UUID.randomUUID());
|
||||
service.stage("cert".getBytes(), "key".getBytes(), null, null, UUID.randomUUID());
|
||||
|
||||
verify(certRepository).delete(existing);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class CertificateServiceTest {
|
||||
void stage_returnsErrorWhenManagerUnavailable() {
|
||||
when(certManager.isAvailable()).thenReturn(false);
|
||||
|
||||
var result = service.stage("cert".getBytes(), "key".getBytes(), null, UUID.randomUUID());
|
||||
var result = service.stage("cert".getBytes(), "key".getBytes(), null, null, UUID.randomUUID());
|
||||
|
||||
assertThat(result.valid()).isFalse();
|
||||
assertThat(result.errors()).contains("Certificate management is not available");
|
||||
|
||||
@@ -37,7 +37,7 @@ class DockerCertificateManagerTest {
|
||||
void stage_validatesAndWritesFiles() throws Exception {
|
||||
byte[][] pair = generateCertAndKey("CN=test.example.com");
|
||||
|
||||
var result = manager.stage(pair[0], pair[1], null);
|
||||
var result = manager.stage(pair[0], pair[1], null, null);
|
||||
|
||||
assertThat(result.valid()).isTrue();
|
||||
assertThat(result.info()).isNotNull();
|
||||
@@ -49,7 +49,7 @@ class DockerCertificateManagerTest {
|
||||
|
||||
@Test
|
||||
void stage_rejectsInvalidCertPem() {
|
||||
var result = manager.stage("not a cert".getBytes(), "not a key".getBytes(), null);
|
||||
var result = manager.stage("not a cert".getBytes(), "not a key".getBytes(), null, null);
|
||||
assertThat(result.valid()).isFalse();
|
||||
assertThat(result.errors()).isNotEmpty();
|
||||
}
|
||||
@@ -59,7 +59,7 @@ class DockerCertificateManagerTest {
|
||||
byte[][] pair1 = generateCertAndKey("CN=test.example.com");
|
||||
byte[][] pair2 = generateCertAndKey("CN=other.example.com");
|
||||
|
||||
var result = manager.stage(pair1[0], pair2[1], null);
|
||||
var result = manager.stage(pair1[0], pair2[1], null, null);
|
||||
assertThat(result.valid()).isFalse();
|
||||
assertThat(result.errors()).anyMatch(e -> e.contains("does not match"));
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class DockerCertificateManagerTest {
|
||||
|
||||
// Stage a second cert
|
||||
byte[][] pair2 = generateCertAndKey("CN=second.example.com");
|
||||
var stageResult = manager.stage(pair2[0], pair2[1], null);
|
||||
var stageResult = manager.stage(pair2[0], pair2[1], null, null);
|
||||
assertThat(stageResult.valid()).isTrue();
|
||||
|
||||
// Activate: first -> archived, second -> active
|
||||
@@ -116,7 +116,7 @@ class DockerCertificateManagerTest {
|
||||
@Test
|
||||
void discardStaged_removesFiles() throws Exception {
|
||||
byte[][] pair = generateCertAndKey("CN=test.example.com");
|
||||
manager.stage(pair[0], pair[1], null);
|
||||
manager.stage(pair[0], pair[1], null, null);
|
||||
|
||||
assertThat(Files.exists(certsDir.resolve("staged/cert.pem"))).isTrue();
|
||||
|
||||
@@ -130,7 +130,7 @@ class DockerCertificateManagerTest {
|
||||
byte[][] pair = generateCertAndKey("CN=test.example.com");
|
||||
|
||||
// Use the cert itself as a "CA" for testing purposes
|
||||
var result = manager.stage(pair[0], pair[1], pair[0]);
|
||||
var result = manager.stage(pair[0], pair[1], pair[0], null);
|
||||
|
||||
assertThat(result.valid()).isTrue();
|
||||
assertThat(result.info().hasCaBundle()).isTrue();
|
||||
@@ -165,7 +165,7 @@ class DockerCertificateManagerTest {
|
||||
|
||||
// Stage new cert
|
||||
byte[][] pair2 = generateCertAndKey("CN=second.example.com");
|
||||
manager.stage(pair2[0], pair2[1], null);
|
||||
manager.stage(pair2[0], pair2[1], null, null);
|
||||
|
||||
// Activate: old archive should be deleted, first becomes archive
|
||||
manager.activate();
|
||||
|
||||
Reference in New Issue
Block a user