Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions attestation-agent/coco_keyprovider/src/enc_mods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InputParams> {
let map: HashMap<&str, &str> = input
.split("::")
Expand Down Expand Up @@ -150,9 +148,23 @@ async fn generate_key_parameters(input_params: &InputParams) -> Result<(Vec<u8>,

/// Normalize the given keyid into (kbs addr, key path), s.t.
/// converting `kbs://...` or `../..` to `(<kbs-addr>, <repository>/<type>/<tag>)`.
/// Supports both `kbs://` and `kbs+<plugin>://` 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
};
Comment on lines +155 to +166
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can import ResourceUri and parse, then extract repository, .. etc

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has slightly different behavior than the resource Uri parsing. It allows other schemes and even kids without any scheme. So I don't think it gives us much to use the resource uri crate here.


let values: Vec<&str> = path.split('/').collect();
if values.len() == 4 {
Ok((
Expand All @@ -164,6 +176,8 @@ fn normalize_path(keyid: &str) -> Result<(String, String)> {
"Resource path {keyid} must follow one of the following formats:
'kbs:///<repository>/<type>/<tag>'
'kbs://<kbs-addr>/<repository>/<type>/<tag>'
'kbs+<plugin>:///<repository>/<type>/<tag>'
'kbs+<plugin>://<kbs-addr>/<repository>/<type>/<tag>'
'<kbs-addr>/<repository>/<type>/<tag>'
'/<repository>/<type>/<tag>'
"
Expand Down Expand Up @@ -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)
Expand All @@ -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(),
Expand All @@ -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)) {
Expand Down
96 changes: 84 additions & 12 deletions attestation-agent/deps/resource_uri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@ const RESOURCE_ID_ERROR_INFO: &str =
"invalid kbs resource uri, should be kbs://<addr-of-kbs>/<repo>/<type>/<tag>";

const SCHEME: &str = "kbs";
const DEFAULT_PLUGIN: &str = "resource";

/// Resource Id document <https://github.com/confidential-containers/guest-components/blob/main/attestation-agent/docs/KBS_URI.md>
/// Resource Id document <https://github.com/confidential-containers/guest-components/blob/main/attestation-agent/docs/RESOURCE_URI.md>
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceUri {
pub kbs_addr: String,
pub repository: String,
pub r#type: String,
pub tag: String,
Comment on lines 23 to 25
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that due to #1468 (comment), we could combine these fields into a Vec<String> for any length paths? wdyt?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am open to this but I think we should do another PR for handling the variable length stuff.

pub query: Option<String>,
pub plugin: Option<String>,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can directly set this as String, and give a default resource if none is given

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes it a little tricky to reconstruct the URI in the whole_uri method. wdyt?

}

impl TryFrom<&str> for ResourceUri {
Expand All @@ -47,9 +49,19 @@ impl TryFrom<url::Url> 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+<plugin>"),
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit

let plugin = match scheme.as_str() {
    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+<plugin>"),
};


if value.path().is_empty() {
return Err(RESOURCE_ID_ERROR_INFO);
Expand All @@ -64,6 +76,7 @@ impl TryFrom<url::Url> 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)
Expand Down Expand Up @@ -106,6 +119,7 @@ impl ResourceUri {
r#type: values[2].into(),
tag: values[3].into(),
query: None,
plugin: None,
})
} else {
bail!(
Expand All @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -158,27 +182,58 @@ mod tests {
use rstest::rstest;

#[rstest]
#[case("kbs:///alice/cosign-key/213", "alice", "cosign-key", "213", None)]
#[case(
"kbs:///plugin/plugname/resourcename?param1=value1&param2=value2",
"plugin",
"plugname",
"resourcename",
Some("param1=value1&param2=value2")
"kbs:///alice/cosign-key/213",
"alice",
"cosign-key",
"213",
None,
None
)]
#[case(
"kbs:///repo/type/tag?param1=value1&param2=value2",
"repo",
"type",
"tag",
Some("param1=value1&param2=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,
#[case] repository: &str,
#[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
Expand All @@ -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));
}
}
36 changes: 0 additions & 36 deletions attestation-agent/docs/KBS_URI.md

This file was deleted.

76 changes: 76 additions & 0 deletions attestation-agent/docs/RESOURCE_URI.md
Original file line number Diff line number Diff line change
@@ -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+<plugin>://<kbs_host>:<kbs_port>/<repository>/<type>/<tag>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this design

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The asymmetry here is that with kbs+<plugin>:// callers need to know the resource is provided by <plugin> but with the resource plugin, they have no way to say the resources comes from vault.

How the plugin URIs can be supported with api-server-rest queries?

Copy link
Copy Markdown
Member

@Xynnn007 Xynnn007 May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vault is not a plugin level conception, but a resource plugin level. Thus it's expected that the caller does not know that.

for ASR, currently is 127.0.0.1:8006/cdh/resource/.... we could generize it as 127.0.0.1:8006/cdh/<plugin-in-name>/... for kbs+<plugin-in-name>:///...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also that the caller doesn't get to request a concrete backing store for their resource is (say, vault or filesystem), this is up to the KBS, no?

I also like this URI scheme 👍

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vault is not a plugin level conception, but a resource plugin level.

yup, my comment was just that the implementation detail is visible to the user and they don't necessarily understand: why kbs+pcks11:// is needed but kbs+vault:// is not.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the pkcs11 apis are different from the vaults. see https://github.com/confidential-containers/trustee/blob/main/kbs/src/plugins/implementations/pkcs11.rs#L135 for example wrap-key

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, also worth noting that the resource plugin only supports one storage backend anyway.

```

### 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.
Comment on lines +54 to +58
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's resource plugin-specific and no requirements on other plugins

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I was also thinking whether it is a good idea to require the same path segment for every plugin, for some this hierarchy might just not make any sense. but it keeps implementation complexity at bay for now and that requirement can be relaxed later once it becomes a nuisance

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - and there is already some implementations that do not follow this, e.g. pkcs11

Copy link
Copy Markdown
Member Author

@fitzthum fitzthum May 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need to clarify this. Currently you cannot access the pkcs11 plugin via the resource uri, which is essentially a bug imo. I am open to allowing arbitrary length requests, like we do with rv uris, but that is not how the resource uri works today.


### 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`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw the original resource URI could also support query string, like kbs+plugin1://a/b/c?d=e


## Mapping

The resource URI is transformed into an HTTP(S) request.
Specifically, the request will be made to `http://<kbs_host>:<kbs_port>/kbs/v0/<plugin>/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).
8 changes: 6 additions & 2 deletions attestation-agent/kbs_protocol/src/client/rcar_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,12 @@ impl KbsClient<Box<dyn EvidenceProvider>> {
impl KbsClientCapabilities for KbsClient<Box<dyn EvidenceProvider>> {
async fn get_resource(&mut self, resource_uri: ResourceUri) -> Result<Vec<u8>> {
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}");
Expand Down
8 changes: 6 additions & 2 deletions attestation-agent/kbs_protocol/src/client/token_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ impl KbsClient<Box<dyn TokenProvider>> {
impl KbsClientCapabilities for KbsClient<Box<dyn TokenProvider>> {
async fn get_resource(&mut self, resource_uri: ResourceUri) -> Result<Vec<u8>> {
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}");
Expand Down
Loading
Loading