use base64::Engine;
use k256::ecdsa::signature::Verifier;
use k256::ecdsa::{Signature, VerifyingKey};
const B64: base64::engine::general_purpose::GeneralPurpose = base64::engine::general_purpose::URL_SAFE_NO_PAD;
const MULTICODEC_SECP256K1: [u8; 2] = [0xe7, 0x01];
const MULTICODEC_P256: [u8; 2] = [0x80, 0x24];
pub const MAX_SERVICE_JWT_LIFETIME_SECS: u64 = 300;
#[derive(Debug, PartialEq, Eq)]
pub enum ServiceJwtError {
Malformed,
BadAlg,
MissingClaim,
BadSignature,
HighS,
AudMismatch,
LxmMismatch,
Expired,
ExcessiveLifetime,
BadKey,
}
#[derive(Debug, PartialEq, Eq)]
pub struct VerifiedClaims {
pub iss: String,
pub lxm: String,
pub jti: String,
pub exp: u64,
}
pub fn sign_bytes(hex_priv: &str, msg: &[u8]) -> Option<String> {
use k256::ecdsa::signature::Signer;
use k256::ecdsa::{Signature, SigningKey};
let priv_bytes = hex::decode(hex_priv.trim()).ok()?;
let sk = SigningKey::from_slice(&priv_bytes).ok()?;
let sig: Signature = sk.sign(msg);
Some(B64.encode(sig.to_bytes()))
}
pub fn mint_service_jwt(hex_priv: &str, iss: &str, aud: &str, lxm: &str, ttl_secs: u64, now_unix: u64) -> Option<String> {
let mut nonce = [0u8; 16];
getrandom::getrandom(&mut nonce).ok()?;
let header = serde_json::json!({ "typ": "JWT", "alg": "ES256K" });
let payload = serde_json::json!({
"iss": iss, "aud": aud, "lxm": lxm,
"iat": now_unix, "exp": now_unix + ttl_secs, "jti": hex::encode(nonce),
});
let signing_input = format!(
"{}.{}",
B64.encode(serde_json::to_vec(&header).ok()?),
B64.encode(serde_json::to_vec(&payload).ok()?)
);
let sig = sign_bytes(hex_priv, signing_input.as_bytes())?;
Some(format!("{signing_input}.{sig}"))
}
pub fn verify_service_jwt(
token: &str,
signer_key: &VerifyingKey,
expected_aud: &str,
expected_lxm: &str,
now_unix: u64,
) -> Result<VerifiedClaims, ServiceJwtError> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(ServiceJwtError::Malformed);
}
let (b64_header, b64_payload, b64_sig) = (parts[0], parts[1], parts[2]);
let header: serde_json::Value = decode_json(b64_header)?;
if header.get("alg").and_then(|v| v.as_str()) != Some("ES256K") {
return Err(ServiceJwtError::BadAlg);
}
let sig_bytes = B64.decode(b64_sig).map_err(|_| ServiceJwtError::Malformed)?;
let sig = Signature::from_slice(&sig_bytes).map_err(|_| ServiceJwtError::Malformed)?;
if sig.normalize_s().is_some() {
return Err(ServiceJwtError::HighS);
}
let signing_input = format!("{b64_header}.{b64_payload}");
signer_key
.verify(signing_input.as_bytes(), &sig)
.map_err(|_| ServiceJwtError::BadSignature)?;
let payload: serde_json::Value = decode_json(b64_payload)?;
let iss = claim_str(&payload, "iss")?;
let aud = claim_str(&payload, "aud")?;
let lxm = claim_str(&payload, "lxm")?;
let exp = claim_u64(&payload, "exp")?;
let _iat = payload.get("iat").and_then(|v| v.as_u64());
let jti = payload
.get("jti")
.and_then(|v| v.as_str())
.or_else(|| payload.get("nonce").and_then(|v| v.as_str()))
.map(str::to_owned)
.ok_or(ServiceJwtError::MissingClaim)?;
if !aud_matches(&aud, expected_aud) {
return Err(ServiceJwtError::AudMismatch);
}
if !lxm_matches(&lxm, expected_lxm, now_unix) {
return Err(ServiceJwtError::LxmMismatch);
}
if now_unix >= exp {
return Err(ServiceJwtError::Expired);
}
if exp - now_unix > MAX_SERVICE_JWT_LIFETIME_SECS {
return Err(ServiceJwtError::ExcessiveLifetime);
}
Ok(VerifiedClaims { iss, lxm, jti, exp })
}
const AUD_FRAGMENT: &str = "#atproto_appview";
fn aud_matches(aud: &str, expected: &str) -> bool {
let bare = |s: &str| -> Option<String> {
let did = s.strip_suffix(AUD_FRAGMENT).unwrap_or(s);
(!did.is_empty() && !did.contains('#')).then(|| did.to_owned())
};
matches!((bare(aud), bare(expected)), (Some(a), Some(e)) if a == e)
}
pub fn multibase_to_verifying_key(s: &str) -> Result<VerifyingKey, ServiceJwtError> {
let b58 = s.strip_prefix('z').ok_or(ServiceJwtError::BadKey)?;
let bytes = bs58::decode(b58).into_vec().map_err(|_| ServiceJwtError::BadKey)?;
if bytes.len() < 2 {
return Err(ServiceJwtError::BadKey);
}
let prefix = [bytes[0], bytes[1]];
if prefix == MULTICODEC_P256 {
return Err(ServiceJwtError::BadKey);
}
if prefix != MULTICODEC_SECP256K1 {
return Err(ServiceJwtError::BadKey);
}
VerifyingKey::from_sec1_bytes(&bytes[2..]).map_err(|_| ServiceJwtError::BadKey)
}
fn lxm_matches(lxm: &str, expected: &str, now_unix: u64) -> bool {
lxm == expected || crate::renames::current_name_at(lxm, now_unix) == Some(expected)
}
pub fn precheck_claims(jwt: &str, expected_aud: &str, expected_lxm: &str, now_unix: u64) -> Option<String> {
let payload_b64 = jwt.split('.').nth(1)?;
let bytes = B64.decode(payload_b64).ok()?;
let payload: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
let aud = payload.get("aud").and_then(|v| v.as_str())?;
if !aud_matches(aud, expected_aud) {
return None;
}
let lxm = payload.get("lxm").and_then(|v| v.as_str())?;
if !lxm_matches(lxm, expected_lxm, now_unix) {
return None;
}
let exp = payload.get("exp").and_then(|v| v.as_u64())?;
if now_unix >= exp {
return None;
}
if exp - now_unix > MAX_SERVICE_JWT_LIFETIME_SECS {
return None;
}
let iss = payload.get("iss").and_then(|v| v.as_str())?;
if !crate::did::is_did(iss) {
return None;
}
Some(iss.to_owned())
}
fn decode_json(seg: &str) -> Result<serde_json::Value, ServiceJwtError> {
let bytes = B64.decode(seg).map_err(|_| ServiceJwtError::Malformed)?;
serde_json::from_slice(&bytes).map_err(|_| ServiceJwtError::Malformed)
}
fn claim_str(payload: &serde_json::Value, key: &str) -> Result<String, ServiceJwtError> {
payload
.get(key)
.and_then(|v| v.as_str())
.map(str::to_owned)
.ok_or(ServiceJwtError::MissingClaim)
}
fn claim_u64(payload: &serde_json::Value, key: &str) -> Result<u64, ServiceJwtError> {
payload
.get(key)
.and_then(|v| v.as_u64())
.ok_or(ServiceJwtError::MissingClaim)
}
#[cfg(test)]
mod tests {
use super::*;
use k256::ecdsa::signature::Signer;
use k256::ecdsa::SigningKey;
const ISS: &str = "did:plc:alice0000000000000000000";
const AUD: &str = "did:web:scenius.makewith.you"; const LXM: &str = "pub.techne.draft.getMyAttention";
const NOW: u64 = 1_750_000_000;
fn keypair() -> (SigningKey, VerifyingKey) {
let sk = SigningKey::from_slice(&[0x11u8; 32]).expect("valid scalar");
let vk = *sk.verifying_key();
(sk, vk)
}
struct MintParams<'a> {
signing_key: &'a SigningKey,
iss: &'a str,
aud: &'a str,
lxm: &'a str,
exp: u64,
iat: Option<u64>,
jti: Option<&'a str>,
nonce: Option<&'a str>,
}
fn mint(p: MintParams<'_>) -> String {
let MintParams { signing_key, iss, aud, lxm, exp, iat, jti, nonce } = p;
let header = serde_json::json!({ "typ": "JWT", "alg": "ES256K" });
let mut payload = serde_json::json!({ "iss": iss, "aud": aud, "lxm": lxm, "exp": exp });
if let Some(iat) = iat {
payload["iat"] = serde_json::json!(iat);
}
if let Some(jti) = jti {
payload["jti"] = serde_json::json!(jti);
}
if let Some(nonce) = nonce {
payload["nonce"] = serde_json::json!(nonce);
}
let b64_header = B64.encode(serde_json::to_vec(&header).unwrap());
let b64_payload = B64.encode(serde_json::to_vec(&payload).unwrap());
let signing_input = format!("{b64_header}.{b64_payload}");
let sig: Signature = signing_key.sign(signing_input.as_bytes());
let b64_sig = B64.encode(sig.to_bytes());
format!("{signing_input}.{b64_sig}")
}
fn good_token(sk: &SigningKey) -> String {
mint(MintParams { signing_key: sk, iss: ISS, aud: AUD, lxm: LXM, exp: NOW + 60, iat: Some(NOW), jti: Some("nonce-0001"), nonce: None })
}
#[test]
fn valid_token_verifies_and_yields_the_issuer_did() {
let (sk, vk) = keypair();
let claims = verify_service_jwt(&good_token(&sk), &vk, AUD, LXM, NOW).expect("should verify");
assert_eq!(claims.iss, ISS, "iss becomes Principal::User(iss)");
assert_eq!(claims.lxm, LXM);
assert_eq!(claims.jti, "nonce-0001");
assert_eq!(claims.exp, NOW + 60);
}
#[test]
fn tampered_payload_fails_signature() {
let (sk, vk) = keypair();
let token = good_token(&sk);
let mut parts: Vec<String> = token.split('.').map(str::to_owned).collect();
let forged = serde_json::json!({
"iss": "did:plc:mallory00000000000000000", "aud": AUD, "lxm": LXM, "exp": NOW + 60, "iat": NOW, "jti": "x",
});
parts[1] = B64.encode(serde_json::to_vec(&forged).unwrap());
let token = parts.join(".");
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::BadSignature));
}
#[test]
fn wrong_signer_key_fails_signature() {
let (sk, _) = keypair();
let other = SigningKey::from_slice(&[0x22u8; 32]).unwrap();
let wrong_vk = *other.verifying_key();
assert_eq!(
verify_service_jwt(&good_token(&sk), &wrong_vk, AUD, LXM, NOW),
Err(ServiceJwtError::BadSignature)
);
}
#[test]
fn token_for_another_service_is_rejected() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: "did:web:someone-else.example", lxm: LXM, exp: NOW + 60, iat: Some(NOW), jti: Some("x"), nonce: None });
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::AudMismatch));
}
#[test]
fn token_for_another_method_is_rejected() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: "pub.techne.draft.deleteEverything", exp: NOW + 60, iat: Some(NOW), jti: Some("x"), nonce: None });
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::LxmMismatch));
}
#[test]
fn expired_token_is_rejected() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: LXM, exp: NOW - 1, iat: Some(NOW - 61), jti: Some("x"), nonce: None });
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::Expired));
}
#[test]
fn excessive_lifetime_is_rejected() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: LXM, exp: NOW + 86_400, iat: Some(NOW), jti: Some("x"), nonce: None });
assert_eq!(
verify_service_jwt(&token, &vk, AUD, LXM, NOW),
Err(ServiceJwtError::ExcessiveLifetime)
);
let at_ceiling = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: LXM, exp: NOW + MAX_SERVICE_JWT_LIFETIME_SECS, iat: Some(NOW), jti: Some("x"), nonce: None });
assert!(verify_service_jwt(&at_ceiling, &vk, AUD, LXM, NOW).is_ok());
}
#[test]
fn non_es256k_alg_is_rejected() {
let (_, vk) = keypair();
let header = B64.encode(br#"{"typ":"JWT","alg":"HS256"}"#);
let payload = B64.encode(br#"{"iss":"x","aud":"x","lxm":"x","exp":9999999999,"iat":1,"jti":"x"}"#);
let token = format!("{header}.{payload}.{}", B64.encode([0u8; 64]));
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::BadAlg));
}
#[test]
fn garbage_is_rejected_not_panicked() {
let (_, vk) = keypair();
for junk in ["", "a.b", "...", "not-a-jwt", "a.b.c.d"] {
assert!(verify_service_jwt(junk, &vk, AUD, LXM, NOW).is_err(), "junk {junk:?} must fail closed");
}
}
#[test]
fn iat_is_optional() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: LXM, exp: NOW + 60, iat: None, jti: Some("x"), nonce: None });
assert!(verify_service_jwt(&token, &vk, AUD, LXM, NOW).is_ok(), "iat must be optional");
}
#[test]
fn lxm_is_required() {
let (sk, vk) = keypair();
let header = B64.encode(br#"{"typ":"JWT","alg":"ES256K"}"#);
let payload = serde_json::json!({ "iss": ISS, "aud": AUD, "exp": NOW + 60, "jti": "x" });
let b64_payload = B64.encode(serde_json::to_vec(&payload).unwrap());
let signing_input = format!("{header}.{b64_payload}");
let sig: Signature = sk.sign(signing_input.as_bytes());
let token = format!("{signing_input}.{}", B64.encode(sig.to_bytes()));
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::MissingClaim));
}
#[test]
fn nonce_is_accepted_when_jti_absent() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: LXM, exp: NOW + 60, iat: Some(NOW), jti: None, nonce: Some("nonce-from-lexserver") });
let claims = verify_service_jwt(&token, &vk, AUD, LXM, NOW).expect("nonce should be accepted");
assert_eq!(claims.jti, "nonce-from-lexserver");
}
#[test]
fn missing_jti_and_nonce_is_rejected() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: LXM, exp: NOW + 60, iat: Some(NOW), jti: None, nonce: None });
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::MissingClaim));
}
#[test]
fn aud_accepts_the_atproto_appview_fragment_form() {
let (sk, vk) = keypair();
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: &format!("{AUD}#atproto_appview"), lxm: LXM, exp: NOW + 60, iat: Some(NOW), jti: Some("x"), nonce: None });
assert!(verify_service_jwt(&token, &vk, AUD, LXM, NOW).is_ok());
let token2 = good_token(&sk);
assert!(verify_service_jwt(&token2, &vk, &format!("{AUD}#atproto_appview"), LXM, NOW).is_ok());
}
#[test]
fn aud_refuses_any_other_fragment() {
let (sk, vk) = keypair();
for aud in [format!("{AUD}#bsky_appview"), format!("{AUD}#atproto_labeler"), format!("{AUD}#"), format!("{AUD}#atproto_appview#atproto_appview")] {
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: &aud, lxm: LXM, exp: NOW + 60, iat: Some(NOW), jti: Some("x"), nonce: None });
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW).err(), Some(ServiceJwtError::AudMismatch), "{aud}");
assert_eq!(precheck_claims(&token, AUD, LXM, NOW), None, "{aud}");
}
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: &format!("{AUD}#other"), lxm: LXM, exp: NOW + 60, iat: Some(NOW), jti: Some("x"), nonce: None });
assert_eq!(verify_service_jwt(&token, &vk, &format!("{AUD}#other"), LXM, NOW).err(), Some(ServiceJwtError::AudMismatch));
assert!(!aud_matches(AUD, &format!("{AUD}#other")));
assert!(!aud_matches("#atproto_appview", "#atproto_appview"));
}
#[test]
fn high_s_signature_is_rejected() {
let (sk, vk) = keypair();
let header = B64.encode(br#"{"typ":"JWT","alg":"ES256K"}"#);
let payload = serde_json::json!({ "iss": ISS, "aud": AUD, "lxm": LXM, "exp": NOW + 60, "jti": "x" });
let b64_payload = B64.encode(serde_json::to_vec(&payload).unwrap());
let signing_input = format!("{header}.{b64_payload}");
let sig: Signature = sk.sign(signing_input.as_bytes());
let high = sig.normalize_s().unwrap_or(sig);
let high_s_sig = {
use k256::elliptic_curve::scalar::IsHigh;
let s = high.s();
let neg_s = -*s;
let candidate = Signature::from_scalars(*high.r(), neg_s).unwrap();
assert!(bool::from(candidate.s().is_high()), "candidate must be high-S");
candidate
};
let token = format!("{signing_input}.{}", B64.encode(high_s_sig.to_bytes()));
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::HighS));
}
fn encode_did_key_multibase(vk: &VerifyingKey) -> String {
let compressed = vk.to_encoded_point(true);
let mut prefixed = Vec::with_capacity(2 + compressed.as_bytes().len());
prefixed.extend_from_slice(&MULTICODEC_SECP256K1);
prefixed.extend_from_slice(compressed.as_bytes());
format!("z{}", bs58::encode(prefixed).into_string())
}
#[test]
fn multibase_round_trips_the_keys_rs_encoder() {
let (_, vk) = keypair();
let mb = encode_did_key_multibase(&vk);
let decoded = multibase_to_verifying_key(&mb).expect("should decode our own encoding");
assert_eq!(decoded, vk, "decode must invert the keys.rs encoder");
}
#[test]
fn multibase_rejects_p256_multicodec() {
let mut bytes = MULTICODEC_P256.to_vec();
bytes.extend_from_slice(&[0x02; 33]);
let mb = format!("z{}", bs58::encode(&bytes).into_string());
assert_eq!(multibase_to_verifying_key(&mb), Err(ServiceJwtError::BadKey));
}
#[test]
fn multibase_rejects_garbage() {
assert_eq!(multibase_to_verifying_key(""), Err(ServiceJwtError::BadKey)); assert_eq!(multibase_to_verifying_key("zzz!!!"), Err(ServiceJwtError::BadKey)); assert_eq!(multibase_to_verifying_key("z"), Err(ServiceJwtError::BadKey)); }
#[test]
fn verify_chains_through_a_resolved_multibase_key() {
let (sk, vk) = keypair();
let mb = encode_did_key_multibase(&vk);
let resolved = multibase_to_verifying_key(&mb).unwrap();
let claims = verify_service_jwt(&good_token(&sk), &resolved, AUD, LXM, NOW).expect("verify");
assert_eq!(claims.iss, ISS);
}
#[test]
fn sign_bytes_round_trips_through_verifying_key() {
use k256::ecdsa::signature::Verifier;
let (sk, vk) = keypair();
let hex_priv = hex::encode(sk.to_bytes());
let msg = b"the quick brown fox";
let sig_b64 = sign_bytes(&hex_priv, msg).expect("valid scalar signs");
let sig_bytes = B64.decode(&sig_b64).expect("base64url-nopad");
let sig = Signature::from_slice(&sig_bytes).expect("64-byte r||s");
assert!(vk.verify(msg, &sig).is_ok(), "the resolved verifying key must accept the signature");
}
#[test]
fn sign_bytes_is_deterministic_and_message_bound() {
let (sk, _) = keypair();
let hex_priv = hex::encode(sk.to_bytes());
let a = sign_bytes(&hex_priv, b"payload a").unwrap();
let b = sign_bytes(&hex_priv, b"payload a").unwrap();
assert_eq!(a, b, "RFC 6979 deterministic ECDSA: same key + same message ⇒ same signature");
let c = sign_bytes(&hex_priv, b"payload b").unwrap();
assert_ne!(a, c, "a different message must yield a different signature");
}
#[test]
fn sign_bytes_is_always_low_s() {
let (sk, _) = keypair();
let hex_priv = hex::encode(sk.to_bytes());
for msg in [&b"one"[..], b"two", b"three-longer-message"] {
let sig_b64 = sign_bytes(&hex_priv, msg).unwrap();
let sig_bytes = B64.decode(&sig_b64).unwrap();
let sig = Signature::from_slice(&sig_bytes).unwrap();
assert!(sig.normalize_s().is_none(), "k256 must produce a low-S signature");
}
}
#[test]
fn sign_bytes_rejects_malformed_key_material() {
assert!(sign_bytes("not-hex", b"msg").is_none());
assert!(sign_bytes("00", b"msg").is_none(), "wrong-length scalar");
}
fn last_day_noon(old: &str) -> u64 {
let doc: serde_json::Value = serde_json::from_str(include_str!("nsid_renames.json")).unwrap();
let early = doc["closeEarlier"].as_object().unwrap().iter().find(|(_, olds)| olds.as_array().unwrap().iter().any(|o| o == old));
let day = early.map(|(d, _)| d.as_str()).unwrap_or_else(|| doc["deleteAfter"].as_str().unwrap());
let date = chrono::NaiveDate::parse_from_str(day, "%Y-%m-%d").unwrap();
date.and_hms_opt(12, 0, 0).unwrap().and_utc().timestamp() as u64
}
#[test]
fn an_old_name_of_the_called_method_binds_inside_its_window_only() {
let (sk, vk) = keypair();
let doc: serde_json::Value = serde_json::from_str(include_str!("nsid_renames.json")).unwrap();
let renames = doc["renames"].as_object().unwrap();
let pair = |pick: &dyn Fn(&str) -> bool| {
let (o, n) = renames.iter().find(|(o, _)| pick(o)).expect("a renamed method");
(o.clone(), n.as_str().unwrap().to_owned())
};
let pre_n1 = pair(&|o| o.starts_with("social.scenius."));
let n1 = pair(&|o| o.ends_with(".commons.runChain"));
for (old_m, new_m) in [pre_n1, n1] {
let (old_m, new_m) = (old_m.as_str(), new_m.as_str());
let token = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: old_m, exp: NOW + 60, iat: Some(NOW), jti: Some("x"), nonce: None });
let claims = verify_service_jwt(&token, &vk, AUD, new_m, NOW).unwrap_or_else(|e| panic!("{old_m} binds {new_m}: {e:?}"));
assert_eq!(claims.lxm, old_m);
assert_eq!(precheck_claims(&token, AUD, new_m, NOW).as_deref(), Some(ISS), "{old_m} prechecks for {new_m}");
let last = last_day_noon(old_m);
let on_last = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: old_m, exp: last + 60, iat: Some(last), jti: Some("w"), nonce: None });
assert!(verify_service_jwt(&on_last, &vk, AUD, new_m, last).is_ok(), "{old_m}: honoured through its last day");
let after = last + 86_400;
let late = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: old_m, exp: after + 60, iat: Some(after), jti: Some("y"), nonce: None });
assert_eq!(verify_service_jwt(&late, &vk, AUD, new_m, after), Err(ServiceJwtError::LxmMismatch), "{old_m} after its window");
assert!(precheck_claims(&late, AUD, new_m, after).is_none());
let current = mint(MintParams { signing_key: &sk, iss: ISS, aud: AUD, lxm: new_m, exp: NOW + 60, iat: Some(NOW), jti: Some("z"), nonce: None });
assert_eq!(verify_service_jwt(¤t, &vk, AUD, old_m, NOW), Err(ServiceJwtError::LxmMismatch));
assert!(precheck_claims(¤t, AUD, old_m, NOW).is_none(), "{new_m} does not precheck for {old_m}");
assert_eq!(verify_service_jwt(&token, &vk, AUD, LXM, NOW), Err(ServiceJwtError::LxmMismatch));
assert!(precheck_claims(&token, AUD, LXM, NOW).is_none());
}
}
}
#[cfg(test)]
mod mint_tests {
use super::*;
#[test]
fn a_minted_token_verifies_for_its_aud_and_lxm_only() {
let hex_priv = hex::encode([0x22u8; 32]);
let sk = k256::ecdsa::SigningKey::from_slice(&[0x22u8; 32]).unwrap();
let vk = *sk.verifying_key();
let lxm = "pub.techne.draft.commons.runChain";
let now = 1_750_000_000;
let jwt = mint_service_jwt(&hex_priv, "did:web:appview.example", "did:web:rules.example", lxm, 60, now).unwrap();
let claims = verify_service_jwt(&jwt, &vk, "did:web:rules.example", lxm, now + 1).unwrap();
assert_eq!(claims.iss, "did:web:appview.example");
assert!(verify_service_jwt(&jwt, &vk, "did:web:other.example", lxm, now + 1).is_err());
assert!(verify_service_jwt(&jwt, &vk, "did:web:rules.example", "pub.techne.draft.getEvent", now + 1).is_err());
assert!(verify_service_jwt(&jwt, &vk, "did:web:rules.example", lxm, now + 120).is_err(), "expired");
assert!(mint_service_jwt("not hex", "a", "b", lxm, 60, now).is_none());
}
}