use core::fmt;
use core::str::FromStr;
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
use super::{
ClaimPath, Disclosure, HashAlgorithm, KeyBinding, PathComponent, Processed, SdJwt,
SdJwtBuilder, SdJwtError, Status,
};
use crate::jose::{Jwk, JwsHeader, Signer};
use crate::vc::JwtClaims;
pub const TYP: &str = "dc+sd-jwt";
pub const LEGACY_TYP: &str = "vc+sd-jwt";
pub const MEDIA_TYPE: &str = "application/dc+sd-jwt";
pub const NON_DISCLOSABLE_CLAIMS: [&str; 8] = [
"iss",
"nbf",
"exp",
"cnf",
"vct",
"vct#integrity",
"aka_vcts",
"status",
];
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Confirmation {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jwk: Option<Jwk>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
#[serde(flatten)]
pub additional: IndexMap<String, Value>,
}
impl Confirmation {
pub fn jwk(jwk: &Jwk) -> Self {
Self {
jwk: Some(jwk.public()),
..Self::default()
}
}
pub fn kid(kid: impl Into<String>) -> Self {
Self {
kid: Some(kid.into()),
..Self::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SdJwtVcClaims {
pub vct: String,
#[serde(
rename = "vct#integrity",
default,
skip_serializing_if = "Option::is_none"
)]
pub vct_integrity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aka_vcts: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::vc::numeric_date"
)]
pub iat: Option<i64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::vc::numeric_date"
)]
pub nbf: Option<i64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::vc::numeric_date"
)]
pub exp: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cnf: Option<Confirmation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<Status>,
#[serde(flatten)]
pub claims: IndexMap<String, Value>,
}
impl SdJwtVcClaims {
pub fn new(vct: impl Into<String>) -> Self {
Self {
vct: vct.into(),
vct_integrity: None,
aka_vcts: None,
iss: None,
sub: None,
jti: None,
iat: None,
nbf: None,
exp: None,
cnf: None,
status: None,
claims: IndexMap::new(),
}
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.iss = Some(issuer.into());
self
}
#[must_use]
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.sub = Some(subject.into());
self
}
#[must_use]
pub fn with_jti(mut self, jti: impl Into<String>) -> Self {
self.jti = Some(jti.into());
self
}
#[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_type_integrity(mut self, integrity: impl Into<String>) -> Self {
self.vct_integrity = Some(integrity.into());
self
}
#[must_use]
pub fn with_aka_vct(mut self, vct: impl Into<String>) -> Self {
self.aka_vcts.get_or_insert_with(Vec::new).push(vct.into());
self
}
#[must_use]
pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.claims.insert(name.into(), value.into());
self
}
pub fn holder_key(&self) -> Option<&Jwk> {
self.cnf.as_ref()?.jwk.as_ref()
}
pub fn check(&self) -> Result<(), SdJwtVcError> {
if let Some(aka) = &self.aka_vcts {
if aka.is_empty() {
return Err(SdJwtVcError::InvalidClaim {
claim: "aka_vcts",
reason: "the array is empty".into(),
});
}
if aka.contains(&self.vct) {
return Err(SdJwtVcError::InvalidClaim {
claim: "aka_vcts",
reason: "the array repeats `vct`".into(),
});
}
}
Ok(())
}
pub fn to_map(&self) -> Map<String, Value> {
match serde_json::to_value(self).expect("claims serialize") {
Value::Object(map) => map,
_ => unreachable!("a struct serializes to an object"),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SdJwtVcError {
#[error(transparent)]
SdJwt(#[from] SdJwtError),
#[error("the issuer-signed JWT has no `typ`; `{TYP}` is required")]
MissingTyp,
#[error("header `typ` is {0:?}, not `{TYP}`")]
UnexpectedTyp(String),
#[error("`{0}` must not be selectively disclosed")]
DisclosedRegisteredClaim(String),
#[error("the payload is not an SD-JWT VC: {0}")]
Claims(String),
#[error("claim `{claim}`: {reason}")]
InvalidClaim {
claim: &'static str,
reason: String,
},
}
pub struct SdJwtVcBuilder {
claims: SdJwtVcClaims,
kid: Option<String>,
concealed: Vec<ClaimPath>,
decoys: usize,
alg: HashAlgorithm,
salts: Option<Box<dyn FnMut() -> String + Send>>,
}
impl SdJwtVcBuilder {
pub fn new(vct: impl Into<String>) -> Self {
Self::from_claims(SdJwtVcClaims::new(vct))
}
pub fn from_claims(claims: SdJwtVcClaims) -> Self {
Self {
claims,
kid: None,
concealed: Vec::new(),
decoys: 0,
alg: HashAlgorithm::default(),
salts: None,
}
}
pub fn claims(&self) -> &SdJwtVcClaims {
&self.claims
}
pub fn clone_claims(&self) -> Self {
Self::from_claims(self.claims.clone())
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.claims = self.claims.with_issuer(issuer);
self
}
#[must_use]
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.claims = self.claims.with_subject(subject);
self
}
#[must_use]
pub fn with_jti(mut self, jti: impl Into<String>) -> Self {
self.claims = self.claims.with_jti(jti);
self
}
#[must_use]
pub fn with_issued_at(mut self, time: DateTime<Utc>) -> Self {
self.claims = self.claims.with_issued_at(time);
self
}
#[must_use]
pub fn with_not_before(mut self, time: DateTime<Utc>) -> Self {
self.claims = self.claims.with_not_before(time);
self
}
#[must_use]
pub fn with_expires_at(mut self, time: DateTime<Utc>) -> Self {
self.claims = self.claims.with_expires_at(time);
self
}
#[must_use]
pub fn with_confirmation(mut self, cnf: Confirmation) -> Self {
self.claims = self.claims.with_confirmation(cnf);
self
}
#[must_use]
pub fn with_holder_key(mut self, jwk: &Jwk) -> Self {
self.claims = self.claims.with_holder_key(jwk);
self
}
#[must_use]
pub fn with_status(mut self, status: Status) -> Self {
self.claims = self.claims.with_status(status);
self
}
#[must_use]
pub fn with_type_integrity(mut self, integrity: impl Into<String>) -> Self {
self.claims = self.claims.with_type_integrity(integrity);
self
}
#[must_use]
pub fn with_aka_vct(mut self, vct: impl Into<String>) -> Self {
self.claims = self.claims.with_aka_vct(vct);
self
}
#[must_use]
pub fn with_claim(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.claims = self.claims.with_claim(name, value);
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<(), SdJwtVcError> {
for path in &self.concealed {
if let Some(PathComponent::Key(name)) = path.components().first()
&& NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
{
return Err(SdJwtError::NotConcealable {
path: path.clone(),
reason: format!("SD-JWT VC does not allow `{name}` to be disclosed"),
}
.into());
}
}
Ok(())
}
pub async fn sign(self, signer: &dyn Signer) -> Result<SdJwtVc, SdJwtVcError> {
self.claims.check()?;
self.check_concealed()?;
let mut header = JwsHeader::new(signer.alg()).with_typ(TYP);
header.kid = self.kid.or_else(|| signer.kid().map(str::to_owned));
let mut builder = SdJwtBuilder::new(self.claims.to_map())
.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?;
SdJwtVc::from_sd_jwt(sd_jwt)
}
}
impl fmt::Debug for SdJwtVcBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SdJwtVcBuilder")
.field("claims", &self.claims)
.field("kid", &self.kid)
.field("concealed", &self.concealed)
.field("decoys", &self.decoys)
.field("alg", &self.alg)
.finish_non_exhaustive()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SdJwtVc {
sd_jwt: SdJwt,
header: JwsHeader,
processed: Processed,
claims: SdJwtVcClaims,
}
impl SdJwtVc {
pub fn parse(s: &str) -> Result<Self, SdJwtVcError> {
Self::from_sd_jwt(SdJwt::parse(s)?)
}
pub fn from_sd_jwt(sd_jwt: SdJwt) -> Result<Self, SdJwtVcError> {
let header = sd_jwt
.jwt()
.decode()
.map_err(SdJwtError::from)?
.header()
.clone();
match header.typ.as_deref() {
None => return Err(SdJwtVcError::MissingTyp),
Some(TYP | LEGACY_TYP) => {}
Some(other) => return Err(SdJwtVcError::UnexpectedTyp(other.to_owned())),
}
let processed = sd_jwt.process()?;
for claim in &processed.disclosed {
if let Some(PathComponent::Key(name)) = claim.path.components().first()
&& NON_DISCLOSABLE_CLAIMS.contains(&name.as_str())
{
return Err(SdJwtVcError::DisclosedRegisteredClaim(name.clone()));
}
}
let claims: SdJwtVcClaims =
serde_json::from_value(Value::Object(processed.payload.clone()))
.map_err(|e| SdJwtVcError::Claims(e.to_string()))?;
claims.check()?;
Ok(Self {
sd_jwt,
header,
processed,
claims,
})
}
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 has_legacy_typ(&self) -> bool {
self.header.typ.as_deref() == Some(LEGACY_TYP)
}
pub fn claims(&self) -> &SdJwtVcClaims {
&self.claims
}
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.claims.holder_key()
}
pub fn disclose<'a>(
&self,
paths: impl IntoIterator<Item = &'a ClaimPath>,
) -> Result<Self, SdJwtVcError> {
Self::from_sd_jwt(self.sd_jwt.disclose(paths)?)
}
pub fn select(&self, keep: impl FnMut(&Disclosure) -> bool) -> Result<Self, SdJwtVcError> {
Self::from_sd_jwt(self.sd_jwt.select(keep)?)
}
pub async fn sign_key_binding(
self,
binding: &KeyBinding,
signer: &dyn Signer,
) -> Result<Self, SdJwtVcError> {
let sd_jwt = self.sd_jwt.sign_key_binding(binding, signer).await?;
Ok(Self { sd_jwt, ..self })
}
}
impl fmt::Debug for SdJwtVc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SdJwtVc")
.field("header", &self.header)
.field("claims", &self.claims)
.field("sd_jwt", &self.sd_jwt)
.finish_non_exhaustive()
}
}
impl fmt::Display for SdJwtVc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.sd_jwt, f)
}
}
impl FromStr for SdJwtVc {
type Err = SdJwtVcError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl Serialize for SdJwtVc {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for SdJwtVc {
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)
}
}