use serde_json::Value;
use super::jws::{b64u, b64u_decode, json_compact_ascii, sha256_b64u, AgentKey, CompactJws, Jwk};
use super::ViError;
#[derive(Debug, Clone)]
pub struct SdJwt {
pub jws: CompactJws,
pub disclosures: Vec<String>,
}
impl SdJwt {
pub fn parse(serialized: &str) -> Result<Self, ViError> {
let mut parts = serialized.trim().split('~');
let jwt = parts.next().unwrap_or_default();
let jws = CompactJws::parse(jwt)?;
let disclosures: Vec<String> = parts
.filter(|p| !p.is_empty())
.map(str::to_string)
.collect();
for d in &disclosures {
decode_disclosure(d)?;
}
Ok(Self { jws, disclosures })
}
pub fn create(
header: &Value,
payload: &Value,
disclosures: Vec<String>,
key: &AgentKey,
) -> Self {
Self {
jws: CompactJws::sign(header, payload, key),
disclosures,
}
}
pub fn header(&self) -> &Value {
&self.jws.header
}
pub fn payload(&self) -> &Value {
&self.jws.payload
}
pub fn base_jwt(&self) -> String {
self.jws.serialize()
}
pub fn serialize(&self) -> String {
selective_presentation(&self.base_jwt(), &self.disclosures)
}
pub fn verify_signature(&self, jwk: &Jwk) -> Result<(), ViError> {
self.jws.verify(jwk)
}
pub fn disclosure_values(&self) -> Vec<Vec<Value>> {
self.disclosures
.iter()
.filter_map(|d| decode_disclosure(d).ok())
.collect()
}
pub fn disclosure_entries(&self) -> Vec<(String, String, Value)> {
self.disclosures
.iter()
.filter_map(|d| {
let arr = decode_disclosure(d).ok()?;
let v = arr.last().cloned()?;
Some((d.clone(), hash_disclosure(d), v))
})
.collect()
}
pub fn find_disclosure(&self, pred: impl Fn(&Value) -> bool) -> Option<String> {
self.disclosure_entries()
.into_iter()
.find(|(_, _, v)| pred(v))
.map(|(d, _, _)| d)
}
pub fn resolve(&self) -> Value {
let mut result = self.payload().clone();
let sd_hashes: Vec<String> = result
.get("_sd")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect()
})
.unwrap_or_default();
let entries: Vec<(String, String, Vec<Value>)> = self
.disclosures
.iter()
.filter_map(|d| {
decode_disclosure(d)
.ok()
.map(|arr| (d.clone(), hash_disclosure(d), arr))
})
.collect();
for (_, h, arr) in &entries {
if sd_hashes.contains(h) && arr.len() == 3 {
if let Some(name) = arr[1].as_str() {
if let Some(obj) = result.as_object_mut() {
obj.insert(name.to_string(), arr[2].clone());
}
}
}
}
if let Some(items) = result
.get("delegate_payload")
.and_then(Value::as_array)
.cloned()
{
let resolved: Vec<Value> = items
.into_iter()
.map(|item| {
let r = item.get("...").and_then(Value::as_str).map(str::to_string);
match r {
Some(h) => entries
.iter()
.find(|(_, eh, _)| *eh == h)
.and_then(|(_, _, arr)| arr.last().cloned())
.unwrap_or(item),
None => item,
}
})
.collect();
if let Some(obj) = result.as_object_mut() {
obj.insert("delegate_payload".into(), Value::Array(resolved));
}
}
result
}
}
pub fn create_disclosure(name: Option<&str>, value: &Value, salt: Option<&str>) -> String {
let salt = salt.map(str::to_string).unwrap_or_else(random_salt);
let arr = match name {
Some(n) => serde_json::json!([salt, n, value]),
None => serde_json::json!([salt, value]),
};
b64u(json_compact_ascii(&arr).as_bytes())
}
pub fn decode_disclosure(disclosure: &str) -> Result<Vec<Value>, ViError> {
let raw = b64u_decode(disclosure)?;
let v: Value = serde_json::from_slice(&raw)
.map_err(|e| ViError::Malformed(format!("disclosure json: {e}")))?;
match v {
Value::Array(a) if a.len() == 2 || a.len() == 3 => Ok(a),
_ => Err(ViError::Malformed(
"disclosure must be a 2- or 3-element array".into(),
)),
}
}
pub fn hash_disclosure(disclosure: &str) -> String {
sha256_b64u(disclosure.as_bytes())
}
pub fn delegate_ref(disclosure_hash: &str) -> Value {
serde_json::json!({ "...": disclosure_hash })
}
pub fn selective_presentation(base_jwt: &str, disclosures: &[String]) -> String {
let mut s = String::from(base_jwt);
for d in disclosures {
s.push('~');
s.push_str(d);
}
s.push('~');
s
}
pub fn presentation_hash(presentation: &str) -> String {
sha256_b64u(presentation.as_bytes())
}
fn random_salt() -> String {
use rand::RngCore;
let mut b = [0u8; 16];
rand::rngs::OsRng.fill_bytes(&mut b);
b64u(&b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disclosure_hash_matches_reference_bytes() {
let d = create_disclosure(None, &serde_json::json!({"id":"x"}), Some("c2FsdA"));
assert_eq!(d, b64u(br#"["c2FsdA",{"id":"x"}]"#));
assert_eq!(hash_disclosure(&d), sha256_b64u(d.as_bytes()));
assert_eq!(decode_disclosure(&d).unwrap().len(), 2);
}
#[test]
fn serialize_round_trips_and_resolves() {
let key = AgentKey::generate();
let d1 = create_disclosure(
None,
&serde_json::json!({"vct":"mandate.checkout.1","checkout_hash":"h"}),
None,
);
let d2 = create_disclosure(Some("email"), &serde_json::json!("a@b.c"), None);
let payload = serde_json::json!({
"nonce": "n", "aud": "a", "iat": 1, "_sd_alg": "sha-256",
"_sd": [hash_disclosure(&d2)],
"delegate_payload": [delegate_ref(&hash_disclosure(&d1))]
});
let header = serde_json::json!({"alg":"ES256","typ":"kb-sd-jwt","kid":key.kid});
let sd = SdJwt::create(&header, &payload, vec![d1.clone(), d2.clone()], &key);
let ser = sd.serialize();
assert!(ser.ends_with('~'));
let back = SdJwt::parse(&ser).unwrap();
assert_eq!(back.serialize(), ser);
back.verify_signature(&key.public_jwk()).unwrap();
let resolved = back.resolve();
assert_eq!(resolved["email"], "a@b.c");
assert_eq!(resolved["delegate_payload"][0]["vct"], "mandate.checkout.1");
assert_eq!(
presentation_hash(&selective_presentation(&back.base_jwt(), &[d1])),
sha256_b64u(format!("{}~{}~", back.base_jwt(), back.disclosures[0]).as_bytes())
);
}
}