CVE-2026-40048
Description
The Camel-PQC FileBasedKeyLifecycleManager class deserializes the contents of <keyId>.key files in the configured key directory using java.io.ObjectInputStream without applying any ObjectInputFilter or class-loading restrictions. The cast to java.security.KeyPair is evaluated only after readObject() has already returned, so any readObject() side effects in the deserialized object run before the type check. An attacker who can write to the key directory used by a Camel application — for example through a path traversal into the directory, misconfigured filesystem permissions on the volume where keys are stored, a compromised key provisioning pipeline, or a symlink attack — can place a crafted serialized Java object that, when deserialized during normal key lifecycle operations, results in arbitrary code execution in the context of the application.
This issue affects Apache Camel: from 4.19.0 before 4.20.0, from 4.18.0 before 4.18.2.
Users are recommended to upgrade to version 4.20.0, which fixes the issue by replacing java.io.ObjectInputStream-based key and metadata storage with standard PKCS#8 (private key) / X.509 SubjectPublicKeyInfo (public key) Base64 JSON encoding. For users on the 4.18.x LTS releases stream, upgrade to 4.18.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.camel:camel-pqcMaven | < 4.18.2 | 4.18.2 |
Affected products
2Patches
25f87a86f4e33CAMEL-23200: Replace Java serialization with PKCS#8/X.509 in FileBasedKeyLifecycleManager
2 files changed · +245 −38
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java+232 −37 modified@@ -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 KeyPair rotateKey(String oldKeyId, String newKeyId, String algorithm) thr @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 KeyPair getKey(String keyId) throws Exception { 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 KeyMetadata getKeyMetadata(String keyId) throws Exception { 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 void revokeKey(String keyId, String reason) throws Exception { } } + 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 @@ private void loadExistingKeys() { } } - 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 @@ private int getDefaultKeySize(String algorithm) { // 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; + } + } }
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadata.java+13 −1 modified@@ -45,9 +45,13 @@ public enum KeyStatus { } 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 Instant getLastUsedAt() { return lastUsedAt; } + public void setLastUsedAt(Instant lastUsedAt) { + this.lastUsedAt = lastUsedAt; + } + public void updateLastUsed() { this.lastUsedAt = Instant.now(); this.usageCount++; @@ -94,6 +102,10 @@ public long getUsageCount() { return usageCount; } + public void setUsageCount(long usageCount) { + this.usageCount = usageCount; + } + public KeyStatus getStatus() { return status; }
5bdd0f1d3289CAMEL-23200 - Camel-PQC: Replace Java serialization with PKCS#8/X.509 in FileBasedKeyLifecycleManager (#22034)
2 files changed · +245 −38
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java+232 −37 modified@@ -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 KeyPair rotateKey(String oldKeyId, String newKeyId, String algorithm) thr @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 KeyPair getKey(String keyId) throws Exception { 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 KeyMetadata getKeyMetadata(String keyId) throws Exception { 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 void revokeKey(String keyId, String reason) throws Exception { } } + 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 @@ private void loadExistingKeys() { } } - 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 @@ private int getDefaultKeySize(String algorithm) { // 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; + } + } }
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadata.java+13 −1 modified@@ -45,9 +45,13 @@ public enum KeyStatus { } 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 Instant getLastUsedAt() { return lastUsedAt; } + public void setLastUsedAt(Instant lastUsedAt) { + this.lastUsedAt = lastUsedAt; + } + public void updateLastUsed() { this.lastUsedAt = Instant.now(); this.usageCount++; @@ -94,6 +102,10 @@ public long getUsageCount() { return usageCount; } + public void setUsageCount(long usageCount) { + this.usageCount = usageCount; + } + public KeyStatus getStatus() { return status; }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- www.openwall.com/lists/oss-security/2026/04/26/6nvdMailing ListThird Party AdvisoryWEB
- camel.apache.org/security/CVE-2026-40048.htmlnvdVendor AdvisoryWEB
- github.com/advisories/GHSA-v3vg-332r-mw99ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-40048ghsaADVISORY
- github.com/apache/camel/commit/5bdd0f1d3289dfa78116deec6c81083708bf432dghsaWEB
- github.com/apache/camel/commit/5f87a86f4e337efc59248d278c6a5650e73b3b7cghsaWEB
- github.com/apache/camel/pull/22034ghsaWEB
- github.com/apache/camel/pull/22495ghsaWEB
- issues.apache.org/jira/browse/CAMEL-23200ghsaWEB
News mentions
0No linked articles in our index yet.