This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-23200 in repository https://gitbox.apache.org/repos/asf/camel.git
commit abfcc6db441f63b3df9b338eb86f5df55fd050c4 Author: Andrea Cosentino <[email protected]> AuthorDate: Fri Mar 13 11:40:18 2026 +0100 CAMEL-23200 - Camel-PQC: Replace Java serialization with PKCS#8/X.509 in FileBasedKeyLifecycleManager Replace ObjectOutputStream/ObjectInputStream serialization with standard cryptographic formats: - Private keys now stored in PKCS#8 format (Base64 JSON) - Public keys now stored in X.509/SubjectPublicKeyInfo format (Base64 JSON) - Metadata now stored as human-readable JSON instead of binary serialization This aligns FileBasedKeyLifecycleManager with the encoding already used by AwsSecretsManagerKeyLifecycleManager and HashicorpVaultKeyLifecycleManager, making stored keys readable by OpenSSL and other PQC tools. Backward compatibility is preserved: legacy Java-serialized key and metadata files are automatically detected and migrated to the new format on first access. KeyMetadata gains a three-arg constructor (keyId, algorithm, createdAt) plus setLastUsedAt() and setUsageCount() setters to support JSON round-trip deserialization without losing timestamp fidelity. Signed-off-by: Andrea Cosentino <[email protected]> --- .../lifecycle/FileBasedKeyLifecycleManager.java | 269 ++++++++++++++++++--- .../camel/component/pqc/lifecycle/KeyMetadata.java | 14 +- 2 files changed, 245 insertions(+), 38 deletions(-) diff --git a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java index 6931ab483e51..7e9061b8828e 100644 --- a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java +++ b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java @@ -17,46 +17,61 @@ package org.apache.camel.component.pqc.lifecycle; import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.IOException; import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; +import java.util.Base64; import java.util.Date; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms; import org.apache.camel.component.pqc.PQCSignatureAlgorithms; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * File-based implementation of KeyLifecycleManager. Stores keys and metadata in a specified directory with secure - * permissions. + * File-based implementation of KeyLifecycleManager. Stores private keys in PKCS#8 format, public keys in X.509 format, + * and metadata as JSON. This is consistent with the encoding used by {@link AwsSecretsManagerKeyLifecycleManager} and + * {@link HashicorpVaultKeyLifecycleManager}. + * <p/> + * For backward compatibility, keys stored in the legacy Java serialization format are automatically migrated to the new + * standard format on first read. */ public class FileBasedKeyLifecycleManager implements KeyLifecycleManager { private static final Logger LOG = LoggerFactory.getLogger(FileBasedKeyLifecycleManager.class); private final Path keyDirectory; + private final ObjectMapper objectMapper; private final ConcurrentHashMap<String, KeyPair> keyCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, KeyMetadata> metadataCache = new ConcurrentHashMap<>(); public FileBasedKeyLifecycleManager(String keyDirectoryPath) throws IOException { this.keyDirectory = Paths.get(keyDirectoryPath); + this.objectMapper = new ObjectMapper(); + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); Files.createDirectories(keyDirectory); LOG.info("Initialized FileBasedKeyLifecycleManager with directory: {}", keyDirectory); loadExistingKeys(); @@ -159,23 +174,33 @@ public class FileBasedKeyLifecycleManager implements KeyLifecycleManager { @Override public void storeKey(String keyId, KeyPair keyPair, KeyMetadata metadata) throws Exception { - // Store key pair - Path keyFile = getKeyFile(keyId); - try (ObjectOutputStream oos = new ObjectOutputStream( - new BufferedOutputStream( - Files.newOutputStream(keyFile, StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING)))) { - oos.writeObject(keyPair); - } - - // Store metadata + // Store private key in PKCS#8 format + Path privateKeyFile = getPrivateKeyFile(keyId); + byte[] privateKeyBytes = keyPair.getPrivate().getEncoded(); + String privateKeyBase64 = Base64.getEncoder().encodeToString(privateKeyBytes); + KeyFileData privateData = new KeyFileData(privateKeyBase64, "PKCS8", metadata.getAlgorithm()); + Files.writeString(privateKeyFile, objectMapper.writeValueAsString(privateData), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + // Store public key in X.509 format + Path publicKeyFile = getPublicKeyFile(keyId); + byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); + String publicKeyBase64 = Base64.getEncoder().encodeToString(publicKeyBytes); + KeyFileData publicData = new KeyFileData(publicKeyBase64, "X509", metadata.getAlgorithm()); + Files.writeString(publicKeyFile, objectMapper.writeValueAsString(publicData), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + // Store metadata as JSON Path metadataFile = getMetadataFile(keyId); - try (ObjectOutputStream oos = new ObjectOutputStream( - new BufferedOutputStream( - Files.newOutputStream(metadataFile, StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING)))) { - oos.writeObject(metadata); - } + MetadataFileData metadataData = MetadataFileData.fromKeyMetadata(metadata); + Files.writeString(metadataFile, objectMapper.writeValueAsString(metadataData), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + // Remove legacy .key file if it exists (migration cleanup) + Files.deleteIfExists(getLegacyKeyFile(keyId)); // Update caches keyCache.put(keyId, keyPair); @@ -190,16 +215,23 @@ public class FileBasedKeyLifecycleManager implements KeyLifecycleManager { return keyCache.get(keyId); } - Path keyFile = getKeyFile(keyId); - if (!Files.exists(keyFile)) { - throw new IllegalArgumentException("Key not found: " + keyId); - } + Path privateKeyFile = getPrivateKeyFile(keyId); + Path publicKeyFile = getPublicKeyFile(keyId); - try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(Files.newInputStream(keyFile)))) { - KeyPair keyPair = (KeyPair) ois.readObject(); + // Check for new format first + if (Files.exists(privateKeyFile) && Files.exists(publicKeyFile)) { + KeyPair keyPair = readStandardKeyPair(keyId, privateKeyFile, publicKeyFile); keyCache.put(keyId, keyPair); return keyPair; } + + // Fall back to legacy format for migration + Path legacyKeyFile = getLegacyKeyFile(keyId); + if (Files.exists(legacyKeyFile)) { + return migrateLegacyKey(keyId); + } + + throw new IllegalArgumentException("Key not found: " + keyId); } @Override @@ -213,29 +245,36 @@ public class FileBasedKeyLifecycleManager implements KeyLifecycleManager { return null; } - try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(Files.newInputStream(metadataFile)))) { - KeyMetadata metadata = (KeyMetadata) ois.readObject(); + String content = Files.readString(metadataFile, StandardCharsets.UTF_8); + + // Detect format: JSON starts with '{', legacy Java serialization starts with binary + if (content.trim().startsWith("{")) { + MetadataFileData data = objectMapper.readValue(content, MetadataFileData.class); + KeyMetadata metadata = data.toKeyMetadata(); metadataCache.put(keyId, metadata); return metadata; + } else { + // Legacy format - read via ObjectInputStream and migrate + return migrateLegacyMetadata(keyId); } } @Override public void updateKeyMetadata(String keyId, KeyMetadata metadata) throws Exception { Path metadataFile = getMetadataFile(keyId); - try (ObjectOutputStream oos = new ObjectOutputStream( - new BufferedOutputStream( - Files.newOutputStream(metadataFile, StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING)))) { - oos.writeObject(metadata); - } + MetadataFileData metadataData = MetadataFileData.fromKeyMetadata(metadata); + Files.writeString(metadataFile, objectMapper.writeValueAsString(metadataData), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); metadataCache.put(keyId, metadata); } @Override public void deleteKey(String keyId) throws Exception { - Files.deleteIfExists(getKeyFile(keyId)); + Files.deleteIfExists(getPrivateKeyFile(keyId)); + Files.deleteIfExists(getPublicKeyFile(keyId)); Files.deleteIfExists(getMetadataFile(keyId)); + Files.deleteIfExists(getLegacyKeyFile(keyId)); keyCache.remove(keyId); metadataCache.remove(keyId); LOG.info("Deleted key: {}", keyId); @@ -290,6 +329,83 @@ public class FileBasedKeyLifecycleManager implements KeyLifecycleManager { } } + private KeyPair readStandardKeyPair(String keyId, Path privateKeyFile, Path publicKeyFile) throws Exception { + String privateJson = Files.readString(privateKeyFile, StandardCharsets.UTF_8); + KeyFileData privateData = objectMapper.readValue(privateJson, KeyFileData.class); + byte[] privateKeyBytes = Base64.getDecoder().decode(privateData.key()); + PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKeyBytes); + + String publicJson = Files.readString(publicKeyFile, StandardCharsets.UTF_8); + KeyFileData publicData = objectMapper.readValue(publicJson, KeyFileData.class); + byte[] publicKeyBytes = Base64.getDecoder().decode(publicData.key()); + X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKeyBytes); + + String algorithm = privateData.algorithm(); + String algorithmName = getAlgorithmName(algorithm); + String provider = determineProvider(algorithm); + + KeyFactory keyFactory; + if (provider != null) { + keyFactory = KeyFactory.getInstance(algorithmName, provider); + } else { + keyFactory = KeyFactory.getInstance(algorithmName); + } + + PrivateKey privateKey = keyFactory.generatePrivate(privateSpec); + PublicKey publicKey = keyFactory.generatePublic(publicSpec); + return new KeyPair(publicKey, privateKey); + } + + /** + * Migrates a legacy Java-serialized key file to the new PKCS#8/X.509 JSON format. + */ + @SuppressWarnings("java:S4508") + private KeyPair migrateLegacyKey(String keyId) throws Exception { + LOG.info("Migrating legacy key format to PKCS#8/X.509 for keyId: {}", keyId); + Path legacyKeyFile = getLegacyKeyFile(keyId); + + KeyPair keyPair; + try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(Files.newInputStream(legacyKeyFile)))) { + keyPair = (KeyPair) ois.readObject(); + } + + // Read or migrate metadata + KeyMetadata metadata = getKeyMetadata(keyId); + if (metadata == null) { + metadata = new KeyMetadata(keyId, "UNKNOWN"); + metadata.setDescription("Migrated from legacy format"); + } + + // Re-store in the new format (this also removes the legacy .key file) + storeKey(keyId, keyPair, metadata); + LOG.info("Successfully migrated key to PKCS#8/X.509 format: {}", keyId); + return keyPair; + } + + /** + * Migrates a legacy Java-serialized metadata file to JSON format. + */ + @SuppressWarnings("java:S4508") + private KeyMetadata migrateLegacyMetadata(String keyId) throws Exception { + LOG.info("Migrating legacy metadata format to JSON for keyId: {}", keyId); + Path metadataFile = getMetadataFile(keyId); + + KeyMetadata metadata; + try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(Files.newInputStream(metadataFile)))) { + metadata = (KeyMetadata) ois.readObject(); + } + + // Re-store in JSON format + MetadataFileData metadataData = MetadataFileData.fromKeyMetadata(metadata); + Files.writeString(metadataFile, objectMapper.writeValueAsString(metadataData), + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + metadataCache.put(keyId, metadata); + LOG.info("Successfully migrated metadata to JSON format: {}", keyId); + return metadata; + } + private void loadExistingKeys() { try (Stream<Path> files = Files.list(keyDirectory)) { files.filter(path -> path.toString().endsWith(".metadata")) @@ -309,14 +425,22 @@ public class FileBasedKeyLifecycleManager implements KeyLifecycleManager { } } - private Path getKeyFile(String keyId) { - return keyDirectory.resolve(keyId + ".key"); + private Path getPrivateKeyFile(String keyId) { + return keyDirectory.resolve(keyId + ".private.json"); + } + + private Path getPublicKeyFile(String keyId) { + return keyDirectory.resolve(keyId + ".public.json"); } private Path getMetadataFile(String keyId) { return keyDirectory.resolve(keyId + ".metadata"); } + private Path getLegacyKeyFile(String keyId) { + return keyDirectory.resolve(keyId + ".key"); + } + private String determineProvider(String algorithm) { try { PQCSignatureAlgorithms sigAlg = PQCSignatureAlgorithms.valueOf(algorithm); @@ -402,4 +526,75 @@ public class FileBasedKeyLifecycleManager implements KeyLifecycleManager { // For PQC algorithms, key size is usually determined by parameter specs return 256; } + + /** + * JSON structure for storing key data (private or public) in files. + */ + record KeyFileData( + @JsonProperty("key") String key, + @JsonProperty("format") String format, + @JsonProperty("algorithm") String algorithm) { + + @JsonCreator + KeyFileData { + } + } + + /** + * JSON structure for storing key metadata in files. + */ + static final class MetadataFileData { + @JsonProperty("keyId") + String keyId; + @JsonProperty("algorithm") + String algorithm; + @JsonProperty("createdAt") + String createdAt; + @JsonProperty("lastUsedAt") + String lastUsedAt; + @JsonProperty("expiresAt") + String expiresAt; + @JsonProperty("nextRotationAt") + String nextRotationAt; + @JsonProperty("usageCount") + long usageCount; + @JsonProperty("status") + String status; + @JsonProperty("description") + String description; + + MetadataFileData() { + } + + static MetadataFileData fromKeyMetadata(KeyMetadata metadata) { + MetadataFileData data = new MetadataFileData(); + data.keyId = metadata.getKeyId(); + data.algorithm = metadata.getAlgorithm(); + data.createdAt = metadata.getCreatedAt().toString(); + data.lastUsedAt = metadata.getLastUsedAt() != null ? metadata.getLastUsedAt().toString() : null; + data.expiresAt = metadata.getExpiresAt() != null ? metadata.getExpiresAt().toString() : null; + data.nextRotationAt = metadata.getNextRotationAt() != null ? metadata.getNextRotationAt().toString() : null; + data.usageCount = metadata.getUsageCount(); + data.status = metadata.getStatus().name(); + data.description = metadata.getDescription(); + return data; + } + + KeyMetadata toKeyMetadata() { + KeyMetadata metadata = new KeyMetadata(keyId, algorithm, Instant.parse(createdAt)); + if (lastUsedAt != null) { + metadata.setLastUsedAt(Instant.parse(lastUsedAt)); + } + if (expiresAt != null) { + metadata.setExpiresAt(Instant.parse(expiresAt)); + } + if (nextRotationAt != null) { + metadata.setNextRotationAt(Instant.parse(nextRotationAt)); + } + metadata.setUsageCount(usageCount); + metadata.setStatus(KeyMetadata.KeyStatus.valueOf(status)); + metadata.setDescription(description); + return metadata; + } + } } diff --git a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadata.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadata.java index fe7643ac232a..281b4c6b5a55 100644 --- a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadata.java +++ b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadata.java @@ -45,9 +45,13 @@ public class KeyMetadata implements Serializable { } public KeyMetadata(String keyId, String algorithm) { + this(keyId, algorithm, Instant.now()); + } + + public KeyMetadata(String keyId, String algorithm, Instant createdAt) { this.keyId = keyId; this.algorithm = algorithm; - this.createdAt = Instant.now(); + this.createdAt = createdAt; this.lastUsedAt = createdAt; this.usageCount = 0; this.status = KeyStatus.ACTIVE; @@ -69,6 +73,10 @@ public class KeyMetadata implements Serializable { return lastUsedAt; } + public void setLastUsedAt(Instant lastUsedAt) { + this.lastUsedAt = lastUsedAt; + } + public void updateLastUsed() { this.lastUsedAt = Instant.now(); this.usageCount++; @@ -94,6 +102,10 @@ public class KeyMetadata implements Serializable { return usageCount; } + public void setUsageCount(long usageCount) { + this.usageCount = usageCount; + } + public KeyStatus getStatus() { return status; }
