Verifiable Presentations
A verifiable presentation is the recommended data format for sharing one or more verifiable credentials. It is constructed and signed by a holder to prove control over their credentials and can be presented to a verifier for validation.
For instance: after an issuer creates and issues a verifiable credential to a holder, such as a university issuing a degree to a graduate, the holder stores it securely until asked to present it. A company could then request proof of that university degree: the holder can create a verifiable presentation containing their credential, which is already signed by their university, and present it to the company to validate.
Note that verifiable presentations that contain personal data should, as with verifiable credentials, be transmitted and stored securely off-chain to satisfy data privacy regulations such as GDPR.
See the Verifiable Credentials Data Model Specification for more information on verifiable presentations.
Creation
The IOTA Identity Framework enables holders to construct verifiable presentations easily. As demonstrated in the example, holders need only pass in their credentials to present and sign the presentation.
The following properties may be specified on a presentation:
- ID: optional URI identifier for the presentation.
- Context: list of JSON-LD context URIs. Includes
"https://www.w3.org/2018/credentials/v1"
by default. - Types: list of types describing the presentation. Includes
"VerifiablePresentation"
by default. - Credentials: list of verifiable credentials to present.
- Holder: optional URI, typically a DID, of the entity that generated the presentation.
- Refresh Service: optional link to a service where the recipient may refresh the included credentials.
- Terms of Use: optional list of policies defining obligations, prohibitions, or permissions of the presentation recipient.
Of the above, only the list of credentials is required when creating a presentation using the framework. However, the holder property should be included to satisfy subject-holder relationship checks during validation.
After creation, the holder signs the verifiable presentation using a private key linked to one of the verification methods in their DID Document and transmits it to a verifier for validation.
Proof Options
A digital signature on a verifiable presentation both provides data integrity and proves the DID of the holder. The proof section embedded in a presentation may also include additional metadata.
The following metadata properties can be configured by the framework and are optional and omitted by default:
- Created: timestamp of when the presentation was signed.
- Expires: timestamp after which the presentation is no longer considered valid.
- Challenge: arbitrary random string. Sent by the verifier and mitigates replay attacks; should be sufficiently random and uniquely generated per presentation request.
- Domain: arbitrary string. Sent by the verifier and can help mitigate replay attacks when used with a challenge.
- Proof Purpose: indicates the purpose of the signature.
- AssertionMethod: to assert a claim. The signing verification method must have an
assertionMethod
relationship to be valid. - Authentication: to authenticate the signer. The signing verification method must have an
authentication
relationship to be valid.
- AssertionMethod: to assert a claim. The signing verification method must have an
Verifiers should always send a challenge and domain to mitigate replay attacks, see Security Considerations.
A verifier could also choose to ignore some or all of these options. See Proof Verifier Options for more information.
Validation
The IOTA Identity Framework provides several options for verifiers to validate various sections of a verifiable presentation. See the example for a demonstration of how to validate a presentation.
The framework checks:
- Semantic structure: ensures the presentation and its credentials adhere to the specification.
- Presentation proof: verifies the presentation signature against the DID Document of the holder.
- Credential proofs: verifies the credential signatures against the DID Documents of their respective issuers.
- Optional validations: additional checks on signatures and credential fields can be configured by the verifier.
Note that a verifier may specify which DID Documents to use for the holder and issuers, otherwise they are resolved from the Tangle automatically.
Currently, the following are not checked automatically:
- Data schemas: credentials that specify a schema property should be examined to ensure conformance.
- Fitness for purpose: whether the credentials in a presentation and the data within them are acceptable and valid depends on the context in which they are used. Verifiers should ensure that the credential types, subjects, and schemas sent by a holder match what was requested.
- Issuer trustworthiness: verifiers must check that they trust the issuer on each individual credential in a presentation. The framework only verifies that the issuer's signature on each credential is current and valid against the given options.
The default validation behaviour may be modified by the following options.
Proof Verifier Options
While the framework always verifies that the digital signature on a verifiable presentation is valid, a verifier may validate additional fields in the proof on a presentation. Notably, to mitigate potential replay attacks a verifier should always check that the challenge and domain fields match what was sent to the holder when requesting the presentation. See Security Considerations for more information.
The following options are available:
- Method Scope: check the signing verification method has a particular verification relationship. Overridden by the proof purpose check.
- Method Type: check the signing verification method has a particular type.
- Challenge: check the challenge field matches this string.
- Domain: check the domain field matches this string.
- Proof Purpose: require a specific purpose on the proof. See Proof Options.
- Allow Expired: accept proofs where the current datetime is after their expiration. Default is to reject expired proofs.
See Proof Options for more information on setting these properties as a holder when signing a verifiable presentation.
Subject-Holder Relationship
Specifies the expected relationship between the holder that signed the verifiable presentation and the subject specified in each verifiable credential.
This can be restricted by the nonTransferable
property,
which indicates that a verifiable credential must only be encapsulated into a verifiable presentation whose holder matches the credential subject.
By default, the framework always enforces that the holder matches the subject on all credentials. The following options are available to modify that behaviour:
AlwaysSubject
(default): the holder DID that signed the presentation must match thecredentialSubject
id
field in each of the attached credentials. This is the safest option which ensures holders may only present credentials that were directly issued to their DID. An error is thrown on a mismatch or if no subjectid
is present.SubjectOnNonTransferable
: the holder DID must match the subject only for credentials where thenonTransferable
property istrue
. This is appropriate for accepting bearer credentials while still adhering to the specification.Any
: the holder DID is not required to have any kind of relationship to any credential subject. This option performs no checks and ignores thenonTransferable
property.
See the Verifiable Credentials Data Model Specification for further discussion on the different subject-holder relationships.
Credential Validation Options
These options specify conditions that all credentials in a verifiable presentation must satisfy.
- Expiry Date: check that the
expirationDate
property, if present, is not before a specific datetime. Defaults to the current datetime if unset. - Issuance Date: check that the
issuanceDate
property, if present, is not after a specific datetime. Defaults to the current datetime if unset. - Verifier Options: see Proof Verifier Options for details.
Security Considerations
Replay Attacks
A verifiable presentation without challenge and domain properties could potentially be stored by a malicious actor and replayed to a different verifier, impersonating the holder. This is because the holder's signature on a presentation would still be seen as valid indefinitely, until they rotate the verification method used.
To mitigate this, verifiers should always send a unique challenge and domain when requesting a verifiable presentation. These properties are then included in the proof section of the presentation by the holder during signing using Proof Options. The digital signature prevents these properties from being altered as it would invalidate the signature, effectively preventing a malicious actor from injecting different values into old verifiable presentations. A presentation without a challenge and domain in its proof that matches what was sent by the verifier should be considered invalid.
The challenge string should be sufficiently random and unique for each verifiable presentation requested by a verifier to avoid being predicted. The domain, which does not need to be random, is an additional measure. In the unlikely occurrence of two verifiers generating the same random challenge, the domain would sufficiently distinguish those requests.
Holders may additionally specify that their signature on a verifiable presentation expires after a short duration, as per Proof Options. However, verifiers and different implementations could choose to ignore that property, so setting a signature expiration alone should not be relied upon.
Example
The following code demonstrates how to use the IOTA Identity Framework end-to-end to create and sign a verifiable presentation as a holder, serialize it to JSON for transmission, deserialize it on the receiving side as a verifier, and finally validate it with various options.
- Rust
- Node.js
// Copyright 2020-2022 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
//! A Verifiable Presentation (VP) represents a bundle of one or more Verifiable Credentials.
//! This example demonstrates building and usage of VPs.
//!
//! cargo run --example account_create_vp
use identity_iota::account::Account;
use identity_iota::account::AccountBuilder;
use identity_iota::account::IdentitySetup;
use identity_iota::account::MethodContent;
use identity_iota::core::json;
use identity_iota::core::Duration;
use identity_iota::core::FromJson;
use identity_iota::core::Timestamp;
use identity_iota::core::ToJson;
use identity_iota::core::Url;
use identity_iota::credential::Credential;
use identity_iota::credential::CredentialBuilder;
use identity_iota::credential::Presentation;
use identity_iota::credential::PresentationBuilder;
use identity_iota::credential::Subject;
use identity_iota::crypto::ProofOptions;
use identity_iota::did::verifiable::VerifierOptions;
use identity_iota::account::Result;
use identity_iota::client::CredentialValidationOptions;
use identity_iota::client::FailFast;
use identity_iota::client::PresentationValidationOptions;
use identity_iota::client::Resolver;
use identity_iota::client::SubjectHolderRelationship;
#[tokio::main]
async fn main() -> Result<()> {
// ===========================================================================
// Step 1: Create identities for the issuer and the holder.
// ===========================================================================
// Create an account builder with in-memory storage for simplicity.
// See `create_did` example to configure Stronghold storage.
let mut builder: AccountBuilder = Account::builder();
// Create an identity for the issuer.
let mut issuer: Account = builder.create_identity(IdentitySetup::default()).await?;
// Add a dedicated verification method to the issuer, with which to sign credentials.
issuer
.update_identity()
.create_method()
.content(MethodContent::GenerateEd25519)
.fragment("issuerKey")
.apply()
.await?;
// Create an identity for the holder, in this case also the subject.
let mut alice: Account = builder.create_identity(IdentitySetup::default()).await?;
// Add verification method to the holder.
alice
.update_identity()
.create_method()
.content(MethodContent::GenerateEd25519)
.fragment("aliceKey")
.apply()
.await?;
// ===========================================================================
// Step 2: Issuer creates and signs a Verifiable Credential.
// ===========================================================================
// Create VC "subject" field containing subject ID and claims about it.
let subject: Subject = Subject::from_json_value(json!({
"id": alice.did().to_string(),
"name": "Alice",
"degree": {
"type": "BachelorDegree",
"name": "Bachelor of Science and Arts",
},
"GPA": "4.0",
}))?;
// Build credential using subject above and issuer.
let mut credential: Credential = CredentialBuilder::default()
.id(Url::parse("https://example.edu/credentials/3732")?)
.issuer(Url::parse(issuer.did().to_string())?)
.type_("UniversityDegreeCredential")
.subject(subject)
.build()?;
// Sign the Credential with the issuers default key.
issuer
.sign("#issuerKey", &mut credential, ProofOptions::default())
.await?;
println!("Credential JSON > {:#}", credential);
// ===========================================================================
// Step 3: Issuer sends the Verifiable Credential to the holder.
// ===========================================================================
// The credential is then serialized to JSON and transmitted to the holder in a secure manner.
// Note that the credential is NOT published to the IOTA Tangle. It is sent and stored off-chain.
let credential_json: String = credential.to_json()?;
// ===========================================================================
// Step 4: Verifier sends the holder a challenge and requests a signed Verifiable Presentation.
// ===========================================================================
// A unique random challenge generated by the requester per presentation can mitigate replay attacks
let challenge: &str = "475a7984-1bb5-4c4c-a56f-822bccd46440";
// The verifier and holder also agree that the signature should have an expiry date
// 10 minutes from now.
let expires: Timestamp = Timestamp::now_utc().checked_add(Duration::minutes(10)).unwrap();
// ===========================================================================
// Step 5: Holder creates and signs a verifiable presentation from the issued credential.
// ===========================================================================
// Deserialize the credential.
let credential: Credential = Credential::from_json(credential_json.as_str())?;
// Create an unsigned Presentation from the previously issued Verifiable Credential.
let mut presentation: Presentation = PresentationBuilder::default()
.holder(Url::parse(alice.did().as_ref())?)
.credential(credential)
.build()?;
// Sign the verifiable presentation using the holder's verification method
// and include the requested challenge and expiry timestamp.
alice
.sign(
"#aliceKey",
&mut presentation,
ProofOptions::new().challenge(challenge.to_string()).expires(expires),
)
.await?;
// ===========================================================================
// Step 6: Holder sends a verifiable presentation to the verifier.
// ===========================================================================
// Convert the Verifiable Presentation to JSON to send it to the verifier.
let presentation_json: String = presentation.to_json()?;
// ===========================================================================
// Step 7: Verifier receives the Verifiable Presentation and verifies it.
// ===========================================================================
// Deserialize the presentation from the holder:
let presentation: Presentation = Presentation::from_json(&presentation_json)?;
// The verifier wants the following requirements to be satisfied:
// - Signature verification (including checking the requested challenge to mitigate replay attacks)
// - Presentation validation must fail if credentials expiring within the next 10 hours are encountered
// - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property
// - The issuance date must not be in the future.
let presentation_verifier_options: VerifierOptions = VerifierOptions::new()
.challenge(challenge.to_owned())
.allow_expired(false);
// Do not allow credentials that expire within the next 10 hours.
let credential_validation_options: CredentialValidationOptions = CredentialValidationOptions::default()
.earliest_expiry_date(Timestamp::now_utc().checked_add(Duration::hours(10)).unwrap());
let presentation_validation_options = PresentationValidationOptions::default()
.presentation_verifier_options(presentation_verifier_options.clone())
.shared_validation_options(credential_validation_options)
.subject_holder_relationship(SubjectHolderRelationship::AlwaysSubject);
// Validate the presentation and all the credentials included in it.
let resolver: Resolver = Resolver::new().await?;
resolver
.verify_presentation(
&presentation,
&presentation_validation_options,
FailFast::FirstError,
None,
None,
)
.await?;
// Since no errors were thrown by `verify_presentation` we know that the validation was successful.
println!("VP successfully validated");
// Note that we did not declare a latest allowed issuance date for credentials. This is because we only want to check
// that the credentials do not have an issuance date in the future which is a default check.
Ok(())
}
// Copyright 2020-2021 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
import {
AccountBuilder,
Credential,
CredentialValidationOptions,
Duration,
FailFast,
Presentation,
PresentationValidationOptions,
ProofOptions,
Resolver,
SubjectHolderRelationship,
Timestamp,
VerifierOptions,
Storage,
MethodContent,
Network
} from './../../node/identity_wasm.js';
/**
This example shows how to create a Verifiable Presentation and validate it.
A Verifiable Presentation is the format in which a (collection of) Verifiable Credential(s) gets shared.
It is signed by the subject, to prove control over the Verifiable Credential with a nonce or timestamp.
**/
async function createVP(storage?: Storage) {
// ===========================================================================
// Step 1: Create identities for the issuer and the holder.
// ===========================================================================
// Create Account builder.
const builder = new AccountBuilder({
storage,
});
// Create an identity for the issuer.
const issuer = await builder.createIdentity();
// Add a dedicated verification method to the issuer, with which to sign credentials.
await issuer.createMethod({
content: MethodContent.GenerateEd25519(),
fragment: "issuerKey"
})
// Create an identity for the holder, in this case also the subject.
const alice = await builder.createIdentity();
// Add verification method to the holder.
await alice.createMethod({
content: MethodContent.GenerateEd25519(),
fragment: "aliceKey"
})
// ===========================================================================
// Step 2: Issuer creates and signs a Verifiable Credential.
// ===========================================================================
// Create a credential subject indicating the degree earned by Alice, linked to their DID.
const subject = {
id: alice.document().id(),
name: "Alice",
degreeName: "Bachelor of Science and Arts",
degreeType: "BachelorDegree",
GPA: "4.0"
};
// Create an unsigned `UniversityDegree` credential for Alice
const unsignedVc = new Credential({
id: "https://example.edu/credentials/3732",
type: "UniversityDegreeCredential",
issuer: issuer.document().id(),
credentialSubject: subject
});
// Created a signed credential by the issuer.
const signedVc = await issuer.createSignedCredential(
"#issuerKey",
unsignedVc,
ProofOptions.default(),
);
// ===========================================================================
// Step 3: Issuer sends the Verifiable Credential to the holder.
// ===========================================================================
// The credential is then serialized to JSON and transmitted to the holder in a secure manner.
// Note that the credential is NOT published to the IOTA Tangle. It is sent and stored off-chain.
const signedVcJson = signedVc.toJSON();
console.log(`Credential JSON >`, JSON.stringify(signedVcJson, null, 2));
// ===========================================================================
// Step 4: Verifier sends the holder a challenge and requests a signed Verifiable Presentation.
// ===========================================================================
// A unique random challenge generated by the requester per presentation can mitigate replay attacks.
const challenge = "475a7984-1bb5-4c4c-a56f-822bccd46440";
// The verifier and holder also agree that the signature should have an expiry date
// 10 minutes from now.
const expires = Timestamp.nowUTC().checkedAdd(Duration.minutes(10));
// ===========================================================================
// Step 5: Holder creates a verifiable presentation from the issued credential for the verifier to validate.
// ===========================================================================
// Deserialize the credential.
const receivedVc = Credential.fromJSON(signedVcJson);
// Create a Verifiable Presentation from the Credential
const unsignedVp = new Presentation({
holder: alice.did(),
verifiableCredential: receivedVc
})
// Sign the verifiable presentation using the holder's verification method
// and include the requested challenge and expiry timestamp.
const signedVp = await alice.createSignedPresentation(
"#aliceKey",
unsignedVp,
new ProofOptions({
challenge: challenge,
expires
})
);
// ===========================================================================
// Step 6: Holder sends a verifiable presentation to the verifier.
// ===========================================================================
// Convert the Verifiable Presentation to JSON to send it to the verifier.
const signedVpJSON = signedVp.toJSON();
// ===========================================================================
// Step 7: Verifier receives the Verifiable Presentation and verifies it.
// ===========================================================================
// Deserialize the presentation from the holder.
const presentation = Presentation.fromJSON(signedVpJSON);
// The verifier wants the following requirements to be satisfied:
// - Signature verification (including checking the requested challenge to mitigate replay attacks)
// - Presentation validation must fail if credentials expiring within the next 10 hours are encountered
// - The presentation holder must always be the subject, regardless of the presence of the nonTransferable property
// - The issuance date must not be in the future.
// Declare that the challenge must match our expectation:
const presentationVerifierOptions = new VerifierOptions({
challenge: "475a7984-1bb5-4c4c-a56f-822bccd46440",
allowExpired: false,
});
// Declare that any credential contained in the presentation are not allowed to expire within the next 10 hours:
const earliestExpiryDate = Timestamp.nowUTC().checkedAdd(Duration.hours(10));
const credentialValidationOptions = new CredentialValidationOptions({
earliestExpiryDate: earliestExpiryDate,
});
// Declare that the presentation holder's DID must match the subject ID on all credentials in the presentation.
const subjectHolderRelationship = SubjectHolderRelationship.AlwaysSubject;
const presentationValidationOptions = new PresentationValidationOptions({
sharedValidationOptions: credentialValidationOptions,
presentationVerifierOptions: presentationVerifierOptions,
subjectHolderRelationship: subjectHolderRelationship,
});
// In order to validate presentations and credentials one needs to resolve the DID Documents of
// the presentation holder and of credential issuers. This is something the `Resolver` can help with.
const resolver = new Resolver();
// Validate the presentation and all the credentials included in it according to the validation options
await resolver.verifyPresentation(
presentation,
presentationValidationOptions,
FailFast.FirstError
);
// Since no errors were thrown by `verifyPresentation` we know that the validation was successful.
console.log(`VP successfully validated`);
// Note that the `verifyPresentation` method we called automatically resolved all DID Documents that are necessary to validate the presentation.
// It is also possible to supply extra arguments to avoid some resolutions if one already has up-to-date resolved documents of
// either the holder or issuers (see the method's documentation).
}
export { createVP };