use core::fmt;
use core::str::FromStr;
use std::collections::BTreeSet;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
use super::{
ClaimPath, Confirmation, Disclosure, HashAlgorithm, KeyBinding, PathComponent, Processed,
SdJwt, SdJwtBuilder, SdJwtError, Status,
};
use crate::jose::{Jwk, JwsHeader, Signer};
use crate::vc::{CredentialError, CredentialV2, JwtClaims};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SdJwtCredentialV2Error {
#[error(transparent)]
SdJwt(#[from] SdJwtError),
#[error("header `typ` is {0:?}, not `vc+sd-jwt`")]
UnexpectedTyp(String),
#[error("`{0}` must not be selectively disclosed")]
DisclosedRegisteredClaim(String),
#[error("the payload is not a JWT claims set: {0}")]
Payload(String),
#[error("claim `{claim}`: {reason}")]
InvalidClaim {
claim: &'static str,
reason: String,
},
#[error(transparent)]
Credential(#[from] CredentialError),
}
#[derive(Clone, PartialEq, Eq)]
pub struct SdJwtCredentialV2 {
sd_jwt: SdJwt,
header: JwsHeader,
processed: Processed,
claims: JwtClaims,
credential: CredentialV2,
cnf: Option<Confirmation>,
status: Option<Status>,
}
impl SdJwtCredentialV2 {
pub const TYP: &'static str = "vc+sd-jwt";
pub const MEDIA_TYPE: &'static str = "application/vc+sd-jwt";
pub const NON_DISCLOSABLE_CLAIMS: [&'static str; 14] = [
"@context",
"type",
"issuer",
"validFrom",
"validUntil",
"credentialStatus",
"credentialSchema",
"relatedResource",
"_sd_alg",
"iss",
"nbf",
"exp",
"cnf",
"status",
];
pub fn parse(s: &str) -> Result<Self, SdJwtCredentialV2Error> {
Self::from_sd_jwt(SdJwt::parse(s)?)
}
pub fn from_sd_jwt(sd_jwt: SdJwt) -> Result<Self, SdJwtCredentialV2Error> {
Self::from_sd_jwt_with(sd_jwt, &BTreeSet::new())
}
pub fn from_sd_jwt_with(
sd_jwt: SdJwt,
additional_typ: &BTreeSet<String>,
) -> Result<Self, SdJwtCredentialV2Error> {
let header = sd_jwt
.jwt()
.decode()
.map_err(SdJwtError::from)?
.header()
.clone();
if let Some(typ) = header.typ.as_deref()
&& typ != Self::TYP
&& !additional_typ.contains(typ)
{
return Err(SdJwtCredentialV2Error::UnexpectedTyp(typ.to_owned()));
}
let processed = sd_jwt.process()?;
for claim in &processed.disclosed {
if let Some(PathComponent::Key(name)) = claim.path.components().first()
&& Self::NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
{
return Err(SdJwtCredentialV2Error::DisclosedRegisteredClaim(
name.clone(),
));
}
}
let claims: JwtClaims = serde_json::from_value(Value::Object(processed.payload.clone()))
.map_err(|e| SdJwtCredentialV2Error::Payload(e.to_string()))?;
let mut rest = claims.clone();
let cnf = take_claim(&mut rest, "cnf")?;
let status = take_claim(&mut rest, "status")?;
let credential = CredentialV2::from_jwt_claims(&rest)?;
Ok(Self {
sd_jwt,
header,
processed,
claims,
credential,
cnf,
status,
})
}
pub fn sd_jwt(&self) -> &SdJwt {
&self.sd_jwt
}
pub fn into_sd_jwt(self) -> SdJwt {
self.sd_jwt
}
pub fn header(&self) -> &JwsHeader {
&self.header
}
pub fn credential(&self) -> &CredentialV2 {
&self.credential
}
pub fn claims(&self) -> &JwtClaims {
&self.claims
}
pub fn confirmation(&self) -> Option<&Confirmation> {
self.cnf.as_ref()
}
pub fn status(&self) -> Option<&Status> {
self.status.as_ref()
}
pub fn payload(&self) -> &Map<String, Value> {
&self.processed.payload
}
pub fn processed(&self) -> &Processed {
&self.processed
}
pub fn holder_key(&self) -> Option<&Jwk> {
self.cnf.as_ref()?.jwk.as_ref()
}
pub fn disclose<'a>(
&self,
paths: impl IntoIterator<Item = &'a ClaimPath>,
) -> Result<Self, SdJwtCredentialV2Error> {
Self::from_sd_jwt_with(self.sd_jwt.disclose(paths)?, &self.accepted_typ())
}
pub fn select(
&self,
keep: impl FnMut(&Disclosure) -> bool,
) -> Result<Self, SdJwtCredentialV2Error> {
Self::from_sd_jwt_with(self.sd_jwt.select(keep)?, &self.accepted_typ())
}
pub async fn sign_key_binding(
self,
binding: &KeyBinding,
signer: &dyn Signer,
) -> Result<Self, SdJwtCredentialV2Error> {
let sd_jwt = self.sd_jwt.sign_key_binding(binding, signer).await?;
Ok(Self { sd_jwt, ..self })
}
fn accepted_typ(&self) -> BTreeSet<String> {
self.header.typ.iter().cloned().collect()
}
}
fn take_claim<T: serde::de::DeserializeOwned>(
claims: &mut JwtClaims,
claim: &'static str,
) -> Result<Option<T>, SdJwtCredentialV2Error> {
claims
.additional
.shift_remove(claim)
.map(|value| {
serde_json::from_value(value).map_err(|e| SdJwtCredentialV2Error::InvalidClaim {
claim,
reason: e.to_string(),
})
})
.transpose()
}
impl fmt::Debug for SdJwtCredentialV2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SdJwtCredentialV2")
.field("header", &self.header)
.field("credential", &self.credential)
.field("cnf", &self.cnf)
.field("status", &self.status)
.field("sd_jwt", &self.sd_jwt)
.finish_non_exhaustive()
}
}
impl fmt::Display for SdJwtCredentialV2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.sd_jwt, f)
}
}
impl FromStr for SdJwtCredentialV2 {
type Err = SdJwtCredentialV2Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl Serialize for SdJwtCredentialV2 {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for SdJwtCredentialV2 {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
pub struct SdJwtCredentialV2Builder {
credential: CredentialV2,
iat: Option<i64>,
nbf: Option<i64>,
exp: Option<i64>,
cnf: Option<Confirmation>,
status: Option<Status>,
kid: Option<String>,
concealed: Vec<ClaimPath>,
decoys: usize,
alg: HashAlgorithm,
salts: Option<Box<dyn FnMut() -> String + Send>>,
}
impl SdJwtCredentialV2Builder {
pub fn new(credential: CredentialV2) -> Self {
Self {
credential,
iat: None,
nbf: None,
exp: None,
cnf: None,
status: None,
kid: None,
concealed: Vec::new(),
decoys: 0,
alg: HashAlgorithm::default(),
salts: None,
}
}
pub fn credential(&self) -> &CredentialV2 {
&self.credential
}
pub fn clone_credential(&self) -> Self {
Self::new(self.credential.clone())
}
#[must_use]
pub fn with_issued_at(mut self, time: DateTime<Utc>) -> Self {
self.iat = Some(JwtClaims::numeric_date(time));
self
}
#[must_use]
pub fn with_not_before(mut self, time: DateTime<Utc>) -> Self {
self.nbf = Some(JwtClaims::numeric_date(time));
self
}
#[must_use]
pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
self.exp = Some(JwtClaims::numeric_date(time));
self
}
#[must_use]
pub fn with_confirmation(mut self, cnf: Confirmation) -> Self {
self.cnf = Some(cnf);
self
}
#[must_use]
pub fn with_holder_key(self, jwk: &Jwk) -> Self {
self.with_confirmation(Confirmation::jwk(jwk))
}
#[must_use]
pub fn with_status(mut self, status: Status) -> Self {
self.status = Some(status);
self
}
#[must_use]
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
self.kid = Some(kid.into());
self
}
#[must_use]
pub fn conceal(mut self, path: impl Into<ClaimPath>) -> Self {
self.concealed.push(path.into());
self
}
#[must_use]
pub fn with_decoys(mut self, count: usize) -> Self {
self.decoys = count;
self
}
#[must_use]
pub fn with_hash_algorithm(mut self, alg: HashAlgorithm) -> Self {
self.alg = alg;
self
}
#[must_use]
pub fn with_salt_generator(mut self, salts: impl FnMut() -> String + Send + 'static) -> Self {
self.salts = Some(Box::new(salts));
self
}
pub fn check_concealed(&self) -> Result<(), SdJwtCredentialV2Error> {
for path in &self.concealed {
if let Some(PathComponent::Key(name)) = path.components().first()
&& SdJwtCredentialV2::NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
{
return Err(SdJwtError::NotConcealable {
path: path.clone(),
reason: format!("a credential secured with SD-JWT keeps `{name}` disclosed"),
}
.into());
}
}
Ok(())
}
fn claims(&self) -> JwtClaims {
let mut claims = self.credential.to_jwt_claims();
claims.iss = Some(self.credential.issuer.id().to_string());
claims.iat = self.iat;
claims.nbf = self.nbf;
claims.exp = self.exp;
if let Some(cnf) = &self.cnf {
claims.additional.insert(
"cnf".to_owned(),
serde_json::to_value(cnf).expect("a confirmation serializes"),
);
}
if let Some(status) = &self.status {
claims.additional.insert(
"status".to_owned(),
serde_json::to_value(status).expect("a status serializes"),
);
}
claims
}
pub async fn sign(
self,
signer: &dyn Signer,
) -> Result<SdJwtCredentialV2, SdJwtCredentialV2Error> {
self.check_concealed()?;
let claims = match serde_json::to_value(self.claims()).expect("claims serialize") {
Value::Object(map) => map,
_ => unreachable!("a struct serializes to an object"),
};
let mut header = JwsHeader::new(signer.alg()).with_typ(SdJwtCredentialV2::TYP);
header.kid = self.kid.or_else(|| signer.kid().map(str::to_owned));
let mut builder = SdJwtBuilder::new(claims)
.with_decoys(self.decoys)
.with_hash_algorithm(self.alg);
for path in self.concealed {
builder = builder.conceal(path);
}
if let Some(salts) = self.salts {
builder = builder.with_salt_generator(salts);
}
let sd_jwt = builder.sign(&header, signer).await?;
SdJwtCredentialV2::from_sd_jwt(sd_jwt)
}
}
impl fmt::Debug for SdJwtCredentialV2Builder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SdJwtCredentialV2Builder")
.field("credential", &self.credential)
.field("iat", &self.iat)
.field("nbf", &self.nbf)
.field("exp", &self.exp)
.field("cnf", &self.cnf)
.field("status", &self.status)
.field("kid", &self.kid)
.field("concealed", &self.concealed)
.field("decoys", &self.decoys)
.field("alg", &self.alg)
.finish_non_exhaustive()
}
}