Skip to content
Draft
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
95 changes: 95 additions & 0 deletions examples/0_basic/9_vc_v2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2020-2025 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

//! This example shows how to create a Verifiable Credential and validate it.
//! In this example, alice takes the role of the subject, while we also have an issuer.
//! The issuer signs a UniversityDegreeCredential type verifiable credential with Alice's name and DID.
//! This Verifiable Credential can be verified by anyone, allowing Alice to take control of it and share it with
//! whomever they please.
//!
//! cargo run --release --example 9_vc_v2

use examples::create_did_document;
use examples::get_funded_client;
use examples::get_memstorage;
use identity_eddsa_verifier::EdDSAJwsVerifier;
use identity_iota::core::Object;

use identity_iota::credential::DecodedJwtCredentialV2;
use identity_iota::credential::Jwt;
use identity_iota::credential::JwtCredentialValidationOptions;
use identity_iota::credential::JwtCredentialValidator;
use identity_iota::storage::JwkDocumentExt;
use identity_iota::storage::JwsSignatureOptions;

use identity_iota::core::json;
use identity_iota::core::FromJson;
use identity_iota::core::Url;
use identity_iota::credential::credential_v2::Credential;
use identity_iota::credential::CredentialBuilder;
use identity_iota::credential::FailFast;
use identity_iota::credential::Subject;
use identity_iota::did::DID;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
// create new issuer account with did document
let issuer_storage = get_memstorage()?;
let issuer_identity_client = get_funded_client(&issuer_storage).await?;
let (issuer_document, issuer_vm_fragment) = create_did_document(&issuer_identity_client, &issuer_storage).await?;

// create new holder account with did document
let holder_storage = get_memstorage()?;
let holder_identity_client = get_funded_client(&holder_storage).await?;
let (holder_document, _) = create_did_document(&holder_identity_client, &holder_storage).await?;

// Create a credential subject indicating the degree earned by Alice.
let subject: Subject = Subject::from_json_value(json!({
"id": holder_document.id().as_str(),
"name": "Alice",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts",
},
"GPA": "4.0",
}))?;

// Build credential using subject above and issuer.
let credential: Credential = CredentialBuilder::default()
.id(Url::parse("https://example.edu/credentials/3732")?)
.issuer(Url::parse(issuer_document.id().as_str())?)
.type_("UniversityDegreeCredential")
.subject(subject)
.build_v2()?;

let credential_jwt: Jwt = issuer_document
.create_credential_jwt(
&credential,
&issuer_storage,
&issuer_vm_fragment,
&JwsSignatureOptions::default(),
None,
)
.await?;

// Before sending this credential to the holder the issuer wants to validate that some properties
// of the credential satisfy their expectations.

// Validate the credential's signature using the issuer's DID Document, the credential's semantic structure,
// that the issuance date is not in the future and that the expiration date is not in the past:
let decoded_credential: DecodedJwtCredentialV2<Object> =
JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default())
.validate_v2::<_, Object>(
&credential_jwt,
&issuer_document,
&JwtCredentialValidationOptions::default(),
FailFast::FirstError,
)
.unwrap();

println!("VC successfully validated");

println!("Credential JSON > {:#}", decoded_credential.credential);

Ok(())
}
4 changes: 4 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ name = "7_revoke_vc"
path = "0_basic/8_legacy_stronghold.rs"
name = "8_legacy_stronghold"

[[example]]
path = "0_basic/9_vc_v2.rs"
name = "9_vc_v2"

[[example]]
path = "1_advanced/0_did_controls_did.rs"
name = "0_did_controls_did"
Expand Down
35 changes: 23 additions & 12 deletions identity_credential/src/credential/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use identity_core::common::Timestamp;
use identity_core::common::Url;
use identity_core::common::Value;

use crate::credential::credential_v2::Credential as CredentialV2;
use crate::credential::Credential;
use crate::credential::Evidence;
use crate::credential::Issuer;
Expand All @@ -20,7 +21,7 @@ use crate::error::Result;
use super::Proof;

