Skip to content

Commit a44ead1

Browse files
Add KV identity graph with CAS concurrency control
Implement Story 3 (#536): KV-backed identity graph with compare-and-swap concurrency, partner ID upserts, tombstone writes for consent withdrawal, and revive semantics. Includes schema types, metadata, 300s last-seen debounce, and comprehensive unit tests. Also incorporates earlier foundation work: EC module restructure, config migration from [edge_cookie] to [ec], cookie domain computation, consent gating fixes, and integration proxy revocation support.
1 parent e52ff39 commit a44ead1

File tree

20 files changed

+1314
-3697
lines changed

20 files changed

+1314
-3697
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ regex = "1.12.3"
8282
serde = { version = "1.0", features = ["derive"] }
8383
serde_json = "1.0.149"
8484
sha2 = "0.10.9"
85-
subtle = "2.6"
8685
temp-env = "0.3.6"
8786
tokio = { version = "1.49", features = ["sync", "macros", "io-util", "rt", "time"] }
8887
tokio-test = "0.4"

crates/integration-tests/fixtures/configs/viceroy-template.toml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,17 @@
77
[local_server.backends]
88

99
[local_server.kv_stores]
10-
# These inline placeholders satisfy Viceroy's local KV configuration
11-
# requirements without exercising KV-backed application behavior.
12-
[[local_server.kv_stores.creative_store]]
10+
# These inline placeholders satisfy Viceroy's local KV configuration
11+
# requirements without exercising KV-backed application behavior.
12+
[[local_server.kv_stores.counter_store]]
13+
key = "placeholder"
14+
data = "placeholder"
15+
16+
[[local_server.kv_stores.opid_store]]
17+
key = "placeholder"
18+
data = "placeholder"
19+
20+
[[local_server.kv_stores.creative_store]]
1321
key = "placeholder"
1422
data = "placeholder"
1523

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"ec_counter": 10
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"ec_counter": 10
3+
}

crates/trusted-server-core/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ regex = { workspace = true }
4040
serde = { workspace = true }
4141
serde_json = { workspace = true }
4242
sha2 = { workspace = true }
43-
subtle = { workspace = true }
4443
tokio = { workspace = true }
4544
toml = { workspace = true }
4645
trusted-server-js = { path = "../js" }

crates/trusted-server-core/src/auth.rs

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use base64::{engine::general_purpose::STANDARD, Engine as _};
22
use fastly::http::{header, StatusCode};
33
use fastly::{Request, Response};
4-
use sha2::{Digest as _, Sha256};
5-
use subtle::ConstantTimeEq as _;
64

75
use crate::settings::Settings;
86

@@ -32,19 +30,7 @@ pub fn enforce_basic_auth(settings: &Settings, req: &Request) -> Option<Response
3230
None => return Some(unauthorized_response()),
3331
};
3432

