diff --git a/Cargo.lock b/Cargo.lock index 596b5be081..301f2cd1ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -555,7 +555,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attestation-agent" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=19b727ade7591f6b29e3c092ec6a1b1566f95a62#19b727ade7591f6b29e3c092ec6a1b1566f95a62" +source = "git+https://github.com/salmanyam/guest-components.git?rev=bd8d941#bd8d941b7e7f18aabead2f0a1e7cafe81ad3c6b9" dependencies = [ "anyhow", "async-trait", @@ -630,7 +630,7 @@ dependencies = [ [[package]] name = "attester" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=19b727ade7591f6b29e3c092ec6a1b1566f95a62#19b727ade7591f6b29e3c092ec6a1b1566f95a62" +source = "git+https://github.com/salmanyam/guest-components.git?rev=bd8d941#bd8d941b7e7f18aabead2f0a1e7cafe81ad3c6b9" dependencies = [ "anyhow", "async-trait", @@ -1616,7 +1616,7 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=19b727ade7591f6b29e3c092ec6a1b1566f95a62#19b727ade7591f6b29e3c092ec6a1b1566f95a62" +source = "git+https://github.com/salmanyam/guest-components.git?rev=bd8d941#bd8d941b7e7f18aabead2f0a1e7cafe81ad3c6b9" dependencies = [ "aes-gcm", "aes-kw 0.2.1", @@ -3650,6 +3650,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_qs", "serde_with", "serial_test", "sha2 0.11.0", @@ -3705,7 +3706,7 @@ dependencies = [ [[package]] name = "kbs_protocol" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=19b727ade7591f6b29e3c092ec6a1b1566f95a62#19b727ade7591f6b29e3c092ec6a1b1566f95a62" +source = "git+https://github.com/salmanyam/guest-components.git?rev=bd8d941#bd8d941b7e7f18aabead2f0a1e7cafe81ad3c6b9" dependencies = [ "anyhow", "async-trait", @@ -3761,7 +3762,7 @@ dependencies = [ [[package]] name = "kms" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=19b727ade7591f6b29e3c092ec6a1b1566f95a62#19b727ade7591f6b29e3c092ec6a1b1566f95a62" +source = "git+https://github.com/salmanyam/guest-components.git?rev=bd8d941#bd8d941b7e7f18aabead2f0a1e7cafe81ad3c6b9" dependencies = [ "anyhow", "async-trait", @@ -5015,7 +5016,7 @@ dependencies = [ [[package]] name = "protos" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=19b727ade7591f6b29e3c092ec6a1b1566f95a62#19b727ade7591f6b29e3c092ec6a1b1566f95a62" +source = "git+https://github.com/salmanyam/guest-components.git?rev=bd8d941#bd8d941b7e7f18aabead2f0a1e7cafe81ad3c6b9" dependencies = [ "prost", "tonic", @@ -5543,7 +5544,7 @@ dependencies = [ [[package]] name = "resource_uri" version = "0.1.0" -source = "git+https://github.com/confidential-containers/guest-components.git?rev=19b727ade7591f6b29e3c092ec6a1b1566f95a62#19b727ade7591f6b29e3c092ec6a1b1566f95a62" +source = "git+https://github.com/salmanyam/guest-components.git?rev=bd8d941#bd8d941b7e7f18aabead2f0a1e7cafe81ad3c6b9" dependencies = [ "anyhow", "serde", @@ -6139,6 +6140,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "serde_spanned" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index cd5279b70b..d44af22aa8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,9 @@ config = "0.15.22" educe = "0.6" ear = "0.5.0" hex = "0.4.3" -kbs_protocol = { git = "https://github.com/confidential-containers/guest-components.git", rev = "19b727ade7591f6b29e3c092ec6a1b1566f95a62", default-features = false } +kbs_protocol = { git = "https://github.com/salmanyam/guest-components.git", rev = "bd8d941", default-features = false } kbs-types = "0.15.0" -kms = { git = "https://github.com/confidential-containers/guest-components.git", rev = "19b727ade7591f6b29e3c092ec6a1b1566f95a62", default-features = false } +kms = { git = "https://github.com/salmanyam/guest-components.git", rev = "bd8d941", default-features = false } jsonwebtoken = { version = "10", default-features = false, features = [ "aws_lc_rs", "use_pem", @@ -64,6 +64,7 @@ serial_test = { version = "3.4.0", features = ["async"] } sha2 = "0.11" shadow-rs = "2.0.0" strum = { version = "0.28", features = ["derive"] } +serde_qs = "0.13.0" thiserror = "2.0" tokio = { version = "1", features = ["full"], default-features = false } toml = "1.1.2" diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 93a86c4c63..aecf9704d2 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -40,6 +40,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) +keyflux-plugin = [] + # Use HashiCorp Vault KV v1 as KBS backend vault = ["vaultrs"] @@ -76,6 +80,7 @@ rsa.workspace = true rustls = { version = "0.23", default-features = false, features = [ "ring", ], optional = true } +serde_qs.workspace = true semver = "1.0.28" serde = { workspace = true, features = ["derive"] } serde_json.workspace = true diff --git a/kbs/Makefile b/kbs/Makefile index d96d30c5b2..d7d0620d53 100644 --- a/kbs/Makefile +++ b/kbs/Makefile @@ -3,6 +3,7 @@ ALIYUN ?= false NEBULA_CA_PLUGIN ?= false VAULT ?= true EXTERNAL_PLUGIN ?= false +KEYFLUX_PLUGIN ?= false BUILD_ARCH := $(shell uname -m) ARCH ?= $(shell uname -m) @@ -57,6 +58,10 @@ ifeq ($(NEBULA_CA_PLUGIN), true) override FEATURES := $(FEATURES),nebula-ca-plugin endif +ifeq ($(KEYFLUX_PLUGIN), true) + override FEATURES := $(FEATURES),keyflux-plugin +endif + ifeq ($(VAULT), true) override FEATURES := $(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/keyflux.rs b/kbs/src/plugins/implementations/keyflux.rs new file mode 100644 index 0000000000..dc37bd6c1f --- /dev/null +++ b/kbs/src/plugins/implementations/keyflux.rs @@ -0,0 +1,867 @@ +// 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, Error, Result}; +use std::sync::RwLock; +use std::{collections::HashMap, collections::HashSet, sync::Arc}; + +use openssl::asn1::Asn1Time; +use openssl::bn::BigNum; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private}; +use openssl::rsa::Rsa; +use openssl::x509::{ + extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier}, + X509Builder, X509Name, X509NameBuilder, X509, +}; +use serde::{Deserialize, Serialize}; + +use super::super::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_VALIDITY_DAYS: u32 = 3650; +pub const DEFAULT_SERVER_VALIDITY_DAYS: u32 = 180; +pub const DEFAULT_CLIENT_VALIDITY_DAYS: u32 = 180; + +pub const CA_DEFAULT_COMMON_NAME: &str = "KeyFluxCA"; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct KeyFluxPluginConfig { + #[serde(default)] + pub keyflux: KeyFluxSection, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct QueryConfig { + #[serde(default)] + pub required: Vec, + + #[serde(default)] + pub spec: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct SpecConfig { + #[serde(default)] + pub required: bool, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct LimitsConfig { + pub symmetric_key_size: usize, + pub rsa_bits: usize, + pub allow_types: Vec, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[derive(Default)] +pub struct KeyFluxSection { + #[serde(default)] + pub ca: TlsCertDetails, + + #[serde(default)] + pub query: QueryConfig, + + #[serde(default)] + pub limits: LimitsConfig, +} + +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum SecretType { + Tls, + Symmetric, + Ed25519, + Rsa, +} + +#[derive(Deserialize)] +struct CertDetailsWrapper { + client: Option, + server: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(default)] +pub struct TlsCertDetails { + pub country: String, + pub state: String, + pub locality: String, + pub organization: String, + pub org_unit: String, + pub common_name: String, + pub validity_days: u32, +} + +impl Default for TlsCertDetails { + fn default() -> Self { + Self { + 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(), + common_name: "NOT_SET".to_string(), + validity_days: DEFAULT_CA_VALIDITY_DAYS, + } + } +} + +impl TlsCertDetails { + pub fn builder() -> Self { + Default::default() + } + + pub fn common_name(mut self, name: impl Into) -> Self { + self.common_name = name.into(); + self + } +} + +impl Default for KeyFluxPluginConfig { + fn default() -> Self { + Self { + keyflux: KeyFluxSection { + //secrets: vec![], + ca: TlsCertDetails::default(), + query: QueryConfig::default(), + limits: LimitsConfig::default(), + } + } + } +} + +impl Default for QueryConfig { + fn default() -> Self { + Self { + required: vec![ + "id".to_string(), + ], + spec: Some(SpecConfig::default()), + } + } +} + +impl Default for SpecConfig { + fn default() -> Self { + Self { + required: true, + } + } +} + +impl Default for LimitsConfig { + fn default() -> Self { + Self { + symmetric_key_size: 32, + rsa_bits: 2048, + allow_types: vec![ + "tls".to_string(), + "symmetric".to_string(), + "ed25519".to_string(), + "rsa".to_string(), + ], + } + } +} + +impl TryFrom for KeyFluxPlugin { + type Error = Error; + + fn try_from(config: KeyFluxPluginConfig) -> Result { + Ok(KeyFluxPlugin { + ca_config: config.keyflux.ca, + query_config: config.keyflux.query, + limits_config: config.keyflux.limits, + + spec_store: Arc::new(RwLock::new(HashMap::new())), + + server_cert_config_store: Arc::new(RwLock::new(HashMap::new())), + client_cert_config_store: Arc::new(RwLock::new(HashMap::new())), + + ca_store: Arc::new(RwLock::new(HashMap::new())), + + ed25519_store: Arc::new(RwLock::new(HashMap::new())), + rsa_store: Arc::new(RwLock::new(HashMap::new())), + symkey_store: Arc::new(RwLock::new(HashMap::new())), + }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct KeyFluxCA { + pub key: Vec, + pub cert: Vec, +} + +impl KeyFluxCA { + pub fn new(cert_details: &TlsCertDetails) -> Result { + let key = PKey::generate_ed25519()?; + let cert = Self::generate_ca_cert(&key, cert_details)?; + + Ok(Self { + key: key.private_key_to_pem_pkcs8()?, + cert: cert.to_pem()?, + }) + } + + /// Init KeyFluxCA with existing key and cert + pub fn init(key: Vec, cert: Vec) -> Result { + let _ = PKey::private_key_from_pem(&key)?; + let _ = X509::from_pem(&cert)?; + + Ok(Self { key, cert }) + } + + /// Generate private key and certificate + fn generate_credentials( + &self, + ca_cert: &X509, + ca_private_key: &PKey, + cert_details: &TlsCertDetails, + ) -> Result<(PKey, X509)> { + let key = PKey::generate_ed25519()?; + let cert = Self::generate_signed_cert( + &key, + &ca_cert, + &ca_private_key, + cert_details, + )?; + + Ok((key, cert)) + } + + /// Generate signed certificate + fn generate_signed_cert( + private_key: &PKey, + ca_cert: &X509, + ca_private_key: &PKey, + cert_details: &TlsCertDetails, + ) -> Result { + // Build the x509 name + let name = Self::build_x509_name( + &cert_details.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(cert_details.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()) + } + + /// Generate CA certificate + fn generate_ca_cert( + ca_private_key: &PKey, + cert_details: &TlsCertDetails, + ) -> Result { + // Build the x509 name + let name = Self::build_x509_name( + &cert_details.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.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) + } +} + +/// Credentials necessary for secure server-client communication +#[derive(Debug, serde::Serialize)] +pub struct CredentialsOut { + pub key: Vec, + pub cert: Vec, + pub ca_cert: Vec, +} + +#[derive(Debug, Serialize)] +pub struct SecretBundle { + pub entity: String, + pub secrets: HashMap, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "type")] +pub enum SecretMaterial { + Tls { + private_key: Vec, + cert: Vec, + ca_cert: Vec, + }, + Symmetric { + key: Vec, + }, + Ed25519 { + key: Vec, + }, + Rsa { + key: Vec, + }, +} + +/// Generates different secrets (tls, symmetric, rsa, ed255519, etc.) on the +/// fly for kata agent (server) and owner (client) +pub struct KeyFluxPlugin { + pub ca_config: TlsCertDetails, + pub query_config: QueryConfig, + pub limits_config: LimitsConfig, + + // store secret specs + pub spec_store: Arc>>>, + + // store the CAs + pub ca_store: Arc>>, + + pub server_cert_config_store: Arc>>, + pub client_cert_config_store: Arc>>, + + // Ed25519 pub key store for clients + pub ed25519_store: Arc>>>>, + + // Rsa pub key store for clients + pub rsa_store: Arc>>>>, + + // secret stored for clients + pub symkey_store: Arc>>>>, +} + +impl KeyFluxPlugin { + ///construct an identifier using query params + fn construct_key(&self, params: &HashMap) -> String { + self.query_config + .required + .iter() + .map(|k| params.get(k).unwrap()) + .cloned() + .collect::>() + .join("_") + } + + fn validate_query(&self, query: &HashMap) -> Result<()> { + let cfg = &self.query_config; + for key in &cfg.required { + if !query.contains_key(key) { + bail!("Missing required query parameter: {}", key); + } + + if query.get(key).map(|v| v.trim().is_empty()).unwrap_or(true) { + bail!("Query parameter '{}' cannot be empty", key); + } + } + + Ok(()) + } + + fn parse_spec(spec: &str) -> Result> { + let mut result = HashMap::new(); + + for item in spec.split(';') { + let parts: Vec<&str> = item.split(':').collect(); + + if parts.len() != 2 { + return Err(anyhow!("Invalid spec format: {}", item)); + } + + let secret_type = match parts[0] { + "tls" => SecretType::Tls, + "sym" | "symmetric" => SecretType::Symmetric, + "pkey" | "ed25519" => SecretType::Ed25519, + "rsa" => SecretType::Rsa, + other => return Err(anyhow!("Unknown secret type: {}", other)), + }; + + let count: usize = parts[1] + .parse() + .map_err(|_| anyhow!("Invalid count for {}", parts[0]))?; + + if count == 0 { + return Err(anyhow!("Count must be > 0 for {}", parts[0])); + } + + result.insert(secret_type, count); + } + + Ok(result) + } + + fn get_ca(&self, key: &str) -> Option { + let store = self.ca_store.read().unwrap(); + store.get(key).cloned() + } + + fn store_ca(&self, key: &str, ca: KeyFluxCA) { + let mut store = self.ca_store.write().unwrap(); + store.insert(key.to_string(), ca); + } + + fn get_symkey(&self, req_id: &str, spec_id: &str) -> Option> { + let store = self.symkey_store.read().unwrap(); + store + .get(req_id) + .and_then(|inner| inner.get(spec_id)) + .cloned() + } + + fn store_symkey(&self, req_id: &str, spec_id: &str, symkey: Vec) { + let mut store = self.symkey_store.write().unwrap(); + let inner_map = store + .entry(req_id.to_string()) + .or_insert_with(HashMap::new); + + inner_map.insert(spec_id.to_string(), symkey); + } + + fn get_ed25519_pkey(&self, req_id: &str, spec_id: &str) -> Option> { + let store = self.ed25519_store.read().unwrap(); + store + .get(req_id) + .and_then(|inner| inner.get(spec_id)) + .cloned() + } + + fn store_ed25519_pkey(&self, req_id: &str, spec_id: &str, key: Vec) { + let mut store = self.ed25519_store.write().unwrap(); + let inner_map = store + .entry(req_id.to_string()) + .or_insert_with(HashMap::new); + + inner_map.insert(spec_id.to_string(), key); + } + + fn get_rsa_pkey(&self, req_id: &str, spec_id: &str) -> Option> { + let store = self.rsa_store.read().unwrap(); + store + .get(req_id) + .and_then(|inner| inner.get(spec_id)) + .cloned() + } + + fn store_rsa_pkey(&self, req_id: &str, spec_id: &str, key: Vec) { + let mut store = self.rsa_store.write().unwrap(); + let inner_map = store + .entry(req_id.to_string()) + .or_insert_with(HashMap::new); + + inner_map.insert(spec_id.to_string(), key); + } + + async fn build_server_response(&self, + query: &HashMap, + ) -> Result> { + self.validate_query(query)?; + let req_id = self.construct_key(query); + + let spec_str = query + .get("spec") + .ok_or_else(|| anyhow!("Missing spec"))?; + + let spec_map = Self::parse_spec(spec_str)?; + self.spec_store.write().unwrap().insert(req_id.clone(), spec_map.clone()); + + let mut server_secrets_map: HashMap = HashMap::new(); + + for (secret_type, count) in spec_map { + match secret_type { + SecretType::Tls => { + let ca = match self.get_ca(&req_id) { + Some(c) => KeyFluxCA::init(c.key, c.cert)?, + None => { + let ca = KeyFluxCA::new(&self.ca_config)?; + self.store_ca(&req_id, ca.clone()); + ca + } + }; + let ca_cert = X509::from_pem(&ca.cert)?; + let ca_key = PKey::private_key_from_pem(&ca.key)?; + + // generate server Tls secrets + let ka_config = self.server_cert_config_store.read().unwrap() + .get(&req_id) + .cloned() + .unwrap_or_else(|| TlsCertDetails::builder().common_name("Kata Agent")); + + for i in 0..count { + let server_spec_id = format!("{}_{}", "tls", i); + let (key, cert) = ca.generate_credentials( + &ca_cert, + &ca_key, + &ka_config, + )?; + server_secrets_map.insert( + server_spec_id, + SecretMaterial::Tls { + private_key: key.private_key_to_pem_pkcs8()?, + cert: cert.to_pem()?, + ca_cert: ca.cert.clone(), + }, + ); + } + } + SecretType::Symmetric => { + //let size = spec.size.unwrap_or(32) as usize; + let size: usize = 32; + + for i in 0..count { + let mut key = vec![0u8; size]; + openssl::rand::rand_bytes(&mut key)?; + + let sym_id = format!("{}_{}", "sym", i); + server_secrets_map.insert( + sym_id.clone(), + SecretMaterial::Symmetric { key: key.clone() }, + ); + self.store_symkey(&req_id, &sym_id, key); + } + } + SecretType::Ed25519 => { + for i in 0..count { + let key = PKey::generate_ed25519()?; + + let private_key = key.private_key_to_pem_pkcs8()?; + let public_key = key.public_key_to_pem()?; + + let spec_id = format!("{}_{}", "ed25519_key", i); + + server_secrets_map.insert( + spec_id.clone(), + SecretMaterial::Ed25519 { + key: private_key.clone(), + }, + ); + + self.store_ed25519_pkey(&req_id, &spec_id, public_key); + } + } + SecretType::Rsa => { + let bits: u32 = 2048; + + for i in 0..count { + let rsa = Rsa::generate(bits)?; + let key = PKey::from_rsa(rsa)?; + + let private_key_pem = key.private_key_to_pem_pkcs8()?; + let public_key_pem = key.public_key_to_pem()?; + + let spec_id = format!("{}_{}", "rsa_key", i); + + server_secrets_map.insert( + spec_id.clone(), + SecretMaterial::Rsa { + key: private_key_pem.clone(), + }, + ); + + self.store_rsa_pkey(&req_id, &spec_id, public_key_pem); + } + } + } + } + + let bundle = SecretBundle { + entity: "server".to_string(), + secrets: server_secrets_map, + }; + + Ok(serde_json::to_vec(&bundle)?) + } + + async fn list_pods(&self) -> Result> { + let all_keys: Vec = { + let mut keys = HashSet::new(); + keys.extend(self.ca_store.read().unwrap().keys().cloned()); + keys.extend(self.ed25519_store.read().unwrap().keys().cloned()); + keys.extend(self.rsa_store.read().unwrap().keys().cloned()); + keys.extend(self.symkey_store.read().unwrap().keys().cloned()); + + keys.into_iter().collect() + }; + + Ok(serde_json::to_vec(&all_keys)?) + } + + async fn build_client_response(&self, + query: &HashMap, + ) -> Result> { + self.validate_query(query)?; + let req_id = self.construct_key(query); + + let mut client_secrets_map: HashMap = HashMap::new(); + + // retrieve the saved spec map from hashmap + let spec_map = { + let store = self.spec_store.read().unwrap(); + store.get(&req_id) + .cloned() + .ok_or_else(|| anyhow!("Spec not found for req_id: {}", req_id))? + }; + + for (secret_type, count) in spec_map { + match secret_type { + SecretType::Tls => { + // get existing CA, else return error + let ca = self + .get_ca(&req_id) + .ok_or_else(|| anyhow!("CA not found for sandbox: {}", req_id))?; + + let ca_cert = X509::from_pem(&ca.cert)?; + let ca_key = PKey::private_key_from_pem(&ca.key)?; + + // client Tls secrets + let client_config = self.client_cert_config_store.read().unwrap() + .get(&req_id) + .cloned() + .unwrap_or_else(|| TlsCertDetails::builder().common_name("Client")); + + for i in 0..count { + let (key, cert) = ca.generate_credentials( + &ca_cert, + &ca_key, + &client_config, + )?; + + client_secrets_map.insert( + format!("tls_{}", i), + SecretMaterial::Tls { + private_key: key.private_key_to_pem_pkcs8()?, + cert: cert.to_pem()?, + ca_cert: ca.cert.clone(), + }, + ); + } + } + SecretType::Symmetric => { + for i in 0..count { + let sym_id = format!("{}_{}", "sym", i); + let key = self + .get_symkey(&req_id, &sym_id) + .ok_or_else(|| anyhow::anyhow!("Symmetric key not found for spec_id={}", + sym_id + ))?; + + client_secrets_map.insert( + sym_id.clone(), + SecretMaterial::Symmetric { key: key.clone() }, + ); + } + } + SecretType::Ed25519 => { + for i in 0..count { + let spec_id = format!("{}_{}", "ed25519_key", i); + let key = self + .get_ed25519_pkey(&req_id, &spec_id) + .ok_or_else(|| anyhow::anyhow!("Ed25519 public key not found for spec_id={}", + spec_id + ))?; + + client_secrets_map.insert( + spec_id.clone(), + SecretMaterial::Ed25519 { key: key.clone() }, + ); + } + } + SecretType::Rsa => { + for i in 0..count { + let spec_id = format!("{}_{}", "rsa_key", i); + let key = self + .get_rsa_pkey(&req_id, &spec_id) + .ok_or_else(|| anyhow::anyhow!("RSA public key not found for spec_id={}", + spec_id + ))?; + + client_secrets_map.insert( + spec_id.clone(), + SecretMaterial::Rsa { key: key.clone() }, + ); + } + } + } + } + + let bundle = SecretBundle { + entity: "client".to_string(), + secrets: client_secrets_map, + }; + + Ok(serde_json::to_vec(&bundle)?) + } + async fn update_cert_details(&self, + query: &HashMap, + data: &[u8], + ) -> Result<()> { + self.validate_query(query)?; + let req_id = self.construct_key(query); + + let wrapper: CertDetailsWrapper = serde_json::from_slice(data) + .map_err(|e| anyhow!("Failed to deserialize JSON: {}", e))?; + + // update server cert details + if let Some(updates) = wrapper.server { + let mut store = self.server_cert_config_store.write().unwrap(); + + store.entry(req_id.clone()) + .and_modify(|existing| { + *existing = updates.clone(); + }) + .or_insert(updates); + } + + // update client cert details + if let Some(updates) = wrapper.client { + let mut store = self.client_cert_config_store.write().unwrap(); + + store.entry(req_id.clone()) + .and_modify(|existing| { + *existing = updates.clone(); + }) + .or_insert(updates); + } + + Ok(()) + } +} + +#[async_trait::async_trait] +impl ClientPlugin for KeyFluxPlugin { + async fn handle( + &self, + body: &[u8], + query: &HashMap, + path: &[&str], + method: &Method, + ) -> Result> { + if path.len() != 1 { + bail!("Illegal path. Only one path segment is supported"); + } + + match method.as_str() { + "GET" => match path[0] { + "credentials" => { + let credentials = self.build_server_response(query).await?; + Ok(credentials) + } + _ => Err(anyhow!("{} not supported", path[0]))?, + } + "POST" => match path[0] { + "list_pods" => { + let pods = self.list_pods().await?; + Ok(pods) + } + "client_creds" => { + let credentials = self.build_client_response(query).await?; + Ok(credentials) + } + "update_cert" => { + self.update_cert_details(query, body).await?; + Ok(vec![]) + } + _ => Err(anyhow!("{} not supported", path[0]))?, + } + _ => bail!("Illegal HTTP method. Only supports `GET` and `POST`"), + } + } + + async fn validate_auth( + &self, + _body: &[u8], + _query: &HashMap, + _path: &[&str], + method: &Method, + ) -> Result { + if method.as_str() == "POST" { + 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: &HashMap, + _path: &[&str], + method: &Method, + ) -> Result { + if method.as_str() == "GET" { + return Ok(true); + } + + Ok(false) + } +} diff --git a/kbs/src/plugins/implementations/mod.rs b/kbs/src/plugins/implementations/mod.rs index 9fc6ed84a6..1a8c7a22dd 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 = "keyflux-plugin")] +pub mod keyflux; pub mod resource; pub mod sample; @@ -21,3 +23,6 @@ pub use sample::{Sample, SampleConfig}; #[cfg(feature = "external-plugin")] pub use external_plugin::{BackendConfig, ExternalPlugin, ExternalPluginConfig, TlsMode}; + +#[cfg(feature = "keyflux-plugin")] +pub use keyflux::{KeyFluxPlugin, KeyFluxPluginConfig}; diff --git a/kbs/src/plugins/plugin_manager.rs b/kbs/src/plugins/plugin_manager.rs index d597e9cd6b..c7e1cd0332 100644 --- a/kbs/src/plugins/plugin_manager.rs +++ b/kbs/src/plugins/plugin_manager.rs @@ -20,6 +20,9 @@ use super::{Pkcs11Backend, Pkcs11Config}; #[cfg(feature = "external-plugin")] use super::{ExternalPlugin, ExternalPluginConfig}; +#[cfg(feature = "keyflux-plugin")] +use super::{KeyFluxPlugin, KeyFluxPluginConfig}; + type ClientPluginInstance = Arc; #[async_trait::async_trait] @@ -81,6 +84,10 @@ pub enum PluginsConfig { #[cfg(feature = "external-plugin")] #[serde(alias = "external")] ExternalPlugin(ExternalPluginConfig), + + #[cfg(feature = "keyflux-plugin")] + #[serde(alias = "keyflux")] + KeyFluxPlugin(KeyFluxPluginConfig), } impl Display for PluginsConfig { @@ -94,6 +101,8 @@ impl Display for PluginsConfig { PluginsConfig::Pkcs11(_) => f.write_str("pkcs11"), #[cfg(feature = "external-plugin")] PluginsConfig::ExternalPlugin(_) => f.write_str("external"), + #[cfg(feature = "keyflux-plugin")] + PluginsConfig::KeyFluxPlugin(_) => f.write_str("keyflux"), } } } @@ -137,6 +146,12 @@ impl PluginsConfig { .context("Initialize 'external' plugin failed")?; Arc::new(external_plugin) as _ } + #[cfg(feature = "keyflux-plugin")] + PluginsConfig::KeyFluxPlugin(pkivault_config) => { + let pkivault_plugin = KeyFluxPlugin::try_from(pkivault_config) + .context("Initialize 'keyflux' plugin failed")?; + Arc::new(pkivault_plugin) as _ + } }; Ok(plugin)