oscerd commented on code in PR #29098:
URL: https://github.com/apache/camel/pull/29098#discussion_r4466912415


##########
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:
   Fixed in 963ccbd, taking the `computeIfAbsent` option rather than the 
re-check.
   
   Both levels are now atomic: `PQCProducer` wraps claim-and-record in 
`statefulKeyClaims.computeIfAbsent`, and `FileBasedKeyLifecycleManager` does 
open-lock-store in `statefulClaims.computeIfAbsent`. That removes the 
false-positive path for callers of one manager rather than catching it after 
the fact, which also means the OFE handler now only ever runs for a genuinely 
different manager instance — the case its message describes.
   
   New `concurrentFirstClaimsOnOneManagerAllSucceed` covers it: eight threads 
racing the first claim. Against the previous commit it fails with exactly the 
misleading error you predicted.
   
   _Claude Code on behalf of @oscerd_



##########
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);

Review Comment:
   Confirmed and fixed in 963ccbd. I reproduced it on Linux before changing 
anything — one JVM holding the lock, opening a second channel on the same file, 
catching the OFE and closing that channel:
   
   ```
   holder: first lock = true
   holder: got OverlappingFileLockException as expected
   holder: closed second channel; first lock isValid=true   <- still reports 
valid
   --- separate process ---
   prober: acquired lock = true                             <- the lock was gone
   ```
   
   So on this JDK it is not using OFD locks, and `isValid()` keeps answering 
`true` while the lock is gone — the code cannot even detect the loss. The 
channel is no longer closed on the OFE path. It is still closed where no lock 
was taken on it (`tryLock` returning null, or an `IOException`), which is safe, 
and `openAndLock` records why in its javadoc.
   
   _Claude Code on behalf of @oscerd_



##########
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.")

Review Comment:
   Fixed in 963ccbd — `label` removed from `@UriParam`, left on `@Metadata` as 
the surrounding options do. Confirmed no generated metadata changed, so it was 
purely redundant.
   
   _Claude Code on behalf of @oscerd_



##########
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:
   Kept, with the reasoning written down in `releaseStatefulKey` rather than 
deleting the file.
   
   Deleting it is not safe: another process can already hold the same file open 
and locked, and unlinking it lets a third process create a fresh file at that 
path and lock *that* — two owners, each holding a valid lock on a different 
inode. An empty `.lock` beside each stateful key is the price of avoiding that. 
The javadoc now says so, so the next reader does not have to rediscover it.
   
   _Claude Code on behalf of @oscerd_



-- 
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