mirror of https://github.com/apache/cloudstack.git
Add KMS framework
This commit is contained in:
parent
b744824f65
commit
e995b46b20
|
|
@ -71,6 +71,11 @@
|
|||
<artifactId>cloud-framework-direct-download</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-kms</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
|||
|
|
@ -271,6 +271,14 @@ public class EventTypes {
|
|||
public static final String EVENT_CA_CERTIFICATE_REVOKE = "CA.CERTIFICATE.REVOKE";
|
||||
public static final String EVENT_CA_CERTIFICATE_PROVISION = "CA.CERTIFICATE.PROVISION";
|
||||
|
||||
// KMS (Key Management Service) events
|
||||
public static final String EVENT_KMS_KEY_WRAP = "KMS.KEY.WRAP";
|
||||
public static final String EVENT_KMS_KEY_UNWRAP = "KMS.KEY.UNWRAP";
|
||||
public static final String EVENT_KMS_KEK_CREATE = "KMS.KEK.CREATE";
|
||||
public static final String EVENT_KMS_KEK_ROTATE = "KMS.KEK.ROTATE";
|
||||
public static final String EVENT_KMS_KEK_DELETE = "KMS.KEK.DELETE";
|
||||
public static final String EVENT_KMS_HEALTH_CHECK = "KMS.HEALTH.CHECK";
|
||||
|
||||
// Account events
|
||||
public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE";
|
||||
public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE";
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@ public enum ApiCommandResourceType {
|
|||
KubernetesSupportedVersion(null),
|
||||
SharedFS(org.apache.cloudstack.storage.sharedfs.SharedFS.class),
|
||||
Extension(org.apache.cloudstack.extension.Extension.class),
|
||||
ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class);
|
||||
ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class),
|
||||
KmsKey(org.apache.cloudstack.kms.KMSKey.class);
|
||||
|
||||
private final Class<?> clazz;
|
||||
|
||||
|
|
|
|||
|
|
@ -867,6 +867,9 @@ public class ApiConstants {
|
|||
public static final String SORT_BY = "sortby";
|
||||
public static final String CHANGE_CIDR = "changecidr";
|
||||
public static final String PURPOSE = "purpose";
|
||||
public static final String KMS_KEY_ID = "kmskeyid";
|
||||
public static final String KEK_LABEL = "keklabel";
|
||||
public static final String KEY_BITS = "keybits";
|
||||
public static final String IS_TAGGED = "istagged";
|
||||
public static final String INSTANCE_NAME = "instancename";
|
||||
public static final String CONSIDER_LAST_HOST = "considerlasthost";
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ import org.apache.cloudstack.api.response.HypervisorGuestOsNamesResponse;
|
|||
import org.apache.cloudstack.api.response.IPAddressResponse;
|
||||
import org.apache.cloudstack.api.response.ImageStoreResponse;
|
||||
import org.apache.cloudstack.api.response.InstanceGroupResponse;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.kms.KMSKey;
|
||||
import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse;
|
||||
import org.apache.cloudstack.api.response.IpForwardingRuleResponse;
|
||||
import org.apache.cloudstack.api.response.IpQuarantineResponse;
|
||||
|
|
@ -591,4 +593,6 @@ public interface ResponseGenerator {
|
|||
ApiKeyPairResponse createKeyPairResponse(ApiKeyPair keyPair);
|
||||
|
||||
ListResponse<BaseRolePermissionResponse> createKeypairPermissionsResponse(List<ApiKeyPairPermission> permissions);
|
||||
|
||||
KMSKeyResponse createKMSKeyResponse(KMSKey kmsKey);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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.api.command.user.kms;
|
||||
|
||||
import com.cloud.exception.ResourceAllocationException;
|
||||
import com.cloud.user.Account;
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiCommandResourceType;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.user.UserCmd;
|
||||
import org.apache.cloudstack.api.response.DomainResponse;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.api.response.ZoneResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.framework.kms.KMSException;
|
||||
import org.apache.cloudstack.kms.KMSManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@APICommand(name = "createKMSKey",
|
||||
description = "Creates a new KMS key (Key Encryption Key) for encryption",
|
||||
responseObject = KMSKeyResponse.class,
|
||||
since = "4.23.0",
|
||||
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
|
||||
requestHasSensitiveInfo = false,
|
||||
responseHasSensitiveInfo = false)
|
||||
public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
|
||||
private static final String s_name = "createkmskeyresponse";
|
||||
|
||||
@Inject
|
||||
private KMSManager kmsManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Parameter(name = ApiConstants.NAME,
|
||||
required = true,
|
||||
type = CommandType.STRING,
|
||||
description = "Name of the KMS key")
|
||||
private String name;
|
||||
|
||||
@Parameter(name = ApiConstants.DESCRIPTION,
|
||||
type = CommandType.STRING,
|
||||
description = "Description of the KMS key")
|
||||
private String description;
|
||||
|
||||
@Parameter(name = ApiConstants.PURPOSE,
|
||||
required = true,
|
||||
type = CommandType.STRING,
|
||||
description = "Purpose of the key: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET")
|
||||
private String purpose;
|
||||
|
||||
@Parameter(name = ApiConstants.ZONE_ID,
|
||||
required = true,
|
||||
type = CommandType.UUID,
|
||||
entityType = ZoneResponse.class,
|
||||
description = "Zone ID where the key will be valid")
|
||||
private Long zoneId;
|
||||
|
||||
@Parameter(name = ApiConstants.ACCOUNT,
|
||||
type = CommandType.STRING,
|
||||
description = "Account name (for creating keys for child accounts - requires domain admin or admin)")
|
||||
private String accountName;
|
||||
|
||||
@Parameter(name = ApiConstants.DOMAIN_ID,
|
||||
type = CommandType.UUID,
|
||||
entityType = DomainResponse.class,
|
||||
description = "Domain ID (for creating keys for child accounts - requires domain admin or admin)")
|
||||
private Long domainId;
|
||||
|
||||
@Parameter(name = ApiConstants.KEY_BITS,
|
||||
type = CommandType.INTEGER,
|
||||
description = "Key size in bits: 128, 192, or 256 (default: 256)")
|
||||
private Integer keyBits;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
public Long getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return accountName;
|
||||
}
|
||||
|
||||
public Long getDomainId() {
|
||||
return domainId;
|
||||
}
|
||||
|
||||
public Integer getKeyBits() {
|
||||
return keyBits != null ? keyBits : 256; // Default to 256 bits
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void execute() throws ResourceAllocationException {
|
||||
try {
|
||||
KMSKeyResponse response = kmsManager.createKMSKey(this);
|
||||
response.setResponseName(getCommandName());
|
||||
setResponseObject(response);
|
||||
} catch (KMSException e) {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
|
||||
"Failed to create KMS key: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
if (accountName != null || domainId != null) {
|
||||
return _accountService.finalyzeAccountId(accountName, domainId, null, true);
|
||||
}
|
||||
return caller.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiCommandResourceType getApiResourceType() {
|
||||
return ApiCommandResourceType.KmsKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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.api.command.user.kms;
|
||||
|
||||
import com.cloud.event.EventTypes;
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiCommandResourceType;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseAsyncCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.user.UserCmd;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.api.response.SuccessResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.framework.kms.KMSException;
|
||||
import org.apache.cloudstack.kms.KMSManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@APICommand(name = "deleteKMSKey",
|
||||
description = "Deletes a KMS key (only if not in use)",
|
||||
responseObject = SuccessResponse.class,
|
||||
since = "4.23.0",
|
||||
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
|
||||
requestHasSensitiveInfo = false,
|
||||
responseHasSensitiveInfo = false)
|
||||
public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
|
||||
private static final String s_name = "deletekmskeyresponse";
|
||||
|
||||
@Inject
|
||||
private KMSManager kmsManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Parameter(name = ApiConstants.ID,
|
||||
required = true,
|
||||
type = CommandType.UUID,
|
||||
entityType = KMSKeyResponse.class,
|
||||
description = "The UUID of the KMS key to delete")
|
||||
private Long id;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
try {
|
||||
SuccessResponse response = kmsManager.deleteKMSKey(this);
|
||||
response.setResponseName(getCommandName());
|
||||
setResponseObject(response);
|
||||
} catch (KMSException e) {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
|
||||
"Failed to delete KMS key: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return CallContext.current().getCallingAccount().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType() {
|
||||
return EventTypes.EVENT_KMS_KEK_DELETE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventDescription() {
|
||||
return "deleting KMS key: " + getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiCommandResourceType getApiResourceType() {
|
||||
return ApiCommandResourceType.KmsKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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.api.command.user.kms;
|
||||
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseListAccountResourcesCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ResponseObject.ResponseView;
|
||||
import org.apache.cloudstack.api.command.user.UserCmd;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.apache.cloudstack.api.response.ZoneResponse;
|
||||
import org.apache.cloudstack.kms.KMSManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@APICommand(name = "listKMSKeys",
|
||||
description = "Lists KMS keys available to the caller",
|
||||
responseObject = KMSKeyResponse.class,
|
||||
responseView = ResponseView.Restricted,
|
||||
since = "4.23.0",
|
||||
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
|
||||
requestHasSensitiveInfo = false,
|
||||
responseHasSensitiveInfo = false)
|
||||
public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserCmd {
|
||||
private static final String s_name = "listkmskeysresponse";
|
||||
|
||||
@Inject
|
||||
private KMSManager kmsManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Parameter(name = ApiConstants.ID,
|
||||
type = CommandType.UUID,
|
||||
entityType = KMSKeyResponse.class,
|
||||
description = "List KMS key by UUID")
|
||||
private Long id;
|
||||
|
||||
@Parameter(name = ApiConstants.PURPOSE,
|
||||
type = CommandType.STRING,
|
||||
description = "Filter by purpose: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET")
|
||||
private String purpose;
|
||||
|
||||
@Parameter(name = ApiConstants.ZONE_ID,
|
||||
type = CommandType.UUID,
|
||||
entityType = ZoneResponse.class,
|
||||
description = "Filter by zone ID")
|
||||
private Long zoneId;
|
||||
|
||||
@Parameter(name = ApiConstants.STATE,
|
||||
type = CommandType.STRING,
|
||||
description = "Filter by state: Enabled, Disabled")
|
||||
private String state;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
public Long getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
ListResponse<KMSKeyResponse> listResponse = kmsManager.listKMSKeys(this);
|
||||
listResponse.setResponseName(getCommandName());
|
||||
setResponseObject(listResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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.api.command.user.kms;
|
||||
|
||||
import com.cloud.event.EventTypes;
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiCommandResourceType;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.BaseAsyncCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.user.UserCmd;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.framework.kms.KMSException;
|
||||
import org.apache.cloudstack.kms.KMSManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@APICommand(name = "updateKMSKey",
|
||||
description = "Updates KMS key name, description, or state",
|
||||
responseObject = KMSKeyResponse.class,
|
||||
since = "4.23.0",
|
||||
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
|
||||
requestHasSensitiveInfo = false,
|
||||
responseHasSensitiveInfo = false)
|
||||
public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
|
||||
private static final String s_name = "updatekmskeyresponse";
|
||||
|
||||
@Inject
|
||||
private KMSManager kmsManager;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Parameter(name = ApiConstants.ID,
|
||||
required = true,
|
||||
type = CommandType.UUID,
|
||||
entityType = KMSKeyResponse.class,
|
||||
description = "The UUID of the KMS key to update")
|
||||
private Long id;
|
||||
|
||||
@Parameter(name = ApiConstants.NAME,
|
||||
type = CommandType.STRING,
|
||||
description = "New name for the key")
|
||||
private String name;
|
||||
|
||||
@Parameter(name = ApiConstants.DESCRIPTION,
|
||||
type = CommandType.STRING,
|
||||
description = "New description for the key")
|
||||
private String description;
|
||||
|
||||
@Parameter(name = ApiConstants.STATE,
|
||||
type = CommandType.STRING,
|
||||
description = "New state: Enabled or Disabled")
|
||||
private String state;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
try {
|
||||
KMSKeyResponse response = kmsManager.updateKMSKey(this);
|
||||
response.setResponseName(getCommandName());
|
||||
setResponseObject(response);
|
||||
} catch (KMSException e) {
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
|
||||
"Failed to update KMS key: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandName() {
|
||||
return s_name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return CallContext.current().getCallingAccount().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventType() {
|
||||
return EventTypes.EVENT_KMS_KEK_CREATE; // Reuse create event type for updates
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEventDescription() {
|
||||
return "updating KMS key: " + getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiCommandResourceType getApiResourceType() {
|
||||
return ApiCommandResourceType.KmsKey;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* 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.api.response;
|
||||
|
||||
import com.cloud.serializer.Param;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.BaseResponse;
|
||||
import org.apache.cloudstack.api.EntityReference;
|
||||
import org.apache.cloudstack.kms.KMSKey;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@EntityReference(value = KMSKey.class)
|
||||
public class KMSKeyResponse extends BaseResponse implements ControlledEntityResponse {
|
||||
|
||||
@SerializedName(ApiConstants.ID)
|
||||
@Param(description = "the UUID of the key")
|
||||
private String id;
|
||||
|
||||
@SerializedName(ApiConstants.NAME)
|
||||
@Param(description = "the name of the key")
|
||||
private String name;
|
||||
|
||||
@SerializedName(ApiConstants.DESCRIPTION)
|
||||
@Param(description = "the description of the key")
|
||||
private String description;
|
||||
|
||||
@SerializedName(ApiConstants.PURPOSE)
|
||||
@Param(description = "the purpose of the key (VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET)")
|
||||
private String purpose;
|
||||
|
||||
@SerializedName(ApiConstants.ACCOUNT)
|
||||
@Param(description = "the account owning the key")
|
||||
private String accountName;
|
||||
|
||||
@SerializedName(ApiConstants.ACCOUNT_ID)
|
||||
@Param(description = "the account ID owning the key")
|
||||
private String accountId;
|
||||
|
||||
@SerializedName(ApiConstants.DOMAIN_ID)
|
||||
@Param(description = "the domain ID of the key")
|
||||
private String domainId;
|
||||
|
||||
@SerializedName(ApiConstants.DOMAIN)
|
||||
@Param(description = "the domain name of the key")
|
||||
private String domainName;
|
||||
|
||||
@SerializedName(ApiConstants.DOMAIN_PATH)
|
||||
@Param(description = "the domain path of the key")
|
||||
private String domainPath;
|
||||
|
||||
@SerializedName(ApiConstants.ZONE_ID)
|
||||
@Param(description = "the zone ID where the key is valid")
|
||||
private String zoneId;
|
||||
|
||||
@SerializedName(ApiConstants.ZONE_NAME)
|
||||
@Param(description = "the zone name where the key is valid")
|
||||
private String zoneName;
|
||||
|
||||
@SerializedName(ApiConstants.PROVIDER)
|
||||
@Param(description = "the KMS provider (database, pkcs11, etc.)")
|
||||
private String provider;
|
||||
|
||||
@SerializedName(ApiConstants.ALGORITHM)
|
||||
@Param(description = "the encryption algorithm")
|
||||
private String algorithm;
|
||||
|
||||
@SerializedName(ApiConstants.KEY_BITS)
|
||||
@Param(description = "the key size in bits")
|
||||
private Integer keyBits;
|
||||
|
||||
@SerializedName(ApiConstants.STATE)
|
||||
@Param(description = "the state of the key (Enabled, Disabled, Deleted)")
|
||||
private String state;
|
||||
|
||||
@SerializedName(ApiConstants.CREATED)
|
||||
@Param(description = "the creation timestamp")
|
||||
private Date created;
|
||||
|
||||
// KEK label is admin-only for security
|
||||
@SerializedName(ApiConstants.KEK_LABEL)
|
||||
@Param(description = "the provider-specific KEK label (admin only)", authorized = {RoleType.Admin})
|
||||
private String kekLabel;
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
public void setPurpose(String purpose) {
|
||||
this.purpose = purpose;
|
||||
}
|
||||
|
||||
public String getAccountName() {
|
||||
return accountName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAccountName(String accountName) {
|
||||
this.accountName = accountName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProjectId(String projectId) {
|
||||
// KMS keys are not project-scoped
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProjectName(String projectName) {
|
||||
// KMS keys are not project-scoped
|
||||
}
|
||||
|
||||
public String getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
public void setAccountId(String accountId) {
|
||||
this.accountId = accountId;
|
||||
}
|
||||
|
||||
public String getDomainId() {
|
||||
return domainId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDomainId(String domainId) {
|
||||
this.domainId = domainId;
|
||||
}
|
||||
|
||||
public String getDomainName() {
|
||||
return domainName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDomainName(String domainName) {
|
||||
this.domainName = domainName;
|
||||
}
|
||||
|
||||
public String getDomainPath() {
|
||||
return domainPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDomainPath(String domainPath) {
|
||||
this.domainPath = domainPath;
|
||||
}
|
||||
|
||||
public String getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public void setZoneId(String zoneId) {
|
||||
this.zoneId = zoneId;
|
||||
}
|
||||
|
||||
public String getZoneName() {
|
||||
return zoneName;
|
||||
}
|
||||
|
||||
public void setZoneName(String zoneName) {
|
||||
this.zoneName = zoneName;
|
||||
}
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void setProvider(String provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public void setAlgorithm(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public Integer getKeyBits() {
|
||||
return keyBits;
|
||||
}
|
||||
|
||||
public void setKeyBits(Integer keyBits) {
|
||||
this.keyBits = keyBits;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public String getKekLabel() {
|
||||
return kekLabel;
|
||||
}
|
||||
|
||||
public void setKekLabel(String kekLabel) {
|
||||
this.kekLabel = kekLabel;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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 org.apache.cloudstack.acl.ControlledEntity;
|
||||
import org.apache.cloudstack.api.Identity;
|
||||
import org.apache.cloudstack.api.InternalIdentity;
|
||||
import org.apache.cloudstack.framework.kms.KeyPurpose;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* KMS Key (Key Encryption Key) metadata.
|
||||
* Represents a KEK that can be used to wrap/unwrap Data Encryption Keys (DEKs).
|
||||
* KEKs are account-scoped and used for envelope encryption.
|
||||
*/
|
||||
public interface KMSKey extends Identity, InternalIdentity, ControlledEntity {
|
||||
|
||||
/**
|
||||
* Get the user-friendly name of the key
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Get the description of the key
|
||||
*/
|
||||
String getDescription();
|
||||
|
||||
/**
|
||||
* Get the provider-specific KEK label/ID
|
||||
* (internal identifier used by the KMS provider)
|
||||
*/
|
||||
String getKekLabel();
|
||||
|
||||
/**
|
||||
* Get the purpose of this key
|
||||
*/
|
||||
KeyPurpose getPurpose();
|
||||
|
||||
/**
|
||||
* Get the zone ID where this key is valid
|
||||
*/
|
||||
Long getZoneId();
|
||||
|
||||
/**
|
||||
* Get the KMS provider name (e.g., "database", "pkcs11")
|
||||
*/
|
||||
String getProviderName();
|
||||
|
||||
/**
|
||||
* Get the encryption algorithm (e.g., "AES/GCM/NoPadding")
|
||||
*/
|
||||
String getAlgorithm();
|
||||
|
||||
/**
|
||||
* Get the key size in bits (e.g., 128, 192, 256)
|
||||
*/
|
||||
Integer getKeyBits();
|
||||
|
||||
/**
|
||||
* Get the current state of the key
|
||||
*/
|
||||
State getState();
|
||||
|
||||
/**
|
||||
* Get the creation timestamp
|
||||
*/
|
||||
Date getCreated();
|
||||
|
||||
/**
|
||||
* Get the removal timestamp (null if not removed)
|
||||
*/
|
||||
Date getRemoved();
|
||||
|
||||
/**
|
||||
* Key state enumeration
|
||||
*/
|
||||
enum State {
|
||||
/** Key is active and can be used for encryption/decryption */
|
||||
Enabled,
|
||||
/** Key is disabled and cannot be used for new operations */
|
||||
Disabled,
|
||||
/** Key is soft-deleted */
|
||||
Deleted
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,375 @@
|
|||
// 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 com.cloud.utils.component.Manager;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.framework.config.Configurable;
|
||||
import org.apache.cloudstack.api.command.user.kms.CreateKMSKeyCmd;
|
||||
import org.apache.cloudstack.api.command.user.kms.DeleteKMSKeyCmd;
|
||||
import org.apache.cloudstack.api.command.user.kms.ListKMSKeysCmd;
|
||||
import org.apache.cloudstack.api.command.user.kms.UpdateKMSKeyCmd;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.apache.cloudstack.api.response.SuccessResponse;
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* Manager interface for Key Management Service operations.
|
||||
* Provides high-level API for cryptographic key management with zone-scoping,
|
||||
* provider abstraction, and integration with CloudStack's configuration system.
|
||||
*/
|
||||
public interface KMSManager extends Manager, Configurable {
|
||||
|
||||
// ==================== Configuration Keys ====================
|
||||
|
||||
/**
|
||||
* Global: which KMS provider plugin to use by default
|
||||
* Supported values: "database" (default), "pkcs11", or custom provider names
|
||||
*/
|
||||
ConfigKey<String> KMSProviderPlugin = new ConfigKey<>(
|
||||
"Advanced",
|
||||
String.class,
|
||||
"kms.provider.plugin",
|
||||
"database",
|
||||
"The KMS provider plugin to use for cryptographic operations (database, pkcs11, etc.)",
|
||||
true,
|
||||
ConfigKey.Scope.Global
|
||||
);
|
||||
|
||||
/**
|
||||
* Zone-scoped: enable KMS for a specific zone
|
||||
* When false (default), new volumes use legacy passphrase encryption
|
||||
* When true, new volumes use KMS envelope encryption
|
||||
*/
|
||||
ConfigKey<Boolean> KMSEnabled = new ConfigKey<>(
|
||||
"Advanced",
|
||||
Boolean.class,
|
||||
"kms.enabled",
|
||||
"false",
|
||||
"Enable Key Management Service for disk encryption in this zone",
|
||||
true,
|
||||
ConfigKey.Scope.Zone
|
||||
);
|
||||
|
||||
/**
|
||||
* Global: DEK size in bits for volume encryption
|
||||
* Supported: 128, 192, 256
|
||||
*/
|
||||
ConfigKey<Integer> KMSDekSizeBits = new ConfigKey<>(
|
||||
"Advanced",
|
||||
Integer.class,
|
||||
"kms.dek.size.bits",
|
||||
"256",
|
||||
"The size of Data Encryption Keys (DEK) in bits (128, 192, or 256)",
|
||||
true,
|
||||
ConfigKey.Scope.Global
|
||||
);
|
||||
|
||||
/**
|
||||
* Global: retry count for transient KMS failures
|
||||
*/
|
||||
ConfigKey<Integer> KMSRetryCount = new ConfigKey<>(
|
||||
"Advanced",
|
||||
Integer.class,
|
||||
"kms.retry.count",
|
||||
"3",
|
||||
"Number of retry attempts for transient KMS failures",
|
||||
true,
|
||||
ConfigKey.Scope.Global
|
||||
);
|
||||
|
||||
/**
|
||||
* Global: retry delay in milliseconds
|
||||
*/
|
||||
ConfigKey<Integer> KMSRetryDelayMs = new ConfigKey<>(
|
||||
"Advanced",
|
||||
Integer.class,
|
||||
"kms.retry.delay.ms",
|
||||
"1000",
|
||||
"Delay in milliseconds between KMS retry attempts (exponential backoff)",
|
||||
true,
|
||||
ConfigKey.Scope.Global
|
||||
);
|
||||
|
||||
/**
|
||||
* Global: timeout for KMS operations in seconds
|
||||
*/
|
||||
ConfigKey<Integer> KMSOperationTimeoutSec = new ConfigKey<>(
|
||||
"Advanced",
|
||||
Integer.class,
|
||||
"kms.operation.timeout.sec",
|
||||
"30",
|
||||
"Timeout in seconds for KMS cryptographic operations",
|
||||
true,
|
||||
ConfigKey.Scope.Global
|
||||
);
|
||||
|
||||
// ==================== Provider Management ====================
|
||||
|
||||
/**
|
||||
* List all registered KMS providers
|
||||
*
|
||||
* @return list of available providers
|
||||
*/
|
||||
List<? extends KMSProvider> listKMSProviders();
|
||||
|
||||
/**
|
||||
* Get a specific KMS provider by name
|
||||
*
|
||||
* @param name provider name
|
||||
* @return the provider, or null if not found
|
||||
*/
|
||||
KMSProvider getKMSProvider(String name);
|
||||
|
||||
/**
|
||||
* Get the configured provider for a zone
|
||||
*
|
||||
* @param zoneId the zone ID (null for global default)
|
||||
* @return the configured provider
|
||||
* @throws KMSException if no provider configured
|
||||
*/
|
||||
KMSProvider getKMSProviderForZone(Long zoneId) throws KMSException;
|
||||
|
||||
/**
|
||||
* Check if KMS is enabled for a zone
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @return true if KMS is enabled
|
||||
*/
|
||||
boolean isKmsEnabled(Long zoneId);
|
||||
|
||||
// ==================== KEK Management ====================
|
||||
|
||||
/**
|
||||
* Create a new KEK for a zone and purpose
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param purpose the key purpose
|
||||
* @param label optional custom label (null for auto-generated)
|
||||
* @param keyBits key size in bits
|
||||
* @return the KEK identifier
|
||||
* @throws KMSException if creation fails
|
||||
*/
|
||||
String createKek(Long zoneId, KeyPurpose purpose, String label, int keyBits) throws KMSException;
|
||||
|
||||
/**
|
||||
* Delete a KEK (WARNING: makes all DEKs wrapped by it unrecoverable)
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param kekId the KEK identifier
|
||||
* @throws KMSException if deletion fails
|
||||
*/
|
||||
void deleteKek(Long zoneId, String kekId) throws KMSException;
|
||||
|
||||
/**
|
||||
* List KEKs for a zone and purpose
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param purpose the purpose filter (null for all)
|
||||
* @return list of KEK identifiers
|
||||
* @throws KMSException if listing fails
|
||||
*/
|
||||
List<String> listKeks(Long zoneId, KeyPurpose purpose) throws KMSException;
|
||||
|
||||
/**
|
||||
* Check if a KEK is available
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param kekId the KEK identifier
|
||||
* @return true if available
|
||||
* @throws KMSException if check fails
|
||||
*/
|
||||
boolean isKekAvailable(Long zoneId, String kekId) throws KMSException;
|
||||
|
||||
/**
|
||||
* Rotate a KEK (create new one and rewrap all DEKs)
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param purpose the purpose
|
||||
* @param oldKekLabel the old KEK label (must be specified)
|
||||
* @param newKekLabel the new KEK label (null for auto-generated)
|
||||
* @param keyBits the new KEK size
|
||||
* @return the new KEK identifier
|
||||
* @throws KMSException if rotation fails
|
||||
*/
|
||||
String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
|
||||
String newKekLabel, int keyBits) throws KMSException;
|
||||
|
||||
// ==================== DEK Operations ====================
|
||||
|
||||
/**
|
||||
* Unwrap a DEK from a wrapped key
|
||||
* SECURITY: Caller must zeroize returned byte array after use!
|
||||
*
|
||||
* @param wrappedKey the wrapped key from database
|
||||
* @param zoneId the zone ID
|
||||
* @return plaintext DEK (caller must zeroize!)
|
||||
* @throws KMSException if unwrap fails
|
||||
*/
|
||||
byte[] unwrapVolumeKey(WrappedKey wrappedKey, Long zoneId) throws KMSException;
|
||||
|
||||
// ==================== Health & Status ====================
|
||||
|
||||
/**
|
||||
* Check KMS provider health for a zone
|
||||
*
|
||||
* @param zoneId the zone ID (null for global)
|
||||
* @return true if healthy
|
||||
* @throws KMSException if health check fails critically
|
||||
*/
|
||||
boolean healthCheck(Long zoneId) throws KMSException;
|
||||
|
||||
// ==================== User KEK Management ====================
|
||||
|
||||
/**
|
||||
* Create a new KMS key (KEK) for a user account
|
||||
*
|
||||
* @param accountId the account ID
|
||||
* @param domainId the domain ID
|
||||
* @param zoneId the zone ID
|
||||
* @param name user-friendly name
|
||||
* @param description optional description
|
||||
* @param purpose key purpose
|
||||
* @param keyBits key size in bits
|
||||
* @return the created KMS key
|
||||
* @throws KMSException if creation fails
|
||||
*/
|
||||
KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
|
||||
String name, String description, KeyPurpose purpose,
|
||||
Integer keyBits) throws KMSException;
|
||||
|
||||
/**
|
||||
* List KMS keys accessible to a user account
|
||||
*
|
||||
* @param accountId the account ID
|
||||
* @param domainId the domain ID
|
||||
* @param zoneId optional zone filter
|
||||
* @param purpose optional purpose filter
|
||||
* @param state optional state filter
|
||||
* @return list of accessible KMS keys
|
||||
*/
|
||||
List<? extends KMSKey> listUserKMSKeys(Long accountId, Long domainId, Long zoneId,
|
||||
KeyPurpose purpose, KMSKey.State state);
|
||||
|
||||
/**
|
||||
* Get a KMS key by UUID (with permission check)
|
||||
*
|
||||
* @param uuid the key UUID
|
||||
* @param callerAccountId the caller's account ID
|
||||
* @return the KMS key, or null if not found or no permission
|
||||
*/
|
||||
KMSKey getUserKMSKey(String uuid, Long callerAccountId);
|
||||
|
||||
/**
|
||||
* Check if caller has permission to use a KMS key
|
||||
*
|
||||
* @param callerAccountId the caller's account ID
|
||||
* @param keyUuid the key UUID
|
||||
* @return true if caller has permission
|
||||
*/
|
||||
boolean hasPermission(Long callerAccountId, String keyUuid);
|
||||
|
||||
/**
|
||||
* Delete a KMS key (only if not in use)
|
||||
*
|
||||
* @param uuid the key UUID
|
||||
* @param callerAccountId the caller's account ID
|
||||
* @throws KMSException if deletion fails (e.g., key in use)
|
||||
*/
|
||||
void deleteUserKMSKey(String uuid, Long callerAccountId) throws KMSException;
|
||||
|
||||
/**
|
||||
* Update a KMS key's metadata (name, description, state)
|
||||
*
|
||||
* @param uuid the key UUID
|
||||
* @param callerAccountId the caller's account ID
|
||||
* @param name optional new name
|
||||
* @param description optional new description
|
||||
* @param state optional new state
|
||||
* @return the updated KMS key
|
||||
* @throws KMSException if update fails
|
||||
*/
|
||||
KMSKey updateUserKMSKey(String uuid, Long callerAccountId,
|
||||
String name, String description, KMSKey.State state) throws KMSException;
|
||||
|
||||
/**
|
||||
* Unwrap a DEK by wrapped key ID, trying multiple KEK versions if needed
|
||||
*
|
||||
* @param wrappedKeyId the wrapped key database ID
|
||||
* @return plaintext DEK (caller must zeroize!)
|
||||
* @throws KMSException if unwrap fails
|
||||
*/
|
||||
byte[] unwrapKey(Long wrappedKeyId) throws KMSException;
|
||||
|
||||
/**
|
||||
* Generate and wrap a DEK using a specific KMS key UUID
|
||||
*
|
||||
* @param kekUuid the KMS key UUID
|
||||
* @param callerAccountId the caller's account ID
|
||||
* @return wrapped key ready for database storage
|
||||
* @throws KMSException if operation fails
|
||||
*/
|
||||
WrappedKey generateVolumeKeyWithKek(String kekUuid, Long callerAccountId) throws KMSException;
|
||||
|
||||
// ==================== API Response Methods ====================
|
||||
|
||||
/**
|
||||
* Create a KMS key and return the response object.
|
||||
* Handles validation, account resolution, and permission checks.
|
||||
*
|
||||
* @param cmd the create command with all parameters
|
||||
* @return KMSKeyResponse
|
||||
* @throws KMSException if creation fails
|
||||
*/
|
||||
KMSKeyResponse createKMSKey(CreateKMSKeyCmd cmd) throws KMSException;
|
||||
|
||||
/**
|
||||
* List KMS keys and return the response object.
|
||||
* Handles validation and permission checks.
|
||||
*
|
||||
* @param cmd the list command with all parameters
|
||||
* @return ListResponse with KMSKeyResponse objects
|
||||
*/
|
||||
ListResponse<KMSKeyResponse> listKMSKeys(ListKMSKeysCmd cmd);
|
||||
|
||||
/**
|
||||
* Update a KMS key and return the response object.
|
||||
* Handles validation and permission checks.
|
||||
*
|
||||
* @param cmd the update command with all parameters
|
||||
* @return KMSKeyResponse
|
||||
* @throws KMSException if update fails
|
||||
*/
|
||||
KMSKeyResponse updateKMSKey(UpdateKMSKeyCmd cmd) throws KMSException;
|
||||
|
||||
/**
|
||||
* Delete a KMS key and return the response object.
|
||||
* Handles validation and permission checks.
|
||||
*
|
||||
* @param cmd the delete command with all parameters
|
||||
* @return SuccessResponse
|
||||
* @throws KMSException if deletion fails
|
||||
*/
|
||||
SuccessResponse deleteKMSKey(DeleteKMSKeyCmd cmd) throws KMSException;
|
||||
}
|
||||
|
|
@ -366,4 +366,7 @@
|
|||
|
||||
<bean id="sharedFSProvidersRegistry" class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
|
||||
</bean>
|
||||
|
||||
<bean id="kmsProvidersRegistry" class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
|
||||
</bean>
|
||||
</beans>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@
|
|||
<artifactId>cloud-framework-db</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-kms</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
|
|
|
|||
|
|
@ -182,6 +182,9 @@ public class VolumeVO implements Volume {
|
|||
@Column(name = "passphrase_id")
|
||||
private Long passphraseId;
|
||||
|
||||
@Column(name = "kms_wrapped_key_id")
|
||||
private Long kmsWrappedKeyId;
|
||||
|
||||
@Column(name = "encrypt_format")
|
||||
private String encryptFormat;
|
||||
|
||||
|
|
@ -683,6 +686,10 @@ public class VolumeVO implements Volume {
|
|||
|
||||
public void setPassphraseId(Long id) { this.passphraseId = id; }
|
||||
|
||||
public Long getKmsWrappedKeyId() { return kmsWrappedKeyId; }
|
||||
|
||||
public void setKmsWrappedKeyId(Long id) { this.kmsWrappedKeyId = id; }
|
||||
|
||||
public String getEncryptFormat() { return encryptFormat; }
|
||||
|
||||
public void setEncryptFormat(String encryptFormat) { this.encryptFormat = encryptFormat; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
// 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 com.cloud.utils.db.GenericDao;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Database entity for KEK versions.
|
||||
* Tracks multiple KEK versions per KMS key to support gradual rotation.
|
||||
* During rotation, a new version is created (status=Active) and old versions
|
||||
* are marked as Previous (still usable for decryption) or Archived (no longer used).
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "kms_kek_versions")
|
||||
public class KMSKekVersionVO {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "uuid", nullable = false, unique = true)
|
||||
private String uuid;
|
||||
|
||||
@Column(name = "kms_key_id", nullable = false)
|
||||
private Long kmsKeyId;
|
||||
|
||||
@Column(name = "version_number", nullable = false)
|
||||
private Integer versionNumber;
|
||||
|
||||
@Column(name = "kek_label", nullable = false)
|
||||
private String kekLabel;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 32)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Status status;
|
||||
|
||||
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date created;
|
||||
|
||||
@Column(name = GenericDao.REMOVED_COLUMN)
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date removed;
|
||||
|
||||
/**
|
||||
* Status of a KEK version
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* Active version - used for new encryption operations
|
||||
*/
|
||||
Active,
|
||||
/**
|
||||
* Previous version - still usable for decryption during rotation
|
||||
*/
|
||||
Previous,
|
||||
/**
|
||||
* Archived version - no longer used (after re-encryption complete)
|
||||
*/
|
||||
Archived
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor (required by JPA)
|
||||
*/
|
||||
public KMSKekVersionVO() {
|
||||
this.uuid = UUID.randomUUID().toString();
|
||||
this.created = new Date();
|
||||
this.status = Status.Active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a new KEK version
|
||||
*
|
||||
* @param kmsKeyId the KMS key ID this version belongs to
|
||||
* @param versionNumber the version number (1, 2, 3, ...)
|
||||
* @param kekLabel the provider-specific KEK label
|
||||
* @param status the status (typically Active for new versions)
|
||||
*/
|
||||
public KMSKekVersionVO(Long kmsKeyId, Integer versionNumber, String kekLabel, Status status) {
|
||||
this();
|
||||
this.kmsKeyId = kmsKeyId;
|
||||
this.versionNumber = versionNumber;
|
||||
this.kekLabel = kekLabel;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public Long getKmsKeyId() {
|
||||
return kmsKeyId;
|
||||
}
|
||||
|
||||
public void setKmsKeyId(Long kmsKeyId) {
|
||||
this.kmsKeyId = kmsKeyId;
|
||||
}
|
||||
|
||||
public Integer getVersionNumber() {
|
||||
return versionNumber;
|
||||
}
|
||||
|
||||
public void setVersionNumber(Integer versionNumber) {
|
||||
this.versionNumber = versionNumber;
|
||||
}
|
||||
|
||||
public String getKekLabel() {
|
||||
return kekLabel;
|
||||
}
|
||||
|
||||
public void setKekLabel(String kekLabel) {
|
||||
this.kekLabel = kekLabel;
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public Date getRemoved() {
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void setRemoved(Date removed) {
|
||||
this.removed = removed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("KMSKekVersion[id=%d, uuid=%s, kmsKeyId=%d, version=%d, status=%s, kekLabel=%s]",
|
||||
id, uuid, kmsKeyId, versionNumber, status, kekLabel);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
// 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 com.cloud.utils.db.GenericDao;
|
||||
import org.apache.cloudstack.framework.kms.KeyPurpose;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Database entity for KMS Key (Key Encryption Key) metadata.
|
||||
* Tracks ownership, purpose, and lifecycle of KEKs used in envelope encryption.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "kms_keys")
|
||||
public class KMSKeyVO implements KMSKey {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "uuid", nullable = false, unique = true)
|
||||
private String uuid;
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "description", length = 1024)
|
||||
private String description;
|
||||
|
||||
@Column(name = "kek_label", nullable = false)
|
||||
private String kekLabel;
|
||||
|
||||
@Column(name = "purpose", nullable = false, length = 32)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private KeyPurpose purpose;
|
||||
|
||||
@Column(name = "account_id", nullable = false)
|
||||
private Long accountId;
|
||||
|
||||
@Column(name = "domain_id", nullable = false)
|
||||
private Long domainId;
|
||||
|
||||
@Column(name = "zone_id", nullable = false)
|
||||
private Long zoneId;
|
||||
|
||||
@Column(name = "provider_name", nullable = false, length = 64)
|
||||
private String providerName;
|
||||
|
||||
@Column(name = "algorithm", nullable = false, length = 64)
|
||||
private String algorithm;
|
||||
|
||||
@Column(name = "key_bits", nullable = false)
|
||||
private Integer keyBits;
|
||||
|
||||
@Column(name = "state", nullable = false, length = 32)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private State state;
|
||||
|
||||
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date created;
|
||||
|
||||
@Column(name = GenericDao.REMOVED_COLUMN)
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date removed;
|
||||
|
||||
/**
|
||||
* Default constructor (required by JPA)
|
||||
*/
|
||||
public KMSKeyVO() {
|
||||
this.uuid = UUID.randomUUID().toString();
|
||||
this.created = new Date();
|
||||
this.state = State.Enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a new KMS key
|
||||
*/
|
||||
public KMSKeyVO(String name, String description, String kekLabel, KeyPurpose purpose,
|
||||
Long accountId, Long domainId, Long zoneId, String providerName,
|
||||
String algorithm, Integer keyBits) {
|
||||
this();
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.kekLabel = kekLabel;
|
||||
this.purpose = purpose;
|
||||
this.accountId = accountId;
|
||||
this.domainId = domainId;
|
||||
this.zoneId = zoneId;
|
||||
this.providerName = providerName;
|
||||
this.algorithm = algorithm;
|
||||
this.keyBits = keyBits;
|
||||
}
|
||||
|
||||
// Identity interface methods
|
||||
|
||||
@Override
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// KMSKey interface methods
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKekLabel() {
|
||||
return kekLabel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeyPurpose getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getKeyBits() {
|
||||
return keyBits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getRemoved() {
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ControlledEntity interface methods
|
||||
|
||||
@Override
|
||||
public long getAccountId() {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDomainId() {
|
||||
return domainId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getEntityType() {
|
||||
return KMSKey.class;
|
||||
}
|
||||
|
||||
// Setters
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void setKekLabel(String kekLabel) {
|
||||
this.kekLabel = kekLabel;
|
||||
}
|
||||
|
||||
public void setPurpose(KeyPurpose purpose) {
|
||||
this.purpose = purpose;
|
||||
}
|
||||
|
||||
public void setAccountId(Long accountId) {
|
||||
this.accountId = accountId;
|
||||
}
|
||||
|
||||
public void setDomainId(Long domainId) {
|
||||
this.domainId = domainId;
|
||||
}
|
||||
|
||||
public void setZoneId(Long zoneId) {
|
||||
this.zoneId = zoneId;
|
||||
}
|
||||
|
||||
public void setProviderName(String providerName) {
|
||||
this.providerName = providerName;
|
||||
}
|
||||
|
||||
public void setAlgorithm(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public void setKeyBits(Integer keyBits) {
|
||||
this.keyBits = keyBits;
|
||||
}
|
||||
|
||||
public void setState(State state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public void setRemoved(Date removed) {
|
||||
this.removed = removed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("KMSKey[id=%d, uuid=%s, name=%s, purpose=%s, account=%d, zone=%d, state=%s]",
|
||||
id, uuid, name, purpose, accountId, zoneId, state);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
// 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 com.cloud.utils.db.GenericDao;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Temporal;
|
||||
import javax.persistence.TemporalType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Database entity for storing wrapped (encrypted) Data Encryption Keys.
|
||||
* Each entry represents a DEK that has been encrypted by a Key Encryption Key (KEK).
|
||||
* KEK metadata is stored in kms_keys table via the kms_key_id foreign key.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "kms_wrapped_key")
|
||||
public class KMSWrappedKeyVO {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "uuid", nullable = false, unique = true)
|
||||
private String uuid;
|
||||
|
||||
@Column(name = "kms_key_id")
|
||||
private Long kmsKeyId;
|
||||
|
||||
@Column(name = "kek_version_id")
|
||||
private Long kekVersionId;
|
||||
|
||||
@Column(name = "zone_id", nullable = false)
|
||||
private Long zoneId;
|
||||
|
||||
@Column(name = "wrapped_blob", nullable = false, columnDefinition = "VARBINARY(4096)")
|
||||
private byte[] wrappedBlob;
|
||||
|
||||
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date created;
|
||||
|
||||
@Column(name = GenericDao.REMOVED_COLUMN)
|
||||
@Temporal(TemporalType.TIMESTAMP)
|
||||
private Date removed;
|
||||
|
||||
/**
|
||||
* Constructor for creating a new wrapped key entry
|
||||
*/
|
||||
public KMSWrappedKeyVO(KMSKeyVO kmsKey, byte[] wrappedBlob) {
|
||||
this();
|
||||
this.kmsKeyId = kmsKey.getId();
|
||||
this.zoneId = kmsKey.getZoneId();
|
||||
// Defensive copy
|
||||
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating a new wrapped key entry with KEK version
|
||||
*/
|
||||
public KMSWrappedKeyVO(KMSKeyVO kmsKey, Long kekVersionId, byte[] wrappedBlob) {
|
||||
this();
|
||||
this.kmsKeyId = kmsKey.getId();
|
||||
this.kekVersionId = kekVersionId;
|
||||
this.zoneId = kmsKey.getZoneId();
|
||||
// Defensive copy
|
||||
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with explicit parameters
|
||||
*/
|
||||
public KMSWrappedKeyVO(Long kmsKeyId, Long zoneId, byte[] wrappedBlob) {
|
||||
this();
|
||||
this.kmsKeyId = kmsKeyId;
|
||||
this.zoneId = zoneId;
|
||||
// Defensive copy
|
||||
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor with explicit parameters including KEK version
|
||||
*/
|
||||
public KMSWrappedKeyVO(Long kmsKeyId, Long kekVersionId, Long zoneId, byte[] wrappedBlob) {
|
||||
this();
|
||||
this.kmsKeyId = kmsKeyId;
|
||||
this.kekVersionId = kekVersionId;
|
||||
this.zoneId = zoneId;
|
||||
// Defensive copy
|
||||
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default constructor (required by JPA)
|
||||
*/
|
||||
public KMSWrappedKeyVO() {
|
||||
this.uuid = UUID.randomUUID().toString();
|
||||
this.created = new Date();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public Long getKmsKeyId() {
|
||||
return kmsKeyId;
|
||||
}
|
||||
|
||||
public void setKmsKeyId(Long kmsKeyId) {
|
||||
this.kmsKeyId = kmsKeyId;
|
||||
}
|
||||
|
||||
public Long getKekVersionId() {
|
||||
return kekVersionId;
|
||||
}
|
||||
|
||||
public void setKekVersionId(Long kekVersionId) {
|
||||
this.kekVersionId = kekVersionId;
|
||||
}
|
||||
|
||||
public Long getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
public void setZoneId(Long zoneId) {
|
||||
this.zoneId = zoneId;
|
||||
}
|
||||
|
||||
public byte[] getWrappedBlob() {
|
||||
// Return defensive copy
|
||||
return wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
|
||||
}
|
||||
|
||||
public void setWrappedBlob(byte[] wrappedBlob) {
|
||||
// Store defensive copy
|
||||
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public Date getRemoved() {
|
||||
return removed;
|
||||
}
|
||||
|
||||
public void setRemoved(Date removed) {
|
||||
this.removed = removed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KMSWrappedKeyVO{" +
|
||||
"id=" + id +
|
||||
", uuid='" + uuid + '\'' +
|
||||
", kmsKeyId=" + kmsKeyId +
|
||||
", kekVersionId=" + kekVersionId +
|
||||
", zoneId=" + zoneId +
|
||||
", blobSize=" + (wrappedBlob != null ? wrappedBlob.length : 0) +
|
||||
", created=" + created +
|
||||
", removed=" + removed +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
// 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.dao;
|
||||
|
||||
import com.cloud.utils.db.GenericDao;
|
||||
import org.apache.cloudstack.kms.KMSKekVersionVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DAO for KMSKekVersion entities
|
||||
*/
|
||||
public interface KMSKekVersionDao extends GenericDao<KMSKekVersionVO, Long> {
|
||||
|
||||
/**
|
||||
* Find a KEK version by UUID
|
||||
*/
|
||||
KMSKekVersionVO findByUuid(String uuid);
|
||||
|
||||
/**
|
||||
* Get the active version for a KMS key
|
||||
*/
|
||||
KMSKekVersionVO getActiveVersion(Long kmsKeyId);
|
||||
|
||||
/**
|
||||
* Get all versions that can be used for decryption (Active and Previous)
|
||||
*/
|
||||
List<KMSKekVersionVO> getVersionsForDecryption(Long kmsKeyId);
|
||||
|
||||
/**
|
||||
* List all versions for a KMS key
|
||||
*/
|
||||
List<KMSKekVersionVO> listByKmsKeyId(Long kmsKeyId);
|
||||
|
||||
/**
|
||||
* Find a specific version by KMS key ID and version number
|
||||
*/
|
||||
KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber);
|
||||
|
||||
/**
|
||||
* Find a KEK version by KEK label
|
||||
*/
|
||||
KMSKekVersionVO findByKekLabel(String kekLabel);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
// 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.dao;
|
||||
|
||||
import com.cloud.utils.db.GenericDaoBase;
|
||||
import com.cloud.utils.db.SearchBuilder;
|
||||
import com.cloud.utils.db.SearchCriteria;
|
||||
import org.apache.cloudstack.kms.KMSKekVersionVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of KMSKekVersionDao
|
||||
*/
|
||||
@Component
|
||||
public class KMSKekVersionDaoImpl extends GenericDaoBase<KMSKekVersionVO, Long> implements KMSKekVersionDao {
|
||||
|
||||
private final SearchBuilder<KMSKekVersionVO> uuidSearch;
|
||||
private final SearchBuilder<KMSKekVersionVO> kmsKeyIdSearch;
|
||||
private final SearchBuilder<KMSKekVersionVO> activeVersionSearch;
|
||||
private final SearchBuilder<KMSKekVersionVO> decryptionVersionsSearch;
|
||||
private final SearchBuilder<KMSKekVersionVO> versionNumberSearch;
|
||||
private final SearchBuilder<KMSKekVersionVO> kekLabelSearch;
|
||||
|
||||
public KMSKekVersionDaoImpl() {
|
||||
super();
|
||||
|
||||
// Search by UUID
|
||||
uuidSearch = createSearchBuilder();
|
||||
uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ);
|
||||
uuidSearch.and("removed", uuidSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
uuidSearch.done();
|
||||
|
||||
// Search by KMS key ID
|
||||
kmsKeyIdSearch = createSearchBuilder();
|
||||
kmsKeyIdSearch.and("kmsKeyId", kmsKeyIdSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
|
||||
kmsKeyIdSearch.and("removed", kmsKeyIdSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
kmsKeyIdSearch.done();
|
||||
|
||||
// Search for active version by KMS key ID
|
||||
activeVersionSearch = createSearchBuilder();
|
||||
activeVersionSearch.and("kmsKeyId", activeVersionSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
|
||||
activeVersionSearch.and("status", activeVersionSearch.entity().getStatus(), SearchCriteria.Op.EQ);
|
||||
activeVersionSearch.and("removed", activeVersionSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
activeVersionSearch.done();
|
||||
|
||||
// Search for versions usable for decryption (Active or Previous)
|
||||
decryptionVersionsSearch = createSearchBuilder();
|
||||
decryptionVersionsSearch.and("kmsKeyId", decryptionVersionsSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
|
||||
decryptionVersionsSearch.and("status", decryptionVersionsSearch.entity().getStatus(), SearchCriteria.Op.IN);
|
||||
decryptionVersionsSearch.and("removed", decryptionVersionsSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
decryptionVersionsSearch.done();
|
||||
|
||||
// Search by KMS key ID and version number
|
||||
versionNumberSearch = createSearchBuilder();
|
||||
versionNumberSearch.and("kmsKeyId", versionNumberSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
|
||||
versionNumberSearch.and("versionNumber", versionNumberSearch.entity().getVersionNumber(), SearchCriteria.Op.EQ);
|
||||
versionNumberSearch.and("removed", versionNumberSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
versionNumberSearch.done();
|
||||
|
||||
// Search by KEK label
|
||||
kekLabelSearch = createSearchBuilder();
|
||||
kekLabelSearch.and("kekLabel", kekLabelSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
|
||||
kekLabelSearch.and("removed", kekLabelSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
kekLabelSearch.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKekVersionVO findByUuid(String uuid) {
|
||||
SearchCriteria<KMSKekVersionVO> sc = uuidSearch.create();
|
||||
sc.setParameters("uuid", uuid);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKekVersionVO getActiveVersion(Long kmsKeyId) {
|
||||
SearchCriteria<KMSKekVersionVO> sc = activeVersionSearch.create();
|
||||
sc.setParameters("kmsKeyId", kmsKeyId);
|
||||
sc.setParameters("status", KMSKekVersionVO.Status.Active);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSKekVersionVO> getVersionsForDecryption(Long kmsKeyId) {
|
||||
SearchCriteria<KMSKekVersionVO> sc = decryptionVersionsSearch.create();
|
||||
sc.setParameters("kmsKeyId", kmsKeyId);
|
||||
sc.setParameters("status", KMSKekVersionVO.Status.Active, KMSKekVersionVO.Status.Previous);
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSKekVersionVO> listByKmsKeyId(Long kmsKeyId) {
|
||||
SearchCriteria<KMSKekVersionVO> sc = kmsKeyIdSearch.create();
|
||||
sc.setParameters("kmsKeyId", kmsKeyId);
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber) {
|
||||
SearchCriteria<KMSKekVersionVO> sc = versionNumberSearch.create();
|
||||
sc.setParameters("kmsKeyId", kmsKeyId);
|
||||
sc.setParameters("versionNumber", versionNumber);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKekVersionVO findByKekLabel(String kekLabel) {
|
||||
SearchCriteria<KMSKekVersionVO> sc = kekLabelSearch.create();
|
||||
sc.setParameters("kekLabel", kekLabel);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// 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.dao;
|
||||
|
||||
import com.cloud.utils.db.GenericDao;
|
||||
import org.apache.cloudstack.framework.kms.KeyPurpose;
|
||||
import org.apache.cloudstack.kms.KMSKey;
|
||||
import org.apache.cloudstack.kms.KMSKeyVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DAO for KMSKey entities
|
||||
*/
|
||||
public interface KMSKeyDao extends GenericDao<KMSKeyVO, Long> {
|
||||
|
||||
/**
|
||||
* Find a KMS key by UUID
|
||||
*/
|
||||
KMSKeyVO findByUuid(String uuid);
|
||||
|
||||
/**
|
||||
* Find a KMS key by KEK label and provider
|
||||
*/
|
||||
KMSKeyVO findByKekLabel(String kekLabel, String providerName);
|
||||
|
||||
/**
|
||||
* List KMS keys owned by an account
|
||||
*/
|
||||
List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, KMSKey.State state);
|
||||
|
||||
/**
|
||||
* List KMS keys in a domain (optionally including subdomains)
|
||||
*/
|
||||
List<KMSKeyVO> listByDomain(Long domainId, KeyPurpose purpose, KMSKey.State state, boolean includeSubdomains);
|
||||
|
||||
/**
|
||||
* List KMS keys in a zone
|
||||
*/
|
||||
List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, KMSKey.State state);
|
||||
|
||||
/**
|
||||
* List KMS keys accessible to an account (owns or in parent domain)
|
||||
*/
|
||||
List<KMSKeyVO> listAccessibleKeys(Long accountId, Long domainId, Long zoneId, KeyPurpose purpose, KMSKey.State state);
|
||||
|
||||
/**
|
||||
* Count how many wrapped keys reference this KEK
|
||||
*/
|
||||
long countWrappedKeysByKmsKey(Long kmsKeyId);
|
||||
|
||||
/**
|
||||
* Count KEKs by label (to check for duplicates)
|
||||
*/
|
||||
long countByKekLabel(String kekLabel, String providerName);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
// 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.dao;
|
||||
|
||||
import com.cloud.utils.db.GenericDaoBase;
|
||||
import com.cloud.utils.db.SearchBuilder;
|
||||
import com.cloud.utils.db.SearchCriteria;
|
||||
import org.apache.cloudstack.framework.kms.KeyPurpose;
|
||||
import org.apache.cloudstack.kms.KMSKey;
|
||||
import org.apache.cloudstack.kms.KMSKeyVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of KMSKeyDao
|
||||
*/
|
||||
@Component
|
||||
public class KMSKeyDaoImpl extends GenericDaoBase<KMSKeyVO, Long> implements KMSKeyDao {
|
||||
|
||||
private final SearchBuilder<KMSKeyVO> uuidSearch;
|
||||
private final SearchBuilder<KMSKeyVO> kekLabelSearch;
|
||||
private final SearchBuilder<KMSKeyVO> accountSearch;
|
||||
private final SearchBuilder<KMSKeyVO> domainSearch;
|
||||
private final SearchBuilder<KMSKeyVO> zoneSearch;
|
||||
private final SearchBuilder<KMSKeyVO> accessibleSearch;
|
||||
|
||||
@Inject
|
||||
private KMSWrappedKeyDao kmsWrappedKeyDao;
|
||||
|
||||
public KMSKeyDaoImpl() {
|
||||
super();
|
||||
|
||||
// Search by UUID
|
||||
uuidSearch = createSearchBuilder();
|
||||
uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ);
|
||||
uuidSearch.and("removed", uuidSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
uuidSearch.done();
|
||||
|
||||
// Search by KEK label and provider
|
||||
kekLabelSearch = createSearchBuilder();
|
||||
kekLabelSearch.and("kekLabel", kekLabelSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
|
||||
kekLabelSearch.and("providerName", kekLabelSearch.entity().getProviderName(), SearchCriteria.Op.EQ);
|
||||
kekLabelSearch.and("removed", kekLabelSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
kekLabelSearch.done();
|
||||
|
||||
// Search by account
|
||||
accountSearch = createSearchBuilder();
|
||||
accountSearch.and("accountId", accountSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
|
||||
accountSearch.and("purpose", accountSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
|
||||
accountSearch.and("state", accountSearch.entity().getState(), SearchCriteria.Op.EQ);
|
||||
accountSearch.and("removed", accountSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
accountSearch.done();
|
||||
|
||||
// Search by domain
|
||||
domainSearch = createSearchBuilder();
|
||||
domainSearch.and("domainId", domainSearch.entity().getDomainId(), SearchCriteria.Op.EQ);
|
||||
domainSearch.and("purpose", domainSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
|
||||
domainSearch.and("state", domainSearch.entity().getState(), SearchCriteria.Op.EQ);
|
||||
domainSearch.and("removed", domainSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
domainSearch.done();
|
||||
|
||||
// Search by zone
|
||||
zoneSearch = createSearchBuilder();
|
||||
zoneSearch.and("zoneId", zoneSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
|
||||
zoneSearch.and("purpose", zoneSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
|
||||
zoneSearch.and("state", zoneSearch.entity().getState(), SearchCriteria.Op.EQ);
|
||||
zoneSearch.and("removed", zoneSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
zoneSearch.done();
|
||||
|
||||
// Search for accessible keys (by account or domain)
|
||||
accessibleSearch = createSearchBuilder();
|
||||
accessibleSearch.and("accountId", accessibleSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
|
||||
accessibleSearch.and("domainId", accessibleSearch.entity().getDomainId(), SearchCriteria.Op.EQ);
|
||||
accessibleSearch.and("zoneId", accessibleSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
|
||||
accessibleSearch.and("purpose", accessibleSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
|
||||
accessibleSearch.and("state", accessibleSearch.entity().getState(), SearchCriteria.Op.EQ);
|
||||
accessibleSearch.and("removed", accessibleSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
accessibleSearch.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKeyVO findByUuid(String uuid) {
|
||||
SearchCriteria<KMSKeyVO> sc = uuidSearch.create();
|
||||
sc.setParameters("uuid", uuid);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKeyVO findByKekLabel(String kekLabel, String providerName) {
|
||||
SearchCriteria<KMSKeyVO> sc = kekLabelSearch.create();
|
||||
sc.setParameters("kekLabel", kekLabel);
|
||||
sc.setParameters("providerName", providerName);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, KMSKey.State state) {
|
||||
SearchCriteria<KMSKeyVO> sc = accountSearch.create();
|
||||
sc.setParameters("accountId", accountId);
|
||||
if (purpose != null) {
|
||||
sc.setParameters("purpose", purpose);
|
||||
}
|
||||
if (state != null) {
|
||||
sc.setParameters("state", state);
|
||||
}
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSKeyVO> listByDomain(Long domainId, KeyPurpose purpose, KMSKey.State state, boolean includeSubdomains) {
|
||||
SearchCriteria<KMSKeyVO> sc = domainSearch.create();
|
||||
sc.setParameters("domainId", domainId);
|
||||
if (purpose != null) {
|
||||
sc.setParameters("purpose", purpose);
|
||||
}
|
||||
if (state != null) {
|
||||
sc.setParameters("state", state);
|
||||
}
|
||||
// TODO: Implement subdomain traversal if includeSubdomains is true
|
||||
// For now, just return keys in this domain
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, KMSKey.State state) {
|
||||
SearchCriteria<KMSKeyVO> sc = zoneSearch.create();
|
||||
sc.setParameters("zoneId", zoneId);
|
||||
if (purpose != null) {
|
||||
sc.setParameters("purpose", purpose);
|
||||
}
|
||||
if (state != null) {
|
||||
sc.setParameters("state", state);
|
||||
}
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSKeyVO> listAccessibleKeys(Long accountId, Long domainId, Long zoneId, KeyPurpose purpose, KMSKey.State state) {
|
||||
SearchCriteria<KMSKeyVO> sc = accessibleSearch.create();
|
||||
// Keys owned by the account or in the domain
|
||||
sc.setParameters("accountId", accountId);
|
||||
if (zoneId != null) {
|
||||
sc.setParameters("zoneId", zoneId);
|
||||
}
|
||||
if (purpose != null) {
|
||||
sc.setParameters("purpose", purpose);
|
||||
}
|
||||
if (state != null) {
|
||||
sc.setParameters("state", state);
|
||||
}
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countWrappedKeysByKmsKey(Long kmsKeyId) {
|
||||
if (kmsKeyId == null) {
|
||||
return 0;
|
||||
}
|
||||
// Delegate to KMSWrappedKeyDao
|
||||
return kmsWrappedKeyDao.countByKmsKeyId(kmsKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countByKekLabel(String kekLabel, String providerName) {
|
||||
SearchCriteria<KMSKeyVO> sc = kekLabelSearch.create();
|
||||
sc.setParameters("kekLabel", kekLabel);
|
||||
sc.setParameters("providerName", providerName);
|
||||
Integer count = getCount(sc);
|
||||
return count != null ? count.longValue() : 0L;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// 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.dao;
|
||||
|
||||
import com.cloud.utils.db.GenericDao;
|
||||
import org.apache.cloudstack.kms.KMSWrappedKeyVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data Access Object for KMS Wrapped Keys.
|
||||
* This DAO is purpose-agnostic and can be used for any key purpose
|
||||
* (volumes, TLS certs, config secrets, etc.)
|
||||
*/
|
||||
public interface KMSWrappedKeyDao extends GenericDao<KMSWrappedKeyVO, Long> {
|
||||
|
||||
/**
|
||||
* Find a wrapped key by UUID
|
||||
*
|
||||
* @param uuid the key UUID
|
||||
* @return the wrapped key, or null if not found
|
||||
*/
|
||||
KMSWrappedKeyVO findByUuid(String uuid);
|
||||
|
||||
/**
|
||||
* List all wrapped keys using a specific KMS key
|
||||
* (useful for key rotation)
|
||||
*
|
||||
* @param kmsKeyId the KMS key ID (FK to kms_keys)
|
||||
* @return list of wrapped keys
|
||||
*/
|
||||
List<KMSWrappedKeyVO> listByKmsKeyId(Long kmsKeyId);
|
||||
|
||||
/**
|
||||
* List all wrapped keys in a zone
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @return list of wrapped keys
|
||||
*/
|
||||
List<KMSWrappedKeyVO> listByZone(Long zoneId);
|
||||
|
||||
/**
|
||||
* Count wrapped keys using a specific KMS key
|
||||
*
|
||||
* @param kmsKeyId the KMS key ID (FK to kms_keys)
|
||||
* @return count of keys
|
||||
*/
|
||||
long countByKmsKeyId(Long kmsKeyId);
|
||||
|
||||
/**
|
||||
* List all wrapped keys using a specific KEK version
|
||||
*
|
||||
* @param kekVersionId the KEK version ID (FK to kms_kek_versions)
|
||||
* @return list of wrapped keys
|
||||
*/
|
||||
List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
// 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.dao;
|
||||
|
||||
import com.cloud.utils.db.GenericDaoBase;
|
||||
import com.cloud.utils.db.SearchBuilder;
|
||||
import com.cloud.utils.db.SearchCriteria;
|
||||
import org.apache.cloudstack.kms.KMSWrappedKeyVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation of KMSWrappedKeyDao
|
||||
*/
|
||||
@Component
|
||||
public class KMSWrappedKeyDaoImpl extends GenericDaoBase<KMSWrappedKeyVO, Long> implements KMSWrappedKeyDao {
|
||||
|
||||
private final SearchBuilder<KMSWrappedKeyVO> uuidSearch;
|
||||
private final SearchBuilder<KMSWrappedKeyVO> kmsKeyIdSearch;
|
||||
private final SearchBuilder<KMSWrappedKeyVO> kekVersionIdSearch;
|
||||
private final SearchBuilder<KMSWrappedKeyVO> zoneSearch;
|
||||
|
||||
public KMSWrappedKeyDaoImpl() {
|
||||
super();
|
||||
|
||||
// Search by UUID
|
||||
uuidSearch = createSearchBuilder();
|
||||
uuidSearch.and("uuid", uuidSearch.entity().getUuid(), SearchCriteria.Op.EQ);
|
||||
uuidSearch.and("removed", uuidSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
uuidSearch.done();
|
||||
|
||||
// Search by KMS Key ID (FK to kms_keys)
|
||||
kmsKeyIdSearch = createSearchBuilder();
|
||||
kmsKeyIdSearch.and("kmsKeyId", kmsKeyIdSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
|
||||
kmsKeyIdSearch.and("removed", kmsKeyIdSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
kmsKeyIdSearch.done();
|
||||
|
||||
// Search by KEK Version ID (FK to kms_kek_versions)
|
||||
kekVersionIdSearch = createSearchBuilder();
|
||||
kekVersionIdSearch.and("kekVersionId", kekVersionIdSearch.entity().getKekVersionId(), SearchCriteria.Op.EQ);
|
||||
kekVersionIdSearch.and("removed", kekVersionIdSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
kekVersionIdSearch.done();
|
||||
|
||||
// Search by zone
|
||||
zoneSearch = createSearchBuilder();
|
||||
zoneSearch.and("zoneId", zoneSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
|
||||
zoneSearch.and("removed", zoneSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
|
||||
zoneSearch.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSWrappedKeyVO findByUuid(String uuid) {
|
||||
SearchCriteria<KMSWrappedKeyVO> sc = uuidSearch.create();
|
||||
sc.setParameters("uuid", uuid);
|
||||
return findOneBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSWrappedKeyVO> listByKmsKeyId(Long kmsKeyId) {
|
||||
SearchCriteria<KMSWrappedKeyVO> sc = kmsKeyIdSearch.create();
|
||||
sc.setParameters("kmsKeyId", kmsKeyId);
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSWrappedKeyVO> listByZone(Long zoneId) {
|
||||
SearchCriteria<KMSWrappedKeyVO> sc = zoneSearch.create();
|
||||
sc.setParameters("zoneId", zoneId);
|
||||
return listBy(sc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countByKmsKeyId(Long kmsKeyId) {
|
||||
SearchCriteria<KMSWrappedKeyVO> sc = kmsKeyIdSearch.create();
|
||||
sc.setParameters("kmsKeyId", kmsKeyId);
|
||||
Integer count = getCount(sc);
|
||||
return count != null ? count.longValue() : 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId) {
|
||||
SearchCriteria<KMSWrappedKeyVO> sc = kekVersionIdSearch.create();
|
||||
sc.setParameters("kekVersionId", kekVersionId);
|
||||
return listBy(sc);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -312,4 +312,7 @@
|
|||
<bean id="importVMTaskDaoImpl" class="com.cloud.vm.dao.ImportVMTaskDaoImpl" />
|
||||
<bean id="apiKeyPairDaoImpl" class="org.apache.cloudstack.acl.dao.ApiKeyPairDaoImpl" />
|
||||
<bean id="apiKeyPairPermissionsDaoImpl" class="org.apache.cloudstack.acl.dao.ApiKeyPairPermissionsDaoImpl" />
|
||||
<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" />
|
||||
</beans>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`webhook_filter` (
|
|||
CONSTRAINT `fk_webhook_filter__webhook_id` FOREIGN KEY(`webhook_id`) REFERENCES `webhook`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
||||
-- "api_keypair" table for API and secret keys
|
||||
CREATE TABLE IF NOT EXISTS `cloud`.`api_keypair` (
|
||||
`id` bigint(20) unsigned NOT NULL auto_increment,
|
||||
|
|
@ -114,3 +115,77 @@ CALL `cloud`.`IDEMPOTENT_UPDATE_API_PERMISSION`('Resource Admin', 'deleteUserKey
|
|||
|
||||
-- Add conserve mode for VPC offerings
|
||||
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tinyint(1) unsigned NULL DEFAULT 0 COMMENT ''True if the VPC offering is IP conserve mode enabled, allowing public IP services to be used across multiple VPC tiers'' ');
|
||||
|
||||
-- KMS Keys (Key Encryption Key Metadata)
|
||||
-- Account-scoped KEKs for envelope encryption
|
||||
CREATE TABLE IF NOT EXISTS `cloud`.`kms_keys` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
|
||||
`uuid` VARCHAR(40) NOT NULL COMMENT 'UUID - user-facing identifier',
|
||||
`name` VARCHAR(255) NOT NULL COMMENT 'User-friendly name',
|
||||
`description` VARCHAR(1024) COMMENT 'User description',
|
||||
`kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID',
|
||||
`purpose` VARCHAR(32) NOT NULL COMMENT 'Key purpose (VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET)',
|
||||
`account_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning account',
|
||||
`domain_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning domain',
|
||||
`zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone where key is valid',
|
||||
`provider_name` VARCHAR(64) NOT NULL COMMENT 'KMS provider (database, pkcs11, etc.)',
|
||||
`algorithm` VARCHAR(64) NOT NULL DEFAULT 'AES/GCM/NoPadding' COMMENT 'Encryption algorithm',
|
||||
`key_bits` INT NOT NULL DEFAULT 256 COMMENT 'Key size in bits',
|
||||
`state` VARCHAR(32) NOT NULL DEFAULT 'Enabled' COMMENT 'Enabled, Disabled, or Deleted',
|
||||
`created` DATETIME NOT NULL COMMENT 'Creation timestamp',
|
||||
`removed` DATETIME COMMENT 'Removal timestamp for soft delete',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_uuid` (`uuid`),
|
||||
INDEX `idx_account_purpose` (`account_id`, `purpose`, `state`),
|
||||
INDEX `idx_domain_purpose` (`domain_id`, `purpose`, `state`),
|
||||
INDEX `idx_zone_state` (`zone_id`, `state`),
|
||||
INDEX `idx_kek_label_provider` (`kek_label`, `provider_name`),
|
||||
CONSTRAINT `fk_kms_keys__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_kms_keys__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_kms_keys__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS Key (KEK) metadata - account-scoped keys for envelope encryption';
|
||||
|
||||
-- KMS KEK Versions (multiple KEKs per KMS key for gradual rotation)
|
||||
-- Supports multiple KEK versions per logical KMS key during rotation
|
||||
CREATE TABLE IF NOT EXISTS `cloud`.`kms_kek_versions` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
|
||||
`uuid` VARCHAR(40) NOT NULL COMMENT 'UUID',
|
||||
`kms_key_id` BIGINT UNSIGNED NOT NULL COMMENT 'Reference to kms_keys table',
|
||||
`version_number` INT NOT NULL COMMENT 'Version number (1, 2, 3, ...)',
|
||||
`kek_label` VARCHAR(255) NOT NULL COMMENT 'Provider-specific KEK label/ID for this version',
|
||||
`status` VARCHAR(32) NOT NULL DEFAULT 'Active' COMMENT 'Active, Previous, Archived',
|
||||
`created` DATETIME NOT NULL COMMENT 'Creation timestamp',
|
||||
`removed` DATETIME COMMENT 'Removal timestamp for soft delete',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_uuid` (`uuid`),
|
||||
UNIQUE KEY `uk_kms_key_version` (`kms_key_id`, `version_number`, `removed`),
|
||||
INDEX `idx_kms_key_status` (`kms_key_id`, `status`, `removed`),
|
||||
INDEX `idx_kek_label` (`kek_label`),
|
||||
CONSTRAINT `fk_kms_kek_versions__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KEK versions for a KMS key - supports gradual rotation';
|
||||
|
||||
-- KMS Wrapped Keys (Data Encryption Keys)
|
||||
-- Generic table for wrapped DEKs - references kms_keys for metadata and kek_versions for specific KEK version
|
||||
CREATE TABLE IF NOT EXISTS `cloud`.`kms_wrapped_key` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID',
|
||||
`uuid` VARCHAR(40) NOT NULL COMMENT 'UUID',
|
||||
`kms_key_id` BIGINT UNSIGNED COMMENT 'Reference to kms_keys table',
|
||||
`kek_version_id` BIGINT UNSIGNED COMMENT 'Reference to kms_kek_versions table',
|
||||
`zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone ID for zone-scoped keys',
|
||||
`wrapped_blob` VARBINARY(4096) NOT NULL COMMENT 'Encrypted DEK material',
|
||||
`created` DATETIME NOT NULL COMMENT 'Creation timestamp',
|
||||
`removed` DATETIME COMMENT 'Removal timestamp for soft delete',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_uuid` (`uuid`),
|
||||
INDEX `idx_kms_key_id` (`kms_key_id`, `removed`),
|
||||
INDEX `idx_kek_version_id` (`kek_version_id`, `removed`),
|
||||
INDEX `idx_zone_id` (`zone_id`, `removed`),
|
||||
CONSTRAINT `fk_kms_wrapped_key__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_kms_wrapped_key__kek_version_id` FOREIGN KEY (`kek_version_id`) REFERENCES `kms_kek_versions`(`id`) ON DELETE RESTRICT,
|
||||
CONSTRAINT `fk_kms_wrapped_key__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS wrapped encryption keys (DEKs) - references kms_keys for KEK metadata and kek_versions for specific version';
|
||||
|
||||
-- Add KMS wrapped key reference to volumes table
|
||||
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'kms_wrapped_key_id', 'BIGINT UNSIGNED COMMENT ''KMS wrapped key ID for volume encryption''');
|
||||
CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.volumes', 'fk_volumes__kms_wrapped_key_id', '(kms_wrapped_key_id)', '`kms_wrapped_key`(`id`)');
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>cloud-framework-kms</artifactId>
|
||||
<name>Apache CloudStack Framework - Key Management Service</name>
|
||||
<description>Core KMS framework with provider-agnostic interfaces</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloudstack-framework</artifactId>
|
||||
<version>4.23.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-utils</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-config</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
// 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.framework.kms;
|
||||
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
/**
|
||||
* Exception class for KMS-related errors with structured error types
|
||||
* to enable proper retry logic and error handling.
|
||||
*/
|
||||
public class KMSException extends CloudRuntimeException {
|
||||
|
||||
/**
|
||||
* Error types for KMS operations to enable intelligent retry logic
|
||||
*/
|
||||
public enum ErrorType {
|
||||
/**
|
||||
* Provider not initialized or unavailable
|
||||
*/
|
||||
PROVIDER_NOT_INITIALIZED(false),
|
||||
|
||||
/**
|
||||
* KEK not found in backend
|
||||
*/
|
||||
KEK_NOT_FOUND(false),
|
||||
|
||||
/**
|
||||
* KEK with given label already exists
|
||||
*/
|
||||
KEY_ALREADY_EXISTS(false),
|
||||
|
||||
/**
|
||||
* Invalid parameters provided
|
||||
*/
|
||||
INVALID_PARAMETER(false),
|
||||
|
||||
/**
|
||||
* Wrap/unwrap operation failed
|
||||
*/
|
||||
WRAP_UNWRAP_FAILED(true),
|
||||
|
||||
/**
|
||||
* KEK operation (create/delete) failed
|
||||
*/
|
||||
KEK_OPERATION_FAILED(true),
|
||||
|
||||
/**
|
||||
* Health check failed
|
||||
*/
|
||||
HEALTH_CHECK_FAILED(true),
|
||||
|
||||
/**
|
||||
* Transient network or communication error
|
||||
*/
|
||||
TRANSIENT_ERROR(true),
|
||||
|
||||
/**
|
||||
* Unknown error
|
||||
*/
|
||||
UNKNOWN(false);
|
||||
|
||||
private final boolean retryable;
|
||||
|
||||
ErrorType(boolean retryable) {
|
||||
this.retryable = retryable;
|
||||
}
|
||||
|
||||
public boolean isRetryable() {
|
||||
return retryable;
|
||||
}
|
||||
}
|
||||
|
||||
private final ErrorType errorType;
|
||||
|
||||
public KMSException(String message) {
|
||||
super(message);
|
||||
this.errorType = ErrorType.UNKNOWN;
|
||||
}
|
||||
|
||||
public KMSException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorType = ErrorType.UNKNOWN;
|
||||
}
|
||||
|
||||
public KMSException(ErrorType errorType, String message) {
|
||||
super(message);
|
||||
this.errorType = errorType;
|
||||
}
|
||||
|
||||
public KMSException(ErrorType errorType, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.errorType = errorType;
|
||||
}
|
||||
|
||||
public static KMSException providerNotInitialized(String details) {
|
||||
return new KMSException(ErrorType.PROVIDER_NOT_INITIALIZED,
|
||||
"KMS provider not initialized: " + details);
|
||||
}
|
||||
|
||||
public static KMSException kekNotFound(String kekId) {
|
||||
return new KMSException(ErrorType.KEK_NOT_FOUND,
|
||||
"KEK not found: " + kekId);
|
||||
}
|
||||
|
||||
// Static factory methods for common error types
|
||||
|
||||
public static KMSException keyAlreadyExists(String details) {
|
||||
return new KMSException(ErrorType.KEY_ALREADY_EXISTS,
|
||||
"Key already exists: " + details);
|
||||
}
|
||||
|
||||
public static KMSException invalidParameter(String details) {
|
||||
return new KMSException(ErrorType.INVALID_PARAMETER,
|
||||
"Invalid parameter: " + details);
|
||||
}
|
||||
|
||||
public static KMSException wrapUnwrapFailed(String details, Throwable cause) {
|
||||
return new KMSException(ErrorType.WRAP_UNWRAP_FAILED,
|
||||
"Wrap/unwrap operation failed: " + details, cause);
|
||||
}
|
||||
|
||||
public static KMSException wrapUnwrapFailed(String details) {
|
||||
return new KMSException(ErrorType.WRAP_UNWRAP_FAILED,
|
||||
"Wrap/unwrap operation failed: " + details);
|
||||
}
|
||||
|
||||
public static KMSException kekOperationFailed(String details, Throwable cause) {
|
||||
return new KMSException(ErrorType.KEK_OPERATION_FAILED,
|
||||
"KEK operation failed: " + details, cause);
|
||||
}
|
||||
|
||||
public static KMSException kekOperationFailed(String details) {
|
||||
return new KMSException(ErrorType.KEK_OPERATION_FAILED,
|
||||
"KEK operation failed: " + details);
|
||||
}
|
||||
|
||||
public static KMSException healthCheckFailed(String details, Throwable cause) {
|
||||
return new KMSException(ErrorType.HEALTH_CHECK_FAILED,
|
||||
"Health check failed: " + details, cause);
|
||||
}
|
||||
|
||||
public static KMSException transientError(String details, Throwable cause) {
|
||||
return new KMSException(ErrorType.TRANSIENT_ERROR,
|
||||
"Transient error: " + details, cause);
|
||||
}
|
||||
|
||||
public ErrorType getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "KMSException{" +
|
||||
"errorType=" + errorType +
|
||||
", retryable=" + isRetryable() +
|
||||
", message='" + getMessage() + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public boolean isRetryable() {
|
||||
return errorType.isRetryable();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// 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.framework.kms;
|
||||
|
||||
import org.apache.cloudstack.framework.config.Configurable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abstract provider contract for Key Management Service operations.
|
||||
* <p>
|
||||
* Implementations provide the cryptographic backend (HSM via PKCS#11, database, cloud KMS, etc.)
|
||||
* for secure key wrapping/unwrapping using envelope encryption.
|
||||
* <p>
|
||||
* Design principles:
|
||||
* - KEKs (Key Encryption Keys) never leave the secure backend
|
||||
* - DEKs (Data Encryption Keys) are wrapped by KEKs for storage
|
||||
* - Plaintext DEKs exist only transiently in memory during wrap/unwrap
|
||||
* - All operations are purpose-scoped to prevent key reuse
|
||||
* <p>
|
||||
* Thread-safety: Implementations must be thread-safe for concurrent operations.
|
||||
*/
|
||||
public interface KMSProvider extends Configurable {
|
||||
|
||||
/**
|
||||
* Get the unique name of this provider
|
||||
*
|
||||
* @return provider name (e.g., "database", "pkcs11")
|
||||
*/
|
||||
String getProviderName();
|
||||
|
||||
// ==================== KEK Management ====================
|
||||
|
||||
/**
|
||||
* Create a new Key Encryption Key (KEK) in the secure backend
|
||||
*
|
||||
* @param purpose the purpose/scope for this KEK
|
||||
* @param label human-readable label for the KEK (must be unique within purpose)
|
||||
* @param keyBits key size in bits (typically 128, 192, or 256)
|
||||
* @return the KEK identifier (label or handle) for later reference
|
||||
* @throws KMSException if KEK creation fails
|
||||
*/
|
||||
String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException;
|
||||
|
||||
/**
|
||||
* Delete a KEK from the secure backend.
|
||||
* WARNING: This will make all DEKs wrapped by this KEK unrecoverable.
|
||||
*
|
||||
* @param kekId the KEK identifier to delete
|
||||
* @throws KMSException if deletion fails or KEK not found
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param kekId the KEK identifier to check
|
||||
* @return true if KEK is available
|
||||
* @throws KMSException if check fails
|
||||
*/
|
||||
boolean isKekAvailable(String kekId) throws KMSException;
|
||||
|
||||
// ==================== DEK Operations ====================
|
||||
|
||||
/**
|
||||
* Wrap (encrypt) a plaintext Data Encryption Key with a KEK
|
||||
*
|
||||
* @param plainDek the plaintext DEK to wrap (caller must zeroize after call)
|
||||
* @param purpose the intended purpose of this DEK
|
||||
* @param kekLabel the label of the KEK to use for wrapping
|
||||
* @return WrappedKey containing the encrypted DEK and metadata
|
||||
* @throws KMSException if wrapping fails or KEK not found
|
||||
*/
|
||||
WrappedKey wrapKey(byte[] plainDek, KeyPurpose purpose, String kekLabel) throws KMSException;
|
||||
|
||||
/**
|
||||
* Unwrap (decrypt) a wrapped DEK to obtain the plaintext key
|
||||
* <p>
|
||||
* SECURITY: Caller MUST zeroize the returned byte array after use
|
||||
*
|
||||
* @param wrappedKey the wrapped key to decrypt
|
||||
* @return plaintext DEK (caller must zeroize!)
|
||||
* @throws KMSException if unwrapping fails or KEK not found
|
||||
*/
|
||||
byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException;
|
||||
|
||||
/**
|
||||
* Generate a new random DEK and immediately wrap it with a KEK
|
||||
* (convenience method combining generation + wrapping)
|
||||
*
|
||||
* @param purpose the intended purpose of the new DEK
|
||||
* @param kekLabel the label of the KEK to use for wrapping
|
||||
* @param keyBits DEK size in bits (typically 128, 192, or 256)
|
||||
* @return WrappedKey containing the newly generated and wrapped DEK
|
||||
* @throws KMSException if generation or wrapping fails
|
||||
*/
|
||||
WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException;
|
||||
|
||||
/**
|
||||
* Rewrap a DEK with a different KEK (used during key rotation).
|
||||
* This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK.
|
||||
*
|
||||
* @param oldWrappedKey the currently wrapped key
|
||||
* @param newKekLabel the label of the new KEK to wrap with
|
||||
* @return new WrappedKey encrypted with the new KEK
|
||||
* @throws KMSException if rewrapping fails
|
||||
*/
|
||||
WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException;
|
||||
|
||||
// ==================== Health & Status ====================
|
||||
|
||||
/**
|
||||
* Perform health check on the provider backend
|
||||
*
|
||||
* @return true if provider is healthy and operational
|
||||
* @throws KMSException if health check fails with critical error
|
||||
*/
|
||||
boolean healthCheck() throws KMSException;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
// 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.framework.kms;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* High-level service interface for Key Management Service operations.
|
||||
* <p>
|
||||
* This facade abstracts provider-specific details and provides zone-aware
|
||||
* routing, retry logic, and audit logging for KMS operations.
|
||||
* <p>
|
||||
* The service handles:
|
||||
* - Zone-scoped provider selection
|
||||
* - Configuration management (which provider, which KEK)
|
||||
* - Retry logic for transient failures
|
||||
* - Audit event emission
|
||||
* - Health monitoring
|
||||
*/
|
||||
public interface KMSService {
|
||||
|
||||
/**
|
||||
* Get the service name
|
||||
*
|
||||
* @return service name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
// ==================== Provider Management ====================
|
||||
|
||||
/**
|
||||
* List all registered KMS providers
|
||||
*
|
||||
* @return list of available providers
|
||||
*/
|
||||
List<? extends KMSProvider> listProviders();
|
||||
|
||||
/**
|
||||
* Get a specific provider by name
|
||||
*
|
||||
* @param name provider name
|
||||
* @return the provider, or null if not found
|
||||
*/
|
||||
KMSProvider getProvider(String name);
|
||||
|
||||
/**
|
||||
* Get the configured provider for a specific zone.
|
||||
* Falls back to global default if zone has no specific configuration.
|
||||
*
|
||||
* @param zoneId the zone ID (null for global)
|
||||
* @return the configured provider for the zone
|
||||
* @throws KMSException if no provider configured or provider not found
|
||||
*/
|
||||
KMSProvider getProviderForZone(Long zoneId) throws KMSException;
|
||||
|
||||
// ==================== KEK Management ====================
|
||||
|
||||
/**
|
||||
* Create a new KEK for a specific zone and purpose
|
||||
*
|
||||
* @param zoneId the zone ID (null for global)
|
||||
* @param purpose the purpose of the KEK
|
||||
* @param label optional custom label (null for auto-generated)
|
||||
* @param keyBits key size in bits
|
||||
* @return the KEK identifier
|
||||
* @throws KMSException if creation fails
|
||||
*/
|
||||
String createKek(Long zoneId, KeyPurpose purpose, String label, int keyBits) throws KMSException;
|
||||
|
||||
/**
|
||||
* Delete a KEK (use with extreme caution!)
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param kekId the KEK identifier to delete
|
||||
* @throws KMSException if deletion fails
|
||||
*/
|
||||
void deleteKek(Long zoneId, String kekId) throws KMSException;
|
||||
|
||||
/**
|
||||
* List KEKs for a zone and purpose
|
||||
*
|
||||
* @param zoneId the zone ID (null for all zones)
|
||||
* @param purpose the purpose filter (null for all purposes)
|
||||
* @return list of KEK identifiers
|
||||
* @throws KMSException if listing fails
|
||||
*/
|
||||
List<String> listKeks(Long zoneId, KeyPurpose purpose) throws KMSException;
|
||||
|
||||
/**
|
||||
* Check if a KEK is available in a zone
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param kekId the KEK identifier
|
||||
* @return true if available
|
||||
* @throws KMSException if check fails
|
||||
*/
|
||||
boolean isKekAvailable(Long zoneId, String kekId) throws KMSException;
|
||||
|
||||
/**
|
||||
* Rotate a KEK by creating a new one and rewrapping all associated DEKs.
|
||||
* This is an async operation that may take time for large deployments.
|
||||
*
|
||||
* @param zoneId the zone ID
|
||||
* @param purpose the purpose of keys to rotate
|
||||
* @param oldKekLabel the current KEK label (null for configured default)
|
||||
* @param newKekLabel the new KEK label (null for auto-generated)
|
||||
* @param keyBits the new KEK size in bits
|
||||
* @return the new KEK identifier
|
||||
* @throws KMSException if rotation fails
|
||||
*/
|
||||
String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
|
||||
String newKekLabel, int keyBits) throws KMSException;
|
||||
|
||||
// ==================== DEK Operations ====================
|
||||
|
||||
/**
|
||||
* Generate and wrap a new DEK for volume encryption
|
||||
*
|
||||
* @param zoneId the zone ID where the volume resides
|
||||
* @param purpose the key purpose (typically VOLUME_ENCRYPTION)
|
||||
* @param kekLabel the KEK label to use (null for configured default)
|
||||
* @param keyBits DEK size in bits
|
||||
* @return wrapped key ready for database storage
|
||||
* @throws KMSException if operation fails
|
||||
*/
|
||||
WrappedKey generateAndWrapDek(Long zoneId, KeyPurpose purpose,
|
||||
String kekLabel, int keyBits) throws KMSException;
|
||||
|
||||
/**
|
||||
* Unwrap a DEK for use (e.g., attaching encrypted volume)
|
||||
* <p>
|
||||
* SECURITY: Caller must zeroize the returned byte array after use
|
||||
*
|
||||
* @param wrappedKey the wrapped key from database
|
||||
* @return plaintext DEK (caller must zeroize!)
|
||||
* @throws KMSException if unwrap fails
|
||||
*/
|
||||
byte[] unwrapDek(WrappedKey wrappedKey) throws KMSException;
|
||||
|
||||
// ==================== Health & Status ====================
|
||||
|
||||
/**
|
||||
* Check health of KMS provider for a zone
|
||||
*
|
||||
* @param zoneId the zone ID (null for global check)
|
||||
* @return true if healthy
|
||||
* @throws KMSException if health check fails critically
|
||||
*/
|
||||
boolean healthCheck(Long zoneId) throws KMSException;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
// 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.framework.kms;
|
||||
|
||||
/**
|
||||
* Defines the purpose/usage scope for cryptographic keys in the KMS system.
|
||||
* This enables proper key segregation and prevents key reuse across different contexts.
|
||||
*/
|
||||
public enum KeyPurpose {
|
||||
/**
|
||||
* Keys used for encrypting VM disk volumes (LUKS, encrypted storage)
|
||||
*/
|
||||
VOLUME_ENCRYPTION("volume", "Volume disk encryption keys"),
|
||||
|
||||
/**
|
||||
* Keys used for protecting TLS certificate private keys
|
||||
*/
|
||||
TLS_CERT("tls", "TLS certificate private keys"),
|
||||
|
||||
/**
|
||||
* Keys used for encrypting configuration secrets and sensitive settings
|
||||
*/
|
||||
CONFIG_SECRET("config", "Configuration secrets");
|
||||
|
||||
private final String name;
|
||||
private final String description;
|
||||
|
||||
KeyPurpose(String name, String description) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string name to KeyPurpose enum
|
||||
*
|
||||
* @param name the string representation of the purpose
|
||||
* @return matching KeyPurpose
|
||||
* @throws IllegalArgumentException if no matching purpose found
|
||||
*/
|
||||
public static KeyPurpose fromString(String name) {
|
||||
for (KeyPurpose purpose : KeyPurpose.values()) {
|
||||
if (purpose.getName().equalsIgnoreCase(name)) {
|
||||
return purpose;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown KeyPurpose: " + name);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a KEK label with purpose prefix
|
||||
*
|
||||
* @param customLabel optional custom label suffix
|
||||
* @return formatted KEK label (e.g., "volume-kek-v1")
|
||||
*/
|
||||
public String generateKekLabel(String customLabel) {
|
||||
return name + "-kek-" + (customLabel != null ? customLabel : "v1");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
// 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.framework.kms;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Immutable Data Transfer Object representing an encrypted (wrapped) Data Encryption Key.
|
||||
* The wrapped key material contains the DEK encrypted by a Key Encryption Key (KEK)
|
||||
* stored in a secure backend (HSM, database, etc.).
|
||||
* <p>
|
||||
* This follows the envelope encryption pattern:
|
||||
* - DEK: encrypts actual data (e.g., disk volume)
|
||||
* - KEK: encrypts the DEK (never leaves secure storage)
|
||||
* - Wrapped Key: DEK encrypted by KEK, safe to store in database
|
||||
*/
|
||||
public class WrappedKey {
|
||||
private final String id;
|
||||
private final String kekId;
|
||||
private final KeyPurpose purpose;
|
||||
private final String algorithm;
|
||||
private final byte[] wrappedKeyMaterial;
|
||||
private final String providerName;
|
||||
private final Date created;
|
||||
private final Long zoneId;
|
||||
|
||||
/**
|
||||
* Create a new WrappedKey instance
|
||||
*
|
||||
* @param kekId ID/label of the KEK used to wrap this key
|
||||
* @param purpose the intended use of this key
|
||||
* @param algorithm encryption algorithm (e.g., "AES/GCM/NoPadding")
|
||||
* @param wrappedKeyMaterial the encrypted DEK blob
|
||||
* @param providerName name of the KMS provider that created this key
|
||||
* @param created timestamp when key was wrapped
|
||||
* @param zoneId optional zone ID for zone-scoped keys
|
||||
*/
|
||||
public WrappedKey(String kekId, KeyPurpose purpose, String algorithm,
|
||||
byte[] wrappedKeyMaterial, String providerName,
|
||||
Date created, Long zoneId) {
|
||||
this.id = null; // Will be set when persisted to DB
|
||||
this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null");
|
||||
this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null");
|
||||
this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null");
|
||||
this.providerName = providerName;
|
||||
|
||||
// Defensive copy to prevent external modification
|
||||
if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) {
|
||||
throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty");
|
||||
}
|
||||
this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
|
||||
|
||||
this.created = created != null ? new Date(created.getTime()) : new Date();
|
||||
this.zoneId = zoneId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for database-loaded keys with ID
|
||||
*/
|
||||
public WrappedKey(String id, String kekId, KeyPurpose purpose, String algorithm,
|
||||
byte[] wrappedKeyMaterial, String providerName,
|
||||
Date created, Long zoneId) {
|
||||
this.id = id;
|
||||
this.kekId = Objects.requireNonNull(kekId, "kekId cannot be null");
|
||||
this.purpose = Objects.requireNonNull(purpose, "purpose cannot be null");
|
||||
this.algorithm = Objects.requireNonNull(algorithm, "algorithm cannot be null");
|
||||
this.providerName = providerName;
|
||||
|
||||
if (wrappedKeyMaterial == null || wrappedKeyMaterial.length == 0) {
|
||||
throw new IllegalArgumentException("wrappedKeyMaterial cannot be null or empty");
|
||||
}
|
||||
this.wrappedKeyMaterial = Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
|
||||
|
||||
this.created = created != null ? new Date(created.getTime()) : new Date();
|
||||
this.zoneId = zoneId;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getKekId() {
|
||||
return kekId;
|
||||
}
|
||||
|
||||
public KeyPurpose getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wrapped key material. Returns a defensive copy to prevent modification.
|
||||
* Caller is responsible for zeroizing the returned array after use.
|
||||
*/
|
||||
public byte[] getWrappedKeyMaterial() {
|
||||
return Arrays.copyOf(wrappedKeyMaterial, wrappedKeyMaterial.length);
|
||||
}
|
||||
|
||||
public String getProviderName() {
|
||||
return providerName;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created != null ? new Date(created.getTime()) : null;
|
||||
}
|
||||
|
||||
public Long getZoneId() {
|
||||
return zoneId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(id, kekId, purpose, algorithm, providerName);
|
||||
result = 31 * result + Arrays.hashCode(wrappedKeyMaterial);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
WrappedKey that = (WrappedKey) o;
|
||||
return Objects.equals(id, that.id) &&
|
||||
Objects.equals(kekId, that.kekId) &&
|
||||
purpose == that.purpose &&
|
||||
Objects.equals(algorithm, that.algorithm) &&
|
||||
Arrays.equals(wrappedKeyMaterial, that.wrappedKeyMaterial) &&
|
||||
Objects.equals(providerName, that.providerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "WrappedKey{" +
|
||||
"id='" + id + '\'' +
|
||||
", kekId='" + kekId + '\'' +
|
||||
", purpose=" + purpose +
|
||||
", algorithm='" + algorithm + '\'' +
|
||||
", providerName='" + providerName + '\'' +
|
||||
", materialLength=" + (wrappedKeyMaterial != null ? wrappedKeyMaterial.length : 0) +
|
||||
", created=" + created +
|
||||
", zoneId=" + zoneId +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
<module>extensions</module>
|
||||
<module>ipc</module>
|
||||
<module>jobs</module>
|
||||
<module>kms</module>
|
||||
<module>managed-context</module>
|
||||
<module>quota</module>
|
||||
<module>rest</module>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>cloud-plugin-kms-database</artifactId>
|
||||
<name>Apache CloudStack Plugin - KMS Database Provider</name>
|
||||
<description>Database-backed KMS provider for encrypted key storage</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloudstack-kms-plugins</artifactId>
|
||||
<version>4.23.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-kms</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-config</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-utils</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.crypto.tink</groupId>
|
||||
<artifactId>tink</artifactId>
|
||||
<version>${cs.tink.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
// 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;
|
||||
|
||||
import com.google.crypto.tink.subtle.AesGcmJce;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
|
||||
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
|
||||
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.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Database-backed KMS provider that stores master KEKs encrypted in the configuration table.
|
||||
* Uses AES-256-GCM for all cryptographic operations.
|
||||
* <p>
|
||||
* This provider is suitable for deployments that don't have access to HSM hardware.
|
||||
* The master KEKs are stored encrypted using CloudStack's existing DBEncryptionUtil.
|
||||
*/
|
||||
public class DatabaseKMSProvider implements KMSProvider {
|
||||
// Configuration keys
|
||||
public static final ConfigKey<Boolean> CacheEnabled = new ConfigKey<>(
|
||||
"Advanced",
|
||||
Boolean.class,
|
||||
"kms.database.cache.enabled",
|
||||
"true",
|
||||
"Enable in-memory caching of KEKs for better performance",
|
||||
true,
|
||||
ConfigKey.Scope.Global
|
||||
);
|
||||
private static final Logger logger = LogManager.getLogger(DatabaseKMSProvider.class);
|
||||
private static final String PROVIDER_NAME = "database";
|
||||
private static final String KEK_CONFIG_PREFIX = "kms.database.kek.";
|
||||
private static final int GCM_IV_LENGTH = 12; // 96 bits recommended for GCM
|
||||
private static final int GCM_TAG_LENGTH = 16; // 128 bits
|
||||
private static final String ALGORITHM = "AES/GCM/NoPadding";
|
||||
// In-memory cache of KEKs (encrypted form cached, decrypted on demand)
|
||||
private final Map<String, byte[]> kekCache = new ConcurrentHashMap<>();
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
@Inject
|
||||
private ConfigurationDao configDao;
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return PROVIDER_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createKek(KeyPurpose purpose, String label, int keyBits) throws KMSException {
|
||||
if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
|
||||
throw KMSException.invalidParameter("Key size must be 128, 192, or 256 bits");
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(label)) {
|
||||
label = generateKekLabel(purpose);
|
||||
}
|
||||
|
||||
String configKey = buildConfigKey(label);
|
||||
|
||||
// Check if KEK already exists
|
||||
ConfigurationVO existing = configDao.findByName(configKey);
|
||||
if (existing != null) {
|
||||
throw KMSException.keyAlreadyExists("KEK with label " + label + " already exists");
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate random KEK
|
||||
byte[] kekBytes = new byte[keyBits / 8];
|
||||
secureRandom.nextBytes(kekBytes);
|
||||
|
||||
// Store in configuration table (will be encrypted automatically due to "Secure" category)
|
||||
String kekBase64 = java.util.Base64.getEncoder().encodeToString(kekBytes);
|
||||
ConfigurationVO config = new ConfigurationVO(
|
||||
"Secure", // Category - triggers encryption
|
||||
"DEFAULT",
|
||||
getConfigComponentName(),
|
||||
configKey,
|
||||
kekBase64,
|
||||
"KMS KEK for " + purpose.getName() + " (label: " + label + ")"
|
||||
);
|
||||
configDao.persist(config);
|
||||
|
||||
// Cache the KEK
|
||||
if (CacheEnabled.value()) {
|
||||
kekCache.put(label, kekBytes);
|
||||
}
|
||||
|
||||
logger.info("Created KEK with label {} for purpose {}", label, purpose);
|
||||
return label;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw KMSException.kekOperationFailed("Failed to create KEK: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigComponentName() {
|
||||
return DatabaseKMSProvider.class.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigKey<?>[] getConfigKeys() {
|
||||
return new ConfigKey<?>[]{
|
||||
CacheEnabled
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteKek(String kekId) throws KMSException {
|
||||
String configKey = buildConfigKey(kekId);
|
||||
|
||||
ConfigurationVO config = configDao.findByName(configKey);
|
||||
if (config == null) {
|
||||
throw KMSException.kekNotFound("KEK with label " + kekId + " not found");
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove from configuration (name is the primary key)
|
||||
configDao.remove(config.getName());
|
||||
|
||||
// Remove from cache
|
||||
byte[] cachedKek = kekCache.remove(kekId);
|
||||
if (cachedKek != null) {
|
||||
Arrays.fill(cachedKek, (byte) 0); // Zeroize
|
||||
}
|
||||
|
||||
logger.warn("Deleted KEK with label {}. All DEKs wrapped with this KEK are now unrecoverable!", kekId);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw KMSException.kekOperationFailed("Failed to delete KEK: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listKeks(KeyPurpose purpose) throws KMSException {
|
||||
try {
|
||||
List<String> keks = new ArrayList<>();
|
||||
|
||||
// We can't efficiently list all KEKs without a custom query
|
||||
// For now, return cached keys only - KEKs will be tracked via cache
|
||||
// TODO: Add custom DAO method or maintain KEK registry
|
||||
logger.debug("listKeks called for purpose: {}. Returning cached keys only.", purpose);
|
||||
|
||||
// Return keys from cache
|
||||
for (String label : kekCache.keySet()) {
|
||||
if (purpose == null || label.startsWith(purpose.getName())) {
|
||||
keks.add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return keks;
|
||||
} catch (Exception e) {
|
||||
throw KMSException.kekOperationFailed("Failed to list KEKs: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKekAvailable(String kekId) throws KMSException {
|
||||
try {
|
||||
String configKey = buildConfigKey(kekId);
|
||||
ConfigurationVO config = configDao.findByName(configKey);
|
||||
return config != null && config.getValue() != null;
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error checking KEK availability: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WrappedKey wrapKey(byte[] plainKey, KeyPurpose purpose, String kekLabel) throws KMSException {
|
||||
if (plainKey == null || plainKey.length == 0) {
|
||||
throw KMSException.invalidParameter("Plain key cannot be null or empty");
|
||||
}
|
||||
|
||||
byte[] kekBytes = loadKek(kekLabel);
|
||||
|
||||
try {
|
||||
// Create AES-GCM cipher with the KEK
|
||||
// Tink's AesGcmJce automatically generates a random IV and prepends it to the ciphertext
|
||||
AesGcmJce aesgcm = new AesGcmJce(kekBytes);
|
||||
|
||||
// Encrypt the DEK (Tink's encrypt returns [IV][ciphertext+tag] format)
|
||||
byte[] wrappedBlob = aesgcm.encrypt(plainKey, new byte[0]); // Empty associated data
|
||||
|
||||
WrappedKey wrapped = new WrappedKey(
|
||||
kekLabel,
|
||||
purpose,
|
||||
ALGORITHM,
|
||||
wrappedBlob,
|
||||
PROVIDER_NAME,
|
||||
new Date(),
|
||||
null // zoneId set by caller
|
||||
);
|
||||
|
||||
logger.debug("Wrapped {} key with KEK {}", purpose, kekLabel);
|
||||
return wrapped;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw KMSException.wrapUnwrapFailed("Failed to wrap key: " + e.getMessage(), e);
|
||||
} finally {
|
||||
// Zeroize KEK
|
||||
Arrays.fill(kekBytes, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] unwrapKey(WrappedKey wrappedKey) throws KMSException {
|
||||
if (wrappedKey == null) {
|
||||
throw KMSException.invalidParameter("Wrapped key cannot be null");
|
||||
}
|
||||
|
||||
byte[] kekBytes = loadKek(wrappedKey.getKekId());
|
||||
|
||||
try {
|
||||
// Create AES-GCM cipher with the KEK
|
||||
AesGcmJce aesgcm = new AesGcmJce(kekBytes);
|
||||
|
||||
// Tink's decrypt expects [IV][ciphertext+tag] format (same as encrypt returns)
|
||||
byte[] blob = wrappedKey.getWrappedKeyMaterial();
|
||||
if (blob.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
|
||||
throw new KMSException(KMSException.ErrorType.WRAP_UNWRAP_FAILED,
|
||||
"Invalid wrapped key format: too short");
|
||||
}
|
||||
|
||||
// Decrypt the DEK (Tink extracts IV from the blob automatically)
|
||||
byte[] plainKey = aesgcm.decrypt(blob, new byte[0]); // Empty associated data
|
||||
|
||||
logger.debug("Unwrapped {} key with KEK {}", wrappedKey.getPurpose(), wrappedKey.getKekId());
|
||||
return plainKey;
|
||||
|
||||
} catch (KMSException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw KMSException.wrapUnwrapFailed("Failed to unwrap key: " + e.getMessage(), e);
|
||||
} finally {
|
||||
// Zeroize KEK
|
||||
Arrays.fill(kekBytes, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits) throws KMSException {
|
||||
if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
|
||||
throw KMSException.invalidParameter("DEK size must be 128, 192, or 256 bits");
|
||||
}
|
||||
|
||||
// Generate random DEK
|
||||
byte[] dekBytes = new byte[keyBits / 8];
|
||||
secureRandom.nextBytes(dekBytes);
|
||||
|
||||
try {
|
||||
return wrapKey(dekBytes, purpose, kekLabel);
|
||||
} finally {
|
||||
// Zeroize DEK (wrapped version is in WrappedKey)
|
||||
Arrays.fill(dekBytes, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException {
|
||||
// Unwrap with old KEK
|
||||
byte[] plainKey = unwrapKey(oldWrappedKey);
|
||||
|
||||
try {
|
||||
// Wrap with new KEK
|
||||
return wrapKey(plainKey, oldWrappedKey.getPurpose(), newKekLabel);
|
||||
} finally {
|
||||
// Zeroize plaintext DEK
|
||||
Arrays.fill(plainKey, (byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean healthCheck() throws KMSException {
|
||||
try {
|
||||
// Verify we can access configuration
|
||||
if (configDao == null) {
|
||||
logger.error("Configuration DAO is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to list KEKs (lightweight operation)
|
||||
List<String> keks = listKeks(null);
|
||||
logger.debug("Health check passed. Found {} KEKs", keks.size());
|
||||
|
||||
// Optionally verify we can perform wrap/unwrap
|
||||
byte[] testKey = new byte[32];
|
||||
secureRandom.nextBytes(testKey);
|
||||
|
||||
// If we have any KEK, test it
|
||||
if (!keks.isEmpty()) {
|
||||
String testKek = keks.get(0);
|
||||
WrappedKey wrapped = wrapKey(testKey, KeyPurpose.VOLUME_ENCRYPTION, testKek);
|
||||
byte[] unwrapped = unwrapKey(wrapped);
|
||||
|
||||
boolean matches = Arrays.equals(testKey, unwrapped);
|
||||
Arrays.fill(unwrapped, (byte) 0);
|
||||
|
||||
if (!matches) {
|
||||
logger.error("Health check failed: wrap/unwrap test failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Arrays.fill(testKey, (byte) 0);
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw KMSException.healthCheckFailed("Health check failed: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Private Helper Methods ====================
|
||||
|
||||
private byte[] loadKek(String kekLabel) throws KMSException {
|
||||
// Check cache first
|
||||
if (CacheEnabled.value()) {
|
||||
byte[] cached = kekCache.get(kekLabel);
|
||||
if (cached != null) {
|
||||
return Arrays.copyOf(cached, cached.length); // Return copy
|
||||
}
|
||||
}
|
||||
|
||||
// Load from database
|
||||
String configKey = buildConfigKey(kekLabel);
|
||||
ConfigurationVO config = configDao.findByName(configKey);
|
||||
|
||||
if (config == null) {
|
||||
throw KMSException.kekNotFound("KEK with label " + kekLabel + " not found");
|
||||
}
|
||||
|
||||
try {
|
||||
// getValue() automatically decrypts
|
||||
String kekBase64 = config.getValue();
|
||||
if (StringUtils.isEmpty(kekBase64)) {
|
||||
throw KMSException.kekNotFound("KEK value is empty for label " + kekLabel);
|
||||
}
|
||||
|
||||
byte[] kekBytes = java.util.Base64.getDecoder().decode(kekBase64);
|
||||
|
||||
// Cache for future use
|
||||
if (CacheEnabled.value()) {
|
||||
kekCache.put(kekLabel, Arrays.copyOf(kekBytes, kekBytes.length));
|
||||
}
|
||||
|
||||
return kekBytes;
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw KMSException.kekOperationFailed("Invalid KEK encoding for label " + kekLabel, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String buildConfigKey(String label) {
|
||||
return KEK_CONFIG_PREFIX + label;
|
||||
}
|
||||
|
||||
private String generateKekLabel(KeyPurpose purpose) {
|
||||
return purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# 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.
|
||||
|
||||
name=database-kms
|
||||
parent=kmsProvidersRegistry
|
||||
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:context="http://www.springframework.org/schema/context"
|
||||
xmlns:aop="http://www.springframework.org/schema/aop"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/aop
|
||||
http://www.springframework.org/schema/aop/spring-aop.xsd
|
||||
http://www.springframework.org/schema/context
|
||||
http://www.springframework.org/schema/context/spring-context.xsd"
|
||||
>
|
||||
|
||||
<!-- Database KMS Provider (default provider) -->
|
||||
<bean id="databaseKMSProvider" class="org.apache.cloudstack.kms.provider.DatabaseKMSProvider">
|
||||
<property name="name" value="DatabaseKMSProvider" />
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>cloudstack-kms-plugins</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Apache CloudStack Plugin - KMS</name>
|
||||
<description>Key Management Service providers</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloudstack-plugins</artifactId>
|
||||
<version>4.23.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
<module>database</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
@ -97,6 +97,8 @@
|
|||
<module>integrations/prometheus</module>
|
||||
<module>integrations/kubernetes-service</module>
|
||||
|
||||
<module>kms</module>
|
||||
|
||||
<module>metrics</module>
|
||||
|
||||
<module>network-elements/bigswitch</module>
|
||||
|
|
|
|||
|
|
@ -69,6 +69,11 @@
|
|||
<artifactId>cloud-framework-ca</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-kms</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-framework-jobs</artifactId>
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ import org.apache.cloudstack.api.response.HypervisorGuestOsResponse;
|
|||
import org.apache.cloudstack.api.response.IPAddressResponse;
|
||||
import org.apache.cloudstack.api.response.ImageStoreResponse;
|
||||
import org.apache.cloudstack.api.response.InstanceGroupResponse;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse;
|
||||
import org.apache.cloudstack.api.response.IpForwardingRuleResponse;
|
||||
import org.apache.cloudstack.api.response.IpQuarantineResponse;
|
||||
|
|
@ -424,6 +425,7 @@ import com.cloud.user.AccountManager;
|
|||
import com.cloud.user.SSHKeyPair;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import org.apache.cloudstack.kms.KMSKey;
|
||||
import com.cloud.user.UserData;
|
||||
import com.cloud.user.UserStatisticsVO;
|
||||
import com.cloud.user.dao.UserDataDao;
|
||||
|
|
@ -5802,4 +5804,48 @@ protected Map<String, ResourceIcon> getResourceIconsUsingOsCategory(List<Templat
|
|||
response.setResponses(permissionResponses);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKeyResponse createKMSKeyResponse(KMSKey kmsKey) {
|
||||
KMSKeyResponse response = new KMSKeyResponse();
|
||||
response.setId(kmsKey.getUuid());
|
||||
response.setName(kmsKey.getName());
|
||||
response.setDescription(kmsKey.getDescription());
|
||||
response.setPurpose(kmsKey.getPurpose().getName());
|
||||
response.setAccountId(String.valueOf(kmsKey.getAccountId()));
|
||||
response.setDomainId(String.valueOf(kmsKey.getDomainId()));
|
||||
response.setZoneId(String.valueOf(kmsKey.getZoneId()));
|
||||
response.setProvider(kmsKey.getProviderName());
|
||||
response.setAlgorithm(kmsKey.getAlgorithm());
|
||||
response.setKeyBits(kmsKey.getKeyBits());
|
||||
response.setState(kmsKey.getState().toString());
|
||||
response.setCreated(kmsKey.getCreated());
|
||||
|
||||
// Set account name
|
||||
Account account = ApiDBUtils.findAccountById(kmsKey.getAccountId());
|
||||
if (account != null) {
|
||||
response.setAccountName(account.getAccountName());
|
||||
}
|
||||
|
||||
// Set domain name
|
||||
Domain domain = ApiDBUtils.findDomainById(kmsKey.getDomainId());
|
||||
if (domain != null) {
|
||||
response.setDomainName(domain.getName());
|
||||
}
|
||||
|
||||
// Set zone name
|
||||
DataCenter zone = ApiDBUtils.findZoneById(kmsKey.getZoneId());
|
||||
if (zone != null) {
|
||||
response.setZoneName(zone.getName());
|
||||
}
|
||||
|
||||
// Set KEK label (admin only)
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
if (caller != null && (caller.getType() == Account.Type.ADMIN || caller.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN)) {
|
||||
response.setKekLabel(kmsKey.getKekLabel());
|
||||
}
|
||||
|
||||
response.setObjectName("kmskey");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,910 @@
|
|||
// 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 com.cloud.event.ActionEvent;
|
||||
import com.cloud.event.EventTypes;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
import com.cloud.utils.component.PluggableService;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ResponseGenerator;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.command.user.kms.CreateKMSKeyCmd;
|
||||
import org.apache.cloudstack.api.command.user.kms.DeleteKMSKeyCmd;
|
||||
import org.apache.cloudstack.api.command.user.kms.ListKMSKeysCmd;
|
||||
import org.apache.cloudstack.api.command.user.kms.UpdateKMSKeyCmd;
|
||||
import org.apache.cloudstack.api.response.KMSKeyResponse;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.apache.cloudstack.api.response.SuccessResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
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.KMSKekVersionDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSKeyDao;
|
||||
import org.apache.cloudstack.kms.dao.KMSWrappedKeyDao;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Implementation of KMS Manager.
|
||||
* Provides high-level KMS operations with provider abstraction, zone-scoping,
|
||||
* retry logic, and audit logging.
|
||||
*/
|
||||
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
|
||||
private KMSKeyDao kmsKeyDao;
|
||||
@Inject
|
||||
private KMSKekVersionDao kmsKekVersionDao;
|
||||
@Inject
|
||||
private AccountManager accountManager;
|
||||
@Inject
|
||||
private ResponseGenerator responseGenerator;
|
||||
private List<KMSProvider> kmsProviders;
|
||||
|
||||
// ==================== Provider Management ====================
|
||||
|
||||
@Override
|
||||
public List<? extends KMSProvider> listKMSProviders() {
|
||||
return kmsProviders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSProvider getKMSProvider(String name) {
|
||||
if (StringUtils.isEmpty(name)) {
|
||||
return getConfiguredKmsProvider();
|
||||
}
|
||||
|
||||
String providerName = name.toLowerCase();
|
||||
if (!kmsProviderMap.containsKey(providerName)) {
|
||||
throw new CloudRuntimeException(String.format("KMS provider '%s' not found", providerName));
|
||||
}
|
||||
|
||||
KMSProvider provider = kmsProviderMap.get(providerName);
|
||||
if (provider == null) {
|
||||
throw new CloudRuntimeException(String.format("KMS provider '%s' returned is null", providerName));
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSProvider getKMSProviderForZone(Long zoneId) throws KMSException {
|
||||
// For now, use global provider
|
||||
// In future, could support zone-specific providers via zone-scoped config
|
||||
return getConfiguredKmsProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKmsEnabled(Long zoneId) {
|
||||
if (zoneId == null) {
|
||||
return false;
|
||||
}
|
||||
return KMSEnabled.valueIn(zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "creating KEK", async = false)
|
||||
public String createKek(Long zoneId, KeyPurpose purpose, String label, int keyBits) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
||||
KMSProvider provider = getKMSProviderForZone(zoneId);
|
||||
|
||||
try {
|
||||
logger.info("Creating KEK for zone {} with purpose {} and {} bits", zoneId, purpose, keyBits);
|
||||
return retryOperation(() -> provider.createKek(purpose, label, keyBits));
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create KEK for zone {}: {}", zoneId, e.getMessage());
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== KEK Management ====================
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_DELETE, eventDescription = "deleting KEK", async = false)
|
||||
public void deleteKek(Long zoneId, String kekId) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
||||
// TODO: Check if any wrapped keys use this KEK
|
||||
// This requires finding KMSKeyVO by kekLabel first, then checking wrapped keys
|
||||
// For now, allow deletion (will be fixed in Phase 5)
|
||||
|
||||
KMSProvider provider = getKMSProviderForZone(zoneId);
|
||||
|
||||
try {
|
||||
logger.warn("Deleting KEK {} for zone {}", kekId, zoneId);
|
||||
retryOperation(() -> {
|
||||
provider.deleteKek(kekId);
|
||||
return null;
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to delete KEK {} for zone {}: {}", kekId, zoneId, e.getMessage());
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> listKeks(Long zoneId, KeyPurpose purpose) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
||||
KMSProvider provider = getKMSProviderForZone(zoneId);
|
||||
|
||||
try {
|
||||
return retryOperation(() -> provider.listKeks(purpose));
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to list KEKs for zone {}: {}", zoneId, e.getMessage());
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKekAvailable(Long zoneId, String kekId) throws KMSException {
|
||||
if (!isKmsEnabled(zoneId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
KMSProvider provider = getKMSProviderForZone(zoneId);
|
||||
return provider.isKekAvailable(kekId);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error checking KEK availability: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_ROTATE, eventDescription = "rotating KEK", async = true)
|
||||
public String rotateKek(Long zoneId, KeyPurpose purpose, String oldKekLabel,
|
||||
String newKekLabel, int keyBits) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
||||
if (StringUtils.isEmpty(oldKekLabel)) {
|
||||
throw KMSException.invalidParameter("oldKekLabel must be specified");
|
||||
}
|
||||
|
||||
KMSProvider provider = getKMSProviderForZone(zoneId);
|
||||
|
||||
try {
|
||||
logger.info("Starting KEK rotation from {} to {} for zone {} and purpose {}",
|
||||
oldKekLabel, newKekLabel, zoneId, purpose);
|
||||
|
||||
// Find KMS key by old KEK label
|
||||
KMSKeyVO kmsKey = kmsKeyDao.findByKekLabel(oldKekLabel, provider.getProviderName());
|
||||
if (kmsKey == null) {
|
||||
throw KMSException.kekNotFound("KMS key not found for KEK label: " + oldKekLabel);
|
||||
}
|
||||
|
||||
// Generate new KEK label if not provided
|
||||
if (StringUtils.isEmpty(newKekLabel)) {
|
||||
newKekLabel = purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
|
||||
// Create new KEK in provider
|
||||
String newKekId = provider.createKek(purpose, newKekLabel, keyBits);
|
||||
|
||||
// Create new KEK version (marks old as Previous, new as Active)
|
||||
KMSKekVersionVO newVersion = createKekVersion(kmsKey.getId(), newKekId, keyBits);
|
||||
|
||||
logger.info("KEK rotation: KMS key {} now has {} versions (active: v{}, previous: v{})",
|
||||
kmsKey.getUuid(), newVersion.getVersionNumber(), newVersion.getVersionNumber(),
|
||||
newVersion.getVersionNumber() - 1);
|
||||
|
||||
// TODO: Schedule background job to rewrap all DEKs (Phase 5)
|
||||
// This will gradually rewrap wrapped keys to use the new KEK version
|
||||
|
||||
return newKekId;
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("KEK rotation failed for zone {}: {}", zoneId, e.getMessage());
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DEK Operations ====================
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_UNWRAP, eventDescription = "unwrapping volume key", async = false)
|
||||
public byte[] unwrapVolumeKey(WrappedKey wrappedKey, Long zoneId) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
||||
return unwrapDek(wrappedKey);
|
||||
}
|
||||
|
||||
private byte[] unwrapDek(WrappedKey wrappedKey) throws KMSException {
|
||||
// Determine provider from wrapped key
|
||||
String providerName = wrappedKey.getProviderName();
|
||||
KMSProvider provider = getKMSProvider(providerName);
|
||||
|
||||
try {
|
||||
logger.debug("Unwrapping {} key", wrappedKey.getPurpose());
|
||||
return retryOperation(() -> provider.unwrapKey(wrappedKey));
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to unwrap key: {}", e.getMessage());
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_HEALTH_CHECK, eventDescription = "KMS health check", async = false)
|
||||
public boolean healthCheck(Long zoneId) throws KMSException {
|
||||
if (!isKmsEnabled(zoneId)) {
|
||||
logger.debug("KMS is not enabled for zone {}", zoneId);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
KMSProvider provider = getKMSProviderForZone(zoneId);
|
||||
return provider.healthCheck();
|
||||
} catch (Exception e) {
|
||||
logger.error("Health check failed for zone {}: {}", zoneId, e.getMessage());
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "creating user KMS key", async = false)
|
||||
public KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
|
||||
String name, String description, KeyPurpose purpose,
|
||||
Integer keyBits) throws KMSException {
|
||||
validateKmsEnabled(zoneId);
|
||||
|
||||
KMSProvider provider = getKMSProviderForZone(zoneId);
|
||||
|
||||
// Generate unique KEK label
|
||||
String kekLabel = purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
// Create KEK in provider
|
||||
String providerKekLabel;
|
||||
try {
|
||||
providerKekLabel = retryOperation(() -> provider.createKek(purpose, kekLabel, keyBits));
|
||||
} catch (Exception e) {
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
|
||||
// Create metadata entry
|
||||
KMSKeyVO kmsKey = new KMSKeyVO(name, description, providerKekLabel, purpose,
|
||||
accountId, domainId, zoneId, provider.getProviderName(),
|
||||
"AES/GCM/NoPadding", keyBits);
|
||||
kmsKey = kmsKeyDao.persist(kmsKey);
|
||||
|
||||
// Create initial KEK version (version 1, status=Active)
|
||||
KMSKekVersionVO initialVersion = new KMSKekVersionVO(kmsKey.getId(), 1, providerKekLabel,
|
||||
KMSKekVersionVO.Status.Active);
|
||||
initialVersion = kmsKekVersionDao.persist(initialVersion);
|
||||
|
||||
logger.info("Created KMS key '{}' (UUID: {}) with initial KEK version {} for account {} in zone {}",
|
||||
name, kmsKey.getUuid(), initialVersion.getVersionNumber(), accountId, zoneId);
|
||||
return kmsKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends KMSKey> listUserKMSKeys(Long accountId, Long domainId, Long zoneId,
|
||||
KeyPurpose purpose, KMSKey.State state) {
|
||||
// List keys accessible to the account (owned by account or in domain)
|
||||
return kmsKeyDao.listAccessibleKeys(accountId, domainId, zoneId, purpose, state);
|
||||
}
|
||||
|
||||
// ==================== Health Check ====================
|
||||
|
||||
@Override
|
||||
public KMSKey getUserKMSKey(String uuid, Long callerAccountId) {
|
||||
KMSKeyVO key = kmsKeyDao.findByUuid(uuid);
|
||||
if (key == null || key.getState() == KMSKey.State.Deleted) {
|
||||
return null;
|
||||
}
|
||||
// Check permission
|
||||
if (!hasPermission(callerAccountId, uuid)) {
|
||||
return null;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
// ==================== Helper Methods ====================
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Long callerAccountId, String keyUuid) {
|
||||
KMSKeyVO key = kmsKeyDao.findByUuid(keyUuid);
|
||||
if (key == null || key.getState() == KMSKey.State.Deleted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Owner always has permission
|
||||
if (key.getAccountId() == callerAccountId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Domain admin can access keys in their domain/subdomains
|
||||
// For now, only owner has permission
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_DELETE, eventDescription = "deleting user KMS key", async = false)
|
||||
public void deleteUserKMSKey(String uuid, Long callerAccountId) throws KMSException {
|
||||
KMSKeyVO key = kmsKeyDao.findByUuid(uuid);
|
||||
if (key == null) {
|
||||
throw KMSException.kekNotFound("KMS key not found: " + uuid);
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if (!hasPermission(callerAccountId, uuid)) {
|
||||
throw KMSException.invalidParameter("No permission to delete KMS key: " + uuid);
|
||||
}
|
||||
|
||||
// Check if key is in use
|
||||
long wrappedKeyCount = kmsKeyDao.countWrappedKeysByKmsKey(key.getId());
|
||||
if (wrappedKeyCount > 0) {
|
||||
throw KMSException.invalidParameter("Cannot delete KMS key: " + wrappedKeyCount +
|
||||
" wrapped key(s) still reference this key");
|
||||
}
|
||||
|
||||
// Soft delete
|
||||
key.setState(KMSKey.State.Deleted);
|
||||
key.setRemoved(new java.util.Date());
|
||||
kmsKeyDao.update(key.getId(), key);
|
||||
|
||||
// Optionally delete KEK from provider (but keep metadata for audit)
|
||||
// provider.deleteKek(key.getKekLabel());
|
||||
|
||||
logger.info("Deleted KMS key '{}' (UUID: {})", key.getName(), uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "updating user KMS key", async = false)
|
||||
public KMSKey updateUserKMSKey(String uuid, Long callerAccountId,
|
||||
String name, String description, KMSKey.State state) throws KMSException {
|
||||
KMSKeyVO key = kmsKeyDao.findByUuid(uuid);
|
||||
if (key == null) {
|
||||
throw KMSException.kekNotFound("KMS key not found: " + uuid);
|
||||
}
|
||||
|
||||
// Check permission
|
||||
if (!hasPermission(callerAccountId, uuid)) {
|
||||
throw KMSException.invalidParameter("No permission to update KMS key: " + uuid);
|
||||
}
|
||||
|
||||
boolean updated = false;
|
||||
if (name != null && !name.equals(key.getName())) {
|
||||
key.setName(name);
|
||||
updated = true;
|
||||
}
|
||||
if (description != null && !description.equals(key.getDescription())) {
|
||||
key.setDescription(description);
|
||||
updated = true;
|
||||
}
|
||||
if (state != null && state != key.getState()) {
|
||||
if (state == KMSKey.State.Deleted) {
|
||||
throw KMSException.invalidParameter("Cannot set state to Deleted. Use deleteKMSKey instead.");
|
||||
}
|
||||
key.setState(state);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
kmsKeyDao.update(key.getId(), key);
|
||||
logger.info("Updated KMS key '{}' (UUID: {})", key.getName(), uuid);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap a DEK by wrapped key ID, trying multiple KEK versions if needed
|
||||
*/
|
||||
@Override
|
||||
public byte[] unwrapKey(Long wrappedKeyId) throws KMSException {
|
||||
KMSWrappedKeyVO wrappedVO = kmsWrappedKeyDao.findById(wrappedKeyId);
|
||||
if (wrappedVO == null) {
|
||||
throw KMSException.kekNotFound("Wrapped key not found: " + wrappedKeyId);
|
||||
}
|
||||
|
||||
KMSKeyVO kmsKey = kmsKeyDao.findById(wrappedVO.getKmsKeyId());
|
||||
if (kmsKey == null) {
|
||||
throw KMSException.kekNotFound("KMS key not found for wrapped key: " + wrappedKeyId);
|
||||
}
|
||||
|
||||
KMSProvider provider = getKMSProvider(kmsKey.getProviderName());
|
||||
|
||||
// Try the specific version first if available
|
||||
if (wrappedVO.getKekVersionId() != null) {
|
||||
KMSKekVersionVO version = kmsKekVersionDao.findById(wrappedVO.getKekVersionId());
|
||||
if (version != null && version.getStatus() != KMSKekVersionVO.Status.Archived) {
|
||||
try {
|
||||
WrappedKey wrapped = new WrappedKey(version.getKekLabel(), kmsKey.getPurpose(),
|
||||
kmsKey.getAlgorithm(), wrappedVO.getWrappedBlob(),
|
||||
kmsKey.getProviderName(), wrappedVO.getCreated(), kmsKey.getZoneId());
|
||||
byte[] dek = retryOperation(() -> provider.unwrapKey(wrapped));
|
||||
logger.debug("Successfully unwrapped key {} with KEK version {}", wrappedKeyId,
|
||||
version.getVersionNumber());
|
||||
return dek;
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to unwrap with version {}: {}", version.getVersionNumber(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try all available versions for decryption
|
||||
List<KMSKekVersionVO> versions = getKekVersionsForDecryption(kmsKey.getId());
|
||||
for (KMSKekVersionVO version : versions) {
|
||||
try {
|
||||
WrappedKey wrapped = new WrappedKey(version.getKekLabel(), kmsKey.getPurpose(),
|
||||
kmsKey.getAlgorithm(), wrappedVO.getWrappedBlob(),
|
||||
kmsKey.getProviderName(), wrappedVO.getCreated(), kmsKey.getZoneId());
|
||||
byte[] dek = retryOperation(() -> provider.unwrapKey(wrapped));
|
||||
logger.info("Successfully unwrapped key {} with KEK version {} (fallback)", wrappedKeyId,
|
||||
version.getVersionNumber());
|
||||
return dek;
|
||||
} catch (Exception e) {
|
||||
logger.debug("Failed to unwrap with version {}: {}", version.getVersionNumber(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
throw KMSException.wrapUnwrapFailed("Failed to unwrap key with any available KEK version");
|
||||
}
|
||||
|
||||
// ==================== Lifecycle Methods ====================
|
||||
|
||||
/**
|
||||
* Get all KEK versions that can be used for decryption (Active and Previous)
|
||||
*/
|
||||
private List<KMSKekVersionVO> getKekVersionsForDecryption(Long kmsKeyId) {
|
||||
return kmsKekVersionDao.getVersionsForDecryption(kmsKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_WRAP,
|
||||
eventDescription = "generating volume key with specified KEK", async = false)
|
||||
public WrappedKey generateVolumeKeyWithKek(String kekUuid, Long callerAccountId) throws KMSException {
|
||||
// Get and validate KMS key
|
||||
KMSKey kmsKey = getUserKMSKey(kekUuid, callerAccountId);
|
||||
if (kmsKey == null) {
|
||||
throw KMSException.kekNotFound("KMS key not found or no permission: " + kekUuid);
|
||||
}
|
||||
|
||||
if (kmsKey.getState() != KMSKey.State.Enabled) {
|
||||
throw KMSException.invalidParameter("KMS key is not enabled: " + kekUuid);
|
||||
}
|
||||
|
||||
if (kmsKey.getPurpose() != KeyPurpose.VOLUME_ENCRYPTION) {
|
||||
throw KMSException.invalidParameter("KMS key purpose is not VOLUME_ENCRYPTION: " + kekUuid);
|
||||
}
|
||||
|
||||
// Get provider
|
||||
KMSProvider provider = getKMSProviderForZone(kmsKey.getZoneId());
|
||||
|
||||
// Get active KEK version
|
||||
KMSKekVersionVO activeVersion = getActiveKekVersion(kmsKey.getId());
|
||||
|
||||
// Generate and wrap DEK using active KEK version
|
||||
int dekSize = KMSDekSizeBits.value();
|
||||
WrappedKey wrappedKey;
|
||||
try {
|
||||
wrappedKey = retryOperation(() ->
|
||||
provider.generateAndWrapDek(KeyPurpose.VOLUME_ENCRYPTION, activeVersion.getKekLabel(), dekSize));
|
||||
// Store the wrapped key in database
|
||||
KMSWrappedKeyVO wrappedKeyVO = new KMSWrappedKeyVO(kmsKey.getId(), activeVersion.getId(),
|
||||
kmsKey.getZoneId(), wrappedKey.getWrappedKeyMaterial());
|
||||
wrappedKeyVO = kmsWrappedKeyDao.persist(wrappedKeyVO);
|
||||
|
||||
// Return WrappedKey with database UUID so it can be looked up later
|
||||
// Note: Volume creation code should look up by UUID and set volume.kmsWrappedKeyId
|
||||
WrappedKey persistedWrappedKey = new WrappedKey(
|
||||
wrappedKeyVO.getUuid(),
|
||||
wrappedKey.getKekId(),
|
||||
wrappedKey.getPurpose(),
|
||||
wrappedKey.getAlgorithm(),
|
||||
wrappedKey.getWrappedKeyMaterial(),
|
||||
wrappedKey.getProviderName(),
|
||||
wrappedKey.getCreated(),
|
||||
wrappedKey.getZoneId()
|
||||
);
|
||||
wrappedKey = persistedWrappedKey;
|
||||
} catch (Exception e) {
|
||||
throw handleKmsException(e);
|
||||
}
|
||||
|
||||
logger.debug("Generated volume key using KMS key '{}' (UUID: {}) with KEK version {}, wrapped key UUID: {}",
|
||||
kmsKey.getName(), kekUuid, activeVersion.getVersionNumber(), wrappedKey.getId());
|
||||
return wrappedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active KEK version for a KMS key
|
||||
*/
|
||||
private KMSKekVersionVO getActiveKekVersion(Long kmsKeyId) throws KMSException {
|
||||
KMSKekVersionVO activeVersion = kmsKekVersionDao.getActiveVersion(kmsKeyId);
|
||||
if (activeVersion == null) {
|
||||
throw KMSException.kekNotFound("No active KEK version found for KMS key ID: " + kmsKeyId);
|
||||
}
|
||||
return activeVersion;
|
||||
}
|
||||
|
||||
// ==================== Configurable Implementation ====================
|
||||
|
||||
@Override
|
||||
public KMSKeyResponse createKMSKey(CreateKMSKeyCmd cmd) throws KMSException {
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
Account targetAccount = caller;
|
||||
|
||||
// If account/domain specified, validate permissions and resolve account
|
||||
if (cmd.getAccountName() != null || cmd.getDomainId() != null) {
|
||||
// Only admins and domain admins can create keys for other accounts
|
||||
if (!accountManager.isAdmin(caller.getId()) &&
|
||||
!accountManager.isDomainAdmin(caller.getId())) {
|
||||
throw new ServerApiException(ApiErrorCode.UNAUTHORIZED,
|
||||
"Only admins and domain admins can create keys for other accounts");
|
||||
}
|
||||
|
||||
if (cmd.getAccountName() != null && cmd.getDomainId() != null) {
|
||||
targetAccount = accountManager.getActiveAccountByName(cmd.getAccountName(), cmd.getDomainId());
|
||||
if (targetAccount == null) {
|
||||
throw KMSException.invalidParameter(
|
||||
"Unable to find account " + cmd.getAccountName() + " in domain " + cmd.getDomainId());
|
||||
}
|
||||
// Check access
|
||||
accountManager.checkAccess(caller, null, true, targetAccount);
|
||||
} else {
|
||||
throw KMSException.invalidParameter("Both accountName and domainId must be specified together");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate purpose
|
||||
KeyPurpose keyPurpose;
|
||||
try {
|
||||
keyPurpose = KeyPurpose.fromString(cmd.getPurpose());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw KMSException.invalidParameter("Invalid purpose: " + cmd.getPurpose() +
|
||||
". Valid values: VOLUME_ENCRYPTION, TLS_CERT, CONFIG_SECRET");
|
||||
}
|
||||
|
||||
// Validate key bits
|
||||
int bits = cmd.getKeyBits();
|
||||
if (bits != 128 && bits != 192 && bits != 256) {
|
||||
throw KMSException.invalidParameter("Key bits must be 128, 192, or 256");
|
||||
}
|
||||
|
||||
// Create the KMS key
|
||||
KMSKey kmsKey = createUserKMSKey(
|
||||
targetAccount.getId(),
|
||||
targetAccount.getDomainId(),
|
||||
cmd.getZoneId(),
|
||||
cmd.getName(),
|
||||
cmd.getDescription(),
|
||||
keyPurpose,
|
||||
bits
|
||||
);
|
||||
|
||||
return responseGenerator.createKMSKeyResponse(kmsKey);
|
||||
}
|
||||
|
||||
// ==================== KEK Version Management ====================
|
||||
|
||||
@Override
|
||||
public ListResponse<KMSKeyResponse> listKMSKeys(ListKMSKeysCmd cmd) {
|
||||
Account caller = CallContext.current().getCallingAccount();
|
||||
if (caller == null) {
|
||||
ListResponse<KMSKeyResponse> response = new ListResponse<>();
|
||||
response.setResponses(new java.util.ArrayList<>(), 0);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Parse purpose if provided
|
||||
KeyPurpose keyPurpose = null;
|
||||
if (cmd.getPurpose() != null) {
|
||||
try {
|
||||
keyPurpose = KeyPurpose.fromString(cmd.getPurpose());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid purpose - will be ignored
|
||||
}
|
||||
}
|
||||
|
||||
// Parse state if provided
|
||||
KMSKey.State keyState = null;
|
||||
if (cmd.getState() != null) {
|
||||
try {
|
||||
keyState = KMSKey.State.valueOf(cmd.getState());
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid state - will be ignored
|
||||
}
|
||||
}
|
||||
|
||||
// If specific ID requested
|
||||
if (cmd.getId() != null) {
|
||||
// Look up key by ID to get UUID
|
||||
KMSKeyVO key = kmsKeyDao.findById(cmd.getId());
|
||||
if (key == null) {
|
||||
// Key not found - return empty list
|
||||
ListResponse<KMSKeyResponse> listResponse = new ListResponse<>();
|
||||
listResponse.setResponses(new java.util.ArrayList<>(), 0);
|
||||
return listResponse;
|
||||
}
|
||||
KMSKey kmsKey = getUserKMSKey(key.getUuid(), caller.getId());
|
||||
List<KMSKeyResponse> responses = new java.util.ArrayList<>();
|
||||
if (kmsKey != null && hasPermission(caller.getId(), kmsKey.getUuid())) {
|
||||
responses.add(responseGenerator.createKMSKeyResponse(kmsKey));
|
||||
}
|
||||
ListResponse<KMSKeyResponse> listResponse = new ListResponse<>();
|
||||
listResponse.setResponses(responses, responses.size());
|
||||
return listResponse;
|
||||
}
|
||||
|
||||
// List accessible keys
|
||||
List<? extends KMSKey> keys = listUserKMSKeys(
|
||||
caller.getId(),
|
||||
caller.getDomainId(),
|
||||
cmd.getZoneId(),
|
||||
keyPurpose,
|
||||
keyState
|
||||
);
|
||||
|
||||
List<KMSKeyResponse> responses = new java.util.ArrayList<>();
|
||||
for (KMSKey key : keys) {
|
||||
responses.add(responseGenerator.createKMSKeyResponse(key));
|
||||
}
|
||||
|
||||
ListResponse<KMSKeyResponse> listResponse = new ListResponse<>();
|
||||
listResponse.setResponses(responses, responses.size());
|
||||
return listResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMSKeyResponse updateKMSKey(UpdateKMSKeyCmd cmd) throws KMSException {
|
||||
Long callerAccountId = CallContext.current().getCallingAccount().getId();
|
||||
|
||||
// Parse state if provided
|
||||
KMSKey.State keyState = null;
|
||||
if (cmd.getState() != null) {
|
||||
try {
|
||||
keyState = KMSKey.State.valueOf(cmd.getState());
|
||||
if (keyState == KMSKey.State.Deleted) {
|
||||
throw KMSException.invalidParameter("Cannot set state to Deleted. Use deleteKMSKey instead.");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw KMSException.invalidParameter(
|
||||
"Invalid state: " + cmd.getState() + ". Valid values: Enabled, Disabled");
|
||||
}
|
||||
}
|
||||
|
||||
// Look up key by ID to get UUID
|
||||
KMSKeyVO key = kmsKeyDao.findById(cmd.getId());
|
||||
if (key == null) {
|
||||
throw KMSException.kekNotFound("KMS key not found: " + cmd.getId());
|
||||
}
|
||||
|
||||
KMSKey updatedKey = updateUserKMSKey(key.getUuid(), callerAccountId,
|
||||
cmd.getName(), cmd.getDescription(), keyState);
|
||||
return responseGenerator.createKMSKeyResponse(updatedKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuccessResponse deleteKMSKey(DeleteKMSKeyCmd cmd) throws KMSException {
|
||||
Long callerAccountId = CallContext.current().getCallingAccount().getId();
|
||||
|
||||
// Look up key by ID to get UUID
|
||||
KMSKeyVO key = kmsKeyDao.findById(cmd.getId());
|
||||
if (key == null) {
|
||||
throw KMSException.kekNotFound("KMS key not found: " + cmd.getId());
|
||||
}
|
||||
|
||||
deleteUserKMSKey(key.getUuid(), callerAccountId);
|
||||
SuccessResponse response = new SuccessResponse();
|
||||
return response;
|
||||
}
|
||||
|
||||
// ==================== User KEK Management ====================
|
||||
|
||||
/**
|
||||
* Create a new KEK version for a KMS key
|
||||
*/
|
||||
private KMSKekVersionVO createKekVersion(Long kmsKeyId, String kekLabel, int keyBits) throws KMSException {
|
||||
// Get existing versions to determine next version number
|
||||
List<KMSKekVersionVO> existingVersions = kmsKekVersionDao.listByKmsKeyId(kmsKeyId);
|
||||
int nextVersion = existingVersions.stream()
|
||||
.mapToInt(KMSKekVersionVO::getVersionNumber)
|
||||
.max()
|
||||
.orElse(0) + 1;
|
||||
|
||||
// Mark current active version as Previous
|
||||
KMSKekVersionVO currentActive = kmsKekVersionDao.getActiveVersion(kmsKeyId);
|
||||
if (currentActive != null) {
|
||||
currentActive.setStatus(KMSKekVersionVO.Status.Previous);
|
||||
kmsKekVersionDao.update(currentActive.getId(), currentActive);
|
||||
}
|
||||
|
||||
// Create new active version
|
||||
KMSKekVersionVO newVersion = new KMSKekVersionVO(kmsKeyId, nextVersion, kekLabel,
|
||||
KMSKekVersionVO.Status.Active);
|
||||
newVersion = kmsKekVersionDao.persist(newVersion);
|
||||
|
||||
logger.info("Created KEK version {} for KMS key {} (label: {})", nextVersion, kmsKeyId, kekLabel);
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
private void validateKmsEnabled(Long zoneId) throws KMSException {
|
||||
if (zoneId == null) {
|
||||
throw KMSException.invalidParameter("Zone ID cannot be null");
|
||||
}
|
||||
|
||||
if (!isKmsEnabled(zoneId)) {
|
||||
throw KMSException.providerNotInitialized(
|
||||
"KMS is not enabled for zone " + zoneId + ". Set kms.enabled=true for this zone.");
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T retryOperation(KmsOperation<T> operation) throws Exception {
|
||||
int maxRetries = KMSRetryCount.value();
|
||||
int retryDelay = KMSRetryDelayMs.value();
|
||||
|
||||
Exception lastException = null;
|
||||
|
||||
for (int attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return operation.execute();
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
|
||||
// Check if retryable
|
||||
if (e instanceof KMSException && !((KMSException) e).isRetryable()) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
logger.warn("KMS operation failed (attempt {}/{}): {}. Retrying...",
|
||||
attempt + 1, maxRetries + 1, e.getMessage());
|
||||
|
||||
try {
|
||||
Thread.sleep((long) retryDelay * (attempt + 1)); // Exponential backoff
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new CloudRuntimeException("Interrupted during retry", ie);
|
||||
}
|
||||
} else {
|
||||
logger.error("KMS operation failed after {} attempts", maxRetries + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
|
||||
throw new CloudRuntimeException("KMS operation failed with no exception details");
|
||||
}
|
||||
|
||||
private KMSException handleKmsException(Exception e) {
|
||||
if (e instanceof KMSException) {
|
||||
return (KMSException) e;
|
||||
}
|
||||
return KMSException.transientError("KMS operation failed: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
private KMSProvider getConfiguredKmsProvider() {
|
||||
if (configuredKmsProvider != null) {
|
||||
return configuredKmsProvider;
|
||||
}
|
||||
|
||||
String providerName = KMSProviderPlugin.value();
|
||||
if (kmsProviderMap.containsKey(providerName) && kmsProviderMap.get(providerName) != null) {
|
||||
configuredKmsProvider = kmsProviderMap.get(providerName);
|
||||
return configuredKmsProvider;
|
||||
}
|
||||
|
||||
throw new CloudRuntimeException("Failed to find default configured KMS provider plugin: " + providerName);
|
||||
}
|
||||
|
||||
public void setKmsProviders(List<KMSProvider> kmsProviders) {
|
||||
this.kmsProviders = kmsProviders;
|
||||
initializeKmsProviderMap();
|
||||
}
|
||||
|
||||
// ==================== API Response Methods ====================
|
||||
|
||||
private void initializeKmsProviderMap() {
|
||||
if (kmsProviderMap != null && kmsProviderMap.size() != kmsProviders.size()) {
|
||||
for (KMSProvider provider : kmsProviders) {
|
||||
kmsProviderMap.put(provider.getProviderName().toLowerCase(), provider);
|
||||
logger.info("Registered KMS provider: {}", provider.getProviderName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean start() {
|
||||
super.start();
|
||||
initializeKmsProviderMap();
|
||||
|
||||
String configuredProviderName = KMSProviderPlugin.value();
|
||||
if (kmsProviderMap.containsKey(configuredProviderName)) {
|
||||
configuredKmsProvider = kmsProviderMap.get(configuredProviderName);
|
||||
logger.info("Configured KMS provider: {}", configuredKmsProvider.getProviderName());
|
||||
}
|
||||
|
||||
if (configuredKmsProvider == null) {
|
||||
logger.warn("No valid configured KMS provider found. KMS functionality will be unavailable.");
|
||||
// Don't fail - KMS is optional
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run health check on startup
|
||||
try {
|
||||
boolean healthy = configuredKmsProvider.healthCheck();
|
||||
if (healthy) {
|
||||
logger.info("KMS provider {} health check passed", configuredKmsProvider.getProviderName());
|
||||
} else {
|
||||
logger.warn("KMS provider {} health check failed", configuredKmsProvider.getProviderName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("KMS provider health check error: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getConfigComponentName() {
|
||||
return KMSManager.class.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigKey<?>[] getConfigKeys() {
|
||||
return new ConfigKey<?>[]{
|
||||
KMSProviderPlugin,
|
||||
KMSEnabled,
|
||||
KMSDekSizeBits,
|
||||
KMSRetryCount,
|
||||
KMSRetryDelayMs,
|
||||
KMSOperationTimeoutSec
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> getCommands() {
|
||||
List<Class<?>> cmdList = new ArrayList<>();
|
||||
cmdList.add(ListKMSKeysCmd.class);
|
||||
cmdList.add(CreateKMSKeyCmd.class);
|
||||
cmdList.add(UpdateKMSKeyCmd.class);
|
||||
cmdList.add(DeleteKMSKeyCmd.class);
|
||||
|
||||
return cmdList;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface KmsOperation<T> {
|
||||
T execute() throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -330,6 +330,11 @@
|
|||
<property name="caProviders" value="#{caProvidersRegistry.registered}" />
|
||||
</bean>
|
||||
|
||||
<!-- KMS manager -->
|
||||
<bean id="kmsManager" class="org.apache.cloudstack.kms.KMSManagerImpl">
|
||||
<property name="kmsProviders" value="#{kmsProvidersRegistry.registered}" />
|
||||
</bean>
|
||||
|
||||
<bean id="annotationService" class="org.apache.cloudstack.annotation.AnnotationManagerImpl">
|
||||
<property name="kubernetesServiceHelpers" value="#{kubernetesServiceHelperRegistry.registered}" />
|
||||
</bean>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ known_categories = {
|
|||
'HypervisorGuestOsNames': 'Guest OS',
|
||||
'Domain': 'Domain',
|
||||
'Template': 'Template',
|
||||
'KMS': 'KMS',
|
||||
'Iso': 'ISO',
|
||||
'Volume': 'Volume',
|
||||
'Vlan': 'VLAN',
|
||||
|
|
|
|||
Loading…
Reference in New Issue