use std::borrow::Cow;
#[cfg(feature = "jpt-bbs-plus")]
use jsonprooftoken::jpt::claims::JptClaims;
use serde::Deserialize;
use serde::Serialize;
use identity_core::common::Context;
use identity_core::common::Object;
use identity_core::common::OneOrMany;
use identity_core::common::Timestamp;
use identity_core::common::Url;
use serde::de::DeserializeOwned;
use crate::credential::Credential;
use crate::credential::Evidence;
use crate::credential::Issuer;
use crate::credential::Policy;
use crate::credential::Proof;
use crate::credential::RefreshService;
use crate::credential::Schema;
use crate::credential::Status;
use crate::credential::Subject;
use crate::Error;
use crate::Result;
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
pub struct JwtCredential(CredentialJwtClaims<'static>);
#[cfg(feature = "validator")]
impl TryFrom<JwtCredential> for Credential {
type Error = Error;
fn try_from(value: JwtCredential) -> std::result::Result<Self, Self::Error> {
value.0.try_into_credential()
}
}
#[derive(Serialize, Deserialize)]
pub(crate) struct CredentialJwtClaims<'credential, T = Object>
where
T: ToOwned + Serialize,
<T as ToOwned>::Owned: DeserializeOwned,
{
#[serde(skip_serializing_if = "Option::is_none")]
exp: Option<i64>,
pub(crate) iss: Cow<'credential, Issuer>,
#[serde(flatten)]
issuance_date: IssuanceDateClaims,
#[serde(skip_serializing_if = "Option::is_none")]
jti: Option<Cow<'credential, Url>>,
#[serde(skip_serializing_if = "Option::is_none")]
sub: Option<Cow<'credential, Url>>,
vc: InnerCredential<'credential, T>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub(crate) custom: Option<Object>,
}
impl<'credential, T> CredentialJwtClaims<'credential, T>
where
T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
{
pub(super) fn new(credential: &'credential Credential<T>, custom: Option<Object>) -> Result<Self> {
let Credential {
context,
id,
types,
credential_subject: OneOrMany::One(subject),
issuer,
issuance_date,
expiration_date,
credential_status,
credential_schema,
refresh_service,
terms_of_use,
evidence,
non_transferable,
properties,
proof,
} = credential
else {
return Err(Error::MoreThanOneSubjectInJwt);
};
Ok(Self {
exp: expiration_date.map(|value| Timestamp::to_unix(&value)),
iss: Cow::Borrowed(issuer),
issuance_date: IssuanceDateClaims::new(*issuance_date),
jti: id.as_ref().map(Cow::Borrowed),
sub: subject.id.as_ref().map(Cow::Borrowed),
vc: InnerCredential {
context: Cow::Borrowed(context),
id: None,
types: Cow::Borrowed(types),
credential_subject: InnerCredentialSubject::new(subject),
issuance_date: None,
expiration_date: None,
issuer: None,
credential_schema: Cow::Borrowed(credential_schema),
credential_status: credential_status.as_ref().map(Cow::Borrowed),
refresh_service: Cow::Borrowed(refresh_service),
terms_of_use: Cow::Borrowed(terms_of_use),
evidence: Cow::Borrowed(evidence),
non_transferable: *non_transferable,
properties: Cow::Borrowed(properties),
proof: proof.as_ref().map(Cow::Borrowed),
},
custom,
})
}
}
#[cfg(feature = "validator")]
impl<T> CredentialJwtClaims<'_, T>
where
T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
{
fn check_consistency(&self) -> Result<()> {
let issuer_from_claims: &Issuer = self.iss.as_ref();
if !self
.vc
.issuer
.as_ref()
.map(|value| value == issuer_from_claims)
.unwrap_or(true)
{
return Err(Error::InconsistentCredentialJwtClaims("inconsistent issuer"));
};
let issuance_date_from_claims = self.issuance_date.to_issuance_date()?;
if !self
.vc
.issuance_date
.map(|value| value == issuance_date_from_claims)
.unwrap_or(true)
{
return Err(Error::InconsistentCredentialJwtClaims("inconsistent issuanceDate"));
};
if !self
.vc
.expiration_date
.map(|value| self.exp.filter(|exp| *exp == value.to_unix()).is_some())
.unwrap_or(true)
{
return Err(Error::InconsistentCredentialJwtClaims(
"inconsistent credential expirationDate",
));
};
if !self
.vc
.id
.as_ref()
.map(|value| self.jti.as_ref().filter(|jti| jti.as_ref() == value).is_some())
.unwrap_or(true)
{
return Err(Error::InconsistentCredentialJwtClaims("inconsistent credential id"));
};
if let Some(ref inner_credential_subject_id) = self.vc.credential_subject.id {
let subject_claim = self.sub.as_ref().ok_or(Error::InconsistentCredentialJwtClaims(
"inconsistent credentialSubject: expected identifier in sub",
))?;
if subject_claim.as_ref() != inner_credential_subject_id {
return Err(Error::InconsistentCredentialJwtClaims(
"inconsistent credentialSubject: identifiers do not match",
));
}
};
Ok(())
}
pub(crate) fn try_into_credential(self) -> Result<Credential<T>> {
self.check_consistency()?;
let Self {
exp,
iss,
issuance_date,
jti,
sub,
vc,
custom: _,
} = self;
let InnerCredential {
context,
id: _,
types,
credential_subject,
credential_status,
credential_schema,
refresh_service,
terms_of_use,
evidence,
non_transferable,
properties,
proof,
issuance_date: _,
issuer: _,
expiration_date: _,
} = vc;
Ok(Credential {
context: context.into_owned(),
id: jti.map(Cow::into_owned),
types: types.into_owned(),
credential_subject: {
OneOrMany::One(Subject {
id: sub.map(Cow::into_owned),
properties: credential_subject.properties.into_owned(),
})
},
issuer: iss.into_owned(),
issuance_date: issuance_date.to_issuance_date()?,
expiration_date: exp
.map(Timestamp::from_unix)
.transpose()
.map_err(|_| Error::TimestampConversionError)?,
credential_status: credential_status.map(Cow::into_owned),
credential_schema: credential_schema.into_owned(),
refresh_service: refresh_service.into_owned(),
terms_of_use: terms_of_use.into_owned(),
evidence: evidence.into_owned(),
non_transferable,
properties: properties.into_owned(),
proof: proof.map(Cow::into_owned),
})
}
}
#[derive(Serialize, Deserialize, Clone, Copy)]
pub(crate) struct IssuanceDateClaims {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) iat: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) nbf: Option<i64>,
}
impl IssuanceDateClaims {
pub(crate) fn new(issuance_date: Timestamp) -> Self {
Self {
iat: None,
nbf: Some(issuance_date.to_unix()),
}
}
#[cfg(feature = "validator")]
pub(crate) fn to_issuance_date(self) -> Result<Timestamp> {
if let Some(timestamp) = self
.nbf
.map(Timestamp::from_unix)
.transpose()
.map_err(|_| Error::TimestampConversionError)?
{
Ok(timestamp)
} else {
Timestamp::from_unix(self.iat.ok_or(Error::TimestampConversionError)?)
.map_err(|_| Error::TimestampConversionError)
}
}
}
#[derive(Serialize, Deserialize)]
struct InnerCredentialSubject<'credential> {
#[cfg(feature = "validator")]
#[serde(skip_serializing)]
id: Option<Url>,
#[serde(flatten)]
properties: Cow<'credential, Object>,
}
impl<'credential> InnerCredentialSubject<'credential> {
fn new(subject: &'credential Subject) -> Self {
Self {
#[cfg(feature = "validator")]
id: None,
properties: Cow::Borrowed(&subject.properties),
}
}
}
#[derive(Serialize, Deserialize)]
struct InnerCredential<'credential, T = Object>
where
T: ToOwned + Serialize,
<T as ToOwned>::Owned: DeserializeOwned,
{
#[serde(rename = "@context")]
context: Cow<'credential, OneOrMany<Context>>,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<Url>,
#[serde(rename = "type")]
types: Cow<'credential, OneOrMany<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
issuer: Option<Issuer>,
#[serde(rename = "credentialSubject")]
credential_subject: InnerCredentialSubject<'credential>,
#[serde(rename = "issuanceDate", skip_serializing_if = "Option::is_none")]
issuance_date: Option<Timestamp>,
#[serde(rename = "expirationDate", skip_serializing_if = "Option::is_none")]
expiration_date: Option<Timestamp>,
#[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")]
credential_status: Option<Cow<'credential, Status>>,
#[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")]
credential_schema: Cow<'credential, OneOrMany<Schema>>,
#[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
refresh_service: Cow<'credential, OneOrMany<RefreshService>>,
#[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
terms_of_use: Cow<'credential, OneOrMany<Policy>>,
#[serde(default, skip_serializing_if = "OneOrMany::is_empty")]
evidence: Cow<'credential, OneOrMany<Evidence>>,
#[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")]
non_transferable: Option<bool>,
#[serde(flatten)]
properties: Cow<'credential, T>,
#[serde(skip_serializing_if = "Option::is_none")]
proof: Option<Cow<'credential, Proof>>,
}
#[cfg(feature = "jpt-bbs-plus")]
impl<'credential, T> From<CredentialJwtClaims<'credential, T>> for JptClaims
where
T: ToOwned + Serialize,
<T as ToOwned>::Owned: DeserializeOwned,
{
fn from(item: CredentialJwtClaims<'credential, T>) -> Self {
let CredentialJwtClaims {
exp,
iss,
issuance_date,
jti,
sub,
vc,
custom,
} = item;
let mut claims = JptClaims::new();
if let Some(exp) = exp {
claims.set_exp(exp);
}
claims.set_iss(iss.url().to_string());
if let Some(iat) = issuance_date.iat {
claims.set_iat(iat);
}
if let Some(nbf) = issuance_date.nbf {
claims.set_nbf(nbf);
}
if let Some(jti) = jti {
claims.set_jti(jti.to_string());
}
if let Some(sub) = sub {
claims.set_sub(sub.to_string());
}
claims.set_claim(Some("vc"), vc, true);
if let Some(custom) = custom {
claims.set_claim(None, custom, true);
}
claims
}
}
#[cfg(test)]
mod tests {
use identity_core::common::Object;
use identity_core::convert::FromJson;
use identity_core::convert::ToJson;
use crate::credential::Credential;
use crate::Error;
use super::CredentialJwtClaims;
#[test]
fn roundtrip() {
let credential_json: &str = r#"
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/14",
"issuanceDate": "2010-01-01T19:23:24Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}"#;
let expected_serialization_json: &str = r#"
{
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"jti": "http://example.edu/credentials/3732",
"iss": "https://example.edu/issuers/14",
"nbf": 1262373804,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"credentialSubject": {
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}
}"#;
let credential: Credential = Credential::from_json(credential_json).unwrap();
let jwt_credential_claims: CredentialJwtClaims<'_> = CredentialJwtClaims::new(&credential, None).unwrap();
let jwt_credential_claims_serialized: String = jwt_credential_claims.to_json().unwrap();
assert_eq!(
Object::from_json(expected_serialization_json).unwrap(),
Object::from_json(&jwt_credential_claims_serialized).unwrap()
);
let retrieved_credential: Credential = {
CredentialJwtClaims::<'static, Object>::from_json(&jwt_credential_claims_serialized)
.unwrap()
.try_into_credential()
.unwrap()
};
assert_eq!(credential, retrieved_credential);
}
#[test]
fn claims_duplication() {
let credential_json: &str = r#"
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/14",
"issuanceDate": "2010-01-01T19:23:24Z",
"expirationDate": "2025-09-13T15:56:23Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}"#;
let claims_json: &str = r#"
{
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"jti": "http://example.edu/credentials/3732",
"iss": "https://example.edu/issuers/14",
"nbf": 1262373804,
"exp": 1757778983,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/14",
"issuanceDate": "2010-01-01T19:23:24Z",
"expirationDate": "2025-09-13T15:56:23Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}
}"#;
let credential: Credential = Credential::from_json(credential_json).unwrap();
let credential_from_claims: Credential = CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
.unwrap()
.try_into_credential()
.unwrap();
assert_eq!(credential, credential_from_claims);
}
#[test]
fn inconsistent_issuer() {
let claims_json: &str = r#"
{
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"jti": "http://example.edu/credentials/3732",
"iss": "https://example.edu/issuers/14",
"nbf": 1262373804,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/15",
"credentialSubject": {
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}
}"#;
let credential_from_claims_result: Result<Credential, _> =
CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
.unwrap()
.try_into_credential();
assert!(matches!(
credential_from_claims_result.unwrap_err(),
Error::InconsistentCredentialJwtClaims("inconsistent issuer")
));
}
#[test]
fn inconsistent_id() {
let claims_json: &str = r#"
{
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"jti": "http://example.edu/credentials/3732",
"iss": "https://example.edu/issuers/14",
"nbf": 1262373804,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"id": "http://example.edu/credentials/1111",
"credentialSubject": {
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}
}"#;
let credential_from_claims_result: Result<Credential, _> =
CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
.unwrap()
.try_into_credential();
assert!(matches!(
credential_from_claims_result.unwrap_err(),
Error::InconsistentCredentialJwtClaims("inconsistent credential id")
));
}
#[test]
fn inconsistent_subject() {
let claims_json: &str = r#"
{
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"jti": "http://example.edu/credentials/3732",
"iss": "https://example.edu/issuers/14",
"nbf": 1262373804,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/14",
"issuanceDate": "2010-01-01T19:23:24Z",
"credentialSubject": {
"id": "did:example:1111111111111111111111111",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}
}"#;
let credential_from_claims_result: Result<Credential, _> =
CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
.unwrap()
.try_into_credential();
assert!(matches!(
credential_from_claims_result.unwrap_err(),
Error::InconsistentCredentialJwtClaims("inconsistent credentialSubject: identifiers do not match")
));
}
#[test]
fn inconsistent_issuance_date() {
let claims_json: &str = r#"
{
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"jti": "http://example.edu/credentials/3732",
"iss": "https://example.edu/issuers/14",
"nbf": 1262373804,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/14",
"issuanceDate": "2020-01-01T19:23:24Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}
}"#;
let credential_from_claims_result: Result<Credential, _> =
CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
.unwrap()
.try_into_credential();
assert!(matches!(
credential_from_claims_result.unwrap_err(),
Error::InconsistentCredentialJwtClaims("inconsistent issuanceDate")
));
}
#[test]
fn inconsistent_expiration_date() {
let claims_json: &str = r#"
{
"sub": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"jti": "http://example.edu/credentials/3732",
"iss": "https://example.edu/issuers/14",
"nbf": 1262373804,
"exp": 1757778983,
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "http://example.edu/credentials/3732",
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"issuer": "https://example.edu/issuers/14",
"issuanceDate": "2010-01-01T19:23:24Z",
"expirationDate": "2026-09-13T15:56:23Z",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science in Mechanical Engineering"
}
}
}
}"#;
let credential_from_claims_result: Result<Credential, _> =
CredentialJwtClaims::<'_, Object>::from_json(&claims_json)
.unwrap()
.try_into_credential();
assert!(matches!(
credential_from_claims_result.unwrap_err(),
Error::InconsistentCredentialJwtClaims("inconsistent credential expirationDate")
));
}
}