Skip to content

Commit 3d63afa

Browse files
Add JWKS key generation, rotation, request signing and verifying
1 parent a133c99 commit 3d63afa

File tree

15 files changed

+1868
-0
lines changed

15 files changed

+1868
-0
lines changed

Cargo.lock

Lines changed: 378 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ handlebars = "6.3.2"
3333
hex = "0.4.3"
3434
hmac = "0.12.1"
3535
http = "1.3.1"
36+
jose-jwk = "0.1.2"
3637
log = "0.4.27"
3738
log-fastly = "0.11.7"
3839
lol_html = "2.6.0"
3940
pin-project-lite = "0.2"
41+
rand = "0.8"
4042
regex = "1.1.1"
4143
serde = { version = "1.0", features = ["derive"] }
4244
serde_json = "1.0.145"

crates/common/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ hex = { workspace = true }
2424
hmac = { workspace = true }
2525
chacha20poly1305 = { workspace = true }
2626
http = { workspace = true }
27+
jose-jwk = { workspace = true }
2728
log = { workspace = true }
29+
rand = { workspace = true }
2830
log-fastly = { workspace = true }
2931
serde = { workspace = true }
3032
serde_json = { workspace = true }
@@ -37,6 +39,7 @@ urlencoding = { workspace = true }
3739
lol_html = { workspace = true }
3840
pin-project-lite = { workspace = true }
3941
trusted-server-js = { path = "../js" }
42+
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
4043

