diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 63f050f..ae3ce24 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,10 @@ updates: schedule: interval: "daily" - package-ecosystem: "cargo" - directory: "/" # Location of the Cargo.toml file + directory: "/spiffe" # Location of the Cargo.toml file for spiffe crate + schedule: + interval: "daily" + - package-ecosystem: "cargo" + directory: "/spire-api" # Location of the Cargo.toml file for spire-api crate schedule: interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8efa3e..470e257 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ jobs: steps: - name: Check out code uses: actions/checkout@v3 + with: + submodules: recursive - name: Install dependencies and common setup uses: ./.github/actions/setup-env - name: Lint Rust code with rustfmt and clippy @@ -23,25 +25,39 @@ jobs: steps: - name: Check out code uses: actions/checkout@v3 + with: + submodules: recursive - name: Install dependencies and common setup uses: ./.github/actions/setup-env - name: Build Rust project run: cargo build test: - name: Run SPIFFE Integration Tests + name: Run spiffe and spire-api Integration Tests runs-on: ubuntu-latest env: SPIFFE_ENDPOINT_SOCKET: unix:/tmp/spire-agent/public/api.sock + SPIRE_ADMIN_ENDPOINT_SOCKET: unix:/tmp/spire-agent/admin/api.sock needs: build steps: - name: Check out code uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install dependencies and common setup uses: ./.github/actions/setup-env + - name: Start SPIRE run: ./scripts/run-spire.sh & + - name: Execute spiffe Integration Tests run: RUST_BACKTRACE=1 cargo test --features integration-tests + working-directory: spiffe + + - name: Execute spire-api Integration Tests + run: RUST_BACKTRACE=1 cargo test --features integration-tests + working-directory: spire-api + - name: Clean up SPIRE run: ./scripts/cleanup-spire.sh diff --git a/.gitignore b/.gitignore index 0927da7..fa6c7a6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock +**/Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7f2b5a7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "spire-api-sdk"] + path = spire-api-sdk + url = https://github.com/spiffe/spire-api-sdk.git diff --git a/Cargo.toml b/Cargo.toml index 3693e1e..cfed8d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,52 +1,5 @@ -[package] -edition = "2018" -name = "spiffe" -# When releasing to crates.io: -# - Update CHANGELOG.md. -# - Create a new tag -version = "0.3.1" -authors = ["Max Lambrecht "] -description = "Rust client library implementation for SPIFFE" -license = "Apache-2.0" -repository = "https://github.com/maxlambrecht/rust-spiffe" -documentation = "https://docs.rs/spiffe" -readme = "README.md" -categories = ["cryptography"] -keywords = ["SPIFFE", "X509", "JWT"] - -[dependencies] -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] -jsonwebkey = { version = "0.3", features = ["generate"] } -tokio-test = "0.4" -once_cell = "1.18" - -# used to verify in tests that the certificates bytes from the X.509 SVIDs and bundle authorities -# are parseable as OpenSSL X.509 certificates. -openssl = { version = "0.10", features = ["vendored"] } - -[build-dependencies] -tonic-build = { version = "0.9", default-features = false, features = ["prost"] } -prost-build = "0.11" -anyhow = "1.0.65" - -[features] -integration-tests = [] +[workspace] +members = [ + "spiffe", + "spire-api", +] \ No newline at end of file diff --git a/README.md b/README.md index 1fa7255..82efda7 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,27 @@ -# Rust SPIFFE Library +# Rust SPIRE Libraries -This utility library enables interaction with the [SPIFFE Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md). It allows fetching of X.509 and JWT SVIDs, bundles and supports watch/stream updates. The types in the library are in compliance with [SPIFFE standards](https://github.com/spiffe/spiffe/tree/main/standards). More about SPIFFE can be found at [spiffe.io](https://spiffe.io/). +This repository contains two distinct Rust libraries focused on supporting SPIRE functionalities: -[![crates.io](https://img.shields.io/crates/v/spiffe.svg)](https://crates.io/crates/spiffe) -[![docs.rs](https://docs.rs/spiffe/badge.svg)](https://docs.rs/spiffe) -![CI](https://github.com/maxlambrecht/rust-spiffe/workflows/Continuous%20Integration/badge.svg) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/maxlambrecht/rust-spiffe/blob/main/LICENSE) +## [spiffe](./spiffe) -## Getting Started - -Include `spiffe` in your `Cargo.toml` dependencies: - -```toml -[dependencies] -spiffe = "0.3.1" -``` - -## Examples of Usage - -### Creating a `WorkloadApiClient` - -Create client using the endpoint socket path: - -```rust -let mut client = WorkloadApiClient::new_from_path("unix:/tmp/spire-agent/public/api.sock").await?; -``` - -Or by using the `SPIFFE_ENDPOINT_SOCKET` environment variable: - -```rust -let mut client = WorkloadApiClient::default().await?; -``` - -### Fetching X.509 Materials - -Fetch the default X.509 SVID, a set of X.509 bundles, all X.509 materials, or watch for updates on the X.509 context and bundles. - -```rust -// fetch the default X.509 SVID -let x509_svid: X509Svid = client.fetch_x509_svid().await?; - -// fetch a set of X.509 bundles (X.509 public key authorities) -let x509_bundles: X509BundleSet = client.fetch_x509_bundles().await?; +The `spiffe` crate enables interaction with +the [SPIFFE Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md). It allows +fetching of X.509 and JWT SVIDs, bundles, and supports watch/stream updates. The types in the library are in compliance +with [SPIFFE standards](https://github.com/spiffe/spiffe/tree/main/standards). More about SPIFFE can be found +at [spiffe.io](https://spiffe.io/). -// fetch all the X.509 materials (SVIDs and bundles) -let x509_context: X509Context = client.fetch_x509_context().await?; +- [Read the README](./spiffe/README.md) for more information. -// get the X.509 chain of certificates from the SVID -let cert_chain: &Vec = x509_svid.cert_chain(); +## [spire-api](./spire-api) -// get the private key from the SVID -let private_key: &PrivateKey = x509_svid.private_key(); +The `spire-api` crate provides support for SPIRE specific APIs, including the Delegated Identity API. -// parse a SPIFFE trust domain -let trust_domain = TrustDomain::try_from("example.org")?; +- [Read the README](./spire-api/README.md) for more information. -// get the X.509 bundle associated to the trust domain -let x509_bundle: &X509Bundle = x509_bundles.get_bundle(&trust_domain)?; - -// get the X.509 authorities (public keys) in the bundle -let x509_authorities: &Vec = x509_bundle.authorities(); - -// watch for updates on the X.509 context -let mut x509_context_stream = client.watch_x509_context_stream().await?; -while let Some(x509_context_update) = x509_context_stream.next().await { - match x509_context_update { - Ok(update) => { - // handle the updated X509Context - } - Err(e) => { - // handle the error - } - } -} - -// watch for updates on the X.509 bundles -let mut x509_bundle_stream = client.watch_x509_bundles_stream().await?; -while let Some(x509_bundle_update) = x509_bundle_stream.next().await { - match x509_bundle_update { - Ok(update) => { - // handle the updated X509 bundle - } - Err(e) => { - // handle the error - } - } -} -``` - -### Fetching and Validating JWT Tokens and Bundles - -Fetch JWT tokens, parse and validate them, fetch JWT bundles, or watch for updates on the JWT bundles. - -```rust -// parse a SPIFFE ID to ask a token for -let spiffe_id = SpiffeId::try_from("spiffe://example.org/my-service")?; - -// fetch a jwt token for the provided SPIFFE-ID and with the target audience `service1.com` -let jwt_token = client.fetch_jwt_token(&["audience1", "audience2"], Some(&spiffe_id)).await?; - -// fetch the jwt token and parses it as a `JwtSvid` -let jwt_svid = client.fetch_jwt_svid(&["audience1", "audience2"], Some(&spiffe_id)).await?; - -// fetch a set of jwt bundles (public keys for validating jwt token) -let jwt_bundles = client.fetch_jwt_bundles().await?; - -// parse a SPIFFE trust domain -let trust_domain = TrustDomain::try_from("example.org")?; - -// get the JWT bundle associated to the trust domain -let jwt_bundle: &JwtBundle = jwt_bundles.get_bundle(&trust_domain)?; - -// get the JWT authorities (public keys) in the bundle -let jwt_authority: &JwtAuthority = jwt_bundle.find_jwt_authority("a_key_id")?; +## Getting Started -// parse a `JwtSvid` validating the token signature with a JWT bundle source. -let validated_jwt_svid = JwtSvid::parse_and_validate(&jwt_token, &jwt_bundles_set, &["service1.com"])?; +Follow the links above to the individual README files for detailed information on how to use each library. -// watch for updates on the JWT bundles -let mut jwt_bundle_stream = client.watch_jwt_bundles_stream().await?; -while let Some(jwt_bundle_update) = jwt_bundle_stream.next().await { - match jwt_bundle_update { - Ok(update) => { - // handle the updated JWT bundle - } - Err(e) => { - // handle the error - } - } -} -``` +## License -For more detailed examples and additional features, refer to the [documentation](https://docs.rs/spiffe). +This project is licensed under [LICENSE NAME](./LICENSE). diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..17f74bc --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,55 @@ +# Releasing Crates + +Follow these steps to create a new release for a specific crate in the repository. + +## 1. Prepare the Release + +- Determine the crate you are releasing and the new version number. +- Update the crate's `Cargo.toml` file with the new version number on a dedicated branch. +- Update the CHANGELOG.md file with detailed notes about the new release. Include any new features, bug fixes, and other + relevant information. +- Update any relevant documentation, including README files and any public-facing documents related to the crate. + +## 2. Create a Pull Request (PR) for Preparing the Release + +- Push the branch and create a pull request to merge the changes into the main branch. +- Include the changes to the `Cargo.toml`, CHANGELOG.md, and documentation files in the PR. +- Engage the maintainers for a thorough review of the changes. + +## 3. Merge the PR + +- Once the PR is approved, merge it into the main branch. + +## 4. Create the Release Branch + +- Checkout the main branch. +- Create a new branch for the release, named with the pattern `release/CRATE-NAME-VERSION`. + +For example: +```sh +git checkout -b release/spiffe-v0.3.2 +``` + +## 5. Create a Git Tag + +- Create a Git tag for the new release, using the same pattern `CRATE-NAME-VERSION`. + +For example: +```sh +git tag spiffe-v0.3.2 +``` + +- Push the tag and the branch to the repository. + +## 6. Publish the Crate + +For example: +```sh +cargo publish --manifest-path spiffe/Cargo.toml +``` + +## 7 Create a GitHub Release + +Navigate to the "Releases" section in the repository on GitHub. +Draft a new release using the tag created earlier, and include the notes from the CHANGELOG.md. +Publish the release. \ No newline at end of file diff --git a/scripts/agent.conf b/scripts/agent.conf new file mode 100644 index 0000000..dbbef62 --- /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_SPIRE_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..f1954ef 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_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 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/CHANGELOG.md b/spiffe/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to spiffe/CHANGELOG.md diff --git a/spiffe/Cargo.toml b/spiffe/Cargo.toml new file mode 100644 index 0000000..e84b02d --- /dev/null +++ b/spiffe/Cargo.toml @@ -0,0 +1,57 @@ +[package] +edition = "2018" +name = "spiffe" +# When releasing to crates.io: +# - Update CHANGELOG.md. +# - Create a new tag +version = "0.3.1" +authors = ["Max Lambrecht "] +description = "Rust client library implementation for SPIFFE" +license = "Apache-2.0" +repository = "https://github.com/maxlambrecht/rust-spiffe" +documentation = "https://docs.rs/spiffe" +readme = "README.md" +categories = ["cryptography"] +keywords = ["SPIFFE", "X509", "JWT"] + +[dependencies] +# spiffe-types dependencies: +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" +tonic = { version = "0.9"} + +# workload-api dependencies: +prost = { version = "0.11", optional = true } +prost-types = { version = "0.11", optional = true } +tokio = { version = "1", features = ["net", "test-util"], optional = true } +tokio-stream = { version = "0.1", optional = true } +tower = { version = "0.4", features = ["util"], optional = true } + +[dev-dependencies] +jsonwebkey = { version = "0.3", features = ["generate"] } +tokio-test = "0.4" +once_cell = "1.18" + +# used to verify in tests that the certificates bytes from the X.509 SVIDs and bundle authorities +# are parseable as OpenSSL X.509 certificates. +openssl = { version = "0.10", features = ["vendored"] } + +[build-dependencies] +tonic-build = { version = "0.9", default-features = false, features = ["prost"] } +prost-build = "0.11" +anyhow = "1.0.65" + +[features] +default = ["spiffe-types", "workload-api"] +spiffe-types = [] +workload-api = ["prost", "prost-types", "tokio", "tokio-stream", "tower"] +integration-tests = [] diff --git a/spiffe/README.md b/spiffe/README.md new file mode 100644 index 0000000..ee03509 --- /dev/null +++ b/spiffe/README.md @@ -0,0 +1,139 @@ +# Rust SPIFFE Library + +This utility library enables interaction with the [SPIFFE Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md). It allows fetching of X.509 and JWT SVIDs, bundles and supports watch/stream updates. The types in the library are in compliance with [SPIFFE standards](https://github.com/spiffe/spiffe/tree/main/standards). More about SPIFFE can be found at [spiffe.io](https://spiffe.io/). + +[![crates.io](https://img.shields.io/crates/v/spiffe.svg)](https://crates.io/crates/spiffe) +[![docs.rs](https://docs.rs/spiffe/badge.svg)](https://docs.rs/spiffe) +![CI](https://github.com/maxlambrecht/rust-spiffe/workflows/Continuous%20Integration/badge.svg) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/maxlambrecht/rust-spiffe/blob/main/LICENSE) + +## Getting Started + +Include `spiffe` in your `Cargo.toml` dependencies to get both the SPIFFE types (`spiffe-types`) and the Workload API +client (`workload-api`) by default: + +```toml +[dependencies] +spiffe = "0.3.1" +``` + +## Examples of Usage + +### Creating a `WorkloadApiClient` + +Create client using the endpoint socket path: + +```rust +let mut client = WorkloadApiClient::new_from_path("unix:/tmp/spire-agent/public/api.sock").await?; +``` + +Or by using the `SPIFFE_ENDPOINT_SOCKET` environment variable: + +```rust +let mut client = WorkloadApiClient::default().await?; +``` + +### Fetching X.509 Materials + +Fetch the default X.509 SVID, a set of X.509 bundles, all X.509 materials, or watch for updates on the X.509 context and bundles. + +```rust +// fetch the default X.509 SVID +let x509_svid: X509Svid = client.fetch_x509_svid().await?; + +// fetch a set of X.509 bundles (X.509 public key authorities) +let x509_bundles: X509BundleSet = client.fetch_x509_bundles().await?; + +// fetch all the X.509 materials (SVIDs and bundles) +let x509_context: X509Context = client.fetch_x509_context().await?; + +// get the X.509 chain of certificates from the SVID +let cert_chain: &Vec = x509_svid.cert_chain(); + +// get the private key from the SVID +let private_key: &PrivateKey = x509_svid.private_key(); + +// parse a SPIFFE trust domain +let trust_domain = TrustDomain::try_from("example.org")?; + +// get the X.509 bundle associated to the trust domain +let x509_bundle: &X509Bundle = x509_bundles.get_bundle(&trust_domain)?; + +// get the X.509 authorities (public keys) in the bundle +let x509_authorities: &Vec = x509_bundle.authorities(); + +// watch for updates on the X.509 context +let mut x509_context_stream = client.stream_x509_contexts().await?; +while let Some(x509_context_update) = x509_context_stream.next().await { + match x509_context_update { + Ok(update) => { + // handle the updated X509Context + } + Err(e) => { + // handle the error + } + } +} + +// watch for updates on the X.509 bundles +let mut x509_bundle_stream = client.stream_x509_bundles().await?; +while let Some(x509_bundle_update) = x509_bundle_stream.next().await { + match x509_bundle_update { + Ok(update) => { + // handle the updated X509 bundle + } + Err(e) => { + // handle the error + } + } +} +``` + +### Fetching and Validating JWT Tokens and Bundles + +Fetch JWT tokens, parse and validate them, fetch JWT bundles, or watch for updates on the JWT bundles. + +```rust +// parse a SPIFFE ID to ask a token for +let spiffe_id = SpiffeId::try_from("spiffe://example.org/my-service")?; + +// fetch a jwt token for the provided SPIFFE-ID and with the target audience `service1.com` +let jwt_token = client.fetch_jwt_token(&["audience1", "audience2"], Some(&spiffe_id)).await?; + +// fetch the jwt token and parses it as a `JwtSvid` +let jwt_svid = client.fetch_jwt_svid(&["audience1", "audience2"], Some(&spiffe_id)).await?; + +// fetch a set of jwt bundles (public keys for validating jwt token) +let jwt_bundles = client.fetch_jwt_bundles().await?; + +// parse a SPIFFE trust domain +let trust_domain = TrustDomain::try_from("example.org")?; + +// get the JWT bundle associated to the trust domain +let jwt_bundle: &JwtBundle = jwt_bundles.get_bundle(&trust_domain)?; + +// get the JWT authorities (public keys) in the bundle +let jwt_authority: &JwtAuthority = jwt_bundle.find_jwt_authority("a_key_id")?; + +// parse a `JwtSvid` validating the token signature with a JWT bundle source. +let validated_jwt_svid = JwtSvid::parse_and_validate(&jwt_token, &jwt_bundles_set, &["service1.com"])?; + +// watch for updates on the JWT bundles +let mut jwt_bundle_stream = client.stream_jwt_bundles().await?; +while let Some(jwt_bundle_update) = jwt_bundle_stream.next().await { + match jwt_bundle_update { + Ok(update) => { + // handle the updated JWT bundle + } + Err(e) => { + // handle the error + } + } +} +``` + +For more detailed examples and additional features, refer to the [documentation](https://docs.rs/spiffe). + +## License + +This library is licensed under the Apache License. See the [LICENSE.md](../LICENSE) file for details. diff --git a/build.rs b/spiffe/build.rs similarity index 100% rename from build.rs rename to spiffe/build.rs diff --git a/src/bundle/jwt/mod.rs b/spiffe/src/bundle/jwt/mod.rs similarity index 100% rename from src/bundle/jwt/mod.rs rename to spiffe/src/bundle/jwt/mod.rs diff --git a/src/bundle/mod.rs b/spiffe/src/bundle/mod.rs similarity index 100% rename from src/bundle/mod.rs rename to spiffe/src/bundle/mod.rs diff --git a/src/bundle/x509/mod.rs b/spiffe/src/bundle/x509/mod.rs similarity index 100% rename from src/bundle/x509/mod.rs rename to spiffe/src/bundle/x509/mod.rs diff --git a/src/cert/errors.rs b/spiffe/src/cert/errors.rs similarity index 100% rename from src/cert/errors.rs rename to spiffe/src/cert/errors.rs diff --git a/src/cert/mod.rs b/spiffe/src/cert/mod.rs similarity index 100% rename from src/cert/mod.rs rename to spiffe/src/cert/mod.rs diff --git a/src/cert/parsing.rs b/spiffe/src/cert/parsing.rs similarity index 100% rename from src/cert/parsing.rs rename to spiffe/src/cert/parsing.rs diff --git a/spiffe/src/constants.rs b/spiffe/src/constants.rs new file mode 100644 index 0000000..9465ac7 --- /dev/null +++ b/spiffe/src/constants.rs @@ -0,0 +1,11 @@ +//! Module defining constants used within the Rust-Spiffe library. + +/// Specifies the index of the default SVID (Secure Vector Identifier) within a list. +/// This constant is used to identify the first SVID in the list returned by the Workload API, +/// which is considered the default for operations involving multiple SVIDs. +pub const DEFAULT_SVID: usize = 0; + +/// Name of the environment variable that is used to configure the socket endpoint path for SPIFFE. +/// This path is required for communication between SPIFFE-enabled systems and should be set within +/// the environment variables of the host. +pub const SPIFFE_SOCKET_ENV: &str = "SPIFFE_ENDPOINT_SOCKET"; diff --git a/src/workload_api/address.rs b/spiffe/src/endpoint.rs similarity index 75% rename from src/workload_api/address.rs rename to spiffe/src/endpoint.rs index fb3d826..c067979 100644 --- a/src/workload_api/address.rs +++ b/spiffe/src/endpoint.rs @@ -4,11 +4,9 @@ use std::env; use std::net::IpAddr; use std::str::FromStr; -use thiserror::Error; -use url::{ParseError, Url}; - -/// Name of the environment variable that holds the default socket endpoint path. -pub const SOCKET_ENV: &str = "SPIFFE_ENDPOINT_SOCKET"; +use crate::constants::SPIFFE_SOCKET_ENV; +use crate::error::SocketPathError; +use url::Url; const TCP_SCHEME: &str = "tcp"; const UNIX_SCHEME: &str = "unix"; @@ -16,53 +14,12 @@ const UNIX_SCHEME: &str = "unix"; /// 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_default_socket_path() -> Option { - match env::var(SOCKET_ENV) { + match env::var(SPIFFE_SOCKET_ENV) { Ok(addr) => Some(addr), Err(_) => None, } } -/// An error that arises validating a SPIFFE endpoint socket path. -#[derive(Debug, Error, PartialEq, Copy, Clone)] -#[non_exhaustive] -pub enum SocketPathError { - /// The SPIFFE endpoint socket URI has a scheme other than 'unix' or 'tcp'. - #[error("workload endpoint socket URI must have a tcp:// or unix:// scheme")] - InvalidScheme, - - /// The SPIFFE endpoint unix socket URI does not include a path. - #[error("workload endpoint unix socket URI must include a path")] - UnixAddressEmptyPath, - - /// The SPIFFE endpoint tcp socket URI include a path. - #[error("workload endpoint tcp socket URI must not include a path")] - TcpAddressNonEmptyPath, - - /// The SPIFFE endpoint socket URI has query values. - #[error("workload endpoint socket URI must not include query values")] - HasQueryValues, - - /// The SPIFFE endpoint socket URI has a fragment. - #[error("workload endpoint socket URI must not include a fragment")] - HasFragment, - - /// The SPIFFE endpoint socket URI has query user info. - #[error("workload endpoint socket URI must not include user info")] - HasUserInfo, - - /// The SPIFFE endpoint tcp socket URI has misses a host. - #[error("workload endpoint tcp socket URI must include a host")] - TcpEmptyHost, - - /// The SPIFFE endpoint tcp socket URI has misses a port. - #[error("workload endpoint tcp socket URI host component must be an IP:port")] - TcpAddressNoIpPort, - - /// Error returned by the URI parsing library. - #[error("workload endpoint socket is not a valid URI")] - Parse(#[from] ParseError), -} - /// Validates that the `socket_path` complies with [SPIFFE standard](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_Endpoint.md#4-locating-the-endpoint). pub fn validate_socket_path(socket_path: &str) -> Result<(), SocketPathError> { let url = Url::parse(socket_path)?; @@ -112,6 +69,7 @@ pub fn validate_socket_path(socket_path: &str) -> Result<(), SocketPathError> { #[cfg(test)] mod tests { use super::*; + use url::ParseError; #[test] fn test_validate_correct_unix_address() { diff --git a/spiffe/src/error.rs b/spiffe/src/error.rs new file mode 100644 index 0000000..a8f60a3 --- /dev/null +++ b/spiffe/src/error.rs @@ -0,0 +1,100 @@ +//! Defines errors related to interactions with the GRPC client, including handling of X.509 and JWT materials, +//! SPIFFE endpoint socket path validation, and other potential failure points within the Rust-Spiffe library. +//! This encompasses errors related to endpoint configuration, response handling, data processing, and specific +//! errors for various SPIFFE components. + +use crate::bundle::jwt::JwtBundleError; +use crate::bundle::x509::X509BundleError; +use crate::spiffe_id::SpiffeIdError; +use crate::svid::jwt::JwtSvidError; +use crate::svid::x509::X509SvidError; +use thiserror::Error; +use url::ParseError; + +/// Errors that may arise while interacting with and fetching materials from a GRPC client. +/// Includes errors related to endpoint configuration, response handling, and data processing. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum GrpcClientError { + /// Missing environment variable for the endpoint socket address. + #[error("missing endpoint socket address environment variable (SPIFFE_ENDPOINT_SOCKET)")] + MissingEndpointSocketPath, + + /// The GRPC client received an empty response. + #[error("received an empty response from the GRPC client")] + EmptyResponse, + + /// Invalid endpoint socket path configuration. + #[error("invalid endpoint socket path")] + InvalidEndpointSocketPath(#[from] SocketPathError), + + /// Failed to parse the X509Svid response from the client. + #[error("failed to process X509Svid response")] + InvalidX509Svid(#[from] X509SvidError), + + /// Failed to parse the JwtSvid response from the client. + #[error("failed to process JwtSvid response")] + InvalidJwtSvid(#[from] JwtSvidError), + + /// Failed to parse the X509Bundle response from the client. + #[error("failed to process X509Bundle response")] + InvalidX509Bundle(#[from] X509BundleError), + + /// Failed to parse the JwtBundle response from the client. + #[error("failed to process JwtBundle response")] + InvalidJwtBundle(#[from] JwtBundleError), + + /// Invalid trust domain in the bundles response. + #[error("invalid trust domain in bundles response")] + InvalidTrustDomain(#[from] SpiffeIdError), + + /// Error returned by the GRPC library for error responses from the client. + #[error("error response from the GRPC client")] + Grpc(#[from] tonic::Status), + + /// Error returned by the GRPC library when creating a transport channel. + #[error("error creating transport channel to the GRPC client")] + Transport(#[from] tonic::transport::Error), +} + +/// Errors related to the validation of a SPIFFE endpoint socket path. +/// These cover scenarios such as invalid URI schemes, missing components, and unexpected URI structure. +#[derive(Debug, Error, PartialEq, Copy, Clone)] +#[non_exhaustive] +pub enum SocketPathError { + /// The SPIFFE endpoint socket URI has a scheme other than 'unix' or 'tcp'. + #[error("workload endpoint socket URI must have a tcp:// or unix:// scheme")] + InvalidScheme, + + /// The SPIFFE endpoint unix socket URI does not include a path. + #[error("workload endpoint unix socket URI must include a path")] + UnixAddressEmptyPath, + + /// The SPIFFE endpoint tcp socket URI include a path. + #[error("workload endpoint tcp socket URI must not include a path")] + TcpAddressNonEmptyPath, + + /// The SPIFFE endpoint socket URI has query values. + #[error("workload endpoint socket URI must not include query values")] + HasQueryValues, + + /// The SPIFFE endpoint socket URI has a fragment. + #[error("workload endpoint socket URI must not include a fragment")] + HasFragment, + + /// The SPIFFE endpoint socket URI has query user info. + #[error("workload endpoint socket URI must not include user info")] + HasUserInfo, + + /// The SPIFFE endpoint tcp socket URI has misses a host. + #[error("workload endpoint tcp socket URI must include a host")] + TcpEmptyHost, + + /// The SPIFFE endpoint tcp socket URI has misses a port. + #[error("workload endpoint tcp socket URI host component must be an IP:port")] + TcpAddressNoIpPort, + + /// Error returned by the URI parsing library. + #[error("workload endpoint socket is not a valid URI")] + Parse(#[from] ParseError), +} diff --git a/src/lib.rs b/spiffe/src/lib.rs similarity index 90% rename from src/lib.rs rename to spiffe/src/lib.rs index 14ce3f4..deb93d0 100644 --- a/src/lib.rs +++ b/spiffe/src/lib.rs @@ -81,9 +81,29 @@ //! # } //! ``` +#[cfg(feature = "spiffe-types")] +pub mod constants; + +#[cfg(feature = "spiffe-types")] pub mod bundle; + +#[cfg(feature = "spiffe-types")] pub mod cert; -pub(crate) mod proto; + +#[cfg(feature = "spiffe-types")] pub mod spiffe_id; + +#[cfg(feature = "spiffe-types")] pub mod svid; + +#[cfg(feature = "spiffe-types")] +pub mod error; + +#[cfg(feature = "spiffe-types")] +pub mod endpoint; + +#[cfg(feature = "workload-api")] +pub(crate) mod proto; + +#[cfg(feature = "workload-api")] pub mod workload_api; diff --git a/src/proto/google/protobuf/struct.proto b/spiffe/src/proto/google/protobuf/struct.proto similarity index 100% rename from src/proto/google/protobuf/struct.proto rename to spiffe/src/proto/google/protobuf/struct.proto diff --git a/src/proto/mod.rs b/spiffe/src/proto/mod.rs similarity index 100% rename from src/proto/mod.rs rename to spiffe/src/proto/mod.rs diff --git a/src/proto/workload.proto b/spiffe/src/proto/workload.proto similarity index 100% rename from src/proto/workload.proto rename to spiffe/src/proto/workload.proto diff --git a/src/proto/workload.rs b/spiffe/src/proto/workload.rs similarity index 100% rename from src/proto/workload.rs rename to spiffe/src/proto/workload.rs diff --git a/src/spiffe_id/mod.rs b/spiffe/src/spiffe_id/mod.rs similarity index 100% rename from src/spiffe_id/mod.rs rename to spiffe/src/spiffe_id/mod.rs diff --git a/src/svid/jwt/mod.rs b/spiffe/src/svid/jwt/mod.rs similarity index 100% rename from src/svid/jwt/mod.rs rename to spiffe/src/svid/jwt/mod.rs diff --git a/src/svid/mod.rs b/spiffe/src/svid/mod.rs similarity index 100% rename from src/svid/mod.rs rename to spiffe/src/svid/mod.rs diff --git a/src/svid/x509/mod.rs b/spiffe/src/svid/x509/mod.rs similarity index 100% rename from src/svid/x509/mod.rs rename to spiffe/src/svid/x509/mod.rs diff --git a/src/svid/x509/validations.rs b/spiffe/src/svid/x509/validations.rs similarity index 100% rename from src/svid/x509/validations.rs rename to spiffe/src/svid/x509/validations.rs diff --git a/src/workload_api/client.rs b/spiffe/src/workload_api/client.rs similarity index 76% rename from src/workload_api/client.rs rename to spiffe/src/workload_api/client.rs index 4c9bf84..6e85114 100644 --- a/src/workload_api/client.rs +++ b/spiffe/src/workload_api/client.rs @@ -33,7 +33,7 @@ //! let x509_context: X509Context = client.fetch_x509_context().await?; //! //! // watch for updates on the X.509 context -//! let mut x509_context_stream = client.watch_x509_context_stream().await?; +//! let mut x509_context_stream = client.stream_x509_contexts().await?; //! while let Some(x509_context_update) = x509_context_stream.next().await { //! match x509_context_update { //! Ok(context) => { @@ -51,22 +51,20 @@ use std::str::FromStr; -use thiserror::Error; - -use crate::bundle::jwt::{JwtBundle, JwtBundleError, JwtBundleSet}; -use crate::bundle::x509::{X509Bundle, X509BundleError, X509BundleSet}; -use crate::spiffe_id::{SpiffeId, SpiffeIdError, TrustDomain}; -use crate::svid::jwt::{JwtSvid, JwtSvidError}; -use crate::svid::x509::{X509Svid, X509SvidError}; -use crate::workload_api::address::{ - get_default_socket_path, validate_socket_path, SocketPathError, -}; +use crate::bundle::jwt::{JwtBundle, JwtBundleSet}; +use crate::bundle::x509::{X509Bundle, X509BundleSet}; +use crate::endpoint::{get_default_socket_path, validate_socket_path}; +use crate::spiffe_id::{SpiffeId, TrustDomain}; +use crate::svid::jwt::JwtSvid; +use crate::svid::x509::X509Svid; use crate::workload_api::x509_context::X509Context; use std::convert::TryFrom; use tokio::net::UnixStream; use tokio_stream::{Stream, StreamExt}; +use crate::constants::DEFAULT_SVID; +use crate::error::GrpcClientError; use crate::proto::workload::{ spiffe_workload_api_client::SpiffeWorkloadApiClient, JwtBundlesRequest, JwtBundlesResponse, JwtsvidRequest, JwtsvidResponse, ValidateJwtsvidRequest, ValidateJwtsvidResponse, @@ -75,57 +73,9 @@ use crate::proto::workload::{ use tonic::transport::{Endpoint, Uri}; use tower::service_fn; -/// The default SVID is the first in the list of SVIDs returned by the Workload API. -pub const DEFAULT_SVID: usize = 0; - const SPIFFE_HEADER_KEY: &str = "workload.spiffe.io"; const SPIFFE_HEADER_VALUE: &str = "true"; -/// An error that may arise fetching X.509 and JWT materials with the [`WorkloadApiClient`]. -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum ClientError { - /// The environment variable `SPIFFE_ENDPOINT_SOCKET` is not set. - #[error("endpoint socket address environment variable is not set")] - MissingEndpointSocketPath, - - /// The Workload API returned an empty response. - #[error("empty response from the Workload API")] - EmptyResponse, - - /// The configured Endpoint Socket path is not valid. - #[error("invalid endpoint socket path")] - EndpointSocketPath(#[from] SocketPathError), - - /// The Workload API response cannot be parsed as a [`X509Svid`]. - #[error("cannot process X509Svid response")] - InvalidX509Svid(#[from] X509SvidError), - - /// The Workload API response cannot be parsed as a [`JwtSvid`]. - #[error("cannot process X509Svid response")] - InvalidJwtSvid(#[from] JwtSvidError), - - /// The Workload API response cannot be parsed as a [`X509Bundle`]. - #[error("cannot process X509Bundle response")] - InvalidX509Bundle(#[from] X509BundleError), - - /// The Workload API response cannot be parsed as a [`JwtBundle`]. - #[error("cannot process JwtBundle response")] - InvalidJwtBundle(#[from] JwtBundleError), - - /// The Workload API response contains an invalid [`TrustDomain`] - #[error("trust domain in bundles response is invalid")] - InvalidTrustDomain(#[from] SpiffeIdError), - - /// Error returned by the GRPC library, when there is an error response from the Workload API. - #[error("error response from the Workload API")] - Grpc(#[from] tonic::Status), - - /// Error returned by the GRPC library when there is an error creating a transport channel to the Workload API. - #[error("error creating transport")] - Transport(#[from] tonic::transport::Error), -} - /// This type represents a client to interact with the Workload API. /// /// Supports one-shot calls and streaming updates for X.509 and JWT SVIDs and bundles. @@ -174,7 +124,7 @@ impl WorkloadApiClient { /// # 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 { + pub async fn new_from_path(path: &str) -> Result { validate_socket_path(path)?; // Strip the 'unix:' prefix for tonic compatibility. @@ -202,11 +152,11 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function returns a variant of [`ClientError`] if environment variable is not set or if + /// The function returns a variant of [`GrpcClientError`] if environment variable is not set or if /// the provided socket path is not valid. - pub async fn default() -> Result { + pub async fn default() -> Result { let socket_path = - get_default_socket_path().ok_or(ClientError::MissingEndpointSocketPath)?; + get_default_socket_path().ok_or(GrpcClientError::MissingEndpointSocketPath)?; Self::new_from_path(socket_path.as_str()).await } @@ -219,7 +169,7 @@ impl WorkloadApiClient { /// # Returns /// /// A `Result` containing a `WorkloadApiClient` if successful, or a `ClientError` if an error occurs. - pub fn new(conn: tonic::transport::Channel) -> Result { + pub fn new(conn: tonic::transport::Channel) -> Result { Ok(WorkloadApiClient { client: SpiffeWorkloadApiClient::with_interceptor(conn, MetadataAdder {}), }) @@ -232,12 +182,12 @@ impl WorkloadApiClient { /// # 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`]. + /// If the fetch operation or the parsing fails, it returns a [`GrpcClientError`]. /// /// # 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) -> Result { + /// Returns [`GrpcClientError`] 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) -> Result { let request = X509svidRequest::default(); let grpc_stream_response: tonic::Response> = @@ -247,7 +197,7 @@ impl WorkloadApiClient { .into_inner() .message() .await? - .ok_or(ClientError::EmptyResponse)?; + .ok_or(GrpcClientError::EmptyResponse)?; WorkloadApiClient::parse_x509_svid_from_grpc_response(response) } @@ -259,13 +209,13 @@ impl WorkloadApiClient { /// # Returns /// /// On success, it returns a `Vec` containing valid [`X509Svid`] instances, each representing a parsed SVID. - /// If the fetch operation or any parsing fails, it returns a [`ClientError`]. + /// If the fetch operation or any parsing fails, it returns a [`GrpcClientError`]. /// /// # Errors /// - /// Returns [`ClientError`] if the gRPC call fails, if the SVIDs could not be parsed from the gRPC response, + /// Returns [`GrpcClientError`] if the gRPC call fails, if the SVIDs could not be parsed from the gRPC response, /// or if the stream unexpectedly terminates. - pub async fn fetch_all_x509_svids(&mut self) -> Result, ClientError> { + pub async fn fetch_all_x509_svids(&mut self) -> Result, GrpcClientError> { let request = X509svidRequest::default(); let grpc_stream_response: tonic::Response> = @@ -275,7 +225,7 @@ impl WorkloadApiClient { .into_inner() .message() .await? - .ok_or(ClientError::EmptyResponse)?; + .ok_or(GrpcClientError::EmptyResponse)?; WorkloadApiClient::parse_x509_svids_from_grpc_response(response) } @@ -283,9 +233,9 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// The function returns a variant of [`GrpcClientError`] 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 = X509BundlesRequest::default(); let grpc_stream_response: tonic::Response> = @@ -295,7 +245,7 @@ impl WorkloadApiClient { .into_inner() .message() .await? - .ok_or(ClientError::EmptyResponse)?; + .ok_or(GrpcClientError::EmptyResponse)?; WorkloadApiClient::parse_x509_bundle_set_from_grpc_response(response) } @@ -303,9 +253,9 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// The function returns a variant of [`GrpcClientError`] 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 { + pub async fn fetch_jwt_bundles(&mut self) -> Result { let request = JwtBundlesRequest::default(); let grpc_stream_response: tonic::Response> = @@ -315,7 +265,7 @@ impl WorkloadApiClient { .into_inner() .message() .await? - .ok_or(ClientError::EmptyResponse)?; + .ok_or(GrpcClientError::EmptyResponse)?; WorkloadApiClient::parse_jwt_bundle_set_from_grpc_response(response) } @@ -324,9 +274,9 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or /// there is a problem processing the response. - pub async fn fetch_x509_context(&mut self) -> Result { + pub async fn fetch_x509_context(&mut self) -> Result { let request = X509svidRequest::default(); let grpc_stream_response: tonic::Response> = @@ -336,7 +286,7 @@ impl WorkloadApiClient { .into_inner() .message() .await? - .ok_or(ClientError::EmptyResponse)?; + .ok_or(GrpcClientError::EmptyResponse)?; WorkloadApiClient::parse_x509_context_from_grpc_response(response) } @@ -350,24 +300,22 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or /// there is a problem processing the response. /// /// IMPORTANT: If there's no registration entries with the requested [`SpiffeId`] mapped to the calling workload, - /// it will return a [`ClientError::EmptyResponse`]. + /// it will return a [`GrpcClientError::EmptyResponse`]. pub async fn fetch_jwt_svid + ToString>( &mut self, audience: &[T], spiffe_id: Option<&SpiffeId>, - ) -> Result { + ) -> Result { let response = self.fetch_jwt(audience, spiffe_id).await?; response .svids .get(DEFAULT_SVID) - .ok_or(ClientError::EmptyResponse) - .and_then(|r| { - JwtSvid::from_str(&r.svid).map_err(|err| ClientError::InvalidJwtSvid(err)) - }) + .ok_or(GrpcClientError::EmptyResponse) + .and_then(|r| JwtSvid::from_str(&r.svid).map_err(GrpcClientError::InvalidJwtSvid)) } /// Fetches a JWT token for the given audience and [`SpiffeId`]. @@ -380,22 +328,22 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or /// there is a problem processing the response. /// /// IMPORTANT: If there's no registration entries with the requested [`SpiffeId`] mapped to the calling workload, - /// it will return a [`ClientError::EmptyResponse`]. + /// it will return a [`GrpcClientError::EmptyResponse`]. pub async fn fetch_jwt_token + ToString>( &mut self, audience: &[T], spiffe_id: Option<&SpiffeId>, - ) -> Result { + ) -> Result { let response = self.fetch_jwt(audience, spiffe_id).await?; response .svids .get(DEFAULT_SVID) .map(|r| r.svid.to_string()) - .ok_or(ClientError::EmptyResponse) + .ok_or(GrpcClientError::EmptyResponse) } /// Validates a JWT SVID token against the given audience. Returns the [`JwtSvid`] parsed from @@ -408,13 +356,13 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function returns a variant of [`ClientError`] if there is en error connecting to the Workload API or + /// The function returns a variant of [`GrpcClientError`] if there is en error connecting to the Workload API or /// there is a problem processing the response. pub async fn validate_jwt_token + ToString>( &mut self, audience: T, jwt_token: &str, - ) -> Result { + ) -> Result { // validate token with Workload API, the returned claims and spiffe_id are ignored as // they are parsed from token when the `JwtSvid` object is created, this way we avoid having // to validate that the response from the Workload API contains correct claims. @@ -435,20 +383,20 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// The function can return an error variant of [`GrpcClientError`] 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 watch_x509_context_stream( + pub async fn stream_x509_contexts( &mut self, - ) -> Result>, ClientError> { + ) -> Result>, GrpcClientError> { let request = X509svidRequest::default(); let response = self.client.fetch_x509svid(request).await?; let stream = response.into_inner().map(|message| { message - .map_err(ClientError::from) + .map_err(GrpcClientError::from) .and_then(WorkloadApiClient::parse_x509_context_from_grpc_response) }); Ok(stream) @@ -466,20 +414,20 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// The function can return an error variant of [`GrpcClientError`] 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 watch_x509_svid_stream( + pub async fn stream_x509_svids( &mut self, - ) -> Result>, ClientError> { + ) -> Result>, GrpcClientError> { let request = X509svidRequest::default(); let response = self.client.fetch_x509svid(request).await?; let stream = response.into_inner().map(|message| { message - .map_err(ClientError::from) + .map_err(GrpcClientError::from) .and_then(WorkloadApiClient::parse_x509_svid_from_grpc_response) }); Ok(stream) @@ -497,20 +445,20 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// The function can return an error variant of [`GrpcClientError`] 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 watch_x509_bundles_stream( + pub async fn stream_x509_bundles( &mut self, - ) -> Result>, ClientError> { + ) -> Result>, GrpcClientError> { let request = X509BundlesRequest::default(); let response = self.client.fetch_x509_bundles(request).await?; let stream = response.into_inner().map(|message| { message - .map_err(ClientError::from) + .map_err(GrpcClientError::from) .and_then(WorkloadApiClient::parse_x509_bundle_set_from_grpc_response) }); Ok(stream) @@ -528,20 +476,20 @@ impl WorkloadApiClient { /// /// # Errors /// - /// The function can return an error variant of [`ClientError`] in the following scenarios: + /// The function can return an error variant of [`GrpcClientError`] 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 watch_jwt_bundles_stream( + pub async fn stream_jwt_bundles( &mut self, - ) -> Result>, ClientError> { + ) -> Result>, GrpcClientError> { let request = JwtBundlesRequest::default(); let response = self.client.fetch_jwt_bundles(request).await?; let stream = response.into_inner().map(|message| { message - .map_err(ClientError::from) + .map_err(GrpcClientError::from) .and_then(WorkloadApiClient::parse_jwt_bundle_set_from_grpc_response) }); Ok(stream) @@ -554,7 +502,7 @@ impl WorkloadApiClient { &mut self, audience: &[T], spiffe_id: Option<&SpiffeId>, - ) -> Result { + ) -> Result { let request = JwtsvidRequest { spiffe_id: spiffe_id.map(ToString::to_string).unwrap_or_default(), audience: audience.iter().map(|s| s.to_string()).collect(), @@ -567,7 +515,7 @@ impl WorkloadApiClient { &mut self, audience: T, jwt_svid: &str, - ) -> Result { + ) -> Result { let request = ValidateJwtsvidRequest { audience: audience.as_ref().into(), svid: jwt_svid.into(), @@ -578,25 +526,25 @@ impl WorkloadApiClient { fn parse_x509_svid_from_grpc_response( response: X509svidResponse, - ) -> Result { + ) -> Result { let svid = response .svids .get(DEFAULT_SVID) - .ok_or(ClientError::EmptyResponse)?; + .ok_or(GrpcClientError::EmptyResponse)?; X509Svid::parse_from_der(svid.x509_svid.as_ref(), svid.x509_svid_key.as_ref()) - .map_err(ClientError::from) + .map_err(GrpcClientError::from) } fn parse_x509_svids_from_grpc_response( response: X509svidResponse, - ) -> Result, ClientError> { + ) -> Result, GrpcClientError> { let mut svids_vec = Vec::new(); for svid in response.svids.iter() { let parsed_svid = X509Svid::parse_from_der(svid.x509_svid.as_ref(), svid.x509_svid_key.as_ref()) - .map_err(ClientError::from)?; + .map_err(GrpcClientError::from)?; svids_vec.push(parsed_svid); } @@ -606,13 +554,14 @@ impl WorkloadApiClient { fn parse_x509_bundle_set_from_grpc_response( response: X509BundlesResponse, - ) -> Result { + ) -> Result { let bundles: Result, _> = response .bundles .into_iter() .map(|(td, bundle_data)| { let trust_domain = TrustDomain::try_from(td)?; - X509Bundle::parse_from_der(trust_domain, &bundle_data).map_err(ClientError::from) + X509Bundle::parse_from_der(trust_domain, &bundle_data) + .map_err(GrpcClientError::from) }) .collect(); @@ -626,13 +575,13 @@ impl WorkloadApiClient { fn parse_jwt_bundle_set_from_grpc_response( response: JwtBundlesResponse, - ) -> Result { + ) -> 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)?; + .map_err(GrpcClientError::from)?; bundle_set.add_bundle(bundle); } @@ -642,20 +591,20 @@ impl WorkloadApiClient { fn parse_x509_context_from_grpc_response( response: X509svidResponse, - ) -> Result { + ) -> Result { let mut svids = Vec::new(); let mut bundle_set = X509BundleSet::new(); for svid in response.svids.into_iter() { let x509_svid = X509Svid::parse_from_der(svid.x509_svid.as_ref(), svid.x509_svid_key.as_ref()) - .map_err(ClientError::from)?; + .map_err(GrpcClientError::from)?; let trust_domain = x509_svid.spiffe_id().trust_domain().clone(); svids.push(x509_svid); let bundle = X509Bundle::parse_from_der(trust_domain, svid.bundle.as_ref()) - .map_err(ClientError::from)?; + .map_err(GrpcClientError::from)?; bundle_set.add_bundle(bundle); } diff --git a/src/workload_api/mod.rs b/spiffe/src/workload_api/mod.rs similarity index 98% rename from src/workload_api/mod.rs rename to spiffe/src/workload_api/mod.rs index ddc9c8a..ef6b67b 100644 --- a/src/workload_api/mod.rs +++ b/spiffe/src/workload_api/mod.rs @@ -3,7 +3,7 @@ //! # Examples //! //! ```no_run -//! +//! //! use std::error::Error; //! use spiffe::workload_api::client::WorkloadApiClient; //! @@ -39,6 +39,5 @@ //! # Ok(()) //! # } //! ``` -pub mod address; pub mod client; pub mod x509_context; diff --git a/src/workload_api/x509_context.rs b/spiffe/src/workload_api/x509_context.rs similarity index 95% rename from src/workload_api/x509_context.rs rename to spiffe/src/workload_api/x509_context.rs index 94ae911..ed6f8e2 100644 --- a/src/workload_api/x509_context.rs +++ b/spiffe/src/workload_api/x509_context.rs @@ -1,8 +1,8 @@ //! Defines a type that holds all the X.509 materials for a workload (i.e. X.509 SVIDs and bundles) use crate::bundle::x509::X509BundleSet; +use crate::constants::DEFAULT_SVID; use crate::svid::x509::X509Svid; -use crate::workload_api::client::DEFAULT_SVID; /// Represents all X.509 materials fetched from the Workload API. #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/tests/testdata/bundle/x509/bundle.der b/spiffe/tests/testdata/bundle/x509/bundle.der similarity index 100% rename from tests/testdata/bundle/x509/bundle.der rename to spiffe/tests/testdata/bundle/x509/bundle.der diff --git a/tests/testdata/bundle/x509/cert1.der b/spiffe/tests/testdata/bundle/x509/cert1.der similarity index 100% rename from tests/testdata/bundle/x509/cert1.der rename to spiffe/tests/testdata/bundle/x509/cert1.der diff --git a/tests/testdata/bundle/x509/cert2.der b/spiffe/tests/testdata/bundle/x509/cert2.der similarity index 100% rename from tests/testdata/bundle/x509/cert2.der rename to spiffe/tests/testdata/bundle/x509/cert2.der diff --git a/tests/testdata/bundle/x509/corrupted b/spiffe/tests/testdata/bundle/x509/corrupted similarity index 100% rename from tests/testdata/bundle/x509/corrupted rename to spiffe/tests/testdata/bundle/x509/corrupted diff --git a/tests/testdata/svid/x509/1-key.der b/spiffe/tests/testdata/svid/x509/1-key.der similarity index 100% rename from tests/testdata/svid/x509/1-key.der rename to spiffe/tests/testdata/svid/x509/1-key.der diff --git a/tests/testdata/svid/x509/1-svid-chain.der b/spiffe/tests/testdata/svid/x509/1-svid-chain.der similarity index 100% rename from tests/testdata/svid/x509/1-svid-chain.der rename to spiffe/tests/testdata/svid/x509/1-svid-chain.der diff --git a/tests/testdata/svid/x509/corrupted b/spiffe/tests/testdata/svid/x509/corrupted similarity index 100% rename from tests/testdata/svid/x509/corrupted rename to spiffe/tests/testdata/svid/x509/corrupted diff --git a/tests/testdata/svid/x509/svid-with-dns-key.der b/spiffe/tests/testdata/svid/x509/svid-with-dns-key.der similarity index 100% rename from tests/testdata/svid/x509/svid-with-dns-key.der rename to spiffe/tests/testdata/svid/x509/svid-with-dns-key.der diff --git a/tests/testdata/svid/x509/svid-with-dns.der b/spiffe/tests/testdata/svid/x509/svid-with-dns.der similarity index 100% rename from tests/testdata/svid/x509/svid-with-dns.der rename to spiffe/tests/testdata/svid/x509/svid-with-dns.der diff --git a/tests/testdata/svid/x509/wrong-intermediate-no-ca.der b/spiffe/tests/testdata/svid/x509/wrong-intermediate-no-ca.der similarity index 100% rename from tests/testdata/svid/x509/wrong-intermediate-no-ca.der rename to spiffe/tests/testdata/svid/x509/wrong-intermediate-no-ca.der diff --git a/tests/testdata/svid/x509/wrong-intermediate-no-key-cert-sign.der b/spiffe/tests/testdata/svid/x509/wrong-intermediate-no-key-cert-sign.der similarity index 100% rename from tests/testdata/svid/x509/wrong-intermediate-no-key-cert-sign.der rename to spiffe/tests/testdata/svid/x509/wrong-intermediate-no-key-cert-sign.der diff --git a/tests/testdata/svid/x509/wrong-leaf-ca-true.der b/spiffe/tests/testdata/svid/x509/wrong-leaf-ca-true.der similarity index 100% rename from tests/testdata/svid/x509/wrong-leaf-ca-true.der rename to spiffe/tests/testdata/svid/x509/wrong-leaf-ca-true.der diff --git a/tests/testdata/svid/x509/wrong-leaf-cert-sign.der b/spiffe/tests/testdata/svid/x509/wrong-leaf-cert-sign.der similarity index 100% rename from tests/testdata/svid/x509/wrong-leaf-cert-sign.der rename to spiffe/tests/testdata/svid/x509/wrong-leaf-cert-sign.der diff --git a/tests/testdata/svid/x509/wrong-leaf-crl-sign.der b/spiffe/tests/testdata/svid/x509/wrong-leaf-crl-sign.der similarity index 100% rename from tests/testdata/svid/x509/wrong-leaf-crl-sign.der rename to spiffe/tests/testdata/svid/x509/wrong-leaf-crl-sign.der diff --git a/tests/testdata/svid/x509/wrong-leaf-empty-id.der b/spiffe/tests/testdata/svid/x509/wrong-leaf-empty-id.der similarity index 100% rename from tests/testdata/svid/x509/wrong-leaf-empty-id.der rename to spiffe/tests/testdata/svid/x509/wrong-leaf-empty-id.der diff --git a/tests/testdata/svid/x509/wrong-leaf-no-digital-signature.der b/spiffe/tests/testdata/svid/x509/wrong-leaf-no-digital-signature.der similarity index 100% rename from tests/testdata/svid/x509/wrong-leaf-no-digital-signature.der rename to spiffe/tests/testdata/svid/x509/wrong-leaf-no-digital-signature.der diff --git a/tests/workload_api_client_test.rs b/spiffe/tests/workload_api_client_test.rs similarity index 97% rename from tests/workload_api_client_test.rs rename to spiffe/tests/workload_api_client_test.rs index eb8fe76..9aeefb0 100644 --- a/tests/workload_api_client_test.rs +++ b/spiffe/tests/workload_api_client_test.rs @@ -182,7 +182,7 @@ mod integration_tests { } #[tokio::test] - async fn watch_x509_context_stream() { + async fn stream_x509_contexts() { let mut client = get_client().await; let test_duration = std::time::Duration::from_secs(60); let expected_ids = vec![&*SPIFFE_ID_1, &*SPIFFE_ID_2]; @@ -190,7 +190,7 @@ mod integration_tests { let result = tokio::time::timeout(test_duration, async { let mut update_count = 0; let mut stream = client - .watch_x509_context_stream() + .stream_x509_contexts() .await .expect("Failed to get stream"); @@ -234,7 +234,7 @@ mod integration_tests { } #[tokio::test] - async fn watch_x509_svid_stream() { + async fn stream_x509_svids() { let mut client = get_client().await; let test_duration = std::time::Duration::from_secs(60); let expected_ids = vec![&*SPIFFE_ID_1, &*SPIFFE_ID_2]; @@ -242,7 +242,7 @@ mod integration_tests { let result = tokio::time::timeout(test_duration, async { let mut update_count = 0; let mut stream = client - .watch_x509_svid_stream() + .stream_x509_svids() .await .expect("Failed to get stream"); @@ -275,13 +275,13 @@ mod integration_tests { } #[tokio::test] - async fn watch_x509_bundles_stream() { + async fn stream_x509_bundles() { let mut client = get_client().await; let test_duration = std::time::Duration::from_secs(60); let result = tokio::time::timeout(test_duration, async { let mut stream = client - .watch_x509_bundles_stream() + .stream_x509_bundles() .await .expect("Failed to get stream"); if let Some(update) = stream.next().await { @@ -308,13 +308,13 @@ mod integration_tests { } #[tokio::test] - async fn watch_jwt_bundles_stream() { + async fn stream_jwt_bundles() { let mut client = get_client().await; let test_duration = std::time::Duration::from_secs(60); let result = tokio::time::timeout(test_duration, async { let mut stream = client - .watch_jwt_bundles_stream() + .stream_jwt_bundles() .await .expect("Failed to get stream"); if let Some(update) = stream.next().await { diff --git a/tests/x509_bundle_test.rs b/spiffe/tests/x509_bundle_test.rs similarity index 100% rename from tests/x509_bundle_test.rs rename to spiffe/tests/x509_bundle_test.rs diff --git a/tests/x509_svid_test.rs b/spiffe/tests/x509_svid_test.rs similarity index 100% rename from tests/x509_svid_test.rs rename to spiffe/tests/x509_svid_test.rs diff --git a/spire-api-sdk b/spire-api-sdk new file mode 160000 index 0000000..2523933 --- /dev/null +++ b/spire-api-sdk @@ -0,0 +1 @@ +Subproject commit 2523933d05a856488d88c3d54237a384be0aa883 diff --git a/spire-api/CHANGELOG.md b/spire-api/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/spire-api/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/spire-api/Cargo.toml b/spire-api/Cargo.toml new file mode 100644 index 0000000..62f576f --- /dev/null +++ b/spire-api/Cargo.toml @@ -0,0 +1,36 @@ +[package] +edition = "2018" +name = "spire-api" +# When releasing to crates.io: +# - Update CHANGELOG.md. +# - Create a new tag +version = "0.1.0" +authors = ["Max Lambrecht "] +description = "Rust library for the SPIRE APIs" +license = "Apache-2.0" +repository = "https://github.com/maxlambrecht/rust-spiffe" +documentation = "https://docs.rs/spire-api" +readme = "README.md" +categories = ["cryptography"] +keywords = ["SPIFFE", "SPIRE"] + +[dependencies] +spiffe = { path = "../spiffe", default-features = false, features = ["spiffe-types"] } +bytes = { version = "1", features = ["serde"] } +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"] } + +[dev-dependencies] +once_cell = "1.18" + +[build-dependencies] +tonic-build = { version = "0.9", default-features = false, features = ["prost"] } +prost-build = "0.11" +anyhow = "1.0.65" + +[features] +integration-tests = [] diff --git a/spire-api/README.md b/spire-api/README.md new file mode 100644 index 0000000..989d1ec --- /dev/null +++ b/spire-api/README.md @@ -0,0 +1,36 @@ +# Rust SPIRE API Library + +This library provides support for SPIRE specific APIs in Rust. + +## Features + +- **Delegated Identity API support**: Facilitates authorized workloads to obtain SVIDs (SPIFFE Verifiable Identity + Documents) and bundles on behalf of others that cannot be directly attested by SPIRE Agent. This feature enhances + identity support for complex scenarios, including those where workloads cannot be directly recognized by SPIRE. + +## Installation + +Include this line in your `Cargo.toml`: + +```toml +[dependencies] +spire-api = "0.1.0" +``` + +## Usage + +A basic example: + +```rust +use spire_api::delegated_identity; +// Your code here... +``` + +## Delegated Identity API + +For more information about the SPIRE Delegated Identity API, refer to +the [official documentation](https://spiffe.io/docs/latest/spire/using/getting-started/). + +## License + +This library is licensed under the Apache License. See the [LICENSE.md](../LICENSE) file for details. diff --git a/spire-api/build.rs b/spire-api/build.rs new file mode 100644 index 0000000..25651ed --- /dev/null +++ b/spire-api/build.rs @@ -0,0 +1,14 @@ +fn main() -> Result<(), anyhow::Error> { + let mut proto_config = prost_build::Config::new(); + proto_config.bytes(["."]); + tonic_build::configure() + .build_client(true) + .out_dir("src/proto") + .compile_with_config( + proto_config, + &["../spire-api-sdk/proto/spire/api/agent/delegatedidentity/v1/delegatedidentity.proto"], + &["../spire-api-sdk/proto"], + )?; + + Ok(()) +} diff --git a/spire-api/src/agent/delegated_identity.rs b/spire-api/src/agent/delegated_identity.rs new file mode 100644 index 0000000..429b785 --- /dev/null +++ b/spire-api/src/agent/delegated_identity.rs @@ -0,0 +1,394 @@ +//! 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 crate::proto::spire::api::agent::delegatedidentity::v1::{ + delegated_identity_client::DelegatedIdentityClient as DelegatedIdentityApiClient, + FetchJwtsviDsRequest, SubscribeToJwtBundlesRequest, SubscribeToJwtBundlesResponse, + SubscribeToX509BundlesRequest, SubscribeToX509BundlesResponse, SubscribeToX509sviDsRequest, + SubscribeToX509sviDsResponse, +}; +use crate::proto::spire::api::types::Jwtsvid as ProtoJwtSvid; +use spiffe::bundle::jwt::{JwtBundle, JwtBundleSet}; +use spiffe::bundle::x509::{X509Bundle, X509BundleSet}; +use spiffe::endpoint::validate_socket_path; +use spiffe::spiffe_id::TrustDomain; +use spiffe::svid::jwt::JwtSvid; +use spiffe::svid::x509::X509Svid; +use tokio_stream::{Stream, StreamExt}; + +use crate::selectors::Selector; +use spiffe::constants::DEFAULT_SVID; +use spiffe::error::GrpcClientError; +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 = "SPIRE_ADMIN_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) { + 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 [`GrpcClientError`] 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(GrpcClientError::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 [`GrpcClientError`]. + /// + /// # Errors + /// + /// Returns [`GrpcClientError`] 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(GrpcClientError::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 [`GrpcClientError`] 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>, GrpcClientError> { + 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(GrpcClientError::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 [`GrpcClientError`] 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 [`GrpcClientError`] 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>, GrpcClientError> { + 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(GrpcClientError::from) + .and_then(DelegatedIdentityClient::parse_x509_bundle_set_from_grpc_response) + }); + + 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 [`GrpcClientError`] 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, GrpcClientError> { + 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 [`GrpcClientError`] 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>, GrpcClientError> { + let request = SubscribeToJwtBundlesRequest::default(); + let response = self.client.subscribe_to_jwt_bundles(request).await?; + Ok(response.into_inner().map(|message| { + message + .map_err(GrpcClientError::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 [`GrpcClientError`] 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(GrpcClientError::EmptyResponse)?, + ) + } +} + +impl DelegatedIdentityClient { + fn parse_x509_svid_from_grpc_response( + response: SubscribeToX509sviDsResponse, + ) -> Result { + let svid = response + .x509_svids + .get(DEFAULT_SVID) + .ok_or(GrpcClientError::EmptyResponse)?; + + let x509_svid = svid + .x509_svid + .as_ref() + .ok_or(GrpcClientError::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_bytes, svid.x509_svid_key.as_ref()) + .map_err(|e| e.into()) + } + + fn parse_jwt_svid_from_grpc_response( + svids: Vec, + ) -> Result, GrpcClientError> { + let result: Result, GrpcClientError> = svids + .iter() + .map(|r| JwtSvid::from_str(&r.token).map_err(GrpcClientError::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(GrpcClientError::from)?; + + bundle_set.add_bundle(bundle); + } + + Ok(bundle_set) + } + + 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(GrpcClientError::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..23dbc96 --- /dev/null +++ b/spire-api/src/agent/mod.rs @@ -0,0 +1,10 @@ +//! Agent API +//! +//! Consists of the following APIs: +//! - `delegated_identity`: For managing delegated identities. +//! - `debug`: (Not yet implemented). +//! +//! # Note +//! 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/lib.rs b/spire-api/src/lib.rs new file mode 100644 index 0000000..afb9a58 --- /dev/null +++ b/spire-api/src/lib.rs @@ -0,0 +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/proto/mod.rs b/spire-api/src/proto/mod.rs new file mode 100644 index 0000000..f1007f0 --- /dev/null +++ b/spire-api/src/proto/mod.rs @@ -0,0 +1,15 @@ +pub mod spire { + pub mod api { + pub mod agent { + pub mod delegatedidentity { + pub mod v1 { + include!("spire.api.agent.delegatedidentity.v1.rs"); + } + } + } + + pub mod types { + include!("spire.api.types.rs"); + } + } +} diff --git a/spire-api/src/proto/spire.api.agent.delegatedidentity.v1.rs b/spire-api/src/proto/spire.api.agent.delegatedidentity.v1.rs new file mode 100644 index 0000000..cdab0c1 --- /dev/null +++ b/spire-api/src/proto/spire.api.agent.delegatedidentity.v1.rs @@ -0,0 +1,692 @@ +/// X.509 SPIFFE Verifiable Identity Document with the private key. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct X509svidWithKey { + /// The workload X509-SVID. + #[prost(message, optional, tag = "1")] + pub x509_svid: ::core::option::Option, + /// Private key (encoding DER PKCS#8). + #[prost(bytes = "bytes", tag = "2")] + pub x509_svid_key: ::prost::bytes::Bytes, +} +/// SubscribeToX509SVIDsRequest is used by clients to subscribe the set of SVIDs that +/// any given workload is entitled to. Clients subscribe to a workload's SVIDs by providing +/// a set of selectors describing the workload. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeToX509sviDsRequest { + /// Required. Selectors describing the workload to subscribe to. + #[prost(message, repeated, tag = "1")] + pub selectors: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeToX509sviDsResponse { + #[prost(message, repeated, tag = "1")] + pub x509_svids: ::prost::alloc::vec::Vec, + /// Names of the trust domains that this workload should federates with. + #[prost(string, repeated, tag = "2")] + pub federates_with: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeToX509BundlesRequest {} +/// SubscribeToX509BundlesResponse contains all bundles that the agent is tracking, +/// including the local bundle. When an update occurs, or bundles are added or removed, +/// a new response with the full set of bundles is sent. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeToX509BundlesResponse { + /// A map keyed by trust domain name, with ASN.1 DER-encoded + /// X.509 CA certificates as the values + #[prost(map = "string, bytes", tag = "1")] + pub ca_certificates: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::bytes::Bytes, + >, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FetchJwtsviDsRequest { + /// Required. The audience(s) the workload intends to authenticate against. + #[prost(string, repeated, tag = "1")] + pub audience: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// Required. Selectors describing the workload to fetch. + #[prost(message, repeated, tag = "2")] + pub selectors: ::prost::alloc::vec::Vec, +} +/// The FetchJWTSVIDsResponse message conveys JWT-SVIDs. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct FetchJwtsviDsResponse { + /// Required. The list of returned JWT-SVIDs. + #[prost(message, repeated, tag = "1")] + pub svids: ::prost::alloc::vec::Vec, +} +/// The SubscribeToJWTBundlesRequest message conveys parameters for requesting JWKS bundles. +/// There are currently no such parameters. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeToJwtBundlesRequest {} +/// The SubscribeToJWTBundlesReponse conveys JWKS bundles. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SubscribeToJwtBundlesResponse { + /// Required. JWK encoded JWT bundles, keyed by the SPIFFE ID of the trust + /// domain. + #[prost(map = "string, bytes", tag = "1")] + pub bundles: ::std::collections::HashMap< + ::prost::alloc::string::String, + ::prost::bytes::Bytes, + >, +} +/// Generated client implementations. +pub mod delegated_identity_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// The delegatedIdentity service provides an interface to get the SVIDs of other + /// workloads on the host. This service is intended for use cases where a process + /// (different than the workload one) should access the workload's SVID to + /// perform actions on behalf of the workload. One example of is using a single + /// node instance of Envoy that upgrades TCP connections for different processes + /// running in such a node. + /// + /// The caller must be local and its identity must be listed in the allowed + /// clients on the spire-agent configuration. + #[derive(Debug, Clone)] + pub struct DelegatedIdentityClient { + inner: tonic::client::Grpc, + } + impl DelegatedIdentityClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> DelegatedIdentityClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + DelegatedIdentityClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// Subscribe to get X.509-SVIDs for workloads that match the given selectors. + /// The lifetime of the subscription aligns to the lifetime of the stream. + pub async fn subscribe_to_x509svi_ds( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/SubscribeToX509SVIDs", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "spire.api.agent.delegatedidentity.v1.DelegatedIdentity", + "SubscribeToX509SVIDs", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Subscribe to get local and all federated bundles. + /// The lifetime of the subscription aligns to the lifetime of the stream. + pub async fn subscribe_to_x509_bundles( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/SubscribeToX509Bundles", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "spire.api.agent.delegatedidentity.v1.DelegatedIdentity", + "SubscribeToX509Bundles", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + /// Fetch JWT-SVIDs for workloads that match the given selectors, and + /// for the requested audience. + pub async fn fetch_jwtsvi_ds( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/FetchJWTSVIDs", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "spire.api.agent.delegatedidentity.v1.DelegatedIdentity", + "FetchJWTSVIDs", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Subscribe to get local and all federated JWKS bundles. + /// The lifetime of the subscription aligns to the lifetime of the stream. + pub async fn subscribe_to_jwt_bundles( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response< + tonic::codec::Streaming, + >, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/SubscribeToJWTBundles", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "spire.api.agent.delegatedidentity.v1.DelegatedIdentity", + "SubscribeToJWTBundles", + ), + ); + self.inner.server_streaming(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod delegated_identity_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with DelegatedIdentityServer. + #[async_trait] + pub trait DelegatedIdentity: Send + Sync + 'static { + /// Server streaming response type for the SubscribeToX509SVIDs method. + type SubscribeToX509SVIDsStream: futures_core::Stream< + Item = std::result::Result< + super::SubscribeToX509sviDsResponse, + tonic::Status, + >, + > + + Send + + 'static; + /// Subscribe to get X.509-SVIDs for workloads that match the given selectors. + /// The lifetime of the subscription aligns to the lifetime of the stream. + async fn subscribe_to_x509svi_ds( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Server streaming response type for the SubscribeToX509Bundles method. + type SubscribeToX509BundlesStream: futures_core::Stream< + Item = std::result::Result< + super::SubscribeToX509BundlesResponse, + tonic::Status, + >, + > + + Send + + 'static; + /// Subscribe to get local and all federated bundles. + /// The lifetime of the subscription aligns to the lifetime of the stream. + async fn subscribe_to_x509_bundles( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Fetch JWT-SVIDs for workloads that match the given selectors, and + /// for the requested audience. + async fn fetch_jwtsvi_ds( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + /// Server streaming response type for the SubscribeToJWTBundles method. + type SubscribeToJWTBundlesStream: futures_core::Stream< + Item = std::result::Result< + super::SubscribeToJwtBundlesResponse, + tonic::Status, + >, + > + + Send + + 'static; + /// Subscribe to get local and all federated JWKS bundles. + /// The lifetime of the subscription aligns to the lifetime of the stream. + async fn subscribe_to_jwt_bundles( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// The delegatedIdentity service provides an interface to get the SVIDs of other + /// workloads on the host. This service is intended for use cases where a process + /// (different than the workload one) should access the workload's SVID to + /// perform actions on behalf of the workload. One example of is using a single + /// node instance of Envoy that upgrades TCP connections for different processes + /// running in such a node. + /// + /// The caller must be local and its identity must be listed in the allowed + /// clients on the spire-agent configuration. + #[derive(Debug)] + pub struct DelegatedIdentityServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl DelegatedIdentityServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for DelegatedIdentityServer + where + T: DelegatedIdentity, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/SubscribeToX509SVIDs" => { + #[allow(non_camel_case_types)] + struct SubscribeToX509SVIDsSvc(pub Arc); + impl< + T: DelegatedIdentity, + > tonic::server::ServerStreamingService< + super::SubscribeToX509sviDsRequest, + > for SubscribeToX509SVIDsSvc { + type Response = super::SubscribeToX509sviDsResponse; + type ResponseStream = T::SubscribeToX509SVIDsStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + (*inner).subscribe_to_x509svi_ds(request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = SubscribeToX509SVIDsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.server_streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/SubscribeToX509Bundles" => { + #[allow(non_camel_case_types)] + struct SubscribeToX509BundlesSvc(pub Arc); + impl< + T: DelegatedIdentity, + > tonic::server::ServerStreamingService< + super::SubscribeToX509BundlesRequest, + > for SubscribeToX509BundlesSvc { + type Response = super::SubscribeToX509BundlesResponse; + type ResponseStream = T::SubscribeToX509BundlesStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + (*inner).subscribe_to_x509_bundles(request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = SubscribeToX509BundlesSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.server_streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/FetchJWTSVIDs" => { + #[allow(non_camel_case_types)] + struct FetchJWTSVIDsSvc(pub Arc); + impl< + T: DelegatedIdentity, + > tonic::server::UnaryService + for FetchJWTSVIDsSvc { + type Response = super::FetchJwtsviDsResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + (*inner).fetch_jwtsvi_ds(request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = FetchJWTSVIDsSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/spire.api.agent.delegatedidentity.v1.DelegatedIdentity/SubscribeToJWTBundles" => { + #[allow(non_camel_case_types)] + struct SubscribeToJWTBundlesSvc(pub Arc); + impl< + T: DelegatedIdentity, + > tonic::server::ServerStreamingService< + super::SubscribeToJwtBundlesRequest, + > for SubscribeToJWTBundlesSvc { + type Response = super::SubscribeToJwtBundlesResponse; + type ResponseStream = T::SubscribeToJWTBundlesStream; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + (*inner).subscribe_to_jwt_bundles(request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = SubscribeToJWTBundlesSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.server_streaming(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for DelegatedIdentityServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService + for DelegatedIdentityServer { + const NAME: &'static str = "spire.api.agent.delegatedidentity.v1.DelegatedIdentity"; + } +} diff --git a/spire-api/src/proto/spire.api.types.rs b/spire-api/src/proto/spire.api.types.rs new file mode 100644 index 0000000..47db65c --- /dev/null +++ b/spire-api/src/proto/spire.api.types.rs @@ -0,0 +1,173 @@ +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Selector { + /// The type of the selector. This is typically the name of the plugin that + /// produces the selector. + #[prost(string, tag = "1")] + pub r#type: ::prost::alloc::string::String, + /// The value of the selector. + #[prost(string, tag = "2")] + pub value: ::prost::alloc::string::String, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct SelectorMatch { + /// The set of selectors to match on. + #[prost(message, repeated, tag = "1")] + pub selectors: ::prost::alloc::vec::Vec, + /// How to match the selectors. + #[prost(enumeration = "selector_match::MatchBehavior", tag = "2")] + pub r#match: i32, +} +/// Nested message and enum types in `SelectorMatch`. +pub mod selector_match { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum MatchBehavior { + /// Indicates that the selectors in this match are equal to the + /// candidate selectors, independent of ordering. + /// Example: + /// Given: + /// - 'e1 { Selectors: ["a:1", "b:2", "c:3"]}' + /// - 'e2 { Selectors: ["a:1", "b:2"]}' + /// - 'e3 { Selectors: \["a:1"\]}' + /// Operation: + /// - MATCH_EXACT ["a:1", "b:2"] + /// Entries that match: + /// - 'e2' + MatchExact = 0, + /// Indicates that all candidates which have a non-empty subset + /// of the provided set of selectors will match. + /// Example: + /// Given: + /// - 'e1 { Selectors: ["a:1", "b:2", "c:3"]}' + /// - 'e2 { Selectors: ["a:1", "b:2"]}' + /// - 'e3 { Selectors: \["a:1"\]}' + /// Operation: + /// - MATCH_SUBSET \["a:1"\] + /// Entries that match: + /// - 'e1' + MatchSubset = 1, + /// Indicates that all candidates which are a superset + /// of the provided selectors will match. + /// Example: + /// Given: + /// - 'e1 { Selectors: ["a:1", "b:2", "c:3"]}' + /// - 'e2 { Selectors: ["a:1", "b:2"]}' + /// - 'e3 { Selectors: \["a:1"\]}' + /// Operation: + /// - MATCH_SUPERSET ["a:1", "b:2"] + /// Entries that match: + /// - 'e1' + /// - 'e2' + MatchSuperset = 2, + /// Indicates that all candidates which have at least one + /// of the provided set of selectors will match. + /// Example: + /// Given: + /// - 'e1 { Selectors: ["a:1", "b:2", "c:3"]}' + /// - 'e2 { Selectors: ["a:1", "b:2"]}' + /// - 'e3 { Selectors: \["a:1"\]}' + /// Operation: + /// - MATCH_ANY \["a:1"\] + /// Entries that match: + /// - 'e1' + /// - 'e2' + /// - 'e3' + MatchAny = 3, + } + impl MatchBehavior { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + MatchBehavior::MatchExact => "MATCH_EXACT", + MatchBehavior::MatchSubset => "MATCH_SUBSET", + MatchBehavior::MatchSuperset => "MATCH_SUPERSET", + MatchBehavior::MatchAny => "MATCH_ANY", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "MATCH_EXACT" => Some(Self::MatchExact), + "MATCH_SUBSET" => Some(Self::MatchSubset), + "MATCH_SUPERSET" => Some(Self::MatchSuperset), + "MATCH_ANY" => Some(Self::MatchAny), + _ => None, + } + } + } +} +/// A SPIFFE ID, consisting of the trust domain name and a path portions of +/// the SPIFFE ID URI. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Spiffeid { + /// Trust domain portion the SPIFFE ID (e.g. "example.org") + #[prost(string, tag = "1")] + pub trust_domain: ::prost::alloc::string::String, + /// The path component of the SPIFFE ID (e.g. "/foo/bar/baz"). The path + /// SHOULD have a leading slash. Consumers MUST normalize the path before + /// making any sort of comparison between IDs. + #[prost(string, tag = "2")] + pub path: ::prost::alloc::string::String, +} +/// X.509 SPIFFE Verifiable Identity Document. It contains the raw X.509 +/// certificate data as well as a few denormalized fields for convenience. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct X509svid { + /// Certificate and intermediates required to form a chain of trust back to + /// the X.509 authorities of the trust domain (ASN.1 DER encoded). + #[prost(bytes = "bytes", repeated, tag = "1")] + pub cert_chain: ::prost::alloc::vec::Vec<::prost::bytes::Bytes>, + /// SPIFFE ID of the SVID. + #[prost(message, optional, tag = "2")] + pub id: ::core::option::Option, + /// Expiration timestamp (seconds since Unix epoch). + #[prost(int64, tag = "3")] + pub expires_at: i64, + /// Optional. An operator-specified string used to provide guidance on how this + /// identity should be used by a workload when more than one SVID is returned. + /// For example, `internal` and `external` to indicate an SVID for internal or + /// external use, respectively. + #[prost(string, tag = "4")] + pub hint: ::prost::alloc::string::String, +} +/// JWT SPIFFE Verifiable Identity Document. It contains the raw JWT token +/// as well as a few denormalized fields for convenience. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Jwtsvid { + /// The serialized JWT token. + #[prost(string, tag = "1")] + pub token: ::prost::alloc::string::String, + /// The SPIFFE ID of the JWT-SVID. + #[prost(message, optional, tag = "2")] + pub id: ::core::option::Option, + /// Expiration timestamp (seconds since Unix epoch). + #[prost(int64, tag = "3")] + pub expires_at: i64, + /// Issuance timestamp (seconds since Unix epoch). + #[prost(int64, tag = "4")] + pub issued_at: i64, + /// Optional. An operator-specified string used to provide guidance on how this + /// identity should be used by a workload when more than one SVID is returned. + /// For example, `internal` and `external` to indicate an SVID for internal or + /// external use, respectively. + #[prost(string, tag = "5")] + pub hint: ::prost::alloc::string::String, +} diff --git a/spire-api/src/selectors.rs b/spire-api/src/selectors.rs new file mode 100644 index 0000000..ea023c8 --- /dev/null +++ b/spire-api/src/selectors.rs @@ -0,0 +1,129 @@ +//! Selectors conforming to SPIRE standards. +use crate::proto::spire::api::types::Selector as SpiffeSelector; + +const K8S_TYPE: &str = "k8s"; +const UNIX_TYPE: &str = "unix"; + +/// Converts user-defined selectors into SPIFFE selectors. +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)] +/// Represents various types of SPIFFE identity selectors. +pub enum Selector { + /// Represents a SPIFFE identity selector based on Kubernetes constructs. + K8s(K8s), + /// Represents a SPIFFE identity selector based on Unix system constructs such as PID, GID, and UID. + Unix(Unix), + /// 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 { + K8s::ServiceAccount(s) => format!("{}:{}", K8S_SA_TYPE, s), + K8s::Namespace(s) => format!("{}:{}", K8S_NS_TYPE, s), + } + } +} + +#[derive(Debug, Clone)] +/// Represents a SPIFFE identity selector for Kubernetes. +pub enum K8s { + /// SPIFFE identity selector for a Kubernetes service account. + ServiceAccount(String), + /// SPIFFE identity 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"; + +/// Converts a Unix selector into a formatted string representation. +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)] +/// Represents SPIFFE identity selectors based on Unix process-related attributes. +pub enum Unix { + /// Specifies a selector for a Unix process ID (PID). + Pid(u16), + /// Specifies a selector for a Unix group ID (GID). + Gid(u16), + /// Specifies a selector for a Unix user ID (UID). + 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"); + } +} diff --git a/spire-api/tests/delegated_identity_api_client_test.rs b/spire-api/tests/delegated_identity_api_client_test.rs new file mode 100644 index 0000000..b611946 --- /dev/null +++ b/spire-api/tests/delegated_identity_api_client_test.rs @@ -0,0 +1,179 @@ +// These tests requires a running SPIRE server and agent with workloads registered (see script `scripts/run-spire.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 once_cell::sync::Lazy; + use spiffe::bundle::jwt::JwtBundleSet; + use spiffe::bundle::BundleRefSource; + use spiffe::spiffe_id::TrustDomain; + use spire_api::agent::delegated_identity::DelegatedIdentityClient; + use spire_api::selectors; + use std::process::Command; + use tokio_stream::StreamExt; + + static TRUST_DOMAIN: Lazy = Lazy::new(|| TrustDomain::new("example.org").unwrap()); + + 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") + } + + async fn get_client() -> DelegatedIdentityClient { + DelegatedIdentityClient::default() + .await + .expect("failed to create client") + } + + #[tokio::test] + 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 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, + ))]) + .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 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 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(&*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; + } +}