use base64::Engine;
use ed25519_dalek::{Signature, VerifyingKey};
use sha2::{Digest, Sha256};
pub fn verify_jws(jws: &str, public_key_b64: &str) -> anyhow::Result<bool> {
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let parts: Vec<&str> = jws.splitn(3, '.').collect();
if parts.len() != 3 {
return Ok(false);
}
if !header_alg_is_eddsa(&b64, parts[0]) {
return Ok(false);
}
let signing_input = format!("{}.{}", parts[0], parts[1]);
let sig_bytes = b64
.decode(parts[2])
.map_err(|e| anyhow::anyhow!("base64 signature: {e}"))?;
let sig_arr: [u8; 64] = sig_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Ed25519 signature must be 64 bytes"))?;
let signature = Signature::from_bytes(&sig_arr);
let key_bytes = b64
.decode(public_key_b64)
.map_err(|e| anyhow::anyhow!("base64 public key: {e}"))?;
let key_arr: [u8; 32] = key_bytes
.as_slice()
.try_into()
.map_err(|_| anyhow::anyhow!("Ed25519 public key must be 32 bytes"))?;
let verifying_key =
VerifyingKey::from_bytes(&key_arr).map_err(|e| anyhow::anyhow!("invalid key: {e}"))?;
Ok(verifying_key
.verify_strict(signing_input.as_bytes(), &signature)
.is_ok())
}
fn jwk_ed25519_x(jwk: &serde_json::Value) -> Option<String> {
if jwk.get("kty")?.as_str()? != "OKP" {
return None;
}
if jwk.get("crv")?.as_str()? != "Ed25519" {
return None;
}
jwk.get("x")?.as_str().map(String::from)
}
fn assertion_method_ids(did_document: &serde_json::Value) -> Vec<String> {
did_document
.get("assertionMethod")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|e| e.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
fn vm_is_assertion_authorized(vm: &serde_json::Value, authorized: &[String]) -> bool {
vm.get("id")
.and_then(|v| v.as_str())
.is_some_and(|id| authorized.iter().any(|a| a == id))
}
pub(crate) fn header_alg_is_eddsa(
b64: &base64::engine::general_purpose::GeneralPurpose,
header_b64: &str,
) -> bool {
b64.decode(header_b64)
.ok()
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).ok())
.and_then(|h| h.get("alg").and_then(|v| v.as_str()).map(str::to_owned))
.map(|alg| super::algorithm::is_allowed_alg(&alg))
.unwrap_or(false)
}
pub fn extract_primary_public_key(did_document: &serde_json::Value) -> Option<String> {
let authorized = assertion_method_ids(did_document);
did_document["verificationMethod"]
.as_array()?
.iter()
.find_map(|vm| {
if vm_is_assertion_authorized(vm, &authorized) {
jwk_ed25519_x(vm.get("publicKeyJwk")?)
} else {
None
}
})
}
pub fn extract_kid_from_jws(jws: &str) -> Option<String> {
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let header_b64 = jws.split('.').next()?;
let header_bytes = b64.decode(header_b64).ok()?;
let header: serde_json::Value = serde_json::from_slice(&header_bytes).ok()?;
header.get("kid")?.as_str().map(String::from)
}
pub fn extract_key_by_fingerprint(did_document: &serde_json::Value, kid: &str) -> Option<String> {
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
let authorized = assertion_method_ids(did_document);
did_document["verificationMethod"]
.as_array()?
.iter()
.find_map(|vm| {
if !vm_is_assertion_authorized(vm, &authorized) {
return None;
}
let x = jwk_ed25519_x(vm.get("publicKeyJwk")?)?;
let raw = b64.decode(&x).ok()?;
if hex::encode(Sha256::digest(&raw)) == kid {
Some(x)
} else {
None
}
})
}