35-
// Hash before comparing to normalise lengths — `ct_eq` on raw byte slices
36-
// short-circuits when lengths differ, which would leak credential length.
37-
// SHA-256 produces fixed-size digests so the comparison is truly constant-time.
38-
//
39-
// Note: constant-time guarantees are best-effort on WASM targets because the
40-
// runtime optimiser/JIT may re-introduce variable-time paths. This is an
41-
// inherent limitation of all constant-time code in managed runtimes.
42-
let username_match = Sha256::digest(handler.username.expose().as_bytes())
43-
.ct_eq(&Sha256::digest(username.as_bytes()));
44-
let password_match = Sha256::digest(handler.password.expose().as_bytes())
45-
.ct_eq(&Sha256::digest(password.as_bytes()));
46-
47-
if bool::from(username_match & password_match) {
33+
if handler.username == username && handler.password == password {
4834
None
4935
} else {
5036
Some(unauthorized_response())
@@ -90,19 +76,24 @@ mod tests {
9076
use base64::engine::general_purpose::STANDARD;
9177
use fastly::http::{header, Method};
9278

93-
use crate::test_support::tests::create_test_settings;
79+
use crate::test_support::tests::crate_test_settings_str;
80+
81+
fn settings_with_handlers() -> Settings {
82+
let config = crate_test_settings_str();
83+
Settings::from_toml(&config).expect("should parse settings with handlers")
84+
}
9485

9586
#[test]
9687
fn no_challenge_for_non_protected_path() {
97-
let settings = create_test_settings();
88+
let settings = settings_with_handlers();
9889
let req = Request::new(Method::GET, "https://example.com/open");
9990

10091
assert!(enforce_basic_auth(&settings, &req).is_none());
10192
}
10293

10394
#[test]
10495
fn challenge_when_missing_credentials() {
105-
let settings = create_test_settings();
96+
let settings = settings_with_handlers();
10697
let req = Request::new(Method::GET, "https://example.com/secure");
10798

10899
let response = enforce_basic_auth(&settings, &req).expect("should challenge");
@@ -115,7 +106,7 @@ mod tests {
115106

116107
#[test]
117108
fn allow_when_credentials_match() {
118-
let settings = create_test_settings();
109+
let settings = settings_with_handlers();
119110
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
120111
let token = STANDARD.encode("user:pass");
121112
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
@@ -124,20 +115,19 @@ mod tests {
124115
}
125116

126117
#[test]
127-
fn challenge_when_both_credentials_wrong() {
128-
let settings = create_test_settings();
118+
fn challenge_when_credentials_mismatch() {
119+
let settings = settings_with_handlers();
129120
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
130-
let token = STANDARD.encode("wrong:wrong");
121+
let token = STANDARD.encode("user:wrong");
131122
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
132123

133-
let response = enforce_basic_auth(&settings, &req)
134-
.expect("should challenge when both username and password are wrong");
124+
let response = enforce_basic_auth(&settings, &req).expect("should challenge");
135125
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
136126
}
137127

138128
#[test]
139129
fn challenge_when_scheme_is_not_basic() {
140-
let settings = create_test_settings();
130+
let settings = settings_with_handlers();
141131
let mut req = Request::new(Method::GET, "https://example.com/secure");
142132
req.set_header(header::AUTHORIZATION, "Bearer token");
143133

@@ -147,7 +137,7 @@ mod tests {
147137

148138
#[test]
149139
fn allow_admin_path_with_valid_credentials() {
150-
let settings = create_test_settings();
140+
let settings = settings_with_handlers();
151141
let mut req = Request::new(Method::POST, "https://example.com/admin/keys/rotate");
152142
let token = STANDARD.encode("admin:admin-pass");
153143
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
@@ -160,7 +150,7 @@ mod tests {
160150

161151
#[test]
162152
fn challenge_admin_path_with_wrong_credentials() {
163-
let settings = create_test_settings();
153+
let settings = settings_with_handlers();
164154
let mut req = Request::new(Method::POST, "https://example.com/admin/keys/rotate");
165155
let token = STANDARD.encode("admin:wrong");
166156
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
@@ -172,35 +162,11 @@ mod tests {
172162

173163
#[test]
174164
fn challenge_admin_path_with_missing_credentials() {
175-
let settings = create_test_settings();
165+
let settings = settings_with_handlers();
176166
let req = Request::new(Method::POST, "https://example.com/admin/keys/rotate");
177167

178168
let response = enforce_basic_auth(&settings, &req)
179169
.expect("should challenge admin path with missing credentials");
180170
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
181171
}
182-
183-
#[test]
184-
fn challenge_when_username_wrong_password_correct() {
185-
let settings = create_test_settings();
186-
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
187-
let token = STANDARD.encode("wrong:pass");
188-
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
189-
190-
let response = enforce_basic_auth(&settings, &req)
191-
.expect("should challenge when only username is wrong");
192-
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
193-
}
194-
195-
#[test]
196-
fn challenge_when_username_correct_password_wrong() {
197-
let settings = create_test_settings();
198-
let mut req = Request::new(Method::GET, "https://example.com/secure/data");
199-
let token = STANDARD.encode("user:wrong");
200-
req.set_header(header::AUTHORIZATION, format!("Basic {token}"));
201-
202-
let response = enforce_basic_auth(&settings, &req)
203-
.expect("should challenge when only password is wrong");
204-
assert_eq!(response.get_status(), StatusCode::UNAUTHORIZED);
205-
}
206172
}

0 commit comments

Comments
 (0)