diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 40d8057caf..09f0d7aa43 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -34,7 +34,7 @@ wasm-bindgen-futures = { version = "0.4", default-features = false } version = "1.0.0" path = "../../identity_iota" default-features = false -features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt"] +features = ["client", "revocation-bitmap", "resolver", "domain-linkage", "sd-jwt", "status-list-2021"] [dev-dependencies] rand = "0.8.5" diff --git a/bindings/wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js b/bindings/wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js new file mode 100644 index 0000000000..11a0a4617c --- /dev/null +++ b/bindings/wasm/cypress/e2e/1_advanced/7_status_list_2021.cy.js @@ -0,0 +1,11 @@ +import { statusList2021 } from "../../../examples/dist/web/1_advanced/7_status_list_2021"; +import { setup } from "../../support/setup"; + +describe( + "statusList2021", + () => { + it("Status List 2021", async () => { + await setup(statusList2021); + }); + }, +); diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 0ed364a27c..e17fcf0c2c 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -159,6 +159,18 @@ with their corresponding disclosure digests.

Service

A DID Document Service used to enable trusted interactions associated with a DID subject.

+
StatusList2021
+

StatusList2021 data structure as described in W3C's VC status list 2021.

+
+
StatusList2021Credential
+

A parsed StatusList2021Credential.

+
+
StatusList2021CredentialBuilder
+

Builder type to construct valid StatusList2021Credential istances.

+
+
StatusList2021Entry
+

StatusList2021Entry implementation.

+
Storage

A type wrapping a JwkStorage and KeyIdStorage that should always be used together when working with storage backed DID documents.

@@ -175,22 +187,10 @@ working with storage backed DID documents.

## Members
-
StatusCheck
-

Controls validation behaviour when checking whether or not a credential has been revoked by its -credentialStatus.

-
-
Strict
-

Validate the status if supported, reject any unsupported -credentialStatus types.

-

Only RevocationBitmap2022 is currently supported.

-

This is the default.

-
-
SkipUnsupported
-

Validate the status if supported, skip any unsupported -credentialStatus types.

-
-
SkipAll
-

Skip all status checks.

+
StateMetadataEncoding
+
+
StatusPurpose
+

Purpose of a StatusList2021.

SubjectHolderRelationship

Declares how credential subjects must relate to the presentation holder.

@@ -215,7 +215,24 @@ This variant is the default.

FirstError

Return after the first error occurs.

-
StateMetadataEncoding
+
StatusCheck
+

Controls validation behaviour when checking whether or not a credential has been revoked by its +credentialStatus.

+
+
Strict
+

Validate the status if supported, reject any unsupported +credentialStatus types.

+

Only RevocationBitmap2022 is currently supported.

+

This is the default.

+
+
SkipUnsupported
+

Validate the status if supported, skip any unsupported +credentialStatus types.

+
+
SkipAll
+

Skip all status checks.

+
+
CredentialStatus
MethodRelationship
@@ -232,15 +249,15 @@ This variant is the default.

This function does not check whether alg = EdDSA in the protected header. Callers are expected to assert this prior to calling the function.

-
start()
-

Initializes the console error panic hook for better error messages

-
encodeB64(data)string

Encode the given bytes in url-safe base64.

decodeB64(data)Uint8Array

Decode the given url-safe base64-encoded slice into its raw bytes.

+
start()
+

Initializes the console error panic hook for better error messages

