Copilot commented on code in PR #14453:
URL: https://github.com/apache/cloudstack/pull/14453#discussion_r4534949500


##########
server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java:
##########
@@ -1781,12 +1899,174 @@ void rewrapSingleKey(KMSWrappedKeyVO wrappedKeyVO, 
KMSKeyVO kmsKey,
         }
     }
 
+    /**
+     * 4.23 migrated legacy volume passphrases to KMS as their base64 text 
instead of their raw bytes, so those
+     * volumes got a wrong passphrase. The upgrade flags every wrapped key 
that existed at that point; this task
+     * checks each one once and rewraps the affected ones with the raw bytes. 
A key that can't be unwrapped (e.g. the
+     * HSM is down) stays flagged and is retried. The task stops once no 
flagged key is left.
+     */
+    private void scheduleKeyFormatRepair() {
+        if (kmsWrappedKeyDao.listFormatCheckPending(0, 1).isEmpty()) {
+            return;
+        }
+        ScheduledExecutorService executor = 
Executors.newSingleThreadScheduledExecutor(r -> {
+            Thread t = new Thread(r, "KMSKeyFormatRepair");
+            t.setDaemon(true);
+            return t;
+        });
+        keyFormatRepairExecutor = executor;
+        executor.scheduleWithFixedDelay(new ManagedContextRunnable() {
+            @Override
+            protected void runInContext() {
+                try {
+                    if (repairKeyFormatBatch()) {
+                        logger.info("All KMS wrapped keys flagged at the 
upgrade from 4.23 have been checked");
+                        executor.shutdown();
+                    }
+                } catch (final Exception e) {
+                    logger.error("Error while checking the format of KMS 
wrapped keys", e);
+                }
+            }
+        }, 30, 60, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Checks the next batch of flagged keys. A batch ends at the first key 
that can't be checked (e.g. its HSM is
+     * down), so the lock shared with the rewrap worker isn't held through 
retries; the next batch resumes after that
+     * key so it doesn't block the others.
+     *
+     * @return true when no flagged key is left
+     */
+    boolean repairKeyFormatBatch() {
+        GlobalLock lock = GlobalLock.getInternLock("kms.rewrap.worker");
+        try {
+            if (!lock.lock(5)) {
+                return false;
+            }
+            try {
+                List<KMSWrappedKeyVO> keys = 
kmsWrappedKeyDao.listFormatCheckPending(keyFormatRepairCursor,
+                        KMSRewrapBatchSize.value());
+                if (keys.isEmpty()) {
+                    boolean done = keyFormatRepairCursor == 0;
+                    keyFormatRepairCursor = 0;
+                    return done;
+                }
+                for (KMSWrappedKeyVO key : keys) {
+                    keyFormatRepairCursor = key.getId();
+                    try {
+                        repairKeyFormat(key);
+                    } catch (Exception e) {
+                        logger.warn("Could not check the format of KMS wrapped 
key {}, will retry: {}", key, e.getMessage());
+                        logger.debug("Failed to check the format of KMS 
wrapped key {}", key, e);
+                        break;
+                    }
+                }
+                return false;
+            } finally {
+                lock.unlock();
+            }
+        } finally {
+            lock.releaseRef();
+        }
+    }
+
+    private void repairKeyFormat(KMSWrappedKeyVO wrappedKeyVO) {
+        byte[] key;
+        try {
+            key = unwrapKey(wrappedKeyVO.getId());
+        } catch (KMSException e) {
+            if (e.getErrorType() != KMSException.ErrorType.KEK_NOT_FOUND) {
+                throw e;
+            }
+            // Gone for good, e.g. its KMS key or KEK was deleted, so there is 
nothing to repair
+            logger.warn("KMS wrapped key {} can't be unwrapped anymore, not 
checking its format: {}", wrappedKeyVO,
+                    e.getMessage());
+            markFormatChecked(wrappedKeyVO);
+            return;
+        }
+        byte[] passphrase = decodeLegacyMigratedKey(key);
+        try {
+            if (passphrase != null) {
+                rewrapLegacyMigratedKey(wrappedKeyVO, passphrase);
+            }
+            markFormatChecked(wrappedKeyVO);
+        } finally {
+            Arrays.fill(key, (byte) 0);
+            if (passphrase != null) {
+                Arrays.fill(passphrase, (byte) 0);
+            }
+        }
+    }
+
+    private void rewrapLegacyMigratedKey(KMSWrappedKeyVO wrappedKeyVO, byte[] 
passphrase) {
+        List<VolumeVO> volumes = 
volumeDao.findByKmsWrappedKeyId(wrappedKeyVO.getId());
+        List<VolumeVO> createdAfterMigration = volumes.stream()
+                .filter(volume -> 
volume.getCreated().after(wrappedKeyVO.getCreated()))

Review Comment:
   The creation-time guard excludes removed and expunged volumes, including 
4.23 storage-migration copies whose snapshots still need the double-encoded 
passphrase. Repairing their shared key changes that passphrase and makes those 
snapshots unreadable. Include historical volume references with non-destroyed 
snapshots in this guard, while ignoring historical copies that no longer need 
the key.



##########
engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeObject.java:
##########
@@ -995,20 +1008,26 @@ public void 
doInTransactionWithoutResult(TransactionStatus status) {
     public byte[] getPassphrase() {
         // First check for KMS-encrypted volume
         if (volumeVO.getKmsWrappedKeyId() != null) {
+            byte[] dekBytes;
             try {
                 // Unwrap the DEK from KMS (returns raw binary bytes)
-                byte[] dekBytes = 
kmsManager.unwrapKey(volumeVO.getKmsWrappedKeyId());
-                // Base64-encode the DEK for consistency with legacy 
passphrases
-                // and for use with qemu-img which expects base64 format
-                String base64Dek = 
java.util.Base64.getEncoder().encodeToString(dekBytes);
-                // Zeroize the raw DEK bytes
-                java.util.Arrays.fill(dekBytes, (byte) 0);
-                // Return UTF-8 bytes of the base64 string
-                return 
base64Dek.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+                dekBytes = kmsManager.unwrapKey(volumeVO.getKmsWrappedKeyId());
             } catch (KMSException e) {
+                if (volumeVO.getRemoved() != null && e.getErrorType() == 
KMSException.ErrorType.KEK_NOT_FOUND) {
+                    // The key of a removed volume can be gone, e.g. released 
by an older release while snapshots of
+                    // the volume still existed. Deleting such a snapshot 
doesn't need the key, so don't fail it.
+                    // Other errors (e.g. the HSM is unreachable) must not 
look like an unencrypted volume.

Review Comment:
   When a KEK is missing from the HSM but its database rows remain, 
`unwrapKey()` converts the provider's `KEK_NOT_FOUND` into 
`WRAP_UNWRAP_FAILED`, even for CBC where no fallback is attempted. This 
bypasses the removed-volume fallback here, so snapshot deletion still fails and 
format repair keeps retrying the missing key. Preserve the missing-key 
classification when all usable candidates are definitively missing, without 
masking transient failures, and cover this path through the manager.



##########
server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java:
##########
@@ -773,6 +837,40 @@ String rotateKek(KMSKeyVO kmsKey, String oldKekLabel, 
String newKekLabel, int ke
         }
     }
 
+    /**
+     * Wraps and unwraps a random key with a newly created KEK, so an HSM that 
doesn't support the profile's wrap
+     * algorithm (e.g. GCM with a caller-supplied IV in FIPS mode) fails here 
rather than at the first volume
+     * creation or rewrap. On failure the KEK is deleted again.
+     */
+    private void verifyKekCanWrap(KMSProvider provider, KeyPurpose purpose, 
String kekLabel, HSMProfileVO profile) {
+        String algorithm = null;
+        byte[] testKey = new byte[32];
+        new SecureRandom().nextBytes(testKey);
+        byte[] unwrapped = null;
+        try {
+            algorithm = provider.getWrapAlgorithm(profile.getId());
+            WrappedKey wrapped = provider.wrapKey(testKey, purpose, kekLabel, 
profile.getId());
+            unwrapped = provider.unwrapKey(wrapped, profile.getId());
+            if (!Arrays.equals(testKey, unwrapped)) {
+                throw KMSException.wrapUnwrapFailed("unwrapped key does not 
match the original");
+            }
+        } catch (Exception e) {
+            try {
+                provider.deleteKek(kekLabel, profile.getId());

Review Comment:
   These new HSM calls execute directly on the create/rotate request thread, 
bypassing kms.operation.timeout.sec and the bounded retryOperation() executor 
used for KEK creation. If the token stalls during the wrap test, creating or 
rotating a key can now hang indefinitely; the direct deletion in the failure 
handler can hang as well. Apply the existing timeout mechanism to the probe and 
its cleanup.



##########
server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java:
##########
@@ -1781,12 +1899,174 @@ void rewrapSingleKey(KMSWrappedKeyVO wrappedKeyVO, 
KMSKeyVO kmsKey,
         }
     }
 
+    /**
+     * 4.23 migrated legacy volume passphrases to KMS as their base64 text 
instead of their raw bytes, so those
+     * volumes got a wrong passphrase. The upgrade flags every wrapped key 
that existed at that point; this task
+     * checks each one once and rewraps the affected ones with the raw bytes. 
A key that can't be unwrapped (e.g. the
+     * HSM is down) stays flagged and is retried. The task stops once no 
flagged key is left.
+     */
+    private void scheduleKeyFormatRepair() {
+        if (kmsWrappedKeyDao.listFormatCheckPending(0, 1).isEmpty()) {
+            return;
+        }
+        ScheduledExecutorService executor = 
Executors.newSingleThreadScheduledExecutor(r -> {
+            Thread t = new Thread(r, "KMSKeyFormatRepair");
+            t.setDaemon(true);
+            return t;
+        });
+        keyFormatRepairExecutor = executor;
+        executor.scheduleWithFixedDelay(new ManagedContextRunnable() {
+            @Override
+            protected void runInContext() {
+                try {
+                    if (repairKeyFormatBatch()) {
+                        logger.info("All KMS wrapped keys flagged at the 
upgrade from 4.23 have been checked");
+                        executor.shutdown();
+                    }
+                } catch (final Exception e) {
+                    logger.error("Error while checking the format of KMS 
wrapped keys", e);
+                }
+            }
+        }, 30, 60, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Checks the next batch of flagged keys. A batch ends at the first key 
that can't be checked (e.g. its HSM is
+     * down), so the lock shared with the rewrap worker isn't held through 
retries; the next batch resumes after that
+     * key so it doesn't block the others.
+     *
+     * @return true when no flagged key is left
+     */
+    boolean repairKeyFormatBatch() {
+        GlobalLock lock = GlobalLock.getInternLock("kms.rewrap.worker");
+        try {
+            if (!lock.lock(5)) {
+                return false;
+            }
+            try {
+                List<KMSWrappedKeyVO> keys = 
kmsWrappedKeyDao.listFormatCheckPending(keyFormatRepairCursor,
+                        KMSRewrapBatchSize.value());
+                if (keys.isEmpty()) {
+                    boolean done = keyFormatRepairCursor == 0;
+                    keyFormatRepairCursor = 0;
+                    return done;
+                }
+                for (KMSWrappedKeyVO key : keys) {
+                    keyFormatRepairCursor = key.getId();
+                    try {
+                        repairKeyFormat(key);
+                    } catch (Exception e) {
+                        logger.warn("Could not check the format of KMS wrapped 
key {}, will retry: {}", key, e.getMessage());
+                        logger.debug("Failed to check the format of KMS 
wrapped key {}", key, e);
+                        break;
+                    }
+                }
+                return false;
+            } finally {
+                lock.unlock();
+            }
+        } finally {
+            lock.releaseRef();
+        }
+    }
+
+    private void repairKeyFormat(KMSWrappedKeyVO wrappedKeyVO) {
+        byte[] key;
+        try {
+            key = unwrapKey(wrappedKeyVO.getId());
+        } catch (KMSException e) {
+            if (e.getErrorType() != KMSException.ErrorType.KEK_NOT_FOUND) {
+                throw e;
+            }
+            // Gone for good, e.g. its KMS key or KEK was deleted, so there is 
nothing to repair
+            logger.warn("KMS wrapped key {} can't be unwrapped anymore, not 
checking its format: {}", wrappedKeyVO,
+                    e.getMessage());
+            markFormatChecked(wrappedKeyVO);
+            return;
+        }
+        byte[] passphrase = decodeLegacyMigratedKey(key);
+        try {
+            if (passphrase != null) {
+                rewrapLegacyMigratedKey(wrappedKeyVO, passphrase);
+            }
+            markFormatChecked(wrappedKeyVO);
+        } finally {
+            Arrays.fill(key, (byte) 0);
+            if (passphrase != null) {
+                Arrays.fill(passphrase, (byte) 0);
+            }
+        }
+    }
+
+    private void rewrapLegacyMigratedKey(KMSWrappedKeyVO wrappedKeyVO, byte[] 
passphrase) {
+        List<VolumeVO> volumes = 
volumeDao.findByKmsWrappedKeyId(wrappedKeyVO.getId());
+        List<VolumeVO> createdAfterMigration = volumes.stream()
+                .filter(volume -> 
volume.getCreated().after(wrappedKeyVO.getCreated()))
+                .collect(Collectors.toList());
+        if (!createdAfterMigration.isEmpty()) {
+            // e.g. the copy made by a storage migration on 4.23, which was 
encrypted with the passphrase 4.23 derived
+            // from this key. Repairing the key would make such a volume 
unreadable, so leave it to the admin.
+            logger.warn("Not repairing KMS wrapped key {}, which 4.23 migrated 
in a wrong format: volume(s) {} were "
+                    + "created after the migration and may be encrypted with 
the passphrase 4.23 used. Check these "
+                    + "volumes manually.", wrappedKeyVO, 
createdAfterMigration);
+            return;
+        }
+
+        KMSKeyVO kmsKey = kmsKeyDao.findById(wrappedKeyVO.getKmsKeyId());
+        KMSKekVersionVO activeVersion = getActiveKekVersion(kmsKey.getId());
+        HSMProfileVO hsmProfile = 
hsmProfileDao.findById(activeVersion.getHsmProfileId());
+        WrappedKey rewrapped = 
getKMSProvider(hsmProfile.getProtocol()).wrapKey(passphrase,
+                kmsKey.getPurpose(), activeVersion.getKekLabel(), 
activeVersion.getHsmProfileId());

Review Comment:
   The repair unwrap is timeout-protected, but this wrap calls the HSM 
synchronously while repairKeyFormatBatch() holds the cluster-wide 
kms.rewrap.worker lock. A token that stops responding during encryption can 
therefore hold that lock indefinitely, blocking both format repair and normal 
KEK rewrapping on every management server. Route this operation through 
retryOperation() as well so a failed batch can release the lock.



##########
plugins/kms/pkcs11/src/main/java/org/apache/cloudstack/kms/provider/pkcs11/PKCS11HSMProvider.java:
##########
@@ -925,60 +979,112 @@ private void validateKeySize(int keyBits) throws 
KMSException {
             }
         }
 
+        /**
+         * Generates random bytes with the token's random number generator 
(C_GenerateRandom), so that keys come from
+         * the HSM. Falls back to the JVM's {@link SecureRandom} if the token 
has no random number generator.
+         *
+         * @param length Number of bytes to generate
+         * @return Random bytes; the caller zeroizes them once they're no 
longer needed
+         */
+        byte[] generateRandom(int length) {
+            SecureRandom random;
+            try {
+                random = SecureRandom.getInstance("PKCS11", provider);
+            } catch (NoSuchAlgorithmException e) {
+                // SunPKCS11 only offers SecureRandom when the token reports a 
random number generator
+                logger.debug("HSM token has no random number generator, using 
the JVM's SecureRandom");
+                random = SECURE_RANDOM;
+            }
+            byte[] bytes = new byte[length];
+            random.nextBytes(bytes);
+            return bytes;

Review Comment:
   Token random-generation failures from `SecureRandom.nextBytes()` escape as 
`ProviderException`, bypassing the `KMSException` recovery in 
`executeWithSession()`. After errors such as `CKR_DEVICE_ERROR`, retries can 
reuse stale provider state because pool invalidation and the restart counter 
update are skipped. Route the exception through `handlePKCS11Exception()` and 
wipe the partially generated buffer on failure.



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