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:
@@ -77,6 +77,7 @@ public class CertificateController {
|
|||||||
@RequestParam("cert") MultipartFile certFile,
|
@RequestParam("cert") MultipartFile certFile,
|
||||||
@RequestParam("key") MultipartFile keyFile,
|
@RequestParam("key") MultipartFile keyFile,
|
||||||
@RequestParam(value = "ca", required = false) MultipartFile caFile,
|
@RequestParam(value = "ca", required = false) MultipartFile caFile,
|
||||||
|
@RequestParam(value = "password", required = false) String keyPassword,
|
||||||
@AuthenticationPrincipal Jwt jwt) {
|
@AuthenticationPrincipal Jwt jwt) {
|
||||||
try {
|
try {
|
||||||
byte[] certPem = certFile.getBytes();
|
byte[] certPem = certFile.getBytes();
|
||||||
@@ -84,7 +85,7 @@ public class CertificateController {
|
|||||||
byte[] caPem = caFile != null ? caFile.getBytes() : null;
|
byte[] caPem = caFile != null ? caFile.getBytes() : null;
|
||||||
UUID actorId = resolveActorId(jwt);
|
UUID actorId = resolveActorId(jwt);
|
||||||
|
|
||||||
CertValidationResult result = certificateService.stage(certPem, keyPem, caPem, actorId);
|
CertValidationResult result = certificateService.stage(certPem, keyPem, caPem, keyPassword, actorId);
|
||||||
|
|
||||||
if (!result.valid()) {
|
if (!result.valid()) {
|
||||||
return ResponseEntity.badRequest().body(
|
return ResponseEntity.badRequest().body(
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ public interface CertificateManager {
|
|||||||
/**
|
/**
|
||||||
* Write cert+key+ca to staging area and validate.
|
* Write cert+key+ca to staging area and validate.
|
||||||
* Does NOT activate — call {@link #activate()} to promote.
|
* Does NOT activate — call {@link #activate()} to promote.
|
||||||
|
* @param keyPassword optional password for encrypted private keys (null if unencrypted)
|
||||||
*/
|
*/
|
||||||
CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem);
|
CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem, String keyPassword);
|
||||||
|
|
||||||
/** Promote staged -> active. Moves current active to archive (deleting previous archive). */
|
/** Promote staged -> active. Moves current active to archive (deleting previous archive). */
|
||||||
void activate();
|
void activate();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class CertificateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem, UUID actorId) {
|
public CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem, String keyPassword, UUID actorId) {
|
||||||
if (!certManager.isAvailable()) {
|
if (!certManager.isAvailable()) {
|
||||||
return CertValidationResult.fail(List.of("Certificate management is not available"));
|
return CertValidationResult.fail(List.of("Certificate management is not available"));
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ public class CertificateService {
|
|||||||
certRepository.findByStatus(CertificateEntity.Status.STAGED).ifPresent(certRepository::delete);
|
certRepository.findByStatus(CertificateEntity.Status.STAGED).ifPresent(certRepository::delete);
|
||||||
|
|
||||||
// Stage files and validate
|
// Stage files and validate
|
||||||
CertValidationResult result = certManager.stage(certPem, keyPem, caBundlePem);
|
CertValidationResult result = certManager.stage(certPem, keyPem, caBundlePem, keyPassword);
|
||||||
if (!result.valid()) {
|
if (!result.valid()) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class DisabledCertificateManager implements CertificateManager {
|
|||||||
public CertificateInfo getArchived() { return null; }
|
public CertificateInfo getArchived() { return null; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem) {
|
public CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem, String keyPassword) {
|
||||||
return CertValidationResult.fail(List.of("Certificate management is not available"));
|
return CertValidationResult.fail(List.of("Certificate management is not available"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class DockerCertificateManager implements CertificateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem) {
|
public CertValidationResult stage(byte[] certPem, byte[] keyPem, byte[] caBundlePem, String keyPassword) {
|
||||||
List<String> errors = new ArrayList<>();
|
List<String> errors = new ArrayList<>();
|
||||||
|
|
||||||
// Parse certificate
|
// Parse certificate
|
||||||
@@ -71,9 +71,15 @@ public class DockerCertificateManager implements CertificateManager {
|
|||||||
return CertValidationResult.fail(errors);
|
return CertValidationResult.fail(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse private key
|
// Parse private key (may be password-protected)
|
||||||
|
java.security.PrivateKey privateKey;
|
||||||
|
byte[] decryptedKeyPem = keyPem;
|
||||||
try {
|
try {
|
||||||
var privateKey = parsePrivateKey(keyPem);
|
privateKey = parsePrivateKey(keyPem, keyPassword);
|
||||||
|
// Re-encode as unencrypted PKCS8 PEM for Traefik (which needs cleartext)
|
||||||
|
if (isEncryptedKey(keyPem)) {
|
||||||
|
decryptedKeyPem = encodePrivateKeyPem(privateKey);
|
||||||
|
}
|
||||||
// Verify key matches cert
|
// Verify key matches cert
|
||||||
if (cert.getPublicKey() instanceof RSAPublicKey rsaPub
|
if (cert.getPublicKey() instanceof RSAPublicKey rsaPub
|
||||||
&& privateKey instanceof RSAPrivateKey rsaPriv) {
|
&& privateKey instanceof RSAPrivateKey rsaPriv) {
|
||||||
@@ -107,7 +113,7 @@ public class DockerCertificateManager implements CertificateManager {
|
|||||||
Files.createDirectories(stagedDir);
|
Files.createDirectories(stagedDir);
|
||||||
|
|
||||||
writeAtomic(stagedDir.resolve("cert.pem"), certPem);
|
writeAtomic(stagedDir.resolve("cert.pem"), certPem);
|
||||||
writeAtomic(stagedDir.resolve("key.pem"), keyPem);
|
writeAtomic(stagedDir.resolve("key.pem"), decryptedKeyPem);
|
||||||
if (caBundlePem != null && caBundlePem.length > 0) {
|
if (caBundlePem != null && caBundlePem.length > 0) {
|
||||||
writeAtomic(stagedDir.resolve("ca.pem"), caBundlePem);
|
writeAtomic(stagedDir.resolve("ca.pem"), caBundlePem);
|
||||||
} else {
|
} else {
|
||||||
@@ -286,9 +292,42 @@ public class DockerCertificateManager implements CertificateManager {
|
|||||||
return certs.stream().map(c -> (X509Certificate) c).toList();
|
return certs.stream().map(c -> (X509Certificate) c).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static java.security.PrivateKey parsePrivateKey(byte[] pem) throws Exception {
|
static boolean isEncryptedKey(byte[] pem) {
|
||||||
|
String s = new String(pem);
|
||||||
|
return s.contains("ENCRYPTED PRIVATE KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
static java.security.PrivateKey parsePrivateKey(byte[] pem, String password) throws Exception {
|
||||||
String pemStr = new String(pem);
|
String pemStr = new String(pem);
|
||||||
// Extract base64 content between PEM markers (handles Bag Attributes etc.)
|
|
||||||
|
// Encrypted PKCS#8 key
|
||||||
|
if (pemStr.contains("BEGIN ENCRYPTED PRIVATE KEY")) {
|
||||||
|
if (password == null || password.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Private key is encrypted but no password was provided");
|
||||||
|
}
|
||||||
|
var matcher = java.util.regex.Pattern
|
||||||
|
.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.+?)-----END ENCRYPTED PRIVATE KEY-----",
|
||||||
|
java.util.regex.Pattern.DOTALL)
|
||||||
|
.matcher(pemStr);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
throw new IllegalArgumentException("Malformed encrypted private key PEM");
|
||||||
|
}
|
||||||
|
String base64 = matcher.group(1).replaceAll("\\s+", "");
|
||||||
|
byte[] decoded = Base64.getDecoder().decode(base64);
|
||||||
|
|
||||||
|
var encryptedInfo = new javax.crypto.EncryptedPrivateKeyInfo(decoded);
|
||||||
|
var pbeKeySpec = new javax.crypto.spec.PBEKeySpec(password.toCharArray());
|
||||||
|
var keyFactory = javax.crypto.SecretKeyFactory.getInstance(encryptedInfo.getAlgName());
|
||||||
|
var secretKey = keyFactory.generateSecret(pbeKeySpec);
|
||||||
|
|
||||||
|
var cipher = javax.crypto.Cipher.getInstance(encryptedInfo.getAlgName());
|
||||||
|
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKey, encryptedInfo.getAlgParameters());
|
||||||
|
|
||||||
|
var pkcs8Spec = encryptedInfo.getKeySpec(cipher);
|
||||||
|
return KeyFactory.getInstance("RSA").generatePrivate(pkcs8Spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unencrypted key — extract base64 between PEM markers (handles Bag Attributes etc.)
|
||||||
var matcher = java.util.regex.Pattern
|
var matcher = java.util.regex.Pattern
|
||||||
.compile("-----BEGIN (?:RSA )?PRIVATE KEY-----(.+?)-----END (?:RSA )?PRIVATE KEY-----",
|
.compile("-----BEGIN (?:RSA )?PRIVATE KEY-----(.+?)-----END (?:RSA )?PRIVATE KEY-----",
|
||||||
java.util.regex.Pattern.DOTALL)
|
java.util.regex.Pattern.DOTALL)
|
||||||
@@ -302,6 +341,14 @@ public class DockerCertificateManager implements CertificateManager {
|
|||||||
return KeyFactory.getInstance("RSA").generatePrivate(spec);
|
return KeyFactory.getInstance("RSA").generatePrivate(spec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static byte[] encodePrivateKeyPem(java.security.PrivateKey key) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.append("-----BEGIN PRIVATE KEY-----\n");
|
||||||
|
sb.append(Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(key.getEncoded()));
|
||||||
|
sb.append("\n-----END PRIVATE KEY-----\n");
|
||||||
|
return sb.toString().getBytes();
|
||||||
|
}
|
||||||
|
|
||||||
private void writeAtomic(Path target, byte[] data) throws IOException {
|
private void writeAtomic(Path target, byte[] data) throws IOException {
|
||||||
Path wip = target.resolveSibling(target.getFileName() + ".wip");
|
Path wip = target.resolveSibling(target.getFileName() + ".wip");
|
||||||
Files.write(wip, data);
|
Files.write(wip, data);
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ class CertificateServiceTest {
|
|||||||
var info = new CertificateInfo("CN=test", "CN=test", Instant.now(),
|
var info = new CertificateInfo("CN=test", "CN=test", Instant.now(),
|
||||||
Instant.now().plusSeconds(86400), false, true, "AA:BB");
|
Instant.now().plusSeconds(86400), false, true, "AA:BB");
|
||||||
when(certManager.isAvailable()).thenReturn(true);
|
when(certManager.isAvailable()).thenReturn(true);
|
||||||
when(certManager.stage(any(), any(), any()))
|
when(certManager.stage(any(), any(), any(), any()))
|
||||||
.thenReturn(CertValidationResult.ok(info));
|
.thenReturn(CertValidationResult.ok(info));
|
||||||
when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
|
when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
|
||||||
.thenReturn(Optional.empty());
|
.thenReturn(Optional.empty());
|
||||||
when(certRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
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();
|
assertThat(result.valid()).isTrue();
|
||||||
var captor = ArgumentCaptor.forClass(CertificateEntity.class);
|
var captor = ArgumentCaptor.forClass(CertificateEntity.class);
|
||||||
@@ -63,10 +63,10 @@ class CertificateServiceTest {
|
|||||||
when(certManager.isAvailable()).thenReturn(true);
|
when(certManager.isAvailable()).thenReturn(true);
|
||||||
when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
|
when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
|
||||||
.thenReturn(Optional.of(existing));
|
.thenReturn(Optional.of(existing));
|
||||||
when(certManager.stage(any(), any(), any()))
|
when(certManager.stage(any(), any(), any(), any()))
|
||||||
.thenReturn(CertValidationResult.fail(List.of("bad cert")));
|
.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);
|
verify(certRepository).delete(existing);
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ class CertificateServiceTest {
|
|||||||
void stage_returnsErrorWhenManagerUnavailable() {
|
void stage_returnsErrorWhenManagerUnavailable() {
|
||||||
when(certManager.isAvailable()).thenReturn(false);
|
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.valid()).isFalse();
|
||||||
assertThat(result.errors()).contains("Certificate management is not available");
|
assertThat(result.errors()).contains("Certificate management is not available");
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class DockerCertificateManagerTest {
|
|||||||
void stage_validatesAndWritesFiles() throws Exception {
|
void stage_validatesAndWritesFiles() throws Exception {
|
||||||
byte[][] pair = generateCertAndKey("CN=test.example.com");
|
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.valid()).isTrue();
|
||||||
assertThat(result.info()).isNotNull();
|
assertThat(result.info()).isNotNull();
|
||||||
@@ -49,7 +49,7 @@ class DockerCertificateManagerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void stage_rejectsInvalidCertPem() {
|
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.valid()).isFalse();
|
||||||
assertThat(result.errors()).isNotEmpty();
|
assertThat(result.errors()).isNotEmpty();
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ class DockerCertificateManagerTest {
|
|||||||
byte[][] pair1 = generateCertAndKey("CN=test.example.com");
|
byte[][] pair1 = generateCertAndKey("CN=test.example.com");
|
||||||
byte[][] pair2 = generateCertAndKey("CN=other.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.valid()).isFalse();
|
||||||
assertThat(result.errors()).anyMatch(e -> e.contains("does not match"));
|
assertThat(result.errors()).anyMatch(e -> e.contains("does not match"));
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ class DockerCertificateManagerTest {
|
|||||||
|
|
||||||
// Stage a second cert
|
// Stage a second cert
|
||||||
byte[][] pair2 = generateCertAndKey("CN=second.example.com");
|
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();
|
assertThat(stageResult.valid()).isTrue();
|
||||||
|
|
||||||
// Activate: first -> archived, second -> active
|
// Activate: first -> archived, second -> active
|
||||||
@@ -116,7 +116,7 @@ class DockerCertificateManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void discardStaged_removesFiles() throws Exception {
|
void discardStaged_removesFiles() throws Exception {
|
||||||
byte[][] pair = generateCertAndKey("CN=test.example.com");
|
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();
|
assertThat(Files.exists(certsDir.resolve("staged/cert.pem"))).isTrue();
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ class DockerCertificateManagerTest {
|
|||||||
byte[][] pair = generateCertAndKey("CN=test.example.com");
|
byte[][] pair = generateCertAndKey("CN=test.example.com");
|
||||||
|
|
||||||
// Use the cert itself as a "CA" for testing purposes
|
// 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.valid()).isTrue();
|
||||||
assertThat(result.info().hasCaBundle()).isTrue();
|
assertThat(result.info().hasCaBundle()).isTrue();
|
||||||
@@ -165,7 +165,7 @@ class DockerCertificateManagerTest {
|
|||||||
|
|
||||||
// Stage new cert
|
// Stage new cert
|
||||||
byte[][] pair2 = generateCertAndKey("CN=second.example.com");
|
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
|
// Activate: old archive should be deleted, first becomes archive
|
||||||
manager.activate();
|
manager.activate();
|
||||||
|
|||||||
17
ui/src/pages/vendor/CertificatesPage.tsx
vendored
17
ui/src/pages/vendor/CertificatesPage.tsx
vendored
@@ -1,9 +1,10 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Input,
|
||||||
Spinner,
|
Spinner,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
@@ -139,6 +140,7 @@ export function CertificatesPage() {
|
|||||||
const certInputRef = useRef<HTMLInputElement>(null);
|
const certInputRef = useRef<HTMLInputElement>(null);
|
||||||
const keyInputRef = useRef<HTMLInputElement>(null);
|
const keyInputRef = useRef<HTMLInputElement>(null);
|
||||||
const caInputRef = useRef<HTMLInputElement>(null);
|
const caInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [keyPassword, setKeyPassword] = useState('');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -172,6 +174,7 @@ export function CertificatesPage() {
|
|||||||
formData.append('cert', certFile);
|
formData.append('cert', certFile);
|
||||||
formData.append('key', keyFile);
|
formData.append('key', keyFile);
|
||||||
if (caFile) formData.append('ca', caFile);
|
if (caFile) formData.append('ca', caFile);
|
||||||
|
if (keyPassword) formData.append('password', keyPassword);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await stageMutation.mutateAsync(formData);
|
const result = await stageMutation.mutateAsync(formData);
|
||||||
@@ -180,6 +183,7 @@ export function CertificatesPage() {
|
|||||||
if (certInputRef.current) certInputRef.current.value = '';
|
if (certInputRef.current) certInputRef.current.value = '';
|
||||||
if (keyInputRef.current) keyInputRef.current.value = '';
|
if (keyInputRef.current) keyInputRef.current.value = '';
|
||||||
if (caInputRef.current) caInputRef.current.value = '';
|
if (caInputRef.current) caInputRef.current.value = '';
|
||||||
|
setKeyPassword('');
|
||||||
} else {
|
} else {
|
||||||
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
|
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
|
||||||
}
|
}
|
||||||
@@ -285,6 +289,17 @@ export function CertificatesPage() {
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
<FileField label="Certificate (PEM) *" inputRef={certInputRef} accept=".pem,.crt,.cer" />
|
<FileField label="Certificate (PEM) *" inputRef={certInputRef} accept=".pem,.crt,.cer" />
|
||||||
<FileField label="Private Key (PEM) *" inputRef={keyInputRef} accept=".pem,.key" />
|
<FileField label="Private Key (PEM) *" inputRef={keyInputRef} accept=".pem,.key" />
|
||||||
|
<div>
|
||||||
|
<label style={{ fontSize: 13, color: 'var(--text-secondary)', display: 'block', marginBottom: 4 }}>
|
||||||
|
Key Password (if encrypted)
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Leave empty if key is not encrypted"
|
||||||
|
value={keyPassword}
|
||||||
|
onChange={(e) => setKeyPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<FileField label="CA Bundle (PEM, optional)" inputRef={caInputRef} accept=".pem,.crt,.cer" />
|
<FileField label="CA Bundle (PEM, optional)" inputRef={caInputRef} accept=".pem,.crt,.cer" />
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
|||||||
Reference in New Issue
Block a user