Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support JWT Presentations #1175

Merged
merged 27 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion identity_credential/src/credential/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

/// A wrapper around a JSON Web Token (JWK).
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct Jwt(String);

impl Jwt {
Expand Down
10 changes: 5 additions & 5 deletions identity_credential/src/credential/jwt_serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,15 @@ where
/// but `iat` is also used in the ecosystem. This type aims to take care of this discrepancy on
/// a best effort basis.
#[derive(Serialize, Deserialize, Clone, Copy)]
struct IssuanceDateClaims {
pub(crate) struct IssuanceDateClaims {
#[serde(skip_serializing_if = "Option::is_none")]
iat: Option<i64>,
pub(crate) iat: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
nbf: Option<i64>,
pub(crate) nbf: Option<i64>,
}

impl IssuanceDateClaims {
fn new(issuance_date: Timestamp) -> Self {
pub(crate) fn new(issuance_date: Timestamp) -> Self {
Self {
iat: None,
nbf: Some(issuance_date.to_unix()),
Expand All @@ -248,7 +248,7 @@ impl IssuanceDateClaims {
/// Produces the `issuanceDate` value from `nbf` if it is set,
/// otherwise falls back to `iat`. If none of these values are set an error is returned.
#[cfg(feature = "validator")]
fn to_issuance_date(self) -> Result<Timestamp> {
pub(crate) fn to_issuance_date(self) -> Result<Timestamp> {
if let Some(timestamp) = self
.nbf
.map(Timestamp::from_unix)
Expand Down
1 change: 0 additions & 1 deletion identity_credential/src/credential/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,4 @@ pub use self::schema::Schema;
pub use self::status::Status;
pub use self::subject::Subject;

#[cfg(feature = "validator")]
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) use self::jwt_serialization::*;
4 changes: 4 additions & 0 deletions identity_credential/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub enum Error {
#[error("could not convert JWT to the VC data model: {0}")]
InconsistentCredentialJwtClaims(&'static str),

/// Caused when attempting to convert a JWT to a `JwtPresentation` that has conflicting values
/// between the registered claims and those in the `vp` object.
#[error("could not convert JWT to the VP data model: {0}")]
InconsistentPresentationJwtClaims(&'static str),
/// Caused when attempting to parse a timestamp value that is outside the
/// valid range defined in [RFC 3339](https://tools.ietf.org/html/rfc3339).
#[error("timestamp conversion failed")]
Expand Down
155 changes: 155 additions & 0 deletions identity_credential/src/presentation/jwt_presentation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use core::fmt::Display;
use core::fmt::Formatter;

use identity_core::convert::ToJson;
use serde::Serialize;

use identity_core::common::Context;
use identity_core::common::Object;
use identity_core::common::OneOrMany;
use identity_core::common::Url;
use identity_core::convert::FmtJson;
use identity_verification::MethodUriType;
use identity_verification::TryMethod;

use crate::credential::Credential;
use crate::credential::Jwt;
use crate::credential::Policy;
use crate::credential::RefreshService;
use crate::error::Error;
use crate::error::Result;

use super::jwt_serialization::PresentationJwtClaims;
use super::JwtPresentationBuilder;
use super::JwtPresentationOptions;

/// Represents a bundle of one or more [`Credential`]s expressed as [`Jwt`]s.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct JwtPresentation<T = Object> {
/// The JSON-LD context(s) applicable to the `Presentation`.
#[serde(rename = "@context")]
pub context: OneOrMany<Context>,
/// A unique `URI` that may be used to identify the `Presentation`.
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Url>,
/// One or more URIs defining the type of the `Presentation`.
#[serde(rename = "type")]
pub types: OneOrMany<String>,
/// Credential(s) expressing the claims of the `Presentation`.
#[serde(default = "Default::default", rename = "verifiableCredential")]
pub verifiable_credential: OneOrMany<Jwt>,
/// The entity that generated the `Presentation`.
pub holder: Url,
/// Service(s) used to refresh an expired [`Credential`] in the `Presentation`.
#[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")]
pub refresh_service: OneOrMany<RefreshService>,
/// Terms-of-use specified by the `Presentation` holder.
#[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")]
pub terms_of_use: OneOrMany<Policy>,
/// Miscellaneous properties.
#[serde(flatten)]
pub properties: T,
/// Optional proof that can be verified by users in addition to JWS.
#[serde(skip_serializing_if = "Option::is_none")]
pub proof: Option<Object>,
}

impl<T> JwtPresentation<T> {
/// Returns the base JSON-LD context for `JwtPresentation`s.
pub fn base_context() -> &'static Context {
Credential::<Object>::base_context()
}

/// Returns the base type for `JwtPresentation`s.
pub const fn base_type() -> &'static str {
"VerifiablePresentation"
}

/// Creates a `JwtPresentationBuilder` to configure a new Presentation.
///
/// This is the same as [JwtPresentationBuilder::new].
pub fn builder(holder: Url, properties: T) -> JwtPresentationBuilder<T> {
JwtPresentationBuilder::new(holder, properties)
}

/// Returns a new `JwtPresentation` based on the `JwtPresentationBuilder` configuration.
pub fn from_builder(builder: JwtPresentationBuilder<T>) -> Result<Self> {
let this: Self = Self {
context: builder.context.into(),
id: builder.id,
types: builder.types.into(),
verifiable_credential: builder.credentials.into(),
holder: builder.holder,
refresh_service: builder.refresh_service.into(),
terms_of_use: builder.terms_of_use.into(),
properties: builder.properties,
proof: None,
};
this.check_structure()?;

Ok(this)
}

/// Validates the semantic structure of the `JwtPresentation`.
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
///
/// # Warning
///
/// This does not check the semantic structure of the contained credentials. This needs to be done as part of
/// signature validation on the credentials as they are encoded as JWTs.
pub fn check_structure(&self) -> Result<()> {
abdulmth marked this conversation as resolved.
Show resolved Hide resolved
// Ensure the base context is present and in the correct location
match self.context.get(0) {
Some(context) if context == Self::base_context() => {}
Some(_) | None => return Err(Error::MissingBaseContext),
}

// The set of types MUST contain the base type
if !self.types.iter().any(|type_| type_ == Self::base_type()) {
return Err(Error::MissingBaseType);
}
Ok(())
}

/// Serializes the [`JwtPresentation`] as a JWT claims set
/// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1).
///
/// The resulting string can be used as the payload of a JWS when issuing the credential.
pub fn serialize_jwt(&self, options: &JwtPresentationOptions) -> Result<String>
where
T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
{
let jwt_representation: PresentationJwtClaims<'_, T> = PresentationJwtClaims::new(self, options)?;
jwt_representation
.to_json()
.map_err(|err| Error::JwtClaimsSetSerializationError(err.into()))
}

/// Returns a reference to the `JwtPresentation` proof, if it exists.
///
/// Note that this is not the JWS or JWT of the presentation but a separate field that can be used to
/// prove additional claims or include proofs not based on digital signatures like Proof-of-Work.
pub fn proof(&self) -> Option<&Object> {
self.proof.as_ref()
}

/// Sets the value of the proof property.
pub fn set_proof(&mut self, proof: Option<Object>) {
self.proof = proof;
}
}

impl<T> Display for JwtPresentation<T>
where
T: Serialize,
{
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
self.fmt_json(f)
}
}

impl<T> TryMethod for JwtPresentation<T> {
const TYPE: MethodUriType = MethodUriType::Absolute;
}
183 changes: 183 additions & 0 deletions identity_credential/src/presentation/jwt_presentation_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright 2020-2023 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use identity_core::common::Context;
use identity_core::common::Object;
use identity_core::common::Url;
use identity_core::common::Value;

use crate::credential::Jwt;
use crate::credential::Policy;
use crate::credential::RefreshService;
use crate::error::Result;
use crate::presentation::Presentation;

use super::JwtPresentation;

/// A `JwtPresentationBuilder` is used to create a customized [JwtPresentation].
#[derive(Clone, Debug)]
pub struct JwtPresentationBuilder<T = Object> {
pub(crate) context: Vec<Context>,
pub(crate) id: Option<Url>,
pub(crate) types: Vec<String>,
pub(crate) credentials: Vec<Jwt>,
pub(crate) holder: Url,
pub(crate) refresh_service: Vec<RefreshService>,
pub(crate) terms_of_use: Vec<Policy>,
pub(crate) properties: T,
}

impl<T> JwtPresentationBuilder<T> {
/// Creates a new `JwtPresentationBuilder`.
pub fn new(holder: Url, properties: T) -> Self {
Self {
context: vec![Presentation::<T>::base_context().clone()],
id: None,
types: vec![Presentation::<T>::base_type().into()],
credentials: Vec::new(),
holder,
refresh_service: Vec::new(),
terms_of_use: Vec::new(),
properties,
}
}

/// Adds a value to the `context` set.
#[must_use]
pub fn context(mut self, value: impl Into<Context>) -> Self {
self.context.push(value.into());
self
}

/// Sets the unique identifier of the presentation.
#[must_use]
pub fn id(mut self, value: Url) -> Self {
self.id = Some(value);
self
}

/// Adds a value to the `type` set.
#[must_use]
pub fn type_(mut self, value: impl Into<String>) -> Self {
self.types.push(value.into());
self
}

/// Adds a value to the `verifiableCredential` set.
#[must_use]
pub fn credential(mut self, value: Jwt) -> Self {
self.credentials.push(value);
self
}

/// Adds a value to the `refreshService` set.
#[must_use]
pub fn refresh_service(mut self, value: RefreshService) -> Self {
self.refresh_service.push(value);
self
}

/// Adds a value to the `termsOfUse` set.
#[must_use]
pub fn terms_of_use(mut self, value: Policy) -> Self {
self.terms_of_use.push(value);
self
}

/// Returns a new `Presentation` based on the `PresentationBuilder` configuration.
pub fn build(self) -> Result<JwtPresentation<T>> {
JwtPresentation::from_builder(self)
}
}

impl JwtPresentationBuilder<Object> {
/// Adds a new custom property.
#[must_use]
pub fn property<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<Value>,
{
self.properties.insert(key.into(), value.into());
self
}

/// Adds a series of custom properties.
#[must_use]
pub fn properties<K, V, I>(mut self, iter: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<Value>,
{
self
.properties
.extend(iter.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}
}

#[cfg(test)]
mod tests {
use serde_json::json;
use serde_json::Value;

use identity_core::common::Object;
use identity_core::common::Url;
use identity_core::convert::FromJson;

use crate::credential::Credential;
use crate::credential::CredentialBuilder;
use crate::credential::Jwt;
use crate::credential::Subject;
use crate::presentation::JwtPresentation;
use crate::presentation::JwtPresentationBuilder;

fn subject() -> Subject {
let json: Value = json!({
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts"
}
});

Subject::from_json_value(json).unwrap()
}

fn issuer() -> Url {
Url::parse("did:example:issuer").unwrap()
}

#[test]
fn test_presentation_builder_valid() {
let credential: Credential = CredentialBuilder::default()
.type_("ExampleCredential")
.subject(subject())
.issuer(issuer())
.build()
.unwrap();

let credential_jwt = Jwt::new(credential.serialize_jwt().unwrap());

let presentation: JwtPresentation =
JwtPresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new())
.type_("ExamplePresentation")
.credential(credential_jwt)
.build()
.unwrap();

assert_eq!(presentation.context.len(), 1);
assert_eq!(
presentation.context.get(0).unwrap(),
JwtPresentation::<Object>::base_context()
);
assert_eq!(presentation.types.len(), 2);
assert_eq!(
presentation.types.get(0).unwrap(),
JwtPresentation::<Object>::base_type()
);
assert_eq!(presentation.types.get(1).unwrap(), "ExamplePresentation");
assert_eq!(presentation.verifiable_credential.len(), 1);
}
}
Loading