From 0847fe4944f7b9006725ef560f046a2962832b2f Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Tue, 12 May 2026 13:20:54 -0700 Subject: [PATCH 1/5] docs: update resource URI and add plugin support Our resource URI doc is out-of-date. Let's update it to reflect how we actually use resource URIs and to clarify the relationship between resource URIs and the KBS API. This has confused a number of people in the past. Also, let's introduce a mechanism for specifying the plugin in the resource URI. Specifically, let's allow people to specify the plugin in the scheme. Signed-off-by: Tobin Feldman-Fitzthum --- attestation-agent/docs/KBS_URI.md | 36 ------------ attestation-agent/docs/RESOURCE_URI.md | 76 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 36 deletions(-) delete mode 100644 attestation-agent/docs/KBS_URI.md create mode 100644 attestation-agent/docs/RESOURCE_URI.md diff --git a/attestation-agent/docs/KBS_URI.md b/attestation-agent/docs/KBS_URI.md deleted file mode 100644 index 4220e1bd2..000000000 --- a/attestation-agent/docs/KBS_URI.md +++ /dev/null @@ -1,36 +0,0 @@ -# KBS Resource URI - -## Introduction - -To uniquely identify every resource/key in the CoCo Key Broker System, a __KBS Resource URI__ is defined. - -## Specification - -A KBS Resource URI must comply with the following format: - -```plaintext -kbs://:/// -``` - -where: - -- `kbs://`: This is the fixed, custom KBS resource scheme. It indicates that this URI for a [CoCo KBS](https://github.com/confidential-containers/kbs/tree/main/kbs) resource. -- `:`: This the KBS host address and port. It is either an IP address or a domain name, and an *optional* TCP/UDP port. Also can be treated as a `confidential resource registry`. -- `//`: This is the resource path. Typically, `` would be a user name, `` would be the type of the resource, and `` would help distinguish between different resource instances of the same type. - -For example: `kbs://example.cckbs.org:8081/alice/decryption-key/1` - -## How Different KBC/KBS uses a KBS Resource URI - -### CC-KBC - -`CC-KBC` will convert a KBS Resource URI into a [CoCo KBS Resource API](https://github.com/confidential-containers/kbs/blob/main/kbs/docs/kbs.yaml#L100) compliant HTTP/HTTPS request. -For example, a KBS Resource URI `kbs://example.cckbs.org/alice/decryption-key/1` will be converted to `http://example.cckbs.org/kbs/v0/resource/alice/decryption-key/1`. - -### EAA KBC & Online SEV KBC - -Both KBCs will use the `//` as key/resource id in their requests. - -### Offline KBCs (e.g FS KBC) - -Offline KBCs should ignore the `:` host part of the URI, and use the resource path (`//`) to locally fetch the resource. diff --git a/attestation-agent/docs/RESOURCE_URI.md b/attestation-agent/docs/RESOURCE_URI.md new file mode 100644 index 000000000..5b3112cdd --- /dev/null +++ b/attestation-agent/docs/RESOURCE_URI.md @@ -0,0 +1,76 @@ +# Resource URIs + +## Introduction + +Resource URIs are used across Confidential Containers to identify resources. +For example, the metadata of an encrypted image can contain a resource URI +referencing the image decryption key. +An image signature policy or a sealed secret can contain a resource URI. + +Although these have been referred to as KBS Resource URIs, this abstraction +is implemented by the CDH. + +The resource URI is not the same thing as the path for requesting a resource +from Trustee's REST API. The mapping between these is described below. + +Technically, the Resource URI is not tied to any specific KBS, but this document +mainly focuses on Trustee and the CC_KBC and describes how the resource URI +relates to Trustee. +Some legacy code, such as the SEV KBC or the FS KBC may fulfill resource URIs +in different ways. + +## Specification + +A Resource URI must comply with the following format: + +```plaintext +kbs+://:/// +``` + +### Scheme + +The scheme always begins with `kbs`. Typically the scheme is simply `kbs://`, but a plus sign +can be used to specify that the resource should be fulfilled by a particular plugin. + +For instance, to represent a resource fulfilled by the Trustee `pkcs11` plugin, the +scheme would be `kbs+pkcs11://`. + +If no plugin is specified, the CDH will request a resource from the `resource` plugin. + +### Host and Port + +The host and port point to the KBS instance that will serve the resource. +Today, the CDH ignores these fields and instead gets the KBS URI and port +from the CDH config file. +This way, the resource URI does not need to be updated if the KBS URI changes. +This means that generally only one KBS serves resources for a pod, there are ways +to work around this with sealed secrets. +Multi-KBS workloads may be supported in the future. + +Since these fields are ignored, most resource URIs leave them out. +This results in three slashes in a row. +For example, `kbs:///repository/type/tag`. + +### Repository, Type, and Tag + +Currently resource URIs have three levels of identifiers/scope. +The terms `repository`, `type`, and `tag` are somewhat arbitrary. +These identifiers can be used in any way. + +### Query Strings + +The resource URI also supports query strings. + +## Examples + +* `kbs://example.cckbs.org:8081/alice/decryption-key/1` +* `kbs:///a/b/c` +* `kbs+pkcs11:///a/b/c` + +## Mapping + +The resource URI is transformed into an HTTP(S) request. +Specifically, the request will be made to `http://:/kbs/v0//alice/decryption-key/1`. + +If no plugin is specified in the resource URI, `resource` will be used. +More information on the KBS API can be found [here](https://github.com/confidential-containers/trustee/blob/main/kbs/docs/kbs.yaml). From 5da4bc4ed1ef7f30131572d1d907d5f0c19dca55 Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Thu, 14 May 2026 12:39:02 -0700 Subject: [PATCH 2/5] resource-uri: add plugin suppor to resource uri Allow the plugin to be specifid as part of the scheme. For example, kbs+plugin:///a/b/c Signed-off-by: Tobin Feldman-Fitzthum --- .../deps/resource_uri/src/lib.rs | 96 ++++++++++++++++--- .../kbs_protocol/src/client/rcar_client.rs | 8 +- .../kbs_protocol/src/client/token_client.rs | 8 +- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/attestation-agent/deps/resource_uri/src/lib.rs b/attestation-agent/deps/resource_uri/src/lib.rs index 487140196..8f67e33c0 100644 --- a/attestation-agent/deps/resource_uri/src/lib.rs +++ b/attestation-agent/deps/resource_uri/src/lib.rs @@ -14,8 +14,9 @@ const RESOURCE_ID_ERROR_INFO: &str = "invalid kbs resource uri, should be kbs://///"; const SCHEME: &str = "kbs"; +const DEFAULT_PLUGIN: &str = "resource"; -/// Resource Id document +/// Resource Id document #[derive(Clone, Debug, PartialEq, Eq)] pub struct ResourceUri { pub kbs_addr: String, @@ -23,6 +24,7 @@ pub struct ResourceUri { pub r#type: String, pub tag: String, pub query: Option, + pub plugin: Option, } impl TryFrom<&str> for ResourceUri { @@ -47,9 +49,19 @@ impl TryFrom for ResourceUri { } } - if value.scheme() != SCHEME { - return Err("scheme must be kbs"); - } + let scheme = value.scheme(); + + let plugin = match scheme { + SCHEME => None, + s if s.starts_with("kbs+") => { + let plugin_name = s.trim_start_matches("kbs+"); + if plugin_name.is_empty() { + return Err("scheme kbs+ requires a plugin name, e.g. kbs+pkcs11"); + } + Some(plugin_name.to_string()) + } + _ => return Err("scheme must be kbs or kbs+"), + }; if value.path().is_empty() { return Err(RESOURCE_ID_ERROR_INFO); @@ -64,6 +76,7 @@ impl TryFrom for ResourceUri { r#type: values[1].into(), tag: values[2].into(), query: value.query().map(|s| s.to_string()), + plugin, }) } else { Err(RESOURCE_ID_ERROR_INFO) @@ -106,6 +119,7 @@ impl ResourceUri { r#type: values[2].into(), tag: values[3].into(), query: None, + plugin: None, }) } else { bail!( @@ -115,8 +129,12 @@ impl ResourceUri { } pub fn whole_uri(&self) -> String { + let scheme = match &self.plugin { + Some(p) => format!("{SCHEME}+{p}"), + None => SCHEME.to_string(), + }; let uri = format!( - "{SCHEME}://{}/{}/{}/{}", + "{scheme}://{}/{}/{}/{}", self.kbs_addr, self.repository, self.r#type, self.tag ); match &self.query { @@ -125,6 +143,12 @@ impl ResourceUri { } } + /// Returns the plugin name. If no plugin was specified in the URI, + /// returns the default plugin "resource". + pub fn plugin(&self) -> &str { + self.plugin.as_deref().unwrap_or(DEFAULT_PLUGIN) + } + /// Only return the resource path. This function is used /// currently because up to now the kbs-uri is given /// to create an AA instance. @@ -158,13 +182,38 @@ mod tests { use rstest::rstest; #[rstest] - #[case("kbs:///alice/cosign-key/213", "alice", "cosign-key", "213", None)] #[case( - "kbs:///plugin/plugname/resourcename?param1=value1¶m2=value2", - "plugin", - "plugname", - "resourcename", - Some("param1=value1¶m2=value2") + "kbs:///alice/cosign-key/213", + "alice", + "cosign-key", + "213", + None, + None + )] + #[case( + "kbs:///repo/type/tag?param1=value1¶m2=value2", + "repo", + "type", + "tag", + Some("param1=value1¶m2=value2"), + None + )] + #[case( + "kbs+pkcs11:///repo/type/tag", + "repo", + "type", + "tag", + None, + Some("pkcs11") + )] + #[case("kbs+myplugin:///a/b/c", "a", "b", "c", None, Some("myplugin"))] + #[case( + "kbs+custom://example.com:8080/repo/type/tag", + "repo", + "type", + "tag", + None, + Some("custom") )] fn test_resource_uri_serialization_conversion( #[case] url: &str, @@ -172,13 +221,19 @@ mod tests { #[case] r#type: &str, #[case] tag: &str, #[case] query: Option<&str>, + #[case] plugin: Option<&str>, ) { let resource = ResourceUri { - kbs_addr: "".into(), + kbs_addr: if url.contains("example.com") { + "example.com:8080".into() + } else { + "".into() + }, repository: repository.into(), r#type: r#type.into(), tag: tag.into(), query: query.map(|s| s.to_string()), + plugin: plugin.map(|s| s.to_string()), }; // Deserialization @@ -201,4 +256,21 @@ mod tests { ResourceUri::try_from(url_from_string).expect("failed to try from url"); assert_eq!(resource_from_url, resource); } + + #[rstest] + #[case("kbs:///repo/type/tag", "resource")] + #[case("kbs+pkcs11:///repo/type/tag", "pkcs11")] + fn test_plugin_accessor(#[case] uri: &str, #[case] plugin: &str) { + let uri: ResourceUri = uri.try_into().expect("failed to parse uri"); + assert_eq!(uri.plugin(), plugin); + } + + #[rstest] + #[case("http:///repo/type/tag", "scheme must be kbs")] + #[case("kbs+:///repo/type/tag", "requires a plugin name")] + fn test_invalid_scheme(#[case] uri: &str, #[case] error: &str) { + let result = ResourceUri::try_from(uri); + assert!(result.is_err()); + assert!(result.unwrap_err().contains(error)); + } } diff --git a/attestation-agent/kbs_protocol/src/client/rcar_client.rs b/attestation-agent/kbs_protocol/src/client/rcar_client.rs index 949b97f81..d988490e6 100644 --- a/attestation-agent/kbs_protocol/src/client/rcar_client.rs +++ b/attestation-agent/kbs_protocol/src/client/rcar_client.rs @@ -330,8 +330,12 @@ impl KbsClient> { impl KbsClientCapabilities for KbsClient> { async fn get_resource(&mut self, resource_uri: ResourceUri) -> Result> { let mut remote_url = format!( - "{}/{KBS_PREFIX}/resource/{}/{}/{}", - self.kbs_host_url, resource_uri.repository, resource_uri.r#type, resource_uri.tag + "{}/{KBS_PREFIX}/{}/{}/{}/{}", + self.kbs_host_url, + resource_uri.plugin(), + resource_uri.repository, + resource_uri.r#type, + resource_uri.tag ); if let Some(ref q) = resource_uri.query { remote_url = format!("{remote_url}?{q}"); diff --git a/attestation-agent/kbs_protocol/src/client/token_client.rs b/attestation-agent/kbs_protocol/src/client/token_client.rs index fbe4bc901..a03ff8ddd 100644 --- a/attestation-agent/kbs_protocol/src/client/token_client.rs +++ b/attestation-agent/kbs_protocol/src/client/token_client.rs @@ -32,8 +32,12 @@ impl KbsClient> { impl KbsClientCapabilities for KbsClient> { async fn get_resource(&mut self, resource_uri: ResourceUri) -> Result> { let mut remote_url = format!( - "{}/{KBS_PREFIX}/resource/{}/{}/{}", - self.kbs_host_url, resource_uri.repository, resource_uri.r#type, resource_uri.tag + "{}/{KBS_PREFIX}/{}/{}/{}/{}", + self.kbs_host_url, + resource_uri.plugin(), + resource_uri.repository, + resource_uri.r#type, + resource_uri.tag ); if let Some(ref q) = resource_uri.query { remote_url = format!("{remote_url}?{q}"); From b5424e83ca1c89f8014e9f9b8ca891f419336f3a Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Thu, 14 May 2026 12:42:17 -0700 Subject: [PATCH 3/5] cdh: allow for plugin resource uris Fixup a couple of checks based on the scheme to allow for plugins Signed-off-by: Tobin Feldman-Fitzthum --- confidential-data-hub/hub/src/secret/mod.rs | 3 ++- .../hub/src/storage/volume_type/blockdevice/mod.rs | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/confidential-data-hub/hub/src/secret/mod.rs b/confidential-data-hub/hub/src/secret/mod.rs index bbd699fb0..6d939c70a 100644 --- a/confidential-data-hub/hub/src/secret/mod.rs +++ b/confidential-data-hub/hub/src/secret/mod.rs @@ -21,6 +21,7 @@ use self::layout::{envelope::EnvelopeSecret, vault::VaultSecret}; use kms::{Annotations, ProviderSettings}; use crate::hub::CDH_BASE_DIR; +use resource_uri::ResourceUri; pub use error::{Result, SecretError}; @@ -142,7 +143,7 @@ impl Secret { /// Otherwise, look for a local credential (provisioned to the fs /// at CDH startup). async fn get_kid(kid: &String) -> Result> { - if kid.starts_with("kbs://") { + if ResourceUri::try_from(kid.as_str()).is_ok() { let verification_key = kms::new_getter("kbs", ProviderSettings::default()) .await? .get_secret(kid, &Annotations::default()) diff --git a/confidential-data-hub/hub/src/storage/volume_type/blockdevice/mod.rs b/confidential-data-hub/hub/src/storage/volume_type/blockdevice/mod.rs index 74fd1749b..9070de191 100644 --- a/confidential-data-hub/hub/src/storage/volume_type/blockdevice/mod.rs +++ b/confidential-data-hub/hub/src/storage/volume_type/blockdevice/mod.rs @@ -28,6 +28,8 @@ use tokio::{ use tracing::{debug, info}; use zeroize::Zeroizing; +use resource_uri::ResourceUri; + #[derive(Serialize, Deserialize, Display, Debug, PartialEq, Eq)] #[serde(tag = "encryptionType")] pub enum BlockDeviceEncryptType { @@ -48,7 +50,7 @@ async fn get_plaintext_key(key_uri: &str) -> Result>> { .map_err(|source| BlockDeviceError::GetKeyFailed { source: source.into(), })? - } else if key_uri.starts_with("kbs://") { + } else if ResourceUri::try_from(key_uri).is_ok() { debug!("get key from kbs"); kms::new_getter("kbs", ProviderSettings::default()) .await From c7bcb8fee49da05d82477eb8132eed894de63556 Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Thu, 14 May 2026 12:43:17 -0700 Subject: [PATCH 4/5] keyprovider: make uri handling work with plugins The keyprovider does some manipulation of the resource uri. Update this so that it will work if the uri specifies a plugin. Signed-off-by: Tobin Feldman-Fitzthum --- .../coco_keyprovider/src/enc_mods/mod.rs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/attestation-agent/coco_keyprovider/src/enc_mods/mod.rs b/attestation-agent/coco_keyprovider/src/enc_mods/mod.rs index c4d158a90..295e12186 100644 --- a/attestation-agent/coco_keyprovider/src/enc_mods/mod.rs +++ b/attestation-agent/coco_keyprovider/src/enc_mods/mod.rs @@ -66,8 +66,6 @@ const HARD_CODED_KEYID: &str = "kbs:///default/test-key/1"; /// with this prefix. const DEFAULT_KEY_REPO_PATH: &str = "/default/image-kek"; -const KBS_RESOURCE_URL_PREFIX: &str = "kbs://"; - fn parse_input_params(input: &str) -> Result { let map: HashMap<&str, &str> = input .split("::") @@ -150,9 +148,23 @@ async fn generate_key_parameters(input_params: &InputParams) -> Result<(Vec, /// Normalize the given keyid into (kbs addr, key path), s.t. /// converting `kbs://...` or `../..` to `(, //)`. +/// Supports both `kbs://` and `kbs+://` schemes. fn normalize_path(keyid: &str) -> Result<(String, String)> { debug!("normalize key id {keyid}"); - let path = keyid.strip_prefix(KBS_RESOURCE_URL_PREFIX).unwrap_or(keyid); + + let path = if let Some(rest) = keyid.strip_prefix("kbs://") { + rest + } else if let Some(pos) = keyid.find("://") { + let scheme = &keyid[..pos]; + if scheme.starts_with("kbs+") { + &keyid[pos + 3..] + } else { + keyid + } + } else { + keyid + }; + let values: Vec<&str> = path.split('/').collect(); if values.len() == 4 { Ok(( @@ -164,6 +176,8 @@ fn normalize_path(keyid: &str) -> Result<(String, String)> { "Resource path {keyid} must follow one of the following formats: 'kbs://///' 'kbs://///' + 'kbs+://///' + 'kbs+://///' '///' '///' " @@ -195,7 +209,7 @@ pub async fn enc_optsdata_gen_anno( .await .context("generating key params")?; - let (kbs_addr, k_path) = normalize_path(&kid)?; + let (_kbs_addr, k_path) = normalize_path(&kid)?; let algorithm = input_params.algorithm; let encrypt_optsdata = crypto::encrypt(optsdata, &key, &iv, &algorithm) @@ -213,7 +227,7 @@ pub async fn enc_optsdata_gen_anno( let engine = base64::engine::general_purpose::STANDARD; let annotation = AnnotationPacket { - kid: format!("{KBS_RESOURCE_URL_PREFIX}{kbs_addr}/{k_path}"), + kid: kid.clone(), wrapped_data: engine.encode(encrypt_optsdata), iv: engine.encode(iv), wrap_type: algorithm.to_string(), @@ -229,6 +243,9 @@ mod tests { #[rstest] #[case("kbs://a/b/c/d", ("a", "b/c/d"))] #[case("kbs:///b/c/d", ("", "b/c/d"))] + #[case("kbs+pkcs11://a/b/c/d", ("a", "b/c/d"))] + #[case("kbs+pkcs11:///b/c/d", ("", "b/c/d"))] + #[case("kbs+myplugin:///repo/type/tag", ("", "repo/type/tag"))] #[case("a/b/c/d", ("a", "b/c/d"))] #[case("/b/c/d", ("", "b/c/d"))] fn test_normalize_keypath(#[case] input: &str, #[case] expected: (&str, &str)) { From 9e7932dc2aa24189d213da768b5c8f63238c4bfe Mon Sep 17 00:00:00 2001 From: Tobin Feldman-Fitzthum Date: Thu, 14 May 2026 12:44:28 -0700 Subject: [PATCH 5/5] image-rs: fixup resource uri scheme checks Allow plugins in the resource uri scheme Signed-off-by: Tobin Feldman-Fitzthum --- image-rs/src/resource/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/image-rs/src/resource/mod.rs b/image-rs/src/resource/mod.rs index 71462b3ed..b31b723d0 100644 --- a/image-rs/src/resource/mod.rs +++ b/image-rs/src/resource/mod.rs @@ -80,8 +80,9 @@ impl ResourceProvider { let url = url::Url::parse(&uri).map_err(|e| ResourceError::GetResource { source: anyhow!("Failed to parse resource uri: {:?}", e), })?; - match url.scheme() { - "kbs" => { + let scheme = url.scheme(); + match scheme { + s if s == "kbs" || s.starts_with("kbs+") => { #[cfg(feature = "kbs")] { self.secure_channel