4144
[build-dependencies]
4245
serde = { workspace = true }
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
use std::io::Read;
2+
3+
use fastly::{ConfigStore, Request, Response, SecretStore};
4+
use http::StatusCode;
5+
6+
use crate::backend::ensure_backend_from_url;
7+
use crate::error::TrustedServerError;
8+
9+
pub struct FastlyConfigStore {
10+
store_name: String,
11+
}
12+
13+
impl FastlyConfigStore {
14+
pub fn new(store_name: impl Into<String>) -> Self {
15+
Self {
16+
store_name: store_name.into(),
17+
}
18+
}
19+
20+
pub fn get(&self, key: &str) -> Result<String, TrustedServerError> {
21+
// TODO use try_open and return the error
22+
let store = ConfigStore::open(&self.store_name);
23+
store
24+
.get(key)
25+
.ok_or_else(|| TrustedServerError::Configuration {
26+
message: format!(
27+
"Key '{}' not found in config store '{}'",
28+
key, self.store_name
29+
),
30+
})
31+
}
32+
}
33+
34+
pub struct FastlySecretStore {
35+
store_name: String,
36+
}
37+
38+
impl FastlySecretStore {
39+
pub fn new(store_name: impl Into<String>) -> Self {
40+
Self {
41+
store_name: store_name.into(),
42+
}
43+
}
44+
45+
pub fn get(&self, key: &str) -> Result<Vec<u8>, TrustedServerError> {
46+
let store =
47+
SecretStore::open(&self.store_name).map_err(|_| TrustedServerError::Configuration {
48+
message: format!("Failed to open SecretStore '{}'", self.store_name),
49+
})?;
50+
51+
let secret = store
52+
.get(key)
53+
.ok_or_else(|| TrustedServerError::Configuration {
54+
message: format!(
55+
"Secret '{}' not found in secret store '{}'",
56+
key, self.store_name
57+
),
58+
})?;
59+
60+
secret
61+
.try_plaintext()
62+
.map_err(|_| TrustedServerError::Configuration {
63+
message: "Failed to get secret plaintext".into(),
64+
})
65+
.map(|bytes| bytes.into_iter().collect())
66+
}
67+
68+
pub fn get_string(&self, key: &str) -> Result<String, TrustedServerError> {
69+
let bytes = self.get(key)?;
70+
String::from_utf8(bytes).map_err(|e| TrustedServerError::Configuration {
71+
message: format!("Failed to decode secret as UTF-8: {}", e),
72+
})
73+
}
74+
}
75+
76+
pub struct FastlyApiClient {
77+
api_key: Vec<u8>,
78+
base_url: String,
79+
}
80+
81+
impl FastlyApiClient {
82+
pub fn new() -> Result<Self, TrustedServerError> {
83+
Self::from_secret_store("api-keys", "api_key")
84+
}
85+
86+
pub fn from_secret_store(store_name: &str, key_name: &str) -> Result<Self, TrustedServerError> {
87+
ensure_backend_from_url("https://api.fastly.com").map_err(|e| {
88+
TrustedServerError::Configuration {
89+
message: format!("Failed to ensure API backend: {}", e),
90+
}
91+
})?;
92+
93+
let secret_store = FastlySecretStore::new(store_name);
94+
let api_key = secret_store.get(key_name)?;
95+
96+
Ok(Self {
97+
api_key,
98+
base_url: "https://api.fastly.com".to_string(),
99+
})
100+
}
101+
102+
fn make_request(
103+
&self,
104+
method: &str,
105+
path: &str,
106+
body: Option<String>,
107+
content_type: &str,
108+
) -> Result<Response, TrustedServerError> {
109+
let url = format!("{}{}", self.base_url, path);
110+
111+
let api_key_str = String::from_utf8_lossy(&self.api_key).to_string();
112+
113+
let mut request = match method {
114+
"GET" => Request::get(&url),
115+
"POST" => Request::post(&url),
116+
"PUT" => Request::put(&url),
117+
"DELETE" => Request::delete(&url),
118+
_ => {
119+
return Err(TrustedServerError::Configuration {
120+
message: format!("Unsupported HTTP method: {}", method),
121+
})
122+
}
123+
};
124+
125+
request = request
126+
.with_header("Fastly-Key", api_key_str)
127+
.with_header("Accept", "application/json");
128+
129+
if let Some(body_content) = body {
130+
request = request
131+
.with_header("Content-Type", content_type)
132+
.with_body(body_content);
133+
}
134+
135+
request.send("backend_https_api_fastly_com").map_err(|e| {
136+
TrustedServerError::Configuration {
137+
message: format!("Failed to send API request: {}", e),
138+
}
139+
})
140+
}
141+
142+
pub fn update_config_item(
143+
&self,
144+
store_id: &str,
145+
key: &str,
146+
value: &str,
147+
) -> Result<(), TrustedServerError> {
148+
let path = format!("/resources/stores/config/{}/item/{}", store_id, key);
149+
let payload = format!("item_value={}", value);
150+
151+
let mut response = self.make_request(
152+
"PUT",
153+
&path,
154+
Some(payload),
155+
"application/x-www-form-urlencoded",
156+
)?;
157+
158+
let mut buf = String::new();
159+
response
160+
.get_body_mut()
161+
.read_to_string(&mut buf)
162+
.map_err(|e| TrustedServerError::Configuration {
163+
message: format!("Failed to read API response: {}", e),
164+
})?;
165+
166+
if response.get_status() == StatusCode::OK {
167+
Ok(())
168+
} else {
169+
Err(TrustedServerError::Configuration {
170+
message: format!(
171+
"Failed to update config item: HTTP {} - {}",
172+
response.get_status(),
173+
buf
174+
),
175+
})
176+
}
177+
}
178+
179+
pub fn create_secret(
180+
&self,
181+
store_id: &str,
182+
secret_name: &str,
183+
secret_value: &str,
184+
) -> Result<(), TrustedServerError> {
185+
let path = format!("/resources/stores/secret/{}/secrets", store_id);
186+
187+
let payload = serde_json::json!({
188+
"name": secret_name,
189+
"secret": secret_value
190+
});
191+
192+
let mut response =
193+
self.make_request("POST", &path, Some(payload.to_string()), "application/json")?;
194+
195+
let mut buf = String::new();
196+
response
197+
.get_body_mut()
198+
.read_to_string(&mut buf)
199+
.map_err(|e| TrustedServerError::Configuration {
200+
message: format!("Failed to read API response: {}", e),
201+
})?;
202+
203+
if response.get_status() == StatusCode::OK {
204+
Ok(())
205+
} else {
206+
Err(TrustedServerError::Configuration {
207+
message: format!(
208+
"Failed to create secret: HTTP {} - {}",
209+
response.get_status(),
210+
buf
211+
),
212+
})
213+
}
214+
}
215+
216+
pub fn delete_config_item(&self, store_id: &str, key: &str) -> Result<(), TrustedServerError> {
217+
let path = format!("/resources/stores/config/{}/item/{}", store_id, key);
218+
219+
let mut response = self.make_request("DELETE", &path, None, "application/json")?;
220+
221+
let mut buf = String::new();
222+
response
223+
.get_body_mut()
224+
.read_to_string(&mut buf)
225+
.map_err(|e| TrustedServerError::Configuration {
226+
message: format!("Failed to read API response: {}", e),
227+
})?;
228+
229+
if response.get_status() == StatusCode::OK
230+
|| response.get_status() == StatusCode::NO_CONTENT
231+
{
232+
Ok(())
233+
} else {
234+
Err(TrustedServerError::Configuration {
235+
message: format!(
236+
"Failed to delete config item: HTTP {} - {}",
237+
response.get_status(),
238+
buf
239+
),
240+
})
241+
}
242+
}
243+
244+
pub fn delete_secret(
245+
&self,
246+
store_id: &str,
247+
secret_name: &str,
248+
) -> Result<(), TrustedServerError> {
249+
let path = format!(
250+
"/resources/stores/secret/{}/secrets/{}",
251+
store_id, secret_name
252+
);
253+
254+
let mut response = self.make_request("DELETE", &path, None, "application/json")?;
255+
256+
let mut buf = String::new();
257+
response
258+
.get_body_mut()
259+
.read_to_string(&mut buf)
260+
.map_err(|e| TrustedServerError::Configuration {
261+
message: format!("Failed to read API response: {}", e),
262+
})?;
263+
264+
if response.get_status() == StatusCode::OK
265+
|| response.get_status() == StatusCode::NO_CONTENT
266+
{
267+
Ok(())
268+
} else {
269+
Err(TrustedServerError::Configuration {
270+
message: format!(
271+
"Failed to delete secret: HTTP {} - {}",
272+
response.get_status(),
273+
buf
274+
),
275+
})
276+
}
277+
}
278+
}
279+
280+
#[cfg(test)]
281+
mod tests {
282+
use super::*;
283+
284+
#[test]
285+
fn test_config_store_new() {
286+
let store = FastlyConfigStore::new("test_store");
287+
assert_eq!(store.store_name, "test_store");
288+
}
289+
290+
#[test]
291+
fn test_secret_store_new() {
292+
let store = FastlySecretStore::new("test_secrets");
293+
assert_eq!(store.store_name, "test_secrets");
294+
}
295+
296+
#[test]
297+
fn test_config_store_get() {
298+
let store = FastlyConfigStore::new("jwks_store");
299+
let result = store.get("current-kid");
300+
match result {
301+
Ok(kid) => println!("Current KID: {}", kid),
302+
Err(e) => println!("Expected error in test environment: {}", e),
303+
}
304+
}
305+
306+
#[test]
307+
fn test_secret_store_get() {
308+
let store = FastlySecretStore::new("signing_keys");
309+
let config_store = FastlyConfigStore::new("jwks_store");
310+
311+
match config_store.get("current-kid") {
312+
Ok(kid) => match store.get(&kid) {
313+
Ok(bytes) => {
314+
println!("Successfully loaded secret, {} bytes", bytes.len());
315+
assert!(!bytes.is_empty());
316+
}
317+
Err(e) => println!("Error loading secret: {}", e),
318+
},
319+
Err(e) => println!("Error getting current kid: {}", e),
320+
}
321+
}
322+
323+
#[test]
324+
fn test_api_client_creation() {
325+
let result = FastlyApiClient::new();
326+
match result {
327+
Ok(_client) => println!("Successfully created API client"),
328+
Err(e) => println!("Expected error in test environment: {}", e),
329+
}
330+
}
331+
332+
#[test]
333+
fn test_update_config_item() {
334+
let result = FastlyApiClient::new();
335+
if let Ok(client) = result {
336+
let result =
337+
client.update_config_item("5WNlRjznCUAGTU0QeYU8x2", "test-key", "test-value");
338+
match result {
339+
Ok(()) => println!("Successfully updated config item"),
340+
Err(e) => println!("Failed to update config item: {}", e),
341+
}
342+
}
343+
}
344+
345+
#[test]
346+
fn test_create_secret() {
347+
let result = FastlyApiClient::new();
348+
if let Ok(client) = result {
349+
let result = client.create_secret(
350+
"Ltf3CkSGV0Yn2PIC2lDcZx",
351+
"test-secret-new",
352+
"SGVsbG8sIHdvcmxkIQ==",
353+
);
354+
match result {
355+
Ok(()) => println!("Successfully created secret"),
356+
Err(e) => println!("Failed to create secret: {}", e),
357+
}
358+
}
359+
}
360+
}

0 commit comments

Comments
 (0)