diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 0cb70751b9..631b6a8194 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -26,6 +26,11 @@

Note that having an instance of this type only means the JWS it was constructed from was verified. It does not imply anything about a potentially present proof property on the credential itself.

+
DecodedJwtPresentation
+

A cryptographically verified and decoded presentation.

+

Note that having an instance of this type only means the JWS it was constructed from was verified. +It does not imply anything about a potentially present proof property on the presentation itself.

+
DomainLinkageConfiguration

DID Configuration Resource which contains Domain Linkage Credentials. It can be placed in an origin's .well-known directory to prove linkage between the origin and a DID. @@ -79,6 +84,15 @@ and resolution of DID documents in Alias Outputs.

JwtCredentialValidator

A type for decoding and validating Credentials.

+
JwtPresentation
+
+
JwtPresentationOptions
+
+
JwtPresentationValidationOptions
+

Options to declare validation criteria when validating presentation.

+
+
JwtPresentationValidator
+
KeyPair
LinkedDomainService
@@ -151,12 +165,8 @@ See IVerifierOptions.

## Members
-
KeyType
-
StateMetadataEncoding
-
MethodRelationship
-
StatusCheck

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

@@ -199,20 +209,15 @@ This variant is the default used if no other variant is specified when construct
FirstError

Return after the first error occurs.

+
KeyType
+
+
MethodRelationship
+
## Functions
-
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.

-
verifyEdDSA(alg, signingInput, decodedSignature, publicKey)

Verify a JWS signature secured with the JwsAlgorithm::EdDSA algorithm. Only the EdCurve::Ed25519 variant is supported for now.

@@ -222,6 +227,15 @@ the IOTA Identity Framework.

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

+
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

