mirror of https://github.com/apache/cloudstack.git
Add some tests
This commit is contained in:
parent
6abcffa9d7
commit
0d39a7b0be
|
|
@ -315,4 +315,6 @@
|
|||
<bean id="kmsKeyDaoImpl" class="org.apache.cloudstack.kms.dao.KMSKeyDaoImpl" />
|
||||
<bean id="kmsKekVersionDaoImpl" class="org.apache.cloudstack.kms.dao.KMSKekVersionDaoImpl" />
|
||||
<bean id="kmsWrappedKeyDaoImpl" class="org.apache.cloudstack.kms.dao.KMSWrappedKeyDaoImpl" />
|
||||
<bean id="hsmProfileDaoImpl" class="org.apache.cloudstack.kms.dao.HSMProfileDaoImpl" />
|
||||
<bean id="hsmProfileDetailsDaoImpl" class="org.apache.cloudstack.kms.dao.HSMProfileDetailsDaoImpl" />
|
||||
</beans>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public class KMSException extends CloudRuntimeException {
|
|||
* Error types for KMS operations to enable intelligent retry logic
|
||||
*/
|
||||
public enum ErrorType {
|
||||
CONNECTION_FAILED(true),
|
||||
/**
|
||||
* Provider not initialized or unavailable
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import org.apache.cloudstack.framework.config.Configurable;
|
|||
|
||||
import com.cloud.utils.component.Adapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abstract provider contract for Key Management Service operations.
|
||||
* <p>
|
||||
|
|
@ -83,14 +81,6 @@ public interface KMSProvider extends Configurable, Adapter {
|
|||
*/
|
||||
void deleteKek(String kekId) throws KMSException;
|
||||
|
||||
/**
|
||||
* List all KEK identifiers for a given purpose
|
||||
*
|
||||
* @param purpose the key purpose to filter by (null = all purposes)
|
||||
* @return list of KEK identifiers
|
||||
* @throws KMSException if listing fails
|
||||
*/
|
||||
List<String> listKeks(KeyPurpose purpose) throws KMSException;
|
||||
|
||||
/**
|
||||
* Check if a KEK exists and is accessible
|
||||
|
|
|
|||
|
|
@ -183,31 +183,6 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listKeks(KeyPurpose purpose) throws KMSException {
|
||||
try {
|
||||
List<String> keks = new ArrayList<>();
|
||||
|
||||
List<KMSDatabaseKekObjectVO> kekObjects;
|
||||
if (purpose != null) {
|
||||
kekObjects = kekObjectDao.listByPurpose(purpose);
|
||||
} else {
|
||||
kekObjects = kekObjectDao.listAll();
|
||||
}
|
||||
|
||||
for (KMSDatabaseKekObjectVO kekObject : kekObjects) {
|
||||
if (kekObject.getRemoved() == null) {
|
||||
keks.add(kekObject.getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("listKeks called for purpose: {}. Found {} KEKs.", purpose, keks.size());
|
||||
return keks;
|
||||
} catch (Exception e) {
|
||||
throw KMSException.kekOperationFailed("Failed to list KEKs: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKekAvailable(String kekId) throws KMSException {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -54,19 +54,19 @@ import com.cloud.utils.crypt.DBEncryptionUtil;
|
|||
public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
||||
private static final Logger logger = LogManager.getLogger(PKCS11HSMProvider.class);
|
||||
private static final String PROVIDER_NAME = "pkcs11";
|
||||
|
||||
|
||||
@Inject
|
||||
private HSMProfileDao hsmProfileDao;
|
||||
|
||||
|
||||
@Inject
|
||||
private HSMProfileDetailsDao hsmProfileDetailsDao;
|
||||
|
||||
|
||||
@Inject
|
||||
private KMSKekVersionDao kmsKekVersionDao;
|
||||
|
||||
// Session pool per HSM profile
|
||||
private final Map<Long, HSMSessionPool> sessionPools = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
// Profile configuration caching
|
||||
private final Map<Long, Map<String, String>> profileConfigCache = new ConcurrentHashMap<>();
|
||||
|
||||
|
|
@ -80,6 +80,16 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
return PROVIDER_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The name of the component that provided this configuration
|
||||
* variable. This value is saved in the database so someone can easily
|
||||
* identify who provides this variable.
|
||||
**/
|
||||
@Override
|
||||
public String getConfigComponentName() {
|
||||
return PKCS11HSMProvider.class.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigKey<?>[] getConfigKeys() {
|
||||
return new ConfigKey<?>[0];
|
||||
|
|
@ -90,7 +100,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
if (hsmProfileId == null) {
|
||||
throw KMSException.invalidParameter("HSM Profile ID is required for PKCS#11 provider");
|
||||
}
|
||||
|
||||
|
||||
if (StringUtils.isEmpty(label)) {
|
||||
label = generateKekLabel(purpose);
|
||||
}
|
||||
|
|
@ -142,14 +152,14 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel, Long targetHsmProfileId) throws KMSException {
|
||||
// 1. Unwrap with old KEK
|
||||
byte[] plainKey = unwrapKey(oldWrappedKey, null); // Auto-resolve old profile
|
||||
|
||||
|
||||
try {
|
||||
// 2. Wrap with new KEK
|
||||
Long profileId = targetHsmProfileId;
|
||||
if (profileId == null) {
|
||||
profileId = resolveProfileId(newKekLabel);
|
||||
}
|
||||
|
||||
|
||||
return wrapKey(plainKey, oldWrappedKey.getPurpose(), newKekLabel, profileId);
|
||||
} finally {
|
||||
// Zeroize plaintext key
|
||||
|
|
@ -183,16 +193,11 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listKeks(KeyPurpose purpose) throws KMSException {
|
||||
throw new KMSException(KMSException.ErrorType.OPERATION_FAILED, "Listing KEKs directly from HSMs not supported, use DB");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKekAvailable(String kekId) throws KMSException {
|
||||
Long hsmProfileId = resolveProfileId(kekId);
|
||||
if (hsmProfileId == null) return false;
|
||||
|
||||
|
||||
HSMSessionPool pool = getSessionPool(hsmProfileId);
|
||||
PKCS11Session session = null;
|
||||
try {
|
||||
|
|
@ -210,7 +215,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
return true;
|
||||
}
|
||||
|
||||
private Long resolveProfileId(String kekLabel) throws KMSException {
|
||||
Long resolveProfileId(String kekLabel) throws KMSException {
|
||||
KMSKekVersionVO version = kmsKekVersionDao.findByKekLabel(kekLabel);
|
||||
if (version != null && version.getHsmProfileId() != null) {
|
||||
return version.getHsmProfileId();
|
||||
|
|
@ -218,12 +223,12 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
throw new KMSException(KMSException.ErrorType.KEK_NOT_FOUND, "Could not resolve HSM profile for KEK: " + kekLabel);
|
||||
}
|
||||
|
||||
private HSMSessionPool getSessionPool(Long profileId) {
|
||||
return sessionPools.computeIfAbsent(profileId,
|
||||
HSMSessionPool getSessionPool(Long profileId) {
|
||||
return sessionPools.computeIfAbsent(profileId,
|
||||
id -> new HSMSessionPool(id, loadProfileConfig(id)));
|
||||
}
|
||||
|
||||
private Map<String, String> loadProfileConfig(Long profileId) {
|
||||
Map<String, String> loadProfileConfig(Long profileId) {
|
||||
return profileConfigCache.computeIfAbsent(profileId, id -> {
|
||||
List<HSMProfileDetailsVO> details = hsmProfileDetailsDao.listByProfileId(id);
|
||||
Map<String, String> config = new HashMap<>();
|
||||
|
|
@ -238,14 +243,14 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
});
|
||||
}
|
||||
|
||||
private boolean isSensitiveKey(String key) {
|
||||
return key.equalsIgnoreCase("pin") ||
|
||||
key.equalsIgnoreCase("password") ||
|
||||
boolean isSensitiveKey(String key) {
|
||||
return key.equalsIgnoreCase("pin") ||
|
||||
key.equalsIgnoreCase("password") ||
|
||||
key.toLowerCase().contains("secret") ||
|
||||
key.equalsIgnoreCase("private_key");
|
||||
}
|
||||
|
||||
private String generateKekLabel(KeyPurpose purpose) {
|
||||
String generateKekLabel(KeyPurpose purpose) {
|
||||
return purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
|
||||
|
|
@ -256,14 +261,14 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
private final Map<String, String> config;
|
||||
private final int maxSessions;
|
||||
private final int minIdleSessions;
|
||||
|
||||
|
||||
HSMSessionPool(Long profileId, Map<String, String> config) {
|
||||
this.profileId = profileId;
|
||||
this.config = config;
|
||||
this.maxSessions = Integer.parseInt(config.getOrDefault("max_sessions", "10"));
|
||||
this.minIdleSessions = Integer.parseInt(config.getOrDefault("min_idle_sessions", "2"));
|
||||
this.availableSessions = new ArrayBlockingQueue<>(maxSessions);
|
||||
|
||||
|
||||
// Pre-warm
|
||||
for (int i = 0; i < minIdleSessions; i++) {
|
||||
try {
|
||||
|
|
@ -273,7 +278,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PKCS11Session acquireSession(long timeoutMs) throws KMSException {
|
||||
try {
|
||||
PKCS11Session session = availableSessions.poll();
|
||||
|
|
@ -288,7 +293,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED, "Failed to acquire HSM session", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void releaseSession(PKCS11Session session) {
|
||||
if (session != null && session.isValid()) {
|
||||
if (!availableSessions.offer(session)) {
|
||||
|
|
@ -296,7 +301,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private PKCS11Session createNewSession() throws KMSException {
|
||||
return new PKCS11Session(config);
|
||||
}
|
||||
|
|
@ -307,12 +312,12 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
private final Map<String, String> config;
|
||||
private KeyStore keyStore;
|
||||
private Provider provider;
|
||||
|
||||
|
||||
PKCS11Session(Map<String, String> config) throws KMSException {
|
||||
this.config = config;
|
||||
connect();
|
||||
}
|
||||
|
||||
|
||||
private void connect() throws KMSException {
|
||||
try {
|
||||
String libraryPath = config.get("library_path");
|
||||
|
|
@ -324,33 +329,33 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
|
|||
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED, "Failed to connect to HSM: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
boolean isValid() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void close() {
|
||||
if (provider != null) {
|
||||
Security.removeProvider(provider.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String generateKey(String label, int keyBits, KeyPurpose purpose) throws KMSException {
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
byte[] wrapKey(byte[] plainDek, String kekLabel) throws KMSException {
|
||||
return "wrapped_blob".getBytes();
|
||||
}
|
||||
|
||||
|
||||
byte[] unwrapKey(byte[] wrappedBlob, String kekLabel) throws KMSException {
|
||||
return new byte[32]; // 256 bits
|
||||
}
|
||||
|
||||
|
||||
void deleteKey(String label) throws KMSException {
|
||||
// Stub
|
||||
}
|
||||
|
||||
|
||||
boolean checkKeyExists(String label) throws KMSException {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,287 @@
|
|||
// 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.cloudstack.kms.provider.pkcs11;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cloudstack.framework.kms.KMSException;
|
||||
import org.apache.cloudstack.framework.kms.KeyPurpose;
|
||||
import org.apache.cloudstack.kms.HSMProfileDetailsVO;
|
||||
import org.apache.cloudstack.kms.KMSKekVersionVO;
|
||||
import org.apache.cloudstack.kms.dao.HSMProfileDetailsDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSKekVersionDao;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for PKCS11HSMProvider
|
||||
* Tests provider-specific logic: config loading, profile resolution, sensitive key detection
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class PKCS11HSMProviderTest {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private PKCS11HSMProvider provider;
|
||||
|
||||
@Mock
|
||||
private HSMProfileDetailsDao hsmProfileDetailsDao;
|
||||
|
||||
@Mock
|
||||
private KMSKekVersionDao kmsKekVersionDao;
|
||||
|
||||
private Long testProfileId = 1L;
|
||||
private String testKekLabel = "test-kek-label";
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// Minimal setup
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveProfileId successfully finds profile from KEK label
|
||||
*/
|
||||
@Test
|
||||
public void testResolveProfileId_FindsFromKekLabel() throws KMSException {
|
||||
// Setup: KEK version with profile ID
|
||||
KMSKekVersionVO kekVersion = mock(KMSKekVersionVO.class);
|
||||
when(kekVersion.getHsmProfileId()).thenReturn(testProfileId);
|
||||
when(kmsKekVersionDao.findByKekLabel(testKekLabel)).thenReturn(kekVersion);
|
||||
|
||||
// Test
|
||||
Long result = provider.resolveProfileId(testKekLabel);
|
||||
|
||||
// Verify
|
||||
assertNotNull("Should return profile ID", result);
|
||||
assertEquals("Should return correct profile ID", testProfileId, result);
|
||||
verify(kmsKekVersionDao).findByKekLabel(testKekLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveProfileId throws exception when KEK version not found
|
||||
*/
|
||||
@Test(expected = KMSException.class)
|
||||
public void testResolveProfileId_ThrowsExceptionWhenVersionNotFound() throws KMSException {
|
||||
// Setup: No KEK version found
|
||||
when(kmsKekVersionDao.findByKekLabel(testKekLabel)).thenReturn(null);
|
||||
|
||||
// Test - should throw exception
|
||||
provider.resolveProfileId(testKekLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveProfileId throws exception when profile ID is null
|
||||
*/
|
||||
@Test(expected = KMSException.class)
|
||||
public void testResolveProfileId_ThrowsExceptionWhenProfileIdNull() throws KMSException {
|
||||
// Setup: KEK version exists but has null profile ID
|
||||
KMSKekVersionVO kekVersion = mock(KMSKekVersionVO.class);
|
||||
when(kekVersion.getHsmProfileId()).thenReturn(null);
|
||||
when(kmsKekVersionDao.findByKekLabel(testKekLabel)).thenReturn(kekVersion);
|
||||
|
||||
// Test - should throw exception
|
||||
provider.resolveProfileId(testKekLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: loadProfileConfig loads and decrypts sensitive values
|
||||
*/
|
||||
@Test
|
||||
public void testLoadProfileConfig_DecryptsSensitiveValues() {
|
||||
// Setup: Profile details with encrypted pin
|
||||
HSMProfileDetailsVO detail1 = mock(HSMProfileDetailsVO.class);
|
||||
when(detail1.getName()).thenReturn("library_path");
|
||||
when(detail1.getValue()).thenReturn("/path/to/lib.so");
|
||||
|
||||
HSMProfileDetailsVO detail2 = mock(HSMProfileDetailsVO.class);
|
||||
when(detail2.getName()).thenReturn("pin");
|
||||
when(detail2.getValue()).thenReturn("ENC(encrypted_pin)");
|
||||
|
||||
HSMProfileDetailsVO detail3 = mock(HSMProfileDetailsVO.class);
|
||||
when(detail3.getName()).thenReturn("slot_id");
|
||||
when(detail3.getValue()).thenReturn("0");
|
||||
|
||||
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(
|
||||
Arrays.asList(detail1, detail2, detail3));
|
||||
|
||||
// Test
|
||||
Map<String, String> config = provider.loadProfileConfig(testProfileId);
|
||||
|
||||
// Verify
|
||||
assertNotNull("Config should not be null", config);
|
||||
assertEquals(3, config.size());
|
||||
assertEquals("/path/to/lib.so", config.get("library_path"));
|
||||
// Note: In real code, DBEncryptionUtil.decrypt would be called
|
||||
// Here we just verify the structure is correct
|
||||
assertTrue("Config should contain pin", config.containsKey("pin"));
|
||||
assertEquals("0", config.get("slot_id"));
|
||||
|
||||
verify(hsmProfileDetailsDao).listByProfileId(testProfileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: loadProfileConfig handles empty details
|
||||
*/
|
||||
@Test
|
||||
public void testLoadProfileConfig_HandlesEmptyDetails() {
|
||||
// Setup
|
||||
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList());
|
||||
|
||||
// Test
|
||||
Map<String, String> config = provider.loadProfileConfig(testProfileId);
|
||||
|
||||
// Verify
|
||||
assertNotNull("Config should not be null", config);
|
||||
assertEquals(0, config.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey correctly identifies sensitive keys
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_IdentifiesSensitiveKeys() {
|
||||
// Test
|
||||
assertTrue(provider.isSensitiveKey("pin"));
|
||||
assertTrue(provider.isSensitiveKey("password"));
|
||||
assertTrue(provider.isSensitiveKey("api_secret"));
|
||||
assertTrue(provider.isSensitiveKey("private_key"));
|
||||
assertTrue(provider.isSensitiveKey("PIN")); // Case-insensitive
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey correctly identifies non-sensitive keys
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_IdentifiesNonSensitiveKeys() {
|
||||
// Test
|
||||
assertFalse(provider.isSensitiveKey("library_path"));
|
||||
assertFalse(provider.isSensitiveKey("slot_id"));
|
||||
assertFalse(provider.isSensitiveKey("endpoint"));
|
||||
assertFalse(provider.isSensitiveKey("max_sessions"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: generateKekLabel creates valid label
|
||||
*/
|
||||
@Test
|
||||
public void testGenerateKekLabel_CreatesValidLabel() {
|
||||
// Test
|
||||
String label = provider.generateKekLabel(KeyPurpose.VOLUME_ENCRYPTION);
|
||||
|
||||
// Verify
|
||||
assertNotNull("Label should not be null", label);
|
||||
assertTrue("Label should start with purpose", label.startsWith(KeyPurpose.VOLUME_ENCRYPTION.getName()));
|
||||
assertTrue("Label should contain UUID", label.length() > (KeyPurpose.VOLUME_ENCRYPTION.getName() + "-kek-").length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: getProviderName returns correct name
|
||||
*/
|
||||
@Test
|
||||
public void testGetProviderName() {
|
||||
assertEquals("pkcs11", provider.getProviderName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createKek requires hsmProfileId
|
||||
*/
|
||||
@Test(expected = KMSException.class)
|
||||
public void testCreateKek_RequiresProfileId() throws KMSException {
|
||||
provider.createKek(
|
||||
KeyPurpose.VOLUME_ENCRYPTION,
|
||||
"test-label",
|
||||
256,
|
||||
null // null profile ID should throw exception
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: loadProfileConfig caches configuration
|
||||
*/
|
||||
@Test
|
||||
public void testLoadProfileConfig_CachesConfiguration() {
|
||||
// Setup
|
||||
HSMProfileDetailsVO detail = mock(HSMProfileDetailsVO.class);
|
||||
when(detail.getName()).thenReturn("library_path");
|
||||
when(detail.getValue()).thenReturn("/path/to/lib.so");
|
||||
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList(detail));
|
||||
|
||||
// Load twice
|
||||
provider.loadProfileConfig(testProfileId);
|
||||
provider.loadProfileConfig(testProfileId);
|
||||
|
||||
// DAO should only be called once due to caching
|
||||
verify(hsmProfileDetailsDao, times(1)).listByProfileId(testProfileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: getSessionPool creates pool for new profile
|
||||
*/
|
||||
@Test
|
||||
public void testGetSessionPool_CreatesPoolForNewProfile() {
|
||||
// Setup
|
||||
HSMProfileDetailsVO detail = mock(HSMProfileDetailsVO.class);
|
||||
when(detail.getName()).thenReturn("library_path");
|
||||
when(detail.getValue()).thenReturn("/path/to/lib.so");
|
||||
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList(detail));
|
||||
|
||||
// Test
|
||||
Object pool = provider.getSessionPool(testProfileId);
|
||||
|
||||
// Verify
|
||||
assertNotNull("Pool should be created", pool);
|
||||
verify(hsmProfileDetailsDao).listByProfileId(testProfileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: getSessionPool reuses pool for same profile
|
||||
*/
|
||||
@Test
|
||||
public void testGetSessionPool_ReusesPoolForSameProfile() {
|
||||
// Setup
|
||||
HSMProfileDetailsVO detail = mock(HSMProfileDetailsVO.class);
|
||||
when(detail.getName()).thenReturn("library_path");
|
||||
when(detail.getValue()).thenReturn("/path/to/lib.so");
|
||||
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList(detail));
|
||||
|
||||
// Test
|
||||
Object pool1 = provider.getSessionPool(testProfileId);
|
||||
Object pool2 = provider.getSessionPool(testProfileId);
|
||||
|
||||
// Verify
|
||||
assertNotNull("Pool should be created", pool1);
|
||||
assertEquals("Should reuse same pool", pool1, pool2);
|
||||
// Config should only be loaded once
|
||||
verify(hsmProfileDetailsDao, times(1)).listByProfileId(testProfileId);
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +81,6 @@ import java.util.stream.Collectors;
|
|||
public class KMSManagerImpl extends ManagerBase implements KMSManager, PluggableService {
|
||||
private static final Logger logger = LogManager.getLogger(KMSManagerImpl.class);
|
||||
private static final Map<String, KMSProvider> kmsProviderMap = new HashMap<>();
|
||||
private static KMSProvider configuredKmsProvider;
|
||||
@Inject
|
||||
private KMSWrappedKeyDao kmsWrappedKeyDao;
|
||||
@Inject
|
||||
|
|
@ -147,7 +146,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
/**
|
||||
* Internal method to rotate a KEK (create new version and update KMS key state)
|
||||
*/
|
||||
private String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
|
||||
String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
|
||||
String newKekLabel, int keyBits, Long newProfileId) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
||||
|
|
@ -235,7 +234,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
return createUserKMSKey(accountId, domainId, zoneId, name, description, purpose, keyBits, null);
|
||||
}
|
||||
|
||||
private KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
|
||||
KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
|
||||
String name, String description, KeyPurpose purpose,
|
||||
Integer keyBits, String hsmProfileName) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
|
@ -291,7 +290,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
return kmsKey;
|
||||
}
|
||||
|
||||
private Long resolveHSMProfile(Long accountId, Long zoneId, String providerName) {
|
||||
Long resolveHSMProfile(Long accountId, Long zoneId, String providerName) {
|
||||
// Only applicable for providers that use profiles (pkcs11, kmip)
|
||||
if ("database".equalsIgnoreCase(providerName)) {
|
||||
return null;
|
||||
|
|
@ -326,7 +325,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
throw new CloudRuntimeException("No suitable HSM profile found for provider " + providerName + " for account " + accountId);
|
||||
}
|
||||
|
||||
private boolean isProviderMatch(HSMProfileVO profile, String providerName) {
|
||||
boolean isProviderMatch(HSMProfileVO profile, String providerName) {
|
||||
// Simple mapping: PKCS11 -> pkcs11, KMIP -> kmip
|
||||
return profile.getProtocol().equalsIgnoreCase(providerName);
|
||||
}
|
||||
|
|
@ -790,7 +789,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
* @param newVersion the new KEK version to wrap with
|
||||
* @param provider the KMS provider
|
||||
*/
|
||||
private void rewrapSingleKey(KMSWrappedKeyVO wrappedKeyVO, KMSKeyVO kmsKey,
|
||||
void rewrapSingleKey(KMSWrappedKeyVO wrappedKeyVO, KMSKeyVO kmsKey,
|
||||
KMSKekVersionVO newVersion, KMSProvider provider) {
|
||||
byte[] dek = null;
|
||||
try {
|
||||
|
|
@ -1013,15 +1012,10 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
}
|
||||
|
||||
private KMSProvider getConfiguredKmsProvider() {
|
||||
if (configuredKmsProvider != null) {
|
||||
return configuredKmsProvider;
|
||||
}
|
||||
|
||||
String providerName = KMSProviderPlugin.value();
|
||||
String providerKey = providerName != null ? providerName.toLowerCase() : null;
|
||||
if (providerKey != null && kmsProviderMap.containsKey(providerKey) && kmsProviderMap.get(providerKey) != null) {
|
||||
configuredKmsProvider = kmsProviderMap.get(providerKey);
|
||||
return configuredKmsProvider;
|
||||
return kmsProviderMap.get(providerKey);
|
||||
}
|
||||
|
||||
throw new CloudRuntimeException("Failed to find default configured KMS provider plugin: " + providerName);
|
||||
|
|
@ -1063,12 +1057,13 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
|
||||
String configuredProviderName = KMSProviderPlugin.value();
|
||||
String providerKey = configuredProviderName != null ? configuredProviderName.toLowerCase() : null;
|
||||
KMSProvider provider = null;
|
||||
if (providerKey != null && kmsProviderMap.containsKey(providerKey)) {
|
||||
configuredKmsProvider = kmsProviderMap.get(providerKey);
|
||||
logger.info("Configured KMS provider: {}", configuredKmsProvider.getProviderName());
|
||||
provider = kmsProviderMap.get(providerKey);
|
||||
logger.info("Configured KMS provider: {}", provider.getProviderName());
|
||||
}
|
||||
|
||||
if (configuredKmsProvider == null) {
|
||||
if (provider == null) {
|
||||
logger.warn("No valid configured KMS provider found. KMS functionality will be unavailable.");
|
||||
// Don't fail - KMS is optional
|
||||
return true;
|
||||
|
|
@ -1076,11 +1071,11 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
|
||||
// Run health check on startup
|
||||
try {
|
||||
boolean healthy = configuredKmsProvider.healthCheck();
|
||||
boolean healthy = provider.healthCheck();
|
||||
if (healthy) {
|
||||
logger.info("KMS provider {} health check passed", configuredKmsProvider.getProviderName());
|
||||
logger.info("KMS provider {} health check passed", provider.getProviderName());
|
||||
} else {
|
||||
logger.warn("KMS provider {} health check failed", configuredKmsProvider.getProviderName());
|
||||
logger.warn("KMS provider {} health check failed", provider.getProviderName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("KMS provider health check error: {}", e.getMessage());
|
||||
|
|
@ -1301,6 +1296,11 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
cmdList.add(DeleteKMSKeyCmd.class);
|
||||
cmdList.add(RotateKMSKeyCmd.class);
|
||||
cmdList.add(MigrateVolumesToKMSCmd.class);
|
||||
cmdList.add(MigrateVolumesToKMSCmd.class);
|
||||
cmdList.add(AddHSMProfileCmd.class);
|
||||
cmdList.add(ListHSMProfilesCmd.class);
|
||||
cmdList.add(UpdateHSMProfileCmd.class);
|
||||
cmdList.add(DeleteHSMProfileCmd.class);
|
||||
|
||||
return cmdList;
|
||||
}
|
||||
|
|
@ -1314,7 +1314,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
if (StringUtils.isEmpty(protocol)) {
|
||||
throw KMSException.invalidParameter("Protocol cannot be empty");
|
||||
}
|
||||
|
||||
|
||||
// Ensure provider exists for protocol
|
||||
try {
|
||||
getKMSProvider(protocol);
|
||||
|
|
@ -1330,25 +1330,25 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
cmd.getZoneId(),
|
||||
cmd.getVendorName()
|
||||
);
|
||||
|
||||
|
||||
// Persist profile
|
||||
profile = hsmProfileDao.persist(profile);
|
||||
|
||||
|
||||
// Persist details
|
||||
if (cmd.getDetails() != null) {
|
||||
for (Map.Entry<String, String> entry : cmd.getDetails().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
|
||||
// Encrypt sensitive values
|
||||
if (isSensitiveKey(key)) {
|
||||
value = DBEncryptionUtil.encrypt(value);
|
||||
}
|
||||
|
||||
|
||||
hsmProfileDetailsDao.persist(profile.getId(), key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
|
@ -1356,12 +1356,12 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
public List<HSMProfile> listHSMProfiles(ListHSMProfilesCmd cmd) {
|
||||
Long accountId = CallContext.current().getCallingAccount().getId();
|
||||
boolean isAdmin = accountManager.isAdmin(accountId);
|
||||
|
||||
|
||||
List<HSMProfile> result = new ArrayList<>();
|
||||
|
||||
|
||||
// 1. User's own profiles
|
||||
result.addAll(hsmProfileDao.listByAccountId(accountId));
|
||||
|
||||
|
||||
// 2. Admin provided profiles (global and zone-scoped)
|
||||
// If cmd filters by zone, use it. Else return all relevant ones.
|
||||
if (cmd.getZoneId() != null) {
|
||||
|
|
@ -1373,7 +1373,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
// How to list all zone-specific ones? listAdminProfiles() only gets globals?
|
||||
// Need a way to get all. For now simplified.
|
||||
}
|
||||
|
||||
|
||||
// Apply memory filtering for protocol and enabled status
|
||||
return result.stream()
|
||||
.filter(p -> cmd.getProtocol() == null || p.getProtocol().equalsIgnoreCase(cmd.getProtocol()))
|
||||
|
|
@ -1387,19 +1387,19 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
if (profile == null) {
|
||||
throw KMSException.invalidParameter("HSM Profile not found");
|
||||
}
|
||||
|
||||
|
||||
// Check permissions (handled by BaseCmd entity owner usually, but double check)
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
// Permission check logic here...
|
||||
|
||||
|
||||
// Check if in use by any KEK versions
|
||||
// Need a method in kmsKekVersionDao to count by profile ID
|
||||
// Assuming such logic exists or added:
|
||||
// if (kmsKekVersionDao.countByProfileId(profile.getId()) > 0) { ... }
|
||||
|
||||
|
||||
// Delete details
|
||||
hsmProfileDetailsDao.deleteDetails(profile.getId());
|
||||
|
||||
|
||||
// Delete profile
|
||||
return hsmProfileDao.remove(profile.getId());
|
||||
}
|
||||
|
|
@ -1410,31 +1410,31 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
if (profile == null) {
|
||||
throw KMSException.invalidParameter("HSM Profile not found");
|
||||
}
|
||||
|
||||
|
||||
if (cmd.getName() != null) {
|
||||
profile.setName(cmd.getName());
|
||||
}
|
||||
if (cmd.getEnabled() != null) {
|
||||
profile.setEnabled(cmd.getEnabled());
|
||||
}
|
||||
|
||||
|
||||
hsmProfileDao.update(profile.getId(), profile);
|
||||
|
||||
|
||||
if (cmd.getDetails() != null) {
|
||||
for (Map.Entry<String, String> entry : cmd.getDetails().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
|
||||
|
||||
// If sensitive, check if it's already encrypted (starts with ENC()) or needs encryption
|
||||
// Assuming client sends plaintext for updates usually.
|
||||
// Assuming client sends plaintext for updates usually.
|
||||
// Or if they send back the encrypted string from a previous list response, we should detect and keep it.
|
||||
// Simple heuristic: if isSensitiveKey and doesn't look encrypted (DBEncryptionUtil logic), encrypt it.
|
||||
// For now, simpler: always encrypt new sensitive values.
|
||||
|
||||
// For now, simpler: always encrypt new sensitive values.
|
||||
|
||||
if (isSensitiveKey(key)) {
|
||||
value = DBEncryptionUtil.encrypt(value);
|
||||
}
|
||||
|
||||
|
||||
HSMProfileDetailsVO detail = hsmProfileDetailsDao.findDetail(profile.getId(), key);
|
||||
if (detail != null) {
|
||||
detail.setValue(value);
|
||||
|
|
@ -1444,7 +1444,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
|
|
@ -1457,7 +1457,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
response.setVendorName(profile.getVendorName());
|
||||
response.setEnabled(profile.isEnabled());
|
||||
response.setCreated(profile.getCreated());
|
||||
|
||||
|
||||
if (profile.getAccountId() != null) {
|
||||
Account account = accountManager.getAccount(profile.getAccountId());
|
||||
if (account != null) {
|
||||
|
|
@ -1465,7 +1465,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
response.setAccountName(account.getAccountName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Populate details
|
||||
List<HSMProfileDetailsVO> details = hsmProfileDetailsDao.listByProfileId(profile.getId());
|
||||
Map<String, String> detailsMap = new HashMap<>();
|
||||
|
|
@ -1473,14 +1473,14 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
|
|||
detailsMap.put(detail.getName(), detail.getValue()); // Return encrypted values as-is
|
||||
}
|
||||
response.setDetails(detailsMap);
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private boolean isSensitiveKey(String key) {
|
||||
boolean isSensitiveKey(String key) {
|
||||
// List of keys known to be sensitive
|
||||
return key.equalsIgnoreCase("pin") ||
|
||||
key.equalsIgnoreCase("password") ||
|
||||
return key.equalsIgnoreCase("pin") ||
|
||||
key.equalsIgnoreCase("password") ||
|
||||
key.toLowerCase().contains("secret") ||
|
||||
key.equalsIgnoreCase("private_key");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,305 @@
|
|||
// 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.cloudstack.kms;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.cloudstack.api.response.HSMProfileResponse;
|
||||
import org.apache.cloudstack.kms.dao.HSMProfileDao;
|
||||
import org.apache.cloudstack.kms.dao.HSMProfileDetailsDao;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import com.cloud.exception.PermissionDeniedException;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
/**
|
||||
* Unit tests for HSM-related business logic in KMSManagerImpl
|
||||
* Tests sensitive key detection, profile resolution hierarchy, and provider matching
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class KMSManagerImplHSMTest {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private KMSManagerImpl kmsManager;
|
||||
|
||||
@Mock
|
||||
private HSMProfileDao hsmProfileDao;
|
||||
|
||||
@Mock
|
||||
private HSMProfileDetailsDao hsmProfileDetailsDao;
|
||||
|
||||
@Mock
|
||||
private AccountManager accountManager;
|
||||
|
||||
private Long testAccountId = 100L;
|
||||
private Long testZoneId = 1L;
|
||||
private String testProviderName = "pkcs11";
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey correctly identifies "pin" as sensitive
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_DetectsPin() {
|
||||
boolean result = kmsManager.isSensitiveKey("pin");
|
||||
assertTrue("'pin' should be detected as sensitive", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey correctly identifies "password" as sensitive
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_DetectsPassword() {
|
||||
boolean result = kmsManager.isSensitiveKey("password");
|
||||
assertTrue("'password' should be detected as sensitive", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey correctly identifies keys containing "secret" as sensitive
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_DetectsSecret() {
|
||||
boolean result = kmsManager.isSensitiveKey("api_secret");
|
||||
assertTrue("'api_secret' should be detected as sensitive", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey correctly identifies "private_key" as sensitive
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_DetectsPrivateKey() {
|
||||
boolean result = kmsManager.isSensitiveKey("private_key");
|
||||
assertTrue("'private_key' should be detected as sensitive", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey correctly identifies non-sensitive keys
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_DoesNotDetectNonSensitive() {
|
||||
boolean result = kmsManager.isSensitiveKey("library_path");
|
||||
assertFalse("'library_path' should not be detected as sensitive", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isSensitiveKey is case-insensitive
|
||||
*/
|
||||
@Test
|
||||
public void testIsSensitiveKey_CaseInsensitive() {
|
||||
boolean resultUpper = kmsManager.isSensitiveKey("PIN");
|
||||
boolean resultMixed = kmsManager.isSensitiveKey("Password");
|
||||
|
||||
assertTrue("'PIN' (uppercase) should be detected as sensitive", resultUpper);
|
||||
assertTrue("'Password' (mixed case) should be detected as sensitive", resultMixed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveHSMProfile selects user profile when available
|
||||
*/
|
||||
@Test
|
||||
public void testResolveHSMProfile_SelectsUserProfile() {
|
||||
// Setup: User has a profile
|
||||
HSMProfileVO userProfile = mock(HSMProfileVO.class);
|
||||
when(userProfile.getId()).thenReturn(1L);
|
||||
when(userProfile.isEnabled()).thenReturn(true);
|
||||
when(userProfile.getProtocol()).thenReturn(testProviderName);
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(Arrays.asList(userProfile));
|
||||
|
||||
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
|
||||
|
||||
assertNotNull("Should return user profile ID", result);
|
||||
assertEquals("Should select user profile", userProfile.getId(), result.longValue());
|
||||
verify(hsmProfileDao).listByAccountId(testAccountId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveHSMProfile falls back to zone admin profile when no user profile
|
||||
*/
|
||||
@Test
|
||||
public void testResolveHSMProfile_FallbackToZoneAdmin() {
|
||||
// Setup: No user profile, but zone admin profile exists
|
||||
HSMProfileVO zoneProfile = mock(HSMProfileVO.class);
|
||||
when(zoneProfile.getId()).thenReturn(2L);
|
||||
when(zoneProfile.isEnabled()).thenReturn(true);
|
||||
when(zoneProfile.getProtocol()).thenReturn(testProviderName);
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
|
||||
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(Arrays.asList(zoneProfile));
|
||||
|
||||
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
|
||||
|
||||
assertNotNull("Should return zone admin profile ID", result);
|
||||
assertEquals("Should select zone admin profile", zoneProfile.getId(), result.longValue());
|
||||
verify(hsmProfileDao).listByAccountId(testAccountId);
|
||||
verify(hsmProfileDao).listAdminProfiles(testZoneId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveHSMProfile falls back to global admin profile when no user or zone profile
|
||||
*/
|
||||
@Test
|
||||
public void testResolveHSMProfile_FallbackToGlobal() {
|
||||
// Setup: No user or zone profile, but global admin profile exists
|
||||
HSMProfileVO globalProfile = mock(HSMProfileVO.class);
|
||||
when(globalProfile.getId()).thenReturn(3L);
|
||||
when(globalProfile.isEnabled()).thenReturn(true);
|
||||
when(globalProfile.getProtocol()).thenReturn(testProviderName);
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
|
||||
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(new ArrayList<>());
|
||||
when(hsmProfileDao.listAdminProfiles()).thenReturn(Arrays.asList(globalProfile));
|
||||
|
||||
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
|
||||
|
||||
assertNotNull("Should return global admin profile ID", result);
|
||||
assertEquals("Should select global admin profile", globalProfile.getId(), result.longValue());
|
||||
verify(hsmProfileDao).listByAccountId(testAccountId);
|
||||
verify(hsmProfileDao).listAdminProfiles(testZoneId);
|
||||
verify(hsmProfileDao).listAdminProfiles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveHSMProfile throws exception when no profile found
|
||||
*/
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testResolveHSMProfile_ThrowsExceptionWhenNoneFound() {
|
||||
// Setup: No profiles at any level
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
|
||||
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(new ArrayList<>());
|
||||
when(hsmProfileDao.listAdminProfiles()).thenReturn(new ArrayList<>());
|
||||
|
||||
kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveHSMProfile skips disabled profiles
|
||||
*/
|
||||
@Test
|
||||
public void testResolveHSMProfile_SkipsDisabledProfiles() {
|
||||
// Setup: User has disabled profile, zone has enabled profile
|
||||
HSMProfileVO disabledProfile = mock(HSMProfileVO.class);
|
||||
when(disabledProfile.isEnabled()).thenReturn(false);
|
||||
|
||||
HSMProfileVO enabledZoneProfile = mock(HSMProfileVO.class);
|
||||
when(enabledZoneProfile.getId()).thenReturn(5L);
|
||||
when(enabledZoneProfile.isEnabled()).thenReturn(true);
|
||||
when(enabledZoneProfile.getProtocol()).thenReturn(testProviderName);
|
||||
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(Arrays.asList(disabledProfile));
|
||||
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(Arrays.asList(enabledZoneProfile));
|
||||
|
||||
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
|
||||
|
||||
assertNotNull("Should return zone profile ID (skip disabled)", result);
|
||||
assertEquals("Should select zone profile (not disabled user profile)", enabledZoneProfile.getId(), result.longValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: resolveHSMProfile returns null for database provider
|
||||
*/
|
||||
@Test
|
||||
public void testResolveHSMProfile_ReturnsNullForDatabaseProvider() {
|
||||
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, "database");
|
||||
|
||||
assertNull("Should return null for database provider", result);
|
||||
verify(hsmProfileDao, never()).listByAccountId(anyLong());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isProviderMatch correctly matches PKCS11 protocol
|
||||
*/
|
||||
@Test
|
||||
public void testIsProviderMatch_MatchesPKCS11() {
|
||||
HSMProfileVO profile = mock(HSMProfileVO.class);
|
||||
when(profile.getProtocol()).thenReturn("PKCS11");
|
||||
|
||||
boolean result = kmsManager.isProviderMatch(profile, "pkcs11");
|
||||
|
||||
assertTrue("Should match PKCS11 (case-insensitive)", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: isProviderMatch is case-insensitive
|
||||
*/
|
||||
@Test
|
||||
public void testIsProviderMatch_MatchesDifferentCases() {
|
||||
HSMProfileVO profile = mock(HSMProfileVO.class);
|
||||
when(profile.getProtocol()).thenReturn("pkcs11");
|
||||
|
||||
boolean resultUpper = kmsManager.isProviderMatch(profile, "PKCS11");
|
||||
boolean resultMixed = kmsManager.isProviderMatch(profile, "Pkcs11");
|
||||
|
||||
assertTrue("Should match PKCS11 (uppercase)", resultUpper);
|
||||
assertTrue("Should match Pkcs11 (mixed case)", resultMixed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createHSMProfileResponse populates details correctly
|
||||
*/
|
||||
@Test
|
||||
public void testCreateHSMProfileResponse_PopulatesDetails() {
|
||||
Long profileId = 10L;
|
||||
|
||||
HSMProfileVO profile = mock(HSMProfileVO.class);
|
||||
when(profile.getId()).thenReturn(profileId);
|
||||
when(profile.getUuid()).thenReturn("profile-uuid");
|
||||
when(profile.getName()).thenReturn("test-profile");
|
||||
when(profile.getProtocol()).thenReturn("PKCS11");
|
||||
when(profile.getAccountId()).thenReturn(testAccountId);
|
||||
when(profile.getVendorName()).thenReturn("TestVendor");
|
||||
when(profile.isEnabled()).thenReturn(true);
|
||||
when(profile.getCreated()).thenReturn(new java.util.Date());
|
||||
|
||||
HSMProfileDetailsVO detail1 = mock(HSMProfileDetailsVO.class);
|
||||
when(detail1.getName()).thenReturn("library_path");
|
||||
when(detail1.getValue()).thenReturn("/path/to/lib.so");
|
||||
|
||||
HSMProfileDetailsVO detail2 = mock(HSMProfileDetailsVO.class);
|
||||
when(detail2.getName()).thenReturn("pin");
|
||||
when(detail2.getValue()).thenReturn("ENC(encrypted_value)");
|
||||
|
||||
when(hsmProfileDetailsDao.listByProfileId(profileId)).thenReturn(Arrays.asList(detail1, detail2));
|
||||
|
||||
com.cloud.user.Account mockAccount = mock(com.cloud.user.Account.class);
|
||||
when(mockAccount.getUuid()).thenReturn("account-uuid");
|
||||
when(mockAccount.getAccountName()).thenReturn("testaccount");
|
||||
when(accountManager.getAccount(testAccountId)).thenReturn(mockAccount);
|
||||
|
||||
HSMProfileResponse response = kmsManager.createHSMProfileResponse(profile);
|
||||
|
||||
assertNotNull("Response should not be null", response);
|
||||
verify(accountManager).getAccount(testAccountId);
|
||||
verify(hsmProfileDetailsDao).listByProfileId(profileId);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
// 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.cloudstack.kms;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.apache.cloudstack.framework.kms.KMSException;
|
||||
import org.apache.cloudstack.framework.kms.KMSProvider;
|
||||
import org.apache.cloudstack.framework.kms.KeyPurpose;
|
||||
import org.apache.cloudstack.kms.dao.HSMProfileDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSKekVersionDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSKeyDao;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for KMS key creation logic in KMSManagerImpl
|
||||
* Tests key creation with explicit and auto-resolved HSM profiles
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class KMSManagerImplKeyCreationTest {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private KMSManagerImpl kmsManager;
|
||||
|
||||
@Mock
|
||||
private KMSKeyDao kmsKeyDao;
|
||||
|
||||
@Mock
|
||||
private KMSKekVersionDao kmsKekVersionDao;
|
||||
|
||||
@Mock
|
||||
private HSMProfileDao hsmProfileDao;
|
||||
|
||||
@Mock
|
||||
private KMSProvider kmsProvider;
|
||||
|
||||
private Long testAccountId = 100L;
|
||||
private Long testDomainId = 1L;
|
||||
private Long testZoneId = 1L;
|
||||
private String testProviderName = "pkcs11";
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// Setup provider
|
||||
when(kmsProvider.getProviderName()).thenReturn(testProviderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createUserKMSKey uses explicit HSM profile when provided
|
||||
*/
|
||||
@Test
|
||||
public void testCreateUserKMSKey_WithExplicitProfile() throws Exception {
|
||||
// Setup: Explicit profile name provided
|
||||
String hsmProfileName = "user-hsm-profile";
|
||||
Long hsmProfileId = 10L;
|
||||
|
||||
HSMProfileVO profile = mock(HSMProfileVO.class);
|
||||
when(profile.getId()).thenReturn(hsmProfileId);
|
||||
when(profile.getAccountId()).thenReturn(testAccountId);
|
||||
when(hsmProfileDao.findByName(hsmProfileName)).thenReturn(profile);
|
||||
|
||||
// Mock provider KEK creation
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(hsmProfileId)))
|
||||
.thenReturn("test-kek-label");
|
||||
|
||||
// Mock DAO persist operations
|
||||
KMSKeyVO mockKey = mock(KMSKeyVO.class);
|
||||
when(mockKey.getId()).thenReturn(1L);
|
||||
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
|
||||
|
||||
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
|
||||
|
||||
// Mock getKMSProviderForZone to return our mock provider
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
|
||||
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileName);
|
||||
|
||||
// Verify explicit profile was used
|
||||
assertNotNull(result);
|
||||
verify(hsmProfileDao).findByName(hsmProfileName);
|
||||
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(hsmProfileId));
|
||||
|
||||
// Verify KMSKeyVO was created with correct profile ID
|
||||
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
|
||||
verify(kmsKeyDao).persist(keyCaptor.capture());
|
||||
KMSKeyVO createdKey = keyCaptor.getValue();
|
||||
assertEquals(hsmProfileId, createdKey.getHsmProfileId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createUserKMSKey auto-resolves profile when not provided
|
||||
*/
|
||||
@Test
|
||||
public void testCreateUserKMSKey_AutoResolvesProfile() throws Exception {
|
||||
// Setup: No explicit profile name, should auto-resolve
|
||||
Long autoResolvedProfileId = 20L;
|
||||
|
||||
// Mock profile resolution hierarchy - user has a profile
|
||||
HSMProfileVO userProfile = mock(HSMProfileVO.class);
|
||||
when(userProfile.getId()).thenReturn(autoResolvedProfileId);
|
||||
when(userProfile.isEnabled()).thenReturn(true);
|
||||
when(userProfile.getProtocol()).thenReturn(testProviderName);
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(Arrays.asList(userProfile));
|
||||
|
||||
// Mock provider KEK creation
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(autoResolvedProfileId)))
|
||||
.thenReturn("test-kek-label");
|
||||
|
||||
// Mock DAO persist operations
|
||||
KMSKeyVO mockKey = mock(KMSKeyVO.class);
|
||||
when(mockKey.getId()).thenReturn(1L);
|
||||
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
|
||||
|
||||
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
|
||||
|
||||
// Mock getKMSProviderForZone
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
|
||||
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
|
||||
|
||||
// Verify profile was auto-resolved
|
||||
assertNotNull(result);
|
||||
verify(hsmProfileDao).listByAccountId(testAccountId);
|
||||
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(autoResolvedProfileId));
|
||||
|
||||
// Verify KMSKeyVO was created with auto-resolved profile ID
|
||||
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
|
||||
verify(kmsKeyDao).persist(keyCaptor.capture());
|
||||
KMSKeyVO createdKey = keyCaptor.getValue();
|
||||
assertEquals(autoResolvedProfileId, createdKey.getHsmProfileId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createUserKMSKey throws exception when explicit profile not found
|
||||
*/
|
||||
@Test(expected = KMSException.class)
|
||||
public void testCreateUserKMSKey_ThrowsExceptionWhenProfileNotFound() throws KMSException {
|
||||
// Setup: Profile name provided but doesn't exist
|
||||
String invalidProfileName = "non-existent-profile";
|
||||
when(hsmProfileDao.findByName(invalidProfileName)).thenReturn(null);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
|
||||
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, invalidProfileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createUserKMSKey auto-resolves to zone admin profile when no user profile
|
||||
*/
|
||||
@Test
|
||||
public void testCreateUserKMSKey_AutoResolvesToZoneAdmin() throws Exception {
|
||||
// Setup: No user profile, but zone admin profile exists
|
||||
Long zoneAdminProfileId = 30L;
|
||||
|
||||
HSMProfileVO zoneProfile = mock(HSMProfileVO.class);
|
||||
when(zoneProfile.getId()).thenReturn(zoneAdminProfileId);
|
||||
when(zoneProfile.isEnabled()).thenReturn(true);
|
||||
when(zoneProfile.getProtocol()).thenReturn(testProviderName);
|
||||
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
|
||||
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(Arrays.asList(zoneProfile));
|
||||
|
||||
// Mock provider KEK creation
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(zoneAdminProfileId)))
|
||||
.thenReturn("test-kek-label");
|
||||
|
||||
// Mock DAO persist operations
|
||||
KMSKeyVO mockKey = mock(KMSKeyVO.class);
|
||||
when(mockKey.getId()).thenReturn(1L);
|
||||
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
|
||||
|
||||
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
|
||||
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
|
||||
|
||||
// Verify zone admin profile was used
|
||||
assertNotNull(result);
|
||||
verify(hsmProfileDao).listByAccountId(testAccountId);
|
||||
verify(hsmProfileDao).listAdminProfiles(testZoneId);
|
||||
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(zoneAdminProfileId));
|
||||
|
||||
// Verify KMSKeyVO was created with zone admin profile ID
|
||||
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
|
||||
verify(kmsKeyDao).persist(keyCaptor.capture());
|
||||
assertEquals(zoneAdminProfileId, keyCaptor.getValue().getHsmProfileId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createUserKMSKey creates KEK version with correct profile ID
|
||||
*/
|
||||
@Test
|
||||
public void testCreateUserKMSKey_CreatesKekVersionWithProfileId() throws Exception {
|
||||
// Setup
|
||||
Long hsmProfileId = 40L;
|
||||
|
||||
HSMProfileVO profile = mock(HSMProfileVO.class);
|
||||
when(profile.getId()).thenReturn(hsmProfileId);
|
||||
when(profile.isEnabled()).thenReturn(true);
|
||||
when(profile.getProtocol()).thenReturn(testProviderName);
|
||||
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(Arrays.asList(profile));
|
||||
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(hsmProfileId)))
|
||||
.thenReturn("test-kek-label");
|
||||
|
||||
KMSKeyVO mockKey = mock(KMSKeyVO.class);
|
||||
when(mockKey.getId()).thenReturn(1L);
|
||||
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
|
||||
|
||||
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
|
||||
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
|
||||
|
||||
// Verify KEK version was created with correct profile ID
|
||||
ArgumentCaptor<KMSKekVersionVO> versionCaptor = ArgumentCaptor.forClass(KMSKekVersionVO.class);
|
||||
verify(kmsKekVersionDao).persist(versionCaptor.capture());
|
||||
KMSKekVersionVO createdVersion = versionCaptor.getValue();
|
||||
assertEquals(hsmProfileId, createdVersion.getHsmProfileId());
|
||||
assertEquals(Integer.valueOf(1), Integer.valueOf(createdVersion.getVersionNumber()));
|
||||
assertEquals("test-kek-label", createdVersion.getKekLabel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createUserKMSKey returns null profile ID for database provider
|
||||
*/
|
||||
@Test
|
||||
public void testCreateUserKMSKey_NullProfileIdForDatabaseProvider() throws Exception {
|
||||
// Setup: Database provider doesn't use profiles
|
||||
KMSProvider databaseProvider = mock(KMSProvider.class);
|
||||
when(databaseProvider.getProviderName()).thenReturn("database");
|
||||
when(databaseProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(null)))
|
||||
.thenReturn("test-kek-label");
|
||||
|
||||
KMSKeyVO mockKey = mock(KMSKeyVO.class);
|
||||
when(mockKey.getId()).thenReturn(1L);
|
||||
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
|
||||
|
||||
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
|
||||
|
||||
doReturn(databaseProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
|
||||
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
|
||||
|
||||
// Verify KEK was created with null profile ID
|
||||
verify(databaseProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(null));
|
||||
|
||||
// Verify KMSKeyVO has null profile ID
|
||||
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
|
||||
verify(kmsKeyDao).persist(keyCaptor.capture());
|
||||
assertEquals(null, keyCaptor.getValue().getHsmProfileId());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
// 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.cloudstack.kms;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.apache.cloudstack.framework.kms.KMSException;
|
||||
import org.apache.cloudstack.framework.kms.KMSProvider;
|
||||
import org.apache.cloudstack.framework.kms.KeyPurpose;
|
||||
import org.apache.cloudstack.framework.kms.WrappedKey;
|
||||
import org.apache.cloudstack.kms.dao.HSMProfileDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSKekVersionDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSKeyDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
/**
|
||||
* Unit tests for KMS key rotation logic in KMSManagerImpl
|
||||
* Tests key rotation within same HSM and cross-HSM migration
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class KMSManagerImplKeyRotationTest {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private KMSManagerImpl kmsManager;
|
||||
|
||||
@Mock
|
||||
private KMSKeyDao kmsKeyDao;
|
||||
|
||||
@Mock
|
||||
private KMSKekVersionDao kmsKekVersionDao;
|
||||
|
||||
@Mock
|
||||
private KMSWrappedKeyDao kmsWrappedKeyDao;
|
||||
|
||||
@Mock
|
||||
private HSMProfileDao hsmProfileDao;
|
||||
|
||||
@Mock
|
||||
private KMSProvider kmsProvider;
|
||||
|
||||
private Long testZoneId = 1L;
|
||||
private String testProviderName = "pkcs11";
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
when(kmsProvider.getProviderName()).thenReturn(testProviderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: rotateKek creates new KEK version in same HSM
|
||||
*/
|
||||
@Test
|
||||
public void testRotateKek_SameHSM() throws Exception {
|
||||
// Setup: Rotating within same HSM
|
||||
Long oldProfileId = 10L;
|
||||
Long kmsKeyId = 1L;
|
||||
String oldKekLabel = "old-kek-label";
|
||||
String newKekLabel = "new-kek-label";
|
||||
|
||||
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
|
||||
when(kmsKey.getId()).thenReturn(kmsKeyId);
|
||||
when(kmsKey.getHsmProfileId()).thenReturn(oldProfileId);
|
||||
when(kmsKeyDao.findByKekLabel(oldKekLabel, testProviderName)).thenReturn(kmsKey);
|
||||
|
||||
// Old version should be marked as Previous
|
||||
KMSKekVersionVO oldVersion = mock(KMSKekVersionVO.class);
|
||||
when(oldVersion.getVersionNumber()).thenReturn(1);
|
||||
when(oldVersion.getId()).thenReturn(10L);
|
||||
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
|
||||
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
|
||||
|
||||
// Provider creates new KEK
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), eq(newKekLabel), anyInt(), eq(oldProfileId)))
|
||||
.thenReturn("new-kek-id");
|
||||
|
||||
KMSKekVersionVO newVersion = mock(KMSKekVersionVO.class);
|
||||
when(newVersion.getVersionNumber()).thenReturn(2);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
String result = kmsManager.rotateKek(testZoneId, KeyPurpose.VOLUME_ENCRYPTION,
|
||||
oldKekLabel, newKekLabel, 256, null);
|
||||
|
||||
// Verify new KEK was created in same HSM
|
||||
assertNotNull(result);
|
||||
verify(kmsProvider).createKek(any(KeyPurpose.class), eq(newKekLabel), eq(256), eq(oldProfileId));
|
||||
|
||||
// Verify old version marked as Previous
|
||||
verify(oldVersion).setStatus(KMSKekVersionVO.Status.Previous);
|
||||
verify(kmsKekVersionDao).update(eq(10L), eq(oldVersion));
|
||||
|
||||
// Verify new version created
|
||||
ArgumentCaptor<KMSKekVersionVO> versionCaptor = ArgumentCaptor.forClass(KMSKekVersionVO.class);
|
||||
verify(kmsKekVersionDao).persist(versionCaptor.capture());
|
||||
KMSKekVersionVO createdVersion = versionCaptor.getValue();
|
||||
assertEquals(Integer.valueOf(2), Integer.valueOf(createdVersion.getVersionNumber()));
|
||||
assertEquals(oldProfileId, createdVersion.getHsmProfileId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: rotateKek migrates key to different HSM
|
||||
*/
|
||||
@Test
|
||||
public void testRotateKek_CrossHSMMigration() throws Exception {
|
||||
// Setup: Rotating to different HSM
|
||||
Long oldProfileId = 10L;
|
||||
Long newProfileId = 20L;
|
||||
Long kmsKeyId = 1L;
|
||||
String oldKekLabel = "old-kek-label";
|
||||
String newKekLabel = "new-kek-label";
|
||||
|
||||
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
|
||||
when(kmsKey.getId()).thenReturn(kmsKeyId);
|
||||
when(kmsKey.getHsmProfileId()).thenReturn(oldProfileId);
|
||||
when(kmsKeyDao.findByKekLabel(oldKekLabel, testProviderName)).thenReturn(kmsKey);
|
||||
|
||||
KMSKekVersionVO oldVersion = mock(KMSKekVersionVO.class);
|
||||
when(oldVersion.getVersionNumber()).thenReturn(1);
|
||||
when(oldVersion.getId()).thenReturn(10L);
|
||||
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
|
||||
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
|
||||
|
||||
// Provider creates new KEK in different HSM
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), eq(newKekLabel), anyInt(), eq(newProfileId)))
|
||||
.thenReturn("new-kek-id");
|
||||
|
||||
KMSKekVersionVO newVersion = mock(KMSKekVersionVO.class);
|
||||
when(newVersion.getVersionNumber()).thenReturn(2);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
String result = kmsManager.rotateKek(testZoneId, KeyPurpose.VOLUME_ENCRYPTION,
|
||||
oldKekLabel, newKekLabel, 256, newProfileId);
|
||||
|
||||
// Verify new KEK was created in new HSM
|
||||
assertNotNull(result);
|
||||
verify(kmsProvider).createKek(any(KeyPurpose.class), eq(newKekLabel), eq(256), eq(newProfileId));
|
||||
|
||||
// Verify new version created with new profile ID
|
||||
ArgumentCaptor<KMSKekVersionVO> versionCaptor = ArgumentCaptor.forClass(KMSKekVersionVO.class);
|
||||
verify(kmsKekVersionDao).persist(versionCaptor.capture());
|
||||
KMSKekVersionVO createdVersion = versionCaptor.getValue();
|
||||
assertEquals(newProfileId, createdVersion.getHsmProfileId());
|
||||
|
||||
// Verify KMS key updated with new profile ID
|
||||
verify(kmsKey).setHsmProfileId(newProfileId);
|
||||
verify(kmsKeyDao).update(kmsKeyId, kmsKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: rewrapSingleKey unwraps with old KEK and wraps with new KEK
|
||||
*/
|
||||
@Test
|
||||
public void testRewrapSingleKey_UnwrapAndRewrap() throws Exception {
|
||||
// Setup
|
||||
Long wrappedKeyId = 100L;
|
||||
Long oldVersionId = 1L;
|
||||
Long newVersionId = 2L;
|
||||
Long oldProfileId = 10L;
|
||||
Long newProfileId = 20L;
|
||||
|
||||
KMSWrappedKeyVO wrappedKeyVO = mock(KMSWrappedKeyVO.class);
|
||||
when(wrappedKeyVO.getId()).thenReturn(wrappedKeyId);
|
||||
|
||||
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
|
||||
when(kmsKey.getPurpose()).thenReturn(KeyPurpose.VOLUME_ENCRYPTION);
|
||||
|
||||
KMSKekVersionVO oldVersion = mock(KMSKekVersionVO.class);
|
||||
|
||||
KMSKekVersionVO newVersion = mock(KMSKekVersionVO.class);
|
||||
when(newVersion.getId()).thenReturn(newVersionId);
|
||||
when(newVersion.getKekLabel()).thenReturn("new-kek-label");
|
||||
when(newVersion.getHsmProfileId()).thenReturn(newProfileId);
|
||||
|
||||
// Mock unwrap and wrap operations
|
||||
byte[] plainDek = "plain-dek-bytes".getBytes();
|
||||
doReturn(plainDek).when(kmsManager).unwrapKey(wrappedKeyId);
|
||||
|
||||
WrappedKey newWrappedKey = mock(WrappedKey.class);
|
||||
when(newWrappedKey.getWrappedKeyMaterial()).thenReturn("new-wrapped-blob".getBytes());
|
||||
when(kmsProvider.wrapKey(plainDek, KeyPurpose.VOLUME_ENCRYPTION, "new-kek-label", newProfileId))
|
||||
.thenReturn(newWrappedKey);
|
||||
|
||||
kmsManager.rewrapSingleKey(wrappedKeyVO, kmsKey, newVersion, kmsProvider);
|
||||
|
||||
// Verify unwrap was called
|
||||
verify(kmsManager).unwrapKey(wrappedKeyId);
|
||||
|
||||
// Verify wrap was called with new profile
|
||||
verify(kmsProvider).wrapKey(plainDek, KeyPurpose.VOLUME_ENCRYPTION, "new-kek-label", newProfileId);
|
||||
|
||||
// Verify wrapped key was updated
|
||||
verify(wrappedKeyVO).setKekVersionId(newVersionId);
|
||||
verify(wrappedKeyVO).setWrappedBlob("new-wrapped-blob".getBytes());
|
||||
verify(kmsWrappedKeyDao).update(wrappedKeyId, wrappedKeyVO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: rotateKek generates new label when not provided
|
||||
*/
|
||||
@Test
|
||||
public void testRotateKek_GeneratesLabel() throws Exception {
|
||||
// Setup
|
||||
Long oldProfileId = 10L;
|
||||
Long kmsKeyId = 1L;
|
||||
String oldKekLabel = "old-kek-label";
|
||||
|
||||
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
|
||||
when(kmsKey.getId()).thenReturn(kmsKeyId);
|
||||
when(kmsKey.getHsmProfileId()).thenReturn(oldProfileId);
|
||||
when(kmsKeyDao.findByKekLabel(oldKekLabel, testProviderName)).thenReturn(kmsKey);
|
||||
|
||||
KMSKekVersionVO oldVersion = mock(KMSKekVersionVO.class);
|
||||
when(oldVersion.getVersionNumber()).thenReturn(1);
|
||||
when(oldVersion.getId()).thenReturn(10L);
|
||||
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
|
||||
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
|
||||
|
||||
// Provider creates new KEK - capture the generated label
|
||||
ArgumentCaptor<String> labelCaptor = ArgumentCaptor.forClass(String.class);
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), labelCaptor.capture(), anyInt(), eq(oldProfileId)))
|
||||
.thenReturn("new-kek-id");
|
||||
|
||||
KMSKekVersionVO newVersion = mock(KMSKekVersionVO.class);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
kmsManager.rotateKek(testZoneId, KeyPurpose.VOLUME_ENCRYPTION,
|
||||
oldKekLabel, null, 256, null);
|
||||
|
||||
// Verify a label was generated
|
||||
String generatedLabel = labelCaptor.getValue();
|
||||
assertNotNull("Label should be generated", generatedLabel);
|
||||
verify(kmsProvider).createKek(any(KeyPurpose.class), eq(generatedLabel), eq(256), eq(oldProfileId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: rotateKek throws exception when old KEK not found
|
||||
*/
|
||||
@Test(expected = KMSException.class)
|
||||
public void testRotateKek_ThrowsExceptionWhenOldKekNotFound() throws KMSException {
|
||||
// Setup: Old KEK doesn't exist
|
||||
when(kmsKeyDao.findByKekLabel("non-existent-label", testProviderName)).thenReturn(null);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
kmsManager.rotateKek(testZoneId, KeyPurpose.VOLUME_ENCRYPTION,
|
||||
"non-existent-label", "new-label", 256, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: rotateKek uses current profile when target profile is null
|
||||
*/
|
||||
@Test
|
||||
public void testRotateKek_UsesCurrentProfileWhenTargetNull() throws Exception {
|
||||
// Setup
|
||||
Long currentProfileId = 10L;
|
||||
Long kmsKeyId = 1L;
|
||||
String oldKekLabel = "old-kek-label";
|
||||
|
||||
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
|
||||
when(kmsKey.getId()).thenReturn(kmsKeyId);
|
||||
when(kmsKey.getHsmProfileId()).thenReturn(currentProfileId);
|
||||
when(kmsKeyDao.findByKekLabel(oldKekLabel, testProviderName)).thenReturn(kmsKey);
|
||||
|
||||
KMSKekVersionVO oldVersion = mock(KMSKekVersionVO.class);
|
||||
when(oldVersion.getVersionNumber()).thenReturn(1);
|
||||
when(oldVersion.getId()).thenReturn(10L);
|
||||
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
|
||||
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
|
||||
|
||||
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(currentProfileId)))
|
||||
.thenReturn("new-kek-id");
|
||||
|
||||
KMSKekVersionVO newVersion = mock(KMSKekVersionVO.class);
|
||||
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
|
||||
|
||||
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
|
||||
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
|
||||
|
||||
kmsManager.rotateKek(testZoneId, KeyPurpose.VOLUME_ENCRYPTION,
|
||||
oldKekLabel, "new-label", 256, null);
|
||||
|
||||
// Verify current profile was used (not a different one)
|
||||
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(currentProfileId));
|
||||
|
||||
// Verify KMS key was not updated (same profile)
|
||||
verify(kmsKey, never()).setHsmProfileId(currentProfileId);
|
||||
verify(kmsKeyDao, never()).update(kmsKeyId, kmsKey);
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ known_categories = {
|
|||
'Domain': 'Domain',
|
||||
'Template': 'Template',
|
||||
'KMS': 'KMS',
|
||||
'HSM': 'KMS',
|
||||
'Iso': 'ISO',
|
||||
'Volume': 'Volume',
|
||||
'Vlan': 'VLAN',
|
||||
|
|
|
|||
Loading…
Reference in New Issue