use super::tests::{ISSUER, NOW, role, signer};
use super::{AUDIENCE, Gate, Unreachable};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use ed25519_dalek::Signer;
use lys_pass::{Error, KeySet};
use serde_json::json;
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn keys(kid: &str) -> Result<KeySet, Error> {
let x = URL_SAFE_NO_PAD.encode(signer().verifying_key().as_bytes());
KeySet::from_json(
&json!({"keys":[{"kty":"OKP","crv":"Ed25519","kid":kid,"x":x,"alg":"EdDSA","use":"sig"}]})
.to_string(),
)
}
fn token(kid: &str) -> String {
let header = URL_SAFE_NO_PAD.encode(json!({"alg":"EdDSA","typ":"JWT","kid":kid}).to_string());
let claims = URL_SAFE_NO_PAD.encode(json!({
"iss": ISSUER, "aud":AUDIENCE, "sub":"reader", "iat":NOW - 10, "exp":NOW + 600,
"holder":{"id":"reader","kind":"person","responsible":null}, "rights":role(&["read"],"grant")
}).to_string());
let signature = URL_SAFE_NO_PAD.encode(
signer()
.sign(format!("{header}.{claims}").as_bytes())
.to_bytes(),
);
format!("{header}.{claims}.{signature}")
}
#[test]
fn rotated_issuer_keys_are_refetched_once_for_concurrent_passes() -> TestResult {
let asks = Arc::new(AtomicUsize::new(0));
let count = Arc::clone(&asks);
let gate = Gate::fetching(
move || {
let ask = count.fetch_add(1, Ordering::SeqCst);
keys(if ask == 0 { "old" } else { "rotated" })
},
ISSUER,
"wards",
Arc::new(Unreachable),
)?;
gate.keys.wait();
let new = token("rotated");
std::thread::scope(|scope| -> TestResult {
let workers: Vec<_> = (0..8)
.map(|_| scope.spawn(|| gate.present(&new, NOW)))
.collect();
for worker in workers {
let answer = worker.join().map_err(|_| "verification panicked")?;
assert!(answer.is_ok(), "rotated issuer refused a pass: {answer:?}");
}
Ok(())
})?;
assert_eq!(
asks.load(Ordering::SeqCst),
2,
"rotation was not single-flight"
);
for _ in 0..5 {
let refusal = gate
.present(&token("unpublished"), NOW)
.err()
.ok_or("unpublished key accepted")?;
assert_eq!(refusal.code, "unpublished_key");
}
assert_eq!(
asks.load(Ordering::SeqCst),
2,
"unknown ids bypassed the refresh interval"
);
Ok(())
}
#[test]
fn hello_remains_available_while_issuer_keys_are_pending() -> TestResult {
use crate::serve::protocol::{HelloAgreement, Operation};
use std::sync::{Mutex, mpsc};
let (release, released) = mpsc::channel();
let released = Mutex::new(released);
let gate = Gate::fetching(
move || {
released
.lock()
.map_err(|_| Error::CannotAsk("fixture poisoned"))?
.recv()
.map_err(|_| Error::CannotAsk("fixture closed"))?;
keys("old")
},
ISSUER,
"wards",
Arc::new(Unreachable),
)?;
let result = gate.authorize(None, &Operation::Hello(HelloAgreement::client()), None, NOW);
release.send(())?;
gate.keys.wait();
assert!(
result.is_ok(),
"hello was gated on pending issuer keys: {result:?}"
);
Ok(())
}