This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new feea08e7847f CAMEL-23726: Use JSON instead of Java serialization for 
key metadata in AWS and HashiCorp Vault lifecycle managers (#23912)
feea08e7847f is described below

commit feea08e7847f35dc0e177652b0b02bd45f6c1b4f
Author: Andrea Cosentino <[email protected]>
AuthorDate: Thu Jun 11 08:51:34 2026 +0200

    CAMEL-23726: Use JSON instead of Java serialization for key metadata in AWS 
and HashiCorp Vault lifecycle managers (#23912)
    
    Co-Authored-By: Claude Fable 5 <[email protected]>
---
 .../AwsSecretsManagerKeyLifecycleManager.java      |  79 +++--------
 .../lifecycle/FileBasedKeyLifecycleManager.java    |  24 +++-
 .../HashicorpVaultKeyLifecycleManager.java         |  28 ++--
 .../component/pqc/lifecycle/KeyMetadataCodec.java  | 144 +++++++++++++++++++++
 .../camel/component/pqc/PQCKeyLifecycleTest.java   |  55 ++++++++
 .../pqc/lifecycle/KeyMetadataCodecTest.java        | 122 +++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc    |  11 ++
 .../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc    |  11 ++
 8 files changed, 395 insertions(+), 79 deletions(-)

diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/AwsSecretsManagerKeyLifecycleManager.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/AwsSecretsManagerKeyLifecycleManager.java
index 6aba8fb4122a..267625497f12 100644
--- 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/AwsSecretsManagerKeyLifecycleManager.java
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/AwsSecretsManagerKeyLifecycleManager.java
@@ -17,9 +17,7 @@
 package org.apache.camel.component.pqc.lifecycle;
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.net.URI;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
@@ -34,6 +32,7 @@ import java.util.Date;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.camel.component.pqc.PQCKeyEncapsulationAlgorithms;
 import org.apache.camel.component.pqc.PQCSignatureAlgorithms;
@@ -264,7 +263,6 @@ public class AwsSecretsManagerKeyLifecycleManager 
implements KeyLifecycleManager
         byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); // 
X.509/SubjectPublicKeyInfo format
         String privateKeyBase64 = 
Base64.getEncoder().encodeToString(privateKeyBytes);
         String publicKeyBase64 = 
Base64.getEncoder().encodeToString(publicKeyBytes);
-        String metadataBase64 = serializeMetadata(metadata);
 
         // Store private key separately (strict IAM policy recommended in 
production)
         String privateSecretName = getSecretName(keyId, "private");
@@ -284,12 +282,9 @@ public class AwsSecretsManagerKeyLifecycleManager 
implements KeyLifecycleManager
 
         createOrUpdateSecret(publicSecretName, publicSecretValue, "PQC Public 
Key: " + keyId);
 
-        // Store metadata separately
+        // Store metadata separately as JSON (see KeyMetadataCodec)
         String metadataSecretName = getSecretName(keyId, "metadata");
-        String metadataSecretValue = objectMapper.writeValueAsString(new 
MetadataData(
-                metadataBase64,
-                keyId,
-                metadata.getAlgorithm()));
+        String metadataSecretValue = KeyMetadataCodec.toJson(metadata);
 
         createOrUpdateSecret(metadataSecretName, metadataSecretValue, "PQC Key 
Metadata: " + keyId);
 
@@ -347,8 +342,16 @@ public class AwsSecretsManagerKeyLifecycleManager 
implements KeyLifecycleManager
 
         try {
             GetSecretValueResponse response = getSecret(metadataSecretName);
-            MetadataData metadataData = 
objectMapper.readValue(response.secretString(), MetadataData.class);
-            KeyMetadata metadata = 
deserializeMetadata(metadataData.getMetadata());
+            String secret = response.secretString();
+
+            KeyMetadata metadata;
+            JsonNode node = objectMapper.readTree(secret);
+            if (node.has("metadata")) {
+                // Legacy format: a Base64-encoded, Java-serialized 
KeyMetadata wrapped in an envelope
+                metadata = deserializeMetadata(node.get("metadata").asText());
+            } else {
+                metadata = KeyMetadataCodec.fromJson(secret);
+            }
 
             // Cache it
             metadataCache.put(keyId, metadata);
@@ -525,18 +528,16 @@ public class AwsSecretsManagerKeyLifecycleManager 
implements KeyLifecycleManager
         }
     }
 
-    private String serializeMetadata(KeyMetadata metadata) throws Exception {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
-            oos.writeObject(metadata);
-        }
-        return Base64.getEncoder().encodeToString(baos.toByteArray());
-    }
-
+    /**
+     * Reads a legacy (pre-JSON) Base64-encoded, Java-serialized {@link 
KeyMetadata}. The deserialization is constrained
+     * to the expected types via {@link KeyMetadataCodec#METADATA_FILTER}. 
Retained for backward compatibility with
+     * metadata written by older versions; new metadata is stored as JSON.
+     */
     private KeyMetadata deserializeMetadata(String base64) throws Exception {
         byte[] data = Base64.getDecoder().decode(base64);
         ByteArrayInputStream bais = new ByteArrayInputStream(data);
         try (ObjectInputStream ois = new ObjectInputStream(bais)) {
+            ois.setObjectInputFilter(KeyMetadataCodec.METADATA_FILTER);
             return (KeyMetadata) ois.readObject();
         }
     }
