Skip to content

Commit

Permalink
Migrate library to using to tonic and prost and enhance functiona…
Browse files Browse the repository at this point in the history
…lity of the Workload API client (#33)

Signed-off-by: Max Lambrecht <[email protected]>
  • Loading branch information
maxlambrecht authored Aug 8, 2023
1 parent 9f6c6cc commit 23473fa
Show file tree
Hide file tree
Showing 15 changed files with 1,659 additions and 3,020 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Install Protoc
uses: arduino/setup-protoc@v2

- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: rustfmt, clippy

- name: Execute rustfmt
run: cargo +nightly fmt

- name: Execute clippy
run: cargo +nightly clippy

Expand All @@ -23,9 +29,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Install Protoc
uses: arduino/setup-protoc@v2

- name: Setup Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable

- name: Run the script
run: ./ci.sh
20 changes: 16 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name = "spiffe"
# When releasing to crates.io:
# - Update CHANGELOG.md.
# - Create a new tag
version = "0.2.2"
version = "0.3.0"
authors = ["Max Lambrecht <[email protected]>"]
description = "Rust client library implementation for SPIFFE"
license = "Apache-2.0"
Expand All @@ -15,8 +15,12 @@ categories = ["cryptography"]
keywords = ["SPIFFE", "X509", "JWT"]

[dependencies]
protobuf = "2.28"
futures = "0.3"
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" }
Expand All @@ -28,13 +32,21 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
zeroize = { version = "1.6", features = ["zeroize_derive"] }
time = "0.3"
grpcio = { version = "0.12", default-features = false, features = ["protobuf-codec"] }


[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 = []
94 changes: 67 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,50 @@
# Rust SPIFFE library
# Rust SPIFFE Library

An utility library to interact with the [SPIFFE Workload API](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md)
to fetch X.509 and JWT SVIDs and Bundles. It also provides types that comply with the [SPIFFE standards](https://github.com/spiffe/spiffe/tree/main/standards).
See [spiffe.io](https://spiffe.io/).
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)

## Usage
## Getting Started

To use `spiffe`, add this to your `Cargo.toml`:
Include `spiffe` in your `Cargo.toml` dependencies:

```toml
[dependencies]
spiffe = "0.2.2"
spiffe = "0.3.0"
```

### Examples
## Examples of Usage

#### Create a `WorkloadApiClient`
### Creating a `WorkloadApiClient`

Providing the endpoint socket path as parameter:
Create client using the endpoint socket path:

```rust
let client = WorkloadApiClient::new("unix:/tmp/spire-agent/api/public.sock")?;
let mut client = WorkloadApiClient::new_from_path("unix:/tmp/spire-agent/public/api.sock").await?;
```

Providing the endpoint socket path through the environment variable `SPIFFE_ENDPOINT_SOCKET`:
Or by using the `SPIFFE_ENDPOINT_SOCKET` environment variable:

```rust
let client = WorkloadApiClient::default()?;
let mut client = WorkloadApiClient::default().await?;
```

#### Fetching X.509 materials
### Fetching X.509 Materials

```rust
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()?;
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()?;
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()?;
let x509_context: X509Context = client.fetch_x509_context().await?;

// get the X.509 chain of certificates from the SVID
let cert_chain: &Vec<Certificate> = x509_svid.cert_chain();
Expand All @@ -57,38 +56,79 @@ let private_key: &PrivateKey = x509_svid.private_key();
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).unwrap();
let x509_bundle: &X509Bundle = x509_bundles.get_bundle(&trust_domain)?;

// get the X.509 authorities (public keys) in the bundle
let x509_authorities: &Vec<Certificate> = 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

#### Fetching JWT tokens and bundles and validating tokens
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))?;
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))?;
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()?;
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).unwrap();
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").unwrap();
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"])?;
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.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
}
}
}
```

For more detailed examples and additional features, refer to the [documentation](https://docs.rs/spiffe).
14 changes: 14 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::fs;

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, &["src/proto/workload.proto"], &["src/proto"])?;

fs::rename("src/proto/_.rs", "src/proto/workload.rs")?;

Ok(())
}
9 changes: 6 additions & 3 deletions ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,12 @@ if [ ${spire_agent_started} -ne 1 ]; then
exit 1
fi

# Register the workload through UID with the SPIFFE ID "spiffe://example.org/myservice" with a TTL of 3 seconds
bin/spire-server entry create -parentID ${agent_id} -spiffeID spiffe://example.org/myservice -selector unix:uid:$(id -u) -ttl 3
# Register the workload through UID with the SPIFFE ID "spiffe://example.org/myservice" with a TTL of 5 seconds
bin/spire-server entry create -parentID ${agent_id} -spiffeID spiffe://example.org/myservice -selector unix:uid:$(id -u) -ttl 5
sleep 10 # this value is derived from the default Agent sync interval
# Register the workload through UID with the SPIFFE ID "spiffe://example.org/myservice2" with a TTL of 5 seconds
bin/spire-server entry create -parentID ${agent_id} -spiffeID spiffe://example.org/myservice2 -selector unix:uid:$(id -u) -ttl 5
sleep 10 # this value is derived from the default Agent sync interval
popd

RUST_BACKTRACE=1 cargo test -- --include-ignored
RUST_BACKTRACE=1 cargo test --features integration-tests
10 changes: 4 additions & 6 deletions src/cert/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,12 @@ pub(crate) fn parse_der_encoded_bytes_as_x509_certificate(
}

// Returns the X.509 extension in the certificate the for the provided OID.
pub(crate) fn get_x509_extension<'a, 'b>(
pub(crate) fn get_x509_extension<'a>(
cert: &'a X509Certificate<'_>,
oid: &'b Oid<'a>,
oid: Oid<'a>,
) -> Result<&'a ParsedExtension<'a>, CertificateError> {
let parsed_extension = match &cert.tbs_certificate.get_extension_unique(&oid)? {
None => {
return Err(CertificateError::MissingX509Extension(oid.to_string()))
}
let parsed_extension = match cert.tbs_certificate.get_extension_unique(&oid)? {
None => return Err(CertificateError::MissingX509Extension(oid.to_string())),
Some(s) => s.parsed_extension(),
};
Ok(parsed_extension)
Expand Down
21 changes: 13 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@
//! use std::convert::TryFrom;
//! use std::error::Error;
//!
//! # fn main() -> Result<(), Box< dyn Error>> {
//! # async fn some_function() -> Result<(), Box< dyn Error>> {
//!
//! // create a new Workload API client connecting to the provided endpoint socket path
//! let client = WorkloadApiClient::new("unix:/tmp/spire-agent/api/public.sock")?;
//! let mut client =
//! WorkloadApiClient::new_from_path("unix:/tmp/spire-agent/api/public.sock").await?;
//!
//! // fetch the default X.509 SVID
//! let x509_svid: X509Svid = client.fetch_x509_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()?;
//! 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()?;
//! let x509_context: X509Context = client.fetch_x509_context().await?;
//!
//! // get the X.509 chain of certificates from the SVID
//! let cert_chain: &Vec<Certificate> = x509_svid.cert_chain();
Expand All @@ -54,13 +55,17 @@
//!
//! let target_audience = &["service1", "service2"];
//! // fetch a jwt token for the provided SPIFFE-ID and with the target audience `service1.com`
//! let jwt_token = client.fetch_jwt_token(target_audience, Some(&spiffe_id))?;
//! let jwt_token = client
//! .fetch_jwt_token(target_audience, Some(&spiffe_id))
//! .await?;
//!
//! // fetch the jwt token and parses it as a `JwtSvid`
//! let jwt_svid = client.fetch_jwt_svid(target_audience, Some(&spiffe_id))?;
//! let jwt_svid = client
//! .fetch_jwt_svid(target_audience, Some(&spiffe_id))
//! .await?;
//!
//! // fetch a set of jwt bundles (public keys for validating jwt token)
//! let jwt_bundles_set = client.fetch_jwt_bundles()?;
//! let jwt_bundles_set = client.fetch_jwt_bundles().await?;
//!
//! // get the JWT bundle associated to the trust domain
//! let jwt_bundle: &JwtBundle = jwt_bundles_set.get_bundle(&trust_domain).unwrap();
Expand Down
1 change: 0 additions & 1 deletion src/proto/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
pub(crate) mod workload;
pub(crate) mod workload_grpc;
Loading

0 comments on commit 23473fa

Please sign in to comment.