This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch task-31 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 20479f1c48e6928173cd8a65225c6b88e0115001 Author: Andrea Cosentino <[email protected]> AuthorDate: Wed Mar 18 10:43:27 2026 +0100 CAMEL-23210 - Camel-PQC: Add input validation for algorithm combinations and key sizes in PQCProducer Add startup validation in PQCProducer.doStart() to reject invalid symmetric key lengths for KEM operations. A static map defines valid key sizes for each of the 15 symmetric algorithms with fixed key requirements (AES, ARIA, CAMELLIA, CAST6, CHACHA7539, DSTU7624, GOST28147, GOST3412_2015, GRAIN128, HC128, HC256, SALSA20, SEED, SM4, DESEDE). Algorithms with variable key lengths (RC2, RC5, CAST5) are intentionally excluded. Validation only applies to the six KEM operation types; signature, lifecycle, and stateful operations are unaffected. Additionally, warn at startup about non-recommended hybrid combinations (RSA in hybrid signatures, non-NIST PQC algorithms) and log NIST parameter set guidance for ML-KEM, ML-DSA, and SLH-DSA. Includes 12 new unit tests in PQCInputValidationTest covering invalid key lengths, valid key lengths, hybrid KEM validation, and non-KEM passthrough scenarios. Signed-off-by: Andrea Cosentino <[email protected]> # in VALID_SYMMETRIC_KEY_LENGTHS map to improve traceability and avoid # potential typos # assertThatThrownBy/assertThatCode for more precise failure messages --- .../apache/camel/component/pqc/PQCProducer.java | 160 +++++++++++++++++ .../component/pqc/PQCInputValidationTest.java | 190 +++++++++++++++++++++ 2 files changed, 350 insertions(+) diff --git a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java index d2d7993551ff..25a35c2e1d15 100644 --- a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java +++ b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java @@ -18,7 +18,9 @@ package org.apache.camel.component.pqc; import java.security.*; import java.security.cert.Certificate; +import java.util.Arrays; import java.util.List; +import java.util.Map; import javax.crypto.KeyAgreement; import javax.crypto.KeyGenerator; @@ -51,6 +53,24 @@ public class PQCProducer extends DefaultProducer { private static final Logger LOG = LoggerFactory.getLogger(PQCProducer.class); + // Valid symmetric key lengths per algorithm for startup validation + private static final Map<String, int[]> VALID_SYMMETRIC_KEY_LENGTHS = Map.ofEntries( + Map.entry(PQCSymmetricAlgorithms.AES.name(), new int[] { 128, 192, 256 }), + Map.entry(PQCSymmetricAlgorithms.ARIA.name(), new int[] { 128, 192, 256 }), + Map.entry(PQCSymmetricAlgorithms.CAMELLIA.name(), new int[] { 128, 192, 256 }), + Map.entry(PQCSymmetricAlgorithms.CAST6.name(), new int[] { 128, 160, 192, 224, 256 }), + Map.entry(PQCSymmetricAlgorithms.CHACHA7539.name(), new int[] { 256 }), + Map.entry(PQCSymmetricAlgorithms.DSTU7624.name(), new int[] { 128, 256, 512 }), + Map.entry(PQCSymmetricAlgorithms.GOST28147.name(), new int[] { 256 }), + Map.entry(PQCSymmetricAlgorithms.GOST3412_2015.name(), new int[] { 256 }), + Map.entry(PQCSymmetricAlgorithms.GRAIN128.name(), new int[] { 128 }), + Map.entry(PQCSymmetricAlgorithms.HC128.name(), new int[] { 128 }), + Map.entry(PQCSymmetricAlgorithms.HC256.name(), new int[] { 256 }), + Map.entry(PQCSymmetricAlgorithms.SALSA20.name(), new int[] { 128, 256 }), + Map.entry(PQCSymmetricAlgorithms.SEED.name(), new int[] { 128 }), + Map.entry(PQCSymmetricAlgorithms.SM4.name(), new int[] { 128 }), + Map.entry(PQCSymmetricAlgorithms.DESEDE.name(), new int[] { 128, 192 })); + private Signature signer; private KeyGenerator keyGenerator; private KeyPair keyPair; @@ -249,6 +269,7 @@ public class PQCProducer extends DefaultProducer { @Override protected void doStart() throws Exception { super.doStart(); + validateConfiguration(); if (getConfiguration().getOperation().equals(PQCOperations.sign) || getConfiguration().getOperation().equals(PQCOperations.verify)) { @@ -763,4 +784,143 @@ public class PQCProducer extends DefaultProducer { } } + // ========== Configuration Validation ========== + + /** + * Validates the producer configuration at startup to catch invalid or non-recommended algorithm combinations and + * key sizes early, before any cryptographic operation is attempted. + */ + private void validateConfiguration() { + PQCConfiguration config = getConfiguration(); + PQCOperations op = config.getOperation(); + + validateSymmetricKeyLength(config, op); + warnHybridCombinations(config, op); + logNistRecommendations(config); + } + + private void validateSymmetricKeyLength(PQCConfiguration config, PQCOperations op) { + if (!isKEMOperation(op)) { + return; + } + String symAlg = config.getSymmetricKeyAlgorithm(); + if (ObjectHelper.isEmpty(symAlg)) { + return; + } + int keyLen = config.getSymmetricKeyLength(); + int[] validLengths = VALID_SYMMETRIC_KEY_LENGTHS.get(symAlg); + if (validLengths != null) { + boolean valid = false; + for (int len : validLengths) { + if (len == keyLen) { + valid = true; + break; + } + } + if (!valid) { + throw new IllegalArgumentException( + "Invalid symmetric key length " + keyLen + " for algorithm " + symAlg + + ". Valid key lengths: " + Arrays.toString(validLengths)); + } + } + } + + private void warnHybridCombinations(PQCConfiguration config, PQCOperations op) { + if (op == PQCOperations.hybridSign || op == PQCOperations.hybridVerify) { + String classicalAlg = config.getClassicalSignatureAlgorithm(); + if (ObjectHelper.isNotEmpty(classicalAlg)) { + try { + PQCClassicalSignatureAlgorithms classical = PQCClassicalSignatureAlgorithms.valueOf(classicalAlg); + if (classical.isRSA()) { + LOG.warn("Using RSA ({}) in hybrid signature mode. ECDSA or EdDSA (Ed25519/Ed448) " + + "is recommended for new hybrid deployments due to smaller signature sizes " + + "and better performance.", + classicalAlg); + } + } catch (IllegalArgumentException e) { + // Unknown classical algorithm - will fail later during init + } + } + String pqcAlg = config.getSignatureAlgorithm(); + if (ObjectHelper.isNotEmpty(pqcAlg) && isNonNistSignature(pqcAlg)) { + LOG.warn("PQC signature algorithm {} is not NIST-standardized. Consider using " + + "ML-DSA (FIPS 204) or SLH-DSA (FIPS 205) for production hybrid deployments.", + pqcAlg); + } + } + + if (op == PQCOperations.hybridGenerateSecretKeyEncapsulation + || op == PQCOperations.hybridExtractSecretKeyEncapsulation + || op == PQCOperations.hybridExtractSecretKeyFromEncapsulation) { + String pqcAlg = config.getKeyEncapsulationAlgorithm(); + if (ObjectHelper.isNotEmpty(pqcAlg) && isNonNistKEM(pqcAlg)) { + LOG.warn("PQC KEM algorithm {} is not NIST-standardized. Consider using " + + "ML-KEM (FIPS 203) for production hybrid deployments.", + pqcAlg); + } + } + } + + private void logNistRecommendations(PQCConfiguration config) { + String kemAlg = config.getKeyEncapsulationAlgorithm(); + if ("MLKEM".equals(kemAlg)) { + LOG.info("Using ML-KEM (NIST FIPS 203). Available parameter sets: " + + "ML-KEM-512 (Level 1), ML-KEM-768 (Level 3, recommended), ML-KEM-1024 (Level 5)"); + } + + String sigAlg = config.getSignatureAlgorithm(); + if ("MLDSA".equals(sigAlg)) { + LOG.info("Using ML-DSA (NIST FIPS 204). Available parameter sets: " + + "ML-DSA-44 (Level 2), ML-DSA-65 (Level 3, recommended), ML-DSA-87 (Level 5)"); + } else if ("SLHDSA".equals(sigAlg)) { + LOG.info("Using SLH-DSA (NIST FIPS 205). Stateless hash-based signature scheme suitable for " + + "applications where stateful key management is not feasible"); + } + } + + private boolean isKEMOperation(PQCOperations op) { + switch (op) { + case generateSecretKeyEncapsulation: + case extractSecretKeyEncapsulation: + case extractSecretKeyFromEncapsulation: + case hybridGenerateSecretKeyEncapsulation: + case hybridExtractSecretKeyEncapsulation: + case hybridExtractSecretKeyFromEncapsulation: + return true; + default: + return false; + } + } + + private boolean isNonNistSignature(String alg) { + switch (alg) { + case "DILITHIUM": + case "FALCON": + case "PICNIC": + case "SNOVA": + case "MAYO": + case "SPHINCSPLUS": + return true; + default: + return false; + } + } + + private boolean isNonNistKEM(String alg) { + switch (alg) { + case "BIKE": + case "HQC": + case "CMCE": + case "SABER": + case "FRODO": + case "NTRU": + case "NTRULPRime": + case "SNTRUPrime": + case "KYBER": + return true; + default: + return false; + } + } + } diff --git a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCInputValidationTest.java b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCInputValidationTest.java new file mode 100644 index 000000000000..586b059d04ae --- /dev/null +++ b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/PQCInputValidationTest.java @@ -0,0 +1,190 @@ +/* + * 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; + +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for input validation of algorithm combinations and key sizes in PQCProducer. Validates that invalid symmetric + * key lengths are rejected at startup and that non-KEM operations are not affected by key length validation. + */ +public class PQCInputValidationTest extends CamelTestSupport { + + @Override + public boolean isUseRouteBuilder() { + return false; + } + + // -- Invalid symmetric key length tests (should throw at startup) -- + + @Test + void testInvalidAESKeyLength64ThrowsAtStartup() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=AES&symmetricKeyLength=64" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatThrownBy(producer::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid symmetric key length 64 for algorithm AES"); + } + + @Test + void testInvalidAESKeyLength512ThrowsAtStartup() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=AES&symmetricKeyLength=512" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatThrownBy(producer::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid symmetric key length 512 for algorithm AES"); + } + + @Test + void testInvalidChaChaKeyLength128ThrowsAtStartup() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=CHACHA7539&symmetricKeyLength=128" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatThrownBy(producer::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid symmetric key length 128 for algorithm CHACHA7539"); + } + + @Test + void testInvalidGOSTKeyLength128ThrowsAtStartup() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=GOST28147&symmetricKeyLength=128" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatThrownBy(producer::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid symmetric key length 128 for algorithm GOST28147"); + } + + @Test + void testInvalidHC256KeyLength128ThrowsAtStartup() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=extractSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=HC256&symmetricKeyLength=128" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatThrownBy(producer::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid symmetric key length 128 for algorithm HC256"); + } + + @Test + void testInvalidDESedeKeyLength64ThrowsAtStartup() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=DESEDE&symmetricKeyLength=64" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatThrownBy(producer::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid symmetric key length 64 for algorithm DESEDE"); + } + + @Test + void testInvalidKeyLengthForHybridKEMThrowsAtStartup() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=hybridGenerateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=AES&symmetricKeyLength=64" + + "&keyEncapsulationAlgorithm=MLKEM" + + "&classicalKEMAlgorithm=X25519"); + + assertThatThrownBy(producer::start) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Invalid symmetric key length 64 for algorithm AES"); + } + + // -- Valid symmetric key length tests (should start without error) -- + + @Test + void testValidAESKeyLength128Accepted() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=AES&symmetricKeyLength=128" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatCode(producer::start).doesNotThrowAnyException(); + producer.stop(); + } + + @Test + void testValidAESKeyLength256Accepted() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=AES&symmetricKeyLength=256" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatCode(producer::start).doesNotThrowAnyException(); + producer.stop(); + } + + @Test + void testValidChaChaKeyLength256Accepted() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=CHACHA7539&symmetricKeyLength=256" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatCode(producer::start).doesNotThrowAnyException(); + producer.stop(); + } + + @Test + void testValidDESedeKeyLength192Accepted() throws Exception { + PQCProducer producer = createProducer( + "pqc:test?operation=generateSecretKeyEncapsulation" + + "&symmetricKeyAlgorithm=DESEDE&symmetricKeyLength=192" + + "&keyEncapsulationAlgorithm=MLKEM"); + + assertThatCode(producer::start).doesNotThrowAnyException(); + producer.stop(); + } + + // -- Key length not validated for non-KEM operations -- + + @Test + void testKeyLengthNotValidatedForSignatureOperations() throws Exception { + // symmetricKeyLength=64 is invalid for AES but should not matter for sign operations + PQCProducer producer = createProducer( + "pqc:test?operation=sign&signatureAlgorithm=MLDSA" + + "&symmetricKeyAlgorithm=AES&symmetricKeyLength=64"); + + assertThatCode(producer::start).doesNotThrowAnyException(); + producer.stop(); + } + + // -- Helper -- + + private PQCProducer createProducer(String uri) throws Exception { + PQCComponent component = context.getComponent("pqc", PQCComponent.class); + PQCEndpoint endpoint = (PQCEndpoint) component.createEndpoint(uri); + endpoint.start(); + return new PQCProducer(endpoint); + } +}
