diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index f531006124..7a9ba1a14b 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -37,6 +37,10 @@ pkcs11 = ["cryptoki"] # that want to join a Nebula overlay network nebula-ca-plugin = [] +# Use PKI Vault plugin to provide credentials for mutual TLS communication +# between a server running in the sandbox (pod) and a client (admin or owner) +pki-vault-plugin = [] + # Use HashiCorp Vault KV v1 as KBS backend vault = ["vaultrs"] diff --git a/kbs/Makefile b/kbs/Makefile index ffea7970b7..36fd639327 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -2,6 +2,8 @@ AS_TYPE ?= coco-as ALIYUN ?= false NEBULA_CA_PLUGIN ?= false VAULT ?= false +SPLITAPI_PLUGIN ?= false +PKI_VAULT_PLUGIN ?= false BUILD_ARCH := $(shell uname -m) ARCH ?= $(shell uname -m) @@ -56,6 +58,10 @@ ifeq ($(NEBULA_CA_PLUGIN), true) FEATURES += nebula-ca-plugin endif +ifeq ($(PKI_VAULT_PLUGIN), true) + FEATURES += pki-vault-plugin +endif + ifeq ($(VAULT), true) FEATURES += vault endif diff --git a/kbs/docs/plugins/pki_vault.md b/kbs/docs/plugins/pki_vault.md new file mode 100644 index 0000000000..c41e439660 --- /dev/null +++ b/kbs/docs/plugins/pki_vault.md @@ -0,0 +1,147 @@ +# PKI Vault plugin + +This plugin currently generates credentials (keys and certificates) for the confidential VM (aka pod VM or sandbox) and for the owner of the confidential workload that runs inside the confidential VM as a confidential container. The credentials allow us to establish a secured client-server communication where the owner acts as a client for the server. Such a design is useful for the SplitAPI (kata-containers/kata-containers#9159 and +kata-containers/kata-containers#9752) and peer-pods. The current design of the plugin prioritizes the mutual authentication between the server and the client. + +This plugin also delivers the credentials to the confidential pod VM through the invocation of the `get-resource` API call by specifying the `plugin-name`. Currently, the plugin requires that the pod VM sends an identifier (required) and any additinal parameters as part of the query string passed to the `get-resource` API as the `resource-path`. The identifier is used as a key to obtain the credentials from the KBS. + +Once receiving the credential request from pod VM through `get-resource` API, the plugin creates a CA and two key pairs (one for pod VM and another for workload owner), and signs the key pairs using the self-signed CA. Currently, the generated credentials are stored in a hashmap with the identifer as the key for each pod VM. PKI Vault plugin responds to a request from the pod VM by sending the server specific credentials (key, cert) along with the CA certificate. A request from workload owner gets the client specific credentials (key, cert) and CA's certificate. + +# Setup + +1. Build the KBS with the cargo feature `pki-vault-plugin` enabled. + +```bash +make background-check-kbs POLICY_ENGINE=opa PKI_VAULT_PLUGIN=true +``` + +2. Configure the `pki-vault` plugin. Simply specifying the plugin name should be enough for the configuration, just add the lines below to the [KBS config](#kbs/config/docker-compose/kbs-config.toml). But one can specify addition details to the configuration file. + +```toml +[[plugins]] +name = "pki_vault" +``` +The following addition details can be set to the configuration file. +```toml +[[plugins]] +name = "pki_vault" +plugin_dir = "/opt/confidential-containers/kbs/plugin/splitapi" +cred_filename = "certificates.json" + +[plugins.pkivault_cert_details] +country = "AA" +state = "Default State" +locality = "Default City" +organization = "Default Organization" +org_unit = "Default Unit" + +[plugins.pkivault_cert_details.ca] +common_name = "grpc-tls CA" +validity_days = 3650 + +[plugins.pkivault_cert_details.server] +common_name = "server" +validity_days = 180 + +[plugins.pkivault_cert_details.client] +common_name = "client" +validity_days = 180 +``` + +3. Start trustee + +```bash +sudo ../target/release/kbs --config-file ./config/kbs-config.toml +``` +# Design choices +There can be three key design choices one can consider to configure PKI Vault plugin. As of now, PKI Vault only supports the separte CA per Pod approach with a non-persistent credential storage. + +1. **Separate CA per Pod VM**: +Each pod VM gets its own Certificate Authority (CA), meaning every sandbox has an independent root of trust. This design isolates security domains — if one pod’s CA is compromised, others remain safe. It also simplifies per-pod credential generation and teardown. However, it creates more CA certificates to manage and requires persistent storage so that a pod’s CA isn’t lost after a restart. + +2. **Single CA for All Pod VMs**: +Using one global CA to sign credentials for all pod VMs simplifies trust management, since all pods share the same root certificate. It reduces operational overhead and avoids losing trust links after service restarts. The downside is that it introduces a single point of failure — if the CA key is compromised, all pods are affected — and reduces isolation between sandboxes. + +3. **Persistent vs. Non-Persistent Credential Storage**: +Persistent storage keeps CA keys and certificates on disk so they survive restarts, maintaining continuity in authentication. Non-persistent storage is simpler and more secure in the sense that credentials vanish after shutdown, but it breaks mutual authentication if the service restarts. In this design, persistence is chosen to ensure pod CAs and credentials remain valid even after trustee restarts. + +# Runtime services + +All the supported runtime services for both the Pod VM and workload owner are described in the following sections. + +## Credentials service for Pod VM + +A Pod VM requests the the credentials (e.g., to initiate a server inside the pod VM) through the `GET` request. + +Only the `GET` request is supported, e.g., `GET /kbs/v0/pki_vault/credentials?token=podToken12345&name=pod51&ip=60.11.12.89`. Currently, the `GET` takes `name`, `token`, and `ip` parameters. The Pod VM can pass the `name` and `ip` parameters to the request, but owner has to set the `token` to the Pod VM through init data. The `token`, together with the `name` and `ip`, serves as a unique identifier for associating a Pod VM with its record. This identifier is used as the key in a hashmap to store the Pod VM’s credentials. + +The request takes parameters via URL query string. All parameters supported are described in the table below. + +>**Note:** Currently, all parameters are required. We have plan to support additional parameters in the query string in later version. A policy can help check the query string. + +| Property | Type | Required | Description | Example | +|---------------------|--------|----------|-------------------------|-------------------------------------------| +| `token` | String | Yes | Token assigned to the Pod by owner | `credentials?token=podToken12345` | +| `name` | String | Yes | Pod name | `credentials?name=pod51` | +| `ip` | String | Yes | IPv4 address of a pod to assign to the certificate | `credentials?ip=60.11.12.89` | + + +The request will be processed only if the node passes the attestation, otherwise an error is returned. If the credentials for the Pod VM already exists in Trustee, the plugin simply returns the existing credentials. Otherwise, the plugin generates the credentials. + +Once the request is processed, the following structure is returned in JSON format. + +```rust +pub struct CredentialsOut { + pub key: Vec, // Key created + pub cert: Vec, // Self-signed certificate created + pub ca_cert: Vec, // CA certificate +} +``` +Example credentials are below: +```rust +{ + "key":[45,45,45,45,45,66,69,71,73,78,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10,77,67,52,67,65,81,65,119,66,81,89,68,75,50,86,119,66,67,73,69,73,79,109,112,47,49,69,73,50,82,65,90,73,99,117,87,99,108,54,80,111,100,104,101,79,85,68,119,65,57,52,82,109,109,78,83,81,114,86,98,50,50,113,55,10,45,45,45,45,45,69,78,68,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10], + "cert":[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,110,68,67,67,65,107,54,103,65,119,73,66,65,103,73,66,65,106,65,70,66,103,77,114,90,88,65,119,103,89,103,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,10,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,10,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,70,68,65,83,10,66,103,78,86,66,65,77,77,67,50,100,121,99,71,77,116,100,71,120,122,73,69,78,66,77,66,52,88,68,84,73,49,77,84,69,120,77,84,69,50,77,122,81,121,78,49,111,88,68,84,73,50,77,68,85,120,77,68,69,50,77,122,81,121,10,78,49,111,119,103,89,77,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,10,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,10,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,68,122,65,78,66,103,78,86,66,65,77,77,66,110,78,108,99,110,90,108,99,106,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,80,122,80,102,119,105,57,69,51,48,47,88,114,117,99,88,66,88,119,97,120,68,118,109,108,102,47,122,77,57,88,115,51,56,53,84,72,69,104,72,110,111,76,111,52,72,102,77,73,72,99,77,65,119,71,65,49,85,100,10,69,119,69,66,47,119,81,67,77,65,65,119,67,119,89,68,86,82,48,80,66,65,81,68,65,103,87,103,77,66,48,71,65,49,85,100,68,103,81,87,66,66,84,66,69,75,99,69,89,65,122,67,57,88,85,72,51,105,43,98,71,113,101,68,10,84,74,54,79,97,84,67,66,110,119,89,68,86,82,48,106,66,73,71,88,77,73,71,85,111,89,71,79,112,73,71,76,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,10,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,10,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,10,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,89,73,66,65,68,65,70,66,103,77,114,90,88,65,68,81,81,67,104,54,98,66,117,77,53,65,79,119,52,80,116,117,109,90,49,54,111,55,83,10,103,112,99,104,121,90,99,51,102,71,97,78,78,51,43,65,53,47,111,81,99,98,70,48,108,73,108,106,75,88,121,56,121,90,90,113,51,119,89,87,78,116,99,115,122,48,54,118,102,116,117,99,86,88,105,66,47,103,109,120,82,86,85,77,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10], + "ca_cert":[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,66,117,106,67,67,65,87,119,67,65,81,65,119,66,81,89,68,75,50,86,119,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,10,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,10,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,10,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,101,70,119,48,121,78,84,69,120,77,84,69,120,78,106,77,48,77,106,100,97,70,119,48,122,78,84,69,120,77,68,107,120,78,106,77,48,77,106,100,97,77,73,71,73,10,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,10,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,10,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,74,116,89,115,52,68,83,115,51,81,53,117,78,54,75,51,55,77,103,51,50,76,104,88,103,100,89,111,49,69,55,101,104,120,70,82,100,119,65,102,122,57,71,77,65,85,71,65,121,116,108,99,65,78,66,65,79,73,103,10,90,105,109,66,47,101,120,106,77,47,78,113,117,52,106,65,86,116,68,74,47,107,108,109,84,97,103,76,79,98,109,103,122,107,114,110,55,50,102,53,69,51,84,104,121,116,69,49,79,85,104,83,114,109,102,98,97,118,83,114,78,57,73,90,10,102,119,57,98,66,81,110,79,101,87,78,113,122,109,101,74,115,119,52,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10] +} +``` + +## Credentials services for workload owner + +The workload owners can retrieve the client (or owner) specific credentials from Trustee. A workload woner (who has the private key of Trustee) can invoke the following APIs. + +### `list_pods` +Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/list_pods`. Here, `list_pods` is an API that the client (or workload owner) can invoke to get the list of pod names and their additional information. + +### `client_credentials` +Only `GET` request is supported, e.g. `GET /kbs/v0/pki_vault/client_credentials?token=podToken12345&name=pod51&ip=60.11.12.89`. Here, `client_credentials` is an API that the workload owner can invoke to get the owner or client-specific credentails for a pod. A Pod VM obtains the server-specific credentials through the `get_resource` API. The `client_credentials` API also need to pass the same set of parameters through the `query` string to indicate the target pod. + +All APIs supported are described in the table below. + +| API | Description | Example | +|---------------------|-------------------------|-------------------------------------------| +| `list_pods` | To get the list of pods that have server credentials created| `kbs/v0/pki_vault/list_pods` | +| `client_credentials` | To get the client credentials for a pod | `/kbs/v0/pki_vault/client_credentials?token=podToken12345&name=pod51&ip=60.11.12.89` | + +The request will be processed only if the request is authenticated, otherwise an error is returned. The credentials for the client already already exists in the KBS as they have been already created as part of the response of server credential request, so the plugin simply returns the existing client credentials. + +Once the request is processed, the following structure is returned in JSON format. + +```rust +pub struct CredentialsOut { + pub key: Vec, // Key created + pub cert: Vec, // Self-signed certificate created + pub ca_cert: Vec, // CA certificate +} +``` + +Example credentials are below: + +```rust +{ + key:[45,45,45,45,45,66,69,71,73,78,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10,77,67,52,67,65,81,65,119,66,81,89,68,75,50,86,119,66,67,73,69,73,66,114,83,49,51,47,79,56,65,76,53,108,116,122,117,105,78,105,53,82,120,48,82,56,57,90,105,116,49,103,88,67,77,88,89,51,80,47,87,81,56,86,97,10,45,45,45,45,45,69,78,68,32,80,82,73,86,65,84,69,32,75,69,89,45,45,45,45,45,10], + cert:[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,110,68,67,67,65,107,54,103,65,119,73,66,65,103,73,66,65,106,65,70,66,103,77,114,90,88,65,119,103,89,103,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,10,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,10,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,70,68,65,83,10,66,103,78,86,66,65,77,77,67,50,100,121,99,71,77,116,100,71,120,122,73,69,78,66,77,66,52,88,68,84,73,49,77,84,69,120,77,84,69,50,78,68,89,121,78,49,111,88,68,84,73,50,77,68,85,120,77,68,69,50,78,68,89,121,10,78,49,111,119,103,89,77,120,67,122,65,74,66,103,78,86,66,65,89,84,65,107,70,66,77,82,89,119,70,65,89,68,86,81,81,73,68,65,49,69,90,87,90,104,100,87,120,48,73,70,78,48,89,88,82,108,77,82,85,119,69,119,89,68,10,86,81,81,72,68,65,120,69,90,87,90,104,100,87,120,48,73,69,78,112,100,72,107,120,72,84,65,98,66,103,78,86,66,65,111,77,70,69,82,108,90,109,70,49,98,72,81,103,84,51,74,110,89,87,53,112,101,109,70,48,97,87,57,117,10,77,82,85,119,69,119,89,68,86,81,81,76,68,65,120,69,90,87,90,104,100,87,120,48,73,70,86,117,97,88,81,120,68,122,65,78,66,103,78,86,66,65,77,77,66,110,78,108,99,110,90,108,99,106,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,66,88,105,90,70,54,106,115,102,48,72,53,88,121,43,66,117,107,109,65,65,115,90,81,51,85,66,89,108,86,121,119,66,103,82,88,72,71,77,54,102,103,56,111,52,72,102,77,73,72,99,77,65,119,71,65,49,85,100,10,69,119,69,66,47,119,81,67,77,65,65,119,67,119,89,68,86,82,48,80,66,65,81,68,65,103,87,103,77,66,48,71,65,49,85,100,68,103,81,87,66,66,81,68,108,55,70,52,85,90,106,104,71,55,80,90,50,43,105,52,70,70,74,75,10,104,114,72,77,55,122,67,66,110,119,89,68,86,82,48,106,66,73,71,88,77,73,71,85,111,89,71,79,112,73,71,76,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,10,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,10,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,10,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,89,73,66,65,68,65,70,66,103,77,114,90,88,65,68,81,81,65,54,48,80,122,65,67,74,121,87,109,77,104,67,43,71,57,98,118,102,67,80,10,116,66,102,109,86,67,89,89,90,116,108,84,115,90,99,104,51,112,118,116,76,102,81,114,55,113,105,79,111,112,71,57,87,50,87,49,104,75,77,50,87,103,105,104,51,114,108,106,67,80,115,70,83,70,90,115,86,99,85,119,118,56,69,71,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10], + ca_cert:[45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,66,117,106,67,67,65,87,119,67,65,81,65,119,66,81,89,68,75,50,86,119,77,73,71,73,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,10,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,10,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,10,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,101,70,119,48,121,78,84,69,120,77,84,69,120,78,106,77,48,77,106,100,97,70,119,48,122,78,84,69,120,77,68,107,120,78,106,77,48,77,106,100,97,77,73,71,73,10,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,66,81,84,69,87,77,66,81,71,65,49,85,69,67,65,119,78,82,71,86,109,89,88,86,115,100,67,66,84,100,71,70,48,90,84,69,86,77,66,77,71,65,49,85,69,66,119,119,77,10,82,71,86,109,89,88,86,115,100,67,66,68,97,88,82,53,77,82,48,119,71,119,89,68,86,81,81,75,68,66,82,69,90,87,90,104,100,87,120,48,73,69,57,121,90,50,70,117,97,88,112,104,100,71,108,118,98,106,69,86,77,66,77,71,10,65,49,85,69,67,119,119,77,82,71,86,109,89,88,86,115,100,67,66,86,98,109,108,48,77,82,81,119,69,103,89,68,86,81,81,68,68,65,116,110,99,110,66,106,76,88,82,115,99,121,66,68,81,84,65,113,77,65,85,71,65,121,116,108,10,99,65,77,104,65,74,116,89,115,52,68,83,115,51,81,53,117,78,54,75,51,55,77,103,51,50,76,104,88,103,100,89,111,49,69,55,101,104,120,70,82,100,119,65,102,122,57,71,77,65,85,71,65,121,116,108,99,65,78,66,65,79,73,103,10,90,105,109,66,47,101,120,106,77,47,78,113,117,52,106,65,86,116,68,74,47,107,108,109,84,97,103,76,79,98,109,103,122,107,114,110,55,50,102,53,69,51,84,104,121,116,69,49,79,85,104,83,114,109,102,98,97,118,83,114,78,57,73,90,10,102,119,57,98,66,81,110,79,101,87,78,113,122,109,101,74,115,119,52,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10] + +} +``` \ No newline at end of file diff --git a/kbs/src/plugins/implementations/mod.rs b/kbs/src/plugins/implementations/mod.rs index 7e72422f33..fbbd08d004 100644 --- a/kbs/src/plugins/implementations/mod.rs +++ b/kbs/src/plugins/implementations/mod.rs @@ -6,6 +6,8 @@ pub mod nebula_ca; #[cfg(feature = "pkcs11")] pub mod pkcs11; +#[cfg(feature = "pki-vault-plugin")] +pub mod pki_vault; pub mod resource; pub mod sample; @@ -13,5 +15,7 @@ pub mod sample; pub use nebula_ca::{NebulaCaPlugin, NebulaCaPluginConfig}; #[cfg(feature = "pkcs11")] pub use pkcs11::{Pkcs11Backend, Pkcs11Config}; +#[cfg(feature = "pki-vault-plugin")] +pub use pki_vault::{PKIVaultPlugin, PKIVaultPluginConfig}; pub use resource::{RepositoryConfig, ResourceStorage}; pub use sample::{Sample, SampleConfig}; diff --git a/kbs/src/plugins/implementations/pki_vault.rs b/kbs/src/plugins/implementations/pki_vault.rs new file mode 100644 index 0000000000..8b52f7a47c --- /dev/null +++ b/kbs/src/plugins/implementations/pki_vault.rs @@ -0,0 +1,557 @@ +// Copyright (c) 2025 by IBM Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use actix_web::http::Method; +use anyhow::{anyhow, bail, Context, Error, Result}; +use std::sync::RwLock; +use std::{collections::HashMap, sync::Arc}; + +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private}; +use openssl::x509::{ + extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier}, + X509Builder, X509Name, X509NameBuilder, X509, +}; +use serde::{Deserialize, Serialize}; + +use crate::plugins::plugin_manager::ClientPlugin; + +/// Default certificate details if not configured +pub const DEFAULT_COUNTRY: &str = "AA"; +pub const DEFAULT_STATE: &str = "Default State"; +pub const DEFAULT_LOCALITY: &str = "Default City"; +pub const DEFAULT_ORGANIZATION: &str = "Default Organization"; +pub const DEFAULT_ORG_UNIT: &str = "Default Unit"; +pub const DEFAULT_CA_COMMON_NAME: &str = "grpc-tls CA"; +pub const DEFAULT_SERVER_COMMON_NAME: &str = "server"; +pub const DEFAULT_CLIENT_COMMON_NAME: &str = "client"; +pub const DEFAULT_CA_VALIDITY_DAYS: u32 = 3650; +pub const DEFAULT_SERVER_VALIDITY_DAYS: u32 = 180; +pub const DEFAULT_CLIENT_VALIDITY_DAYS: u32 = 180; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct PKIVaultCertDetails { + /// Two-letter country code represents the country in which the entity resides + pub country: String, + /// State or province where the entity is located + pub state: String, + /// Locality or city where the entity is located + pub locality: String, + /// Organization or company name + pub organization: String, + /// An organizational unit within the organization + pub org_unit: String, + /// Information regarding the CA certificate + pub ca: CaCrtDetails, + /// Information regarding the server certificate + pub server: ServerCrtDetails, + /// Information regarding the client certificate + pub client: ClientCrtDetails, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct CaCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for CaCrtDetails { + fn default() -> Self { + CaCrtDetails { + common_name: DEFAULT_CA_COMMON_NAME.to_string(), + validity_days: DEFAULT_CA_VALIDITY_DAYS, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct ServerCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for ServerCrtDetails { + fn default() -> Self { + ServerCrtDetails { + common_name: DEFAULT_SERVER_COMMON_NAME.to_string(), + validity_days: DEFAULT_SERVER_VALIDITY_DAYS, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct ClientCrtDetails { + pub common_name: String, + pub validity_days: u32, +} + +impl Default for ClientCrtDetails { + fn default() -> Self { + ClientCrtDetails { + common_name: DEFAULT_CLIENT_COMMON_NAME.to_string(), + validity_days: DEFAULT_CLIENT_VALIDITY_DAYS, + } + } +} + +impl Default for PKIVaultCertDetails { + fn default() -> Self { + PKIVaultCertDetails { + country: DEFAULT_COUNTRY.to_string(), + state: DEFAULT_STATE.to_string(), + locality: DEFAULT_LOCALITY.to_string(), + organization: DEFAULT_ORGANIZATION.to_string(), + org_unit: DEFAULT_ORG_UNIT.to_string(), + ca: CaCrtDetails::default(), + server: ServerCrtDetails::default(), + client: ClientCrtDetails::default(), + } + } +} + +/// Credentials necessary for mutual TLS communication +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PKIVaultCA { + pub key: Vec, + pub cert: Vec, +} + +impl PKIVaultCA { + pub fn new(cert_details: &PKIVaultCertDetails) -> Result { + // Private keys for CA, Server, and Client + let key = PKey::generate_ed25519()?; + + // Generate CA certificate + let cert = Self::generate_ca_cert(&key, cert_details)?; + + Ok(Self { + key: key.private_key_to_pem_pkcs8()?, + cert: cert.to_pem()?, + }) + } + + /// Initializes a PKIVaultCA from existing key and certificate bytes. + pub fn init(key: Vec, cert: Vec) -> Result { + // Optional: Validate that the key and cert are valid before returning + let _ = PKey::private_key_from_pem(&key)?; + let _ = X509::from_pem(&cert)?; + + Ok(Self { key, cert }) + } + + fn generate_credentials( + &self, + ca_cert: &X509, + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + for_podvm_or_owner: &str, + ) -> Result<(PKey, X509)> { + // Generate private key and certificate + let key = PKey::generate_ed25519()?; + let cert = Self::generate_signed_cert( + &key, + &ca_cert, + &ca_private_key, + cert_details, + for_podvm_or_owner, + )?; + + Ok((key, cert)) + } + + fn generate_signed_cert( + private_key: &PKey, + ca_cert: &X509, + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + server_or_client: &str, + ) -> Result { + // Check if the certificate is for server or client + let (common_name, validity_days) = if server_or_client == "server" { + ( + &cert_details.server.common_name, + cert_details.server.validity_days, + ) + } else { + ( + &cert_details.client.common_name, + cert_details.client.validity_days, + ) + }; + + // Build the x509 name + let name = Self::build_x509_name( + common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + + let mut x509_builder = X509Builder::new()?; + x509_builder.set_version(2)?; + x509_builder.set_subject_name(&name)?; + x509_builder.set_issuer_name(ca_cert.subject_name())?; + x509_builder.set_pubkey(private_key)?; + + let serial_number = BigNum::from_u32(2)?.to_asn1_integer()?; + x509_builder.set_serial_number(&serial_number)?; + x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; + x509_builder.set_not_after(Asn1Time::days_from_now(validity_days)?.as_ref())?; + + // Add extensions from the certificate extensions file + x509_builder.append_extension(BasicConstraints::new().critical().build()?)?; + x509_builder.append_extension( + KeyUsage::new() + .digital_signature() + .key_encipherment() + .build()?, + )?; + x509_builder.append_extension( + SubjectKeyIdentifier::new().build(&x509_builder.x509v3_context(None, None))?, + )?; + x509_builder.append_extension( + AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&x509_builder.x509v3_context(Some(ca_cert), None))?, + )?; + + x509_builder.sign(ca_private_key, MessageDigest::null())?; + Ok(x509_builder.build()) + } + + fn generate_ca_cert( + ca_private_key: &PKey, + cert_details: &PKIVaultCertDetails, + ) -> Result { + // Build the x509 name + let name = Self::build_x509_name( + &cert_details.ca.common_name, + &cert_details.country, + &cert_details.state, + &cert_details.locality, + &cert_details.organization, + &cert_details.org_unit, + )?; + + // Build the X.509 certificate + let mut x509_builder = X509Builder::new()?; + x509_builder.set_subject_name(&name)?; + x509_builder.set_issuer_name(&name)?; + x509_builder.set_pubkey(ca_private_key)?; + + // Set certificate validity period + x509_builder.set_not_before(Asn1Time::days_from_now(0)?.as_ref())?; + x509_builder + .set_not_after(Asn1Time::days_from_now(cert_details.ca.validity_days)?.as_ref())?; + + // Sign the certificate + x509_builder.sign(ca_private_key, MessageDigest::null())?; + Ok(x509_builder.build()) + } + + fn build_x509_name( + common_name: &str, + country: &str, + state: &str, + locality: &str, + organization: &str, + org_unit: &str, + ) -> Result { + let mut name_builder = X509NameBuilder::new()?; + name_builder.append_entry_by_text("C", country)?; + name_builder.append_entry_by_text("ST", state)?; + name_builder.append_entry_by_text("L", locality)?; + name_builder.append_entry_by_text("O", organization)?; + name_builder.append_entry_by_text("OU", org_unit)?; + name_builder.append_entry_by_text("CN", common_name)?; + let name = name_builder.build(); + + Ok(name) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct PKIVaultPluginConfig { + pub pkivault_cert_details: PKIVaultCertDetails, +} + +impl Default for PKIVaultPluginConfig { + fn default() -> Self { + PKIVaultPluginConfig { + pkivault_cert_details: PKIVaultCertDetails::default(), + } + } +} + +impl TryFrom for PKIVaultPlugin { + type Error = Error; + + fn try_from(config: PKIVaultPluginConfig) -> Result { + let empty_cas: HashMap = HashMap::new(); + + // Initializing the PKI Vault plugin with existing credentials data from file + Ok(PKIVaultPlugin { + cert_details: config.pkivault_cert_details, + ca_store: Arc::new(RwLock::new(empty_cas)), + }) + } +} + +/// Parameters for the credentials request +/// +/// These parameters are provided in the request via URL query string. +/// Parameters taken by the "pki-vault" plugin to generate a unique key +/// for a sandbox store and retrieve credentials specific to the sandbox. +#[derive(Debug, PartialEq, serde::Deserialize)] +pub struct SandboxParams { + /// Required: Token assigned to the Pod + pub token: String, + + /// Required: Pod name (unique within a namespace) + pub name: String, + + /// Required: Pod IP address + pub ip: String, +} + +impl TryFrom<&str> for SandboxParams { + type Error = Error; + + fn try_from(query: &str) -> Result { + let params: SandboxParams = serde_qs::from_str(query)?; + Ok(params) + } +} + +/// Credentials necessary for secure server-client communication +#[derive(Debug, serde::Serialize)] +pub struct CredentialsOut { + pub key: Vec, + pub cert: Vec, + pub ca_cert: Vec, +} + +/// Manages the credentials generation, handling requests +/// from backend, and credentials persistence storage +pub struct PKIVaultPlugin { + pub cert_details: PKIVaultCertDetails, + pub ca_store: Arc>>, +} + +impl PKIVaultPlugin { + fn construct_key(&self, params: &SandboxParams) -> String { + format!("{}_{}_{}", params.name, params.ip, params.token) + } + + fn get_ca(&self, key: &str) -> Option { + let ca_store = self.ca_store.read().unwrap(); + ca_store.get(key).cloned() + } + + fn store_ca(&self, key: &str, ca: PKIVaultCA) { + let mut ca_store = self.ca_store.write().unwrap(); + ca_store.insert(key.to_string(), ca); + } + + async fn generate_pod_credentials(&self, params: &SandboxParams) -> Result> { + let key = self.construct_key(¶ms); + + // Return the stored CA credentials if they are present in the hashmap + let ca = if let Some(stored_ca) = self.get_ca(&key) { + log::info!("Generating credentials using existing CA!"); + PKIVaultCA::init(stored_ca.key, stored_ca.cert)? + } else { + log::info!("Generating credentials using new CA!"); + let new_ca = PKIVaultCA::new(&self.cert_details)?; + + // Store the newly created CA for future use + self.store_ca(&key, new_ca.clone()); + + new_ca + }; + + let (pod_key, pod_cert) = ca.generate_credentials( + &X509::from_pem(&ca.cert)?, + &PKey::private_key_from_pem(&ca.key)?, + &self.cert_details, + "server", + )?; + + let resource = CredentialsOut { + key: pod_key.private_key_to_pem_pkcs8()?, + cert: pod_cert.to_pem()?, + ca_cert: ca.cert, + }; + + Ok(serde_json::to_vec(&resource)?) + } + + async fn list_pods(&self) -> Result> { + let ca_store = self.ca_store.read().unwrap(); + let keys: Vec = ca_store.keys().cloned().collect(); + Ok(serde_json::to_vec(&keys).expect("Failed to deserialize it!")) + } + + async fn generate_client_credentials(&self, params: &SandboxParams) -> Result> { + let key_hash = self.construct_key(¶ms); + + // Return the stored CA credentials if they are present in the hashmap + if let Some(stored_ca) = self.get_ca(&key_hash) { + log::info!("Returning client credentials!"); + + let ca = PKIVaultCA::init(stored_ca.key, stored_ca.cert)?; + + let (pod_key, pod_cert) = ca.generate_credentials( + &X509::from_pem(&ca.cert)?, + &PKey::private_key_from_pem(&ca.key)?, + &self.cert_details, + "server", + )?; + + let resource = CredentialsOut { + key: pod_key.private_key_to_pem_pkcs8()?, + cert: pod_cert.to_pem()?, + ca_cert: ca.cert, + }; + + return Ok(serde_json::to_vec(&resource)?); + } else { + log::info!("Credentails cannot be generated. No CA found!"); + + return Ok(serde_json::to_vec("")?); + } + } +} + +#[async_trait::async_trait] +impl ClientPlugin for PKIVaultPlugin { + async fn handle( + &self, + _body: &[u8], + query: &str, + path: &str, + method: &Method, + ) -> Result> { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + + match method.as_str() { + "GET" => match sub_path { + "credentials" => { + let params = SandboxParams::try_from(query)?; + let credentials = self.generate_pod_credentials(¶ms).await?; + + Ok(credentials) + } + "list_pods" => { + let pods = self.list_pods().await?; + + Ok(pods) + } + "client_credentials" => { + let params = SandboxParams::try_from(query)?; + let credentials = self.generate_client_credentials(¶ms).await?; + + Ok(credentials) + } + _ => Err(anyhow!("{} not supported", sub_path))?, + }, + _ => bail!("Illegal HTTP method. Only supports `GET` and `POST`"), + } + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &str, + path: &str, + method: &Method, + ) -> Result { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + + if method.as_str() == "GET" { + if sub_path != "credentials" { + return Ok(true); + } + } + + Ok(false) + } + + /// Whether the body needs to be encrypted via TEE key pair. + /// If returns `Ok(true)`, the KBS server will encrypt the whole body + /// with TEE key pair and use KBS protocol's Response format. + async fn encrypted( + &self, + _body: &[u8], + _query: &str, + path: &str, + method: &Method, + ) -> Result { + let sub_path = path + .strip_prefix('/') + .context("accessed path is illegal, should start with `/`")?; + + if method.as_str() == "GET" { + if sub_path == "credentials" { + return Ok(true); + } + } + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio; + + #[tokio::test] + async fn test_handle() { + let config = PKIVaultPluginConfig::default(); + let plugin = PKIVaultPlugin::try_from(config).unwrap(); + + // Define sample inputs + let body: &[u8] = b""; + let query = "token=podToken12345&name=pod51&ip=60.11.12.89"; + let path = "/credentials"; + let method = &Method::GET; + + // Act: call the handle method + let result = plugin.handle(body, query, path, method).await; + + // Assert: check the result + match result { + Ok(response) => { + // Expected results + let key = String::from("podToken12345_pod51_60.11.12.89"); + + if let Some(credentials) = plugin.get_credentials(&key) { + let resource = CredentialsOut { + key: credentials.server_key, + cert: credentials.server_cert, + ca_cert: credentials.ca_cert, + }; + + let expected_response = serde_json::to_vec(&resource).unwrap(); + assert_eq!(response, expected_response); + }; + } + Err(e) => panic!("Expected Ok, got Err: {:?}", e), + } + } +} diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs index c4d91ca552..b1836ac375 100644 --- a/kbs/src/plugins/plugin_manager.rs +++ b/kbs/src/plugins/plugin_manager.rs @@ -16,6 +16,9 @@ use super::{NebulaCaPlugin, NebulaCaPluginConfig}; #[cfg(feature = "pkcs11")] use super::{Pkcs11Backend, Pkcs11Config}; +#[cfg(feature = "pki-vault-plugin")] +use super::{PKIVaultPlugin, PKIVaultPluginConfig}; + type ClientPluginInstance = Arc; #[async_trait::async_trait] @@ -73,6 +76,10 @@ pub enum PluginsConfig { #[cfg(feature = "pkcs11")] #[serde(alias = "pkcs11")] Pkcs11(Pkcs11Config), + + #[cfg(feature = "pki-vault-plugin")] + #[serde(alias = "pki_vault")] + PKIVaultPlugin(PKIVaultPluginConfig), } impl Display for PluginsConfig { @@ -84,6 +91,8 @@ impl Display for PluginsConfig { PluginsConfig::NebulaCaPlugin(_) => f.write_str("nebula-ca"), #[cfg(feature = "pkcs11")] PluginsConfig::Pkcs11(_) => f.write_str("pkcs11"), + #[cfg(feature = "pki-vault-plugin")] + PluginsConfig::PKIVaultPlugin(_) => f.write_str("pki_vault"), } } } @@ -109,6 +118,12 @@ impl TryInto for PluginsConfig { .context("Initialize 'nebula-ca-plugin' failed")?; Arc::new(nebula_ca) as _ } + #[cfg(feature = "pki-vault-plugin")] + PluginsConfig::PKIVaultPlugin(pkivault_config) => { + let pkivault_plugin = PKIVaultPlugin::try_from(pkivault_config) + .context("Initialize 'PKI_Vault' plugin failed")?; + Arc::new(pkivault_plugin) as _ + } #[cfg(feature = "pkcs11")] PluginsConfig::Pkcs11(pkcs11_config) => { let pkcs11 = Pkcs11Backend::try_from(pkcs11_config)