+
@@ -455,6 +469,7 @@ A method-agnostic DID Document. * [.purgeMethod(storage, id)](#CoreDocument+purgeMethod) ⇒ Promise.<void> * [.createJws(storage, fragment, payload, options)](#CoreDocument+createJws) ⇒ [Promise.<Jws>](#Jws) * [.createCredentialJwt(storage, fragment, credential, options)](#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) @@ -888,6 +903,19 @@ produced by the corresponding private key backed by the `storage` in accordance | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | + + +### coreDocument.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options) ⇒ [Promise.<Jwt>](#Jwt) +**Kind**: instance method of [CoreDocument](#CoreDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| fragment | string | +| presentation | [JwtPresentation](#JwtPresentation) | +| signature_options | [JwsSignatureOptions](#JwsSignatureOptions) | +| presentation_options | [JwtPresentationOptions](#JwtPresentationOptions) | + ### CoreDocument.fromJSON(json) ⇒ [CoreDocument](#CoreDocument) @@ -1520,6 +1548,61 @@ Consumes the object and returns the decoded credential. This destroys the `DecodedCredential` object. **Kind**: instance method of [DecodedJwtCredential](#DecodedJwtCredential) + + +## DecodedJwtPresentation +A cryptographically verified and decoded presentation. + +Note that having an instance of this type only means the JWS it was constructed from was verified. +It does not imply anything about a potentially present proof property on the presentation itself. + +**Kind**: global class + +* [DecodedJwtPresentation](#DecodedJwtPresentation) + * [.presentation()](#DecodedJwtPresentation+presentation) ⇒ [JwtPresentation](#JwtPresentation) + * [.protectedHeader()](#DecodedJwtPresentation+protectedHeader) ⇒ [JwsHeader](#JwsHeader) + * [.intoCredential()](#DecodedJwtPresentation+intoCredential) ⇒ [JwtPresentation](#JwtPresentation) + * [.expirationDate()](#DecodedJwtPresentation+expirationDate) ⇒ [Timestamp](#Timestamp) \| undefined + * [.issuanceDate()](#DecodedJwtPresentation+issuanceDate) ⇒ [Timestamp](#Timestamp) \| undefined + * [.credentials()](#DecodedJwtPresentation+credentials) ⇒ [Array.<DecodedJwtCredential>](#DecodedJwtCredential) + + + +### decodedJwtPresentation.presentation() ⇒ [JwtPresentation](#JwtPresentation) +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.protectedHeader() ⇒ [JwsHeader](#JwsHeader) +Returns a copy of the protected header parsed from the decoded JWS. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.intoCredential() ⇒ [JwtPresentation](#JwtPresentation) +Consumes the object and returns the decoded presentation. + +### Warning +This destroys the `DecodedJwtPresentation` object. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.expirationDate() ⇒ [Timestamp](#Timestamp) \| undefined +The expiration date parsed from the JWT claims. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.issuanceDate() ⇒ [Timestamp](#Timestamp) \| undefined +The issuance dated parsed from the JWT claims. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.credentials() ⇒ [Array.<DecodedJwtCredential>](#DecodedJwtCredential) +The credentials included in the presentation (decoded). + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) ## DomainLinkageConfiguration @@ -2058,6 +2141,7 @@ Deserializes an instance from a JSON object. * [.purgeMethod(storage, id)](#IotaDocument+purgeMethod) ⇒ Promise.<void> * [.createJwt(storage, fragment, payload, options)](#IotaDocument+createJwt) ⇒ [Promise.<Jws>](#Jws) * [.createCredentialJwt(storage, fragment, credential, options)](#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) * [.unpackFromOutput(did, aliasOutput, allowEmpty, tokenSupply)](#IotaDocument.unpackFromOutput) ⇒ [IotaDocument](#IotaDocument) @@ -2564,6 +2648,25 @@ produced by the corresponding private key backed by the `storage` in accordance | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | + + +### iotaDocument.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options) ⇒ [Promise.<Jwt>](#Jwt) +Produces a JWT where the payload is produced from the given `presentation` +in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). + +The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be +produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| fragment | string | +| presentation | [JwtPresentation](#JwtPresentation) | +| signature_options | [JwsSignatureOptions](#JwsSignatureOptions) | +| presentation_options | [JwtPresentationOptions](#JwtPresentationOptions) | + ### IotaDocument.newWithId(id) ⇒ [IotaDocument](#IotaDocument) @@ -3815,6 +3918,297 @@ Fails if the issuer field is not a valid DID. | --- | --- | | credential | [Credential](#Credential) | + + +## JwtPresentation +**Kind**: global class + +* [JwtPresentation](#JwtPresentation) + * [new JwtPresentation(values)](#new_JwtPresentation_new) + * _instance_ + * [.context()](#JwtPresentation+context) ⇒ Array.<(string\|Record.<string, any>)> + * [.id()](#JwtPresentation+id) ⇒ string \| undefined + * [.type()](#JwtPresentation+type) ⇒ Array.<string> + * [.verifiableCredential()](#JwtPresentation+verifiableCredential) ⇒ [Array.<Jwt>](#Jwt) + * [.holder()](#JwtPresentation+holder) ⇒ string + * [.refreshService()](#JwtPresentation+refreshService) ⇒ Array.<RefreshService> + * [.termsOfUse()](#JwtPresentation+termsOfUse) ⇒ Array.<Policy> + * [.proof()](#JwtPresentation+proof) ⇒ Map.<string, any> \| undefined + * [.properties()](#JwtPresentation+properties) ⇒ Map.<string, any> + * [.toJSON()](#JwtPresentation+toJSON) ⇒ any + * [.clone()](#JwtPresentation+clone) ⇒ [JwtPresentation](#JwtPresentation) + * _static_ + * [.BaseContext()](#JwtPresentation.BaseContext) ⇒ string + * [.BaseType()](#JwtPresentation.BaseType) ⇒ string + * [.fromJSON(json)](#JwtPresentation.fromJSON) ⇒ [JwtPresentation](#JwtPresentation) + + + +### new JwtPresentation(values) +Constructs a new presentation. + + +| Param | Type | +| --- | --- | +| values | IJwtPresentation | + + + +### jwtPresentation.context() ⇒ Array.<(string\|Record.<string, any>)> +Returns a copy of the JSON-LD context(s) applicable to the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.id() ⇒ string \| undefined +Returns a copy of the unique `URI` identifying the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.type() ⇒ Array.<string> +Returns a copy of the URIs defining the type of the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.verifiableCredential() ⇒ [Array.<Jwt>](#Jwt) +Returns a copy of the [Credential](#Credential)(s) expressing the claims of the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.holder() ⇒ string +Returns a copy of the URI of the entity that generated the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.refreshService() ⇒ Array.<RefreshService> +Returns a copy of the service(s) used to refresh an expired [Credential](#Credential) in the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.termsOfUse() ⇒ Array.<Policy> +Returns a copy of the terms-of-use specified by the presentation holder + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.proof() ⇒ Map.<string, any> \| undefined +Returns a copy of the proof property. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.properties() ⇒ Map.<string, any> +Returns a copy of the miscellaneous properties on the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.clone() ⇒ [JwtPresentation](#JwtPresentation) +Deep clones the object. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### JwtPresentation.BaseContext() ⇒ string +Returns the base JSON-LD context. + +**Kind**: static method of [JwtPresentation](#JwtPresentation) + + +### JwtPresentation.BaseType() ⇒ string +Returns the base type. + +**Kind**: static method of [JwtPresentation](#JwtPresentation) + + +### JwtPresentation.fromJSON(json) ⇒ [JwtPresentation](#JwtPresentation) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwtPresentation](#JwtPresentation) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JwtPresentationOptions +**Kind**: global class + +* [JwtPresentationOptions](#JwtPresentationOptions) + * [new JwtPresentationOptions(options)](#new_JwtPresentationOptions_new) + * _instance_ + * [.toJSON()](#JwtPresentationOptions+toJSON) ⇒ any + * [.clone()](#JwtPresentationOptions+clone) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) + * _static_ + * [.default()](#JwtPresentationOptions.default) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) + * [.fromJSON(json)](#JwtPresentationOptions.fromJSON) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) + + + +### new JwtPresentationOptions(options) +Creates a new `JwtPresentationOptions` from the given fields. + +Throws an error if any of the options are invalid. + + +| Param | Type | +| --- | --- | +| options | IJwtPresentationOptions \| undefined | + + + +### jwtPresentationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwtPresentationOptions](#JwtPresentationOptions) + + +### jwtPresentationOptions.clone() ⇒ [JwtPresentationOptions](#JwtPresentationOptions) +Deep clones the object. + +**Kind**: instance method of [JwtPresentationOptions](#JwtPresentationOptions) + + +### JwtPresentationOptions.default() ⇒ [JwtPresentationOptions](#JwtPresentationOptions) +Creates a new `JwtPresentationOptions` with defaults. + +**Kind**: static method of [JwtPresentationOptions](#JwtPresentationOptions) + + +### JwtPresentationOptions.fromJSON(json) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwtPresentationOptions](#JwtPresentationOptions) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JwtPresentationValidationOptions +Options to declare validation criteria when validating presentation. + +**Kind**: global class + +* [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + * [new JwtPresentationValidationOptions(options)](#new_JwtPresentationValidationOptions_new) + * _instance_ + * [.toJSON()](#JwtPresentationValidationOptions+toJSON) ⇒ any + * [.clone()](#JwtPresentationValidationOptions+clone) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + * _static_ + * [.default()](#JwtPresentationValidationOptions.default) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + * [.fromJSON(json)](#JwtPresentationValidationOptions.fromJSON) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + + +### new JwtPresentationValidationOptions(options) +Creates a new `JwtPresentationValidationOptions` from the given fields. + +Throws an error if any of the options are invalid. + + +| Param | Type | +| --- | --- | +| options | IJwtPresentationValidationOptions | + + + +### jwtPresentationValidationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + +### jwtPresentationValidationOptions.clone() ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) +Deep clones the object. + +**Kind**: instance method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + +### JwtPresentationValidationOptions.default() ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) +Creates a new `JwtPresentationValidationOptions` with defaults. + +**Kind**: static method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + +### JwtPresentationValidationOptions.fromJSON(json) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JwtPresentationValidator +**Kind**: global class + +* [JwtPresentationValidator](#JwtPresentationValidator) + * [new JwtPresentationValidator(signature_verifier)](#new_JwtPresentationValidator_new) + * _instance_ + * [.validate(presentation_jwt, holder, issuers, options, fail_fast)](#JwtPresentationValidator+validate) ⇒ [DecodedJwtPresentation](#DecodedJwtPresentation) + * _static_ + * [.checkStructure(presentation)](#JwtPresentationValidator.checkStructure) + * [.extractDids(presentation)](#JwtPresentationValidator.extractDids) ⇒ JwtPresentationDids + + + +### new JwtPresentationValidator(signature_verifier) +Creates a new `JwtPresentationValidator`. If a `signature_verifier` is provided it will be used when +verifying decoded JWS signatures, otherwise the default which is only capable of handling the `EdDSA` +algorithm will be used. + + +| Param | Type | +| --- | --- | +| signature_verifier | IJwsVerifier \| undefined | + + + +### jwtPresentationValidator.validate(presentation_jwt, holder, issuers, options, fail_fast) ⇒ [DecodedJwtPresentation](#DecodedJwtPresentation) +**Kind**: instance method of [JwtPresentationValidator](#JwtPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation_jwt | [Jwt](#Jwt) | +| holder | [CoreDocument](#CoreDocument) \| IToCoreDocument | +| issuers | Array.<(CoreDocument\|IToCoreDocument)> | +| options | [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) | +| fail_fast | number | + + + +### JwtPresentationValidator.checkStructure(presentation) +**Kind**: static method of [JwtPresentationValidator](#JwtPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation | [JwtPresentation](#JwtPresentation) | + + + +### JwtPresentationValidator.extractDids(presentation) ⇒ JwtPresentationDids +**Kind**: static method of [JwtPresentationValidator](#JwtPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation | [Jwt](#Jwt) | + ## KeyPair @@ -5431,18 +5825,10 @@ This is possible because Ed25519 is birationally equivalent to Curve25519 used b | --- | --- | | publicKey | Uint8Array | - - -## KeyType -**Kind**: global variable ## StateMetadataEncoding **Kind**: global variable - - -## MethodRelationship -**Kind**: global variable ## StatusCheck @@ -5521,12 +5907,36 @@ Return all errors that occur during validation. Return after the first error occurs. **Kind**: global variable - + -## start() -Initializes the console error panic hook for better error messages +## KeyType +**Kind**: global variable + + +## MethodRelationship +**Kind**: global variable + + +## verifyEdDSA(alg, signingInput, decodedSignature, publicKey) +Verify a JWS signature secured with the `JwsAlgorithm::EdDSA` algorithm. +Only the `EdCurve::Ed25519` variant is supported for now. + +This function is useful when one is building an `IJwsVerifier` that extends the default provided by +the IOTA Identity Framework. + +# Warning +This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this +prior to calling the function. **Kind**: global function + +| Param | Type | +| --- | --- | +| alg | JwsAlgorithm | +| signingInput | Uint8Array | +| decodedSignature | Uint8Array | +| publicKey | [Jwk](#Jwk) | + ## encodeB64(data) ⇒ string @@ -5549,25 +5959,9 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | - - -## verifyEdDSA(alg, signingInput, decodedSignature, publicKey) -Verify a JWS signature secured with the `JwsAlgorithm::EdDSA` algorithm. -Only the `EdCurve::Ed25519` variant is supported for now. - -This function is useful when one is building an `IJwsVerifier` that extends the default provided by -the IOTA Identity Framework. + -# Warning -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 **Kind**: global function - -| Param | Type | -| --- | --- | -| alg | JwsAlgorithm | -| signingInput | Uint8Array | -| decodedSignature | Uint8Array | -| publicKey | [Jwk](#Jwk) | - diff --git a/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs b/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs index 14dfb1ce5a..4630c82e45 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs @@ -37,3 +37,9 @@ impl WasmDecodedJwtCredential { WasmCredential(self.0.credential) } } + +impl From for WasmDecodedJwtCredential { + fn from(credential: DecodedJwtCredential) -> Self { + Self(credential) + } +} diff --git a/bindings/wasm/src/credential/jwt_credential_validation/options.rs b/bindings/wasm/src/credential/jwt_credential_validation/options.rs index 84a40393fe..9c1af47991 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/options.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/options.rs @@ -69,6 +69,5 @@ interface IJwtCredentialValidationOptions { readonly status?: StatusCheck; /** Options which affect the verification of the signature on the credential. */ - readonly verifierOptions?: VerifierOptions; - + readonly verifierOptions?: JwsVerificationOptions; }"#; diff --git a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs new file mode 100644 index 0000000000..05209b90dd --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs @@ -0,0 +1,147 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Context; +use identity_iota::core::Object; +use identity_iota::credential::JwtPresentation; +use identity_iota::credential::JwtPresentationBuilder; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +use crate::common::ArrayString; +use crate::common::MapStringAny; +use crate::credential::jwt_presentation::jwt_presentation_builder::IJwtPresentation; +use crate::credential::ArrayContext; +use crate::credential::ArrayJwt; +use crate::credential::ArrayPolicy; +use crate::credential::ArrayRefreshService; +use crate::credential::WasmJwt; +use crate::error::Result; +use crate::error::WasmResult; + +#[wasm_bindgen(js_name = JwtPresentation, inspectable)] +pub struct WasmJwtPresentation(pub(crate) JwtPresentation); + +#[wasm_bindgen(js_class = JwtPresentation)] +impl WasmJwtPresentation { + /// Returns the base JSON-LD context. + #[wasm_bindgen(js_name = "BaseContext")] + pub fn base_context() -> Result { + match JwtPresentation::::base_context() { + Context::Url(url) => Ok(url.to_string()), + Context::Obj(_) => Err(JsError::new("JwtPresentation.BaseContext should be a single URL").into()), + } + } + + /// Returns the base type. + #[wasm_bindgen(js_name = "BaseType")] + pub fn base_type() -> String { + JwtPresentation::::base_type().to_owned() + } + + /// Constructs a new presentation. + #[wasm_bindgen(constructor)] + pub fn new(values: IJwtPresentation) -> Result { + let builder: JwtPresentationBuilder = JwtPresentationBuilder::try_from(values)?; + builder.build().map(Self).wasm_result() + } + + /// Returns a copy of the JSON-LD context(s) applicable to the presentation. + #[wasm_bindgen] + pub fn context(&self) -> Result { + self + .0 + .context + .iter() + .map(JsValue::from_serde) + .collect::>() + .wasm_result() + .map(|value| value.unchecked_into::()) + } + + /// Returns a copy of the unique `URI` identifying the presentation. + #[wasm_bindgen] + pub fn id(&self) -> Option { + self.0.id.as_ref().map(|url| url.to_string()) + } + + /// Returns a copy of the URIs defining the type of the presentation. + #[wasm_bindgen(js_name = "type")] + pub fn types(&self) -> ArrayString { + self + .0 + .types + .iter() + .map(|s| s.as_str()) + .map(JsValue::from_str) + .collect::() + .unchecked_into::() + } + + /// Returns the JWT credentials expressing the claims of the presentation. + #[wasm_bindgen(js_name = verifiableCredential)] + pub fn verifiable_credential(&self) -> ArrayJwt { + self + .0 + .verifiable_credential + .iter() + .cloned() + .map(WasmJwt::new) + .map(JsValue::from) + .collect::() + .unchecked_into::() + } + + /// Returns a copy of the URI of the entity that generated the presentation. + #[wasm_bindgen] + pub fn holder(&self) -> String { + self.0.holder.as_ref().to_string() + } + + /// Returns a copy of the service(s) used to refresh an expired {@link Credential} in the presentation. + #[wasm_bindgen(js_name = "refreshService")] + pub fn refresh_service(&self) -> Result { + self + .0 + .refresh_service + .iter() + .map(JsValue::from_serde) + .collect::>() + .wasm_result() + .map(|value| value.unchecked_into::()) + } + + /// Returns a copy of the terms-of-use specified by the presentation holder + #[wasm_bindgen(js_name = "termsOfUse")] + pub fn terms_of_use(&self) -> Result { + self + .0 + .terms_of_use + .iter() + .map(JsValue::from_serde) + .collect::>() + .wasm_result() + .map(|value| value.unchecked_into::()) + } + + /// Optional proof that can be verified by users in addition to JWS. + #[wasm_bindgen] + pub fn proof(&self) -> Result> { + self.0.proof.clone().map(MapStringAny::try_from).transpose() + } + + /// Returns a copy of the miscellaneous properties on the presentation. + #[wasm_bindgen] + pub fn properties(&self) -> Result { + MapStringAny::try_from(&self.0.properties) + } +} + +impl_wasm_json!(WasmJwtPresentation, JwtPresentation); +impl_wasm_clone!(WasmJwtPresentation, JwtPresentation); + +impl From for WasmJwtPresentation { + fn from(presentation: JwtPresentation) -> WasmJwtPresentation { + Self(presentation) + } +} diff --git a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs new file mode 100644 index 0000000000..5fee2025a3 --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs @@ -0,0 +1,102 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Context; +use identity_iota::core::Object; +use identity_iota::core::OneOrMany; +use identity_iota::core::Url; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtPresentationBuilder; +use identity_iota::credential::Policy; +use identity_iota::credential::RefreshService; +use proc_typescript::typescript; +use wasm_bindgen::prelude::*; + +use crate::error::WasmResult; + +impl TryFrom for JwtPresentationBuilder { + type Error = JsValue; + + fn try_from(values: IJwtPresentation) -> std::result::Result { + let IJwtPresentationHelper { + context, + id, + r#type, + verifiable_credential, + holder, + refresh_service, + terms_of_use, + properties, + } = values.into_serde::().wasm_result()?; + + let mut builder: JwtPresentationBuilder = + JwtPresentationBuilder::new(Url::parse(holder).wasm_result()?, properties); + + if let Some(context) = context { + for value in context.into_vec() { + builder = builder.context(value); + } + } + if let Some(id) = id { + builder = builder.id(Url::parse(id).wasm_result()?); + } + if let Some(types) = r#type { + for value in types.iter() { + builder = builder.type_(value); + } + } + for credential in verifiable_credential.into_vec() { + builder = builder.credential(Jwt::new(credential)); + } + if let Some(refresh_service) = refresh_service { + for service in refresh_service.into_vec() { + builder = builder.refresh_service(service); + } + } + if let Some(terms_of_use) = terms_of_use { + for policy in terms_of_use.into_vec() { + builder = builder.terms_of_use(policy); + } + } + + Ok(builder) + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwtPresentation")] + pub type IJwtPresentation; +} + +/// Fields for constructing a new {@link JwtPresentation}. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[typescript(name = "IJwtPresentation", readonly, optional)] +struct IJwtPresentationHelper { + /// The JSON-LD context(s) applicable to the presentation. + #[typescript(type = "string | Record | Array>")] + context: Option>, + /// A unique URI that may be used to identify the presentation. + #[typescript(type = "string")] + id: Option, + /// One or more URIs defining the type of the presentation. Contains the base context by default. + #[typescript(name = "type", type = "string | Array")] + r#type: Option>, + /// JWT Credential(s) expressing the claims of the presentation. + #[typescript(optional = false, name = "verifiableCredential", type = "string | Array")] + verifiable_credential: OneOrMany, + /// The entity that generated the presentation. + #[typescript(optional = false, type = "string | CoreDID | IotaDID ")] + holder: String, + /// Service(s) used to refresh an expired {@link Credential} in the presentation. + #[typescript(name = "refreshService", type = "RefreshService | Array")] + refresh_service: Option>, + /// Terms-of-use specified by the presentation holder. + #[typescript(name = "termsOfUse", type = "Policy | Array")] + terms_of_use: Option>, + /// Miscellaneous properties. + #[serde(flatten)] + #[typescript(optional = false, name = "[properties: string]", type = "unknown")] + properties: Object, +} diff --git a/bindings/wasm/src/credential/jwt_presentation/mod.rs b/bindings/wasm/src/credential/jwt_presentation/mod.rs new file mode 100644 index 0000000000..3f4bb3b90c --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod jwt_presentation; +mod jwt_presentation_builder; + +pub use self::jwt_presentation::*; diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs new file mode 100644 index 0000000000..71ae171405 --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs @@ -0,0 +1,79 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::credential::DecodedJwtPresentation; +use wasm_bindgen::prelude::*; + +use crate::common::WasmTimestamp; +use crate::credential::jwt_presentation::WasmJwtPresentation; +use crate::credential::ArrayDecodedJwtCredential; +use crate::credential::WasmDecodedJwtCredential; +use crate::jose::WasmJwsHeader; + +/// A cryptographically verified and decoded presentation. +/// +/// Note that having an instance of this type only means the JWS it was constructed from was verified. +/// It does not imply anything about a potentially present proof property on the presentation itself. +#[wasm_bindgen(js_name = DecodedJwtPresentation)] +pub struct WasmDecodedJwtPresentation(pub(crate) DecodedJwtPresentation); + +#[wasm_bindgen(js_class = DecodedJwtPresentation)] +impl WasmDecodedJwtPresentation { + #[wasm_bindgen] + pub fn presentation(&self) -> WasmJwtPresentation { + WasmJwtPresentation(self.0.presentation.clone()) + } + + /// Returns a copy of the protected header parsed from the decoded JWS. + #[wasm_bindgen(js_name = protectedHeader)] + pub fn protected_header(&self) -> WasmJwsHeader { + WasmJwsHeader(self.0.header.as_ref().clone()) + } + + /// Consumes the object and returns the decoded presentation. + /// + /// ### Warning + /// This destroys the `DecodedJwtPresentation` object. + #[wasm_bindgen(js_name = intoPresentation)] + pub fn into_presentation(self) -> WasmJwtPresentation { + WasmJwtPresentation(self.0.presentation) + } + + /// The expiration date parsed from the JWT claims. + #[wasm_bindgen(js_name = expirationDate)] + pub fn expiration_date(&self) -> Option { + self.0.expiration_date.map(WasmTimestamp::from) + } + + /// The issuance date parsed from the JWT claims. + #[wasm_bindgen(js_name = "issuanceDate")] + pub fn issuance_date(&self) -> Option { + self.0.issuance_date.map(WasmTimestamp::from) + } + + /// The `aud` property parsed from JWT claims. + #[wasm_bindgen] + pub fn audience(&self) -> Option { + self.0.aud.clone().map(|aud| aud.to_string()) + } + + /// The credentials included in the presentation (decoded). + #[wasm_bindgen(js_name = "credentials")] + pub fn credentials(&self) -> ArrayDecodedJwtCredential { + self + .0 + .credentials + .iter() + .cloned() + .map(WasmDecodedJwtCredential::from) + .map(JsValue::from) + .collect::() + .unchecked_into::() + } +} + +impl From for WasmDecodedJwtPresentation { + fn from(decoded_presentation: DecodedJwtPresentation) -> Self { + Self(decoded_presentation) + } +} diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs new file mode 100644 index 0000000000..b159bc16b9 --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs @@ -0,0 +1,118 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::decoded_jwt_presentation::WasmDecodedJwtPresentation; +use super::options::WasmJwtPresentationValidationOptions; +use crate::common::ImportedDocumentLock; +use crate::common::ImportedDocumentReadGuard; +use crate::credential::jwt_presentation::WasmJwtPresentation; +use crate::credential::JwtPresentationDids; +use crate::credential::WasmFailFast; +use crate::credential::WasmJwt; +use crate::did::ArrayIToCoreDocument; +use crate::did::IToCoreDocument; +use crate::error::Result; +use crate::error::WasmResult; +use crate::verification::IJwsVerifier; +use crate::verification::WasmJwsVerifier; +use identity_iota::core::Object; +use identity_iota::core::OneOrMany; +use identity_iota::credential::JwtPresentationValidator; +use identity_iota::did::CoreDID; +use std::collections::BTreeMap; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JwtPresentationValidator, inspectable)] +pub struct WasmJwtPresentationValidator(JwtPresentationValidator); + +#[wasm_bindgen(js_class = JwtPresentationValidator)] +impl WasmJwtPresentationValidator { + /// Creates a new `JwtPresentationValidator`. If a `signature_verifier` is provided it will be used when + /// verifying decoded JWS signatures, otherwise the default which is only capable of handling the `EdDSA` + /// algorithm will be used. + #[wasm_bindgen(constructor)] + pub fn new(signature_verifier: Option) -> WasmJwtPresentationValidator { + let signature_verifier = WasmJwsVerifier::new(signature_verifier); + WasmJwtPresentationValidator(JwtPresentationValidator::with_signature_verifier(signature_verifier)) + } + + /// Validates a `JwtPresentation`. + /// + /// The following properties are validated according to `options`: + /// - the JWT can be decoded into semantically valid presentation. + /// - the expiration and issuance date contained in the JWT claims. + /// - the holder's signature. + /// - the relationship between the holder and the credential subjects. + /// - the signatures and some properties of the constituent credentials (see `CredentialValidator`). + /// + /// Validation is done with respect to the properties set in `options`. + /// + /// # Warning + /// The lack of an error returned from this method is in of itself not enough to conclude that the presentation can be + /// trusted. This section contains more information on additional checks that should be carried out before and after + /// calling this method. + /// + /// ## The state of the supplied DID Documents. + /// The caller must ensure that the DID Documents in `holder` and `issuers` are up-to-date. + /// + /// ## Properties that are not validated + /// There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as: + /// `credentialStatus`, `type`, `credentialSchema`, `refreshService`, **and more**. + /// These should be manually checked after validation, according to your requirements. + /// + /// # Errors + /// An error is returned whenever a validated condition is not satisfied or when decoding fails. + #[wasm_bindgen] + pub fn validate( + &self, + presentation_jwt: &WasmJwt, + holder: &IToCoreDocument, + issuers: &ArrayIToCoreDocument, + validation_options: &WasmJwtPresentationValidationOptions, + fail_fast: WasmFailFast, + ) -> Result { + let issuer_locks: Vec = issuers.into(); + let issuers_guards: Vec> = + issuer_locks.iter().map(ImportedDocumentLock::blocking_read).collect(); + + let holder_lock = ImportedDocumentLock::from(holder); + let holder_guard = holder_lock.blocking_read(); + + self + .0 + .validate( + &presentation_jwt.0, + &holder_guard, + &issuers_guards, + &validation_options.0, + fail_fast.into(), + ) + .map(WasmDecodedJwtPresentation::from) + .wasm_result() + } + + /// Validates the semantic structure of the `JwtPresentation`. + #[wasm_bindgen(js_name = checkStructure)] + pub fn check_structure(presentation: &WasmJwtPresentation) -> Result<()> { + JwtPresentationValidator::check_structure(&presentation.0).wasm_result()?; + Ok(()) + } + + /// Attempt to extract the holder of the presentation and the issuers of the included + /// credentials. + /// + /// # Errors: + /// * If deserialization/decoding of the presentation or any of the constituent credentials + /// fails. + /// * If the holder or any of the issuers can't be parsed as DIDs. + #[wasm_bindgen(js_name = extractDids)] + pub fn extract_dids(presentation: &WasmJwt) -> Result { + let (holder, issuers) = + JwtPresentationValidator::extract_dids::(&presentation.0).wasm_result()?; + let mut map = BTreeMap::<&str, OneOrMany>::new(); + map.insert("holder", OneOrMany::One(holder)); + map.insert("issuers", OneOrMany::Many(issuers)); + + Ok(JsValue::from_serde(&map).wasm_result()?.unchecked_into()) + } +} diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs new file mode 100644 index 0000000000..12c556852e --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jwt_presentation; +mod jwt_presentation_validator; +mod options; + +pub use self::decoded_jwt_presentation::*; +pub use self::jwt_presentation_validator::*; +pub use self::options::*; diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/options.rs b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs new file mode 100644 index 0000000000..7fd6f4781d --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs @@ -0,0 +1,85 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JwtPresentationValidationOptions; +use wasm_bindgen::prelude::*; + +/// Options to declare validation criteria when validating presentation. +#[wasm_bindgen(js_name = JwtPresentationValidationOptions)] +pub struct WasmJwtPresentationValidationOptions(pub(crate) JwtPresentationValidationOptions); + +#[wasm_bindgen(js_class = JwtPresentationValidationOptions)] +impl WasmJwtPresentationValidationOptions { + /// Creates a new `JwtPresentationValidationOptions` from the given fields. + /// + /// Throws an error if any of the options are invalid. + #[wasm_bindgen(constructor)] + pub fn new(options: IJwtPresentationValidationOptions) -> Result { + let options: JwtPresentationValidationOptions = options.into_serde().wasm_result()?; + Ok(WasmJwtPresentationValidationOptions::from(options)) + } + + /// Creates a new `JwtPresentationValidationOptions` with defaults. + #[allow(clippy::should_implement_trait)] + #[wasm_bindgen] + pub fn default() -> WasmJwtPresentationValidationOptions { + WasmJwtPresentationValidationOptions::from(JwtPresentationValidationOptions::default()) + } +} + +impl_wasm_json!(WasmJwtPresentationValidationOptions, JwtPresentationValidationOptions); +impl_wasm_clone!(WasmJwtPresentationValidationOptions, JwtPresentationValidationOptions); + +impl From for WasmJwtPresentationValidationOptions { + fn from(options: JwtPresentationValidationOptions) -> Self { + Self(options) + } +} + +impl From for JwtPresentationValidationOptions { + fn from(options: WasmJwtPresentationValidationOptions) -> Self { + options.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwtPresentationValidationOptions")] + pub type IJwtPresentationValidationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JWT_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#" +/** Holds options to create a new `JwtPresentationValidationOptions`. */ +interface IJwtPresentationValidationOptions { + /** + * Options which affect the validation of *all* credentials in the presentation. + */ + readonly sharedValidationOptions?: JwtCredentialValidationOptions; + + /** + * Options which affect the verification of the signature on the presentation. + */ + readonly presentationVerifierOptions?: JwsVerificationOptions; + + /** + * Declare how the presentation's credential subjects must relate to the holder. + * + * Default: `SubjectHolderRelationship.AlwaysSubject` + */ + readonly subjectHolderRelationship?: SubjectHolderRelationship; + + /** + * Declare that the presentation is **not** considered valid if it expires before this `Timestamp`. + * Uses the current datetime during validation if not set. + */ + readonly earliestExpiryDate?: Timestamp; + + /** + * Declare that the presentation is **not** considered valid if it was issued later than this `Timestamp`. + * Uses the current datetime during validation if not set. + */ + readonly latestIssuanceDate?: Timestamp; +}"#; diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index c5e562d143..63b6654d2f 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2022 IOTA Stiftung +// Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 #![allow(clippy::module_inception)] @@ -10,6 +10,8 @@ pub use self::domain_linkage_configuration::WasmDomainLinkageConfiguration; pub use self::jws::WasmJws; pub use self::jwt::WasmJwt; pub use self::jwt_credential_validation::*; +pub use self::jwt_presentation::*; +pub use self::jwt_presentation_validation::*; pub use self::presentation::WasmPresentation; pub use self::presentation_builder::*; pub use self::presentation_validator::WasmPresentationValidator; @@ -28,6 +30,8 @@ mod domain_linkage_validator; mod jws; mod jwt; mod jwt_credential_validation; +mod jwt_presentation; +mod jwt_presentation_validation; mod linked_domain_service; mod presentation; mod presentation_builder; diff --git a/bindings/wasm/src/credential/types.rs b/bindings/wasm/src/credential/types.rs index 54c94c86b9..615d4c0441 100644 --- a/bindings/wasm/src/credential/types.rs +++ b/bindings/wasm/src/credential/types.rs @@ -31,6 +31,18 @@ extern "C" { #[wasm_bindgen(typescript_type = "Array")] pub type ArrayCredential; + + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayDecodedJwtCredential; + + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayJwt; + + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayCoreDID; + + #[wasm_bindgen(typescript_type = "JwtPresentationDids")] + pub type JwtPresentationDids; } #[wasm_bindgen(typescript_custom_section)] @@ -126,3 +138,19 @@ interface Subject { /** Additional properties of the credential subject. */ readonly [properties: string]: unknown; }"#; + +#[wasm_bindgen(typescript_custom_section)] +const I_JWT_PRESENTATOIN_DIDS: &'static str = r#" +/** + * DIDs of the presentation holder and the issuers of the contained credentials. + */ +interface JwtPresentationDids { + /** + * Presentation holder. + */ + holder: CoreDID; + /** + * Issuers of the verifiable credentials contained in the presentation. + */ + issuers: Array; +}"#; diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 99683450fd..6b0150692b 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -18,6 +18,7 @@ use crate::common::UOneOrManyNumber; use crate::credential::WasmCredential; use crate::credential::WasmJws; use crate::credential::WasmJwt; +use crate::credential::WasmJwtPresentation; use crate::crypto::WasmProofOptions; use crate::did::service::WasmService; use crate::did::wasm_did_url::WasmDIDUrl; @@ -27,6 +28,7 @@ use crate::error::WasmResult; use crate::jose::WasmDecodedJws; use crate::jose::WasmJwsAlgorithm; use crate::storage::WasmJwsSignatureOptions; +use crate::storage::WasmJwtPresentationOptions; use crate::storage::WasmStorage; use crate::storage::WasmStorageInner; use crate::verification::IJwsVerifier; @@ -41,6 +43,8 @@ use identity_iota::core::OneOrSet; use identity_iota::core::OrderedSet; use identity_iota::core::Url; use identity_iota::credential::Credential; +use identity_iota::credential::JwtPresentation; +use identity_iota::credential::JwtPresentationOptions; use identity_iota::credential::RevocationDocumentExt; use identity_iota::crypto::PrivateKey; use identity_iota::crypto::ProofOptions; @@ -702,13 +706,11 @@ impl WasmCoreDocument { Ok(promise.unchecked_into()) } - /// Produces a JWS where the payload is produced from the given `credential` + /// Produces a JWT where the payload is produced from the given `credential` /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. - // TODO: Perhaps this should be called `signCredential` (and the old `signCredential` method would have to be updated - // or removed)? #[wasm_bindgen(js_name = createCredentialJwt)] pub fn create_credential_jwt( &self, @@ -733,6 +735,44 @@ impl WasmCoreDocument { }); Ok(promise.unchecked_into()) } + + /// Produces a JWT where the payload is produced from the given presentation. + /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). + /// + /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be + /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwt)] + pub fn create_presentation_jwt( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmJwtPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: JwtPresentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .sign_presentation( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } } #[wasm_bindgen] diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 122f2b0cf9..a345af664a 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_iota::credential::CompoundJwtPresentationValidationError; use identity_iota::resolver; use identity_iota::storage::key_id_storage::KeyIdStorageError; use identity_iota::storage::key_id_storage::KeyIdStorageErrorKind; @@ -259,6 +260,15 @@ impl From for WasmError<'_> { + fn from(error: CompoundJwtPresentationValidationError) -> Self { + Self { + name: Cow::Borrowed("CompoundJwtPresentationValidationError"), + message: Cow::Owned(ErrorMessage(&error).to_string()), + } + } +} + /// Convenience struct to convert Result to errors in the Rust library. pub struct JsValueResult(pub(crate) Result); diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 5f21d0d465..ff965ef8b0 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -8,6 +8,8 @@ use identity_iota::core::OrderedSet; use identity_iota::core::Timestamp; use identity_iota::core::Url; use identity_iota::credential::Credential; +use identity_iota::credential::JwtPresentation; +use identity_iota::credential::JwtPresentationOptions; use identity_iota::credential::Presentation; use identity_iota::crypto::PrivateKey; use identity_iota::crypto::ProofOptions; @@ -46,6 +48,7 @@ use crate::common::WasmTimestamp; use crate::credential::WasmCredential; use crate::credential::WasmJws; use crate::credential::WasmJwt; +use crate::credential::WasmJwtPresentation; use crate::credential::WasmPresentation; use crate::crypto::WasmProofOptions; use crate::did::CoreDocumentLock; @@ -65,6 +68,7 @@ use crate::iota::WasmStateMetadataEncoding; use crate::jose::WasmDecodedJws; use crate::jose::WasmJwsAlgorithm; use crate::storage::WasmJwsSignatureOptions; +use crate::storage::WasmJwtPresentationOptions; use crate::storage::WasmStorage; use crate::storage::WasmStorageInner; use crate::verification::IJwsVerifier; @@ -827,8 +831,6 @@ impl WasmIotaDocument { /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. - // TODO: Perhaps this should be called `signCredential` (and the old `signCredential` method would have to be updated - // or removed)? #[wasm_bindgen(js_name = createCredentialJwt)] pub fn create_credential_jwt( &self, @@ -853,6 +855,44 @@ impl WasmIotaDocument { }); Ok(promise.unchecked_into()) } + + /// Produces a JWT where the payload is produced from the given presentation. + /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). + /// + /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be + /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwt)] + pub fn create_presentation_jwt( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmJwtPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: JwtPresentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .sign_presentation( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } } impl From for WasmIotaDocument { diff --git a/bindings/wasm/src/storage/jwt_presentation_options.rs b/bindings/wasm/src/storage/jwt_presentation_options.rs new file mode 100644 index 0000000000..ef41c6ff3a --- /dev/null +++ b/bindings/wasm/src/storage/jwt_presentation_options.rs @@ -0,0 +1,81 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JwtPresentationOptions; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JwtPresentationOptions)] +pub struct WasmJwtPresentationOptions(pub(crate) JwtPresentationOptions); + +#[wasm_bindgen(js_class = JwtPresentationOptions)] +impl WasmJwtPresentationOptions { + /// Creates a new `JwtPresentationOptions` from the given fields. + /// + /// Throws an error if any of the options are invalid. + #[wasm_bindgen(constructor)] + pub fn new(options: Option) -> Result { + if let Some(options) = options { + let options: JwtPresentationOptions = options.into_serde().wasm_result()?; + Ok(WasmJwtPresentationOptions::from(options)) + } else { + Ok(WasmJwtPresentationOptions::from(JwtPresentationOptions::default())) + } + } + + /// Creates a new `JwtPresentationOptions` with defaults. + #[allow(clippy::should_implement_trait)] + #[wasm_bindgen] + pub fn default() -> WasmJwtPresentationOptions { + WasmJwtPresentationOptions::from(JwtPresentationOptions::default()) + } +} + +impl_wasm_json!(WasmJwtPresentationOptions, JwtPresentationOptions); +impl_wasm_clone!(WasmJwtPresentationOptions, JwtPresentationOptions); + +impl From for WasmJwtPresentationOptions { + fn from(options: JwtPresentationOptions) -> Self { + Self(options) + } +} + +impl From for JwtPresentationOptions { + fn from(options: WasmJwtPresentationOptions) -> Self { + options.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwtPresentationOptions")] + pub type IJwtPresentationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_JWT_PRESENTATION_OPTIONS: &'static str = r#" +/** Options to be set in the JWT claims of a verifiable presentation. */ +interface IJwtPresentationOptions { + /** + * Set the presentation's expiration date. + * Default: `undefined`. + **/ + readonly expirationDate?: Timestamp; + + /** + * Set the presentation's issuance date. + * Default: current datetime. + */ + readonly issuanceDate?: Timestamp; + + /** + * Sets the audience for presentation (`aud` property in JWT claims). + * + * ## Note: + * Value must be a valid URL. + * + * Default: `undefined`. + */ + readonly audience?: string; +}"#; diff --git a/bindings/wasm/src/storage/mod.rs b/bindings/wasm/src/storage/mod.rs index 88a36f4c04..8295d95e88 100644 --- a/bindings/wasm/src/storage/mod.rs +++ b/bindings/wasm/src/storage/mod.rs @@ -3,6 +3,7 @@ mod jwk_gen_output; mod jwk_storage; +mod jwt_presentation_options; mod key_id_storage; mod method_digest; mod signature_options; @@ -10,6 +11,7 @@ mod wasm_storage; pub use jwk_gen_output::*; pub use jwk_storage::*; +pub use jwt_presentation_options::*; pub use key_id_storage::*; pub use method_digest::*; pub use signature_options::*; diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index 100132839b..d9c3b1dd4a 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -1,8 +1,9 @@ const assert = require("assert"); -import { RandomHelper } from "@iota/util.js"; import { CoreDocument, Credential, + DecodedJwtPresentation, + Duration, FailFast, IJwsVerifier, IotaDocument, @@ -10,11 +11,17 @@ import { JwsAlgorithm, JwsSignatureOptions, JwsVerificationOptions, + Jwt, JwtCredentialValidationOptions, JwtCredentialValidator, + JwtPresentation, + JwtPresentationOptions, + JwtPresentationValidationOptions, + JwtPresentationValidator, MethodDigest, MethodScope, Storage, + Timestamp, VerificationMethod, verifyEdDSA, } from "../node"; @@ -109,30 +116,23 @@ describe("#JwkStorageDocument", function() { // Check that the credentialJwt can be decoded and verified let credentialValidator = new JwtCredentialValidator(); - const credentialRetrieved = credentialValidator.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), - FailFast.FirstError, - ).credential(); + const credentialRetrieved = credentialValidator + .validate(credentialJwt, doc, JwtCredentialValidationOptions.default(), FailFast.FirstError) + .credential(); assert.deepStrictEqual(credentialRetrieved.toJSON(), credential.toJSON()); // Also check using our custom verifier let credentialValidatorCustom = new JwtCredentialValidator(customVerifier); - const credentialRetrievedCustom = credentialValidatorCustom.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), - FailFast.AllErrors, - ).credential(); + const credentialRetrievedCustom = credentialValidatorCustom + .validate(credentialJwt, doc, JwtCredentialValidationOptions.default(), FailFast.AllErrors) + .credential(); // Check that customVerifer.verify was indeed called assert.deepStrictEqual(customVerifier.verifications(), 2); assert.deepStrictEqual(credentialRetrievedCustom.toJSON(), credential.toJSON()); // Delete the method const methodId = (method as VerificationMethod).id(); - await doc.purgeMethod(storage, methodId); - // Check that the method can no longer be resolved. + await doc.purgeMethod(storage, methodId); // Check that the method can no longer be resolved. assert.deepStrictEqual(doc.resolveMethod(fragment), undefined); // The storage should now be empty assert.deepStrictEqual((storage.keyIdStorage() as KeyIdMemStore).count(), 0); @@ -186,58 +186,132 @@ describe("#JwkStorageDocument", function() { issuer: doc.id(), issuanceDate: "2010-01-01T00:00:00Z", }; + }); + + it("JwtPresentation should work", async () => { + const keystore = new JwkMemStore(); + const keyIdStore = new KeyIdMemStore(); + const storage = new Storage(keystore, keyIdStore); + const issuerDoc = new IotaDocument("tst1"); + const fragment = "#key-1"; + await issuerDoc.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + fragment, + MethodScope.VerificationMethod(), + ); + + const holderDoc = new IotaDocument("tst2"); + await holderDoc.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + fragment, + MethodScope.VerificationMethod(), + ); + + let customVerifier = new CustomVerifier(); + const credentialFields = { + context: "https://www.w3.org/2018/credentials/examples/v1", + id: "https://example.edu/credentials/3732", + type: "UniversityDegreeCredential", + credentialSubject: { + id: holderDoc.id(), + degree: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts", + }, + }, + issuer: issuerDoc.id(), + issuanceDate: Timestamp.nowUTC(), + }; const credential = new Credential(credentialFields); - // Create the JWT - const credentialJwt = await doc.createCredentialJwt(storage, fragment, credential, new JwsSignatureOptions()); + const credentialJwt: Jwt = await issuerDoc.createCredentialJwt( + storage, + fragment, + credential, + new JwsSignatureOptions(), + ); - // Check that the credentialJwt can be decoded and verified - let credentialValidator = new JwtCredentialValidator(); - const credentialRetrieved = credentialValidator.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), + const presentation = new JwtPresentation({ + holder: holderDoc.id(), + verifiableCredential: [credentialJwt.toString(), credentialJwt.toString()], + }); + + const expirationDate = Timestamp.nowUTC().checkedAdd(Duration.days(2)); + const audience = "did:test:123"; + const presentationJwt = await holderDoc.createPresentationJwt( + storage, + fragment, + presentation, + new JwsSignatureOptions(), + new JwtPresentationOptions({ + expirationDate, + issuanceDate: Timestamp.nowUTC(), + audience, + }), + ); + + let validator = new JwtPresentationValidator(customVerifier); + let decoded: DecodedJwtPresentation = validator.validate( + presentationJwt, + holderDoc, + [issuerDoc], + JwtPresentationValidationOptions.default(), FailFast.FirstError, - ).credential(); - assert.deepStrictEqual(credentialRetrieved.toJSON(), credential.toJSON()); + ); - // Also check using our custom verifier - let credentialValidatorCustom = new JwtCredentialValidator(customVerifier); - const credentialRetrievedCustom = credentialValidatorCustom.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), - FailFast.AllErrors, - ).credential(); - // Check that customVerifer.verify was indeed called - assert.deepStrictEqual(customVerifier.verifications(), 2); - assert.deepStrictEqual(credentialRetrievedCustom.toJSON(), credential.toJSON()); + assert.deepStrictEqual(decoded.credentials()[0].credential().toJSON(), credential.toJSON()); + assert.equal(decoded.expirationDate()!.toString(), expirationDate!.toString()); + assert.deepStrictEqual(decoded.presentation().toJSON(), presentation.toJSON()); + assert.equal(decoded.audience(), audience); - // Delete the method - const methodId = (method as VerificationMethod).id(); - await doc.purgeMethod(storage, methodId); - // Check that the method can no longer be resolved. - assert.deepStrictEqual(doc.resolveMethod(fragment), undefined); - // The storage should now be empty - assert.deepStrictEqual((storage.keyIdStorage() as KeyIdMemStore).count(), 0); - assert.deepStrictEqual((storage.keyStorage() as JwkMemStore).count(), 0); + // check issuance date validation. + let options = new JwtPresentationValidationOptions({ + latestIssuanceDate: Timestamp.nowUTC().checkedSub(Duration.days(1)), + }); + assert.throws(() => { + validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); + }); + + // Check expiration date validation. + options = new JwtPresentationValidationOptions({ + earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(1)), + }); + validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); + + options = new JwtPresentationValidationOptions({ + earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(3)), + }); + assert.throws(() => { + validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); + }); + + // Check `extractDids`. + let presentationDids = JwtPresentationValidator.extractDids(presentationJwt); + assert.equal(presentationDids.holder.toString(), holderDoc.id().toString()); + assert.equal(presentationDids.issuers.length, 2); + assert.equal(presentationDids.issuers[0].toString(), issuerDoc.id().toString()); + assert.equal(presentationDids.issuers[1].toString(), issuerDoc.id().toString()); }); -}); -class CustomVerifier implements IJwsVerifier { - private _verifications: number; + class CustomVerifier implements IJwsVerifier { + private _verifications: number; - constructor() { - this._verifications = 0; - } + constructor() { + this._verifications = 0; + } - public verifications(): number { - return this._verifications; - } + public verifications(): number { + return this._verifications; + } - public verify(alg: JwsAlgorithm, signingInput: Uint8Array, decodedSignature: Uint8Array, publicKey: Jwk): void { - verifyEdDSA(alg, signingInput, decodedSignature, publicKey); - this._verifications += 1; - return; + public verify(alg: JwsAlgorithm, signingInput: Uint8Array, decodedSignature: Uint8Array, publicKey: Jwk): void { + verifyEdDSA(alg, signingInput, decodedSignature, publicKey); + this._verifications += 1; + return; + } } -} +}); diff --git a/identity_credential/src/presentation/jwt_presentation_options.rs b/identity_credential/src/presentation/jwt_presentation_options.rs index 925ffe0812..926d4621a4 100644 --- a/identity_credential/src/presentation/jwt_presentation_options.rs +++ b/identity_credential/src/presentation/jwt_presentation_options.rs @@ -5,7 +5,8 @@ use identity_core::common::Timestamp; use identity_core::common::Url; /// Options to be set in the JWT claims of a verifiable presentation. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JwtPresentationOptions { /// Set the presentation's expiration date. /// Default: `None`. diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index c81c6a1130..d84c5130bb 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -22,7 +22,7 @@ pub struct DecodedJwtPresentation { pub header: Box, /// The expiration date parsed from the JWT claims. pub expiration_date: Option, - /// The issuance dated parsed from the JWT claims. + /// The issuance date parsed from the JWT claims. pub issuance_date: Option, /// The `aud` property parsed from the JWT claims. pub aud: Option, diff --git a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs index 595e6a8f95..605a7aa5a4 100644 --- a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs @@ -195,13 +195,6 @@ where Ok(decoded_jwt_presentation) } - /// Validates the semantic structure of the `JwtPresentation`. - pub fn check_structure(presentation: &JwtPresentation) -> Result<(), ValidationError> { - presentation - .check_structure() - .map_err(ValidationError::PresentationStructure) - } - fn validate_credentials( &self, presentation: &JwtPresentation, @@ -291,4 +284,11 @@ impl JwtPresentationValidator { } Ok((holder, issuers)) } + + /// Validates the semantic structure of the `JwtPresentation`. + pub fn check_structure(presentation: &JwtPresentation) -> Result<(), ValidationError> { + presentation + .check_structure() + .map_err(ValidationError::PresentationStructure) + } } diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index ade03d8b63..2177a9139a 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -81,7 +81,7 @@ pub trait JwkDocumentExt: private::Sealed { K: JwkStorage, I: KeyIdStorage; - /// Produces a JWS where the payload is produced from the given `credential` + /// Produces a JWT where the payload is produced from the given `credential` /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be @@ -98,7 +98,7 @@ pub trait JwkDocumentExt: private::Sealed { I: KeyIdStorage, T: ToOwned + Serialize + DeserializeOwned + Sync; - /// Produces a JWS where the payload is produced from the given `presentation` + /// Produces a JWT where the payload is produced from the given `presentation` /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be