@@ -675,46 +676,4 @@ public class AwsSecretsManagerKeyLifecycleManager 
implements KeyLifecycleManager
             this.algorithm = algorithm;
         }
     }
-
-    /**
-     * Helper class for storing metadata in JSON format
-     */
-    private static class MetadataData {
-        private String metadata;
-        private String keyId;
-        private String algorithm;
-
-        public MetadataData() {
-        }
-
-        public MetadataData(String metadata, String keyId, String algorithm) {
-            this.metadata = metadata;
-            this.keyId = keyId;
-            this.algorithm = algorithm;
-        }
-
-        public String getMetadata() {
-            return metadata;
-        }
-
-        public void setMetadata(String metadata) {
-            this.metadata = metadata;
-        }
-
-        public String getKeyId() {
-            return keyId;
-        }
-
-        public void setKeyId(String keyId) {
-            this.keyId = keyId;
-        }
-
-        public String getAlgorithm() {
-            return algorithm;
-        }
-
-        public void setAlgorithm(String algorithm) {
-            this.algorithm = algorithm;
-        }
-    }
 }
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 46b44bde4415..543b0088e37d 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
@@ -248,10 +248,10 @@ public class FileBasedKeyLifecycleManager implements 
KeyLifecycleManager {
             return null;
         }
 
