This is an automated email from the ASF dual-hosted git repository.
acosentino pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 5f87a86f4e33 CAMEL-23200: Replace Java serialization with PKCS#8/X.509
in FileBasedKeyLifecycleManager
5f87a86f4e33 is described below
commit 5f87a86f4e337efc59248d278c6a5650e73b3b7c
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Apr 8 20:35:30 2026 +0200
CAMEL-23200: Replace Java serialization with PKCS#8/X.509 in
FileBasedKeyLifecycleManager
Backport to camel-4.18.x. Fixes CWE-502 by replacing ObjectInputStream
deserialization with standard PKCS#8/X.509 (Base64 JSON) for keys and JSON
for metadata. Legacy Java-serialized files are automatically migrated on
first access. Aligns with the encoding used by
AwsSecretsManagerKeyLifecycleManager and HashicorpVaultKeyLifecycleManager.
Closes #22495
---
.../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;
}