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


##########
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java:
##########
@@ -572,14 +577,37 @@ protected void doStop() throws Exception {
             healthCheckRepository.removeHealthCheck(producerHealthCheck);
             producerHealthCheck = null;
         }
+        releaseClaimedStatefulKeys();
         metrics.close();
         metrics = PQCMetrics.NOOP;
         super.doStop();
     }
 
+    /**
+     * Gives back the stateful keys this producer claimed, so another process 
can take them over once this one is
+     * stopped. A claim that cannot be released is logged rather than failing 
the stop: the operating system releases it
+     * with the process anyway.
+     */
+    private void releaseClaimedStatefulKeys() {
+        KeyLifecycleManager klm = getConfiguration().getKeyLifecycleManager();
+        if (klm == null) {
+            claimedStatefulKeys.clear();
+            return;
+        }
+        for (String keyId : claimedStatefulKeys) {

Review Comment:
   Folded in after all, in 1f647786205a, since both reviews asked for it here: 
the claims of one `FileBasedKeyLifecycleManager` are now counted per key (a 
`holders` count on the claim), so producers that share the manager do not 
release each other's claim and the key is given back by the last holder. The 
interface Javadoc of `claimStatefulKey`/`releaseStatefulKey` says so, and the 
key-lifecycle page tells a custom manager to count its claims too. 
`aClaimIsGrantedAndTakesALockFile` now checks that a key claimed twice is still 
held after one release and free after the second. CAMEL-26588 is covered by 
this.
   
   _Claude Code on behalf of oscerd_
   



##########
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java:
##########
@@ -485,6 +492,101 @@ 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 {
+        // computeIfAbsent so the whole open-lock-store sequence happens once 
per key: a plain containsKey check
+        // followed by a separate put lets two concurrent first-time signers 
both open a channel, and the loser then
+        // takes the OverlappingFileLockException path below for no reason.
+        try {
+            statefulClaims.computeIfAbsent(keyId, this::openAndLock);
+        } catch (UncheckedIOException e) {
+            throw e.getCause();
+        }
+        return true;
+    }
+
+    /**
+     * Opens the lock file of a key and takes an exclusive lock on it, 
returning the channel that holds it.
+     * <p/>
+     * A channel is only ever closed here when this process holds no lock on 
the file. Closing one that may hold the
+     * lock is not safe: on this platform closing any descriptor for the file 
releases the lock this process holds
+     * through every other descriptor, while {@code FileLock.isValid()} keeps 
reporting {@code true}, so the claim would
+     * be silently gone with nothing able to detect it.
+     */
+    private FileChannel openAndLock(String keyId) {
+        Path lockFile = resolveKeyFile(keyId, LOCK_SUFFIX);
+        FileChannel channel;
+        try {
+            channel = FileChannel.open(lockFile, StandardOpenOption.CREATE, 
StandardOpenOption.WRITE);
+            restrictToOwner(lockFile);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+
+        FileLock lock;
+        try {
+            lock = channel.tryLock();
+        } catch (OverlappingFileLockException e) {
+            // Another KeyLifecycleManager instance in this JVM holds the 
lock. The channel is deliberately left
+            // open: closing it would drop that instance's lock, see 
openAndLock's javadoc.
+            throw new IllegalStateException(

Review Comment:
   Applied in 1f647786205a: a JVM-wide `ConcurrentHashMap<Path, 
FileBasedKeyLifecycleManager>` keyed by the real path of the lock file, 
consulted inside `compute` before `FileChannel.open`. A second manager of the 
same directory, also through a symbolic link, is refused there and no channel 
is opened, so there is nothing for the `Cleaner` to close and no descriptor 
leaks per refused exchange. `OverlappingFileLockException` can now only mean a 
lock taken through a channel this class did not open; such a channel is kept 
referenced for the life of the JVM rather than closed or left to the collector. 
Tests: `aSecondHolderOfTheSameKeyDirectoryIsRefused` (comment updated) and the 
new `aSecondHolderThroughAnotherPathToTheSameDirectoryIsRefused`.
   
   _Claude Code on behalf of oscerd_
   



##########
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java:
##########
@@ -1244,6 +1279,116 @@ private void statefulDeleteKeyState(Exchange exchange) 
throws Exception {
         getRequiredKeyLifecycleManager().deleteKey(keyId);
     }
 
+    /**
+     * 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;
+        }
+
+        // The claim is taken once per key. computeIfAbsent rather than a 
contains/claim/add sequence, so two
+        // concurrent first-time signers cannot both reach claimStatefulKey 
for the same key.
+        statefulKeyClaims.computeIfAbsent(keyId, id -> {
+            doClaimStatefulKey(klm, id);
+            adoptStoredStatefulKeyIfAhead(klm, id);
+            claimedStatefulKeys.add(id);
+            return Boolean.TRUE;
+        });
+    }
+
+    private void doClaimStatefulKey(KeyLifecycleManager klm, String keyId) {
+        boolean claimed;
+        try {
+            claimed = klm.claimStatefulKey(keyId);
+        } catch (Exception e) {
+            throw new IllegalStateException(
+                    "Cannot claim the stateful key '" + keyId + "': " + 
e.getMessage(), e);
+        }
+
+        if (!claimed) {
+            if (getConfiguration().isAllowSharedStatefulKey()) {
+                if (sharedStatefulKeyWarned.add(keyId)) {
+                    LOG.warn("Signing with the stateful key '{}', which {} 
cannot claim for this process alone,"
+                             + " because allowSharedStatefulKey is enabled. If 
a second process signs with the same"
+                             + " key, both hand out the same one-time index 
and the key is broken.",
+                            keyId, klm.getClass().getSimpleName());
+                }
+            } else {
+                throw new IllegalStateException(
+                        "The stateful key '" + keyId + "' cannot be claimed 
for this process: "
+                                                + 
klm.getClass().getSimpleName()
+                                                + " cannot guarantee that no 
other process holds it. "
+                                                + 
keyPair.getPrivate().getAlgorithm()
+                                                + " is a one-time-signature 
scheme, and two processes signing from the"
+                                                + " same index break the key. 
Use a KeyLifecycleManager that can claim"
+                                                + " a key, such as 
FileBasedKeyLifecycleManager, or set"
+                                                + " 
allowSharedStatefulKey=true if a single signer is guaranteed"
+                                                + " elsewhere.");
+            }
+        }
+    }
+
+    /**
+     * Re-reads the stored key once the claim is granted, and adopts it when 
its index has moved past the one this
+     * producer loaded.
+     * <p/>
+     * Without this the claim does not stop what it is meant to stop. {@code 
keyPair} is read once in
+     * {@link #doStart()}, so in a rolling update the arriving instance loads 
the key at some index, waits for the claim
+     * while the leaving instance keeps signing, and then starts from its own 
stale index - handing out every index the
+     * other instance already used. The claim only serialises the signers; it 
is this read that makes the second one
+     * resume where the first stopped.
+     */
+    private void adoptStoredStatefulKeyIfAhead(KeyLifecycleManager klm, String 
keyId) {
+        KeyPair stored;
+        try {
+            stored = klm.getKey(keyId);

Review Comment:
   Applied in 1f647786205a: `openAndLock` evicts `keyCache` and `metadataCache` 
for the key once the lock is taken, so the re-read in 
`adoptStoredStatefulKeyIfAhead` comes from the store. New test 
`aNewClaimReadsTheKeyAndItsMetadataFromTheStore` with two 
`FileBasedKeyLifecycleManager` instances on one directory: the arriving one 
serves its cached key and metadata until it claims, and the stored key and 
usage count after.
   
   _Claude Code on behalf of oscerd_
   



##########
components/camel-pqc/src/main/java/org/apache/camel/component/pqc/PQCProducer.java:
##########
@@ -1244,6 +1279,116 @@ private void statefulDeleteKeyState(Exchange exchange) 
throws Exception {
         getRequiredKeyLifecycleManager().deleteKey(keyId);
     }
 
+    /**
+     * 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;
+        }
+
+        // The claim is taken once per key. computeIfAbsent rather than a 
contains/claim/add sequence, so two
+        // concurrent first-time signers cannot both reach claimStatefulKey 
for the same key.
+        statefulKeyClaims.computeIfAbsent(keyId, id -> {
+            doClaimStatefulKey(klm, id);
+            adoptStoredStatefulKeyIfAhead(klm, id);
+            claimedStatefulKeys.add(id);
+            return Boolean.TRUE;
+        });
+    }
+
+    private void doClaimStatefulKey(KeyLifecycleManager klm, String keyId) {
+        boolean claimed;
+        try {
+            claimed = klm.claimStatefulKey(keyId);
+        } catch (Exception e) {
+            throw new IllegalStateException(
+                    "Cannot claim the stateful key '" + keyId + "': " + 
e.getMessage(), e);
+        }
+
+        if (!claimed) {
+            if (getConfiguration().isAllowSharedStatefulKey()) {
+                if (sharedStatefulKeyWarned.add(keyId)) {
+                    LOG.warn("Signing with the stateful key '{}', which {} 
cannot claim for this process alone,"
+                             + " because allowSharedStatefulKey is enabled. If 
a second process signs with the same"
+                             + " key, both hand out the same one-time index 
and the key is broken.",
+                            keyId, klm.getClass().getSimpleName());
+                }
+            } else {
+                throw new IllegalStateException(
+                        "The stateful key '" + keyId + "' cannot be claimed 
for this process: "
+                                                + 
klm.getClass().getSimpleName()
+                                                + " cannot guarantee that no 
other process holds it. "
+                                                + 
keyPair.getPrivate().getAlgorithm()
+                                                + " is a one-time-signature 
scheme, and two processes signing from the"
+                                                + " same index break the key. 
Use a KeyLifecycleManager that can claim"
+                                                + " a key, such as 
FileBasedKeyLifecycleManager, or set"
+                                                + " 
allowSharedStatefulKey=true if a single signer is guaranteed"
+                                                + " elsewhere.");
+            }
+        }
+    }
+
+    /**
+     * Re-reads the stored key once the claim is granted, and adopts it when 
its index has moved past the one this
+     * producer loaded.
+     * <p/>
+     * Without this the claim does not stop what it is meant to stop. {@code 
keyPair} is read once in
+     * {@link #doStart()}, so in a rolling update the arriving instance loads 
the key at some index, waits for the claim
+     * while the leaving instance keeps signing, and then starts from its own 
stale index - handing out every index the
+     * other instance already used. The claim only serialises the signers; it 
is this read that makes the second one
+     * resume where the first stopped.
+     */
+    private void adoptStoredStatefulKeyIfAhead(KeyLifecycleManager klm, String 
keyId) {
+        KeyPair stored;
+        try {
+            stored = klm.getKey(keyId);
+        } catch (Exception e) {
+            LOG.warn("Claimed the stateful key '{}' but could not re-read it 
from the store, so this producer keeps"
+                     + " the key it loaded at startup. If another process 
advanced that key meanwhile, signing now"
+                     + " reuses a one-time index.",
+                    keyId, e);
+            return;
+        }
+
+        if (stored == null || stored.getPrivate() == null) {
+            return;
+        }
+
+        long storedRemaining = getStatefulKeyRemaining(stored.getPrivate());
+        long loadedRemaining = getStatefulKeyRemaining(keyPair.getPrivate());
+        if (storedRemaining < 0) {
+            return;
+        }
+
+        // Fewer remaining signatures means the stored key has been used more 
than the one held here
+        if (storedRemaining < loadedRemaining) {
+            LOG.warn("The stored stateful key '{}' has {} signatures left 
while the one loaded at startup has {}."
+                     + " Another process advanced it. Adopting the stored key 
so this producer resumes from its"
+                     + " index instead of reusing indices that have already 
been handed out.",
+                    keyId, storedRemaining, loadedRemaining);
+            keyPair = stored;

Review Comment:
   Applied in 1f647786205a: `adoptStoredStatefulKeyIfAhead` keeps the key 
loaded at startup, with a warning, when the stored public key encodes 
differently (`MessageDigest.isEqual` on the encodings; keys that give no 
encoding cannot be told apart and are not treated as different). New test 
`aStoredKeyThatIsAnotherKeyIsNotAdopted`: another LMS key stored under the same 
id, further ahead, is not adopted, and the producer signs once from the index 
it loaded.
   
   _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