use chrono::{DateTime, Utc};
use serde_json::{Map, Value};
use dpp_crypto::sd_jwt::SdJwt;
use super::TYP;
use super::error::SdJwtVcError;
const TEMPORAL_CLAIMS: [&str; 3] = ["iat", "nbf", "exp"];
pub fn verify(
serialised: &str,
public_key_b64: &str,
expected_vct: &str,
now: DateTime<Utc>,
) -> Result<Map<String, Value>, SdJwtVcError> {
let sd_jwt = SdJwt::parse(serialised)?;
match dpp_crypto::jws::verifier::verify_jws(sd_jwt.jwt(), public_key_b64) {
Ok(true) => {}
_ => return Err(SdJwtVcError::BadSignature),
}
let typ = jwt_header_typ(sd_jwt.jwt()).unwrap_or_default();
if typ != TYP {
return Err(SdJwtVcError::WrongTyp(typ));
}
let payload = sd_jwt.disclosed_payload()?;
if payload.get("iss").and_then(Value::as_str).is_none() {
return Err(SdJwtVcError::MissingClaim("iss"));
}
let Some(found) = payload.get("vct").and_then(Value::as_str) else {
return Err(SdJwtVcError::MissingClaim("vct"));
};
if found != expected_vct {
return Err(SdJwtVcError::UnexpectedVct {
expected: expected_vct.to_owned(),
found: found.to_owned(),
});
}
check_validity_window(&payload, now)?;
Ok(payload)
}
fn check_validity_window(
payload: &Map<String, Value>,
now: DateTime<Utc>,
) -> Result<(), SdJwtVcError> {
for claim in TEMPORAL_CLAIMS {
let Some(value) = payload.get(claim) else {
continue;
};
let Some(seconds) = value.as_f64().filter(|s| s.is_finite()) else {
return Err(SdJwtVcError::MalformedTemporalClaim(claim));
};
let whole = seconds.floor();
#[expect(
clippy::cast_possible_truncation,
reason = "guarded: `whole` is finite and range-checked by from_timestamp below"
)]
let nanos = ((seconds - whole) * 1e9).round() as u32;
#[expect(
clippy::cast_possible_truncation,
reason = "same guard; an out-of-range i64 cast is caught by from_timestamp"
)]
let Some(instant) = DateTime::from_timestamp(whole as i64, nanos.min(999_999_999)) else {
return Err(SdJwtVcError::MalformedTemporalClaim(claim));
};
match claim {
"exp" if now >= instant => return Err(SdJwtVcError::Expired { at: instant }),
"nbf" if now < instant => return Err(SdJwtVcError::NotYetValid { from: instant }),
_ => {}
}
}
Ok(())
}
fn jwt_header_typ(jwt: &str) -> Option<String> {
use base64::Engine;
let header_b64 = jwt.split('.').next()?;
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(header_b64)
.ok()?;
let header: Value = serde_json::from_slice(&bytes).ok()?;
header.get("typ")?.as_str().map(str::to_owned)
}