use ed25519_dalek::{Signature, VerifyingKey};
use serde_json::{Map, Value};
use time::OffsetDateTime;
use crate::canonical::canonicalize;
use crate::didkey::decode_did_key;
use crate::error::VerifyError;
const MUTABLE_FIELDS: [&str; 2] = ["proof", "credentialStatus"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CredentialResult {
pub signature_valid: bool,
pub not_expired: bool,
pub not_revoked: bool,
pub structure_valid: bool,
}
impl CredentialResult {
pub fn verify(&self) -> bool {
self.signature_valid && self.not_expired && self.not_revoked
}
}
pub fn verify_credential(vc: &Value, now: OffsetDateTime) -> Result<CredentialResult, VerifyError> {
let obj = vc
.as_object()
.ok_or(VerifyError::Structure("credential must be a JSON object"))?;
let issuer_did = obj
.get("issuer")
.and_then(|i| i.get("id"))
.and_then(Value::as_str)
.or_else(|| obj.get("issuer").and_then(Value::as_str))
.ok_or(VerifyError::Structure("missing issuer.id"))?;
let pubkey = decode_did_key(issuer_did)?;
let proof_value = obj
.get("proof")
.and_then(|p| p.get("proofValue"))
.and_then(Value::as_str)
.ok_or(VerifyError::Structure("missing proof.proofValue"))?;
let structure_valid = true;
let signature_valid = verify_proof(obj, &pubkey, proof_value)?;
let not_expired = check_not_expired(obj, now)?;
let not_revoked = check_not_revoked(obj);
Ok(CredentialResult {
signature_valid,
not_expired,
not_revoked,
structure_valid,
})
}
fn verify_proof(
obj: &Map<String, Value>,
pubkey: &[u8; 32],
proof_value: &str,
) -> Result<bool, VerifyError> {
let mut signing: Map<String, Value> = Map::new();
for (k, v) in obj {
if !MUTABLE_FIELDS.contains(&k.as_str()) {
signing.insert(k.clone(), v.clone());
}
}
let canonical = canonicalize(&Value::Object(signing));
let sig_bytes = decode_b64url(proof_value)?;
let sig = match Signature::from_slice(&sig_bytes) {
Ok(s) => s,
Err(_) => return Ok(false),
};
let key = match VerifyingKey::from_bytes(pubkey) {
Ok(k) => k,
Err(_) => return Ok(false),
};
Ok(key.verify_strict(&canonical, &sig).is_ok())
}
fn check_not_expired(obj: &Map<String, Value>, now: OffsetDateTime) -> Result<bool, VerifyError> {
match obj.get("expirationDate").and_then(Value::as_str) {
None => Ok(true),
Some(exp_str) => {
let exp = parse_rfc3339(exp_str)?;
Ok(now < exp)
}
}
}
fn check_not_revoked(obj: &Map<String, Value>) -> bool {
match obj.get("credentialStatus") {
None => true,
Some(status) => match status.get("revoked") {
Some(Value::Bool(b)) => !b,
Some(Value::Null) | None => true,
Some(other) => other.as_bool().map(|b| !b).unwrap_or(false),
},
}
}
fn decode_b64url(s: &str) -> Result<Vec<u8>, VerifyError> {
use base64::engine::general_purpose::URL_SAFE;
use base64::Engine;
URL_SAFE
.decode(s.as_bytes())
.map_err(|_| VerifyError::Base64("invalid padded base64url"))
}
fn parse_rfc3339(s: &str) -> Result<OffsetDateTime, VerifyError> {
OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
.map_err(|_| VerifyError::Time("invalid RFC 3339 timestamp"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn revoked_true_is_revoked() {
let v: Value = serde_json::from_str(r#"{"credentialStatus":{"revoked":true}}"#).unwrap();
assert!(!check_not_revoked(v.as_object().unwrap()));
}
#[test]
fn revoked_false_is_not_revoked() {
let v: Value = serde_json::from_str(r#"{"credentialStatus":{"revoked":false}}"#).unwrap();
assert!(check_not_revoked(v.as_object().unwrap()));
}
}