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;
     }

Reply via email to