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 all 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 examples/utils/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ async fn get_address_balance(client: &Client, address: &str) -> anyhow::Result<u
])
.await?;

let outputs: _ = client.get_outputs(output_ids.items).await?;
let outputs = client.get_outputs(output_ids.items).await?;

let mut total_amount = 0;
for output_response in outputs {
Expand Down
2 changes: 1 addition & 1 deletion identity_core/src/crypto/key/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ mod tests {
let message = BaseEncoding::decode(MESSAGE_HEX, Base::Base16Lower).unwrap();
let signature = Ed25519::sign(&message, &private_key).unwrap();
assert_eq!(&BaseEncoding::encode(&signature, Base::Base16Lower), SIGNATURE_HEX);
let verified: _ = Ed25519::verify(
let verified = Ed25519::verify(
&BaseEncoding::decode(MESSAGE_HEX, Base::Base16Lower).unwrap()[..],
&signature,
&public_key,
Expand Down
4 changes: 2 additions & 2 deletions identity_core/src/crypto/proof/jcs_ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ mod tests {
assert!(Verifier::verify(&input, &signature, &badkey).is_err());

// Fails when the signature is mutated
let signature: _ = ProofValue::Signature("IOTA".into());
let signature = ProofValue::Signature("IOTA".into());
assert!(Verifier::verify(&input, &signature, &public).is_err());
}
}
Expand Down Expand Up @@ -173,7 +173,7 @@ mod tests {
let data1: Value = json!({ "msg": "IOTA Identity" });
let data2: Value = json!({ "msg": "IOTA Identity 2" });

let signature: _ = Signer::sign(&data1, key1.private()).unwrap();
let signature = Signer::sign(&data1, key1.private()).unwrap();

// The signature should be valid
assert!(Verifier::verify(&data1, &signature, key1.public()).is_ok());
Expand Down
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
12 changes: 6 additions & 6 deletions identity_credential/src/credential/jwt_serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ where
#[serde(skip_serializing_if = "Option::is_none")]
exp: Option<i64>,
/// Represents the issuer.
iss: Cow<'credential, Issuer>,
pub(crate) iss: Cow<'credential, Issuer>,

/// Represents the issuanceDate encoded as a UNIX timestamp.
#[serde(flatten)]
Expand Down 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
4 changes: 3 additions & 1 deletion identity_credential/src/credential/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ 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::*;
pub(crate) use self::jwt_serialization::CredentialJwtClaims;
#[cfg(feature = "presentation")]
pub(crate) use self::jwt_serialization::IssuanceDateClaims;
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;
}
Loading