gnodet-bot commented on code in PR #29098:
URL: https://github.com/apache/camel/pull/29098#discussion_r4456227760


##########
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java:
##########
@@ -1250,6 +1279,65 @@ private void statefulDeleteKeyState(Exchange exchange) 
throws Exception {
      *
      * @throws IllegalStateException if the key has zero remaining signatures
      */
+    /**
+     * Claims a stateful signature key for this process before it is used.
+     * <p/>
+     * XMSS, XMSSMT, LMS and HSS are one-time-signature schemes, and a store 
several processes read from - a shared
+     * directory, a vault - hands each of them the same key at the same index. 
The in-process ordering of
+     * {@link #recordKeyUsageAfterSign(Exchange)} does nothing about that, so 
the key is claimed from the
+     * {@link KeyLifecycleManager} the first time it is signed with, and held 
until this producer stops.
+     */
+    private void claimStatefulKeyBeforeSign(Exchange exchange) {
+        if (keyPair == null || keyPair.getPrivate() == null
+                || getStatefulKeyRemaining(keyPair.getPrivate()) < 0) {
+            return;
+        }
+
+        KeyLifecycleManager klm = getConfiguration().getKeyLifecycleManager();
+        String keyId = exchange.getMessage().getHeader(PQCConstants.KEY_ID, 
String.class);
+        if (klm == null || ObjectHelper.isEmpty(keyId)) {
+            // There is no store to claim from, and no key to name. 
recordKeyUsageAfterSign already warns that the
+            // index of this signature is not being persisted at all, which is 
the larger problem of the two.
+            return;
+        }
+
+        if (claimedStatefulKeys.contains(keyId)) {
+            return;

Review Comment:
   ⚠️ **Concurrency race: `contains()` and `add()` are not atomic**
   
   `claimedStatefulKeys` is a `ConcurrentHashMap`-backed set, but the 
check-then-act sequence across lines 377–411 is not atomic:
   
   ```
   if (claimedStatefulKeys.contains(keyId)) { return; }  // ← thread A and B 
both pass here
   // ... calls klm.claimStatefulKey(keyId) ...
   claimedStatefulKeys.add(keyId);                        // ← only thread A 
gets here first
   ```
   
   Two concurrent threads handling different exchanges for the same `keyId` can 
both pass the `contains()` guard before either adds to the set. Thread A 
acquires the `FileLock`; thread B then calls `claimStatefulKey()` on the same 
`FileChannel` and hits `OverlappingFileLockException`, which 
`FileBasedKeyLifecycleManager` re-throws as `IllegalStateException("already 
claimed in this JVM ... share the KeyLifecycleManager instance")` — a 
misleading error, because the managers *are* the same instance.
   
   **Fix options:**
   - Use `computeIfAbsent` in `claimedStatefulKeys` to make the check-then-add 
atomic, and call `klm.claimStatefulKey()` inside the compute lambda only when 
the entry is new.
   - Or synchronize the entire `contains → klm.claim → add` block on 
`keyId.intern()` (or a `ConcurrentHashMap<String, Object>` of per-key locks).



##########
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCConfiguration.java:
##########
@@ -125,6 +125,15 @@ public class PQCConfiguration implements Cloneable {
     @Metadata(label = "advanced")
     private double statefulKeyWarningThreshold = 0.1;
 
+    @UriParam(defaultValue = "false", label = "security,advanced", security = 
"insecure:dev",
+              description = "Whether to sign with a stateful key (XMSS, 
XMSSMT, LMS/HSS) that the configured "
+                            + "KeyLifecycleManager cannot claim for this 
process alone. These are one-time-signature "
+                            + "schemes: when two processes load the same key 
from a shared store, both sign from the "
+                            + "same index and the key is broken. Only enable 
it when something outside Camel "
+                            + "guarantees a single signer.")
+    @Metadata(label = "security,advanced")
+    private boolean allowSharedStatefulKey;

Review Comment:
   🔧 **Nit — redundant `label` in `@UriParam`**
   
   The `label` attribute is set in both `@UriParam` and `@Metadata`. The 
existing convention in this class (e.g. `statefulKeyWarningThreshold` just 
above) puts `label` only in `@Metadata`, not in `@UriParam`. Remove it from 
`@UriParam` for consistency.
   
   ```suggestion
       @UriParam(defaultValue = "false", security = "insecure:dev",
                 description = "Whether to sign with a stateful key (XMSS, 
XMSSMT, LMS/HSS) that the configured "
                               + "KeyLifecycleManager cannot claim for this 
process alone. These are one-time-signature "
                               + "schemes: when two processes load the same key 
from a shared store, both sign from the "
                               + "same index and the key is broken. Only enable 
it when something outside Camel "
                               + "guarantees a single signer.")
   ```



##########
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java:
##########
@@ -485,6 +491,70 @@ private Path getLegacyKeyFile(String keyId) {
         return resolveKeyFile(keyId, ".key");
     }
 
+    /**
+     * Takes an exclusive {@link FileLock} on a lock file beside the key, so 
that a second process reading the same key
+     * directory - two containers on one volume, say - cannot sign from the 
same one-time index.
+     * <p/>
+     * The lock lives as long as the channel, which this manager keeps open, 
so the operating system releases it if the
+     * process dies without releasing the claim.
+     */
+    @Override
+    public boolean claimStatefulKey(String keyId) throws IOException {
+        if (statefulClaims.containsKey(keyId)) {
+            return true;
+        }
+
+        Path lockFile = resolveKeyFile(keyId, LOCK_SUFFIX);
+        FileChannel channel = FileChannel.open(lockFile, 
StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+        restrictToOwner(lockFile);
+
+        FileLock lock;
+        try {
+            lock = channel.tryLock();
+        } catch (OverlappingFileLockException e) {
+            closeQuietly(channel);
+            throw new IllegalStateException(
+                    "The stateful key '" + keyId + "' is already claimed in 
this JVM through " + lockFile
+                                            + ". A stateful signature key must 
have a single owner: share the"
+                                            + " KeyLifecycleManager instance 
rather than the key directory.",
+                    e);
+        } catch (IOException e) {
+            closeQuietly(channel);
+            throw e;
+        }
+
+        if (lock == null) {
+            closeQuietly(channel);
+            throw new IllegalStateException(
+                    "The stateful key '" + keyId + "' is held by another 
process through " + lockFile
+                                            + ". Signing from it here would 
hand out a one-time index twice, which"
+                                            + " breaks the key. Give each 
process its own stateful key, or stop the"
+                                            + " other one.");
+        }
+
+        statefulClaims.put(keyId, channel);
+        LOG.info("Claimed the stateful key '{}' through {}", keyId, lockFile);
+        return true;
+    }
+
+    @Override
+    public void releaseStatefulKey(String keyId) {
+        FileChannel channel = statefulClaims.remove(keyId);

Review Comment:
   💡 **Nit — `.lock` files accumulate after release**
   
   `releaseStatefulKey` closes the `FileChannel` (which releases the OS lock) 
but never deletes the `.lock` file. The next `claimStatefulKey` call for the 
same `keyId` recreates it via `StandardOpenOption.CREATE`, so it is idempotent 
— but the files accumulate in the key directory across restarts and repeated 
claim/release cycles, which is surprising to users inspecting the directory.
   
   Consider adding `Files.deleteIfExists(lockFile)` after 
`closeQuietly(channel)`:
   
   ```java
   closeQuietly(channel);
   try {
       Files.deleteIfExists(resolveKeyFile(keyId, LOCK_SUFFIX));
   } catch (IOException e) {
       LOG.debug("Could not delete lock file for key '{}'", keyId, e);
   }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to