Skip to content

Commit a823ff9

Browse files
authored
Improve X509Source (#184)
Signed-off-by: Max Lambrecht <[email protected]>
1 parent 1744e87 commit a823ff9

File tree

11 files changed

+713
-804
lines changed

11 files changed

+713
-804
lines changed

.github/workflows/ci.yml

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
name: Build
22

3-
on: [push, pull_request]
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
415

516
jobs:
617
setup-and-lint:
@@ -11,17 +22,21 @@ jobs:
1122
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
1223
with:
1324
submodules: recursive
14-
- name: Cache Project
15-
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
25+
1626
- name: Install toolchain
1727
uses: ./.github/actions/setup-env
1828
with:
19-
repo-token: ${{ secrets.GITHUB_TOKEN }}
29+
github_token: ${{ secrets.GITHUB_TOKEN }}
30+
31+
- name: Cache Project
32+
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
33+
with:
34+
shared-key: ${{ runner.os }}-cargo
2035

2136
- name: Lint code with rustfmt and clippy
2237
run: |
2338
cargo +nightly fmt -- --check
24-
cargo clippy -- -D warnings
39+
cargo clippy --all-targets -- -D warnings
2540
2641
build-and-test:
2742
name: Build and Test
@@ -30,26 +45,37 @@ jobs:
3045
env:
3146
SPIFFE_ENDPOINT_SOCKET: unix:/tmp/spire-agent/public/api.sock
3247
SPIRE_ADMIN_ENDPOINT_SOCKET: unix:/tmp/spire-agent/admin/api.sock
48+
RUST_BACKTRACE: "1"
3349
steps:
3450
- name: Check out code
3551
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
3652
with:
3753
submodules: recursive
38-
- name: Cache Project
39-
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
54+
4055
- name: Install toolchain
4156
uses: ./.github/actions/setup-env
4257
with:
58+
github_token: ${{ secrets.GITHUB_TOKEN }}
59+
60+
- name: Cache Project
61+
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
62+
with:
63+
shared-key: ${{ runner.os }}-cargo
64+
65+
- name: Install protoc
66+
uses: arduino/setup-protoc@v3
67+
with:
68+
version: "25.3"
4369
repo-token: ${{ secrets.GITHUB_TOKEN }}
4470

4571
- name: Build Rust project
46-
run: cargo build
72+
run: cargo build --all-targets --all-features
4773

4874
- name: Start SPIRE
4975
run: .github/workflows/scripts/run-spire.sh
5076

5177
- name: Run Integration Tests
52-
run: RUST_BACKTRACE=1 cargo test --features integration-tests
78+
run: cargo test --features integration-tests
5379

5480
- name: Clean up SPIRE
5581
run: .github/workflows/scripts/cleanup-spire.sh

spiffe/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ zeroize = { version = "1", features = ["zeroize_derive"] }
2828
time = "0.3"
2929
tonic = "0.14.0"
3030
tonic-prost = "0.14"
31+
arc-swap = "1"
3132

3233
# workload-api dependencies:
3334
prost = { version = "0.14", optional = true }

spiffe/README.md

Lines changed: 96 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,154 @@
1-
# Rust SPIFFE Library
1+
# Rust SPIFFE
22

3-
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/).
3+
A Rust library for interacting with the **SPIFFE Workload API**.
4+
It provides idiomatic access to SPIFFE identities and trust material, including:
5+
6+
- X.509 SVIDs and bundles
7+
- JWT SVIDs and bundles
8+
- Streaming updates (watch semantics)
9+
- Strongly typed SPIFFE primitives compliant with the SPIFFE standards
10+
11+
For background on SPIFFE, see <https://spiffe.io>.
12+
For the Workload API specification, see the
13+
[SPIFFE Workload API standard](https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Workload_API.md).
414

