Skip to content

Commit 6da607a

Browse files
authored
Tests for pathologcial jwk cases (#13880)
Tests for handling duplicated / conflicting JWKs. These cases are unlikely to happen in practice, as oauth providers will almost certainly remain well-behaved. Nevertheless, we want to continue behaving correctly even with pathological inputs.
1 parent 5ab1c69 commit 6da607a

File tree

2 files changed

+83
-8
lines changed

2 files changed

+83
-8
lines changed

crates/sui-framework/packages/sui-framework/sources/authenticator_state.move

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,20 @@ module sui::authenticator_state {
8080
}
8181
}
8282

83-
fun jwk_equal(a: &ActiveJwk, b: &ActiveJwk): bool {
83+
fun active_jwk_equal(a: &ActiveJwk, b: &ActiveJwk): bool {
8484
// note: epoch is ignored
85-
(&a.jwk.kty == &b.jwk.kty) &&
86-
(&a.jwk.e == &b.jwk.e) &&
87-
(&a.jwk.n == &b.jwk.n) &&
88-
(&a.jwk.alg == &b.jwk.alg) &&
89-
(&a.jwk_id.iss == &b.jwk_id.iss) &&
90-
(&a.jwk_id.kid == &b.jwk_id.kid)
85+
jwk_equal(&a.jwk, &b.jwk) && jwk_id_equal(&a.jwk_id, &b.jwk_id)
86+
}
87+
88+
fun jwk_equal(a: &JWK, b: &JWK): bool {
89+
(&a.kty == &b.kty) &&
90+
(&a.e == &b.e) &&
91+
(&a.n == &b.n) &&
92+
(&a.alg == &b.alg)
93+
}
94+
95+
fun jwk_id_equal(a: &JwkId, b: &JwkId): bool {
96+
(&a.iss == &b.iss) && (&a.kid == &b.kid)
9197
}
9298

9399
// Compare the underlying byte arrays lexicographically. Since the strings may be utf8 this
@@ -213,6 +219,7 @@ module sui::authenticator_state {
213219
assert!(tx_context::sender(ctx) == @0x0, ENotSystemAddress);
214220

215221
check_sorted(&new_active_jwks);
222+
let new_active_jwks = deduplicate(new_active_jwks);
216223

217224
let inner = load_inner_mut(self);
218225

@@ -227,12 +234,19 @@ module sui::authenticator_state {
227234
let new_jwk = vector::borrow(&new_active_jwks, j);
228235

229236
// when they are equal, push only one, but use the max epoch of the two
230-
if (jwk_equal(old_jwk, new_jwk)) {
237+
if (active_jwk_equal(old_jwk, new_jwk)) {
231238
let jwk = *old_jwk;
232239
jwk.epoch = math::max(old_jwk.epoch, new_jwk.epoch);
233240
vector::push_back(&mut res, jwk);
234241
i = i + 1;
235242
j = j + 1;
243+
} else if (jwk_id_equal(&old_jwk.jwk_id, &new_jwk.jwk_id)) {
244+
// if only jwk_id is equal, then the key has changed. Providers should not send
245+
// JWKs like this, but if they do, we must ignore the new JWK to avoid having a
246+
// liveness / forking issues
247+
vector::push_back(&mut res, *old_jwk);
248+
i = i + 1;
249+
j = j + 1;
236250
} else if (jwk_lt(old_jwk, new_jwk)) {
237251
vector::push_back(&mut res, *old_jwk);
238252
i = i + 1;
@@ -254,6 +268,27 @@ module sui::authenticator_state {
254268
inner.active_jwks = res;
255269
}
256270

271+
fun deduplicate(jwks: vector<ActiveJwk>): vector<ActiveJwk> {
272+
let res = vector[];
273+
let i = 0;
274+
let prev: Option<JwkId> = option::none();
275+
while (i < vector::length(&jwks)) {
276+
let jwk = vector::borrow(&jwks, i);
277+
if (option::is_none(&prev)) {
278+
option::fill(&mut prev, jwk.jwk_id);
279+
} else if (jwk_id_equal(option::borrow(&prev), &jwk.jwk_id)) {
280+
// skip duplicate jwks in input
281+
i = i + 1;
282+
continue;
283+
} else {
284+
*option::borrow_mut(&mut prev) = jwk.jwk_id;
285+
};
286+
vector::push_back(&mut res, *jwk);
287+
i = i + 1;
288+
};
289+
res
290+
}
291+
257292
#[allow(unused_function)]
258293
// Called directly by rust when constructing the ChangeEpoch transaction.
259294
fun expire_jwks(

crates/sui-framework/packages/sui-framework/tests/authenticator_state_tests.move

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module sui::authenticator_state_tests {
1616
update_authenticator_state_for_testing,
1717
get_active_jwks_for_testing,
1818
expire_jwks_for_testing,
19+
ActiveJwk,
1920
};
2021
use sui::tx_context;
2122

@@ -65,6 +66,45 @@ module sui::authenticator_state_tests {
6566
test_scenario::end(scenario_val);
6667
}
6768

69+
#[test]
70+
fun authenticator_state_tests_deduplication() {
71+
let scenario_val = test_scenario::begin(@0x0);
72+
let scenario = &mut scenario_val;
73+
74+
authenticator_state::create_for_testing(test_scenario::ctx(scenario));
75+
test_scenario::next_tx(scenario, @0x0);
76+
77+
let auth_state = test_scenario::take_shared<AuthenticatorState>(scenario);
78+
79+
let jwk1 = create_active_jwk(utf8(b"https://accounts.google.com"), utf8(b"kid2"), utf8(b"k1"), 0);
80+
update_authenticator_state_for_testing(&mut auth_state, vector[jwk1], test_scenario::ctx(scenario));
81+
82+
let recorded_jwks = get_active_jwks_for_testing(&auth_state, test_scenario::ctx(scenario));
83+
assert!(vector::length(&recorded_jwks) == 1, 0);
84+
assert!(vector::borrow(&recorded_jwks, 0) == &jwk1, 0);
85+
86+
let jwk2 = create_active_jwk(utf8(b"https://www.facebook.com"), utf8(b"kid1"), utf8(b"k2"), 0);
87+
let jwk3 = create_active_jwk(utf8(b"https://accounts.google.com"), utf8(b"kid2"), utf8(b"k3"), 0);
88+
update_authenticator_state_for_testing(&mut auth_state, vector[jwk2, jwk3], test_scenario::ctx(scenario));
89+
90+
let recorded_jwks = get_active_jwks_for_testing(&auth_state, test_scenario::ctx(scenario));
91+
assert!(vector::length(&recorded_jwks) == 2, 0);
92+
// jwk2 sorts before 1, and 3 is dropped because its a duplicated
93+
assert!(vector::borrow(&recorded_jwks, 0) == &jwk2, 0);
94+
assert!(vector::borrow(&recorded_jwks, 1) == &jwk1, 0);
95+
96+
let jwk4 = create_active_jwk(utf8(b"https://accounts.google.com"), utf8(b"kid4"), utf8(b"k4"), 0);
97+
update_authenticator_state_for_testing(&mut auth_state, vector[jwk4], test_scenario::ctx(scenario));
98+
let recorded_jwks = get_active_jwks_for_testing(&auth_state, test_scenario::ctx(scenario));
99+
assert!(vector::length(&recorded_jwks) == 3, 0);
100+
assert!(vector::borrow(&recorded_jwks, 0) == &jwk2, 0);
101+
assert!(vector::borrow(&recorded_jwks, 1) == &jwk1, 0);
102+
assert!(vector::borrow(&recorded_jwks, 2) == &jwk4, 0);
103+
104+
test_scenario::return_shared(auth_state);
105+
test_scenario::end(scenario_val);
106+
}
107+
68108
#[test]
69109
fun authenticator_state_tests_expiry_edge_cases() {
70110
let scenario_val = test_scenario::begin(@0x0);

0 commit comments

Comments
 (0)