use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::{base64_url_decode, VerifyConfig};
use crate::error::VerifyError;
pub struct ParsedReceipt {
pub header: Value,
pub payload: Value,
pub signing_pubkey: [u8; 32],
}
pub fn kid_for_pubkey(pubkey: &[u8]) -> String {
let mut h = Sha256::new();
h.update(pubkey);
let digest = h.finalize();
hex::encode(&digest[..])[..16].to_string()
}
pub fn parse_and_verify(jwt: &str, cfg: &VerifyConfig) -> Result<ParsedReceipt, VerifyError> {
let parts: Vec<&str> = jwt.split('.').collect();
if parts.len() != 3 {
return Err(VerifyError::SchemaViolation(
format!("JWT must have 3 segments, got {}", parts.len())));
}
let header_bytes = base64_url_decode(parts[0])
.map_err(|_| VerifyError::SchemaViolation("header is not base64url".into()))?;
let header: Value = serde_json::from_slice(&header_bytes)
.map_err(|_| VerifyError::SchemaViolation("header is not JSON".into()))?;
let alg = header.get("alg").and_then(|v| v.as_str()).unwrap_or("");
if alg != "EdDSA" {
return Err(VerifyError::SchemaViolation(
format!("alg must be EdDSA, got {alg:?}")));
}
let kid = header.get("kid").and_then(|v| v.as_str()).unwrap_or("");
if kid.is_empty() {
return Err(VerifyError::SchemaViolation("header missing kid".into()));
}
let pubkey_bytes = cfg.lookup_authority(kid).ok_or(VerifyError::WrongSigner)?;
let pubkey_arr: [u8; 32] = pubkey_bytes.try_into()
.map_err(|_| VerifyError::SchemaViolation("pinned authority key wrong length".into()))?;
let vk = VerifyingKey::from_bytes(&pubkey_arr)
.map_err(|e| VerifyError::SignatureInvalid(format!("bad authority pubkey: {e}")))?;
let signing_input = format!("{}.{}", parts[0], parts[1]);
let sig_bytes = base64_url_decode(parts[2])
.map_err(|_| VerifyError::SignatureInvalid("signature segment not base64url".into()))?;
let sig = Signature::from_slice(&sig_bytes)
.map_err(|e| VerifyError::SignatureInvalid(format!("malformed signature: {e}")))?;
vk.verify(signing_input.as_bytes(), &sig)
.map_err(|e| VerifyError::SignatureInvalid(format!("EdDSA verify failed: {e}")))?;
let payload_bytes = base64_url_decode(parts[1])
.map_err(|_| VerifyError::SchemaViolation("payload is not base64url".into()))?;
let payload: Value = serde_json::from_slice(&payload_bytes)
.map_err(|_| VerifyError::SchemaViolation("payload is not JSON".into()))?;
Ok(ParsedReceipt { header, payload, signing_pubkey: pubkey_arr })
}