515
[![crates.io](https://img.shields.io/crates/v/spiffe.svg)](https://crates.io/crates/spiffe)
616
[![Build](https://github.com/maxlambrecht/rust-spiffe/actions/workflows/ci.yml/badge.svg)](https://github.com/maxlambrecht/rust-spiffe/actions/workflows/ci.yml)
717
[![docs.rs](https://docs.rs/spiffe/badge.svg)](https://docs.rs/spiffe)
8-
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/maxlambrecht/rust-spiffe/blob/main/LICENSE)
18+
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
919

10-
## Getting Started
20+
---
1121

12-
Include `spiffe` in your `Cargo.toml` dependencies to get both the SPIFFE types (`spiffe-types`) and the Workload API
13-
client (`workload-api`) by default:
22+
## Installation
23+
24+
Add `spiffe` to your `Cargo.toml`:
1425

1526
```toml
1627
[dependencies]
1728
spiffe = "0.6.7"
18-
```
29+
````
1930

20-
## Examples of Usage
31+
This includes both SPIFFE core types and a Workload API client.
2132

22-
### Creating a `WorkloadApiClient`
33+
---
2334

24-
Create client using the endpoint socket path:
35+
## Quick Start
2536

26-
```rust
27-
let mut client = WorkloadApiClient::new_from_path("unix:/tmp/spire-agent/public/api.sock").await?;
28-
```
37+
### Create a Workload API client
2938

30-
Or by using the `SPIFFE_ENDPOINT_SOCKET` environment variable:
39+
Using an explicit socket path:
3140

3241
```rust
33-
let mut client = WorkloadApiClient::default().await?;
34-
```
42+
use spiffe::WorkloadApiClient;
3543

36-
### Fetching X.509 Materials
44+
let client = WorkloadApiClient::new_from_path(
45+
"unix:///tmp/spire-agent/public/api.sock",
46+
).await?;
47+
```
3748

38-
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.
49+
Or via the `SPIFFE_ENDPOINT_SOCKET` environment variable:
3950

4051
```rust
41-
// fetch the default X.509 SVID
42-
let x509_svid: X509Svid = client.fetch_x509_svid().await?;
52+
use spiffe::WorkloadApiClient;
4353

44-
// fetch a set of X.509 bundles (X.509 public key authorities)
45-
let x509_bundles: X509BundleSet = client.fetch_x509_bundles().await?;
54+
let client = WorkloadApiClient::default().await?;
55+
```
56+
57+
---
4658

47-
// fetch all the X.509 materials (SVIDs and bundles)
48-
let x509_context: X509Context = client.fetch_x509_context().await?;
59+
## X.509 identities
4960

50-
// get the X.509 chain of certificates from the SVID
51-
let cert_chain: &Vec<Certificate> = x509_svid.cert_chain();
61+
### Fetch X.509 materials directly
62+
63+
```rust
64+
use spiffe::{TrustDomain, X509Context};
5265

53-
// get the private key from the SVID
54-
let private_key: &PrivateKey = x509_svid.private_key();
66+
let svid = client.fetch_x509_svid().await?;
67+
let bundles = client.fetch_x509_bundles().await?;
68+
let context: X509Context = client.fetch_x509_context().await?;
5569

56-
// parse a SPIFFE trust domain
5770
let trust_domain = TrustDomain::try_from("example.org")?;
71+
let bundle = bundles.get_bundle(&trust_domain)?;
72+
```
5873

59-
// get the X.509 bundle associated to the trust domain
60-
let x509_bundle: &X509Bundle = x509_bundles.get_bundle(&trust_domain)?;
61-
62-
// get the X.509 authorities (public keys) in the bundle
63-
let x509_authorities: &Vec<Certificate> = x509_bundle.authorities();
64-
65-
// watch for updates on the X.509 context
66-
let mut x509_context_stream = client.stream_x509_contexts().await?;
67-
while let Some(x509_context_update) = x509_context_stream.next().await {
68-
match x509_context_update {
69-
Ok(update) => {
70-
// handle the updated X509Context
71-
}
72-
Err(e) => {
73-
// handle the error
74-
}
75-
}
76-
}
74+
### Watch for updates
75+
76+
```rust
77+
let mut stream = client.stream_x509_contexts().await?;
7778

78-
// watch for updates on the X.509 bundles
79-
let mut x509_bundle_stream = client.stream_x509_bundles().await?;
80-
while let Some(x509_bundle_update) = x509_bundle_stream.next().await {
81-
match x509_bundle_update {
82-
Ok(update) => {
83-
// handle the updated X509 bundle
84-
}
85-
Err(e) => {
86-
// handle the error
87-
}
88-
}
79+
while let Some(update) = stream.next().await {
80+
let context = update?;
81+
// react to updated SVIDs / bundles
8982
}
9083
```
9184

92-
### Fetching X.509 Materials using `X509Source`
85+
---
9386

94-
A convenient way to fetch X.509 materials is by using the `X509Source`:
87+
## X.509Source (recommended)
88+
89+
`X509Source` maintains a locally cached, automatically refreshed view of X.509
90+
SVIDs and bundles.
9591

9692
```rust
9793
use spiffe::X509Source;
98-
use spiffe::BundleSource;
99-
use spiffe::TrustDomain;
100-
use spiffe::X509Svid;
101-
use spiffe::SvidSource;
10294

103-
async fn fetch_x509_materials() -> Result<(), Box<dyn std::error::Error>> {
104-
// Create a new X509Source
105-
let x509_source = X509Source::default().await?;
95+
let source = X509Source::new().await?;
10696

107-
// Fetch the SVID
108-
let svid = x509_source.get_svid()?.ok_or("No X509Svid found")?;
97+
// Default SVID
98+
let svid = source.get_svid()?.expect("no SVID available");
10999

110-
// Fetch the bundle for a specific trust domain
111-
let trust_domain = spiffe::TrustDomain::new("example.org"); // Replace with the appropriate trust domain
112-
let bundle = x509_source.get_bundle_for_trust_domain(&trust_domain)?.ok_or("No bundle found for trust domain")?;
113-
114-
Ok(())
115-
}
100+
// Bundle for a trust domain
101+
let bundle = source
102+
.get_bundle_for_trust_domain(&"example.org".try_into()?)?
103+
.expect("no bundle found");
116104
```
117105

118-
### Fetching and Validating JWT Tokens and Bundles
106+
---
119107

120-
Fetch JWT tokens, parse and validate them, fetch JWT bundles, or watch for updates on the JWT bundles.
108+
## JWT identities
109+
110+
### Fetch and validate JWT SVIDs
121111

122112
```rust
123-
// parse a SPIFFE ID to ask a token for
113+
use spiffe::{JwtSvid, SpiffeId};
114+
124115
let spiffe_id = SpiffeId::try_from("spiffe://example.org/my-service")?;
125116

126-
// fetch a jwt token for the provided SPIFFE-ID and with the target audience `service1.com`
127-
let jwt_token = client.fetch_jwt_token(&["audience1", "audience2"], Some(&spiffe_id)).await?;
117+
let jwt = client
118+
.fetch_jwt_svid(&["audience1", "audience2"], Some(&spiffe_id))
119+
.await?;
120+
```
128121

129-
// fetch the jwt token and parses it as a `JwtSvid`
130-
let jwt_svid = client.fetch_jwt_svid(&["audience1", "audience2"], Some(&spiffe_id)).await?;
122+
### Fetch JWT bundles
131123

132-
// fetch a set of jwt bundles (public keys for validating jwt token)
133-
let jwt_bundles = client.fetch_jwt_bundles().await?;
124+
```rust
125+
use spiffe::TrustDomain;
134126

135-
// parse a SPIFFE trust domain
127+
let bundles = client.fetch_jwt_bundles().await?;
136128
let trust_domain = TrustDomain::try_from("example.org")?;
129+
let bundle = bundles.get_bundle(&trust_domain)?;
130+
```
131+
132+
### Watch JWT bundle updates
133+
134+
```rust
135+
let mut stream = client.stream_jwt_bundles().await?;
137136

138-
// get the JWT bundle associated to the trust domain
139-
let jwt_bundle: &JwtBundle = jwt_bundles.get_bundle(&trust_domain)?;
140-
141-
// get the JWT authorities (public keys) in the bundle
142-
let jwt_authority: &JwtAuthority = jwt_bundle.find_jwt_authority("a_key_id")?;
143-
144-
// parse a `JwtSvid` validating the token signature with a JWT bundle source.
145-
let validated_jwt_svid = JwtSvid::parse_and_validate(&jwt_token, &jwt_bundles_set, &["service1.com"])?;
146-
147-
// watch for updates on the JWT bundles
148-
let mut jwt_bundle_stream = client.stream_jwt_bundles().await?;
149-
while let Some(jwt_bundle_update) = jwt_bundle_stream.next().await {
150-
match jwt_bundle_update {
151-
Ok(update) => {
152-
// handle the updated JWT bundle
153-
}
154-
Err(e) => {
155-
// handle the error
156-
}
157-
}
137+
while let Some(update) = stream.next().await {
138+
let bundles = update?;
139+
// react to updated JWT authorities
158140
}
159141
```
160142

161-
For more detailed examples and additional features, refer to the [documentation](https://docs.rs/spiffe).
143+
---
144+
145+
## Documentation
146+
147+
API documentation and additional examples are available on [docs.rs](https://docs.rs/spiffe).
148+
149+
---
162150

163151
## License
164152

165-
This library is licensed under the Apache License. See the [LICENSE.md](../LICENSE) file for details.
153+
Licensed under the Apache License, Version 2.0.
154+
See [LICENSE](../LICENSE) for details.

spiffe/src/endpoint.rs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -97,22 +97,22 @@ mod tests {
9797
}
9898

9999
validate_socket_path_error_tests! {
100-
test_validate_empty_str: (" ", SocketPathError::Parse(ParseError::RelativeUrlWithoutBase), "workload endpoint socket is not a valid URI"),
101-
test_validate_str_missing_scheme: ("foo", SocketPathError::Parse(ParseError::RelativeUrlWithoutBase), "workload endpoint socket is not a valid URI"),
102-
test_validate_uri_invalid_scheme: ("other:///path", SocketPathError::InvalidScheme, "workload endpoint socket URI must have a tcp:// or unix:// scheme"),
103-
104-
test_validate_unix_uri_empty_path: ("unix://", SocketPathError::UnixAddressEmptyPath, "workload endpoint unix socket URI must include a path"),
105-
test_validate_unix_uri_empty_path_slash: ("unix:///", SocketPathError::UnixAddressEmptyPath, "workload endpoint unix socket URI must include a path"),
106-
test_validate_unix_uri_with_query_values: ("unix:///foo?whatever", SocketPathError::HasQueryValues, "workload endpoint socket URI must not include query values"),
107-
test_validate_unix_uri_with_fragment: ("unix:///foo#whatever", SocketPathError::HasFragment, "workload endpoint socket URI must not include a fragment"),
108-
test_validate_unix_uri_with_user_info: ("unix://john:doe@foo/path", SocketPathError::HasUserInfo, "workload endpoint socket URI must not include user info"),
109-
110-
test_validate_tcp_uri_non_empty_path: ("tcp://1.2.3.4:80/path", SocketPathError::TcpAddressNonEmptyPath, "workload endpoint tcp socket URI must not include a path"),
111-
test_validate_tcp_uri_with_query_values: ("tcp://1.2.3.4:80?whatever", SocketPathError::HasQueryValues, "workload endpoint socket URI must not include query values"),
112-
test_validate_tcp_uri_with_fragment: ("tcp://1.2.3.4:80#whatever", SocketPathError::HasFragment, "workload endpoint socket URI must not include a fragment"),
113-
test_validate_tcp_uri_with_user_info: ("tcp://john:[email protected]:80", SocketPathError::HasUserInfo, "workload endpoint socket URI must not include user info"),
114-
test_validate_tcp_uri_no_ip: ("tcp://foo:80", SocketPathError::TcpAddressNoIpPort, "workload endpoint tcp socket URI host component must be an IP:port"),
115-
test_validate_tcp_uri_no_ip_and_port: ("tcp://foo", SocketPathError::TcpAddressNoIpPort, "workload endpoint tcp socket URI host component must be an IP:port"),
116-
test_validate_tcp_uri_no_port: ("tcp://1.2.3.4", SocketPathError::TcpAddressNoIpPort, "workload endpoint tcp socket URI host component must be an IP:port"),
100+
test_validate_empty_str: (" ", SocketPathError::Parse(ParseError::RelativeUrlWithoutBase), "endpoint socket is not a valid URI"),
101+
test_validate_str_missing_scheme: ("foo", SocketPathError::Parse(ParseError::RelativeUrlWithoutBase), "endpoint socket is not a valid URI"),
102+
test_validate_uri_invalid_scheme: ("other:///path", SocketPathError::InvalidScheme, "endpoint socket URI scheme must be tcp:// or unix://"),
103+
104+
test_validate_unix_uri_empty_path: ("unix://", SocketPathError::UnixAddressEmptyPath, "unix:// endpoint socket URI must include a path"),
105+
test_validate_unix_uri_empty_path_slash: ("unix:///", SocketPathError::UnixAddressEmptyPath, "unix:// endpoint socket URI must include a path"),
106+
test_validate_unix_uri_with_query_values: ("unix:///foo?whatever", SocketPathError::HasQueryValues, "endpoint socket URI must not include query values"),
107+
test_validate_unix_uri_with_fragment: ("unix:///foo#whatever", SocketPathError::HasFragment, "endpoint socket URI must not include a fragment"),
108+
test_validate_unix_uri_with_user_info: ("unix://john:doe@foo/path", SocketPathError::HasUserInfo, "endpoint socket URI must not include user info"),
109+
110+
test_validate_tcp_uri_non_empty_path: ("tcp://1.2.3.4:80/path", SocketPathError::TcpAddressNonEmptyPath, "tcp:// endpoint socket URI must not include a path"),
111+
test_validate_tcp_uri_with_query_values: ("tcp://1.2.3.4:80?whatever", SocketPathError::HasQueryValues, "endpoint socket URI must not include query values"),
112+
test_validate_tcp_uri_with_fragment: ("tcp://1.2.3.4:80#whatever", SocketPathError::HasFragment, "endpoint socket URI must not include a fragment"),
113+
test_validate_tcp_uri_with_user_info: ("tcp://john:[email protected]:80", SocketPathError::HasUserInfo, "endpoint socket URI must not include user info"),
114+
test_validate_tcp_uri_no_ip: ("tcp://foo:80", SocketPathError::TcpAddressNoIpPort, "tcp:// endpoint socket URI host must be an IP:port"),
115+
test_validate_tcp_uri_no_ip_and_port: ("tcp://foo", SocketPathError::TcpAddressNoIpPort, "tcp:// endpoint socket URI host must be an IP:port"),
116+
test_validate_tcp_uri_no_port: ("tcp://1.2.3.4", SocketPathError::TcpAddressNoIpPort, "tcp:// endpoint socket URI host must be an IP:port"),
117117
}
118118
}

0 commit comments

Comments
 (0)