/// A `CredentialBuilder` is used to create a customized `Credential`.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct CredentialBuilder<T = Object> {
pub(crate) context: Vec<Context>,
pub(crate) id: Option<Url>,
Expand All @@ -43,9 +44,9 @@ impl<T> CredentialBuilder<T> {
/// Creates a new `CredentialBuilder`.
pub fn new(properties: T) -> Self {
Self {
context: vec![Credential::<T>::base_context().clone()],
context: Vec::new(),
id: None,
types: vec![Credential::<T>::base_type().into()],
types: Vec::new(),
subject: Vec::new(),
issuer: None,
issuance_date: None,
Expand Down Expand Up @@ -119,6 +120,20 @@ impl<T> CredentialBuilder<T> {
self
}

/// Sets the value of the `Credential` `validFrom`.
#[must_use]
pub fn valid_from(mut self, value: Timestamp) -> Self {
self.issuance_date = Some(value);
self
}

/// Sets the value of the `Credential` `validUntil`.
#[must_use]
pub fn valid_until(mut self, value: Timestamp) -> Self {
self.expiration_date = Some(value);
self
}

/// Adds a value to the `credentialStatus` set.
#[must_use]
pub fn status(mut self, value: impl Into<Status>) -> Self {
Expand Down Expand Up @@ -172,6 +187,11 @@ impl<T> CredentialBuilder<T> {
pub fn build(self) -> Result<Credential<T>> {
Credential::from_builder(self)
}

/// Returns a new [CredentialV2] based on the builder's configuration.
pub fn build_v2(self) -> Result<CredentialV2<T>> {
CredentialV2::from_builder(self)
}
}

impl CredentialBuilder {
Expand Down Expand Up @@ -201,15 +221,6 @@ impl CredentialBuilder {
}
}

impl<T> Default for CredentialBuilder<T>
where
T: Default,
{
fn default() -> Self {
Self::new(T::default())
}
}

#[cfg(test)]
mod tests {
use identity_core::common::Object;
Expand Down
65 changes: 64 additions & 1 deletion identity_credential/src/credential/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use identity_core::common::Url;
use identity_core::convert::FmtJson;

use crate::credential::CredentialBuilder;
use crate::credential::CredentialSealed;
use crate::credential::CredentialT;
use crate::credential::Evidence;
use crate::credential::Issuer;
use crate::credential::Policy;
Expand Down Expand Up @@ -104,7 +106,15 @@ impl<T> Credential<T> {
}

/// Returns a new `Credential` based on the `CredentialBuilder` configuration.
pub fn from_builder(builder: CredentialBuilder<T>) -> Result<Self> {
pub fn from_builder(mut builder: CredentialBuilder<T>) -> Result<Self> {
if builder.context.first() != Some(Self::base_context()) {
builder.context.insert(0, Self::base_context().clone());
}

if builder.types.first().map(String::as_str) != Some(Self::base_type()) {
builder.types.insert(0, Self::base_type().to_owned());
}

let this: Self = Self {
context: OneOrMany::Many(builder.context),
id: builder.id,
Expand Down Expand Up @@ -197,6 +207,59 @@ where
}
}

impl<T> CredentialSealed for Credential<T> {}

impl<T> CredentialT for Credential<T>
where
T: ToOwned<Owned = T> + serde::Serialize + serde::de::DeserializeOwned,
{
type Properties = T;

fn base_context(&self) -> &'static Context {
Self::base_context()
}

fn type_(&self) -> &OneOrMany<String> {
&self.types
}

fn context(&self) -> &OneOrMany<Context> {
&self.context
}

fn subject(&self) -> &OneOrMany<Subject> {
&self.credential_subject
}

fn issuer(&self) -> &Issuer {
&self.issuer
}

fn valid_from(&self) -> Timestamp {
self.issuance_date
}

fn valid_until(&self) -> Option<Timestamp> {
self.expiration_date
}

fn properties(&self) -> &Self::Properties {
&self.properties
}

fn status(&self) -> Option<&Status> {
self.credential_status.as_ref()
}

fn non_transferable(&self) -> bool {
self.non_transferable.unwrap_or_default()
}

fn serialize_jwt(&self, custom_claims: Option<Object>) -> Result<String> {
self.serialize_jwt(custom_claims)
}
}

#[cfg(test)]
mod tests {
use identity_core::common::OneOrMany;
Expand Down
Loading
Loading