-        String content = Files.readString(metadataFile, 
StandardCharsets.UTF_8);
-
-        // Detect format: JSON starts with '{', legacy Java serialization 
starts with binary
-        if (content.trim().startsWith("{")) {
+        // Detect the format from the raw bytes: a JSON document starts with 
'{', whereas a legacy
+        // Java-serialized file is binary (and not valid UTF-8), so it must 
not be read as a String first.
+        byte[] content = Files.readAllBytes(metadataFile);
+        if (isJsonContent(content)) {
             MetadataFileData data = objectMapper.readValue(content, 
MetadataFileData.class);
             KeyMetadata metadata = data.toKeyMetadata();
             metadataCache.put(keyId, metadata);
@@ -369,6 +369,7 @@ public class FileBasedKeyLifecycleManager implements 
KeyLifecycleManager {
 
         KeyPair keyPair;
         try (ObjectInputStream ois = new ObjectInputStream(new 
BufferedInputStream(Files.newInputStream(legacyKeyFile)))) {
+            ois.setObjectInputFilter(KeyMetadataCodec.KEY_PAIR_FILTER);
             keyPair = (KeyPair) ois.readObject();
         }
 
@@ -395,6 +396,7 @@ public class FileBasedKeyLifecycleManager implements 
KeyLifecycleManager {
 
         KeyMetadata metadata;
         try (ObjectInputStream ois = new ObjectInputStream(new 
BufferedInputStream(Files.newInputStream(metadataFile)))) {
+            ois.setObjectInputFilter(KeyMetadataCodec.METADATA_FILTER);
             metadata = (KeyMetadata) ois.readObject();
         }
 
@@ -444,6 +446,20 @@ public class FileBasedKeyLifecycleManager implements 
KeyLifecycleManager {
         return keyDirectory.resolve(keyId + ".key");
     }
 
+    /**
+     * Detects whether the given file content is a JSON document (the current 
format) by inspecting the first
+     * non-whitespace byte, without decoding the bytes as text (a legacy 
Java-serialized file is binary).
+     */
+    private static boolean isJsonContent(byte[] content) {
+        for (byte b : content) {
+            if (b == ' ' || b == '\t' || b == '\n' || b == '\r') {
+                continue;
+            }
+            return b == '{';
+        }
+        return false;
+    }
+
     private String determineProvider(String algorithm) {
         try {
             PQCSignatureAlgorithms sigAlg = 
PQCSignatureAlgorithms.valueOf(algorithm);
diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/HashicorpVaultKeyLifecycleManager.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/HashicorpVaultKeyLifecycleManager.java
index 5d80fc1373a4..b9a3dade2430 100644
--- 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/HashicorpVaultKeyLifecycleManager.java
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/HashicorpVaultKeyLifecycleManager.java
@@ -17,9 +17,7 @@
 package org.apache.camel.component.pqc.lifecycle;
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.PrivateKey;
@@ -280,7 +278,7 @@ public class HashicorpVaultKeyLifecycleManager implements 
KeyLifecycleManager {
         byte[] publicKeyBytes = keyPair.getPublic().getEncoded(); // 
X.509/SubjectPublicKeyInfo format
         String privateKeyBase64 = 
Base64.getEncoder().encodeToString(privateKeyBytes);
         String publicKeyBase64 = 
Base64.getEncoder().encodeToString(publicKeyBytes);
-        String metadataBase64 = serializeMetadata(metadata);
+        String metadataJson = KeyMetadataCodec.toJson(metadata);
 
         VaultKeyValueOperations keyValue = 
vaultTemplate.opsForKeyValue(secretsEngine,
                 VaultKeyValueOperationsSupport.KeyValueBackend.versioned());
@@ -299,9 +297,9 @@ public class HashicorpVaultKeyLifecycleManager implements 
KeyLifecycleManager {
         publicKeyData.put("algorithm", metadata.getAlgorithm());
         keyValue.put(getKeyPath(keyId) + "/public", publicKeyData);
 
-        // Store metadata separately
+        // Store metadata separately as JSON (see KeyMetadataCodec)
         Map<String, Object> metadataData = new HashMap<>();
-        metadataData.put("metadata", metadataBase64);
+        metadataData.put("metadata", metadataJson);
         metadataData.put("keyId", keyId);
         metadataData.put("algorithm", metadata.getAlgorithm());
         keyValue.put(getKeyPath(keyId) + "/metadata", metadataData);
@@ -393,8 +391,10 @@ public class HashicorpVaultKeyLifecycleManager implements 
KeyLifecycleManager {
             return null;
         }
 
-        String metadataBase64 = (String) secretData.get("metadata");
-        KeyMetadata metadata = deserializeMetadata(metadataBase64);
+        String storedMetadata = (String) secretData.get("metadata");
+        KeyMetadata metadata = KeyMetadataCodec.isJson(storedMetadata)
+                ? KeyMetadataCodec.fromJson(storedMetadata)
+                : deserializeMetadata(storedMetadata);
 
         // Cache it
         metadataCache.put(keyId, metadata);
@@ -551,18 +551,16 @@ public class HashicorpVaultKeyLifecycleManager implements 
KeyLifecycleManager {
         }
     }
 
-    private String serializeMetadata(KeyMetadata metadata) throws Exception {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
-            oos.writeObject(metadata);
-        }
-        return Base64.getEncoder().encodeToString(baos.toByteArray());
-    }
-
+    /**
+     * Reads a legacy (pre-JSON) Base64-encoded, Java-serialized {@link 
KeyMetadata}. The deserialization is constrained
+     * to the expected types via {@link KeyMetadataCodec#METADATA_FILTER}. 
Retained for backward compatibility with
+     * metadata written by older versions; new metadata is stored as JSON.
+     */
     private KeyMetadata deserializeMetadata(String base64) throws Exception {
         byte[] data = Base64.getDecoder().decode(base64);
         ByteArrayInputStream bais = new ByteArrayInputStream(data);
         try (ObjectInputStream ois = new ObjectInputStream(bais)) {
+            ois.setObjectInputFilter(KeyMetadataCodec.METADATA_FILTER);
             return (KeyMetadata) ois.readObject();
         }
     }
diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadataCodec.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadataCodec.java
new file mode 100644
index 000000000000..309f2b03d5e7
--- /dev/null
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/KeyMetadataCodec.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.pqc.lifecycle;
+
+import java.io.ObjectInputFilter;
+import java.time.Instant;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Shared helpers for persisting {@link KeyMetadata} as JSON instead of Java 
serialization, and for safely reading
+ * values written by older versions that used Java serialization.
+ * <p>
+ * {@link AwsSecretsManagerKeyLifecycleManager} and {@link 
HashicorpVaultKeyLifecycleManager} store key metadata as
+ * JSON, consistent with {@link FileBasedKeyLifecycleManager}. Values written 
by older versions (a Base64-encoded,
+ * Java-serialized {@link KeyMetadata}) are still read for backward 
compatibility, but the deserialization is
+ * constrained to the expected types through an {@link ObjectInputFilter}.
+ */
+final class KeyMetadataCodec {
+
+    private static final String METADATA_PATTERN
+            = 
"maxdepth=20;java.lang.*;java.time.**;org.apache.camel.component.pqc.lifecycle.*;!*";
+
+    private static final String KEY_PAIR_PATTERN
+            = 
"maxdepth=20;java.lang.*;java.util.*;java.time.**;java.security.**;javax.crypto.**;"
+              + 
"org.apache.camel.component.pqc.lifecycle.*;org.bouncycastle.**;!*";
+
+    /**
+     * Allow-list filter for reading a legacy Java-serialized {@link 
KeyMetadata}. Only the JDK types that make up a
+     * {@code KeyMetadata} (its {@link Instant} timestamps and {@link 
KeyMetadata.KeyStatus} enum) and the metadata
+     * class itself are permitted; everything else is rejected.
+     */
+    static final ObjectInputFilter METADATA_FILTER = 
ObjectInputFilter.Config.createFilter(METADATA_PATTERN);
+
+    /**
+     * Allow-list filter for reading a legacy Java-serialized {@link 
java.security.KeyPair}. In addition to the metadata
+     * types it permits the JDK and Bouncy Castle key classes required to 
reconstruct a key pair; everything else is
+     * rejected.
+     */
+    static final ObjectInputFilter KEY_PAIR_FILTER = 
ObjectInputFilter.Config.createFilter(KEY_PAIR_PATTERN);
+
+    private static final ObjectMapper MAPPER
+            = new 
ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
+    private KeyMetadataCodec() {
+    }
+
+    /**
+     * Whether the stored value is the JSON representation (new format) rather 
than a Base64-encoded, Java-serialized
+     * value (legacy format).
+     */
+    static boolean isJson(String value) {
+        return value != null && value.stripLeading().startsWith("{");
+    }
+
+    /**
+     * Serializes the given metadata to its JSON representation.
+     */
+    static String toJson(KeyMetadata metadata) throws Exception {
+        return MAPPER.writeValueAsString(Data.from(metadata));
+    }
+
+    /**
+     * Parses metadata from its JSON representation.
+     */
+    static KeyMetadata fromJson(String json) throws Exception {
+        return MAPPER.readValue(json, Data.class).toKeyMetadata();
+    }
+
+    /**
+     * JSON structure mirroring {@link KeyMetadata}, matching the 
representation already used by
+     * {@link FileBasedKeyLifecycleManager}.
+     */
+    static final class Data {
+        @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;
+
+        Data() {
+        }
+
+        static Data from(KeyMetadata metadata) {
+            Data data = new Data();
+            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/test/java/org/apache/camel/component/pqc/PQCKeyLifecycleTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyLifecycleTest.java
index 842d887f3c82..c52302d4fb93 100644
--- 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyLifecycleTest.java
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCKeyLifecycleTest.java
@@ -16,10 +16,13 @@
  */
 package org.apache.camel.component.pqc;
 
+import java.io.ObjectOutputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.security.KeyPair;
 import java.security.Security;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.List;
 
@@ -346,6 +349,58 @@ public class PQCKeyLifecycleTest {
         assertEquals(0, age);
     }
 
+    @Test
+    void testLegacyKeyPairMigration() throws Exception {
+        // Seed a real PQC key pair via the manager
+        FileBasedKeyLifecycleManager seedManager = new 
FileBasedKeyLifecycleManager(tempDir.toString());
+        KeyPair original = seedManager.generateKeyPair("DILITHIUM", 
"seed-key", DilithiumParameterSpec.dilithium2);
+
+        // Write it out in the legacy Java-serialized ".key" format under a 
fresh keyId
+        Path legacyKeyFile = tempDir.resolve("legacy-key.key");
+        try (ObjectOutputStream oos = new 
ObjectOutputStream(Files.newOutputStream(legacyKeyFile))) {
+            oos.writeObject(original);
+        }
+
+        // A fresh manager must transparently migrate the legacy key (with the 
deserialization filter applied)
+        keyManager = new FileBasedKeyLifecycleManager(tempDir.toString());
+        KeyPair migrated = keyManager.getKey("legacy-key");
+
+        assertNotNull(migrated);
+        assertArrayEquals(original.getPublic().getEncoded(), 
migrated.getPublic().getEncoded());
+        assertArrayEquals(original.getPrivate().getEncoded(), 
migrated.getPrivate().getEncoded());
+
+        // Legacy file is replaced by the PKCS#8/X.509 JSON format
+        assertFalse(Files.exists(legacyKeyFile));
+        assertTrue(Files.exists(tempDir.resolve("legacy-key.private.json")));
+        assertTrue(Files.exists(tempDir.resolve("legacy-key.public.json")));
+    }
+
+    @Test
+    void testLegacyMetadataMigration() throws Exception {
+        // Write a legacy Java-serialized ".metadata" file
+        KeyMetadata original = new KeyMetadata("legacy-meta", "DILITHIUM", 
Instant.parse("2026-01-02T03:04:05Z"));
+        original.setStatus(KeyMetadata.KeyStatus.EXPIRED);
+        original.setUsageCount(11);
+        Path metadataFile = tempDir.resolve("legacy-meta.metadata");
+        try (ObjectOutputStream oos = new 
ObjectOutputStream(Files.newOutputStream(metadataFile))) {
+            oos.writeObject(original);
+        }
+
+        // A fresh manager migrates legacy metadata to JSON (with the 
deserialization filter applied)
+        keyManager = new FileBasedKeyLifecycleManager(tempDir.toString());
+        KeyMetadata migrated = keyManager.getKeyMetadata("legacy-meta");
+
+        assertNotNull(migrated);
+        assertEquals("legacy-meta", migrated.getKeyId());
+        assertEquals("DILITHIUM", migrated.getAlgorithm());
+        assertEquals(KeyMetadata.KeyStatus.EXPIRED, migrated.getStatus());
+        assertEquals(11, migrated.getUsageCount());
+
+        // The file is now stored as JSON
+        String content = Files.readString(metadataFile);
+        assertTrue(content.stripLeading().startsWith("{"));
+    }
+
     @Test
     void testMultipleKeyFormats() throws Exception {
         keyManager = new FileBasedKeyLifecycleManager(tempDir.toString());
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/KeyMetadataCodecTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/KeyMetadataCodecTest.java
new file mode 100644
index 000000000000..a939638e5ba5
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/KeyMetadataCodecTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.pqc.lifecycle;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InvalidClassException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.time.Instant;
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class KeyMetadataCodecTest {
+
+    @Test
+    void jsonRoundTripPreservesAllFields() throws Exception {
+        KeyMetadata original = new KeyMetadata("key-1", "DILITHIUM", 
Instant.parse("2026-01-01T00:00:00Z"));
+        original.setLastUsedAt(Instant.parse("2026-02-01T10:15:30Z"));
+        original.setExpiresAt(Instant.parse("2027-01-01T00:00:00Z"));
+        original.setNextRotationAt(Instant.parse("2026-06-01T00:00:00Z"));
+        original.setUsageCount(42);
+        original.setStatus(KeyMetadata.KeyStatus.DEPRECATED);
+        original.setDescription("rotated");
+
+        String json = KeyMetadataCodec.toJson(original);
+        assertTrue(KeyMetadataCodec.isJson(json));
+
+        KeyMetadata restored = KeyMetadataCodec.fromJson(json);
+        assertEquals(original.getKeyId(), restored.getKeyId());
+        assertEquals(original.getAlgorithm(), restored.getAlgorithm());
+        assertEquals(original.getCreatedAt(), restored.getCreatedAt());
+        assertEquals(original.getLastUsedAt(), restored.getLastUsedAt());
+        assertEquals(original.getExpiresAt(), restored.getExpiresAt());
+        assertEquals(original.getNextRotationAt(), 
restored.getNextRotationAt());
+        assertEquals(original.getUsageCount(), restored.getUsageCount());
+        assertEquals(original.getStatus(), restored.getStatus());
+        assertEquals(original.getDescription(), restored.getDescription());
+    }
+
+    @Test
+    void jsonRoundTripWithMinimalMetadata() throws Exception {
+        KeyMetadata original = new KeyMetadata("key-2", "FALCON");
+
+        KeyMetadata restored = 
KeyMetadataCodec.fromJson(KeyMetadataCodec.toJson(original));
+        assertEquals("key-2", restored.getKeyId());
+        assertEquals("FALCON", restored.getAlgorithm());
+        assertEquals(KeyMetadata.KeyStatus.ACTIVE, restored.getStatus());
+        assertEquals(0, restored.getUsageCount());
+    }
+
+    @Test
+    void isJsonDistinguishesJsonFromLegacyBase64() {
+        assertTrue(KeyMetadataCodec.isJson("{\"keyId\":\"x\"}"));
+        assertTrue(KeyMetadataCodec.isJson("   \n {\"keyId\":\"x\"}"));
+        // Base64 of a Java-serialized object never starts with '{'
+        assertFalse(KeyMetadataCodec.isJson("rO0ABXNyAB1vcmcuYXBhY2hl"));
+        assertFalse(KeyMetadataCodec.isJson(null));
+        assertFalse(KeyMetadataCodec.isJson(""));
+    }
+
+    @Test
+    void metadataFilterAllowsLegacySerializedKeyMetadata() throws Exception {
+        KeyMetadata original = new KeyMetadata("legacy", "DILITHIUM", 
Instant.parse("2026-03-03T03:03:03Z"));
+        original.setExpiresAt(Instant.parse("2027-03-03T03:03:03Z"));
+        original.setStatus(KeyMetadata.KeyStatus.REVOKED);
+        original.setUsageCount(7);
+
+        byte[] serialized = javaSerialize(original);
+
+        KeyMetadata restored;
+        try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(serialized))) {
+            ois.setObjectInputFilter(KeyMetadataCodec.METADATA_FILTER);
+            restored = (KeyMetadata) ois.readObject();
+        }
+
+        assertEquals("legacy", restored.getKeyId());
+        assertEquals(KeyMetadata.KeyStatus.REVOKED, restored.getStatus());
+        assertEquals(original.getExpiresAt(), restored.getExpiresAt());
+        assertEquals(7, restored.getUsageCount());
+    }
+
+    @Test
+    void metadataFilterRejectsUnexpectedType() throws Exception {
+        ArrayList<String> notMetadata = new ArrayList<>();
+        notMetadata.add("payload");
+        byte[] serialized = javaSerialize(notMetadata);
+
+        try (ObjectInputStream ois = new ObjectInputStream(new 
ByteArrayInputStream(serialized))) {
+            ois.setObjectInputFilter(KeyMetadataCodec.METADATA_FILTER);
+            assertThrows(InvalidClassException.class, ois::readObject);
+        }
+    }
+
+    private static byte[] javaSerialize(Object o) throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+            oos.writeObject(o);
+        }
+        return baos.toByteArray();
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index efec0a0beafd..fcf43e8de478 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -1625,3 +1625,14 @@ non-`Camel`-prefixed application headers and map them to 
the corresponding
 the `salesforce:` `to`. As defence-in-depth, strip inbound Camel-internal 
headers
 arriving from untrusted producers with `removeHeaders("CamelSalesforce*")` (or 
the
 broader `removeHeaders("Camel*")`) before the producer.
+
+=== camel-pqc
+
+The key lifecycle managers now store key metadata as JSON instead of using 
Java serialization.
+`AwsSecretsManagerKeyLifecycleManager` and `HashicorpVaultKeyLifecycleManager` 
previously stored the
+`KeyMetadata` as a Base64-encoded, Java-serialized value; they now store it as 
JSON, consistent with
+`FileBasedKeyLifecycleManager`. Metadata written by previous versions is still 
read transparently and
+is migrated to JSON the next time the metadata is updated.
+
+Because older versions cannot read the new JSON metadata, downgrading after 
new key metadata has been
+written is not supported.
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index fa6b65448c7b..f4dab43820a5 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -2183,3 +2183,14 @@ non-`Camel`-prefixed application headers and map them to 
the corresponding
 the `salesforce:` `to`. As defence-in-depth, strip inbound Camel-internal 
headers
 arriving from untrusted producers with `removeHeaders("CamelSalesforce*")` (or 
the
 broader `removeHeaders("Camel*")`) before the producer.
+
+=== camel-pqc
+
+The key lifecycle managers now store key metadata as JSON instead of using 
Java serialization.
+`AwsSecretsManagerKeyLifecycleManager` and `HashicorpVaultKeyLifecycleManager` 
previously stored the
+`KeyMetadata` as a Base64-encoded, Java-serialized value; they now store it as 
JSON, consistent with
+`FileBasedKeyLifecycleManager`. Metadata written by previous versions is still 
read transparently and
+is migrated to JSON the next time the metadata is updated.
+
+Because older versions cannot read the new JSON metadata, downgrading after 
new key metadata has been
+written is not supported.

Reply via email to