use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
use crate::did::{Context, OneOrMany, Uri};
use super::credential::push;
use super::model::{CONTEXT_V2, append_context, check_context_and_type, push_type};
use super::{CredentialError, Jwt, JwtClaims, Party, Subject, TypedObject, claims};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(remote = "Self")]
pub struct CredentialV2 {
#[serde(rename = "@context")]
pub context: Context,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Uri>,
#[serde(rename = "type")]
pub types: OneOrMany<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<Value>,
pub issuer: Party,
#[serde(rename = "validFrom", default, skip_serializing_if = "Option::is_none")]
pub valid_from: Option<DateTime<Utc>>,
#[serde(
rename = "validUntil",
default,
skip_serializing_if = "Option::is_none"
)]
pub valid_until: Option<DateTime<Utc>>,
#[serde(
rename = "credentialStatus",
default,
skip_serializing_if = "Option::is_none"
)]
pub credential_status: Option<OneOrMany<TypedObject>>,
#[serde(rename = "credentialSubject")]
pub credential_subject: OneOrMany<Subject>,
#[serde(
rename = "credentialSchema",
default,
skip_serializing_if = "Option::is_none"
)]
pub credential_schema: Option<OneOrMany<TypedObject>>,
#[serde(flatten)]
pub properties: IndexMap<String, Value>,
}
impl Serialize for CredentialV2 {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Self::serialize(self, serializer)
}
}
impl<'de> Deserialize<'de> for CredentialV2 {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let credential = Self::deserialize(deserializer)?;
credential.check().map_err(de::Error::custom)?;
Ok(credential)
}
}
impl CredentialV2 {
pub const CONTEXT: &'static str = CONTEXT_V2;
pub const TYPE: &'static str = "VerifiableCredential";
pub const JWT_TYP: &'static str = "vc+jwt";
pub const JWT_MEDIA_TYPE: &'static str = "application/vc+jwt";
pub fn new(issuer: impl Into<Party>, subject: Subject) -> Self {
Self {
context: Context::uri(Self::CONTEXT),
id: None,
types: OneOrMany::One(Self::TYPE.to_owned()),
name: None,
description: None,
issuer: issuer.into(),
valid_from: None,
valid_until: None,
credential_status: None,
credential_subject: OneOrMany::One(subject),
credential_schema: None,
properties: IndexMap::new(),
}
}
#[must_use]
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = append_context(self.context, context.into());
self
}
#[must_use]
pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_type(mut self, credential_type: impl Into<String>) -> Self {
push_type(&mut self.types, credential_type.into());
self
}
#[must_use]
pub fn with_name(mut self, name: impl Into<Value>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<Value>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_valid_from(mut self, valid_from: DateTime<Utc>) -> Self {
self.valid_from = Some(valid_from);
self
}
#[must_use]
pub fn with_valid_until(mut self, valid_until: DateTime<Utc>) -> Self {
self.valid_until = Some(valid_until);
self
}
#[must_use]
pub fn with_subject(mut self, subject: Subject) -> Self {
let mut subjects =
core::mem::replace(&mut self.credential_subject, OneOrMany::Many(Vec::new()))
.into_vec();
subjects.push(subject);
self.credential_subject = OneOrMany::Many(subjects);
self
}
#[must_use]
pub fn with_status(mut self, status: TypedObject) -> Self {
self.credential_status = Some(push(self.credential_status.take(), status));
self
}
#[must_use]
pub fn with_schema(mut self, schema: TypedObject) -> Self {
self.credential_schema = Some(push(self.credential_schema.take(), schema));
self
}
#[must_use]
pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.properties.insert(name.into(), value.into());
self
}
pub fn subject(&self) -> Option<&Subject> {
(self.credential_subject.len() == 1).then(|| self.credential_subject.first())?
}
pub fn has_type(&self, credential_type: &str) -> bool {
self.types.iter().any(|t| t == credential_type)
}
pub fn statuses(&self) -> impl Iterator<Item = &TypedObject> {
self.credential_status.iter().flat_map(OneOrMany::iter)
}
pub fn check(&self) -> Result<(), CredentialError> {
check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)?;
if self.credential_subject.is_empty() {
return Err(CredentialError::NoSubject);
}
if let (Some(from), Some(until)) = (self.valid_from, self.valid_until)
&& until < from
{
return Err(CredentialError::InvalidValidity {
from: "validFrom",
until: "validUntil",
});
}
Ok(())
}
pub fn to_jwt_claims(&self) -> JwtClaims {
JwtClaims {
additional: claims::to_object(self).into_iter().collect(),
..JwtClaims::default()
}
}
pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
let mut vc = payload_object(jwt_claims, "vc")?;
claims::merge_id(&mut vc, "id", "jti", jwt_claims.jti.as_deref())?;
claims::merge_id(&mut vc, "issuer", "iss", jwt_claims.iss.as_deref())?;
claims::merge_subject(&mut vc, jwt_claims.sub.as_deref())?;
serde_json::from_value(Value::Object(vc)).map_err(|e| CredentialError::InvalidClaim {
claim: "@context",
reason: e.to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(remote = "Self")]
pub struct PresentationV2 {
#[serde(rename = "@context")]
pub context: Context,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Uri>,
#[serde(rename = "type")]
pub types: OneOrMany<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub holder: Option<Party>,
#[serde(
rename = "verifiableCredential",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub verifiable_credential: Vec<EnvelopedVc>,
#[serde(flatten)]
pub properties: IndexMap<String, Value>,
}
impl Serialize for PresentationV2 {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Self::serialize(self, serializer)
}
}
impl<'de> Deserialize<'de> for PresentationV2 {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let presentation = Self::deserialize(deserializer)?;
presentation.check().map_err(de::Error::custom)?;
Ok(presentation)
}
}
impl Default for PresentationV2 {
fn default() -> Self {
Self::new()
}
}
impl PresentationV2 {
pub const CONTEXT: &'static str = CONTEXT_V2;
pub const TYPE: &'static str = "VerifiablePresentation";
pub const JWT_TYP: &'static str = "vp+jwt";
pub const JWT_MEDIA_TYPE: &'static str = "application/vp+jwt";
pub fn new() -> Self {
Self {
context: Context::uri(Self::CONTEXT),
id: None,
types: OneOrMany::One(Self::TYPE.to_owned()),
holder: None,
verifiable_credential: Vec::new(),
properties: IndexMap::new(),
}
}
#[must_use]
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = append_context(self.context, context.into());
self
}
#[must_use]
pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_type(mut self, presentation_type: impl Into<String>) -> Self {
push_type(&mut self.types, presentation_type.into());
self
}
#[must_use]
pub fn with_holder(mut self, holder: impl Into<Party>) -> Self {
self.holder = Some(holder.into());
self
}
#[must_use]
pub fn with_credential(mut self, credential: EnvelopedVc) -> Self {
self.verifiable_credential.push(credential);
self
}
#[must_use]
pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.properties.insert(name.into(), value.into());
self
}
pub fn has_type(&self, presentation_type: &str) -> bool {
self.types.iter().any(|t| t == presentation_type)
}
pub fn check(&self) -> Result<(), CredentialError> {
check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)
}
pub fn to_jwt_claims(&self) -> JwtClaims {
JwtClaims {
additional: claims::to_object(self).into_iter().collect(),
..JwtClaims::default()
}
}
pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
let mut vp = payload_object(jwt_claims, "vp")?;
claims::merge_id(&mut vp, "id", "jti", jwt_claims.jti.as_deref())?;
claims::merge_id(&mut vp, "holder", "iss", jwt_claims.iss.as_deref())?;
serde_json::from_value(Value::Object(vp)).map_err(|e| CredentialError::InvalidClaim {
claim: "@context",
reason: e.to_string(),
})
}
}
fn payload_object(
jwt_claims: &JwtClaims,
forbidden: &'static str,
) -> Result<serde_json::Map<String, Value>, CredentialError> {
if jwt_claims.additional.contains_key(forbidden) {
return Err(CredentialError::InvalidClaim {
claim: forbidden,
reason: "must not be present in a VC-JOSE-COSE payload".to_owned(),
});
}
Ok(jwt_claims
.additional
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect())
}
fn parse_data_url(uri: &str) -> Option<(&str, bool, &str)> {
let (meta, data) = uri.strip_prefix("data:")?.split_once(',')?;
let (media_type, base64) = match meta.strip_suffix(";base64") {
Some(media_type) => (media_type, true),
None => (meta, false),
};
Some((media_type, base64, data))
}
macro_rules! enveloped {
(
$(#[$doc:meta])*
$name:ident, $kind:literal, $object_type:literal, $media_type:expr
) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(remote = "Self")]
pub struct $name {
/// `@context`, which must define the type; the base context does.
#[serde(rename = "@context")]
pub context: Context,
pub id: Uri,
#[serde(rename = "type")]
pub types: OneOrMany<String>,
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Self::serialize(self, serializer)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let enveloped = Self::deserialize(deserializer)?;
enveloped.check().map_err(de::Error::custom)?;
Ok(enveloped)
}
}
impl $name {
pub const TYPE: &'static str = $object_type;
pub const JWT_MEDIA_TYPE: &'static str = $media_type;
pub fn new(media_type: &str, data: &str) -> Result<Self, CredentialError> {
let id = Uri::parse(&format!("data:{media_type},{data}")).map_err(|error| {
CredentialError::InvalidClaim {
claim: "id",
reason: format!("not a `data:` URL: {error}"),
}
})?;
Ok(Self {
context: Context::uri(CONTEXT_V2),
id,
types: OneOrMany::One(Self::TYPE.to_owned()),
})
}
pub fn from_jwt(jwt: &Jwt) -> Self {
Self::new(Self::JWT_MEDIA_TYPE, jwt.as_str())
.expect("a compact JWS is base64url and dots, all URI characters")
}
pub fn media_type(&self) -> &str {
parse_data_url(self.id.as_str()).map_or("", |(media_type, _, _)| media_type)
}
pub fn data(&self) -> &str {
parse_data_url(self.id.as_str()).map_or("", |(_, _, data)| data)
}
pub fn to_jwt(&self) -> Result<Jwt, CredentialError> {
let (media_type, base64, data) = parse_data_url(self.id.as_str())
.ok_or_else(|| CredentialError::NotDataUrl(self.id.to_string()))?;
if media_type != Self::JWT_MEDIA_TYPE || base64 {
return Err(CredentialError::UnsupportedMediaType {
kind: $kind,
media_type: if base64 {
format!("{media_type};base64")
} else {
media_type.to_owned()
},
expected: Self::JWT_MEDIA_TYPE,
});
}
Jwt::new(data).map_err(|e| CredentialError::InvalidClaim {
claim: "id",
reason: e.to_string(),
})
}
pub fn check(&self) -> Result<(), CredentialError> {
if !self.types.iter().any(|t| t == Self::TYPE) {
return Err(CredentialError::MissingType(Self::TYPE));
}
if parse_data_url(self.id.as_str()).is_none() {
return Err(CredentialError::NotDataUrl(self.id.to_string()));
}
Ok(())
}
}
};
}
enveloped!(
EnvelopedVc,
"credential",
"EnvelopedVerifiableCredential",
CredentialV2::JWT_MEDIA_TYPE
);
enveloped!(
EnvelopedVp,
"presentation",
"EnvelopedVerifiablePresentation",
PresentationV2::JWT_MEDIA_TYPE
);
#[cfg(feature = "sd-jwt")]
#[cfg_attr(docsrs, doc(cfg(feature = "sd-jwt")))]
impl EnvelopedVc {
pub const SD_JWT_MEDIA_TYPE: &'static str = crate::sd_jwt_vc::SdJwtCredentialV2::MEDIA_TYPE;
pub fn from_sd_jwt(sd_jwt: &crate::sd_jwt_vc::SdJwt) -> Self {
Self::new(Self::SD_JWT_MEDIA_TYPE, &sd_jwt.to_string())
.expect("a compact SD-JWT is base64url, dots and tildes, all URI characters")
}
pub fn to_sd_jwt(&self) -> Result<crate::sd_jwt_vc::SdJwt, CredentialError> {
let (media_type, base64, data) = parse_data_url(self.id.as_str())
.ok_or_else(|| CredentialError::NotDataUrl(self.id.to_string()))?;
if media_type != Self::SD_JWT_MEDIA_TYPE || base64 {
return Err(CredentialError::UnsupportedMediaType {
kind: "credential",
media_type: if base64 {
format!("{media_type};base64")
} else {
media_type.to_owned()
},
expected: Self::SD_JWT_MEDIA_TYPE,
});
}
crate::sd_jwt_vc::SdJwt::parse(data).map_err(|e| CredentialError::InvalidClaim {
claim: "id",
reason: e.to_string(),
})
}
pub fn is_sd_jwt(&self) -> bool {
parse_data_url(self.id.as_str())
.is_some_and(|(media_type, base64, _)| media_type == Self::SD_JWT_MEDIA_TYPE && !base64)
}
}