+
@@ -457,14 +474,14 @@ if the object is being concurrently modified. * [.insertService(service)](#CoreDocument+insertService) * [.removeService(didUrl)](#CoreDocument+removeService) ⇒ [Service](#Service) \| undefined * [.resolveService(query)](#CoreDocument+resolveService) ⇒ [Service](#Service) \| undefined - * [.methods(scope)](#CoreDocument+methods) ⇒ [Array.<VerificationMethod>](#VerificationMethod) + * [.methods([scope])](#CoreDocument+methods) ⇒ [Array.<VerificationMethod>](#VerificationMethod) * [.verificationRelationships()](#CoreDocument+verificationRelationships) ⇒ Array.<(DIDUrl\|VerificationMethod)> * [.insertMethod(method, scope)](#CoreDocument+insertMethod) * [.removeMethod(did)](#CoreDocument+removeMethod) ⇒ [VerificationMethod](#VerificationMethod) \| undefined - * [.resolveMethod(query, scope)](#CoreDocument+resolveMethod) ⇒ [VerificationMethod](#VerificationMethod) \| undefined + * [.resolveMethod(query, [scope])](#CoreDocument+resolveMethod) ⇒ [VerificationMethod](#VerificationMethod) \| undefined * [.attachMethodRelationship(didUrl, relationship)](#CoreDocument+attachMethodRelationship) ⇒ boolean * [.detachMethodRelationship(didUrl, relationship)](#CoreDocument+detachMethodRelationship) ⇒ boolean - * [.verifyJws(jws, options, signatureVerifier, detachedPayload)](#CoreDocument+verifyJws) ⇒ [DecodedJws](#DecodedJws) + * [.verifyJws(jws, options, signatureVerifier, [detachedPayload])](#CoreDocument+verifyJws) ⇒ [DecodedJws](#DecodedJws) * [.revokeCredentials(serviceQuery, indices)](#CoreDocument+revokeCredentials) * [.unrevokeCredentials(serviceQuery, indices)](#CoreDocument+unrevokeCredentials) * [.clone()](#CoreDocument+clone) ⇒ [CoreDocument](#CoreDocument) @@ -474,7 +491,7 @@ if the object is being concurrently modified. * [.generateMethod(storage, keyType, alg, fragment, scope)](#CoreDocument+generateMethod) ⇒ Promise.<string> * [.purgeMethod(storage, id)](#CoreDocument+purgeMethod) ⇒ Promise.<void> * [.createJws(storage, fragment, payload, options)](#CoreDocument+createJws) ⇒ [Promise.<Jws>](#Jws) - * [.createCredentialJwt(storage, fragment, credential, options, custom_claims)](#CoreDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.createCredentialJwt(storage, fragment, credential, options, [custom_claims])](#CoreDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#CoreDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) * _static_ * [.fromJSON(json)](#CoreDocument.fromJSON) ⇒ [CoreDocument](#CoreDocument) @@ -654,7 +671,7 @@ if present. -### coreDocument.methods(scope) ⇒ [Array.<VerificationMethod>](#VerificationMethod) +### coreDocument.methods([scope]) ⇒ [Array.<VerificationMethod>](#VerificationMethod) Returns a list of all [VerificationMethod](#VerificationMethod) in the DID Document, whose verification relationship matches `scope`. @@ -664,7 +681,7 @@ If `scope` is not set, a list over the **embedded** methods is returned. | Param | Type | | --- | --- | -| scope | [MethodScope](#MethodScope) \| undefined | +| [scope] | [MethodScope](#MethodScope) \| undefined | @@ -697,7 +714,7 @@ Removes all references to the specified Verification Method. -### coreDocument.resolveMethod(query, scope) ⇒ [VerificationMethod](#VerificationMethod) \| undefined +### coreDocument.resolveMethod(query, [scope]) ⇒ [VerificationMethod](#VerificationMethod) \| undefined Returns a copy of the first verification method with an `id` property matching the provided `query` and the verification relationship specified by `scope`, if present. @@ -707,7 +724,7 @@ specified by `scope`, if present. | Param | Type | | --- | --- | | query | [DIDUrl](#DIDUrl) \| string | -| scope | [MethodScope](#MethodScope) \| undefined | +| [scope] | [MethodScope](#MethodScope) \| undefined | @@ -722,7 +739,7 @@ so it cannot be an embedded one. | Param | Type | | --- | --- | | didUrl | [DIDUrl](#DIDUrl) | -| relationship | number | +| relationship | [MethodRelationship](#MethodRelationship) | @@ -734,11 +751,11 @@ Detaches the given relationship from the given method, if the method exists. | Param | Type | | --- | --- | | didUrl | [DIDUrl](#DIDUrl) | -| relationship | number | +| relationship | [MethodRelationship](#MethodRelationship) | -### coreDocument.verifyJws(jws, options, signatureVerifier, detachedPayload) ⇒ [DecodedJws](#DecodedJws) +### coreDocument.verifyJws(jws, options, signatureVerifier, [detachedPayload]) ⇒ [DecodedJws](#DecodedJws) Decodes and verifies the provided JWS according to the passed `options` and `signatureVerifier`. If no `signatureVerifier` argument is provided a default verifier will be used that is (only) capable of verifying EdDSA signatures. @@ -756,7 +773,7 @@ or set explicitly in the `options`. | jws | [Jws](#Jws) | | options | [JwsVerificationOptions](#JwsVerificationOptions) | | signatureVerifier | IJwsVerifier | -| detachedPayload | string \| undefined | +| [detachedPayload] | string \| undefined | @@ -865,7 +882,7 @@ See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1). -### coreDocument.createCredentialJwt(storage, fragment, credential, options, custom_claims) ⇒ [Promise.<Jwt>](#Jwt) +### coreDocument.createCredentialJwt(storage, fragment, credential, options, [custom_claims]) ⇒ [Promise.<Jwt>](#Jwt) Produces a JWT where the payload is produced from the given `credential` in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). @@ -883,7 +900,7 @@ The `custom_claims` can be used to set additional claims on the resulting JWT. | fragment | string | | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | -| custom_claims | Record.<string, any> \| undefined | +| [custom_claims] | Record.<string, any> \| undefined | @@ -939,8 +956,8 @@ Deserializes an instance from a plain JS representation. * [.nonTransferable()](#Credential+nonTransferable) ⇒ boolean \| undefined * [.proof()](#Credential+proof) ⇒ [Proof](#Proof) \| undefined * [.properties()](#Credential+properties) ⇒ Map.<string, any> - * [.setProof(proof)](#Credential+setProof) - * [.toJwtClaims(custom_claims)](#Credential+toJwtClaims) ⇒ Record.<string, any> + * [.setProof([proof])](#Credential+setProof) + * [.toJwtClaims([custom_claims])](#Credential+toJwtClaims) ⇒ Record.<string, any> * [.toJSON()](#Credential+toJSON) ⇒ any * [.clone()](#Credential+clone) ⇒ [Credential](#Credential) * _static_ @@ -1052,7 +1069,7 @@ Returns a copy of the miscellaneous properties on the [Credential](#Credential). **Kind**: instance method of [Credential](#Credential) -### credential.setProof(proof) +### credential.setProof([proof]) Sets the `proof` property of the [Credential](#Credential). Note that this proof is not related to JWT. @@ -1061,11 +1078,11 @@ Note that this proof is not related to JWT. | Param | Type | | --- | --- | -| proof | [Proof](#Proof) \| undefined | +| [proof] | [Proof](#Proof) \| undefined | -### credential.toJwtClaims(custom_claims) ⇒ Record.<string, any> +### credential.toJwtClaims([custom_claims]) ⇒ Record.<string, any> Serializes the `Credential` as a JWT claims set in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). @@ -1075,7 +1092,7 @@ The resulting object can be used as the payload of a JWS when issuing the creden | Param | Type | | --- | --- | -| custom_claims | Record.<string, any> \| undefined | +| [custom_claims] | Record.<string, any> \| undefined | @@ -1133,11 +1150,11 @@ A method agnostic DID Url. * [.did()](#DIDUrl+did) ⇒ [CoreDID](#CoreDID) * [.urlStr()](#DIDUrl+urlStr) ⇒ string * [.fragment()](#DIDUrl+fragment) ⇒ string \| undefined - * [.setFragment(value)](#DIDUrl+setFragment) + * [.setFragment([value])](#DIDUrl+setFragment) * [.path()](#DIDUrl+path) ⇒ string \| undefined - * [.setPath(value)](#DIDUrl+setPath) + * [.setPath([value])](#DIDUrl+setPath) * [.query()](#DIDUrl+query) ⇒ string \| undefined - * [.setQuery(value)](#DIDUrl+setQuery) + * [.setQuery([value])](#DIDUrl+setQuery) * [.join(segment)](#DIDUrl+join) ⇒ [DIDUrl](#DIDUrl) * [.toString()](#DIDUrl+toString) ⇒ string * [.toJSON()](#DIDUrl+toJSON) ⇒ any @@ -1166,14 +1183,14 @@ Returns a copy of the [DIDUrl](#DIDUrl) method fragment, if any. Excludes the le **Kind**: instance method of [DIDUrl](#DIDUrl) -### didUrl.setFragment(value) +### didUrl.setFragment([value]) Sets the `fragment` component of the [DIDUrl](#DIDUrl). **Kind**: instance method of [DIDUrl](#DIDUrl) | Param | Type | | --- | --- | -| value | string \| undefined | +| [value] | string \| undefined | @@ -1183,14 +1200,14 @@ Returns a copy of the [DIDUrl](#DIDUrl) path. **Kind**: instance method of [DIDUrl](#DIDUrl) -### didUrl.setPath(value) +### didUrl.setPath([value]) Sets the `path` component of the [DIDUrl](#DIDUrl). **Kind**: instance method of [DIDUrl](#DIDUrl) | Param | Type | | --- | --- | -| value | string \| undefined | +| [value] | string \| undefined | @@ -1200,14 +1217,14 @@ Returns a copy of the [DIDUrl](#DIDUrl) method query, if any. Excludes the leadi **Kind**: instance method of [DIDUrl](#DIDUrl) -### didUrl.setQuery(value) +### didUrl.setQuery([value]) Sets the `query` component of the [DIDUrl](#DIDUrl). **Kind**: instance method of [DIDUrl](#DIDUrl) | Param | Type | | --- | --- | -| value | string \| undefined | +| [value] | string \| undefined | @@ -1958,13 +1975,13 @@ if the object is being concurrently modified. * [.insertService(service)](#IotaDocument+insertService) * [.removeService(did)](#IotaDocument+removeService) ⇒ [Service](#Service) \| undefined * [.resolveService(query)](#IotaDocument+resolveService) ⇒ [Service](#Service) \| undefined - * [.methods(scope)](#IotaDocument+methods) ⇒ [Array.<VerificationMethod>](#VerificationMethod) + * [.methods([scope])](#IotaDocument+methods) ⇒ [Array.<VerificationMethod>](#VerificationMethod) * [.insertMethod(method, scope)](#IotaDocument+insertMethod) * [.removeMethod(did)](#IotaDocument+removeMethod) ⇒ [VerificationMethod](#VerificationMethod) \| undefined - * [.resolveMethod(query, scope)](#IotaDocument+resolveMethod) ⇒ [VerificationMethod](#VerificationMethod) \| undefined + * [.resolveMethod(query, [scope])](#IotaDocument+resolveMethod) ⇒ [VerificationMethod](#VerificationMethod) \| undefined * [.attachMethodRelationship(didUrl, relationship)](#IotaDocument+attachMethodRelationship) ⇒ boolean * [.detachMethodRelationship(didUrl, relationship)](#IotaDocument+detachMethodRelationship) ⇒ boolean - * [.verifyJws(jws, options, signatureVerifier, detachedPayload)](#IotaDocument+verifyJws) ⇒ [DecodedJws](#DecodedJws) + * [.verifyJws(jws, options, signatureVerifier, [detachedPayload])](#IotaDocument+verifyJws) ⇒ [DecodedJws](#DecodedJws) * [.pack()](#IotaDocument+pack) ⇒ Uint8Array * [.packWithEncoding(encoding)](#IotaDocument+packWithEncoding) ⇒ Uint8Array * [.metadata()](#IotaDocument+metadata) ⇒ [IotaDocumentMetadata](#IotaDocumentMetadata) @@ -1973,7 +1990,7 @@ if the object is being concurrently modified. * [.metadataUpdated()](#IotaDocument+metadataUpdated) ⇒ [Timestamp](#Timestamp) \| undefined * [.setMetadataUpdated(timestamp)](#IotaDocument+setMetadataUpdated) * [.metadataDeactivated()](#IotaDocument+metadataDeactivated) ⇒ boolean \| undefined - * [.setMetadataDeactivated(deactivated)](#IotaDocument+setMetadataDeactivated) + * [.setMetadataDeactivated([deactivated])](#IotaDocument+setMetadataDeactivated) * [.metadataStateControllerAddress()](#IotaDocument+metadataStateControllerAddress) ⇒ string \| undefined * [.metadataGovernorAddress()](#IotaDocument+metadataGovernorAddress) ⇒ string \| undefined * [.setMetadataPropertyUnchecked(key, value)](#IotaDocument+setMetadataPropertyUnchecked) @@ -1988,7 +2005,7 @@ if the object is being concurrently modified. * [.purgeMethod(storage, id)](#IotaDocument+purgeMethod) ⇒ Promise.<void> * ~~[.createJwt(storage, fragment, payload, options)](#IotaDocument+createJwt) ⇒ [Promise.<Jws>](#Jws)~~ * [.createJws(storage, fragment, payload, options)](#IotaDocument+createJws) ⇒ [Promise.<Jws>](#Jws) - * [.createCredentialJwt(storage, fragment, credential, options, custom_claims)](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.createCredentialJwt(storage, fragment, credential, options, [custom_claims])](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#IotaDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) * _static_ * [.newWithId(id)](#IotaDocument.newWithId) ⇒ [IotaDocument](#IotaDocument) @@ -2108,7 +2125,7 @@ if present. -### iotaDocument.methods(scope) ⇒ [Array.<VerificationMethod>](#VerificationMethod) +### iotaDocument.methods([scope]) ⇒ [Array.<VerificationMethod>](#VerificationMethod) Returns a list of all [VerificationMethod](#VerificationMethod) in the DID Document, whose verification relationship matches `scope`. @@ -2118,7 +2135,7 @@ If `scope` is not set, a list over the **embedded** methods is returned. | Param | Type | | --- | --- | -| scope | [MethodScope](#MethodScope) \| undefined | +| [scope] | [MethodScope](#MethodScope) \| undefined | @@ -2145,7 +2162,7 @@ Removes all references to the specified Verification Method. -### iotaDocument.resolveMethod(query, scope) ⇒ [VerificationMethod](#VerificationMethod) \| undefined +### iotaDocument.resolveMethod(query, [scope]) ⇒ [VerificationMethod](#VerificationMethod) \| undefined Returns a copy of the first verification method with an `id` property matching the provided `query` and the verification relationship specified by `scope`, if present. @@ -2155,7 +2172,7 @@ specified by `scope`, if present. | Param | Type | | --- | --- | | query | [DIDUrl](#DIDUrl) \| string | -| scope | [MethodScope](#MethodScope) \| undefined | +| [scope] | [MethodScope](#MethodScope) \| undefined | @@ -2170,7 +2187,7 @@ so it cannot be an embedded one. | Param | Type | | --- | --- | | didUrl | [DIDUrl](#DIDUrl) | -| relationship | number | +| relationship | [MethodRelationship](#MethodRelationship) | @@ -2182,11 +2199,11 @@ Detaches the given relationship from the given method, if the method exists. | Param | Type | | --- | --- | | didUrl | [DIDUrl](#DIDUrl) | -| relationship | number | +| relationship | [MethodRelationship](#MethodRelationship) | -### iotaDocument.verifyJws(jws, options, signatureVerifier, detachedPayload) ⇒ [DecodedJws](#DecodedJws) +### iotaDocument.verifyJws(jws, options, signatureVerifier, [detachedPayload]) ⇒ [DecodedJws](#DecodedJws) Decodes and verifies the provided JWS according to the passed `options` and `signatureVerifier`. If no `signatureVerifier` argument is provided a default verifier will be used that is (only) capable of verifying EdDSA signatures. @@ -2203,7 +2220,7 @@ take place. | jws | [Jws](#Jws) | | options | [JwsVerificationOptions](#JwsVerificationOptions) | | signatureVerifier | IJwsVerifier | -| detachedPayload | string \| undefined | +| [detachedPayload] | string \| undefined | @@ -2221,7 +2238,7 @@ Serializes the document for inclusion in an Alias Output's state metadata. | Param | Type | | --- | --- | -| encoding | number | +| encoding | [StateMetadataEncoding](#StateMetadataEncoding) | @@ -2274,14 +2291,14 @@ Returns a copy of the deactivated status of the DID document. **Kind**: instance method of [IotaDocument](#IotaDocument) -### iotaDocument.setMetadataDeactivated(deactivated) +### iotaDocument.setMetadataDeactivated([deactivated]) Sets the deactivated status of the DID document. **Kind**: instance method of [IotaDocument](#IotaDocument) | Param | Type | | --- | --- | -| deactivated | boolean \| undefined | +| [deactivated] | boolean \| undefined | @@ -2441,7 +2458,7 @@ See [RFC7515 section 3.1](https://www.rfc-editor.org/rfc/rfc7515#section-3.1). -### iotaDocument.createCredentialJwt(storage, fragment, credential, options, custom_claims) ⇒ [Promise.<Jwt>](#Jwt) +### iotaDocument.createCredentialJwt(storage, fragment, credential, options, [custom_claims]) ⇒ [Promise.<Jwt>](#Jwt) Produces a JWS where the payload is produced from the given `credential` in accordance with [VC Data Model v1.1](https://www.w3.org/TR/vc-data-model/#json-web-token). @@ -2459,7 +2476,7 @@ The `custom_claims` can be used to set additional claims on the resulting JWT. | fragment | string | | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | -| custom_claims | Record.<string, any> \| undefined | +| [custom_claims] | Record.<string, any> \| undefined | @@ -2628,7 +2645,7 @@ and resolution of DID documents in Alias Outputs. **Kind**: global class * [IotaIdentityClientExt](#IotaIdentityClientExt) - * [.newDidOutput(client, address, document, rentStructure)](#IotaIdentityClientExt.newDidOutput) ⇒ Promise.<AliasOutputBuilderParams> + * [.newDidOutput(client, address, document, [rentStructure])](#IotaIdentityClientExt.newDidOutput) ⇒ Promise.<AliasOutputBuilderParams> * [.updateDidOutput(client, document)](#IotaIdentityClientExt.updateDidOutput) ⇒ Promise.<AliasOutputBuilderParams> * [.deactivateDidOutput(client, did)](#IotaIdentityClientExt.deactivateDidOutput) ⇒ Promise.<AliasOutputBuilderParams> * [.resolveDid(client, did)](#IotaIdentityClientExt.resolveDid) ⇒ [Promise.<IotaDocument>](#IotaDocument) @@ -2636,7 +2653,7 @@ and resolution of DID documents in Alias Outputs. -### IotaIdentityClientExt.newDidOutput(client, address, document, rentStructure) ⇒ Promise.<AliasOutputBuilderParams> +### IotaIdentityClientExt.newDidOutput(client, address, document, [rentStructure]) ⇒ Promise.<AliasOutputBuilderParams> Create a DID with a new Alias Output containing the given `document`. The `address` will be set as the state controller and governor unlock conditions. @@ -2653,7 +2670,7 @@ NOTE: this does *not* publish the Alias Output. | client | IIotaIdentityClient | | address | Address | | document | [IotaDocument](#IotaDocument) | -| rentStructure | IRent \| undefined | +| [rentStructure] | IRent \| undefined | @@ -3302,7 +3319,7 @@ Deserializes an instance from a JSON object. **Kind**: global class * [JwsSignatureOptions](#JwsSignatureOptions) - * [new JwsSignatureOptions(options)](#new_JwsSignatureOptions_new) + * [new JwsSignatureOptions([options])](#new_JwsSignatureOptions_new) * _instance_ * [.setAttachJwk(value)](#JwsSignatureOptions+setAttachJwk) * [.setB64(value)](#JwsSignatureOptions+setB64) @@ -3320,11 +3337,11 @@ Deserializes an instance from a JSON object. -### new JwsSignatureOptions(options) +### new JwsSignatureOptions([options]) | Param | Type | | --- | --- | -| options | IJwsSignatureOptions \| undefined | +| [options] | IJwsSignatureOptions \| undefined | @@ -3454,7 +3471,7 @@ Deserializes an instance from a JSON object. **Kind**: global class * [JwsVerificationOptions](#JwsVerificationOptions) - * [new JwsVerificationOptions(options)](#new_JwsVerificationOptions_new) + * [new JwsVerificationOptions([options])](#new_JwsVerificationOptions_new) * _instance_ * [.setNonce(value)](#JwsVerificationOptions+setNonce) * [.setMethodScope(value)](#JwsVerificationOptions+setMethodScope) @@ -3466,13 +3483,13 @@ Deserializes an instance from a JSON object. -### new JwsVerificationOptions(options) +### new JwsVerificationOptions([options]) Creates a new [JwsVerificationOptions](#JwsVerificationOptions) from the given fields. | Param | Type | | --- | --- | -| options | IJwsVerificationOptions \| undefined | +| [options] | IJwsVerificationOptions \| undefined | @@ -3593,7 +3610,7 @@ Options to declare validation criteria when validating credentials. **Kind**: global class * [JwtCredentialValidationOptions](#JwtCredentialValidationOptions) - * [new JwtCredentialValidationOptions(options)](#new_JwtCredentialValidationOptions_new) + * [new JwtCredentialValidationOptions([options])](#new_JwtCredentialValidationOptions_new) * _instance_ * [.toJSON()](#JwtCredentialValidationOptions+toJSON) ⇒ any * [.clone()](#JwtCredentialValidationOptions+clone) ⇒ [JwtCredentialValidationOptions](#JwtCredentialValidationOptions) @@ -3602,11 +3619,11 @@ Options to declare validation criteria when validating credentials. -### new JwtCredentialValidationOptions(options) +### new JwtCredentialValidationOptions([options]) | Param | Type | | --- | --- | -| options | IJwtCredentialValidationOptions \| undefined | +| [options] | IJwtCredentialValidationOptions \| undefined | @@ -3648,6 +3665,7 @@ A type for decoding and validating [Credential](#Credential). * [.checkIssuedOnOrBefore(credential, timestamp)](#JwtCredentialValidator.checkIssuedOnOrBefore) * [.checkSubjectHolderRelationship(credential, holder, relationship)](#JwtCredentialValidator.checkSubjectHolderRelationship) * [.checkStatus(credential, trustedIssuers, statusCheck)](#JwtCredentialValidator.checkStatus) + * [.checkStatusWithStatusList2021(credential, status_list, status_check)](#JwtCredentialValidator.checkStatusWithStatusList2021) * [.extractIssuer(credential)](#JwtCredentialValidator.extractIssuer) ⇒ [CoreDID](#CoreDID) * [.extractIssuerFromJwt(credential)](#JwtCredentialValidator.extractIssuerFromJwt) ⇒ [CoreDID](#CoreDID) @@ -3698,7 +3716,7 @@ An error is returned whenever a validated condition is not satisfied. | credential_jwt | [Jwt](#Jwt) | | issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | | options | [JwtCredentialValidationOptions](#JwtCredentialValidationOptions) | -| fail_fast | number | +| fail_fast | [FailFast](#FailFast) | @@ -3764,7 +3782,7 @@ Validate that the relationship between the `holder` and the credential subjects | --- | --- | | credential | [Credential](#Credential) | | holder | string | -| relationship | number | +| relationship | [SubjectHolderRelationship](#SubjectHolderRelationship) | @@ -3779,7 +3797,20 @@ Only supports `RevocationBitmap2022`. | --- | --- | | credential | [Credential](#Credential) | | trustedIssuers | Array.<(CoreDocument\|IToCoreDocument)> | -| statusCheck | number | +| statusCheck | [StatusCheck](#StatusCheck) | + + + +### JwtCredentialValidator.checkStatusWithStatusList2021(credential, status_list, status_check) +Checks wheter the credential status has been revoked using `StatusList2021`. + +**Kind**: static method of [JwtCredentialValidator](#JwtCredentialValidator) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| status_list | [StatusList2021Credential](#StatusList2021Credential) | +| status_check | [StatusCheck](#StatusCheck) | @@ -3885,7 +3916,7 @@ Error will be thrown in case the validation fails. **Kind**: global class * [JwtPresentationOptions](#JwtPresentationOptions) - * [new JwtPresentationOptions(options)](#new_JwtPresentationOptions_new) + * [new JwtPresentationOptions([options])](#new_JwtPresentationOptions_new) * _instance_ * [.toJSON()](#JwtPresentationOptions+toJSON) ⇒ any * [.clone()](#JwtPresentationOptions+clone) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) @@ -3894,7 +3925,7 @@ Error will be thrown in case the validation fails. -### new JwtPresentationOptions(options) +### new JwtPresentationOptions([options]) Creates a new [JwtPresentationOptions](#JwtPresentationOptions) from the given fields. Throws an error if any of the options are invalid. @@ -3902,7 +3933,7 @@ Throws an error if any of the options are invalid. | Param | Type | | --- | --- | -| options | IJwtPresentationOptions \| undefined | +| [options] | IJwtPresentationOptions \| undefined | @@ -3935,7 +3966,7 @@ Options to declare validation criteria when validating presentation. **Kind**: global class * [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) - * [new JwtPresentationValidationOptions(options)](#new_JwtPresentationValidationOptions_new) + * [new JwtPresentationValidationOptions([options])](#new_JwtPresentationValidationOptions_new) * _instance_ * [.toJSON()](#JwtPresentationValidationOptions+toJSON) ⇒ any * [.clone()](#JwtPresentationValidationOptions+clone) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) @@ -3944,7 +3975,7 @@ Options to declare validation criteria when validating presentation. -### new JwtPresentationValidationOptions(options) +### new JwtPresentationValidationOptions([options]) Creates a new [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) from the given fields. Throws an error if any of the options are invalid. @@ -3952,7 +3983,7 @@ Throws an error if any of the options are invalid. | Param | Type | | --- | --- | -| options | IJwtPresentationValidationOptions \| undefined | +| [options] | IJwtPresentationValidationOptions \| undefined | @@ -4072,7 +4103,7 @@ Options to declare validation criteria when validating credentials. **Kind**: global class * [KeyBindingJWTValidationOptions](#KeyBindingJWTValidationOptions) - * [new KeyBindingJWTValidationOptions(options)](#new_KeyBindingJWTValidationOptions_new) + * [new KeyBindingJWTValidationOptions([options])](#new_KeyBindingJWTValidationOptions_new) * _instance_ * [.toJSON()](#KeyBindingJWTValidationOptions+toJSON) ⇒ any * [.clone()](#KeyBindingJWTValidationOptions+clone) ⇒ [KeyBindingJWTValidationOptions](#KeyBindingJWTValidationOptions) @@ -4081,11 +4112,11 @@ Options to declare validation criteria when validating credentials. -### new KeyBindingJWTValidationOptions(options) +### new KeyBindingJWTValidationOptions([options]) | Param | Type | | --- | --- | -| options | IKeyBindingJWTValidationOptions \| undefined | +| [options] | IKeyBindingJWTValidationOptions \| undefined | @@ -4118,7 +4149,7 @@ Claims set for key binding JWT. **Kind**: global class * [KeyBindingJwtClaims](#KeyBindingJwtClaims) - * [new KeyBindingJwtClaims(jwt, disclosures, nonce, aud, issued_at, custom_properties)](#new_KeyBindingJwtClaims_new) + * [new KeyBindingJwtClaims(jwt, disclosures, nonce, aud, [issued_at], [custom_properties])](#new_KeyBindingJwtClaims_new) * _instance_ * [.toString()](#KeyBindingJwtClaims+toString) ⇒ string * [.iat()](#KeyBindingJwtClaims+iat) ⇒ bigint @@ -4134,7 +4165,7 @@ Claims set for key binding JWT. -### new KeyBindingJwtClaims(jwt, disclosures, nonce, aud, issued_at, custom_properties) +### new KeyBindingJwtClaims(jwt, disclosures, nonce, aud, [issued_at], [custom_properties]) Creates a new [`KeyBindingJwtClaims`]. When `issued_at` is left as None, it will automatically default to the current time. @@ -4148,8 +4179,8 @@ When `issued_at` is set to `None` and the system returns time earlier than `Syst | disclosures | Array.<string> | | nonce | string | | aud | string | -| issued_at | [Timestamp](#Timestamp) \| undefined | -| custom_properties | Record.<string, any> \| undefined | +| [issued_at] | [Timestamp](#Timestamp) \| undefined | +| [custom_properties] | Record.<string, any> \| undefined | @@ -4586,7 +4617,7 @@ Deserializes an instance from a JSON object. * [.refreshService()](#Presentation+refreshService) ⇒ Array.<RefreshService> * [.termsOfUse()](#Presentation+termsOfUse) ⇒ Array.<Policy> * [.proof()](#Presentation+proof) ⇒ [Proof](#Proof) \| undefined - * [.setProof(proof)](#Presentation+setProof) + * [.setProof([proof])](#Presentation+setProof) * [.properties()](#Presentation+properties) ⇒ Map.<string, any> * [.toJSON()](#Presentation+toJSON) ⇒ any * [.clone()](#Presentation+clone) ⇒ [Presentation](#Presentation) @@ -4655,7 +4686,7 @@ Optional cryptographic proof, unrelated to JWT. **Kind**: instance method of [Presentation](#Presentation) -### presentation.setProof(proof) +### presentation.setProof([proof]) Sets the proof property of the [Presentation](#Presentation). Note that this proof is not related to JWT. @@ -4664,7 +4695,7 @@ Note that this proof is not related to JWT. | Param | Type | | --- | --- | -| proof | [Proof](#Proof) \| undefined | +| [proof] | [Proof](#Proof) \| undefined | @@ -4953,7 +4984,7 @@ Representation of an SD-JWT of the format **Kind**: global class * [SdJwt](#SdJwt) - * [new SdJwt(jwt, disclosures, key_binding_jwt)](#new_SdJwt_new) + * [new SdJwt(jwt, disclosures, [key_binding_jwt])](#new_SdJwt_new) * _instance_ * [.presentation()](#SdJwt+presentation) ⇒ string * [.toString()](#SdJwt+toString) ⇒ string @@ -4968,7 +4999,7 @@ Representation of an SD-JWT of the format -### new SdJwt(jwt, disclosures, key_binding_jwt) +### new SdJwt(jwt, disclosures, [key_binding_jwt]) Creates a new `SdJwt` from its components. @@ -4976,7 +5007,7 @@ Creates a new `SdJwt` from its components. | --- | --- | | jwt | string | | disclosures | Array.<string> | -| key_binding_jwt | string \| undefined | +| [key_binding_jwt] | string \| undefined | @@ -5108,7 +5139,7 @@ An error is returned whenever a validated condition is not satisfied. | sd_jwt | [SdJwt](#SdJwt) | | issuer | [CoreDocument](#CoreDocument) \| IToCoreDocument | | options | [JwtCredentialValidationOptions](#JwtCredentialValidationOptions) | -| fail_fast | number | +| fail_fast | [FailFast](#FailFast) | @@ -5201,8 +5232,8 @@ Note: digests are created using the sha-256 algorithm. * [SdObjectEncoder](#SdObjectEncoder) * [new SdObjectEncoder(object)](#new_SdObjectEncoder_new) - * [.conceal(path, salt)](#SdObjectEncoder+conceal) ⇒ [Disclosure](#Disclosure) - * [.concealArrayEntry(path, element_index, salt)](#SdObjectEncoder+concealArrayEntry) ⇒ [Disclosure](#Disclosure) + * [.conceal(path, [salt])](#SdObjectEncoder+conceal) ⇒ [Disclosure](#Disclosure) + * [.concealArrayEntry(path, element_index, [salt])](#SdObjectEncoder+concealArrayEntry) ⇒ [Disclosure](#Disclosure) * [.addSdAlgProperty()](#SdObjectEncoder+addSdAlgProperty) * [.encodeToString()](#SdObjectEncoder+encodeToString) ⇒ string * [.toString()](#SdObjectEncoder+toString) ⇒ string @@ -5222,7 +5253,7 @@ Creates a new `SdObjectEncoder` with `sha-256` hash function. -### sdObjectEncoder.conceal(path, salt) ⇒ [Disclosure](#Disclosure) +### sdObjectEncoder.conceal(path, [salt]) ⇒ [Disclosure](#Disclosure) Substitutes a value with the digest of its disclosure. If no salt is provided, the disclosure will be created with a random salt value. @@ -5241,11 +5272,11 @@ Use `concealArrayEntry` for values in arrays. | Param | Type | | --- | --- | | path | Array.<string> | -| salt | string \| undefined | +| [salt] | string \| undefined | -### sdObjectEncoder.concealArrayEntry(path, element_index, salt) ⇒ [Disclosure](#Disclosure) +### sdObjectEncoder.concealArrayEntry(path, element_index, [salt]) ⇒ [Disclosure](#Disclosure) Substitutes a value within an array with the digest of its disclosure. If no salt is provided, the disclosure will be created with random salt value. @@ -5263,7 +5294,7 @@ the index of the element to be concealed (index start at 0). | --- | --- | | path | Array.<string> | | element_index | number | -| salt | string \| undefined | +| [salt] | string \| undefined | @@ -5383,6 +5414,365 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## StatusList2021 +StatusList2021 data structure as described in [W3C's VC status list 2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/). + +**Kind**: global class + +* [StatusList2021](#StatusList2021) + * [new StatusList2021([size])](#new_StatusList2021_new) + * _instance_ + * [.clone()](#StatusList2021+clone) ⇒ [StatusList2021](#StatusList2021) + * [.len()](#StatusList2021+len) ⇒ number + * [.get(index)](#StatusList2021+get) ⇒ boolean + * [.set(index, value)](#StatusList2021+set) + * [.intoEncodedStr()](#StatusList2021+intoEncodedStr) ⇒ string + * _static_ + * [.fromEncodedStr(s)](#StatusList2021.fromEncodedStr) ⇒ [StatusList2021](#StatusList2021) + + + +### new StatusList2021([size]) +Creates a new [StatusList2021](#StatusList2021) of `size` entries. + + +| Param | Type | +| --- | --- | +| [size] | number \| undefined | + + + +### statusList2021.clone() ⇒ [StatusList2021](#StatusList2021) +Deep clones the object. + +**Kind**: instance method of [StatusList2021](#StatusList2021) + + +### statusList2021.len() ⇒ number +Returns the number of entries in this [StatusList2021](#StatusList2021). + +**Kind**: instance method of [StatusList2021](#StatusList2021) + + +### statusList2021.get(index) ⇒ boolean +Returns whether the entry at `index` is set. + +**Kind**: instance method of [StatusList2021](#StatusList2021) + +| Param | Type | +| --- | --- | +| index | number | + + + +### statusList2021.set(index, value) +Sets the value of the `index`-th entry. + +**Kind**: instance method of [StatusList2021](#StatusList2021) + +| Param | Type | +| --- | --- | +| index | number | +| value | boolean | + + + +### statusList2021.intoEncodedStr() ⇒ string +Encodes this [StatusList2021](#StatusList2021) into its compressed +base64 string representation. + +**Kind**: instance method of [StatusList2021](#StatusList2021) + + +### StatusList2021.fromEncodedStr(s) ⇒ [StatusList2021](#StatusList2021) +Attempts to decode a [StatusList2021](#StatusList2021) from a string. + +**Kind**: static method of [StatusList2021](#StatusList2021) + +| Param | Type | +| --- | --- | +| s | string | + + + +## StatusList2021Credential +A parsed [StatusList2021Credential](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021credential). + +**Kind**: global class + +* [StatusList2021Credential](#StatusList2021Credential) + * [new StatusList2021Credential(credential)](#new_StatusList2021Credential_new) + * _instance_ + * [.id()](#StatusList2021Credential+id) ⇒ string + * [.setCredentialStatus(credential, index, value)](#StatusList2021Credential+setCredentialStatus) ⇒ [StatusList2021Entry](#StatusList2021Entry) + * [.purpose()](#StatusList2021Credential+purpose) ⇒ [StatusPurpose](#StatusPurpose) + * [.entry(index)](#StatusList2021Credential+entry) ⇒ [CredentialStatus](#CredentialStatus) + * [.clone()](#StatusList2021Credential+clone) ⇒ [StatusList2021Credential](#StatusList2021Credential) + * [.toJSON()](#StatusList2021Credential+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#StatusList2021Credential.fromJSON) ⇒ [StatusList2021Credential](#StatusList2021Credential) + + + +### new StatusList2021Credential(credential) +Creates a new [StatusList2021Credential](#StatusList2021Credential). + + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | + + + +### statusList2021Credential.id() ⇒ string +**Kind**: instance method of [StatusList2021Credential](#StatusList2021Credential) + + +### statusList2021Credential.setCredentialStatus(credential, index, value) ⇒ [StatusList2021Entry](#StatusList2021Entry) +Sets the given credential's status using the `index`-th entry of this status list. +Returns the created `credentialStatus`. + +**Kind**: instance method of [StatusList2021Credential](#StatusList2021Credential) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | +| index | number | +| value | boolean | + + + +### statusList2021Credential.purpose() ⇒ [StatusPurpose](#StatusPurpose) +Returns the [StatusPurpose](#StatusPurpose) of this [StatusList2021Credential](#StatusList2021Credential). + +**Kind**: instance method of [StatusList2021Credential](#StatusList2021Credential) + + +### statusList2021Credential.entry(index) ⇒ [CredentialStatus](#CredentialStatus) +Returns the state of the `index`-th entry, if any. + +**Kind**: instance method of [StatusList2021Credential](#StatusList2021Credential) + +| Param | Type | +| --- | --- | +| index | number | + + + +### statusList2021Credential.clone() ⇒ [StatusList2021Credential](#StatusList2021Credential) +**Kind**: instance method of [StatusList2021Credential](#StatusList2021Credential) + + +### statusList2021Credential.toJSON() ⇒ any +**Kind**: instance method of [StatusList2021Credential](#StatusList2021Credential) + + +### StatusList2021Credential.fromJSON(json) ⇒ [StatusList2021Credential](#StatusList2021Credential) +**Kind**: static method of [StatusList2021Credential](#StatusList2021Credential) + +| Param | Type | +| --- | --- | +| json | any | + + + +## StatusList2021CredentialBuilder +Builder type to construct valid [StatusList2021Credential](#StatusList2021Credential) istances. + +**Kind**: global class + +* [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [new StatusList2021CredentialBuilder([status_list])](#new_StatusList2021CredentialBuilder_new) + * [.purpose(purpose)](#StatusList2021CredentialBuilder+purpose) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [.subjectId(id)](#StatusList2021CredentialBuilder+subjectId) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [.expirationDate(time)](#StatusList2021CredentialBuilder+expirationDate) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [.issuer(issuer)](#StatusList2021CredentialBuilder+issuer) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [.context(context)](#StatusList2021CredentialBuilder+context) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [.type(t)](#StatusList2021CredentialBuilder+type) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [.proof(proof)](#StatusList2021CredentialBuilder+proof) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + * [.build()](#StatusList2021CredentialBuilder+build) ⇒ [StatusList2021Credential](#StatusList2021Credential) + + + +### new StatusList2021CredentialBuilder([status_list]) +Creates a new [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder). + + +| Param | Type | +| --- | --- | +| [status_list] | [StatusList2021](#StatusList2021) \| undefined | + + + +### statusList2021CredentialBuilder.purpose(purpose) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) +Sets the purpose of the [StatusList2021Credential](#StatusList2021Credential) that is being created. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + +| Param | Type | +| --- | --- | +| purpose | [StatusPurpose](#StatusPurpose) | + + + +### statusList2021CredentialBuilder.subjectId(id) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) +Sets `credentialSubject.id`. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + +| Param | Type | +| --- | --- | +| id | string | + + + +### statusList2021CredentialBuilder.expirationDate(time) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) +Sets the expiration date of the credential. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + +| Param | Type | +| --- | --- | +| time | [Timestamp](#Timestamp) | + + + +### statusList2021CredentialBuilder.issuer(issuer) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) +Sets the issuer of the credential. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + +| Param | Type | +| --- | --- | +| issuer | string | + + + +### statusList2021CredentialBuilder.context(context) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) +Sets the context of the credential. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + +| Param | Type | +| --- | --- | +| context | string | + + + +### statusList2021CredentialBuilder.type(t) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) +Adds a credential type. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + +| Param | Type | +| --- | --- | +| t | string | + + + +### statusList2021CredentialBuilder.proof(proof) ⇒ [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) +Adds a credential's proof. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + +| Param | Type | +| --- | --- | +| proof | [Proof](#Proof) | + + + +### statusList2021CredentialBuilder.build() ⇒ [StatusList2021Credential](#StatusList2021Credential) +Attempts to build a valid [StatusList2021Credential](#StatusList2021Credential) with the previously provided data. + +**Kind**: instance method of [StatusList2021CredentialBuilder](#StatusList2021CredentialBuilder) + + +## StatusList2021Entry +[StatusList2021Entry](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021entry) implementation. + +**Kind**: global class + +* [StatusList2021Entry](#StatusList2021Entry) + * [new StatusList2021Entry(status_list, purpose, index, [id])](#new_StatusList2021Entry_new) + * _instance_ + * [.id()](#StatusList2021Entry+id) ⇒ string + * [.purpose()](#StatusList2021Entry+purpose) ⇒ [StatusPurpose](#StatusPurpose) + * [.index()](#StatusList2021Entry+index) ⇒ number + * [.status_list_credential()](#StatusList2021Entry+status_list_credential) ⇒ string + * [.toStatus()](#StatusList2021Entry+toStatus) ⇒ Status + * [.clone()](#StatusList2021Entry+clone) ⇒ [StatusList2021Entry](#StatusList2021Entry) + * [.toJSON()](#StatusList2021Entry+toJSON) ⇒ any + * _static_ + * [.fromJSON(json)](#StatusList2021Entry.fromJSON) ⇒ [StatusList2021Entry](#StatusList2021Entry) + + + +### new StatusList2021Entry(status_list, purpose, index, [id]) +Creates a new [StatusList2021Entry](#StatusList2021Entry). + + +| Param | Type | +| --- | --- | +| status_list | string | +| purpose | [StatusPurpose](#StatusPurpose) | +| index | number | +| [id] | string \| undefined | + + + +### statusList2021Entry.id() ⇒ string +Returns this `credentialStatus`'s `id`. + +**Kind**: instance method of [StatusList2021Entry](#StatusList2021Entry) + + +### statusList2021Entry.purpose() ⇒ [StatusPurpose](#StatusPurpose) +Returns the purpose of this entry. + +**Kind**: instance method of [StatusList2021Entry](#StatusList2021Entry) + + +### statusList2021Entry.index() ⇒ number +Returns the index of this entry. + +**Kind**: instance method of [StatusList2021Entry](#StatusList2021Entry) + + +### statusList2021Entry.status\_list\_credential() ⇒ string +Returns the referenced [StatusList2021Credential](#StatusList2021Credential)'s url. + +**Kind**: instance method of [StatusList2021Entry](#StatusList2021Entry) + + +### statusList2021Entry.toStatus() ⇒ Status +Downcasts [this](this) to [Status](Status) + +**Kind**: instance method of [StatusList2021Entry](#StatusList2021Entry) + + +### statusList2021Entry.clone() ⇒ [StatusList2021Entry](#StatusList2021Entry) +Deep clones the object. + +**Kind**: instance method of [StatusList2021Entry](#StatusList2021Entry) + + +### statusList2021Entry.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [StatusList2021Entry](#StatusList2021Entry) + + +### StatusList2021Entry.fromJSON(json) ⇒ [StatusList2021Entry](#StatusList2021Entry) +Deserializes an instance from a JSON object. + +**Kind**: static method of [StatusList2021Entry](#StatusList2021Entry) + +| Param | Type | +| --- | --- | +| json | any | + ## Storage @@ -5585,7 +5975,7 @@ A DID Document Verification Method. * [.toJSON()](#VerificationMethod+toJSON) ⇒ any * [.clone()](#VerificationMethod+clone) ⇒ [VerificationMethod](#VerificationMethod) * _static_ - * [.newFromJwk(did, key, fragment)](#VerificationMethod.newFromJwk) ⇒ [VerificationMethod](#VerificationMethod) + * [.newFromJwk(did, key, [fragment])](#VerificationMethod.newFromJwk) ⇒ [VerificationMethod](#VerificationMethod) * [.fromJSON(json)](#VerificationMethod.fromJSON) ⇒ [VerificationMethod](#VerificationMethod) @@ -5693,7 +6083,7 @@ Deep clones the object. **Kind**: instance method of [VerificationMethod](#VerificationMethod) -### VerificationMethod.newFromJwk(did, key, fragment) ⇒ [VerificationMethod](#VerificationMethod) +### VerificationMethod.newFromJwk(did, key, [fragment]) ⇒ [VerificationMethod](#VerificationMethod) Creates a new [VerificationMethod](#VerificationMethod) from the given `did` and [Jwk](#Jwk). If `fragment` is not given the `kid` value of the given `key` will be used, if present, otherwise an error is returned. @@ -5710,7 +6100,7 @@ done automatically if `None` is passed in as the fragment. | --- | --- | | did | [CoreDID](#CoreDID) \| IToCoreDID | | key | [Jwk](#Jwk) | -| fragment | string \| undefined | +| [fragment] | string \| undefined | @@ -5723,35 +6113,14 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - - -## StatusCheck -Controls validation behaviour when checking whether or not a credential has been revoked by its -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). - -**Kind**: global variable - - -## Strict -Validate the status if supported, reject any unsupported -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. - -Only `RevocationBitmap2022` is currently supported. - -This is the default. - -**Kind**: global variable - - -## SkipUnsupported -Validate the status if supported, skip any unsupported -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. + +## StateMetadataEncoding **Kind**: global variable - + -## SkipAll -Skip all status checks. +## StatusPurpose +Purpose of a [StatusList2021](#StatusList2021). **Kind**: global variable @@ -5799,9 +6168,40 @@ Return all errors that occur during validation. Return after the first error occurs. **Kind**: global variable - + + +## StatusCheck +Controls validation behaviour when checking whether or not a credential has been revoked by its +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). -## StateMetadataEncoding +**Kind**: global variable + + +## Strict +Validate the status if supported, reject any unsupported +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. + +Only `RevocationBitmap2022` is currently supported. + +This is the default. + +**Kind**: global variable + + +## SkipUnsupported +Validate the status if supported, skip any unsupported +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. + +**Kind**: global variable + + +## SkipAll +Skip all status checks. + +**Kind**: global variable + + +## CredentialStatus **Kind**: global variable @@ -5829,12 +6229,6 @@ prior to calling the function. | decodedSignature | Uint8Array | | publicKey | [Jwk](#Jwk) | - - -## start() -Initializes the console error panic hook for better error messages - -**Kind**: global function ## encodeB64(data) ⇒ string @@ -5857,3 +6251,9 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | + + +## start() +Initializes the console error panic hook for better error messages + +**Kind**: global function diff --git a/bindings/wasm/examples/README.md b/bindings/wasm/examples/README.md index 98cec6b1f7..74914481b9 100644 --- a/bindings/wasm/examples/README.md +++ b/bindings/wasm/examples/README.md @@ -60,6 +60,7 @@ The following advanced examples are available: | [4_custom_resolution](src/1_advanced/4_custom_resolution.ts) | Demonstrates how to set up a resolver using custom handlers. | | [5_domain_linkage](src/1_advanced/5_domain_linkage.ts) | Demonstrates how to link a domain and a DID and verify the linkage. | | [6_sd_jwt](src/1_advanced/6_sd_jwt.ts) | Demonstrates how to create a selective disclosure verifiable credential | +| [7_domain_linkage](src/1_advanced/7_status_list_2021.ts) | Demonstrates how to revoke a credential using `StatusList2021`. | ## Browser diff --git a/bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts b/bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts new file mode 100644 index 0000000000..4e70d8fa19 --- /dev/null +++ b/bindings/wasm/examples/src/1_advanced/7_status_list_2021.ts @@ -0,0 +1,169 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { + Credential, + EdDSAJwsVerifier, + FailFast, + JwkMemStore, + JwsSignatureOptions, + JwtCredentialValidationOptions, + JwtCredentialValidator, + KeyIdMemStore, + StatusCheck, + StatusList2021, + StatusList2021Credential, + StatusList2021CredentialBuilder, + StatusList2021Entry, + StatusPurpose, + Storage, +} from "@iota/identity-wasm/node"; +import { Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; +import { API_ENDPOINT, createDid } from "../util"; + +export async function statusList2021() { + // =========================================================================== + // Create a Verifiable Credential. + // =========================================================================== + + const client = new Client({ + primaryNode: API_ENDPOINT, + localPow: true, + }); + + // Generate a random mnemonic for the issuer. + const issuerSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + + // Create an identity for the issuer with one verification method `key-1`. + const issuerStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: issuerDocument, fragment: issuerFragment } = await createDid( + client, + issuerSecretManager, + issuerStorage, + ); + + // Generate a random mnemonic for Alice. + const aliceSecretManager: MnemonicSecretManager = { + mnemonic: Utils.generateMnemonic(), + }; + + // Create an identity for the holder, in this case also the subject. + const aliceStorage: Storage = new Storage( + new JwkMemStore(), + new KeyIdMemStore(), + ); + let { document: aliceDocument } = await createDid( + client, + aliceSecretManager, + aliceStorage, + ); + + // Create a new empty status list. No credentials have been revoked yet. + const statusList = new StatusList2021(); + + // Create a status list credential so that the status list can be stored anywhere. + // The issuer makes this credential available on `http://example.com/credential/status`. + // For the purposes of this example, the credential will be used directly without fetching. + const statusListCredential = new StatusList2021CredentialBuilder(statusList) + .purpose(StatusPurpose.Revocation) + .subjectId("http://example.com/credential/status") + .issuer(issuerDocument.id().toString()) + .build(); + const statusListCredentialJSON = statusListCredential.toJSON(); + console.log("Status list credential > " + statusListCredential); + + // Create a credential subject indicating the degree earned by Alice, linked to their DID. + const subject = { + id: aliceDocument.id(), + name: "Alice", + degreeName: "Bachelor of Science and Arts", + degreeType: "BachelorDegree", + GPA: "4.0", + }; + + // Create an unsigned `UniversityDegree` credential for Alice. + // The issuer also chooses a unique `StatusList2021` index to be able to revoke it later. + const CREDENTIAL_INDEX = 5; + const status = new StatusList2021Entry(statusListCredential.id(), statusListCredential.purpose(), CREDENTIAL_INDEX) + .toStatus(); + const credential = new Credential({ + id: "https://example.edu/credentials/3732", + type: "UniversityDegreeCredential", + credentialStatus: status, + issuer: issuerDocument.id(), + credentialSubject: subject, + }); + + // Create signed JWT credential. + const credentialJwt = await issuerDocument.createCredentialJwt( + issuerStorage, + issuerFragment, + credential, + new JwsSignatureOptions(), + ); + console.log(`Credential JWT > ${credentialJwt.toString()}`); + + // Validate the credential using the issuer's DID Document. + const validationOptions = new JwtCredentialValidationOptions({ status: StatusCheck.SkipUnsupported }); + // The validator has no way of retrieving the status list to check for the + // revocation of the credential. Let's skip that pass and perform the operation manually. + let jwtCredentialValidator = new JwtCredentialValidator(new EdDSAJwsVerifier()); + + try { + jwtCredentialValidator.validate( + credentialJwt, + issuerDocument, + validationOptions, + FailFast.FirstError, + ); + // Check manually for revocation + JwtCredentialValidator.checkStatusWithStatusList2021( + credential, + statusListCredential, + StatusCheck.Strict, + ); + } catch (e) { + // This line shouldn't be called as the credential is valid and unrevoked + console.log("Something went wrong: " + e); + } + + // =========================================================================== + // Revocation of the Verifiable Credential. + // =========================================================================== + + // At a later time, the issuer university found out that Alice cheated in her final exam. + // The issuer will revoke Alice's credential. + + // The issuer retrieves the status list credential. + const refetchedStatusListCredential = new StatusList2021Credential(new Credential(statusListCredentialJSON as any)); + + // Update the status list credential. + // This revokes the credential's unique index. + refetchedStatusListCredential.setCredentialStatus(credential, CREDENTIAL_INDEX, true); + + // Credential verification now fails. + try { + jwtCredentialValidator.validate( + credentialJwt, + issuerDocument, + validationOptions, + FailFast.FirstError, + ); + /// Since the credential has been revoked, this validation step will throw an error. + JwtCredentialValidator.checkStatusWithStatusList2021( + credential, + refetchedStatusListCredential, + StatusCheck.Strict, + ); + // In case the revocation failed for some reason we will hit this point + console.log("Revocation Failed!"); + } catch (e) { + /// The credential has been revoked. + console.log("The credential has been successfully revoked."); + } +} diff --git a/bindings/wasm/examples/src/main.ts b/bindings/wasm/examples/src/main.ts index bf71211e3e..145980e649 100644 --- a/bindings/wasm/examples/src/main.ts +++ b/bindings/wasm/examples/src/main.ts @@ -16,6 +16,7 @@ import { didIssuesTokens } from "./1_advanced/3_did_issues_tokens"; import { customResolution } from "./1_advanced/4_custom_resolution"; import { domainLinkage } from "./1_advanced/5_domain_linkage"; import { sdJwt } from "./1_advanced/6_sd_jwt"; +import { statusList2021 } from "./1_advanced/7_status_list_2021"; async function main() { // Extract example name. @@ -55,6 +56,8 @@ async function main() { return await domainLinkage(); case "6_sd_jwt": return await sdJwt(); + case "7_status_list_2021": + return await statusList2021(); default: throw "Unknown example name: '" + argument + "'"; } diff --git a/bindings/wasm/examples/src/tests/7_status_list_2021.ts b/bindings/wasm/examples/src/tests/7_status_list_2021.ts new file mode 100644 index 0000000000..2698e210c4 --- /dev/null +++ b/bindings/wasm/examples/src/tests/7_status_list_2021.ts @@ -0,0 +1,8 @@ +import { statusList2021 } from "../1_advanced/7_status_list_2021"; + +// Only verifies that no uncaught exceptions are thrown, including syntax errors etc. +describe("Test node examples", function() { + it("StatusList2021", async () => { + await statusList2021(); + }); +}); diff --git a/bindings/wasm/src/credential/credential.rs b/bindings/wasm/src/credential/credential.rs index 98158bd97e..69ef827834 100644 --- a/bindings/wasm/src/credential/credential.rs +++ b/bindings/wasm/src/credential/credential.rs @@ -30,6 +30,7 @@ use crate::error::Result; use crate::error::WasmResult; #[wasm_bindgen(js_name = Credential, inspectable)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct WasmCredential(pub(crate) Credential); #[wasm_bindgen(js_class = Credential)] diff --git a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs index 74682d3e0d..9434a6d521 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/jwt_credential_validator.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use identity_iota::core::Object; @@ -13,6 +13,7 @@ use crate::common::ImportedDocumentLock; use crate::common::ImportedDocumentReadGuard; use crate::common::WasmTimestamp; use crate::credential::options::WasmStatusCheck; +use crate::credential::revocation::status_list_2021::WasmStatusList2021Credential; use crate::credential::WasmCredential; use crate::credential::WasmDecodedJwtCredential; use crate::credential::WasmFailFast; @@ -170,6 +171,21 @@ impl WasmJwtCredentialValidator { JwtCredentialValidatorUtils::check_status(&credential.0, &trusted_issuers, status_check).wasm_result() } + /// Checks wheter the credential status has been revoked using `StatusList2021`. + #[wasm_bindgen(js_name = checkStatusWithStatusList2021)] + pub fn check_status_with_status_list_2021( + credential: &WasmCredential, + status_list: &WasmStatusList2021Credential, + status_check: WasmStatusCheck, + ) -> Result<()> { + JwtCredentialValidatorUtils::check_status_with_status_list_2021( + &credential.0, + &status_list.inner, + status_check.into(), + ) + .wasm_result() + } + /// Utility for extracting the issuer field of a {@link Credential} as a DID. /// /// ### Errors diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 755ce11afe..832eac1cd4 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -14,6 +14,7 @@ pub use self::options::WasmFailFast; pub use self::options::WasmSubjectHolderRelationship; pub use self::presentation::*; pub use self::proof::WasmProof; +pub use self::revocation::*; pub use self::types::*; mod credential; @@ -29,4 +30,5 @@ mod linked_domain_service; mod options; mod presentation; mod proof; +mod revocation; mod types; diff --git a/bindings/wasm/src/credential/revocation/mod.rs b/bindings/wasm/src/credential/revocation/mod.rs new file mode 100644 index 0000000000..7ad04980b4 --- /dev/null +++ b/bindings/wasm/src/credential/revocation/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub mod status_list_2021; diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/credential.rs b/bindings/wasm/src/credential/revocation/status_list_2021/credential.rs new file mode 100644 index 0000000000..d440dc8814 --- /dev/null +++ b/bindings/wasm/src/credential/revocation/status_list_2021/credential.rs @@ -0,0 +1,244 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; + +use identity_iota::core::Context; +use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::CredentialStatus; +use identity_iota::credential::status_list_2021::StatusList2021Credential; +use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder; +use identity_iota::credential::status_list_2021::StatusPurpose; +use identity_iota::credential::Issuer; +use wasm_bindgen::prelude::*; + +use crate::common::WasmTimestamp; +use crate::credential::WasmCredential; +use crate::credential::WasmProof; +use crate::error::Result; +use crate::error::WasmResult; + +use super::WasmStatusList2021; +use super::WasmStatusList2021Entry; + +#[wasm_bindgen(js_name = CredentialStatus)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum WasmCredentialStatus { + Revoked = 0, + Suspended = 1, + Valid = 2, +} + +impl From for WasmCredentialStatus { + fn from(value: CredentialStatus) -> Self { + match value { + CredentialStatus::Revoked => Self::Revoked, + CredentialStatus::Suspended => Self::Suspended, + CredentialStatus::Valid => Self::Valid, + } + } +} + +impl From for CredentialStatus { + fn from(value: WasmCredentialStatus) -> Self { + match value { + WasmCredentialStatus::Revoked => Self::Revoked, + WasmCredentialStatus::Suspended => Self::Suspended, + WasmCredentialStatus::Valid => Self::Valid, + } + } +} + +/// Purpose of a {@link StatusList2021}. +#[wasm_bindgen(js_name = StatusPurpose)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub enum WasmStatusPurpose { + Revocation = 0, + Suspension = 1, +} + +impl From for WasmStatusPurpose { + fn from(value: StatusPurpose) -> Self { + match value { + StatusPurpose::Revocation => Self::Revocation, + StatusPurpose::Suspension => Self::Suspension, + } + } +} + +impl From for StatusPurpose { + fn from(value: WasmStatusPurpose) -> Self { + match value { + WasmStatusPurpose::Revocation => Self::Revocation, + WasmStatusPurpose::Suspension => Self::Suspension, + } + } +} + +/// A parsed [StatusList2021Credential](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021credential). +#[wasm_bindgen(js_name = "StatusList2021Credential", inspectable)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(from = "StatusList2021Credential", into = "StatusList2021Credential")] +pub struct WasmStatusList2021Credential { + pub(crate) inner: StatusList2021Credential, + wasm_credential: WasmCredential, +} + +impl Deref for WasmStatusList2021Credential { + type Target = WasmCredential; + fn deref(&self) -> &Self::Target { + &self.wasm_credential + } +} + +impl From for WasmStatusList2021Credential { + fn from(value: StatusList2021Credential) -> Self { + Self { + wasm_credential: WasmCredential(value.clone().into_inner()), + inner: value, + } + } +} + +impl From for StatusList2021Credential { + fn from(value: WasmStatusList2021Credential) -> Self { + value.inner + } +} + +#[wasm_bindgen(js_class = StatusList2021Credential)] +impl WasmStatusList2021Credential { + /// Creates a new {@link StatusList2021Credential}. + #[wasm_bindgen(constructor)] + pub fn new(credential: WasmCredential) -> Result { + StatusList2021Credential::try_from(credential.0) + .map(Into::into) + .wasm_result() + } + + #[wasm_bindgen] + pub fn id(&self) -> String { + self.inner.id.as_deref().map(ToString::to_string).unwrap() + } + + /// Sets the given credential's status using the `index`-th entry of this status list. + /// Returns the created `credentialStatus`. + #[wasm_bindgen(js_name = "setCredentialStatus")] + pub fn set_credential_status( + &mut self, + credential: &mut WasmCredential, + index: usize, + revoked_or_suspended: bool, + ) -> Result { + let entry = self + .inner + .set_credential_status(&mut credential.0, index, revoked_or_suspended) + .wasm_result()?; + self.wasm_credential = WasmCredential(self.inner.clone().into_inner()); + + Ok(WasmStatusList2021Entry(entry)) + } + + /// Returns the {@link StatusPurpose} of this {@link StatusList2021Credential}. + #[wasm_bindgen] + pub fn purpose(&self) -> WasmStatusPurpose { + self.inner.purpose().into() + } + + /// Returns the state of the `index`-th entry, if any. + #[wasm_bindgen] + pub fn entry(&self, index: usize) -> Result { + self.inner.entry(index).map(WasmCredentialStatus::from).wasm_result() + } + + #[wasm_bindgen(js_name = "clone")] + pub fn wasm_clone(&self) -> WasmStatusList2021Credential { + self.clone() + } + + #[wasm_bindgen(js_name = "fromJSON")] + pub fn from_json(json: JsValue) -> Result { + json.into_serde::().wasm_result() + } + + #[wasm_bindgen(js_name = "toJSON")] + pub fn to_json(&self) -> Result { + JsValue::from_serde(self).wasm_result() + } +} + +/// Builder type to construct valid {@link StatusList2021Credential} istances. +#[wasm_bindgen(js_name = StatusList2021CredentialBuilder)] +pub struct WasmStatusList2021CredentialBuilder(StatusList2021CredentialBuilder); + +#[wasm_bindgen(js_class = StatusList2021CredentialBuilder)] +impl WasmStatusList2021CredentialBuilder { + /// Creates a new {@link StatusList2021CredentialBuilder}. + #[wasm_bindgen(constructor)] + pub fn new(status_list: Option) -> WasmStatusList2021CredentialBuilder { + Self(StatusList2021CredentialBuilder::new(status_list.unwrap_or_default().0)) + } + + /// Sets the purpose of the {@link StatusList2021Credential} that is being created. + #[wasm_bindgen] + pub fn purpose(mut self, purpose: WasmStatusPurpose) -> WasmStatusList2021CredentialBuilder { + self.0 = self.0.purpose(purpose.into()); + self + } + + /// Sets `credentialSubject.id`. + #[wasm_bindgen(js_name = "subjectId")] + pub fn subject_id(mut self, id: String) -> Result { + let id = Url::parse(id).wasm_result()?; + self.0 = self.0.subject_id(id); + + Ok(self) + } + + /// Sets the expiration date of the credential. + #[wasm_bindgen(js_name = "expirationDate")] + pub fn expiration_date(mut self, time: WasmTimestamp) -> WasmStatusList2021CredentialBuilder { + self.0 = self.0.expiration_date(time.0); + self + } + + /// Sets the issuer of the credential. + #[wasm_bindgen] + pub fn issuer(mut self, issuer: String) -> Result { + let issuer = Url::parse(issuer).wasm_result()?; + self.0 = self.0.issuer(Issuer::Url(issuer)); + + Ok(self) + } + + /// Sets the context of the credential. + #[wasm_bindgen] + pub fn context(mut self, context: String) -> Result { + let ctx = Context::Url(Url::parse(context).wasm_result()?); + self.0 = self.0.context(ctx); + + Ok(self) + } + + /// Adds a credential type. + #[wasm_bindgen(js_name = "type")] + pub fn r#type(mut self, t: String) -> WasmStatusList2021CredentialBuilder { + self.0 = self.0.add_type(t); + self + } + + /// Adds a credential's proof. + #[wasm_bindgen] + pub fn proof(mut self, proof: WasmProof) -> WasmStatusList2021CredentialBuilder { + self.0 = self.0.proof(proof.0); + self + } + + /// Attempts to build a valid {@link StatusList2021Credential} with the previously provided data. + #[wasm_bindgen] + pub fn build(self) -> Result { + let credential = self.0.build().wasm_result()?; + + WasmStatusList2021Credential::new(WasmCredential(credential.into_inner())) + } +} diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/entry.rs b/bindings/wasm/src/credential/revocation/status_list_2021/entry.rs new file mode 100644 index 0000000000..85ecb2eafe --- /dev/null +++ b/bindings/wasm/src/credential/revocation/status_list_2021/entry.rs @@ -0,0 +1,72 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::WasmStatusPurpose; +use crate::credential::types::WasmStatus; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::StatusList2021Entry; +use identity_iota::credential::Status; +use wasm_bindgen::prelude::*; + +/// [StatusList2021Entry](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021entry) implementation. +#[wasm_bindgen(js_name = StatusList2021Entry, inspectable)] +pub struct WasmStatusList2021Entry(pub(crate) StatusList2021Entry); + +#[wasm_bindgen(js_class = StatusList2021Entry)] +impl WasmStatusList2021Entry { + /// Creates a new {@link StatusList2021Entry}. + #[wasm_bindgen(constructor)] + pub fn new( + status_list: &str, + purpose: WasmStatusPurpose, + index: usize, + id: Option, + ) -> Result { + let status_list = Url::parse(status_list).map_err(|e| JsError::new(&e.to_string()))?; + let id = if let Some(id) = id { + Some(Url::parse(id).map_err(|e| JsError::new(&e.to_string()))?) + } else { + None + }; + Ok(Self(StatusList2021Entry::new(status_list, purpose.into(), index, id))) + } + + /// Returns this `credentialStatus`'s `id`. + #[wasm_bindgen] + pub fn id(&self) -> String { + self.0.id().to_string() + } + + /// Returns the purpose of this entry. + #[wasm_bindgen] + pub fn purpose(&self) -> WasmStatusPurpose { + self.0.purpose().into() + } + + /// Returns the index of this entry. + #[wasm_bindgen] + pub fn index(&self) -> usize { + self.0.index() + } + + /// Returns the referenced {@link StatusList2021Credential}'s url. + #[wasm_bindgen(js_name = "statusListCredential")] + pub fn status_list_credential(&self) -> String { + self.0.status_list_credential().to_string() + } + + /// Downcasts {@link this} to {@link Status} + #[wasm_bindgen(js_name = "toStatus")] + pub fn to_status(self) -> Result { + Ok( + JsValue::from_serde(&Status::from(self.0)) + .wasm_result()? + .unchecked_into(), + ) + } +} + +impl_wasm_clone!(WasmStatusList2021Entry, StatusList2021Entry); +impl_wasm_json!(WasmStatusList2021Entry, StatusList2021Entry); diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/mod.rs b/bindings/wasm/src/credential/revocation/status_list_2021/mod.rs new file mode 100644 index 0000000000..62ef3590f1 --- /dev/null +++ b/bindings/wasm/src/credential/revocation/status_list_2021/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod credential; +mod entry; +mod status_list; + +pub use credential::*; +pub use entry::*; +pub use status_list::WasmStatusList2021; diff --git a/bindings/wasm/src/credential/revocation/status_list_2021/status_list.rs b/bindings/wasm/src/credential/revocation/status_list_2021/status_list.rs new file mode 100644 index 0000000000..8e3cb763d8 --- /dev/null +++ b/bindings/wasm/src/credential/revocation/status_list_2021/status_list.rs @@ -0,0 +1,58 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::status_list_2021::StatusList2021; +use wasm_bindgen::prelude::*; + +/// StatusList2021 data structure as described in [W3C's VC status list 2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/). +#[wasm_bindgen(js_name = StatusList2021, inspectable)] +#[derive(Default, Debug)] +pub struct WasmStatusList2021(pub(crate) StatusList2021); + +impl_wasm_clone!(WasmStatusList2021, StatusList2021); + +#[wasm_bindgen(js_class = StatusList2021)] +impl WasmStatusList2021 { + /// Creates a new {@link StatusList2021} of `size` entries. + #[wasm_bindgen(constructor)] + pub fn new(size: Option) -> Result { + Ok(Self(match size { + Some(size) => StatusList2021::new(size).map_err(|e| JsError::new(&e.to_string()))?, + None => StatusList2021::default(), + })) + } + + /// Returns the number of entries in this {@link StatusList2021}. + #[wasm_bindgen] + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns whether the entry at `index` is set. + #[wasm_bindgen] + pub fn get(&self, index: usize) -> Result { + self.0.get(index).wasm_result() + } + + /// Sets the value of the `index`-th entry. + #[wasm_bindgen] + pub fn set(&mut self, index: usize, value: bool) -> Result<()> { + self.0.set(index, value).wasm_result() + } + + /// Encodes this {@link StatusList2021} into its compressed + /// base64 string representation. + #[wasm_bindgen(js_name = "intoEncodedStr")] + pub fn into_encoded_str(self) -> String { + self.0.into_encoded_str() + } + + #[wasm_bindgen(js_name = "fromEncodedStr")] + /// Attempts to decode a {@link StatusList2021} from a string. + pub fn from_encoded_str(s: &str) -> Result { + StatusList2021::try_from_encoded_str(s).map(Self).wasm_result() + } +} diff --git a/bindings/wasm/src/credential/types.rs b/bindings/wasm/src/credential/types.rs index 4255bb75df..3ca291d9a9 100644 --- a/bindings/wasm/src/credential/types.rs +++ b/bindings/wasm/src/credential/types.rs @@ -43,6 +43,9 @@ extern "C" { #[wasm_bindgen(typescript_type = "Array")] pub type ArrayCoreDID; + + #[wasm_bindgen(typescript_type = "Status")] + pub type WasmStatus; } #[wasm_bindgen(typescript_custom_section)] diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 44fc571946..d7e8dfa3d8 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -106,7 +106,9 @@ impl_wasm_error_from!( identity_iota::verification::Error, identity_iota::credential::DomainLinkageValidationError, identity_iota::sd_jwt_payload::Error, - identity_iota::credential::KeyBindingJwtError + identity_iota::credential::KeyBindingJwtError, + identity_iota::credential::status_list_2021::StatusListError, + identity_iota::credential::status_list_2021::StatusList2021CredentialError ); // Similar to `impl_wasm_error_from`, but uses the types name instead of requiring/calling Into &'static str diff --git a/examples/1_advanced/8_status_list_2021.rs b/examples/1_advanced/8_status_list_2021.rs new file mode 100644 index 0000000000..3f41823bfe --- /dev/null +++ b/examples/1_advanced/8_status_list_2021.rs @@ -0,0 +1,192 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use examples::create_did; +use examples::random_stronghold_path; +use examples::MemStorage; +use examples::API_ENDPOINT; +use identity_eddsa_verifier::EdDSAJwsVerifier; + +use identity_iota::core::FromJson; +use identity_iota::core::Object; + +use identity_iota::core::ToJson; +use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::StatusList2021; +use identity_iota::credential::status_list_2021::StatusList2021Credential; +use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder; +use identity_iota::credential::status_list_2021::StatusList2021Entry; +use identity_iota::credential::status_list_2021::StatusPurpose; + +use identity_iota::credential::Credential; +use identity_iota::credential::CredentialBuilder; + +use identity_iota::credential::FailFast; +use identity_iota::credential::Issuer; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtCredentialValidationOptions; +use identity_iota::credential::JwtCredentialValidator; +use identity_iota::credential::JwtCredentialValidatorUtils; +use identity_iota::credential::JwtValidationError; +use identity_iota::credential::Status; +use identity_iota::credential::StatusCheck; +use identity_iota::credential::Subject; +use identity_iota::did::DID; +use identity_iota::iota::IotaDocument; +use identity_iota::storage::JwkDocumentExt; +use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::KeyIdMemstore; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +use iota_sdk::client::Client; +use iota_sdk::client::Password; +use iota_sdk::types::block::address::Address; +use serde_json::json; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // =========================================================================== + // Create a Verifiable Credential. + // =========================================================================== + + // Create a new client to interact with the IOTA ledger. + let client: Client = Client::builder() + .with_primary_node(API_ENDPOINT, None)? + .finish() + .await?; + + let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_1".to_owned())) + .build(random_stronghold_path())?, + ); + + // Create an identity for the issuer with one verification method `key-1`. + let storage_issuer: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let (_, issuer_document, fragment_issuer): (Address, IotaDocument, String) = + create_did(&client, &mut secret_manager_issuer, &storage_issuer).await?; + + // Create an identity for the holder, in this case also the subject. + let mut secret_manager_alice: SecretManager = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password_2".to_owned())) + .build(random_stronghold_path())?, + ); + let storage_alice: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let (_, alice_document, _): (Address, IotaDocument, String) = + create_did(&client, &mut secret_manager_alice, &storage_alice).await?; + + // Create a new empty status list. No credentials have been revoked yet. + let status_list: StatusList2021 = StatusList2021::default(); + + // Create a status list credential so that the status list can be stored anywhere. + // The issuer makes this credential available on `http://example.com/credential/status`. + // For the purposes of this example, the credential will be used directly without fetching. + let status_list_credential: StatusList2021Credential = StatusList2021CredentialBuilder::new(status_list) + .purpose(StatusPurpose::Revocation) + .subject_id(Url::parse("http://example.com/credential/status")?) + .issuer(Issuer::Url(issuer_document.id().to_url().into())) + .build()?; + + println!("Status list credential > {status_list_credential:#}"); + + // Create a credential subject indicating the degree earned by Alice. + let subject: Subject = Subject::from_json_value(json!({ + "id": alice_document.id().as_str(), + "name": "Alice", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + // Create an unsigned `UniversityDegree` credential for Alice. + // The issuer also chooses a unique `StatusList2021` index to be able to revoke it later. + let credential_index: usize = 420; + let status: Status = StatusList2021Entry::new( + status_list_credential.id().cloned().unwrap(), + status_list_credential.purpose(), + credential_index, + None, + ) + .into(); + + // Build credential using subject above, status, and issuer. + let mut credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer_document.id().as_str())?) + .type_("UniversityDegreeCredential") + .status(status) + .subject(subject) + .build()?; + + println!("Credential JSON > {credential:#}"); + + let credential_jwt: Jwt = issuer_document + .create_credential_jwt( + &credential, + &storage_issuer, + &fragment_issuer, + &JwsSignatureOptions::default(), + None, + ) + .await?; + + let validator: JwtCredentialValidator = + JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); + + // The validator has no way of retriving the status list to check for the + // revocation of the credential. Let's skip that pass and perform the operation manually. + let mut validation_options = JwtCredentialValidationOptions::default(); + validation_options.status = StatusCheck::SkipUnsupported; + // Validate the credential's signature using the issuer's DID Document. + validator.validate::<_, Object>( + &credential_jwt, + &issuer_document, + &validation_options, + FailFast::FirstError, + )?; + // Check manually for revocation + JwtCredentialValidatorUtils::check_status_with_status_list_2021( + &credential, + &status_list_credential, + StatusCheck::Strict, + )?; + println!("Credential is valid."); + + let status_list_credential_json = status_list_credential.to_json().unwrap(); + + // =========================================================================== + // Revocation of the Verifiable Credential. + // =========================================================================== + + // At a later time, the issuer university found out that Alice cheated in her final exam. + // The issuer will revoke Alice's credential. + + // The issuer retrieves the status list credential. + let mut status_list_credential = + serde_json::from_str::(status_list_credential_json.as_str()).unwrap(); + + // Set the value of the chosen index entry to true to revoke the credential + status_list_credential.set_credential_status(&mut credential, credential_index, true)?; + + // validate the credential and check for revocation + validator.validate::<_, Object>( + &credential_jwt, + &issuer_document, + &validation_options, + FailFast::FirstError, + )?; + let revocation_result = JwtCredentialValidatorUtils::check_status_with_status_list_2021( + &credential, + &status_list_credential, + StatusCheck::Strict, + ); + + assert!(revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + println!("The credential has been successfully revoked."); + + Ok(()) +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 7651e67de9..1a9df88899 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } -identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage"] } +identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] } identity_stronghold = { path = "../identity_stronghold", default-features = false } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } primitive-types = "0.12.1" @@ -87,3 +87,7 @@ name = "6_domain_linkage" [[example]] path = "1_advanced/7_sd_jwt.rs" name = "7_sd_jwt" + +[[example]] +path = "1_advanced/8_status_list_2021.rs" +name = "8_status_list_2021" diff --git a/examples/README.md b/examples/README.md index 92ba330adf..2076f8b4b2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -47,4 +47,5 @@ The following advanced examples are available: | [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. | | [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. | | [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. | -| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. | +| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. | +| [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. | diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 332e695521..653f9a354c 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -26,6 +26,7 @@ reqwest = { version = "0.11", default-features = false, features = ["default-tls roaring = { version = "0.10", default-features = false, optional = true } sd-jwt-payload = { version = "0.1.2", default-features = false, features = ["sha"], optional = true } serde.workspace = true +serde-aux = { version = "4.3.1", default-features = false, optional = true } serde_json.workspace = true serde_repr = { version = "0.1", default-features = false, optional = true } strum.workspace = true @@ -33,6 +34,7 @@ thiserror.workspace = true url = { version = "2.5", default-features = false } [dev-dependencies] +anyhow = "1.0.62" identity_eddsa_verifier = { version = "=1.0.0", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "std", "random"] } proptest = { version = "1.4.0", default-features = false, features = ["std"] } @@ -49,6 +51,7 @@ default = ["revocation-bitmap", "validator", "credential", "presentation", "doma credential = [] presentation = ["credential"] revocation-bitmap = ["dep:dataurl", "dep:flate2", "dep:roaring"] +status-list-2021 = ["revocation-bitmap", "dep:serde-aux"] validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] diff --git a/identity_credential/src/credential/builder.rs b/identity_credential/src/credential/builder.rs index 6014578221..f95771c500 100644 --- a/identity_credential/src/credential/builder.rs +++ b/identity_credential/src/credential/builder.rs @@ -121,8 +121,8 @@ impl CredentialBuilder { /// Adds a value to the `credentialStatus` set. #[must_use] - pub fn status(mut self, value: Status) -> Self { - self.status = Some(value); + pub fn status(mut self, value: impl Into) -> Self { + self.status = Some(value.into()); self } diff --git a/identity_credential/src/credential/status.rs b/identity_credential/src/credential/status.rs index 7496aea10b..bc5d0d3f8d 100644 --- a/identity_credential/src/credential/status.rs +++ b/identity_credential/src/credential/status.rs @@ -40,7 +40,7 @@ impl Status { mod tests { use identity_core::convert::FromJson; - use crate::credential::Status; + use super::*; const JSON: &str = include_str!("../../tests/fixtures/status-1.json"); @@ -48,6 +48,6 @@ mod tests { fn test_from_json() { let status: Status = Status::from_json(JSON).unwrap(); assert_eq!(status.id.as_str(), "https://example.edu/status/24"); - assert_eq!(status.type_, "CredentialStatusList2017".to_owned()); + assert_eq!(status.type_, "CredentialStatusList2017"); } } diff --git a/identity_credential/src/revocation/mod.rs b/identity_credential/src/revocation/mod.rs index ca8773309f..6732ff4194 100644 --- a/identity_credential/src/revocation/mod.rs +++ b/identity_credential/src/revocation/mod.rs @@ -1,13 +1,14 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -//! Contains an implementation of RevocationBitmap2022 for managing credential revocation. +//! Contains the implementations for all the credential revocation methods that can be used with IOTA's Identity +//! framework. -mod bitmap; -mod document_ext; mod error; +mod revocation_bitmap_2022; +#[cfg(feature = "status-list-2021")] +pub mod status_list_2021; -pub use self::bitmap::RevocationBitmap; -pub use self::document_ext::RevocationDocumentExt; pub use self::error::RevocationError; pub use self::error::RevocationResult; +pub use revocation_bitmap_2022::*; diff --git a/identity_credential/src/revocation/bitmap.rs b/identity_credential/src/revocation/revocation_bitmap_2022/bitmap.rs similarity index 99% rename from identity_credential/src/revocation/bitmap.rs rename to identity_credential/src/revocation/revocation_bitmap_2022/bitmap.rs index 947dc94aaa..6f47db97be 100644 --- a/identity_credential/src/revocation/bitmap.rs +++ b/identity_credential/src/revocation/revocation_bitmap_2022/bitmap.rs @@ -14,7 +14,7 @@ use identity_core::convert::BaseEncoding; use identity_did::DIDUrl; use roaring::RoaringBitmap; -use super::error::RevocationError; +use crate::revocation::error::RevocationError; use identity_document::service::Service; use identity_document::service::ServiceEndpoint; diff --git a/identity_credential/src/revocation/document_ext.rs b/identity_credential/src/revocation/revocation_bitmap_2022/document_ext.rs similarity index 98% rename from identity_credential/src/revocation/document_ext.rs rename to identity_credential/src/revocation/revocation_bitmap_2022/document_ext.rs index a0318d6774..65e2789f5e 100644 --- a/identity_credential/src/revocation/document_ext.rs +++ b/identity_credential/src/revocation/revocation_bitmap_2022/document_ext.rs @@ -7,8 +7,8 @@ use identity_document::service::Service; use identity_document::utils::DIDUrlQuery; use identity_document::utils::Queryable; -use super::RevocationError; -use super::RevocationResult; +use crate::revocation::RevocationError; +use crate::revocation::RevocationResult; /// Extension trait providing convenience methods to update a `RevocationBitmap2022` service /// in a [`CoreDocument`](::identity_document::document::CoreDocument). diff --git a/identity_credential/src/revocation/revocation_bitmap_2022/mod.rs b/identity_credential/src/revocation/revocation_bitmap_2022/mod.rs new file mode 100644 index 0000000000..609cba5277 --- /dev/null +++ b/identity_credential/src/revocation/revocation_bitmap_2022/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod bitmap; +mod document_ext; + +pub use bitmap::*; +pub use document_ext::*; diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs new file mode 100644 index 0000000000..3588772e82 --- /dev/null +++ b/identity_credential/src/revocation/status_list_2021/credential.rs @@ -0,0 +1,406 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; +use std::ops::Deref; +use std::str::FromStr; + +use identity_core::common::Context; +use identity_core::common::OneOrMany; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; +use thiserror::Error; + +/// The type of a `StatusList2021Credential`. +pub const CREDENTIAL_TYPE: &str = "StatusList2021Credential"; +const CREDENTIAL_SUBJECT_TYPE: &str = "StatusList2021"; + +/// [Error](std::error::Error) type that represents the possible errors that can be +/// encountered when dealing with [`StatusList2021Credential`]s. +#[derive(Clone, Debug, Error, strum::IntoStaticStr)] +pub enum StatusList2021CredentialError { + /// The provided [`Credential`] has more than one `credentialSubject`. + #[error("A StatusList2021Credential may only have one credentialSubject")] + MultipleCredentialSubject, + /// The provided [`Credential`] has an invalid property. + #[error("Invalid property \"{0}\"")] + InvalidProperty(&'static str), + /// The provided [`Credential`] doesn't have a mandatory property. + #[error("Missing property \"{0}\"")] + MissingProperty(&'static str), + /// Inner status list failures. + #[error(transparent)] + StatusListError(#[from] StatusListError), + /// Missing status list id + #[error("Cannot set the status of a credential without a \"credentialSubject.id\".")] + Unreferenceable, +} + +use crate::credential::Credential; +use crate::credential::CredentialBuilder; +use crate::credential::Issuer; +use crate::credential::Proof; +use crate::credential::Subject; + +use super::status_list::StatusListError; +use super::StatusList2021; +use super::StatusList2021Entry; + +/// A parsed [StatusList2021Credential](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021credential). +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "Credential", into = "Credential")] +pub struct StatusList2021Credential { + inner: Credential, + subject: StatusList2021CredentialSubject, +} + +impl Display for StatusList2021Credential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.inner) + } +} + +impl From for Credential { + fn from(value: StatusList2021Credential) -> Self { + value.into_inner() + } +} + +impl Deref for StatusList2021Credential { + type Target = Credential; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl TryFrom for StatusList2021Credential { + type Error = StatusList2021CredentialError; + fn try_from(mut credential: Credential) -> Result { + let has_right_credential_type = credential.types.contains(&CREDENTIAL_TYPE.to_owned()); + let subject = StatusList2021CredentialSubject::try_from_credential(&mut credential)?; + + if has_right_credential_type { + Ok(Self { + inner: credential, + subject, + }) + } else { + Err(StatusList2021CredentialError::InvalidProperty("type")) + } + } +} + +impl StatusList2021Credential { + /// Returns the inner "raw" [`Credential`]. + pub fn into_inner(self) -> Credential { + let Self { mut inner, subject } = self; + inner.credential_subject = OneOrMany::One(subject.into()); + inner + } + + /// Returns the id of this credential. + pub fn id(&self) -> Option<&Url> { + self.subject.id.as_ref() + } + + /// Returns the purpose of this status list. + pub fn purpose(&self) -> StatusPurpose { + self.subject.status_purpose + } + + fn status_list(&self) -> Result { + StatusList2021::try_from_encoded_str(&self.subject.encoded_list) + } + + /// Sets the credential status of a given [`Credential`], + /// mapping it to the `index`-th entry of this [`StatusList2021Credential`]. + pub fn set_credential_status( + &mut self, + credential: &mut Credential, + index: usize, + revoked_or_suspended: bool, + ) -> Result { + let id = self + .id() + .cloned() + .ok_or(StatusList2021CredentialError::Unreferenceable)?; + let entry = StatusList2021Entry::new(id, self.purpose(), index, None); + + self.set_entry(index, revoked_or_suspended)?; + credential.credential_status = Some(entry.clone().into()); + + Ok(entry) + } + + /// Sets the `index`-th entry to `value` + pub(crate) fn set_entry(&mut self, index: usize, value: bool) -> Result<(), StatusList2021CredentialError> { + let mut status_list = self.status_list()?; + status_list.set(index, value)?; + self.subject.encoded_list = status_list.into_encoded_str(); + + Ok(()) + } + + /// Returns the status of the `index-th` entry. + pub fn entry(&self, index: usize) -> Result { + let status_list = self.status_list()?; + Ok(match (self.purpose(), status_list.get(index)?) { + (StatusPurpose::Revocation, true) => CredentialStatus::Revoked, + (StatusPurpose::Suspension, true) => CredentialStatus::Suspended, + _ => CredentialStatus::Valid, + }) + } +} + +/// The status of a credential referenced inside a [`StatusList2021Credential`] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CredentialStatus { + /// A revoked credential + Revoked, + /// A suspended credential + Suspended, + /// A valid credential + Valid, +} + +/// [`StatusList2021Credential`]'s purpose. +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum StatusPurpose { + /// Used for revocation. + #[default] + Revocation, + /// Used for suspension. + Suspension, +} + +impl Display for StatusPurpose { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Revocation => "revocation", + Self::Suspension => "suspension", + }; + write!(f, "{s}") + } +} + +impl FromStr for StatusPurpose { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "revocation" => Ok(Self::Revocation), + "suspension" => Ok(Self::Suspension), + _ => Err(()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +struct StatusList2021CredentialSubject { + status_purpose: StatusPurpose, + encoded_list: String, + id: Option, +} + +impl From for Subject { + fn from(value: StatusList2021CredentialSubject) -> Self { + let properties = [ + ( + "statusPurpose".to_owned(), + Value::String(value.status_purpose.to_string()), + ), + ("type".to_owned(), Value::String(CREDENTIAL_SUBJECT_TYPE.to_owned())), + ("encodedList".to_owned(), Value::String(value.encoded_list)), + ] + .into_iter() + .collect(); + + if let Some(id) = value.id { + Subject::with_id_and_properties(id, properties) + } else { + Subject::with_properties(properties) + } + } +} + +impl StatusList2021CredentialSubject { + /// Parse a StatusListCredentialSubject out of a credential, without copying. + fn try_from_credential(credential: &mut Credential) -> Result { + let OneOrMany::One(subject) = &mut credential.credential_subject else { + return Err(StatusList2021CredentialError::MultipleCredentialSubject); + }; + if let Some(subject_type) = subject.properties.get("type") { + if !subject_type.as_str().is_some_and(|t| t == CREDENTIAL_SUBJECT_TYPE) { + return Err(StatusList2021CredentialError::InvalidProperty("credentialSubject.type")); + } + } else { + return Err(StatusList2021CredentialError::MissingProperty("credentialSubject.type")); + } + let status_purpose = subject + .properties + .get("statusPurpose") + .ok_or(StatusList2021CredentialError::MissingProperty( + "credentialSubject.statusPurpose", + )) + .and_then(|value| { + value + .as_str() + .and_then(|purpose| StatusPurpose::from_str(purpose).ok()) + .ok_or(StatusList2021CredentialError::InvalidProperty( + "credentialSubject.statusPurpose", + )) + })?; + let encoded_list = subject + .properties + .get_mut("encodedList") + .ok_or(StatusList2021CredentialError::MissingProperty( + "credentialSubject.encodedList", + )) + .and_then(|value| { + if let Value::String(ref mut s) = value { + Ok(s) + } else { + Err(StatusList2021CredentialError::InvalidProperty( + "credentialSubject.encodedList", + )) + } + }) + .map(std::mem::take)?; + + Ok(StatusList2021CredentialSubject { + id: std::mem::take(&mut subject.id), + encoded_list, + status_purpose, + }) + } +} + +/// Builder type for [`StatusList2021Credential`]. +#[derive(Debug, Default)] +pub struct StatusList2021CredentialBuilder { + inner_builder: CredentialBuilder, + credential_subject: StatusList2021CredentialSubject, +} + +impl StatusList2021CredentialBuilder { + /// Creates a new [`StatusList2021CredentialBuilder`] from a [`StatusList2021`]. + pub fn new(status_list: StatusList2021) -> Self { + let credential_subject = StatusList2021CredentialSubject { + encoded_list: status_list.into_encoded_str(), + ..Default::default() + }; + Self { + credential_subject, + ..Default::default() + } + } + + /// Sets `credentialSubject.statusPurpose`. + pub const fn purpose(mut self, purpose: StatusPurpose) -> Self { + self.credential_subject.status_purpose = purpose; + self + } + + /// Sets `credentialSubject.id`. + pub fn subject_id(mut self, id: Url) -> Self { + self.credential_subject.id = Some(id); + self + } + + /// Sets `expirationDate`. + pub const fn expiration_date(mut self, time: Timestamp) -> Self { + self.inner_builder.expiration_date = Some(time); + self + } + + /// Sets `issuer`. + pub fn issuer(mut self, issuer: Issuer) -> Self { + self.inner_builder.issuer = Some(issuer); + self + } + + /// Adds a `@context` entry. + pub fn context(mut self, ctx: Context) -> Self { + self.inner_builder.context.push(ctx); + self + } + + /// Adds a `type` entry. + pub fn add_type(mut self, type_: String) -> Self { + self.inner_builder.types.push(type_); + self + } + + /// Adds a credential proof. + pub fn proof(mut self, proof: Proof) -> Self { + self.inner_builder.proof = Some(proof); + self + } + + /// Consumes this [`StatusList2021CredentialBuilder`] into a [`StatusList2021Credential`]. + pub fn build(mut self) -> Result { + let id = self.credential_subject.id.clone().map(|mut url| { + url.set_fragment(None); + url + }); + self.inner_builder.id = id; + self + .inner_builder + .type_(CREDENTIAL_TYPE) + .issuance_date(Timestamp::now_utc()) + .subject(self.credential_subject.clone().into()) + .build() + .map(|credential| StatusList2021Credential { + subject: self.credential_subject, + inner: credential, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const STATUS_LIST_2021_CREDENTIAL_SAMPLE: &str = r#" +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "https://example.com/credentials/status/3", + "type": ["VerifiableCredential", "StatusList2021Credential"], + "issuer": "did:example:12345", + "issuanceDate": "2021-04-05T14:27:40Z", + "credentialSubject": { + "id": "https://example.com/status/3#list", + "type": "StatusList2021", + "statusPurpose": "revocation", + "encodedList": "H4sIAAAAAAAAA-3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA" + } +} + "#; + + #[test] + fn status_purpose_serialization_works() { + assert_eq!( + serde_json::to_string(&StatusPurpose::Revocation).ok(), + Some(format!("\"{}\"", StatusPurpose::Revocation)) + ); + } + #[test] + fn status_purpose_deserialization_works() { + assert_eq!( + serde_json::from_str::("\"suspension\"").ok(), + Some(StatusPurpose::Suspension), + ) + } + #[test] + fn status_list_2021_credential_deserialization_works() { + let credential = serde_json::from_str::(STATUS_LIST_2021_CREDENTIAL_SAMPLE) + .expect("Failed to deserialize"); + assert_eq!(credential.purpose(), StatusPurpose::Revocation); + } +} diff --git a/identity_credential/src/revocation/status_list_2021/entry.rs b/identity_credential/src/revocation/status_list_2021/entry.rs new file mode 100644 index 0000000000..7eecf2f28e --- /dev/null +++ b/identity_credential/src/revocation/status_list_2021/entry.rs @@ -0,0 +1,145 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Url; +use serde::de::Error; +use serde::de::Visitor; +use serde::Deserialize; +use serde::Serialize; + +use crate::credential::Status; + +use super::credential::StatusPurpose; + +const CREDENTIAL_STATUS_TYPE: &str = "StatusList2021Entry"; + +fn deserialize_status_entry_type<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct ExactStrVisitor(&'static str); + impl<'a> Visitor<'a> for ExactStrVisitor { + type Value = &'static str; + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "the exact string \"{}\"", self.0) + } + fn visit_str(self, str: &str) -> Result { + if str == self.0 { + Ok(self.0) + } else { + Err(E::custom(format!("not \"{}\"", self.0))) + } + } + } + + deserializer + .deserialize_str(ExactStrVisitor(CREDENTIAL_STATUS_TYPE)) + .map(ToOwned::to_owned) +} + +/// [StatusList2021Entry](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021entry) implementation. +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct StatusList2021Entry { + id: Url, + #[serde(rename = "type", deserialize_with = "deserialize_status_entry_type")] + type_: String, + status_purpose: StatusPurpose, + #[serde(deserialize_with = "serde_aux::prelude::deserialize_number_from_string")] + status_list_index: usize, + status_list_credential: Url, +} + +impl TryFrom<&Status> for StatusList2021Entry { + type Error = serde_json::Error; + fn try_from(status: &Status) -> Result { + let json_status = serde_json::to_value(status)?; + serde_json::from_value(json_status) + } +} + +impl From for Status { + fn from(entry: StatusList2021Entry) -> Self { + let json_status = serde_json::to_value(entry).unwrap(); // Safety: shouldn't go out of memory + serde_json::from_value(json_status).unwrap() // Safety: `StatusList2021Entry` is a credential status + } +} + +impl StatusList2021Entry { + /// Creates a new [`StatusList2021Entry`]. + pub fn new(status_list: Url, purpose: StatusPurpose, index: usize, id: Option) -> Self { + let id = id.unwrap_or_else(|| { + let mut id = status_list.clone(); + id.set_fragment(None); + id + }); + + Self { + id, + type_: CREDENTIAL_STATUS_TYPE.to_owned(), + status_purpose: purpose, + status_list_credential: status_list, + status_list_index: index, + } + } + + /// Returns this `credentialStatus`'s `id`. + pub const fn id(&self) -> &Url { + &self.id + } + + /// Returns the purpose of this entry. + pub const fn purpose(&self) -> StatusPurpose { + self.status_purpose + } + + /// Returns the index of this entry. + pub const fn index(&self) -> usize { + self.status_list_index + } + + /// Returns the referenced [`StatusList2021Credential`]'s [`Url`]. + pub const fn status_list_credential(&self) -> &Url { + &self.status_list_credential + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const STATUS_LIST_ENTRY_SAMPLE: &str = r#" +{ + "id": "https://example.com/credentials/status/3#94567", + "type": "StatusList2021Entry", + "statusPurpose": "revocation", + "statusListIndex": "94567", + "statusListCredential": "https://example.com/credentials/status/3" +}"#; + + #[test] + fn entry_deserialization_works() { + let deserialized = + serde_json::from_str::(STATUS_LIST_ENTRY_SAMPLE).expect("Failed to deserialize"); + let status = StatusList2021Entry::new( + Url::parse("https://example.com/credentials/status/3").unwrap(), + StatusPurpose::Revocation, + 94567, + Url::parse("https://example.com/credentials/status/3#94567").ok(), + ); + assert_eq!(status, deserialized); + } + + #[test] + #[should_panic] + fn deserializing_wrong_status_type_fails() { + let status = serde_json::json!({ + "id": "https://example.com/credentials/status/3#94567", + "type": "Whatever2024", + "statusPurpose": "revocation", + "statusListIndex": "94567", + "statusListCredential": "https://example.com/credentials/status/3" + }); + serde_json::from_value::(status).expect("wrong type"); + } +} diff --git a/identity_credential/src/revocation/status_list_2021/mod.rs b/identity_credential/src/revocation/status_list_2021/mod.rs new file mode 100644 index 0000000000..39c4dfbcf4 --- /dev/null +++ b/identity_credential/src/revocation/status_list_2021/mod.rs @@ -0,0 +1,13 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! Implementation of [StatusList2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/). + +/// Implementation of [StatusList2021Credential](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021credential). +mod credential; +mod entry; +mod status_list; + +pub use credential::*; +pub use entry::*; +pub use status_list::*; diff --git a/identity_credential/src/revocation/status_list_2021/status_list.rs b/identity_credential/src/revocation/status_list_2021/status_list.rs new file mode 100644 index 0000000000..30ad6a8582 --- /dev/null +++ b/identity_credential/src/revocation/status_list_2021/status_list.rs @@ -0,0 +1,174 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use flate2::read::GzDecoder; +use flate2::write::GzEncoder; +use flate2::Compression; +use identity_core::convert::Base; +use identity_core::convert::BaseEncoding; +use std::io::Write; +use thiserror::Error; + +const MINIMUM_LIST_SIZE: usize = 16 * 1024 * 8; + +/// [`std::error::Error`] type for [`StatusList2021`]'s operations. +#[derive(Debug, Error, PartialEq, Eq, Clone, strum::IntoStaticStr)] +pub enum StatusListError { + /// Requested entry is not in the list. + #[error("The requested entry is not in the list.")] + IndexOutOfBounds, + /// Improperly encoded status list. + #[error("\"{0}\" is not a valid encoded status list.")] + InvalidEncoding(String), + /// Invalid list size + #[error("A StatusList2021 must have at least {MINIMUM_LIST_SIZE} entries.")] + InvalidListSize, +} + +/// StatusList2021 data structure as described in [W3C's VC status list 2021](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/). +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct StatusList2021(Box<[u8]>); + +impl Default for StatusList2021 { + fn default() -> Self { + StatusList2021::new(MINIMUM_LIST_SIZE).unwrap() + } +} + +impl StatusList2021 { + /// Returns a new zero-filled [`StatusList2021`] that can hold `num_entries` credential statuses. + /// + /// ## Notes: + /// - The actual length of the list will be rounded up to the closest multiple of 8 to accomodate for byte sizes. + /// - `num_entries` must be at least 131,072 which corresponds to a size of 16KB. + pub fn new(num_entries: usize) -> Result { + if num_entries < MINIMUM_LIST_SIZE { + return Err(StatusListError::InvalidListSize); + } + + let size = num_entries / 8 + (num_entries % 8 != 0) as usize; + let store = vec![0; size]; + + Ok(StatusList2021(store.into_boxed_slice())) + } + + /// Returns the number of entries. + #[allow(clippy::len_without_is_empty)] + pub const fn len(&self) -> usize { + self.0.len() * 8 + } + + /// Returns the status of the entry at `index` without bound checking. + /// ## Panic: + /// * if `index` is greater than or equal to `self.len()`. + const fn get_unchecked(&self, index: usize) -> bool { + let (i, offset) = Self::entry_index_to_store_index(index); + self.0[i] & (0b1000_0000 >> offset) != 0 + } + + /// Sets the status of the `index`-th entry to `value`. + /// + /// ## Panic: + /// * if `index` is greater than or equal to `self.len()`. + fn set_unchecked(&mut self, index: usize, value: bool) { + let (i, offset) = Self::entry_index_to_store_index(index); + if value { + self.0[i] |= 0b1000_0000 >> offset + } else { + self.0[i] &= 0b0111_1111 >> offset + } + } + + /// Returns the status of the `index`-th entry, if it exists. + pub fn get(&self, index: usize) -> Result { + (index < self.len()) + .then_some(self.get_unchecked(index)) + .ok_or(StatusListError::IndexOutOfBounds) + } + + /// Sets the status of the `index`-th entry to `value`. + pub fn set(&mut self, index: usize, value: bool) -> Result<(), StatusListError> { + if index < self.len() { + self.set_unchecked(index, value); + Ok(()) + } else { + Err(StatusListError::IndexOutOfBounds) + } + } + + /// Attempts to parse a [`StatusList2021`] from a string, following the + /// [StatusList2021 expansion algorithm](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#bitstring-expansion-algorithm). + pub fn try_from_encoded_str(s: &str) -> Result { + let compressed_status_list = + BaseEncoding::decode(s, Base::Base64).or(Err(StatusListError::InvalidEncoding(s.to_owned())))?; + let status_list = { + use std::io::Read; + + let mut decompressor = GzDecoder::new(&compressed_status_list[..]); + let mut status_list = vec![]; + decompressor + .read_to_end(&mut status_list) + .or(Err(StatusListError::InvalidEncoding(s.to_owned())))?; + + StatusList2021(status_list.into_boxed_slice()) + }; + + Ok(status_list) + } + + /// Encode this [`StatusList2021`] into its string representation following + /// [StatusList2021 generation algorithm](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#bitstring-generation-algorithm). + pub fn into_encoded_str(self) -> String { + let compressed_status_list = { + let mut compressor = GzEncoder::new(vec![], Compression::best()); + compressor.write_all(&self.0).unwrap(); + compressor.finish().unwrap() + }; + + BaseEncoding::encode(&compressed_status_list[..], Base::Base64) + } + + /// Returns the byte location and the bit location within it. + const fn entry_index_to_store_index(index: usize) -> (usize, usize) { + (index / 8, index % 8) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_status_list() { + let mut status_list = StatusList2021::default(); + status_list.set(131071, true).unwrap(); + assert!(status_list.get(131071).unwrap()); + assert_eq!(status_list.set(131072, true), Err(StatusListError::IndexOutOfBounds)); + } + + #[test] + fn status_list_too_short_fails() { + assert_eq!(StatusList2021::new(100), Err(StatusListError::InvalidListSize)); + } + + #[test] + fn status_list_entry_access() { + let mut status_list = StatusList2021::default(); + status_list.set(42, true).unwrap(); + assert!(status_list.get(42).unwrap()); + + status_list.set(42, false).unwrap(); + assert_eq!(status_list, StatusList2021::default()); + } + + #[test] + fn status_list_encode_decode() { + let mut status_list = StatusList2021::default(); + status_list.set(42, true).unwrap(); + status_list.set(420, true).unwrap(); + status_list.set(4200, true).unwrap(); + let encoded = status_list.clone().into_encoded_str(); + let decoded = StatusList2021::try_from_encoded_str(&encoded).unwrap(); + assert_eq!(decoded, status_list); + } +} diff --git a/identity_credential/src/validator/jwt_credential_validation/error.rs b/identity_credential/src/validator/jwt_credential_validation/error.rs index c418db9676..073ffe303c 100644 --- a/identity_credential/src/validator/jwt_credential_validation/error.rs +++ b/identity_credential/src/validator/jwt_credential_validation/error.rs @@ -101,6 +101,9 @@ pub enum JwtValidationError { /// Indicates that the credential has been revoked. #[error("credential has been revoked")] Revoked, + /// Indicates that the credential has been suspended. + #[error("credential has been suspended")] + Suspended, } /// Specifies whether an error is related to a credential issuer or the presentation holder. diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs index 83e21ba3a6..e7a43bcdab 100644 --- a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 IOTA Stiftung +// Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 use std::str::FromStr; @@ -15,6 +15,8 @@ use super::SignerContext; use crate::credential::Credential; use crate::credential::CredentialJwtClaims; use crate::credential::Jwt; +#[cfg(feature = "status-list-2021")] +use crate::revocation::status_list_2021::StatusList2021Credential; use crate::validator::SubjectHolderRelationship; /// Utility functions for verifying JWT credentials. @@ -82,6 +84,46 @@ impl JwtCredentialValidatorUtils { .ok_or(JwtValidationError::SubjectHolderRelationship) } + /// Checks whether the status specified in `credentialStatus` has been set by the issuer. + /// + /// Only supports `StatusList2021`. + #[cfg(feature = "status-list-2021")] + pub fn check_status_with_status_list_2021( + credential: &Credential, + status_list_credential: &StatusList2021Credential, + status_check: crate::validator::StatusCheck, + ) -> ValidationUnitResult { + use crate::revocation::status_list_2021::CredentialStatus; + use crate::revocation::status_list_2021::StatusList2021Entry; + + if status_check == crate::validator::StatusCheck::SkipAll { + return Ok(()); + } + + match &credential.credential_status { + None => Ok(()), + Some(status) => { + let status = StatusList2021Entry::try_from(status) + .map_err(|e| JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(e.to_string())))?; + if Some(status.status_list_credential()) == status_list_credential.id.as_ref() + && status.purpose() == status_list_credential.purpose() + { + let entry_status = status_list_credential + .entry(status.index()) + .map_err(|e| JwtValidationError::InvalidStatus(crate::Error::InvalidStatus(e.to_string())))?; + match entry_status { + CredentialStatus::Revoked => Err(JwtValidationError::Revoked), + CredentialStatus::Suspended => Err(JwtValidationError::Suspended), + CredentialStatus::Valid => Ok(()), + } + } else { + Err(JwtValidationError::InvalidStatus(crate::Error::InvalidStatus( + "The given statusListCredential doesn't match the credential's status".to_owned(), + ))) + } + } + } + } /// Checks whether the credential status has been revoked. /// /// Only supports `RevocationBitmap2022`. diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 38d406989e..52a9e2b8d3 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -43,6 +43,9 @@ revocation-bitmap = [ "identity_resolver?/revocation-bitmap", ] +# Enables revocation with `StatusList2021`. +status-list-2021 = ["revocation-bitmap", "identity_credential/status-list-2021"] + # Enables support for the `Resolver`. resolver = ["dep:identity_resolver"]