VYPR
High severity7.8NVD Advisory· Published Apr 27, 2026· Updated Apr 28, 2026

CVE-2026-40048

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.

PackageAffected versionsPatched versions
org.apache.camel:camel-pqcMaven
< 4.18.24.18.2

Affected products

2
  • Apache/Camel2 versions
    cpe:2.3:a:apache:camel:*:*:*:*:*:*:*:*+ 1 more
    • cpe:2.3:a:apache:camel:*:*:*:*:*:*:*:*range: >=4.18.0,<4.18.2
    • cpe:2.3:a:apache:camel:4.19.0:*:*:*:*:*:*:*

Patches

2
5f87a86f4e33

CAMEL-23200: Replace Java serialization with PKCS#8/X.509 in FileBasedKeyLifecycleManager

https://github.com/apache/camelAndrea CosentinoApr 8, 2026via ghsa
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;
         }
    
5bdd0f1d3289

CAMEL-23200 - Camel-PQC: Replace Java serialization with PKCS#8/X.509 in FileBasedKeyLifecycleManager (#22034)

https://github.com/apache/camelAndrea CosentinoMar 16, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.