Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/camel-pqc/src/main/docs/pqc-component.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ This will be true for standardized algorithms and for experimental ones.

TIP: To combine a classical signature (ECDSA, Ed25519, RSA) with a PQC signature for defense-in-depth during the post-quantum transition, use the hybrid signature operations described in xref:others:pqc-hybrid.adoc[Hybrid Cryptography].

NOTE: The sign and verify operations stream the message body. A `byte[]` or `InputStream` body is fed to the signature in chunks without being materialised as a String, so large payloads (files, large messages) can be signed and verified without loading them fully into memory. When the body is a re-readable `StreamCache` (for example with stream caching enabled) it is reset after reading so downstream processors still see the payload.
NOTE: The sign and verify operations stream the message body. A `byte[]` or `InputStream` body is fed to the signature in chunks without being materialised as a String, so large payloads (files, large messages) can be signed and verified without loading them fully into memory. When the body is a re-readable `StreamCache` (for example with stream caching enabled) it is reset after reading so downstream processors still see the payload. Any other body type falls back to its String representation, which is always encoded as **UTF-8** so that signing and verifying agree regardless of the JVM default charset.

== Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.Certificate;
import java.util.Arrays;
Expand Down Expand Up @@ -514,7 +515,7 @@ private void verification(Exchange exchange)
* Feeds the message body into the given {@link Signature} without materialising the whole payload as a String. A
* {@code byte[]} body is used directly, an {@link InputStream} body is read in chunks (and reset afterwards when it
* is a re-readable {@link StreamCache} so downstream processors still see the payload), and any other body type
* falls back to its String representation.
* falls back to its String representation, encoded as UTF-8.
*/
private static void updateSignatureFromBody(Signature signature, Message message)
throws InvalidPayloadException, SignatureException, IOException {
Expand All @@ -534,14 +535,17 @@ private static void updateSignatureFromBody(Signature signature, Message message
}
}
} else {
signature.update(message.getMandatoryBody(String.class).getBytes());
// Pin UTF-8: the JVM default charset is platform dependent, so signing and verifying on JVMs with a
// different default would disagree on the bytes for a non-ASCII payload
signature.update(message.getMandatoryBody(String.class).getBytes(StandardCharsets.UTF_8));
}
}

/**
* Returns the message body as a byte array without a String round-trip when it is already binary. Used by the
* hybrid operations, which need the whole payload in memory. An {@link InputStream} is fully read (and reset
* afterwards when it is a re-readable {@link StreamCache}).
* afterwards when it is a re-readable {@link StreamCache}). Any other body type falls back to its String
* representation, encoded as UTF-8.
*/
private static byte[] bodyToByteArray(Message message) throws InvalidPayloadException, IOException {
Object body = message.getBody();
Expand All @@ -556,7 +560,8 @@ private static byte[] bodyToByteArray(Message message) throws InvalidPayloadExce
}
}
} else {
return message.getMandatoryBody(String.class).getBytes();
// Pin UTF-8, as updateSignatureFromBody does
return message.getMandatoryBody(String.class).getBytes(StandardCharsets.UTF_8);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* 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 java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.Arrays;

import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.test.junit6.CamelTestSupport;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* A String body must always be encoded as UTF-8 when signing and verifying, never with the JVM default charset, so that
* a signature produced on one JVM verifies on another with a different default.
*/
public class PQCSignatureCharsetTest extends CamelTestSupport {

private static final String NON_ASCII = "héllo wörld — ünïcode";

@BeforeAll
public static void startup() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}

@Override
protected RouteBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
from("direct:signOnly").to("pqc:sign?operation=sign&signatureAlgorithm=MLDSA");
from("direct:verifyOnly").to("pqc:verify?operation=verify&signatureAlgorithm=MLDSA");
}
};
}

private byte[] sign(Object body) {
Exchange out = template.request("direct:signOnly", e -> e.getMessage().setBody(body));
return out.getMessage().getHeader(PQCConstants.SIGNATURE, byte[].class);
}

private boolean verify(Object body, byte[] signature) {
Exchange out = template.request("direct:verifyOnly", e -> {
e.getMessage().setBody(body);
e.getMessage().setHeader(PQCConstants.SIGNATURE, signature);
});
return out.getMessage().getHeader(PQCConstants.VERIFY, Boolean.class);
}

@Test
void testStringBodyIsInterchangeableWithItsUtf8Bytes() {
byte[] utf8 = NON_ASCII.getBytes(StandardCharsets.UTF_8);

byte[] fromString = sign(NON_ASCII);
assertNotNull(fromString);
assertTrue(verify(utf8, fromString), "a String body must be signed as UTF-8");

byte[] fromBytes = sign(utf8);
assertTrue(verify(NON_ASCII, fromBytes), "a String body must be verified as UTF-8");
}

@Test
void testStringBodyIsNotEncodedWithAnotherCharset() {
byte[] latin1 = NON_ASCII.getBytes(StandardCharsets.ISO_8859_1);
// sanity: the two encodings really do differ for this text
assertFalse(Arrays.equals(latin1, NON_ASCII.getBytes(StandardCharsets.UTF_8)));

// so a signature over the String must not verify against the ISO-8859-1 bytes
assertFalse(verify(latin1, sign(NON_ASCII)),
"a String body must not be encoded with the platform charset (ISO-8859-1 here)");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,17 @@ already required by `JdbcAggregationRepository` for reading and updating aggrega
`recoveryByInstance` is enabled, the completed table must have the `instance_id VARCHAR(255)`
column as documented.

=== camel-pqc - String payloads are signed and verified as UTF-8

The `sign`, `verify`, `hybridSign` and `hybridVerify` operations previously encoded a `String`
message body using the JVM default charset (`String.getBytes()`). The default charset is platform
dependent, so signing on one JVM and verifying on another with a different default could disagree
on the bytes of a non-ASCII payload and make verification fail.

`String` bodies are now always encoded as UTF-8. On Java 18 and newer the default charset is
already UTF-8 (JEP 400), so this is a no-op there; on Java 17 with a non-UTF-8 default, the
signature of a non-ASCII `String` payload changes. Binary bodies (`byte[]`, `InputStream`) are
unaffected, as they are signed byte-for-byte.
=== camel-sql - Schema-qualified aggregation repository names

The table-name validation in `JdbcAggregationRepository` now accepts schema-qualified names
Expand Down