From 7ca32131293d1a26a9bb813432b695677bbaebe4 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 14 Aug 2023 12:02:30 +0000 Subject: [PATCH 1/3] Add delegated identity API support to spire-api package Signed-off-by: Eitan Yarmush --- .github/workflows/ci.yml | 1 + scripts/agent.conf | 32 ++ scripts/run-spire.sh | 14 + spire-api/Cargo.toml | 17 +- spire-api/src/agent/delegated_identity.rs | 293 ++++++++++++++++++ spire-api/src/agent/mod.rs | 13 + spire-api/src/lib.rs | 9 + spire-api/src/selectors.rs | 128 ++++++++ .../delegated_identity_api_client_test.rs | 57 +++- 9 files changed, 559 insertions(+), 5 deletions(-) create mode 100644 scripts/agent.conf create mode 100644 spire-api/src/agent/delegated_identity.rs create mode 100644 spire-api/src/agent/mod.rs create mode 100644 spire-api/src/selectors.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 673dc35..8653b45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: runs-on: ubuntu-latest env: SPIFFE_ENDPOINT_SOCKET: unix:/tmp/spire-agent/public/api.sock + SPIFFE_ADMIN_ENDPOINT_SOCKET: unix:/tmp/spire-agent/admin/api.sock needs: build steps: - name: Check out code diff --git a/scripts/agent.conf b/scripts/agent.conf new file mode 100644 index 0000000..0a891b8 --- /dev/null +++ b/scripts/agent.conf @@ -0,0 +1,32 @@ +agent { + data_dir = "./data/agent" + log_level = "DEBUG" + trust_domain = "example.org" + server_address = "localhost" + server_port = 8081 + + # Insecure bootstrap is NOT appropriate for production use but is ok for + # simple testing/evaluation purposes. + insecure_bootstrap = true + + admin_socket_path = "$STRIPPED_SPIFFE_ADMIN_ENDPOINT_SOCKET" + authorized_delegates = [ + "spiffe://example.org/myservice", + ] +} + +plugins { + KeyManager "disk" { + plugin_data { + directory = "./data/agent" + } + } + + NodeAttestor "join_token" { + plugin_data {} + } + + WorkloadAttestor "unix" { + plugin_data {} + } +} \ No newline at end of file diff --git a/scripts/run-spire.sh b/scripts/run-spire.sh index 39d5fa4..d0b228e 100755 --- a/scripts/run-spire.sh +++ b/scripts/run-spire.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + # Constants spire_version="1.7.1" spire_folder="spire-${spire_version}" @@ -35,6 +37,9 @@ mkdir -p /tmp/spire-server bin/spire-server run -config conf/server/server.conf > "${spire_server_log_file}" 2>&1 & wait_for_service "bin/spire-server healthcheck" "SPIRE Server" "${spire_server_log_file}" +export STRIPPED_SPIFFE_ADMIN_ENDPOINT_SOCKET=$(echo $SPIFFE_ADMIN_ENDPOINT_SOCKET| cut -c6-) +cat $SCRIPT_DIR/agent.conf | envsubst > "conf/agent/agent.conf" + # Run the SPIRE agent with the joint token bin/spire-server token generate -spiffeID ${agent_id} > token cut -d ' ' -f 2 token > token_stripped @@ -48,4 +53,13 @@ for service in "myservice" "myservice2"; do sleep 10 # Derived from the default Agent sync interval done + +uid=$(id -u) +# The UID in the test has to match this, so take the current UID and add 1 +uid_plus_one=$((uid + 1)) +# Register a different UID with the SPIFFE ID "spiffe://example.org/different-process" with a TTL of 5 seconds +bin/spire-server entry create -parentID ${agent_id} -spiffeID spiffe://example.org/different-process -selector unix:uid:${uid_plus_one} -ttl 5 +sleep 10 + + popd diff --git a/spire-api/Cargo.toml b/spire-api/Cargo.toml index 6347ba8..9a5b623 100644 --- a/spire-api/Cargo.toml +++ b/spire-api/Cargo.toml @@ -15,10 +15,25 @@ categories = ["cryptography"] keywords = ["SPIFFE", "SPIRE"] [dependencies] -spiffe = { version = "0.3.1", path = "../spiffe" } +bytes = { version = "1", features = ["serde"] } +spiffe = { path = "../spiffe" } tonic = { version = "0.9", default-features = false, features = ["prost", "codegen", "transport"]} prost = { version = "0.11"} prost-types = {version = "0.11"} +tokio = { "version" = "1", features = ["net", "test-util"]} +tokio-stream = "0.1" +tower = { version = "0.4", features = ["util"] } +thiserror = "1.0" +url = "2.2" +asn1 = { package = "simple_asn1", version = "0.6" } +x509-parser = "0.15" +pkcs8 = "0.10" +jsonwebtoken = "8.3" +jsonwebkey = { version = "0.3", features = ["jsonwebtoken", "jwt-convert"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +zeroize = { version = "1.6", features = ["zeroize_derive"] } +time = "0.3" [dev-dependencies] diff --git a/spire-api/src/agent/delegated_identity.rs b/spire-api/src/agent/delegated_identity.rs new file mode 100644 index 0000000..c3a5c77 --- /dev/null +++ b/spire-api/src/agent/delegated_identity.rs @@ -0,0 +1,293 @@ +//! This module provides an API surface to interact with the DelegateIdentity API. +//! The protobuf definition can be found [here](https://github.com/spiffe/spire-api-sdk/blob/main/proto/spire/api/agent/delegatedidentity/v1/delegatedidentity.proto) +//! +//! More information on it's usage can be found in the [SPIFFE docs](https://spiffe.io/docs/latest/deploying/spire_agent/#delegated-identity-api) +//! +//! Most importantly, this API cannot be used over the standard endpoint, it must be used over the admin socket. +//! The admin socket can be configured in the SPIRE agent configuration document. + +use spiffe::bundle::x509::{X509Bundle, X509BundleSet}; +use crate::proto::spire::api::agent::delegatedidentity::v1::{ + delegated_identity_client::DelegatedIdentityClient as DelegatedIdentityApiClient, + SubscribeToX509BundlesRequest, SubscribeToX509BundlesResponse, SubscribeToX509sviDsRequest, + SubscribeToX509sviDsResponse, +}; +use spiffe::spiffe_id::TrustDomain; +use spiffe::svid::x509::X509Svid; +use spiffe::workload_api::address::validate_socket_path; +use tokio_stream::{Stream, StreamExt}; + +use crate::selectors::Selector; +use spiffe::workload_api::client::{ClientError, DEFAULT_SVID}; +use std::convert::{Into, TryFrom}; +use tokio::net::UnixStream; +use tonic::transport::{Endpoint, Uri}; +use tower::service_fn; + + +/// Name of the environment variable that holds the default socket endpoint path. +pub const ADMIN_SOCKET_ENV: &str = "SPIFFE_ADMIN_ENDPOINT_SOCKET"; + +/// Gets the endpoint socket endpoint path from the environment variable `SPIFFE_ENDPOINT_SOCKET`, +/// as described in [SPIFFE standard](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_Endpoint.md#4-locating-the-endpoint). +pub fn get_admin_socket_path() -> Option { + match std::env::var(ADMIN_SOCKET_ENV) { + Ok(addr) => Some(addr), + Err(_) => None, + } +} + +/// Impl for DelegatedIdentity API +#[derive(Debug, Clone)] +pub struct DelegatedIdentityClient { + client: DelegatedIdentityApiClient, +} + +/// Constructors +impl DelegatedIdentityClient { + const UNIX_PREFIX: &'static str = "unix:"; + const TONIC_DEFAULT_URI: &'static str = "http://[::]:50051"; + + /// Creates a new instance of `DelegatedIdentityClient` by connecting to the specified socket path. + /// + /// # Arguments + /// + /// * `path` - The path to the UNIX domain socket, which can optionally start with "unix:". + /// + /// # Returns + /// + /// * `Result` - Returns an instance of `DelegatedIdentityClient` if successful, otherwise returns an error. + /// + /// # Errors + /// + /// This function will return an error if the provided socket path is invalid or if there are issues connecting. + pub async fn new_from_path(path: &str) -> Result { + validate_socket_path(path)?; + + // Strip the 'unix:' prefix for tonic compatibility. + let stripped_path = path + .strip_prefix(Self::UNIX_PREFIX) + .unwrap_or(path) + .to_string(); + + let channel = Endpoint::try_from(Self::TONIC_DEFAULT_URI)? + .connect_with_connector(service_fn(move |_: Uri| { + // Connect to the UDS socket using the modified path. + UnixStream::connect(stripped_path.clone()) + })) + .await?; + + Ok(DelegatedIdentityClient { + client: DelegatedIdentityApiClient::new(channel), + }) + } + + /// Creates a new `DelegatedIdentityClient` using the default socket endpoint address. + /// + /// Requires that the environment variable `SPIFFE_ENDPOINT_SOCKET` be set with + /// the path to the Workload API endpoint socket. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if environment variable is not set or if + /// the provided socket path is not valid. + pub async fn default() -> Result { + let socket_path = match get_admin_socket_path() { + None => return Err(ClientError::MissingEndpointSocketPath), + Some(s) => s, + }; + Self::new_from_path(socket_path.as_str()).await + } + + /// Constructs a new `DelegatedIdentityClient` using the provided Tonic transport channel. + /// + /// # Arguments + /// + /// * `conn`: A `tonic::transport::Channel` used for gRPC communication. + /// + /// # Returns + /// + /// A `Result` containing a `DelegatedIdentityClient` if successful, or a `ClientError` if an error occurs. + pub fn new(conn: tonic::transport::Channel) -> Result { + Ok(DelegatedIdentityClient { + client: DelegatedIdentityApiClient::new(conn), + }) + } +} + +impl DelegatedIdentityClient { + + /// Fetches a single X509 SPIFFE Verifiable Identity Document (SVID). + /// + /// This method connects to the SPIFFE Workload API and returns the first X509 SVID in the response. + /// + /// # Arguments + /// + /// * `selectors` - A list of selectors to filter the stream of [`X509Svid`] updates. + /// + /// # Returns + /// + /// On success, it returns a valid [`X509Svid`] which represents the parsed SVID. + /// If the fetch operation or the parsing fails, it returns a [`ClientError`]. + /// + /// # Errors + /// + /// Returns [`ClientError`] if the gRPC call fails or if the SVID could not be parsed from the gRPC response. + pub async fn fetch_x509_svid( + mut self, + selectors: Vec, + ) -> Result { + let request = SubscribeToX509sviDsRequest { + selectors: selectors.into_iter().map(|s| s.into()).collect(), + }; + + self.client + .subscribe_to_x509svi_ds(request) + .await? + .into_inner() + .message() + .await? + .ok_or(ClientError::EmptyResponse) + .and_then(DelegatedIdentityClient::parse_x509_svid_from_grpc_response) + } + + /// Watches the stream of [`X509Svid`] updates. + /// + /// This function establishes a stream with the Workload API to continuously receive updates for the [`X509Svid`]. + /// The returned stream can be used to asynchronously yield new `X509Svid` updates as they become available. + /// + /// # Arguments + /// + /// * `selectors` - A list of selectors to filter the stream of [`X509Svid`] updates. + /// + /// # Returns + /// + /// Returns a stream of `Result`. Each item represents an updated [`X509Svid`] or an error if + /// there was a problem processing an update from the stream. + /// + /// # Errors + /// + /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// + /// * There's an issue connecting to the Workload API. + /// * An error occurs while setting up the stream. + /// + /// Individual stream items might also be errors if there's an issue processing the response for a specific update. + pub async fn stream_x509_svids( + mut self, + selectors: Vec, + ) -> Result>, ClientError> { + let request = SubscribeToX509sviDsRequest { + selectors: selectors.into_iter().map(|s| s.into()).collect(), + }; + + let response: tonic::Response> = + self.client.subscribe_to_x509svi_ds(request).await?; + + let stream = response.into_inner().map(|message| { + message + .map_err(ClientError::from) + .and_then(DelegatedIdentityClient::parse_x509_svid_from_grpc_response) + }); + + Ok(stream) + } + + /// Fetches [`X509BundleSet`], that is a set of [`X509Bundle`] keyed by the trust domain to which they belong. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// there is a problem processing the response. + pub async fn fetch_x509_bundles(mut self) -> Result { + let request = SubscribeToX509BundlesRequest::default(); + + let response: tonic::Response> = + self.client.subscribe_to_x509_bundles(request).await?; + let initial = response.into_inner().message().await?; + DelegatedIdentityClient::parse_x509_bundle_set_from_grpc_response( + initial.unwrap_or_default(), + ) + } + + /// Watches the stream of [`X509Bundle`] updates. + /// + /// This function establishes a stream with the Workload API to continuously receive updates for the [`X509Bundle`]. + /// The returned stream can be used to asynchronously yield new `X509Bundle` updates as they become available. + /// + /// # Returns + /// + /// Returns a stream of `Result`. Each item represents an updated [`X509Bundle`] or an error if + /// there was a problem processing an update from the stream. + /// + /// # Errors + /// + /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// + /// * There's an issue connecting to the Admin API. + /// * An error occurs while setting up the stream. + /// + /// Individual stream items might also be errors if there's an issue processing the response for a specific update. + pub async fn stream_x509_bundles( + mut self, + ) -> Result>, ClientError> { + let request = SubscribeToX509BundlesRequest::default(); + + let response: tonic::Response> = + self.client.subscribe_to_x509_bundles(request).await?; + + let stream = response.into_inner().map(|message| { + message + .map_err(ClientError::from) + .and_then(DelegatedIdentityClient::parse_x509_bundle_set_from_grpc_response) + }); + + Ok(stream) + } +} + +impl DelegatedIdentityClient { + fn parse_x509_svid_from_grpc_response( + response: SubscribeToX509sviDsResponse, + ) -> Result { + let svid = match response.x509_svids.get(DEFAULT_SVID) { + None => return Err(ClientError::EmptyResponse), + Some(s) => s, + }; + + // OPTIMIZE THIS + let mut total_length = 0; + svid.x509_svid + .as_ref() + .ok_or(ClientError::EmptyResponse)? + .cert_chain + .iter() + .for_each(|c| total_length += c.len()); + let mut cert_chain = bytes::BytesMut::with_capacity(total_length); + svid.x509_svid + .as_ref() + .ok_or(ClientError::EmptyResponse)? + .cert_chain + .iter() + .for_each(|c| cert_chain.extend(c)); + + X509Svid::parse_from_der(cert_chain.as_ref(), svid.x509_svid_key.as_ref()) + .map_err(|e| e.into()) + } + + fn parse_x509_bundle_set_from_grpc_response( + response: SubscribeToX509BundlesResponse, + ) -> Result { + let mut bundle_set = X509BundleSet::new(); + + for (td, bundle) in response.ca_certificates.into_iter() { + let trust_domain = TrustDomain::try_from(td)?; + + bundle_set.add_bundle( + X509Bundle::parse_from_der(trust_domain, &bundle) + .map_err(ClientError::InvalidX509Bundle)?, + ); + } + Ok(bundle_set) + } +} diff --git a/spire-api/src/agent/mod.rs b/spire-api/src/agent/mod.rs new file mode 100644 index 0000000..0d6c7c0 --- /dev/null +++ b/spire-api/src/agent/mod.rs @@ -0,0 +1,13 @@ +//! Agent API +//! +//! The agent API consists of APIs: +//! - delegated_identity +//! - debug (not implemented) +//! +//! # Note +//! Both of these APIs must be accesed via the admin_socket_path which can be set +//! in the [agent configuration file](https://spiffe.io/docs/latest/deploying/spire_agent/#agent-configuration-file). +//! +//! + +pub mod delegated_identity; \ No newline at end of file diff --git a/spire-api/src/lib.rs b/spire-api/src/lib.rs index 95f1441..afb9a58 100644 --- a/spire-api/src/lib.rs +++ b/spire-api/src/lib.rs @@ -1 +1,10 @@ +#![deny(missing_docs)] +#![warn(missing_debug_implementations)] +// #![warn(rust_2018_idioms)] + +//! This library provides functions to interact with the SPIRE GRPC APIs as defined in the [SDK](https://github.com/spiffe/spire-api-sdk). + mod proto; + +pub mod agent; +pub mod selectors; diff --git a/spire-api/src/selectors.rs b/spire-api/src/selectors.rs new file mode 100644 index 0000000..6e80035 --- /dev/null +++ b/spire-api/src/selectors.rs @@ -0,0 +1,128 @@ +//! Selectors which conform to SPIRE standards. +//! +use crate::proto::spire::api::types::Selector as SpiffeSelector; + +const K8S_TYPE: &str = "k8s"; +const UNIX_TYPE: &str = "unix"; + +/// User-facing version of underlying proto selector type +impl From for SpiffeSelector { + fn from(s: Selector) -> SpiffeSelector { + match s { + Selector::K8s(k8s_selector) => SpiffeSelector { + r#type: K8S_TYPE.to_string(), + value: k8s_selector.into(), + }, + Selector::Unix(unix_selector) => SpiffeSelector { + r#type: UNIX_TYPE.to_string(), + value: unix_selector.into(), + }, + Selector::Generic((k, v)) => SpiffeSelector { + r#type: k, + value: v, + }, + } + } +} + +#[derive(Debug, Clone)] +/// Selector represents a SPIFFE ID selector. +pub enum Selector { + /// K8s represents a SPIFFE ID selector. + K8s(K8s), + /// Selector represents a SPIFFE ID selector. + Unix(Unix), + /// Selector represents a SPIFFE ID selector. + Generic((String, String)), +} + +const K8S_SA_TYPE: &str = "sa"; +const K8S_NS_TYPE: &str = "ns"; + +impl From for String { + fn from(k: K8s) -> String { + match k { + K8s::ServiceAccount(s) => format!("{}:{}", K8S_SA_TYPE, s), + K8s::Namespace(s) => format!("{}:{}", K8S_NS_TYPE, s), + } + } +} + +#[derive(Debug, Clone)] +/// K8s is a helper type to create a SPIFFE ID selector for Kubernetes. +pub enum K8s { + /// ServiceAccount represents the SPIFFE ID selector for a Kubernetes service account. + ServiceAccount(String), + /// Namespace represents the SPIFFE ID selector for a Kubernetes namespace. + Namespace(String), +} + +const UNIX_PID_TYPE: &str = "pid"; +const UNIX_GID_TYPE: &str = "gid"; +const UNIX_UID_TYPE: &str = "uid"; + +impl From for String { + fn from(value: Unix) -> Self { + match value { + Unix::Pid(s) => format!("{}:{}", UNIX_PID_TYPE, s), + Unix::Gid(s) => format!("{}:{}", UNIX_GID_TYPE, s), + Unix::Uid(s) => format!("{}:{}", UNIX_UID_TYPE, s), + } + } +} + +#[derive(Debug, Clone)] +/// K8s is a helper type to create a SPIFFE ID unix process constructs. +pub enum Unix { + /// PID represents the SPIFFE ID selector for a process ID. + Pid(u16), + /// GID represents the SPIFFE ID selector for a group ID. + Gid(u16), + /// UID represents the SPIFFE ID selector for a User ID. + Uid(u16), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_k8s_sa_selector() { + let selector = Selector::K8s(K8s::ServiceAccount("foo".to_string())); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, K8S_TYPE); + assert_eq!(spiffe_selector.value, "sa:foo"); + } + + #[test] + fn test_k8s_ns_selector() { + let selector = Selector::K8s(K8s::Namespace("foo".to_string())); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, K8S_TYPE); + assert_eq!(spiffe_selector.value, "ns:foo"); + } + + #[test] + fn test_unix_pid_selector() { + let selector = Selector::Unix(Unix::Pid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "pid:500"); + } + + #[test] + fn test_unix_gid_selector() { + let selector = Selector::Unix(Unix::Gid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "gid:500"); + } + + #[test] + fn test_unix_uid_selector() { + let selector = Selector::Unix(Unix::Uid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "uid:500"); + } +} \ No newline at end of file diff --git a/spire-api/tests/delegated_identity_api_client_test.rs b/spire-api/tests/delegated_identity_api_client_test.rs index e1ed519..70d0924 100644 --- a/spire-api/tests/delegated_identity_api_client_test.rs +++ b/spire-api/tests/delegated_identity_api_client_test.rs @@ -1,10 +1,59 @@ -// These tests requires a running SPIRE server and agent (see `scripts/run-spire.sh`). +// These tests requires a running SPIRE server and agent with workloads registered (see script `ci.sh`). +// In addition it requires the admin endpoint to be exposed, and the running user to registered +// as an authorized_delegate. #[cfg(feature = "integration-tests")] mod integration_tests { + use spire_api::selectors; + use std::process::Command; - #[test] - fn dummy_test() { - assert!(true); + fn get_uid() -> u16 { + let mut uid = String::from_utf8( + Command::new("id") + .arg("-u") + .output() + .expect("could not get UID") + .stdout, + ) + .expect("could not parse to string"); + uid.truncate(uid.len() - 1); + uid.parse().expect("could not parse uid to number") + } + + #[tokio::test] + async fn fetch_delegate_svid() { + // let uid: u16 = std::env::var("UID").expect("UID env var not present").parse().expect("could not parse uid to number"); + let client = spire_api::agent::delegated_identity::DelegatedIdentityClient::default() + .await + .expect("failed to create client"); + let response: spiffe::svid::x509::X509Svid = client + .fetch_x509_svid(vec![selectors::Selector::Unix(selectors::Unix::Uid(get_uid() + 1))]) + .await + .expect("Failed to fetch delegate SVID"); + // Not checking the chain as the root is generated by spire. + // In the future we could look in the downloaded spire directory for the keys. + assert_eq!(response.cert_chain().len(), 1); + assert_eq!( + response.spiffe_id().to_string(), + "spiffe://example.org/different-process" + ); + } + + #[tokio::test] + async fn fetch_trust_bundles() { + // let uid: u16 = std::env::var("UID").expect("UID env var not present").parse().expect("could not parse uid to number"); + let client = spire_api::agent::delegated_identity::DelegatedIdentityClient::default() + .await + .expect("failed to create client"); + let response = client + .fetch_x509_bundles() + .await + .expect("Failed to fetch trust bundles"); + response + .get_bundle( + &spiffe::spiffe_id::TrustDomain::new("example.org".as_ref()) + .expect("Failed to parse trust domain ="), + ) + .expect("Failed to get bundle"); } } From a93c14e965dd4f356cbf539977df95a83ddbb611 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 14 Aug 2023 16:00:08 +0000 Subject: [PATCH 2/3] cargo fmt Signed-off-by: Eitan Yarmush --- spiffe/src/workload_api/client.rs | 2 +- spire-api/src/agent/delegated_identity.rs | 20 ++- spire-api/src/agent/mod.rs | 12 +- spire-api/src/selectors.rs | 130 +++++++++--------- .../delegated_identity_api_client_test.rs | 4 +- 5 files changed, 84 insertions(+), 84 deletions(-) diff --git a/spiffe/src/workload_api/client.rs b/spiffe/src/workload_api/client.rs index 4c9bf84..a0e8381 100644 --- a/spiffe/src/workload_api/client.rs +++ b/spiffe/src/workload_api/client.rs @@ -366,7 +366,7 @@ impl WorkloadApiClient { .get(DEFAULT_SVID) .ok_or(ClientError::EmptyResponse) .and_then(|r| { - JwtSvid::from_str(&r.svid).map_err(|err| ClientError::InvalidJwtSvid(err)) + JwtSvid::from_str(&r.svid).map_err(ClientError::InvalidJwtSvid) }) } diff --git a/spire-api/src/agent/delegated_identity.rs b/spire-api/src/agent/delegated_identity.rs index c3a5c77..58cdc68 100644 --- a/spire-api/src/agent/delegated_identity.rs +++ b/spire-api/src/agent/delegated_identity.rs @@ -2,16 +2,16 @@ //! The protobuf definition can be found [here](https://github.com/spiffe/spire-api-sdk/blob/main/proto/spire/api/agent/delegatedidentity/v1/delegatedidentity.proto) //! //! More information on it's usage can be found in the [SPIFFE docs](https://spiffe.io/docs/latest/deploying/spire_agent/#delegated-identity-api) -//! +//! //! Most importantly, this API cannot be used over the standard endpoint, it must be used over the admin socket. //! The admin socket can be configured in the SPIRE agent configuration document. -use spiffe::bundle::x509::{X509Bundle, X509BundleSet}; use crate::proto::spire::api::agent::delegatedidentity::v1::{ delegated_identity_client::DelegatedIdentityClient as DelegatedIdentityApiClient, SubscribeToX509BundlesRequest, SubscribeToX509BundlesResponse, SubscribeToX509sviDsRequest, SubscribeToX509sviDsResponse, }; +use spiffe::bundle::x509::{X509Bundle, X509BundleSet}; use spiffe::spiffe_id::TrustDomain; use spiffe::svid::x509::X509Svid; use spiffe::workload_api::address::validate_socket_path; @@ -24,17 +24,16 @@ use tokio::net::UnixStream; use tonic::transport::{Endpoint, Uri}; use tower::service_fn; - /// Name of the environment variable that holds the default socket endpoint path. pub const ADMIN_SOCKET_ENV: &str = "SPIFFE_ADMIN_ENDPOINT_SOCKET"; /// Gets the endpoint socket endpoint path from the environment variable `SPIFFE_ENDPOINT_SOCKET`, /// as described in [SPIFFE standard](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_Endpoint.md#4-locating-the-endpoint). pub fn get_admin_socket_path() -> Option { - match std::env::var(ADMIN_SOCKET_ENV) { - Ok(addr) => Some(addr), - Err(_) => None, - } + match std::env::var(ADMIN_SOCKET_ENV) { + Ok(addr) => Some(addr), + Err(_) => None, + } } /// Impl for DelegatedIdentity API @@ -116,13 +115,12 @@ impl DelegatedIdentityClient { } impl DelegatedIdentityClient { - /// Fetches a single X509 SPIFFE Verifiable Identity Document (SVID). /// /// This method connects to the SPIFFE Workload API and returns the first X509 SVID in the response. - /// + /// /// # Arguments - /// + /// /// * `selectors` - A list of selectors to filter the stream of [`X509Svid`] updates. /// /// # Returns @@ -157,7 +155,7 @@ impl DelegatedIdentityClient { /// The returned stream can be used to asynchronously yield new `X509Svid` updates as they become available. /// /// # Arguments - /// + /// /// * `selectors` - A list of selectors to filter the stream of [`X509Svid`] updates. /// /// # Returns diff --git a/spire-api/src/agent/mod.rs b/spire-api/src/agent/mod.rs index 0d6c7c0..46d6c37 100644 --- a/spire-api/src/agent/mod.rs +++ b/spire-api/src/agent/mod.rs @@ -1,13 +1,13 @@ //! Agent API -//! +//! //! The agent API consists of APIs: //! - delegated_identity //! - debug (not implemented) -//! +//! //! # Note -//! Both of these APIs must be accesed via the admin_socket_path which can be set +//! Both of these APIs must be accesed via the admin_socket_path which can be set //! in the [agent configuration file](https://spiffe.io/docs/latest/deploying/spire_agent/#agent-configuration-file). -//! -//! +//! +//! -pub mod delegated_identity; \ No newline at end of file +pub mod delegated_identity; diff --git a/spire-api/src/selectors.rs b/spire-api/src/selectors.rs index 6e80035..d63f171 100644 --- a/spire-api/src/selectors.rs +++ b/spire-api/src/selectors.rs @@ -1,5 +1,5 @@ //! Selectors which conform to SPIRE standards. -//! +//! use crate::proto::spire::api::types::Selector as SpiffeSelector; const K8S_TYPE: &str = "k8s"; @@ -7,22 +7,22 @@ const UNIX_TYPE: &str = "unix"; /// User-facing version of underlying proto selector type impl From for SpiffeSelector { - fn from(s: Selector) -> SpiffeSelector { - match s { - Selector::K8s(k8s_selector) => SpiffeSelector { - r#type: K8S_TYPE.to_string(), - value: k8s_selector.into(), - }, - Selector::Unix(unix_selector) => SpiffeSelector { - r#type: UNIX_TYPE.to_string(), - value: unix_selector.into(), - }, - Selector::Generic((k, v)) => SpiffeSelector { - r#type: k, - value: v, - }, - } - } + fn from(s: Selector) -> SpiffeSelector { + match s { + Selector::K8s(k8s_selector) => SpiffeSelector { + r#type: K8S_TYPE.to_string(), + value: k8s_selector.into(), + }, + Selector::Unix(unix_selector) => SpiffeSelector { + r#type: UNIX_TYPE.to_string(), + value: unix_selector.into(), + }, + Selector::Generic((k, v)) => SpiffeSelector { + r#type: k, + value: v, + }, + } + } } #[derive(Debug, Clone)] @@ -41,10 +41,10 @@ const K8S_NS_TYPE: &str = "ns"; impl From for String { fn from(k: K8s) -> String { - match k { - K8s::ServiceAccount(s) => format!("{}:{}", K8S_SA_TYPE, s), - K8s::Namespace(s) => format!("{}:{}", K8S_NS_TYPE, s), - } + match k { + K8s::ServiceAccount(s) => format!("{}:{}", K8S_SA_TYPE, s), + K8s::Namespace(s) => format!("{}:{}", K8S_NS_TYPE, s), + } } } @@ -62,13 +62,13 @@ const UNIX_GID_TYPE: &str = "gid"; const UNIX_UID_TYPE: &str = "uid"; impl From for String { - fn from(value: Unix) -> Self { - match value { - Unix::Pid(s) => format!("{}:{}", UNIX_PID_TYPE, s), - Unix::Gid(s) => format!("{}:{}", UNIX_GID_TYPE, s), - Unix::Uid(s) => format!("{}:{}", UNIX_UID_TYPE, s), - } - } + fn from(value: Unix) -> Self { + match value { + Unix::Pid(s) => format!("{}:{}", UNIX_PID_TYPE, s), + Unix::Gid(s) => format!("{}:{}", UNIX_GID_TYPE, s), + Unix::Uid(s) => format!("{}:{}", UNIX_UID_TYPE, s), + } + } } #[derive(Debug, Clone)] @@ -84,45 +84,45 @@ pub enum Unix { #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn test_k8s_sa_selector() { - let selector = Selector::K8s(K8s::ServiceAccount("foo".to_string())); - let spiffe_selector: SpiffeSelector = selector.into(); - assert_eq!(spiffe_selector.r#type, K8S_TYPE); - assert_eq!(spiffe_selector.value, "sa:foo"); - } + #[test] + fn test_k8s_sa_selector() { + let selector = Selector::K8s(K8s::ServiceAccount("foo".to_string())); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, K8S_TYPE); + assert_eq!(spiffe_selector.value, "sa:foo"); + } - #[test] - fn test_k8s_ns_selector() { - let selector = Selector::K8s(K8s::Namespace("foo".to_string())); - let spiffe_selector: SpiffeSelector = selector.into(); - assert_eq!(spiffe_selector.r#type, K8S_TYPE); - assert_eq!(spiffe_selector.value, "ns:foo"); - } + #[test] + fn test_k8s_ns_selector() { + let selector = Selector::K8s(K8s::Namespace("foo".to_string())); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, K8S_TYPE); + assert_eq!(spiffe_selector.value, "ns:foo"); + } - #[test] - fn test_unix_pid_selector() { - let selector = Selector::Unix(Unix::Pid(500)); - let spiffe_selector: SpiffeSelector = selector.into(); - assert_eq!(spiffe_selector.r#type, UNIX_TYPE); - assert_eq!(spiffe_selector.value, "pid:500"); - } + #[test] + fn test_unix_pid_selector() { + let selector = Selector::Unix(Unix::Pid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "pid:500"); + } - #[test] - fn test_unix_gid_selector() { - let selector = Selector::Unix(Unix::Gid(500)); - let spiffe_selector: SpiffeSelector = selector.into(); - assert_eq!(spiffe_selector.r#type, UNIX_TYPE); - assert_eq!(spiffe_selector.value, "gid:500"); - } + #[test] + fn test_unix_gid_selector() { + let selector = Selector::Unix(Unix::Gid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "gid:500"); + } - #[test] - fn test_unix_uid_selector() { - let selector = Selector::Unix(Unix::Uid(500)); - let spiffe_selector: SpiffeSelector = selector.into(); - assert_eq!(spiffe_selector.r#type, UNIX_TYPE); - assert_eq!(spiffe_selector.value, "uid:500"); - } -} \ No newline at end of file + #[test] + fn test_unix_uid_selector() { + let selector = Selector::Unix(Unix::Uid(500)); + let spiffe_selector: SpiffeSelector = selector.into(); + assert_eq!(spiffe_selector.r#type, UNIX_TYPE); + assert_eq!(spiffe_selector.value, "uid:500"); + } +} diff --git a/spire-api/tests/delegated_identity_api_client_test.rs b/spire-api/tests/delegated_identity_api_client_test.rs index 70d0924..f2a5146 100644 --- a/spire-api/tests/delegated_identity_api_client_test.rs +++ b/spire-api/tests/delegated_identity_api_client_test.rs @@ -27,7 +27,9 @@ mod integration_tests { .await .expect("failed to create client"); let response: spiffe::svid::x509::X509Svid = client - .fetch_x509_svid(vec![selectors::Selector::Unix(selectors::Unix::Uid(get_uid() + 1))]) + .fetch_x509_svid(vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))]) .await .expect("Failed to fetch delegate SVID"); // Not checking the chain as the root is generated by spire. From 274781a50f731bd7a87c5747b91a4b21cc25fc81 Mon Sep 17 00:00:00 2001 From: Eitan Yarmush Date: Mon, 14 Aug 2023 17:49:38 -0400 Subject: [PATCH 3/3] Adddressed PR comments and included suggested comments Signed-off-by: Eitan Yarmush --- .github/workflows/ci.yml | 2 +- scripts/agent.conf | 2 +- scripts/run-spire.sh | 2 +- spire-api/Cargo.toml | 12 +- spire-api/src/agent/delegated_identity.rs | 157 ++++++++++++++---- spire-api/src/agent/mod.rs | 11 +- spire-api/src/selectors.rs | 29 ++-- .../delegated_identity_api_client_test.rs | 142 ++++++++++++++-- 8 files changed, 282 insertions(+), 75 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8653b45..470e257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest env: SPIFFE_ENDPOINT_SOCKET: unix:/tmp/spire-agent/public/api.sock - SPIFFE_ADMIN_ENDPOINT_SOCKET: unix:/tmp/spire-agent/admin/api.sock + SPIRE_ADMIN_ENDPOINT_SOCKET: unix:/tmp/spire-agent/admin/api.sock needs: build steps: - name: Check out code diff --git a/scripts/agent.conf b/scripts/agent.conf index 0a891b8..dbbef62 100644 --- a/scripts/agent.conf +++ b/scripts/agent.conf @@ -9,7 +9,7 @@ agent { # simple testing/evaluation purposes. insecure_bootstrap = true - admin_socket_path = "$STRIPPED_SPIFFE_ADMIN_ENDPOINT_SOCKET" + admin_socket_path = "$STRIPPED_SPIRE_ADMIN_ENDPOINT_SOCKET" authorized_delegates = [ "spiffe://example.org/myservice", ] diff --git a/scripts/run-spire.sh b/scripts/run-spire.sh index d0b228e..f1954ef 100755 --- a/scripts/run-spire.sh +++ b/scripts/run-spire.sh @@ -37,7 +37,7 @@ mkdir -p /tmp/spire-server bin/spire-server run -config conf/server/server.conf > "${spire_server_log_file}" 2>&1 & wait_for_service "bin/spire-server healthcheck" "SPIRE Server" "${spire_server_log_file}" -export STRIPPED_SPIFFE_ADMIN_ENDPOINT_SOCKET=$(echo $SPIFFE_ADMIN_ENDPOINT_SOCKET| cut -c6-) +export STRIPPED_SPIRE_ADMIN_ENDPOINT_SOCKET=$(echo $SPIRE_ADMIN_ENDPOINT_SOCKET| cut -c6-) cat $SCRIPT_DIR/agent.conf | envsubst > "conf/agent/agent.conf" # Run the SPIRE agent with the joint token diff --git a/spire-api/Cargo.toml b/spire-api/Cargo.toml index 9a5b623..11ac0b9 100644 --- a/spire-api/Cargo.toml +++ b/spire-api/Cargo.toml @@ -23,19 +23,9 @@ prost-types = {version = "0.11"} tokio = { "version" = "1", features = ["net", "test-util"]} tokio-stream = "0.1" tower = { version = "0.4", features = ["util"] } -thiserror = "1.0" -url = "2.2" -asn1 = { package = "simple_asn1", version = "0.6" } -x509-parser = "0.15" -pkcs8 = "0.10" -jsonwebtoken = "8.3" -jsonwebkey = { version = "0.3", features = ["jsonwebtoken", "jwt-convert"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -zeroize = { version = "1.6", features = ["zeroize_derive"] } -time = "0.3" [dev-dependencies] +once_cell = "1.18" [build-dependencies] tonic-build = { version = "0.9", default-features = false, features = ["prost"] } diff --git a/spire-api/src/agent/delegated_identity.rs b/spire-api/src/agent/delegated_identity.rs index 58cdc68..8fe25b0 100644 --- a/spire-api/src/agent/delegated_identity.rs +++ b/spire-api/src/agent/delegated_identity.rs @@ -8,11 +8,15 @@ use crate::proto::spire::api::agent::delegatedidentity::v1::{ delegated_identity_client::DelegatedIdentityClient as DelegatedIdentityApiClient, - SubscribeToX509BundlesRequest, SubscribeToX509BundlesResponse, SubscribeToX509sviDsRequest, - SubscribeToX509sviDsResponse, + FetchJwtsviDsRequest, SubscribeToX509BundlesRequest, SubscribeToX509BundlesResponse, + SubscribeToX509sviDsRequest, SubscribeToX509sviDsResponse, SubscribeToJwtBundlesRequest, + SubscribeToJwtBundlesResponse, }; +use crate::proto::spire::api::types::Jwtsvid as ProtoJwtSvid; use spiffe::bundle::x509::{X509Bundle, X509BundleSet}; +use spiffe::bundle::jwt::{JwtBundleSet, JwtBundle}; use spiffe::spiffe_id::TrustDomain; +use spiffe::svid::jwt::JwtSvid; use spiffe::svid::x509::X509Svid; use spiffe::workload_api::address::validate_socket_path; use tokio_stream::{Stream, StreamExt}; @@ -20,14 +24,15 @@ use tokio_stream::{Stream, StreamExt}; use crate::selectors::Selector; use spiffe::workload_api::client::{ClientError, DEFAULT_SVID}; use std::convert::{Into, TryFrom}; +use std::str::FromStr; use tokio::net::UnixStream; use tonic::transport::{Endpoint, Uri}; use tower::service_fn; /// Name of the environment variable that holds the default socket endpoint path. -pub const ADMIN_SOCKET_ENV: &str = "SPIFFE_ADMIN_ENDPOINT_SOCKET"; +pub const ADMIN_SOCKET_ENV: &str = "SPIRE_ADMIN_ENDPOINT_SOCKET"; -/// Gets the endpoint socket endpoint path from the environment variable `SPIFFE_ENDPOINT_SOCKET`, +/// Gets the endpoint socket endpoint path from the environment variable `ADMIN_SOCKET_ENV`, /// as described in [SPIFFE standard](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_Endpoint.md#4-locating-the-endpoint). pub fn get_admin_socket_path() -> Option { match std::env::var(ADMIN_SOCKET_ENV) { @@ -132,7 +137,7 @@ impl DelegatedIdentityClient { /// /// Returns [`ClientError`] if the gRPC call fails or if the SVID could not be parsed from the gRPC response. pub async fn fetch_x509_svid( - mut self, + &mut self, selectors: Vec, ) -> Result { let request = SubscribeToX509sviDsRequest { @@ -172,7 +177,7 @@ impl DelegatedIdentityClient { /// /// Individual stream items might also be errors if there's an issue processing the response for a specific update. pub async fn stream_x509_svids( - mut self, + &mut self, selectors: Vec, ) -> Result>, ClientError> { let request = SubscribeToX509sviDsRequest { @@ -197,7 +202,7 @@ impl DelegatedIdentityClient { /// /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or /// there is a problem processing the response. - pub async fn fetch_x509_bundles(mut self) -> Result { + pub async fn fetch_x509_bundles(&mut self) -> Result { let request = SubscribeToX509BundlesRequest::default(); let response: tonic::Response> = @@ -227,7 +232,7 @@ impl DelegatedIdentityClient { /// /// Individual stream items might also be errors if there's an issue processing the response for a specific update. pub async fn stream_x509_bundles( - mut self, + &mut self, ) -> Result>, ClientError> { let request = SubscribeToX509BundlesRequest::default(); @@ -242,37 +247,133 @@ impl DelegatedIdentityClient { Ok(stream) } + + /// Fetches a list of [`JwtSvid`] parsing the JWT token in the Workload API response, for the given audience and selectors. + /// + /// # Arguments + /// + /// * `audience` - A list of audiences to include in the JWT token. Cannot be empty nor contain only empty strings. + /// * `selectors` - A list of selectors to filter the list of [`JwtSvid`]. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// there is a problem processing the response. + pub async fn fetch_jwt_svids + ToString>( + &mut self, + audience: &[T], + selectors: Vec, + ) -> Result, ClientError> { + let request = FetchJwtsviDsRequest { + audience: audience.iter().map(|s| s.to_string()).collect(), + selectors: selectors.into_iter().map(|s| s.into()).collect(), + }; + + DelegatedIdentityClient::parse_jwt_svid_from_grpc_response( + self.client + .fetch_jwtsvi_ds(request) + .await? + .into_inner() + .svids, + ) + } + + + + /// Watches the stream of [`JwtBundleSet`] updates. + /// + /// This function establishes a stream with the Workload API to continuously receive updates for the [`JwtBundleSet`]. + /// The returned stream can be used to asynchronously yield new `JwtBundleSet` updates as they become available. + /// + /// # Returns + /// + /// Returns a stream of `Result`. Each item represents an updated [`JwtBundleSet`] or an error if + /// there was a problem processing an update from the stream. + /// + /// # Errors + /// + /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// + /// * There's an issue connecting to the Workload API. + /// * An error occurs while setting up the stream. + /// + /// Individual stream items might also be errors if there's an issue processing the response for a specific update. + pub async fn stream_jwt_bundles( + &mut self, + ) -> Result>, ClientError> { + let request = SubscribeToJwtBundlesRequest::default(); + let response = self.client.subscribe_to_jwt_bundles(request).await?; + Ok(response.into_inner().map(|message| { + message + .map_err(ClientError::from) + .and_then(DelegatedIdentityClient::parse_jwt_bundle_set_from_grpc_response) + })) + } + + /// Fetches [`JwtBundleSet`] that is a set of [`JwtBundle`] keyed by the trust domain to which they belong. + /// + /// # Errors + /// + /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// there is a problem processing the response. + pub async fn fetch_jwt_bundles( + &mut self, + ) -> Result { + let request = SubscribeToJwtBundlesRequest::default(); + let response = self.client.subscribe_to_jwt_bundles(request).await?; + let initial = response.into_inner().message().await?; + DelegatedIdentityClient::parse_jwt_bundle_set_from_grpc_response(initial.ok_or(ClientError::EmptyResponse)?) + } } impl DelegatedIdentityClient { fn parse_x509_svid_from_grpc_response( response: SubscribeToX509sviDsResponse, ) -> Result { - let svid = match response.x509_svids.get(DEFAULT_SVID) { - None => return Err(ClientError::EmptyResponse), - Some(s) => s, - }; + let svid = response + .x509_svids + .get(DEFAULT_SVID) + .ok_or(ClientError::EmptyResponse)?; - // OPTIMIZE THIS - let mut total_length = 0; - svid.x509_svid - .as_ref() - .ok_or(ClientError::EmptyResponse)? - .cert_chain - .iter() - .for_each(|c| total_length += c.len()); - let mut cert_chain = bytes::BytesMut::with_capacity(total_length); - svid.x509_svid - .as_ref() - .ok_or(ClientError::EmptyResponse)? - .cert_chain - .iter() - .for_each(|c| cert_chain.extend(c)); + let x509_svid = svid.x509_svid.as_ref().ok_or(ClientError::EmptyResponse)?; + + let total_length = x509_svid.cert_chain.iter().map(|c| c.len()).sum(); + let mut cert_chain_bytes = Vec::with_capacity(total_length); + + for c in &x509_svid.cert_chain { + cert_chain_bytes.extend_from_slice(c); + } - X509Svid::parse_from_der(cert_chain.as_ref(), svid.x509_svid_key.as_ref()) + X509Svid::parse_from_der(&cert_chain_bytes, svid.x509_svid_key.as_ref()) .map_err(|e| e.into()) } + fn parse_jwt_svid_from_grpc_response( + svids: Vec, + ) -> Result, ClientError> { + let result: Result, ClientError> = svids + .iter() + .map(|r| JwtSvid::from_str(&r.token).map_err(ClientError::InvalidJwtSvid)) + .collect(); + result + } + + fn parse_jwt_bundle_set_from_grpc_response( + response: SubscribeToJwtBundlesResponse, + ) -> Result { + let mut bundle_set = JwtBundleSet::new(); + + for (td, bundle_data) in response.bundles.into_iter() { + let trust_domain = TrustDomain::try_from(td)?; + let bundle = JwtBundle::from_jwt_authorities(trust_domain, &bundle_data) + .map_err(ClientError::from)?; + + bundle_set.add_bundle(bundle); + } + + Ok(bundle_set) + } + fn parse_x509_bundle_set_from_grpc_response( response: SubscribeToX509BundlesResponse, ) -> Result { diff --git a/spire-api/src/agent/mod.rs b/spire-api/src/agent/mod.rs index 46d6c37..23dbc96 100644 --- a/spire-api/src/agent/mod.rs +++ b/spire-api/src/agent/mod.rs @@ -1,13 +1,10 @@ //! Agent API //! -//! The agent API consists of APIs: -//! - delegated_identity -//! - debug (not implemented) +//! Consists of the following APIs: +//! - `delegated_identity`: For managing delegated identities. +//! - `debug`: (Not yet implemented). //! //! # Note -//! Both of these APIs must be accesed via the admin_socket_path which can be set -//! in the [agent configuration file](https://spiffe.io/docs/latest/deploying/spire_agent/#agent-configuration-file). +//! Access these APIs via the `admin_socket_path` in the [agent configuration file](https://spiffe.io/docs/latest/deploying/spire_agent/#agent-configuration-file). //! -//! - pub mod delegated_identity; diff --git a/spire-api/src/selectors.rs b/spire-api/src/selectors.rs index d63f171..ea023c8 100644 --- a/spire-api/src/selectors.rs +++ b/spire-api/src/selectors.rs @@ -1,11 +1,10 @@ -//! Selectors which conform to SPIRE standards. -//! +//! Selectors conforming to SPIRE standards. use crate::proto::spire::api::types::Selector as SpiffeSelector; const K8S_TYPE: &str = "k8s"; const UNIX_TYPE: &str = "unix"; -/// User-facing version of underlying proto selector type +/// Converts user-defined selectors into SPIFFE selectors. impl From for SpiffeSelector { fn from(s: Selector) -> SpiffeSelector { match s { @@ -26,19 +25,20 @@ impl From for SpiffeSelector { } #[derive(Debug, Clone)] -/// Selector represents a SPIFFE ID selector. +/// Represents various types of SPIFFE identity selectors. pub enum Selector { - /// K8s represents a SPIFFE ID selector. + /// Represents a SPIFFE identity selector based on Kubernetes constructs. K8s(K8s), - /// Selector represents a SPIFFE ID selector. + /// Represents a SPIFFE identity selector based on Unix system constructs such as PID, GID, and UID. Unix(Unix), - /// Selector represents a SPIFFE ID selector. + /// Represents a generic SPIFFE identity selector defined by a key-value pair. Generic((String, String)), } const K8S_SA_TYPE: &str = "sa"; const K8S_NS_TYPE: &str = "ns"; +/// Converts Kubernetes selectors to their string representation. impl From for String { fn from(k: K8s) -> String { match k { @@ -49,11 +49,11 @@ impl From for String { } #[derive(Debug, Clone)] -/// K8s is a helper type to create a SPIFFE ID selector for Kubernetes. +/// Represents a SPIFFE identity selector for Kubernetes. pub enum K8s { - /// ServiceAccount represents the SPIFFE ID selector for a Kubernetes service account. + /// SPIFFE identity selector for a Kubernetes service account. ServiceAccount(String), - /// Namespace represents the SPIFFE ID selector for a Kubernetes namespace. + /// SPIFFE identity selector for a Kubernetes namespace. Namespace(String), } @@ -61,6 +61,7 @@ const UNIX_PID_TYPE: &str = "pid"; const UNIX_GID_TYPE: &str = "gid"; const UNIX_UID_TYPE: &str = "uid"; +/// Converts a Unix selector into a formatted string representation. impl From for String { fn from(value: Unix) -> Self { match value { @@ -72,13 +73,13 @@ impl From for String { } #[derive(Debug, Clone)] -/// K8s is a helper type to create a SPIFFE ID unix process constructs. +/// Represents SPIFFE identity selectors based on Unix process-related attributes. pub enum Unix { - /// PID represents the SPIFFE ID selector for a process ID. + /// Specifies a selector for a Unix process ID (PID). Pid(u16), - /// GID represents the SPIFFE ID selector for a group ID. + /// Specifies a selector for a Unix group ID (GID). Gid(u16), - /// UID represents the SPIFFE ID selector for a User ID. + /// Specifies a selector for a Unix user ID (UID). Uid(u16), } diff --git a/spire-api/tests/delegated_identity_api_client_test.rs b/spire-api/tests/delegated_identity_api_client_test.rs index f2a5146..9173995 100644 --- a/spire-api/tests/delegated_identity_api_client_test.rs +++ b/spire-api/tests/delegated_identity_api_client_test.rs @@ -4,8 +4,16 @@ #[cfg(feature = "integration-tests")] mod integration_tests { + use once_cell::sync::Lazy; + use spiffe::bundle::BundleRefSource; + use spiffe::bundle::jwt::JwtBundleSet; + use spiffe::spiffe_id::TrustDomain; use spire_api::selectors; use std::process::Command; + use tokio_stream::StreamExt; + use spire_api::agent::delegated_identity::DelegatedIdentityClient; + + static TRUST_DOMAIN: Lazy = Lazy::new(|| TrustDomain::new("example.org").unwrap()); fn get_uid() -> u16 { let mut uid = String::from_utf8( @@ -20,12 +28,31 @@ mod integration_tests { uid.parse().expect("could not parse uid to number") } + async fn get_client() -> DelegatedIdentityClient { + DelegatedIdentityClient::default() + .await + .expect("failed to create client") + } + #[tokio::test] - async fn fetch_delegate_svid() { - // let uid: u16 = std::env::var("UID").expect("UID env var not present").parse().expect("could not parse uid to number"); - let client = spire_api::agent::delegated_identity::DelegatedIdentityClient::default() + async fn fetch_delegate_jwt_svid() { + let mut client = get_client().await; + let svid = client + .fetch_jwt_svids( + &["my_audience"], + vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))], + ) .await - .expect("failed to create client"); + .expect("Failed to fetch JWT SVID"); + assert_eq!(svid.len(), 1); + assert_eq!(svid[0].audience(), &["my_audience"]); + } + + #[tokio::test] + async fn fetch_delegate_x509_svid() { + let mut client = get_client().await; let response: spiffe::svid::x509::X509Svid = client .fetch_x509_svid(vec![selectors::Selector::Unix(selectors::Unix::Uid( get_uid() + 1, @@ -42,20 +69,111 @@ mod integration_tests { } #[tokio::test] - async fn fetch_trust_bundles() { - // let uid: u16 = std::env::var("UID").expect("UID env var not present").parse().expect("could not parse uid to number"); - let client = spire_api::agent::delegated_identity::DelegatedIdentityClient::default() + async fn stream_delegate_x509_svid() { + let test_duration = std::time::Duration::from_secs(60); + let mut client = get_client().await; + let mut stream = client + .stream_x509_svids(vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))]) .await - .expect("failed to create client"); + .expect("Failed to fetch delegate SVID"); + + let result = tokio::time::timeout(test_duration, stream.next()) + .await + .expect("Test did not complete in the expected duration"); + let response = result.expect("empty result").expect("error in stream"); + // Not checking the chain as the root is generated by spire. + // In the future we could look in the downloaded spire directory for the keys. + assert_eq!(response.cert_chain().len(), 1); + assert_eq!( + response.spiffe_id().to_string(), + "spiffe://example.org/different-process" + ); + } + + #[tokio::test] + async fn fetch_delegated_x509_trust_bundles() { + let mut client = get_client().await; let response = client .fetch_x509_bundles() .await .expect("Failed to fetch trust bundles"); response - .get_bundle( - &spiffe::spiffe_id::TrustDomain::new("example.org".as_ref()) - .expect("Failed to parse trust domain ="), - ) + .get_bundle(&*TRUST_DOMAIN) + .expect("Failed to get bundle"); + } + + #[tokio::test] + async fn stream_delegated_x509_trust_bundles() { + let test_duration = std::time::Duration::from_secs(60); + let mut client = get_client().await; + let mut stream = client + .stream_x509_bundles() + .await + .expect("Failed to fetch trust bundles"); + + let result = tokio::time::timeout(test_duration, stream.next()) + .await + .expect("Test did not complete in the expected duration"); + let response = result.expect("empty result").expect("error in stream"); + response + .get_bundle(&*TRUST_DOMAIN) .expect("Failed to get bundle"); } + + async fn verify_jwt( + client: &mut DelegatedIdentityClient, + bundles: JwtBundleSet, + ) { + let svids = client + .fetch_jwt_svids( + &["my_audience"], + vec![selectors::Selector::Unix(selectors::Unix::Uid( + get_uid() + 1, + ))], + ) + .await + .expect("Failed to fetch JWT SVID"); + let svid = svids.first().expect("no items in jwt bundle list"); + let key_id = svid.key_id(); + + let bundle = bundles.get_bundle_for_trust_domain(&*TRUST_DOMAIN); + let bundle = bundle + .expect("Bundle was None") + .expect("Failed to unwrap bundle"); + assert_eq!(bundle.trust_domain(), &*TRUST_DOMAIN); + assert_eq!( + bundle.find_jwt_authority(key_id).unwrap().key_id, + Some(key_id.to_string()) + ); + } + + #[tokio::test] + async fn fetch_delegated_jwt_trust_bundles() { + let mut client = get_client().await; + let response = client + .fetch_jwt_bundles() + .await + .expect("Failed to fetch trust bundles"); + + + verify_jwt(&mut client, response).await; + } + + #[tokio::test] + async fn stream_delegated_jwt_trust_bundles() { + let mut client = get_client().await; + let test_duration = std::time::Duration::from_secs(60); + let mut stream = client + .stream_jwt_bundles() + .await + .expect("Failed to fetch trust bundles"); + + let result = tokio::time::timeout(test_duration, stream.next()) + .await + .expect("Test did not complete in the expected duration"); + + verify_jwt(&mut client, result.expect("empty result").expect("error in stream")).await; + } }