diff --git a/Cargo.lock b/Cargo.lock index 98ab97af8..1a84509fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6565,6 +6565,23 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trustee-client" +version = "0.10.0" +dependencies = [ + "anyhow", + "base64", + "clap", + "config", + "env_logger", + "kbs_protocol", + "log", + "rstest", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 174b59390..4cf863272 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "confidential-data-hub/storage", "image-rs", "ocicrypt-rs", + "trustee-client", ] [workspace.dependencies] diff --git a/README.md b/README.md index dbf5efc81..92e7077c0 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ Confidential Data Hub. [coco-keyprovider](attestation-agent/coco_keyprovider/) CoCo Keyprovider. Used to encrypt the container images. +[trustee-client](trustee-client) +A simple client to fetch secrets from Trustee + ## Tools [secret-cli](confidential-data-hub/secret) diff --git a/trustee-client/Cargo.toml b/trustee-client/Cargo.toml new file mode 100644 index 000000000..ce5984e3e --- /dev/null +++ b/trustee-client/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "trustee-client" +version = "0.10.0" +edition = "2021" + +[dependencies] +anyhow.workspace = true +base64.workspace = true +clap = { workspace = true, features = ["derive"] } +config.workspace = true +tokio = { workspace = true, features = ["macros", "rt"] } +env_logger.workspace = true +log.workspace = true +kbs_protocol = { path = "../attestation-agent/kbs_protocol", features=["openssl", "all-attesters", "background_check", "passport" ] } +rstest.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true diff --git a/trustee-client/README.md b/trustee-client/README.md new file mode 100644 index 000000000..84cf495a8 --- /dev/null +++ b/trustee-client/README.md @@ -0,0 +1,29 @@ +Trustee client -- a simple tool to attest and fetch secrets from Trustee + +Trustee client is using attestation-agent's kbs_protocol client and +attesters to gather hardware-based confidential-computing evidence +and send it over to Trustee. + +Trustee client is a part of [confidential-containers](https://github.com/confidential-containers) +[guest-components](https://github.com/confidential-containers/guest-components) +project but can be used for confidential VMs as well. + + + +Build with: + cargo build [--no-default-features] + + +Configuration file: +trustee-client configuration must contain the trustee (server) URL. +Possibly it can also contain the trustee https certificate, either +as a string in the configuration file or in another file (but not both). + +A configuration file path is an optional argument to trustee-client +If no configuration file path is provided /etc/trustee-client.conf is used. + +Run: + $ trustee-client [--config-file ] get-resource --path + +Example: + $ trustee-client get-resource --path default/keys/dummy diff --git a/trustee-client/src/main.rs b/trustee-client/src/main.rs new file mode 100644 index 000000000..dfb30193c --- /dev/null +++ b/trustee-client/src/main.rs @@ -0,0 +1,83 @@ +// Copyright (c) 2023 by Alibaba. +// Copyright (c) 2024 Red Hat, Inc +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +//! A simple client for fetching resources from Trustee. + +use anyhow::Result; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use clap::{Parser, Subcommand}; +use log::debug; + +use kbs_protocol::evidence_provider::NativeEvidenceProvider; +use kbs_protocol::KbsClientBuilder; +use kbs_protocol::KbsClientCapabilities; + +pub mod tcconfig; +use tcconfig::TrusteeClientConfig; + +#[derive(Parser)] +struct Cli { + /// A configuration file for trustee-client (default is /etc/trustee-client.conf) + #[clap(long, value_parser)] + config_file: Option, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Get confidential resource + #[clap(arg_required_else_help = true)] + GetResource { + /// KBS Resource path, e.g my_repo/resource_type/123abc + /// Document: https://github.com/confidential-containers/attestation-agent/blob/main/docs/KBS_URI.md + #[clap(long, value_parser)] + path: String, + }, +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let cli = Cli::parse(); + + let tcc = TrusteeClientConfig::new(cli.config_file)?; + + let url = tcc.url.clone(); + let cert = tcc.get_cert(); + + debug!("url {}", url); + debug!("cert {:?}", cert); + + let evidence_provider = Box::new(NativeEvidenceProvider::new()?); + + // build a kbs_protocol client with evidence_provider + let mut client_builder = KbsClientBuilder::with_evidence_provider(evidence_provider, &url); + + // if a certificate is given, use it + if let Some(c) = cert { + client_builder = client_builder.add_kbs_cert(&c) + } + + // Build the client. This client is used throughout the program + let mut client = client_builder.build()?; + + match cli.command { + Commands::GetResource { path } => { + // get resource + let resource_uri = format!("kbs:///{}", path); + let resource_bytes = client + .get_resource(serde_json::from_str(&format!("\"{resource_uri}\""))?) + .await?; + + println!("{}", STANDARD.encode(resource_bytes)); + } + }; + + Ok(()) +} diff --git a/trustee-client/src/tcconfig.rs b/trustee-client/src/tcconfig.rs new file mode 100644 index 000000000..e45efaf50 --- /dev/null +++ b/trustee-client/src/tcconfig.rs @@ -0,0 +1,148 @@ +// Copyright (c) 2024 Alibaba Cloud +// Copyright (c) 2024 Red Hat, Inc +// +// SPDX-License-Identifier: Apache-2.0 +// + +use anyhow::{bail, Context, Result}; +use config::{Config, File, FileFormat}; +use log::{debug, info}; +use serde::Deserialize; +use std::fs; +use std::path::Path; + +const DEFAULT_TCCONFIG_FILE_PATH: &str = "/etc/trustee-client.conf"; +#[derive(Deserialize, Debug, PartialEq)] +pub struct TrusteeClientConfig { + /// URL Address of Trustee. + pub url: String, + + /// https:// certificate for Trustee as a string + pub cert: Option, + + /// https:// certificate for Trustee in a cert_file + pub cert_file: Option, +} + +impl TrusteeClientConfig { + pub fn new(path_arg: Option) -> Result { + let path = match path_arg { + Some(f) => f, + None => DEFAULT_TCCONFIG_FILE_PATH.to_string(), + }; + + debug!("Using configuration file {path}"); + if !Path::new(&path).exists() { + bail!("Config file {path} not found.") + } + + let c = Config::builder() + .add_source(File::new(&path as &str, FileFormat::Toml)) + .build()?; + + let tcc: TrusteeClientConfig = + c.try_deserialize().context("failed to parse config_file")?; + + Ok(tcc) + } + + // get the certificate from the configuration file, if exists + // If cert does not exists but cert_file does, read cert from cert_file + pub fn get_cert(&self) -> Option { + debug!( + "cert={:?} cert_file={:?}", + self.cert.clone(), + self.cert_file.clone() + ); + if let Some(c) = &self.cert { + debug!("Some cert {}", c.clone()); + return Some(c.clone()); + } + + if let Some(cf) = &self.cert_file { + debug!("Some cert_file {}", cf.clone()); + let newcert = fs::read_to_string(cf.clone()).ok()?; + return Some(newcert); + } + None + } + + pub fn is_valid(&self) -> Result<()> { + if self.cert.is_some() && self.cert_file.is_some() { + bail!("Please provide only one of 'cert' and 'cert_file'"); + } + if self.url.starts_with("https://") && self.cert.is_none() { + info!("An https:// URL is used but no certificate is provided"); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::TrusteeClientConfig; + use rstest::rstest; + use std::fs::File as fsfile; + use std::io::Write; + + #[rstest] + #[case::good_http( + 1, + r#" +url = "http://localhost:50000" +"#, + Some(TrusteeClientConfig { + url : "http://localhost:50000".to_string(), + cert : None, + cert_file: None, + }))] + #[case::good_https_with_cert( + 2, + r#" +url = "https://localhost:50000" +cert = "Trustee Certificate" +"#, + Some(TrusteeClientConfig { + url : "https://localhost:50000".to_string(), + cert : Some("Trustee Certificate".to_string()), + cert_file: None, + }) + )] + #[case::good_https_with_cert_file( + 3, + r#" +url = "https://localhost:50000" +cert_file = "/tmp/test_cert_file.conf" +"#, + Some(TrusteeClientConfig { + url : "https://localhost:50000".to_string(), + cert : None, + cert_file: Some("/tmp/test_cert_file.conf".to_string()), + }) + )] + #[case::bad_empty(4, r#""#, None)] + #[case::bad_nourl_only_cert_file( + 5, + r#" +cert_file = "/tmp/test_cert_file" +"#, + None + )] + fn check_trustee_config_file( + #[case] n: i32, + #[case] config: &str, + #[case] expected: Option, + ) { + let testfilename = format!("/tmp/tccconfigtest{n}.conf"); + { + let mut f = fsfile::create(testfilename.clone()).unwrap(); + f.write_all(config.as_bytes()).unwrap(); + f.sync_all().unwrap(); + } // close f + let tcc = TrusteeClientConfig::new(Some(testfilename)); + match expected { + Some(cfg) => assert_eq!(cfg, tcc.unwrap()), + None => assert!(tcc.is_err()), + } + } +}