From 7bb8621665fb33a5a6197d5f3c2f6590d876427f Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 29 Sep 2023 09:26:34 +0100 Subject: [PATCH 01/78] Add base types for the compliant DIDs --- packages/types/src/DidV2.ts | 54 +++++++++++++++++++++++++++++++++++++ yarn.lock | 4 +-- 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 packages/types/src/DidV2.ts diff --git a/packages/types/src/DidV2.ts b/packages/types/src/DidV2.ts new file mode 100644 index 0000000000..1e50c64aad --- /dev/null +++ b/packages/types/src/DidV2.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { KiltAddress } from './Address' + +type AuthenticationKeyType = '00' | '01' +type DidUriVersion = '' | `v${string}:` +type LightDidEncodedData = '' | `:${string}` + +/** + * A string containing a KILT DID Uri. + */ +type DidUri = + | `did:kilt:${DidUriVersion}${KiltAddress}` + | `did:kilt:light:${DidUriVersion}${AuthenticationKeyType}${KiltAddress}${LightDidEncodedData}` + +/** + * The fragment part of the DID URI including the `#` character. + */ +type UriFragment = `#${string}` +/** + * URI for DID resources like keys or service endpoints. + */ +type DidResourceUri = `${DidUri}${UriFragment}` + +type Base58BtcMultibaseString = `z${string}` + +interface VerificationMethod { + id: DidResourceUri + controller: DidUri + type: 'MultiKey' + publicKeyMultibase: Base58BtcMultibaseString +} + +interface Service { + id: DidResourceUri + type: string + serviceEndpoint: string[] +} + +interface DidDocument { + id: DidUri + alsoKnownAs?: string[] + verificationMethod: VerificationMethod[] + authentication: DidResourceUri[] + keyAgreement?: DidResourceUri[] + capabilityInvocation?: DidResourceUri[] + capabilityDelegation?: DidResourceUri[] + service?: Service[] +} diff --git a/yarn.lock b/yarn.lock index 828cf120b7..3e9081f91a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9338,11 +9338,11 @@ typescript@^4.8.3: "typescript@patch:typescript@^4.8.3#~builtin": version: 4.8.3 - resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=aae4e6" + resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=3b564f" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 2222d2382fb3146089b1d27ce2b55e9d1f99cc64118f1aba75809b693b856c5d3c324f052f60c75b577947fc538bc1c27bad0eb76cbdba9a63a253489504ba7e + checksum: dfe2ee3b9da1d74b9e06784ae90c20c435db9ad6ab23172911f6cdbfd7ab7213ae3611c4254c5a2c6dc2e89f05a658b95493890bf62d218267033b3d8a2e4dd6 languageName: node linkType: hard From 6d1963bac858e8eb361a93703f9cba9ee071a1ae Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 29 Sep 2023 10:05:17 +0100 Subject: [PATCH 02/78] Add base types for the compliant DID resolution --- packages/types/src/DidResolverV2.ts | 68 ++++++++++++++++++++++++++++ packages/types/src/DidV2.ts | 69 +++++++++++++++++++++++++---- 2 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 packages/types/src/DidResolverV2.ts diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts new file mode 100644 index 0000000000..f54ea83836 --- /dev/null +++ b/packages/types/src/DidResolverV2.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidUri, DidDocument } from './DidV2.js' + +type ResolutionOptions = { + /* + * The Media Type of the caller's preferred representation of the DID document. The Media Type MUST be expressed as an ASCII string. The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. + */ + accept?: string +} + +/* + * This specification defines the following common error values: + * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. (See 3.1 DID Syntax.) + * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + */ +type DidResolutionMetadataError = 'invalidDid' | 'notFound' + +/* + * The possible properties within this structure and their possible values are registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + */ +type DidResolutionMetadata = { + /* + * The error code from the resolution process. This property is REQUIRED when there is an error in the resolution process. The value of this property MUST be a single keyword ASCII string. The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + */ + error?: DidResolutionMetadataError +} + +/* + * The possible properties within this structure and their possible values SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + */ +type DidDocumentMetadata = { + /* + * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. + */ + deactivated?: true + /* + * DID document metadata MAY include a canonicalId property. If present, the value MUST be a string that conforms to the rules in Section 3.1 DID Syntax. The relationship is a statement that the canonicalId value is logically equivalent to the id property value and that the canonicalId value is defined by the DID method to be the canonical ID for the DID subject in the scope of the containing DID document. A canonicalId value MUST be produced by, and a form of, the same DID method as the id property value. (e.g., did:example:abc == did:example:ABC). + */ + canonicalId?: DidUri +} + +type ResolutionResult = { + didResolutionMetadata: DidResolutionMetadata + didDocument?: DidDocument + didDocumentMetadata: DidDocumentMetadata +} + +export interface DidResolver { + /* + * The resolve function returns the DID document in its abstract form (a map). + */ + resolve: ( + /* + * This is the DID to resolve. This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. + */ + did: DidUri, + /* + * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. This input is REQUIRED, but the structure MAY be empty. + */ + resolutionOptions: ResolutionOptions + ) => Promise +} diff --git a/packages/types/src/DidV2.ts b/packages/types/src/DidV2.ts index 1e50c64aad..fb41f229a7 100644 --- a/packages/types/src/DidV2.ts +++ b/packages/types/src/DidV2.ts @@ -14,7 +14,7 @@ type LightDidEncodedData = '' | `:${string}` /** * A string containing a KILT DID Uri. */ -type DidUri = +export type DidUri = | `did:kilt:${DidUriVersion}${KiltAddress}` | `did:kilt:light:${DidUriVersion}${AuthenticationKeyType}${KiltAddress}${LightDidEncodedData}` @@ -29,26 +29,77 @@ type DidResourceUri = `${DidUri}${UriFragment}` type Base58BtcMultibaseString = `z${string}` -interface VerificationMethod { +/* + * The verification method map MUST include the id, type, controller, and specific verification material properties that are determined by the value of type and are defined in 5.2.1 Verification Material. A verification method MAY include additional properties. Verification methods SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + */ +type VerificationMethod = { + /* + * The value of the id property for a verification method MUST be a string that conforms to the rules in Section 3.2 DID URL Syntax. + */ id: DidResourceUri - controller: DidUri + /* + * The value of the type property MUST be a string that references exactly one verification method type. In order to maximize global interoperability, the verification method type SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + */ type: 'MultiKey' + /* + * The value of the controller property MUST be a string that conforms to the rules in 3.1 DID Syntax. + */ + controller: DidUri + /* + * The publicKeyMultibase property is OPTIONAL. This feature is non-normative. If present, the value MUST be a string representation of a [MULTIBASE] encoded public key. + */ publicKeyMultibase: Base58BtcMultibaseString } -interface Service { +/* + * Each service map MUST contain id, type, and serviceEndpoint properties. Each service extension MAY include additional properties and MAY further restrict the properties associated with the extension. + */ +type Service = { + /* + * The value of the id property MUST be a URI conforming to [RFC3986]. A conforming producer MUST NOT produce multiple service entries with the same id. A conforming consumer MUST produce an error if it detects multiple service entries with the same id. + */ id: DidResourceUri + /* + * The value of the type property MUST be a string or a set of strings. In order to maximize interoperability, the service type and its associated properties SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + */ type: string + /* + * The value of the serviceEndpoint property MUST be a string, a map, or a set composed of one or more strings and/or maps. All string values MUST be valid URIs conforming to [RFC3986] and normalized according to the Normalization and Comparison rules in RFC3986 and to any normalization rules in its applicable URI scheme specification. + */ serviceEndpoint: string[] } -interface DidDocument { +export type DidDocument = { + /* + * The value of id MUST be a string that conforms to the rules in 3.1 DID Syntax and MUST exist in the root map of the data model for the DID document. + */ id: DidUri + /* + * The alsoKnownAs property is OPTIONAL. If present, the value MUST be a set where each item in the set is a URI conforming to [RFC3986]. + */ alsoKnownAs?: string[] + /* + * The verificationMethod property is OPTIONAL. If present, the value MUST be a set of verification methods, where each verification method is expressed using a map. + */ verificationMethod: VerificationMethod[] - authentication: DidResourceUri[] - keyAgreement?: DidResourceUri[] - capabilityInvocation?: DidResourceUri[] - capabilityDelegation?: DidResourceUri[] + /* + * The authentication property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + authentication: UriFragment[] + /* + * The assertionMethod property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + assertionMethod?: UriFragment[] + /* + * The keyAgreement property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + keyAgreement?: UriFragment[] + /* + * The capabilityDelegation property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + capabilityDelegation?: UriFragment[] + /* + * The service property is OPTIONAL. If present, the associated value MUST be a set of services, where each service is described by a map. + */ service?: Service[] } From 8c2ddea9078299224d557abbb07e01ce596ac289 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 29 Sep 2023 11:01:28 +0100 Subject: [PATCH 03/78] Add dereferencing --- packages/types/src/DidResolverV2.ts | 142 ++++++++++++++++++++++++---- packages/types/src/DidV2.ts | 6 +- 2 files changed, 129 insertions(+), 19 deletions(-) diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index f54ea83836..8a7cd29101 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -5,64 +5,174 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri, DidDocument } from './DidV2.js' +import type { + DidUri, + DidDocument, + DidResourceUri, + VerificationMethod, + Service, +} from './DidV2.js' + +type DidContentType = 'application/did+ld+json' | 'application/did+json' type ResolutionOptions = { /* - * The Media Type of the caller's preferred representation of the DID document. The Media Type MUST be expressed as an ASCII string. The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. + * The Media Type of the caller's preferred representation of the DID document. + * The Media Type MUST be expressed as an ASCII string. + * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. + * This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. */ - accept?: string + accept?: DidContentType } /* * This specification defines the following common error values: - * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. (See 3.1 DID Syntax.) + * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. */ -type DidResolutionMetadataError = 'invalidDid' | 'notFound' +type DidResolutionMetadataError = + | 'invalidDid' + | 'notFound' + | 'representationNotSupported' -/* - * The possible properties within this structure and their possible values are registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. - */ type DidResolutionMetadata = { /* - * The error code from the resolution process. This property is REQUIRED when there is an error in the resolution process. The value of this property MUST be a single keyword ASCII string. The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + * The error code from the resolution process. + * This property is REQUIRED when there is an error in the resolution process. + * The value of this property MUST be a single keyword ASCII string. + * The possible property values of this field SHOULD be registered in the DID Specification Registries. */ error?: DidResolutionMetadataError } -/* - * The possible properties within this structure and their possible values SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. - */ type DidDocumentMetadata = { /* - * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. + * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. + * If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. */ deactivated?: true /* - * DID document metadata MAY include a canonicalId property. If present, the value MUST be a string that conforms to the rules in Section 3.1 DID Syntax. The relationship is a statement that the canonicalId value is logically equivalent to the id property value and that the canonicalId value is defined by the DID method to be the canonical ID for the DID subject in the scope of the containing DID document. A canonicalId value MUST be produced by, and a form of, the same DID method as the id property value. (e.g., did:example:abc == did:example:ABC). + * DID document metadata MAY include a canonicalId property. + * If present, the value MUST be a string that conforms to the rules in Section 3.1 DID Syntax. + * The relationship is a statement that the canonicalId value is logically equivalent to the id property value and that the canonicalId value is defined by the DID method to be the canonical ID for the DID subject in the scope of the containing DID document. + * A canonicalId value MUST be produced by, and a form of, the same DID method as the id property value. (e.g., did:example:abc == did:example:ABC). */ canonicalId?: DidUri } type ResolutionResult = { + /* + * A metadata structure consisting of values relating to the results of the DID resolution process which typically changes between invocations of the resolve and resolveRepresentation functions, as it represents data about the resolution process itself. + * This structure is REQUIRED, and in the case of an error in the resolution process, this MUST NOT be empty. + * If resolveRepresentation was called, this structure MUST contain a contentType property containing the Media Type of the representation found in the didDocumentStream. + * If the resolution is not successful, this structure MUST contain an error property describing the error. + * The possible properties within this structure and their possible values are registered in the DID Specification Registries. + */ didResolutionMetadata: DidResolutionMetadata + /* + * If the resolution is successful, and if the resolve function was called, this MUST be a DID document abstract data model (a map) as described in 4. Data Model that is capable of being transformed into a conforming DID Document (representation), using the production rules specified by the representation. + * The value of id in the resolved DID document MUST match the DID that was resolved. + * If the resolution is unsuccessful, this value MUST be empty. + */ didDocument?: DidDocument + /* + * If the resolution is successful, this MUST be a metadata structure. + * This structure contains metadata about the DID document contained in the didDocument property. + * This metadata typically does not change between invocations of the resolve and resolveRepresentation functions unless the DID document changes, as it represents metadata about the DID document. + * If the resolution is unsuccessful, this output MUST be an empty metadata structure. + * The possible properties within this structure and their possible values SHOULD be registered in the DID Specification Registries. + */ didDocumentMetadata: DidDocumentMetadata } +type DereferenceOptions = { + /* + * The Media Type that the caller prefers for contentStream. + * The Media Type MUST be expressed as an ASCII string. + * The DID URL dereferencing implementation SHOULD use this value to determine the contentType of the representation contained in the returned value if such a representation is supported and available. + */ + accept?: DidContentType +} + +/* + * This specification defines the following common error values: + * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) + * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. + */ +type DidDereferenceMetadataError = 'invalidDidUrl' | 'notFound' + +type DereferencingMetadata = { + /* + * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. + * The Media Type value MUST be expressed as an ASCII string. + */ + contentType?: DidContentType + /* + * The error code from the dereferencing process. + * This property is REQUIRED when there is an error in the dereferencing process. + * The value of this property MUST be a single keyword expressed as an ASCII string. + * The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + */ + error?: DidDereferenceMetadataError +} + +type DereferenceContentStream = DidDocument | VerificationMethod | Service + +type DereferenceContentMetadata = DidDocumentMetadata | Record + +type DereferenceResult = { + /* + * A metadata structure consisting of values relating to the results of the DID URL dereferencing process. + * This structure is REQUIRED, and in the case of an error in the dereferencing process, this MUST NOT be empty. + * Properties defined by this specification are in 7.2.2 DID URL Dereferencing Metadata. + * If the dereferencing is not successful, this structure MUST contain an error property describing the error. + */ + dereferencingMetadata: DereferencingMetadata + /* + * If the dereferencing function was called and successful, this MUST contain a resource corresponding to the DID URL. + * The contentStream MAY be a resource such as a DID document that is serializable in one of the conformant representations, a Verification Method, a service, or any other resource format that can be identified via a Media Type and obtained through the resolution process. + * If the dereferencing is unsuccessful, this value MUST be empty. + */ + contentStream?: DereferenceContentStream + /* + * If the dereferencing is successful, this MUST be a metadata structure, but the structure MAY be empty. + * This structure contains metadata about the contentStream. + * If the contentStream is a DID document, this MUST be a didDocumentMetadata structure as described in DID Resolution. + * If the dereferencing is unsuccessful, this output MUST be an empty metadata structure. + */ + contentMetadata: DereferenceContentMetadata +} + export interface DidResolver { /* * The resolve function returns the DID document in its abstract form (a map). */ resolve: ( /* - * This is the DID to resolve. This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. + * This is the DID to resolve. + * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. */ did: DidUri, /* - * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. This input is REQUIRED, but the structure MAY be empty. + * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. + * This input is REQUIRED, but the structure MAY be empty. */ resolutionOptions: ResolutionOptions ) => Promise + + dereference: ( + /* + * A conformant DID URL as a single string. + * This is the DID URL to dereference. + * To dereference a DID fragment, the complete DID URL including the DID fragment MUST be used. This input is REQUIRED. + */ + didUrl: DidResourceUri, + /* + * A metadata structure consisting of input options to the dereference function in addition to the didUrl itself. + * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. + * This input is REQUIRED, but the structure MAY be empty. + */ + dereferenceOptions: DereferenceOptions + ) => Promise } diff --git a/packages/types/src/DidV2.ts b/packages/types/src/DidV2.ts index fb41f229a7..a8eb5d7d54 100644 --- a/packages/types/src/DidV2.ts +++ b/packages/types/src/DidV2.ts @@ -25,14 +25,14 @@ type UriFragment = `#${string}` /** * URI for DID resources like keys or service endpoints. */ -type DidResourceUri = `${DidUri}${UriFragment}` +export type DidResourceUri = `${DidUri}${UriFragment}` type Base58BtcMultibaseString = `z${string}` /* * The verification method map MUST include the id, type, controller, and specific verification material properties that are determined by the value of type and are defined in 5.2.1 Verification Material. A verification method MAY include additional properties. Verification methods SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ -type VerificationMethod = { +export type VerificationMethod = { /* * The value of the id property for a verification method MUST be a string that conforms to the rules in Section 3.2 DID URL Syntax. */ @@ -54,7 +54,7 @@ type VerificationMethod = { /* * Each service map MUST contain id, type, and serviceEndpoint properties. Each service extension MAY include additional properties and MAY further restrict the properties associated with the extension. */ -type Service = { +export type Service = { /* * The value of the id property MUST be a URI conforming to [RFC3986]. A conforming producer MUST NOT produce multiple service entries with the same id. A conforming consumer MUST produce an error if it detects multiple service entries with the same id. */ From 9faa011d5bbbb14b4cba6817241edf0abd864063 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 29 Sep 2023 12:04:32 +0100 Subject: [PATCH 04/78] Refactor of Did.utils and Did.chain --- packages/did/src/Did2.chain.ts | 538 ++++++++++++++++++ packages/did/src/Did2.utils.ts | 219 +++++++ packages/did/src/DidDetailsv2/DidDetailsV2.ts | 46 ++ .../did/src/DidDetailsv2/FullDidDetailsV2.ts | 249 ++++++++ packages/types/src/CryptoCallbacks.ts | 2 +- .../types/src/{DidV2.ts => DidDocumentV2.ts} | 4 +- packages/types/src/DidResolverV2.ts | 2 +- packages/types/src/index.ts | 3 + 8 files changed, 1059 insertions(+), 4 deletions(-) create mode 100644 packages/did/src/Did2.chain.ts create mode 100644 packages/did/src/Did2.utils.ts create mode 100644 packages/did/src/DidDetailsv2/DidDetailsV2.ts create mode 100644 packages/did/src/DidDetailsv2/FullDidDetailsV2.ts rename packages/types/src/{DidV2.ts => DidDocumentV2.ts} (98%) diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts new file mode 100644 index 0000000000..c63f3a9dc3 --- /dev/null +++ b/packages/did/src/Did2.chain.ts @@ -0,0 +1,538 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { Option } from '@polkadot/types' +import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' +import type { AnyNumber } from '@polkadot/types/types' + +import type { + DidDidDetails, + DidDidDetailsDidAuthorizedCallOperation, + DidDidDetailsDidPublicKey, + DidDidDetailsDidPublicKeyDetails, + DidServiceEndpointsDidEndpoint, + KiltSupportDeposit, +} from '@kiltprotocol/augment-api' + +import type { + Deposit, + DidDocumentV2, + KiltAddress, + BN, + NewDidVerificationKey, + NewDidEncryptionKey, + SignRequestData, + SignResponseData, + SubmittableExtrinsic, + SignExtrinsicCallback, +} from '@kiltprotocol/types' + +import { ConfigService } from '@kiltprotocol/config' +import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' + +import { + EncryptionKeyType, + RelativeServiceEndpoint, + VerificationKeyType, + verificationKeyTypes, + VerificationMethodRelationship, +} from './DidDetailsv2/DidDetailsV2.js' + +import { + EncodedEncryptionKey, + EncodedKey, + EncodedSignature, + EncodedVerificationKey, + getAddressByKey, + getFullDidUri, + parse, +} from './Did2.utils.js' + +// ### Chain type definitions + +export type ChainDidPublicKey = DidDidDetailsDidPublicKey +export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails + +// ### RAW QUERYING (lowest layer) + +/** + * Format a DID to be used as a parameter for the blockchain API functions. + + * @param did The DID to format. + * @returns The blockchain-formatted DID. + */ +export function toChain(did: DidDocumentV2.DidUri): KiltAddress { + return parse(did).address +} + +/** + * Format a DID resource ID to be used as a parameter for the blockchain API functions. + + * @param id The DID resource ID to format. + * @returns The blockchain-formatted ID. + */ +export function resourceIdToChain(id: DidDocumentV2.UriFragment): string { + return id.replace(/^#/, '') +} + +/** + * Convert the deposit data coming from the blockchain to JS object. + * + * @param deposit The blockchain-formatted deposit data. + * @returns The deposit data. + */ +export function depositFromChain(deposit: KiltSupportDeposit): Deposit { + return { + owner: Crypto.encodeAddress(deposit.owner, ss58Format), + amount: deposit.amount.toBn(), + } +} + +type ChainBaseDidVerificationMethod = { + /** + * Relative key URI: `#` sign followed by fragment part of URI. + */ + id: DidDocumentV2.UriFragment + /** + * The public key material. + */ + publicKey: Uint8Array + /** + * The inclusion block of the key, if stored on chain. + */ + includedAt?: BN + /** + * The type of the key. + */ + type: string +} +export type ChainDidVerificationKey = ChainBaseDidVerificationMethod & { + type: VerificationKeyType +} +export type ChainDidEncryptionKey = ChainBaseDidVerificationMethod & { + type: EncryptionKeyType +} +type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey + +export type ChainDidService = { + id: string + serviceTypes: string[] + urls: string[] +} + +// ### DECODED QUERYING types + +type ChainDidDetails = { + authentication: [ChainDidVerificationKey] + assertionMethod?: [ChainDidVerificationKey] + capabilityDelegation?: [ChainDidVerificationKey] + keyAgreement?: ChainDidEncryptionKey[] + + service?: ChainDidService[] + + lastTxCounter: BN + deposit: Deposit +} + +// ### DECODED QUERYING (builds on top of raw querying) + +function didPublicKeyDetailsFromChain( + keyId: Hash, + keyDetails: ChainDidPublicKeyDetails +): ChainDidVerificationKey { + const key = keyDetails.key.isPublicEncryptionKey + ? keyDetails.key.asPublicEncryptionKey + : keyDetails.key.asPublicVerificationKey + return { + id: `#${keyId.toHex()}`, + type: key.type.toLowerCase() as ChainDidVerificationKey['type'], + publicKey: key.value.toU8a(), + } +} + +/** + * Convert the DID data from blockchain format to the DID URI. + * + * @param encoded The chain-formatted DID. + * @returns The DID URI. + */ +export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { + return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) +} + +/** + * Convert the DID Document data from the blockchain format to a JS object. + * + * @param encoded The chain-formatted DID Document. + * @returns The DID Document. + */ +export function documentFromChain( + encoded: Option +): ChainDidDetails { + const { + publicKeys, + authenticationKey, + attestationKey, + delegationKey, + keyAgreementKeys, + lastTxCounter, + deposit, + } = encoded.unwrap() + + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => + didPublicKeyDetailsFromChain(keyId, keyDetails) + ) + .reduce((res, key) => { + res[resourceIdToChain(key.id)] = key + return res + }, {}) + + const authentication = keys[ + authenticationKey.toHex() + ] as ChainDidVerificationKey + + const didRecord: ChainDidDetails = { + authentication: [authentication], + lastTxCounter: lastTxCounter.toBn(), + deposit: depositFromChain(deposit), + } + if (attestationKey.isSome) { + const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey + didRecord.assertionMethod = [key] + } + if (delegationKey.isSome) { + const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey + didRecord.capabilityDelegation = [key] + } + + const keyAgreementKeyIds = [...keyAgreementKeys.values()].map((keyId) => + keyId.toHex() + ) + if (keyAgreementKeyIds.length > 0) { + didRecord.keyAgreement = keyAgreementKeyIds.map( + (id) => keys[id] as ChainDidEncryptionKey + ) + } + + return didRecord +} + +/** + * Checks if a string is a valid URI according to RFC#3986. + * + * @param str String to be checked. + * @returns Whether `str` is a valid URI. + */ +function isUri(str: string): boolean { + try { + const url = new URL(str) // this actually accepts any URI but throws if it can't be parsed + return url.href === str || encodeURI(decodeURI(str)) === str // make sure our URI has not been converted implicitly by URL + } catch { + return false + } +} + +const UriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ + +/** + * Checks if a string is a valid URI fragment according to RFC#3986. + * + * @param str String to be checked. + * @returns Whether `str` is a valid URI fragment. + */ +function isUriFragment(str: string): boolean { + try { + return UriFragmentRegex.test(str) && !!decodeURIComponent(str) + } catch { + return false + } +} + +/** + * Performs sanity checks on service endpoint data, making sure that the following conditions are met: + * - The `id` property is a string containing a valid URI fragment according to RFC#3986, not a complete DID URI. + * - If the `uris` property contains one or more strings, they must be valid URIs according to RFC#3986. + * + * @param endpoint A service endpoint object to check. + */ +export function validateService(endpoint: RelativeServiceEndpoint): void { + const { id, serviceEndpoint } = endpoint + if (id.startsWith('did:kilt')) { + throw new SDKErrors.DidError( + `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` + ) + } + if (!isUriFragment(resourceIdToChain(id))) { + throw new SDKErrors.DidError( + `The service ID must be valid as a URI fragment according to RFC#3986, which "${id}" is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding the desired id.` + ) + } + serviceEndpoint.forEach((uri) => { + if (!isUri(uri)) { + throw new SDKErrors.DidError( + `A service URI must be a URI according to RFC#3986, which "${uri}" (service id "${id}") is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding resource locators beforehand.` + ) + } + }) +} + +/** + * Format the DID service to be used as a parameter for the blockchain API functions. + * + * @param service The DID service to format. + * @returns The blockchain-formatted DID service. + */ +export function serviceToChain( + service: RelativeServiceEndpoint +): ChainDidService { + validateService(service) + const { id, type, serviceEndpoint } = service + return { + id: resourceIdToChain(id), + serviceTypes: type, + urls: serviceEndpoint, + } +} + +/** + * Convert the DID service data coming from the blockchain to JS object. + * + * @param encoded The blockchain-formatted DID service data. + * @returns The DID service. + */ +export function serviceFromChain( + encoded: Option +): RelativeServiceEndpoint { + const { id, serviceTypes, urls } = encoded.unwrap() + return { + id: `#${id.toUtf8()}`, + type: serviceTypes.map((type) => type.toUtf8()), + serviceEndpoint: urls.map((url) => url.toUtf8()), + } +} + +// ### EXTRINSICS types + +export type AuthorizeCallInput = { + did: DidDocumentV2.DidUri + txCounter: AnyNumber + call: Extrinsic + submitter: KiltAddress + blockNumber?: AnyNumber +} + +// ### EXTRINSICS + +export function publicKeyToChain( + key: NewDidVerificationKey +): EncodedVerificationKey +export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey + +/** + * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. + * + * @param key Object describing data associated with a public key. + * @returns Data restructured to allow SCALE encoding by polkadot api. + */ +export function publicKeyToChain( + key: NewDidVerificationKey | NewDidEncryptionKey +): EncodedKey { + // TypeScript can't infer type here, so we have to add a type assertion. + return { [key.type]: key.publicKey } as EncodedKey +} + +interface GetStoreTxInput { + authentication: [NewDidVerificationKey] + assertionMethod?: [NewDidVerificationKey] + capabilityDelegation?: [NewDidVerificationKey] + keyAgreement?: NewDidEncryptionKey[] + + service?: RelativeServiceEndpoint[] +} + +export type GetStoreTxSignCallback = ( + signData: Omit +) => Promise> + +/** + * Create a DID creation operation which includes the information provided. + * + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys and service endpoints. + * + * A DID creation operation can contain at most 25 new service endpoints. + * Additionally, each service endpoint must respect the following conditions: + * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. + * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * + * @param input The DID keys and services to store, also accepts DidDocument, so you can store a light DID for example. + * @param submitter The KILT address authorized to submit the creation operation. + * @param sign The sign callback. The authentication key has to be used. + * + * @returns The SubmittableExtrinsic for the DID creation operation. + */ +export async function getStoreTx( + input: GetStoreTxInput, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const api = ConfigService.get('api') + + const { + authentication, + assertionMethod, + capabilityDelegation, + keyAgreement = [], + service = [], + } = input + + if (!('authentication' in input) || typeof authentication[0] !== 'object') { + throw new SDKErrors.DidError( + `The provided DID does not have an authentication key to sign the creation operation` + ) + } + + // For now, it only takes the first attestation key, if present. + if (assertionMethod && assertionMethod.length > 1) { + throw new SDKErrors.DidError( + `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` + ) + } + + // For now, it only takes the first delegation key, if present. + if (capabilityDelegation && capabilityDelegation.length > 1) { + throw new SDKErrors.DidError( + `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` + ) + } + + const maxKeyAgreementKeys = api.consts.did.maxNewKeyAgreementKeys.toNumber() + if (keyAgreement.length > maxKeyAgreementKeys) { + throw new SDKErrors.DidError( + `The number of key agreement keys in the creation operation is greater than the maximum allowed, which is ${maxKeyAgreementKeys}` + ) + } + + const maxNumberOfServicesPerDid = + api.consts.did.maxNumberOfServicesPerDid.toNumber() + if (service.length > maxNumberOfServicesPerDid) { + throw new SDKErrors.DidError( + `Cannot store more than ${maxNumberOfServicesPerDid} service endpoints per DID` + ) + } + + const [authenticationKey] = authentication + const did = getAddressByKey(authenticationKey) + + const newAttestationKey = + assertionMethod && + assertionMethod.length > 0 && + publicKeyToChain(assertionMethod[0]) + + const newDelegationKey = + capabilityDelegation && + capabilityDelegation.length > 0 && + publicKeyToChain(capabilityDelegation[0]) + + const newKeyAgreementKeys = keyAgreement.map(publicKeyToChain) + const newServiceDetails = service.map(serviceToChain) + + const apiInput = { + did, + submitter, + newAttestationKey, + newDelegationKey, + newKeyAgreementKeys, + newServiceDetails, + } + + const encoded = api.registry + .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) + .toU8a() + + const signature = await sign({ + data: encoded, + verificationMethodRelationship: 'authentication', + }) + const encodedSignature = { + [signature.keyType]: signature.signature, + } as EncodedSignature + return api.tx.did.create(encoded, encodedSignature) +} + +export interface SigningOptions { + sign: SignExtrinsicCallback + verificationMethodRelationship: VerificationMethodRelationship +} + +/** + * DID related operations on the KILT blockchain require authorization by a full DID. This is realized by requiring that relevant extrinsics are signed with a key featured by a full DID as a verification method. + * Such extrinsics can be produced using this function. + * + * @param params Object wrapping all input to the function. + * @param params.did Full DID. + * @param params.verificationMethodRelationship DID verification relationship to be used for authorization. + * @param params.sign The callback to interface with the key store managing the private key to be used. + * @param params.call The call or extrinsic to be authorized. + * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. + * @param params.submitter Payment account allowed to submit this extrinsic and cover its fees, which will end up owning any deposit associated with newly created records. + * @param params.blockNumber Block number for determining the validity period of this authorization. If omitted, the current block number will be fetched from chain. + * @returns A DID authorized extrinsic that, after signing with the payment account mentioned in the params, is ready for submission. + */ +export async function generateDidAuthenticatedTx({ + did, + verificationMethodRelationship, + sign, + call, + txCounter, + submitter, + blockNumber, +}: AuthorizeCallInput & SigningOptions): Promise { + const api = ConfigService.get('api') + const signableCall = + api.registry.createType( + api.tx.did.submitDidCall.meta.args[0].type.toString(), + { + txCounter, + did: toChain(did), + call, + submitter, + blockNumber: blockNumber ?? (await api.query.system.number()), + } + ) + const signature = await sign({ + data: signableCall.toU8a(), + verificationMethodRelationship, + did, + }) + const encodedSignature = { + [signature.keyType]: signature.signature, + } as EncodedSignature + return api.tx.did.submitDidCall(signableCall, encodedSignature) +} + +// ### Chain utils +/** + * Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics. + * + * @param key Object describing data associated with a public key. + * @param signature Object containing a signature generated with a full DID associated public key. + * @returns Data restructured to allow SCALE encoding by polkadot api. + */ +export function didSignatureToChain( + key: ChainDidVerificationKey, + signature: Uint8Array +): EncodedSignature { + if (!verificationKeyTypes.includes(key.type)) { + throw new SDKErrors.DidError( + `encodedDidSignature requires a verification key. A key of type "${key.type}" was used instead` + ) + } + + return { [key.type]: signature } as EncodedSignature +} diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts new file mode 100644 index 0000000000..cef10c1dd5 --- /dev/null +++ b/packages/did/src/Did2.utils.ts @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' +import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' + +import type { DidDocumentV2, KiltAddress } from '@kiltprotocol/types' + +import type { VerificationKeyType } from './DidDetailsv2/DidDetailsV2.js' + +// The latest version for KILT light DIDs. +const LIGHT_DID_LATEST_VERSION = 1 + +// The latest version for KILT full DIDs. +const FULL_DID_LATEST_VERSION = 1 + +// NOTICE: The following regex patterns must be kept in sync with DidUri type in @kiltprotocol/types + +// Matches the following full DIDs +// - did:kilt: +// - did:kilt:# +const FULL_KILT_DID_REGEX = + /^did:kilt:(?
4[1-9a-km-zA-HJ-NP-Z]{47})(?#[^#\n]+)?$/ + +// Matches the following light DIDs +// - did:kilt:light:00 +// - did:kilt:light:01: +// - did:kilt:light:10# +// - did:kilt:light:99:# +const LIGHT_KILT_DID_REGEX = + /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?#[^#\n]+)?$/ + +type IDidParsingResult = { + did: DidDocumentV2.DidUri + version: number + type: 'light' | 'full' + address: KiltAddress + fragment?: DidDocumentV2.UriFragment + authKeyTypeEncoding?: string + encodedDetails?: string +} + +/** + * Parses a KILT DID uri and returns the information contained within in a structured form. + * + * @param didUri A KILT DID uri as a string. + * @returns Object containing information extracted from the DID uri. + */ +export function parse( + didUri: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri +): IDidParsingResult { + let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups + if (matches) { + const { version: versionString, fragment } = matches + const address = matches.address as KiltAddress + const version = versionString + ? parseInt(versionString, 10) + : FULL_DID_LATEST_VERSION + return { + did: didUri.replace(fragment || '', '') as DidDocumentV2.DidUri, + version, + type: 'full', + address, + fragment: + fragment === '#' ? undefined : (fragment as DidDocumentV2.UriFragment), + } + } + + // If it fails to parse full DID, try with light DID + matches = LIGHT_KILT_DID_REGEX.exec(didUri)?.groups + if (matches) { + const { + authKeyType, + version: versionString, + encodedDetails, + fragment, + } = matches + const address = matches.address as KiltAddress + const version = versionString + ? parseInt(versionString, 10) + : LIGHT_DID_LATEST_VERSION + return { + did: didUri.replace(fragment || '', '') as DidDocumentV2.DidUri, + version, + type: 'light', + address, + fragment: + fragment === '#' ? undefined : (fragment as DidDocumentV2.UriFragment), + encodedDetails, + authKeyTypeEncoding: authKeyType, + } + } + + throw new SDKErrors.InvalidDidFormatError(didUri) +} + +/** + * Returns true if both didA and didB refer to the same DID subject, i.e., whether they have the same identifier as specified in the method spec. + * + * @param didA A KILT DID uri as a string. + * @param didB A second KILT DID uri as a string. + * @returns Whether didA and didB refer to the same DID subject. + */ +export function isSameSubject( + didA: DidDocumentV2.DidUri, + didB: DidDocumentV2.DidUri +): boolean { + return parse(didA).address === parse(didB).address +} + +export type EncodedVerificationKey = + | { sr25519: Uint8Array } + | { ed25519: Uint8Array } + | { ecdsa: Uint8Array } + +export type EncodedEncryptionKey = { x25519: Uint8Array } + +export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey + +export type EncodedSignature = EncodedVerificationKey + +/** + * Checks that a string (or other input) is a valid KILT DID uri with or without a URI fragment. + * Throws otherwise. + * + * @param input Arbitrary input. + * @param expectType `ResourceUri` if the URI is expected to have a fragment (following '#'), `Did` if it is expected not to have one. Default allows both. + */ +export function validateUri( + input: unknown, + expectType?: 'Did' | 'ResourceUri' +): void { + if (typeof input !== 'string') { + throw new TypeError(`DID string expected, got ${typeof input}`) + } + const { address, fragment } = parse(input as DidDocumentV2.DidUri) + + if ( + fragment && + (expectType === 'Did' || + // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. + (typeof expectType === 'boolean' && expectType === false)) + ) { + throw new SDKErrors.DidError( + 'Expected a Kilt DidUri but got a DidResourceUri (containing a #fragment)' + ) + } + + if (!fragment && expectType === 'ResourceUri') { + throw new SDKErrors.DidError( + 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' + ) + } + + DataUtils.verifyKiltAddress(address) +} + +/** + * Internal: derive the address part of the DID when it is created from authentication key. + * + * @param input The authentication key. + * @param input.publicKey The public key. + * @param input.type The type of the key. + * @returns The expected address of the DID. + */ +export function getAddressByKey({ + publicKey, + type, +}: { + publicKey: Uint8Array + type: VerificationKeyType +}): KiltAddress { + if (type === 'ed25519' || type === 'sr25519') { + return encodeAddress(publicKey, ss58Format) + } + + // Otherwise it’s ecdsa. + // Taken from https://github.com/polkadot-js/common/blob/master/packages/keyring/src/pair/index.ts#L44 + const address = publicKey.length > 32 ? blake2AsU8a(publicKey) : publicKey + return encodeAddress(address, ss58Format) +} + +/** + * Builds the URI a light DID will have after it’s stored on the blockchain. + * + * @param didOrAddress The URI of the light DID. Internally it’s used with the DID "address" as well. + * @param version The version of the DID URI to use. + * @returns The expected full DID URI. + */ +export function getFullDidUri( + didOrAddress: DidDocumentV2.DidUri | KiltAddress, + version = FULL_DID_LATEST_VERSION +): DidDocumentV2.DidUri { + const address = DataUtils.isKiltAddress(didOrAddress) + ? didOrAddress + : parse(didOrAddress as DidDocumentV2.DidUri).address + const versionString = version === 1 ? '' : `v${version}` + return `did:kilt:${versionString}${address}` as DidDocumentV2.DidUri +} + +/** + * Builds the URI of a full DID if it is created with the authentication key provided. + * + * @param key The key that will be used as DID authentication key. + * @param key.publicKey The public key. + * @param key.type The type of the key. + * @returns The expected full DID URI. + */ +export function getFullDidUriFromKey(key: { + publicKey: Uint8Array + type: VerificationKeyType +}): DidDocumentV2.DidUri { + const address = getAddressByKey(key) + return getFullDidUri(address) +} diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts new file mode 100644 index 0000000000..889b7b1fdc --- /dev/null +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' + +export type VerificationMethodRelationship = + | 'authentication' + | 'capabilityDelegation' + | 'assertionMethod' + +/** + * Possible types for a DID verification key. + */ +const verificationKeyTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const +export const verificationKeyTypes = verificationKeyTypesC as unknown as string[] +export type VerificationKeyType = typeof verificationKeyTypesC[number] +// `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 + +/** + * Currently, a light DID does not support the use of an ECDSA key as its authentication key. + */ +export type LightDidSupportedVerificationKeyType = Extract< + VerificationKeyType, + 'ed25519' | 'sr25519' +> + +/** + * Subset of key relationships which pertain to key agreement/encryption keys. + */ +export type EncryptionKeyRelationship = Extract + +/** + * Possible types for a DID encryption key. + */ +const encryptionKeyTypesC = ['x25519'] as const +export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] +export type EncryptionKeyType = typeof encryptionKeyTypesC[number] + +export type RelativeServiceEndpoint = Pick< + DidDocumentV2.Service, + 'serviceEndpoint' | 'type' +> & { id: DidDocumentV2.UriFragment } diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts new file mode 100644 index 0000000000..599e8ad36a --- /dev/null +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { BN } from '@polkadot/util' + +import { ConfigService } from '@kiltprotocol/config' + +import type { Extrinsic } from '@polkadot/types/interfaces' + +import type { DidDocumentV2 } from '@kiltprotocol/types' + +import type { VerificationMethodRelationship } from './DidDetailsV2.js' + +// Must be in sync with what's implemented in impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for Call +// in https://github.com/KILTprotocol/mashnet-node/blob/develop/runtimes/spiritnet/src/lib.rs +// TODO: Should have an RPC or something similar to avoid inconsistencies in the future. +const methodMapping: Record = { + attestation: 'assertionMethod', + ctype: 'assertionMethod', + delegation: 'capabilityDelegation', + did: 'authentication', + 'did.create': undefined, + 'did.reclaimDeposit': undefined, + 'did.submitDidCall': undefined, + didLookup: 'authentication', + publicCredentials: 'assertionMethod', + web3Names: 'authentication', +} + +function getKeyRelationshipForMethod( + call: Extrinsic['method'] +): VerificationMethodRelationship | undefined { + const { section, method } = call + + // get the VerificationKeyRelationship of a batched call + if ( + section === 'utility' && + ['batch', 'batchAll', 'forceBatch'].includes(method) && + call.args[0].toRawType() === 'Vec' + ) { + // map all calls to their VerificationKeyRelationship and deduplicate the items + return (call.args[0] as unknown as Array) + .map(getKeyRelationshipForMethod) + .reduce((prev, value) => (prev === value ? prev : undefined)) + } + + const signature = `${section}.${method}` + if (signature in methodMapping) { + return methodMapping[signature] + } + + return methodMapping[section] +} + +/** + * Detect the key relationship for a key which should be used to DID-authorize the provided extrinsic. + * + * @param extrinsic The unsigned extrinsic to inspect. + * @returns The key relationship. + */ +export function getKeyRelationshipForTx( + extrinsic: Extrinsic +): VerificationMethodRelationship | undefined { + return getKeyRelationshipForMethod(extrinsic.method) +} + +// Max nonce value is (2^64) - 1 +const maxNonceValue = new BN(2).pow(new BN(64)).subn(1) + +function increaseNonce(currentNonce: BN, increment = 1): BN { + // Wrap around the max u64 value when reached. + // FIXME: can we do better than this? Maybe we could expose an RPC function for this, to keep it consistent over time. + return currentNonce.eq(maxNonceValue) + ? new BN(increment) + : currentNonce.addn(increment) +} + +/** + * Returns the next nonce to use to sign a DID operation. + * Normally, this function should not be called directly by SDK users. Nevertheless, in advanced cases where there might be race conditions, this function can be used as the basis on which to build parallel operation queues. + * + * @param did The DID data. + * @returns The next valid nonce, i.e., the nonce currently stored on the blockchain + 1, wrapping around the max value when reached. + */ +async function getNextNonce(did: DidDocumentV2.DidUri): Promise { + const api = ConfigService.get('api') + const queried = await api.query.did.did(toChain(did)) + const currentNonce = queried.isSome + ? documentFromChain(queried).lastTxCounter + : new BN(0) + return increaseNonce(currentNonce) +} + +/** + * Signs and returns the provided unsigned extrinsic with the right DID key, if present. Otherwise, it will throw an error. + * + * @param did The DID data. + * @param extrinsic The unsigned extrinsic to sign. + * @param sign The callback to sign the operation. + * @param submitterAccount The KILT account to bind the DID operation to (to avoid MitM and replay attacks). + * @param signingOptions The signing options. + * @param signingOptions.txCounter The optional DID nonce to include in the operation signatures. By default, it uses the next value of the nonce stored on chain. + * @returns The DID-signed submittable extrinsic. + */ +export async function authorizeTx( + did: DidUri, + extrinsic: Extrinsic, + sign: SignExtrinsicCallback, + submitterAccount: KiltAddress, + { + txCounter, + }: { + txCounter?: BN + } = {} +): Promise { + if (parse(did).type === 'light') { + throw new SDKErrors.DidError( + `An extrinsic can only be authorized with a full DID, not with "${did}"` + ) + } + + const keyRelationship = getKeyRelationshipForTx(extrinsic) + if (keyRelationship === undefined) { + throw new SDKErrors.SDKError('No key relationship found for extrinsic') + } + + return generateDidAuthenticatedTx({ + did, + keyRelationship, + sign, + call: extrinsic, + txCounter: txCounter || (await getNextNonce(did)), + submitter: submitterAccount, + }) +} + +type GroupedExtrinsics = Array<{ + extrinsics: Extrinsic[] + keyRelationship: VerificationMethodRelationship +}> + +function groupExtrinsicsByKeyRelationship( + extrinsics: Extrinsic[] +): GroupedExtrinsics { + const [first, ...rest] = extrinsics.map((extrinsic) => { + const keyRelationship = getKeyRelationshipForTx(extrinsic) + if (!keyRelationship) { + throw new SDKErrors.DidBatchError( + 'Can only batch extrinsics that require a DID signature' + ) + } + return { extrinsic, keyRelationship } + }) + + const groups: GroupedExtrinsics = [ + { + extrinsics: [first.extrinsic], + keyRelationship: first.keyRelationship, + }, + ] + + rest.forEach(({ extrinsic, keyRelationship }) => { + const currentGroup = groups[groups.length - 1] + const useCurrentGroup = keyRelationship === currentGroup.keyRelationship + if (useCurrentGroup) { + currentGroup.extrinsics.push(extrinsic) + } else { + groups.push({ + extrinsics: [extrinsic], + keyRelationship, + }) + } + }) + + return groups +} + +/** + * Authorizes/signs a list of extrinsics grouping them in batches by required key type. + * + * @param input The object with named parameters. + * @param input.batchFunction The batch function to use, for example `api.tx.utility.batchAll`. + * @param input.did The DID document. + * @param input.extrinsics The array of unsigned extrinsics to sign. + * @param input.sign The callback to sign the operation. + * @param input.submitter The KILT account to bind the DID operation to (to avoid MitM and replay attacks). + * @param input.nonce The optional nonce to use for the first batch, next batches will use incremented value. + * @returns The DID-signed submittable extrinsic. + */ +export async function authorizeBatch({ + batchFunction, + did, + extrinsics, + nonce, + sign, + submitter, +}: { + batchFunction: SubmittableExtrinsicFunction<'promise'> + did: DidUri + extrinsics: Extrinsic[] + nonce?: BN + sign: SignExtrinsicCallback + submitter: KiltAddress +}): Promise { + if (extrinsics.length === 0) { + throw new SDKErrors.DidBatchError( + 'Cannot build a batch with no transactions' + ) + } + + if (parse(did).type === 'light') { + throw new SDKErrors.DidError( + `An extrinsic can only be authorized with a full DID, not with "${did}"` + ) + } + + if (extrinsics.length === 1) { + return authorizeTx(did, extrinsics[0], sign, submitter, { + txCounter: nonce, + }) + } + + const groups = groupExtrinsicsByKeyRelationship(extrinsics) + const firstNonce = nonce || (await getNextNonce(did)) + + const promises = groups.map(async (group, batchIndex) => { + const list = group.extrinsics + const call = list.length === 1 ? list[0] : batchFunction(list) + const txCounter = increaseNonce(firstNonce, batchIndex) + + const { keyRelationship } = group + + return generateDidAuthenticatedTx({ + did, + keyRelationship, + sign, + call, + txCounter, + submitter, + }) + }) + const batches = await Promise.all(promises) + + return batches.length === 1 ? batches[0] : batchFunction(batches) +} \ No newline at end of file diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 3bb8225b11..9d599e6818 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -24,7 +24,7 @@ export interface SignRequestData { /** * The did key relationship to be used. */ - keyRelationship: VerificationKeyRelationship + verificationMethodRelationship: VerificationKeyRelationship /** * The DID to be used for signing. diff --git a/packages/types/src/DidV2.ts b/packages/types/src/DidDocumentV2.ts similarity index 98% rename from packages/types/src/DidV2.ts rename to packages/types/src/DidDocumentV2.ts index a8eb5d7d54..132353f48e 100644 --- a/packages/types/src/DidV2.ts +++ b/packages/types/src/DidDocumentV2.ts @@ -21,7 +21,7 @@ export type DidUri = /** * The fragment part of the DID URI including the `#` character. */ -type UriFragment = `#${string}` +export type UriFragment = `#${string}` /** * URI for DID resources like keys or service endpoints. */ @@ -62,7 +62,7 @@ export type Service = { /* * The value of the type property MUST be a string or a set of strings. In order to maximize interoperability, the service type and its associated properties SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ - type: string + type: string[] /* * The value of the serviceEndpoint property MUST be a string, a map, or a set composed of one or more strings and/or maps. All string values MUST be valid URIs conforming to [RFC3986] and normalized according to the Normalization and Comparison rules in RFC3986 and to any normalization rules in its applicable URI scheme specification. */ diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index 8a7cd29101..f57c88758f 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -11,7 +11,7 @@ import type { DidResourceUri, VerificationMethod, Service, -} from './DidV2.js' +} from './DidDocumentV2' type DidContentType = 'application/did+ld+json' | 'application/did+json' diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index be510ee7af..94c635c4bc 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -27,3 +27,6 @@ export * from './DidResolver.js' export * from './DidDocumentExporter.js' export * from './PublicCredential.js' export * from './Imported.js' + +export * as DidDocumentV2 from './DidDocumentV2.js' +export * as DidResolverV2 from './DidResolverV2.js' From c8b22fc027b6435f9f7e09edcc6b36fa26bd605a Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 29 Sep 2023 14:40:38 +0100 Subject: [PATCH 05/78] Work on DID.signature --- packages/did/src/Did2.chain.ts | 100 +++++++++--------- packages/did/src/Did2.signature.ts | 22 ++++ packages/did/src/Did2.utils.ts | 11 -- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 63 ++++++++++- packages/types/src/DidResolverV2.ts | 12 ++- 5 files changed, 140 insertions(+), 68 deletions(-) create mode 100644 packages/did/src/Did2.signature.ts diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index c63f3a9dc3..f338bda0e1 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -19,16 +19,14 @@ import type { } from '@kiltprotocol/augment-api' import type { + BN, Deposit, DidDocumentV2, KiltAddress, - BN, - NewDidVerificationKey, - NewDidEncryptionKey, + SignExtrinsicCallback, SignRequestData, SignResponseData, SubmittableExtrinsic, - SignExtrinsicCallback, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' @@ -36,27 +34,33 @@ import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { EncryptionKeyType, - RelativeServiceEndpoint, + NewDidEncryptionKey, + NewDidVerificationKey, + NewServiceEndpoint, VerificationKeyType, verificationKeyTypes, VerificationMethodRelationship, } from './DidDetailsv2/DidDetailsV2.js' -import { - EncodedEncryptionKey, - EncodedKey, - EncodedSignature, - EncodedVerificationKey, - getAddressByKey, - getFullDidUri, - parse, -} from './Did2.utils.js' +import { getAddressByKey, getFullDidUri, parse } from './Did2.utils.js' // ### Chain type definitions +export type ChainDidIdentifier = KiltAddress export type ChainDidPublicKey = DidDidDetailsDidPublicKey export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails +export type EncodedVerificationKey = + | { sr25519: Uint8Array } + | { ed25519: Uint8Array } + | { ecdsa: Uint8Array } + +export type EncodedEncryptionKey = { x25519: Uint8Array } + +export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey + +export type EncodedSignature = EncodedVerificationKey + // ### RAW QUERYING (lowest layer) /** @@ -65,7 +69,7 @@ export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails * @param did The DID to format. * @returns The blockchain-formatted DID. */ -export function toChain(did: DidDocumentV2.DidUri): KiltAddress { +export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { return parse(did).address } @@ -75,10 +79,20 @@ export function toChain(did: DidDocumentV2.DidUri): KiltAddress { * @param id The DID resource ID to format. * @returns The blockchain-formatted ID. */ -export function resourceIdToChain(id: DidDocumentV2.UriFragment): string { +export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { return id.replace(/^#/, '') } +/** + * Convert the DID data from blockchain format to the DID URI. + * + * @param encoded The chain-formatted DID. + * @returns The DID URI. + */ +export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { + return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) +} + /** * Convert the deposit data coming from the blockchain to JS object. * @@ -92,7 +106,7 @@ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { } } -type ChainBaseDidVerificationMethod = { +export type ChainDidBaseKey = { /** * Relative key URI: `#` sign followed by fragment part of URI. */ @@ -110,13 +124,13 @@ type ChainBaseDidVerificationMethod = { */ type: string } -export type ChainDidVerificationKey = ChainBaseDidVerificationMethod & { +export type ChainDidVerificationKey = ChainDidBaseKey & { type: VerificationKeyType } -export type ChainDidEncryptionKey = ChainBaseDidVerificationMethod & { +export type ChainDidEncryptionKey = ChainDidBaseKey & { type: EncryptionKeyType } -type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey +export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey export type ChainDidService = { id: string @@ -124,9 +138,7 @@ export type ChainDidService = { urls: string[] } -// ### DECODED QUERYING types - -type ChainDidDetails = { +export type ChainDidDetails = { authentication: [ChainDidVerificationKey] assertionMethod?: [ChainDidVerificationKey] capabilityDelegation?: [ChainDidVerificationKey] @@ -138,32 +150,22 @@ type ChainDidDetails = { deposit: Deposit } -// ### DECODED QUERYING (builds on top of raw querying) - function didPublicKeyDetailsFromChain( keyId: Hash, keyDetails: ChainDidPublicKeyDetails -): ChainDidVerificationKey { +): ChainDidKey { const key = keyDetails.key.isPublicEncryptionKey ? keyDetails.key.asPublicEncryptionKey : keyDetails.key.asPublicVerificationKey return { id: `#${keyId.toHex()}`, - type: key.type.toLowerCase() as ChainDidVerificationKey['type'], publicKey: key.value.toU8a(), + type: key.type.toLowerCase() as + | ChainDidVerificationKey['type'] + | ChainDidEncryptionKey['type'], } } -/** - * Convert the DID data from blockchain format to the DID URI. - * - * @param encoded The chain-formatted DID. - * @returns The DID URI. - */ -export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) -} - /** * Convert the DID Document data from the blockchain format to a JS object. * @@ -188,7 +190,7 @@ export function documentFromChain( didPublicKeyDetailsFromChain(keyId, keyDetails) ) .reduce((res, key) => { - res[resourceIdToChain(key.id)] = key + res[fragmentIdToChain(key.id)] = key return res }, {}) @@ -237,7 +239,7 @@ function isUri(str: string): boolean { } } -const UriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ +const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ /** * Checks if a string is a valid URI fragment according to RFC#3986. @@ -247,7 +249,7 @@ const UriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ */ function isUriFragment(str: string): boolean { try { - return UriFragmentRegex.test(str) && !!decodeURIComponent(str) + return uriFragmentRegex.test(str) && !!decodeURIComponent(str) } catch { return false } @@ -260,14 +262,14 @@ function isUriFragment(str: string): boolean { * * @param endpoint A service endpoint object to check. */ -export function validateService(endpoint: RelativeServiceEndpoint): void { +export function validateNewService(endpoint: NewServiceEndpoint): void { const { id, serviceEndpoint } = endpoint if (id.startsWith('did:kilt')) { throw new SDKErrors.DidError( `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` ) } - if (!isUriFragment(resourceIdToChain(id))) { + if (!isUriFragment(fragmentIdToChain(id))) { throw new SDKErrors.DidError( `The service ID must be valid as a URI fragment according to RFC#3986, which "${id}" is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding the desired id.` ) @@ -287,13 +289,11 @@ export function validateService(endpoint: RelativeServiceEndpoint): void { * @param service The DID service to format. * @returns The blockchain-formatted DID service. */ -export function serviceToChain( - service: RelativeServiceEndpoint -): ChainDidService { - validateService(service) +export function serviceToChain(service: NewServiceEndpoint): ChainDidService { + validateNewService(service) const { id, type, serviceEndpoint } = service return { - id: resourceIdToChain(id), + id: fragmentIdToChain(id), serviceTypes: type, urls: serviceEndpoint, } @@ -316,7 +316,7 @@ export function serviceFromChain( } } -// ### EXTRINSICS types +// ### EXTRINSICS export type AuthorizeCallInput = { did: DidDocumentV2.DidUri @@ -326,8 +326,6 @@ export type AuthorizeCallInput = { blockNumber?: AnyNumber } -// ### EXTRINSICS - export function publicKeyToChain( key: NewDidVerificationKey ): EncodedVerificationKey @@ -352,7 +350,7 @@ interface GetStoreTxInput { capabilityDelegation?: [NewDidVerificationKey] keyAgreement?: NewDidEncryptionKey[] - service?: RelativeServiceEndpoint[] + service?: NewServiceEndpoint[] } export type GetStoreTxSignCallback = ( diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts new file mode 100644 index 0000000000..809e5804d6 --- /dev/null +++ b/packages/did/src/Did2.signature.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { + DidDocumentV2, + DidResolveKey, + VerificationKeyRelationship, +} from '@kiltprotocol/types' + +export type DidSignatureVerificationInput = { + message: string | Uint8Array + signature: Uint8Array + keyUri: DidDocumentV2.DidResourceUri + expectedSigner?: DidDocumentV2.DidUri + allowUpgraded?: boolean + expectedVerificationMethod?: VerificationKeyRelationship + didResolveKey?: DidResolveKey +} diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index cef10c1dd5..3b7ffe308c 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -112,17 +112,6 @@ export function isSameSubject( return parse(didA).address === parse(didB).address } -export type EncodedVerificationKey = - | { sr25519: Uint8Array } - | { ed25519: Uint8Array } - | { ecdsa: Uint8Array } - -export type EncodedEncryptionKey = { x25519: Uint8Array } - -export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey - -export type EncodedSignature = EncodedVerificationKey - /** * Checks that a string (or other input) is a valid KILT DID uri with or without a URI fragment. * Throws otherwise. diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 889b7b1fdc..168e9acb54 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' +import type { BN, DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' export type VerificationMethodRelationship = | 'authentication' @@ -40,7 +40,66 @@ const encryptionKeyTypesC = ['x25519'] as const export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] export type EncryptionKeyType = typeof encryptionKeyTypesC[number] -export type RelativeServiceEndpoint = Pick< +/** + * The SDK-specific base details of a DID key. + */ +export type BaseDidKey = { + /** + * Relative key URI: `#` sign followed by fragment part of URI. + */ + id: DidDocumentV2.UriFragment + /** + * The public key material. + */ + publicKey: Uint8Array + /** + * The inclusion block of the key, if stored on chain. + */ + includedAt?: BN + /** + * The type of the key. + */ + type: string +} + +/** + * Type of a new key material to add under a DID. + */ +export type BaseNewDidKey = { + publicKey: Uint8Array + type: string +} + +/** + * Type of a new verification key to add under a DID. + */ +export type NewDidVerificationKey = BaseNewDidKey & { + type: VerificationKeyType +} +/** + * A new public key specified when creating a new light DID. + */ +export type NewLightDidVerificationKey = NewDidVerificationKey & { + type: LightDidSupportedVerificationKeyType +} +/** + * Type of a new encryption key to add under a DID. + */ +export type NewDidEncryptionKey = BaseNewDidKey & { type: EncryptionKeyType } +/** + * The SDK-specific details of a DID verification key. + */ +export type DidVerificationKey = BaseDidKey & { type: VerificationKeyType } +/** + * The SDK-specific details of a DID encryption key. + */ +export type DidEncryptionKey = BaseDidKey & { type: EncryptionKeyType } +/** + * The SDK-specific details of a DID key. + */ +export type DidKey = DidVerificationKey | DidEncryptionKey + +export type NewServiceEndpoint = Pick< DidDocumentV2.Service, 'serviceEndpoint' | 'type' > & { id: DidDocumentV2.UriFragment } diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index f57c88758f..880c25a701 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -144,10 +144,10 @@ type DereferenceResult = { contentMetadata: DereferenceContentMetadata } -export interface DidResolver { - /* - * The resolve function returns the DID document in its abstract form (a map). - */ +/* + * The resolve function returns the DID document in its abstract form (a map). + */ +export interface ResolveDid { resolve: ( /* * This is the DID to resolve. @@ -160,7 +160,9 @@ export interface DidResolver { */ resolutionOptions: ResolutionOptions ) => Promise +} +export interface DereferenceDidUrl { dereference: ( /* * A conformant DID URL as a single string. @@ -176,3 +178,5 @@ export interface DidResolver { dereferenceOptions: DereferenceOptions ) => Promise } + +export interface DidResolver extends ResolveDid, DereferenceDidUrl {} From a6a77346cbd4d950dfd3cad40630c2a15242a6f6 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 29 Sep 2023 15:47:41 +0100 Subject: [PATCH 06/78] Bit more mess here and there --- packages/did/package.json | 3 +- packages/did/src/Did.signature.spec.ts | 32 +- packages/did/src/Did.signature.ts | 4 +- packages/did/src/Did2.chain.ts | 2 +- packages/did/src/Did2.utils.ts | 74 +++- packages/did/src/Did3.chain.ts | 360 ++++++++++++++++++ packages/did/src/DidDetailsv2/DidDetailsV2.ts | 132 ++++--- 7 files changed, 511 insertions(+), 96 deletions(-) create mode 100644 packages/did/src/Did3.chain.ts diff --git a/packages/did/package.json b/packages/did/package.json index 05c3a631c6..00f41c2f1a 100644 --- a/packages/did/package.json +++ b/packages/did/package.json @@ -44,6 +44,7 @@ "@polkadot/types": "^10.4.0", "@polkadot/types-codec": "^10.4.0", "@polkadot/util": "^12.0.0", - "@polkadot/util-crypto": "^12.0.0" + "@polkadot/util-crypto": "^12.0.0", + "multibase": "^4.0.6" } } diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 449e698136..a577a72911 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -59,7 +59,7 @@ describe('light DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -90,13 +90,13 @@ describe('light DID', () => { const deserialized = signatureFromJson(oldSignature) expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.keyUri).toStrictEqual(keyUri) + expect(deserialized.verificationMethodUri).toStrictEqual(keyUri) expect(deserialized).not.toHaveProperty('keyId') }) it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: SIGNED_BYTES, did: did.uri, keyRelationship: 'authentication', @@ -113,7 +113,7 @@ describe('light DID', () => { it('fails if relationship does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -131,7 +131,7 @@ describe('light DID', () => { it('fails if key id does not match', async () => { const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, keyUri } = await sign({ + let { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -150,7 +150,7 @@ describe('light DID', () => { it('fails if signature does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -169,7 +169,7 @@ describe('light DID', () => { jest.mocked(resolveKey).mockRestore() const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, keyUri } = await sign({ + let { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -189,7 +189,7 @@ describe('light DID', () => { it('does not verify if migrated to Full DID', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Migrated')) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -214,7 +214,7 @@ describe('light DID', () => { it('detects signer expectation mismatch if signature is by unrelated did', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -237,7 +237,7 @@ describe('light DID', () => { it('allows variations of the same light did', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -285,7 +285,7 @@ describe('full DID', () => { } sign = async ({ data }) => ({ signature: keypair.sign(data), - keyUri: `${did.uri}#0x12345`, + verificationMethodUri: `${did.uri}#0x12345`, keyType: 'sr25519', }) }) @@ -303,7 +303,7 @@ describe('full DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -320,7 +320,7 @@ describe('full DID', () => { it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: SIGNED_BYTES, did: did.uri, keyRelationship: 'authentication', @@ -338,7 +338,7 @@ describe('full DID', () => { it('does not verify if deactivated', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Deactivated')) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -356,7 +356,7 @@ describe('full DID', () => { it('does not verify if not on chain', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Not on chain')) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -373,7 +373,7 @@ describe('full DID', () => { it('accepts signature of full did for light did if enabled', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethodUri: keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 07a853d79a..9dba017c87 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -129,7 +129,7 @@ export function isDidSignature( */ export function signatureToJson({ signature, - keyUri, + verificationMethodUri: keyUri, }: SignResponseData): DidSignature { return { signature: Crypto.u8aToHex(signature), keyUri } } @@ -146,5 +146,5 @@ export function signatureFromJson( ): Pick { const keyUri = 'keyUri' in input ? input.keyUri : input.keyId const signature = Crypto.coToUInt8(input.signature) - return { signature, keyUri } + return { signature, verificationMethodUri: keyUri } } diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index f338bda0e1..cf69028a14 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -350,7 +350,7 @@ interface GetStoreTxInput { capabilityDelegation?: [NewDidVerificationKey] keyAgreement?: NewDidEncryptionKey[] - service?: NewServiceEndpoint[] + service?: RelativeServiceEndpoint[] } export type GetStoreTxSignCallback = ( diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index 3b7ffe308c..0ba9b5bf41 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -5,12 +5,18 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { decode as multibaseDecode } from 'multibase' + import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' import type { DidDocumentV2, KiltAddress } from '@kiltprotocol/types' -import type { VerificationKeyType } from './DidDetailsv2/DidDetailsV2.js' +import type { + DidKeyType, + VerificationKeyType, + VerificationMethodRelationship, +} from './DidDetailsv2/DidDetailsV2.js' // The latest version for KILT light DIDs. const LIGHT_DID_LATEST_VERSION = 1 @@ -98,6 +104,48 @@ export function parse( throw new SDKErrors.InvalidDidFormatError(didUri) } +type DecodedVerificationMethod = { + publicKey: Uint8Array + keyType: DidKeyType +} + +const multicodecPrefixes: Record = { + 0xe7: ['ecdsa', 33], + 0xec: ['x25519', 32], + 0xed: ['ed25519', 32], + 0xef: ['sr25519', 32], +} + +/** + * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. + * + * @param verificationMethod The verification method. + * @returns The decoded public key and [DidKeyType]. + */ +export function decodeMulticodecVerificationMethod( + verificationMethod: Pick< + DidDocumentV2.VerificationMethod, + 'publicKeyMultibase' + > +): DecodedVerificationMethod { + const decodedMulticodecPublicKey = multibaseDecode( + verificationMethod.publicKeyMultibase + ) + const [keyTypeFlag, publicKey] = [ + decodedMulticodecPublicKey.subarray(0, 1)[0], + decodedMulticodecPublicKey.subarray(1), + ] + const [keyType, expectedPublicKeyLength] = multicodecPrefixes[keyTypeFlag] + if (keyType !== undefined && publicKey.length === expectedPublicKeyLength) { + return { + keyType, + publicKey, + } + } + // TODO: Change to proper error + throw new Error('Invalid encoding of the verification method.') +} + /** * Returns true if both didA and didB refer to the same DID subject, i.e., whether they have the same identifier as specified in the method spec. * @@ -148,21 +196,15 @@ export function validateUri( DataUtils.verifyKiltAddress(address) } -/** - * Internal: derive the address part of the DID when it is created from authentication key. - * - * @param input The authentication key. - * @param input.publicKey The public key. - * @param input.type The type of the key. - * @returns The expected address of the DID. - */ -export function getAddressByKey({ - publicKey, - type, -}: { - publicKey: Uint8Array - type: VerificationKeyType -}): KiltAddress { +// TODO: Fix JSDoc +export function getAddressByVerificationMethod( + verificationMethod: Pick< + DidDocumentV2.VerificationMethod, + 'publicKeyMultibase' + > +): KiltAddress { + const { keyType: type, publicKey } = + decodeMulticodecVerificationMethod(verificationMethod) if (type === 'ed25519' || type === 'sr25519') { return encodeAddress(publicKey, ss58Format) } diff --git a/packages/did/src/Did3.chain.ts b/packages/did/src/Did3.chain.ts new file mode 100644 index 0000000000..1c5cf42a2f --- /dev/null +++ b/packages/did/src/Did3.chain.ts @@ -0,0 +1,360 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { Option } from '@polkadot/types' +import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' +import type { AnyNumber } from '@polkadot/types/types' + +import type { + DidDidDetails, + DidDidDetailsDidAuthorizedCallOperation, + DidDidDetailsDidPublicKey, + DidDidDetailsDidPublicKeyDetails, + DidServiceEndpointsDidEndpoint, + KiltSupportDeposit, +} from '@kiltprotocol/augment-api' + +import type { + BN, + Deposit, + DidDocumentV2, + KiltAddress, + SignExtrinsicCallback, + SignRequestData, + SignResponseData, + SubmittableExtrinsic, +} from '@kiltprotocol/types' + +import { ConfigService } from '@kiltprotocol/config' +import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' + +import { + DidKeyType, + EncryptionKeyType, + NewServiceEndpoint, + NewVerificationMethod, + VerificationKeyType, + verificationKeyTypes, + VerificationMethodRelationship, +} from './DidDetailsv2/DidDetailsV2.js' + +import { decodeMulticodecVerificationMethod, getAddressByKey, getAddressByVerificationMethod, getFullDidUri, parse } from './Did2.utils.js' +import { ChainDidPublicKeyDetails, EncodedSignature } from './Did2.chain.js' + +export type ChainDidIdentifier = KiltAddress + +export type EncodedVerificationKey = + | { sr25519: Uint8Array } + | { ed25519: Uint8Array } + | { ecdsa: Uint8Array } + +export type EncodedEncryptionKey = { x25519: Uint8Array } +export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey + +export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { + return parse(did).address +} + +export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { + return id.replace(/^#/, '') +} + +export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { + return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) +} + +export function depositFromChain(deposit: KiltSupportDeposit): Deposit { + return { + owner: Crypto.encodeAddress(deposit.owner, ss58Format), + amount: deposit.amount.toBn(), + } +} + +export type ChainDidBaseKey = { + id: DidDocumentV2.UriFragment + publicKey: Uint8Array + includedAt?: BN + type: string +} +export type ChainDidVerificationKey = ChainDidBaseKey & { + type: VerificationKeyType +} +export type ChainDidEncryptionKey = ChainDidBaseKey & { + type: EncryptionKeyType +} +export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey + +export type ChainDidService = { + id: string + serviceTypes: string[] + urls: string[] +} + +export type ChainDidDetails = { + authentication: [ChainDidVerificationKey] + assertionMethod?: [ChainDidVerificationKey] + capabilityDelegation?: [ChainDidVerificationKey] + keyAgreement?: ChainDidEncryptionKey[] + + service?: ChainDidService[] + + lastTxCounter: BN + deposit: Deposit +} + +function didPublicKeyDetailsFromChain( + keyId: Hash, + keyDetails: ChainDidPublicKeyDetails +): ChainDidKey { + const key = keyDetails.key.isPublicEncryptionKey + ? keyDetails.key.asPublicEncryptionKey + : keyDetails.key.asPublicVerificationKey + return { + id: `#${keyId.toHex()}`, + publicKey: key.value.toU8a(), + type: key.type.toLowerCase() as + | ChainDidVerificationKey['type'] + | ChainDidEncryptionKey['type'], + } +} + +export function documentFromChain( + encoded: Option +): ChainDidDetails { + const { + publicKeys, + authenticationKey, + attestationKey, + delegationKey, + keyAgreementKeys, + lastTxCounter, + deposit, + } = encoded.unwrap() + + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => + didPublicKeyDetailsFromChain(keyId, keyDetails) + ) + .reduce((res, key) => { + res[fragmentIdToChain(key.id)] = key + return res + }, {}) + + const authentication = keys[ + authenticationKey.toHex() + ] as ChainDidVerificationKey + + const didRecord: ChainDidDetails = { + authentication: [authentication], + lastTxCounter: lastTxCounter.toBn(), + deposit: depositFromChain(deposit), + } + if (attestationKey.isSome) { + const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey + didRecord.assertionMethod = [key] + } + if (delegationKey.isSome) { + const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey + didRecord.capabilityDelegation = [key] + } + + const keyAgreementKeyIds = [...keyAgreementKeys.values()].map((keyId) => + keyId.toHex() + ) + if (keyAgreementKeyIds.length > 0) { + didRecord.keyAgreement = keyAgreementKeyIds.map( + (id) => keys[id] as ChainDidEncryptionKey + ) + } + + return didRecord +} + +export function verificationMethodToChain( + verificationMethod: Pick< + DidDocumentV2.VerificationMethod, + 'publicKeyMultibase' + > +): EncodedKey { + const { keyType, publicKey } = + decodeMulticodecVerificationMethod(verificationMethod) + return { + [keyType]: publicKey, + } as EncodedKey +} + +function isUri(str: string): boolean { + try { + const url = new URL(str) // this actually accepts any URI but throws if it can't be parsed + return url.href === str || encodeURI(decodeURI(str)) === str // make sure our URI has not been converted implicitly by URL + } catch { + return false + } +} + +const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ + +function isUriFragment(str: string): boolean { + try { + return uriFragmentRegex.test(str) && !!decodeURIComponent(str) + } catch { + return false + } +} + +export function validateNewService(endpoint: NewServiceEndpoint): void { + const { id, serviceEndpoint } = endpoint + if (id.startsWith('did:kilt')) { + throw new SDKErrors.DidError( + `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` + ) + } + if (!isUriFragment(fragmentIdToChain(id))) { + throw new SDKErrors.DidError( + `The service ID must be valid as a URI fragment according to RFC#3986, which "${id}" is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding the desired id.` + ) + } + serviceEndpoint.forEach((uri) => { + if (!isUri(uri)) { + throw new SDKErrors.DidError( + `A service URI must be a URI according to RFC#3986, which "${uri}" (service id "${id}") is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding resource locators beforehand.` + ) + } + }) +} + +export function serviceToChain(service: NewServiceEndpoint): ChainDidService { + validateNewService(service) + const { id, type, serviceEndpoint } = service + return { + id: fragmentIdToChain(id), + serviceTypes: type, + urls: serviceEndpoint, + } +} + +export function serviceFromChain( + encoded: Option +): NewServiceEndpoint { + const { id, serviceTypes, urls } = encoded.unwrap() + return { + id: `#${id.toUtf8()}`, + type: serviceTypes.map((type) => type.toUtf8()), + serviceEndpoint: urls.map((url) => url.toUtf8()), + } +} + +export type AuthorizeCallInput = { + did: DidDocumentV2.DidUri + txCounter: AnyNumber + call: Extrinsic + submitter: KiltAddress + blockNumber?: AnyNumber +} + +interface GetStoreTxInput { + authentication: [NewVerificationMethod] + assertionMethod?: [NewVerificationMethod] + capabilityDelegation?: [NewVerificationMethod] + keyAgreement?: NewVerificationMethod[] + + service?: NewServiceEndpoint[] +} + +export type GetStoreTxSignCallback = ( + signData: Omit +) => Promise + +export async function getStoreTx( + input: GetStoreTxInput, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const api = ConfigService.get('api') + + const { + authentication, + assertionMethod, + capabilityDelegation, + keyAgreement = [], + service = [], + } = input + + if (!('authentication' in input) || typeof authentication[0] !== 'object') { + throw new SDKErrors.DidError( + `The provided DID does not have an authentication key to sign the creation operation` + ) + } + + // For now, it only takes the first attestation key, if present. + if (assertionMethod && assertionMethod.length > 1) { + throw new SDKErrors.DidError( + `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` + ) + } + + // For now, it only takes the first delegation key, if present. + if (capabilityDelegation && capabilityDelegation.length > 1) { + throw new SDKErrors.DidError( + `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` + ) + } + + const maxKeyAgreementKeys = api.consts.did.maxNewKeyAgreementKeys.toNumber() + if (keyAgreement.length > maxKeyAgreementKeys) { + throw new SDKErrors.DidError( + `The number of key agreement keys in the creation operation is greater than the maximum allowed, which is ${maxKeyAgreementKeys}` + ) + } + + const maxNumberOfServicesPerDid = + api.consts.did.maxNumberOfServicesPerDid.toNumber() + if (service.length > maxNumberOfServicesPerDid) { + throw new SDKErrors.DidError( + `Cannot store more than ${maxNumberOfServicesPerDid} service endpoints per DID` + ) + } + + const [authenticationKey] = authentication + const did = getAddressByVerificationMethod(authenticationKey) + + const newAttestationKey = + assertionMethod && + assertionMethod.length > 0 && + getAddressByVerificationMethod(assertionMethod[0]) + + const newDelegationKey = + capabilityDelegation && + capabilityDelegation.length > 0 && + getAddressByVerificationMethod(capabilityDelegation[0]) + + const newKeyAgreementKeys = keyAgreement.map(getAddressByVerificationMethod) + const newServiceDetails = service.map(serviceToChain) + + const apiInput = { + did, + submitter, + newAttestationKey, + newDelegationKey, + newKeyAgreementKeys, + newServiceDetails, + } + + const encoded = api.registry + .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) + .toU8a() + + const { verificationMethod, signature } = await sign({ + data: encoded, + verificationMethodRelationship: 'authentication', + }) + const { keyType } = decodeMulticodecVerificationMethod(verificationMethod) + const encodedSignature = { + [keyType]: signature, + } as EncodedSignature + return api.tx.did.create(encoded, encodedSignature) +} diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 168e9acb54..a2e7410a5b 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -7,10 +7,14 @@ import type { BN, DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' -export type VerificationMethodRelationship = +export type SignatureVerificationMethodRelationship = | 'authentication' | 'capabilityDelegation' | 'assertionMethod' +export type EncryptionMethodRelationship = 'keyAgreementKey' + +export type VerificationMethodRelationship = + SignatureVerificationMethodRelationship & EncryptionMethodRelationship /** * Possible types for a DID verification key. @@ -40,66 +44,74 @@ const encryptionKeyTypesC = ['x25519'] as const export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] export type EncryptionKeyType = typeof encryptionKeyTypesC[number] -/** - * The SDK-specific base details of a DID key. - */ -export type BaseDidKey = { - /** - * Relative key URI: `#` sign followed by fragment part of URI. - */ - id: DidDocumentV2.UriFragment - /** - * The public key material. - */ - publicKey: Uint8Array - /** - * The inclusion block of the key, if stored on chain. - */ - includedAt?: BN - /** - * The type of the key. - */ - type: string -} +export type DidKeyType = VerificationKeyType | EncryptionKeyType -/** - * Type of a new key material to add under a DID. - */ -export type BaseNewDidKey = { - publicKey: Uint8Array - type: string -} +// /** +// * The SDK-specific base details of a DID key. +// */ +// export type BaseDidKey = { +// /** +// * Relative key URI: `#` sign followed by fragment part of URI. +// */ +// id: DidDocumentV2.UriFragment +// /** +// * The public key material. +// */ +// publicKey: Uint8Array +// /** +// * The inclusion block of the key, if stored on chain. +// */ +// includedAt?: BN +// /** +// * The type of the key. +// */ +// type: string +// } -/** - * Type of a new verification key to add under a DID. - */ -export type NewDidVerificationKey = BaseNewDidKey & { - type: VerificationKeyType -} -/** - * A new public key specified when creating a new light DID. - */ -export type NewLightDidVerificationKey = NewDidVerificationKey & { - type: LightDidSupportedVerificationKeyType +// /** +// * Type of a new key material to add under a DID. +// */ +// export type BaseNewDidKey = { +// publicKey: Uint8Array +// type: string +// } + +// /** +// * Type of a new verification key to add under a DID. +// */ +// export type NewDidVerificationKey = BaseNewDidKey & { +// type: VerificationKeyType +// } +// /** +// * A new public key specified when creating a new light DID. +// */ +// export type NewLightDidVerificationKey = NewDidVerificationKey & { +// type: LightDidSupportedVerificationKeyType +// } +// /** +// * Type of a new encryption key to add under a DID. +// */ +// export type NewDidEncryptionKey = BaseNewDidKey & { type: EncryptionKeyType } +// /** +// * The SDK-specific details of a DID verification key. +// */ +// export type DidVerificationKey = BaseDidKey & { type: VerificationKeyType } +// /** +// * The SDK-specific details of a DID encryption key. +// */ +// export type DidEncryptionKey = BaseDidKey & { type: EncryptionKeyType } +// /** +// * The SDK-specific details of a DID key. +// */ +// export type DidKey = DidVerificationKey | DidEncryptionKey + +export type NewVerificationMethod = Omit< + DidDocumentV2.VerificationMethod, + 'controller' +> & { + id: DidDocumentV2.UriFragment } -/** - * Type of a new encryption key to add under a DID. - */ -export type NewDidEncryptionKey = BaseNewDidKey & { type: EncryptionKeyType } -/** - * The SDK-specific details of a DID verification key. - */ -export type DidVerificationKey = BaseDidKey & { type: VerificationKeyType } -/** - * The SDK-specific details of a DID encryption key. - */ -export type DidEncryptionKey = BaseDidKey & { type: EncryptionKeyType } -/** - * The SDK-specific details of a DID key. - */ -export type DidKey = DidVerificationKey | DidEncryptionKey -export type NewServiceEndpoint = Pick< - DidDocumentV2.Service, - 'serviceEndpoint' | 'type' -> & { id: DidDocumentV2.UriFragment } +export type NewServiceEndpoint = DidDocumentV2.Service & { + id: DidDocumentV2.UriFragment +} From 22672d5af28160cf0708daf69d258593f9d3bf91 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 2 Oct 2023 12:02:23 +0100 Subject: [PATCH 07/78] yarn check working fine --- packages/did/src/Did.signature.spec.ts | 32 +- packages/did/src/Did.signature.ts | 4 +- packages/did/src/Did2.chain.ts | 217 +++-------- packages/did/src/Did2.signature.ts | 46 ++- packages/did/src/Did2.utils.ts | 62 ++- packages/did/src/Did3.chain.ts | 360 ------------------ packages/did/src/DidDetailsv2/DidDetailsV2.ts | 74 +--- .../did/src/DidDetailsv2/FullDidDetailsV2.ts | 73 ++-- .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 288 ++++++++++++++ packages/did/src/DidResolver/DidResolverV2.ts | 6 + packages/types/src/CryptoCallbacks.ts | 2 +- packages/types/src/CryptoCallbacksV2.ts | 140 +++++++ packages/types/src/DidDocumentV2.ts | 14 +- packages/types/src/DidResolverV2.ts | 2 +- packages/types/src/index.ts | 1 + yarn.lock | 17 + 16 files changed, 666 insertions(+), 672 deletions(-) delete mode 100644 packages/did/src/Did3.chain.ts create mode 100644 packages/did/src/DidDetailsv2/LightDidDetailsV2.ts create mode 100644 packages/did/src/DidResolver/DidResolverV2.ts create mode 100644 packages/types/src/CryptoCallbacksV2.ts diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index a577a72911..449e698136 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -59,7 +59,7 @@ describe('light DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -90,13 +90,13 @@ describe('light DID', () => { const deserialized = signatureFromJson(oldSignature) expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.verificationMethodUri).toStrictEqual(keyUri) + expect(deserialized.keyUri).toStrictEqual(keyUri) expect(deserialized).not.toHaveProperty('keyId') }) it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: SIGNED_BYTES, did: did.uri, keyRelationship: 'authentication', @@ -113,7 +113,7 @@ describe('light DID', () => { it('fails if relationship does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -131,7 +131,7 @@ describe('light DID', () => { it('fails if key id does not match', async () => { const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, verificationMethodUri: keyUri } = await sign({ + let { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -150,7 +150,7 @@ describe('light DID', () => { it('fails if signature does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -169,7 +169,7 @@ describe('light DID', () => { jest.mocked(resolveKey).mockRestore() const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, verificationMethodUri: keyUri } = await sign({ + let { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -189,7 +189,7 @@ describe('light DID', () => { it('does not verify if migrated to Full DID', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Migrated')) const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -214,7 +214,7 @@ describe('light DID', () => { it('detects signer expectation mismatch if signature is by unrelated did', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -237,7 +237,7 @@ describe('light DID', () => { it('allows variations of the same light did', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -285,7 +285,7 @@ describe('full DID', () => { } sign = async ({ data }) => ({ signature: keypair.sign(data), - verificationMethodUri: `${did.uri}#0x12345`, + keyUri: `${did.uri}#0x12345`, keyType: 'sr25519', }) }) @@ -303,7 +303,7 @@ describe('full DID', () => { it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -320,7 +320,7 @@ describe('full DID', () => { it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: SIGNED_BYTES, did: did.uri, keyRelationship: 'authentication', @@ -338,7 +338,7 @@ describe('full DID', () => { it('does not verify if deactivated', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Deactivated')) const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -356,7 +356,7 @@ describe('full DID', () => { it('does not verify if not on chain', async () => { jest.mocked(resolveKey).mockRejectedValue(new Error('Not on chain')) const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', @@ -373,7 +373,7 @@ describe('full DID', () => { it('accepts signature of full did for light did if enabled', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethodUri: keyUri } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.uri, keyRelationship: 'authentication', diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 9dba017c87..07a853d79a 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -129,7 +129,7 @@ export function isDidSignature( */ export function signatureToJson({ signature, - verificationMethodUri: keyUri, + keyUri, }: SignResponseData): DidSignature { return { signature: Crypto.u8aToHex(signature), keyUri } } @@ -146,5 +146,5 @@ export function signatureFromJson( ): Pick { const keyUri = 'keyUri' in input ? input.keyUri : input.keyId const signature = Crypto.coToUInt8(input.signature) - return { signature, verificationMethodUri: keyUri } + return { signature, keyUri } } diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index cf69028a14..91f8c5960c 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -12,7 +12,6 @@ import type { AnyNumber } from '@polkadot/types/types' import type { DidDidDetails, DidDidDetailsDidAuthorizedCallOperation, - DidDidDetailsDidPublicKey, DidDidDetailsDidPublicKeyDetails, DidServiceEndpointsDidEndpoint, KiltSupportDeposit, @@ -20,12 +19,10 @@ import type { import type { BN, + CryptoCallbacksV2, Deposit, DidDocumentV2, KiltAddress, - SignExtrinsicCallback, - SignRequestData, - SignResponseData, SubmittableExtrinsic, } from '@kiltprotocol/types' @@ -34,20 +31,20 @@ import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { EncryptionKeyType, - NewDidEncryptionKey, - NewDidVerificationKey, NewServiceEndpoint, + NewVerificationMethod, VerificationKeyType, verificationKeyTypes, - VerificationMethodRelationship, } from './DidDetailsv2/DidDetailsV2.js' -import { getAddressByKey, getFullDidUri, parse } from './Did2.utils.js' - -// ### Chain type definitions +import { + decodeMultikeyVerificationMethod, + getAddressByVerificationMethod, + getFullDidUri, + parse, +} from './Did2.utils.js' export type ChainDidIdentifier = KiltAddress -export type ChainDidPublicKey = DidDidDetailsDidPublicKey export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails export type EncodedVerificationKey = @@ -56,49 +53,21 @@ export type EncodedVerificationKey = | { ecdsa: Uint8Array } export type EncodedEncryptionKey = { x25519: Uint8Array } - export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey - export type EncodedSignature = EncodedVerificationKey -// ### RAW QUERYING (lowest layer) - -/** - * Format a DID to be used as a parameter for the blockchain API functions. - - * @param did The DID to format. - * @returns The blockchain-formatted DID. - */ export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { return parse(did).address } -/** - * Format a DID resource ID to be used as a parameter for the blockchain API functions. - - * @param id The DID resource ID to format. - * @returns The blockchain-formatted ID. - */ export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { return id.replace(/^#/, '') } -/** - * Convert the DID data from blockchain format to the DID URI. - * - * @param encoded The chain-formatted DID. - * @returns The DID URI. - */ export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) } -/** - * Convert the deposit data coming from the blockchain to JS object. - * - * @param deposit The blockchain-formatted deposit data. - * @returns The deposit data. - */ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { return { owner: Crypto.encodeAddress(deposit.owner, ss58Format), @@ -107,21 +76,9 @@ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { } export type ChainDidBaseKey = { - /** - * Relative key URI: `#` sign followed by fragment part of URI. - */ id: DidDocumentV2.UriFragment - /** - * The public key material. - */ publicKey: Uint8Array - /** - * The inclusion block of the key, if stored on chain. - */ includedAt?: BN - /** - * The type of the key. - */ type: string } export type ChainDidVerificationKey = ChainDidBaseKey & { @@ -166,12 +123,6 @@ function didPublicKeyDetailsFromChain( } } -/** - * Convert the DID Document data from the blockchain format to a JS object. - * - * @param encoded The chain-formatted DID Document. - * @returns The DID Document. - */ export function documentFromChain( encoded: Option ): ChainDidDetails { @@ -224,12 +175,19 @@ export function documentFromChain( return didRecord } -/** - * Checks if a string is a valid URI according to RFC#3986. - * - * @param str String to be checked. - * @returns Whether `str` is a valid URI. - */ +export function verificationMethodToChain( + verificationMethod: Pick< + DidDocumentV2.VerificationMethod, + 'publicKeyMultibase' + > +): EncodedKey { + const { keyType, publicKey } = + decodeMultikeyVerificationMethod(verificationMethod) + return { + [keyType]: publicKey, + } as EncodedKey +} + function isUri(str: string): boolean { try { const url = new URL(str) // this actually accepts any URI but throws if it can't be parsed @@ -241,12 +199,6 @@ function isUri(str: string): boolean { const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ -/** - * Checks if a string is a valid URI fragment according to RFC#3986. - * - * @param str String to be checked. - * @returns Whether `str` is a valid URI fragment. - */ function isUriFragment(str: string): boolean { try { return uriFragmentRegex.test(str) && !!decodeURIComponent(str) @@ -255,13 +207,6 @@ function isUriFragment(str: string): boolean { } } -/** - * Performs sanity checks on service endpoint data, making sure that the following conditions are met: - * - The `id` property is a string containing a valid URI fragment according to RFC#3986, not a complete DID URI. - * - If the `uris` property contains one or more strings, they must be valid URIs according to RFC#3986. - * - * @param endpoint A service endpoint object to check. - */ export function validateNewService(endpoint: NewServiceEndpoint): void { const { id, serviceEndpoint } = endpoint if (id.startsWith('did:kilt')) { @@ -283,12 +228,6 @@ export function validateNewService(endpoint: NewServiceEndpoint): void { }) } -/** - * Format the DID service to be used as a parameter for the blockchain API functions. - * - * @param service The DID service to format. - * @returns The blockchain-formatted DID service. - */ export function serviceToChain(service: NewServiceEndpoint): ChainDidService { validateNewService(service) const { id, type, serviceEndpoint } = service @@ -299,15 +238,9 @@ export function serviceToChain(service: NewServiceEndpoint): ChainDidService { } } -/** - * Convert the DID service data coming from the blockchain to JS object. - * - * @param encoded The blockchain-formatted DID service data. - * @returns The DID service. - */ export function serviceFromChain( encoded: Option -): RelativeServiceEndpoint { +): NewServiceEndpoint { const { id, serviceTypes, urls } = encoded.unwrap() return { id: `#${id.toUtf8()}`, @@ -316,8 +249,6 @@ export function serviceFromChain( } } -// ### EXTRINSICS - export type AuthorizeCallInput = { did: DidDocumentV2.DidUri txCounter: AnyNumber @@ -326,54 +257,19 @@ export type AuthorizeCallInput = { blockNumber?: AnyNumber } -export function publicKeyToChain( - key: NewDidVerificationKey -): EncodedVerificationKey -export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey - -/** - * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. - * - * @param key Object describing data associated with a public key. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ -export function publicKeyToChain( - key: NewDidVerificationKey | NewDidEncryptionKey -): EncodedKey { - // TypeScript can't infer type here, so we have to add a type assertion. - return { [key.type]: key.publicKey } as EncodedKey -} - interface GetStoreTxInput { - authentication: [NewDidVerificationKey] - assertionMethod?: [NewDidVerificationKey] - capabilityDelegation?: [NewDidVerificationKey] - keyAgreement?: NewDidEncryptionKey[] + authentication: [NewVerificationMethod] + assertionMethod?: [NewVerificationMethod] + capabilityDelegation?: [NewVerificationMethod] + keyAgreement?: NewVerificationMethod[] - service?: RelativeServiceEndpoint[] + service?: NewServiceEndpoint[] } export type GetStoreTxSignCallback = ( - signData: Omit -) => Promise> + signData: Omit +) => Promise -/** - * Create a DID creation operation which includes the information provided. - * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys and service endpoints. - * - * A DID creation operation can contain at most 25 new service endpoints. - * Additionally, each service endpoint must respect the following conditions: - * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. - * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. - * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. - * - * @param input The DID keys and services to store, also accepts DidDocument, so you can store a light DID for example. - * @param submitter The KILT address authorized to submit the creation operation. - * @param sign The sign callback. The authentication key has to be used. - * - * @returns The SubmittableExtrinsic for the DID creation operation. - */ export async function getStoreTx( input: GetStoreTxInput, submitter: KiltAddress, @@ -425,19 +321,19 @@ export async function getStoreTx( } const [authenticationKey] = authentication - const did = getAddressByKey(authenticationKey) + const did = getAddressByVerificationMethod(authenticationKey) const newAttestationKey = assertionMethod && assertionMethod.length > 0 && - publicKeyToChain(assertionMethod[0]) + getAddressByVerificationMethod(assertionMethod[0]) const newDelegationKey = capabilityDelegation && capabilityDelegation.length > 0 && - publicKeyToChain(capabilityDelegation[0]) + getAddressByVerificationMethod(capabilityDelegation[0]) - const newKeyAgreementKeys = keyAgreement.map(publicKeyToChain) + const newKeyAgreementKeys = keyAgreement.map(getAddressByVerificationMethod) const newServiceDetails = service.map(serviceToChain) const apiInput = { @@ -453,35 +349,22 @@ export async function getStoreTx( .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) .toU8a() - const signature = await sign({ + const { signature, verificationMethod } = await sign({ data: encoded, verificationMethodRelationship: 'authentication', }) + const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) const encodedSignature = { - [signature.keyType]: signature.signature, + [keyType]: signature, } as EncodedSignature return api.tx.did.create(encoded, encodedSignature) } export interface SigningOptions { - sign: SignExtrinsicCallback - verificationMethodRelationship: VerificationMethodRelationship + sign: CryptoCallbacksV2.SignExtrinsicCallback + verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship } -/** - * DID related operations on the KILT blockchain require authorization by a full DID. This is realized by requiring that relevant extrinsics are signed with a key featured by a full DID as a verification method. - * Such extrinsics can be produced using this function. - * - * @param params Object wrapping all input to the function. - * @param params.did Full DID. - * @param params.verificationMethodRelationship DID verification relationship to be used for authorization. - * @param params.sign The callback to interface with the key store managing the private key to be used. - * @param params.call The call or extrinsic to be authorized. - * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. - * @param params.submitter Payment account allowed to submit this extrinsic and cover its fees, which will end up owning any deposit associated with newly created records. - * @param params.blockNumber Block number for determining the validity period of this authorization. If omitted, the current block number will be fetched from chain. - * @returns A DID authorized extrinsic that, after signing with the payment account mentioned in the params, is ready for submission. - */ export async function generateDidAuthenticatedTx({ did, verificationMethodRelationship, @@ -503,34 +386,28 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const signature = await sign({ + const { verificationMethod, signature } = await sign({ data: signableCall.toU8a(), verificationMethodRelationship, did, }) + const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) const encodedSignature = { - [signature.keyType]: signature.signature, + [keyType]: signature, } as EncodedSignature return api.tx.did.submitDidCall(signableCall, encodedSignature) } -// ### Chain utils -/** - * Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics. - * - * @param key Object describing data associated with a public key. - * @param signature Object containing a signature generated with a full DID associated public key. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ export function didSignatureToChain( - key: ChainDidVerificationKey, - signature: Uint8Array + signature: Uint8Array, + verificationMethod: DidDocumentV2.VerificationMethod ): EncodedSignature { - if (!verificationKeyTypes.includes(key.type)) { + const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) + if (!verificationKeyTypes.includes(keyType)) { throw new SDKErrors.DidError( - `encodedDidSignature requires a verification key. A key of type "${key.type}" was used instead` + `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` ) } - return { [key.type]: signature } as EncodedSignature + return { [keyType]: signature } as EncodedSignature } diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts index 809e5804d6..9edcb4da82 100644 --- a/packages/did/src/Did2.signature.ts +++ b/packages/did/src/Did2.signature.ts @@ -6,17 +6,53 @@ */ import type { + CryptoCallbacksV2, DidDocumentV2, - DidResolveKey, - VerificationKeyRelationship, + DidResolverV2, } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' export type DidSignatureVerificationInput = { message: string | Uint8Array signature: Uint8Array - keyUri: DidDocumentV2.DidResourceUri + verificationMethodUri: DidDocumentV2.DidResourceUri expectedSigner?: DidDocumentV2.DidUri allowUpgraded?: boolean - expectedVerificationMethod?: VerificationKeyRelationship - didResolveKey?: DidResolveKey + expectedVerificationMethodRelationship?: DidDocumentV2.SignatureVerificationMethodRelationship + dereferenceDidUrl?: DidResolverV2.DereferenceDidUrl +} + +export type DidSignature = { + verificationMethod: DidDocumentV2.VerificationMethod + signature: string +} + +// Used solely for retro-compatibility with previously-generated DID signatures. +// It is reasonable to think that it will be removed at some point in the future. +type OldDidSignatureV1 = { + signature: string + keyId: DidDocumentV2.DidResourceUri +} +type OldDidSignatureV2 = { + signature: string + keyUri: DidDocumentV2.DidResourceUri +} + +// TODO: JSDocs +export function signatureFromJson( + input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 +): Pick & { + verificationMethodUri: DidDocumentV2.DidResourceUri +} { + const verificationMethodUri = (() => { + if ('keyUri' in input) { + return input.keyUri + } + if ('keyId' in input) { + return input.keyId + } + return `${input.verificationMethod.controller}${input.verificationMethod.id}` as DidDocumentV2.DidResourceUri + })() + const signature = Crypto.coToUInt8(input.signature) + return { signature, verificationMethodUri } } diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index 0ba9b5bf41..9c5dda8451 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -5,18 +5,14 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { decode as multibaseDecode } from 'multibase' +import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' import type { DidDocumentV2, KiltAddress } from '@kiltprotocol/types' -import type { - DidKeyType, - VerificationKeyType, - VerificationMethodRelationship, -} from './DidDetailsv2/DidDetailsV2.js' +import type { DidKeyType } from './DidDetailsv2/DidDetailsV2.js' // The latest version for KILT light DIDs. const LIGHT_DID_LATEST_VERSION = 1 @@ -115,6 +111,12 @@ const multicodecPrefixes: Record = { 0xed: ['ed25519', 32], 0xef: ['sr25519', 32], } +const multicodecReversePrefixes: Record = { + ecdsa: 0xe7, + ed25519: 0xed, + sr25519: 0xef, + x25519: 0xec, +} /** * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. @@ -122,7 +124,7 @@ const multicodecPrefixes: Record = { * @param verificationMethod The verification method. * @returns The decoded public key and [DidKeyType]. */ -export function decodeMulticodecVerificationMethod( +export function decodeMultikeyVerificationMethod( verificationMethod: Pick< DidDocumentV2.VerificationMethod, 'publicKeyMultibase' @@ -146,6 +148,34 @@ export function decodeMulticodecVerificationMethod( throw new Error('Invalid encoding of the verification method.') } +export function encodeVerificationMethodToMultiKey( + controller: DidDocumentV2.VerificationMethod['controller'], + id: DidDocumentV2.VerificationMethod['id'], + { keyType, publicKey }: DecodedVerificationMethod +): DidDocumentV2.VerificationMethod { + const multiCodecPublicKeyPrefix = multicodecReversePrefixes[keyType] + if (multiCodecPublicKeyPrefix === undefined) { + // TODO: Proper error + throw new Error(`Invalid key type provided: ${keyType}.`) + } + const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] + if (publicKey.length !== expectedPublicKeySize) { + // TODO: Proper error + throw new Error( + `Provided public key does not match the expected length: ${expectedPublicKeySize}.` + ) + } + const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] + return { + controller, + id, + type: 'MultiKey', + publicKeyMultibase: Buffer.from( + multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) + ).toString() as `z${string}`, + } +} + /** * Returns true if both didA and didB refer to the same DID subject, i.e., whether they have the same identifier as specified in the method spec. * @@ -204,7 +234,7 @@ export function getAddressByVerificationMethod( > ): KiltAddress { const { keyType: type, publicKey } = - decodeMulticodecVerificationMethod(verificationMethod) + decodeMultikeyVerificationMethod(verificationMethod) if (type === 'ed25519' || type === 'sr25519') { return encodeAddress(publicKey, ss58Format) } @@ -236,15 +266,15 @@ export function getFullDidUri( /** * Builds the URI of a full DID if it is created with the authentication key provided. * - * @param key The key that will be used as DID authentication key. - * @param key.publicKey The public key. - * @param key.type The type of the key. + * @param verificationMethod The DID verification method. * @returns The expected full DID URI. */ -export function getFullDidUriFromKey(key: { - publicKey: Uint8Array - type: VerificationKeyType -}): DidDocumentV2.DidUri { - const address = getAddressByKey(key) +export function getFullDidUriFromVerificationMethod( + verificationMethod: Pick< + DidDocumentV2.VerificationMethod, + 'publicKeyMultibase' + > +): DidDocumentV2.DidUri { + const address = getAddressByVerificationMethod(verificationMethod) return getFullDidUri(address) } diff --git a/packages/did/src/Did3.chain.ts b/packages/did/src/Did3.chain.ts deleted file mode 100644 index 1c5cf42a2f..0000000000 --- a/packages/did/src/Did3.chain.ts +++ /dev/null @@ -1,360 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { Option } from '@polkadot/types' -import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' -import type { AnyNumber } from '@polkadot/types/types' - -import type { - DidDidDetails, - DidDidDetailsDidAuthorizedCallOperation, - DidDidDetailsDidPublicKey, - DidDidDetailsDidPublicKeyDetails, - DidServiceEndpointsDidEndpoint, - KiltSupportDeposit, -} from '@kiltprotocol/augment-api' - -import type { - BN, - Deposit, - DidDocumentV2, - KiltAddress, - SignExtrinsicCallback, - SignRequestData, - SignResponseData, - SubmittableExtrinsic, -} from '@kiltprotocol/types' - -import { ConfigService } from '@kiltprotocol/config' -import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' - -import { - DidKeyType, - EncryptionKeyType, - NewServiceEndpoint, - NewVerificationMethod, - VerificationKeyType, - verificationKeyTypes, - VerificationMethodRelationship, -} from './DidDetailsv2/DidDetailsV2.js' - -import { decodeMulticodecVerificationMethod, getAddressByKey, getAddressByVerificationMethod, getFullDidUri, parse } from './Did2.utils.js' -import { ChainDidPublicKeyDetails, EncodedSignature } from './Did2.chain.js' - -export type ChainDidIdentifier = KiltAddress - -export type EncodedVerificationKey = - | { sr25519: Uint8Array } - | { ed25519: Uint8Array } - | { ecdsa: Uint8Array } - -export type EncodedEncryptionKey = { x25519: Uint8Array } -export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey - -export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { - return parse(did).address -} - -export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { - return id.replace(/^#/, '') -} - -export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) -} - -export function depositFromChain(deposit: KiltSupportDeposit): Deposit { - return { - owner: Crypto.encodeAddress(deposit.owner, ss58Format), - amount: deposit.amount.toBn(), - } -} - -export type ChainDidBaseKey = { - id: DidDocumentV2.UriFragment - publicKey: Uint8Array - includedAt?: BN - type: string -} -export type ChainDidVerificationKey = ChainDidBaseKey & { - type: VerificationKeyType -} -export type ChainDidEncryptionKey = ChainDidBaseKey & { - type: EncryptionKeyType -} -export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey - -export type ChainDidService = { - id: string - serviceTypes: string[] - urls: string[] -} - -export type ChainDidDetails = { - authentication: [ChainDidVerificationKey] - assertionMethod?: [ChainDidVerificationKey] - capabilityDelegation?: [ChainDidVerificationKey] - keyAgreement?: ChainDidEncryptionKey[] - - service?: ChainDidService[] - - lastTxCounter: BN - deposit: Deposit -} - -function didPublicKeyDetailsFromChain( - keyId: Hash, - keyDetails: ChainDidPublicKeyDetails -): ChainDidKey { - const key = keyDetails.key.isPublicEncryptionKey - ? keyDetails.key.asPublicEncryptionKey - : keyDetails.key.asPublicVerificationKey - return { - id: `#${keyId.toHex()}`, - publicKey: key.value.toU8a(), - type: key.type.toLowerCase() as - | ChainDidVerificationKey['type'] - | ChainDidEncryptionKey['type'], - } -} - -export function documentFromChain( - encoded: Option -): ChainDidDetails { - const { - publicKeys, - authenticationKey, - attestationKey, - delegationKey, - keyAgreementKeys, - lastTxCounter, - deposit, - } = encoded.unwrap() - - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => - didPublicKeyDetailsFromChain(keyId, keyDetails) - ) - .reduce((res, key) => { - res[fragmentIdToChain(key.id)] = key - return res - }, {}) - - const authentication = keys[ - authenticationKey.toHex() - ] as ChainDidVerificationKey - - const didRecord: ChainDidDetails = { - authentication: [authentication], - lastTxCounter: lastTxCounter.toBn(), - deposit: depositFromChain(deposit), - } - if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey - didRecord.assertionMethod = [key] - } - if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey - didRecord.capabilityDelegation = [key] - } - - const keyAgreementKeyIds = [...keyAgreementKeys.values()].map((keyId) => - keyId.toHex() - ) - if (keyAgreementKeyIds.length > 0) { - didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as ChainDidEncryptionKey - ) - } - - return didRecord -} - -export function verificationMethodToChain( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > -): EncodedKey { - const { keyType, publicKey } = - decodeMulticodecVerificationMethod(verificationMethod) - return { - [keyType]: publicKey, - } as EncodedKey -} - -function isUri(str: string): boolean { - try { - const url = new URL(str) // this actually accepts any URI but throws if it can't be parsed - return url.href === str || encodeURI(decodeURI(str)) === str // make sure our URI has not been converted implicitly by URL - } catch { - return false - } -} - -const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ - -function isUriFragment(str: string): boolean { - try { - return uriFragmentRegex.test(str) && !!decodeURIComponent(str) - } catch { - return false - } -} - -export function validateNewService(endpoint: NewServiceEndpoint): void { - const { id, serviceEndpoint } = endpoint - if (id.startsWith('did:kilt')) { - throw new SDKErrors.DidError( - `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` - ) - } - if (!isUriFragment(fragmentIdToChain(id))) { - throw new SDKErrors.DidError( - `The service ID must be valid as a URI fragment according to RFC#3986, which "${id}" is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding the desired id.` - ) - } - serviceEndpoint.forEach((uri) => { - if (!isUri(uri)) { - throw new SDKErrors.DidError( - `A service URI must be a URI according to RFC#3986, which "${uri}" (service id "${id}") is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding resource locators beforehand.` - ) - } - }) -} - -export function serviceToChain(service: NewServiceEndpoint): ChainDidService { - validateNewService(service) - const { id, type, serviceEndpoint } = service - return { - id: fragmentIdToChain(id), - serviceTypes: type, - urls: serviceEndpoint, - } -} - -export function serviceFromChain( - encoded: Option -): NewServiceEndpoint { - const { id, serviceTypes, urls } = encoded.unwrap() - return { - id: `#${id.toUtf8()}`, - type: serviceTypes.map((type) => type.toUtf8()), - serviceEndpoint: urls.map((url) => url.toUtf8()), - } -} - -export type AuthorizeCallInput = { - did: DidDocumentV2.DidUri - txCounter: AnyNumber - call: Extrinsic - submitter: KiltAddress - blockNumber?: AnyNumber -} - -interface GetStoreTxInput { - authentication: [NewVerificationMethod] - assertionMethod?: [NewVerificationMethod] - capabilityDelegation?: [NewVerificationMethod] - keyAgreement?: NewVerificationMethod[] - - service?: NewServiceEndpoint[] -} - -export type GetStoreTxSignCallback = ( - signData: Omit -) => Promise - -export async function getStoreTx( - input: GetStoreTxInput, - submitter: KiltAddress, - sign: GetStoreTxSignCallback -): Promise { - const api = ConfigService.get('api') - - const { - authentication, - assertionMethod, - capabilityDelegation, - keyAgreement = [], - service = [], - } = input - - if (!('authentication' in input) || typeof authentication[0] !== 'object') { - throw new SDKErrors.DidError( - `The provided DID does not have an authentication key to sign the creation operation` - ) - } - - // For now, it only takes the first attestation key, if present. - if (assertionMethod && assertionMethod.length > 1) { - throw new SDKErrors.DidError( - `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` - ) - } - - // For now, it only takes the first delegation key, if present. - if (capabilityDelegation && capabilityDelegation.length > 1) { - throw new SDKErrors.DidError( - `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` - ) - } - - const maxKeyAgreementKeys = api.consts.did.maxNewKeyAgreementKeys.toNumber() - if (keyAgreement.length > maxKeyAgreementKeys) { - throw new SDKErrors.DidError( - `The number of key agreement keys in the creation operation is greater than the maximum allowed, which is ${maxKeyAgreementKeys}` - ) - } - - const maxNumberOfServicesPerDid = - api.consts.did.maxNumberOfServicesPerDid.toNumber() - if (service.length > maxNumberOfServicesPerDid) { - throw new SDKErrors.DidError( - `Cannot store more than ${maxNumberOfServicesPerDid} service endpoints per DID` - ) - } - - const [authenticationKey] = authentication - const did = getAddressByVerificationMethod(authenticationKey) - - const newAttestationKey = - assertionMethod && - assertionMethod.length > 0 && - getAddressByVerificationMethod(assertionMethod[0]) - - const newDelegationKey = - capabilityDelegation && - capabilityDelegation.length > 0 && - getAddressByVerificationMethod(capabilityDelegation[0]) - - const newKeyAgreementKeys = keyAgreement.map(getAddressByVerificationMethod) - const newServiceDetails = service.map(serviceToChain) - - const apiInput = { - did, - submitter, - newAttestationKey, - newDelegationKey, - newKeyAgreementKeys, - newServiceDetails, - } - - const encoded = api.registry - .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) - .toU8a() - - const { verificationMethod, signature } = await sign({ - data: encoded, - verificationMethodRelationship: 'authentication', - }) - const { keyType } = decodeMulticodecVerificationMethod(verificationMethod) - const encodedSignature = { - [keyType]: signature, - } as EncodedSignature - return api.tx.did.create(encoded, encodedSignature) -} diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index a2e7410a5b..3b45131789 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -5,16 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { BN, DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' - -export type SignatureVerificationMethodRelationship = - | 'authentication' - | 'capabilityDelegation' - | 'assertionMethod' -export type EncryptionMethodRelationship = 'keyAgreementKey' - -export type VerificationMethodRelationship = - SignatureVerificationMethodRelationship & EncryptionMethodRelationship +import type { DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' /** * Possible types for a DID verification key. @@ -46,65 +37,6 @@ export type EncryptionKeyType = typeof encryptionKeyTypesC[number] export type DidKeyType = VerificationKeyType | EncryptionKeyType -// /** -// * The SDK-specific base details of a DID key. -// */ -// export type BaseDidKey = { -// /** -// * Relative key URI: `#` sign followed by fragment part of URI. -// */ -// id: DidDocumentV2.UriFragment -// /** -// * The public key material. -// */ -// publicKey: Uint8Array -// /** -// * The inclusion block of the key, if stored on chain. -// */ -// includedAt?: BN -// /** -// * The type of the key. -// */ -// type: string -// } - -// /** -// * Type of a new key material to add under a DID. -// */ -// export type BaseNewDidKey = { -// publicKey: Uint8Array -// type: string -// } - -// /** -// * Type of a new verification key to add under a DID. -// */ -// export type NewDidVerificationKey = BaseNewDidKey & { -// type: VerificationKeyType -// } -// /** -// * A new public key specified when creating a new light DID. -// */ -// export type NewLightDidVerificationKey = NewDidVerificationKey & { -// type: LightDidSupportedVerificationKeyType -// } -// /** -// * Type of a new encryption key to add under a DID. -// */ -// export type NewDidEncryptionKey = BaseNewDidKey & { type: EncryptionKeyType } -// /** -// * The SDK-specific details of a DID verification key. -// */ -// export type DidVerificationKey = BaseDidKey & { type: VerificationKeyType } -// /** -// * The SDK-specific details of a DID encryption key. -// */ -// export type DidEncryptionKey = BaseDidKey & { type: EncryptionKeyType } -// /** -// * The SDK-specific details of a DID key. -// */ -// export type DidKey = DidVerificationKey | DidEncryptionKey - export type NewVerificationMethod = Omit< DidDocumentV2.VerificationMethod, 'controller' @@ -112,6 +44,4 @@ export type NewVerificationMethod = Omit< id: DidDocumentV2.UriFragment } -export type NewServiceEndpoint = DidDocumentV2.Service & { - id: DidDocumentV2.UriFragment -} +export type NewServiceEndpoint = DidDocumentV2.Service diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts index 599e8ad36a..4b6502ebcf 100644 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts @@ -8,17 +8,32 @@ import { BN } from '@polkadot/util' import { ConfigService } from '@kiltprotocol/config' +import { SDKErrors } from '@kiltprotocol/utils' import type { Extrinsic } from '@polkadot/types/interfaces' +import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' -import type { DidDocumentV2 } from '@kiltprotocol/types' +import type { + CryptoCallbacksV2, + DidDocumentV2, + KiltAddress, + SubmittableExtrinsic, +} from '@kiltprotocol/types' -import type { VerificationMethodRelationship } from './DidDetailsV2.js' +import { parse } from '../Did2.utils.js' +import { + documentFromChain, + generateDidAuthenticatedTx, + toChain, +} from '../Did2.chain.js' // Must be in sync with what's implemented in impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for Call // in https://github.com/KILTprotocol/mashnet-node/blob/develop/runtimes/spiritnet/src/lib.rs // TODO: Should have an RPC or something similar to avoid inconsistencies in the future. -const methodMapping: Record = { +const methodMapping: Record< + string, + DidDocumentV2.SignatureVerificationMethodRelationship | undefined +> = { attestation: 'assertionMethod', ctype: 'assertionMethod', delegation: 'capabilityDelegation', @@ -31,9 +46,9 @@ const methodMapping: Record web3Names: 'authentication', } -function getKeyRelationshipForMethod( +function getVerificationMethodRelationshipForRuntimeCall( call: Extrinsic['method'] -): VerificationMethodRelationship | undefined { +): DidDocumentV2.SignatureVerificationMethodRelationship | undefined { const { section, method } = call // get the VerificationKeyRelationship of a batched call @@ -44,7 +59,7 @@ function getKeyRelationshipForMethod( ) { // map all calls to their VerificationKeyRelationship and deduplicate the items return (call.args[0] as unknown as Array) - .map(getKeyRelationshipForMethod) + .map(getVerificationMethodRelationshipForRuntimeCall) .reduce((prev, value) => (prev === value ? prev : undefined)) } @@ -62,10 +77,10 @@ function getKeyRelationshipForMethod( * @param extrinsic The unsigned extrinsic to inspect. * @returns The key relationship. */ -export function getKeyRelationshipForTx( +export function getVerificationMethodRelationshipForTx( extrinsic: Extrinsic -): VerificationMethodRelationship | undefined { - return getKeyRelationshipForMethod(extrinsic.method) +): DidDocumentV2.SignatureVerificationMethodRelationship | undefined { + return getVerificationMethodRelationshipForRuntimeCall(extrinsic.method) } // Max nonce value is (2^64) - 1 @@ -107,9 +122,9 @@ async function getNextNonce(did: DidDocumentV2.DidUri): Promise { * @returns The DID-signed submittable extrinsic. */ export async function authorizeTx( - did: DidUri, + did: DidDocumentV2.DidUri, extrinsic: Extrinsic, - sign: SignExtrinsicCallback, + sign: CryptoCallbacksV2.SignExtrinsicCallback, submitterAccount: KiltAddress, { txCounter, @@ -123,14 +138,15 @@ export async function authorizeTx( ) } - const keyRelationship = getKeyRelationshipForTx(extrinsic) - if (keyRelationship === undefined) { + const verificationMethodRelationship = + getVerificationMethodRelationshipForTx(extrinsic) + if (verificationMethodRelationship === undefined) { throw new SDKErrors.SDKError('No key relationship found for extrinsic') } return generateDidAuthenticatedTx({ did, - keyRelationship, + verificationMethodRelationship, sign, call: extrinsic, txCounter: txCounter || (await getNextNonce(did)), @@ -140,38 +156,41 @@ export async function authorizeTx( type GroupedExtrinsics = Array<{ extrinsics: Extrinsic[] - keyRelationship: VerificationMethodRelationship + verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship }> function groupExtrinsicsByKeyRelationship( extrinsics: Extrinsic[] ): GroupedExtrinsics { const [first, ...rest] = extrinsics.map((extrinsic) => { - const keyRelationship = getKeyRelationshipForTx(extrinsic) - if (!keyRelationship) { + const verificationMethodRelationship = + getVerificationMethodRelationshipForTx(extrinsic) + if (!verificationMethodRelationship) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) } - return { extrinsic, keyRelationship } + return { extrinsic, verificationMethodRelationship } }) const groups: GroupedExtrinsics = [ { extrinsics: [first.extrinsic], - keyRelationship: first.keyRelationship, + verificationMethodRelationship: first.verificationMethodRelationship, }, ] - rest.forEach(({ extrinsic, keyRelationship }) => { + rest.forEach(({ extrinsic, verificationMethodRelationship }) => { const currentGroup = groups[groups.length - 1] - const useCurrentGroup = keyRelationship === currentGroup.keyRelationship + const useCurrentGroup = + verificationMethodRelationship === + currentGroup.verificationMethodRelationship if (useCurrentGroup) { currentGroup.extrinsics.push(extrinsic) } else { groups.push({ extrinsics: [extrinsic], - keyRelationship, + verificationMethodRelationship, }) } }) @@ -200,10 +219,10 @@ export async function authorizeBatch({ submitter, }: { batchFunction: SubmittableExtrinsicFunction<'promise'> - did: DidUri + did: DidDocumentV2.DidUri extrinsics: Extrinsic[] nonce?: BN - sign: SignExtrinsicCallback + sign: CryptoCallbacksV2.SignExtrinsicCallback submitter: KiltAddress }): Promise { if (extrinsics.length === 0) { @@ -232,11 +251,11 @@ export async function authorizeBatch({ const call = list.length === 1 ? list[0] : batchFunction(list) const txCounter = increaseNonce(firstNonce, batchIndex) - const { keyRelationship } = group + const { verificationMethodRelationship } = group return generateDidAuthenticatedTx({ did, - keyRelationship, + verificationMethodRelationship, sign, call, txCounter, @@ -246,4 +265,4 @@ export async function authorizeBatch({ const batches = await Promise.all(promises) return batches.length === 1 ? batches[0] : batchFunction(batches) -} \ No newline at end of file +} diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts new file mode 100644 index 0000000000..a7f6820cae --- /dev/null +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -0,0 +1,288 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { DidDocumentV2, encryptionKeyTypes } from '@kiltprotocol/types' +import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import { + base58Decode, + base58Encode, + decodeAddress, +} from '@polkadot/util-crypto' +import { + decodeMultikeyVerificationMethod, + encodeVerificationMethodToMultiKey, + getAddressByVerificationMethod, + parse, +} from '../Did2.utils.js' +import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' +import type { + NewVerificationMethod, + NewServiceEndpoint, + DidKeyType, +} from './DidDetailsV2.js' + +/** + * Currently, a light DID does not support the use of an ECDSA key as its authentication key. + */ +export type LightDidSupportedVerificationKeyType = Extract< + DidKeyType, + 'ed25519' | 'sr25519' +> +type LightDidEncoding = '00' | '01' + +const authenticationKeyId = '#authentication' +const encryptionKeyId = '#encryption' + +const verificationKeyTypeToLightDidEncoding: Record< + LightDidSupportedVerificationKeyType, + LightDidEncoding +> = { + sr25519: '00', + ed25519: '01', +} + +const lightDidEncodingToVerificationKeyType: Record< + LightDidEncoding, + LightDidSupportedVerificationKeyType +> = { + '00': 'sr25519', + '01': 'ed25519', +} + +export type CreateDocumentInput = { + /** + * The DID authentication key. This is mandatory and will be used as the first authentication key + * of the full DID upon migration. + */ + authentication: [NewVerificationMethod] + /** + * The optional DID encryption key. If present, it will be used as the first key agreement key + * of the full DID upon migration. + */ + keyAgreement?: [NewVerificationMethod] + /** + * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. + * The service ID must not contain the DID prefix when used to create a new DID. + */ + service?: NewServiceEndpoint[] +} + +function validateCreateDocumentInput({ + authentication, + keyAgreement, + service: services, +}: CreateDocumentInput): void { + // Check authentication key type + const { keyType: authenticationKeyType } = decodeMultikeyVerificationMethod( + authentication[0] + ) + const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[ + authenticationKeyType + ] as LightDidEncoding + + if (!authenticationKeyTypeEncoding) { + throw new SDKErrors.UnsupportedKeyError(authentication[0].type) + } + + if (keyAgreement?.[0] !== undefined) { + const { keyType: keyAgreementKeyType } = decodeMultikeyVerificationMethod( + keyAgreement[0] + ) + if (!encryptionKeyTypes.includes(keyAgreementKeyType)) { + throw new SDKErrors.DidError( + `Encryption key type "${keyAgreementKeyType}" is not supported` + ) + } + } + + // Checks that for all service IDs have regular strings as their ID and not a full DID. + // Plus, we forbid a service ID to be `authentication` or `encryption` as that would create confusion + // when upgrading to a full DID. + services?.forEach((service) => { + // A service ID cannot have a reserved ID that is used for key IDs. + if (service.id === '#authentication' || service.id === '#encryption') { + throw new SDKErrors.DidError( + `Cannot specify a service ID with the name "${service.id}" as it is a reserved keyword` + ) + } + validateNewService(service) + }) +} + +const KEY_AGREEMENT_MAP_KEY = 'e' +const SERVICES_MAP_KEY = 's' + +interface SerializableStructure { + [KEY_AGREEMENT_MAP_KEY]?: NewVerificationMethod + [SERVICES_MAP_KEY]?: Array< + Partial> & { + id: string + } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their service endpoints. + > +} + +function serializeAdditionalLightDidDetails({ + keyAgreement, + service, +}: Pick): string | undefined { + const objectToSerialize: SerializableStructure = {} + if (keyAgreement) { + const key = keyAgreement[0] + objectToSerialize[KEY_AGREEMENT_MAP_KEY] = key + } + if (service && service.length > 0) { + objectToSerialize[SERVICES_MAP_KEY] = service.map(({ id, ...rest }) => ({ + id: fragmentIdToChain(id), + ...rest, + })) + } + + if (Object.keys(objectToSerialize).length === 0) { + return undefined + } + + const serializationVersion = 0x0 + const serialized = cbor.encode(objectToSerialize) + return base58Encode([serializationVersion, ...serialized], true) +} + +function deserializeAdditionalLightDidDetails( + rawInput: string, + version = 1 +): Pick { + if (version !== 1) { + throw new SDKErrors.DidError('Serialization version not supported') + } + + const decoded = base58Decode(rawInput, true) + const serializationVersion = decoded[0] + const serialized = decoded.slice(1) + + if (serializationVersion !== 0x0) { + throw new SDKErrors.DidError('Serialization algorithm not supported') + } + const deserialized: SerializableStructure = cbor.decode(serialized) + + const keyAgreement = deserialized[KEY_AGREEMENT_MAP_KEY] + return { + keyAgreement: keyAgreement && [keyAgreement], + service: deserialized[SERVICES_MAP_KEY]?.map( + ({ id, type, serviceEndpoint, types, urls }) => ({ + id: `#${id}`, + // types for retro-compatibility + type: (type ?? types) as string[], + // urls for retro-compatibility + serviceEndpoint: (serviceEndpoint ?? urls) as string[], + }) + ), + } +} + +export function createLightDidDocument({ + authentication, + keyAgreement = undefined, + service, +}: CreateDocumentInput): DidDocumentV2.DidDocument { + validateCreateDocumentInput({ + authentication, + keyAgreement, + service, + }) + const encodedDetails = serializeAdditionalLightDidDetails({ + keyAgreement, + service, + }) + // Validity is checked in validateCreateDocumentInput + const { keyType: authenticationKeyType, publicKey: authenticationPublicKey } = + decodeMultikeyVerificationMethod(authentication[0]) + const authenticationKeyTypeEncoding = + verificationKeyTypeToLightDidEncoding[authenticationKeyType] + const address = getAddressByVerificationMethod(authentication[0]) + + const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' + const uri = + `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as DidDocumentV2.DidUri + + const did: DidDocumentV2.DidDocument = { + id: uri, + authentication: [authenticationKeyId], + verificationMethod: [ + encodeVerificationMethodToMultiKey(uri, authenticationKeyId, { + keyType: authenticationKeyType, + publicKey: authenticationPublicKey, + }), + ], + service, + } + + if (keyAgreement !== undefined) { + const { keyType: keyAgreementKeyType, publicKey: keyAgreementPublicKey } = + decodeMultikeyVerificationMethod(keyAgreement[0]) + did.keyAgreement = [encryptionKeyId] + did.verificationMethod.push( + encodeVerificationMethodToMultiKey(uri, encryptionKeyId, { + keyType: keyAgreementKeyType, + publicKey: keyAgreementPublicKey, + }) + ) + } + + return did +} + +export function parseDocumentFromLightDid( + uri: DidDocumentV2.DidUri, + failIfFragmentPresent = true +): DidDocumentV2.DidDocument { + const { + address, + version, + encodedDetails, + fragment, + type, + authKeyTypeEncoding, + } = parse(uri) + + if (type !== 'light') { + throw new SDKErrors.DidError( + `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` + ) + } + if (fragment && failIfFragmentPresent) { + throw new SDKErrors.DidError( + `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` + ) + } + const keyType = + authKeyTypeEncoding && + lightDidEncodingToVerificationKeyType[authKeyTypeEncoding] + + if (keyType === undefined) { + throw new SDKErrors.DidError( + `Authentication key encoding "${authKeyTypeEncoding}" does not match any supported key type` + ) + } + const publicKey = decodeAddress(address, false, ss58Format) + const authentication: [NewVerificationMethod] = [ + encodeVerificationMethodToMultiKey(uri, authenticationKeyId, { + keyType, + publicKey, + }), + ] + if (!encodedDetails) { + return createLightDidDocument({ authentication }) + } + const { keyAgreement, service } = deserializeAdditionalLightDidDetails( + encodedDetails, + version + ) + return createLightDidDocument({ + authentication, + keyAgreement, + service, + }) +} diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts new file mode 100644 index 0000000000..9f8b08853f --- /dev/null +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -0,0 +1,6 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 9d599e6818..3bb8225b11 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -24,7 +24,7 @@ export interface SignRequestData { /** * The did key relationship to be used. */ - verificationMethodRelationship: VerificationKeyRelationship + keyRelationship: VerificationKeyRelationship /** * The DID to be used for signing. diff --git a/packages/types/src/CryptoCallbacksV2.ts b/packages/types/src/CryptoCallbacksV2.ts new file mode 100644 index 0000000000..c56f74e442 --- /dev/null +++ b/packages/types/src/CryptoCallbacksV2.ts @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { DidUri } from './DidDocumentV2' +import { DidDocumentV2 } from './index.js' + +/** + * Base interface for all signing requests. + */ +export interface SignRequestData { + /** + * Data to be signed. + */ + data: Uint8Array + + /** + * The did key relationship to be used. + */ + verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship + + /** + * The DID to be used for signing. + */ + did: DidUri +} + +/** + * Base interface for responses to signing requests. + */ +export interface SignResponseData { + /** + * Result of the signing. + */ + signature: Uint8Array + /** + * The did key uri used for signing. + */ + verificationMethod: DidDocumentV2.VerificationMethod +} + +/** + * A callback function to sign data. + */ +export type SignCallback = ( + signData: SignRequestData +) => Promise + +/** + * A callback function to sign extrinsics. + */ +export type SignExtrinsicCallback = ( + signData: SignRequestData +) => Promise + +/** + * Base interface for encryption requests. + */ +export interface EncryptRequestData { + /** + * Data to be encrypted. + */ + data: Uint8Array + /** + * The other party's public key to be used for x25519 Diffie-Hellman key agreement. + */ + peerPublicKey: Uint8Array + /** + * The DID to be used for encryption. + */ + did: DidUri +} + +/** + * Base interface for responses to encryption requests. + */ +export interface EncryptResponseData { + /** + * Result of the encryption. + */ + data: Uint8Array + /** + * A random nonce generated in the encryption process. + */ + nonce: Uint8Array + /** + * The did key uri used for the encryption. + */ + verificationMethod: DidDocumentV2.VerificationMethod +} + +/** + * Uses stored key material to encrypt a message encoded as u8a. + * + * @param requestData The data to be encrypted, the peers public key and the sender's DID. + * @returns [[EncryptResponseData]] which additionally to the data contains a `nonce` randomly generated in the encryption process (required for decryption). + */ +export interface EncryptCallback { + (requestData: EncryptRequestData): Promise +} + +export interface DecryptRequestData { + /** + * Data to be encrypted. + */ + data: Uint8Array + /** + * The other party's public key to be used for x25519 Diffie-Hellman key agreement. + */ + peerPublicKey: Uint8Array + /** + * The random nonce generated during encryption as u8a. + */ + nonce: Uint8Array + /** + * The did key uri, which should be used for decryption. + */ + verificationMethod: DidDocumentV2.VerificationMethod +} + +export interface DecryptResponseData { + /** + * Result of the decryption. + */ + data: Uint8Array +} + +/** + * Uses stored key material to decrypt a message encoded as u8a. + * + * @param requestData [[DecryptRequestData]] containing both our and their public keys, the nonce used for encryption, the data to be decrypted. + * @param requestData.nonce The random nonce generated during encryption as u8a. + * @returns A Promise resolving to [[DecryptResponseData]] containing the decrypted message or rejecting if a key is unknown or does not match. + */ +export interface DecryptCallback { + (requestData: DecryptRequestData): Promise +} diff --git a/packages/types/src/DidDocumentV2.ts b/packages/types/src/DidDocumentV2.ts index 132353f48e..58e4e9e21f 100644 --- a/packages/types/src/DidDocumentV2.ts +++ b/packages/types/src/DidDocumentV2.ts @@ -27,6 +27,16 @@ export type UriFragment = `#${string}` */ export type DidResourceUri = `${DidUri}${UriFragment}` +export type SignatureVerificationMethodRelationship = + | 'authentication' + | 'capabilityDelegation' + | 'assertionMethod' +export type EncryptionMethodRelationship = 'keyAgreementKey' + +export type VerificationMethodRelationship = + | SignatureVerificationMethodRelationship + | EncryptionMethodRelationship + type Base58BtcMultibaseString = `z${string}` /* @@ -36,7 +46,7 @@ export type VerificationMethod = { /* * The value of the id property for a verification method MUST be a string that conforms to the rules in Section 3.2 DID URL Syntax. */ - id: DidResourceUri + id: UriFragment /* * The value of the type property MUST be a string that references exactly one verification method type. In order to maximize global interoperability, the verification method type SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ @@ -58,7 +68,7 @@ export type Service = { /* * The value of the id property MUST be a URI conforming to [RFC3986]. A conforming producer MUST NOT produce multiple service entries with the same id. A conforming consumer MUST produce an error if it detects multiple service entries with the same id. */ - id: DidResourceUri + id: UriFragment /* * The value of the type property MUST be a string or a set of strings. In order to maximize interoperability, the service type and its associated properties SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index 880c25a701..6bac2ccea3 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -179,4 +179,4 @@ export interface DereferenceDidUrl { ) => Promise } -export interface DidResolver extends ResolveDid, DereferenceDidUrl {} +export interface DidResolver extends ResolveDid, DereferenceDidUrl {} \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 94c635c4bc..50445a8be2 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -30,3 +30,4 @@ export * from './Imported.js' export * as DidDocumentV2 from './DidDocumentV2.js' export * as DidResolverV2 from './DidResolverV2.js' +export * as CryptoCallbacksV2 from './CryptoCallbacksV2.js' diff --git a/yarn.lock b/yarn.lock index 3e9081f91a..e2b9bc14c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2022,6 +2022,7 @@ __metadata: "@polkadot/types-codec": ^10.4.0 "@polkadot/util": ^12.0.0 "@polkadot/util-crypto": ^12.0.0 + multibase: ^4.0.6 rimraf: ^3.0.2 typescript: ^4.8.3 languageName: unknown @@ -2113,6 +2114,13 @@ __metadata: languageName: unknown linkType: soft +"@multiformats/base-x@npm:^4.0.1": + version: 4.0.1 + resolution: "@multiformats/base-x@npm:4.0.1" + checksum: ecbf84bdd7613fd795e4a41f20f3e8cc7df8bbee84690b7feed383d45a638ed228a80ff6f5c930373cbf24539f64857b66023ee3c1e914f6bac9995c76414a87 + languageName: node + linkType: hard + "@noble/curves@npm:1.0.0, @noble/curves@npm:^1.0.0": version: 1.0.0 resolution: "@noble/curves@npm:1.0.0" @@ -7389,6 +7397,15 @@ fsevents@^2.3.2: languageName: node linkType: hard +"multibase@npm:^4.0.6": + version: 4.0.6 + resolution: "multibase@npm:4.0.6" + dependencies: + "@multiformats/base-x": ^4.0.1 + checksum: 891ce47f509c6070d2306e7e00aef3ef41fbb50a848a1e1bec5e75ca63c5032015a436cf09e9e3939b5b2ca81e74804151eb410a388f10e9aabf7a2f5a35d272 + languageName: node + linkType: hard + "multiformats@npm:^9.4.2, multiformats@npm:^9.6.5": version: 9.9.0 resolution: "multiformats@npm:9.9.0" From 1332a7c7f52992febc7b30ec2c63a1bb020783f4 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 2 Oct 2023 13:09:06 +0100 Subject: [PATCH 08/78] did.resolve implemented --- packages/did/src/DidResolver/DidResolverV2.ts | 151 ++++++++++++++++++ packages/types/src/DidResolverV2.ts | 24 +-- 2 files changed, 158 insertions(+), 17 deletions(-) diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index 9f8b08853f..7602e5d64d 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -4,3 +4,154 @@ * This source code is licensed under the BSD 4-Clause "Original" license * found in the LICENSE file in the root directory of this source tree. */ + +import { ConfigService } from '@kiltprotocol/config' +import { DidDocumentV2, DidResolverV2 } from '@kiltprotocol/types' +import { linkedInfoFromChain } from '../Did.rpc' +import { toChain } from '../Did2.chain.js' +import { + encodeVerificationMethodToMultiKey, + getFullDidUri, + parse, + validateUri, +} from '../Did2.utils.js' +import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' + +type InternalResolutionResult = { + document?: DidDocumentV2.DidDocument + documentMetadata: DidResolverV2.DidDocumentMetadata +} + +async function resolveInternal( + did: DidDocumentV2.DidUri +): Promise { + const { type } = parse(did) + const api = ConfigService.get('api') + + const { document, web3Name } = await api.call.did + .query(toChain(did)) + .then(linkedInfoFromChain) + .catch(() => ({ document: undefined, web3Name: undefined })) + + if (type === 'full' && document !== undefined) { + const newDidDocument: DidDocumentV2.DidDocument = { + id: did, + authentication: [document.authentication[0].id], + verificationMethod: [ + encodeVerificationMethodToMultiKey(did, document.authentication[0].id, { + publicKey: document.authentication[0].publicKey, + keyType: document.authentication[0].type, + }), + ], + service: document.service, + } + if (document.assertionMethod !== undefined) { + newDidDocument.assertionMethod = document.assertionMethod.map((k) => k.id) + newDidDocument.verificationMethod.push( + ...document.assertionMethod.map((k) => + encodeVerificationMethodToMultiKey(did, k.id, { + keyType: k.type, + publicKey: k.publicKey, + }) + ) + ) + } + if (document.keyAgreement !== undefined) { + newDidDocument.assertionMethod = document.keyAgreement.map((k) => k.id) + newDidDocument.verificationMethod.push( + ...document.keyAgreement.map((k) => + encodeVerificationMethodToMultiKey(did, k.id, { + keyType: k.type, + publicKey: k.publicKey, + }) + ) + ) + } + if (document.capabilityDelegation !== undefined) { + newDidDocument.assertionMethod = document.capabilityDelegation.map( + (k) => k.id + ) + newDidDocument.verificationMethod.push( + ...document.capabilityDelegation.map((k) => + encodeVerificationMethodToMultiKey(did, k.id, { + keyType: k.type, + publicKey: k.publicKey, + }) + ) + ) + } + if (web3Name !== undefined) { + newDidDocument.alsoKnownAs = [web3Name] + } + + return { + document: newDidDocument, + documentMetadata: {}, + } + } + + const isFullDidDeleted = (await api.query.did.didBlacklist(toChain(did))) + .isSome + if (isFullDidDeleted) { + return { + // No canonicalId and no details are returned as we consider this DID deactivated/deleted. + documentMetadata: { + deactivated: true, + }, + } + } + + if (type === 'full') { + return null + } + + const lightDocument = parseDocumentFromLightDid(did, false) + // If a full DID with same subject is present, return the resolution metadata accordingly. + if (document !== undefined) { + return { + documentMetadata: { + canonicalId: getFullDidUri(did), + }, + } + } + + // If no full DID details nor deletion info is found, the light DID is un-migrated. + // Metadata will simply contain `deactivated: false`. + return { + document: lightDocument, + documentMetadata: {}, + } +} + +async function resolve( + did: DidDocumentV2.DidUri, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + resolutionOptions: DidResolverV2.DidResolutionOptions +): Promise { + const result: DidResolverV2.ResolutionResult = { + didDocumentMetadata: {}, + didResolutionMetadata: {}, + } + + try { + validateUri(did, 'Did') + } catch (error) { + result.didResolutionMetadata.error = 'invalidDid' + return result + } + + const resolutionResult = await resolveInternal(did) + if (!resolutionResult) { + result.didResolutionMetadata.error = 'notFound' + return result + } + + const { documentMetadata, document } = resolutionResult + + result.didDocument = document + result.didDocumentMetadata = documentMetadata + + return result +} + +// TODO: Implement DID dereferencing \ No newline at end of file diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index 6bac2ccea3..a003739498 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -13,17 +13,7 @@ import type { Service, } from './DidDocumentV2' -type DidContentType = 'application/did+ld+json' | 'application/did+json' - -type ResolutionOptions = { - /* - * The Media Type of the caller's preferred representation of the DID document. - * The Media Type MUST be expressed as an ASCII string. - * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. - * This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. - */ - accept?: DidContentType -} +export type DidResolutionOptions = Record /* * This specification defines the following common error values: @@ -46,7 +36,7 @@ type DidResolutionMetadata = { error?: DidResolutionMetadataError } -type DidDocumentMetadata = { +export type DidDocumentMetadata = { /* * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. * If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. @@ -61,7 +51,7 @@ type DidDocumentMetadata = { canonicalId?: DidUri } -type ResolutionResult = { +export type ResolutionResult = { /* * A metadata structure consisting of values relating to the results of the DID resolution process which typically changes between invocations of the resolve and resolveRepresentation functions, as it represents data about the resolution process itself. * This structure is REQUIRED, and in the case of an error in the resolution process, this MUST NOT be empty. @@ -92,7 +82,7 @@ type DereferenceOptions = { * The Media Type MUST be expressed as an ASCII string. * The DID URL dereferencing implementation SHOULD use this value to determine the contentType of the representation contained in the returned value if such a representation is supported and available. */ - accept?: DidContentType + accept?: string } /* @@ -107,7 +97,7 @@ type DereferencingMetadata = { * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. * The Media Type value MUST be expressed as an ASCII string. */ - contentType?: DidContentType + contentType?: string /* * The error code from the dereferencing process. * This property is REQUIRED when there is an error in the dereferencing process. @@ -158,7 +148,7 @@ export interface ResolveDid { * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. * This input is REQUIRED, but the structure MAY be empty. */ - resolutionOptions: ResolutionOptions + resolutionOptions: DidResolutionOptions ) => Promise } @@ -179,4 +169,4 @@ export interface DereferenceDidUrl { ) => Promise } -export interface DidResolver extends ResolveDid, DereferenceDidUrl {} \ No newline at end of file +export interface DidResolver extends ResolveDid, DereferenceDidUrl {} From d135a1ce48551e998d28945ab1c7cc863e33c46f Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 2 Oct 2023 15:23:56 +0100 Subject: [PATCH 09/78] DID resolver ok --- packages/did/src/DidResolver/DidResolverV2.ts | 78 ++++++++++++++++++- packages/types/src/DidResolverV2.ts | 17 ++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index 7602e5d64d..88f2f693cc 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -123,10 +123,10 @@ async function resolveInternal( } } -async function resolve( +export async function resolve( did: DidDocumentV2.DidUri, // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolutionOptions: DidResolverV2.DidResolutionOptions + resolutionOptions: DidResolverV2.DidResolutionOptions = {} ): Promise { const result: DidResolverV2.ResolutionResult = { didDocumentMetadata: {}, @@ -154,4 +154,76 @@ async function resolve( return result } -// TODO: Implement DID dereferencing \ No newline at end of file +type InternalDereferenceResult = { + contentStream?: DidResolverV2.DereferenceContentStream + contentMetadata: DidResolverV2.DereferenceContentMetadata +} + +async function dereferenceInternal( + didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + dereferenceOptions: DidResolverV2.DereferenceOptions +): Promise { + const { did, fragment } = parse(didUrl) + + const { didDocument, didDocumentMetadata } = await resolve(did) + + if (fragment === undefined) { + return { + contentStream: didDocument, + contentMetadata: didDocumentMetadata, + } + } + const dereferencedResource = (() => { + const verificationMethod = didDocument?.verificationMethod.find( + (m) => m.id === fragment + ) + if (verificationMethod !== undefined) { + return verificationMethod + } + + const service = didDocument?.service?.find((s) => s.id === fragment) + return service + })() + + return { + contentStream: dereferencedResource, + contentMetadata: {}, + } +} + +export async function dereference( + didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + dereferenceOptions: DidResolverV2.DereferenceOptions = {} +): Promise { + const result: DidResolverV2.DereferenceResult = { + contentMetadata: {}, + dereferencingMetadata: {}, + } + + try { + validateUri(didUrl) + } catch (error) { + result.dereferencingMetadata.error = 'invalidDidUrl' + return result + } + + const resolutionResult = await dereferenceInternal(didUrl, dereferenceOptions) + if (!resolutionResult) { + result.dereferencingMetadata.error = 'notFound' + return result + } + + const { contentMetadata, contentStream } = resolutionResult + + result.contentMetadata = contentMetadata + result.contentStream = contentStream + + return result +} + +export const resolver: DidResolverV2.DidResolver = { + dereference, + resolve, +} diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index a003739498..895d2d9542 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -76,7 +76,7 @@ export type ResolutionResult = { didDocumentMetadata: DidDocumentMetadata } -type DereferenceOptions = { +export type DereferenceOptions = { /* * The Media Type that the caller prefers for contentStream. * The Media Type MUST be expressed as an ASCII string. @@ -92,7 +92,7 @@ type DereferenceOptions = { */ type DidDereferenceMetadataError = 'invalidDidUrl' | 'notFound' -type DereferencingMetadata = { +export type DereferencingMetadata = { /* * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. * The Media Type value MUST be expressed as an ASCII string. @@ -107,11 +107,16 @@ type DereferencingMetadata = { error?: DidDereferenceMetadataError } -type DereferenceContentStream = DidDocument | VerificationMethod | Service +export type DereferenceContentStream = + | DidDocument + | VerificationMethod + | Service -type DereferenceContentMetadata = DidDocumentMetadata | Record +export type DereferenceContentMetadata = + | DidDocumentMetadata + | Record -type DereferenceResult = { +export type DereferenceResult = { /* * A metadata structure consisting of values relating to the results of the DID URL dereferencing process. * This structure is REQUIRED, and in the case of an error in the dereferencing process, this MUST NOT be empty. @@ -159,7 +164,7 @@ export interface DereferenceDidUrl { * This is the DID URL to dereference. * To dereference a DID fragment, the complete DID URL including the DID fragment MUST be used. This input is REQUIRED. */ - didUrl: DidResourceUri, + didUrl: DidUri | DidResourceUri, /* * A metadata structure consisting of input options to the dereference function in addition to the didUrl itself. * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. From b43cc8059b113b2b3f4e9019f3ed313b0d9b1b13 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 3 Oct 2023 08:27:42 +0100 Subject: [PATCH 10/78] AccountLinks2.chain.ts --- .../did/src/DidLinks/AccountLinks2.chain.ts | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 packages/did/src/DidLinks/AccountLinks2.chain.ts diff --git a/packages/did/src/DidLinks/AccountLinks2.chain.ts b/packages/did/src/DidLinks/AccountLinks2.chain.ts new file mode 100644 index 0000000000..b0c5402e9c --- /dev/null +++ b/packages/did/src/DidLinks/AccountLinks2.chain.ts @@ -0,0 +1,291 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' +import type { TypeDef } from '@polkadot/types/types' +import type { KeypairType } from '@polkadot/util-crypto/types' +import { + stringToU8a, + U8A_WRAP_ETHEREUM, + u8aConcatStrict, + u8aToHex, + u8aWrapBytes, + BN, +} from '@polkadot/util' +import { ApiPromise } from '@polkadot/api' + +import { SDKErrors } from '@kiltprotocol/utils' +import { ConfigService } from '@kiltprotocol/config' +import type { + HexString, + DidDocumentV2, + KeyringPair, + KiltAddress, +} from '@kiltprotocol/types' + +import { toChain, EncodedSignature } from '../Did2.chain.js' + +/** + * A chain-agnostic address, which can be encoded using any network prefix. + */ +export type SubstrateAddress = KeyringPair['address'] + +export type EthereumAddress = HexString + +export type Address = KiltAddress | SubstrateAddress | EthereumAddress + +type EncodedMultiAddress = + | { AccountId20: Uint8Array } + | { AccountId32: Uint8Array } + +/** + * Detects whether the spec version indicates presence of Ethereum linking enabled pallet. + * + * @param api The api object. + * @returns True if Ethereum linking is supported. + */ +function isEthereumEnabled(api: ApiPromise): boolean { + return api.runtimeVersion.specVersion.gten(11000) +} + +/** + * Prepares encoding a LinkableAccountId. + * + * @param address 20 or 32 byte address as string (hex or ss58 encoded). + * @returns `{ AccountId20 | AccountId32: Uint8Array }`. + */ +function encodeMultiAddress(address: Address): EncodedMultiAddress { + const accountDecoded = decodeAddress(address) + const isEthereumAddress = accountDecoded.length === 20 + return isEthereumAddress + ? { AccountId20: accountDecoded } + : { AccountId32: accountDecoded } +} + +/* ### QUERY ### */ + +/** + * Format a blockchain address to be used as a parameter for the blockchain API functions. + * + * @param account The account to format. + * @returns The blockchain-formatted account. + */ +export function accountToChain(account: Address): Address { + const api = ConfigService.get('api') + if (!isEthereumEnabled(api)) { + // No change for the old blockchain version + return account + } + const encoded: EncodedMultiAddress = encodeMultiAddress(account) + // Force type cast to enable the old blockchain types to accept the future format + return encoded as unknown as Address +} + +/* ### EXTRINSICS ### */ + +type LinkingInfo = [Address, unknown] +type AssociateAccountToChainResult = [ + ( + | { + Polkadot: LinkingInfo + } + | { + Ethereum: LinkingInfo + } + ), + BN +] + +/* ### HELPERS ### */ + +function getUnprefixedSignature( + message: HexString, + signature: Uint8Array, + address: Address +): { signature: Uint8Array; type: KeypairType } { + try { + // try to verify the signature without the prefix first + const unprefixed = signature.subarray(1) + const { crypto, isValid } = signatureVerify(message, unprefixed, address) + if (isValid) { + return { + signature: unprefixed, + type: crypto as KeypairType, + } + } + } catch { + // if it fails, maybe the signature prefix caused that, so we try to verify the whole signature + } + + const { crypto, isValid } = signatureVerify(message, signature, address) + if (isValid) { + return { + signature, + type: crypto as KeypairType, + } + } + + throw new SDKErrors.SignatureUnverifiableError() +} + +async function getLinkingChallengeV1( + did: DidDocumentV2.DidUri, + validUntil: BN +): Promise { + const api = ConfigService.get('api') + + // Gets the current definition of BlockNumber (second tx argument) from the metadata. + const BlockNumber = + api.tx.didLookup.associateAccount.meta.args[1].type.toString() + // This is some magic on the polkadot types internals to get the DidAddress definition from the metadata. + // We get it from the connectedAccounts storage, which is a double map from (DidAddress, Account) -> Null. + const DidAddress = ( + api.registry.lookup.getTypeDef( + // gets the type id of the keys on the connectedAccounts storage (which is a double map). + api.query.didLookup.connectedAccounts.creator.meta.type.asMap.key + ).sub as TypeDef[] + )[0].type // get the type of the first key, which is the DidAddress + + return api + .createType(`(${DidAddress}, ${BlockNumber})`, [toChain(did), validUntil]) + .toU8a() +} + +function getLinkingChallengeV2( + did: DidDocumentV2.DidUri, + validUntil: BN +): Uint8Array { + return stringToU8a( + `Publicly link the signing address to ${did} before block number ${validUntil}` + ) +} + +/** + * Generates the challenge that links a DID to an account. + * The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will + * link the account to the DID. + * + * @param did The URI of the DID that that should be linked to an account. + * @param validUntil Last blocknumber that this challenge is valid for. + * @returns The encoded challenge. + */ +export async function getLinkingChallenge( + did: DidDocumentV2.DidUri, + validUntil: BN +): Promise { + const api = ConfigService.get('api') + if (isEthereumEnabled(api)) { + return getLinkingChallengeV2(did, validUntil) + } + return getLinkingChallengeV1(did, validUntil) +} + +/** + * Generates the arguments for the extrinsic that links an account to a DID. + * + * @param accountAddress Address of the account to be linked. + * @param validUntil Last blocknumber that this challenge is valid for. + * @param signature The signature for the linking challenge. + * @param type The key type used to sign the challenge. + * @returns The arguments for the call that links account and DID. + */ +export async function getLinkingArguments( + accountAddress: Address, + validUntil: BN, + signature: Uint8Array, + type: KeypairType +): Promise { + const api = ConfigService.get('api') + + const proof = { [type]: signature } as EncodedSignature + + if (isEthereumEnabled(api)) { + if (type === 'ethereum') { + return [{ Ethereum: [accountAddress, signature] }, validUntil] + } + return [{ Polkadot: [accountAddress, proof] }, validUntil] + } + + if (type === 'ethereum') + throw new SDKErrors.CodecMismatchError( + 'Ethereum linking is not yet supported by this chain' + ) + // Force type cast to enable the new blockchain types to accept the historic format + return [ + accountAddress, + validUntil, + proof, + ] as unknown as AssociateAccountToChainResult +} + +/** + * Identifies the strategy to wrap raw bytes for signing. + */ +export type WrappingStrategy = 'ethereum' | 'polkadot' + +/** + * Wraps the provided challenge according to the key type. + * + * Ethereum addresses will cause the challenge to be prefixed with + * `\x19Ethereum Signed Message:\n` and the length of the message. + * + * For all other key types the message will be wrapped in `..`. + * + * @param type The key type that will sign the challenge. + * @param challenge The challenge to proof ownership of both account and DID. + * @returns The wrapped challenge. + */ +export function getWrappedChallenge( + type: WrappingStrategy, + challenge: Uint8Array +): Uint8Array { + if (type === 'ethereum') { + const length = stringToU8a(String(challenge.length)) + return u8aConcatStrict([U8A_WRAP_ETHEREUM, length, challenge]) + } + return u8aWrapBytes(challenge) +} + +/** + * Builds the parameters for an extrinsic to link the `account` to the `did` where the fees and deposit are covered by some third account. + * This extrinsic must be authorized using the same full DID. + * Note that in addition to the signing account and DID used here, the submitting account will also be able to dissolve the link via reclaiming its deposit! + * + * @param accountAddress Address of the account to be linked. + * @param did Full DID to be linked. + * @param sign The sign callback that generates the account signature over the encoded (DidAddress, BlockNumber) tuple. + * @param nBlocksValid The link request will be rejected if submitted later than (current block number + nBlocksValid)? + * @returns An array of parameters for `api.tx.didLookup.associateAccount` that must be DID-authorized by the full DID used. + */ +export async function associateAccountToChainArgs( + accountAddress: Address, + did: DidDocumentV2.DidUri, + sign: (encodedLinkingDetails: HexString) => Promise, + nBlocksValid = 10 +): Promise { + const api = ConfigService.get('api') + + const blockNo = await api.query.system.number() + const validTill = blockNo.addn(nBlocksValid) + + const challenge = await getLinkingChallenge(did, validTill) + + // ethereum addresses are 42 characters long since they are 20 bytes hex encoded strings + // (they start with 0x, 2 characters per byte) + const predictedType = accountAddress.length === 42 ? 'ethereum' : 'polkadot' + const wrappedChallenge = u8aToHex( + getWrappedChallenge(predictedType, challenge) + ) + + const { signature, type } = getUnprefixedSignature( + wrappedChallenge, + await sign(wrappedChallenge), + accountAddress + ) + + return getLinkingArguments(accountAddress, validTill, signature, type) +} From ed5d240b3b81d1c3db07bb1ab8ca621bb4661afc Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 3 Oct 2023 09:39:28 +0100 Subject: [PATCH 11/78] Add resolveRepresentation to resolver --- packages/did/src/DidResolver/DidContextsV2.ts | 110 +++++++++++++++++ packages/did/src/DidResolver/DidResolverV2.ts | 58 ++++++++- packages/types/src/DidDocumentV2.ts | 2 + packages/types/src/DidResolverV2.ts | 114 +++++++++++++----- 4 files changed, 249 insertions(+), 35 deletions(-) create mode 100644 packages/did/src/DidResolver/DidContextsV2.ts diff --git a/packages/did/src/DidResolver/DidContextsV2.ts b/packages/did/src/DidResolver/DidContextsV2.ts new file mode 100644 index 0000000000..196e35a2df --- /dev/null +++ b/packages/did/src/DidResolver/DidContextsV2.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +// @ts-expect-error not a TS package +import securityContexts from '@digitalbazaar/security-context' + +const securityContextsMap: Map< + string, + Record +> = securityContexts.contexts + +/** + * IPFS URL identifying a JSON-LD context file describing terms used in DID documents of the KILT method that are not defined in the W3C DID core context. + * Should be the second entry in the ordered set of contexts after [[W3C_DID_CONTEXT_URL]] in the JSON-LD representation of a KILT DID document. + */ +export const KILT_DID_CONTEXT_URL = + 'ipfs://QmU7QkuTCPz7NmD5bD7Z7mQVz2UsSPaEK58B5sYnjnPRNW' +/** + * URL identifying the JSON-LD context file that is part of the W3C DID core specifications describing the terms defined by the core data model. + * Must be the first entry in the ordered set of contexts in a JSON-LD representation of a DID document. + * See https://www.w3.org/TR/did-core/#json-ld. + */ +export const W3C_DID_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' +/** + * URL identifying a JSON-LD context file proposed by the W3C Credentials Community Group defining a number of terms which are used in verification methods on KILT DID documents. + * See https://w3c-ccg.github.io/security-vocab/. + * This document is extended by the context file available under the [[KILT_DID_CONTEXT_URL]]. + */ +export const W3C_SECURITY_CONTEXT_URL = securityContexts.SECURITY_CONTEXT_V2_URL +/** + * An object containing static copies of JSON-LD context files relevant to KILT DID documents, of the form -> context. + * These context definitions are not supposed to change; therefore, a cached version can (and should) be used to avoid unexpected changes in definitions. + */ +export const DID_CONTEXTS = { + [KILT_DID_CONTEXT_URL]: { + '@context': [ + W3C_SECURITY_CONTEXT_URL, + { + '@protected': true, + KiltPublishedCredentialCollectionV1: + 'https://github.com/KILTprotocol/spec-KiltPublishedCredentialCollectionV1', + Sr25519VerificationKey2020: + 'https://github.com/KILTprotocol/spec-kilt-did#sr25519', + }, + ], + }, + [W3C_DID_CONTEXT_URL]: { + '@context': { + '@protected': true, + id: '@id', + type: '@type', + + alsoKnownAs: { + '@id': 'https://www.w3.org/ns/activitystreams#alsoKnownAs', + '@type': '@id', + }, + assertionMethod: { + '@id': 'https://w3id.org/security#assertionMethod', + '@type': '@id', + '@container': '@set', + }, + authentication: { + '@id': 'https://w3id.org/security#authenticationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityDelegation: { + '@id': 'https://w3id.org/security#capabilityDelegationMethod', + '@type': '@id', + '@container': '@set', + }, + capabilityInvocation: { + '@id': 'https://w3id.org/security#capabilityInvocationMethod', + '@type': '@id', + '@container': '@set', + }, + controller: { + '@id': 'https://w3id.org/security#controller', + '@type': '@id', + }, + keyAgreement: { + '@id': 'https://w3id.org/security#keyAgreementMethod', + '@type': '@id', + '@container': '@set', + }, + service: { + '@id': 'https://www.w3.org/ns/did#service', + '@type': '@id', + '@context': { + '@protected': true, + id: '@id', + type: '@type', + serviceEndpoint: { + '@id': 'https://www.w3.org/ns/did#serviceEndpoint', + '@type': '@id', + }, + }, + }, + verificationMethod: { + '@id': 'https://w3id.org/security#verificationMethod', + '@type': '@id', + }, + }, + }, + ...Object.fromEntries(securityContextsMap), +} diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index 88f2f693cc..e055b5f7d7 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -7,6 +7,7 @@ import { ConfigService } from '@kiltprotocol/config' import { DidDocumentV2, DidResolverV2 } from '@kiltprotocol/types' +import { cbor } from '@kiltprotocol/utils' import { linkedInfoFromChain } from '../Did.rpc' import { toChain } from '../Did2.chain.js' import { @@ -16,10 +17,11 @@ import { validateUri, } from '../Did2.utils.js' import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' +import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2' type InternalResolutionResult = { document?: DidDocumentV2.DidDocument - documentMetadata: DidResolverV2.DidDocumentMetadata + documentMetadata: DidResolverV2.ResolutionDocumentMetadata } async function resolveInternal( @@ -126,7 +128,7 @@ async function resolveInternal( export async function resolve( did: DidDocumentV2.DidUri, // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolutionOptions: DidResolverV2.DidResolutionOptions = {} + resolutionOptions: DidResolverV2.ResolutionOptions = {} ): Promise { const result: DidResolverV2.ResolutionResult = { didDocumentMetadata: {}, @@ -154,6 +156,55 @@ export async function resolve( return result } +export async function resolveRepresentation( + did: DidDocumentV2.DidUri, + resolutionOptions: DidResolverV2.RepresentationResolutionOptions = { + accept: 'application/did+json', + } +): Promise { + const result: DidResolverV2.RepresentationResolutionResult = { + didDocumentMetadata: {}, + didResolutionMetadata: {}, + } + + const { accept } = resolutionOptions + if ( + accept !== 'application/did+json' && + accept !== 'application/did+ld+json' && + accept !== 'application/did+cbor' + ) { + result.didResolutionMetadata.error = 'representationNotSupported' + return result + } + + const { didDocumentMetadata, didResolutionMetadata, didDocument } = + await resolve(did) + + result.didDocumentMetadata = didDocumentMetadata + result.didResolutionMetadata = didResolutionMetadata + + if (didDocument === undefined) { + return result + } + + if (accept === 'application/did+json') { + result.didResolutionMetadata.contentType = 'application/did+json' + result.didDocumentStream = Buffer.from(JSON.stringify(didDocument)) + } else if (accept === 'application/did+ld+json') { + const jsonLdDocument: DidDocumentV2.JsonLdDidDocument = { + ...didDocument, + '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + } + result.didResolutionMetadata.contentType = 'application/did+ld+json' + result.didDocumentStream = Buffer.from(JSON.stringify(jsonLdDocument)) + } else if (accept === 'application/did+cbor') { + result.didResolutionMetadata.contentType = 'application/did+cbor' + result.didDocumentStream = Buffer.from(cbor.encode(didDocument)) + } + + return result +} + type InternalDereferenceResult = { contentStream?: DidResolverV2.DereferenceContentStream contentMetadata: DidResolverV2.DereferenceContentMetadata @@ -224,6 +275,7 @@ export async function dereference( } export const resolver: DidResolverV2.DidResolver = { - dereference, resolve, + resolveRepresentation, + dereference, } diff --git a/packages/types/src/DidDocumentV2.ts b/packages/types/src/DidDocumentV2.ts index 58e4e9e21f..28f6329130 100644 --- a/packages/types/src/DidDocumentV2.ts +++ b/packages/types/src/DidDocumentV2.ts @@ -113,3 +113,5 @@ export type DidDocument = { */ service?: Service[] } + +export type JsonLdDidDocument = DidDocument & { '@context': string[] } diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index 895d2d9542..b62cfb0e77 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -13,30 +13,26 @@ import type { Service, } from './DidDocumentV2' -export type DidResolutionOptions = Record +export type ResolutionOptions = Record /* * This specification defines the following common error values: * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. - * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. */ -type DidResolutionMetadataError = - | 'invalidDid' - | 'notFound' - | 'representationNotSupported' +type ResolutionMetadataError = 'invalidDid' | 'notFound' -type DidResolutionMetadata = { +export type ResolutionMetadata = { /* * The error code from the resolution process. * This property is REQUIRED when there is an error in the resolution process. * The value of this property MUST be a single keyword ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries. */ - error?: DidResolutionMetadataError + error?: ResolutionMetadataError } -export type DidDocumentMetadata = { +export type ResolutionDocumentMetadata = { /* * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. * If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. @@ -59,7 +55,7 @@ export type ResolutionResult = { * If the resolution is not successful, this structure MUST contain an error property describing the error. * The possible properties within this structure and their possible values are registered in the DID Specification Registries. */ - didResolutionMetadata: DidResolutionMetadata + didResolutionMetadata: ResolutionMetadata /* * If the resolution is successful, and if the resolve function was called, this MUST be a DID document abstract data model (a map) as described in 4. Data Model that is capable of being transformed into a conforming DID Document (representation), using the production rules specified by the representation. * The value of id in the resolved DID document MUST match the DID that was resolved. @@ -73,7 +69,79 @@ export type ResolutionResult = { * If the resolution is unsuccessful, this output MUST be an empty metadata structure. * The possible properties within this structure and their possible values SHOULD be registered in the DID Specification Registries. */ - didDocumentMetadata: DidDocumentMetadata + didDocumentMetadata: ResolutionDocumentMetadata +} + +export type RepresentationResolutionOptions = { + /* + * The Media Type of the caller's preferred representation of the DID document. + * The Media Type MUST be expressed as an ASCII string. + * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. + * This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. + */ + accept?: string +} + +/* + * This specification defines the following common error values: + * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. + */ +type RepresentationResolutionMetadataError = + | 'invalidDid' + | 'notFound' + | 'representationNotSupported' + +export type RepresentationResolutionMetadata = { + error?: RepresentationResolutionMetadataError + /* + * The Media Type of the returned didDocumentStream. + * This property is REQUIRED if resolution is successful and if the resolveRepresentation function was called. + * This property MUST NOT be present if the resolve function was called. + * The value of this property MUST be an ASCII string that is the Media Type of the conformant representations. + * The caller of the resolveRepresentation function MUST use this value when determining how to parse and process the didDocumentStream returned by this function into the data model. + */ + contentType?: string +} + +export type RepresentationResolutionDocumentMetadata = + ResolutionDocumentMetadata + +export type RepresentationResolutionResult = Pick< + ResolutionResult, + 'didDocumentMetadata' +> & { + /* + * If the resolution is successful, and if the resolveRepresentation function was called, this MUST be a byte stream of the resolved DID document in one of the conformant representations. + * The byte stream might then be parsed by the caller of the resolveRepresentation function into a data model, which can in turn be validated and processed. + * If the resolution is unsuccessful, this value MUST be an empty stream. + */ + didDocumentStream?: Buffer + didResolutionMetadata: RepresentationResolutionMetadata +} + +/* + * The resolve function returns the DID document in its abstract form (a map). + */ +export interface ResolveDid { + resolve: ( + /* + * This is the DID to resolve. + * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. + */ + did: DidUri, + /* + * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. + * This input is REQUIRED, but the structure MAY be empty. + */ + resolutionOptions: ResolutionOptions + ) => Promise + + resolveRepresentation: ( + did: DidUri, + resolutionOptions: RepresentationResolutionOptions + ) => Promise } export type DereferenceOptions = { @@ -92,7 +160,7 @@ export type DereferenceOptions = { */ type DidDereferenceMetadataError = 'invalidDidUrl' | 'notFound' -export type DereferencingMetadata = { +export type DereferenceMetadata = { /* * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. * The Media Type value MUST be expressed as an ASCII string. @@ -113,7 +181,7 @@ export type DereferenceContentStream = | Service export type DereferenceContentMetadata = - | DidDocumentMetadata + | ResolutionDocumentMetadata | Record export type DereferenceResult = { @@ -123,7 +191,7 @@ export type DereferenceResult = { * Properties defined by this specification are in 7.2.2 DID URL Dereferencing Metadata. * If the dereferencing is not successful, this structure MUST contain an error property describing the error. */ - dereferencingMetadata: DereferencingMetadata + dereferencingMetadata: DereferenceMetadata /* * If the dereferencing function was called and successful, this MUST contain a resource corresponding to the DID URL. * The contentStream MAY be a resource such as a DID document that is serializable in one of the conformant representations, a Verification Method, a service, or any other resource format that can be identified via a Media Type and obtained through the resolution process. @@ -139,24 +207,6 @@ export type DereferenceResult = { contentMetadata: DereferenceContentMetadata } -/* - * The resolve function returns the DID document in its abstract form (a map). - */ -export interface ResolveDid { - resolve: ( - /* - * This is the DID to resolve. - * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. - */ - did: DidUri, - /* - * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. - * This input is REQUIRED, but the structure MAY be empty. - */ - resolutionOptions: DidResolutionOptions - ) => Promise -} - export interface DereferenceDidUrl { dereference: ( /* From dd3148c0faa76e9cea98263fa253f06929ab0190 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 3 Oct 2023 10:59:54 +0100 Subject: [PATCH 12/78] Better DID resolver --- packages/did/src/Did2.signature.ts | 2 +- packages/did/src/DidResolver/DidResolverV2.ts | 178 +++++++++++------- packages/types/src/DidDocumentV2.ts | 2 +- packages/types/src/DidResolverV2.ts | 92 ++++++--- 4 files changed, 181 insertions(+), 93 deletions(-) diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts index 9edcb4da82..955dda2470 100644 --- a/packages/did/src/Did2.signature.ts +++ b/packages/did/src/Did2.signature.ts @@ -19,7 +19,7 @@ export type DidSignatureVerificationInput = { expectedSigner?: DidDocumentV2.DidUri allowUpgraded?: boolean expectedVerificationMethodRelationship?: DidDocumentV2.SignatureVerificationMethodRelationship - dereferenceDidUrl?: DidResolverV2.DereferenceDidUrl + dereferenceDidUrl?: DidResolverV2.DereferenceDidUrl } export type DidSignature = { diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index e055b5f7d7..ccb901ef59 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -17,7 +17,20 @@ import { validateUri, } from '../Did2.utils.js' import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2' +import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' + +const DID_JSON = 'application/did+json' +const DID_JSON_LD = 'application/did+ld+json' +const DID_CBOR = 'application/did+cbor' + +export type SupportedContentType = + | typeof DID_JSON + | typeof DID_JSON_LD + | typeof DID_CBOR + +function isValidContentType(input: unknown): input is SupportedContentType { + return input === DID_JSON || input === DID_JSON_LD || input === DID_CBOR +} type InternalResolutionResult = { document?: DidDocumentV2.DidDocument @@ -130,79 +143,82 @@ export async function resolve( // eslint-disable-next-line @typescript-eslint/no-unused-vars resolutionOptions: DidResolverV2.ResolutionOptions = {} ): Promise { - const result: DidResolverV2.ResolutionResult = { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - } - try { validateUri(did, 'Did') } catch (error) { - result.didResolutionMetadata.error = 'invalidDid' - return result + return { + didResolutionMetadata: { + error: 'invalidDid', + }, + didDocumentMetadata: {}, + } } const resolutionResult = await resolveInternal(did) if (!resolutionResult) { - result.didResolutionMetadata.error = 'notFound' - return result + return { + didResolutionMetadata: { + error: 'notFound', + }, + didDocumentMetadata: {}, + } } - const { documentMetadata, document } = resolutionResult - - result.didDocument = document - result.didDocumentMetadata = documentMetadata + const { documentMetadata: didDocumentMetadata, document: didDocument } = resolutionResult - return result + return { + didResolutionMetadata: {}, + didDocumentMetadata, + didDocument, + } } export async function resolveRepresentation( did: DidDocumentV2.DidUri, - resolutionOptions: DidResolverV2.RepresentationResolutionOptions = { - accept: 'application/did+json', + { accept }: DidResolverV2.DereferenceOptions = { + accept: DID_JSON, } -): Promise { - const result: DidResolverV2.RepresentationResolutionResult = { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - } - - const { accept } = resolutionOptions - if ( - accept !== 'application/did+json' && - accept !== 'application/did+ld+json' && - accept !== 'application/did+cbor' - ) { - result.didResolutionMetadata.error = 'representationNotSupported' - return result +): Promise> { + if (!isValidContentType(accept)) { + return { + didResolutionMetadata: { + error: 'representationNotSupported', + }, + didDocumentMetadata: {}, + } } const { didDocumentMetadata, didResolutionMetadata, didDocument } = await resolve(did) - result.didDocumentMetadata = didDocumentMetadata - result.didResolutionMetadata = didResolutionMetadata - if (didDocument === undefined) { - return result + return { + // Metadata is the same, since the `representationNotSupported` is already accounted for above. + didResolutionMetadata, + didDocumentMetadata, + } as DidResolverV2.RepresentationResolutionResult } - if (accept === 'application/did+json') { - result.didResolutionMetadata.contentType = 'application/did+json' - result.didDocumentStream = Buffer.from(JSON.stringify(didDocument)) - } else if (accept === 'application/did+ld+json') { - const jsonLdDocument: DidDocumentV2.JsonLdDidDocument = { - ...didDocument, - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + const bufferInput = (() => { + if (accept === 'application/did+json') { + return JSON.stringify(didDocument) } - result.didResolutionMetadata.contentType = 'application/did+ld+json' - result.didDocumentStream = Buffer.from(JSON.stringify(jsonLdDocument)) - } else if (accept === 'application/did+cbor') { - result.didResolutionMetadata.contentType = 'application/did+cbor' - result.didDocumentStream = Buffer.from(cbor.encode(didDocument)) - } + if (accept === 'application/did+ld+json') { + const jsonLdDoc: DidDocumentV2.JsonLd = { + ...didDocument, + '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + } + return JSON.stringify(jsonLdDoc) + } + // contentType === 'application/did+cbor + return cbor.encode(didDocument) + })() - return result + return { + didDocumentMetadata, + didResolutionMetadata, + didDocumentStream: Buffer.from(bufferInput), + } as DidResolverV2.RepresentationResolutionResult } type InternalDereferenceResult = { @@ -213,8 +229,8 @@ type InternalDereferenceResult = { async function dereferenceInternal( didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri, // eslint-disable-next-line @typescript-eslint/no-unused-vars - dereferenceOptions: DidResolverV2.DereferenceOptions -): Promise { + dereferenceOptions: DidResolverV2.DereferenceOptions +): Promise { const { did, fragment } = parse(didUrl) const { didDocument, didDocumentMetadata } = await resolve(did) @@ -246,35 +262,63 @@ async function dereferenceInternal( export async function dereference( didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri, // eslint-disable-next-line @typescript-eslint/no-unused-vars - dereferenceOptions: DidResolverV2.DereferenceOptions = {} -): Promise { - const result: DidResolverV2.DereferenceResult = { - contentMetadata: {}, - dereferencingMetadata: {}, + { accept }: DidResolverV2.DereferenceOptions = { + accept: DID_JSON, } +): Promise> { + // The spec does not include an error for unsupported content types for dereferences + const contentType = isValidContentType(accept) ? accept : DID_JSON try { validateUri(didUrl) } catch (error) { - result.dereferencingMetadata.error = 'invalidDidUrl' - return result + return { + dereferencingMetadata: { + error: 'invalidDidUrl', + }, + contentMetadata: {}, + } } - const resolutionResult = await dereferenceInternal(didUrl, dereferenceOptions) - if (!resolutionResult) { - result.dereferencingMetadata.error = 'notFound' - return result - } + const resolutionResult = await dereferenceInternal(didUrl, { + accept: contentType, + }) const { contentMetadata, contentStream } = resolutionResult - result.contentMetadata = contentMetadata - result.contentStream = contentStream + if (contentStream === undefined) { + return { + dereferencingMetadata: { + error: 'notFound', + }, + contentMetadata, + } + } - return result + const stream = (() => { + if (contentType === 'application/did+json') { + return contentStream + } + if (contentType === 'application/did+ld+json') { + return { + ...contentStream, + '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + } + } + // contentType === 'application/did+cbor' + return Buffer.from(cbor.encode(contentStream)) + })() + + return { + dereferencingMetadata: { + contentType, + }, + contentMetadata, + contentStream: stream, + } } -export const resolver: DidResolverV2.DidResolver = { +export const resolver: DidResolverV2.DidResolver = { resolve, resolveRepresentation, dereference, diff --git a/packages/types/src/DidDocumentV2.ts b/packages/types/src/DidDocumentV2.ts index 28f6329130..48ce64ff11 100644 --- a/packages/types/src/DidDocumentV2.ts +++ b/packages/types/src/DidDocumentV2.ts @@ -114,4 +114,4 @@ export type DidDocument = { service?: Service[] } -export type JsonLdDidDocument = DidDocument & { '@context': string[] } +export type JsonLd = T & { '@context': string[] } diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index b62cfb0e77..8195bd7b40 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -11,8 +11,13 @@ import type { DidResourceUri, VerificationMethod, Service, + JsonLd, } from './DidDocumentV2' +/* + * The `accept` header must not be used for the regular `resolve` function, so we enforce that statically. + * For more info, please refer to https://www.w3.org/TR/did-core/#did-resolution-options. + */ export type ResolutionOptions = Record /* @@ -22,16 +27,21 @@ export type ResolutionOptions = Record */ type ResolutionMetadataError = 'invalidDid' | 'notFound' -export type ResolutionMetadata = { +export type SuccessfulResolutionMetadata = Record +export type FailedResolutionMetadata = { /* * The error code from the resolution process. * This property is REQUIRED when there is an error in the resolution process. * The value of this property MUST be a single keyword ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries. */ - error?: ResolutionMetadataError + error: ResolutionMetadataError } +export type ResolutionMetadata = + | SuccessfulResolutionMetadata + | FailedResolutionMetadata + export type ResolutionDocumentMetadata = { /* * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. @@ -72,14 +82,14 @@ export type ResolutionResult = { didDocumentMetadata: ResolutionDocumentMetadata } -export type RepresentationResolutionOptions = { +export type RepresentationResolutionOptions = { /* * The Media Type of the caller's preferred representation of the DID document. * The Media Type MUST be expressed as an ASCII string. * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. * This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. */ - accept?: string + accept?: Accept } /* @@ -93,8 +103,9 @@ type RepresentationResolutionMetadataError = | 'notFound' | 'representationNotSupported' -export type RepresentationResolutionMetadata = { - error?: RepresentationResolutionMetadataError +export type SuccessfulRepresentationResolutionMetadata< + ContentType extends string +> = { /* * The Media Type of the returned didDocumentStream. * This property is REQUIRED if resolution is successful and if the resolveRepresentation function was called. @@ -102,13 +113,26 @@ export type RepresentationResolutionMetadata = { * The value of this property MUST be an ASCII string that is the Media Type of the conformant representations. * The caller of the resolveRepresentation function MUST use this value when determining how to parse and process the didDocumentStream returned by this function into the data model. */ - contentType?: string + contentType: ContentType } +export type FailedRepresentationResolutionMetadata = { + /* + * The error code from the resolution process. + * This property is REQUIRED when there is an error in the resolution process. + * The value of this property MUST be a single keyword ASCII string. + * The possible property values of this field SHOULD be registered in the DID Specification Registries. + */ + error: RepresentationResolutionMetadataError +} + +export type RepresentationResolutionMetadata = + | SuccessfulRepresentationResolutionMetadata + | FailedRepresentationResolutionMetadata export type RepresentationResolutionDocumentMetadata = ResolutionDocumentMetadata -export type RepresentationResolutionResult = Pick< +export type RepresentationResolutionResult = Pick< ResolutionResult, 'didDocumentMetadata' > & { @@ -118,13 +142,13 @@ export type RepresentationResolutionResult = Pick< * If the resolution is unsuccessful, this value MUST be an empty stream. */ didDocumentStream?: Buffer - didResolutionMetadata: RepresentationResolutionMetadata + didResolutionMetadata: RepresentationResolutionMetadata } /* * The resolve function returns the DID document in its abstract form (a map). */ -export interface ResolveDid { +export interface ResolveDid { resolve: ( /* * This is the DID to resolve. @@ -139,18 +163,26 @@ export interface ResolveDid { ) => Promise resolveRepresentation: ( + /* + * This is the DID to resolve. + * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. + */ did: DidUri, - resolutionOptions: RepresentationResolutionOptions - ) => Promise + /* + * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. + * This input is REQUIRED, but the structure MAY be empty. + */ + resolutionOptions: RepresentationResolutionOptions + ) => Promise> } -export type DereferenceOptions = { +export type DereferenceOptions = { /* * The Media Type that the caller prefers for contentStream. * The Media Type MUST be expressed as an ASCII string. * The DID URL dereferencing implementation SHOULD use this value to determine the contentType of the representation contained in the returned value if such a representation is supported and available. */ - accept?: string + accept?: Accept } /* @@ -158,40 +190,50 @@ export type DereferenceOptions = { * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. */ -type DidDereferenceMetadataError = 'invalidDidUrl' | 'notFound' +type DereferenceMetadataError = 'invalidDidUrl' | 'notFound' -export type DereferenceMetadata = { +export type SuccessfulDereferenceMetadata = { /* * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. * The Media Type value MUST be expressed as an ASCII string. */ - contentType?: string + contentType: ContentType +} +export type FailedDereferenceMetadata = { /* * The error code from the dereferencing process. * This property is REQUIRED when there is an error in the dereferencing process. * The value of this property MUST be a single keyword expressed as an ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ - error?: DidDereferenceMetadataError + error: DereferenceMetadataError } +export type DereferenceMetadata = + | SuccessfulDereferenceMetadata + | FailedDereferenceMetadata + export type DereferenceContentStream = | DidDocument + | JsonLd | VerificationMethod + | JsonLd | Service + | JsonLd + | Buffer export type DereferenceContentMetadata = | ResolutionDocumentMetadata | Record -export type DereferenceResult = { +export type DereferenceResult = { /* * A metadata structure consisting of values relating to the results of the DID URL dereferencing process. * This structure is REQUIRED, and in the case of an error in the dereferencing process, this MUST NOT be empty. * Properties defined by this specification are in 7.2.2 DID URL Dereferencing Metadata. * If the dereferencing is not successful, this structure MUST contain an error property describing the error. */ - dereferencingMetadata: DereferenceMetadata + dereferencingMetadata: DereferenceMetadata /* * If the dereferencing function was called and successful, this MUST contain a resource corresponding to the DID URL. * The contentStream MAY be a resource such as a DID document that is serializable in one of the conformant representations, a Verification Method, a service, or any other resource format that can be identified via a Media Type and obtained through the resolution process. @@ -207,7 +249,7 @@ export type DereferenceResult = { contentMetadata: DereferenceContentMetadata } -export interface DereferenceDidUrl { +export interface DereferenceDidUrl { dereference: ( /* * A conformant DID URL as a single string. @@ -220,8 +262,10 @@ export interface DereferenceDidUrl { * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. * This input is REQUIRED, but the structure MAY be empty. */ - dereferenceOptions: DereferenceOptions - ) => Promise + dereferenceOptions: DereferenceOptions + ) => Promise> } -export interface DidResolver extends ResolveDid, DereferenceDidUrl {} +export interface DidResolver + extends ResolveDid, + DereferenceDidUrl {} From 812a3606a53856c6f3d8af9f1a2a469a718f9385 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 3 Oct 2023 11:08:00 +0100 Subject: [PATCH 13/78] Resolver improvements --- packages/did/src/DidResolver/DidResolverV2.ts | 3 +- packages/types/src/DidResolverV2.ts | 54 ++++++------------- 2 files changed, 19 insertions(+), 38 deletions(-) diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index ccb901ef59..fea6b0f1ce 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -164,7 +164,8 @@ export async function resolve( } } - const { documentMetadata: didDocumentMetadata, document: didDocument } = resolutionResult + const { documentMetadata: didDocumentMetadata, document: didDocument } = + resolutionResult return { didResolutionMetadata: {}, diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index 8195bd7b40..4ae3bdc2b4 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -20,28 +20,19 @@ import type { */ export type ResolutionOptions = Record -/* - * This specification defines the following common error values: - * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. - * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. - */ -type ResolutionMetadataError = 'invalidDid' | 'notFound' - -export type SuccessfulResolutionMetadata = Record -export type FailedResolutionMetadata = { +export type ResolutionMetadata = { /* * The error code from the resolution process. * This property is REQUIRED when there is an error in the resolution process. * The value of this property MUST be a single keyword ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries. + * This specification defines the following common error values: + * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. */ - error: ResolutionMetadataError + error?: 'invalidDid' | 'notFound' } -export type ResolutionMetadata = - | SuccessfulResolutionMetadata - | FailedResolutionMetadata - export type ResolutionDocumentMetadata = { /* * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. @@ -92,17 +83,6 @@ export type RepresentationResolutionOptions = { accept?: Accept } -/* - * This specification defines the following common error values: - * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. - * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. - * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. - */ -type RepresentationResolutionMetadataError = - | 'invalidDid' - | 'notFound' - | 'representationNotSupported' - export type SuccessfulRepresentationResolutionMetadata< ContentType extends string > = { @@ -121,10 +101,15 @@ export type FailedRepresentationResolutionMetadata = { * This property is REQUIRED when there is an error in the resolution process. * The value of this property MUST be a single keyword ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries. + * This specification defines the following common error values: + * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. */ - error: RepresentationResolutionMetadataError + error: 'invalidDid' | 'notFound' | 'representationNotSupported' } +// Either success with `contentType` or failure with `error` export type RepresentationResolutionMetadata = | SuccessfulRepresentationResolutionMetadata | FailedRepresentationResolutionMetadata @@ -185,13 +170,6 @@ export type DereferenceOptions = { accept?: Accept } -/* - * This specification defines the following common error values: - * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) - * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. - */ -type DereferenceMetadataError = 'invalidDidUrl' | 'notFound' - export type SuccessfulDereferenceMetadata = { /* * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. @@ -205,10 +183,14 @@ export type FailedDereferenceMetadata = { * This property is REQUIRED when there is an error in the dereferencing process. * The value of this property MUST be a single keyword expressed as an ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + * This specification defines the following common error values: + * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) + * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. */ - error: DereferenceMetadataError + error: 'invalidDidUrl' | 'notFound' } +// Either success with `contentType` or failure with `error` export type DereferenceMetadata = | SuccessfulDereferenceMetadata | FailedDereferenceMetadata @@ -222,9 +204,7 @@ export type DereferenceContentStream = | JsonLd | Buffer -export type DereferenceContentMetadata = - | ResolutionDocumentMetadata - | Record +export type DereferenceContentMetadata = ResolutionDocumentMetadata export type DereferenceResult = { /* From 54c8022a5924ea80d4d862b5506a116d80c43051 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 3 Oct 2023 15:58:39 +0100 Subject: [PATCH 14/78] Improve API surface to keep the same abstraction level --- packages/did/src/Did2.chain.ts | 280 +++++++++--- packages/did/src/Did2.utils.ts | 64 ++- .../did/src/DidDetails/LightDidDetails.ts | 3 +- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 44 +- .../did/src/DidDetailsv2/FullDidDetailsV2.ts | 2 +- .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 108 +++-- packages/did/src/DidDetailsv2/index.ts | 10 + packages/did/src/DidResolver/DidResolverV2.ts | 10 +- packages/did/src/index.ts | 7 + packages/types/src/CryptoCallbacksV2.ts | 2 +- tests/testUtils/TestUtils2.ts | 400 ++++++++++++++++++ 11 files changed, 771 insertions(+), 159 deletions(-) create mode 100644 packages/did/src/DidDetailsv2/index.ts create mode 100644 tests/testUtils/TestUtils2.ts diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index 91f8c5960c..10b1a53ac6 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -17,57 +17,71 @@ import type { KiltSupportDeposit, } from '@kiltprotocol/augment-api' -import type { +import { BN, CryptoCallbacksV2, Deposit, DidDocumentV2, + encryptionKeyTypesMap, KiltAddress, + NewDidEncryptionKey, + NewDidVerificationKey, SubmittableExtrinsic, + verificationKeyTypesMap, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { - EncryptionKeyType, - NewServiceEndpoint, - NewVerificationMethod, - VerificationKeyType, + DidEncryptionKeyType, + NewService, + DidVerificationKeyType, verificationKeyTypes, } from './DidDetailsv2/DidDetailsV2.js' import { - decodeMultikeyVerificationMethod, - getAddressByVerificationMethod, + multibaseKeyToDidKey, + keypairToMultibaseKey, + getAddressFromVerificationMethod, getFullDidUri, parse, } from './Did2.utils.js' export type ChainDidIdentifier = KiltAddress -export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails export type EncodedVerificationKey = | { sr25519: Uint8Array } | { ed25519: Uint8Array } | { ecdsa: Uint8Array } - export type EncodedEncryptionKey = { x25519: Uint8Array } -export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey +export type EncodedDidKey = EncodedVerificationKey | EncodedEncryptionKey export type EncodedSignature = EncodedVerificationKey +/** + * @param did + */ export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { return parse(did).address } +/** + * @param id + */ export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { return id.replace(/^#/, '') } +/** + * @param encoded + */ export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) } +/** + * @param deposit + */ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { return { owner: Crypto.encodeAddress(deposit.owner, ss58Format), @@ -82,19 +96,17 @@ export type ChainDidBaseKey = { type: string } export type ChainDidVerificationKey = ChainDidBaseKey & { - type: VerificationKeyType + type: DidVerificationKeyType } export type ChainDidEncryptionKey = ChainDidBaseKey & { - type: EncryptionKeyType + type: DidEncryptionKeyType } export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey - export type ChainDidService = { id: string serviceTypes: string[] urls: string[] } - export type ChainDidDetails = { authentication: [ChainDidVerificationKey] assertionMethod?: [ChainDidVerificationKey] @@ -107,9 +119,9 @@ export type ChainDidDetails = { deposit: Deposit } -function didPublicKeyDetailsFromChain( +function publicKeyFromChain( keyId: Hash, - keyDetails: ChainDidPublicKeyDetails + keyDetails: DidDidDetailsDidPublicKeyDetails ): ChainDidKey { const key = keyDetails.key.isPublicEncryptionKey ? keyDetails.key.asPublicEncryptionKey @@ -123,6 +135,9 @@ function didPublicKeyDetailsFromChain( } } +/** + * @param encoded + */ export function documentFromChain( encoded: Option ): ChainDidDetails { @@ -137,9 +152,7 @@ export function documentFromChain( } = encoded.unwrap() const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => - didPublicKeyDetailsFromChain(keyId, keyDetails) - ) + .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) .reduce((res, key) => { res[fragmentIdToChain(key.id)] = key return res @@ -175,17 +188,22 @@ export function documentFromChain( return didRecord } -export function verificationMethodToChain( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > -): EncodedKey { - const { keyType, publicKey } = - decodeMultikeyVerificationMethod(verificationMethod) - return { - [keyType]: publicKey, - } as EncodedKey +export function publicKeyToChain( + key: NewDidVerificationKey +): EncodedVerificationKey +export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey + +/** + * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. + * + * @param key Object describing data associated with a public key. + * @returns Data restructured to allow SCALE encoding by polkadot api. + */ +export function publicKeyToChain( + key: NewDidVerificationKey | NewDidEncryptionKey +): EncodedDidKey { + // TypeScript can't infer type here, so we have to add a type assertion. + return { [key.type]: key.publicKey } as EncodedDidKey } function isUri(str: string): boolean { @@ -207,9 +225,12 @@ function isUriFragment(str: string): boolean { } } -export function validateNewService(endpoint: NewServiceEndpoint): void { +/** + * @param endpoint + */ +export function validateNewService(endpoint: NewService): void { const { id, serviceEndpoint } = endpoint - if (id.startsWith('did:kilt')) { + if ((id as string).startsWith('did:kilt')) { throw new SDKErrors.DidError( `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` ) @@ -228,7 +249,10 @@ export function validateNewService(endpoint: NewServiceEndpoint): void { }) } -export function serviceToChain(service: NewServiceEndpoint): ChainDidService { +/** + * @param service + */ +export function serviceToChain(service: NewService): ChainDidService { validateNewService(service) const { id, type, serviceEndpoint } = service return { @@ -238,9 +262,12 @@ export function serviceToChain(service: NewServiceEndpoint): ChainDidService { } } +/** + * @param encoded + */ export function serviceFromChain( encoded: Option -): NewServiceEndpoint { +): NewService { const { id, serviceTypes, urls } = encoded.unwrap() return { id: `#${id.toUtf8()}`, @@ -258,19 +285,24 @@ export type AuthorizeCallInput = { } interface GetStoreTxInput { - authentication: [NewVerificationMethod] - assertionMethod?: [NewVerificationMethod] - capabilityDelegation?: [NewVerificationMethod] - keyAgreement?: NewVerificationMethod[] + authentication: [NewDidVerificationKey] + assertionMethod?: [NewDidVerificationKey] + capabilityDelegation?: [NewDidVerificationKey] + keyAgreement?: NewDidEncryptionKey[] - service?: NewServiceEndpoint[] + service?: NewService[] } export type GetStoreTxSignCallback = ( signData: Omit ) => Promise -export async function getStoreTx( +/** + * @param input + * @param submitter + * @param sign + */ +export async function getStoreTxFromInput( input: GetStoreTxInput, submitter: KiltAddress, sign: GetStoreTxSignCallback @@ -292,14 +324,14 @@ export async function getStoreTx( } // For now, it only takes the first attestation key, if present. - if (assertionMethod && assertionMethod.length > 1) { + if (assertionMethod !== undefined && assertionMethod.length > 1) { throw new SDKErrors.DidError( `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` ) } // For now, it only takes the first delegation key, if present. - if (capabilityDelegation && capabilityDelegation.length > 1) { + if (capabilityDelegation !== undefined && capabilityDelegation.length > 1) { throw new SDKErrors.DidError( `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` ) @@ -321,19 +353,21 @@ export async function getStoreTx( } const [authenticationKey] = authentication - const did = getAddressByVerificationMethod(authenticationKey) + const did = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authenticationKey), + }) const newAttestationKey = assertionMethod && assertionMethod.length > 0 && - getAddressByVerificationMethod(assertionMethod[0]) + publicKeyToChain(assertionMethod[0]) const newDelegationKey = capabilityDelegation && capabilityDelegation.length > 0 && - getAddressByVerificationMethod(capabilityDelegation[0]) + publicKeyToChain(capabilityDelegation[0]) - const newKeyAgreementKeys = keyAgreement.map(getAddressByVerificationMethod) + const newKeyAgreementKeys = keyAgreement.map(publicKeyToChain) const newServiceDetails = service.map(serviceToChain) const apiInput = { @@ -349,22 +383,157 @@ export async function getStoreTx( .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) .toU8a() - const { signature, verificationMethod } = await sign({ + const { signature, verificationMethodPublicKey } = await sign({ data: encoded, verificationMethodRelationship: 'authentication', }) - const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) + const { keyType } = multibaseKeyToDidKey(verificationMethodPublicKey) const encodedSignature = { [keyType]: signature, } as EncodedSignature return api.tx.did.create(encoded, encodedSignature) } +/** + * @param input + * @param submitter + * @param sign + */ +export async function getStoreTxFromDidDocument( + input: DidDocumentV2.DidDocument, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const { + authentication, + assertionMethod, + keyAgreement, + capabilityDelegation, + service, + verificationMethod, + } = input + + const authKey = (() => { + const authVerificationMethod = verificationMethod.find( + (vm) => vm.id === authentication[0] + ) + if (authVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + authVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const keyAgreementKey = (() => { + if (keyAgreement === undefined) { + return undefined + } + const keyAgreementVerificationMethod = verificationMethod.find( + (vm) => vm.id === keyAgreement?.[0] + ) + if (keyAgreementVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + keyAgreementVerificationMethod.publicKeyMultibase + ) + if (encryptionKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidEncryptionKey + })() + + const assertionMethodKey = (() => { + if (assertionMethod === undefined) { + return undefined + } + const assertionMethodVerificationMethod = verificationMethod.find( + (vm) => vm.id === assertionMethod?.[0] + ) + if (assertionMethodVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + assertionMethodVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const capabilityDelegationKey = (() => { + if (capabilityDelegation === undefined) { + return undefined + } + const capabilityDelegationVerificationMethod = verificationMethod.find( + (vm) => vm.id === capabilityDelegation?.[0] + ) + if (capabilityDelegationVerificationMethod === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + const { keyType, publicKey } = multibaseKeyToDidKey( + capabilityDelegationVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + // TODO: Better error + throw new Error('Malformed DID document.') + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const storeTxInput: GetStoreTxInput = { + authentication: [authKey], + assertionMethod: assertionMethodKey ? [assertionMethodKey] : undefined, + capabilityDelegation: capabilityDelegationKey + ? [capabilityDelegationKey] + : undefined, + keyAgreement: keyAgreementKey ? [keyAgreementKey] : undefined, + service, + } + + return getStoreTxFromInput(storeTxInput, submitter, sign) +} + export interface SigningOptions { sign: CryptoCallbacksV2.SignExtrinsicCallback verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship } +/** + * @param root0 + * @param root0.did + * @param root0.verificationMethodRelationship + * @param root0.sign + * @param root0.call + * @param root0.txCounter + * @param root0.submitter + * @param root0.blockNumber + */ export async function generateDidAuthenticatedTx({ did, verificationMethodRelationship, @@ -386,23 +555,28 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const { verificationMethod, signature } = await sign({ + const { signature, verificationMethodPublicKey } = await sign({ data: signableCall.toU8a(), verificationMethodRelationship, did, }) - const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) + const { keyType } = multibaseKeyToDidKey(verificationMethodPublicKey) const encodedSignature = { [keyType]: signature, } as EncodedSignature return api.tx.did.submitDidCall(signableCall, encodedSignature) } +/** + * @param root0 + * @param root0.publicKeyMultibase + * @param signature + */ export function didSignatureToChain( - signature: Uint8Array, - verificationMethod: DidDocumentV2.VerificationMethod + { publicKeyMultibase }: DidDocumentV2.VerificationMethod, + signature: Uint8Array ): EncodedSignature { - const { keyType } = decodeMultikeyVerificationMethod(verificationMethod) + const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) if (!verificationKeyTypes.includes(keyType)) { throw new SDKErrors.DidError( `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index 9c5dda8451..fc84c212a7 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -10,7 +10,11 @@ import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' -import type { DidDocumentV2, KiltAddress } from '@kiltprotocol/types' +import type { + DidDocumentV2, + KeyringPair, + KiltAddress, +} from '@kiltprotocol/types' import type { DidKeyType } from './DidDetailsv2/DidDetailsV2.js' @@ -121,18 +125,13 @@ const multicodecReversePrefixes: Record = { /** * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. * - * @param verificationMethod The verification method. + * @param publicKeyMultibase The verification method's public key multibase. * @returns The decoded public key and [DidKeyType]. */ -export function decodeMultikeyVerificationMethod( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > +export function multibaseKeyToDidKey( + publicKeyMultibase: DidDocumentV2.VerificationMethod['publicKeyMultibase'] ): DecodedVerificationMethod { - const decodedMulticodecPublicKey = multibaseDecode( - verificationMethod.publicKeyMultibase - ) + const decodedMulticodecPublicKey = multibaseDecode(publicKeyMultibase) const [keyTypeFlag, publicKey] = [ decodedMulticodecPublicKey.subarray(0, 1)[0], decodedMulticodecPublicKey.subarray(1), @@ -148,7 +147,32 @@ export function decodeMultikeyVerificationMethod( throw new Error('Invalid encoding of the verification method.') } -export function encodeVerificationMethodToMultiKey( +export function keypairToMultibaseKey({ + type, + publicKey, +}: Pick< + KeyringPair, + 'publicKey' | 'type' +>): DidDocumentV2.VerificationMethod['publicKeyMultibase'] { + const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] + if (multiCodecPublicKeyPrefix === undefined) { + // TODO: Proper error + throw new Error(`Invalid key type provided: ${type}.`) + } + const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] + if (publicKey.length !== expectedPublicKeySize) { + // TODO: Proper error + throw new Error( + `Provided public key does not match the expected length: ${expectedPublicKeySize}.` + ) + } + const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] + return Buffer.from( + multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) + ).toString() as `z${string}` +} + +export function didKeyToVerificationMethod( controller: DidDocumentV2.VerificationMethod['controller'], id: DidDocumentV2.VerificationMethod['id'], { keyType, publicKey }: DecodedVerificationMethod @@ -207,7 +231,7 @@ export function validateUri( const { address, fragment } = parse(input as DidDocumentV2.DidUri) if ( - fragment && + fragment !== undefined && (expectType === 'Did' || // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. (typeof expectType === 'boolean' && expectType === false)) @@ -217,7 +241,7 @@ export function validateUri( ) } - if (!fragment && expectType === 'ResourceUri') { + if (fragment === undefined && expectType === 'ResourceUri') { throw new SDKErrors.DidError( 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' ) @@ -227,14 +251,10 @@ export function validateUri( } // TODO: Fix JSDoc -export function getAddressByVerificationMethod( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > -): KiltAddress { - const { keyType: type, publicKey } = - decodeMultikeyVerificationMethod(verificationMethod) +export function getAddressFromVerificationMethod({ + publicKeyMultibase, +}: Pick): KiltAddress { + const { keyType: type, publicKey } = multibaseKeyToDidKey(publicKeyMultibase) if (type === 'ed25519' || type === 'sr25519') { return encodeAddress(publicKey, ss58Format) } @@ -275,6 +295,6 @@ export function getFullDidUriFromVerificationMethod( 'publicKeyMultibase' > ): DidDocumentV2.DidUri { - const address = getAddressByVerificationMethod(verificationMethod) + const address = getAddressFromVerificationMethod(verificationMethod) return getFullDidUri(address) } diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 30a752f6bd..98deecc078 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -25,6 +25,7 @@ import { SDKErrors, ss58Format, cbor } from '@kiltprotocol/utils' import { getAddressByKey, parse } from '../Did.utils.js' import { resourceIdToChain, validateService } from '../Did.chain.js' +import { NewService } from '../DidDetailsv2/DidDetailsV2.js' const authenticationKeyId = '#authentication' const encryptionKeyId = '#encryption' @@ -65,7 +66,7 @@ export type CreateDocumentInput = { * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ - service?: DidServiceEndpoint[] + service?: NewService[] } function validateCreateDocumentInput({ diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 3b45131789..333a7853e9 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -5,43 +5,47 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocumentV2, KeyRelationship } from '@kiltprotocol/types' +import type { DidDocumentV2 } from '@kiltprotocol/types' /** * Possible types for a DID verification key. */ const verificationKeyTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const export const verificationKeyTypes = verificationKeyTypesC as unknown as string[] -export type VerificationKeyType = typeof verificationKeyTypesC[number] +export type DidVerificationKeyType = typeof verificationKeyTypesC[number] // `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 -/** - * Currently, a light DID does not support the use of an ECDSA key as its authentication key. - */ -export type LightDidSupportedVerificationKeyType = Extract< - VerificationKeyType, - 'ed25519' | 'sr25519' -> - -/** - * Subset of key relationships which pertain to key agreement/encryption keys. - */ -export type EncryptionKeyRelationship = Extract - /** * Possible types for a DID encryption key. */ const encryptionKeyTypesC = ['x25519'] as const export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] -export type EncryptionKeyType = typeof encryptionKeyTypesC[number] +export type DidEncryptionKeyType = typeof encryptionKeyTypesC[number] -export type DidKeyType = VerificationKeyType | EncryptionKeyType +export type DidKeyType = DidVerificationKeyType | DidEncryptionKeyType export type NewVerificationMethod = Omit< DidDocumentV2.VerificationMethod, 'controller' -> & { - id: DidDocumentV2.UriFragment +> +export type NewService = DidDocumentV2.Service + +/** + * Type of a new key material to add under a DID. + */ +export type BaseNewDidKey = { + publicKey: Uint8Array + type: string +} + +/** + * Type of a new verification key to add under a DID. + */ +export type NewDidVerificationKey = BaseNewDidKey & { + type: DidVerificationKeyType } -export type NewServiceEndpoint = DidDocumentV2.Service +/** + * Type of a new encryption key to add under a DID. + */ +export type NewDidEncryptionKey = BaseNewDidKey & { type: DidEncryptionKeyType } diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts index 4b6502ebcf..d998be6c14 100644 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts @@ -165,7 +165,7 @@ function groupExtrinsicsByKeyRelationship( const [first, ...rest] = extrinsics.map((extrinsic) => { const verificationMethodRelationship = getVerificationMethodRelationshipForTx(extrinsic) - if (!verificationMethodRelationship) { + if (verificationMethodRelationship === undefined) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index a7f6820cae..140e3a2201 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -5,7 +5,12 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidDocumentV2, encryptionKeyTypes } from '@kiltprotocol/types' +import { + DidDocumentV2, + encryptionKeyTypes, + NewDidEncryptionKey, + NewDidVerificationKey, +} from '@kiltprotocol/types' import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { base58Decode, @@ -13,25 +18,28 @@ import { decodeAddress, } from '@polkadot/util-crypto' import { - decodeMultikeyVerificationMethod, - encodeVerificationMethodToMultiKey, - getAddressByVerificationMethod, + keypairToMultibaseKey, + didKeyToVerificationMethod, + getAddressFromVerificationMethod, parse, } from '../Did2.utils.js' import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' -import type { - NewVerificationMethod, - NewServiceEndpoint, - DidKeyType, -} from './DidDetailsV2.js' +import type { NewService, DidVerificationKeyType } from './DidDetailsV2.js' /** * Currently, a light DID does not support the use of an ECDSA key as its authentication key. */ export type LightDidSupportedVerificationKeyType = Extract< - DidKeyType, + DidVerificationKeyType, 'ed25519' | 'sr25519' > +/** + * A new public key specified when creating a new light DID. + */ +export type NewLightDidVerificationKey = NewDidVerificationKey & { + type: LightDidSupportedVerificationKeyType +} + type LightDidEncoding = '00' | '01' const authenticationKeyId = '#authentication' @@ -55,61 +63,54 @@ const lightDidEncodingToVerificationKeyType: Record< export type CreateDocumentInput = { /** - * The DID authentication key. This is mandatory and will be used as the first authentication key + * The DID authentication verification method. This is mandatory and will be used as the first authentication verification method * of the full DID upon migration. */ - authentication: [NewVerificationMethod] + authentication: [NewDidVerificationKey] /** - * The optional DID encryption key. If present, it will be used as the first key agreement key + * The optional DID encryption verification method. If present, it will be used as the first key agreement verification method * of the full DID upon migration. */ - keyAgreement?: [NewVerificationMethod] + keyAgreement?: [NewDidEncryptionKey] /** * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ - service?: NewServiceEndpoint[] + service?: NewService[] } function validateCreateDocumentInput({ authentication, keyAgreement, - service: services, + service, }: CreateDocumentInput): void { // Check authentication key type - const { keyType: authenticationKeyType } = decodeMultikeyVerificationMethod( - authentication[0] - ) - const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[ - authenticationKeyType - ] as LightDidEncoding + const authenticationKeyTypeEncoding = + verificationKeyTypeToLightDidEncoding[authentication[0].type] - if (!authenticationKeyTypeEncoding) { + if (authenticationKeyTypeEncoding !== undefined) { throw new SDKErrors.UnsupportedKeyError(authentication[0].type) } - - if (keyAgreement?.[0] !== undefined) { - const { keyType: keyAgreementKeyType } = decodeMultikeyVerificationMethod( - keyAgreement[0] + if ( + keyAgreement?.[0].type && + !encryptionKeyTypes.includes(keyAgreement[0].type) + ) { + throw new SDKErrors.DidError( + `Encryption key type "${keyAgreement[0].type}" is not supported` ) - if (!encryptionKeyTypes.includes(keyAgreementKeyType)) { - throw new SDKErrors.DidError( - `Encryption key type "${keyAgreementKeyType}" is not supported` - ) - } } // Checks that for all service IDs have regular strings as their ID and not a full DID. // Plus, we forbid a service ID to be `authentication` or `encryption` as that would create confusion // when upgrading to a full DID. - services?.forEach((service) => { + service?.forEach((s) => { // A service ID cannot have a reserved ID that is used for key IDs. - if (service.id === '#authentication' || service.id === '#encryption') { + if (s.id === '#authentication' || s.id === '#encryption') { throw new SDKErrors.DidError( - `Cannot specify a service ID with the name "${service.id}" as it is a reserved keyword` + `Cannot specify a service ID with the name "${s.id}" as it is a reserved keyword` ) } - validateNewService(service) + validateNewService(s) }) } @@ -117,9 +118,9 @@ const KEY_AGREEMENT_MAP_KEY = 'e' const SERVICES_MAP_KEY = 's' interface SerializableStructure { - [KEY_AGREEMENT_MAP_KEY]?: NewVerificationMethod + [KEY_AGREEMENT_MAP_KEY]?: NewDidEncryptionKey [SERVICES_MAP_KEY]?: Array< - Partial> & { + Partial> & { id: string } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their service endpoints. > @@ -197,11 +198,11 @@ export function createLightDidDocument({ service, }) // Validity is checked in validateCreateDocumentInput - const { keyType: authenticationKeyType, publicKey: authenticationPublicKey } = - decodeMultikeyVerificationMethod(authentication[0]) const authenticationKeyTypeEncoding = - verificationKeyTypeToLightDidEncoding[authenticationKeyType] - const address = getAddressByVerificationMethod(authentication[0]) + verificationKeyTypeToLightDidEncoding[authentication[0].type] + const address = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authentication[0]), + }) const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' const uri = @@ -211,22 +212,20 @@ export function createLightDidDocument({ id: uri, authentication: [authenticationKeyId], verificationMethod: [ - encodeVerificationMethodToMultiKey(uri, authenticationKeyId, { - keyType: authenticationKeyType, - publicKey: authenticationPublicKey, + didKeyToVerificationMethod(uri, authenticationKeyId, { + keyType: authentication[0].type, + publicKey: authentication[0].publicKey, }), ], service, } if (keyAgreement !== undefined) { - const { keyType: keyAgreementKeyType, publicKey: keyAgreementPublicKey } = - decodeMultikeyVerificationMethod(keyAgreement[0]) did.keyAgreement = [encryptionKeyId] did.verificationMethod.push( - encodeVerificationMethodToMultiKey(uri, encryptionKeyId, { - keyType: keyAgreementKeyType, - publicKey: keyAgreementPublicKey, + didKeyToVerificationMethod(uri, encryptionKeyId, { + keyType: keyAgreement[0].type, + publicKey: keyAgreement[0].publicKey, }) ) } @@ -252,7 +251,7 @@ export function parseDocumentFromLightDid( `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` ) } - if (fragment && failIfFragmentPresent) { + if (fragment !== undefined && failIfFragmentPresent) { throw new SDKErrors.DidError( `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` ) @@ -267,11 +266,8 @@ export function parseDocumentFromLightDid( ) } const publicKey = decodeAddress(address, false, ss58Format) - const authentication: [NewVerificationMethod] = [ - encodeVerificationMethodToMultiKey(uri, authenticationKeyId, { - keyType, - publicKey, - }), + const authentication: [NewLightDidVerificationKey] = [ + { publicKey, type: keyType }, ] if (!encodedDetails) { return createLightDidDocument({ authentication }) diff --git a/packages/did/src/DidDetailsv2/index.ts b/packages/did/src/DidDetailsv2/index.ts new file mode 100644 index 0000000000..9977d6fdb4 --- /dev/null +++ b/packages/did/src/DidDetailsv2/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +export * from './DidDetailsV2.js' +export * from './LightDidDetailsV2.js' +export * from './FullDidDetailsV2.js' diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index fea6b0f1ce..9d49bfdc6d 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -11,7 +11,7 @@ import { cbor } from '@kiltprotocol/utils' import { linkedInfoFromChain } from '../Did.rpc' import { toChain } from '../Did2.chain.js' import { - encodeVerificationMethodToMultiKey, + didKeyToVerificationMethod, getFullDidUri, parse, validateUri, @@ -53,7 +53,7 @@ async function resolveInternal( id: did, authentication: [document.authentication[0].id], verificationMethod: [ - encodeVerificationMethodToMultiKey(did, document.authentication[0].id, { + didKeyToVerificationMethod(did, document.authentication[0].id, { publicKey: document.authentication[0].publicKey, keyType: document.authentication[0].type, }), @@ -64,7 +64,7 @@ async function resolveInternal( newDidDocument.assertionMethod = document.assertionMethod.map((k) => k.id) newDidDocument.verificationMethod.push( ...document.assertionMethod.map((k) => - encodeVerificationMethodToMultiKey(did, k.id, { + didKeyToVerificationMethod(did, k.id, { keyType: k.type, publicKey: k.publicKey, }) @@ -75,7 +75,7 @@ async function resolveInternal( newDidDocument.assertionMethod = document.keyAgreement.map((k) => k.id) newDidDocument.verificationMethod.push( ...document.keyAgreement.map((k) => - encodeVerificationMethodToMultiKey(did, k.id, { + didKeyToVerificationMethod(did, k.id, { keyType: k.type, publicKey: k.publicKey, }) @@ -88,7 +88,7 @@ async function resolveInternal( ) newDidDocument.verificationMethod.push( ...document.capabilityDelegation.map((k) => - encodeVerificationMethodToMultiKey(did, k.id, { + didKeyToVerificationMethod(did, k.id, { keyType: k.type, publicKey: k.publicKey, }) diff --git a/packages/did/src/index.ts b/packages/did/src/index.ts index 38320500db..4901cdb293 100644 --- a/packages/did/src/index.ts +++ b/packages/did/src/index.ts @@ -17,3 +17,10 @@ export * from './Did.rpc.js' export * from './Did.utils.js' export * from './Did.signature.js' export * from './DidLinks/AccountLinks.chain.js' + +export * as DidDetailsV2 from './DidDetailsv2/index.js' +export * as DidResolverV2 from './DidResolver/DidResolverV2.js' +export * as DidChainV2 from './Did2.chain.js' +export * as DidUtilsV2 from './Did2.utils.js' +export * as DidSignatureV2 from './Did2.signature.js' +export * as DidLinksV2 from './DidLinks/AccountLinks2.chain.js' diff --git a/packages/types/src/CryptoCallbacksV2.ts b/packages/types/src/CryptoCallbacksV2.ts index c56f74e442..4771cabefd 100644 --- a/packages/types/src/CryptoCallbacksV2.ts +++ b/packages/types/src/CryptoCallbacksV2.ts @@ -39,7 +39,7 @@ export interface SignResponseData { /** * The did key uri used for signing. */ - verificationMethod: DidDocumentV2.VerificationMethod + verificationMethodPublicKey: DidDocumentV2.VerificationMethod['publicKeyMultibase'] } /** diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts new file mode 100644 index 0000000000..3354e86051 --- /dev/null +++ b/tests/testUtils/TestUtils2.ts @@ -0,0 +1,400 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' + +import type { + CryptoCallbacksV2, + DidDocument, + DidDocumentV2, + DidKey, + DidServiceEndpoint, + DidVerificationKey, + KeyRelationship, + KeyringPair, + KiltEncryptionKeypair, + KiltKeyringPair, + LightDidSupportedVerificationKeyType, + NewLightDidVerificationKey, +} from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import * as Did from '@kiltprotocol/did' + +import { Blockchain } from '@kiltprotocol/chain-helpers' +import { ConfigService } from '@kiltprotocol/config' +import { getStoreTxFromDidDocument } from 'did/src/Did2.chain' + +export type EncryptionKeyToolCallback = ( + didDocument: DidDocumentV2.DidDocument +) => CryptoCallbacksV2.EncryptCallback + +/** + * Generates a callback that can be used for encryption. + * + * @param secretKey The options parameter. + * @param secretKey.secretKey The key to use for encryption. + * @returns The callback. + */ +export function makeEncryptCallback({ + secretKey, +}: KiltEncryptionKeypair): EncryptionKeyToolCallback { + return (didDocument) => { + return async function encryptCallback({ data, peerPublicKey }) { + const keyId = didDocument.keyAgreement?.[0] + if (keyId === undefined) { + throw new Error(`Encryption key not found in did "${didDocument.id}"`) + } + const verificationMethod = didDocument.verificationMethod.find( + (v) => v.id === keyId + ) as DidDocumentV2.VerificationMethod + const { box, nonce } = Crypto.encryptAsymmetric( + data, + peerPublicKey, + secretKey + ) + return { + nonce, + data: box, + verificationMethod, + } + } + } +} + +/** + * Generates a callback that can be used for decryption. + * + * @param secretKey The options parameter. + * @param secretKey.secretKey The key to use for decryption. + * @returns The callback. + */ +export function makeDecryptCallback({ + secretKey, +}: KiltEncryptionKeypair): CryptoCallbacksV2.DecryptCallback { + return async function decryptCallback({ data, nonce, peerPublicKey }) { + const decrypted = Crypto.decryptAsymmetric( + { box: data, nonce }, + peerPublicKey, + secretKey + ) + if (decrypted === false) throw new Error('Decryption failed') + return { data: decrypted } + } +} + +export interface EncryptionKeyTool { + keyAgreement: [KiltEncryptionKeypair] + encrypt: EncryptionKeyToolCallback + decrypt: CryptoCallbacksV2.DecryptCallback +} + +/** + * Generates a keypair suitable for encryption. + * + * @param seed {string} Input to generate the keypair from. + * @returns Object with secret and public key and the key type. + */ +export function makeEncryptionKeyTool(seed: string): EncryptionKeyTool { + const keypair = Crypto.makeEncryptionKeypairFromSeed(blake2AsU8a(seed, 256)) + + const encrypt = makeEncryptCallback(keypair) + const decrypt = makeDecryptCallback(keypair) + + return { + keyAgreement: [keypair], + encrypt, + decrypt, + } +} + +export type KeyToolSignCallback = ( + didDocument: DidDocumentV2.DidDocument +) => CryptoCallbacksV2.SignCallback + +/** + * Generates a callback that can be used for signing. + * + * @param keypair The keypair to use for signing. + * @returns The callback. + */ +export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { + return (didDocument) => + async function sign({ data, verificationMethodRelationship }) { + const keyId = didDocument[verificationMethodRelationship]?.[0] + if (keyId === undefined) { + throw new Error( + `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` + ) + } + const verificationMethod = didDocument.verificationMethod.find( + (vm) => vm.id === keyId + ) + if (verificationMethod === undefined) { + throw new Error( + `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` + ) + } + const signature = keypair.sign(data, { withType: false }) + + return { + signature, + verificationMethodPublicKey: verificationMethod.publicKeyMultibase, + } + } +} + +type StoreDidCallback = Parameters< + typeof Did.DidChainV2.getStoreTxFromInput +>['2'] + +/** + * Generates a callback that can be used for signing. + * + * @param keypair The keypair to use for signing. + * @returns The callback. + */ +export function makeStoreDidCallback( + keypair: KiltKeyringPair +): StoreDidCallback { + return async function sign({ data }) { + const signature = keypair.sign(data, { withType: false }) + return { + signature, + verificationMethodPublicKey: + Did.DidUtilsV2.keypairToMultibaseKey(keypair), + } + } +} + +export interface KeyTool { + keypair: KiltKeyringPair + getSignCallback: KeyToolSignCallback + storeDidCallback: StoreDidCallback + authentication: [NewLightDidVerificationKey] +} + +/** + * Generates a keypair usable for signing and a few related values. + * + * @param type The type to use for the keypair. + * @returns The keypair, matching sign callback, a key usable as DID authentication key. + */ +export function makeSigningKeyTool( + type: KiltKeyringPair['type'] = 'sr25519' +): KeyTool { + const keypair = Crypto.makeKeypairFromSeed(undefined, type) + const getSignCallback = makeSignCallback(keypair) + const storeDidCallback = makeStoreDidCallback(keypair) + + return { + keypair, + getSignCallback, + storeDidCallback, + authentication: [keypair as NewLightDidVerificationKey], + } +} + +/** + * Given a keypair, creates a light DID with an authentication and an encryption key. + * + * @param keypair KeyringPair instance for authentication key. + * @returns DidDocument. + */ +export async function createMinimalLightDidFromKeypair( + keypair: KeyringPair +): Promise { + const type = keypair.type as LightDidSupportedVerificationKeyType + return Did.DidDetailsV2.createLightDidDocument({ + authentication: [{ publicKey: keypair.publicKey, type }], + keyAgreement: makeEncryptionKeyTool(`${keypair.publicKey}//enc`) + .keyAgreement, + }) +} + +// Mock function to generate a key ID without having to rely on a real chain metadata. +export function computeKeyId(key: DidKey['publicKey']): DidKey['id'] { + return `#${blake2AsHex(key, 256)}` +} + +function makeDidKeyFromKeypair({ + publicKey, + type, +}: KiltKeyringPair): DidVerificationKey { + return { + id: computeKeyId(publicKey), + publicKey, + type, + } +} + +/** + * Creates [[DidDocument]] for local use, e.g., in testing. Will not work on-chain because key IDs are generated ad-hoc. + * + * @param keypair The KeyringPair for authentication key, other keys derived from it. + * @param generationOptions The additional options for generation. + * @param generationOptions.keyRelationships The set of key relationships to indicate which keys must be added to the DID. + * @param generationOptions.endpoints The set of service endpoints that must be added to the DID. + * + * @returns A promise resolving to a [[DidDocument]] object. The resulting object is NOT stored on chain. + */ +export async function createLocalDemoFullDidFromKeypair( + keypair: KiltKeyringPair, + { + keyRelationships = new Set([ + 'assertionMethod', + 'capabilityDelegation', + 'keyAgreement', + ]), + endpoints = [], + }: { + keyRelationships?: Set> + endpoints?: DidServiceEndpoint[] + } = {} +): Promise { + const authKey = makeDidKeyFromKeypair(keypair) + const uri = Did.getFullDidUriFromKey(authKey) + + const result: DidDocument = { + uri, + authentication: [authKey], + service: endpoints, + } + + if (keyRelationships.has('keyAgreement')) { + const encryptionKeypair = makeEncryptionKeyTool(`${keypair.publicKey}//enc`) + .keyAgreement[0] + const encKey = { + ...encryptionKeypair, + id: computeKeyId(encryptionKeypair.publicKey), + } + result.keyAgreement = [encKey] + } + if (keyRelationships.has('assertionMethod')) { + const attKey = makeDidKeyFromKeypair( + keypair.derive('//att') as KiltKeyringPair + ) + result.assertionMethod = [attKey] + } + if (keyRelationships.has('capabilityDelegation')) { + const delKey = makeDidKeyFromKeypair( + keypair.derive('//del') as KiltKeyringPair + ) + result.capabilityDelegation = [delKey] + } + + return result +} + +/** + * Creates a full DID from a light DID where the verification keypair is enabled for all verification purposes (authentication, assertionMethod, capabilityDelegation). + * This is not recommended, use for demo purposes only! + * + * @param lightDid The light DID whose keys will be used on the full DID. + * @returns A full DID instance that is not yet written to the blockchain. + */ +export async function createLocalDemoFullDidFromLightDid( + lightDid: DidDocument +): Promise { + const { uri, authentication } = lightDid + + return { + uri: Did.getFullDidUri(uri), + authentication, + assertionMethod: authentication, + capabilityDelegation: authentication, + keyAgreement: lightDid.keyAgreement, + } +} + +// It takes the auth key from the light DID and use it as attestation and delegation key as well. +export async function createFullDidFromLightDid( + payer: KiltKeyringPair, + lightDidForId: DidDocumentV2.DidDocument, + sign: StoreDidCallback +): Promise { + const api = ConfigService.get('api') + const fullDidDocumentToBeCreated = lightDidForId + fullDidDocumentToBeCreated.assertionMethod = [ + fullDidDocumentToBeCreated.authentication[0], + ] + fullDidDocumentToBeCreated.capabilityDelegation = [ + fullDidDocumentToBeCreated.authentication[0], + ] + const tx = await getStoreTxFromDidDocument( + fullDidDocumentToBeCreated, + payer.address, + sign + ) + await Blockchain.signAndSubmitTx(tx, payer) + const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid + const encodedDidDetails = await queryFunction( + Did.DidChainV2.toChain(Did.getFullDidUri(fullDidDocumentToBeCreated.id)) + ) + const { + document: { + authentication, + uri, + assertionMethod, + capabilityDelegation, + keyAgreement, + service, + }, + } = await Did.linkedInfoFromChain(encodedDidDetails) + const didDocument: DidDocumentV2.DidDocument = { + id: uri, + authentication: [authentication[0].id], + verificationMethod: [ + Did.DidUtilsV2.didKeyToVerificationMethod(uri, authentication[0].id, { + keyType: authentication[0].type, + publicKey: authentication[0].publicKey, + }), + ], + service, + } + if (assertionMethod !== undefined) { + didDocument.assertionMethod = [assertionMethod[0].id] + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod(uri, assertionMethod[0].id, { + keyType: assertionMethod[0].type, + publicKey: assertionMethod[0].publicKey, + }) + ) + } + if (capabilityDelegation !== undefined) { + didDocument.capabilityDelegation = [capabilityDelegation[0].id] + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod( + uri, + capabilityDelegation[0].id, + { + keyType: capabilityDelegation[0].type, + publicKey: capabilityDelegation[0].publicKey, + } + ) + ) + } + if (keyAgreement !== undefined) { + didDocument.capabilityDelegation = [keyAgreement[0].id] + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod(uri, keyAgreement[0].id, { + keyType: keyAgreement[0].type, + publicKey: keyAgreement[0].publicKey, + }) + ) + } + + return didDocument +} + +export async function createFullDidFromSeed( + payer: KiltKeyringPair, + keypair: KiltKeyringPair +): Promise { + const lightDid = await createMinimalLightDidFromKeypair(keypair) + const sign = makeStoreDidCallback(keypair) + return createFullDidFromLightDid(payer, lightDid, sign) +} From 9f7c71a25c137fba81ba5486b98d72e397cb21c4 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 4 Oct 2023 10:02:02 +0100 Subject: [PATCH 15/78] First DID integration test passing! --- packages/did/src/Did2.chain.ts | 4 +- packages/did/src/Did2.rpc.ts | 221 +++ packages/did/src/DidDetailsv2/DidDetailsV2.ts | 5 - .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 2 +- packages/did/src/index.ts | 1 + tests/integration/Did2.spec.ts | 1279 +++++++++++++++++ tests/testUtils/TestUtils2.ts | 3 +- tests/testUtils/index.ts | 2 + 8 files changed, 1507 insertions(+), 10 deletions(-) create mode 100644 packages/did/src/Did2.rpc.ts create mode 100644 tests/integration/Did2.spec.ts diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index 10b1a53ac6..641c0d9ccd 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -119,7 +119,7 @@ export type ChainDidDetails = { deposit: Deposit } -function publicKeyFromChain( +export function publicKeyFromChain( keyId: Hash, keyDetails: DidDidDetailsDidPublicKeyDetails ): ChainDidKey { @@ -267,7 +267,7 @@ export function serviceToChain(service: NewService): ChainDidService { */ export function serviceFromChain( encoded: Option -): NewService { +): DidDocumentV2.Service { const { id, serviceTypes, urls } = encoded.unwrap() return { id: `#${id.toUtf8()}`, diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts new file mode 100644 index 0000000000..3ece1ca6a0 --- /dev/null +++ b/packages/did/src/Did2.rpc.ts @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { Option, Vec } from '@polkadot/types' +import type { Codec } from '@polkadot/types/types' +import type { + RawDidLinkedInfo, + DidDidDetails, + DidServiceEndpointsDidEndpoint, + PalletDidLookupLinkableAccountLinkableAccountId, +} from '@kiltprotocol/augment-api' +import type { KiltAddress, DidDocumentV2 } from '@kiltprotocol/types' + +import { ss58Format } from '@kiltprotocol/utils' +import { encodeAddress } from '@polkadot/keyring' +import { ethereumEncode } from '@polkadot/util-crypto' +import { Address, SubstrateAddress } from './DidLinks/AccountLinks.chain.js' +import { didKeyToVerificationMethod } from './Did2.utils.js' +import { + ChainDidDetails, + ChainDidEncryptionKey, + ChainDidKey, + ChainDidVerificationKey, + depositFromChain, + fragmentIdToChain, + fromChain, + publicKeyFromChain, +} from './Did2.chain.js' + +function documentFromChain(encoded: DidDidDetails): ChainDidDetails { + const { + publicKeys, + authenticationKey, + attestationKey, + delegationKey, + keyAgreementKeys, + lastTxCounter, + deposit, + } = encoded + + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) + .reduce((res, key) => { + res[fragmentIdToChain(key.id)] = key + return res + }, {}) + + const authentication = keys[ + authenticationKey.toHex() + ] as ChainDidVerificationKey + + const didRecord: ChainDidDetails = { + authentication: [authentication], + lastTxCounter: lastTxCounter.toBn(), + deposit: depositFromChain(deposit), + } + if (attestationKey.isSome) { + const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey + didRecord.assertionMethod = [key] + } + if (delegationKey.isSome) { + const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey + didRecord.capabilityDelegation = [key] + } + + const keyAgreementKeyIds = [...keyAgreementKeys.values()].map((keyId) => + keyId.toHex() + ) + if (keyAgreementKeyIds.length > 0) { + didRecord.keyAgreement = keyAgreementKeyIds.map( + (id) => keys[id] as ChainDidEncryptionKey + ) + } + + return didRecord +} + +function serviceFromChain( + encoded: DidServiceEndpointsDidEndpoint +): DidDocumentV2.Service { + const { id, serviceTypes, urls } = encoded + return { + id: `#${id.toUtf8()}`, + type: serviceTypes.map((type) => type.toUtf8()), + serviceEndpoint: urls.map((url) => url.toUtf8()), + } +} + +function servicesFromChain( + encoded: DidServiceEndpointsDidEndpoint[] +): DidDocumentV2.Service[] { + return encoded.map((encodedValue) => serviceFromChain(encodedValue)) +} + +function isLinkableAccountId( + arg: Codec +): arg is PalletDidLookupLinkableAccountLinkableAccountId { + return 'isAccountId32' in arg && 'isAccountId20' in arg +} + +function accountFromChain( + account: Codec, + networkPrefix = ss58Format +): KiltAddress | SubstrateAddress { + if (isLinkableAccountId(account)) { + // linked account is substrate address (ethereum-enabled storage version) + if (account.isAccountId32) + return encodeAddress(account.asAccountId32, networkPrefix) + // linked account is ethereum address (ethereum-enabled storage version) + if (account.isAccountId20) return ethereumEncode(account.asAccountId20) + } + // linked account is substrate account (legacy storage version) + return encodeAddress(account.toU8a(), networkPrefix) +} + +function connectedAccountsFromChain( + encoded: Vec, + networkPrefix = ss58Format +): Array { + return encoded.map((account) => + accountFromChain(account, networkPrefix) + ) +} + +export interface LinkedDidInfo { + document: DidDocumentV2.DidDocument + accounts: Address[] +} + +/** + * Decodes accounts, DID, and web3name linked to the provided account. + * + * @param encoded The data returned by `api.call.did.queryByAccount()`, `api.call.did.query()`, and `api.call.did.queryByWeb3Name()`. + * @param networkPrefix The optional network prefix to use to encode the returned addresses. Defaults to KILT prefix (38). Use `42` for the chain-agnostic wildcard Substrate prefix. + * @returns The accounts, DID, and web3name. + */ +export function linkedInfoFromChain( + encoded: Option, + networkPrefix = ss58Format +): LinkedDidInfo { + const { identifier, accounts, w3n, serviceEndpoints, details } = + encoded.unwrap() + + const didRec = documentFromChain(details) + const did: DidDocumentV2.DidDocument = { + id: fromChain(identifier), + authentication: [didRec.authentication[0].id], + verificationMethod: [ + didKeyToVerificationMethod( + fromChain(identifier), + didRec.authentication[0].id, + { + keyType: didRec.authentication[0].type, + publicKey: didRec.authentication[0].publicKey, + } + ), + ], + } + + if (didRec.keyAgreement !== undefined) { + did.keyAgreement = [didRec.keyAgreement[0].id] + did.verificationMethod.push( + didKeyToVerificationMethod( + fromChain(identifier), + didRec.keyAgreement[0].id, + { + keyType: didRec.keyAgreement[0].type, + publicKey: didRec.keyAgreement[0].publicKey, + } + ) + ) + } + + if (didRec.assertionMethod !== undefined) { + did.assertionMethod = [didRec.assertionMethod[0].id] + did.verificationMethod.push( + didKeyToVerificationMethod( + fromChain(identifier), + didRec.assertionMethod[0].id, + { + keyType: didRec.assertionMethod[0].type, + publicKey: didRec.assertionMethod[0].publicKey, + } + ) + ) + } + + if (didRec.capabilityDelegation !== undefined) { + did.capabilityDelegation = [didRec.capabilityDelegation[0].id] + did.verificationMethod.push( + didKeyToVerificationMethod( + fromChain(identifier), + didRec.capabilityDelegation[0].id, + { + keyType: didRec.capabilityDelegation[0].type, + publicKey: didRec.capabilityDelegation[0].publicKey, + } + ) + ) + } + + const services = servicesFromChain(serviceEndpoints) + if (services.length > 0) { + did.service = services + } + + const web3Name = w3n.isNone ? undefined : w3n.unwrap().toHuman() + if (web3Name !== undefined) { + did.alsoKnownAs = [`w3n:${web3Name}`] + } + const linkedAccounts = connectedAccountsFromChain(accounts, networkPrefix) + + return { + document: did, + accounts: linkedAccounts, + } +} diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 333a7853e9..9c20114472 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -44,8 +44,3 @@ export type BaseNewDidKey = { export type NewDidVerificationKey = BaseNewDidKey & { type: DidVerificationKeyType } - -/** - * Type of a new encryption key to add under a DID. - */ -export type NewDidEncryptionKey = BaseNewDidKey & { type: DidEncryptionKeyType } diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index 140e3a2201..527cd59df2 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -88,7 +88,7 @@ function validateCreateDocumentInput({ const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[authentication[0].type] - if (authenticationKeyTypeEncoding !== undefined) { + if (authenticationKeyTypeEncoding === undefined) { throw new SDKErrors.UnsupportedKeyError(authentication[0].type) } if ( diff --git a/packages/did/src/index.ts b/packages/did/src/index.ts index 4901cdb293..0c11446f3a 100644 --- a/packages/did/src/index.ts +++ b/packages/did/src/index.ts @@ -23,4 +23,5 @@ export * as DidResolverV2 from './DidResolver/DidResolverV2.js' export * as DidChainV2 from './Did2.chain.js' export * as DidUtilsV2 from './Did2.utils.js' export * as DidSignatureV2 from './Did2.signature.js' +export * as DidRpc2 from './Did2.rpc.js' export * as DidLinksV2 from './DidLinks/AccountLinks2.chain.js' diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts new file mode 100644 index 0000000000..fa8d19c55e --- /dev/null +++ b/tests/integration/Did2.spec.ts @@ -0,0 +1,1279 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { ApiPromise } from '@polkadot/api' + +import { disconnect } from '@kiltprotocol/core' +import * as Did from '@kiltprotocol/did' +import { + DidDocumentV2, + KiltKeyringPair, + NewDidVerificationKey, +} from '@kiltprotocol/types' + +import { TestUtilsV2 } from '../testUtils/index.js' +import { + createEndowedTestAccount, + devBob, + initializeApi, + submitTx, +} from './utils.js' + +let paymentAccount: KiltKeyringPair +let api: ApiPromise + +beforeAll(async () => { + api = await initializeApi() +}, 30_000) + +beforeAll(async () => { + paymentAccount = await createEndowedTestAccount() +}, 30_000) + +describe.only('write and didDeleteTx', () => { + let did: DidDocumentV2.DidDocument + let key: TestUtilsV2.KeyTool + + beforeAll(async () => { + key = TestUtilsV2.makeSigningKeyTool() + did = await TestUtilsV2.createMinimalLightDidFromKeypair(key.keypair) + }) + + it('fails to create a new DID on chain with a different submitter than the one in the creation operation', async () => { + const otherAccount = devBob + const tx = await Did.DidChainV2.getStoreTxFromDidDocument( + did, + otherAccount.address, + key.storeDidCallback + ) + + await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ + isBadOrigin: true, + }) + }, 60_000) + + it.only('writes a new DID record to chain', async () => { + const { publicKeyMultibase } = did.verificationMethod.find( + (vm) => vm.id === did.authentication[0] + ) as DidDocumentV2.VerificationMethod + const { keyType, publicKey: authPublicKey } = + Did.DidUtilsV2.multibaseKeyToDidKey(publicKeyMultibase) + const newDid = Did.DidDetailsV2.createLightDidDocument({ + authentication: [{ publicKey: authPublicKey, type: keyType }] as [ + NewDidVerificationKey + ], + service: [ + { + id: '#test-id-1', + type: ['test-type-1'], + serviceEndpoint: ['x:test-url-1'], + }, + { + id: '#test-id-2', + type: ['test-type-2'], + serviceEndpoint: ['x:test-url-2'], + }, + ], + }) + + const tx = await Did.DidChainV2.getStoreTxFromDidDocument( + newDid, + paymentAccount.address, + key.storeDidCallback + ) + + await submitTx(tx, paymentAccount) + + const fullDidUri = Did.DidUtilsV2.getFullDidUri(newDid.id) + const fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(fullDidUri) + ) + const { document: fullDid } = + Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + + expect(fullDid).toMatchObject(>{ + id: fullDidUri, + service: [ + { + id: '#test-id-1', + serviceEndpoint: ['x:test-url-1'], + type: ['test-type-1'], + }, + { + id: '#test-id-2', + serviceEndpoint: ['x:test-url-2'], + type: ['test-type-2'], + }, + ], + verificationMethod: [ + expect.objectContaining(>{ + controller: fullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: authPublicKey, + }), + }), + ], + }) + }, 60_000) + + // it('should return no results for empty accounts', async () => { + // const emptyDid = Did.getFullDidUriFromKey( + // makeSigningKeyTool().authentication[0] + // ) + + // const encodedDid = Did.toChain(emptyDid) + // expect((await api.call.did.query(encodedDid)).isSome).toBe(false) + // }) + + // it('fails to delete the DID using a different submitter than the one specified in the DID operation or using a services count that is too low', async () => { + // // We verify that the DID to delete is on chain. + // const fullDidLinkedInfo = await api.call.did.query( + // Did.toChain(Did.getFullDidUri(did.id)) + // ) + // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + // expect(fullDid).not.toBeNull() + + // const otherAccount = devBob + + // // 10 is an example value. It is not used here since we are testing another error + // let call = api.tx.did.delete(new BN(10)) + + // let submittable = await Did.authorizeTx( + // fullDid.uri, + // call, + // signCallback, + // // Use a different account than the submitter one + // otherAccount.address + // ) + + // await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ + // section: 'did', + // name: 'BadDidOrigin', + // }) + + // // We use 1 here and this should fail as there are two service endpoints stored. + // call = api.tx.did.delete(new BN(1)) + + // submittable = await Did.authorizeTx( + // fullDid.uri, + // call, + // signCallback, + // paymentAccount.address + // ) + + // // Will fail because count provided is too low + // await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ + // section: 'did', + // name: expect.stringMatching( + // /^(StoredEndpointsCountTooLarge|MaxStoredEndpointsCountExceeded)$/ + // ), + // }) + // }, 60_000) + + // it('deletes DID from previous step', async () => { + // // We verify that the DID to delete is on chain. + // const fullDidLinkedInfo = await api.call.did.query( + // Did.toChain(Did.getFullDidUri(did.uri)) + // ) + // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + // expect(fullDid).not.toBeNull() + + // const encodedDid = Did.toChain(fullDid.uri) + // const linkedInfo = Did.linkedInfoFromChain( + // await api.call.did.query(encodedDid) + // ) + // const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 + // const call = api.tx.did.delete(storedEndpointsCount) + + // const submittable = await Did.authorizeTx( + // fullDid.uri, + // call, + // signCallback, + // paymentAccount.address + // ) + + // // Check that DID is not blacklisted. + // expect((await api.query.did.didBlacklist(encodedDid)).isNone).toBe(true) + + // await submitTx(submittable, paymentAccount) + + // expect((await api.call.did.query(encodedDid)).isNone).toBe(true) + + // // Check that DID is now blacklisted. + // expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) + // }, 60_000) +}) + +// it('creates and updates DID, and then reclaims the deposit back', async () => { +// const { keypair, getSignCallback, storeDidCallback } = makeSigningKeyTool() +// const newDid = await createMinimalLightDidFromKeypair(keypair) + +// const tx = await Did.getStoreTx( +// newDid, +// paymentAccount.address, +// storeDidCallback +// ) + +// await submitTx(tx, paymentAccount) + +// // This will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. +// let fullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUri(newDid.uri)) +// ) +// let { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + +// const newKey = makeSigningKeyTool() + +// const updateAuthenticationKeyCall = api.tx.did.setAuthenticationKey( +// Did.publicKeyToChain(newKey.authentication[0]) +// ) +// const tx2 = await Did.authorizeTx( +// fullDid.uri, +// updateAuthenticationKeyCall, +// getSignCallback(fullDid), +// paymentAccount.address +// ) +// await submitTx(tx2, paymentAccount) + +// // Authentication key changed, so did must be updated. +// // Also this will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. +// fullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUri(newDid.uri)) +// ) +// fullDid = Did.linkedInfoFromChain(fullDidLinkedInfo).document + +// // Add a new service endpoint +// const newEndpoint: DidServiceEndpoint = { +// id: '#new-endpoint', +// type: ['new-type'], +// serviceEndpoint: ['x:new-url'], +// } +// const updateEndpointCall = api.tx.did.addServiceEndpoint( +// Did.serviceToChain(newEndpoint) +// ) + +// const tx3 = await Did.authorizeTx( +// fullDid.uri, +// updateEndpointCall, +// newKey.getSignCallback(fullDid), +// paymentAccount.address +// ) +// await submitTx(tx3, paymentAccount) + +// const encodedDid = Did.toChain(fullDid.uri) +// const linkedInfo = Did.linkedInfoFromChain( +// await api.call.did.query(encodedDid) +// ) +// expect(Did.getService(linkedInfo.document, newEndpoint.id)).toStrictEqual( +// newEndpoint +// ) + +// // Delete the added service endpoint +// const removeEndpointCall = api.tx.did.removeServiceEndpoint( +// Did.resourceIdToChain(newEndpoint.id) +// ) +// const tx4 = await Did.authorizeTx( +// fullDid.uri, +// removeEndpointCall, +// newKey.getSignCallback(fullDid), +// paymentAccount.address +// ) +// await submitTx(tx4, paymentAccount) + +// // There should not be any endpoint with the given ID now. +// const linkedInfo2 = Did.linkedInfoFromChain( +// await api.call.did.query(encodedDid) +// ) +// expect(Did.getService(linkedInfo2.document, newEndpoint.id)).toBe(undefined) + +// // Claim the deposit back +// const storedEndpointsCount = linkedInfo2.document.service?.length ?? 0 +// const reclaimDepositTx = api.tx.did.reclaimDeposit( +// encodedDid, +// storedEndpointsCount +// ) +// await submitTx(reclaimDepositTx, paymentAccount) +// // Verify that the DID has been deleted +// expect((await api.call.did.query(encodedDid)).isNone).toBe(true) +// }, 80_000) + +// describe('DID migration', () => { +// it('migrates light DID with ed25519 auth key and encryption key', async () => { +// const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') +// const { keyAgreement } = makeEncryptionKeyTool( +// '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' +// ) +// const lightDid = Did.createLightDidDocument({ +// authentication, +// keyAgreement, +// }) + +// const storeTx = await Did.getStoreTx( +// lightDid, +// paymentAccount.address, +// storeDidCallback +// ) + +// await submitTx(storeTx, paymentAccount) +// const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) +// const migratedFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(migratedFullDidUri) +// ) +// const { document: migratedFullDid } = Did.linkedInfoFromChain( +// migratedFullDidLinkedInfo +// ) + +// expect(migratedFullDid).toMatchObject({ +// uri: migratedFullDidUri, +// authentication: [ +// expect.objectContaining({ +// publicKey: lightDid.authentication[0].publicKey, +// type: 'ed25519', +// }), +// ], +// keyAgreement: [ +// expect.objectContaining({ +// publicKey: lightDid.keyAgreement?.[0].publicKey, +// type: 'x25519', +// }), +// ], +// }) + +// expect( +// (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome +// ).toBe(true) + +// const { metadata } = (await Did.resolve( +// lightDid.uri +// )) as DidResolutionResult + +// expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) +// expect(metadata.deactivated).toBe(false) +// }) + +// it('migrates light DID with sr25519 auth key', async () => { +// const { authentication, storeDidCallback } = makeSigningKeyTool() +// const lightDid = Did.createLightDidDocument({ +// authentication, +// }) + +// const storeTx = await Did.getStoreTx( +// lightDid, +// paymentAccount.address, +// storeDidCallback +// ) + +// await submitTx(storeTx, paymentAccount) +// const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) +// const migratedFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(migratedFullDidUri) +// ) +// const { document: migratedFullDid } = Did.linkedInfoFromChain( +// migratedFullDidLinkedInfo +// ) + +// expect(migratedFullDid).toMatchObject({ +// uri: migratedFullDidUri, +// authentication: [ +// expect.objectContaining({ +// publicKey: lightDid.authentication[0].publicKey, +// type: 'sr25519', +// }), +// ], +// }) + +// expect( +// (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome +// ).toBe(true) + +// const { metadata } = (await Did.resolve( +// lightDid.uri +// )) as DidResolutionResult + +// expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) +// expect(metadata.deactivated).toBe(false) +// }) + +// it('migrates light DID with ed25519 auth key, encryption key, and service endpoints', async () => { +// const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') +// const { keyAgreement } = makeEncryptionKeyTool( +// '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' +// ) +// const service: DidServiceEndpoint[] = [ +// { +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }, +// ] +// const lightDid = Did.createLightDidDocument({ +// authentication, +// keyAgreement, +// service, +// }) + +// const storeTx = await Did.getStoreTx( +// lightDid, +// paymentAccount.address, +// storeDidCallback +// ) + +// await submitTx(storeTx, paymentAccount) +// const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) +// const migratedFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(migratedFullDidUri) +// ) +// const { document: migratedFullDid } = Did.linkedInfoFromChain( +// migratedFullDidLinkedInfo +// ) + +// expect(migratedFullDid).toMatchObject({ +// uri: migratedFullDidUri, +// authentication: [ +// expect.objectContaining({ +// publicKey: lightDid.authentication[0].publicKey, +// type: 'ed25519', +// }), +// ], +// keyAgreement: [ +// expect.objectContaining({ +// publicKey: lightDid.keyAgreement?.[0].publicKey, +// type: 'x25519', +// }), +// ], +// service: [ +// { +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }, +// ], +// }) + +// const encodedDid = Did.toChain(migratedFullDid.uri) +// expect((await api.call.did.query(encodedDid)).isSome).toBe(true) + +// const { metadata } = (await Did.resolve( +// lightDid.uri +// )) as DidResolutionResult + +// expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) +// expect(metadata.deactivated).toBe(false) + +// // Remove and claim the deposit back +// const linkedInfo = Did.linkedInfoFromChain( +// await api.call.did.query(encodedDid) +// ) +// const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 +// const reclaimDepositTx = api.tx.did.reclaimDeposit( +// encodedDid, +// storedEndpointsCount +// ) +// await submitTx(reclaimDepositTx, paymentAccount) + +// expect((await api.call.did.query(encodedDid)).isNone).toBe(true) +// expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) +// }, 60_000) +// }) + +// describe('DID authorization', () => { +// // Light DIDs cannot authorize extrinsics +// let did: DidDocument +// const { getSignCallback, storeDidCallback, authentication } = +// makeSigningKeyTool('ed25519') + +// beforeAll(async () => { +// const createTx = await Did.getStoreTx( +// { +// authentication, +// assertionMethod: authentication, +// capabilityDelegation: authentication, +// }, +// paymentAccount.address, +// storeDidCallback +// ) +// await submitTx(createTx, paymentAccount) +// const didLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) +// ) +// did = Did.linkedInfoFromChain(didLinkedInfo).document +// }, 60_000) + +// it('authorizes ctype creation with DID signature', async () => { +// const cType = CType.fromProperties(UUID.generate(), {}) +// const call = api.tx.ctype.add(CType.toChain(cType)) +// const tx = await Did.authorizeTx( +// did.uri, +// call, +// getSignCallback(did), +// paymentAccount.address +// ) +// await submitTx(tx, paymentAccount) + +// await expect(CType.verifyStored(cType)).resolves.not.toThrow() +// }, 60_000) + +// it('no longer authorizes ctype creation after DID deletion', async () => { +// const linkedInfo = Did.linkedInfoFromChain( +// await api.call.did.query(Did.toChain(did.uri)) +// ) +// const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 +// const deleteCall = api.tx.did.delete(storedEndpointsCount) +// const tx = await Did.authorizeTx( +// did.uri, +// deleteCall, +// getSignCallback(did), +// paymentAccount.address +// ) +// await submitTx(tx, paymentAccount) + +// const cType = CType.fromProperties(UUID.generate(), {}) +// const call = api.tx.ctype.add(CType.toChain(cType)) +// const tx2 = await Did.authorizeTx( +// did.uri, +// call, +// getSignCallback(did), +// paymentAccount.address +// ) +// await expect(submitTx(tx2, paymentAccount)).rejects.toMatchObject({ +// section: 'did', +// name: expect.stringMatching(/^(DidNotPresent|NotFound)$/), +// }) + +// await expect(CType.verifyStored(cType)).rejects.toThrow() +// }, 60_000) +// }) + +// describe('DID management batching', () => { +// describe('FullDidCreationBuilder', () => { +// it('Build a complete full DID', async () => { +// const { keypair, storeDidCallback, authentication } = makeSigningKeyTool() +// const extrinsic = await Did.getStoreTx( +// { +// authentication, +// assertionMethod: [ +// { +// publicKey: new Uint8Array(32).fill(1), +// type: 'sr25519', +// }, +// ], +// capabilityDelegation: [ +// { +// publicKey: new Uint8Array(33).fill(1), +// type: 'ecdsa', +// }, +// ], +// keyAgreement: [ +// { +// publicKey: new Uint8Array(32).fill(1), +// type: 'x25519', +// }, +// { +// publicKey: new Uint8Array(32).fill(2), +// type: 'x25519', +// }, +// { +// publicKey: new Uint8Array(32).fill(3), +// type: 'x25519', +// }, +// ], +// service: [ +// { +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }, +// { +// id: '#id-2', +// type: ['type-2'], +// serviceEndpoint: ['x:url-2'], +// }, +// { +// id: '#id-3', +// type: ['type-3'], +// serviceEndpoint: ['x:url-3'], +// }, +// ], +// }, +// paymentAccount.address, +// storeDidCallback +// ) +// await submitTx(extrinsic, paymentAccount) +// const fullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) +// ) +// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + +// expect(fullDid).not.toBeNull() +// expect(fullDid).toMatchObject({ +// authentication: [ +// expect.objectContaining({ +// publicKey: keypair.publicKey, +// type: 'sr25519', +// }), +// ], +// assertionMethod: [ +// expect.objectContaining({ +// publicKey: new Uint8Array(32).fill(1), +// type: 'sr25519', +// }), +// ], +// capabilityDelegation: [ +// expect.objectContaining({ +// publicKey: new Uint8Array(33).fill(1), +// type: 'ecdsa', +// }), +// ], +// keyAgreement: [ +// expect.objectContaining({ +// publicKey: new Uint8Array(32).fill(3), +// type: 'x25519', +// }), +// expect.objectContaining({ +// publicKey: new Uint8Array(32).fill(2), +// type: 'x25519', +// }), +// expect.objectContaining({ +// publicKey: new Uint8Array(32).fill(1), +// type: 'x25519', +// }), +// ], +// service: [ +// { +// id: '#id-3', +// type: ['type-3'], +// serviceEndpoint: ['x:url-3'], +// }, +// { +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }, +// { +// id: '#id-2', +// type: ['type-2'], +// serviceEndpoint: ['x:url-2'], +// }, +// ], +// }) +// }) + +// it('Build a minimal full DID with an Ecdsa key', async () => { +// const { keypair, storeDidCallback } = makeSigningKeyTool('ecdsa') +// const didAuthKey: NewDidVerificationKey = { +// publicKey: keypair.publicKey, +// type: 'ecdsa', +// } + +// const extrinsic = await Did.getStoreTx( +// { authentication: [didAuthKey] }, +// paymentAccount.address, +// storeDidCallback +// ) +// await submitTx(extrinsic, paymentAccount) + +// const fullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUriFromKey(didAuthKey)) +// ) +// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + +// expect(fullDid).not.toBeNull() +// expect(fullDid?.authentication).toMatchObject([ +// { +// publicKey: keypair.publicKey, +// type: 'ecdsa', +// }, +// ]) +// }) +// }) + +// describe('FullDidUpdateBuilder', () => { +// it('Build from a complete full DID and remove everything but the authentication key', async () => { +// const { keypair, getSignCallback, storeDidCallback, authentication } = +// makeSigningKeyTool() + +// const createTx = await Did.getStoreTx( +// { +// authentication, +// keyAgreement: [ +// { +// publicKey: new Uint8Array(32).fill(1), +// type: 'x25519', +// }, +// { +// publicKey: new Uint8Array(32).fill(2), +// type: 'x25519', +// }, +// ], +// assertionMethod: [ +// { +// publicKey: new Uint8Array(32).fill(1), +// type: 'sr25519', +// }, +// ], +// capabilityDelegation: [ +// { +// publicKey: new Uint8Array(33).fill(1), +// type: 'ecdsa', +// }, +// ], +// service: [ +// { +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }, +// { +// id: '#id-2', +// type: ['type-2'], +// serviceEndpoint: ['x:url-2'], +// }, +// ], +// }, +// paymentAccount.address, +// storeDidCallback +// ) +// await submitTx(createTx, paymentAccount) + +// const initialFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) +// ) +// const { document: initialFullDid } = Did.linkedInfoFromChain( +// initialFullDidLinkedInfo +// ) + +// const encryptionKeys = initialFullDid.keyAgreement +// if (!encryptionKeys) throw new Error('No key agreement keys') + +// const extrinsic = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batchAll, +// did: initialFullDid.uri, +// extrinsics: [ +// api.tx.did.removeKeyAgreementKey( +// Did.resourceIdToChain(encryptionKeys[0].id) +// ), +// api.tx.did.removeKeyAgreementKey( +// Did.resourceIdToChain(encryptionKeys[1].id) +// ), +// api.tx.did.removeAttestationKey(), +// api.tx.did.removeDelegationKey(), +// api.tx.did.removeServiceEndpoint('id-1'), +// api.tx.did.removeServiceEndpoint('id-2'), +// ], +// sign: getSignCallback(initialFullDid), +// submitter: paymentAccount.address, +// }) +// await submitTx(extrinsic, paymentAccount) + +// const finalFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(initialFullDid.uri) +// ) +// const { document: finalFullDid } = Did.linkedInfoFromChain( +// finalFullDidLinkedInfo +// ) + +// expect(finalFullDid).not.toBeNull() + +// expect( +// finalFullDid.authentication[0] +// ).toMatchObject({ +// publicKey: keypair.publicKey, +// type: 'sr25519', +// }) + +// expect(finalFullDid.keyAgreement).toBeUndefined() +// expect(finalFullDid.assertionMethod).toBeUndefined() +// expect(finalFullDid.capabilityDelegation).toBeUndefined() +// expect(finalFullDid.service).toBeUndefined() +// }, 40_000) + +// it('Correctly handles rotation of the authentication key', async () => { +// const { authentication, getSignCallback, storeDidCallback } = +// makeSigningKeyTool() +// const { +// authentication: [newAuthKey], +// } = makeSigningKeyTool('ed25519') + +// const createTx = await Did.getStoreTx( +// { authentication }, +// paymentAccount.address, +// storeDidCallback +// ) +// await submitTx(createTx, paymentAccount) + +// const initialFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) +// ) +// const { document: initialFullDid } = Did.linkedInfoFromChain( +// initialFullDidLinkedInfo +// ) + +// const extrinsic = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batchAll, +// did: initialFullDid.uri, +// extrinsics: [ +// api.tx.did.addServiceEndpoint( +// Did.serviceToChain({ +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }) +// ), +// api.tx.did.setAuthenticationKey(Did.publicKeyToChain(newAuthKey)), +// api.tx.did.addServiceEndpoint( +// Did.serviceToChain({ +// id: '#id-2', +// type: ['type-2'], +// serviceEndpoint: ['x:url-2'], +// }) +// ), +// ], +// sign: getSignCallback(initialFullDid), +// submitter: paymentAccount.address, +// }) + +// await submitTx(extrinsic, paymentAccount) + +// const finalFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(initialFullDid.uri) +// ) +// const { document: finalFullDid } = Did.linkedInfoFromChain( +// finalFullDidLinkedInfo +// ) + +// expect(finalFullDid).not.toBeNull() + +// expect(finalFullDid.authentication[0]).toMatchObject({ +// publicKey: newAuthKey.publicKey, +// type: newAuthKey.type, +// }) + +// expect(finalFullDid.keyAgreement).toBeUndefined() +// expect(finalFullDid.assertionMethod).toBeUndefined() +// expect(finalFullDid.capabilityDelegation).toBeUndefined() +// expect(finalFullDid.service).toHaveLength(2) +// }, 40_000) + +// it('simple `batch` succeeds despite failures of some extrinsics', async () => { +// const { authentication, getSignCallback, storeDidCallback } = +// makeSigningKeyTool() +// const tx = await Did.getStoreTx( +// { +// authentication, +// service: [ +// { +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }, +// ], +// }, +// paymentAccount.address, +// storeDidCallback +// ) +// // Create the full DID with a service endpoint +// await submitTx(tx, paymentAccount) +// const fullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) +// ) +// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + +// expect(fullDid.assertionMethod).toBeUndefined() + +// // Try to set a new attestation key and a duplicate service endpoint +// const updateTx = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batch, +// did: fullDid.uri, +// extrinsics: [ +// api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), +// api.tx.did.addServiceEndpoint( +// Did.serviceToChain({ +// id: '#id-1', +// type: ['type-2'], +// serviceEndpoint: ['x:url-2'], +// }) +// ), +// ], +// sign: getSignCallback(fullDid), +// submitter: paymentAccount.address, +// }) +// // Now the second operation fails but the batch succeeds +// await submitTx(updateTx, paymentAccount) + +// const updatedFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(fullDid.uri) +// ) +// const { document: updatedFullDid } = Did.linkedInfoFromChain( +// updatedFullDidLinkedInfo +// ) + +// // .setAttestationKey() extrinsic went through in the batch +// expect(updatedFullDid.assertionMethod?.[0]).toBeDefined() +// // The service endpoint will match the one manually added, and not the one set in the batch +// expect( +// Did.getService(updatedFullDid, '#id-1') +// ).toStrictEqual({ +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }) +// }, 60_000) + +// it('batchAll fails if any extrinsics fails', async () => { +// const { authentication, getSignCallback, storeDidCallback } = +// makeSigningKeyTool() +// const createTx = await Did.getStoreTx( +// { +// authentication, +// service: [ +// { +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }, +// ], +// }, +// paymentAccount.address, +// storeDidCallback +// ) +// await submitTx(createTx, paymentAccount) +// const fullDidLinkedInfo = await api.call.did.query( +// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) +// ) +// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + +// expect(fullDid.assertionMethod).toBeUndefined() + +// // Use batchAll to set a new attestation key and a duplicate service endpoint +// const updateTx = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batchAll, +// did: fullDid.uri, +// extrinsics: [ +// api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), +// api.tx.did.addServiceEndpoint( +// Did.serviceToChain({ +// id: '#id-1', +// type: ['type-2'], +// serviceEndpoint: ['x:url-2'], +// }) +// ), +// ], +// sign: getSignCallback(fullDid), +// submitter: paymentAccount.address, +// }) + +// // Now, submitting will result in the second operation to fail AND the batch to fail, so we can test the atomic flag. +// await expect(submitTx(updateTx, paymentAccount)).rejects.toMatchObject({ +// section: 'did', +// name: expect.stringMatching(/^ServiceAlready(Exists|Present)$/), +// }) + +// const updatedFullDidLinkedInfo = await api.call.did.query( +// Did.toChain(fullDid.uri) +// ) +// const { document: updatedFullDid } = Did.linkedInfoFromChain( +// updatedFullDidLinkedInfo +// ) +// // .setAttestationKey() extrinsic went through but it was then reverted +// expect(updatedFullDid.assertionMethod).toBeUndefined() +// // The service endpoint will match the one manually added, and not the one set in the builder. +// expect( +// Did.getService(updatedFullDid, '#id-1') +// ).toStrictEqual({ +// id: '#id-1', +// type: ['type-1'], +// serviceEndpoint: ['x:url-1'], +// }) +// }, 60_000) +// }) +// }) + +// describe('DID extrinsics batching', () => { +// let fullDid: DidDocument +// let key: KeyTool + +// beforeAll(async () => { +// key = makeSigningKeyTool() +// fullDid = await createFullDidFromSeed(paymentAccount, key.keypair) +// }, 50_000) + +// it('simple batch succeeds despite failures of some extrinsics', async () => { +// const cType = CType.fromProperties(UUID.generate(), {}) +// const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) +// const rootNode = DelegationNode.newRoot({ +// account: fullDid.uri, +// permissions: [Permission.DELEGATE], +// cTypeHash: CType.idToHash(cType.$id), +// }) +// const delegationStoreTx = await rootNode.getStoreTx() +// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) +// const tx = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batch, +// did: fullDid.uri, +// extrinsics: [ +// ctypeStoreTx, +// // Will fail since the delegation cannot be revoked before it is added +// delegationRevocationTx, +// delegationStoreTx, +// ], +// sign: key.getSignCallback(fullDid), +// submitter: paymentAccount.address, +// }) + +// // The entire submission promise is resolves and does not throw +// await submitTx(tx, paymentAccount) + +// // The ctype has been created, even though the delegation operations failed. +// await expect(CType.verifyStored(cType)).resolves.not.toThrow() +// }) + +// it('batchAll fails if any extrinsics fail', async () => { +// const cType = CType.fromProperties(UUID.generate(), {}) +// const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) +// const rootNode = DelegationNode.newRoot({ +// account: fullDid.uri, +// permissions: [Permission.DELEGATE], +// cTypeHash: CType.idToHash(cType.$id), +// }) +// const delegationStoreTx = await rootNode.getStoreTx() +// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) +// const tx = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batchAll, +// did: fullDid.uri, +// extrinsics: [ +// ctypeStoreTx, +// // Will fail since the delegation cannot be revoked before it is added +// delegationRevocationTx, +// delegationStoreTx, +// ], +// sign: key.getSignCallback(fullDid), +// submitter: paymentAccount.address, +// }) + +// // The entire submission promise is rejected and throws. +// await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ +// section: 'delegation', +// name: 'DelegationNotFound', +// }) + +// // The ctype has not been created, since atomicity ensures the whole batch is reverted in case of failure. +// await expect(CType.verifyStored(cType)).rejects.toThrow() +// }) + +// it('can batch extrinsics for the same required key type', async () => { +// const web3NameClaimTx = api.tx.web3Names.claim('test-1') +// const authorizedTx = await Did.authorizeTx( +// fullDid.uri, +// web3NameClaimTx, +// key.getSignCallback(fullDid), +// paymentAccount.address +// ) +// await submitTx(authorizedTx, paymentAccount) + +// const web3Name1ReleaseExt = api.tx.web3Names.releaseByOwner() +// const web3Name2ClaimExt = api.tx.web3Names.claim('test-2') +// const tx = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batch, +// did: fullDid.uri, +// extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], +// sign: key.getSignCallback(fullDid), +// submitter: paymentAccount.address, +// }) +// await submitTx(tx, paymentAccount) + +// // Test for correct creation and deletion +// const encoded1 = await api.call.did.queryByWeb3Name('test-1') +// expect(encoded1.isSome).toBe(false) +// // Test for correct creation of second web3 name +// const encoded2 = await api.call.did.queryByWeb3Name('test-2') +// expect(Did.linkedInfoFromChain(encoded2).document.uri).toStrictEqual( +// fullDid.uri +// ) +// }, 30_000) + +// it('can batch extrinsics for different required key types', async () => { +// // Authentication key +// const web3NameReleaseExt = api.tx.web3Names.releaseByOwner() +// // Attestation key +// const ctype1 = CType.fromProperties(UUID.generate(), {}) +// const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) +// // Delegation key +// const rootNode = DelegationNode.newRoot({ +// account: fullDid.uri, +// permissions: [Permission.DELEGATE], +// cTypeHash: CType.idToHash(ctype1.$id), +// }) +// const delegationHierarchyCreation = await rootNode.getStoreTx() + +// // Authentication key +// const web3NameNewClaimExt = api.tx.web3Names.claim('test-2') +// // Attestation key +// const ctype2 = CType.fromProperties(UUID.generate(), {}) +// const ctype2Creation = api.tx.ctype.add(CType.toChain(ctype2)) +// // Delegation key +// const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.uri) + +// const batchedExtrinsics = await Did.authorizeBatch({ +// batchFunction: api.tx.utility.batchAll, +// did: fullDid.uri, +// extrinsics: [ +// web3NameReleaseExt, +// ctype1Creation, +// delegationHierarchyCreation, +// web3NameNewClaimExt, +// ctype2Creation, +// delegationHierarchyRemoval, +// ], +// sign: key.getSignCallback(fullDid), +// submitter: paymentAccount.address, +// }) + +// await submitTx(batchedExtrinsics, paymentAccount) + +// // Test correct use of authentication keys +// const encoded = await api.call.did.queryByWeb3Name('test') +// expect(encoded.isSome).toBe(false) + +// const { +// document: { uri }, +// } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name('test-2')) +// expect(uri).toStrictEqual(fullDid.uri) + +// // Test correct use of attestation keys +// await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() +// await expect(CType.verifyStored(ctype2)).resolves.not.toThrow() + +// // Test correct use of delegation keys +// const node = await DelegationNode.fetch(rootNode.id) +// expect(node.revoked).toBe(true) +// }) +// }) + +// describe('Runtime constraints', () => { +// let testAuthKey: NewDidVerificationKey +// const { keypair, storeDidCallback } = makeSigningKeyTool('ed25519') + +// beforeAll(async () => { +// testAuthKey = { +// publicKey: keypair.publicKey, +// type: 'ed25519', +// } +// }) +// describe('DID creation', () => { +// it('should not be possible to create a DID with too many encryption keys', async () => { +// // Maximum is 10 +// const newKeyAgreementKeys = Array(10).map( +// (_, index): NewDidEncryptionKey => ({ +// publicKey: Uint8Array.from(new Array(32).fill(index)), +// type: 'x25519', +// }) +// ) +// await Did.getStoreTx( +// { +// authentication: [testAuthKey], +// keyAgreement: newKeyAgreementKeys, +// }, +// paymentAccount.address, +// storeDidCallback +// ) +// // One more than the maximum +// newKeyAgreementKeys.push({ +// publicKey: Uint8Array.from(new Array(32).fill(100)), +// type: 'x25519', +// }) +// await expect( +// Did.getStoreTx( +// { +// authentication: [testAuthKey], +// keyAgreement: newKeyAgreementKeys, +// }, + +// paymentAccount.address, +// storeDidCallback +// ) +// ).rejects.toThrowErrorMatchingInlineSnapshot( +// `"The number of key agreement keys in the creation operation is greater than the maximum allowed, which is 10"` +// ) +// }, 30_000) + +// it('should not be possible to create a DID with too many service endpoints', async () => { +// // Maximum is 25 +// const newServiceEndpoints = Array(25).map( +// (_, index): DidServiceEndpoint => ({ +// id: `#service-${index}`, +// type: [`type-${index}`], +// serviceEndpoint: [`x:url-${index}`], +// }) +// ) +// await Did.getStoreTx( +// { +// authentication: [testAuthKey], +// service: newServiceEndpoints, +// }, +// paymentAccount.address, +// storeDidCallback +// ) +// // One more than the maximum +// newServiceEndpoints.push({ +// id: '#service-100', +// type: ['type-100'], +// serviceEndpoint: ['x:url-100'], +// }) +// await expect( +// Did.getStoreTx( +// { +// authentication: [testAuthKey], +// service: newServiceEndpoints, +// }, + +// paymentAccount.address, +// storeDidCallback +// ) +// ).rejects.toThrowErrorMatchingInlineSnapshot( +// `"Cannot store more than 25 service endpoints per DID"` +// ) +// }, 30_000) + +// it('should not be possible to create a DID with a service endpoint that is too long', async () => { +// const serviceId = '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +// const limit = api.consts.did.maxServiceIdLength.toNumber() +// expect(serviceId.length).toBeGreaterThan(limit) +// }) + +// it('should not be possible to create a DID with a service endpoint that has too many types', async () => { +// const types = ['type-1', 'type-2'] +// const limit = api.consts.did.maxNumberOfTypesPerService.toNumber() +// expect(types.length).toBeGreaterThan(limit) +// }) + +// it('should not be possible to create a DID with a service endpoint that has too many URIs', async () => { +// const uris = ['x:url-1', 'x:url-2', 'x:url-3'] +// const limit = api.consts.did.maxNumberOfUrlsPerService.toNumber() +// expect(uris.length).toBeGreaterThan(limit) +// }) + +// it('should not be possible to create a DID with a service endpoint that has a type that is too long', async () => { +// const type = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +// const limit = api.consts.did.maxServiceTypeLength.toNumber() +// expect(type.length).toBeGreaterThan(limit) +// }) + +// it('should not be possible to create a DID with a service endpoint that has a URI that is too long', async () => { +// const uri = +// 'a:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +// const limit = api.consts.did.maxServiceUrlLength.toNumber() +// expect(uri.length).toBeGreaterThan(limit) +// }) +// }) +// }) + +afterAll(async () => { + await disconnect() +}) diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 3354e86051..64a7aae9b1 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -26,7 +26,6 @@ import * as Did from '@kiltprotocol/did' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' -import { getStoreTxFromDidDocument } from 'did/src/Did2.chain' export type EncryptionKeyToolCallback = ( didDocument: DidDocumentV2.DidDocument @@ -324,7 +323,7 @@ export async function createFullDidFromLightDid( fullDidDocumentToBeCreated.capabilityDelegation = [ fullDidDocumentToBeCreated.authentication[0], ] - const tx = await getStoreTxFromDidDocument( + const tx = await Did.DidChainV2.getStoreTxFromDidDocument( fullDidDocumentToBeCreated, payer.address, sign diff --git a/tests/testUtils/index.ts b/tests/testUtils/index.ts index 093a7233d5..73ad2d773a 100644 --- a/tests/testUtils/index.ts +++ b/tests/testUtils/index.ts @@ -7,3 +7,5 @@ export * as ApiMocks from './mocks/index.js' export * from './TestUtils.js' + +export * as TestUtilsV2 from './TestUtils2.js' From 050a04323a01e0ca7124156522bf4dd24147688a Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 4 Oct 2023 10:29:45 +0100 Subject: [PATCH 16/78] More DID integration tests uncommented --- tests/integration/Did2.spec.ts | 178 +++++++++++++++++---------------- 1 file changed, 92 insertions(+), 86 deletions(-) diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts index fa8d19c55e..40fef53906 100644 --- a/tests/integration/Did2.spec.ts +++ b/tests/integration/Did2.spec.ts @@ -6,10 +6,12 @@ */ import type { ApiPromise } from '@polkadot/api' +import { BN } from '@polkadot/util' import { disconnect } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import { + CryptoCallbacksV2, DidDocumentV2, KiltKeyringPair, NewDidVerificationKey, @@ -37,10 +39,12 @@ beforeAll(async () => { describe.only('write and didDeleteTx', () => { let did: DidDocumentV2.DidDocument let key: TestUtilsV2.KeyTool + let signCallback: CryptoCallbacksV2.SignCallback beforeAll(async () => { key = TestUtilsV2.makeSigningKeyTool() did = await TestUtilsV2.createMinimalLightDidFromKeypair(key.keypair) + signCallback = key.getSignCallback(did) }) it('fails to create a new DID on chain with a different submitter than the one in the creation operation', async () => { @@ -123,92 +127,94 @@ describe.only('write and didDeleteTx', () => { }) }, 60_000) - // it('should return no results for empty accounts', async () => { - // const emptyDid = Did.getFullDidUriFromKey( - // makeSigningKeyTool().authentication[0] - // ) - - // const encodedDid = Did.toChain(emptyDid) - // expect((await api.call.did.query(encodedDid)).isSome).toBe(false) - // }) - - // it('fails to delete the DID using a different submitter than the one specified in the DID operation or using a services count that is too low', async () => { - // // We verify that the DID to delete is on chain. - // const fullDidLinkedInfo = await api.call.did.query( - // Did.toChain(Did.getFullDidUri(did.id)) - // ) - // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - // expect(fullDid).not.toBeNull() - - // const otherAccount = devBob - - // // 10 is an example value. It is not used here since we are testing another error - // let call = api.tx.did.delete(new BN(10)) - - // let submittable = await Did.authorizeTx( - // fullDid.uri, - // call, - // signCallback, - // // Use a different account than the submitter one - // otherAccount.address - // ) - - // await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ - // section: 'did', - // name: 'BadDidOrigin', - // }) - - // // We use 1 here and this should fail as there are two service endpoints stored. - // call = api.tx.did.delete(new BN(1)) - - // submittable = await Did.authorizeTx( - // fullDid.uri, - // call, - // signCallback, - // paymentAccount.address - // ) - - // // Will fail because count provided is too low - // await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ - // section: 'did', - // name: expect.stringMatching( - // /^(StoredEndpointsCountTooLarge|MaxStoredEndpointsCountExceeded)$/ - // ), - // }) - // }, 60_000) - - // it('deletes DID from previous step', async () => { - // // We verify that the DID to delete is on chain. - // const fullDidLinkedInfo = await api.call.did.query( - // Did.toChain(Did.getFullDidUri(did.uri)) - // ) - // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - // expect(fullDid).not.toBeNull() - - // const encodedDid = Did.toChain(fullDid.uri) - // const linkedInfo = Did.linkedInfoFromChain( - // await api.call.did.query(encodedDid) - // ) - // const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 - // const call = api.tx.did.delete(storedEndpointsCount) - - // const submittable = await Did.authorizeTx( - // fullDid.uri, - // call, - // signCallback, - // paymentAccount.address - // ) - - // // Check that DID is not blacklisted. - // expect((await api.query.did.didBlacklist(encodedDid)).isNone).toBe(true) - - // await submitTx(submittable, paymentAccount) - - // expect((await api.call.did.query(encodedDid)).isNone).toBe(true) - - // // Check that DID is now blacklisted. - // expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) - // }, 60_000) + it('should return no results for empty accounts', async () => { + const emptyDid = Did.DidUtilsV2.getFullDidUri( + TestUtilsV2.makeSigningKeyTool().keypair.address + ) + + const encodedDid = Did.DidChainV2.toChain(emptyDid) + expect((await api.call.did.query(encodedDid)).isSome).toBe(false) + }) + + it('fails to delete the DID using a different submitter than the one specified in the DID operation or using a services count that is too low', async () => { + // We verify that the DID to delete is on chain. + const fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(did.id)) + ) + const { document: fullDid } = + Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + expect(fullDid).not.toBeNull() + + const otherAccount = devBob + + // 10 is an example value. It is not used here since we are testing another error + let call = api.tx.did.delete(new BN(10)) + + let submittable = await Did.DidDetailsV2.authorizeTx( + fullDid.id, + call, + signCallback, + // Use a different account than the submitter one + otherAccount.address + ) + + await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ + section: 'did', + name: 'BadDidOrigin', + }) + + // We use 1 here and this should fail as there are two service endpoints stored. + call = api.tx.did.delete(new BN(1)) + + submittable = await Did.DidDetailsV2.authorizeTx( + fullDid.id, + call, + signCallback, + paymentAccount.address + ) + + // Will fail because count provided is too low + await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ + section: 'did', + name: expect.stringMatching( + /^(StoredEndpointsCountTooLarge|MaxStoredEndpointsCountExceeded)$/ + ), + }) + }, 60_000) + + it('deletes DID from previous step', async () => { + // We verify that the DID to delete is on chain. + const fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(Did.getFullDidUri(did.id)) + ) + const { document: fullDid } = + Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + expect(fullDid).not.toBeNull() + + const encodedDid = Did.toChain(fullDid.id) + const linkedInfo = Did.DidRpc2.linkedInfoFromChain( + await api.call.did.query(encodedDid) + ) + const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 + const call = api.tx.did.delete(storedEndpointsCount) + + const submittable = await Did.DidDetailsV2.authorizeTx( + fullDid.id, + call, + signCallback, + paymentAccount.address + ) + + // Check that DID is not blacklisted. + expect((await api.query.did.didBlacklist(encodedDid)).isNone).toBe(true) + + await submitTx(submittable, paymentAccount) + + expect((await api.call.did.query(encodedDid)).isNone).toBe(true) + + // Check that DID is now blacklisted. + expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) + }, 60_000) }) // it('creates and updates DID, and then reclaims the deposit back', async () => { From 40361ec2fe37eae11ff2924cab49a759ab6d0847 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 4 Oct 2023 11:43:25 +0100 Subject: [PATCH 17/78] More DID integration tests passing --- packages/did/src/Did2.utils.ts | 7 +- tests/integration/Did2.spec.ts | 534 +++++++++++++++++---------------- 2 files changed, 284 insertions(+), 257 deletions(-) diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index fc84c212a7..c11bf02f56 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -150,10 +150,9 @@ export function multibaseKeyToDidKey( export function keypairToMultibaseKey({ type, publicKey, -}: Pick< - KeyringPair, - 'publicKey' | 'type' ->): DidDocumentV2.VerificationMethod['publicKeyMultibase'] { +}: Pick & { + type: DidKeyType +}): DidDocumentV2.VerificationMethod['publicKeyMultibase'] { const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] if (multiCodecPublicKeyPrefix === undefined) { // TODO: Proper error diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts index 40fef53906..6624222138 100644 --- a/tests/integration/Did2.spec.ts +++ b/tests/integration/Did2.spec.ts @@ -13,6 +13,7 @@ import * as Did from '@kiltprotocol/did' import { CryptoCallbacksV2, DidDocumentV2, + DidResolverV2, KiltKeyringPair, NewDidVerificationKey, } from '@kiltprotocol/types' @@ -36,7 +37,7 @@ beforeAll(async () => { paymentAccount = await createEndowedTestAccount() }, 30_000) -describe.only('write and didDeleteTx', () => { +describe('write and didDeleteTx', () => { let did: DidDocumentV2.DidDocument let key: TestUtilsV2.KeyTool let signCallback: CryptoCallbacksV2.SignCallback @@ -60,7 +61,7 @@ describe.only('write and didDeleteTx', () => { }) }, 60_000) - it.only('writes a new DID record to chain', async () => { + it('writes a new DID record to chain', async () => { const { publicKeyMultibase } = did.verificationMethod.find( (vm) => vm.id === did.authentication[0] ) as DidDocumentV2.VerificationMethod @@ -185,13 +186,13 @@ describe.only('write and didDeleteTx', () => { it('deletes DID from previous step', async () => { // We verify that the DID to delete is on chain. const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(Did.getFullDidUri(did.id)) + Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(did.id)) ) const { document: fullDid } = Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - const encodedDid = Did.toChain(fullDid.id) + const encodedDid = Did.DidChainV2.toChain(fullDid.id) const linkedInfo = Did.DidRpc2.linkedInfoFromChain( await api.call.did.query(encodedDid) ) @@ -217,277 +218,304 @@ describe.only('write and didDeleteTx', () => { }, 60_000) }) -// it('creates and updates DID, and then reclaims the deposit back', async () => { -// const { keypair, getSignCallback, storeDidCallback } = makeSigningKeyTool() -// const newDid = await createMinimalLightDidFromKeypair(keypair) - -// const tx = await Did.getStoreTx( -// newDid, -// paymentAccount.address, -// storeDidCallback -// ) - -// await submitTx(tx, paymentAccount) - -// // This will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. -// let fullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUri(newDid.uri)) -// ) -// let { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - -// const newKey = makeSigningKeyTool() - -// const updateAuthenticationKeyCall = api.tx.did.setAuthenticationKey( -// Did.publicKeyToChain(newKey.authentication[0]) -// ) -// const tx2 = await Did.authorizeTx( -// fullDid.uri, -// updateAuthenticationKeyCall, -// getSignCallback(fullDid), -// paymentAccount.address -// ) -// await submitTx(tx2, paymentAccount) - -// // Authentication key changed, so did must be updated. -// // Also this will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. -// fullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUri(newDid.uri)) -// ) -// fullDid = Did.linkedInfoFromChain(fullDidLinkedInfo).document - -// // Add a new service endpoint -// const newEndpoint: DidServiceEndpoint = { -// id: '#new-endpoint', -// type: ['new-type'], -// serviceEndpoint: ['x:new-url'], -// } -// const updateEndpointCall = api.tx.did.addServiceEndpoint( -// Did.serviceToChain(newEndpoint) -// ) - -// const tx3 = await Did.authorizeTx( -// fullDid.uri, -// updateEndpointCall, -// newKey.getSignCallback(fullDid), -// paymentAccount.address -// ) -// await submitTx(tx3, paymentAccount) - -// const encodedDid = Did.toChain(fullDid.uri) -// const linkedInfo = Did.linkedInfoFromChain( -// await api.call.did.query(encodedDid) -// ) -// expect(Did.getService(linkedInfo.document, newEndpoint.id)).toStrictEqual( -// newEndpoint -// ) - -// // Delete the added service endpoint -// const removeEndpointCall = api.tx.did.removeServiceEndpoint( -// Did.resourceIdToChain(newEndpoint.id) -// ) -// const tx4 = await Did.authorizeTx( -// fullDid.uri, -// removeEndpointCall, -// newKey.getSignCallback(fullDid), -// paymentAccount.address -// ) -// await submitTx(tx4, paymentAccount) - -// // There should not be any endpoint with the given ID now. -// const linkedInfo2 = Did.linkedInfoFromChain( -// await api.call.did.query(encodedDid) -// ) -// expect(Did.getService(linkedInfo2.document, newEndpoint.id)).toBe(undefined) - -// // Claim the deposit back -// const storedEndpointsCount = linkedInfo2.document.service?.length ?? 0 -// const reclaimDepositTx = api.tx.did.reclaimDeposit( -// encodedDid, -// storedEndpointsCount -// ) -// await submitTx(reclaimDepositTx, paymentAccount) -// // Verify that the DID has been deleted -// expect((await api.call.did.query(encodedDid)).isNone).toBe(true) -// }, 80_000) - -// describe('DID migration', () => { -// it('migrates light DID with ed25519 auth key and encryption key', async () => { -// const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') -// const { keyAgreement } = makeEncryptionKeyTool( -// '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' -// ) -// const lightDid = Did.createLightDidDocument({ -// authentication, -// keyAgreement, -// }) +it('creates and updates DID, and then reclaims the deposit back', async () => { + const { keypair, getSignCallback, storeDidCallback } = + TestUtilsV2.makeSigningKeyTool() + const newDid = await TestUtilsV2.createMinimalLightDidFromKeypair(keypair) + + const tx = await Did.DidChainV2.getStoreTxFromDidDocument( + newDid, + paymentAccount.address, + storeDidCallback + ) + + await submitTx(tx, paymentAccount) + + // This will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. + let fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(newDid.id)) + ) + let { document: fullDid } = Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + + const newKey = TestUtilsV2.makeSigningKeyTool() + + const updateAuthenticationKeyCall = api.tx.did.setAuthenticationKey( + Did.DidChainV2.publicKeyToChain(newKey.authentication[0]) + ) + const tx2 = await Did.DidDetailsV2.authorizeTx( + fullDid.id, + updateAuthenticationKeyCall, + getSignCallback(fullDid), + paymentAccount.address + ) + await submitTx(tx2, paymentAccount) + + // Authentication key changed, so did must be updated. + // Also this will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. + fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(newDid.id)) + ) + fullDid = Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo).document + + // Add a new service endpoint + const newEndpoint: Did.DidDetailsV2.NewService = { + id: '#new-endpoint', + type: ['new-type'], + serviceEndpoint: ['x:new-url'], + } + const updateEndpointCall = api.tx.did.addServiceEndpoint( + Did.DidChainV2.serviceToChain(newEndpoint) + ) + + const tx3 = await Did.DidDetailsV2.authorizeTx( + fullDid.id, + updateEndpointCall, + newKey.getSignCallback(fullDid), + paymentAccount.address + ) + await submitTx(tx3, paymentAccount) + + const encodedDid = Did.DidChainV2.toChain(fullDid.id) + const linkedInfo = Did.DidRpc2.linkedInfoFromChain( + await api.call.did.query(encodedDid) + ) + expect( + linkedInfo.document.service?.find((s) => s.id === newEndpoint.id) + ).toStrictEqual(newEndpoint) + + // Delete the added service endpoint + const removeEndpointCall = api.tx.did.removeServiceEndpoint( + Did.DidChainV2.fragmentIdToChain(newEndpoint.id) + ) + const tx4 = await Did.DidDetailsV2.authorizeTx( + fullDid.id, + removeEndpointCall, + newKey.getSignCallback(fullDid), + paymentAccount.address + ) + await submitTx(tx4, paymentAccount) + + // There should not be any endpoint with the given ID now. + const linkedInfo2 = Did.DidRpc2.linkedInfoFromChain( + await api.call.did.query(encodedDid) + ) + expect( + linkedInfo2.document.service?.find((s) => s.id === newEndpoint.id) + ).toBe(undefined) + + // Claim the deposit back + const storedEndpointsCount = linkedInfo2.document.service?.length ?? 0 + const reclaimDepositTx = api.tx.did.reclaimDeposit( + encodedDid, + storedEndpointsCount + ) + await submitTx(reclaimDepositTx, paymentAccount) + // Verify that the DID has been deleted + expect((await api.call.did.query(encodedDid)).isNone).toBe(true) +}, 80_000) + +describe('DID migration', () => { + it('migrates light DID with ed25519 auth key and encryption key', async () => { + const { storeDidCallback, authentication } = + TestUtilsV2.makeSigningKeyTool('ed25519') + const { keyAgreement } = TestUtilsV2.makeEncryptionKeyTool( + '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + ) + const lightDid = Did.DidDetailsV2.createLightDidDocument({ + authentication, + keyAgreement, + }) -// const storeTx = await Did.getStoreTx( -// lightDid, -// paymentAccount.address, -// storeDidCallback -// ) + const storeTx = await Did.DidChainV2.getStoreTxFromDidDocument( + lightDid, + paymentAccount.address, + storeDidCallback + ) -// await submitTx(storeTx, paymentAccount) -// const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) -// const migratedFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(migratedFullDidUri) -// ) -// const { document: migratedFullDid } = Did.linkedInfoFromChain( -// migratedFullDidLinkedInfo -// ) + await submitTx(storeTx, paymentAccount) + const migratedFullDidUri = Did.DidUtilsV2.getFullDidUri(lightDid.id) + const migratedFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(migratedFullDidUri) + ) + const { document: migratedFullDid } = Did.DidRpc2.linkedInfoFromChain( + migratedFullDidLinkedInfo + ) -// expect(migratedFullDid).toMatchObject({ -// uri: migratedFullDidUri, -// authentication: [ -// expect.objectContaining({ -// publicKey: lightDid.authentication[0].publicKey, -// type: 'ed25519', -// }), -// ], -// keyAgreement: [ -// expect.objectContaining({ -// publicKey: lightDid.keyAgreement?.[0].publicKey, -// type: 'x25519', -// }), -// ], -// }) + expect(migratedFullDid).toMatchObject(>{ + id: migratedFullDidUri, + verificationMethod: [ + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: authentication[0].publicKey, + }), + }), + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'x25519', + publicKey: keyAgreement[0].publicKey, + }), + }), + ], + }) -// expect( -// (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome -// ).toBe(true) + expect( + (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))).isSome + ).toBe(true) -// const { metadata } = (await Did.resolve( -// lightDid.uri -// )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( + lightDid.id + )) as DidResolverV2.ResolutionResult -// expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) -// expect(metadata.deactivated).toBe(false) -// }) + expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.deactivated).toBe(undefined) + }) -// it('migrates light DID with sr25519 auth key', async () => { -// const { authentication, storeDidCallback } = makeSigningKeyTool() -// const lightDid = Did.createLightDidDocument({ -// authentication, -// }) + it('migrates light DID with sr25519 auth key', async () => { + const { authentication, storeDidCallback } = + TestUtilsV2.makeSigningKeyTool() + const lightDid = Did.DidDetailsV2.createLightDidDocument({ + authentication, + }) -// const storeTx = await Did.getStoreTx( -// lightDid, -// paymentAccount.address, -// storeDidCallback -// ) + const storeTx = await Did.DidChainV2.getStoreTxFromDidDocument( + lightDid, + paymentAccount.address, + storeDidCallback + ) -// await submitTx(storeTx, paymentAccount) -// const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) -// const migratedFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(migratedFullDidUri) -// ) -// const { document: migratedFullDid } = Did.linkedInfoFromChain( -// migratedFullDidLinkedInfo -// ) + await submitTx(storeTx, paymentAccount) + const migratedFullDidUri = Did.DidUtilsV2.getFullDidUri(lightDid.id) + const migratedFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(migratedFullDidUri) + ) + const { document: migratedFullDid } = Did.DidRpc2.linkedInfoFromChain( + migratedFullDidLinkedInfo + ) -// expect(migratedFullDid).toMatchObject({ -// uri: migratedFullDidUri, -// authentication: [ -// expect.objectContaining({ -// publicKey: lightDid.authentication[0].publicKey, -// type: 'sr25519', -// }), -// ], -// }) + expect(migratedFullDid).toMatchObject(>{ + id: migratedFullDidUri, + verificationMethod: [ + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: authentication[0].publicKey, + }), + }), + ], + }) -// expect( -// (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome -// ).toBe(true) + expect( + (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))).isSome + ).toBe(true) -// const { metadata } = (await Did.resolve( -// lightDid.uri -// )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( + lightDid.id + )) as DidResolverV2.ResolutionResult -// expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) -// expect(metadata.deactivated).toBe(false) -// }) + expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.deactivated).toBe(undefined) + }) -// it('migrates light DID with ed25519 auth key, encryption key, and service endpoints', async () => { -// const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') -// const { keyAgreement } = makeEncryptionKeyTool( -// '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' -// ) -// const service: DidServiceEndpoint[] = [ -// { -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }, -// ] -// const lightDid = Did.createLightDidDocument({ -// authentication, -// keyAgreement, -// service, -// }) + it('migrates light DID with ed25519 auth key, encryption key, and service endpoints', async () => { + const { storeDidCallback, authentication } = + TestUtilsV2.makeSigningKeyTool('ed25519') + const { keyAgreement } = TestUtilsV2.makeEncryptionKeyTool( + '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' + ) + const service: Did.DidDetailsV2.NewService[] = [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + ] + const lightDid = Did.DidDetailsV2.createLightDidDocument({ + authentication, + keyAgreement, + service, + }) -// const storeTx = await Did.getStoreTx( -// lightDid, -// paymentAccount.address, -// storeDidCallback -// ) + const storeTx = await Did.DidChainV2.getStoreTxFromDidDocument( + lightDid, + paymentAccount.address, + storeDidCallback + ) -// await submitTx(storeTx, paymentAccount) -// const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) -// const migratedFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(migratedFullDidUri) -// ) -// const { document: migratedFullDid } = Did.linkedInfoFromChain( -// migratedFullDidLinkedInfo -// ) + await submitTx(storeTx, paymentAccount) + const migratedFullDidUri = Did.DidUtilsV2.getFullDidUri(lightDid.id) + const migratedFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(migratedFullDidUri) + ) + const { document: migratedFullDid } = Did.DidRpc2.linkedInfoFromChain( + migratedFullDidLinkedInfo + ) -// expect(migratedFullDid).toMatchObject({ -// uri: migratedFullDidUri, -// authentication: [ -// expect.objectContaining({ -// publicKey: lightDid.authentication[0].publicKey, -// type: 'ed25519', -// }), -// ], -// keyAgreement: [ -// expect.objectContaining({ -// publicKey: lightDid.keyAgreement?.[0].publicKey, -// type: 'x25519', -// }), -// ], -// service: [ -// { -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }, -// ], -// }) + expect(migratedFullDid).toMatchObject(>{ + id: migratedFullDidUri, + verificationMethod: [ + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: authentication[0].publicKey, + }), + }), + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'x25519', + publicKey: keyAgreement[0].publicKey, + }), + }), + ], + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + ], + }) -// const encodedDid = Did.toChain(migratedFullDid.uri) -// expect((await api.call.did.query(encodedDid)).isSome).toBe(true) + const encodedDid = Did.DidChainV2.toChain(migratedFullDid.id) + expect((await api.call.did.query(encodedDid)).isSome).toBe(true) -// const { metadata } = (await Did.resolve( -// lightDid.uri -// )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( + lightDid.id + )) as DidResolverV2.ResolutionResult -// expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) -// expect(metadata.deactivated).toBe(false) + expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.deactivated).toBe(undefined) -// // Remove and claim the deposit back -// const linkedInfo = Did.linkedInfoFromChain( -// await api.call.did.query(encodedDid) -// ) -// const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 -// const reclaimDepositTx = api.tx.did.reclaimDeposit( -// encodedDid, -// storedEndpointsCount -// ) -// await submitTx(reclaimDepositTx, paymentAccount) + // Remove and claim the deposit back + const linkedInfo = Did.DidRpc2.linkedInfoFromChain( + await api.call.did.query(encodedDid) + ) + const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 + const reclaimDepositTx = api.tx.did.reclaimDeposit( + encodedDid, + storedEndpointsCount + ) + await submitTx(reclaimDepositTx, paymentAccount) -// expect((await api.call.did.query(encodedDid)).isNone).toBe(true) -// expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) -// }, 60_000) -// }) + expect((await api.call.did.query(encodedDid)).isNone).toBe(true) + expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) + }, 60_000) +}) // describe('DID authorization', () => { // // Light DIDs cannot authorize extrinsics From b88def811ac794a97bd29530b593501a759e2d63 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 4 Oct 2023 17:00:08 +0100 Subject: [PATCH 18/78] More DID integration tests --- packages/did/src/Did2.rpc.ts | 21 +- tests/integration/Did2.spec.ts | 1204 +++++++++++++++++--------------- 2 files changed, 662 insertions(+), 563 deletions(-) diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index 3ece1ca6a0..8c3921ca0b 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -161,18 +161,17 @@ export function linkedInfoFromChain( ], } - if (didRec.keyAgreement !== undefined) { - did.keyAgreement = [didRec.keyAgreement[0].id] - did.verificationMethod.push( - didKeyToVerificationMethod( - fromChain(identifier), - didRec.keyAgreement[0].id, - { - keyType: didRec.keyAgreement[0].type, - publicKey: didRec.keyAgreement[0].publicKey, - } + if (didRec.keyAgreement !== undefined && didRec.keyAgreement.length > 0) { + did.keyAgreement = [] + didRec.keyAgreement.forEach(({ id, publicKey, type: keyType }) => { + did.keyAgreement?.push(id) + did.verificationMethod.push( + didKeyToVerificationMethod(fromChain(identifier), id, { + keyType, + publicKey, + }) ) - ) + }) } if (didRec.assertionMethod !== undefined) { diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts index 6624222138..a13d22a608 100644 --- a/tests/integration/Did2.spec.ts +++ b/tests/integration/Did2.spec.ts @@ -8,14 +8,14 @@ import type { ApiPromise } from '@polkadot/api' import { BN } from '@polkadot/util' -import { disconnect } from '@kiltprotocol/core' +import { CType, disconnect } from '@kiltprotocol/core' +import { UUID } from '@kiltprotocol/utils' import * as Did from '@kiltprotocol/did' import { CryptoCallbacksV2, DidDocumentV2, DidResolverV2, KiltKeyringPair, - NewDidVerificationKey, } from '@kiltprotocol/types' import { TestUtilsV2 } from '../testUtils/index.js' @@ -69,7 +69,7 @@ describe('write and didDeleteTx', () => { Did.DidUtilsV2.multibaseKeyToDidKey(publicKeyMultibase) const newDid = Did.DidDetailsV2.createLightDidDocument({ authentication: [{ publicKey: authPublicKey, type: keyType }] as [ - NewDidVerificationKey + Did.DidDetailsV2.NewDidVerificationKey ], service: [ { @@ -126,6 +126,10 @@ describe('write and didDeleteTx', () => { }), ], }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.keyAgreement).toBe(undefined) + expect(fullDid.assertionMethod).toBe(undefined) + expect(fullDid.capabilityDelegation).toBe(undefined) }, 60_000) it('should return no results for empty accounts', async () => { @@ -348,25 +352,28 @@ describe('DID migration', () => { controller: migratedFullDidUri, type: 'MultiKey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'ed25519', - publicKey: authentication[0].publicKey, - }), + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), }), expect.objectContaining(>{ controller: migratedFullDidUri, type: 'MultiKey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'x25519', - publicKey: keyAgreement[0].publicKey, - }), + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + keyAgreement[0] + ), }), ], }) + expect(migratedFullDid.authentication).toHaveLength(1) + expect(migratedFullDid.keyAgreement).toHaveLength(1) + expect(migratedFullDid.assertionMethod).toBe(undefined) + expect(migratedFullDid.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))).isSome + (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))) + .isSome ).toBe(true) const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( @@ -406,16 +413,20 @@ describe('DID migration', () => { controller: migratedFullDidUri, type: 'MultiKey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'sr25519', - publicKey: authentication[0].publicKey, - }), + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), }), ], }) + expect(migratedFullDid.authentication).toHaveLength(1) + expect(migratedFullDid.keyAgreement).toBe(undefined) + expect(migratedFullDid.assertionMethod).toBe(undefined) + expect(migratedFullDid.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))).isSome + (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))) + .isSome ).toBe(true) const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( @@ -467,19 +478,17 @@ describe('DID migration', () => { controller: migratedFullDidUri, type: 'MultiKey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'ed25519', - publicKey: authentication[0].publicKey, - }), + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), }), expect.objectContaining(>{ controller: migratedFullDidUri, type: 'MultiKey', // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'x25519', - publicKey: keyAgreement[0].publicKey, - }), + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + keyAgreement[0] + ), }), ], service: [ @@ -490,6 +499,10 @@ describe('DID migration', () => { }, ], }) + expect(migratedFullDid.authentication).toHaveLength(1) + expect(migratedFullDid.keyAgreement).toHaveLength(1) + expect(migratedFullDid.assertionMethod).toBe(undefined) + expect(migratedFullDid.capabilityDelegation).toBe(undefined) const encodedDid = Did.DidChainV2.toChain(migratedFullDid.id) expect((await api.call.did.query(encodedDid)).isSome).toBe(true) @@ -517,540 +530,624 @@ describe('DID migration', () => { }, 60_000) }) -// describe('DID authorization', () => { -// // Light DIDs cannot authorize extrinsics -// let did: DidDocument -// const { getSignCallback, storeDidCallback, authentication } = -// makeSigningKeyTool('ed25519') - -// beforeAll(async () => { -// const createTx = await Did.getStoreTx( -// { -// authentication, -// assertionMethod: authentication, -// capabilityDelegation: authentication, -// }, -// paymentAccount.address, -// storeDidCallback -// ) -// await submitTx(createTx, paymentAccount) -// const didLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) -// ) -// did = Did.linkedInfoFromChain(didLinkedInfo).document -// }, 60_000) - -// it('authorizes ctype creation with DID signature', async () => { -// const cType = CType.fromProperties(UUID.generate(), {}) -// const call = api.tx.ctype.add(CType.toChain(cType)) -// const tx = await Did.authorizeTx( -// did.uri, -// call, -// getSignCallback(did), -// paymentAccount.address -// ) -// await submitTx(tx, paymentAccount) - -// await expect(CType.verifyStored(cType)).resolves.not.toThrow() -// }, 60_000) - -// it('no longer authorizes ctype creation after DID deletion', async () => { -// const linkedInfo = Did.linkedInfoFromChain( -// await api.call.did.query(Did.toChain(did.uri)) -// ) -// const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 -// const deleteCall = api.tx.did.delete(storedEndpointsCount) -// const tx = await Did.authorizeTx( -// did.uri, -// deleteCall, -// getSignCallback(did), -// paymentAccount.address -// ) -// await submitTx(tx, paymentAccount) - -// const cType = CType.fromProperties(UUID.generate(), {}) -// const call = api.tx.ctype.add(CType.toChain(cType)) -// const tx2 = await Did.authorizeTx( -// did.uri, -// call, -// getSignCallback(did), -// paymentAccount.address -// ) -// await expect(submitTx(tx2, paymentAccount)).rejects.toMatchObject({ -// section: 'did', -// name: expect.stringMatching(/^(DidNotPresent|NotFound)$/), -// }) - -// await expect(CType.verifyStored(cType)).rejects.toThrow() -// }, 60_000) -// }) - -// describe('DID management batching', () => { -// describe('FullDidCreationBuilder', () => { -// it('Build a complete full DID', async () => { -// const { keypair, storeDidCallback, authentication } = makeSigningKeyTool() -// const extrinsic = await Did.getStoreTx( -// { -// authentication, -// assertionMethod: [ -// { -// publicKey: new Uint8Array(32).fill(1), -// type: 'sr25519', -// }, -// ], -// capabilityDelegation: [ -// { -// publicKey: new Uint8Array(33).fill(1), -// type: 'ecdsa', -// }, -// ], -// keyAgreement: [ -// { -// publicKey: new Uint8Array(32).fill(1), -// type: 'x25519', -// }, -// { -// publicKey: new Uint8Array(32).fill(2), -// type: 'x25519', -// }, -// { -// publicKey: new Uint8Array(32).fill(3), -// type: 'x25519', -// }, -// ], -// service: [ -// { -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }, -// { -// id: '#id-2', -// type: ['type-2'], -// serviceEndpoint: ['x:url-2'], -// }, -// { -// id: '#id-3', -// type: ['type-3'], -// serviceEndpoint: ['x:url-3'], -// }, -// ], -// }, -// paymentAccount.address, -// storeDidCallback -// ) -// await submitTx(extrinsic, paymentAccount) -// const fullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) -// ) -// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - -// expect(fullDid).not.toBeNull() -// expect(fullDid).toMatchObject({ -// authentication: [ -// expect.objectContaining({ -// publicKey: keypair.publicKey, -// type: 'sr25519', -// }), -// ], -// assertionMethod: [ -// expect.objectContaining({ -// publicKey: new Uint8Array(32).fill(1), -// type: 'sr25519', -// }), -// ], -// capabilityDelegation: [ -// expect.objectContaining({ -// publicKey: new Uint8Array(33).fill(1), -// type: 'ecdsa', -// }), -// ], -// keyAgreement: [ -// expect.objectContaining({ -// publicKey: new Uint8Array(32).fill(3), -// type: 'x25519', -// }), -// expect.objectContaining({ -// publicKey: new Uint8Array(32).fill(2), -// type: 'x25519', -// }), -// expect.objectContaining({ -// publicKey: new Uint8Array(32).fill(1), -// type: 'x25519', -// }), -// ], -// service: [ -// { -// id: '#id-3', -// type: ['type-3'], -// serviceEndpoint: ['x:url-3'], -// }, -// { -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }, -// { -// id: '#id-2', -// type: ['type-2'], -// serviceEndpoint: ['x:url-2'], -// }, -// ], -// }) -// }) - -// it('Build a minimal full DID with an Ecdsa key', async () => { -// const { keypair, storeDidCallback } = makeSigningKeyTool('ecdsa') -// const didAuthKey: NewDidVerificationKey = { -// publicKey: keypair.publicKey, -// type: 'ecdsa', -// } - -// const extrinsic = await Did.getStoreTx( -// { authentication: [didAuthKey] }, -// paymentAccount.address, -// storeDidCallback -// ) -// await submitTx(extrinsic, paymentAccount) - -// const fullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUriFromKey(didAuthKey)) -// ) -// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - -// expect(fullDid).not.toBeNull() -// expect(fullDid?.authentication).toMatchObject([ -// { -// publicKey: keypair.publicKey, -// type: 'ecdsa', -// }, -// ]) -// }) -// }) - -// describe('FullDidUpdateBuilder', () => { -// it('Build from a complete full DID and remove everything but the authentication key', async () => { -// const { keypair, getSignCallback, storeDidCallback, authentication } = -// makeSigningKeyTool() - -// const createTx = await Did.getStoreTx( -// { -// authentication, -// keyAgreement: [ -// { -// publicKey: new Uint8Array(32).fill(1), -// type: 'x25519', -// }, -// { -// publicKey: new Uint8Array(32).fill(2), -// type: 'x25519', -// }, -// ], -// assertionMethod: [ -// { -// publicKey: new Uint8Array(32).fill(1), -// type: 'sr25519', -// }, -// ], -// capabilityDelegation: [ -// { -// publicKey: new Uint8Array(33).fill(1), -// type: 'ecdsa', -// }, -// ], -// service: [ -// { -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }, -// { -// id: '#id-2', -// type: ['type-2'], -// serviceEndpoint: ['x:url-2'], -// }, -// ], -// }, -// paymentAccount.address, -// storeDidCallback -// ) -// await submitTx(createTx, paymentAccount) - -// const initialFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) -// ) -// const { document: initialFullDid } = Did.linkedInfoFromChain( -// initialFullDidLinkedInfo -// ) - -// const encryptionKeys = initialFullDid.keyAgreement -// if (!encryptionKeys) throw new Error('No key agreement keys') - -// const extrinsic = await Did.authorizeBatch({ -// batchFunction: api.tx.utility.batchAll, -// did: initialFullDid.uri, -// extrinsics: [ -// api.tx.did.removeKeyAgreementKey( -// Did.resourceIdToChain(encryptionKeys[0].id) -// ), -// api.tx.did.removeKeyAgreementKey( -// Did.resourceIdToChain(encryptionKeys[1].id) -// ), -// api.tx.did.removeAttestationKey(), -// api.tx.did.removeDelegationKey(), -// api.tx.did.removeServiceEndpoint('id-1'), -// api.tx.did.removeServiceEndpoint('id-2'), -// ], -// sign: getSignCallback(initialFullDid), -// submitter: paymentAccount.address, -// }) -// await submitTx(extrinsic, paymentAccount) - -// const finalFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(initialFullDid.uri) -// ) -// const { document: finalFullDid } = Did.linkedInfoFromChain( -// finalFullDidLinkedInfo -// ) - -// expect(finalFullDid).not.toBeNull() - -// expect( -// finalFullDid.authentication[0] -// ).toMatchObject({ -// publicKey: keypair.publicKey, -// type: 'sr25519', -// }) - -// expect(finalFullDid.keyAgreement).toBeUndefined() -// expect(finalFullDid.assertionMethod).toBeUndefined() -// expect(finalFullDid.capabilityDelegation).toBeUndefined() -// expect(finalFullDid.service).toBeUndefined() -// }, 40_000) - -// it('Correctly handles rotation of the authentication key', async () => { -// const { authentication, getSignCallback, storeDidCallback } = -// makeSigningKeyTool() -// const { -// authentication: [newAuthKey], -// } = makeSigningKeyTool('ed25519') - -// const createTx = await Did.getStoreTx( -// { authentication }, -// paymentAccount.address, -// storeDidCallback -// ) -// await submitTx(createTx, paymentAccount) - -// const initialFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) -// ) -// const { document: initialFullDid } = Did.linkedInfoFromChain( -// initialFullDidLinkedInfo -// ) - -// const extrinsic = await Did.authorizeBatch({ -// batchFunction: api.tx.utility.batchAll, -// did: initialFullDid.uri, -// extrinsics: [ -// api.tx.did.addServiceEndpoint( -// Did.serviceToChain({ -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }) -// ), -// api.tx.did.setAuthenticationKey(Did.publicKeyToChain(newAuthKey)), -// api.tx.did.addServiceEndpoint( -// Did.serviceToChain({ -// id: '#id-2', -// type: ['type-2'], -// serviceEndpoint: ['x:url-2'], -// }) -// ), -// ], -// sign: getSignCallback(initialFullDid), -// submitter: paymentAccount.address, -// }) - -// await submitTx(extrinsic, paymentAccount) +describe('DID authorization', () => { + // Light DIDs cannot authorize extrinsics + let did: DidDocumentV2.DidDocument + const { getSignCallback, storeDidCallback, authentication } = + TestUtilsV2.makeSigningKeyTool('ed25519') -// const finalFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(initialFullDid.uri) -// ) -// const { document: finalFullDid } = Did.linkedInfoFromChain( -// finalFullDidLinkedInfo -// ) + beforeAll(async () => { + const createTx = await Did.DidChainV2.getStoreTxFromInput( + { + authentication, + assertionMethod: authentication, + capabilityDelegation: authentication, + }, + paymentAccount.address, + storeDidCallback + ) + await submitTx(createTx, paymentAccount) + const didLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain( + Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }) + ) + ) + did = Did.DidRpc2.linkedInfoFromChain(didLinkedInfo).document + }, 60_000) -// expect(finalFullDid).not.toBeNull() + it('authorizes ctype creation with DID signature', async () => { + const cType = CType.fromProperties(UUID.generate(), {}) + const call = api.tx.ctype.add(CType.toChain(cType)) + const tx = await Did.DidDetailsV2.authorizeTx( + did.id, + call, + getSignCallback(did), + paymentAccount.address + ) + await submitTx(tx, paymentAccount) -// expect(finalFullDid.authentication[0]).toMatchObject({ -// publicKey: newAuthKey.publicKey, -// type: newAuthKey.type, -// }) + await expect(CType.verifyStored(cType)).resolves.not.toThrow() + }, 60_000) -// expect(finalFullDid.keyAgreement).toBeUndefined() -// expect(finalFullDid.assertionMethod).toBeUndefined() -// expect(finalFullDid.capabilityDelegation).toBeUndefined() -// expect(finalFullDid.service).toHaveLength(2) -// }, 40_000) + it('no longer authorizes ctype creation after DID deletion', async () => { + const linkedInfo = Did.DidRpc2.linkedInfoFromChain( + await api.call.did.query(Did.toChain(did.id)) + ) + const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 + const deleteCall = api.tx.did.delete(storedEndpointsCount) + const tx = await Did.DidDetailsV2.authorizeTx( + did.id, + deleteCall, + getSignCallback(did), + paymentAccount.address + ) + await submitTx(tx, paymentAccount) -// it('simple `batch` succeeds despite failures of some extrinsics', async () => { -// const { authentication, getSignCallback, storeDidCallback } = -// makeSigningKeyTool() -// const tx = await Did.getStoreTx( -// { -// authentication, -// service: [ -// { -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }, -// ], -// }, -// paymentAccount.address, -// storeDidCallback -// ) -// // Create the full DID with a service endpoint -// await submitTx(tx, paymentAccount) -// const fullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) -// ) -// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - -// expect(fullDid.assertionMethod).toBeUndefined() - -// // Try to set a new attestation key and a duplicate service endpoint -// const updateTx = await Did.authorizeBatch({ -// batchFunction: api.tx.utility.batch, -// did: fullDid.uri, -// extrinsics: [ -// api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), -// api.tx.did.addServiceEndpoint( -// Did.serviceToChain({ -// id: '#id-1', -// type: ['type-2'], -// serviceEndpoint: ['x:url-2'], -// }) -// ), -// ], -// sign: getSignCallback(fullDid), -// submitter: paymentAccount.address, -// }) -// // Now the second operation fails but the batch succeeds -// await submitTx(updateTx, paymentAccount) + const cType = CType.fromProperties(UUID.generate(), {}) + const call = api.tx.ctype.add(CType.toChain(cType)) + const tx2 = await Did.DidDetailsV2.authorizeTx( + did.id, + call, + getSignCallback(did), + paymentAccount.address + ) + await expect(submitTx(tx2, paymentAccount)).rejects.toMatchObject({ + section: 'did', + name: expect.stringMatching(/^(DidNotPresent|NotFound)$/), + }) -// const updatedFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(fullDid.uri) -// ) -// const { document: updatedFullDid } = Did.linkedInfoFromChain( -// updatedFullDidLinkedInfo -// ) + await expect(CType.verifyStored(cType)).rejects.toThrow() + }, 60_000) +}) -// // .setAttestationKey() extrinsic went through in the batch -// expect(updatedFullDid.assertionMethod?.[0]).toBeDefined() -// // The service endpoint will match the one manually added, and not the one set in the batch -// expect( -// Did.getService(updatedFullDid, '#id-1') -// ).toStrictEqual({ -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }) -// }, 60_000) +describe('DID management batching', () => { + describe('FullDidCreationBuilder', () => { + it('Build a complete full DID', async () => { + const { storeDidCallback, authentication } = + TestUtilsV2.makeSigningKeyTool() + const extrinsic = await Did.DidChainV2.getStoreTxFromInput( + { + authentication, + assertionMethod: [ + { + publicKey: new Uint8Array(32).fill(1), + type: 'sr25519', + }, + ], + capabilityDelegation: [ + { + publicKey: new Uint8Array(33).fill(1), + type: 'ecdsa', + }, + ], + keyAgreement: [ + { + publicKey: new Uint8Array(32).fill(1), + type: 'x25519', + }, + { + publicKey: new Uint8Array(32).fill(2), + type: 'x25519', + }, + { + publicKey: new Uint8Array(32).fill(3), + type: 'x25519', + }, + ], + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#id-2', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }, + { + id: '#id-3', + type: ['type-3'], + serviceEndpoint: ['x:url-3'], + }, + ], + }, + paymentAccount.address, + storeDidCallback + ) + await submitTx(extrinsic, paymentAccount) + const fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain( + Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }) + ) + ) + const { document: fullDid } = + Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + + expect(fullDid).not.toBeNull() + expect(fullDid.verificationMethod).toEqual< + Partial + >( + expect.arrayContaining([ + expect.objectContaining({ + // Authentication + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }), + // Assertion method + expect.objectContaining({ + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: new Uint8Array(32).fill(1), + }), + }), + // Capability delegation + expect.objectContaining({ + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'ecdsa', + publicKey: new Uint8Array(33).fill(1), + }), + }), + // Key agreement 1 + expect.objectContaining({ + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(1), + }), + }), + // Key agreement 2 + expect.objectContaining({ + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(2), + }), + }), + // Key agreement 3 + expect.objectContaining({ + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(3), + }), + }), + ]) + ) + expect(fullDid).toMatchObject(>{ + service: [ + { + id: '#id-3', + type: ['type-3'], + serviceEndpoint: ['x:url-3'], + }, + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#id-2', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }, + ], + }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.assertionMethod).toHaveLength(1) + expect(fullDid.capabilityDelegation).toHaveLength(1) + expect(fullDid.keyAgreement).toHaveLength(3) + }) -// it('batchAll fails if any extrinsics fails', async () => { -// const { authentication, getSignCallback, storeDidCallback } = -// makeSigningKeyTool() -// const createTx = await Did.getStoreTx( -// { -// authentication, -// service: [ -// { -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }, -// ], -// }, -// paymentAccount.address, -// storeDidCallback -// ) -// await submitTx(createTx, paymentAccount) -// const fullDidLinkedInfo = await api.call.did.query( -// Did.toChain(Did.getFullDidUriFromKey(authentication[0])) -// ) -// const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - -// expect(fullDid.assertionMethod).toBeUndefined() - -// // Use batchAll to set a new attestation key and a duplicate service endpoint -// const updateTx = await Did.authorizeBatch({ -// batchFunction: api.tx.utility.batchAll, -// did: fullDid.uri, -// extrinsics: [ -// api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), -// api.tx.did.addServiceEndpoint( -// Did.serviceToChain({ -// id: '#id-1', -// type: ['type-2'], -// serviceEndpoint: ['x:url-2'], -// }) -// ), -// ], -// sign: getSignCallback(fullDid), -// submitter: paymentAccount.address, -// }) + it('Build a minimal full DID with an Ecdsa key', async () => { + const { keypair, storeDidCallback } = + TestUtilsV2.makeSigningKeyTool('ecdsa') + const didAuthKey: Did.DidDetailsV2.NewDidVerificationKey = { + publicKey: keypair.publicKey, + type: 'ecdsa', + } + + const extrinsic = await Did.DidChainV2.getStoreTxFromInput( + { authentication: [didAuthKey] }, + paymentAccount.address, + storeDidCallback + ) + await submitTx(extrinsic, paymentAccount) + + const fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain( + Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: + Did.DidUtilsV2.keypairToMultibaseKey(didAuthKey), + }) + ) + ) + const { document: fullDid } = + Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + + expect(fullDid).not.toBeNull() + expect(fullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: + Did.DidUtilsV2.keypairToMultibaseKey(didAuthKey), + }), + ], + }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.assertionMethod).toBe(undefined) + expect(fullDid.capabilityDelegation).toBe(undefined) + expect(fullDid.keyAgreement).toBe(undefined) + }) + }) -// // Now, submitting will result in the second operation to fail AND the batch to fail, so we can test the atomic flag. -// await expect(submitTx(updateTx, paymentAccount)).rejects.toMatchObject({ -// section: 'did', -// name: expect.stringMatching(/^ServiceAlready(Exists|Present)$/), -// }) + describe('FullDidUpdateBuilder', () => { + it('Build from a complete full DID and remove everything but the authentication key', async () => { + const { keypair, getSignCallback, storeDidCallback, authentication } = + TestUtilsV2.makeSigningKeyTool() -// const updatedFullDidLinkedInfo = await api.call.did.query( -// Did.toChain(fullDid.uri) -// ) -// const { document: updatedFullDid } = Did.linkedInfoFromChain( -// updatedFullDidLinkedInfo -// ) -// // .setAttestationKey() extrinsic went through but it was then reverted -// expect(updatedFullDid.assertionMethod).toBeUndefined() -// // The service endpoint will match the one manually added, and not the one set in the builder. -// expect( -// Did.getService(updatedFullDid, '#id-1') -// ).toStrictEqual({ -// id: '#id-1', -// type: ['type-1'], -// serviceEndpoint: ['x:url-1'], -// }) -// }, 60_000) -// }) -// }) + const createTx = await Did.DidChainV2.getStoreTxFromInput( + { + authentication, + keyAgreement: [ + { + publicKey: new Uint8Array(32).fill(1), + type: 'x25519', + }, + { + publicKey: new Uint8Array(32).fill(2), + type: 'x25519', + }, + ], + assertionMethod: [ + { + publicKey: new Uint8Array(32).fill(1), + type: 'sr25519', + }, + ], + capabilityDelegation: [ + { + publicKey: new Uint8Array(33).fill(1), + type: 'ecdsa', + }, + ], + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#id-2', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }, + ], + }, + paymentAccount.address, + storeDidCallback + ) + await submitTx(createTx, paymentAccount) + + const initialFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain( + Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }) + ) + ) + const { document: initialFullDid } = Did.DidRpc2.linkedInfoFromChain( + initialFullDidLinkedInfo + ) + + const encryptionKeys = initialFullDid.keyAgreement + if (!encryptionKeys) throw new Error('No key agreement keys') + + const extrinsic = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batchAll, + did: initialFullDid.id, + extrinsics: [ + api.tx.did.removeKeyAgreementKey( + Did.DidChainV2.fragmentIdToChain( + initialFullDid.verificationMethod.find( + (vm) => vm.id === encryptionKeys[0] + )!.id + ) + ), + api.tx.did.removeKeyAgreementKey( + Did.DidChainV2.fragmentIdToChain( + initialFullDid.verificationMethod.find( + (vm) => vm.id === encryptionKeys[1] + )!.id + ) + ), + api.tx.did.removeAttestationKey(), + api.tx.did.removeDelegationKey(), + api.tx.did.removeServiceEndpoint('id-1'), + api.tx.did.removeServiceEndpoint('id-2'), + ], + sign: getSignCallback(initialFullDid), + submitter: paymentAccount.address, + }) + await submitTx(extrinsic, paymentAccount) + + const finalFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(initialFullDid.id) + ) + const { document: finalFullDid } = Did.DidRpc2.linkedInfoFromChain( + finalFullDidLinkedInfo + ) + + expect(finalFullDid).not.toBeNull() + + expect(finalFullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: finalFullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + publicKey: keypair.publicKey, + type: 'sr25519', + }), + }), + ], + }) + expect(finalFullDid.authentication).toHaveLength(1) + expect(finalFullDid.assertionMethod).toBe(undefined) + expect(finalFullDid.capabilityDelegation).toBe(undefined) + expect(finalFullDid.keyAgreement).toBe(undefined) + }, 40_000) + + // it('Correctly handles rotation of the authentication key', async () => { + // const { authentication, getSignCallback, storeDidCallback } = + // makeSigningKeyTool() + // const { + // authentication: [newAuthKey], + // } = makeSigningKeyTool('ed25519') + + // const createTx = await Did.getStoreTx( + // { authentication }, + // paymentAccount.address, + // storeDidCallback + // ) + // await submitTx(createTx, paymentAccount) + + // const initialFullDidLinkedInfo = await api.call.did.query( + // Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + // ) + // const { document: initialFullDid } = Did.linkedInfoFromChain( + // initialFullDidLinkedInfo + // ) + + // const extrinsic = await Did.authorizeBatch({ + // batchFunction: api.tx.utility.batchAll, + // did: initialFullDid.id, + // extrinsics: [ + // api.tx.did.addServiceEndpoint( + // Did.serviceToChain({ + // id: '#id-1', + // type: ['type-1'], + // serviceEndpoint: ['x:url-1'], + // }) + // ), + // api.tx.did.setAuthenticationKey(Did.publicKeyToChain(newAuthKey)), + // api.tx.did.addServiceEndpoint( + // Did.serviceToChain({ + // id: '#id-2', + // type: ['type-2'], + // serviceEndpoint: ['x:url-2'], + // }) + // ), + // ], + // sign: getSignCallback(initialFullDid), + // submitter: paymentAccount.address, + // }) + + // await submitTx(extrinsic, paymentAccount) + + // const finalFullDidLinkedInfo = await api.call.did.query( + // Did.toChain(initialFullDid.id) + // ) + // const { document: finalFullDid } = Did.linkedInfoFromChain( + // finalFullDidLinkedInfo + // ) + + // expect(finalFullDid).not.toBeNull() + + // expect(finalFullDid.authentication[0]).toMatchObject({ + // publicKey: newAuthKey.publicKey, + // type: newAuthKey.type, + // }) + + // expect(finalFullDid.keyAgreement).toBeUndefined() + // expect(finalFullDid.assertionMethod).toBeUndefined() + // expect(finalFullDid.capabilityDelegation).toBeUndefined() + // expect(finalFullDid.service).toHaveLength(2) + // }, 40_000) + + // it('simple `batch` succeeds despite failures of some extrinsics', async () => { + // const { authentication, getSignCallback, storeDidCallback } = + // makeSigningKeyTool() + // const tx = await Did.getStoreTx( + // { + // authentication, + // service: [ + // { + // id: '#id-1', + // type: ['type-1'], + // serviceEndpoint: ['x:url-1'], + // }, + // ], + // }, + // paymentAccount.address, + // storeDidCallback + // ) + // // Create the full DID with a service endpoint + // await submitTx(tx, paymentAccount) + // const fullDidLinkedInfo = await api.call.did.query( + // Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + // ) + // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + + // expect(fullDid.assertionMethod).toBeUndefined() + + // // Try to set a new attestation key and a duplicate service endpoint + // const updateTx = await Did.authorizeBatch({ + // batchFunction: api.tx.utility.batch, + // did: fullDid.id, + // extrinsics: [ + // api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), + // api.tx.did.addServiceEndpoint( + // Did.serviceToChain({ + // id: '#id-1', + // type: ['type-2'], + // serviceEndpoint: ['x:url-2'], + // }) + // ), + // ], + // sign: getSignCallback(fullDid), + // submitter: paymentAccount.address, + // }) + // // Now the second operation fails but the batch succeeds + // await submitTx(updateTx, paymentAccount) + + // const updatedFullDidLinkedInfo = await api.call.did.query( + // Did.toChain(fullDid.id) + // ) + // const { document: updatedFullDid } = Did.linkedInfoFromChain( + // updatedFullDidLinkedInfo + // ) + + // // .setAttestationKey() extrinsic went through in the batch + // expect(updatedFullDid.assertionMethod?.[0]).toBeDefined() + // // The service endpoint will match the one manually added, and not the one set in the batch + // expect( + // Did.getService(updatedFullDid, '#id-1') + // ).toStrictEqual({ + // id: '#id-1', + // type: ['type-1'], + // serviceEndpoint: ['x:url-1'], + // }) + // }, 60_000) + + // it('batchAll fails if any extrinsics fails', async () => { + // const { authentication, getSignCallback, storeDidCallback } = + // makeSigningKeyTool() + // const createTx = await Did.getStoreTx( + // { + // authentication, + // service: [ + // { + // id: '#id-1', + // type: ['type-1'], + // serviceEndpoint: ['x:url-1'], + // }, + // ], + // }, + // paymentAccount.address, + // storeDidCallback + // ) + // await submitTx(createTx, paymentAccount) + // const fullDidLinkedInfo = await api.call.did.query( + // Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + // ) + // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + + // expect(fullDid.assertionMethod).toBeUndefined() + + // // Use batchAll to set a new attestation key and a duplicate service endpoint + // const updateTx = await Did.authorizeBatch({ + // batchFunction: api.tx.utility.batchAll, + // did: fullDid.id, + // extrinsics: [ + // api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), + // api.tx.did.addServiceEndpoint( + // Did.serviceToChain({ + // id: '#id-1', + // type: ['type-2'], + // serviceEndpoint: ['x:url-2'], + // }) + // ), + // ], + // sign: getSignCallback(fullDid), + // submitter: paymentAccount.address, + // }) + + // // Now, submitting will result in the second operation to fail AND the batch to fail, so we can test the atomic flag. + // await expect(submitTx(updateTx, paymentAccount)).rejects.toMatchObject({ + // section: 'did', + // name: expect.stringMatching(/^ServiceAlready(Exists|Present)$/), + // }) + + // const updatedFullDidLinkedInfo = await api.call.did.query( + // Did.toChain(fullDid.id) + // ) + // const { document: updatedFullDid } = Did.linkedInfoFromChain( + // updatedFullDidLinkedInfo + // ) + // // .setAttestationKey() extrinsic went through but it was then reverted + // expect(updatedFullDid.assertionMethod).toBeUndefined() + // // The service endpoint will match the one manually added, and not the one set in the builder. + // expect( + // Did.getService(updatedFullDid, '#id-1') + // ).toStrictEqual({ + // id: '#id-1', + // type: ['type-1'], + // serviceEndpoint: ['x:url-1'], + // }) + // }, 60_000) + }) +}) // describe('DID extrinsics batching', () => { -// let fullDid: DidDocument -// let key: KeyTool +// let fullDid: DidDocumentV2.DidDocument +// let key: TestUtilsV2.KeyTool // beforeAll(async () => { -// key = makeSigningKeyTool() -// fullDid = await createFullDidFromSeed(paymentAccount, key.keypair) +// key = TestUtilsV2.makeSigningKeyTool() +// fullDid = await TestUtilsV2.createFullDidFromSeed( +// paymentAccount, +// key.keypair +// ) // }, 50_000) // it('simple batch succeeds despite failures of some extrinsics', async () => { // const cType = CType.fromProperties(UUID.generate(), {}) // const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) // const rootNode = DelegationNode.newRoot({ -// account: fullDid.uri, +// account: fullDid.id, // permissions: [Permission.DELEGATE], // cTypeHash: CType.idToHash(cType.$id), // }) // const delegationStoreTx = await rootNode.getStoreTx() -// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) -// const tx = await Did.authorizeBatch({ +// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) +// const tx = await Did.DidDetailsV2.authorizeBatch({ // batchFunction: api.tx.utility.batch, -// did: fullDid.uri, +// did: fullDid.id, // extrinsics: [ // ctypeStoreTx, // // Will fail since the delegation cannot be revoked before it is added @@ -1072,15 +1169,15 @@ describe('DID migration', () => { // const cType = CType.fromProperties(UUID.generate(), {}) // const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) // const rootNode = DelegationNode.newRoot({ -// account: fullDid.uri, +// account: fullDid.id, // permissions: [Permission.DELEGATE], // cTypeHash: CType.idToHash(cType.$id), // }) // const delegationStoreTx = await rootNode.getStoreTx() -// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) -// const tx = await Did.authorizeBatch({ +// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) +// const tx = await Did.DidDetailsV2.authorizeBatch({ // batchFunction: api.tx.utility.batchAll, -// did: fullDid.uri, +// did: fullDid.id, // extrinsics: [ // ctypeStoreTx, // // Will fail since the delegation cannot be revoked before it is added @@ -1103,8 +1200,8 @@ describe('DID migration', () => { // it('can batch extrinsics for the same required key type', async () => { // const web3NameClaimTx = api.tx.web3Names.claim('test-1') -// const authorizedTx = await Did.authorizeTx( -// fullDid.uri, +// const authorizedTx = await Did.DidDetailsV2.authorizeTx( +// fullDid.id, // web3NameClaimTx, // key.getSignCallback(fullDid), // paymentAccount.address @@ -1113,9 +1210,9 @@ describe('DID migration', () => { // const web3Name1ReleaseExt = api.tx.web3Names.releaseByOwner() // const web3Name2ClaimExt = api.tx.web3Names.claim('test-2') -// const tx = await Did.authorizeBatch({ +// const tx = await Did.DidDetailsV2.authorizeBatch({ // batchFunction: api.tx.utility.batch, -// did: fullDid.uri, +// did: fullDid.id, // extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], // sign: key.getSignCallback(fullDid), // submitter: paymentAccount.address, @@ -1127,8 +1224,8 @@ describe('DID migration', () => { // expect(encoded1.isSome).toBe(false) // // Test for correct creation of second web3 name // const encoded2 = await api.call.did.queryByWeb3Name('test-2') -// expect(Did.linkedInfoFromChain(encoded2).document.uri).toStrictEqual( -// fullDid.uri +// expect(Did.DidRpc2.linkedInfoFromChain(encoded2).document.id).toStrictEqual( +// fullDid.id // ) // }, 30_000) @@ -1140,7 +1237,7 @@ describe('DID migration', () => { // const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) // // Delegation key // const rootNode = DelegationNode.newRoot({ -// account: fullDid.uri, +// account: fullDid.id, // permissions: [Permission.DELEGATE], // cTypeHash: CType.idToHash(ctype1.$id), // }) @@ -1152,11 +1249,11 @@ describe('DID migration', () => { // const ctype2 = CType.fromProperties(UUID.generate(), {}) // const ctype2Creation = api.tx.ctype.add(CType.toChain(ctype2)) // // Delegation key -// const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.uri) +// const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.id) -// const batchedExtrinsics = await Did.authorizeBatch({ +// const batchedExtrinsics = await Did.DidDetailsV2.authorizeBatch({ // batchFunction: api.tx.utility.batchAll, -// did: fullDid.uri, +// did: fullDid.id, // extrinsics: [ // web3NameReleaseExt, // ctype1Creation, @@ -1176,9 +1273,11 @@ describe('DID migration', () => { // expect(encoded.isSome).toBe(false) // const { -// document: { uri }, -// } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name('test-2')) -// expect(uri).toStrictEqual(fullDid.uri) +// document: { id }, +// } = Did.DidRpc2.linkedInfoFromChain( +// await api.call.did.queryByWeb3Name('test-2') +// ) +// expect(id).toStrictEqual(fullDid.id) // // Test correct use of attestation keys // await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() @@ -1192,7 +1291,8 @@ describe('DID migration', () => { // describe('Runtime constraints', () => { // let testAuthKey: NewDidVerificationKey -// const { keypair, storeDidCallback } = makeSigningKeyTool('ed25519') +// const { keypair, storeDidCallback } = +// TestUtilsV2.makeSigningKeyTool('ed25519') // beforeAll(async () => { // testAuthKey = { From 932ed2b493c71279232d6f5b67902ab8dcb5e0e8 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 10:21:22 +0100 Subject: [PATCH 19/78] All DID integration tests passing --- packages/did/src/Did2.rpc.ts | 56 +- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 7 + .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 17 +- packages/did/src/DidResolver/DidResolverV2.ts | 4 +- tests/integration/Did2.spec.ts | 1033 +++++++++-------- tests/testUtils/TestUtils2.ts | 62 +- 6 files changed, 639 insertions(+), 540 deletions(-) diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index 8c3921ca0b..591f318b3f 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -165,41 +165,45 @@ export function linkedInfoFromChain( did.keyAgreement = [] didRec.keyAgreement.forEach(({ id, publicKey, type: keyType }) => { did.keyAgreement?.push(id) - did.verificationMethod.push( - didKeyToVerificationMethod(fromChain(identifier), id, { - keyType, - publicKey, - }) - ) + if (did.verificationMethod.find((vm) => vm.id === id) === undefined) { + did.verificationMethod.push( + didKeyToVerificationMethod(fromChain(identifier), id, { + keyType, + publicKey, + }) + ) + } }) } if (didRec.assertionMethod !== undefined) { - did.assertionMethod = [didRec.assertionMethod[0].id] - did.verificationMethod.push( - didKeyToVerificationMethod( - fromChain(identifier), - didRec.assertionMethod[0].id, - { - keyType: didRec.assertionMethod[0].type, - publicKey: didRec.assertionMethod[0].publicKey, - } + const { id, type, publicKey } = didRec.assertionMethod[0] + did.assertionMethod = [id] + if (did.verificationMethod.find((vm) => vm.id === id) === undefined) { + did.verificationMethod.push( + didKeyToVerificationMethod( + fromChain(identifier), + didRec.assertionMethod[0].id, + { + keyType: type, + publicKey, + } + ) ) - ) + } } if (didRec.capabilityDelegation !== undefined) { - did.capabilityDelegation = [didRec.capabilityDelegation[0].id] - did.verificationMethod.push( - didKeyToVerificationMethod( - fromChain(identifier), - didRec.capabilityDelegation[0].id, - { - keyType: didRec.capabilityDelegation[0].type, - publicKey: didRec.capabilityDelegation[0].publicKey, - } + const { id, type, publicKey } = didRec.capabilityDelegation[0] + did.capabilityDelegation = [id] + if (did.verificationMethod.find((vm) => vm.id === id) === undefined) { + did.verificationMethod.push( + didKeyToVerificationMethod(fromChain(identifier), id, { + keyType: type, + publicKey, + }) ) - ) + } } const services = servicesFromChain(serviceEndpoints) diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 9c20114472..bac35a4fb6 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -44,3 +44,10 @@ export type BaseNewDidKey = { export type NewDidVerificationKey = BaseNewDidKey & { type: DidVerificationKeyType } + +/** + * Type of a new encryption key to add under a DID. + */ +export type NewDidEncryptionKey = BaseNewDidKey & { + type: DidEncryptionKeyType +} diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index 527cd59df2..677cbe9fca 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -222,12 +222,17 @@ export function createLightDidDocument({ if (keyAgreement !== undefined) { did.keyAgreement = [encryptionKeyId] - did.verificationMethod.push( - didKeyToVerificationMethod(uri, encryptionKeyId, { - keyType: keyAgreement[0].type, - publicKey: keyAgreement[0].publicKey, - }) - ) + if ( + did.verificationMethod.find((vm) => vm.id === encryptionKeyId) === + undefined + ) { + did.verificationMethod.push( + didKeyToVerificationMethod(uri, encryptionKeyId, { + keyType: keyAgreement[0].type, + publicKey: keyAgreement[0].publicKey, + }) + ) + } } return did diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index 9d49bfdc6d..bf20cf0475 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -72,7 +72,7 @@ async function resolveInternal( ) } if (document.keyAgreement !== undefined) { - newDidDocument.assertionMethod = document.keyAgreement.map((k) => k.id) + newDidDocument.keyAgreement = document.keyAgreement.map((k) => k.id) newDidDocument.verificationMethod.push( ...document.keyAgreement.map((k) => didKeyToVerificationMethod(did, k.id, { @@ -83,7 +83,7 @@ async function resolveInternal( ) } if (document.capabilityDelegation !== undefined) { - newDidDocument.assertionMethod = document.capabilityDelegation.map( + newDidDocument.capabilityDelegation = document.capabilityDelegation.map( (k) => k.id ) newDidDocument.verificationMethod.push( diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts index a13d22a608..cc94f14549 100644 --- a/tests/integration/Did2.spec.ts +++ b/tests/integration/Did2.spec.ts @@ -8,7 +8,7 @@ import type { ApiPromise } from '@polkadot/api' import { BN } from '@polkadot/util' -import { CType, disconnect } from '@kiltprotocol/core' +import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' import { UUID } from '@kiltprotocol/utils' import * as Did from '@kiltprotocol/did' import { @@ -16,6 +16,7 @@ import { DidDocumentV2, DidResolverV2, KiltKeyringPair, + Permission, } from '@kiltprotocol/types' import { TestUtilsV2 } from '../testUtils/index.js' @@ -575,7 +576,7 @@ describe('DID authorization', () => { it('no longer authorizes ctype creation after DID deletion', async () => { const linkedInfo = Did.DidRpc2.linkedInfoFromChain( - await api.call.did.query(Did.toChain(did.id)) + await api.call.did.query(Did.DidChainV2.toChain(did.id)) ) const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 const deleteCall = api.tx.did.delete(storedEndpointsCount) @@ -922,491 +923,555 @@ describe('DID management batching', () => { expect(finalFullDid.keyAgreement).toBe(undefined) }, 40_000) - // it('Correctly handles rotation of the authentication key', async () => { - // const { authentication, getSignCallback, storeDidCallback } = - // makeSigningKeyTool() - // const { - // authentication: [newAuthKey], - // } = makeSigningKeyTool('ed25519') - - // const createTx = await Did.getStoreTx( - // { authentication }, - // paymentAccount.address, - // storeDidCallback - // ) - // await submitTx(createTx, paymentAccount) - - // const initialFullDidLinkedInfo = await api.call.did.query( - // Did.toChain(Did.getFullDidUriFromKey(authentication[0])) - // ) - // const { document: initialFullDid } = Did.linkedInfoFromChain( - // initialFullDidLinkedInfo - // ) - - // const extrinsic = await Did.authorizeBatch({ - // batchFunction: api.tx.utility.batchAll, - // did: initialFullDid.id, - // extrinsics: [ - // api.tx.did.addServiceEndpoint( - // Did.serviceToChain({ - // id: '#id-1', - // type: ['type-1'], - // serviceEndpoint: ['x:url-1'], - // }) - // ), - // api.tx.did.setAuthenticationKey(Did.publicKeyToChain(newAuthKey)), - // api.tx.did.addServiceEndpoint( - // Did.serviceToChain({ - // id: '#id-2', - // type: ['type-2'], - // serviceEndpoint: ['x:url-2'], - // }) - // ), - // ], - // sign: getSignCallback(initialFullDid), - // submitter: paymentAccount.address, - // }) - - // await submitTx(extrinsic, paymentAccount) - - // const finalFullDidLinkedInfo = await api.call.did.query( - // Did.toChain(initialFullDid.id) - // ) - // const { document: finalFullDid } = Did.linkedInfoFromChain( - // finalFullDidLinkedInfo - // ) - - // expect(finalFullDid).not.toBeNull() - - // expect(finalFullDid.authentication[0]).toMatchObject({ - // publicKey: newAuthKey.publicKey, - // type: newAuthKey.type, - // }) - - // expect(finalFullDid.keyAgreement).toBeUndefined() - // expect(finalFullDid.assertionMethod).toBeUndefined() - // expect(finalFullDid.capabilityDelegation).toBeUndefined() - // expect(finalFullDid.service).toHaveLength(2) - // }, 40_000) - - // it('simple `batch` succeeds despite failures of some extrinsics', async () => { - // const { authentication, getSignCallback, storeDidCallback } = - // makeSigningKeyTool() - // const tx = await Did.getStoreTx( - // { - // authentication, - // service: [ - // { - // id: '#id-1', - // type: ['type-1'], - // serviceEndpoint: ['x:url-1'], - // }, - // ], - // }, - // paymentAccount.address, - // storeDidCallback - // ) - // // Create the full DID with a service endpoint - // await submitTx(tx, paymentAccount) - // const fullDidLinkedInfo = await api.call.did.query( - // Did.toChain(Did.getFullDidUriFromKey(authentication[0])) - // ) - // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - - // expect(fullDid.assertionMethod).toBeUndefined() - - // // Try to set a new attestation key and a duplicate service endpoint - // const updateTx = await Did.authorizeBatch({ - // batchFunction: api.tx.utility.batch, - // did: fullDid.id, - // extrinsics: [ - // api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), - // api.tx.did.addServiceEndpoint( - // Did.serviceToChain({ - // id: '#id-1', - // type: ['type-2'], - // serviceEndpoint: ['x:url-2'], - // }) - // ), - // ], - // sign: getSignCallback(fullDid), - // submitter: paymentAccount.address, - // }) - // // Now the second operation fails but the batch succeeds - // await submitTx(updateTx, paymentAccount) - - // const updatedFullDidLinkedInfo = await api.call.did.query( - // Did.toChain(fullDid.id) - // ) - // const { document: updatedFullDid } = Did.linkedInfoFromChain( - // updatedFullDidLinkedInfo - // ) - - // // .setAttestationKey() extrinsic went through in the batch - // expect(updatedFullDid.assertionMethod?.[0]).toBeDefined() - // // The service endpoint will match the one manually added, and not the one set in the batch - // expect( - // Did.getService(updatedFullDid, '#id-1') - // ).toStrictEqual({ - // id: '#id-1', - // type: ['type-1'], - // serviceEndpoint: ['x:url-1'], - // }) - // }, 60_000) - - // it('batchAll fails if any extrinsics fails', async () => { - // const { authentication, getSignCallback, storeDidCallback } = - // makeSigningKeyTool() - // const createTx = await Did.getStoreTx( - // { - // authentication, - // service: [ - // { - // id: '#id-1', - // type: ['type-1'], - // serviceEndpoint: ['x:url-1'], - // }, - // ], - // }, - // paymentAccount.address, - // storeDidCallback - // ) - // await submitTx(createTx, paymentAccount) - // const fullDidLinkedInfo = await api.call.did.query( - // Did.toChain(Did.getFullDidUriFromKey(authentication[0])) - // ) - // const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - - // expect(fullDid.assertionMethod).toBeUndefined() - - // // Use batchAll to set a new attestation key and a duplicate service endpoint - // const updateTx = await Did.authorizeBatch({ - // batchFunction: api.tx.utility.batchAll, - // did: fullDid.id, - // extrinsics: [ - // api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), - // api.tx.did.addServiceEndpoint( - // Did.serviceToChain({ - // id: '#id-1', - // type: ['type-2'], - // serviceEndpoint: ['x:url-2'], - // }) - // ), - // ], - // sign: getSignCallback(fullDid), - // submitter: paymentAccount.address, - // }) - - // // Now, submitting will result in the second operation to fail AND the batch to fail, so we can test the atomic flag. - // await expect(submitTx(updateTx, paymentAccount)).rejects.toMatchObject({ - // section: 'did', - // name: expect.stringMatching(/^ServiceAlready(Exists|Present)$/), - // }) - - // const updatedFullDidLinkedInfo = await api.call.did.query( - // Did.toChain(fullDid.id) - // ) - // const { document: updatedFullDid } = Did.linkedInfoFromChain( - // updatedFullDidLinkedInfo - // ) - // // .setAttestationKey() extrinsic went through but it was then reverted - // expect(updatedFullDid.assertionMethod).toBeUndefined() - // // The service endpoint will match the one manually added, and not the one set in the builder. - // expect( - // Did.getService(updatedFullDid, '#id-1') - // ).toStrictEqual({ - // id: '#id-1', - // type: ['type-1'], - // serviceEndpoint: ['x:url-1'], - // }) - // }, 60_000) + it('Correctly handles rotation of the authentication key', async () => { + const { authentication, getSignCallback, storeDidCallback } = + TestUtilsV2.makeSigningKeyTool() + const { + authentication: [newAuthKey], + } = TestUtilsV2.makeSigningKeyTool('ed25519') + + const createTx = await Did.DidChainV2.getStoreTxFromInput( + { authentication }, + paymentAccount.address, + storeDidCallback + ) + await submitTx(createTx, paymentAccount) + + const initialFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain( + Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }) + ) + ) + const { document: initialFullDid } = Did.DidRpc2.linkedInfoFromChain( + initialFullDidLinkedInfo + ) + + const extrinsic = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batchAll, + did: initialFullDid.id, + extrinsics: [ + api.tx.did.addServiceEndpoint( + Did.DidChainV2.serviceToChain({ + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }) + ), + api.tx.did.setAuthenticationKey( + Did.DidChainV2.publicKeyToChain(newAuthKey) + ), + api.tx.did.addServiceEndpoint( + Did.DidChainV2.serviceToChain({ + id: '#id-2', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }) + ), + ], + sign: getSignCallback(initialFullDid), + submitter: paymentAccount.address, + }) + + await submitTx(extrinsic, paymentAccount) + + const finalFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(initialFullDid.id) + ) + const { document: finalFullDid } = Did.DidRpc2.linkedInfoFromChain( + finalFullDidLinkedInfo + ) + + expect(finalFullDid).not.toBeNull() + expect(finalFullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: finalFullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + publicKey: newAuthKey.publicKey, + type: 'ed25519', + }), + }), + ], + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#id-2', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }, + ], + }) + + expect(finalFullDid.authentication).toHaveLength(1) + expect(finalFullDid.keyAgreement).toBeUndefined() + expect(finalFullDid.assertionMethod).toBeUndefined() + expect(finalFullDid.capabilityDelegation).toBeUndefined() + }, 40_000) + + it('simple `batch` succeeds despite failures of some extrinsics', async () => { + const { authentication, getSignCallback, storeDidCallback } = + TestUtilsV2.makeSigningKeyTool() + const tx = await Did.DidChainV2.getStoreTxFromInput( + { + authentication, + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + ], + }, + paymentAccount.address, + storeDidCallback + ) + // Create the full DID with a service endpoint + await submitTx(tx, paymentAccount) + const fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain( + Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }) + ) + ) + const { document: fullDid } = + Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + + expect(fullDid.assertionMethod).toBeUndefined() + + // Try to set a new attestation key and a duplicate service endpoint + const updateTx = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batch, + did: fullDid.id, + extrinsics: [ + api.tx.did.setAttestationKey( + Did.DidChainV2.publicKeyToChain(authentication[0]) + ), + api.tx.did.addServiceEndpoint( + Did.DidChainV2.serviceToChain({ + id: '#id-1', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }) + ), + ], + sign: getSignCallback(fullDid), + submitter: paymentAccount.address, + }) + // Now the second operation fails but the batch succeeds + await submitTx(updateTx, paymentAccount) + + const updatedFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(fullDid.id) + ) + const { document: updatedFullDid } = Did.DidRpc2.linkedInfoFromChain( + updatedFullDidLinkedInfo + ) + + expect(updatedFullDid).toMatchObject>({ + verificationMethod: [ + expect.objectContaining({ + // Authentication and assertionMethod + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }), + ], + // Old service maintained + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + ], + }) + + expect(updatedFullDid.authentication).toHaveLength(1) + expect(updatedFullDid.keyAgreement).toBeUndefined() + // .setAttestationKey() extrinsic went through in the batch + expect(updatedFullDid.assertionMethod).toStrictEqual( + updatedFullDid.authentication + ) + expect(updatedFullDid.capabilityDelegation).toBeUndefined() + }, 60_000) + + it('batchAll fails if any extrinsics fails', async () => { + const { authentication, getSignCallback, storeDidCallback } = + TestUtilsV2.makeSigningKeyTool() + const createTx = await Did.DidChainV2.getStoreTxFromInput( + { + authentication, + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + ], + }, + paymentAccount.address, + storeDidCallback + ) + await submitTx(createTx, paymentAccount) + const fullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain( + Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( + authentication[0] + ), + }) + ) + ) + const { document: fullDid } = + Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) + + expect(fullDid.assertionMethod).toBeUndefined() + + // Use batchAll to set a new attestation key and a duplicate service endpoint + const updateTx = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batchAll, + did: fullDid.id, + extrinsics: [ + api.tx.did.setAttestationKey( + Did.DidChainV2.publicKeyToChain(authentication[0]) + ), + api.tx.did.addServiceEndpoint( + Did.DidChainV2.serviceToChain({ + id: '#id-1', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }) + ), + ], + sign: getSignCallback(fullDid), + submitter: paymentAccount.address, + }) + + // Now, submitting will result in the second operation to fail AND the batch to fail, so we can test the atomic flag. + await expect(submitTx(updateTx, paymentAccount)).rejects.toMatchObject({ + section: 'did', + name: expect.stringMatching(/^ServiceAlready(Exists|Present)$/), + }) + + const updatedFullDidLinkedInfo = await api.call.did.query( + Did.DidChainV2.toChain(fullDid.id) + ) + const { document: updatedFullDid } = Did.DidRpc2.linkedInfoFromChain( + updatedFullDidLinkedInfo + ) + // .setAttestationKey() extrinsic went through but it was then reverted + expect(updatedFullDid.assertionMethod).toBeUndefined() + // The service endpoint will match the one manually added, and not the one set in the builder. + expect( + updatedFullDid.service?.find((s) => s.id === '#id-1') + ).toStrictEqual({ + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }) + }, 60_000) + }) +}) + +describe('DID extrinsics batching', () => { + let fullDid: DidDocumentV2.DidDocument + let key: TestUtilsV2.KeyTool + + beforeAll(async () => { + key = TestUtilsV2.makeSigningKeyTool() + fullDid = await TestUtilsV2.createFullDidFromSeed( + paymentAccount, + key.keypair + ) + }, 50_000) + + it('simple batch succeeds despite failures of some extrinsics', async () => { + const cType = CType.fromProperties(UUID.generate(), {}) + const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) + const rootNode = DelegationNode.newRoot({ + account: fullDid.id, + permissions: [Permission.DELEGATE], + cTypeHash: CType.idToHash(cType.$id), + }) + const delegationStoreTx = await rootNode.getStoreTx() + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) + const tx = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batch, + did: fullDid.id, + extrinsics: [ + ctypeStoreTx, + // Will fail since the delegation cannot be revoked before it is added + delegationRevocationTx, + delegationStoreTx, + ], + sign: key.getSignCallback(fullDid), + submitter: paymentAccount.address, + }) + + // The entire submission promise is resolves and does not throw + await submitTx(tx, paymentAccount) + + // The ctype has been created, even though the delegation operations failed. + await expect(CType.verifyStored(cType)).resolves.not.toThrow() + }) + + it('batchAll fails if any extrinsics fail', async () => { + const cType = CType.fromProperties(UUID.generate(), {}) + const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) + const rootNode = DelegationNode.newRoot({ + account: fullDid.id, + permissions: [Permission.DELEGATE], + cTypeHash: CType.idToHash(cType.$id), + }) + const delegationStoreTx = await rootNode.getStoreTx() + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) + const tx = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batchAll, + did: fullDid.id, + extrinsics: [ + ctypeStoreTx, + // Will fail since the delegation cannot be revoked before it is added + delegationRevocationTx, + delegationStoreTx, + ], + sign: key.getSignCallback(fullDid), + submitter: paymentAccount.address, + }) + + // The entire submission promise is rejected and throws. + await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ + section: 'delegation', + name: 'DelegationNotFound', + }) + + // The ctype has not been created, since atomicity ensures the whole batch is reverted in case of failure. + await expect(CType.verifyStored(cType)).rejects.toThrow() + }) + + it('can batch extrinsics for the same required key type', async () => { + const web3NameClaimTx = api.tx.web3Names.claim('test-1') + const authorizedTx = await Did.DidDetailsV2.authorizeTx( + fullDid.id, + web3NameClaimTx, + key.getSignCallback(fullDid), + paymentAccount.address + ) + await submitTx(authorizedTx, paymentAccount) + + const web3Name1ReleaseExt = api.tx.web3Names.releaseByOwner() + const web3Name2ClaimExt = api.tx.web3Names.claim('test-2') + const tx = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batch, + did: fullDid.id, + extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], + sign: key.getSignCallback(fullDid), + submitter: paymentAccount.address, + }) + await submitTx(tx, paymentAccount) + + // Test for correct creation and deletion + const encoded1 = await api.call.did.queryByWeb3Name('test-1') + expect(encoded1.isSome).toBe(false) + // Test for correct creation of second web3 name + const encoded2 = await api.call.did.queryByWeb3Name('test-2') + expect(Did.DidRpc2.linkedInfoFromChain(encoded2).document.id).toStrictEqual( + fullDid.id + ) + }, 30_000) + + it('can batch extrinsics for different required key types', async () => { + // Authentication key + const web3NameReleaseExt = api.tx.web3Names.releaseByOwner() + // Attestation key + const ctype1 = CType.fromProperties(UUID.generate(), {}) + const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) + // Delegation key + const rootNode = DelegationNode.newRoot({ + account: fullDid.id, + permissions: [Permission.DELEGATE], + cTypeHash: CType.idToHash(ctype1.$id), + }) + const delegationHierarchyCreation = await rootNode.getStoreTx() + + // Authentication key + const web3NameNewClaimExt = api.tx.web3Names.claim('test-2') + // Attestation key + const ctype2 = CType.fromProperties(UUID.generate(), {}) + const ctype2Creation = api.tx.ctype.add(CType.toChain(ctype2)) + // Delegation key + const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.id) + + const batchedExtrinsics = await Did.DidDetailsV2.authorizeBatch({ + batchFunction: api.tx.utility.batchAll, + did: fullDid.id, + extrinsics: [ + web3NameReleaseExt, + ctype1Creation, + delegationHierarchyCreation, + web3NameNewClaimExt, + ctype2Creation, + delegationHierarchyRemoval, + ], + sign: key.getSignCallback(fullDid), + submitter: paymentAccount.address, + }) + + await submitTx(batchedExtrinsics, paymentAccount) + + // Test correct use of authentication keys + const encoded = await api.call.did.queryByWeb3Name('test') + expect(encoded.isSome).toBe(false) + + const { + document: { id }, + } = Did.DidRpc2.linkedInfoFromChain( + await api.call.did.queryByWeb3Name('test-2') + ) + expect(id).toStrictEqual(fullDid.id) + + // Test correct use of attestation keys + await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() + await expect(CType.verifyStored(ctype2)).resolves.not.toThrow() + + // Test correct use of delegation keys + const node = await DelegationNode.fetch(rootNode.id) + expect(node.revoked).toBe(true) }) }) -// describe('DID extrinsics batching', () => { -// let fullDid: DidDocumentV2.DidDocument -// let key: TestUtilsV2.KeyTool - -// beforeAll(async () => { -// key = TestUtilsV2.makeSigningKeyTool() -// fullDid = await TestUtilsV2.createFullDidFromSeed( -// paymentAccount, -// key.keypair -// ) -// }, 50_000) - -// it('simple batch succeeds despite failures of some extrinsics', async () => { -// const cType = CType.fromProperties(UUID.generate(), {}) -// const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) -// const rootNode = DelegationNode.newRoot({ -// account: fullDid.id, -// permissions: [Permission.DELEGATE], -// cTypeHash: CType.idToHash(cType.$id), -// }) -// const delegationStoreTx = await rootNode.getStoreTx() -// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) -// const tx = await Did.DidDetailsV2.authorizeBatch({ -// batchFunction: api.tx.utility.batch, -// did: fullDid.id, -// extrinsics: [ -// ctypeStoreTx, -// // Will fail since the delegation cannot be revoked before it is added -// delegationRevocationTx, -// delegationStoreTx, -// ], -// sign: key.getSignCallback(fullDid), -// submitter: paymentAccount.address, -// }) - -// // The entire submission promise is resolves and does not throw -// await submitTx(tx, paymentAccount) - -// // The ctype has been created, even though the delegation operations failed. -// await expect(CType.verifyStored(cType)).resolves.not.toThrow() -// }) - -// it('batchAll fails if any extrinsics fail', async () => { -// const cType = CType.fromProperties(UUID.generate(), {}) -// const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) -// const rootNode = DelegationNode.newRoot({ -// account: fullDid.id, -// permissions: [Permission.DELEGATE], -// cTypeHash: CType.idToHash(cType.$id), -// }) -// const delegationStoreTx = await rootNode.getStoreTx() -// const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) -// const tx = await Did.DidDetailsV2.authorizeBatch({ -// batchFunction: api.tx.utility.batchAll, -// did: fullDid.id, -// extrinsics: [ -// ctypeStoreTx, -// // Will fail since the delegation cannot be revoked before it is added -// delegationRevocationTx, -// delegationStoreTx, -// ], -// sign: key.getSignCallback(fullDid), -// submitter: paymentAccount.address, -// }) - -// // The entire submission promise is rejected and throws. -// await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ -// section: 'delegation', -// name: 'DelegationNotFound', -// }) - -// // The ctype has not been created, since atomicity ensures the whole batch is reverted in case of failure. -// await expect(CType.verifyStored(cType)).rejects.toThrow() -// }) - -// it('can batch extrinsics for the same required key type', async () => { -// const web3NameClaimTx = api.tx.web3Names.claim('test-1') -// const authorizedTx = await Did.DidDetailsV2.authorizeTx( -// fullDid.id, -// web3NameClaimTx, -// key.getSignCallback(fullDid), -// paymentAccount.address -// ) -// await submitTx(authorizedTx, paymentAccount) - -// const web3Name1ReleaseExt = api.tx.web3Names.releaseByOwner() -// const web3Name2ClaimExt = api.tx.web3Names.claim('test-2') -// const tx = await Did.DidDetailsV2.authorizeBatch({ -// batchFunction: api.tx.utility.batch, -// did: fullDid.id, -// extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], -// sign: key.getSignCallback(fullDid), -// submitter: paymentAccount.address, -// }) -// await submitTx(tx, paymentAccount) - -// // Test for correct creation and deletion -// const encoded1 = await api.call.did.queryByWeb3Name('test-1') -// expect(encoded1.isSome).toBe(false) -// // Test for correct creation of second web3 name -// const encoded2 = await api.call.did.queryByWeb3Name('test-2') -// expect(Did.DidRpc2.linkedInfoFromChain(encoded2).document.id).toStrictEqual( -// fullDid.id -// ) -// }, 30_000) - -// it('can batch extrinsics for different required key types', async () => { -// // Authentication key -// const web3NameReleaseExt = api.tx.web3Names.releaseByOwner() -// // Attestation key -// const ctype1 = CType.fromProperties(UUID.generate(), {}) -// const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) -// // Delegation key -// const rootNode = DelegationNode.newRoot({ -// account: fullDid.id, -// permissions: [Permission.DELEGATE], -// cTypeHash: CType.idToHash(ctype1.$id), -// }) -// const delegationHierarchyCreation = await rootNode.getStoreTx() - -// // Authentication key -// const web3NameNewClaimExt = api.tx.web3Names.claim('test-2') -// // Attestation key -// const ctype2 = CType.fromProperties(UUID.generate(), {}) -// const ctype2Creation = api.tx.ctype.add(CType.toChain(ctype2)) -// // Delegation key -// const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.id) - -// const batchedExtrinsics = await Did.DidDetailsV2.authorizeBatch({ -// batchFunction: api.tx.utility.batchAll, -// did: fullDid.id, -// extrinsics: [ -// web3NameReleaseExt, -// ctype1Creation, -// delegationHierarchyCreation, -// web3NameNewClaimExt, -// ctype2Creation, -// delegationHierarchyRemoval, -// ], -// sign: key.getSignCallback(fullDid), -// submitter: paymentAccount.address, -// }) - -// await submitTx(batchedExtrinsics, paymentAccount) - -// // Test correct use of authentication keys -// const encoded = await api.call.did.queryByWeb3Name('test') -// expect(encoded.isSome).toBe(false) - -// const { -// document: { id }, -// } = Did.DidRpc2.linkedInfoFromChain( -// await api.call.did.queryByWeb3Name('test-2') -// ) -// expect(id).toStrictEqual(fullDid.id) - -// // Test correct use of attestation keys -// await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() -// await expect(CType.verifyStored(ctype2)).resolves.not.toThrow() - -// // Test correct use of delegation keys -// const node = await DelegationNode.fetch(rootNode.id) -// expect(node.revoked).toBe(true) -// }) -// }) - -// describe('Runtime constraints', () => { -// let testAuthKey: NewDidVerificationKey -// const { keypair, storeDidCallback } = -// TestUtilsV2.makeSigningKeyTool('ed25519') - -// beforeAll(async () => { -// testAuthKey = { -// publicKey: keypair.publicKey, -// type: 'ed25519', -// } -// }) -// describe('DID creation', () => { -// it('should not be possible to create a DID with too many encryption keys', async () => { -// // Maximum is 10 -// const newKeyAgreementKeys = Array(10).map( -// (_, index): NewDidEncryptionKey => ({ -// publicKey: Uint8Array.from(new Array(32).fill(index)), -// type: 'x25519', -// }) -// ) -// await Did.getStoreTx( -// { -// authentication: [testAuthKey], -// keyAgreement: newKeyAgreementKeys, -// }, -// paymentAccount.address, -// storeDidCallback -// ) -// // One more than the maximum -// newKeyAgreementKeys.push({ -// publicKey: Uint8Array.from(new Array(32).fill(100)), -// type: 'x25519', -// }) -// await expect( -// Did.getStoreTx( -// { -// authentication: [testAuthKey], -// keyAgreement: newKeyAgreementKeys, -// }, - -// paymentAccount.address, -// storeDidCallback -// ) -// ).rejects.toThrowErrorMatchingInlineSnapshot( -// `"The number of key agreement keys in the creation operation is greater than the maximum allowed, which is 10"` -// ) -// }, 30_000) - -// it('should not be possible to create a DID with too many service endpoints', async () => { -// // Maximum is 25 -// const newServiceEndpoints = Array(25).map( -// (_, index): DidServiceEndpoint => ({ -// id: `#service-${index}`, -// type: [`type-${index}`], -// serviceEndpoint: [`x:url-${index}`], -// }) -// ) -// await Did.getStoreTx( -// { -// authentication: [testAuthKey], -// service: newServiceEndpoints, -// }, -// paymentAccount.address, -// storeDidCallback -// ) -// // One more than the maximum -// newServiceEndpoints.push({ -// id: '#service-100', -// type: ['type-100'], -// serviceEndpoint: ['x:url-100'], -// }) -// await expect( -// Did.getStoreTx( -// { -// authentication: [testAuthKey], -// service: newServiceEndpoints, -// }, - -// paymentAccount.address, -// storeDidCallback -// ) -// ).rejects.toThrowErrorMatchingInlineSnapshot( -// `"Cannot store more than 25 service endpoints per DID"` -// ) -// }, 30_000) - -// it('should not be possible to create a DID with a service endpoint that is too long', async () => { -// const serviceId = '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' -// const limit = api.consts.did.maxServiceIdLength.toNumber() -// expect(serviceId.length).toBeGreaterThan(limit) -// }) - -// it('should not be possible to create a DID with a service endpoint that has too many types', async () => { -// const types = ['type-1', 'type-2'] -// const limit = api.consts.did.maxNumberOfTypesPerService.toNumber() -// expect(types.length).toBeGreaterThan(limit) -// }) - -// it('should not be possible to create a DID with a service endpoint that has too many URIs', async () => { -// const uris = ['x:url-1', 'x:url-2', 'x:url-3'] -// const limit = api.consts.did.maxNumberOfUrlsPerService.toNumber() -// expect(uris.length).toBeGreaterThan(limit) -// }) - -// it('should not be possible to create a DID with a service endpoint that has a type that is too long', async () => { -// const type = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' -// const limit = api.consts.did.maxServiceTypeLength.toNumber() -// expect(type.length).toBeGreaterThan(limit) -// }) - -// it('should not be possible to create a DID with a service endpoint that has a URI that is too long', async () => { -// const uri = -// 'a:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' -// const limit = api.consts.did.maxServiceUrlLength.toNumber() -// expect(uri.length).toBeGreaterThan(limit) -// }) -// }) -// }) +describe('Runtime constraints', () => { + let testAuthKey: Did.DidDetailsV2.NewDidVerificationKey + const { keypair, storeDidCallback } = + TestUtilsV2.makeSigningKeyTool('ed25519') + + beforeAll(async () => { + testAuthKey = { + publicKey: keypair.publicKey, + type: 'ed25519', + } + }) + describe('DID creation', () => { + it('should not be possible to create a DID with too many encryption keys', async () => { + // Maximum is 10 + const newKeyAgreementKeys = Array(10).map( + (_, index): Did.DidDetailsV2.NewDidEncryptionKey => ({ + publicKey: Uint8Array.from(new Array(32).fill(index)), + type: 'x25519', + }) + ) + await Did.DidChainV2.getStoreTxFromInput( + { + authentication: [testAuthKey], + keyAgreement: newKeyAgreementKeys, + }, + paymentAccount.address, + storeDidCallback + ) + // One more than the maximum + newKeyAgreementKeys.push({ + publicKey: Uint8Array.from(new Array(32).fill(100)), + type: 'x25519', + }) + await expect( + Did.DidChainV2.getStoreTxFromInput( + { + authentication: [testAuthKey], + keyAgreement: newKeyAgreementKeys, + }, + + paymentAccount.address, + storeDidCallback + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"The number of key agreement keys in the creation operation is greater than the maximum allowed, which is 10"` + ) + }, 30_000) + + it('should not be possible to create a DID with too many service endpoints', async () => { + // Maximum is 25 + const newServiceEndpoints = Array(25).map( + (_, index): Did.DidDetailsV2.NewService => ({ + id: `#service-${index}`, + type: [`type-${index}`], + serviceEndpoint: [`x:url-${index}`], + }) + ) + await Did.DidChainV2.getStoreTxFromInput( + { + authentication: [testAuthKey], + service: newServiceEndpoints, + }, + paymentAccount.address, + storeDidCallback + ) + // One more than the maximum + newServiceEndpoints.push({ + id: '#service-100', + type: ['type-100'], + serviceEndpoint: ['x:url-100'], + }) + await expect( + Did.DidChainV2.getStoreTxFromInput( + { + authentication: [testAuthKey], + service: newServiceEndpoints, + }, + + paymentAccount.address, + storeDidCallback + ) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot store more than 25 service endpoints per DID"` + ) + }, 30_000) + + it('should not be possible to create a DID with a service endpoint that is too long', async () => { + const serviceId = '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const limit = api.consts.did.maxServiceIdLength.toNumber() + expect(serviceId.length).toBeGreaterThan(limit) + }) + + it('should not be possible to create a DID with a service endpoint that has too many types', async () => { + const types = ['type-1', 'type-2'] + const limit = api.consts.did.maxNumberOfTypesPerService.toNumber() + expect(types.length).toBeGreaterThan(limit) + }) + + it('should not be possible to create a DID with a service endpoint that has too many URIs', async () => { + const uris = ['x:url-1', 'x:url-2', 'x:url-3'] + const limit = api.consts.did.maxNumberOfUrlsPerService.toNumber() + expect(uris.length).toBeGreaterThan(limit) + }) + + it('should not be possible to create a DID with a service endpoint that has a type that is too long', async () => { + const type = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const limit = api.consts.did.maxServiceTypeLength.toNumber() + expect(type.length).toBeGreaterThan(limit) + }) + + it('should not be possible to create a DID with a service endpoint that has a URI that is too long', async () => { + const uri = + 'a:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const limit = api.consts.did.maxServiceUrlLength.toNumber() + expect(uri.length).toBeGreaterThan(limit) + }) + }) +}) afterAll(async () => { await disconnect() diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 64a7aae9b1..0850bb2949 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -356,34 +356,52 @@ export async function createFullDidFromLightDid( } if (assertionMethod !== undefined) { didDocument.assertionMethod = [assertionMethod[0].id] - didDocument.verificationMethod.push( - Did.DidUtilsV2.didKeyToVerificationMethod(uri, assertionMethod[0].id, { - keyType: assertionMethod[0].type, - publicKey: assertionMethod[0].publicKey, - }) - ) + if ( + didDocument.verificationMethod.find( + (vm) => vm.id === assertionMethod[0].id + ) === undefined + ) { + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod(uri, assertionMethod[0].id, { + keyType: assertionMethod[0].type, + publicKey: assertionMethod[0].publicKey, + }) + ) + } } if (capabilityDelegation !== undefined) { didDocument.capabilityDelegation = [capabilityDelegation[0].id] - didDocument.verificationMethod.push( - Did.DidUtilsV2.didKeyToVerificationMethod( - uri, - capabilityDelegation[0].id, - { - keyType: capabilityDelegation[0].type, - publicKey: capabilityDelegation[0].publicKey, - } + if ( + didDocument.verificationMethod.find( + (vm) => vm.id === capabilityDelegation[0].id + ) === undefined + ) { + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod( + uri, + capabilityDelegation[0].id, + { + keyType: capabilityDelegation[0].type, + publicKey: capabilityDelegation[0].publicKey, + } + ) ) - ) + } } if (keyAgreement !== undefined) { - didDocument.capabilityDelegation = [keyAgreement[0].id] - didDocument.verificationMethod.push( - Did.DidUtilsV2.didKeyToVerificationMethod(uri, keyAgreement[0].id, { - keyType: keyAgreement[0].type, - publicKey: keyAgreement[0].publicKey, - }) - ) + didDocument.keyAgreement = [keyAgreement[0].id] + if ( + didDocument.verificationMethod.find( + (vm) => vm.id === keyAgreement[0].id + ) === undefined + ) { + didDocument.verificationMethod.push( + Did.DidUtilsV2.didKeyToVerificationMethod(uri, keyAgreement[0].id, { + keyType: keyAgreement[0].type, + publicKey: keyAgreement[0].publicKey, + }) + ) + } } return didDocument From 5db94bc02e08e439923bf537bb62b8bd884414d1 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 11:26:16 +0100 Subject: [PATCH 20/78] Refactoring --- packages/did/src/Did2.rpc.ts | 94 +++++++++---------- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 39 +++++++- .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 22 ++--- packages/did/src/DidResolver/DidResolverV2.ts | 59 ++++++------ packages/types/src/DidDocumentV2.ts | 2 +- tests/testUtils/TestUtils2.ts | 71 ++++++-------- 6 files changed, 153 insertions(+), 134 deletions(-) diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index 591f318b3f..0b49876752 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -30,8 +30,11 @@ import { fromChain, publicKeyFromChain, } from './Did2.chain.js' +import { DidDetailsV2 } from './index.js' -function documentFromChain(encoded: DidDidDetails): ChainDidDetails { +function documentFromChain( + encoded: DidDidDetails +): Omit { const { publicKeys, authenticationKey, @@ -145,65 +148,58 @@ export function linkedInfoFromChain( const { identifier, accounts, w3n, serviceEndpoints, details } = encoded.unwrap() - const didRec = documentFromChain(details) + const { + authentication, + keyAgreement, + capabilityDelegation, + assertionMethod, + } = documentFromChain(details) const did: DidDocumentV2.DidDocument = { id: fromChain(identifier), - authentication: [didRec.authentication[0].id], + authentication: [authentication[0].id], verificationMethod: [ - didKeyToVerificationMethod( - fromChain(identifier), - didRec.authentication[0].id, - { - keyType: didRec.authentication[0].type, - publicKey: didRec.authentication[0].publicKey, - } - ), + didKeyToVerificationMethod(fromChain(identifier), authentication[0].id, { + keyType: authentication[0].type, + publicKey: authentication[0].publicKey, + }), ], } - if (didRec.keyAgreement !== undefined && didRec.keyAgreement.length > 0) { - did.keyAgreement = [] - didRec.keyAgreement.forEach(({ id, publicKey, type: keyType }) => { - did.keyAgreement?.push(id) - if (did.verificationMethod.find((vm) => vm.id === id) === undefined) { - did.verificationMethod.push( - didKeyToVerificationMethod(fromChain(identifier), id, { - keyType, - publicKey, - }) - ) - } + if (keyAgreement !== undefined && keyAgreement.length > 0) { + keyAgreement.forEach(({ id, publicKey, type }) => { + DidDetailsV2.addVerificationMethod( + did, + didKeyToVerificationMethod(fromChain(identifier), id, { + keyType: type, + publicKey, + }), + 'keyAgreement' + ) }) } - if (didRec.assertionMethod !== undefined) { - const { id, type, publicKey } = didRec.assertionMethod[0] - did.assertionMethod = [id] - if (did.verificationMethod.find((vm) => vm.id === id) === undefined) { - did.verificationMethod.push( - didKeyToVerificationMethod( - fromChain(identifier), - didRec.assertionMethod[0].id, - { - keyType: type, - publicKey, - } - ) - ) - } + if (assertionMethod !== undefined) { + const { id, type, publicKey } = assertionMethod[0] + DidDetailsV2.addVerificationMethod( + did, + didKeyToVerificationMethod(fromChain(identifier), id, { + keyType: type, + publicKey, + }), + 'assertionMethod' + ) } - if (didRec.capabilityDelegation !== undefined) { - const { id, type, publicKey } = didRec.capabilityDelegation[0] - did.capabilityDelegation = [id] - if (did.verificationMethod.find((vm) => vm.id === id) === undefined) { - did.verificationMethod.push( - didKeyToVerificationMethod(fromChain(identifier), id, { - keyType: type, - publicKey, - }) - ) - } + if (capabilityDelegation !== undefined) { + const { id, type, publicKey } = capabilityDelegation[0] + DidDetailsV2.addVerificationMethod( + did, + didKeyToVerificationMethod(fromChain(identifier), id, { + keyType: type, + publicKey, + }), + 'capabilityDelegation' + ) } const services = servicesFromChain(serviceEndpoints) diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index bac35a4fb6..fec1d5621c 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocumentV2 } from '@kiltprotocol/types' +import { DidDocumentV2 } from '@kiltprotocol/types' /** * Possible types for a DID verification key. @@ -51,3 +51,40 @@ export type NewDidVerificationKey = BaseNewDidKey & { export type NewDidEncryptionKey = BaseNewDidKey & { type: DidEncryptionKeyType } + +function isVerificationMethodRelationship( + input: unknown +): input is DidDocumentV2.VerificationMethodRelationship { + if ( + input === 'authentication' || + input === 'capabilityDelegation' || + input === 'assertionMethod' || + input === 'keyAgreement' + ) { + return true + } + return false +} + +function doesVerificationMethodExist( + didDocument: DidDocumentV2.DidDocument, + { id }: Pick +): boolean { + return didDocument.verificationMethod.find((vm) => vm.id === id) !== undefined +} + +export function addVerificationMethod( + didDocument: DidDocumentV2.DidDocument, + verificationMethod: DidDocumentV2.VerificationMethod, + relationship: DidDocumentV2.VerificationMethodRelationship +): void { + if (isVerificationMethodRelationship(relationship)) { + const existingRelationship = didDocument[relationship] ?? [] + existingRelationship.push(verificationMethod.id) + // eslint-disable-next-line no-param-reassign + didDocument[relationship] = existingRelationship + if (!doesVerificationMethodExist(didDocument, verificationMethod)) { + didDocument.verificationMethod.push(verificationMethod) + } + } +} diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index 677cbe9fca..e476b8c1fe 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -24,6 +24,7 @@ import { parse, } from '../Did2.utils.js' import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' +import { addVerificationMethod } from './DidDetailsV2.js' import type { NewService, DidVerificationKeyType } from './DidDetailsV2.js' /** @@ -221,18 +222,15 @@ export function createLightDidDocument({ } if (keyAgreement !== undefined) { - did.keyAgreement = [encryptionKeyId] - if ( - did.verificationMethod.find((vm) => vm.id === encryptionKeyId) === - undefined - ) { - did.verificationMethod.push( - didKeyToVerificationMethod(uri, encryptionKeyId, { - keyType: keyAgreement[0].type, - publicKey: keyAgreement[0].publicKey, - }) - ) - } + const { publicKey, type } = keyAgreement[0] + addVerificationMethod( + did, + didKeyToVerificationMethod(uri, encryptionKeyId, { + keyType: type, + publicKey, + }), + 'keyAgreement' + ) } return did diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index bf20cf0475..8f63771a5c 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -16,6 +16,7 @@ import { parse, validateUri, } from '../Did2.utils.js' +import { addVerificationMethod } from '../DidDetailsv2/DidDetailsV2.js' import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' @@ -60,39 +61,41 @@ async function resolveInternal( ], service: document.service, } - if (document.assertionMethod !== undefined) { - newDidDocument.assertionMethod = document.assertionMethod.map((k) => k.id) - newDidDocument.verificationMethod.push( - ...document.assertionMethod.map((k) => - didKeyToVerificationMethod(did, k.id, { - keyType: k.type, - publicKey: k.publicKey, - }) + if ( + document.keyAgreement !== undefined && + document.keyAgreement.length > 0 + ) { + document.keyAgreement.forEach(({ id, type: keyType, publicKey }) => { + addVerificationMethod( + newDidDocument, + didKeyToVerificationMethod(newDidDocument.id, id, { + keyType, + publicKey, + }), + 'keyAgreement' ) - ) + }) } - if (document.keyAgreement !== undefined) { - newDidDocument.keyAgreement = document.keyAgreement.map((k) => k.id) - newDidDocument.verificationMethod.push( - ...document.keyAgreement.map((k) => - didKeyToVerificationMethod(did, k.id, { - keyType: k.type, - publicKey: k.publicKey, - }) - ) + if (document.assertionMethod !== undefined) { + const { id, publicKey, type: keyType } = document.assertionMethod[0] + addVerificationMethod( + newDidDocument, + didKeyToVerificationMethod(newDidDocument.id, id, { + keyType, + publicKey, + }), + 'assertionMethod' ) } if (document.capabilityDelegation !== undefined) { - newDidDocument.capabilityDelegation = document.capabilityDelegation.map( - (k) => k.id - ) - newDidDocument.verificationMethod.push( - ...document.capabilityDelegation.map((k) => - didKeyToVerificationMethod(did, k.id, { - keyType: k.type, - publicKey: k.publicKey, - }) - ) + const { id, publicKey, type: keyType } = document.capabilityDelegation[0] + addVerificationMethod( + newDidDocument, + didKeyToVerificationMethod(newDidDocument.id, id, { + keyType, + publicKey, + }), + 'capabilityDelegation' ) } if (web3Name !== undefined) { diff --git a/packages/types/src/DidDocumentV2.ts b/packages/types/src/DidDocumentV2.ts index 48ce64ff11..d7736bd2b1 100644 --- a/packages/types/src/DidDocumentV2.ts +++ b/packages/types/src/DidDocumentV2.ts @@ -31,7 +31,7 @@ export type SignatureVerificationMethodRelationship = | 'authentication' | 'capabilityDelegation' | 'assertionMethod' -export type EncryptionMethodRelationship = 'keyAgreementKey' +export type EncryptionMethodRelationship = 'keyAgreement' export type VerificationMethodRelationship = | SignatureVerificationMethodRelationship diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 0850bb2949..5d13212e42 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -355,53 +355,38 @@ export async function createFullDidFromLightDid( service, } if (assertionMethod !== undefined) { - didDocument.assertionMethod = [assertionMethod[0].id] - if ( - didDocument.verificationMethod.find( - (vm) => vm.id === assertionMethod[0].id - ) === undefined - ) { - didDocument.verificationMethod.push( - Did.DidUtilsV2.didKeyToVerificationMethod(uri, assertionMethod[0].id, { - keyType: assertionMethod[0].type, - publicKey: assertionMethod[0].publicKey, - }) - ) - } + const { id, publicKey, type: keyType } = assertionMethod[0] + Did.DidDetailsV2.addVerificationMethod( + didDocument, + Did.DidUtilsV2.didKeyToVerificationMethod(didDocument.id, id, { + keyType, + publicKey, + }), + 'assertionMethod' + ) } if (capabilityDelegation !== undefined) { - didDocument.capabilityDelegation = [capabilityDelegation[0].id] - if ( - didDocument.verificationMethod.find( - (vm) => vm.id === capabilityDelegation[0].id - ) === undefined - ) { - didDocument.verificationMethod.push( - Did.DidUtilsV2.didKeyToVerificationMethod( - uri, - capabilityDelegation[0].id, - { - keyType: capabilityDelegation[0].type, - publicKey: capabilityDelegation[0].publicKey, - } - ) - ) - } + const { id, publicKey, type: keyType } = capabilityDelegation[0] + Did.DidDetailsV2.addVerificationMethod( + didDocument, + Did.DidUtilsV2.didKeyToVerificationMethod(didDocument.id, id, { + keyType, + publicKey, + }), + 'capabilityDelegation' + ) } - if (keyAgreement !== undefined) { - didDocument.keyAgreement = [keyAgreement[0].id] - if ( - didDocument.verificationMethod.find( - (vm) => vm.id === keyAgreement[0].id - ) === undefined - ) { - didDocument.verificationMethod.push( - Did.DidUtilsV2.didKeyToVerificationMethod(uri, keyAgreement[0].id, { - keyType: keyAgreement[0].type, - publicKey: keyAgreement[0].publicKey, - }) + if (keyAgreement !== undefined && keyAgreement.length > 0) { + keyAgreement.forEach(({ id, type: keyType, publicKey }) => { + Did.DidDetailsV2.addVerificationMethod( + didDocument, + Did.DidUtilsV2.didKeyToVerificationMethod(didDocument.id, id, { + keyType, + publicKey, + }), + 'keyAgreement' ) - } + }) } return didDocument From 934715ee1f60d280a3dca08baf41cf600e058463 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 11:37:20 +0100 Subject: [PATCH 21/78] Add utility function to add a verification method --- packages/did/src/Did2.rpc.ts | 21 ++++-------- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 17 ++++++++++ .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 9 ++--- packages/did/src/DidResolver/DidResolverV2.ts | 23 ++++--------- tests/testUtils/TestUtils2.ts | 33 ++++++++++--------- 5 files changed, 51 insertions(+), 52 deletions(-) diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index 0b49876752..4dac3cec6c 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -167,12 +167,9 @@ export function linkedInfoFromChain( if (keyAgreement !== undefined && keyAgreement.length > 0) { keyAgreement.forEach(({ id, publicKey, type }) => { - DidDetailsV2.addVerificationMethod( + DidDetailsV2.addKeyAsVerificationMethod( did, - didKeyToVerificationMethod(fromChain(identifier), id, { - keyType: type, - publicKey, - }), + { id, publicKey, type }, 'keyAgreement' ) }) @@ -180,24 +177,18 @@ export function linkedInfoFromChain( if (assertionMethod !== undefined) { const { id, type, publicKey } = assertionMethod[0] - DidDetailsV2.addVerificationMethod( + DidDetailsV2.addKeyAsVerificationMethod( did, - didKeyToVerificationMethod(fromChain(identifier), id, { - keyType: type, - publicKey, - }), + { id, publicKey, type }, 'assertionMethod' ) } if (capabilityDelegation !== undefined) { const { id, type, publicKey } = capabilityDelegation[0] - DidDetailsV2.addVerificationMethod( + DidDetailsV2.addKeyAsVerificationMethod( did, - didKeyToVerificationMethod(fromChain(identifier), id, { - keyType: type, - publicKey, - }), + { id, publicKey, type }, 'capabilityDelegation' ) } diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index fec1d5621c..6cfe5cb197 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -6,6 +6,7 @@ */ import { DidDocumentV2 } from '@kiltprotocol/types' +import { didKeyToVerificationMethod } from '../Did2.utils' /** * Possible types for a DID verification key. @@ -88,3 +89,19 @@ export function addVerificationMethod( } } } + +export function addKeyAsVerificationMethod( + didDocument: DidDocumentV2.DidDocument, + { + id, + publicKey, + type: keyType, + }: BaseNewDidKey & { id: DidDocumentV2.UriFragment }, + relationship: DidDocumentV2.VerificationMethodRelationship +): void { + const verificationMethod = didKeyToVerificationMethod(didDocument.id, id, { + keyType: keyType as DidKeyType, + publicKey, + }) + addVerificationMethod(didDocument, verificationMethod, relationship) +} diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index e476b8c1fe..60d8d740a5 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -24,7 +24,7 @@ import { parse, } from '../Did2.utils.js' import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' -import { addVerificationMethod } from './DidDetailsV2.js' +import { addKeyAsVerificationMethod } from './DidDetailsV2.js' import type { NewService, DidVerificationKeyType } from './DidDetailsV2.js' /** @@ -223,12 +223,9 @@ export function createLightDidDocument({ if (keyAgreement !== undefined) { const { publicKey, type } = keyAgreement[0] - addVerificationMethod( + addKeyAsVerificationMethod( did, - didKeyToVerificationMethod(uri, encryptionKeyId, { - keyType: type, - publicKey, - }), + { id: encryptionKeyId, publicKey, type }, 'keyAgreement' ) } diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index 8f63771a5c..67c7a0458b 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -16,7 +16,7 @@ import { parse, validateUri, } from '../Did2.utils.js' -import { addVerificationMethod } from '../DidDetailsv2/DidDetailsV2.js' +import { addKeyAsVerificationMethod } from '../DidDetailsv2/DidDetailsV2.js' import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' @@ -66,35 +66,26 @@ async function resolveInternal( document.keyAgreement.length > 0 ) { document.keyAgreement.forEach(({ id, type: keyType, publicKey }) => { - addVerificationMethod( + addKeyAsVerificationMethod( newDidDocument, - didKeyToVerificationMethod(newDidDocument.id, id, { - keyType, - publicKey, - }), + { id, publicKey, type: keyType }, 'keyAgreement' ) }) } if (document.assertionMethod !== undefined) { const { id, publicKey, type: keyType } = document.assertionMethod[0] - addVerificationMethod( + addKeyAsVerificationMethod( newDidDocument, - didKeyToVerificationMethod(newDidDocument.id, id, { - keyType, - publicKey, - }), + { id, publicKey, type: keyType }, 'assertionMethod' ) } if (document.capabilityDelegation !== undefined) { const { id, publicKey, type: keyType } = document.capabilityDelegation[0] - addVerificationMethod( + addKeyAsVerificationMethod( newDidDocument, - didKeyToVerificationMethod(newDidDocument.id, id, { - keyType, - publicKey, - }), + { id, publicKey, type: keyType }, 'capabilityDelegation' ) } diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 5d13212e42..b9c3d4b34a 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -355,35 +355,38 @@ export async function createFullDidFromLightDid( service, } if (assertionMethod !== undefined) { - const { id, publicKey, type: keyType } = assertionMethod[0] - Did.DidDetailsV2.addVerificationMethod( + const { id, publicKey, type } = assertionMethod[0] + Did.DidDetailsV2.addKeyAsVerificationMethod( didDocument, - Did.DidUtilsV2.didKeyToVerificationMethod(didDocument.id, id, { - keyType, + { + id, publicKey, - }), + type, + }, 'assertionMethod' ) } if (capabilityDelegation !== undefined) { - const { id, publicKey, type: keyType } = capabilityDelegation[0] - Did.DidDetailsV2.addVerificationMethod( + const { id, publicKey, type } = capabilityDelegation[0] + Did.DidDetailsV2.addKeyAsVerificationMethod( didDocument, - Did.DidUtilsV2.didKeyToVerificationMethod(didDocument.id, id, { - keyType, + { + id, publicKey, - }), + type, + }, 'capabilityDelegation' ) } if (keyAgreement !== undefined && keyAgreement.length > 0) { - keyAgreement.forEach(({ id, type: keyType, publicKey }) => { - Did.DidDetailsV2.addVerificationMethod( + keyAgreement.forEach(({ id, type, publicKey }) => { + Did.DidDetailsV2.addKeyAsVerificationMethod( didDocument, - Did.DidUtilsV2.didKeyToVerificationMethod(didDocument.id, id, { - keyType, + { + id, publicKey, - }), + type, + }, 'keyAgreement' ) }) From 6948e37fd97e96c272cb8448dd6e9960ff9b97b7 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 14:13:45 +0100 Subject: [PATCH 22/78] Minor refactoring --- packages/did/src/Did2.rpc.ts | 6 +++--- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 4 ++-- packages/did/src/DidDetailsv2/LightDidDetailsV2.ts | 4 ++-- packages/did/src/DidResolver/DidResolverV2.ts | 8 ++++---- tests/testUtils/TestUtils2.ts | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index 4dac3cec6c..b0d5551c34 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -167,7 +167,7 @@ export function linkedInfoFromChain( if (keyAgreement !== undefined && keyAgreement.length > 0) { keyAgreement.forEach(({ id, publicKey, type }) => { - DidDetailsV2.addKeyAsVerificationMethod( + DidDetailsV2.addKeypairAsVerificationMethod( did, { id, publicKey, type }, 'keyAgreement' @@ -177,7 +177,7 @@ export function linkedInfoFromChain( if (assertionMethod !== undefined) { const { id, type, publicKey } = assertionMethod[0] - DidDetailsV2.addKeyAsVerificationMethod( + DidDetailsV2.addKeypairAsVerificationMethod( did, { id, publicKey, type }, 'assertionMethod' @@ -186,7 +186,7 @@ export function linkedInfoFromChain( if (capabilityDelegation !== undefined) { const { id, type, publicKey } = capabilityDelegation[0] - DidDetailsV2.addKeyAsVerificationMethod( + DidDetailsV2.addKeypairAsVerificationMethod( did, { id, publicKey, type }, 'capabilityDelegation' diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 6cfe5cb197..281ef991dd 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -6,7 +6,7 @@ */ import { DidDocumentV2 } from '@kiltprotocol/types' -import { didKeyToVerificationMethod } from '../Did2.utils' +import { didKeyToVerificationMethod } from '../Did2.utils.js' /** * Possible types for a DID verification key. @@ -90,7 +90,7 @@ export function addVerificationMethod( } } -export function addKeyAsVerificationMethod( +export function addKeypairAsVerificationMethod( didDocument: DidDocumentV2.DidDocument, { id, diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index 60d8d740a5..b03deeeb60 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -24,7 +24,7 @@ import { parse, } from '../Did2.utils.js' import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' -import { addKeyAsVerificationMethod } from './DidDetailsV2.js' +import { addKeypairAsVerificationMethod } from './DidDetailsV2.js' import type { NewService, DidVerificationKeyType } from './DidDetailsV2.js' /** @@ -223,7 +223,7 @@ export function createLightDidDocument({ if (keyAgreement !== undefined) { const { publicKey, type } = keyAgreement[0] - addKeyAsVerificationMethod( + addKeypairAsVerificationMethod( did, { id: encryptionKeyId, publicKey, type }, 'keyAgreement' diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index 67c7a0458b..cef783f599 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -16,7 +16,7 @@ import { parse, validateUri, } from '../Did2.utils.js' -import { addKeyAsVerificationMethod } from '../DidDetailsv2/DidDetailsV2.js' +import { addKeypairAsVerificationMethod } from '../DidDetailsv2/DidDetailsV2.js' import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' @@ -66,7 +66,7 @@ async function resolveInternal( document.keyAgreement.length > 0 ) { document.keyAgreement.forEach(({ id, type: keyType, publicKey }) => { - addKeyAsVerificationMethod( + addKeypairAsVerificationMethod( newDidDocument, { id, publicKey, type: keyType }, 'keyAgreement' @@ -75,7 +75,7 @@ async function resolveInternal( } if (document.assertionMethod !== undefined) { const { id, publicKey, type: keyType } = document.assertionMethod[0] - addKeyAsVerificationMethod( + addKeypairAsVerificationMethod( newDidDocument, { id, publicKey, type: keyType }, 'assertionMethod' @@ -83,7 +83,7 @@ async function resolveInternal( } if (document.capabilityDelegation !== undefined) { const { id, publicKey, type: keyType } = document.capabilityDelegation[0] - addKeyAsVerificationMethod( + addKeypairAsVerificationMethod( newDidDocument, { id, publicKey, type: keyType }, 'capabilityDelegation' diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index b9c3d4b34a..79b6e4f2d0 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -356,7 +356,7 @@ export async function createFullDidFromLightDid( } if (assertionMethod !== undefined) { const { id, publicKey, type } = assertionMethod[0] - Did.DidDetailsV2.addKeyAsVerificationMethod( + Did.DidDetailsV2.addKeypairAsVerificationMethod( didDocument, { id, @@ -368,7 +368,7 @@ export async function createFullDidFromLightDid( } if (capabilityDelegation !== undefined) { const { id, publicKey, type } = capabilityDelegation[0] - Did.DidDetailsV2.addKeyAsVerificationMethod( + Did.DidDetailsV2.addKeypairAsVerificationMethod( didDocument, { id, @@ -380,7 +380,7 @@ export async function createFullDidFromLightDid( } if (keyAgreement !== undefined && keyAgreement.length > 0) { keyAgreement.forEach(({ id, type, publicKey }) => { - Did.DidDetailsV2.addKeyAsVerificationMethod( + Did.DidDetailsV2.addKeypairAsVerificationMethod( didDocument, { id, From f2fc475b95ffb9948a921d56ae5e354a8d1cc3ac Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 14:18:17 +0100 Subject: [PATCH 23/78] Revert unwanted change --- packages/did/src/DidDetails/LightDidDetails.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 98deecc078..30a752f6bd 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -25,7 +25,6 @@ import { SDKErrors, ss58Format, cbor } from '@kiltprotocol/utils' import { getAddressByKey, parse } from '../Did.utils.js' import { resourceIdToChain, validateService } from '../Did.chain.js' -import { NewService } from '../DidDetailsv2/DidDetailsV2.js' const authenticationKeyId = '#authentication' const encryptionKeyId = '#encryption' @@ -66,7 +65,7 @@ export type CreateDocumentInput = { * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ - service?: NewService[] + service?: DidServiceEndpoint[] } function validateCreateDocumentInput({ From 9f54ea29299990fe5ff751ecf69eb6291709caf0 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 15:31:53 +0100 Subject: [PATCH 24/78] Remove addKeypairAsVerificationMethod function --- packages/did/src/Did2.rpc.ts | 8 ++-- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 37 +++--------------- packages/did/src/DidDetailsv2/index.ts | 12 +++++- tests/testUtils/TestUtils2.ts | 38 +++++++++++++++++-- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index b0d5551c34..e292b8fc21 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -30,7 +30,7 @@ import { fromChain, publicKeyFromChain, } from './Did2.chain.js' -import { DidDetailsV2 } from './index.js' +import { addKeypairAsVerificationMethod } from './DidDetailsv2/DidDetailsV2.js' function documentFromChain( encoded: DidDidDetails @@ -167,7 +167,7 @@ export function linkedInfoFromChain( if (keyAgreement !== undefined && keyAgreement.length > 0) { keyAgreement.forEach(({ id, publicKey, type }) => { - DidDetailsV2.addKeypairAsVerificationMethod( + addKeypairAsVerificationMethod( did, { id, publicKey, type }, 'keyAgreement' @@ -177,7 +177,7 @@ export function linkedInfoFromChain( if (assertionMethod !== undefined) { const { id, type, publicKey } = assertionMethod[0] - DidDetailsV2.addKeypairAsVerificationMethod( + addKeypairAsVerificationMethod( did, { id, publicKey, type }, 'assertionMethod' @@ -186,7 +186,7 @@ export function linkedInfoFromChain( if (capabilityDelegation !== undefined) { const { id, type, publicKey } = capabilityDelegation[0] - DidDetailsV2.addKeypairAsVerificationMethod( + addKeypairAsVerificationMethod( did, { id, publicKey, type }, 'capabilityDelegation' diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 281ef991dd..24b289056d 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -53,41 +53,16 @@ export type NewDidEncryptionKey = BaseNewDidKey & { type: DidEncryptionKeyType } -function isVerificationMethodRelationship( - input: unknown -): input is DidDocumentV2.VerificationMethodRelationship { - if ( - input === 'authentication' || - input === 'capabilityDelegation' || - input === 'assertionMethod' || - input === 'keyAgreement' - ) { - return true - } - return false -} - -function doesVerificationMethodExist( - didDocument: DidDocumentV2.DidDocument, - { id }: Pick -): boolean { - return didDocument.verificationMethod.find((vm) => vm.id === id) !== undefined -} - -export function addVerificationMethod( +function addVerificationMethod( didDocument: DidDocumentV2.DidDocument, verificationMethod: DidDocumentV2.VerificationMethod, relationship: DidDocumentV2.VerificationMethodRelationship ): void { - if (isVerificationMethodRelationship(relationship)) { - const existingRelationship = didDocument[relationship] ?? [] - existingRelationship.push(verificationMethod.id) - // eslint-disable-next-line no-param-reassign - didDocument[relationship] = existingRelationship - if (!doesVerificationMethodExist(didDocument, verificationMethod)) { - didDocument.verificationMethod.push(verificationMethod) - } - } + const existingRelationship = didDocument[relationship] ?? [] + existingRelationship.push(verificationMethod.id) + // eslint-disable-next-line no-param-reassign + didDocument[relationship] = existingRelationship + didDocument.verificationMethod.push(verificationMethod) } export function addKeypairAsVerificationMethod( diff --git a/packages/did/src/DidDetailsv2/index.ts b/packages/did/src/DidDetailsv2/index.ts index 9977d6fdb4..ce1499364b 100644 --- a/packages/did/src/DidDetailsv2/index.ts +++ b/packages/did/src/DidDetailsv2/index.ts @@ -5,6 +5,16 @@ * found in the LICENSE file in the root directory of this source tree. */ -export * from './DidDetailsV2.js' +// We don't export the `add*VerificationMethod` functions, they are meant to be used internally +export { + BaseNewDidKey, + DidEncryptionKeyType, + DidKeyType, + DidVerificationKeyType, + NewDidEncryptionKey, + NewDidVerificationKey, + NewService, + NewVerificationMethod, +} from './DidDetailsV2.js' export * from './LightDidDetailsV2.js' export * from './FullDidDetailsV2.js' diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 79b6e4f2d0..bb3ede035c 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -309,6 +309,38 @@ export async function createLocalDemoFullDidFromLightDid( } } +function addVerificationMethod( + didDocument: DidDocumentV2.DidDocument, + verificationMethod: DidDocumentV2.VerificationMethod, + relationship: DidDocumentV2.VerificationMethodRelationship +): void { + const existingRelationship = didDocument[relationship] ?? [] + existingRelationship.push(verificationMethod.id) + // eslint-disable-next-line no-param-reassign + didDocument[relationship] = existingRelationship + didDocument.verificationMethod.push(verificationMethod) +} + +function addKeypairAsVerificationMethod( + didDocument: DidDocumentV2.DidDocument, + { + id, + publicKey, + type: keyType, + }: Did.DidDetailsV2.BaseNewDidKey & { id: DidDocumentV2.UriFragment }, + relationship: DidDocumentV2.VerificationMethodRelationship +): void { + const verificationMethod = Did.DidUtilsV2.didKeyToVerificationMethod( + didDocument.id, + id, + { + keyType: keyType as Did.DidDetailsV2.DidKeyType, + publicKey, + } + ) + addVerificationMethod(didDocument, verificationMethod, relationship) +} + // It takes the auth key from the light DID and use it as attestation and delegation key as well. export async function createFullDidFromLightDid( payer: KiltKeyringPair, @@ -356,7 +388,7 @@ export async function createFullDidFromLightDid( } if (assertionMethod !== undefined) { const { id, publicKey, type } = assertionMethod[0] - Did.DidDetailsV2.addKeypairAsVerificationMethod( + addKeypairAsVerificationMethod( didDocument, { id, @@ -368,7 +400,7 @@ export async function createFullDidFromLightDid( } if (capabilityDelegation !== undefined) { const { id, publicKey, type } = capabilityDelegation[0] - Did.DidDetailsV2.addKeypairAsVerificationMethod( + addKeypairAsVerificationMethod( didDocument, { id, @@ -380,7 +412,7 @@ export async function createFullDidFromLightDid( } if (keyAgreement !== undefined && keyAgreement.length > 0) { keyAgreement.forEach(({ id, type, publicKey }) => { - Did.DidDetailsV2.addKeypairAsVerificationMethod( + addKeypairAsVerificationMethod( didDocument, { id, From 42378caf2c676afa4124e55402ab5344d0555a60 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 15:47:10 +0100 Subject: [PATCH 25/78] Light DID unit tests passing --- .../DidDetailsv2/LightDidDetailsV2.spec.ts | 237 ++++++++++++++++++ tests/testUtils/TestUtils2.ts | 134 ++++++---- 2 files changed, 322 insertions(+), 49 deletions(-) create mode 100644 packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts new file mode 100644 index 0000000000..bbe4fc7d45 --- /dev/null +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { BN } from '@polkadot/util' +import { randomAsHex } from '@polkadot/util-crypto' + +import { ConfigService } from '@kiltprotocol/config' +import type { + CryptoCallbacksV2, + DidDocumentV2, + KiltKeyringPair, + SubmittableExtrinsic, +} from '@kiltprotocol/types' + +import { + ApiMocks, + TestUtilsV2 +} from '../../../../tests/testUtils' +import { generateDidAuthenticatedTx } from '../Did2.chain.js' +import * as Did from './index.js' + +const augmentedApi = ApiMocks.createAugmentedApi() +const mockedApi: any = ApiMocks.getMockedApi() +ConfigService.set({ api: mockedApi }) + +jest.mock('../Did2.chain') +jest + .mocked(generateDidAuthenticatedTx) + .mockResolvedValue({} as SubmittableExtrinsic) + +/* + * Functions tested in integration tests: + * - getKeysForExtrinsic + * - authorizeExtrinsic + */ + +describe('When creating an instance from the chain', () => { + describe('authorizeBatch', () => { + let keypair: KiltKeyringPair + let sign: CryptoCallbacksV2.SignCallback + let fullDid: DidDocumentV2.DidDocument + + beforeAll(async () => { + const keyTool = TestUtilsV2.makeSigningKeyTool() + keypair = keyTool.keypair + fullDid = await TestUtilsV2.createLocalDemoFullDidFromKeypair( + keyTool.keypair + ) + sign = keyTool.getSignCallback(fullDid) + }) + + describe('.addSingleTx()', () => { + it('fails if the extrinsic does not require a DID', async () => { + const extrinsic = augmentedApi.tx.indices.claim(1) + await expect(async () => + Did.authorizeBatch({ + did: fullDid.id, + batchFunction: augmentedApi.tx.utility.batchAll, + extrinsics: [extrinsic, extrinsic], + sign, + submitter: keypair.address, + }) + ).rejects.toMatchInlineSnapshot( + '[DidBatchError: Can only batch extrinsics that require a DID signature]' + ) + }) + + it('fails if the extrinsic is a utility (batch) extrinsic containing valid extrinsics', async () => { + const extrinsic = augmentedApi.tx.utility.batch([ + await augmentedApi.tx.ctype.add('test-ctype'), + ]) + const batchFunction = + jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll + await Did.authorizeBatch({ + did: fullDid.id, + batchFunction, + extrinsics: [extrinsic, extrinsic], + sign, + submitter: keypair.address, + }) + + expect(batchFunction).toHaveBeenCalledWith([extrinsic, extrinsic]) + }) + + it('adds different batches requiring different keys', async () => { + const ctype1Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + const ctype2Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + const delegationExtrinsic1 = + await augmentedApi.tx.delegation.createHierarchy( + randomAsHex(32), + randomAsHex(32) + ) + const delegationExtrinsic2 = + await augmentedApi.tx.delegation.createHierarchy( + randomAsHex(32), + randomAsHex(32) + ) + const ctype3Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + const ctype4Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + + const batchFunction = + jest.fn() as unknown as typeof augmentedApi.tx.utility.batchAll + const extrinsics = [ + ctype1Extrinsic, + ctype2Extrinsic, + delegationExtrinsic1, + delegationExtrinsic2, + ctype3Extrinsic, + ctype4Extrinsic, + ] + await Did.authorizeBatch({ + did: fullDid.id, + batchFunction, + extrinsics, + nonce: new BN(0), + sign, + submitter: keypair.address, + }) + + expect(batchFunction).toHaveBeenCalledWith([ + ctype1Extrinsic, + ctype2Extrinsic, + ]) + expect(batchFunction).toHaveBeenCalledWith([ + delegationExtrinsic1, + delegationExtrinsic2, + ]) + expect(batchFunction).toHaveBeenCalledWith([ + ctype3Extrinsic, + ctype4Extrinsic, + ]) + }) + }) + + // TODO: complete these tests once SDK has been refactored to work with generic api object + describe('.build()', () => { + it('throws if batch is empty', async () => { + await expect(async () => + Did.authorizeBatch({ + did: fullDid.id, + batchFunction: augmentedApi.tx.utility.batchAll, + extrinsics: [], + sign, + submitter: keypair.address, + }) + ).rejects.toMatchInlineSnapshot( + '[DidBatchError: Cannot build a batch with no transactions]' + ) + }) + it.todo('successfully create a batch with only 1 extrinsic') + it.todo('successfully create a batch with 1 extrinsic per required key') + it.todo('successfully create a batch with 2 extrinsics per required key') + it.todo( + 'successfully create a batch with 1 extrinsic per required key, repeated two times' + ) + }) + }) +}) + +const mockApi = ApiMocks.createAugmentedApi() + +describe('When creating an instance from the chain', () => { + it('Should return correct KeyRelationship for single valid call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return correct KeyRelationship for batched call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.batch([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + ]) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return correct KeyRelationship for batchAll call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.batchAll([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + ]) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return correct KeyRelationship for forceBatch call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.forceBatch([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + ]) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return undefined for batch with mixed KeyRelationship calls', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.forceBatch([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.web3Names.claim('awesomename'), + ]) + ) + expect(keyRelationship).toBeUndefined() + }) +}) diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index bb3ede035c..22c0cd9972 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -197,6 +197,38 @@ export function makeSigningKeyTool( } } +function addVerificationMethod( + didDocument: DidDocumentV2.DidDocument, + verificationMethod: DidDocumentV2.VerificationMethod, + relationship: DidDocumentV2.VerificationMethodRelationship +): void { + const existingRelationship = didDocument[relationship] ?? [] + existingRelationship.push(verificationMethod.id) + // eslint-disable-next-line no-param-reassign + didDocument[relationship] = existingRelationship + didDocument.verificationMethod.push(verificationMethod) +} + +function addKeypairAsVerificationMethod( + didDocument: DidDocumentV2.DidDocument, + { + id, + publicKey, + type: keyType, + }: Did.DidDetailsV2.BaseNewDidKey & { id: DidDocumentV2.UriFragment }, + relationship: DidDocumentV2.VerificationMethodRelationship +): void { + const verificationMethod = Did.DidUtilsV2.didKeyToVerificationMethod( + didDocument.id, + id, + { + keyType: keyType as Did.DidDetailsV2.DidKeyType, + publicKey, + } + ) + addVerificationMethod(didDocument, verificationMethod, relationship) +} + /** * Given a keypair, creates a light DID with an authentication and an encryption key. * @@ -253,36 +285,72 @@ export async function createLocalDemoFullDidFromKeypair( keyRelationships?: Set> endpoints?: DidServiceEndpoint[] } = {} -): Promise { - const authKey = makeDidKeyFromKeypair(keypair) - const uri = Did.getFullDidUriFromKey(authKey) +): Promise { + const { + type: keyType, + publicKey, + id: authKeyId, + } = makeDidKeyFromKeypair(keypair) + const id = Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ + type: keyType, + publicKey, + }), + }) - const result: DidDocument = { - uri, - authentication: [authKey], + const result: DidDocumentV2.DidDocument = { + id, + authentication: [authKeyId], + verificationMethod: [ + Did.DidUtilsV2.didKeyToVerificationMethod(id, authKeyId, { + keyType, + publicKey, + }), + ], service: endpoints, } if (keyRelationships.has('keyAgreement')) { - const encryptionKeypair = makeEncryptionKeyTool(`${keypair.publicKey}//enc`) - .keyAgreement[0] - const encKey = { - ...encryptionKeypair, - id: computeKeyId(encryptionKeypair.publicKey), - } - result.keyAgreement = [encKey] + const { publicKey: encPublicKey, type } = makeEncryptionKeyTool( + `${keypair.publicKey}//enc` + ).keyAgreement[0] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'keyAgreement' + ) } if (keyRelationships.has('assertionMethod')) { - const attKey = makeDidKeyFromKeypair( + const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//att') as KiltKeyringPair ) - result.assertionMethod = [attKey] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'assertionMethod' + ) } if (keyRelationships.has('capabilityDelegation')) { - const delKey = makeDidKeyFromKeypair( + const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//del') as KiltKeyringPair ) - result.capabilityDelegation = [delKey] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'capabilityDelegation' + ) } return result @@ -309,38 +377,6 @@ export async function createLocalDemoFullDidFromLightDid( } } -function addVerificationMethod( - didDocument: DidDocumentV2.DidDocument, - verificationMethod: DidDocumentV2.VerificationMethod, - relationship: DidDocumentV2.VerificationMethodRelationship -): void { - const existingRelationship = didDocument[relationship] ?? [] - existingRelationship.push(verificationMethod.id) - // eslint-disable-next-line no-param-reassign - didDocument[relationship] = existingRelationship - didDocument.verificationMethod.push(verificationMethod) -} - -function addKeypairAsVerificationMethod( - didDocument: DidDocumentV2.DidDocument, - { - id, - publicKey, - type: keyType, - }: Did.DidDetailsV2.BaseNewDidKey & { id: DidDocumentV2.UriFragment }, - relationship: DidDocumentV2.VerificationMethodRelationship -): void { - const verificationMethod = Did.DidUtilsV2.didKeyToVerificationMethod( - didDocument.id, - id, - { - keyType: keyType as Did.DidDetailsV2.DidKeyType, - publicKey, - } - ) - addVerificationMethod(didDocument, verificationMethod, relationship) -} - // It takes the auth key from the light DID and use it as attestation and delegation key as well. export async function createFullDidFromLightDid( payer: KiltKeyringPair, From 417a220343ae60475e91c7562c434f1f7e3683e2 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 16:01:33 +0100 Subject: [PATCH 26/78] Unit tests for light and full DID passing --- .../src/DidDetailsv2/FullDidDetailsV2.spec.ts | 236 +++++++++ .../DidDetailsv2/LightDidDetailsV2.spec.ts | 457 ++++++++++-------- 2 files changed, 485 insertions(+), 208 deletions(-) create mode 100644 packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts new file mode 100644 index 0000000000..e93d79ca7e --- /dev/null +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts @@ -0,0 +1,236 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { BN } from '@polkadot/util' +import { randomAsHex } from '@polkadot/util-crypto' + +import { ConfigService } from '@kiltprotocol/config' +import type { + CryptoCallbacksV2, + DidDocumentV2, + KiltKeyringPair, + SubmittableExtrinsic, +} from '@kiltprotocol/types' + +import { + ApiMocks, TestUtilsV2, +} from '../../../../tests/testUtils' +import { generateDidAuthenticatedTx } from '../Did2.chain.js' +import * as Did from './index.js' + +const augmentedApi = ApiMocks.createAugmentedApi() +const mockedApi: any = ApiMocks.getMockedApi() +ConfigService.set({ api: mockedApi }) + +jest.mock('../Did2.chain') +jest + .mocked(generateDidAuthenticatedTx) + .mockResolvedValue({} as SubmittableExtrinsic) + +/* + * Functions tested in integration tests: + * - getKeysForExtrinsic + * - authorizeExtrinsic + */ + +describe('When creating an instance from the chain', () => { + describe('authorizeBatch', () => { + let keypair: KiltKeyringPair + let sign: CryptoCallbacksV2.SignCallback + let fullDid: DidDocumentV2.DidDocument + + beforeAll(async () => { + const keyTool = TestUtilsV2.makeSigningKeyTool() + keypair = keyTool.keypair + fullDid = await TestUtilsV2.createLocalDemoFullDidFromKeypair( + keyTool.keypair + ) + sign = keyTool.getSignCallback(fullDid) + }) + + describe('.addSingleTx()', () => { + it('fails if the extrinsic does not require a DID', async () => { + const extrinsic = augmentedApi.tx.indices.claim(1) + await expect(async () => + Did.authorizeBatch({ + did: fullDid.id, + batchFunction: augmentedApi.tx.utility.batchAll, + extrinsics: [extrinsic, extrinsic], + sign, + submitter: keypair.address, + }) + ).rejects.toMatchInlineSnapshot( + '[DidBatchError: Can only batch extrinsics that require a DID signature]' + ) + }) + + it('fails if the extrinsic is a utility (batch) extrinsic containing valid extrinsics', async () => { + const extrinsic = augmentedApi.tx.utility.batch([ + await augmentedApi.tx.ctype.add('test-ctype'), + ]) + const batchFunction = + jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll + await Did.authorizeBatch({ + did: fullDid.id, + batchFunction, + extrinsics: [extrinsic, extrinsic], + sign, + submitter: keypair.address, + }) + + expect(batchFunction).toHaveBeenCalledWith([extrinsic, extrinsic]) + }) + + it('adds different batches requiring different keys', async () => { + const ctype1Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + const ctype2Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + const delegationExtrinsic1 = + await augmentedApi.tx.delegation.createHierarchy( + randomAsHex(32), + randomAsHex(32) + ) + const delegationExtrinsic2 = + await augmentedApi.tx.delegation.createHierarchy( + randomAsHex(32), + randomAsHex(32) + ) + const ctype3Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + const ctype4Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) + + const batchFunction = + jest.fn() as unknown as typeof augmentedApi.tx.utility.batchAll + const extrinsics = [ + ctype1Extrinsic, + ctype2Extrinsic, + delegationExtrinsic1, + delegationExtrinsic2, + ctype3Extrinsic, + ctype4Extrinsic, + ] + await Did.authorizeBatch({ + did: fullDid.id, + batchFunction, + extrinsics, + nonce: new BN(0), + sign, + submitter: keypair.address, + }) + + expect(batchFunction).toHaveBeenCalledWith([ + ctype1Extrinsic, + ctype2Extrinsic, + ]) + expect(batchFunction).toHaveBeenCalledWith([ + delegationExtrinsic1, + delegationExtrinsic2, + ]) + expect(batchFunction).toHaveBeenCalledWith([ + ctype3Extrinsic, + ctype4Extrinsic, + ]) + }) + }) + + // TODO: complete these tests once SDK has been refactored to work with generic api object + describe('.build()', () => { + it('throws if batch is empty', async () => { + await expect(async () => + Did.authorizeBatch({ + did: fullDid.id, + batchFunction: augmentedApi.tx.utility.batchAll, + extrinsics: [], + sign, + submitter: keypair.address, + }) + ).rejects.toMatchInlineSnapshot( + '[DidBatchError: Cannot build a batch with no transactions]' + ) + }) + it.todo('successfully create a batch with only 1 extrinsic') + it.todo('successfully create a batch with 1 extrinsic per required key') + it.todo('successfully create a batch with 2 extrinsics per required key') + it.todo( + 'successfully create a batch with 1 extrinsic per required key, repeated two times' + ) + }) + }) +}) + +const mockApi = ApiMocks.createAugmentedApi() + +describe('When creating an instance from the chain', () => { + it('Should return correct KeyRelationship for single valid call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return correct KeyRelationship for batched call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.batch([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + ]) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return correct KeyRelationship for batchAll call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.batchAll([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + ]) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return correct KeyRelationship for forceBatch call', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.forceBatch([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + ]) + ) + expect(keyRelationship).toBe('assertionMethod') + }) + it('Should return undefined for batch with mixed KeyRelationship calls', () => { + const keyRelationship = Did.getVerificationMethodRelationshipForTx( + mockApi.tx.utility.forceBatch([ + mockApi.tx.attestation.add( + new Uint8Array(32), + new Uint8Array(32), + null + ), + mockApi.tx.web3Names.claim('awesomename'), + ]) + ) + expect(keyRelationship).toBeUndefined() + }) +}) diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts index bbe4fc7d45..9d72164888 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts @@ -5,233 +5,274 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { BN } from '@polkadot/util' -import { randomAsHex } from '@polkadot/util-crypto' - -import { ConfigService } from '@kiltprotocol/config' -import type { - CryptoCallbacksV2, - DidDocumentV2, - KiltKeyringPair, - SubmittableExtrinsic, -} from '@kiltprotocol/types' - -import { - ApiMocks, - TestUtilsV2 -} from '../../../../tests/testUtils' -import { generateDidAuthenticatedTx } from '../Did2.chain.js' -import * as Did from './index.js' - -const augmentedApi = ApiMocks.createAugmentedApi() -const mockedApi: any = ApiMocks.getMockedApi() -ConfigService.set({ api: mockedApi }) +import { DidDocumentV2 } from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import { keypairToMultibaseKey, parse } from '../Did2.utils.js' -jest.mock('../Did2.chain') -jest - .mocked(generateDidAuthenticatedTx) - .mockResolvedValue({} as SubmittableExtrinsic) +import * as Did from './index.js' +import type { NewService } from './DidDetailsV2.js' /* + * Functions tested: + * - createLightDidDocument + * - parseDocumentFromLightDid + * * Functions tested in integration tests: * - getKeysForExtrinsic * - authorizeExtrinsic */ -describe('When creating an instance from the chain', () => { - describe('authorizeBatch', () => { - let keypair: KiltKeyringPair - let sign: CryptoCallbacksV2.SignCallback - let fullDid: DidDocumentV2.DidDocument - - beforeAll(async () => { - const keyTool = TestUtilsV2.makeSigningKeyTool() - keypair = keyTool.keypair - fullDid = await TestUtilsV2.createLocalDemoFullDidFromKeypair( - keyTool.keypair - ) - sign = keyTool.getSignCallback(fullDid) - }) +describe('When creating an instance from the details', () => { + it('correctly assign the right sr25519 authentication key, x25519 encryption key, and service endpoints', () => { + const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') + const encKey = Crypto.makeEncryptionKeypairFromSeed( + new Uint8Array(32).fill(1) + ) + const service: NewService[] = [ + { + id: '#service-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#service-2', + type: ['type-21', 'type-22'], + serviceEndpoint: ['x:url-21', 'x:url-22'], + }, + ] - describe('.addSingleTx()', () => { - it('fails if the extrinsic does not require a DID', async () => { - const extrinsic = augmentedApi.tx.indices.claim(1) - await expect(async () => - Did.authorizeBatch({ - did: fullDid.id, - batchFunction: augmentedApi.tx.utility.batchAll, - extrinsics: [extrinsic, extrinsic], - sign, - submitter: keypair.address, - }) - ).rejects.toMatchInlineSnapshot( - '[DidBatchError: Can only batch extrinsics that require a DID signature]' - ) - }) - - it('fails if the extrinsic is a utility (batch) extrinsic containing valid extrinsics', async () => { - const extrinsic = augmentedApi.tx.utility.batch([ - await augmentedApi.tx.ctype.add('test-ctype'), - ]) - const batchFunction = - jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll - await Did.authorizeBatch({ - did: fullDid.id, - batchFunction, - extrinsics: [extrinsic, extrinsic], - sign, - submitter: keypair.address, - }) - - expect(batchFunction).toHaveBeenCalledWith([extrinsic, extrinsic]) - }) - - it('adds different batches requiring different keys', async () => { - const ctype1Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - const ctype2Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - const delegationExtrinsic1 = - await augmentedApi.tx.delegation.createHierarchy( - randomAsHex(32), - randomAsHex(32) - ) - const delegationExtrinsic2 = - await augmentedApi.tx.delegation.createHierarchy( - randomAsHex(32), - randomAsHex(32) - ) - const ctype3Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - const ctype4Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - - const batchFunction = - jest.fn() as unknown as typeof augmentedApi.tx.utility.batchAll - const extrinsics = [ - ctype1Extrinsic, - ctype2Extrinsic, - delegationExtrinsic1, - delegationExtrinsic2, - ctype3Extrinsic, - ctype4Extrinsic, - ] - await Did.authorizeBatch({ - did: fullDid.id, - batchFunction, - extrinsics, - nonce: new BN(0), - sign, - submitter: keypair.address, - }) - - expect(batchFunction).toHaveBeenCalledWith([ - ctype1Extrinsic, - ctype2Extrinsic, - ]) - expect(batchFunction).toHaveBeenCalledWith([ - delegationExtrinsic1, - delegationExtrinsic2, - ]) - expect(batchFunction).toHaveBeenCalledWith([ - ctype3Extrinsic, - ctype4Extrinsic, - ]) - }) + const lightDid = Did.createLightDidDocument({ + authentication: [authKey], + keyAgreement: [encKey], + service, }) - // TODO: complete these tests once SDK has been refactored to work with generic api object - describe('.build()', () => { - it('throws if batch is empty', async () => { - await expect(async () => - Did.authorizeBatch({ - did: fullDid.id, - batchFunction: augmentedApi.tx.utility.batchAll, - extrinsics: [], - sign, - submitter: keypair.address, - }) - ).rejects.toMatchInlineSnapshot( - '[DidBatchError: Cannot build a batch with no transactions]' - ) - }) - it.todo('successfully create a batch with only 1 extrinsic') - it.todo('successfully create a batch with 1 extrinsic per required key') - it.todo('successfully create a batch with 2 extrinsics per required key') - it.todo( - 'successfully create a batch with 1 extrinsic per required key, repeated two times' - ) + expect(lightDid).toEqual({ + id: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ + { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + id: '#authentication', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'sr25519', + }), + type: 'MultiKey', + }, + { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + id: '#encryption', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'MultiKey', + }, + ], + service: [ + { + id: '#service-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#service-2', + type: ['type-21', 'type-22'], + serviceEndpoint: ['x:url-21', 'x:url-22'], + }, + ], }) }) -}) -const mockApi = ApiMocks.createAugmentedApi() - -describe('When creating an instance from the chain', () => { - it('Should return correct KeyRelationship for single valid call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( - mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) + it('correctly assign the right ed25519 authentication key and encryption key', () => { + const authKey = Crypto.makeKeypairFromSeed() + const encKey = Crypto.makeEncryptionKeypairFromSeed( + new Uint8Array(32).fill(1) ) - expect(keyRelationship).toBe('assertionMethod') + + const lightDid = Did.createLightDidDocument({ + authentication: [authKey], + keyAgreement: [encKey], + }) + + expect(parse(lightDid.id).address).toStrictEqual(authKey.address) + + expect(lightDid).toEqual({ + id: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ + { + controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, + id: '#authentication', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'ed25519', + }), + type: 'MultiKey', + }, + { + controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, + id: '#encryption', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'MultiKey', + }, + ], + }) }) - it('Should return correct KeyRelationship for batched call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( - mockApi.tx.utility.batch([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - ]) - ) - expect(keyRelationship).toBe('assertionMethod') + + it('throws for unsupported authentication key type', () => { + const authKey = Crypto.makeKeypairFromSeed(undefined, 'ecdsa') + const invalidInput = { + // Not an authentication key type + authentication: [authKey], + } + expect(() => + Did.createLightDidDocument( + invalidInput as unknown as Did.CreateDocumentInput + ) + ).toThrowError() }) - it('Should return correct KeyRelationship for batchAll call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( - mockApi.tx.utility.batchAll([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - ]) - ) - expect(keyRelationship).toBe('assertionMethod') + + it('throws for unsupported encryption key type', () => { + const authKey = Crypto.makeKeypairFromSeed() + const encKey = Crypto.makeEncryptionKeypairFromSeed() + const invalidInput = { + authentication: [authKey], + // Not an encryption key type + keyAgreement: [{ publicKey: encKey.publicKey, type: 'bls' }], + } + expect(() => + Did.createLightDidDocument( + invalidInput as unknown as Did.CreateDocumentInput + ) + ).toThrowError() }) - it('Should return correct KeyRelationship for forceBatch call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( - mockApi.tx.utility.forceBatch([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - ]) +}) + +describe('When creating an instance from a URI', () => { + it('correctly assign the right authentication key, encryption key, and service endpoints', () => { + const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') + const encKey = Crypto.makeEncryptionKeypairFromSeed( + new Uint8Array(32).fill(1) ) - expect(keyRelationship).toBe('assertionMethod') + const endpoints: NewService[] = [ + { + id: '#service-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#service-2', + type: ['type-21', 'type-22'], + serviceEndpoint: ['x:url-21', 'x:url-22'], + }, + ] + // We are sure this is correct because of the described case above + const expectedLightDid = Did.createLightDidDocument({ + authentication: [authKey], + keyAgreement: [encKey], + service: endpoints, + }) + + const { address } = parse(expectedLightDid.id) + const builtLightDid = Did.parseDocumentFromLightDid(expectedLightDid.id) + + expect(builtLightDid).toStrictEqual(expectedLightDid) + expect(builtLightDid).toStrictEqual({ + id: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ + { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + id: '#authentication', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'sr25519', + }), + type: 'MultiKey', + }, + { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + id: '#encryption', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'MultiKey', + }, + ], + service: [ + { + id: '#service-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#service-2', + type: ['type-21', 'type-22'], + serviceEndpoint: ['x:url-21', 'x:url-22'], + }, + ], + }) }) - it('Should return undefined for batch with mixed KeyRelationship calls', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( - mockApi.tx.utility.forceBatch([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.web3Names.claim('awesomename'), - ]) - ) - expect(keyRelationship).toBeUndefined() + + it('fail if a fragment is present according to the options', () => { + const authKey = Crypto.makeKeypairFromSeed() + const encKey = Crypto.makeEncryptionKeypairFromSeed() + const service: NewService[] = [ + { + id: '#service-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#service-2', + type: ['type-21', 'type-22'], + serviceEndpoint: ['x:url-21', 'x:url-22'], + }, + ] + + // We are sure this is correct because of the described case above + const expectedLightDid = Did.createLightDidDocument({ + authentication: [authKey], + keyAgreement: [encKey], + service, + }) + + const uriWithFragment: DidDocumentV2.DidUri = `${expectedLightDid.id}#authentication` + + expect(() => Did.parseDocumentFromLightDid(uriWithFragment, true)).toThrow() + expect(() => + Did.parseDocumentFromLightDid(uriWithFragment, false) + ).not.toThrow() + }) + + it('fail if the URI is not correct', () => { + const validKiltAddress = Crypto.makeKeypairFromSeed() + const incorrectURIs = [ + 'did:kilt:light:sdasdsadas', + // @ts-ignore not a valid DID uri + 'random-uri', + 'did:kilt:light', + 'did:kilt:light:', + // Wrong auth key encoding + `did:kilt:light:11${validKiltAddress}`, + // Full DID + `did:kilt:${validKiltAddress}`, + // Random encoded details + `did:kilt:light:00${validKiltAddress}:randomdetails`, + ] + incorrectURIs.forEach((uri) => { + expect(() => + Did.parseDocumentFromLightDid(uri as DidDocumentV2.DidUri) + ).toThrow() + }) }) }) From c721c527ed20a7b5f0534a3e44083d5d30b20e25 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 5 Oct 2023 16:58:12 +0100 Subject: [PATCH 27/78] Minor changes to signing interfaces --- packages/did/src/Did2.chain.ts | 23 ++++++++++++++++++----- packages/types/src/CryptoCallbacksV2.ts | 5 ++++- tests/testUtils/TestUtils2.ts | 7 ++++--- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index 641c0d9ccd..62b46569d7 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -293,9 +293,18 @@ interface GetStoreTxInput { service?: NewService[] } +type GetStoreTxSignCallbacResponse = Pick< + CryptoCallbacksV2.SignResponseData, + 'signature' +> & { + verificationMethod: Pick< + DidDocumentV2.VerificationMethod, + 'publicKeyMultibase' + > +} export type GetStoreTxSignCallback = ( signData: Omit -) => Promise +) => Promise /** * @param input @@ -383,11 +392,13 @@ export async function getStoreTxFromInput( .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) .toU8a() - const { signature, verificationMethodPublicKey } = await sign({ + const { signature, verificationMethod } = await sign({ data: encoded, verificationMethodRelationship: 'authentication', }) - const { keyType } = multibaseKeyToDidKey(verificationMethodPublicKey) + const { keyType } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) const encodedSignature = { [keyType]: signature, } as EncodedSignature @@ -555,12 +566,14 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const { signature, verificationMethodPublicKey } = await sign({ + const { signature, verificationMethod } = await sign({ data: signableCall.toU8a(), verificationMethodRelationship, did, }) - const { keyType } = multibaseKeyToDidKey(verificationMethodPublicKey) + const { keyType } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) const encodedSignature = { [keyType]: signature, } as EncodedSignature diff --git a/packages/types/src/CryptoCallbacksV2.ts b/packages/types/src/CryptoCallbacksV2.ts index 4771cabefd..3a1319438e 100644 --- a/packages/types/src/CryptoCallbacksV2.ts +++ b/packages/types/src/CryptoCallbacksV2.ts @@ -39,7 +39,10 @@ export interface SignResponseData { /** * The did key uri used for signing. */ - verificationMethodPublicKey: DidDocumentV2.VerificationMethod['publicKeyMultibase'] + verificationMethod: Pick< + DidDocumentV2.VerificationMethod, + 'publicKeyMultibase' | 'id' + > } /** diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 22c0cd9972..f2979ae926 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -141,7 +141,7 @@ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { return { signature, - verificationMethodPublicKey: verificationMethod.publicKeyMultibase, + verificationMethod, } } } @@ -163,8 +163,9 @@ export function makeStoreDidCallback( const signature = keypair.sign(data, { withType: false }) return { signature, - verificationMethodPublicKey: - Did.DidUtilsV2.keypairToMultibaseKey(keypair), + verificationMethod: { + publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey(keypair), + }, } } } From 4a0a5faf03daa3d7ace16308f290fe4af525806b Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 6 Oct 2023 10:57:20 +0100 Subject: [PATCH 28/78] DIDSignature unit tests passing --- packages/did/src/Did2.signature.spec.ts | 544 ++++++++++++++++++ packages/did/src/Did2.signature.ts | 128 ++++- packages/did/src/Did2.utils.ts | 2 +- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 11 +- packages/did/src/DidResolver/DidResolverV2.ts | 4 +- packages/types/src/DidDocumentV2.ts | 2 +- packages/types/src/DidResolverV2.ts | 4 +- tests/testUtils/TestUtils2.ts | 11 +- 8 files changed, 689 insertions(+), 17 deletions(-) create mode 100644 packages/did/src/Did2.signature.spec.ts diff --git a/packages/did/src/Did2.signature.spec.ts b/packages/did/src/Did2.signature.spec.ts new file mode 100644 index 0000000000..caa738465b --- /dev/null +++ b/packages/did/src/Did2.signature.spec.ts @@ -0,0 +1,544 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import type { + DidDocumentV2, + KiltKeyringPair, + CryptoCallbacksV2, + KeyringPair, +} from '@kiltprotocol/types' +import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' + +import { TestUtilsV2 } from '../../../tests/testUtils' +import { + DidSignature, + isDidSignature, + signatureFromJson, + signatureToJson, + verifyDidSignature, +} from './Did2.signature' +import { resolve } from './DidResolver/DidResolverV2' +import { + keypairToMultibaseKey, + multibaseKeyToDidKey, + parse, +} from './Did2.utils' +import { + createLightDidDocument, + NewLightDidVerificationKey, +} from './DidDetailsv2' + +jest.mock('./DidResolver/DidResolverV2') +jest + .mocked(resolve) + .mockImplementation(jest.requireActual('./DidResolver/DidResolverV2').resolve) + +describe('light DID', () => { + let keypair: KiltKeyringPair + let did: DidDocumentV2.DidDocument + let sign: CryptoCallbacksV2.SignCallback + beforeAll(() => { + const keyTool = TestUtilsV2.makeSigningKeyTool() + keypair = keyTool.keypair + did = createLightDidDocument({ + authentication: keyTool.authentication, + }) + sign = keyTool.getSignCallback(did) + }) + + beforeEach(() => { + jest + .mocked(resolve) + .mockReset() + .mockImplementation(async (didUrl) => { + const { address } = parse(didUrl) + if (address === keypair.address) { + return { + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: did, + } + } + return Promise.reject() + }) + }) + + it('verifies did signature over string', async () => { + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).resolves.not.toThrow() + }) + + it('deserializes old did signature (with `keyId` property) to new format', async () => { + const SIGNED_STRING = 'signed string' + const { signature, signerUrl } = signatureToJson({ + did: did.id, + ...(await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + })), + }) + const oldSignature = { + signature, + keyId: signerUrl, + } + + const deserialized = signatureFromJson(oldSignature) + expect(deserialized.signature).toBeInstanceOf(Uint8Array) + expect(deserialized.verificationMethodUri).toStrictEqual(signerUrl) + expect(deserialized).not.toHaveProperty('keyId') + }) + + it('verifies did signature over bytes', async () => { + const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) + const { signature, verificationMethod } = await sign({ + data: SIGNED_BYTES, + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_BYTES, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).resolves.not.toThrow() + }) + + it('fails if relationship does not match', async () => { + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'assertionMethod', + }) + ).rejects.toThrow() + }) + + it('fails if key id does not match', async () => { + const SIGNED_STRING = 'signed string' + // eslint-disable-next-line prefer-const + let { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + const wrongVerificationMethodId = `${verificationMethod.id}1a` + jest.mocked(resolve).mockRejectedValue(new Error('DID not found')) + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: + `${did.id}${wrongVerificationMethodId}` as DidDocumentV2.DidUrl, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow() + }) + + it('fails if signature does not match', async () => { + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_STRING.substring(1), + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow() + }) + + it('fails if key id malformed', async () => { + jest.mocked(resolve).mockRestore() + const SIGNED_STRING = 'signed string' + // eslint-disable-next-line prefer-const + let { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + const malformedVerificationId = verificationMethod.id.replace('#', '?') + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: + `${did.id}${malformedVerificationId}` as DidDocumentV2.DidUrl, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow() + }) + + it('does not verify if migrated to Full DID', async () => { + jest.mocked(resolve).mockRejectedValue(new Error('Migrated')) + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow() + }) + + it('typeguard accepts legal signature objects', () => { + const signature: DidSignature = { + signerUrl: `${did.id}${did.authentication[0]}`, + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(true) + }) + + it('detects signer expectation mismatch if signature is by unrelated did', async () => { + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + + const expectedSigner = createLightDidDocument({ + authentication: TestUtilsV2.makeSigningKeyTool().authentication, + }).id + + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedSigner, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) + }) + + it('allows variations of the same light did', async () => { + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + + const authKey = did.verificationMethod.find( + (vm) => vm.id === did.authentication[0] + ) + const expectedSignerAuthKey = multibaseKeyToDidKey( + authKey!.publicKeyMultibase + ) + const expectedSigner = createLightDidDocument({ + authentication: [ + { + publicKey: expectedSignerAuthKey.publicKey, + type: expectedSignerAuthKey.keyType, + }, + ] as [NewLightDidVerificationKey], + keyAgreement: [{ type: 'x25519', publicKey: new Uint8Array(32).fill(1) }], + service: [ + { + id: '#service', + type: ['servingService'], + serviceEndpoint: ['http://example.com'], + }, + ], + }).id + + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedSigner, + expectedVerificationMethodRelationship: 'authentication', + }) + ).resolves.not.toThrow() + }) +}) + +describe('full DID', () => { + let keypair: KiltKeyringPair + let did: DidDocumentV2.DidDocument + let sign: CryptoCallbacksV2.SignCallback + beforeAll(() => { + keypair = Crypto.makeKeypairFromSeed() + did = { + id: `did:kilt:${keypair.address}`, + authentication: ['#0x12345'], + verificationMethod: [ + { + controller: `did:kilt:${keypair.address}`, + id: '#0x12345', + publicKeyMultibase: keypairToMultibaseKey(keypair), + type: 'MultiKey', + }, + ], + } + sign = async ({ data }) => ({ + signature: keypair.sign(data), + verificationMethod: { + id: '#0x12345', + publicKeyMultibase: keypairToMultibaseKey(keypair), + }, + }) + }) + + beforeEach(() => { + jest + .mocked(resolve) + .mockReset() + .mockImplementation(async (didUri) => { + const { address } = parse(didUri) + if (address === keypair.address) { + return { + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: did, + } + } + return Promise.reject() + }) + }) + + it('verifies did signature over string', async () => { + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).resolves.not.toThrow() + }) + + it('verifies did signature over bytes', async () => { + const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) + const { signature, verificationMethod } = await sign({ + data: SIGNED_BYTES, + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_BYTES, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).resolves.not.toThrow() + }) + + it('does not verify if deactivated', async () => { + jest.mocked(resolve).mockRejectedValue(new Error('Deactivated')) + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow() + }) + + it('does not verify if not on chain', async () => { + jest.mocked(resolve).mockRejectedValue(new Error('Not on chain')) + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow() + }) + + it('accepts signature of full did for light did if enabled', async () => { + const SIGNED_STRING = 'signed string' + const { signature, verificationMethod } = await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + + const authKey = did.verificationMethod.find( + (vm) => vm.id === did.authentication[0] + ) + const expectedSignerAuthKey = multibaseKeyToDidKey( + authKey!.publicKeyMultibase + ) + const expectedSigner = createLightDidDocument({ + authentication: [ + { + publicKey: expectedSignerAuthKey.publicKey, + type: expectedSignerAuthKey.keyType, + }, + ] as [NewLightDidVerificationKey], + }).id + + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedSigner, + expectedVerificationMethodRelationship: 'authentication', + }) + ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) + + await expect( + verifyDidSignature({ + message: SIGNED_STRING, + signature, + verificationMethodUri: `${did.id}${verificationMethod.id}`, + expectedSigner, + allowUpgraded: true, + expectedVerificationMethodRelationship: 'authentication', + }) + ).resolves.not.toThrow() + }) + + it('typeguard accepts legal signature objects', () => { + const signature: DidSignature = { + signerUrl: `${did.id}${did.authentication[0]}`, + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(true) + }) +}) + +describe('type guard', () => { + let keypair: KeyringPair + beforeAll(() => { + keypair = Crypto.makeKeypairFromSeed() + }) + + it('rejects malformed key uri', () => { + let signature: DidSignature = { + // @ts-expect-error + signerUrl: `did:kilt:${keypair.address}?mykey`, + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(false) + signature = { + // @ts-expect-error + signerUrl: `kilt:did:${keypair.address}#mykey`, + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(false) + signature = { + // @ts-expect-error + signerUrl: `did:kilt:${keypair.address}`, + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(false) + signature = { + // @ts-expect-error + signerUrl: keypair.address, + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(false) + signature = { + // @ts-expect-error + signerUrl: '', + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(false) + }) + + it('rejects unexpected signature type', () => { + const signature: DidSignature = { + signerUrl: `did:kilt:${keypair.address}#mykey` as DidDocumentV2.DidUrl, + signature: '', + } + expect(isDidSignature(signature)).toBe(false) + signature.signature = randomAsHex(32).substring(2) + expect(isDidSignature(signature)).toBe(false) + // @ts-expect-error + signature.signature = randomAsU8a(32) + expect(isDidSignature(signature)).toBe(false) + }) + + it('rejects incomplete objects', () => { + let signature: DidSignature = { + signerUrl: `did:kilt:${keypair.address}#mykey` as DidDocumentV2.DidUrl, + // @ts-expect-error + signature: undefined, + } + expect(isDidSignature(signature)).toBe(false) + signature = { + // @ts-expect-error + signerUrl: undefined, + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(false) + // @ts-expect-error + signature = { + signature: randomAsHex(32), + } + expect(isDidSignature(signature)).toBe(false) + // @ts-expect-error + signature = { + signerUrl: `did:kilt:${keypair.address}#mykey` as DidDocumentV2.DidUrl, + } + expect(isDidSignature(signature)).toBe(false) + // @ts-expect-error + signature = {} + expect(isDidSignature(signature)).toBe(false) + // @ts-expect-error + signature = { signerUrl: null, signature: null } + expect(isDidSignature(signature)).toBe(false) + }) +}) diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts index 955dda2470..39e316cc0c 100644 --- a/packages/did/src/Did2.signature.ts +++ b/packages/did/src/Did2.signature.ts @@ -5,25 +5,28 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { +import { CryptoCallbacksV2, DidDocumentV2, DidResolverV2, } from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' +import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { isHex } from '@polkadot/util' +import { multibaseKeyToDidKey, parse, validateUri } from './Did2.utils.js' +import { resolve } from './DidResolver/DidResolverV2.js' export type DidSignatureVerificationInput = { message: string | Uint8Array signature: Uint8Array - verificationMethodUri: DidDocumentV2.DidResourceUri + verificationMethodUri: DidDocumentV2.DidUrl expectedSigner?: DidDocumentV2.DidUri allowUpgraded?: boolean expectedVerificationMethodRelationship?: DidDocumentV2.SignatureVerificationMethodRelationship - dereferenceDidUrl?: DidResolverV2.DereferenceDidUrl + resolveDid?: DidResolverV2.ResolveDid['resolve'] } export type DidSignature = { - verificationMethod: DidDocumentV2.VerificationMethod + signerUrl: DidDocumentV2.DidUrl signature: string } @@ -31,18 +34,125 @@ export type DidSignature = { // It is reasonable to think that it will be removed at some point in the future. type OldDidSignatureV1 = { signature: string - keyId: DidDocumentV2.DidResourceUri + keyId: DidDocumentV2.DidUrl } type OldDidSignatureV2 = { signature: string - keyUri: DidDocumentV2.DidResourceUri + keyUri: DidDocumentV2.DidUrl +} + +function verifyDidSignatureDataStructure( + input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 +): void { + const verificationMethodUri = (() => { + if ('keyUri' in input) { + return input.keyUri + } + if ('keyId' in input) { + return input.keyId + } + return input.signerUrl + })() + if (!isHex(input.signature)) { + throw new SDKErrors.SignatureMalformedError( + `Expected signature as a hex string, got ${input.signature}` + ) + } + validateUri(verificationMethodUri, 'ResourceUri') +} + +export async function verifyDidSignature({ + message, + signature, + verificationMethodUri, + expectedSigner, + allowUpgraded = false, + expectedVerificationMethodRelationship, + resolveDid = resolve, +}: DidSignatureVerificationInput): Promise { + // checks if key uri points to the right did; alternatively we could check the key's controller + const signer = parse(verificationMethodUri) + if (expectedSigner && expectedSigner !== signer.did) { + // check for allowable exceptions + const expected = parse(expectedSigner) + // NECESSARY CONDITION: subjects and versions match + const subjectVersionMatch = + expected.address === signer.address && expected.version === signer.version + // EITHER: signer is a full did and we allow signatures by corresponding full did + const allowedUpgrade = allowUpgraded && signer.type === 'full' + // OR: both are light dids and their auth key type matches + const keyTypeMatch = + signer.type === 'light' && + expected.type === 'light' && + expected.authKeyTypeEncoding === signer.authKeyTypeEncoding + if (!(subjectVersionMatch && (allowedUpgrade || keyTypeMatch))) { + throw new SDKErrors.DidSubjectMismatchError(signer.did, expected.did) + } + } + + const { didDocument } = await resolveDid(verificationMethodUri, {}) + if (didDocument === undefined) { + // TODO: Better error + throw new Error( + `Error validating the DID signature. Cannot fetch DID Document.` + ) + } + const verificationMethod = didDocument.verificationMethod.find( + (vm) => vm.id === signer.fragment + ) + if (verificationMethod === undefined) { + // TODO: Better error + throw new Error( + `Cannot find verification method with ID "${signer.fragment} in the DID Document.` + ) + } + if ( + expectedVerificationMethodRelationship !== undefined && + didDocument[expectedVerificationMethodRelationship]?.find( + (vm) => vm === signer.fragment + ) === undefined + ) { + // TODO: Better error + throw new Error( + `Cannot find verification method with ID "${signer.fragment} for the relationship "${expectedVerificationMethodRelationship}".` + ) + } + + const { publicKey } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) + Crypto.verify(message, signature, publicKey) +} + +export function isDidSignature( + input: unknown +): input is DidSignature | OldDidSignatureV1 | OldDidSignatureV2 { + try { + verifyDidSignatureDataStructure(input as DidSignature) + return true + } catch (cause) { + return false + } +} + +export function signatureToJson({ + did, + signature, + verificationMethod, +}: CryptoCallbacksV2.SignResponseData & { + did: DidDocumentV2.DidUri +}): DidSignature { + return { + signature: Crypto.u8aToHex(signature), + signerUrl: `${did}${verificationMethod.id}`, + } } // TODO: JSDocs export function signatureFromJson( input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 ): Pick & { - verificationMethodUri: DidDocumentV2.DidResourceUri + verificationMethodUri: DidDocumentV2.DidUrl } { const verificationMethodUri = (() => { if ('keyUri' in input) { @@ -51,7 +161,7 @@ export function signatureFromJson( if ('keyId' in input) { return input.keyId } - return `${input.verificationMethod.controller}${input.verificationMethod.id}` as DidDocumentV2.DidResourceUri + return input.signerUrl })() const signature = Crypto.coToUInt8(input.signature) return { signature, verificationMethodUri } diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index c11bf02f56..a1166ebe7f 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -57,7 +57,7 @@ type IDidParsingResult = { * @returns Object containing information extracted from the DID uri. */ export function parse( - didUri: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri + didUri: DidDocumentV2.DidUri | DidDocumentV2.DidUrl ): IDidParsingResult { let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups if (matches) { diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 24b289056d..aceb31ab74 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -53,6 +53,13 @@ export type NewDidEncryptionKey = BaseNewDidKey & { type: DidEncryptionKeyType } +function doesVerificationMethodExist( + didDocument: DidDocumentV2.DidDocument, + { id }: Pick +): boolean { + return didDocument.verificationMethod.find((vm) => vm.id === id) !== undefined +} + function addVerificationMethod( didDocument: DidDocumentV2.DidDocument, verificationMethod: DidDocumentV2.VerificationMethod, @@ -62,7 +69,9 @@ function addVerificationMethod( existingRelationship.push(verificationMethod.id) // eslint-disable-next-line no-param-reassign didDocument[relationship] = existingRelationship - didDocument.verificationMethod.push(verificationMethod) + if (!doesVerificationMethodExist(didDocument, verificationMethod)) { + didDocument.verificationMethod.push(verificationMethod) + } } export function addKeypairAsVerificationMethod( diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index cef783f599..cf25a03647 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -222,7 +222,7 @@ type InternalDereferenceResult = { } async function dereferenceInternal( - didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri, + didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidUrl, // eslint-disable-next-line @typescript-eslint/no-unused-vars dereferenceOptions: DidResolverV2.DereferenceOptions ): Promise { @@ -255,7 +255,7 @@ async function dereferenceInternal( } export async function dereference( - didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidResourceUri, + didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidUrl, // eslint-disable-next-line @typescript-eslint/no-unused-vars { accept }: DidResolverV2.DereferenceOptions = { accept: DID_JSON, diff --git a/packages/types/src/DidDocumentV2.ts b/packages/types/src/DidDocumentV2.ts index d7736bd2b1..061dcf716e 100644 --- a/packages/types/src/DidDocumentV2.ts +++ b/packages/types/src/DidDocumentV2.ts @@ -25,7 +25,7 @@ export type UriFragment = `#${string}` /** * URI for DID resources like keys or service endpoints. */ -export type DidResourceUri = `${DidUri}${UriFragment}` +export type DidUrl = `${DidUri}${UriFragment}` export type SignatureVerificationMethodRelationship = | 'authentication' diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts index 4ae3bdc2b4..4f63697a77 100644 --- a/packages/types/src/DidResolverV2.ts +++ b/packages/types/src/DidResolverV2.ts @@ -8,7 +8,7 @@ import type { DidUri, DidDocument, - DidResourceUri, + DidUrl, VerificationMethod, Service, JsonLd, @@ -236,7 +236,7 @@ export interface DereferenceDidUrl { * This is the DID URL to dereference. * To dereference a DID fragment, the complete DID URL including the DID fragment MUST be used. This input is REQUIRED. */ - didUrl: DidUri | DidResourceUri, + didUrl: DidUri | DidUrl, /* * A metadata structure consisting of input options to the dereference function in addition to the didUrl itself. * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index f2979ae926..719461024f 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -198,6 +198,13 @@ export function makeSigningKeyTool( } } +function doesVerificationMethodExist( + didDocument: DidDocumentV2.DidDocument, + { id }: Pick +): boolean { + return didDocument.verificationMethod.find((vm) => vm.id === id) !== undefined +} + function addVerificationMethod( didDocument: DidDocumentV2.DidDocument, verificationMethod: DidDocumentV2.VerificationMethod, @@ -207,7 +214,9 @@ function addVerificationMethod( existingRelationship.push(verificationMethod.id) // eslint-disable-next-line no-param-reassign didDocument[relationship] = existingRelationship - didDocument.verificationMethod.push(verificationMethod) + if (!doesVerificationMethodExist(didDocument, verificationMethod)) { + didDocument.verificationMethod.push(verificationMethod) + } } function addKeypairAsVerificationMethod( From 5b4691c878e71bcb1f4dbec9e1d0d1cdb02beeda Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 6 Oct 2023 12:21:55 +0100 Subject: [PATCH 29/78] Halfway through the refactoring --- packages/did/src/Did2.chain.ts | 244 ++++++------------ packages/did/src/Did2.rpc.ts | 2 +- packages/did/src/Did2.signature.spec.ts | 30 +-- packages/did/src/Did2.signature.ts | 47 +++- packages/did/src/Did2.utils.ts | 28 +- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 20 +- .../did/src/DidDetailsv2/FullDidDetailsV2.ts | 8 +- .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 57 +++- packages/did/src/DidResolver/DidResolverV2.ts | 96 +++---- packages/types/src/DidDocumentV2.ts | 4 +- tests/testUtils/TestUtils2.ts | 5 +- 11 files changed, 284 insertions(+), 257 deletions(-) diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index 62b46569d7..3d30565012 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -22,12 +22,8 @@ import { CryptoCallbacksV2, Deposit, DidDocumentV2, - encryptionKeyTypesMap, KiltAddress, - NewDidEncryptionKey, - NewDidVerificationKey, SubmittableExtrinsic, - verificationKeyTypesMap, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' @@ -38,6 +34,8 @@ import { NewService, DidVerificationKeyType, verificationKeyTypes, + NewDidVerificationKey, + NewDidEncryptionKey, } from './DidDetailsv2/DidDetailsV2.js' import { @@ -59,28 +57,40 @@ export type EncodedDidKey = EncodedVerificationKey | EncodedEncryptionKey export type EncodedSignature = EncodedVerificationKey /** - * @param did + * Format a DID to be used as a parameter for the blockchain API functions. + + * @param did The DID to format. + * @returns The blockchain-formatted DID. */ export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { return parse(did).address } /** - * @param id + * Format a DID fragment to be used as a parameter for the blockchain API functions. + + * @param id The DID fragment to format. + * @returns The blockchain-formatted ID. */ export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { return id.replace(/^#/, '') } /** - * @param encoded + * Convert the DID data from blockchain format to the DID URI. + * + * @param encoded The chain-formatted DID. + * @returns The DID URI. */ export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) } /** - * @param deposit + * Convert the deposit data coming from the blockchain to JS object. + * + * @param deposit The blockchain-formatted deposit data. + * @returns The deposit data. */ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { return { @@ -119,6 +129,13 @@ export type ChainDidDetails = { deposit: Deposit } +/** + * Convert a DID public key from the blockchain format to a JS object. + * + * @param keyId The key ID. + * @param keyDetails The associated public key blockchain-formatted details. + * @returns The JS-formatted DID key. + */ export function publicKeyFromChain( keyId: Hash, keyDetails: DidDidDetailsDidPublicKeyDetails @@ -129,14 +146,15 @@ export function publicKeyFromChain( return { id: `#${keyId.toHex()}`, publicKey: key.value.toU8a(), - type: key.type.toLowerCase() as - | ChainDidVerificationKey['type'] - | ChainDidEncryptionKey['type'], + type: key.type.toLowerCase() as ChainDidKey['type'], } } /** - * @param encoded + * Convert the DID Document data from the blockchain format to a JS object. + * + * @param encoded The chain-formatted DID Document. + * @returns The DID Document. */ export function documentFromChain( encoded: Option @@ -217,6 +235,12 @@ function isUri(str: string): boolean { const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ +/** + * Checks if a string is a valid URI fragment according to RFC#3986. + * + * @param str String to be checked. + * @returns Whether `str` is a valid URI fragment. + */ function isUriFragment(str: string): boolean { try { return uriFragmentRegex.test(str) && !!decodeURIComponent(str) @@ -226,7 +250,11 @@ function isUriFragment(str: string): boolean { } /** - * @param endpoint + * Performs sanity checks on service endpoint data, making sure that the following conditions are met: + * - The `id` property is a string containing a valid URI fragment according to RFC#3986, not a complete DID URI. + * - If the `uris` property contains one or more strings, they must be valid URIs according to RFC#3986. + * + * @param endpoint A service endpoint object to check. */ export function validateNewService(endpoint: NewService): void { const { id, serviceEndpoint } = endpoint @@ -250,7 +278,10 @@ export function validateNewService(endpoint: NewService): void { } /** - * @param service + * Format the DID service to be used as a parameter for the blockchain API functions. + * + * @param service The DID service to format. + * @returns The blockchain-formatted DID service. */ export function serviceToChain(service: NewService): ChainDidService { validateNewService(service) @@ -263,7 +294,10 @@ export function serviceToChain(service: NewService): ChainDidService { } /** - * @param encoded + * Convert the DID service data coming from the blockchain to JS object. + * + * @param encoded The blockchain-formatted DID service data. + * @returns The DID service. */ export function serviceFromChain( encoded: Option @@ -297,6 +331,7 @@ type GetStoreTxSignCallbacResponse = Pick< CryptoCallbacksV2.SignResponseData, 'signature' > & { + // We don't need the key ID to dispatch the tx. verificationMethod: Pick< DidDocumentV2.VerificationMethod, 'publicKeyMultibase' @@ -307,9 +342,21 @@ export type GetStoreTxSignCallback = ( ) => Promise /** - * @param input - * @param submitter - * @param sign + * Create a DID creation operation which includes the information provided. + * + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys as verification methods and service endpoints. + * + * A DID creation operation can contain at most 25 new service endpoints. + * Additionally, each service endpoint must respect the following conditions: + * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. + * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * + * @param input The DID keys and services to store. + * @param submitter The KILT address authorized to submit the creation operation. + * @param sign The sign callback. The authentication key has to be used. + * + * @returns The SubmittableExtrinsic for the DID creation operation. */ export async function getStoreTxFromInput( input: GetStoreTxInput, @@ -392,158 +439,34 @@ export async function getStoreTxFromInput( .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) .toU8a() - const { signature, verificationMethod } = await sign({ + const { signature } = await sign({ data: encoded, verificationMethodRelationship: 'authentication', }) - const { keyType } = multibaseKeyToDidKey( - verificationMethod.publicKeyMultibase - ) const encodedSignature = { - [keyType]: signature, + [authenticationKey.type]: signature, } as EncodedSignature return api.tx.did.create(encoded, encodedSignature) } -/** - * @param input - * @param submitter - * @param sign - */ -export async function getStoreTxFromDidDocument( - input: DidDocumentV2.DidDocument, - submitter: KiltAddress, - sign: GetStoreTxSignCallback -): Promise { - const { - authentication, - assertionMethod, - keyAgreement, - capabilityDelegation, - service, - verificationMethod, - } = input - - const authKey = (() => { - const authVerificationMethod = verificationMethod.find( - (vm) => vm.id === authentication[0] - ) - if (authVerificationMethod === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - const { keyType, publicKey } = multibaseKeyToDidKey( - authVerificationMethod.publicKeyMultibase - ) - if (verificationKeyTypesMap[keyType] === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() - - const keyAgreementKey = (() => { - if (keyAgreement === undefined) { - return undefined - } - const keyAgreementVerificationMethod = verificationMethod.find( - (vm) => vm.id === keyAgreement?.[0] - ) - if (keyAgreementVerificationMethod === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - const { keyType, publicKey } = multibaseKeyToDidKey( - keyAgreementVerificationMethod.publicKeyMultibase - ) - if (encryptionKeyTypesMap[keyType] === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - return { - type: keyType, - publicKey, - } as NewDidEncryptionKey - })() - - const assertionMethodKey = (() => { - if (assertionMethod === undefined) { - return undefined - } - const assertionMethodVerificationMethod = verificationMethod.find( - (vm) => vm.id === assertionMethod?.[0] - ) - if (assertionMethodVerificationMethod === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - const { keyType, publicKey } = multibaseKeyToDidKey( - assertionMethodVerificationMethod.publicKeyMultibase - ) - if (verificationKeyTypesMap[keyType] === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() - - const capabilityDelegationKey = (() => { - if (capabilityDelegation === undefined) { - return undefined - } - const capabilityDelegationVerificationMethod = verificationMethod.find( - (vm) => vm.id === capabilityDelegation?.[0] - ) - if (capabilityDelegationVerificationMethod === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - const { keyType, publicKey } = multibaseKeyToDidKey( - capabilityDelegationVerificationMethod.publicKeyMultibase - ) - if (verificationKeyTypesMap[keyType] === undefined) { - // TODO: Better error - throw new Error('Malformed DID document.') - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() - - const storeTxInput: GetStoreTxInput = { - authentication: [authKey], - assertionMethod: assertionMethodKey ? [assertionMethodKey] : undefined, - capabilityDelegation: capabilityDelegationKey - ? [capabilityDelegationKey] - : undefined, - keyAgreement: keyAgreementKey ? [keyAgreementKey] : undefined, - service, - } - - return getStoreTxFromInput(storeTxInput, submitter, sign) -} - export interface SigningOptions { sign: CryptoCallbacksV2.SignExtrinsicCallback verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship } /** - * @param root0 - * @param root0.did - * @param root0.verificationMethodRelationship - * @param root0.sign - * @param root0.call - * @param root0.txCounter - * @param root0.submitter - * @param root0.blockNumber + * DID related operations on the KILT blockchain require authorization by a full DID. This is realized by requiring that relevant extrinsics are signed with a key featured by a full DID as a verification method. + * Such extrinsics can be produced using this function. + * + * @param params Object wrapping all input to the function. + * @param params.did Full DID. + * @param params.verificationMethodRelationship DID verification relationship to be used for authorization. + * @param params.sign The callback to interface with the key store managing the private key to be used. + * @param params.call The call or extrinsic to be authorized. + * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. + * @param params.submitter Payment account allowed to submit this extrinsic and cover its fees, which will end up owning any deposit associated with newly created records. + * @param params.blockNumber Block number for determining the validity period of this authorization. If omitted, the current block number will be fetched from chain. + * @returns A DID authorized extrinsic that, after signing with the payment account mentioned in the params, is ready for submission. */ export async function generateDidAuthenticatedTx({ did, @@ -581,9 +504,12 @@ export async function generateDidAuthenticatedTx({ } /** - * @param root0 - * @param root0.publicKeyMultibase - * @param signature + * Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics. + * + * @param key Object describing data associated with a public key. + * @param key.publicKeyMultibase The multibase, multicodec representation of the signing public key. + * @param signature The signature generated with the full DID associated public key. + * @returns Data restructured to allow SCALE encoding by polkadot api. */ export function didSignatureToChain( { publicKeyMultibase }: DidDocumentV2.VerificationMethod, diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index e292b8fc21..0282647076 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -18,7 +18,7 @@ import type { KiltAddress, DidDocumentV2 } from '@kiltprotocol/types' import { ss58Format } from '@kiltprotocol/utils' import { encodeAddress } from '@polkadot/keyring' import { ethereumEncode } from '@polkadot/util-crypto' -import { Address, SubstrateAddress } from './DidLinks/AccountLinks.chain.js' +import { Address, SubstrateAddress } from './DidLinks/AccountLinks2.chain.js' import { didKeyToVerificationMethod } from './Did2.utils.js' import { ChainDidDetails, diff --git a/packages/did/src/Did2.signature.spec.ts b/packages/did/src/Did2.signature.spec.ts index caa738465b..c86d0c4a21 100644 --- a/packages/did/src/Did2.signature.spec.ts +++ b/packages/did/src/Did2.signature.spec.ts @@ -79,7 +79,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -117,7 +117,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_BYTES, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -134,7 +134,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'assertionMethod', }) ).rejects.toThrow() @@ -154,7 +154,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: + signerUrl: `${did.id}${wrongVerificationMethodId}` as DidDocumentV2.DidUrl, expectedVerificationMethodRelationship: 'authentication', }) @@ -172,7 +172,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING.substring(1), signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() @@ -192,7 +192,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: + signerUrl: `${did.id}${malformedVerificationId}` as DidDocumentV2.DidUrl, expectedVerificationMethodRelationship: 'authentication', }) @@ -211,7 +211,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() @@ -241,7 +241,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, expectedVerificationMethodRelationship: 'authentication', }) @@ -283,7 +283,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, expectedVerificationMethodRelationship: 'authentication', }) @@ -346,7 +346,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -363,7 +363,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_BYTES, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -381,7 +381,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() @@ -399,7 +399,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() @@ -432,7 +432,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, expectedVerificationMethodRelationship: 'authentication', }) @@ -442,7 +442,7 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - verificationMethodUri: `${did.id}${verificationMethod.id}`, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, allowUpgraded: true, expectedVerificationMethodRelationship: 'authentication', diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts index 39e316cc0c..37d9acfa47 100644 --- a/packages/did/src/Did2.signature.ts +++ b/packages/did/src/Did2.signature.ts @@ -18,7 +18,7 @@ import { resolve } from './DidResolver/DidResolverV2.js' export type DidSignatureVerificationInput = { message: string | Uint8Array signature: Uint8Array - verificationMethodUri: DidDocumentV2.DidUrl + signerUrl: DidDocumentV2.DidUrl expectedSigner?: DidDocumentV2.DidUri allowUpgraded?: boolean expectedVerificationMethodRelationship?: DidDocumentV2.SignatureVerificationMethodRelationship @@ -61,17 +61,30 @@ function verifyDidSignatureDataStructure( validateUri(verificationMethodUri, 'ResourceUri') } +/** + * Verify a DID signature given the signer's DID URL. + * A signature verification returns false if a migrated and then deleted DID is used. + * + * @param input Object wrapping all input. + * @param input.message The message that was signed. + * @param input.signature Signature bytes. + * @param input.signerUrl DID URL of the verification method used for signing. + * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. + * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. + * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. + * @param input.resolveDid Allows specifying a custom DID resolve. Defaults to the built-in [[resolve]]. + */ export async function verifyDidSignature({ message, signature, - verificationMethodUri, + signerUrl, expectedSigner, allowUpgraded = false, expectedVerificationMethodRelationship, resolveDid = resolve, }: DidSignatureVerificationInput): Promise { // checks if key uri points to the right did; alternatively we could check the key's controller - const signer = parse(verificationMethodUri) + const signer = parse(signerUrl) if (expectedSigner && expectedSigner !== signer.did) { // check for allowable exceptions const expected = parse(expectedSigner) @@ -90,14 +103,14 @@ export async function verifyDidSignature({ } } - const { didDocument } = await resolveDid(verificationMethodUri, {}) + const { didDocument } = await resolveDid(signerUrl, {}) if (didDocument === undefined) { // TODO: Better error throw new Error( `Error validating the DID signature. Cannot fetch DID Document.` ) } - const verificationMethod = didDocument.verificationMethod.find( + const verificationMethod = didDocument.verificationMethod?.find( (vm) => vm.id === signer.fragment ) if (verificationMethod === undefined) { @@ -124,6 +137,13 @@ export async function verifyDidSignature({ Crypto.verify(message, signature, publicKey) } +/** + * Type guard assuring that the input is a valid DidSignature object, consisting of a signature as hex and the uri of the signing key. + * Does not cryptographically verify the signature itself! + * + * @param input Arbitrary input. + * @returns True if validation of form has passed. + */ export function isDidSignature( input: unknown ): input is DidSignature | OldDidSignatureV1 | OldDidSignatureV2 { @@ -135,6 +155,15 @@ export function isDidSignature( } } +/** + * Transforms the output of a [[SignCallback]] into the [[DidSignature]] format suitable for json-based data exchange. + * + * @param input Signature data returned from the [[SignCallback]]. + * @param input.signature Signature bytes. + * @param input.did The DID URI of the signer. + * @param input.verificationMethod The verification method used to generate the signature. + * @returns A [[DidSignature]] object where signature is hex-encoded. + */ export function signatureToJson({ did, signature, @@ -148,7 +177,13 @@ export function signatureToJson({ } } -// TODO: JSDocs +/** + * Deserializes a [[DidSignature]] for signature verification. + * Handles backwards compatibility to an older version of the interface where the `verificationMethodUri` property was called either `keyUri` or `keyId`. + * + * @param input A [[DidSignature]] object. + * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. + */ export function signatureFromJson( input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 ): Pick & { diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index a1166ebe7f..3ba6690201 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -126,7 +126,7 @@ const multicodecReversePrefixes: Record = { * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. * * @param publicKeyMultibase The verification method's public key multibase. - * @returns The decoded public key and [DidKeyType]. + * @returns The decoded public key and [[DidKeyType]]. */ export function multibaseKeyToDidKey( publicKeyMultibase: DidDocumentV2.VerificationMethod['publicKeyMultibase'] @@ -147,6 +147,14 @@ export function multibaseKeyToDidKey( throw new Error('Invalid encoding of the verification method.') } +/** + * Calculate the multibase, multicodec representation of a keypair given its type and public key. + * + * @param keypair The input keypair to encode as multibase, multicodec. + * @param keypair.type The keypair [[DidKeyType]]. + * @param keypair.publicKey The keypair public key. + * @returns The multicodec, multibase encoding of the provided keypair. + */ export function keypairToMultibaseKey({ type, publicKey, @@ -171,6 +179,16 @@ export function keypairToMultibaseKey({ ).toString() as `z${string}` } +/** + * Export a DID key to a `MultiKey` verification method. + * + * @param controller The verification method controller's DID URI. + * @param id The verification method ID. + * @param key The DID key to export as a verification method. + * @param key.keyType The key type. + * @param key.publicKey The public component of the key. + * @returns The provided key encoded as a [[ DidDocumentV2.VerificationMethod]]. + */ export function didKeyToVerificationMethod( controller: DidDocumentV2.VerificationMethod['controller'], id: DidDocumentV2.VerificationMethod['id'], @@ -249,7 +267,13 @@ export function validateUri( DataUtils.verifyKiltAddress(address) } -// TODO: Fix JSDoc +/** + * Internal: derive the address part of the DID when it is created from the provided authentication verification method. + * + * @param input The authentication verification method. + * @param input.publicKeyMultibase The `publicKeyMultibase` value of the verification method. + * @returns The expected address of the DID. + */ export function getAddressFromVerificationMethod({ publicKeyMultibase, }: Pick): KiltAddress { diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index aceb31ab74..800658115e 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -57,7 +57,9 @@ function doesVerificationMethodExist( didDocument: DidDocumentV2.DidDocument, { id }: Pick ): boolean { - return didDocument.verificationMethod.find((vm) => vm.id === id) !== undefined + return ( + didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined + ) } function addVerificationMethod( @@ -70,10 +72,24 @@ function addVerificationMethod( // eslint-disable-next-line no-param-reassign didDocument[relationship] = existingRelationship if (!doesVerificationMethodExist(didDocument, verificationMethod)) { - didDocument.verificationMethod.push(verificationMethod) + const existingVerificationMethod = didDocument.verificationMethod ?? [] + existingVerificationMethod.push(verificationMethod) + // eslint-disable-next-line no-param-reassign + didDocument.verificationMethod = existingVerificationMethod } } +/** + * Add the provided keypair as a new verification method to the DID Document. + * !!! This function is meant to be used internally and not exposed since it is mostly used as a utility and does not perform extensive checks on the inputs. + * + * @param didDocument The DID Document to add the verification method to. + * @param newKeypair The new keypair to add as a verification method. + * @param newKeypair.id The ID of the new verification method. If a verification method with the same ID already exists, this operation is a no-op. + * @param newKeypair.publicKey The public key of the keypair. + * @param newKeypair.type The type of the public key. + * @param relationship The verification relationship to add the verification method to. + */ export function addKeypairAsVerificationMethod( didDocument: DidDocumentV2.DidDocument, { diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts index d998be6c14..748318c6af 100644 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts @@ -72,10 +72,10 @@ function getVerificationMethodRelationshipForRuntimeCall( } /** - * Detect the key relationship for a key which should be used to DID-authorize the provided extrinsic. + * Detect the verification relationship for a verification method which should be used to DID-authorize the provided extrinsic. * * @param extrinsic The unsigned extrinsic to inspect. - * @returns The key relationship. + * @returns The verification relationship. */ export function getVerificationMethodRelationshipForTx( extrinsic: Extrinsic @@ -111,7 +111,7 @@ async function getNextNonce(did: DidDocumentV2.DidUri): Promise { } /** - * Signs and returns the provided unsigned extrinsic with the right DID key, if present. Otherwise, it will throw an error. + * Signs and returns the provided unsigned extrinsic with the right DID verification method, if present. Otherwise, it will throw an error. * * @param did The DID data. * @param extrinsic The unsigned extrinsic to sign. @@ -199,7 +199,7 @@ function groupExtrinsicsByKeyRelationship( } /** - * Authorizes/signs a list of extrinsics grouping them in batches by required key type. + * Authorizes/signs a list of extrinsics grouping them in batches by required verification relationship. * * @param input The object with named parameters. * @param input.batchFunction The batch function to use, for example `api.tx.utility.batchAll`. diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index b03deeeb60..226dee809f 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -5,12 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { - DidDocumentV2, - encryptionKeyTypes, - NewDidEncryptionKey, - NewDidVerificationKey, -} from '@kiltprotocol/types' +import { DidDocumentV2 } from '@kiltprotocol/types' import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { base58Decode, @@ -24,8 +19,13 @@ import { parse, } from '../Did2.utils.js' import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' -import { addKeypairAsVerificationMethod } from './DidDetailsV2.js' -import type { NewService, DidVerificationKeyType } from './DidDetailsV2.js' +import { addKeypairAsVerificationMethod, encryptionKeyTypes } from './DidDetailsV2.js' +import type { + NewDidEncryptionKey, + NewDidVerificationKey, + NewService, + DidVerificationKeyType, +} from './DidDetailsV2.js' /** * Currently, a light DID does not support the use of an ECDSA key as its authentication key. @@ -62,14 +62,17 @@ const lightDidEncodingToVerificationKeyType: Record< '01': 'ed25519', } +/** + * The options that can be used to create a light DID. + */ export type CreateDocumentInput = { /** - * The DID authentication verification method. This is mandatory and will be used as the first authentication verification method + * The key to be used as the DID authentication verification method. This is mandatory and will be used as the first authentication verification method * of the full DID upon migration. */ authentication: [NewDidVerificationKey] /** - * The optional DID encryption verification method. If present, it will be used as the first key agreement verification method + * The optional encryption key to be used as the DID key agreement verification method. If present, it will be used as the first key agreement verification method * of the full DID upon migration. */ keyAgreement?: [NewDidEncryptionKey] @@ -127,6 +130,15 @@ interface SerializableStructure { > } +/** + * Serialize the optional key agreement key and service endpoints of a light DID using the CBOR serialization algorithm + * and encoding the result in Base58 format with a multibase prefix. + * + * @param details The light DID details to encode. + * @param details.keyAgreement The DID key agreement key. + * @param details.service The DID service endpoints. + * @returns The Base58-encoded and CBOR-serialized light DID optional details. + */ function serializeAdditionalLightDidDetails({ keyAgreement, service, @@ -184,6 +196,18 @@ function deserializeAdditionalLightDidDetails( } } +/** + * Create [[DidDocument]] of a light DID using the provided keys and endpoints. + * Sets proper key IDs, builds light DID URI. + * Private keys are assumed to already live in another storage, as it contains reference only to public keys. + * + * @param input The input. + * @param input.authentication The array containing the public keys to be used as the light DID authentication verification method. + * @param input.keyAgreement The optional array containing the public keys to be used as the light DID key agreement verification methods. + * @param input.service The optional light DID service endpoints. + * + * @returns The resulting [[DidDocument]]. + */ export function createLightDidDocument({ authentication, keyAgreement = undefined, @@ -233,6 +257,19 @@ export function createLightDidDocument({ return did } +/** + * Create [[DidDocument]] of a light DID by parsing the provided input URI. + * Only use for DIDs you control, when you are certain they have not been upgraded to on-chain full DIDs. + * For the DIDs you have received from external sources use [[resolve]] etc. + * + * Parsing is possible because of the self-describing and self-containing nature of light DIDs. + * Private keys are assumed to already live in another storage, as it contains reference only to public keys. + * + * @param uri The DID URI to parse. + * @param failIfFragmentPresent Whether to fail when parsing the URI in case a fragment is present or not, which is not relevant to the creation of the DID. It defaults to true. + * + * @returns The resulting [[DidDocument]]. + */ export function parseDocumentFromLightDid( uri: DidDocumentV2.DidUri, failIfFragmentPresent = true diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index cf25a03647..2c9cb61c0d 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -8,15 +8,9 @@ import { ConfigService } from '@kiltprotocol/config' import { DidDocumentV2, DidResolverV2 } from '@kiltprotocol/types' import { cbor } from '@kiltprotocol/utils' -import { linkedInfoFromChain } from '../Did.rpc' +import { linkedInfoFromChain } from '../Did2.rpc.js' import { toChain } from '../Did2.chain.js' -import { - didKeyToVerificationMethod, - getFullDidUri, - parse, - validateUri, -} from '../Did2.utils.js' -import { addKeypairAsVerificationMethod } from '../DidDetailsv2/DidDetailsV2.js' +import { getFullDidUri, parse, validateUri } from '../Did2.utils.js' import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' @@ -24,6 +18,9 @@ const DID_JSON = 'application/did+json' const DID_JSON_LD = 'application/did+ld+json' const DID_CBOR = 'application/did+cbor' +/** + * Supported types for DID resolution and dereferencing. + */ export type SupportedContentType = | typeof DID_JSON | typeof DID_JSON_LD @@ -44,57 +41,14 @@ async function resolveInternal( const { type } = parse(did) const api = ConfigService.get('api') - const { document, web3Name } = await api.call.did + const { document } = await api.call.did .query(toChain(did)) .then(linkedInfoFromChain) - .catch(() => ({ document: undefined, web3Name: undefined })) + .catch(() => ({ document: undefined })) if (type === 'full' && document !== undefined) { - const newDidDocument: DidDocumentV2.DidDocument = { - id: did, - authentication: [document.authentication[0].id], - verificationMethod: [ - didKeyToVerificationMethod(did, document.authentication[0].id, { - publicKey: document.authentication[0].publicKey, - keyType: document.authentication[0].type, - }), - ], - service: document.service, - } - if ( - document.keyAgreement !== undefined && - document.keyAgreement.length > 0 - ) { - document.keyAgreement.forEach(({ id, type: keyType, publicKey }) => { - addKeypairAsVerificationMethod( - newDidDocument, - { id, publicKey, type: keyType }, - 'keyAgreement' - ) - }) - } - if (document.assertionMethod !== undefined) { - const { id, publicKey, type: keyType } = document.assertionMethod[0] - addKeypairAsVerificationMethod( - newDidDocument, - { id, publicKey, type: keyType }, - 'assertionMethod' - ) - } - if (document.capabilityDelegation !== undefined) { - const { id, publicKey, type: keyType } = document.capabilityDelegation[0] - addKeypairAsVerificationMethod( - newDidDocument, - { id, publicKey, type: keyType }, - 'capabilityDelegation' - ) - } - if (web3Name !== undefined) { - newDidDocument.alsoKnownAs = [web3Name] - } - return { - document: newDidDocument, + document, documentMetadata: {}, } } @@ -121,6 +75,9 @@ async function resolveInternal( documentMetadata: { canonicalId: getFullDidUri(did), }, + document: { + id: did, + }, } } @@ -132,6 +89,15 @@ async function resolveInternal( } } +/** + * Implementation of `resolve` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. + * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. + * + * @param did The DID to resolve. + * @param resolutionOptions The resolution options accepted by the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * @returns The resolution result for the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + */ export async function resolve( did: DidDocumentV2.DidUri, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -168,6 +134,16 @@ export async function resolve( } } +/** + * Implementation of `resolveRepresentation` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. + * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. + * + * @param did The DID to resolve. + * @param resolutionOptions The resolution options accepted by the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * @param resolutionOptions.accept The content type accepted by the requesting client. + * @returns The resolution result for the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + */ export async function resolveRepresentation( did: DidDocumentV2.DidUri, { accept }: DidResolverV2.DereferenceOptions = { @@ -254,6 +230,15 @@ async function dereferenceInternal( } } +/** + * Implementation of `dereference` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). + * If a DID URL is invalid or has not been registered, this is indicated by the `error` property on the `dereferencingMetadata`. + * + * @param didUrl The DID URL to dereference. + * @param resolutionOptions The resolution options accepted by the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). + * @param resolutionOptions.accept The content type accepted by the requesting client. + * @returns The resolution result for the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). + */ export async function dereference( didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidUrl, // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -313,6 +298,9 @@ export async function dereference( } } +/** + * Fully-fledged default resolver capable of resolving DIDs in their canonical form, encoded for a specific content type, and of dereferencing parts of a DID Document according to the dereferencing specification. + */ export const resolver: DidResolverV2.DidResolver = { resolve, resolveRepresentation, diff --git a/packages/types/src/DidDocumentV2.ts b/packages/types/src/DidDocumentV2.ts index 061dcf716e..1fb635ac38 100644 --- a/packages/types/src/DidDocumentV2.ts +++ b/packages/types/src/DidDocumentV2.ts @@ -91,11 +91,11 @@ export type DidDocument = { /* * The verificationMethod property is OPTIONAL. If present, the value MUST be a set of verification methods, where each verification method is expressed using a map. */ - verificationMethod: VerificationMethod[] + verificationMethod?: VerificationMethod[] /* * The authentication property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. */ - authentication: UriFragment[] + authentication?: UriFragment[] /* * The assertionMethod property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. */ diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 719461024f..8cebdc5c59 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ /** * Copyright (c) 2018-2023, BOTLabs GmbH. * @@ -396,10 +397,10 @@ export async function createFullDidFromLightDid( const api = ConfigService.get('api') const fullDidDocumentToBeCreated = lightDidForId fullDidDocumentToBeCreated.assertionMethod = [ - fullDidDocumentToBeCreated.authentication[0], + fullDidDocumentToBeCreated.authentication![0], ] fullDidDocumentToBeCreated.capabilityDelegation = [ - fullDidDocumentToBeCreated.authentication[0], + fullDidDocumentToBeCreated.authentication![0], ] const tx = await Did.DidChainV2.getStoreTxFromDidDocument( fullDidDocumentToBeCreated, From b17110888e2726c4c1964ed56e546397da01f0a7 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 6 Oct 2023 13:55:47 +0100 Subject: [PATCH 30/78] Better error messages --- packages/did/src/Did2.signature.ts | 13 ++++------ packages/did/src/Did2.utils.ts | 39 +++++++++++++++++------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts index 37d9acfa47..903a9e8811 100644 --- a/packages/did/src/Did2.signature.ts +++ b/packages/did/src/Did2.signature.ts @@ -105,18 +105,16 @@ export async function verifyDidSignature({ const { didDocument } = await resolveDid(signerUrl, {}) if (didDocument === undefined) { - // TODO: Better error - throw new Error( - `Error validating the DID signature. Cannot fetch DID Document.` + throw new SDKErrors.SignatureUnverifiableError( + `Error validating the DID signature. Cannot fetch DID Document for "${signerUrl}".` ) } const verificationMethod = didDocument.verificationMethod?.find( (vm) => vm.id === signer.fragment ) if (verificationMethod === undefined) { - // TODO: Better error - throw new Error( - `Cannot find verification method with ID "${signer.fragment} in the DID Document.` + throw new SDKErrors.SignatureUnverifiableError( + `Cannot find verification method with ID "${signer.fragment} in the DID Document for "${signerUrl}".` ) } if ( @@ -125,8 +123,7 @@ export async function verifyDidSignature({ (vm) => vm === signer.fragment ) === undefined ) { - // TODO: Better error - throw new Error( + throw new SDKErrors.SignatureUnverifiableError( `Cannot find verification method with ID "${signer.fragment} for the relationship "${expectedVerificationMethodRelationship}".` ) } diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index 3ba6690201..55f9b3e9c2 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -17,6 +17,7 @@ import type { } from '@kiltprotocol/types' import type { DidKeyType } from './DidDetailsv2/DidDetailsV2.js' +import { DidError } from 'utils/src/SDKErrors.js' // The latest version for KILT light DIDs. const LIGHT_DID_LATEST_VERSION = 1 @@ -137,14 +138,20 @@ export function multibaseKeyToDidKey( decodedMulticodecPublicKey.subarray(1), ] const [keyType, expectedPublicKeyLength] = multicodecPrefixes[keyTypeFlag] - if (keyType !== undefined && publicKey.length === expectedPublicKeyLength) { - return { - keyType, - publicKey, - } + if (keyType === undefined) { + throw new SDKErrors.DidError( + `Cannot decode key type for multibase key "${publicKeyMultibase}".` + ) + } + if (publicKey.length !== expectedPublicKeyLength) { + throw new SDKErrors.DidError( + `Key of type "${keyType}" is expected to be ${expectedPublicKeyLength} bytes long. Provided key is ${publicKey.length} bytes long instead.` + ) + } + return { + keyType, + publicKey, } - // TODO: Change to proper error - throw new Error('Invalid encoding of the verification method.') } /** @@ -163,14 +170,14 @@ export function keypairToMultibaseKey({ }): DidDocumentV2.VerificationMethod['publicKeyMultibase'] { const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] if (multiCodecPublicKeyPrefix === undefined) { - // TODO: Proper error - throw new Error(`Invalid key type provided: ${type}.`) + throw new SDKErrors.DidError( + `The provided key type "${type}" is not supported.` + ) } const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] if (publicKey.length !== expectedPublicKeySize) { - // TODO: Proper error - throw new Error( - `Provided public key does not match the expected length: ${expectedPublicKeySize}.` + throw new SDKErrors.DidError( + `Key of type "${type}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` ) } const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] @@ -196,14 +203,12 @@ export function didKeyToVerificationMethod( ): DidDocumentV2.VerificationMethod { const multiCodecPublicKeyPrefix = multicodecReversePrefixes[keyType] if (multiCodecPublicKeyPrefix === undefined) { - // TODO: Proper error - throw new Error(`Invalid key type provided: ${keyType}.`) + throw new DidError(`Provided key type "${keyType}" not supported.`) } const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] if (publicKey.length !== expectedPublicKeySize) { - // TODO: Proper error - throw new Error( - `Provided public key does not match the expected length: ${expectedPublicKeySize}.` + throw new SDKErrors.DidError( + `Key of type "${keyType}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` ) } const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] From 239b670f8faee34e470893408f2218b07ae334a6 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 6 Oct 2023 14:17:00 +0100 Subject: [PATCH 31/78] Fix imports --- packages/did/src/Did2.chain.ts | 8 ++--- packages/did/src/Did2.rpc.ts | 14 ++++++-- packages/did/src/Did2.signature.spec.ts | 22 ++++++------ packages/did/src/Did2.signature.ts | 4 ++- packages/did/src/Did2.utils.ts | 14 ++++---- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 3 +- .../src/DidDetailsv2/FullDidDetailsV2.spec.ts | 35 +++++++++--------- .../did/src/DidDetailsv2/FullDidDetailsV2.ts | 11 +++--- .../DidDetailsv2/LightDidDetailsV2.spec.ts | 36 ++++++++++--------- .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 22 +++++++----- .../did/src/DidLinks/AccountLinks2.chain.ts | 20 +++++------ packages/did/src/DidResolver/DidResolverV2.ts | 8 +++-- packages/types/src/CryptoCallbacksV2.ts | 18 +++++----- tests/integration/Did2.spec.ts | 12 +++---- 14 files changed, 123 insertions(+), 104 deletions(-) diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index 3d30565012..b0843133b4 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -8,7 +8,6 @@ import type { Option } from '@polkadot/types' import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' import type { AnyNumber } from '@polkadot/types/types' - import type { DidDidDetails, DidDidDetailsDidAuthorizedCallOperation, @@ -16,8 +15,7 @@ import type { DidServiceEndpointsDidEndpoint, KiltSupportDeposit, } from '@kiltprotocol/augment-api' - -import { +import type { BN, CryptoCallbacksV2, Deposit, @@ -29,15 +27,15 @@ import { import { ConfigService } from '@kiltprotocol/config' import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' -import { +import type { DidEncryptionKeyType, NewService, DidVerificationKeyType, - verificationKeyTypes, NewDidVerificationKey, NewDidEncryptionKey, } from './DidDetailsv2/DidDetailsV2.js' +import { verificationKeyTypes } from './DidDetailsv2/DidDetailsV2.js' import { multibaseKeyToDidKey, keypairToMultibaseKey, diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts index 0282647076..f01a5f4630 100644 --- a/packages/did/src/Did2.rpc.ts +++ b/packages/did/src/Did2.rpc.ts @@ -18,18 +18,26 @@ import type { KiltAddress, DidDocumentV2 } from '@kiltprotocol/types' import { ss58Format } from '@kiltprotocol/utils' import { encodeAddress } from '@polkadot/keyring' import { ethereumEncode } from '@polkadot/util-crypto' -import { Address, SubstrateAddress } from './DidLinks/AccountLinks2.chain.js' -import { didKeyToVerificationMethod } from './Did2.utils.js' -import { + +import type { + Address, + SubstrateAddress, +} from './DidLinks/AccountLinks2.chain.js' +import type { ChainDidDetails, ChainDidEncryptionKey, ChainDidKey, ChainDidVerificationKey, +} from './Did2.chain.js' + +import { depositFromChain, fragmentIdToChain, fromChain, publicKeyFromChain, } from './Did2.chain.js' + +import { didKeyToVerificationMethod } from './Did2.utils.js' import { addKeypairAsVerificationMethod } from './DidDetailsv2/DidDetailsV2.js' function documentFromChain( diff --git a/packages/did/src/Did2.signature.spec.ts b/packages/did/src/Did2.signature.spec.ts index c86d0c4a21..96ec8aa169 100644 --- a/packages/did/src/Did2.signature.spec.ts +++ b/packages/did/src/Did2.signature.spec.ts @@ -11,12 +11,15 @@ import type { CryptoCallbacksV2, KeyringPair, } from '@kiltprotocol/types' + import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' +import type { DidSignature } from './Did2.signature' +import type { NewLightDidVerificationKey } from './DidDetailsv2' + import { TestUtilsV2 } from '../../../tests/testUtils' import { - DidSignature, isDidSignature, signatureFromJson, signatureToJson, @@ -28,10 +31,7 @@ import { multibaseKeyToDidKey, parse, } from './Did2.utils' -import { - createLightDidDocument, - NewLightDidVerificationKey, -} from './DidDetailsv2' +import { createLightDidDocument } from './DidDetailsv2' jest.mock('./DidResolver/DidResolverV2') jest @@ -219,7 +219,7 @@ describe('light DID', () => { it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - signerUrl: `${did.id}${did.authentication[0]}`, + signerUrl: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -256,8 +256,8 @@ describe('light DID', () => { verificationMethodRelationship: 'authentication', }) - const authKey = did.verificationMethod.find( - (vm) => vm.id === did.authentication[0] + const authKey = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] ) const expectedSignerAuthKey = multibaseKeyToDidKey( authKey!.publicKeyMultibase @@ -413,8 +413,8 @@ describe('full DID', () => { verificationMethodRelationship: 'authentication', }) - const authKey = did.verificationMethod.find( - (vm) => vm.id === did.authentication[0] + const authKey = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] ) const expectedSignerAuthKey = multibaseKeyToDidKey( authKey!.publicKeyMultibase @@ -452,7 +452,7 @@ describe('full DID', () => { it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - signerUrl: `${did.id}${did.authentication[0]}`, + signerUrl: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts index 903a9e8811..0cabadd3d0 100644 --- a/packages/did/src/Did2.signature.ts +++ b/packages/did/src/Did2.signature.ts @@ -5,13 +5,15 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { +import type { CryptoCallbacksV2, DidDocumentV2, DidResolverV2, } from '@kiltprotocol/types' + import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { isHex } from '@polkadot/util' + import { multibaseKeyToDidKey, parse, validateUri } from './Did2.utils.js' import { resolve } from './DidResolver/DidResolverV2.js' diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts index 55f9b3e9c2..73dd7b3a1f 100644 --- a/packages/did/src/Did2.utils.ts +++ b/packages/did/src/Did2.utils.ts @@ -5,19 +5,17 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' - -import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' -import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' - import type { DidDocumentV2, KeyringPair, KiltAddress, } from '@kiltprotocol/types' +import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' +import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' +import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' + import type { DidKeyType } from './DidDetailsv2/DidDetailsV2.js' -import { DidError } from 'utils/src/SDKErrors.js' // The latest version for KILT light DIDs. const LIGHT_DID_LATEST_VERSION = 1 @@ -203,7 +201,9 @@ export function didKeyToVerificationMethod( ): DidDocumentV2.VerificationMethod { const multiCodecPublicKeyPrefix = multicodecReversePrefixes[keyType] if (multiCodecPublicKeyPrefix === undefined) { - throw new DidError(`Provided key type "${keyType}" not supported.`) + throw new SDKErrors.DidError( + `Provided key type "${keyType}" not supported.` + ) } const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] if (publicKey.length !== expectedPublicKeySize) { diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts index 800658115e..ab55cc94ff 100644 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/DidDetailsV2.ts @@ -5,7 +5,8 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidDocumentV2 } from '@kiltprotocol/types' +import type { DidDocumentV2 } from '@kiltprotocol/types' + import { didKeyToVerificationMethod } from '../Did2.utils.js' /** diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts index e93d79ca7e..3443cd8b90 100644 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts @@ -5,10 +5,6 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { BN } from '@polkadot/util' -import { randomAsHex } from '@polkadot/util-crypto' - -import { ConfigService } from '@kiltprotocol/config' import type { CryptoCallbacksV2, DidDocumentV2, @@ -16,11 +12,16 @@ import type { SubmittableExtrinsic, } from '@kiltprotocol/types' -import { - ApiMocks, TestUtilsV2, -} from '../../../../tests/testUtils' +import { BN } from '@polkadot/util' +import { randomAsHex } from '@polkadot/util-crypto' +import { ConfigService } from '@kiltprotocol/config' + +import { ApiMocks, TestUtilsV2 } from '../../../../tests/testUtils' import { generateDidAuthenticatedTx } from '../Did2.chain.js' -import * as Did from './index.js' +import { + authorizeBatch, + getVerificationMethodRelationshipForTx, +} from './FullDidDetailsV2' const augmentedApi = ApiMocks.createAugmentedApi() const mockedApi: any = ApiMocks.getMockedApi() @@ -56,7 +57,7 @@ describe('When creating an instance from the chain', () => { it('fails if the extrinsic does not require a DID', async () => { const extrinsic = augmentedApi.tx.indices.claim(1) await expect(async () => - Did.authorizeBatch({ + authorizeBatch({ did: fullDid.id, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [extrinsic, extrinsic], @@ -74,7 +75,7 @@ describe('When creating an instance from the chain', () => { ]) const batchFunction = jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll - await Did.authorizeBatch({ + await authorizeBatch({ did: fullDid.id, batchFunction, extrinsics: [extrinsic, extrinsic], @@ -111,7 +112,7 @@ describe('When creating an instance from the chain', () => { ctype3Extrinsic, ctype4Extrinsic, ] - await Did.authorizeBatch({ + await authorizeBatch({ did: fullDid.id, batchFunction, extrinsics, @@ -139,7 +140,7 @@ describe('When creating an instance from the chain', () => { describe('.build()', () => { it('throws if batch is empty', async () => { await expect(async () => - Did.authorizeBatch({ + authorizeBatch({ did: fullDid.id, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [], @@ -164,13 +165,13 @@ const mockApi = ApiMocks.createAugmentedApi() describe('When creating an instance from the chain', () => { it('Should return correct KeyRelationship for single valid call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) ) expect(keyRelationship).toBe('assertionMethod') }) it('Should return correct KeyRelationship for batched call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.batch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -187,7 +188,7 @@ describe('When creating an instance from the chain', () => { expect(keyRelationship).toBe('assertionMethod') }) it('Should return correct KeyRelationship for batchAll call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.batchAll([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -204,7 +205,7 @@ describe('When creating an instance from the chain', () => { expect(keyRelationship).toBe('assertionMethod') }) it('Should return correct KeyRelationship for forceBatch call', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -221,7 +222,7 @@ describe('When creating an instance from the chain', () => { expect(keyRelationship).toBe('assertionMethod') }) it('Should return undefined for batch with mixed KeyRelationship calls', () => { - const keyRelationship = Did.getVerificationMethodRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts index 748318c6af..1f17ec5ca7 100644 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts @@ -5,14 +5,8 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { BN } from '@polkadot/util' - -import { ConfigService } from '@kiltprotocol/config' -import { SDKErrors } from '@kiltprotocol/utils' - import type { Extrinsic } from '@polkadot/types/interfaces' import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' - import type { CryptoCallbacksV2, DidDocumentV2, @@ -20,6 +14,11 @@ import type { SubmittableExtrinsic, } from '@kiltprotocol/types' +import { BN } from '@polkadot/util' + +import { ConfigService } from '@kiltprotocol/config' +import { SDKErrors } from '@kiltprotocol/utils' + import { parse } from '../Did2.utils.js' import { documentFromChain, diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts index 9d72164888..cac2acb49f 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts @@ -5,12 +5,18 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidDocumentV2 } from '@kiltprotocol/types' +import type { DidDocumentV2 } from '@kiltprotocol/types' + import { Crypto } from '@kiltprotocol/utils' -import { keypairToMultibaseKey, parse } from '../Did2.utils.js' -import * as Did from './index.js' import type { NewService } from './DidDetailsV2.js' +import type { CreateDocumentInput } from './LightDidDetailsV2.js' + +import { keypairToMultibaseKey, parse } from '../Did2.utils.js' +import { + createLightDidDocument, + parseDocumentFromLightDid, +} from './LightDidDetailsV2.js' /* * Functions tested: @@ -41,7 +47,7 @@ describe('When creating an instance from the details', () => { }, ] - const lightDid = Did.createLightDidDocument({ + const lightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, @@ -92,7 +98,7 @@ describe('When creating an instance from the details', () => { new Uint8Array(32).fill(1) ) - const lightDid = Did.createLightDidDocument({ + const lightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], }) @@ -133,9 +139,7 @@ describe('When creating an instance from the details', () => { authentication: [authKey], } expect(() => - Did.createLightDidDocument( - invalidInput as unknown as Did.CreateDocumentInput - ) + createLightDidDocument(invalidInput as unknown as CreateDocumentInput) ).toThrowError() }) @@ -148,9 +152,7 @@ describe('When creating an instance from the details', () => { keyAgreement: [{ publicKey: encKey.publicKey, type: 'bls' }], } expect(() => - Did.createLightDidDocument( - invalidInput as unknown as Did.CreateDocumentInput - ) + createLightDidDocument(invalidInput as unknown as CreateDocumentInput) ).toThrowError() }) }) @@ -174,14 +176,14 @@ describe('When creating an instance from a URI', () => { }, ] // We are sure this is correct because of the described case above - const expectedLightDid = Did.createLightDidDocument({ + const expectedLightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service: endpoints, }) const { address } = parse(expectedLightDid.id) - const builtLightDid = Did.parseDocumentFromLightDid(expectedLightDid.id) + const builtLightDid = parseDocumentFromLightDid(expectedLightDid.id) expect(builtLightDid).toStrictEqual(expectedLightDid) expect(builtLightDid).toStrictEqual({ @@ -240,7 +242,7 @@ describe('When creating an instance from a URI', () => { ] // We are sure this is correct because of the described case above - const expectedLightDid = Did.createLightDidDocument({ + const expectedLightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, @@ -248,9 +250,9 @@ describe('When creating an instance from a URI', () => { const uriWithFragment: DidDocumentV2.DidUri = `${expectedLightDid.id}#authentication` - expect(() => Did.parseDocumentFromLightDid(uriWithFragment, true)).toThrow() + expect(() => parseDocumentFromLightDid(uriWithFragment, true)).toThrow() expect(() => - Did.parseDocumentFromLightDid(uriWithFragment, false) + parseDocumentFromLightDid(uriWithFragment, false) ).not.toThrow() }) @@ -271,7 +273,7 @@ describe('When creating an instance from a URI', () => { ] incorrectURIs.forEach((uri) => { expect(() => - Did.parseDocumentFromLightDid(uri as DidDocumentV2.DidUri) + parseDocumentFromLightDid(uri as DidDocumentV2.DidUri) ).toThrow() }) }) diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts index 226dee809f..9cedafe184 100644 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts @@ -5,13 +5,22 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidDocumentV2 } from '@kiltprotocol/types' -import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import type { DidDocumentV2 } from '@kiltprotocol/types' + import { base58Decode, base58Encode, decodeAddress, } from '@polkadot/util-crypto' +import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' + +import type { + NewDidEncryptionKey, + NewDidVerificationKey, + NewService, + DidVerificationKeyType, +} from './DidDetailsV2.js' + import { keypairToMultibaseKey, didKeyToVerificationMethod, @@ -19,12 +28,9 @@ import { parse, } from '../Did2.utils.js' import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' -import { addKeypairAsVerificationMethod, encryptionKeyTypes } from './DidDetailsV2.js' -import type { - NewDidEncryptionKey, - NewDidVerificationKey, - NewService, - DidVerificationKeyType, +import { + addKeypairAsVerificationMethod, + encryptionKeyTypes, } from './DidDetailsV2.js' /** diff --git a/packages/did/src/DidLinks/AccountLinks2.chain.ts b/packages/did/src/DidLinks/AccountLinks2.chain.ts index b0c5402e9c..319b284ab4 100644 --- a/packages/did/src/DidLinks/AccountLinks2.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks2.chain.ts @@ -5,27 +5,27 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' import type { TypeDef } from '@polkadot/types/types' import type { KeypairType } from '@polkadot/util-crypto/types' +import type { ApiPromise } from '@polkadot/api' +import type { BN } from '@polkadot/util' +import type { + HexString, + DidDocumentV2, + KeyringPair, + KiltAddress, +} from '@kiltprotocol/types' + +import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' import { stringToU8a, U8A_WRAP_ETHEREUM, u8aConcatStrict, u8aToHex, u8aWrapBytes, - BN, } from '@polkadot/util' -import { ApiPromise } from '@polkadot/api' - import { SDKErrors } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' -import type { - HexString, - DidDocumentV2, - KeyringPair, - KiltAddress, -} from '@kiltprotocol/types' import { toChain, EncodedSignature } from '../Did2.chain.js' diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts index 2c9cb61c0d..527865753b 100644 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ b/packages/did/src/DidResolver/DidResolverV2.ts @@ -5,14 +5,16 @@ * found in the LICENSE file in the root directory of this source tree. */ +import type { DidDocumentV2, DidResolverV2 } from '@kiltprotocol/types' + import { ConfigService } from '@kiltprotocol/config' -import { DidDocumentV2, DidResolverV2 } from '@kiltprotocol/types' import { cbor } from '@kiltprotocol/utils' + +import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' import { linkedInfoFromChain } from '../Did2.rpc.js' import { toChain } from '../Did2.chain.js' import { getFullDidUri, parse, validateUri } from '../Did2.utils.js' import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' const DID_JSON = 'application/did+json' const DID_JSON_LD = 'application/did+ld+json' @@ -213,7 +215,7 @@ async function dereferenceInternal( } } const dereferencedResource = (() => { - const verificationMethod = didDocument?.verificationMethod.find( + const verificationMethod = didDocument?.verificationMethod?.find( (m) => m.id === fragment ) if (verificationMethod !== undefined) { diff --git a/packages/types/src/CryptoCallbacksV2.ts b/packages/types/src/CryptoCallbacksV2.ts index 3a1319438e..e06fb35b70 100644 --- a/packages/types/src/CryptoCallbacksV2.ts +++ b/packages/types/src/CryptoCallbacksV2.ts @@ -5,8 +5,11 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from './DidDocumentV2' -import { DidDocumentV2 } from './index.js' +import type { + DidUri, + SignatureVerificationMethodRelationship, + VerificationMethod, +} from './DidDocumentV2' /** * Base interface for all signing requests. @@ -20,7 +23,7 @@ export interface SignRequestData { /** * The did key relationship to be used. */ - verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship + verificationMethodRelationship: SignatureVerificationMethodRelationship /** * The DID to be used for signing. @@ -39,10 +42,7 @@ export interface SignResponseData { /** * The did key uri used for signing. */ - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' | 'id' - > + verificationMethod: Pick } /** @@ -92,7 +92,7 @@ export interface EncryptResponseData { /** * The did key uri used for the encryption. */ - verificationMethod: DidDocumentV2.VerificationMethod + verificationMethod: VerificationMethod } /** @@ -121,7 +121,7 @@ export interface DecryptRequestData { /** * The did key uri, which should be used for decryption. */ - verificationMethod: DidDocumentV2.VerificationMethod + verificationMethod: VerificationMethod } export interface DecryptResponseData { diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts index cc94f14549..16928e3955 100644 --- a/tests/integration/Did2.spec.ts +++ b/tests/integration/Did2.spec.ts @@ -6,12 +6,7 @@ */ import type { ApiPromise } from '@polkadot/api' -import { BN } from '@polkadot/util' - -import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' -import { UUID } from '@kiltprotocol/utils' -import * as Did from '@kiltprotocol/did' -import { +import type { CryptoCallbacksV2, DidDocumentV2, DidResolverV2, @@ -19,6 +14,11 @@ import { Permission, } from '@kiltprotocol/types' +import { BN } from '@polkadot/util' +import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' +import { UUID } from '@kiltprotocol/utils' +import * as Did from '@kiltprotocol/did' + import { TestUtilsV2 } from '../testUtils/index.js' import { createEndowedTestAccount, From c5660696ecec486bb5a831333e0b9784b8ea4276 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 6 Oct 2023 14:54:52 +0100 Subject: [PATCH 32/78] Refactoring --- packages/did/src/Did2.chain.ts | 157 +++++++++++++++++++++++++++++++++ tests/integration/Did2.spec.ts | 10 +-- tests/testUtils/TestUtils2.ts | 13 ++- 3 files changed, 171 insertions(+), 9 deletions(-) diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts index b0843133b4..545555ff23 100644 --- a/packages/did/src/Did2.chain.ts +++ b/packages/did/src/Did2.chain.ts @@ -25,6 +25,10 @@ import type { } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' +import { + verificationKeyTypesMap, + encryptionKeyTypesMap, +} from '@kiltprotocol/types' import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' import type { @@ -447,6 +451,159 @@ export async function getStoreTxFromInput( return api.tx.did.create(encoded, encodedSignature) } +/** + * Create a DID creation operation which would write to chain the DID Document provided as input. + * Only the first authentication, assertion, and capability delegation verification methods are considered from the input DID Document. + * All the input DID Document key agreement verification methods are considered. + * + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and service endpoints. + * + * A DID creation operation can contain at most 25 new service endpoints. + * Additionally, each service endpoint must respect the following conditions: + * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. + * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * + * @param input The DID Document to store. + * @param submitter The KILT address authorized to submit the creation operation. + * @param sign The sign callback. The authentication key has to be used. + * + * @returns The SubmittableExtrinsic for the DID creation operation. + */ +export async function getStoreTxFromDidDocument( + input: DidDocumentV2.DidDocument, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const { + authentication, + assertionMethod, + keyAgreement, + capabilityDelegation, + service, + verificationMethod, + } = input + + const authKey = (() => { + const authenticationMethodId = authentication?.[0] + if (authenticationMethodId === undefined) { + throw new SDKErrors.DidError( + 'Cannot create a DID without an authentication method.' + ) + } + const authVerificationMethod = verificationMethod?.find( + (vm) => vm.id === authenticationMethodId + ) + if (authVerificationMethod === undefined) { + throw new SDKErrors.DidError( + `Cannot find the authentication method with ID "${authenticationMethodId}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey( + authVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + throw new SDKErrors.DidError( + `Provided authentication key has an unsupported key type "${keyType}".` + ) + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const keyAgreementKeys = (() => { + if (keyAgreement === undefined || keyAgreement.length === 0) { + return undefined + } + return keyAgreement.map((k) => { + const vm = verificationMethod?.find((_vm) => _vm.id === k) + if (vm === undefined) { + throw new SDKErrors.DidError( + `Cannot find the key agreement method with ID "${k}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey(vm.publicKeyMultibase) + if (encryptionKeyTypesMap[keyType] === undefined) { + throw new SDKErrors.DidError( + `The key agreement key with ID "${k}" has an unsupported key type ${keyType}.` + ) + } + return { + type: keyType, + publicKey, + } as NewDidEncryptionKey + }) + })() + + const assertionMethodKey = (() => { + const assertionMethodId = assertionMethod?.[0] + if (assertionMethodId === undefined) { + return undefined + } + const assertionVerificationMethod = verificationMethod?.find( + (vm) => vm.id === assertionMethodId + ) + if (assertionVerificationMethod === undefined) { + throw new SDKErrors.DidError( + `Cannot find the assertion method with ID "${assertionMethodId}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey( + assertionVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + throw new SDKErrors.DidError( + `The assertion method key with ID "${assertionMethodId}" has an unsupported key type ${keyType}.` + ) + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const capabilityDelegationKey = (() => { + const capabilityDelegationId = capabilityDelegation?.[0] + if (capabilityDelegationId === undefined) { + return undefined + } + const capabilityDelegationVerificationMethod = verificationMethod?.find( + (vm) => vm.id === capabilityDelegationId + ) + if (capabilityDelegationVerificationMethod === undefined) { + throw new SDKErrors.DidError( + `Cannot find the capability delegation method with ID "${capabilityDelegationId}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey( + capabilityDelegationVerificationMethod.publicKeyMultibase + ) + if (verificationKeyTypesMap[keyType] === undefined) { + throw new SDKErrors.DidError( + `The capability delegation method key with ID "${capabilityDelegationId}" has an unsupported key type ${keyType}.` + ) + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const storeTxInput: GetStoreTxInput = { + authentication: [authKey], + assertionMethod: assertionMethodKey ? [assertionMethodKey] : undefined, + capabilityDelegation: capabilityDelegationKey + ? [capabilityDelegationKey] + : undefined, + keyAgreement: keyAgreementKeys, + service, + } + + return getStoreTxFromInput(storeTxInput, submitter, sign) +} + export interface SigningOptions { sign: CryptoCallbacksV2.SignExtrinsicCallback verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts index 16928e3955..6a7f156b77 100644 --- a/tests/integration/Did2.spec.ts +++ b/tests/integration/Did2.spec.ts @@ -11,11 +11,11 @@ import type { DidDocumentV2, DidResolverV2, KiltKeyringPair, - Permission, } from '@kiltprotocol/types' import { BN } from '@polkadot/util' import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' +import { Permission } from '@kiltprotocol/types' import { UUID } from '@kiltprotocol/utils' import * as Did from '@kiltprotocol/did' @@ -63,8 +63,8 @@ describe('write and didDeleteTx', () => { }, 60_000) it('writes a new DID record to chain', async () => { - const { publicKeyMultibase } = did.verificationMethod.find( - (vm) => vm.id === did.authentication[0] + const { publicKeyMultibase } = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] ) as DidDocumentV2.VerificationMethod const { keyType, publicKey: authPublicKey } = Did.DidUtilsV2.multibaseKeyToDidKey(publicKeyMultibase) @@ -873,14 +873,14 @@ describe('DID management batching', () => { extrinsics: [ api.tx.did.removeKeyAgreementKey( Did.DidChainV2.fragmentIdToChain( - initialFullDid.verificationMethod.find( + initialFullDid.verificationMethod!.find( (vm) => vm.id === encryptionKeys[0] )!.id ) ), api.tx.did.removeKeyAgreementKey( Did.DidChainV2.fragmentIdToChain( - initialFullDid.verificationMethod.find( + initialFullDid.verificationMethod!.find( (vm) => vm.id === encryptionKeys[1] )!.id ) diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts index 8cebdc5c59..6b42ecac4d 100644 --- a/tests/testUtils/TestUtils2.ts +++ b/tests/testUtils/TestUtils2.ts @@ -48,7 +48,7 @@ export function makeEncryptCallback({ if (keyId === undefined) { throw new Error(`Encryption key not found in did "${didDocument.id}"`) } - const verificationMethod = didDocument.verificationMethod.find( + const verificationMethod = didDocument.verificationMethod?.find( (v) => v.id === keyId ) as DidDocumentV2.VerificationMethod const { box, nonce } = Crypto.encryptAsymmetric( @@ -130,7 +130,7 @@ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` ) } - const verificationMethod = didDocument.verificationMethod.find( + const verificationMethod = didDocument.verificationMethod?.find( (vm) => vm.id === keyId ) if (verificationMethod === undefined) { @@ -203,7 +203,9 @@ function doesVerificationMethodExist( didDocument: DidDocumentV2.DidDocument, { id }: Pick ): boolean { - return didDocument.verificationMethod.find((vm) => vm.id === id) !== undefined + return ( + didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined + ) } function addVerificationMethod( @@ -216,7 +218,10 @@ function addVerificationMethod( // eslint-disable-next-line no-param-reassign didDocument[relationship] = existingRelationship if (!doesVerificationMethodExist(didDocument, verificationMethod)) { - didDocument.verificationMethod.push(verificationMethod) + const existingVerificationMethod = didDocument.verificationMethod ?? [] + existingVerificationMethod.push(verificationMethod) + // eslint-disable-next-line no-param-reassign + didDocument.verificationMethod = existingVerificationMethod } } From ccb9533d94a626254c2c58cf2ac65621b49d2562 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 10 Oct 2023 09:13:59 +0100 Subject: [PATCH 33/78] First refactor --- .../core/src/credential/Credential.spec.ts | 4 +- packages/core/src/credential/Credential.ts | 14 +- packages/did/src/Did.chain.ts | 433 +++++++---- packages/did/src/Did.rpc.ts | 185 +++-- packages/did/src/Did.signature.spec.ts | 325 +++++---- packages/did/src/Did.signature.ts | 138 ++-- packages/did/src/Did.utils.ts | 173 ++++- packages/did/src/Did2.chain.ts | 681 ------------------ packages/did/src/Did2.rpc.ts | 219 ------ packages/did/src/Did2.signature.spec.ts | 544 -------------- packages/did/src/Did2.signature.ts | 202 ------ packages/did/src/Did2.utils.ts | 328 --------- .../did/src/DidDetails/DidDetails.spec.ts | 122 ---- packages/did/src/DidDetails/DidDetails.ts | 134 +++- .../did/src/DidDetails/FullDidDetails.spec.ts | 53 +- packages/did/src/DidDetails/FullDidDetails.ts | 65 +- .../src/DidDetails/LightDidDetails.spec.ts | 122 ++-- .../did/src/DidDetails/LightDidDetails.ts | 111 +-- packages/did/src/DidDetails/index.ts | 12 +- packages/did/src/DidDetailsv2/DidDetailsV2.ts | 108 --- .../src/DidDetailsv2/FullDidDetailsV2.spec.ts | 237 ------ .../did/src/DidDetailsv2/FullDidDetailsV2.ts | 267 ------- .../DidDetailsv2/LightDidDetailsV2.spec.ts | 280 ------- .../did/src/DidDetailsv2/LightDidDetailsV2.ts | 327 --------- packages/did/src/DidDetailsv2/index.ts | 20 - .../DidDocumentExporter.spec.ts | 336 --------- .../DidDocumentExporter.ts | 114 --- .../did/src/DidDocumentExporter/README.md | 5 - packages/did/src/DidDocumentExporter/index.ts | 9 - .../did/src/DidLinks/AccountLinks.chain.ts | 23 +- .../did/src/DidLinks/AccountLinks2.chain.ts | 291 -------- .../DidContexts.ts | 0 packages/did/src/DidResolver/DidContextsV2.ts | 110 --- packages/did/src/DidResolver/DidResolver.ts | 399 +++++----- packages/did/src/DidResolver/DidResolverV2.ts | 310 -------- ...dResolver.spec.ts => _DidResolver.spec.ts} | 0 packages/did/src/index.ts | 11 +- packages/types/src/CryptoCallbacks.ts | 21 +- packages/types/src/CryptoCallbacksV2.ts | 143 ---- packages/types/src/DidDocument.ts | 193 ++--- packages/types/src/DidDocumentExporter.ts | 108 --- packages/types/src/DidDocumentV2.ts | 117 --- packages/types/src/DidResolver.ts | 279 +++++-- packages/types/src/DidResolverV2.ts | 251 ------- packages/types/src/index.ts | 5 - 45 files changed, 1677 insertions(+), 6152 deletions(-) delete mode 100644 packages/did/src/Did2.chain.ts delete mode 100644 packages/did/src/Did2.rpc.ts delete mode 100644 packages/did/src/Did2.signature.spec.ts delete mode 100644 packages/did/src/Did2.signature.ts delete mode 100644 packages/did/src/Did2.utils.ts delete mode 100644 packages/did/src/DidDetails/DidDetails.spec.ts delete mode 100644 packages/did/src/DidDetailsv2/DidDetailsV2.ts delete mode 100644 packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts delete mode 100644 packages/did/src/DidDetailsv2/FullDidDetailsV2.ts delete mode 100644 packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts delete mode 100644 packages/did/src/DidDetailsv2/LightDidDetailsV2.ts delete mode 100644 packages/did/src/DidDetailsv2/index.ts delete mode 100644 packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts delete mode 100644 packages/did/src/DidDocumentExporter/DidDocumentExporter.ts delete mode 100644 packages/did/src/DidDocumentExporter/README.md delete mode 100644 packages/did/src/DidDocumentExporter/index.ts delete mode 100644 packages/did/src/DidLinks/AccountLinks2.chain.ts rename packages/did/src/{DidDocumentExporter => DidResolver}/DidContexts.ts (100%) delete mode 100644 packages/did/src/DidResolver/DidContextsV2.ts delete mode 100644 packages/did/src/DidResolver/DidResolverV2.ts rename packages/did/src/DidResolver/{DidResolver.spec.ts => _DidResolver.spec.ts} (100%) delete mode 100644 packages/types/src/CryptoCallbacksV2.ts delete mode 100644 packages/types/src/DidDocumentExporter.ts delete mode 100644 packages/types/src/DidDocumentV2.ts delete mode 100644 packages/types/src/DidResolverV2.ts diff --git a/packages/core/src/credential/Credential.spec.ts b/packages/core/src/credential/Credential.spec.ts index ffa25284f6..42b344f4c4 100644 --- a/packages/core/src/credential/Credential.spec.ts +++ b/packages/core/src/credential/Credential.spec.ts @@ -441,7 +441,7 @@ describe('Presentations', () => { async function didResolveKey( keyUri: DidResourceUri - ): Promise { + ): Promise { const { did } = Did.parse(keyUri) const document = [ identityAlice, @@ -450,7 +450,7 @@ describe('Presentations', () => { identityDave, ].find(({ uri }) => uri === did) if (!document) throw new Error('Cannot resolve mocked DID') - return Did.keyToResolvedKey(document.authentication[0], did) + return Did.resolve(document.authentication[0], did) } // TODO: Cleanup file by migrating setup functions and removing duplicate tests. diff --git a/packages/core/src/credential/Credential.ts b/packages/core/src/credential/Credential.ts index ca24f967f2..65e26408c7 100644 --- a/packages/core/src/credential/Credential.ts +++ b/packages/core/src/credential/Credential.ts @@ -21,12 +21,12 @@ import { ConfigService } from '@kiltprotocol/config' import { isDidSignature, verifyDidSignature, - resolveKey, + resolve, signatureToJson, signatureFromJson, } from '@kiltprotocol/did' import type { - DidResolveKey, + ResolveDid, DidUri, Hash, IAttestation, @@ -206,17 +206,17 @@ export function verifyDataStructure(input: ICredential): void { * * @param input - The [[ICredentialPresentation]]. * @param verificationOpts Additional verification options. - * @param verificationOpts.didResolveKey - The function used to resolve the claimer's key. Defaults to [[resolveKey]]. + * @param verificationOpts.didResolve - The function used to resolve the claimer's DID Document and verification method. Defaults to [[resolve]]. * @param verificationOpts.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. */ export async function verifySignature( input: ICredentialPresentation, { challenge, - didResolveKey = resolveKey, + didResolve = resolve, }: { challenge?: string - didResolveKey?: DidResolveKey + didResolve?: ResolveDid } = {} ): Promise { const { claimerSignature } = input @@ -233,8 +233,8 @@ export async function verifySignature( expectedSigner: input.claim.owner, // allow full did to sign presentation if owned by corresponding light did allowUpgraded: true, - expectedVerificationMethod: 'authentication', - didResolveKey, + expectedVerificationMethodRelationship: 'authentication', + didResolve, }) } diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 09daf45e41..fdd54f2b43 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -8,54 +8,61 @@ import type { Option } from '@polkadot/types' import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' import type { AnyNumber } from '@polkadot/types/types' - import type { + DidDidDetails, + DidDidDetailsDidAuthorizedCallOperation, + DidDidDetailsDidPublicKeyDetails, + DidServiceEndpointsDidEndpoint, + KiltSupportDeposit, +} from '@kiltprotocol/augment-api' +import type { + BN, Deposit, DidDocument, - DidEncryptionKey, - DidKey, - DidServiceEndpoint, DidUri, - DidVerificationKey, KiltAddress, - NewDidEncryptionKey, - NewDidVerificationKey, + Service, + SignatureVerificationMethodRelationship, SignExtrinsicCallback, SignRequestData, SignResponseData, SubmittableExtrinsic, UriFragment, - VerificationKeyRelationship, - BN, + VerificationMethod, } from '@kiltprotocol/types' -import { verificationKeyTypes } from '@kiltprotocol/types' -import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' + import { ConfigService } from '@kiltprotocol/config' +import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' + import type { - DidDidDetails, - DidDidDetailsDidAuthorizedCallOperation, - DidDidDetailsDidPublicKey, - DidDidDetailsDidPublicKeyDetails, - DidServiceEndpointsDidEndpoint, - KiltSupportDeposit, -} from '@kiltprotocol/augment-api' + DidEncryptionKeyType, + NewService, + DidVerificationKeyType, + NewDidVerificationKey, + NewDidEncryptionKey, +} from './DidDetails/DidDetails.js' import { - EncodedEncryptionKey, - EncodedKey, - EncodedSignature, - EncodedVerificationKey, - getAddressByKey, + isValidVerificationKeyType, + isValidEncryptionKeyType, +} from './DidDetails/DidDetails.js' +import { + multibaseKeyToDidKey, + keypairToMultibaseKey, + getAddressFromVerificationMethod, getFullDidUri, parse, } from './Did.utils.js' -// ### Chain type definitions +export type ChainDidIdentifier = KiltAddress -export type ChainDidPublicKey = DidDidDetailsDidPublicKey -export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails - -// ### RAW QUERYING (lowest layer) +export type EncodedVerificationKey = + | { sr25519: Uint8Array } + | { ed25519: Uint8Array } + | { ecdsa: Uint8Array } +export type EncodedEncryptionKey = { x25519: Uint8Array } +export type EncodedDidKey = EncodedVerificationKey | EncodedEncryptionKey +export type EncodedSignature = EncodedVerificationKey /** * Format a DID to be used as a parameter for the blockchain API functions. @@ -63,20 +70,30 @@ export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails * @param did The DID to format. * @returns The blockchain-formatted DID. */ -export function toChain(did: DidUri): KiltAddress { +export function toChain(did: DidUri): ChainDidIdentifier { return parse(did).address } /** - * Format a DID resource ID to be used as a parameter for the blockchain API functions. + * Format a DID fragment to be used as a parameter for the blockchain API functions. - * @param id The DID resource ID to format. + * @param id The DID fragment to format. * @returns The blockchain-formatted ID. */ -export function resourceIdToChain(id: UriFragment): string { +export function fragmentIdToChain(id: UriFragment): string { return id.replace(/^#/, '') } +/** + * Convert the DID data from blockchain format to the DID URI. + * + * @param encoded The chain-formatted DID. + * @returns The DID URI. + */ +export function fromChain(encoded: AccountId32): DidUri { + return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) +} + /** * Convert the deposit data coming from the blockchain to JS object. * @@ -90,42 +107,57 @@ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { } } -// ### DECODED QUERYING types +export type ChainDidBaseKey = { + id: UriFragment + publicKey: Uint8Array + includedAt?: BN + type: string +} +export type ChainDidVerificationKey = ChainDidBaseKey & { + type: DidVerificationKeyType +} +export type ChainDidEncryptionKey = ChainDidBaseKey & { + type: DidEncryptionKeyType +} +export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey +export type ChainDidService = { + id: string + serviceTypes: string[] + urls: string[] +} +export type ChainDidDetails = { + authentication: [ChainDidVerificationKey] + assertionMethod?: [ChainDidVerificationKey] + capabilityDelegation?: [ChainDidVerificationKey] + keyAgreement?: ChainDidEncryptionKey[] + + service?: ChainDidService[] -type ChainDocument = Pick< - DidDocument, - 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'keyAgreement' -> & { lastTxCounter: BN deposit: Deposit } -// ### DECODED QUERYING (builds on top of raw querying) - -function didPublicKeyDetailsFromChain( +/** + * Convert a DID public key from the blockchain format to a JS object. + * + * @param keyId The key ID. + * @param keyDetails The associated public key blockchain-formatted details. + * @returns The JS-formatted DID key. + */ +export function publicKeyFromChain( keyId: Hash, - keyDetails: ChainDidPublicKeyDetails -): DidKey { + keyDetails: DidDidDetailsDidPublicKeyDetails +): ChainDidKey { const key = keyDetails.key.isPublicEncryptionKey ? keyDetails.key.asPublicEncryptionKey : keyDetails.key.asPublicVerificationKey return { id: `#${keyId.toHex()}`, - type: key.type.toLowerCase() as DidKey['type'], publicKey: key.value.toU8a(), + type: key.type.toLowerCase() as ChainDidKey['type'], } } -/** - * Convert the DID data from blockchain format to the DID URI. - * - * @param encoded The chain-formatted DID. - * @returns The DID URI. - */ -export function fromChain(encoded: AccountId32): DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) -} - /** * Convert the DID Document data from the blockchain format to a JS object. * @@ -134,7 +166,7 @@ export function fromChain(encoded: AccountId32): DidUri { */ export function documentFromChain( encoded: Option -): ChainDocument { +): ChainDidDetails { const { publicKeys, authenticationKey, @@ -145,28 +177,28 @@ export function documentFromChain( deposit, } = encoded.unwrap() - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => - didPublicKeyDetailsFromChain(keyId, keyDetails) - ) + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) .reduce((res, key) => { - res[resourceIdToChain(key.id)] = key + res[fragmentIdToChain(key.id)] = key return res }, {}) - const authentication = keys[authenticationKey.toHex()] as DidVerificationKey + const authentication = keys[ + authenticationKey.toHex() + ] as ChainDidVerificationKey - const didRecord: ChainDocument = { + const didRecord: ChainDidDetails = { authentication: [authentication], lastTxCounter: lastTxCounter.toBn(), deposit: depositFromChain(deposit), } if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.assertionMethod = [key] } if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.capabilityDelegation = [key] } @@ -175,25 +207,31 @@ export function documentFromChain( ) if (keyAgreementKeyIds.length > 0) { didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as DidEncryptionKey + (id) => keys[id] as ChainDidEncryptionKey ) } return didRecord } -interface ChainEndpoint { - id: string - serviceTypes: DidServiceEndpoint['type'] - urls: DidServiceEndpoint['serviceEndpoint'] -} +export function publicKeyToChain( + key: NewDidVerificationKey +): EncodedVerificationKey +export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey /** - * Checks if a string is a valid URI according to RFC#3986. + * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. * - * @param str String to be checked. - * @returns Whether `str` is a valid URI. + * @param key Object describing data associated with a public key. + * @returns Data restructured to allow SCALE encoding by polkadot api. */ +export function publicKeyToChain( + key: NewDidVerificationKey | NewDidEncryptionKey +): EncodedDidKey { + // TypeScript can't infer type here, so we have to add a type assertion. + return { [key.type]: key.publicKey } as EncodedDidKey +} + function isUri(str: string): boolean { try { const url = new URL(str) // this actually accepts any URI but throws if it can't be parsed @@ -203,7 +241,7 @@ function isUri(str: string): boolean { } } -const UriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ +const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ /** * Checks if a string is a valid URI fragment according to RFC#3986. @@ -213,7 +251,7 @@ const UriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ */ function isUriFragment(str: string): boolean { try { - return UriFragmentRegex.test(str) && !!decodeURIComponent(str) + return uriFragmentRegex.test(str) && !!decodeURIComponent(str) } catch { return false } @@ -226,14 +264,14 @@ function isUriFragment(str: string): boolean { * * @param endpoint A service endpoint object to check. */ -export function validateService(endpoint: DidServiceEndpoint): void { +export function validateNewService(endpoint: NewService): void { const { id, serviceEndpoint } = endpoint - if (id.startsWith('did:kilt')) { + if ((id as string).startsWith('did:kilt')) { throw new SDKErrors.DidError( `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` ) } - if (!isUriFragment(resourceIdToChain(id))) { + if (!isUriFragment(fragmentIdToChain(id))) { throw new SDKErrors.DidError( `The service ID must be valid as a URI fragment according to RFC#3986, which "${id}" is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding the desired id.` ) @@ -253,11 +291,11 @@ export function validateService(endpoint: DidServiceEndpoint): void { * @param service The DID service to format. * @returns The blockchain-formatted DID service. */ -export function serviceToChain(service: DidServiceEndpoint): ChainEndpoint { - validateService(service) +export function serviceToChain(service: NewService): ChainDidService { + validateNewService(service) const { id, type, serviceEndpoint } = service return { - id: resourceIdToChain(id), + id: fragmentIdToChain(id), serviceTypes: type, urls: serviceEndpoint, } @@ -271,7 +309,7 @@ export function serviceToChain(service: DidServiceEndpoint): ChainEndpoint { */ export function serviceFromChain( encoded: Option -): DidServiceEndpoint { +): Service { const { id, serviceTypes, urls } = encoded.unwrap() return { id: `#${id.toUtf8()}`, @@ -280,8 +318,6 @@ export function serviceFromChain( } } -// ### EXTRINSICS types - export type AuthorizeCallInput = { did: DidUri txCounter: AnyNumber @@ -290,43 +326,27 @@ export type AuthorizeCallInput = { blockNumber?: AnyNumber } -// ### EXTRINSICS - -export function publicKeyToChain( - key: NewDidVerificationKey -): EncodedVerificationKey -export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey - -/** - * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. - * - * @param key Object describing data associated with a public key. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ -export function publicKeyToChain( - key: NewDidVerificationKey | NewDidEncryptionKey -): EncodedKey { - // TypeScript can't infer type here, so we have to add a type assertion. - return { [key.type]: key.publicKey } as EncodedKey -} - interface GetStoreTxInput { authentication: [NewDidVerificationKey] assertionMethod?: [NewDidVerificationKey] capabilityDelegation?: [NewDidVerificationKey] keyAgreement?: NewDidEncryptionKey[] - service?: DidServiceEndpoint[] + service?: NewService[] } +type GetStoreTxSignCallbacResponse = Pick & { + // We don't need the key ID to dispatch the tx. + verificationMethod: Pick +} export type GetStoreTxSignCallback = ( signData: Omit -) => Promise> +) => Promise /** * Create a DID creation operation which includes the information provided. * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys and service endpoints. + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys as verification methods and service endpoints. * * A DID creation operation can contain at most 25 new service endpoints. * Additionally, each service endpoint must respect the following conditions: @@ -334,14 +354,14 @@ export type GetStoreTxSignCallback = ( * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. * - * @param input The DID keys and services to store, also accepts DidDocument, so you can store a light DID for example. + * @param input The DID keys and services to store. * @param submitter The KILT address authorized to submit the creation operation. * @param sign The sign callback. The authentication key has to be used. * * @returns The SubmittableExtrinsic for the DID creation operation. */ -export async function getStoreTx( - input: GetStoreTxInput | DidDocument, +export async function getStoreTxFromInput( + input: GetStoreTxInput, submitter: KiltAddress, sign: GetStoreTxSignCallback ): Promise { @@ -362,14 +382,14 @@ export async function getStoreTx( } // For now, it only takes the first attestation key, if present. - if (assertionMethod && assertionMethod.length > 1) { + if (assertionMethod !== undefined && assertionMethod.length > 1) { throw new SDKErrors.DidError( `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` ) } // For now, it only takes the first delegation key, if present. - if (capabilityDelegation && capabilityDelegation.length > 1) { + if (capabilityDelegation !== undefined && capabilityDelegation.length > 1) { throw new SDKErrors.DidError( `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` ) @@ -391,7 +411,9 @@ export async function getStoreTx( } const [authenticationKey] = authentication - const did = getAddressByKey(authenticationKey) + const did = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authenticationKey), + }) const newAttestationKey = assertionMethod && @@ -419,19 +441,172 @@ export async function getStoreTx( .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) .toU8a() - const signature = await sign({ + const { signature } = await sign({ data: encoded, - keyRelationship: 'authentication', + verificationMethodRelationship: 'authentication', }) const encodedSignature = { - [signature.keyType]: signature.signature, + [authenticationKey.type]: signature, } as EncodedSignature return api.tx.did.create(encoded, encodedSignature) } +/** + * Create a DID creation operation which would write to chain the DID Document provided as input. + * Only the first authentication, assertion, and capability delegation verification methods are considered from the input DID Document. + * All the input DID Document key agreement verification methods are considered. + * + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and service endpoints. + * + * A DID creation operation can contain at most 25 new service endpoints. + * Additionally, each service endpoint must respect the following conditions: + * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. + * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * + * @param input The DID Document to store. + * @param submitter The KILT address authorized to submit the creation operation. + * @param sign The sign callback. The authentication key has to be used. + * + * @returns The SubmittableExtrinsic for the DID creation operation. + */ +export async function getStoreTxFromDidDocument( + input: DidDocument, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const { + authentication, + assertionMethod, + keyAgreement, + capabilityDelegation, + service, + verificationMethod, + } = input + + const authKey = (() => { + const authenticationMethodId = authentication?.[0] + if (authenticationMethodId === undefined) { + throw new SDKErrors.DidError( + 'Cannot create a DID without an authentication method.' + ) + } + const authVerificationMethod = verificationMethod?.find( + (vm) => vm.id === authenticationMethodId + ) + if (authVerificationMethod === undefined) { + throw new SDKErrors.DidError( + `Cannot find the authentication method with ID "${authenticationMethodId}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey( + authVerificationMethod.publicKeyMultibase + ) + if (!isValidVerificationKeyType(keyType)) { + throw new SDKErrors.DidError( + `Provided authentication key has an unsupported key type "${keyType}".` + ) + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const keyAgreementKeys = (() => { + if (keyAgreement === undefined || keyAgreement.length === 0) { + return undefined + } + return keyAgreement.map((k) => { + const vm = verificationMethod?.find((_vm) => _vm.id === k) + if (vm === undefined) { + throw new SDKErrors.DidError( + `Cannot find the key agreement method with ID "${k}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey(vm.publicKeyMultibase) + if (!isValidEncryptionKeyType(keyType)) { + throw new SDKErrors.DidError( + `The key agreement key with ID "${k}" has an unsupported key type ${keyType}.` + ) + } + return { + type: keyType, + publicKey, + } as NewDidEncryptionKey + }) + })() + + const assertionMethodKey = (() => { + const assertionMethodId = assertionMethod?.[0] + if (assertionMethodId === undefined) { + return undefined + } + const assertionVerificationMethod = verificationMethod?.find( + (vm) => vm.id === assertionMethodId + ) + if (assertionVerificationMethod === undefined) { + throw new SDKErrors.DidError( + `Cannot find the assertion method with ID "${assertionMethodId}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey( + assertionVerificationMethod.publicKeyMultibase + ) + if (!isValidVerificationKeyType(keyType)) { + throw new SDKErrors.DidError( + `The assertion method key with ID "${assertionMethodId}" has an unsupported key type ${keyType}.` + ) + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const capabilityDelegationKey = (() => { + const capabilityDelegationId = capabilityDelegation?.[0] + if (capabilityDelegationId === undefined) { + return undefined + } + const capabilityDelegationVerificationMethod = verificationMethod?.find( + (vm) => vm.id === capabilityDelegationId + ) + if (capabilityDelegationVerificationMethod === undefined) { + throw new SDKErrors.DidError( + `Cannot find the capability delegation method with ID "${capabilityDelegationId}" in the \`verificationMethod\` property.` + ) + } + const { keyType, publicKey } = multibaseKeyToDidKey( + capabilityDelegationVerificationMethod.publicKeyMultibase + ) + if (!isValidVerificationKeyType(keyType)) { + throw new SDKErrors.DidError( + `The capability delegation method key with ID "${capabilityDelegationId}" has an unsupported key type ${keyType}.` + ) + } + return { + type: keyType, + publicKey, + } as NewDidVerificationKey + })() + + const storeTxInput: GetStoreTxInput = { + authentication: [authKey], + assertionMethod: assertionMethodKey ? [assertionMethodKey] : undefined, + capabilityDelegation: capabilityDelegationKey + ? [capabilityDelegationKey] + : undefined, + keyAgreement: keyAgreementKeys, + service, + } + + return getStoreTxFromInput(storeTxInput, submitter, sign) +} + export interface SigningOptions { sign: SignExtrinsicCallback - keyRelationship: VerificationKeyRelationship + verificationMethodRelationship: SignatureVerificationMethodRelationship } /** @@ -440,7 +615,7 @@ export interface SigningOptions { * * @param params Object wrapping all input to the function. * @param params.did Full DID. - * @param params.keyRelationship DID key relationship to be used for authorization. + * @param params.verificationMethodRelationship DID verification relationship to be used for authorization. * @param params.sign The callback to interface with the key store managing the private key to be used. * @param params.call The call or extrinsic to be authorized. * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. @@ -450,7 +625,7 @@ export interface SigningOptions { */ export async function generateDidAuthenticatedTx({ did, - keyRelationship, + verificationMethodRelationship, sign, call, txCounter, @@ -469,34 +644,38 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const signature = await sign({ + const { signature, verificationMethod } = await sign({ data: signableCall.toU8a(), - keyRelationship, + verificationMethodRelationship, did, }) + const { keyType } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) const encodedSignature = { - [signature.keyType]: signature.signature, + [keyType]: signature, } as EncodedSignature return api.tx.did.submitDidCall(signableCall, encodedSignature) } -// ### Chain utils /** * Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics. * * @param key Object describing data associated with a public key. - * @param signature Object containing a signature generated with a full DID associated public key. + * @param key.publicKeyMultibase The multibase, multicodec representation of the signing public key. + * @param signature The signature generated with the full DID associated public key. * @returns Data restructured to allow SCALE encoding by polkadot api. */ export function didSignatureToChain( - key: DidVerificationKey, + { publicKeyMultibase }: VerificationMethod, signature: Uint8Array ): EncodedSignature { - if (!verificationKeyTypes.includes(key.type)) { + const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) + if (!isValidVerificationKeyType(keyType)) { throw new SDKErrors.DidError( - `encodedDidSignature requires a verification key. A key of type "${key.type}" was used instead` + `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` ) } - return { [key.type]: signature } as EncodedSignature + return { [keyType]: signature } as EncodedSignature } diff --git a/packages/did/src/Did.rpc.ts b/packages/did/src/Did.rpc.ts index da77219d65..cceba661e8 100644 --- a/packages/did/src/Did.rpc.ts +++ b/packages/did/src/Did.rpc.ts @@ -7,74 +7,42 @@ import type { Option, Vec } from '@polkadot/types' import type { Codec } from '@polkadot/types/types' -import type { AccountId32, Hash } from '@polkadot/types/interfaces' import type { RawDidLinkedInfo, - KiltSupportDeposit, - DidDidDetailsDidPublicKeyDetails, DidDidDetails, DidServiceEndpointsDidEndpoint, PalletDidLookupLinkableAccountLinkableAccountId, } from '@kiltprotocol/augment-api' -import type { - Deposit, - DidDocument, - DidEncryptionKey, - DidKey, - DidServiceEndpoint, - DidUri, - DidVerificationKey, - KiltAddress, - UriFragment, - BN, -} from '@kiltprotocol/types' +import type { DidDocument, KiltAddress, Service } from '@kiltprotocol/types' +import { ss58Format } from '@kiltprotocol/utils' import { encodeAddress } from '@polkadot/keyring' import { ethereumEncode } from '@polkadot/util-crypto' -import { u8aToString } from '@polkadot/util' -import { Crypto, ss58Format } from '@kiltprotocol/utils' - -import { Address, SubstrateAddress } from './DidLinks/AccountLinks.chain.js' -import { getFullDidUri } from './Did.utils.js' -function fromChain(encoded: AccountId32): DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) -} - -type RpcDocument = Pick< - DidDocument, - 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'keyAgreement' -> & { - lastTxCounter: BN - deposit: Deposit -} - -function depositFromChain(deposit: KiltSupportDeposit): Deposit { - return { - owner: Crypto.encodeAddress(deposit.owner, ss58Format), - amount: deposit.amount.toBn(), - } -} - -function didPublicKeyDetailsFromChain( - keyId: Hash, - keyDetails: DidDidDetailsDidPublicKeyDetails -): DidKey { - const key = keyDetails.key.isPublicEncryptionKey - ? keyDetails.key.asPublicEncryptionKey - : keyDetails.key.asPublicVerificationKey - return { - id: `#${keyId.toHex()}`, - type: key.type.toLowerCase() as DidKey['type'], - publicKey: key.value.toU8a(), - } -} - -function resourceIdToChain(id: UriFragment): string { - return id.replace(/^#/, '') -} - -function documentFromChain(encoded: DidDidDetails): RpcDocument { +import type { + Address, + SubstrateAddress, +} from './DidLinks/AccountLinks.chain.js' +import type { + ChainDidDetails, + ChainDidEncryptionKey, + ChainDidKey, + ChainDidVerificationKey, +} from './Did.chain.js' + +import { + depositFromChain, + fragmentIdToChain, + fromChain, + publicKeyFromChain, +} from './Did.chain.js' + +import { didKeyToVerificationMethod } from './Did.utils.js' +import { addKeypairAsVerificationMethod } from './DidDetails/DidDetails.js' + +function documentFromChain( + encoded: DidDidDetails +): Omit { const { publicKeys, authenticationKey, @@ -85,29 +53,28 @@ function documentFromChain(encoded: DidDidDetails): RpcDocument { deposit, } = encoded - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => - didPublicKeyDetailsFromChain(keyId, keyDetails) - ) + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) .reduce((res, key) => { - res[resourceIdToChain(key.id)] = key + res[fragmentIdToChain(key.id)] = key return res }, {}) - const authentication = keys[authenticationKey.toHex()] as DidVerificationKey + const authentication = keys[ + authenticationKey.toHex() + ] as ChainDidVerificationKey - const didRecord: RpcDocument = { + const didRecord: ChainDidDetails = { authentication: [authentication], lastTxCounter: lastTxCounter.toBn(), deposit: depositFromChain(deposit), } - if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.assertionMethod = [key] } if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.capabilityDelegation = [key] } @@ -116,27 +83,25 @@ function documentFromChain(encoded: DidDidDetails): RpcDocument { ) if (keyAgreementKeyIds.length > 0) { didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as DidEncryptionKey + (id) => keys[id] as ChainDidEncryptionKey ) } return didRecord } -function serviceFromChain( - encoded: DidServiceEndpointsDidEndpoint -): DidServiceEndpoint { +function serviceFromChain(encoded: DidServiceEndpointsDidEndpoint): Service { const { id, serviceTypes, urls } = encoded return { - id: `#${u8aToString(id)}`, - type: serviceTypes.map(u8aToString), - serviceEndpoint: urls.map(u8aToString), + id: `#${id.toUtf8()}`, + type: serviceTypes.map((type) => type.toUtf8()), + serviceEndpoint: urls.map((url) => url.toUtf8()), } } function servicesFromChain( encoded: DidServiceEndpointsDidEndpoint[] -): DidServiceEndpoint[] { +): Service[] { return encoded.map((encodedValue) => serviceFromChain(encodedValue)) } @@ -170,14 +135,8 @@ function connectedAccountsFromChain( ) } -/** - * Web3Name is the type of nickname for a DID. - */ -export type Web3Name = string - -export interface DidInfo { +export interface LinkedDidInfo { document: DidDocument - web3Name?: Web3Name accounts: Address[] } @@ -191,30 +150,68 @@ export interface DidInfo { export function linkedInfoFromChain( encoded: Option, networkPrefix = ss58Format -): DidInfo { +): LinkedDidInfo { const { identifier, accounts, w3n, serviceEndpoints, details } = encoded.unwrap() - const didRec = documentFromChain(details) + const { + authentication, + keyAgreement, + capabilityDelegation, + assertionMethod, + } = documentFromChain(details) const did: DidDocument = { - uri: fromChain(identifier), - authentication: didRec.authentication, - assertionMethod: didRec.assertionMethod, - capabilityDelegation: didRec.capabilityDelegation, - keyAgreement: didRec.keyAgreement, + id: fromChain(identifier), + authentication: [authentication[0].id], + verificationMethod: [ + didKeyToVerificationMethod(fromChain(identifier), authentication[0].id, { + keyType: authentication[0].type, + publicKey: authentication[0].publicKey, + }), + ], + } + + if (keyAgreement !== undefined && keyAgreement.length > 0) { + keyAgreement.forEach(({ id, publicKey, type }) => { + addKeypairAsVerificationMethod( + did, + { id, publicKey, type }, + 'keyAgreement' + ) + }) + } + + if (assertionMethod !== undefined) { + const { id, type, publicKey } = assertionMethod[0] + addKeypairAsVerificationMethod( + did, + { id, publicKey, type }, + 'assertionMethod' + ) } - const service = servicesFromChain(serviceEndpoints) - if (service.length > 0) { - did.service = service + if (capabilityDelegation !== undefined) { + const { id, type, publicKey } = capabilityDelegation[0] + addKeypairAsVerificationMethod( + did, + { id, publicKey, type }, + 'capabilityDelegation' + ) + } + + const services = servicesFromChain(serviceEndpoints) + if (services.length > 0) { + did.service = services } const web3Name = w3n.isNone ? undefined : w3n.unwrap().toHuman() + if (web3Name !== undefined) { + did.alsoKnownAs = [`w3n:${web3Name}`] + } const linkedAccounts = connectedAccountsFromChain(accounts, networkPrefix) return { document: did, - web3Name, accounts: linkedAccounts, } } diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 449e698136..6093d4b148 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -5,42 +5,44 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' - import type { - DidDocument, - DidResourceUri, - DidSignature, - KeyringPair, KiltKeyringPair, - NewLightDidVerificationKey, + KeyringPair, + DidDocument, SignCallback, + DidUrl, } from '@kiltprotocol/types' + import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' + +import type { DidSignature } from './Did.signature' +import type { NewLightDidVerificationKey } from './DidDetails' -import { makeSigningKeyTool } from '../../../tests/testUtils' +import { TestUtilsV2 } from '../../../tests/testUtils' import { isDidSignature, signatureFromJson, signatureToJson, verifyDidSignature, } from './Did.signature' -import { keyToResolvedKey, resolveKey } from './DidResolver' -import * as Did from './index.js' +import { resolve } from './DidResolver' +import { keypairToMultibaseKey, multibaseKeyToDidKey, parse } from './Did.utils' +import { createLightDidDocument } from './DidDetails' jest.mock('./DidResolver') jest - .mocked(keyToResolvedKey) - .mockImplementation(jest.requireActual('./DidResolver').keyToResolvedKey) + .mocked(resolve) + .mockImplementation(jest.requireActual('./DidResolver').resolve) describe('light DID', () => { let keypair: KiltKeyringPair let did: DidDocument let sign: SignCallback beforeAll(() => { - const keyTool = makeSigningKeyTool() + const keyTool = TestUtilsV2.makeSigningKeyTool() keypair = keyTool.keypair - did = Did.createLightDidDocument({ + did = createLightDidDocument({ authentication: keyTool.authentication, }) sign = keyTool.getSignCallback(did) @@ -48,82 +50,89 @@ describe('light DID', () => { beforeEach(() => { jest - .mocked(resolveKey) + .mocked(resolve) .mockReset() - .mockImplementation(async (didUri, keyRelationship = 'authentication') => - didUri.includes(keypair.address) - ? Did.keyToResolvedKey(did[keyRelationship]![0], did.uri) - : Promise.reject() - ) + .mockImplementation(async (didUrl) => { + const { address } = parse(didUrl) + if (address === keypair.address) { + return { + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: did, + } + } + return Promise.reject() + }) }) it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('deserializes old did signature (with `keyId` property) to new format', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = signatureToJson( - await sign({ + const { signature, signerUrl } = signatureToJson({ + did: did.id, + ...(await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', - }) - ) + did: did.id, + verificationMethodRelationship: 'authentication', + })), + }) const oldSignature = { signature, - keyId: keyUri, + keyId: signerUrl, } const deserialized = signatureFromJson(oldSignature) expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.keyUri).toStrictEqual(keyUri) + expect(deserialized.verificationMethodUri).toStrictEqual(signerUrl) expect(deserialized).not.toHaveProperty('keyId') }) it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: SIGNED_BYTES, - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('fails if relationship does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'assertionMethod', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'assertionMethod', }) ).rejects.toThrow() }) @@ -131,82 +140,82 @@ describe('light DID', () => { it('fails if key id does not match', async () => { const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, keyUri } = await sign({ + let { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) - keyUri = `${keyUri}1a` - jest.mocked(resolveKey).mockRejectedValue(new Error('Key not found')) + const wrongVerificationMethodId = `${verificationMethod.id}1a` + jest.mocked(resolve).mockRejectedValue(new Error('DID not found')) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${wrongVerificationMethodId}` as DidUrl, + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() }) it('fails if signature does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING.substring(1), signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() }) it('fails if key id malformed', async () => { - jest.mocked(resolveKey).mockRestore() + jest.mocked(resolve).mockRestore() const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, keyUri } = await sign({ + let { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) - // @ts-expect-error - keyUri = keyUri.replace('#', '?') + const malformedVerificationId = verificationMethod.id.replace('#', '?') await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: + `${did.id}${malformedVerificationId}` as DidUrl, + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() }) it('does not verify if migrated to Full DID', async () => { - jest.mocked(resolveKey).mockRejectedValue(new Error('Migrated')) + jest.mocked(resolve).mockRejectedValue(new Error('Migrated')) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() }) it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - keyUri: `${did.uri}${did.authentication[0].id}`, + signerUrl: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -214,37 +223,48 @@ describe('light DID', () => { it('detects signer expectation mismatch if signature is by unrelated did', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) - const expectedSigner = Did.createLightDidDocument({ - authentication: makeSigningKeyTool().authentication, - }).uri + const expectedSigner = createLightDidDocument({ + authentication: TestUtilsV2.makeSigningKeyTool().authentication, + }).id await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethod: 'authentication', + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) it('allows variations of the same light did', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) - const expectedSigner = Did.createLightDidDocument({ - authentication: did.authentication as [NewLightDidVerificationKey], + const authKey = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] + ) + const expectedSignerAuthKey = multibaseKeyToDidKey( + authKey!.publicKeyMultibase + ) + const expectedSigner = createLightDidDocument({ + authentication: [ + { + publicKey: expectedSignerAuthKey.publicKey, + type: expectedSignerAuthKey.keyType, + }, + ] as [NewLightDidVerificationKey], keyAgreement: [{ type: 'x25519', publicKey: new Uint8Array(32).fill(1) }], service: [ { @@ -253,15 +273,15 @@ describe('light DID', () => { serviceEndpoint: ['http://example.com'], }, ], - }).uri + }).id await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethod: 'authentication', + expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -274,122 +294,143 @@ describe('full DID', () => { beforeAll(() => { keypair = Crypto.makeKeypairFromSeed() did = { - uri: `did:kilt:${keypair.address}`, - authentication: [ + id: `did:kilt:${keypair.address}`, + authentication: ['#0x12345'], + verificationMethod: [ { + controller: `did:kilt:${keypair.address}`, id: '#0x12345', - type: 'sr25519', - publicKey: keypair.publicKey, + publicKeyMultibase: keypairToMultibaseKey(keypair), + type: 'MultiKey', }, ], } sign = async ({ data }) => ({ signature: keypair.sign(data), - keyUri: `${did.uri}#0x12345`, - keyType: 'sr25519', + verificationMethod: { + id: '#0x12345', + publicKeyMultibase: keypairToMultibaseKey(keypair), + }, }) }) beforeEach(() => { jest - .mocked(resolveKey) + .mocked(resolve) .mockReset() - .mockImplementation(async (didUri) => - didUri.includes(keypair.address) - ? Did.keyToResolvedKey(did.authentication[0], did.uri) - : Promise.reject() - ) + .mockImplementation(async (didUri) => { + const { address } = parse(didUri) + if (address === keypair.address) { + return { + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: did, + } + } + return Promise.reject() + }) }) it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: SIGNED_BYTES, - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('does not verify if deactivated', async () => { - jest.mocked(resolveKey).mockRejectedValue(new Error('Deactivated')) + jest.mocked(resolve).mockRejectedValue(new Error('Deactivated')) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() }) it('does not verify if not on chain', async () => { - jest.mocked(resolveKey).mockRejectedValue(new Error('Not on chain')) + jest.mocked(resolve).mockRejectedValue(new Error('Not on chain')) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() }) it('accepts signature of full did for light did if enabled', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationMethodRelationship: 'authentication', }) - const expectedSigner = Did.createLightDidDocument({ - authentication: did.authentication as [NewLightDidVerificationKey], - }).uri + const authKey = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] + ) + const expectedSignerAuthKey = multibaseKeyToDidKey( + authKey!.publicKeyMultibase + ) + const expectedSigner = createLightDidDocument({ + authentication: [ + { + publicKey: expectedSignerAuthKey.publicKey, + type: expectedSignerAuthKey.keyType, + }, + ] as [NewLightDidVerificationKey], + }).id await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethod: 'authentication', + expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) @@ -397,17 +438,17 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, allowUpgraded: true, - expectedVerificationMethod: 'authentication', + expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - keyUri: `${did.uri}${did.authentication[0].id}`, + signerUrl: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -423,31 +464,31 @@ describe('type guard', () => { it('rejects malformed key uri', () => { let signature: DidSignature = { // @ts-expect-error - keyUri: `did:kilt:${keypair.address}?mykey`, + signerUrl: `did:kilt:${keypair.address}?mykey`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - keyUri: `kilt:did:${keypair.address}#mykey`, + signerUrl: `kilt:did:${keypair.address}#mykey`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - keyUri: `kilt:did:${keypair.address}`, + signerUrl: `did:kilt:${keypair.address}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - keyUri: keypair.address, + signerUrl: keypair.address, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - keyUri: '', + signerUrl: '', signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) @@ -455,7 +496,7 @@ describe('type guard', () => { it('rejects unexpected signature type', () => { const signature: DidSignature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, + signerUrl: `did:kilt:${keypair.address}#mykey` as DidUrl, signature: '', } expect(isDidSignature(signature)).toBe(false) @@ -468,14 +509,14 @@ describe('type guard', () => { it('rejects incomplete objects', () => { let signature: DidSignature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, + signerUrl: `did:kilt:${keypair.address}#mykey` as DidUrl, // @ts-expect-error signature: undefined, } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - keyUri: undefined, + signerUrl: undefined, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) @@ -484,16 +525,16 @@ describe('type guard', () => { signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) - // @ts-expect-error signature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, + // @ts-expect-error + signerUrl: `did:kilt:${keypair.address}#mykey`, } expect(isDidSignature(signature)).toBe(false) // @ts-expect-error signature = {} expect(isDidSignature(signature)).toBe(false) // @ts-expect-error - signature = { keyUri: null, signature: null } + signature = { signerUrl: null, signature: null } expect(isDidSignature(signature)).toBe(false) }) }) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 07a853d79a..888f2a5f33 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -5,79 +5,90 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { isHex } from '@polkadot/util' - -import { - DidResolveKey, - DidResourceUri, - DidSignature, +import type { DidUri, + DidUrl, + ResolveDid, + SignatureVerificationMethodRelationship, SignResponseData, - VerificationKeyRelationship, } from '@kiltprotocol/types' + import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import { isHex } from '@polkadot/util' -import { resolveKey } from './DidResolver/index.js' -import { parse, validateUri } from './Did.utils.js' +import { multibaseKeyToDidKey, parse, validateUri } from './Did.utils.js' +import { resolve } from './DidResolver/DidResolver.js' export type DidSignatureVerificationInput = { message: string | Uint8Array signature: Uint8Array - keyUri: DidResourceUri + signerUrl: DidUrl expectedSigner?: DidUri allowUpgraded?: boolean - expectedVerificationMethod?: VerificationKeyRelationship - didResolveKey?: DidResolveKey + expectedVerificationMethodRelationship?: SignatureVerificationMethodRelationship + resolveDid?: ResolveDid['resolve'] +} + +export type DidSignature = { + signerUrl: DidUrl + signature: string } // Used solely for retro-compatibility with previously-generated DID signatures. // It is reasonable to think that it will be removed at some point in the future. -type OldDidSignature = Pick & { - keyId: DidSignature['keyUri'] +type OldDidSignatureV1 = { + signature: string + keyId: DidUrl +} +type OldDidSignatureV2 = { + signature: string + keyUri: DidUrl } -/** - * Checks whether the input is a valid DidSignature object, consisting of a signature as hex and the uri of the signing key. - * Does not cryptographically verify the signature itself! - * - * @param input Arbitrary input. - */ function verifyDidSignatureDataStructure( - input: DidSignature | OldDidSignature + input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 ): void { - const keyUri = 'keyUri' in input ? input.keyUri : input.keyId + const verificationMethodUri = (() => { + if ('keyUri' in input) { + return input.keyUri + } + if ('keyId' in input) { + return input.keyId + } + return input.signerUrl + })() if (!isHex(input.signature)) { throw new SDKErrors.SignatureMalformedError( `Expected signature as a hex string, got ${input.signature}` ) } - validateUri(keyUri, 'ResourceUri') + validateUri(verificationMethodUri, 'ResourceUri') } /** - * Verify a DID signature given the key URI of the signature. + * Verify a DID signature given the signer's DID URL. * A signature verification returns false if a migrated and then deleted DID is used. * * @param input Object wrapping all input. * @param input.message The message that was signed. * @param input.signature Signature bytes. - * @param input.keyUri DID URI of the key used for signing. + * @param input.signerUrl DID URL of the verification method used for signing. * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. - * @param input.expectedVerificationMethod Which relationship to the signer DID the key must have. - * @param input.didResolveKey Allows specifying a custom DID key resolve. Defaults to the built-in [[resolveKey]]. + * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. + * @param input.resolveDid Allows specifying a custom DID resolve. Defaults to the built-in [[resolve]]. */ export async function verifyDidSignature({ message, signature, - keyUri, + signerUrl, expectedSigner, allowUpgraded = false, - expectedVerificationMethod, - didResolveKey = resolveKey, + expectedVerificationMethodRelationship, + resolveDid = resolve, }: DidSignatureVerificationInput): Promise { // checks if key uri points to the right did; alternatively we could check the key's controller - const signer = parse(keyUri) + const signer = parse(signerUrl) if (expectedSigner && expectedSigner !== signer.did) { // check for allowable exceptions const expected = parse(expectedSigner) @@ -96,8 +107,34 @@ export async function verifyDidSignature({ } } - const { publicKey } = await didResolveKey(keyUri, expectedVerificationMethod) + const { didDocument } = await resolveDid(signerUrl, {}) + if (didDocument === undefined) { + throw new SDKErrors.SignatureUnverifiableError( + `Error validating the DID signature. Cannot fetch DID Document for "${signerUrl}".` + ) + } + const verificationMethod = didDocument.verificationMethod?.find( + (vm) => vm.id === signer.fragment + ) + if (verificationMethod === undefined) { + throw new SDKErrors.SignatureUnverifiableError( + `Cannot find verification method with ID "${signer.fragment} in the DID Document for "${signerUrl}".` + ) + } + if ( + expectedVerificationMethodRelationship !== undefined && + didDocument[expectedVerificationMethodRelationship]?.find( + (vm) => vm === signer.fragment + ) === undefined + ) { + throw new SDKErrors.SignatureUnverifiableError( + `Cannot find verification method with ID "${signer.fragment} for the relationship "${expectedVerificationMethodRelationship}".` + ) + } + const { publicKey } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) Crypto.verify(message, signature, publicKey) } @@ -110,7 +147,7 @@ export async function verifyDidSignature({ */ export function isDidSignature( input: unknown -): input is DidSignature | OldDidSignature { +): input is DidSignature | OldDidSignatureV1 | OldDidSignatureV2 { try { verifyDidSignatureDataStructure(input as DidSignature) return true @@ -124,27 +161,44 @@ export function isDidSignature( * * @param input Signature data returned from the [[SignCallback]]. * @param input.signature Signature bytes. - * @param input.keyUri DID URI of the key used for signing. + * @param input.did The DID URI of the signer. + * @param input.verificationMethod The verification method used to generate the signature. * @returns A [[DidSignature]] object where signature is hex-encoded. */ export function signatureToJson({ + did, signature, - keyUri, -}: SignResponseData): DidSignature { - return { signature: Crypto.u8aToHex(signature), keyUri } + verificationMethod, +}: SignResponseData & { + did: DidUri +}): DidSignature { + return { + signature: Crypto.u8aToHex(signature), + signerUrl: `${did}${verificationMethod.id}`, + } } /** * Deserializes a [[DidSignature]] for signature verification. - * Handles backwards compatibility to an older version of the interface where the `keyUri` property was called `keyId`. + * Handles backwards compatibility to an older version of the interface where the `verificationMethodUri` property was called either `keyUri` or `keyId`. * * @param input A [[DidSignature]] object. * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. */ export function signatureFromJson( - input: DidSignature | OldDidSignature -): Pick { - const keyUri = 'keyUri' in input ? input.keyUri : input.keyId + input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 +): Pick & { + verificationMethodUri: DidUrl +} { + const verificationMethodUri = (() => { + if ('keyUri' in input) { + return input.keyUri + } + if ('keyId' in input) { + return input.keyId + } + return input.signerUrl + })() const signature = Crypto.coToUInt8(input.signature) - return { signature, keyUri } + return { signature, verificationMethodUri } } diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 2cc1eaf631..3d384e2967 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -5,17 +5,21 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' - -import { - DidResourceUri, +import type { DidUri, - DidVerificationKey, + DidUrl, + KeyringPair, KiltAddress, UriFragment, + VerificationMethod, } from '@kiltprotocol/types' + +import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' +import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import type { DidKeyType } from './DidDetails/DidDetails.js' + // The latest version for KILT light DIDs. const LIGHT_DID_LATEST_VERSION = 1 @@ -54,7 +58,7 @@ type IDidParsingResult = { * @param didUri A KILT DID uri as a string. * @returns Object containing information extracted from the DID uri. */ -export function parse(didUri: DidUri | DidResourceUri): IDidParsingResult { +export function parse(didUri: DidUri | DidUrl): IDidParsingResult { let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups if (matches) { const { version: versionString, fragment } = matches @@ -98,6 +102,125 @@ export function parse(didUri: DidUri | DidResourceUri): IDidParsingResult { throw new SDKErrors.InvalidDidFormatError(didUri) } +type DecodedVerificationMethod = { + publicKey: Uint8Array + keyType: DidKeyType +} + +const multicodecPrefixes: Record = { + 0xe7: ['ecdsa', 33], + 0xec: ['x25519', 32], + 0xed: ['ed25519', 32], + 0xef: ['sr25519', 32], +} +const multicodecReversePrefixes: Record = { + ecdsa: 0xe7, + ed25519: 0xed, + sr25519: 0xef, + x25519: 0xec, +} + +/** + * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. + * + * @param publicKeyMultibase The verification method's public key multibase. + * @returns The decoded public key and [[DidKeyType]]. + */ +export function multibaseKeyToDidKey( + publicKeyMultibase: VerificationMethod['publicKeyMultibase'] +): DecodedVerificationMethod { + const decodedMulticodecPublicKey = multibaseDecode(publicKeyMultibase) + const [keyTypeFlag, publicKey] = [ + decodedMulticodecPublicKey.subarray(0, 1)[0], + decodedMulticodecPublicKey.subarray(1), + ] + const [keyType, expectedPublicKeyLength] = multicodecPrefixes[keyTypeFlag] + if (keyType === undefined) { + throw new SDKErrors.DidError( + `Cannot decode key type for multibase key "${publicKeyMultibase}".` + ) + } + if (publicKey.length !== expectedPublicKeyLength) { + throw new SDKErrors.DidError( + `Key of type "${keyType}" is expected to be ${expectedPublicKeyLength} bytes long. Provided key is ${publicKey.length} bytes long instead.` + ) + } + return { + keyType, + publicKey, + } +} + +/** + * Calculate the multibase, multicodec representation of a keypair given its type and public key. + * + * @param keypair The input keypair to encode as multibase, multicodec. + * @param keypair.type The keypair [[DidKeyType]]. + * @param keypair.publicKey The keypair public key. + * @returns The multicodec, multibase encoding of the provided keypair. + */ +export function keypairToMultibaseKey({ + type, + publicKey, +}: Pick & { + type: DidKeyType +}): VerificationMethod['publicKeyMultibase'] { + const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] + if (multiCodecPublicKeyPrefix === undefined) { + throw new SDKErrors.DidError( + `The provided key type "${type}" is not supported.` + ) + } + const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] + if (publicKey.length !== expectedPublicKeySize) { + throw new SDKErrors.DidError( + `Key of type "${type}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` + ) + } + const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] + return Buffer.from( + multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) + ).toString() as `z${string}` +} + +/** + * Export a DID key to a `MultiKey` verification method. + * + * @param controller The verification method controller's DID URI. + * @param id The verification method ID. + * @param key The DID key to export as a verification method. + * @param key.keyType The key type. + * @param key.publicKey The public component of the key. + * @returns The provided key encoded as a [[ DidDocumentV2.VerificationMethod]]. + */ +export function didKeyToVerificationMethod( + controller: VerificationMethod['controller'], + id: VerificationMethod['id'], + { keyType, publicKey }: DecodedVerificationMethod +): VerificationMethod { + const multiCodecPublicKeyPrefix = multicodecReversePrefixes[keyType] + if (multiCodecPublicKeyPrefix === undefined) { + throw new SDKErrors.DidError( + `Provided key type "${keyType}" not supported.` + ) + } + const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] + if (publicKey.length !== expectedPublicKeySize) { + throw new SDKErrors.DidError( + `Key of type "${keyType}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` + ) + } + const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] + return { + controller, + id, + type: 'MultiKey', + publicKeyMultibase: Buffer.from( + multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) + ).toString() as `z${string}`, + } +} + /** * Returns true if both didA and didB refer to the same DID subject, i.e., whether they have the same identifier as specified in the method spec. * @@ -109,17 +232,6 @@ export function isSameSubject(didA: DidUri, didB: DidUri): boolean { return parse(didA).address === parse(didB).address } -export type EncodedVerificationKey = - | { sr25519: Uint8Array } - | { ed25519: Uint8Array } - | { ecdsa: Uint8Array } - -export type EncodedEncryptionKey = { x25519: Uint8Array } - -export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey - -export type EncodedSignature = EncodedVerificationKey - /** * Checks that a string (or other input) is a valid KILT DID uri with or without a URI fragment. * Throws otherwise. @@ -137,7 +249,7 @@ export function validateUri( const { address, fragment } = parse(input as DidUri) if ( - fragment && + fragment !== undefined && (expectType === 'Did' || // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. (typeof expectType === 'boolean' && expectType === false)) @@ -147,7 +259,7 @@ export function validateUri( ) } - if (!fragment && expectType === 'ResourceUri') { + if (fragment === undefined && expectType === 'ResourceUri') { throw new SDKErrors.DidError( 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' ) @@ -157,17 +269,16 @@ export function validateUri( } /** - * Internal: derive the address part of the DID when it is created from authentication key. + * Internal: derive the address part of the DID when it is created from the provided authentication verification method. * - * @param input The authentication key. - * @param input.publicKey The public key. - * @param input.type The type of the key. + * @param input The authentication verification method. + * @param input.publicKeyMultibase The `publicKeyMultibase` value of the verification method. * @returns The expected address of the DID. */ -export function getAddressByKey({ - publicKey, - type, -}: Pick): KiltAddress { +export function getAddressFromVerificationMethod({ + publicKeyMultibase, +}: Pick): KiltAddress { + const { keyType: type, publicKey } = multibaseKeyToDidKey(publicKeyMultibase) if (type === 'ed25519' || type === 'sr25519') { return encodeAddress(publicKey, ss58Format) } @@ -199,12 +310,12 @@ export function getFullDidUri( /** * Builds the URI of a full DID if it is created with the authentication key provided. * - * @param key The key that will be used as DID authentication key. + * @param verificationMethod The DID verification method. * @returns The expected full DID URI. */ -export function getFullDidUriFromKey( - key: Pick +export function getFullDidUriFromVerificationMethod( + verificationMethod: Pick ): DidUri { - const address = getAddressByKey(key) + const address = getAddressFromVerificationMethod(verificationMethod) return getFullDidUri(address) } diff --git a/packages/did/src/Did2.chain.ts b/packages/did/src/Did2.chain.ts deleted file mode 100644 index 545555ff23..0000000000 --- a/packages/did/src/Did2.chain.ts +++ /dev/null @@ -1,681 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { Option } from '@polkadot/types' -import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' -import type { AnyNumber } from '@polkadot/types/types' -import type { - DidDidDetails, - DidDidDetailsDidAuthorizedCallOperation, - DidDidDetailsDidPublicKeyDetails, - DidServiceEndpointsDidEndpoint, - KiltSupportDeposit, -} from '@kiltprotocol/augment-api' -import type { - BN, - CryptoCallbacksV2, - Deposit, - DidDocumentV2, - KiltAddress, - SubmittableExtrinsic, -} from '@kiltprotocol/types' - -import { ConfigService } from '@kiltprotocol/config' -import { - verificationKeyTypesMap, - encryptionKeyTypesMap, -} from '@kiltprotocol/types' -import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' - -import type { - DidEncryptionKeyType, - NewService, - DidVerificationKeyType, - NewDidVerificationKey, - NewDidEncryptionKey, -} from './DidDetailsv2/DidDetailsV2.js' - -import { verificationKeyTypes } from './DidDetailsv2/DidDetailsV2.js' -import { - multibaseKeyToDidKey, - keypairToMultibaseKey, - getAddressFromVerificationMethod, - getFullDidUri, - parse, -} from './Did2.utils.js' - -export type ChainDidIdentifier = KiltAddress - -export type EncodedVerificationKey = - | { sr25519: Uint8Array } - | { ed25519: Uint8Array } - | { ecdsa: Uint8Array } -export type EncodedEncryptionKey = { x25519: Uint8Array } -export type EncodedDidKey = EncodedVerificationKey | EncodedEncryptionKey -export type EncodedSignature = EncodedVerificationKey - -/** - * Format a DID to be used as a parameter for the blockchain API functions. - - * @param did The DID to format. - * @returns The blockchain-formatted DID. - */ -export function toChain(did: DidDocumentV2.DidUri): ChainDidIdentifier { - return parse(did).address -} - -/** - * Format a DID fragment to be used as a parameter for the blockchain API functions. - - * @param id The DID fragment to format. - * @returns The blockchain-formatted ID. - */ -export function fragmentIdToChain(id: DidDocumentV2.UriFragment): string { - return id.replace(/^#/, '') -} - -/** - * Convert the DID data from blockchain format to the DID URI. - * - * @param encoded The chain-formatted DID. - * @returns The DID URI. - */ -export function fromChain(encoded: AccountId32): DidDocumentV2.DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) -} - -/** - * Convert the deposit data coming from the blockchain to JS object. - * - * @param deposit The blockchain-formatted deposit data. - * @returns The deposit data. - */ -export function depositFromChain(deposit: KiltSupportDeposit): Deposit { - return { - owner: Crypto.encodeAddress(deposit.owner, ss58Format), - amount: deposit.amount.toBn(), - } -} - -export type ChainDidBaseKey = { - id: DidDocumentV2.UriFragment - publicKey: Uint8Array - includedAt?: BN - type: string -} -export type ChainDidVerificationKey = ChainDidBaseKey & { - type: DidVerificationKeyType -} -export type ChainDidEncryptionKey = ChainDidBaseKey & { - type: DidEncryptionKeyType -} -export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey -export type ChainDidService = { - id: string - serviceTypes: string[] - urls: string[] -} -export type ChainDidDetails = { - authentication: [ChainDidVerificationKey] - assertionMethod?: [ChainDidVerificationKey] - capabilityDelegation?: [ChainDidVerificationKey] - keyAgreement?: ChainDidEncryptionKey[] - - service?: ChainDidService[] - - lastTxCounter: BN - deposit: Deposit -} - -/** - * Convert a DID public key from the blockchain format to a JS object. - * - * @param keyId The key ID. - * @param keyDetails The associated public key blockchain-formatted details. - * @returns The JS-formatted DID key. - */ -export function publicKeyFromChain( - keyId: Hash, - keyDetails: DidDidDetailsDidPublicKeyDetails -): ChainDidKey { - const key = keyDetails.key.isPublicEncryptionKey - ? keyDetails.key.asPublicEncryptionKey - : keyDetails.key.asPublicVerificationKey - return { - id: `#${keyId.toHex()}`, - publicKey: key.value.toU8a(), - type: key.type.toLowerCase() as ChainDidKey['type'], - } -} - -/** - * Convert the DID Document data from the blockchain format to a JS object. - * - * @param encoded The chain-formatted DID Document. - * @returns The DID Document. - */ -export function documentFromChain( - encoded: Option -): ChainDidDetails { - const { - publicKeys, - authenticationKey, - attestationKey, - delegationKey, - keyAgreementKeys, - lastTxCounter, - deposit, - } = encoded.unwrap() - - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) - .reduce((res, key) => { - res[fragmentIdToChain(key.id)] = key - return res - }, {}) - - const authentication = keys[ - authenticationKey.toHex() - ] as ChainDidVerificationKey - - const didRecord: ChainDidDetails = { - authentication: [authentication], - lastTxCounter: lastTxCounter.toBn(), - deposit: depositFromChain(deposit), - } - if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey - didRecord.assertionMethod = [key] - } - if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey - didRecord.capabilityDelegation = [key] - } - - const keyAgreementKeyIds = [...keyAgreementKeys.values()].map((keyId) => - keyId.toHex() - ) - if (keyAgreementKeyIds.length > 0) { - didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as ChainDidEncryptionKey - ) - } - - return didRecord -} - -export function publicKeyToChain( - key: NewDidVerificationKey -): EncodedVerificationKey -export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey - -/** - * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. - * - * @param key Object describing data associated with a public key. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ -export function publicKeyToChain( - key: NewDidVerificationKey | NewDidEncryptionKey -): EncodedDidKey { - // TypeScript can't infer type here, so we have to add a type assertion. - return { [key.type]: key.publicKey } as EncodedDidKey -} - -function isUri(str: string): boolean { - try { - const url = new URL(str) // this actually accepts any URI but throws if it can't be parsed - return url.href === str || encodeURI(decodeURI(str)) === str // make sure our URI has not been converted implicitly by URL - } catch { - return false - } -} - -const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ - -/** - * Checks if a string is a valid URI fragment according to RFC#3986. - * - * @param str String to be checked. - * @returns Whether `str` is a valid URI fragment. - */ -function isUriFragment(str: string): boolean { - try { - return uriFragmentRegex.test(str) && !!decodeURIComponent(str) - } catch { - return false - } -} - -/** - * Performs sanity checks on service endpoint data, making sure that the following conditions are met: - * - The `id` property is a string containing a valid URI fragment according to RFC#3986, not a complete DID URI. - * - If the `uris` property contains one or more strings, they must be valid URIs according to RFC#3986. - * - * @param endpoint A service endpoint object to check. - */ -export function validateNewService(endpoint: NewService): void { - const { id, serviceEndpoint } = endpoint - if ((id as string).startsWith('did:kilt')) { - throw new SDKErrors.DidError( - `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` - ) - } - if (!isUriFragment(fragmentIdToChain(id))) { - throw new SDKErrors.DidError( - `The service ID must be valid as a URI fragment according to RFC#3986, which "${id}" is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding the desired id.` - ) - } - serviceEndpoint.forEach((uri) => { - if (!isUri(uri)) { - throw new SDKErrors.DidError( - `A service URI must be a URI according to RFC#3986, which "${uri}" (service id "${id}") is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding resource locators beforehand.` - ) - } - }) -} - -/** - * Format the DID service to be used as a parameter for the blockchain API functions. - * - * @param service The DID service to format. - * @returns The blockchain-formatted DID service. - */ -export function serviceToChain(service: NewService): ChainDidService { - validateNewService(service) - const { id, type, serviceEndpoint } = service - return { - id: fragmentIdToChain(id), - serviceTypes: type, - urls: serviceEndpoint, - } -} - -/** - * Convert the DID service data coming from the blockchain to JS object. - * - * @param encoded The blockchain-formatted DID service data. - * @returns The DID service. - */ -export function serviceFromChain( - encoded: Option -): DidDocumentV2.Service { - const { id, serviceTypes, urls } = encoded.unwrap() - return { - id: `#${id.toUtf8()}`, - type: serviceTypes.map((type) => type.toUtf8()), - serviceEndpoint: urls.map((url) => url.toUtf8()), - } -} - -export type AuthorizeCallInput = { - did: DidDocumentV2.DidUri - txCounter: AnyNumber - call: Extrinsic - submitter: KiltAddress - blockNumber?: AnyNumber -} - -interface GetStoreTxInput { - authentication: [NewDidVerificationKey] - assertionMethod?: [NewDidVerificationKey] - capabilityDelegation?: [NewDidVerificationKey] - keyAgreement?: NewDidEncryptionKey[] - - service?: NewService[] -} - -type GetStoreTxSignCallbacResponse = Pick< - CryptoCallbacksV2.SignResponseData, - 'signature' -> & { - // We don't need the key ID to dispatch the tx. - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > -} -export type GetStoreTxSignCallback = ( - signData: Omit -) => Promise - -/** - * Create a DID creation operation which includes the information provided. - * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys as verification methods and service endpoints. - * - * A DID creation operation can contain at most 25 new service endpoints. - * Additionally, each service endpoint must respect the following conditions: - * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. - * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. - * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. - * - * @param input The DID keys and services to store. - * @param submitter The KILT address authorized to submit the creation operation. - * @param sign The sign callback. The authentication key has to be used. - * - * @returns The SubmittableExtrinsic for the DID creation operation. - */ -export async function getStoreTxFromInput( - input: GetStoreTxInput, - submitter: KiltAddress, - sign: GetStoreTxSignCallback -): Promise { - const api = ConfigService.get('api') - - const { - authentication, - assertionMethod, - capabilityDelegation, - keyAgreement = [], - service = [], - } = input - - if (!('authentication' in input) || typeof authentication[0] !== 'object') { - throw new SDKErrors.DidError( - `The provided DID does not have an authentication key to sign the creation operation` - ) - } - - // For now, it only takes the first attestation key, if present. - if (assertionMethod !== undefined && assertionMethod.length > 1) { - throw new SDKErrors.DidError( - `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` - ) - } - - // For now, it only takes the first delegation key, if present. - if (capabilityDelegation !== undefined && capabilityDelegation.length > 1) { - throw new SDKErrors.DidError( - `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` - ) - } - - const maxKeyAgreementKeys = api.consts.did.maxNewKeyAgreementKeys.toNumber() - if (keyAgreement.length > maxKeyAgreementKeys) { - throw new SDKErrors.DidError( - `The number of key agreement keys in the creation operation is greater than the maximum allowed, which is ${maxKeyAgreementKeys}` - ) - } - - const maxNumberOfServicesPerDid = - api.consts.did.maxNumberOfServicesPerDid.toNumber() - if (service.length > maxNumberOfServicesPerDid) { - throw new SDKErrors.DidError( - `Cannot store more than ${maxNumberOfServicesPerDid} service endpoints per DID` - ) - } - - const [authenticationKey] = authentication - const did = getAddressFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(authenticationKey), - }) - - const newAttestationKey = - assertionMethod && - assertionMethod.length > 0 && - publicKeyToChain(assertionMethod[0]) - - const newDelegationKey = - capabilityDelegation && - capabilityDelegation.length > 0 && - publicKeyToChain(capabilityDelegation[0]) - - const newKeyAgreementKeys = keyAgreement.map(publicKeyToChain) - const newServiceDetails = service.map(serviceToChain) - - const apiInput = { - did, - submitter, - newAttestationKey, - newDelegationKey, - newKeyAgreementKeys, - newServiceDetails, - } - - const encoded = api.registry - .createType(api.tx.did.create.meta.args[0].type.toString(), apiInput) - .toU8a() - - const { signature } = await sign({ - data: encoded, - verificationMethodRelationship: 'authentication', - }) - const encodedSignature = { - [authenticationKey.type]: signature, - } as EncodedSignature - return api.tx.did.create(encoded, encodedSignature) -} - -/** - * Create a DID creation operation which would write to chain the DID Document provided as input. - * Only the first authentication, assertion, and capability delegation verification methods are considered from the input DID Document. - * All the input DID Document key agreement verification methods are considered. - * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and service endpoints. - * - * A DID creation operation can contain at most 25 new service endpoints. - * Additionally, each service endpoint must respect the following conditions: - * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. - * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. - * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. - * - * @param input The DID Document to store. - * @param submitter The KILT address authorized to submit the creation operation. - * @param sign The sign callback. The authentication key has to be used. - * - * @returns The SubmittableExtrinsic for the DID creation operation. - */ -export async function getStoreTxFromDidDocument( - input: DidDocumentV2.DidDocument, - submitter: KiltAddress, - sign: GetStoreTxSignCallback -): Promise { - const { - authentication, - assertionMethod, - keyAgreement, - capabilityDelegation, - service, - verificationMethod, - } = input - - const authKey = (() => { - const authenticationMethodId = authentication?.[0] - if (authenticationMethodId === undefined) { - throw new SDKErrors.DidError( - 'Cannot create a DID without an authentication method.' - ) - } - const authVerificationMethod = verificationMethod?.find( - (vm) => vm.id === authenticationMethodId - ) - if (authVerificationMethod === undefined) { - throw new SDKErrors.DidError( - `Cannot find the authentication method with ID "${authenticationMethodId}" in the \`verificationMethod\` property.` - ) - } - const { keyType, publicKey } = multibaseKeyToDidKey( - authVerificationMethod.publicKeyMultibase - ) - if (verificationKeyTypesMap[keyType] === undefined) { - throw new SDKErrors.DidError( - `Provided authentication key has an unsupported key type "${keyType}".` - ) - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() - - const keyAgreementKeys = (() => { - if (keyAgreement === undefined || keyAgreement.length === 0) { - return undefined - } - return keyAgreement.map((k) => { - const vm = verificationMethod?.find((_vm) => _vm.id === k) - if (vm === undefined) { - throw new SDKErrors.DidError( - `Cannot find the key agreement method with ID "${k}" in the \`verificationMethod\` property.` - ) - } - const { keyType, publicKey } = multibaseKeyToDidKey(vm.publicKeyMultibase) - if (encryptionKeyTypesMap[keyType] === undefined) { - throw new SDKErrors.DidError( - `The key agreement key with ID "${k}" has an unsupported key type ${keyType}.` - ) - } - return { - type: keyType, - publicKey, - } as NewDidEncryptionKey - }) - })() - - const assertionMethodKey = (() => { - const assertionMethodId = assertionMethod?.[0] - if (assertionMethodId === undefined) { - return undefined - } - const assertionVerificationMethod = verificationMethod?.find( - (vm) => vm.id === assertionMethodId - ) - if (assertionVerificationMethod === undefined) { - throw new SDKErrors.DidError( - `Cannot find the assertion method with ID "${assertionMethodId}" in the \`verificationMethod\` property.` - ) - } - const { keyType, publicKey } = multibaseKeyToDidKey( - assertionVerificationMethod.publicKeyMultibase - ) - if (verificationKeyTypesMap[keyType] === undefined) { - throw new SDKErrors.DidError( - `The assertion method key with ID "${assertionMethodId}" has an unsupported key type ${keyType}.` - ) - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() - - const capabilityDelegationKey = (() => { - const capabilityDelegationId = capabilityDelegation?.[0] - if (capabilityDelegationId === undefined) { - return undefined - } - const capabilityDelegationVerificationMethod = verificationMethod?.find( - (vm) => vm.id === capabilityDelegationId - ) - if (capabilityDelegationVerificationMethod === undefined) { - throw new SDKErrors.DidError( - `Cannot find the capability delegation method with ID "${capabilityDelegationId}" in the \`verificationMethod\` property.` - ) - } - const { keyType, publicKey } = multibaseKeyToDidKey( - capabilityDelegationVerificationMethod.publicKeyMultibase - ) - if (verificationKeyTypesMap[keyType] === undefined) { - throw new SDKErrors.DidError( - `The capability delegation method key with ID "${capabilityDelegationId}" has an unsupported key type ${keyType}.` - ) - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() - - const storeTxInput: GetStoreTxInput = { - authentication: [authKey], - assertionMethod: assertionMethodKey ? [assertionMethodKey] : undefined, - capabilityDelegation: capabilityDelegationKey - ? [capabilityDelegationKey] - : undefined, - keyAgreement: keyAgreementKeys, - service, - } - - return getStoreTxFromInput(storeTxInput, submitter, sign) -} - -export interface SigningOptions { - sign: CryptoCallbacksV2.SignExtrinsicCallback - verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship -} - -/** - * DID related operations on the KILT blockchain require authorization by a full DID. This is realized by requiring that relevant extrinsics are signed with a key featured by a full DID as a verification method. - * Such extrinsics can be produced using this function. - * - * @param params Object wrapping all input to the function. - * @param params.did Full DID. - * @param params.verificationMethodRelationship DID verification relationship to be used for authorization. - * @param params.sign The callback to interface with the key store managing the private key to be used. - * @param params.call The call or extrinsic to be authorized. - * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. - * @param params.submitter Payment account allowed to submit this extrinsic and cover its fees, which will end up owning any deposit associated with newly created records. - * @param params.blockNumber Block number for determining the validity period of this authorization. If omitted, the current block number will be fetched from chain. - * @returns A DID authorized extrinsic that, after signing with the payment account mentioned in the params, is ready for submission. - */ -export async function generateDidAuthenticatedTx({ - did, - verificationMethodRelationship, - sign, - call, - txCounter, - submitter, - blockNumber, -}: AuthorizeCallInput & SigningOptions): Promise { - const api = ConfigService.get('api') - const signableCall = - api.registry.createType( - api.tx.did.submitDidCall.meta.args[0].type.toString(), - { - txCounter, - did: toChain(did), - call, - submitter, - blockNumber: blockNumber ?? (await api.query.system.number()), - } - ) - const { signature, verificationMethod } = await sign({ - data: signableCall.toU8a(), - verificationMethodRelationship, - did, - }) - const { keyType } = multibaseKeyToDidKey( - verificationMethod.publicKeyMultibase - ) - const encodedSignature = { - [keyType]: signature, - } as EncodedSignature - return api.tx.did.submitDidCall(signableCall, encodedSignature) -} - -/** - * Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics. - * - * @param key Object describing data associated with a public key. - * @param key.publicKeyMultibase The multibase, multicodec representation of the signing public key. - * @param signature The signature generated with the full DID associated public key. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ -export function didSignatureToChain( - { publicKeyMultibase }: DidDocumentV2.VerificationMethod, - signature: Uint8Array -): EncodedSignature { - const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) - if (!verificationKeyTypes.includes(keyType)) { - throw new SDKErrors.DidError( - `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` - ) - } - - return { [keyType]: signature } as EncodedSignature -} diff --git a/packages/did/src/Did2.rpc.ts b/packages/did/src/Did2.rpc.ts deleted file mode 100644 index f01a5f4630..0000000000 --- a/packages/did/src/Did2.rpc.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { Option, Vec } from '@polkadot/types' -import type { Codec } from '@polkadot/types/types' -import type { - RawDidLinkedInfo, - DidDidDetails, - DidServiceEndpointsDidEndpoint, - PalletDidLookupLinkableAccountLinkableAccountId, -} from '@kiltprotocol/augment-api' -import type { KiltAddress, DidDocumentV2 } from '@kiltprotocol/types' - -import { ss58Format } from '@kiltprotocol/utils' -import { encodeAddress } from '@polkadot/keyring' -import { ethereumEncode } from '@polkadot/util-crypto' - -import type { - Address, - SubstrateAddress, -} from './DidLinks/AccountLinks2.chain.js' -import type { - ChainDidDetails, - ChainDidEncryptionKey, - ChainDidKey, - ChainDidVerificationKey, -} from './Did2.chain.js' - -import { - depositFromChain, - fragmentIdToChain, - fromChain, - publicKeyFromChain, -} from './Did2.chain.js' - -import { didKeyToVerificationMethod } from './Did2.utils.js' -import { addKeypairAsVerificationMethod } from './DidDetailsv2/DidDetailsV2.js' - -function documentFromChain( - encoded: DidDidDetails -): Omit { - const { - publicKeys, - authenticationKey, - attestationKey, - delegationKey, - keyAgreementKeys, - lastTxCounter, - deposit, - } = encoded - - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) - .reduce((res, key) => { - res[fragmentIdToChain(key.id)] = key - return res - }, {}) - - const authentication = keys[ - authenticationKey.toHex() - ] as ChainDidVerificationKey - - const didRecord: ChainDidDetails = { - authentication: [authentication], - lastTxCounter: lastTxCounter.toBn(), - deposit: depositFromChain(deposit), - } - if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey - didRecord.assertionMethod = [key] - } - if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey - didRecord.capabilityDelegation = [key] - } - - const keyAgreementKeyIds = [...keyAgreementKeys.values()].map((keyId) => - keyId.toHex() - ) - if (keyAgreementKeyIds.length > 0) { - didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as ChainDidEncryptionKey - ) - } - - return didRecord -} - -function serviceFromChain( - encoded: DidServiceEndpointsDidEndpoint -): DidDocumentV2.Service { - const { id, serviceTypes, urls } = encoded - return { - id: `#${id.toUtf8()}`, - type: serviceTypes.map((type) => type.toUtf8()), - serviceEndpoint: urls.map((url) => url.toUtf8()), - } -} - -function servicesFromChain( - encoded: DidServiceEndpointsDidEndpoint[] -): DidDocumentV2.Service[] { - return encoded.map((encodedValue) => serviceFromChain(encodedValue)) -} - -function isLinkableAccountId( - arg: Codec -): arg is PalletDidLookupLinkableAccountLinkableAccountId { - return 'isAccountId32' in arg && 'isAccountId20' in arg -} - -function accountFromChain( - account: Codec, - networkPrefix = ss58Format -): KiltAddress | SubstrateAddress { - if (isLinkableAccountId(account)) { - // linked account is substrate address (ethereum-enabled storage version) - if (account.isAccountId32) - return encodeAddress(account.asAccountId32, networkPrefix) - // linked account is ethereum address (ethereum-enabled storage version) - if (account.isAccountId20) return ethereumEncode(account.asAccountId20) - } - // linked account is substrate account (legacy storage version) - return encodeAddress(account.toU8a(), networkPrefix) -} - -function connectedAccountsFromChain( - encoded: Vec, - networkPrefix = ss58Format -): Array { - return encoded.map((account) => - accountFromChain(account, networkPrefix) - ) -} - -export interface LinkedDidInfo { - document: DidDocumentV2.DidDocument - accounts: Address[] -} - -/** - * Decodes accounts, DID, and web3name linked to the provided account. - * - * @param encoded The data returned by `api.call.did.queryByAccount()`, `api.call.did.query()`, and `api.call.did.queryByWeb3Name()`. - * @param networkPrefix The optional network prefix to use to encode the returned addresses. Defaults to KILT prefix (38). Use `42` for the chain-agnostic wildcard Substrate prefix. - * @returns The accounts, DID, and web3name. - */ -export function linkedInfoFromChain( - encoded: Option, - networkPrefix = ss58Format -): LinkedDidInfo { - const { identifier, accounts, w3n, serviceEndpoints, details } = - encoded.unwrap() - - const { - authentication, - keyAgreement, - capabilityDelegation, - assertionMethod, - } = documentFromChain(details) - const did: DidDocumentV2.DidDocument = { - id: fromChain(identifier), - authentication: [authentication[0].id], - verificationMethod: [ - didKeyToVerificationMethod(fromChain(identifier), authentication[0].id, { - keyType: authentication[0].type, - publicKey: authentication[0].publicKey, - }), - ], - } - - if (keyAgreement !== undefined && keyAgreement.length > 0) { - keyAgreement.forEach(({ id, publicKey, type }) => { - addKeypairAsVerificationMethod( - did, - { id, publicKey, type }, - 'keyAgreement' - ) - }) - } - - if (assertionMethod !== undefined) { - const { id, type, publicKey } = assertionMethod[0] - addKeypairAsVerificationMethod( - did, - { id, publicKey, type }, - 'assertionMethod' - ) - } - - if (capabilityDelegation !== undefined) { - const { id, type, publicKey } = capabilityDelegation[0] - addKeypairAsVerificationMethod( - did, - { id, publicKey, type }, - 'capabilityDelegation' - ) - } - - const services = servicesFromChain(serviceEndpoints) - if (services.length > 0) { - did.service = services - } - - const web3Name = w3n.isNone ? undefined : w3n.unwrap().toHuman() - if (web3Name !== undefined) { - did.alsoKnownAs = [`w3n:${web3Name}`] - } - const linkedAccounts = connectedAccountsFromChain(accounts, networkPrefix) - - return { - document: did, - accounts: linkedAccounts, - } -} diff --git a/packages/did/src/Did2.signature.spec.ts b/packages/did/src/Did2.signature.spec.ts deleted file mode 100644 index 96ec8aa169..0000000000 --- a/packages/did/src/Did2.signature.spec.ts +++ /dev/null @@ -1,544 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { - DidDocumentV2, - KiltKeyringPair, - CryptoCallbacksV2, - KeyringPair, -} from '@kiltprotocol/types' - -import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' - -import type { DidSignature } from './Did2.signature' -import type { NewLightDidVerificationKey } from './DidDetailsv2' - -import { TestUtilsV2 } from '../../../tests/testUtils' -import { - isDidSignature, - signatureFromJson, - signatureToJson, - verifyDidSignature, -} from './Did2.signature' -import { resolve } from './DidResolver/DidResolverV2' -import { - keypairToMultibaseKey, - multibaseKeyToDidKey, - parse, -} from './Did2.utils' -import { createLightDidDocument } from './DidDetailsv2' - -jest.mock('./DidResolver/DidResolverV2') -jest - .mocked(resolve) - .mockImplementation(jest.requireActual('./DidResolver/DidResolverV2').resolve) - -describe('light DID', () => { - let keypair: KiltKeyringPair - let did: DidDocumentV2.DidDocument - let sign: CryptoCallbacksV2.SignCallback - beforeAll(() => { - const keyTool = TestUtilsV2.makeSigningKeyTool() - keypair = keyTool.keypair - did = createLightDidDocument({ - authentication: keyTool.authentication, - }) - sign = keyTool.getSignCallback(did) - }) - - beforeEach(() => { - jest - .mocked(resolve) - .mockReset() - .mockImplementation(async (didUrl) => { - const { address } = parse(didUrl) - if (address === keypair.address) { - return { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - didDocument: did, - } - } - return Promise.reject() - }) - }) - - it('verifies did signature over string', async () => { - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).resolves.not.toThrow() - }) - - it('deserializes old did signature (with `keyId` property) to new format', async () => { - const SIGNED_STRING = 'signed string' - const { signature, signerUrl } = signatureToJson({ - did: did.id, - ...(await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - })), - }) - const oldSignature = { - signature, - keyId: signerUrl, - } - - const deserialized = signatureFromJson(oldSignature) - expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.verificationMethodUri).toStrictEqual(signerUrl) - expect(deserialized).not.toHaveProperty('keyId') - }) - - it('verifies did signature over bytes', async () => { - const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, verificationMethod } = await sign({ - data: SIGNED_BYTES, - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_BYTES, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).resolves.not.toThrow() - }) - - it('fails if relationship does not match', async () => { - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'assertionMethod', - }) - ).rejects.toThrow() - }) - - it('fails if key id does not match', async () => { - const SIGNED_STRING = 'signed string' - // eslint-disable-next-line prefer-const - let { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - const wrongVerificationMethodId = `${verificationMethod.id}1a` - jest.mocked(resolve).mockRejectedValue(new Error('DID not found')) - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: - `${did.id}${wrongVerificationMethodId}` as DidDocumentV2.DidUrl, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow() - }) - - it('fails if signature does not match', async () => { - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_STRING.substring(1), - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow() - }) - - it('fails if key id malformed', async () => { - jest.mocked(resolve).mockRestore() - const SIGNED_STRING = 'signed string' - // eslint-disable-next-line prefer-const - let { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - const malformedVerificationId = verificationMethod.id.replace('#', '?') - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: - `${did.id}${malformedVerificationId}` as DidDocumentV2.DidUrl, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow() - }) - - it('does not verify if migrated to Full DID', async () => { - jest.mocked(resolve).mockRejectedValue(new Error('Migrated')) - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow() - }) - - it('typeguard accepts legal signature objects', () => { - const signature: DidSignature = { - signerUrl: `${did.id}${did.authentication![0]}`, - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(true) - }) - - it('detects signer expectation mismatch if signature is by unrelated did', async () => { - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - - const expectedSigner = createLightDidDocument({ - authentication: TestUtilsV2.makeSigningKeyTool().authentication, - }).id - - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedSigner, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) - }) - - it('allows variations of the same light did', async () => { - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - - const authKey = did.verificationMethod?.find( - (vm) => vm.id === did.authentication?.[0] - ) - const expectedSignerAuthKey = multibaseKeyToDidKey( - authKey!.publicKeyMultibase - ) - const expectedSigner = createLightDidDocument({ - authentication: [ - { - publicKey: expectedSignerAuthKey.publicKey, - type: expectedSignerAuthKey.keyType, - }, - ] as [NewLightDidVerificationKey], - keyAgreement: [{ type: 'x25519', publicKey: new Uint8Array(32).fill(1) }], - service: [ - { - id: '#service', - type: ['servingService'], - serviceEndpoint: ['http://example.com'], - }, - ], - }).id - - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedSigner, - expectedVerificationMethodRelationship: 'authentication', - }) - ).resolves.not.toThrow() - }) -}) - -describe('full DID', () => { - let keypair: KiltKeyringPair - let did: DidDocumentV2.DidDocument - let sign: CryptoCallbacksV2.SignCallback - beforeAll(() => { - keypair = Crypto.makeKeypairFromSeed() - did = { - id: `did:kilt:${keypair.address}`, - authentication: ['#0x12345'], - verificationMethod: [ - { - controller: `did:kilt:${keypair.address}`, - id: '#0x12345', - publicKeyMultibase: keypairToMultibaseKey(keypair), - type: 'MultiKey', - }, - ], - } - sign = async ({ data }) => ({ - signature: keypair.sign(data), - verificationMethod: { - id: '#0x12345', - publicKeyMultibase: keypairToMultibaseKey(keypair), - }, - }) - }) - - beforeEach(() => { - jest - .mocked(resolve) - .mockReset() - .mockImplementation(async (didUri) => { - const { address } = parse(didUri) - if (address === keypair.address) { - return { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - didDocument: did, - } - } - return Promise.reject() - }) - }) - - it('verifies did signature over string', async () => { - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).resolves.not.toThrow() - }) - - it('verifies did signature over bytes', async () => { - const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, verificationMethod } = await sign({ - data: SIGNED_BYTES, - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_BYTES, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).resolves.not.toThrow() - }) - - it('does not verify if deactivated', async () => { - jest.mocked(resolve).mockRejectedValue(new Error('Deactivated')) - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow() - }) - - it('does not verify if not on chain', async () => { - jest.mocked(resolve).mockRejectedValue(new Error('Not on chain')) - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow() - }) - - it('accepts signature of full did for light did if enabled', async () => { - const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationMethodRelationship: 'authentication', - }) - - const authKey = did.verificationMethod?.find( - (vm) => vm.id === did.authentication?.[0] - ) - const expectedSignerAuthKey = multibaseKeyToDidKey( - authKey!.publicKeyMultibase - ) - const expectedSigner = createLightDidDocument({ - authentication: [ - { - publicKey: expectedSignerAuthKey.publicKey, - type: expectedSignerAuthKey.keyType, - }, - ] as [NewLightDidVerificationKey], - }).id - - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedSigner, - expectedVerificationMethodRelationship: 'authentication', - }) - ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) - - await expect( - verifyDidSignature({ - message: SIGNED_STRING, - signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedSigner, - allowUpgraded: true, - expectedVerificationMethodRelationship: 'authentication', - }) - ).resolves.not.toThrow() - }) - - it('typeguard accepts legal signature objects', () => { - const signature: DidSignature = { - signerUrl: `${did.id}${did.authentication![0]}`, - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(true) - }) -}) - -describe('type guard', () => { - let keypair: KeyringPair - beforeAll(() => { - keypair = Crypto.makeKeypairFromSeed() - }) - - it('rejects malformed key uri', () => { - let signature: DidSignature = { - // @ts-expect-error - signerUrl: `did:kilt:${keypair.address}?mykey`, - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(false) - signature = { - // @ts-expect-error - signerUrl: `kilt:did:${keypair.address}#mykey`, - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(false) - signature = { - // @ts-expect-error - signerUrl: `did:kilt:${keypair.address}`, - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(false) - signature = { - // @ts-expect-error - signerUrl: keypair.address, - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(false) - signature = { - // @ts-expect-error - signerUrl: '', - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(false) - }) - - it('rejects unexpected signature type', () => { - const signature: DidSignature = { - signerUrl: `did:kilt:${keypair.address}#mykey` as DidDocumentV2.DidUrl, - signature: '', - } - expect(isDidSignature(signature)).toBe(false) - signature.signature = randomAsHex(32).substring(2) - expect(isDidSignature(signature)).toBe(false) - // @ts-expect-error - signature.signature = randomAsU8a(32) - expect(isDidSignature(signature)).toBe(false) - }) - - it('rejects incomplete objects', () => { - let signature: DidSignature = { - signerUrl: `did:kilt:${keypair.address}#mykey` as DidDocumentV2.DidUrl, - // @ts-expect-error - signature: undefined, - } - expect(isDidSignature(signature)).toBe(false) - signature = { - // @ts-expect-error - signerUrl: undefined, - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(false) - // @ts-expect-error - signature = { - signature: randomAsHex(32), - } - expect(isDidSignature(signature)).toBe(false) - // @ts-expect-error - signature = { - signerUrl: `did:kilt:${keypair.address}#mykey` as DidDocumentV2.DidUrl, - } - expect(isDidSignature(signature)).toBe(false) - // @ts-expect-error - signature = {} - expect(isDidSignature(signature)).toBe(false) - // @ts-expect-error - signature = { signerUrl: null, signature: null } - expect(isDidSignature(signature)).toBe(false) - }) -}) diff --git a/packages/did/src/Did2.signature.ts b/packages/did/src/Did2.signature.ts deleted file mode 100644 index 0cabadd3d0..0000000000 --- a/packages/did/src/Did2.signature.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { - CryptoCallbacksV2, - DidDocumentV2, - DidResolverV2, -} from '@kiltprotocol/types' - -import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { isHex } from '@polkadot/util' - -import { multibaseKeyToDidKey, parse, validateUri } from './Did2.utils.js' -import { resolve } from './DidResolver/DidResolverV2.js' - -export type DidSignatureVerificationInput = { - message: string | Uint8Array - signature: Uint8Array - signerUrl: DidDocumentV2.DidUrl - expectedSigner?: DidDocumentV2.DidUri - allowUpgraded?: boolean - expectedVerificationMethodRelationship?: DidDocumentV2.SignatureVerificationMethodRelationship - resolveDid?: DidResolverV2.ResolveDid['resolve'] -} - -export type DidSignature = { - signerUrl: DidDocumentV2.DidUrl - signature: string -} - -// Used solely for retro-compatibility with previously-generated DID signatures. -// It is reasonable to think that it will be removed at some point in the future. -type OldDidSignatureV1 = { - signature: string - keyId: DidDocumentV2.DidUrl -} -type OldDidSignatureV2 = { - signature: string - keyUri: DidDocumentV2.DidUrl -} - -function verifyDidSignatureDataStructure( - input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 -): void { - const verificationMethodUri = (() => { - if ('keyUri' in input) { - return input.keyUri - } - if ('keyId' in input) { - return input.keyId - } - return input.signerUrl - })() - if (!isHex(input.signature)) { - throw new SDKErrors.SignatureMalformedError( - `Expected signature as a hex string, got ${input.signature}` - ) - } - validateUri(verificationMethodUri, 'ResourceUri') -} - -/** - * Verify a DID signature given the signer's DID URL. - * A signature verification returns false if a migrated and then deleted DID is used. - * - * @param input Object wrapping all input. - * @param input.message The message that was signed. - * @param input.signature Signature bytes. - * @param input.signerUrl DID URL of the verification method used for signing. - * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. - * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. - * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. - * @param input.resolveDid Allows specifying a custom DID resolve. Defaults to the built-in [[resolve]]. - */ -export async function verifyDidSignature({ - message, - signature, - signerUrl, - expectedSigner, - allowUpgraded = false, - expectedVerificationMethodRelationship, - resolveDid = resolve, -}: DidSignatureVerificationInput): Promise { - // checks if key uri points to the right did; alternatively we could check the key's controller - const signer = parse(signerUrl) - if (expectedSigner && expectedSigner !== signer.did) { - // check for allowable exceptions - const expected = parse(expectedSigner) - // NECESSARY CONDITION: subjects and versions match - const subjectVersionMatch = - expected.address === signer.address && expected.version === signer.version - // EITHER: signer is a full did and we allow signatures by corresponding full did - const allowedUpgrade = allowUpgraded && signer.type === 'full' - // OR: both are light dids and their auth key type matches - const keyTypeMatch = - signer.type === 'light' && - expected.type === 'light' && - expected.authKeyTypeEncoding === signer.authKeyTypeEncoding - if (!(subjectVersionMatch && (allowedUpgrade || keyTypeMatch))) { - throw new SDKErrors.DidSubjectMismatchError(signer.did, expected.did) - } - } - - const { didDocument } = await resolveDid(signerUrl, {}) - if (didDocument === undefined) { - throw new SDKErrors.SignatureUnverifiableError( - `Error validating the DID signature. Cannot fetch DID Document for "${signerUrl}".` - ) - } - const verificationMethod = didDocument.verificationMethod?.find( - (vm) => vm.id === signer.fragment - ) - if (verificationMethod === undefined) { - throw new SDKErrors.SignatureUnverifiableError( - `Cannot find verification method with ID "${signer.fragment} in the DID Document for "${signerUrl}".` - ) - } - if ( - expectedVerificationMethodRelationship !== undefined && - didDocument[expectedVerificationMethodRelationship]?.find( - (vm) => vm === signer.fragment - ) === undefined - ) { - throw new SDKErrors.SignatureUnverifiableError( - `Cannot find verification method with ID "${signer.fragment} for the relationship "${expectedVerificationMethodRelationship}".` - ) - } - - const { publicKey } = multibaseKeyToDidKey( - verificationMethod.publicKeyMultibase - ) - Crypto.verify(message, signature, publicKey) -} - -/** - * Type guard assuring that the input is a valid DidSignature object, consisting of a signature as hex and the uri of the signing key. - * Does not cryptographically verify the signature itself! - * - * @param input Arbitrary input. - * @returns True if validation of form has passed. - */ -export function isDidSignature( - input: unknown -): input is DidSignature | OldDidSignatureV1 | OldDidSignatureV2 { - try { - verifyDidSignatureDataStructure(input as DidSignature) - return true - } catch (cause) { - return false - } -} - -/** - * Transforms the output of a [[SignCallback]] into the [[DidSignature]] format suitable for json-based data exchange. - * - * @param input Signature data returned from the [[SignCallback]]. - * @param input.signature Signature bytes. - * @param input.did The DID URI of the signer. - * @param input.verificationMethod The verification method used to generate the signature. - * @returns A [[DidSignature]] object where signature is hex-encoded. - */ -export function signatureToJson({ - did, - signature, - verificationMethod, -}: CryptoCallbacksV2.SignResponseData & { - did: DidDocumentV2.DidUri -}): DidSignature { - return { - signature: Crypto.u8aToHex(signature), - signerUrl: `${did}${verificationMethod.id}`, - } -} - -/** - * Deserializes a [[DidSignature]] for signature verification. - * Handles backwards compatibility to an older version of the interface where the `verificationMethodUri` property was called either `keyUri` or `keyId`. - * - * @param input A [[DidSignature]] object. - * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. - */ -export function signatureFromJson( - input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 -): Pick & { - verificationMethodUri: DidDocumentV2.DidUrl -} { - const verificationMethodUri = (() => { - if ('keyUri' in input) { - return input.keyUri - } - if ('keyId' in input) { - return input.keyId - } - return input.signerUrl - })() - const signature = Crypto.coToUInt8(input.signature) - return { signature, verificationMethodUri } -} diff --git a/packages/did/src/Did2.utils.ts b/packages/did/src/Did2.utils.ts deleted file mode 100644 index 73dd7b3a1f..0000000000 --- a/packages/did/src/Did2.utils.ts +++ /dev/null @@ -1,328 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { - DidDocumentV2, - KeyringPair, - KiltAddress, -} from '@kiltprotocol/types' - -import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' -import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' -import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' - -import type { DidKeyType } from './DidDetailsv2/DidDetailsV2.js' - -// The latest version for KILT light DIDs. -const LIGHT_DID_LATEST_VERSION = 1 - -// The latest version for KILT full DIDs. -const FULL_DID_LATEST_VERSION = 1 - -// NOTICE: The following regex patterns must be kept in sync with DidUri type in @kiltprotocol/types - -// Matches the following full DIDs -// - did:kilt: -// - did:kilt:# -const FULL_KILT_DID_REGEX = - /^did:kilt:(?
4[1-9a-km-zA-HJ-NP-Z]{47})(?#[^#\n]+)?$/ - -// Matches the following light DIDs -// - did:kilt:light:00 -// - did:kilt:light:01: -// - did:kilt:light:10# -// - did:kilt:light:99:# -const LIGHT_KILT_DID_REGEX = - /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?#[^#\n]+)?$/ - -type IDidParsingResult = { - did: DidDocumentV2.DidUri - version: number - type: 'light' | 'full' - address: KiltAddress - fragment?: DidDocumentV2.UriFragment - authKeyTypeEncoding?: string - encodedDetails?: string -} - -/** - * Parses a KILT DID uri and returns the information contained within in a structured form. - * - * @param didUri A KILT DID uri as a string. - * @returns Object containing information extracted from the DID uri. - */ -export function parse( - didUri: DidDocumentV2.DidUri | DidDocumentV2.DidUrl -): IDidParsingResult { - let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups - if (matches) { - const { version: versionString, fragment } = matches - const address = matches.address as KiltAddress - const version = versionString - ? parseInt(versionString, 10) - : FULL_DID_LATEST_VERSION - return { - did: didUri.replace(fragment || '', '') as DidDocumentV2.DidUri, - version, - type: 'full', - address, - fragment: - fragment === '#' ? undefined : (fragment as DidDocumentV2.UriFragment), - } - } - - // If it fails to parse full DID, try with light DID - matches = LIGHT_KILT_DID_REGEX.exec(didUri)?.groups - if (matches) { - const { - authKeyType, - version: versionString, - encodedDetails, - fragment, - } = matches - const address = matches.address as KiltAddress - const version = versionString - ? parseInt(versionString, 10) - : LIGHT_DID_LATEST_VERSION - return { - did: didUri.replace(fragment || '', '') as DidDocumentV2.DidUri, - version, - type: 'light', - address, - fragment: - fragment === '#' ? undefined : (fragment as DidDocumentV2.UriFragment), - encodedDetails, - authKeyTypeEncoding: authKeyType, - } - } - - throw new SDKErrors.InvalidDidFormatError(didUri) -} - -type DecodedVerificationMethod = { - publicKey: Uint8Array - keyType: DidKeyType -} - -const multicodecPrefixes: Record = { - 0xe7: ['ecdsa', 33], - 0xec: ['x25519', 32], - 0xed: ['ed25519', 32], - 0xef: ['sr25519', 32], -} -const multicodecReversePrefixes: Record = { - ecdsa: 0xe7, - ed25519: 0xed, - sr25519: 0xef, - x25519: 0xec, -} - -/** - * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. - * - * @param publicKeyMultibase The verification method's public key multibase. - * @returns The decoded public key and [[DidKeyType]]. - */ -export function multibaseKeyToDidKey( - publicKeyMultibase: DidDocumentV2.VerificationMethod['publicKeyMultibase'] -): DecodedVerificationMethod { - const decodedMulticodecPublicKey = multibaseDecode(publicKeyMultibase) - const [keyTypeFlag, publicKey] = [ - decodedMulticodecPublicKey.subarray(0, 1)[0], - decodedMulticodecPublicKey.subarray(1), - ] - const [keyType, expectedPublicKeyLength] = multicodecPrefixes[keyTypeFlag] - if (keyType === undefined) { - throw new SDKErrors.DidError( - `Cannot decode key type for multibase key "${publicKeyMultibase}".` - ) - } - if (publicKey.length !== expectedPublicKeyLength) { - throw new SDKErrors.DidError( - `Key of type "${keyType}" is expected to be ${expectedPublicKeyLength} bytes long. Provided key is ${publicKey.length} bytes long instead.` - ) - } - return { - keyType, - publicKey, - } -} - -/** - * Calculate the multibase, multicodec representation of a keypair given its type and public key. - * - * @param keypair The input keypair to encode as multibase, multicodec. - * @param keypair.type The keypair [[DidKeyType]]. - * @param keypair.publicKey The keypair public key. - * @returns The multicodec, multibase encoding of the provided keypair. - */ -export function keypairToMultibaseKey({ - type, - publicKey, -}: Pick & { - type: DidKeyType -}): DidDocumentV2.VerificationMethod['publicKeyMultibase'] { - const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] - if (multiCodecPublicKeyPrefix === undefined) { - throw new SDKErrors.DidError( - `The provided key type "${type}" is not supported.` - ) - } - const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] - if (publicKey.length !== expectedPublicKeySize) { - throw new SDKErrors.DidError( - `Key of type "${type}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` - ) - } - const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] - return Buffer.from( - multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) - ).toString() as `z${string}` -} - -/** - * Export a DID key to a `MultiKey` verification method. - * - * @param controller The verification method controller's DID URI. - * @param id The verification method ID. - * @param key The DID key to export as a verification method. - * @param key.keyType The key type. - * @param key.publicKey The public component of the key. - * @returns The provided key encoded as a [[ DidDocumentV2.VerificationMethod]]. - */ -export function didKeyToVerificationMethod( - controller: DidDocumentV2.VerificationMethod['controller'], - id: DidDocumentV2.VerificationMethod['id'], - { keyType, publicKey }: DecodedVerificationMethod -): DidDocumentV2.VerificationMethod { - const multiCodecPublicKeyPrefix = multicodecReversePrefixes[keyType] - if (multiCodecPublicKeyPrefix === undefined) { - throw new SDKErrors.DidError( - `Provided key type "${keyType}" not supported.` - ) - } - const expectedPublicKeySize = multicodecPrefixes[multiCodecPublicKeyPrefix][1] - if (publicKey.length !== expectedPublicKeySize) { - throw new SDKErrors.DidError( - `Key of type "${keyType}" is expected to be ${expectedPublicKeySize} bytes long. Provided key is ${publicKey.length} bytes long instead.` - ) - } - const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] - return { - controller, - id, - type: 'MultiKey', - publicKeyMultibase: Buffer.from( - multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) - ).toString() as `z${string}`, - } -} - -/** - * Returns true if both didA and didB refer to the same DID subject, i.e., whether they have the same identifier as specified in the method spec. - * - * @param didA A KILT DID uri as a string. - * @param didB A second KILT DID uri as a string. - * @returns Whether didA and didB refer to the same DID subject. - */ -export function isSameSubject( - didA: DidDocumentV2.DidUri, - didB: DidDocumentV2.DidUri -): boolean { - return parse(didA).address === parse(didB).address -} - -/** - * Checks that a string (or other input) is a valid KILT DID uri with or without a URI fragment. - * Throws otherwise. - * - * @param input Arbitrary input. - * @param expectType `ResourceUri` if the URI is expected to have a fragment (following '#'), `Did` if it is expected not to have one. Default allows both. - */ -export function validateUri( - input: unknown, - expectType?: 'Did' | 'ResourceUri' -): void { - if (typeof input !== 'string') { - throw new TypeError(`DID string expected, got ${typeof input}`) - } - const { address, fragment } = parse(input as DidDocumentV2.DidUri) - - if ( - fragment !== undefined && - (expectType === 'Did' || - // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. - (typeof expectType === 'boolean' && expectType === false)) - ) { - throw new SDKErrors.DidError( - 'Expected a Kilt DidUri but got a DidResourceUri (containing a #fragment)' - ) - } - - if (fragment === undefined && expectType === 'ResourceUri') { - throw new SDKErrors.DidError( - 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' - ) - } - - DataUtils.verifyKiltAddress(address) -} - -/** - * Internal: derive the address part of the DID when it is created from the provided authentication verification method. - * - * @param input The authentication verification method. - * @param input.publicKeyMultibase The `publicKeyMultibase` value of the verification method. - * @returns The expected address of the DID. - */ -export function getAddressFromVerificationMethod({ - publicKeyMultibase, -}: Pick): KiltAddress { - const { keyType: type, publicKey } = multibaseKeyToDidKey(publicKeyMultibase) - if (type === 'ed25519' || type === 'sr25519') { - return encodeAddress(publicKey, ss58Format) - } - - // Otherwise it’s ecdsa. - // Taken from https://github.com/polkadot-js/common/blob/master/packages/keyring/src/pair/index.ts#L44 - const address = publicKey.length > 32 ? blake2AsU8a(publicKey) : publicKey - return encodeAddress(address, ss58Format) -} - -/** - * Builds the URI a light DID will have after it’s stored on the blockchain. - * - * @param didOrAddress The URI of the light DID. Internally it’s used with the DID "address" as well. - * @param version The version of the DID URI to use. - * @returns The expected full DID URI. - */ -export function getFullDidUri( - didOrAddress: DidDocumentV2.DidUri | KiltAddress, - version = FULL_DID_LATEST_VERSION -): DidDocumentV2.DidUri { - const address = DataUtils.isKiltAddress(didOrAddress) - ? didOrAddress - : parse(didOrAddress as DidDocumentV2.DidUri).address - const versionString = version === 1 ? '' : `v${version}` - return `did:kilt:${versionString}${address}` as DidDocumentV2.DidUri -} - -/** - * Builds the URI of a full DID if it is created with the authentication key provided. - * - * @param verificationMethod The DID verification method. - * @returns The expected full DID URI. - */ -export function getFullDidUriFromVerificationMethod( - verificationMethod: Pick< - DidDocumentV2.VerificationMethod, - 'publicKeyMultibase' - > -): DidDocumentV2.DidUri { - const address = getAddressFromVerificationMethod(verificationMethod) - return getFullDidUri(address) -} diff --git a/packages/did/src/DidDetails/DidDetails.spec.ts b/packages/did/src/DidDetails/DidDetails.spec.ts deleted file mode 100644 index 26302a1f63..0000000000 --- a/packages/did/src/DidDetails/DidDetails.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { DidDocument, DidKey, DidServiceEndpoint } from '@kiltprotocol/types' - -import { getService, getKey, getKeys } from './DidDetails' - -const minimalDid: DidDocument = { - uri: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - authentication: [ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - ], -} - -const maximalDid: DidDocument = { - uri: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - authentication: [ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - ], - assertionMethod: [ - { - id: '#assertionMethod', - publicKey: new Uint8Array(0), - type: 'ed25519', - }, - ], - capabilityDelegation: [ - { - id: '#capabilityDelegation', - publicKey: new Uint8Array(0), - type: 'ecdsa', - }, - ], - keyAgreement: [ - { - id: '#keyAgreement', - publicKey: new Uint8Array(0), - type: 'x25519', - }, - ], - service: [ - { - id: '#service', - type: ['foo'], - serviceEndpoint: ['https://example.com/'], - }, - ], -} - -describe('DidDetais', () => { - describe('getKeys', () => { - it('should get keys of a minimal DID', async () => { - expect(getKeys(minimalDid)).toEqual([ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - ]) - }) - it('should get keys of a maximal DID', async () => { - expect(getKeys(maximalDid)).toEqual([ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - { - id: '#assertionMethod', - publicKey: new Uint8Array(0), - type: 'ed25519', - }, - { - id: '#capabilityDelegation', - publicKey: new Uint8Array(0), - type: 'ecdsa', - }, - { - id: '#keyAgreement', - publicKey: new Uint8Array(0), - type: 'x25519', - }, - ]) - }) - }) - describe('getKey', () => { - it('should get key by ID', async () => { - expect(getKey(maximalDid, '#capabilityDelegation')).toEqual({ - id: '#capabilityDelegation', - publicKey: new Uint8Array(0), - type: 'ecdsa', - }) - }) - it('should return undefined when key not found', async () => { - expect(getKey(minimalDid, '#capabilityDelegation')).toEqual(undefined) - }) - }) - describe('getService', () => { - it('should get endpoint by ID', async () => { - expect(getService(maximalDid, '#service')).toEqual({ - id: '#service', - serviceEndpoint: ['https://example.com/'], - type: ['foo'], - }) - }) - it('should return undefined when key not found', async () => { - expect(getService(minimalDid, '#service')).toEqual(undefined) - }) - }) -}) diff --git a/packages/did/src/DidDetails/DidDetails.ts b/packages/did/src/DidDetails/DidDetails.ts index 0af5565641..8f4aecd50b 100644 --- a/packages/did/src/DidDetails/DidDetails.ts +++ b/packages/did/src/DidDetails/DidDetails.ts @@ -7,51 +7,117 @@ import type { DidDocument, - DidKey, - DidServiceEndpoint, + Service, + UriFragment, + VerificationMethod, + VerificationMethodRelationship, } from '@kiltprotocol/types' +import { didKeyToVerificationMethod } from '../Did.utils.js' + /** - * Gets all public keys associated with this DID. - * - * @param did The DID data. - * @returns Array of public keys. + * Possible types for a DID verification key. */ -export function getKeys( - did: Partial & Pick -): DidKey[] { - return [ - ...did.authentication, - ...(did.assertionMethod || []), - ...(did.capabilityDelegation || []), - ...(did.keyAgreement || []), - ] +const verificationKeyTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const +export const verificationKeyTypes = verificationKeyTypesC as unknown as string[] +export type DidVerificationKeyType = typeof verificationKeyTypesC[number] +// `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 + +export function isValidVerificationKeyType( + input: string +): input is DidVerificationKeyType { + return verificationKeyTypes.includes(input) } /** - * Returns a key with a given id, if associated with this DID. - * - * @param did The DID data. - * @param id Key id (not the full key uri). - * @returns The respective public key data or undefined. + * Possible types for a DID encryption key. + */ +const encryptionKeyTypesC = ['x25519'] as const +export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] +export type DidEncryptionKeyType = typeof encryptionKeyTypesC[number] + +export function isValidEncryptionKeyType( + input: string +): input is DidEncryptionKeyType { + return encryptionKeyTypes.includes(input) +} + +export type DidKeyType = DidVerificationKeyType | DidEncryptionKeyType + +export function isValidDidKeyType(input: string): input is DidKeyType { + return isValidVerificationKeyType(input) || isValidEncryptionKeyType(input) +} + +export type NewVerificationMethod = Omit +export type NewService = Service + +/** + * Type of a new key material to add under a DID. */ -export function getKey( - did: Partial & Pick, - id: DidKey['id'] -): DidKey | undefined { - return getKeys(did).find((key) => key.id === id) +export type BaseNewDidKey = { + publicKey: Uint8Array + type: string +} + +/** + * Type of a new verification key to add under a DID. + */ +export type NewDidVerificationKey = BaseNewDidKey & { + type: DidVerificationKeyType +} + +/** + * Type of a new encryption key to add under a DID. + */ +export type NewDidEncryptionKey = BaseNewDidKey & { + type: DidEncryptionKeyType +} + +function doesVerificationMethodExist( + didDocument: DidDocument, + { id }: Pick +): boolean { + return ( + didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined + ) +} + +function addVerificationMethod( + didDocument: DidDocument, + verificationMethod: VerificationMethod, + relationship: VerificationMethodRelationship +): void { + const existingRelationship = didDocument[relationship] ?? [] + existingRelationship.push(verificationMethod.id) + // eslint-disable-next-line no-param-reassign + didDocument[relationship] = existingRelationship + if (!doesVerificationMethodExist(didDocument, verificationMethod)) { + const existingVerificationMethod = didDocument.verificationMethod ?? [] + existingVerificationMethod.push(verificationMethod) + // eslint-disable-next-line no-param-reassign + didDocument.verificationMethod = existingVerificationMethod + } } /** - * Returns a service endpoint with a given id, if associated with this DID. + * Add the provided keypair as a new verification method to the DID Document. + * !!! This function is meant to be used internally and not exposed since it is mostly used as a utility and does not perform extensive checks on the inputs. * - * @param did The DID data. - * @param id Endpoint id (not the full endpoint uri). - * @returns The respective endpoint data or undefined. + * @param didDocument The DID Document to add the verification method to. + * @param newKeypair The new keypair to add as a verification method. + * @param newKeypair.id The ID of the new verification method. If a verification method with the same ID already exists, this operation is a no-op. + * @param newKeypair.publicKey The public key of the keypair. + * @param newKeypair.type The type of the public key. + * @param relationship The verification relationship to add the verification method to. */ -export function getService( - did: Pick, - id: DidServiceEndpoint['id'] -): DidServiceEndpoint | undefined { - return did.service?.find((endpoint) => endpoint.id === id) +export function addKeypairAsVerificationMethod( + didDocument: DidDocument, + { id, publicKey, type: keyType }: BaseNewDidKey & { id: UriFragment }, + relationship: VerificationMethodRelationship +): void { + const verificationMethod = didKeyToVerificationMethod(didDocument.id, id, { + keyType: keyType as DidKeyType, + publicKey, + }) + addVerificationMethod(didDocument, verificationMethod, relationship) } diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 3f80049315..8378ea6f99 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -5,10 +5,6 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { BN } from '@polkadot/util' -import { randomAsHex } from '@polkadot/util-crypto' - -import { ConfigService } from '@kiltprotocol/config' import type { DidDocument, KiltKeyringPair, @@ -16,19 +12,22 @@ import type { SubmittableExtrinsic, } from '@kiltprotocol/types' -import { - ApiMocks, - createLocalDemoFullDidFromKeypair, - makeSigningKeyTool, -} from '../../../../tests/testUtils' +import { BN } from '@polkadot/util' +import { randomAsHex } from '@polkadot/util-crypto' +import { ConfigService } from '@kiltprotocol/config' + +import { ApiMocks, TestUtilsV2 } from '../../../../tests/testUtils' import { generateDidAuthenticatedTx } from '../Did.chain.js' -import * as Did from './index.js' +import { + authorizeBatch, + getVerificationMethodRelationshipForTx, +} from './FullDidDetails.js' const augmentedApi = ApiMocks.createAugmentedApi() const mockedApi: any = ApiMocks.getMockedApi() ConfigService.set({ api: mockedApi }) -jest.mock('../Did.chain') +jest.mock('../Did2.chain') jest .mocked(generateDidAuthenticatedTx) .mockResolvedValue({} as SubmittableExtrinsic) @@ -46,9 +45,11 @@ describe('When creating an instance from the chain', () => { let fullDid: DidDocument beforeAll(async () => { - const keyTool = makeSigningKeyTool() + const keyTool = TestUtilsV2.makeSigningKeyTool() keypair = keyTool.keypair - fullDid = await createLocalDemoFullDidFromKeypair(keyTool.keypair) + fullDid = await TestUtilsV2.createLocalDemoFullDidFromKeypair( + keyTool.keypair + ) sign = keyTool.getSignCallback(fullDid) }) @@ -56,8 +57,8 @@ describe('When creating an instance from the chain', () => { it('fails if the extrinsic does not require a DID', async () => { const extrinsic = augmentedApi.tx.indices.claim(1) await expect(async () => - Did.authorizeBatch({ - did: fullDid.uri, + authorizeBatch({ + did: fullDid.id, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [extrinsic, extrinsic], sign, @@ -74,8 +75,8 @@ describe('When creating an instance from the chain', () => { ]) const batchFunction = jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll - await Did.authorizeBatch({ - did: fullDid.uri, + await authorizeBatch({ + did: fullDid.id, batchFunction, extrinsics: [extrinsic, extrinsic], sign, @@ -111,8 +112,8 @@ describe('When creating an instance from the chain', () => { ctype3Extrinsic, ctype4Extrinsic, ] - await Did.authorizeBatch({ - did: fullDid.uri, + await authorizeBatch({ + did: fullDid.id, batchFunction, extrinsics, nonce: new BN(0), @@ -139,8 +140,8 @@ describe('When creating an instance from the chain', () => { describe('.build()', () => { it('throws if batch is empty', async () => { await expect(async () => - Did.authorizeBatch({ - did: fullDid.uri, + authorizeBatch({ + did: fullDid.id, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [], sign, @@ -164,13 +165,13 @@ const mockApi = ApiMocks.createAugmentedApi() describe('When creating an instance from the chain', () => { it('Should return correct KeyRelationship for single valid call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) ) expect(keyRelationship).toBe('assertionMethod') }) it('Should return correct KeyRelationship for batched call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.batch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -187,7 +188,7 @@ describe('When creating an instance from the chain', () => { expect(keyRelationship).toBe('assertionMethod') }) it('Should return correct KeyRelationship for batchAll call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.batchAll([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -204,7 +205,7 @@ describe('When creating an instance from the chain', () => { expect(keyRelationship).toBe('assertionMethod') }) it('Should return correct KeyRelationship for forceBatch call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -221,7 +222,7 @@ describe('When creating an instance from the chain', () => { expect(keyRelationship).toBe('assertionMethod') }) it('Should return undefined for batch with mixed KeyRelationship calls', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + const keyRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index b473a67c05..202df522d0 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -7,30 +7,33 @@ import type { Extrinsic } from '@polkadot/types/interfaces' import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' -import { BN } from '@polkadot/util' - import type { DidUri, KiltAddress, + SignatureVerificationMethodRelationship, SignExtrinsicCallback, SubmittableExtrinsic, - VerificationKeyRelationship, } from '@kiltprotocol/types' -import { SDKErrors } from '@kiltprotocol/utils' +import { BN } from '@polkadot/util' + import { ConfigService } from '@kiltprotocol/config' +import { SDKErrors } from '@kiltprotocol/utils' +import { parse } from '../Did.utils.js' import { documentFromChain, generateDidAuthenticatedTx, toChain, } from '../Did.chain.js' -import { parse } from '../Did.utils.js' // Must be in sync with what's implemented in impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for Call // in https://github.com/KILTprotocol/mashnet-node/blob/develop/runtimes/spiritnet/src/lib.rs // TODO: Should have an RPC or something similar to avoid inconsistencies in the future. -const methodMapping: Record = { +const methodMapping: Record< + string, + SignatureVerificationMethodRelationship | undefined +> = { attestation: 'assertionMethod', ctype: 'assertionMethod', delegation: 'capabilityDelegation', @@ -43,9 +46,9 @@ const methodMapping: Record = { web3Names: 'authentication', } -function getKeyRelationshipForMethod( +function getVerificationMethodRelationshipForRuntimeCall( call: Extrinsic['method'] -): VerificationKeyRelationship | undefined { +): SignatureVerificationMethodRelationship | undefined { const { section, method } = call // get the VerificationKeyRelationship of a batched call @@ -56,7 +59,7 @@ function getKeyRelationshipForMethod( ) { // map all calls to their VerificationKeyRelationship and deduplicate the items return (call.args[0] as unknown as Array) - .map(getKeyRelationshipForMethod) + .map(getVerificationMethodRelationshipForRuntimeCall) .reduce((prev, value) => (prev === value ? prev : undefined)) } @@ -69,15 +72,15 @@ function getKeyRelationshipForMethod( } /** - * Detect the key relationship for a key which should be used to DID-authorize the provided extrinsic. + * Detect the verification relationship for a verification method which should be used to DID-authorize the provided extrinsic. * * @param extrinsic The unsigned extrinsic to inspect. - * @returns The key relationship. + * @returns The verification relationship. */ -export function getKeyRelationshipForTx( +export function getVerificationMethodRelationshipForTx( extrinsic: Extrinsic -): VerificationKeyRelationship | undefined { - return getKeyRelationshipForMethod(extrinsic.method) +): SignatureVerificationMethodRelationship | undefined { + return getVerificationMethodRelationshipForRuntimeCall(extrinsic.method) } // Max nonce value is (2^64) - 1 @@ -108,7 +111,7 @@ async function getNextNonce(did: DidUri): Promise { } /** - * Signs and returns the provided unsigned extrinsic with the right DID key, if present. Otherwise, it will throw an error. + * Signs and returns the provided unsigned extrinsic with the right DID verification method, if present. Otherwise, it will throw an error. * * @param did The DID data. * @param extrinsic The unsigned extrinsic to sign. @@ -135,14 +138,15 @@ export async function authorizeTx( ) } - const keyRelationship = getKeyRelationshipForTx(extrinsic) - if (keyRelationship === undefined) { + const verificationMethodRelationship = + getVerificationMethodRelationshipForTx(extrinsic) + if (verificationMethodRelationship === undefined) { throw new SDKErrors.SDKError('No key relationship found for extrinsic') } return generateDidAuthenticatedTx({ did, - keyRelationship, + verificationMethodRelationship, sign, call: extrinsic, txCounter: txCounter || (await getNextNonce(did)), @@ -152,38 +156,41 @@ export async function authorizeTx( type GroupedExtrinsics = Array<{ extrinsics: Extrinsic[] - keyRelationship: VerificationKeyRelationship + verificationMethodRelationship: SignatureVerificationMethodRelationship }> function groupExtrinsicsByKeyRelationship( extrinsics: Extrinsic[] ): GroupedExtrinsics { const [first, ...rest] = extrinsics.map((extrinsic) => { - const keyRelationship = getKeyRelationshipForTx(extrinsic) - if (!keyRelationship) { + const verificationMethodRelationship = + getVerificationMethodRelationshipForTx(extrinsic) + if (verificationMethodRelationship === undefined) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) } - return { extrinsic, keyRelationship } + return { extrinsic, verificationMethodRelationship } }) const groups: GroupedExtrinsics = [ { extrinsics: [first.extrinsic], - keyRelationship: first.keyRelationship, + verificationMethodRelationship: first.verificationMethodRelationship, }, ] - rest.forEach(({ extrinsic, keyRelationship }) => { + rest.forEach(({ extrinsic, verificationMethodRelationship }) => { const currentGroup = groups[groups.length - 1] - const useCurrentGroup = keyRelationship === currentGroup.keyRelationship + const useCurrentGroup = + verificationMethodRelationship === + currentGroup.verificationMethodRelationship if (useCurrentGroup) { currentGroup.extrinsics.push(extrinsic) } else { groups.push({ extrinsics: [extrinsic], - keyRelationship, + verificationMethodRelationship, }) } }) @@ -192,7 +199,7 @@ function groupExtrinsicsByKeyRelationship( } /** - * Authorizes/signs a list of extrinsics grouping them in batches by required key type. + * Authorizes/signs a list of extrinsics grouping them in batches by required verification relationship. * * @param input The object with named parameters. * @param input.batchFunction The batch function to use, for example `api.tx.utility.batchAll`. @@ -244,11 +251,11 @@ export async function authorizeBatch({ const call = list.length === 1 ? list[0] : batchFunction(list) const txCounter = increaseNonce(firstNonce, batchIndex) - const { keyRelationship } = group + const { verificationMethodRelationship } = group return generateDidAuthenticatedTx({ did, - keyRelationship, + verificationMethodRelationship, sign, call, txCounter, diff --git a/packages/did/src/DidDetails/LightDidDetails.spec.ts b/packages/did/src/DidDetails/LightDidDetails.spec.ts index 2a7a23a926..db0cb858cb 100644 --- a/packages/did/src/DidDetails/LightDidDetails.spec.ts +++ b/packages/did/src/DidDetails/LightDidDetails.spec.ts @@ -5,10 +5,18 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidDocument, DidServiceEndpoint, DidUri } from '@kiltprotocol/types' +import type { DidDocument, DidUri } from '@kiltprotocol/types' + import { Crypto } from '@kiltprotocol/utils' -import * as Did from '../index.js' +import type { NewService } from './DidDetails.js' +import type { CreateDocumentInput } from './LightDidDetails.js' + +import { keypairToMultibaseKey, parse } from '../Did.utils.js' +import { + createLightDidDocument, + parseDocumentFromLightDid, +} from './LightDidDetails.js' /* * Functions tested: @@ -26,7 +34,7 @@ describe('When creating an instance from the details', () => { const encKey = Crypto.makeEncryptionKeypairFromSeed( new Uint8Array(32).fill(1) ) - const service: DidServiceEndpoint[] = [ + const service: NewService[] = [ { id: '#service-1', type: ['type-1'], @@ -39,26 +47,34 @@ describe('When creating an instance from the details', () => { }, ] - const lightDid = Did.createLightDidDocument({ + const lightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, }) expect(lightDid).toEqual({ - uri: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - authentication: [ + id: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#authentication', - publicKey: authKey.publicKey, - type: 'sr25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'sr25519', + }), + type: 'MultiKey', }, - ], - keyAgreement: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#encryption', - publicKey: encKey.publicKey, - type: 'x25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'MultiKey', }, ], service: [ @@ -82,27 +98,35 @@ describe('When creating an instance from the details', () => { new Uint8Array(32).fill(1) ) - const lightDid = Did.createLightDidDocument({ + const lightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], }) - expect(Did.parse(lightDid.uri).address).toStrictEqual(authKey.address) + expect(parse(lightDid.id).address).toStrictEqual(authKey.address) - expect(lightDid).toEqual({ - uri: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, - authentication: [ + expect(lightDid).toEqual({ + id: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ { + controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, id: '#authentication', - publicKey: authKey.publicKey, - type: 'ed25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'ed25519', + }), + type: 'MultiKey', }, - ], - keyAgreement: [ { + controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, id: '#encryption', - publicKey: encKey.publicKey, - type: 'x25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'MultiKey', }, ], }) @@ -115,9 +139,7 @@ describe('When creating an instance from the details', () => { authentication: [authKey], } expect(() => - Did.createLightDidDocument( - invalidInput as unknown as Did.CreateDocumentInput - ) + createLightDidDocument(invalidInput as unknown as CreateDocumentInput) ).toThrowError() }) @@ -130,9 +152,7 @@ describe('When creating an instance from the details', () => { keyAgreement: [{ publicKey: encKey.publicKey, type: 'bls' }], } expect(() => - Did.createLightDidDocument( - invalidInput as unknown as Did.CreateDocumentInput - ) + createLightDidDocument(invalidInput as unknown as CreateDocumentInput) ).toThrowError() }) }) @@ -143,7 +163,7 @@ describe('When creating an instance from a URI', () => { const encKey = Crypto.makeEncryptionKeypairFromSeed( new Uint8Array(32).fill(1) ) - const endpoints: DidServiceEndpoint[] = [ + const endpoints: NewService[] = [ { id: '#service-1', type: ['type-1'], @@ -156,30 +176,38 @@ describe('When creating an instance from a URI', () => { }, ] // We are sure this is correct because of the described case above - const expectedLightDid = Did.createLightDidDocument({ + const expectedLightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service: endpoints, }) - const { address } = Did.parse(expectedLightDid.uri) - const builtLightDid = Did.parseDocumentFromLightDid(expectedLightDid.uri) + const { address } = parse(expectedLightDid.id) + const builtLightDid = parseDocumentFromLightDid(expectedLightDid.id) expect(builtLightDid).toStrictEqual(expectedLightDid) expect(builtLightDid).toStrictEqual({ - uri: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7` as DidUri, - authentication: [ + id: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#authentication', - publicKey: authKey.publicKey, - type: 'sr25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'sr25519', + }), + type: 'MultiKey', }, - ], - keyAgreement: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#encryption', - publicKey: encKey.publicKey, - type: 'x25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'MultiKey', }, ], service: [ @@ -200,7 +228,7 @@ describe('When creating an instance from a URI', () => { it('fail if a fragment is present according to the options', () => { const authKey = Crypto.makeKeypairFromSeed() const encKey = Crypto.makeEncryptionKeypairFromSeed() - const service: DidServiceEndpoint[] = [ + const service: NewService[] = [ { id: '#service-1', type: ['type-1'], @@ -214,17 +242,17 @@ describe('When creating an instance from a URI', () => { ] // We are sure this is correct because of the described case above - const expectedLightDid = Did.createLightDidDocument({ + const expectedLightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, }) - const uriWithFragment: DidUri = `${expectedLightDid.uri}#authentication` + const uriWithFragment: DidUri = `${expectedLightDid.id}#authentication` - expect(() => Did.parseDocumentFromLightDid(uriWithFragment, true)).toThrow() + expect(() => parseDocumentFromLightDid(uriWithFragment, true)).toThrow() expect(() => - Did.parseDocumentFromLightDid(uriWithFragment, false) + parseDocumentFromLightDid(uriWithFragment, false) ).not.toThrow() }) @@ -244,7 +272,7 @@ describe('When creating an instance from a URI', () => { `did:kilt:light:00${validKiltAddress}:randomdetails`, ] incorrectURIs.forEach((uri) => { - expect(() => Did.parseDocumentFromLightDid(uri as DidUri)).toThrow() + expect(() => parseDocumentFromLightDid(uri as DidUri)).toThrow() }) }) }) diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 30a752f6bd..049c504d94 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -5,32 +5,53 @@ * found in the LICENSE file in the root directory of this source tree. */ +import type { DidDocument, DidUri } from '@kiltprotocol/types' + import { base58Decode, base58Encode, decodeAddress, } from '@polkadot/util-crypto' +import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' import type { - DidDocument, - DidServiceEndpoint, - DidUri, - LightDidSupportedVerificationKeyType, NewDidEncryptionKey, - NewLightDidVerificationKey, -} from '@kiltprotocol/types' -import { encryptionKeyTypes } from '@kiltprotocol/types' + NewDidVerificationKey, + NewService, + DidVerificationKeyType, +} from './DidDetails.js' -import { SDKErrors, ss58Format, cbor } from '@kiltprotocol/utils' +import { + keypairToMultibaseKey, + didKeyToVerificationMethod, + getAddressFromVerificationMethod, + parse, +} from '../Did.utils.js' +import { fragmentIdToChain, validateNewService } from '../Did.chain.js' +import { + addKeypairAsVerificationMethod, + encryptionKeyTypes, +} from './DidDetails.js' -import { getAddressByKey, parse } from '../Did.utils.js' -import { resourceIdToChain, validateService } from '../Did.chain.js' +/** + * Currently, a light DID does not support the use of an ECDSA key as its authentication key. + */ +export type LightDidSupportedVerificationKeyType = Extract< + DidVerificationKeyType, + 'ed25519' | 'sr25519' +> +/** + * A new public key specified when creating a new light DID. + */ +export type NewLightDidVerificationKey = NewDidVerificationKey & { + type: LightDidSupportedVerificationKeyType +} + +type LightDidEncoding = '00' | '01' const authenticationKeyId = '#authentication' const encryptionKeyId = '#encryption' -type LightDidEncoding = '00' | '01' - const verificationKeyTypeToLightDidEncoding: Record< LightDidSupportedVerificationKeyType, LightDidEncoding @@ -52,12 +73,12 @@ const lightDidEncodingToVerificationKeyType: Record< */ export type CreateDocumentInput = { /** - * The DID authentication key. This is mandatory and will be used as the first authentication key + * The key to be used as the DID authentication verification method. This is mandatory and will be used as the first authentication verification method * of the full DID upon migration. */ - authentication: [NewLightDidVerificationKey] + authentication: [NewDidVerificationKey] /** - * The optional DID encryption key. If present, it will be used as the first key agreement key + * The optional encryption key to be used as the DID key agreement verification method. If present, it will be used as the first key agreement verification method * of the full DID upon migration. */ keyAgreement?: [NewDidEncryptionKey] @@ -65,22 +86,21 @@ export type CreateDocumentInput = { * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ - service?: DidServiceEndpoint[] + service?: NewService[] } function validateCreateDocumentInput({ authentication, keyAgreement, - service: services, + service, }: CreateDocumentInput): void { // Check authentication key type const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[authentication[0].type] - if (!authenticationKeyTypeEncoding) { + if (authenticationKeyTypeEncoding === undefined) { throw new SDKErrors.UnsupportedKeyError(authentication[0].type) } - if ( keyAgreement?.[0].type && !encryptionKeyTypes.includes(keyAgreement[0].type) @@ -93,14 +113,14 @@ function validateCreateDocumentInput({ // Checks that for all service IDs have regular strings as their ID and not a full DID. // Plus, we forbid a service ID to be `authentication` or `encryption` as that would create confusion // when upgrading to a full DID. - services?.forEach((service) => { + service?.forEach((s) => { // A service ID cannot have a reserved ID that is used for key IDs. - if (service.id === '#authentication' || service.id === '#encryption') { + if (s.id === '#authentication' || s.id === '#encryption') { throw new SDKErrors.DidError( - `Cannot specify a service ID with the name "${service.id}" as it is a reserved keyword` + `Cannot specify a service ID with the name "${s.id}" as it is a reserved keyword` ) } - validateService(service) + validateNewService(s) }) } @@ -110,20 +130,20 @@ const SERVICES_MAP_KEY = 's' interface SerializableStructure { [KEY_AGREEMENT_MAP_KEY]?: NewDidEncryptionKey [SERVICES_MAP_KEY]?: Array< - Partial> & { + Partial> & { id: string } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their service endpoints. > } /** - * Serialize the optional encryption key and service endpoints of an off-chain DID using the CBOR serialization algorithm + * Serialize the optional key agreement key and service endpoints of a light DID using the CBOR serialization algorithm * and encoding the result in Base58 format with a multibase prefix. * * @param details The light DID details to encode. - * @param details.keyAgreement The DID encryption key. + * @param details.keyAgreement The DID key agreement key. * @param details.service The DID service endpoints. - * @returns The Base58-encoded and CBOR-serialized off-chain DID optional details. + * @returns The Base58-encoded and CBOR-serialized light DID optional details. */ function serializeAdditionalLightDidDetails({ keyAgreement, @@ -136,7 +156,7 @@ function serializeAdditionalLightDidDetails({ } if (service && service.length > 0) { objectToSerialize[SERVICES_MAP_KEY] = service.map(({ id, ...rest }) => ({ - id: resourceIdToChain(id), + id: fragmentIdToChain(id), ...rest, })) } @@ -188,8 +208,8 @@ function deserializeAdditionalLightDidDetails( * Private keys are assumed to already live in another storage, as it contains reference only to public keys. * * @param input The input. - * @param input.authentication The array containing light DID authentication key. - * @param input.keyAgreement The optional array containing light DID encryption key. + * @param input.authentication The array containing the public keys to be used as the light DID authentication verification method. + * @param input.keyAgreement The optional array containing the public keys to be used as the light DID key agreement verification methods. * @param input.service The optional light DID service endpoints. * * @returns The resulting [[DidDocument]]. @@ -211,32 +231,33 @@ export function createLightDidDocument({ // Validity is checked in validateCreateDocumentInput const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[authentication[0].type] - const address = getAddressByKey(authentication[0]) + const address = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authentication[0]), + }) const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' const uri = `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as DidUri const did: DidDocument = { - uri, - authentication: [ - { - id: authenticationKeyId, // Authentication key always has the #authentication ID. - type: authentication[0].type, + id: uri, + authentication: [authenticationKeyId], + verificationMethod: [ + didKeyToVerificationMethod(uri, authenticationKeyId, { + keyType: authentication[0].type, publicKey: authentication[0].publicKey, - }, + }), ], service, } if (keyAgreement !== undefined) { - did.keyAgreement = [ - { - id: encryptionKeyId, // Encryption key always has the #encryption ID. - type: keyAgreement[0].type, - publicKey: keyAgreement[0].publicKey, - }, - ] + const { publicKey, type } = keyAgreement[0] + addKeypairAsVerificationMethod( + did, + { id: encryptionKeyId, publicKey, type }, + 'keyAgreement' + ) } return did @@ -273,7 +294,7 @@ export function parseDocumentFromLightDid( `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` ) } - if (fragment && failIfFragmentPresent) { + if (fragment !== undefined && failIfFragmentPresent) { throw new SDKErrors.DidError( `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` ) diff --git a/packages/did/src/DidDetails/index.ts b/packages/did/src/DidDetails/index.ts index 99c4484aa9..25cd6e329c 100644 --- a/packages/did/src/DidDetails/index.ts +++ b/packages/did/src/DidDetails/index.ts @@ -5,6 +5,16 @@ * found in the LICENSE file in the root directory of this source tree. */ -export * from './DidDetails.js' +// We don't export the `add*VerificationMethod` functions, they are meant to be used internally +export { + BaseNewDidKey, + DidEncryptionKeyType, + DidKeyType, + DidVerificationKeyType, + NewDidEncryptionKey, + NewDidVerificationKey, + NewService, + NewVerificationMethod, +} from './DidDetails.js' export * from './LightDidDetails.js' export * from './FullDidDetails.js' diff --git a/packages/did/src/DidDetailsv2/DidDetailsV2.ts b/packages/did/src/DidDetailsv2/DidDetailsV2.ts deleted file mode 100644 index ab55cc94ff..0000000000 --- a/packages/did/src/DidDetailsv2/DidDetailsV2.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { DidDocumentV2 } from '@kiltprotocol/types' - -import { didKeyToVerificationMethod } from '../Did2.utils.js' - -/** - * Possible types for a DID verification key. - */ -const verificationKeyTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const -export const verificationKeyTypes = verificationKeyTypesC as unknown as string[] -export type DidVerificationKeyType = typeof verificationKeyTypesC[number] -// `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 - -/** - * Possible types for a DID encryption key. - */ -const encryptionKeyTypesC = ['x25519'] as const -export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] -export type DidEncryptionKeyType = typeof encryptionKeyTypesC[number] - -export type DidKeyType = DidVerificationKeyType | DidEncryptionKeyType - -export type NewVerificationMethod = Omit< - DidDocumentV2.VerificationMethod, - 'controller' -> -export type NewService = DidDocumentV2.Service - -/** - * Type of a new key material to add under a DID. - */ -export type BaseNewDidKey = { - publicKey: Uint8Array - type: string -} - -/** - * Type of a new verification key to add under a DID. - */ -export type NewDidVerificationKey = BaseNewDidKey & { - type: DidVerificationKeyType -} - -/** - * Type of a new encryption key to add under a DID. - */ -export type NewDidEncryptionKey = BaseNewDidKey & { - type: DidEncryptionKeyType -} - -function doesVerificationMethodExist( - didDocument: DidDocumentV2.DidDocument, - { id }: Pick -): boolean { - return ( - didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined - ) -} - -function addVerificationMethod( - didDocument: DidDocumentV2.DidDocument, - verificationMethod: DidDocumentV2.VerificationMethod, - relationship: DidDocumentV2.VerificationMethodRelationship -): void { - const existingRelationship = didDocument[relationship] ?? [] - existingRelationship.push(verificationMethod.id) - // eslint-disable-next-line no-param-reassign - didDocument[relationship] = existingRelationship - if (!doesVerificationMethodExist(didDocument, verificationMethod)) { - const existingVerificationMethod = didDocument.verificationMethod ?? [] - existingVerificationMethod.push(verificationMethod) - // eslint-disable-next-line no-param-reassign - didDocument.verificationMethod = existingVerificationMethod - } -} - -/** - * Add the provided keypair as a new verification method to the DID Document. - * !!! This function is meant to be used internally and not exposed since it is mostly used as a utility and does not perform extensive checks on the inputs. - * - * @param didDocument The DID Document to add the verification method to. - * @param newKeypair The new keypair to add as a verification method. - * @param newKeypair.id The ID of the new verification method. If a verification method with the same ID already exists, this operation is a no-op. - * @param newKeypair.publicKey The public key of the keypair. - * @param newKeypair.type The type of the public key. - * @param relationship The verification relationship to add the verification method to. - */ -export function addKeypairAsVerificationMethod( - didDocument: DidDocumentV2.DidDocument, - { - id, - publicKey, - type: keyType, - }: BaseNewDidKey & { id: DidDocumentV2.UriFragment }, - relationship: DidDocumentV2.VerificationMethodRelationship -): void { - const verificationMethod = didKeyToVerificationMethod(didDocument.id, id, { - keyType: keyType as DidKeyType, - publicKey, - }) - addVerificationMethod(didDocument, verificationMethod, relationship) -} diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts deleted file mode 100644 index 3443cd8b90..0000000000 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.spec.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { - CryptoCallbacksV2, - DidDocumentV2, - KiltKeyringPair, - SubmittableExtrinsic, -} from '@kiltprotocol/types' - -import { BN } from '@polkadot/util' -import { randomAsHex } from '@polkadot/util-crypto' -import { ConfigService } from '@kiltprotocol/config' - -import { ApiMocks, TestUtilsV2 } from '../../../../tests/testUtils' -import { generateDidAuthenticatedTx } from '../Did2.chain.js' -import { - authorizeBatch, - getVerificationMethodRelationshipForTx, -} from './FullDidDetailsV2' - -const augmentedApi = ApiMocks.createAugmentedApi() -const mockedApi: any = ApiMocks.getMockedApi() -ConfigService.set({ api: mockedApi }) - -jest.mock('../Did2.chain') -jest - .mocked(generateDidAuthenticatedTx) - .mockResolvedValue({} as SubmittableExtrinsic) - -/* - * Functions tested in integration tests: - * - getKeysForExtrinsic - * - authorizeExtrinsic - */ - -describe('When creating an instance from the chain', () => { - describe('authorizeBatch', () => { - let keypair: KiltKeyringPair - let sign: CryptoCallbacksV2.SignCallback - let fullDid: DidDocumentV2.DidDocument - - beforeAll(async () => { - const keyTool = TestUtilsV2.makeSigningKeyTool() - keypair = keyTool.keypair - fullDid = await TestUtilsV2.createLocalDemoFullDidFromKeypair( - keyTool.keypair - ) - sign = keyTool.getSignCallback(fullDid) - }) - - describe('.addSingleTx()', () => { - it('fails if the extrinsic does not require a DID', async () => { - const extrinsic = augmentedApi.tx.indices.claim(1) - await expect(async () => - authorizeBatch({ - did: fullDid.id, - batchFunction: augmentedApi.tx.utility.batchAll, - extrinsics: [extrinsic, extrinsic], - sign, - submitter: keypair.address, - }) - ).rejects.toMatchInlineSnapshot( - '[DidBatchError: Can only batch extrinsics that require a DID signature]' - ) - }) - - it('fails if the extrinsic is a utility (batch) extrinsic containing valid extrinsics', async () => { - const extrinsic = augmentedApi.tx.utility.batch([ - await augmentedApi.tx.ctype.add('test-ctype'), - ]) - const batchFunction = - jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll - await authorizeBatch({ - did: fullDid.id, - batchFunction, - extrinsics: [extrinsic, extrinsic], - sign, - submitter: keypair.address, - }) - - expect(batchFunction).toHaveBeenCalledWith([extrinsic, extrinsic]) - }) - - it('adds different batches requiring different keys', async () => { - const ctype1Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - const ctype2Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - const delegationExtrinsic1 = - await augmentedApi.tx.delegation.createHierarchy( - randomAsHex(32), - randomAsHex(32) - ) - const delegationExtrinsic2 = - await augmentedApi.tx.delegation.createHierarchy( - randomAsHex(32), - randomAsHex(32) - ) - const ctype3Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - const ctype4Extrinsic = await augmentedApi.tx.ctype.add(randomAsHex(32)) - - const batchFunction = - jest.fn() as unknown as typeof augmentedApi.tx.utility.batchAll - const extrinsics = [ - ctype1Extrinsic, - ctype2Extrinsic, - delegationExtrinsic1, - delegationExtrinsic2, - ctype3Extrinsic, - ctype4Extrinsic, - ] - await authorizeBatch({ - did: fullDid.id, - batchFunction, - extrinsics, - nonce: new BN(0), - sign, - submitter: keypair.address, - }) - - expect(batchFunction).toHaveBeenCalledWith([ - ctype1Extrinsic, - ctype2Extrinsic, - ]) - expect(batchFunction).toHaveBeenCalledWith([ - delegationExtrinsic1, - delegationExtrinsic2, - ]) - expect(batchFunction).toHaveBeenCalledWith([ - ctype3Extrinsic, - ctype4Extrinsic, - ]) - }) - }) - - // TODO: complete these tests once SDK has been refactored to work with generic api object - describe('.build()', () => { - it('throws if batch is empty', async () => { - await expect(async () => - authorizeBatch({ - did: fullDid.id, - batchFunction: augmentedApi.tx.utility.batchAll, - extrinsics: [], - sign, - submitter: keypair.address, - }) - ).rejects.toMatchInlineSnapshot( - '[DidBatchError: Cannot build a batch with no transactions]' - ) - }) - it.todo('successfully create a batch with only 1 extrinsic') - it.todo('successfully create a batch with 1 extrinsic per required key') - it.todo('successfully create a batch with 2 extrinsics per required key') - it.todo( - 'successfully create a batch with 1 extrinsic per required key, repeated two times' - ) - }) - }) -}) - -const mockApi = ApiMocks.createAugmentedApi() - -describe('When creating an instance from the chain', () => { - it('Should return correct KeyRelationship for single valid call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( - mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) - ) - expect(keyRelationship).toBe('assertionMethod') - }) - it('Should return correct KeyRelationship for batched call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( - mockApi.tx.utility.batch([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - ]) - ) - expect(keyRelationship).toBe('assertionMethod') - }) - it('Should return correct KeyRelationship for batchAll call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( - mockApi.tx.utility.batchAll([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - ]) - ) - expect(keyRelationship).toBe('assertionMethod') - }) - it('Should return correct KeyRelationship for forceBatch call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( - mockApi.tx.utility.forceBatch([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - ]) - ) - expect(keyRelationship).toBe('assertionMethod') - }) - it('Should return undefined for batch with mixed KeyRelationship calls', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( - mockApi.tx.utility.forceBatch([ - mockApi.tx.attestation.add( - new Uint8Array(32), - new Uint8Array(32), - null - ), - mockApi.tx.web3Names.claim('awesomename'), - ]) - ) - expect(keyRelationship).toBeUndefined() - }) -}) diff --git a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts b/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts deleted file mode 100644 index 1f17ec5ca7..0000000000 --- a/packages/did/src/DidDetailsv2/FullDidDetailsV2.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { Extrinsic } from '@polkadot/types/interfaces' -import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' -import type { - CryptoCallbacksV2, - DidDocumentV2, - KiltAddress, - SubmittableExtrinsic, -} from '@kiltprotocol/types' - -import { BN } from '@polkadot/util' - -import { ConfigService } from '@kiltprotocol/config' -import { SDKErrors } from '@kiltprotocol/utils' - -import { parse } from '../Did2.utils.js' -import { - documentFromChain, - generateDidAuthenticatedTx, - toChain, -} from '../Did2.chain.js' - -// Must be in sync with what's implemented in impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for Call -// in https://github.com/KILTprotocol/mashnet-node/blob/develop/runtimes/spiritnet/src/lib.rs -// TODO: Should have an RPC or something similar to avoid inconsistencies in the future. -const methodMapping: Record< - string, - DidDocumentV2.SignatureVerificationMethodRelationship | undefined -> = { - attestation: 'assertionMethod', - ctype: 'assertionMethod', - delegation: 'capabilityDelegation', - did: 'authentication', - 'did.create': undefined, - 'did.reclaimDeposit': undefined, - 'did.submitDidCall': undefined, - didLookup: 'authentication', - publicCredentials: 'assertionMethod', - web3Names: 'authentication', -} - -function getVerificationMethodRelationshipForRuntimeCall( - call: Extrinsic['method'] -): DidDocumentV2.SignatureVerificationMethodRelationship | undefined { - const { section, method } = call - - // get the VerificationKeyRelationship of a batched call - if ( - section === 'utility' && - ['batch', 'batchAll', 'forceBatch'].includes(method) && - call.args[0].toRawType() === 'Vec' - ) { - // map all calls to their VerificationKeyRelationship and deduplicate the items - return (call.args[0] as unknown as Array) - .map(getVerificationMethodRelationshipForRuntimeCall) - .reduce((prev, value) => (prev === value ? prev : undefined)) - } - - const signature = `${section}.${method}` - if (signature in methodMapping) { - return methodMapping[signature] - } - - return methodMapping[section] -} - -/** - * Detect the verification relationship for a verification method which should be used to DID-authorize the provided extrinsic. - * - * @param extrinsic The unsigned extrinsic to inspect. - * @returns The verification relationship. - */ -export function getVerificationMethodRelationshipForTx( - extrinsic: Extrinsic -): DidDocumentV2.SignatureVerificationMethodRelationship | undefined { - return getVerificationMethodRelationshipForRuntimeCall(extrinsic.method) -} - -// Max nonce value is (2^64) - 1 -const maxNonceValue = new BN(2).pow(new BN(64)).subn(1) - -function increaseNonce(currentNonce: BN, increment = 1): BN { - // Wrap around the max u64 value when reached. - // FIXME: can we do better than this? Maybe we could expose an RPC function for this, to keep it consistent over time. - return currentNonce.eq(maxNonceValue) - ? new BN(increment) - : currentNonce.addn(increment) -} - -/** - * Returns the next nonce to use to sign a DID operation. - * Normally, this function should not be called directly by SDK users. Nevertheless, in advanced cases where there might be race conditions, this function can be used as the basis on which to build parallel operation queues. - * - * @param did The DID data. - * @returns The next valid nonce, i.e., the nonce currently stored on the blockchain + 1, wrapping around the max value when reached. - */ -async function getNextNonce(did: DidDocumentV2.DidUri): Promise { - const api = ConfigService.get('api') - const queried = await api.query.did.did(toChain(did)) - const currentNonce = queried.isSome - ? documentFromChain(queried).lastTxCounter - : new BN(0) - return increaseNonce(currentNonce) -} - -/** - * Signs and returns the provided unsigned extrinsic with the right DID verification method, if present. Otherwise, it will throw an error. - * - * @param did The DID data. - * @param extrinsic The unsigned extrinsic to sign. - * @param sign The callback to sign the operation. - * @param submitterAccount The KILT account to bind the DID operation to (to avoid MitM and replay attacks). - * @param signingOptions The signing options. - * @param signingOptions.txCounter The optional DID nonce to include in the operation signatures. By default, it uses the next value of the nonce stored on chain. - * @returns The DID-signed submittable extrinsic. - */ -export async function authorizeTx( - did: DidDocumentV2.DidUri, - extrinsic: Extrinsic, - sign: CryptoCallbacksV2.SignExtrinsicCallback, - submitterAccount: KiltAddress, - { - txCounter, - }: { - txCounter?: BN - } = {} -): Promise { - if (parse(did).type === 'light') { - throw new SDKErrors.DidError( - `An extrinsic can only be authorized with a full DID, not with "${did}"` - ) - } - - const verificationMethodRelationship = - getVerificationMethodRelationshipForTx(extrinsic) - if (verificationMethodRelationship === undefined) { - throw new SDKErrors.SDKError('No key relationship found for extrinsic') - } - - return generateDidAuthenticatedTx({ - did, - verificationMethodRelationship, - sign, - call: extrinsic, - txCounter: txCounter || (await getNextNonce(did)), - submitter: submitterAccount, - }) -} - -type GroupedExtrinsics = Array<{ - extrinsics: Extrinsic[] - verificationMethodRelationship: DidDocumentV2.SignatureVerificationMethodRelationship -}> - -function groupExtrinsicsByKeyRelationship( - extrinsics: Extrinsic[] -): GroupedExtrinsics { - const [first, ...rest] = extrinsics.map((extrinsic) => { - const verificationMethodRelationship = - getVerificationMethodRelationshipForTx(extrinsic) - if (verificationMethodRelationship === undefined) { - throw new SDKErrors.DidBatchError( - 'Can only batch extrinsics that require a DID signature' - ) - } - return { extrinsic, verificationMethodRelationship } - }) - - const groups: GroupedExtrinsics = [ - { - extrinsics: [first.extrinsic], - verificationMethodRelationship: first.verificationMethodRelationship, - }, - ] - - rest.forEach(({ extrinsic, verificationMethodRelationship }) => { - const currentGroup = groups[groups.length - 1] - const useCurrentGroup = - verificationMethodRelationship === - currentGroup.verificationMethodRelationship - if (useCurrentGroup) { - currentGroup.extrinsics.push(extrinsic) - } else { - groups.push({ - extrinsics: [extrinsic], - verificationMethodRelationship, - }) - } - }) - - return groups -} - -/** - * Authorizes/signs a list of extrinsics grouping them in batches by required verification relationship. - * - * @param input The object with named parameters. - * @param input.batchFunction The batch function to use, for example `api.tx.utility.batchAll`. - * @param input.did The DID document. - * @param input.extrinsics The array of unsigned extrinsics to sign. - * @param input.sign The callback to sign the operation. - * @param input.submitter The KILT account to bind the DID operation to (to avoid MitM and replay attacks). - * @param input.nonce The optional nonce to use for the first batch, next batches will use incremented value. - * @returns The DID-signed submittable extrinsic. - */ -export async function authorizeBatch({ - batchFunction, - did, - extrinsics, - nonce, - sign, - submitter, -}: { - batchFunction: SubmittableExtrinsicFunction<'promise'> - did: DidDocumentV2.DidUri - extrinsics: Extrinsic[] - nonce?: BN - sign: CryptoCallbacksV2.SignExtrinsicCallback - submitter: KiltAddress -}): Promise { - if (extrinsics.length === 0) { - throw new SDKErrors.DidBatchError( - 'Cannot build a batch with no transactions' - ) - } - - if (parse(did).type === 'light') { - throw new SDKErrors.DidError( - `An extrinsic can only be authorized with a full DID, not with "${did}"` - ) - } - - if (extrinsics.length === 1) { - return authorizeTx(did, extrinsics[0], sign, submitter, { - txCounter: nonce, - }) - } - - const groups = groupExtrinsicsByKeyRelationship(extrinsics) - const firstNonce = nonce || (await getNextNonce(did)) - - const promises = groups.map(async (group, batchIndex) => { - const list = group.extrinsics - const call = list.length === 1 ? list[0] : batchFunction(list) - const txCounter = increaseNonce(firstNonce, batchIndex) - - const { verificationMethodRelationship } = group - - return generateDidAuthenticatedTx({ - did, - verificationMethodRelationship, - sign, - call, - txCounter, - submitter, - }) - }) - const batches = await Promise.all(promises) - - return batches.length === 1 ? batches[0] : batchFunction(batches) -} diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts deleted file mode 100644 index cac2acb49f..0000000000 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.spec.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { DidDocumentV2 } from '@kiltprotocol/types' - -import { Crypto } from '@kiltprotocol/utils' - -import type { NewService } from './DidDetailsV2.js' -import type { CreateDocumentInput } from './LightDidDetailsV2.js' - -import { keypairToMultibaseKey, parse } from '../Did2.utils.js' -import { - createLightDidDocument, - parseDocumentFromLightDid, -} from './LightDidDetailsV2.js' - -/* - * Functions tested: - * - createLightDidDocument - * - parseDocumentFromLightDid - * - * Functions tested in integration tests: - * - getKeysForExtrinsic - * - authorizeExtrinsic - */ - -describe('When creating an instance from the details', () => { - it('correctly assign the right sr25519 authentication key, x25519 encryption key, and service endpoints', () => { - const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') - const encKey = Crypto.makeEncryptionKeypairFromSeed( - new Uint8Array(32).fill(1) - ) - const service: NewService[] = [ - { - id: '#service-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#service-2', - type: ['type-21', 'type-22'], - serviceEndpoint: ['x:url-21', 'x:url-22'], - }, - ] - - const lightDid = createLightDidDocument({ - authentication: [authKey], - keyAgreement: [encKey], - service, - }) - - expect(lightDid).toEqual({ - id: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - authentication: ['#authentication'], - keyAgreement: ['#encryption'], - verificationMethod: [ - { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - id: '#authentication', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: authKey.publicKey, - type: 'sr25519', - }), - type: 'MultiKey', - }, - { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - id: '#encryption', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: encKey.publicKey, - type: 'x25519', - }), - type: 'MultiKey', - }, - ], - service: [ - { - id: '#service-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#service-2', - type: ['type-21', 'type-22'], - serviceEndpoint: ['x:url-21', 'x:url-22'], - }, - ], - }) - }) - - it('correctly assign the right ed25519 authentication key and encryption key', () => { - const authKey = Crypto.makeKeypairFromSeed() - const encKey = Crypto.makeEncryptionKeypairFromSeed( - new Uint8Array(32).fill(1) - ) - - const lightDid = createLightDidDocument({ - authentication: [authKey], - keyAgreement: [encKey], - }) - - expect(parse(lightDid.id).address).toStrictEqual(authKey.address) - - expect(lightDid).toEqual({ - id: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, - authentication: ['#authentication'], - keyAgreement: ['#encryption'], - verificationMethod: [ - { - controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, - id: '#authentication', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: authKey.publicKey, - type: 'ed25519', - }), - type: 'MultiKey', - }, - { - controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, - id: '#encryption', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: encKey.publicKey, - type: 'x25519', - }), - type: 'MultiKey', - }, - ], - }) - }) - - it('throws for unsupported authentication key type', () => { - const authKey = Crypto.makeKeypairFromSeed(undefined, 'ecdsa') - const invalidInput = { - // Not an authentication key type - authentication: [authKey], - } - expect(() => - createLightDidDocument(invalidInput as unknown as CreateDocumentInput) - ).toThrowError() - }) - - it('throws for unsupported encryption key type', () => { - const authKey = Crypto.makeKeypairFromSeed() - const encKey = Crypto.makeEncryptionKeypairFromSeed() - const invalidInput = { - authentication: [authKey], - // Not an encryption key type - keyAgreement: [{ publicKey: encKey.publicKey, type: 'bls' }], - } - expect(() => - createLightDidDocument(invalidInput as unknown as CreateDocumentInput) - ).toThrowError() - }) -}) - -describe('When creating an instance from a URI', () => { - it('correctly assign the right authentication key, encryption key, and service endpoints', () => { - const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') - const encKey = Crypto.makeEncryptionKeypairFromSeed( - new Uint8Array(32).fill(1) - ) - const endpoints: NewService[] = [ - { - id: '#service-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#service-2', - type: ['type-21', 'type-22'], - serviceEndpoint: ['x:url-21', 'x:url-22'], - }, - ] - // We are sure this is correct because of the described case above - const expectedLightDid = createLightDidDocument({ - authentication: [authKey], - keyAgreement: [encKey], - service: endpoints, - }) - - const { address } = parse(expectedLightDid.id) - const builtLightDid = parseDocumentFromLightDid(expectedLightDid.id) - - expect(builtLightDid).toStrictEqual(expectedLightDid) - expect(builtLightDid).toStrictEqual({ - id: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - authentication: ['#authentication'], - keyAgreement: ['#encryption'], - verificationMethod: [ - { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - id: '#authentication', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: authKey.publicKey, - type: 'sr25519', - }), - type: 'MultiKey', - }, - { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - id: '#encryption', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: encKey.publicKey, - type: 'x25519', - }), - type: 'MultiKey', - }, - ], - service: [ - { - id: '#service-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#service-2', - type: ['type-21', 'type-22'], - serviceEndpoint: ['x:url-21', 'x:url-22'], - }, - ], - }) - }) - - it('fail if a fragment is present according to the options', () => { - const authKey = Crypto.makeKeypairFromSeed() - const encKey = Crypto.makeEncryptionKeypairFromSeed() - const service: NewService[] = [ - { - id: '#service-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#service-2', - type: ['type-21', 'type-22'], - serviceEndpoint: ['x:url-21', 'x:url-22'], - }, - ] - - // We are sure this is correct because of the described case above - const expectedLightDid = createLightDidDocument({ - authentication: [authKey], - keyAgreement: [encKey], - service, - }) - - const uriWithFragment: DidDocumentV2.DidUri = `${expectedLightDid.id}#authentication` - - expect(() => parseDocumentFromLightDid(uriWithFragment, true)).toThrow() - expect(() => - parseDocumentFromLightDid(uriWithFragment, false) - ).not.toThrow() - }) - - it('fail if the URI is not correct', () => { - const validKiltAddress = Crypto.makeKeypairFromSeed() - const incorrectURIs = [ - 'did:kilt:light:sdasdsadas', - // @ts-ignore not a valid DID uri - 'random-uri', - 'did:kilt:light', - 'did:kilt:light:', - // Wrong auth key encoding - `did:kilt:light:11${validKiltAddress}`, - // Full DID - `did:kilt:${validKiltAddress}`, - // Random encoded details - `did:kilt:light:00${validKiltAddress}:randomdetails`, - ] - incorrectURIs.forEach((uri) => { - expect(() => - parseDocumentFromLightDid(uri as DidDocumentV2.DidUri) - ).toThrow() - }) - }) -}) diff --git a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts b/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts deleted file mode 100644 index 9cedafe184..0000000000 --- a/packages/did/src/DidDetailsv2/LightDidDetailsV2.ts +++ /dev/null @@ -1,327 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { DidDocumentV2 } from '@kiltprotocol/types' - -import { - base58Decode, - base58Encode, - decodeAddress, -} from '@polkadot/util-crypto' -import { cbor, SDKErrors, ss58Format } from '@kiltprotocol/utils' - -import type { - NewDidEncryptionKey, - NewDidVerificationKey, - NewService, - DidVerificationKeyType, -} from './DidDetailsV2.js' - -import { - keypairToMultibaseKey, - didKeyToVerificationMethod, - getAddressFromVerificationMethod, - parse, -} from '../Did2.utils.js' -import { fragmentIdToChain, validateNewService } from '../Did2.chain.js' -import { - addKeypairAsVerificationMethod, - encryptionKeyTypes, -} from './DidDetailsV2.js' - -/** - * Currently, a light DID does not support the use of an ECDSA key as its authentication key. - */ -export type LightDidSupportedVerificationKeyType = Extract< - DidVerificationKeyType, - 'ed25519' | 'sr25519' -> -/** - * A new public key specified when creating a new light DID. - */ -export type NewLightDidVerificationKey = NewDidVerificationKey & { - type: LightDidSupportedVerificationKeyType -} - -type LightDidEncoding = '00' | '01' - -const authenticationKeyId = '#authentication' -const encryptionKeyId = '#encryption' - -const verificationKeyTypeToLightDidEncoding: Record< - LightDidSupportedVerificationKeyType, - LightDidEncoding -> = { - sr25519: '00', - ed25519: '01', -} - -const lightDidEncodingToVerificationKeyType: Record< - LightDidEncoding, - LightDidSupportedVerificationKeyType -> = { - '00': 'sr25519', - '01': 'ed25519', -} - -/** - * The options that can be used to create a light DID. - */ -export type CreateDocumentInput = { - /** - * The key to be used as the DID authentication verification method. This is mandatory and will be used as the first authentication verification method - * of the full DID upon migration. - */ - authentication: [NewDidVerificationKey] - /** - * The optional encryption key to be used as the DID key agreement verification method. If present, it will be used as the first key agreement verification method - * of the full DID upon migration. - */ - keyAgreement?: [NewDidEncryptionKey] - /** - * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. - * The service ID must not contain the DID prefix when used to create a new DID. - */ - service?: NewService[] -} - -function validateCreateDocumentInput({ - authentication, - keyAgreement, - service, -}: CreateDocumentInput): void { - // Check authentication key type - const authenticationKeyTypeEncoding = - verificationKeyTypeToLightDidEncoding[authentication[0].type] - - if (authenticationKeyTypeEncoding === undefined) { - throw new SDKErrors.UnsupportedKeyError(authentication[0].type) - } - if ( - keyAgreement?.[0].type && - !encryptionKeyTypes.includes(keyAgreement[0].type) - ) { - throw new SDKErrors.DidError( - `Encryption key type "${keyAgreement[0].type}" is not supported` - ) - } - - // Checks that for all service IDs have regular strings as their ID and not a full DID. - // Plus, we forbid a service ID to be `authentication` or `encryption` as that would create confusion - // when upgrading to a full DID. - service?.forEach((s) => { - // A service ID cannot have a reserved ID that is used for key IDs. - if (s.id === '#authentication' || s.id === '#encryption') { - throw new SDKErrors.DidError( - `Cannot specify a service ID with the name "${s.id}" as it is a reserved keyword` - ) - } - validateNewService(s) - }) -} - -const KEY_AGREEMENT_MAP_KEY = 'e' -const SERVICES_MAP_KEY = 's' - -interface SerializableStructure { - [KEY_AGREEMENT_MAP_KEY]?: NewDidEncryptionKey - [SERVICES_MAP_KEY]?: Array< - Partial> & { - id: string - } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their service endpoints. - > -} - -/** - * Serialize the optional key agreement key and service endpoints of a light DID using the CBOR serialization algorithm - * and encoding the result in Base58 format with a multibase prefix. - * - * @param details The light DID details to encode. - * @param details.keyAgreement The DID key agreement key. - * @param details.service The DID service endpoints. - * @returns The Base58-encoded and CBOR-serialized light DID optional details. - */ -function serializeAdditionalLightDidDetails({ - keyAgreement, - service, -}: Pick): string | undefined { - const objectToSerialize: SerializableStructure = {} - if (keyAgreement) { - const key = keyAgreement[0] - objectToSerialize[KEY_AGREEMENT_MAP_KEY] = key - } - if (service && service.length > 0) { - objectToSerialize[SERVICES_MAP_KEY] = service.map(({ id, ...rest }) => ({ - id: fragmentIdToChain(id), - ...rest, - })) - } - - if (Object.keys(objectToSerialize).length === 0) { - return undefined - } - - const serializationVersion = 0x0 - const serialized = cbor.encode(objectToSerialize) - return base58Encode([serializationVersion, ...serialized], true) -} - -function deserializeAdditionalLightDidDetails( - rawInput: string, - version = 1 -): Pick { - if (version !== 1) { - throw new SDKErrors.DidError('Serialization version not supported') - } - - const decoded = base58Decode(rawInput, true) - const serializationVersion = decoded[0] - const serialized = decoded.slice(1) - - if (serializationVersion !== 0x0) { - throw new SDKErrors.DidError('Serialization algorithm not supported') - } - const deserialized: SerializableStructure = cbor.decode(serialized) - - const keyAgreement = deserialized[KEY_AGREEMENT_MAP_KEY] - return { - keyAgreement: keyAgreement && [keyAgreement], - service: deserialized[SERVICES_MAP_KEY]?.map( - ({ id, type, serviceEndpoint, types, urls }) => ({ - id: `#${id}`, - // types for retro-compatibility - type: (type ?? types) as string[], - // urls for retro-compatibility - serviceEndpoint: (serviceEndpoint ?? urls) as string[], - }) - ), - } -} - -/** - * Create [[DidDocument]] of a light DID using the provided keys and endpoints. - * Sets proper key IDs, builds light DID URI. - * Private keys are assumed to already live in another storage, as it contains reference only to public keys. - * - * @param input The input. - * @param input.authentication The array containing the public keys to be used as the light DID authentication verification method. - * @param input.keyAgreement The optional array containing the public keys to be used as the light DID key agreement verification methods. - * @param input.service The optional light DID service endpoints. - * - * @returns The resulting [[DidDocument]]. - */ -export function createLightDidDocument({ - authentication, - keyAgreement = undefined, - service, -}: CreateDocumentInput): DidDocumentV2.DidDocument { - validateCreateDocumentInput({ - authentication, - keyAgreement, - service, - }) - const encodedDetails = serializeAdditionalLightDidDetails({ - keyAgreement, - service, - }) - // Validity is checked in validateCreateDocumentInput - const authenticationKeyTypeEncoding = - verificationKeyTypeToLightDidEncoding[authentication[0].type] - const address = getAddressFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(authentication[0]), - }) - - const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' - const uri = - `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as DidDocumentV2.DidUri - - const did: DidDocumentV2.DidDocument = { - id: uri, - authentication: [authenticationKeyId], - verificationMethod: [ - didKeyToVerificationMethod(uri, authenticationKeyId, { - keyType: authentication[0].type, - publicKey: authentication[0].publicKey, - }), - ], - service, - } - - if (keyAgreement !== undefined) { - const { publicKey, type } = keyAgreement[0] - addKeypairAsVerificationMethod( - did, - { id: encryptionKeyId, publicKey, type }, - 'keyAgreement' - ) - } - - return did -} - -/** - * Create [[DidDocument]] of a light DID by parsing the provided input URI. - * Only use for DIDs you control, when you are certain they have not been upgraded to on-chain full DIDs. - * For the DIDs you have received from external sources use [[resolve]] etc. - * - * Parsing is possible because of the self-describing and self-containing nature of light DIDs. - * Private keys are assumed to already live in another storage, as it contains reference only to public keys. - * - * @param uri The DID URI to parse. - * @param failIfFragmentPresent Whether to fail when parsing the URI in case a fragment is present or not, which is not relevant to the creation of the DID. It defaults to true. - * - * @returns The resulting [[DidDocument]]. - */ -export function parseDocumentFromLightDid( - uri: DidDocumentV2.DidUri, - failIfFragmentPresent = true -): DidDocumentV2.DidDocument { - const { - address, - version, - encodedDetails, - fragment, - type, - authKeyTypeEncoding, - } = parse(uri) - - if (type !== 'light') { - throw new SDKErrors.DidError( - `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` - ) - } - if (fragment !== undefined && failIfFragmentPresent) { - throw new SDKErrors.DidError( - `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` - ) - } - const keyType = - authKeyTypeEncoding && - lightDidEncodingToVerificationKeyType[authKeyTypeEncoding] - - if (keyType === undefined) { - throw new SDKErrors.DidError( - `Authentication key encoding "${authKeyTypeEncoding}" does not match any supported key type` - ) - } - const publicKey = decodeAddress(address, false, ss58Format) - const authentication: [NewLightDidVerificationKey] = [ - { publicKey, type: keyType }, - ] - if (!encodedDetails) { - return createLightDidDocument({ authentication }) - } - const { keyAgreement, service } = deserializeAdditionalLightDidDetails( - encodedDetails, - version - ) - return createLightDidDocument({ - authentication, - keyAgreement, - service, - }) -} diff --git a/packages/did/src/DidDetailsv2/index.ts b/packages/did/src/DidDetailsv2/index.ts deleted file mode 100644 index ce1499364b..0000000000 --- a/packages/did/src/DidDetailsv2/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -// We don't export the `add*VerificationMethod` functions, they are meant to be used internally -export { - BaseNewDidKey, - DidEncryptionKeyType, - DidKeyType, - DidVerificationKeyType, - NewDidEncryptionKey, - NewDidVerificationKey, - NewService, - NewVerificationMethod, -} from './DidDetailsV2.js' -export * from './LightDidDetailsV2.js' -export * from './FullDidDetailsV2.js' diff --git a/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts b/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts deleted file mode 100644 index 4a957c40d3..0000000000 --- a/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { BN } from '@polkadot/util' - -import type { - DidServiceEndpoint, - NewDidVerificationKey, - DidDocument, - DidVerificationKey, - DidEncryptionKey, - UriFragment, - DidUri, -} from '@kiltprotocol/types' - -import { exportToDidDocument } from './DidDocumentExporter.js' -import * as Did from '../index.js' -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from '../index.js' - -const did: DidUri = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - -function generateAuthenticationKey(): DidVerificationKey { - return { - id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), - } -} - -function generateEncryptionKey(): DidEncryptionKey { - return { - id: '#enc', - type: 'x25519', - publicKey: new Uint8Array(32).fill(0), - includedAt: new BN(15), - } -} - -function generateAttestationKey(): DidVerificationKey { - return { - id: '#att', - type: 'sr25519', - publicKey: new Uint8Array(32).fill(0), - includedAt: new BN(20), - } -} - -function generateDelegationKey(): DidVerificationKey { - return { - id: '#del', - type: 'ecdsa', - publicKey: new Uint8Array(32).fill(0), - includedAt: new BN(25), - } -} - -function generateServiceEndpoint(serviceId: UriFragment): DidServiceEndpoint { - const fragment = Did.resourceIdToChain(serviceId) - return { - id: serviceId, - type: [`type-${fragment}`], - serviceEndpoint: [`x:url-${fragment}`], - } -} - -const fullDid: DidDocument = { - uri: did, - authentication: [generateAuthenticationKey()], - keyAgreement: [generateEncryptionKey()], - assertionMethod: [generateAttestationKey()], - capabilityDelegation: [generateDelegationKey()], - service: [generateServiceEndpoint('#id-1'), generateServiceEndpoint('#id-2')], -} - -describe('When exporting a DID Document from a full DID', () => { - it('exports the expected application/json W3C DID Document with an Ed25519 authentication key, one x25519 encryption key, an Sr25519 assertion key, an Ecdsa delegation key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(fullDid, 'application/json') - - expect(didDoc).toStrictEqual({ - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - verificationMethod: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Ed25519VerificationKey2018', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Sr25519VerificationKey2020', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'EcdsaSecp256k1VerificationKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'X25519KeyAgreementKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - ], - authentication: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - ], - keyAgreement: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - ], - assertionMethod: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - ], - capabilityDelegation: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - ], - service: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-1', - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-2', - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], - }, - ], - }) - }) - - it('exports the expected application/ld+json W3C DID Document with an Ed25519 authentication key, two x25519 encryption keys, an Sr25519 assertion key, an Ecdsa delegation key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(fullDid, 'application/ld+json') - - expect(didDoc).toStrictEqual({ - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - verificationMethod: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Ed25519VerificationKey2018', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Sr25519VerificationKey2020', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'EcdsaSecp256k1VerificationKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'X25519KeyAgreementKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - ], - authentication: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - ], - keyAgreement: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - ], - assertionMethod: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - ], - capabilityDelegation: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - ], - service: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-1', - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-2', - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], - }, - ], - }) - }) - - it('fails to export to an unsupported mimetype', async () => { - expect(() => - // @ts-ignore - exportToDidDocument(fullDid, 'random-mime-type') - ).toThrow() - }) -}) - -describe('When exporting a DID Document from a light DID', () => { - const authKey = generateAuthenticationKey() as NewDidVerificationKey - const encKey = generateEncryptionKey() - const service = [ - generateServiceEndpoint('#id-1'), - generateServiceEndpoint('#id-2'), - ] - const lightDid = Did.createLightDidDocument({ - authentication: [{ publicKey: authKey.publicKey, type: 'ed25519' }], - keyAgreement: [{ publicKey: encKey.publicKey, type: 'x25519' }], - service, - }) - - it('exports the expected application/json W3C DID Document with an Ed25519 authentication key, one x25519 encryption key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(lightDid, 'application/json') - - expect(didDoc).toMatchInlineSnapshot(` - { - "authentication": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - ], - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "keyAgreement": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - ], - "service": [ - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-1", - "serviceEndpoint": [ - "x:url-id-1", - ], - "type": [ - "type-id-1", - ], - }, - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-2", - "serviceEndpoint": [ - "x:url-id-2", - ], - "type": [ - "type-id-2", - ], - }, - ], - "verificationMethod": [ - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "Ed25519VerificationKey2018", - }, - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "X25519KeyAgreementKey2019", - }, - ], - } - `) - }) - - it('exports the expected application/json+ld W3C DID Document with an Ed25519 authentication key, one x25519 encryption key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(lightDid, 'application/ld+json') - - expect(didDoc).toMatchInlineSnapshot(` - { - "@context": [ - "https://www.w3.org/ns/did/v1", - "ipfs://QmU7QkuTCPz7NmD5bD7Z7mQVz2UsSPaEK58B5sYnjnPRNW", - ], - "authentication": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - ], - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "keyAgreement": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - ], - "service": [ - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-1", - "serviceEndpoint": [ - "x:url-id-1", - ], - "type": [ - "type-id-1", - ], - }, - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-2", - "serviceEndpoint": [ - "x:url-id-2", - ], - "type": [ - "type-id-2", - ], - }, - ], - "verificationMethod": [ - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "Ed25519VerificationKey2018", - }, - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "X25519KeyAgreementKey2019", - }, - ], - } - `) - }) - - it('fails to export to an unsupported mimetype', async () => { - expect(() => - // @ts-ignore - exportToDidDocument(lightDid, 'random-mime-type') - ).toThrow() - }) -}) diff --git a/packages/did/src/DidDocumentExporter/DidDocumentExporter.ts b/packages/did/src/DidDocumentExporter/DidDocumentExporter.ts deleted file mode 100644 index bfaebeb439..0000000000 --- a/packages/did/src/DidDocumentExporter/DidDocumentExporter.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { base58Encode } from '@polkadot/util-crypto' - -import type { - DidDocument, - ConformingDidDocument, - DidResourceUri, - JsonLDDidDocument, - UriFragment, -} from '@kiltprotocol/types' -import { - encryptionKeyTypesMap, - verificationKeyTypesMap, -} from '@kiltprotocol/types' -import { SDKErrors } from '@kiltprotocol/utils' -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' - -function exportToJsonDidDocument(did: DidDocument): ConformingDidDocument { - const { - uri: controller, - authentication, - assertionMethod = [], - capabilityDelegation = [], - keyAgreement = [], - service = [], - } = did - - function toAbsoluteUri(keyId: UriFragment): DidResourceUri { - if (keyId.startsWith(controller)) { - return keyId as DidResourceUri - } - return `${controller}${keyId}` - } - - const verificationMethod: ConformingDidDocument['verificationMethod'] = [ - ...authentication, - ...assertionMethod, - ...capabilityDelegation, - ] - .map((key) => ({ ...key, type: verificationKeyTypesMap[key.type] })) - .concat( - keyAgreement.map((key) => ({ - ...key, - type: encryptionKeyTypesMap[key.type], - })) - ) - .map(({ id, type, publicKey }) => ({ - id: toAbsoluteUri(id), - controller, - type, - publicKeyBase58: base58Encode(publicKey), - })) - .filter( - // remove duplicates - ({ id }, index, array) => - index === array.findIndex((key) => key.id === id) - ) - - return { - id: controller, - verificationMethod, - authentication: [toAbsoluteUri(authentication[0].id)], - ...(assertionMethod[0] && { - assertionMethod: [toAbsoluteUri(assertionMethod[0].id)], - }), - ...(capabilityDelegation[0] && { - capabilityDelegation: [toAbsoluteUri(capabilityDelegation[0].id)], - }), - ...(keyAgreement.length > 0 && { - keyAgreement: [toAbsoluteUri(keyAgreement[0].id)], - }), - ...(service.length > 0 && { - service: service.map((endpoint) => ({ - ...endpoint, - id: `${controller}${endpoint.id}`, - })), - }), - } -} - -function exportToJsonLdDidDocument(did: DidDocument): JsonLDDidDocument { - const document = exportToJsonDidDocument(did) - document['@context'] = [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL] - return document as JsonLDDidDocument -} - -/** - * Export a [[DidDocument]] to a W3C-spec conforming DID Document in the format provided. - * - * @param did The [[DidDocument]]. - * @param mimeType The format for the output DID Document. Accepted values are `application/json` and `application/ld+json`. - * @returns The DID Document formatted according to the mime type provided, or an error if the format specified is not supported. - */ -export function exportToDidDocument( - did: DidDocument, - mimeType: 'application/json' | 'application/ld+json' -): ConformingDidDocument { - switch (mimeType) { - case 'application/json': - return exportToJsonDidDocument(did) - case 'application/ld+json': - return exportToJsonLdDidDocument(did) - default: - throw new SDKErrors.DidExporterError( - `The MIME type "${mimeType}" not supported by any of the available exporters` - ) - } -} diff --git a/packages/did/src/DidDocumentExporter/README.md b/packages/did/src/DidDocumentExporter/README.md deleted file mode 100644 index e9c90965e0..0000000000 --- a/packages/did/src/DidDocumentExporter/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# DID Document exporter - -The DID Document exporter provides the functionality needed to convert an instance of a generic `DidDocument` into a document that is compliant with the [W3C specification](https://www.w3.org/TR/did-core/). This component is required for the KILT plugin for the [DIF Universal Resolver](https://dev.uniresolver.io/). - -For a list of examples and code snippets, please refer to our [official documentation](https://docs.kilt.io/docs/develop/sdk/cookbook/dids/did-export). diff --git a/packages/did/src/DidDocumentExporter/index.ts b/packages/did/src/DidDocumentExporter/index.ts deleted file mode 100644 index 3243aecb8a..0000000000 --- a/packages/did/src/DidDocumentExporter/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -export * from './DidDocumentExporter.js' -export * from './DidContexts.js' diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 882568d3bd..e25f4de136 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -5,30 +5,29 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' import type { TypeDef } from '@polkadot/types/types' import type { KeypairType } from '@polkadot/util-crypto/types' +import type { ApiPromise } from '@polkadot/api' +import type { BN } from '@polkadot/util' +import type { + DidUri, + HexString, + KeyringPair, + KiltAddress, +} from '@kiltprotocol/types' + +import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' import { stringToU8a, U8A_WRAP_ETHEREUM, u8aConcatStrict, u8aToHex, u8aWrapBytes, - BN, } from '@polkadot/util' -import { ApiPromise } from '@polkadot/api' - import { SDKErrors } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' -import type { - DidUri, - HexString, - KeyringPair, - KiltAddress, -} from '@kiltprotocol/types' -import { EncodedSignature } from '../Did.utils.js' -import { toChain } from '../Did.chain.js' +import { toChain, EncodedSignature } from '../Did.chain.js' /** * A chain-agnostic address, which can be encoded using any network prefix. diff --git a/packages/did/src/DidLinks/AccountLinks2.chain.ts b/packages/did/src/DidLinks/AccountLinks2.chain.ts deleted file mode 100644 index 319b284ab4..0000000000 --- a/packages/did/src/DidLinks/AccountLinks2.chain.ts +++ /dev/null @@ -1,291 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { TypeDef } from '@polkadot/types/types' -import type { KeypairType } from '@polkadot/util-crypto/types' -import type { ApiPromise } from '@polkadot/api' -import type { BN } from '@polkadot/util' -import type { - HexString, - DidDocumentV2, - KeyringPair, - KiltAddress, -} from '@kiltprotocol/types' - -import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' -import { - stringToU8a, - U8A_WRAP_ETHEREUM, - u8aConcatStrict, - u8aToHex, - u8aWrapBytes, -} from '@polkadot/util' -import { SDKErrors } from '@kiltprotocol/utils' -import { ConfigService } from '@kiltprotocol/config' - -import { toChain, EncodedSignature } from '../Did2.chain.js' - -/** - * A chain-agnostic address, which can be encoded using any network prefix. - */ -export type SubstrateAddress = KeyringPair['address'] - -export type EthereumAddress = HexString - -export type Address = KiltAddress | SubstrateAddress | EthereumAddress - -type EncodedMultiAddress = - | { AccountId20: Uint8Array } - | { AccountId32: Uint8Array } - -/** - * Detects whether the spec version indicates presence of Ethereum linking enabled pallet. - * - * @param api The api object. - * @returns True if Ethereum linking is supported. - */ -function isEthereumEnabled(api: ApiPromise): boolean { - return api.runtimeVersion.specVersion.gten(11000) -} - -/** - * Prepares encoding a LinkableAccountId. - * - * @param address 20 or 32 byte address as string (hex or ss58 encoded). - * @returns `{ AccountId20 | AccountId32: Uint8Array }`. - */ -function encodeMultiAddress(address: Address): EncodedMultiAddress { - const accountDecoded = decodeAddress(address) - const isEthereumAddress = accountDecoded.length === 20 - return isEthereumAddress - ? { AccountId20: accountDecoded } - : { AccountId32: accountDecoded } -} - -/* ### QUERY ### */ - -/** - * Format a blockchain address to be used as a parameter for the blockchain API functions. - * - * @param account The account to format. - * @returns The blockchain-formatted account. - */ -export function accountToChain(account: Address): Address { - const api = ConfigService.get('api') - if (!isEthereumEnabled(api)) { - // No change for the old blockchain version - return account - } - const encoded: EncodedMultiAddress = encodeMultiAddress(account) - // Force type cast to enable the old blockchain types to accept the future format - return encoded as unknown as Address -} - -/* ### EXTRINSICS ### */ - -type LinkingInfo = [Address, unknown] -type AssociateAccountToChainResult = [ - ( - | { - Polkadot: LinkingInfo - } - | { - Ethereum: LinkingInfo - } - ), - BN -] - -/* ### HELPERS ### */ - -function getUnprefixedSignature( - message: HexString, - signature: Uint8Array, - address: Address -): { signature: Uint8Array; type: KeypairType } { - try { - // try to verify the signature without the prefix first - const unprefixed = signature.subarray(1) - const { crypto, isValid } = signatureVerify(message, unprefixed, address) - if (isValid) { - return { - signature: unprefixed, - type: crypto as KeypairType, - } - } - } catch { - // if it fails, maybe the signature prefix caused that, so we try to verify the whole signature - } - - const { crypto, isValid } = signatureVerify(message, signature, address) - if (isValid) { - return { - signature, - type: crypto as KeypairType, - } - } - - throw new SDKErrors.SignatureUnverifiableError() -} - -async function getLinkingChallengeV1( - did: DidDocumentV2.DidUri, - validUntil: BN -): Promise { - const api = ConfigService.get('api') - - // Gets the current definition of BlockNumber (second tx argument) from the metadata. - const BlockNumber = - api.tx.didLookup.associateAccount.meta.args[1].type.toString() - // This is some magic on the polkadot types internals to get the DidAddress definition from the metadata. - // We get it from the connectedAccounts storage, which is a double map from (DidAddress, Account) -> Null. - const DidAddress = ( - api.registry.lookup.getTypeDef( - // gets the type id of the keys on the connectedAccounts storage (which is a double map). - api.query.didLookup.connectedAccounts.creator.meta.type.asMap.key - ).sub as TypeDef[] - )[0].type // get the type of the first key, which is the DidAddress - - return api - .createType(`(${DidAddress}, ${BlockNumber})`, [toChain(did), validUntil]) - .toU8a() -} - -function getLinkingChallengeV2( - did: DidDocumentV2.DidUri, - validUntil: BN -): Uint8Array { - return stringToU8a( - `Publicly link the signing address to ${did} before block number ${validUntil}` - ) -} - -/** - * Generates the challenge that links a DID to an account. - * The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will - * link the account to the DID. - * - * @param did The URI of the DID that that should be linked to an account. - * @param validUntil Last blocknumber that this challenge is valid for. - * @returns The encoded challenge. - */ -export async function getLinkingChallenge( - did: DidDocumentV2.DidUri, - validUntil: BN -): Promise { - const api = ConfigService.get('api') - if (isEthereumEnabled(api)) { - return getLinkingChallengeV2(did, validUntil) - } - return getLinkingChallengeV1(did, validUntil) -} - -/** - * Generates the arguments for the extrinsic that links an account to a DID. - * - * @param accountAddress Address of the account to be linked. - * @param validUntil Last blocknumber that this challenge is valid for. - * @param signature The signature for the linking challenge. - * @param type The key type used to sign the challenge. - * @returns The arguments for the call that links account and DID. - */ -export async function getLinkingArguments( - accountAddress: Address, - validUntil: BN, - signature: Uint8Array, - type: KeypairType -): Promise { - const api = ConfigService.get('api') - - const proof = { [type]: signature } as EncodedSignature - - if (isEthereumEnabled(api)) { - if (type === 'ethereum') { - return [{ Ethereum: [accountAddress, signature] }, validUntil] - } - return [{ Polkadot: [accountAddress, proof] }, validUntil] - } - - if (type === 'ethereum') - throw new SDKErrors.CodecMismatchError( - 'Ethereum linking is not yet supported by this chain' - ) - // Force type cast to enable the new blockchain types to accept the historic format - return [ - accountAddress, - validUntil, - proof, - ] as unknown as AssociateAccountToChainResult -} - -/** - * Identifies the strategy to wrap raw bytes for signing. - */ -export type WrappingStrategy = 'ethereum' | 'polkadot' - -/** - * Wraps the provided challenge according to the key type. - * - * Ethereum addresses will cause the challenge to be prefixed with - * `\x19Ethereum Signed Message:\n` and the length of the message. - * - * For all other key types the message will be wrapped in `..`. - * - * @param type The key type that will sign the challenge. - * @param challenge The challenge to proof ownership of both account and DID. - * @returns The wrapped challenge. - */ -export function getWrappedChallenge( - type: WrappingStrategy, - challenge: Uint8Array -): Uint8Array { - if (type === 'ethereum') { - const length = stringToU8a(String(challenge.length)) - return u8aConcatStrict([U8A_WRAP_ETHEREUM, length, challenge]) - } - return u8aWrapBytes(challenge) -} - -/** - * Builds the parameters for an extrinsic to link the `account` to the `did` where the fees and deposit are covered by some third account. - * This extrinsic must be authorized using the same full DID. - * Note that in addition to the signing account and DID used here, the submitting account will also be able to dissolve the link via reclaiming its deposit! - * - * @param accountAddress Address of the account to be linked. - * @param did Full DID to be linked. - * @param sign The sign callback that generates the account signature over the encoded (DidAddress, BlockNumber) tuple. - * @param nBlocksValid The link request will be rejected if submitted later than (current block number + nBlocksValid)? - * @returns An array of parameters for `api.tx.didLookup.associateAccount` that must be DID-authorized by the full DID used. - */ -export async function associateAccountToChainArgs( - accountAddress: Address, - did: DidDocumentV2.DidUri, - sign: (encodedLinkingDetails: HexString) => Promise, - nBlocksValid = 10 -): Promise { - const api = ConfigService.get('api') - - const blockNo = await api.query.system.number() - const validTill = blockNo.addn(nBlocksValid) - - const challenge = await getLinkingChallenge(did, validTill) - - // ethereum addresses are 42 characters long since they are 20 bytes hex encoded strings - // (they start with 0x, 2 characters per byte) - const predictedType = accountAddress.length === 42 ? 'ethereum' : 'polkadot' - const wrappedChallenge = u8aToHex( - getWrappedChallenge(predictedType, challenge) - ) - - const { signature, type } = getUnprefixedSignature( - wrappedChallenge, - await sign(wrappedChallenge), - accountAddress - ) - - return getLinkingArguments(accountAddress, validTill, signature, type) -} diff --git a/packages/did/src/DidDocumentExporter/DidContexts.ts b/packages/did/src/DidResolver/DidContexts.ts similarity index 100% rename from packages/did/src/DidDocumentExporter/DidContexts.ts rename to packages/did/src/DidResolver/DidContexts.ts diff --git a/packages/did/src/DidResolver/DidContextsV2.ts b/packages/did/src/DidResolver/DidContextsV2.ts deleted file mode 100644 index 196e35a2df..0000000000 --- a/packages/did/src/DidResolver/DidContextsV2.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -// @ts-expect-error not a TS package -import securityContexts from '@digitalbazaar/security-context' - -const securityContextsMap: Map< - string, - Record -> = securityContexts.contexts - -/** - * IPFS URL identifying a JSON-LD context file describing terms used in DID documents of the KILT method that are not defined in the W3C DID core context. - * Should be the second entry in the ordered set of contexts after [[W3C_DID_CONTEXT_URL]] in the JSON-LD representation of a KILT DID document. - */ -export const KILT_DID_CONTEXT_URL = - 'ipfs://QmU7QkuTCPz7NmD5bD7Z7mQVz2UsSPaEK58B5sYnjnPRNW' -/** - * URL identifying the JSON-LD context file that is part of the W3C DID core specifications describing the terms defined by the core data model. - * Must be the first entry in the ordered set of contexts in a JSON-LD representation of a DID document. - * See https://www.w3.org/TR/did-core/#json-ld. - */ -export const W3C_DID_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' -/** - * URL identifying a JSON-LD context file proposed by the W3C Credentials Community Group defining a number of terms which are used in verification methods on KILT DID documents. - * See https://w3c-ccg.github.io/security-vocab/. - * This document is extended by the context file available under the [[KILT_DID_CONTEXT_URL]]. - */ -export const W3C_SECURITY_CONTEXT_URL = securityContexts.SECURITY_CONTEXT_V2_URL -/** - * An object containing static copies of JSON-LD context files relevant to KILT DID documents, of the form -> context. - * These context definitions are not supposed to change; therefore, a cached version can (and should) be used to avoid unexpected changes in definitions. - */ -export const DID_CONTEXTS = { - [KILT_DID_CONTEXT_URL]: { - '@context': [ - W3C_SECURITY_CONTEXT_URL, - { - '@protected': true, - KiltPublishedCredentialCollectionV1: - 'https://github.com/KILTprotocol/spec-KiltPublishedCredentialCollectionV1', - Sr25519VerificationKey2020: - 'https://github.com/KILTprotocol/spec-kilt-did#sr25519', - }, - ], - }, - [W3C_DID_CONTEXT_URL]: { - '@context': { - '@protected': true, - id: '@id', - type: '@type', - - alsoKnownAs: { - '@id': 'https://www.w3.org/ns/activitystreams#alsoKnownAs', - '@type': '@id', - }, - assertionMethod: { - '@id': 'https://w3id.org/security#assertionMethod', - '@type': '@id', - '@container': '@set', - }, - authentication: { - '@id': 'https://w3id.org/security#authenticationMethod', - '@type': '@id', - '@container': '@set', - }, - capabilityDelegation: { - '@id': 'https://w3id.org/security#capabilityDelegationMethod', - '@type': '@id', - '@container': '@set', - }, - capabilityInvocation: { - '@id': 'https://w3id.org/security#capabilityInvocationMethod', - '@type': '@id', - '@container': '@set', - }, - controller: { - '@id': 'https://w3id.org/security#controller', - '@type': '@id', - }, - keyAgreement: { - '@id': 'https://w3id.org/security#keyAgreementMethod', - '@type': '@id', - '@container': '@set', - }, - service: { - '@id': 'https://www.w3.org/ns/did#service', - '@type': '@id', - '@context': { - '@protected': true, - id: '@id', - type: '@type', - serviceEndpoint: { - '@id': 'https://www.w3.org/ns/did#serviceEndpoint', - '@type': '@id', - }, - }, - }, - verificationMethod: { - '@id': 'https://w3id.org/security#verificationMethod', - '@type': '@id', - }, - }, - }, - ...Object.fromEntries(securityContextsMap), -} diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 16ad075122..4efc93229a 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -6,66 +6,75 @@ */ import type { - ConformingDidResolutionResult, - DidKey, - DidResolutionResult, - DidResourceUri, + DereferenceContentMetadata, + DereferenceContentStream, + DereferenceOptions, + DereferenceResult, + DidDocument, + DidResolver, DidUri, - KeyRelationship, - ResolvedDidKey, - ResolvedDidServiceEndpoint, - UriFragment, + DidUrl, + JsonLd, + RepresentationResolutionResult, + ResolutionDocumentMetadata, + ResolutionOptions, + ResolutionResult, } from '@kiltprotocol/types' -import { SDKErrors } from '@kiltprotocol/utils' + import { ConfigService } from '@kiltprotocol/config' +import { cbor } from '@kiltprotocol/utils' -import * as Did from '../index.js' -import { toChain } from '../Did.chain.js' +import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' import { linkedInfoFromChain } from '../Did.rpc.js' -import { getFullDidUri, parse } from '../Did.utils.js' -import { exportToDidDocument } from '../DidDocumentExporter/DidDocumentExporter.js' +import { toChain } from '../Did.chain.js' +import { getFullDidUri, parse, validateUri } from '../Did.utils.js' +import { parseDocumentFromLightDid } from '../DidDetails/LightDidDetails.js' + +const DID_JSON = 'application/did+json' +const DID_JSON_LD = 'application/did+ld+json' +const DID_CBOR = 'application/did+cbor' /** - * Resolve a DID URI to the DID document and its metadata. - * - * The URI can also identify a key or a service, but it will be ignored during resolution. - * - * @param did The subject's DID. - * @returns The details associated with the DID subject. + * Supported types for DID resolution and dereferencing. */ -export async function resolve( +export type SupportedContentType = + | typeof DID_JSON + | typeof DID_JSON_LD + | typeof DID_CBOR + +function isValidContentType(input: unknown): input is SupportedContentType { + return input === DID_JSON || input === DID_JSON_LD || input === DID_CBOR +} + +type InternalResolutionResult = { + document?: DidDocument + documentMetadata: ResolutionDocumentMetadata +} + +async function resolveInternal( did: DidUri -): Promise { +): Promise { const { type } = parse(did) const api = ConfigService.get('api') - const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid - const { section, version } = queryFunction?.meta ?? {} - if (version > 2) - throw new Error( - `This version of the KILT sdk supports runtime api '${section}' <=v2 , but the blockchain runtime implements ${version}. Please upgrade!` - ) - const { document, web3Name } = await queryFunction(toChain(did)) + + const { document } = await api.call.did + .query(toChain(did)) .then(linkedInfoFromChain) - .catch(() => ({ document: undefined, web3Name: undefined })) + .catch(() => ({ document: undefined })) - if (type === 'full' && document) { + if (type === 'full' && document !== undefined) { return { document, - metadata: { - deactivated: false, - }, - ...(web3Name && { web3Name }), + documentMetadata: {}, } } - // If the full DID has been deleted (or the light DID was upgraded and deleted), - // return the info in the resolution metadata. const isFullDidDeleted = (await api.query.did.didBlacklist(toChain(did))) .isSome if (isFullDidDeleted) { return { // No canonicalId and no details are returned as we consider this DID deactivated/deleted. - metadata: { + documentMetadata: { deactivated: true, }, } @@ -75,13 +84,15 @@ export async function resolve( return null } - const lightDocument = Did.parseDocumentFromLightDid(did, false) + const lightDocument = parseDocumentFromLightDid(did, false) // If a full DID with same subject is present, return the resolution metadata accordingly. - if (document) { + if (document !== undefined) { return { - metadata: { + documentMetadata: { canonicalId: getFullDidUri(did), - deactivated: false, + }, + document: { + id: did, }, } } @@ -90,190 +101,224 @@ export async function resolve( // Metadata will simply contain `deactivated: false`. return { document: lightDocument, - metadata: { - deactivated: false, - }, + documentMetadata: {}, } } /** * Implementation of `resolve` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - * As opposed to `resolve`, which takes a more pragmatic approach, the `didDocument` property contains a fully compliant DID document abstract data model. * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. * * @param did The DID to resolve. - * @returns An object with the properties `didDocument` (a spec-conforming DID document or `undefined`), `didDocumentMetadata` (equivalent to `metadata` returned by [[resolve]]), as well as `didResolutionMetadata` (indicating an `error` if any). + * @param resolutionOptions The resolution options accepted by the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * @returns The resolution result for the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). */ -export async function resolveCompliant( - did: DidUri -): Promise { - const result: ConformingDidResolutionResult = { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - } +export async function resolve( + did: DidUri, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + resolutionOptions: ResolutionOptions = {} +): Promise { try { - Did.validateUri(did, 'Did') + validateUri(did, 'Did') } catch (error) { - result.didResolutionMetadata.error = 'invalidDid' - if (error instanceof Error) { - result.didResolutionMetadata.errorMessage = - error.name + error.message ? `: ${error.message}` : '' + return { + didResolutionMetadata: { + error: 'invalidDid', + }, + didDocumentMetadata: {}, } - return result } - const resolutionResult = await resolve(did) + + const resolutionResult = await resolveInternal(did) if (!resolutionResult) { - result.didResolutionMetadata.error = 'notFound' - result.didResolutionMetadata.errorMessage = `DID ${did} not found (on chain)` - return result - } - const { metadata, document, web3Name } = resolutionResult - result.didDocumentMetadata = metadata - result.didDocument = document - ? exportToDidDocument(document, 'application/json') - : { id: did } - - if (web3Name) { - result.didDocument.alsoKnownAs = [`w3n:${web3Name}`] + return { + didResolutionMetadata: { + error: 'notFound', + }, + didDocumentMetadata: {}, + } } - return result -} + const { documentMetadata: didDocumentMetadata, document: didDocument } = + resolutionResult -/** - * Converts the DID key in the format returned by `resolveKey()`, useful for own implementations of `resolveKey`. - * - * @param key The DID key in the SDK format. - * @param did The DID the key belongs to. - * @returns The key in the resolveKey-format. - */ -export function keyToResolvedKey(key: DidKey, did: DidUri): ResolvedDidKey { - const { id, publicKey, includedAt, type } = key return { - controller: did, - id: `${did}${id}`, - publicKey, - type, - ...(includedAt && { includedAt }), + didResolutionMetadata: {}, + didDocumentMetadata, + didDocument, } } /** - * Converts the DID key returned by the `resolveKey()` into the format used in the SDK. + * Implementation of `resolveRepresentation` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. + * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. * - * @param key The key in the resolveKey-format. - * @returns The key in the SDK format. + * @param did The DID to resolve. + * @param resolutionOptions The resolution options accepted by the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * @param resolutionOptions.accept The content type accepted by the requesting client. + * @returns The resolution result for the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). */ -export function resolvedKeyToKey(key: ResolvedDidKey): DidKey { - const { id, publicKey, includedAt, type } = key - return { - id: Did.parse(id).fragment as UriFragment, - publicKey, - type, - ...(includedAt && { includedAt }), +export async function resolveRepresentation( + did: DidUri, + { accept }: DereferenceOptions = { + accept: DID_JSON, } -} - -/** - * Resolve a DID key URI to the key details. - * - * @param keyUri The DID key URI. - * @param expectedVerificationMethod Optional key relationship the key has to belong to. - * @returns The details associated with the key. - */ -export async function resolveKey( - keyUri: DidResourceUri, - expectedVerificationMethod?: KeyRelationship -): Promise { - const { did, fragment: keyId } = parse(keyUri) - - // A fragment (keyId) IS expected to resolve a key. - if (!keyId) { - throw new SDKErrors.DidError( - `Key URI "${keyUri}" is not a valid DID resource` - ) +): Promise> { + if (!isValidContentType(accept)) { + return { + didResolutionMetadata: { + error: 'representationNotSupported', + }, + didDocumentMetadata: {}, + } } - const resolved = await resolve(did) - if (!resolved) { - throw new SDKErrors.DidNotFoundError() + const { didDocumentMetadata, didResolutionMetadata, didDocument } = + await resolve(did) + + if (didDocument === undefined) { + return { + // Metadata is the same, since the `representationNotSupported` is already accounted for above. + didResolutionMetadata, + didDocumentMetadata, + } as RepresentationResolutionResult } - const { - document, - metadata: { canonicalId }, - } = resolved + const bufferInput = (() => { + if (accept === 'application/did+json') { + return JSON.stringify(didDocument) + } + if (accept === 'application/did+ld+json') { + const jsonLdDoc: JsonLd = { + ...didDocument, + '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + } + return JSON.stringify(jsonLdDoc) + } + // contentType === 'application/did+cbor + return cbor.encode(didDocument) + })() - // If the light DID has been upgraded we consider the old key URI invalid, the full DID URI should be used instead. - if (canonicalId) { - throw new SDKErrors.DidResolveUpgradedDidError() - } - if (!document) { - throw new SDKErrors.DidDeactivatedError() - } + return { + didDocumentMetadata, + didResolutionMetadata, + didDocumentStream: Buffer.from(bufferInput), + } as RepresentationResolutionResult +} - const key = Did.getKey(document, keyId) - if (!key) { - throw new SDKErrors.DidNotFoundError('Key not found in DID') - } +type InternalDereferenceResult = { + contentStream?: DereferenceContentStream + contentMetadata: DereferenceContentMetadata +} - // Check whether the provided key ID is within the keys for a given verification relationship, if provided. - if ( - expectedVerificationMethod && - !document[expectedVerificationMethod]?.some(({ id }) => keyId === id) - ) { - throw new SDKErrors.DidError( - `No key "${keyUri}" for the verification method "${expectedVerificationMethod}"` - ) +async function dereferenceInternal( + didUrl: DidUri | DidUrl, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + dereferenceOptions: DereferenceOptions +): Promise { + const { did, fragment } = parse(didUrl) + + const { didDocument, didDocumentMetadata } = await resolve(did) + + if (fragment === undefined) { + return { + contentStream: didDocument, + contentMetadata: didDocumentMetadata, + } } + const dereferencedResource = (() => { + const verificationMethod = didDocument?.verificationMethod?.find( + (vm) => vm.id === fragment + ) + if (verificationMethod !== undefined) { + return verificationMethod + } + + const service = didDocument?.service?.find((s) => s.id === fragment) + return service + })() - return keyToResolvedKey(key, did) + return { + contentStream: dereferencedResource, + contentMetadata: {}, + } } /** - * Resolve a DID service URI to the service details. + * Implementation of `dereference` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). + * If a DID URL is invalid or has not been registered, this is indicated by the `error` property on the `dereferencingMetadata`. * - * @param serviceUri The DID service URI. - * @returns The details associated with the service endpoint. + * @param didUrl The DID URL to dereference. + * @param resolutionOptions The resolution options accepted by the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). + * @param resolutionOptions.accept The content type accepted by the requesting client. + * @returns The resolution result for the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). */ -export async function resolveService( - serviceUri: DidResourceUri -): Promise { - const { did, fragment: serviceId } = parse(serviceUri) - - // A fragment (serviceId) IS expected to resolve a key. - if (!serviceId) { - throw new SDKErrors.DidError( - `Service URI "${serviceUri}" is not a valid DID resource` - ) +export async function dereference( + didUrl: DidUri | DidUrl, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { accept }: DereferenceOptions = { + accept: DID_JSON, } +): Promise> { + // The spec does not include an error for unsupported content types for dereferences + const contentType = isValidContentType(accept) ? accept : DID_JSON - const resolved = await resolve(did) - if (!resolved) { - throw new SDKErrors.DidNotFoundError() + try { + validateUri(didUrl) + } catch (error) { + return { + dereferencingMetadata: { + error: 'invalidDidUrl', + }, + contentMetadata: {}, + } } - const { - document, - metadata: { canonicalId }, - } = resolved + const resolutionResult = await dereferenceInternal(didUrl, { + accept: contentType, + }) - // If the light DID has been upgraded we consider the old service URI invalid, the full DID URI should be used instead. - if (canonicalId) { - throw new SDKErrors.DidResolveUpgradedDidError() - } - if (!document) { - throw new SDKErrors.DidDeactivatedError() - } + const { contentMetadata, contentStream } = resolutionResult - const service = Did.getService(document, serviceId) - if (!service) { - throw new SDKErrors.DidNotFoundError('Service not found in DID') + if (contentStream === undefined) { + return { + dereferencingMetadata: { + error: 'notFound', + }, + contentMetadata, + } } + const stream = (() => { + if (contentType === 'application/did+json') { + return contentStream + } + if (contentType === 'application/did+ld+json') { + return { + ...contentStream, + '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + } + } + // contentType === 'application/did+cbor' + return Buffer.from(cbor.encode(contentStream)) + })() + return { - ...service, - id: `${did}${serviceId}`, + dereferencingMetadata: { + contentType, + }, + contentMetadata, + contentStream: stream, } } + +/** + * Fully-fledged default resolver capable of resolving DIDs in their canonical form, encoded for a specific content type, and of dereferencing parts of a DID Document according to the dereferencing specification. + */ +export const resolver: DidResolver = { + resolve, + resolveRepresentation, + dereference, +} diff --git a/packages/did/src/DidResolver/DidResolverV2.ts b/packages/did/src/DidResolver/DidResolverV2.ts deleted file mode 100644 index 527865753b..0000000000 --- a/packages/did/src/DidResolver/DidResolverV2.ts +++ /dev/null @@ -1,310 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { DidDocumentV2, DidResolverV2 } from '@kiltprotocol/types' - -import { ConfigService } from '@kiltprotocol/config' -import { cbor } from '@kiltprotocol/utils' - -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContextsV2.js' -import { linkedInfoFromChain } from '../Did2.rpc.js' -import { toChain } from '../Did2.chain.js' -import { getFullDidUri, parse, validateUri } from '../Did2.utils.js' -import { parseDocumentFromLightDid } from '../DidDetailsv2/LightDidDetailsV2.js' - -const DID_JSON = 'application/did+json' -const DID_JSON_LD = 'application/did+ld+json' -const DID_CBOR = 'application/did+cbor' - -/** - * Supported types for DID resolution and dereferencing. - */ -export type SupportedContentType = - | typeof DID_JSON - | typeof DID_JSON_LD - | typeof DID_CBOR - -function isValidContentType(input: unknown): input is SupportedContentType { - return input === DID_JSON || input === DID_JSON_LD || input === DID_CBOR -} - -type InternalResolutionResult = { - document?: DidDocumentV2.DidDocument - documentMetadata: DidResolverV2.ResolutionDocumentMetadata -} - -async function resolveInternal( - did: DidDocumentV2.DidUri -): Promise { - const { type } = parse(did) - const api = ConfigService.get('api') - - const { document } = await api.call.did - .query(toChain(did)) - .then(linkedInfoFromChain) - .catch(() => ({ document: undefined })) - - if (type === 'full' && document !== undefined) { - return { - document, - documentMetadata: {}, - } - } - - const isFullDidDeleted = (await api.query.did.didBlacklist(toChain(did))) - .isSome - if (isFullDidDeleted) { - return { - // No canonicalId and no details are returned as we consider this DID deactivated/deleted. - documentMetadata: { - deactivated: true, - }, - } - } - - if (type === 'full') { - return null - } - - const lightDocument = parseDocumentFromLightDid(did, false) - // If a full DID with same subject is present, return the resolution metadata accordingly. - if (document !== undefined) { - return { - documentMetadata: { - canonicalId: getFullDidUri(did), - }, - document: { - id: did, - }, - } - } - - // If no full DID details nor deletion info is found, the light DID is un-migrated. - // Metadata will simply contain `deactivated: false`. - return { - document: lightDocument, - documentMetadata: {}, - } -} - -/** - * Implementation of `resolve` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. - * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. - * - * @param did The DID to resolve. - * @param resolutionOptions The resolution options accepted by the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - * @returns The resolution result for the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - */ -export async function resolve( - did: DidDocumentV2.DidUri, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolutionOptions: DidResolverV2.ResolutionOptions = {} -): Promise { - try { - validateUri(did, 'Did') - } catch (error) { - return { - didResolutionMetadata: { - error: 'invalidDid', - }, - didDocumentMetadata: {}, - } - } - - const resolutionResult = await resolveInternal(did) - if (!resolutionResult) { - return { - didResolutionMetadata: { - error: 'notFound', - }, - didDocumentMetadata: {}, - } - } - - const { documentMetadata: didDocumentMetadata, document: didDocument } = - resolutionResult - - return { - didResolutionMetadata: {}, - didDocumentMetadata, - didDocument, - } -} - -/** - * Implementation of `resolveRepresentation` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. - * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. - * - * @param did The DID to resolve. - * @param resolutionOptions The resolution options accepted by the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - * @param resolutionOptions.accept The content type accepted by the requesting client. - * @returns The resolution result for the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - */ -export async function resolveRepresentation( - did: DidDocumentV2.DidUri, - { accept }: DidResolverV2.DereferenceOptions = { - accept: DID_JSON, - } -): Promise> { - if (!isValidContentType(accept)) { - return { - didResolutionMetadata: { - error: 'representationNotSupported', - }, - didDocumentMetadata: {}, - } - } - - const { didDocumentMetadata, didResolutionMetadata, didDocument } = - await resolve(did) - - if (didDocument === undefined) { - return { - // Metadata is the same, since the `representationNotSupported` is already accounted for above. - didResolutionMetadata, - didDocumentMetadata, - } as DidResolverV2.RepresentationResolutionResult - } - - const bufferInput = (() => { - if (accept === 'application/did+json') { - return JSON.stringify(didDocument) - } - if (accept === 'application/did+ld+json') { - const jsonLdDoc: DidDocumentV2.JsonLd = { - ...didDocument, - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], - } - return JSON.stringify(jsonLdDoc) - } - // contentType === 'application/did+cbor - return cbor.encode(didDocument) - })() - - return { - didDocumentMetadata, - didResolutionMetadata, - didDocumentStream: Buffer.from(bufferInput), - } as DidResolverV2.RepresentationResolutionResult -} - -type InternalDereferenceResult = { - contentStream?: DidResolverV2.DereferenceContentStream - contentMetadata: DidResolverV2.DereferenceContentMetadata -} - -async function dereferenceInternal( - didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidUrl, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - dereferenceOptions: DidResolverV2.DereferenceOptions -): Promise { - const { did, fragment } = parse(didUrl) - - const { didDocument, didDocumentMetadata } = await resolve(did) - - if (fragment === undefined) { - return { - contentStream: didDocument, - contentMetadata: didDocumentMetadata, - } - } - const dereferencedResource = (() => { - const verificationMethod = didDocument?.verificationMethod?.find( - (m) => m.id === fragment - ) - if (verificationMethod !== undefined) { - return verificationMethod - } - - const service = didDocument?.service?.find((s) => s.id === fragment) - return service - })() - - return { - contentStream: dereferencedResource, - contentMetadata: {}, - } -} - -/** - * Implementation of `dereference` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). - * If a DID URL is invalid or has not been registered, this is indicated by the `error` property on the `dereferencingMetadata`. - * - * @param didUrl The DID URL to dereference. - * @param resolutionOptions The resolution options accepted by the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). - * @param resolutionOptions.accept The content type accepted by the requesting client. - * @returns The resolution result for the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). - */ -export async function dereference( - didUrl: DidDocumentV2.DidUri | DidDocumentV2.DidUrl, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - { accept }: DidResolverV2.DereferenceOptions = { - accept: DID_JSON, - } -): Promise> { - // The spec does not include an error for unsupported content types for dereferences - const contentType = isValidContentType(accept) ? accept : DID_JSON - - try { - validateUri(didUrl) - } catch (error) { - return { - dereferencingMetadata: { - error: 'invalidDidUrl', - }, - contentMetadata: {}, - } - } - - const resolutionResult = await dereferenceInternal(didUrl, { - accept: contentType, - }) - - const { contentMetadata, contentStream } = resolutionResult - - if (contentStream === undefined) { - return { - dereferencingMetadata: { - error: 'notFound', - }, - contentMetadata, - } - } - - const stream = (() => { - if (contentType === 'application/did+json') { - return contentStream - } - if (contentType === 'application/did+ld+json') { - return { - ...contentStream, - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], - } - } - // contentType === 'application/did+cbor' - return Buffer.from(cbor.encode(contentStream)) - })() - - return { - dereferencingMetadata: { - contentType, - }, - contentMetadata, - contentStream: stream, - } -} - -/** - * Fully-fledged default resolver capable of resolving DIDs in their canonical form, encoded for a specific content type, and of dereferencing parts of a DID Document according to the dereferencing specification. - */ -export const resolver: DidResolverV2.DidResolver = { - resolve, - resolveRepresentation, - dereference, -} diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/_DidResolver.spec.ts similarity index 100% rename from packages/did/src/DidResolver/DidResolver.spec.ts rename to packages/did/src/DidResolver/_DidResolver.spec.ts diff --git a/packages/did/src/index.ts b/packages/did/src/index.ts index 0c11446f3a..21dd1faf72 100644 --- a/packages/did/src/index.ts +++ b/packages/did/src/index.ts @@ -10,18 +10,9 @@ */ export * from './DidDetails/index.js' -export * from './DidDocumentExporter/index.js' -export * from './DidResolver/index.js' +export * from './DidResolver/DidResolver.js' export * from './Did.chain.js' export * from './Did.rpc.js' export * from './Did.utils.js' export * from './Did.signature.js' export * from './DidLinks/AccountLinks.chain.js' - -export * as DidDetailsV2 from './DidDetailsv2/index.js' -export * as DidResolverV2 from './DidResolver/DidResolverV2.js' -export * as DidChainV2 from './Did2.chain.js' -export * as DidUtilsV2 from './Did2.utils.js' -export * as DidSignatureV2 from './Did2.signature.js' -export * as DidRpc2 from './Did2.rpc.js' -export * as DidLinksV2 from './DidLinks/AccountLinks2.chain.js' diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 3bb8225b11..03e2114d1c 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -6,11 +6,10 @@ */ import type { - DidResourceUri, DidUri, - DidVerificationKey, - VerificationKeyRelationship, -} from './DidDocument.js' + SignatureVerificationMethodRelationship, + VerificationMethod, +} from './DidDocument' /** * Base interface for all signing requests. @@ -24,7 +23,7 @@ export interface SignRequestData { /** * The did key relationship to be used. */ - keyRelationship: VerificationKeyRelationship + verificationMethodRelationship: SignatureVerificationMethodRelationship /** * The DID to be used for signing. @@ -43,11 +42,7 @@ export interface SignResponseData { /** * The did key uri used for signing. */ - keyUri: DidResourceUri - /** - * The did key type used for signing. - */ - keyType: DidVerificationKey['type'] + verificationMethod: Pick } /** @@ -62,7 +57,7 @@ export type SignCallback = ( */ export type SignExtrinsicCallback = ( signData: SignRequestData -) => Promise> +) => Promise /** * Base interface for encryption requests. @@ -97,7 +92,7 @@ export interface EncryptResponseData { /** * The did key uri used for the encryption. */ - keyUri: DidResourceUri + verificationMethod: VerificationMethod } /** @@ -126,7 +121,7 @@ export interface DecryptRequestData { /** * The did key uri, which should be used for decryption. */ - keyUri: DidResourceUri + verificationMethod: VerificationMethod } export interface DecryptResponseData { diff --git a/packages/types/src/CryptoCallbacksV2.ts b/packages/types/src/CryptoCallbacksV2.ts deleted file mode 100644 index e06fb35b70..0000000000 --- a/packages/types/src/CryptoCallbacksV2.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { - DidUri, - SignatureVerificationMethodRelationship, - VerificationMethod, -} from './DidDocumentV2' - -/** - * Base interface for all signing requests. - */ -export interface SignRequestData { - /** - * Data to be signed. - */ - data: Uint8Array - - /** - * The did key relationship to be used. - */ - verificationMethodRelationship: SignatureVerificationMethodRelationship - - /** - * The DID to be used for signing. - */ - did: DidUri -} - -/** - * Base interface for responses to signing requests. - */ -export interface SignResponseData { - /** - * Result of the signing. - */ - signature: Uint8Array - /** - * The did key uri used for signing. - */ - verificationMethod: Pick -} - -/** - * A callback function to sign data. - */ -export type SignCallback = ( - signData: SignRequestData -) => Promise - -/** - * A callback function to sign extrinsics. - */ -export type SignExtrinsicCallback = ( - signData: SignRequestData -) => Promise - -/** - * Base interface for encryption requests. - */ -export interface EncryptRequestData { - /** - * Data to be encrypted. - */ - data: Uint8Array - /** - * The other party's public key to be used for x25519 Diffie-Hellman key agreement. - */ - peerPublicKey: Uint8Array - /** - * The DID to be used for encryption. - */ - did: DidUri -} - -/** - * Base interface for responses to encryption requests. - */ -export interface EncryptResponseData { - /** - * Result of the encryption. - */ - data: Uint8Array - /** - * A random nonce generated in the encryption process. - */ - nonce: Uint8Array - /** - * The did key uri used for the encryption. - */ - verificationMethod: VerificationMethod -} - -/** - * Uses stored key material to encrypt a message encoded as u8a. - * - * @param requestData The data to be encrypted, the peers public key and the sender's DID. - * @returns [[EncryptResponseData]] which additionally to the data contains a `nonce` randomly generated in the encryption process (required for decryption). - */ -export interface EncryptCallback { - (requestData: EncryptRequestData): Promise -} - -export interface DecryptRequestData { - /** - * Data to be encrypted. - */ - data: Uint8Array - /** - * The other party's public key to be used for x25519 Diffie-Hellman key agreement. - */ - peerPublicKey: Uint8Array - /** - * The random nonce generated during encryption as u8a. - */ - nonce: Uint8Array - /** - * The did key uri, which should be used for decryption. - */ - verificationMethod: VerificationMethod -} - -export interface DecryptResponseData { - /** - * Result of the decryption. - */ - data: Uint8Array -} - -/** - * Uses stored key material to decrypt a message encoded as u8a. - * - * @param requestData [[DecryptRequestData]] containing both our and their public keys, the nonce used for encryption, the data to be decrypted. - * @param requestData.nonce The random nonce generated during encryption as u8a. - * @returns A Promise resolving to [[DecryptResponseData]] containing the decrypted message or rejecting if a key is unknown or does not match. - */ -export interface DecryptCallback { - (requestData: DecryptRequestData): Promise -} diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 9bf9a75025..1fb635ac38 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -5,15 +5,12 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { BN } from './Imported' import type { KiltAddress } from './Address' type AuthenticationKeyType = '00' | '01' type DidUriVersion = '' | `v${string}:` type LightDidEncodedData = '' | `:${string}` -// NOTICE: The following string pattern types must be kept in sync with regex patterns @kiltprotocol/did/Utils - /** * A string containing a KILT DID Uri. */ @@ -28,149 +25,93 @@ export type UriFragment = `#${string}` /** * URI for DID resources like keys or service endpoints. */ -export type DidResourceUri = `${DidUri}${UriFragment}` - -/** - * DID keys are purpose-bound. Their role or purpose is indicated by the verification or key relationship type. - */ -const keyRelationshipsC = [ - 'authentication', - 'capabilityDelegation', - 'assertionMethod', - 'keyAgreement', -] as const -export const keyRelationships = keyRelationshipsC as unknown as string[] -export type KeyRelationship = typeof keyRelationshipsC[number] - -/** - * Subset of key relationships which pertain to signing/verification keys. - */ -export type VerificationKeyRelationship = Extract< - KeyRelationship, - 'authentication' | 'capabilityDelegation' | 'assertionMethod' -> - -/** - * Possible types for a DID verification key. - */ -const verificationKeyTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const -export const verificationKeyTypes = verificationKeyTypesC as unknown as string[] -export type VerificationKeyType = typeof verificationKeyTypesC[number] -// `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 - -/** - * Currently, a light DID does not support the use of an ECDSA key as its authentication key. - */ -export type LightDidSupportedVerificationKeyType = Extract< - VerificationKeyType, - 'ed25519' | 'sr25519' -> +export type DidUrl = `${DidUri}${UriFragment}` -/** - * Subset of key relationships which pertain to key agreement/encryption keys. - */ -export type EncryptionKeyRelationship = Extract +export type SignatureVerificationMethodRelationship = + | 'authentication' + | 'capabilityDelegation' + | 'assertionMethod' +export type EncryptionMethodRelationship = 'keyAgreement' -/** - * Possible types for a DID encryption key. - */ -const encryptionKeyTypesC = ['x25519'] as const -export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] -export type EncryptionKeyType = typeof encryptionKeyTypesC[number] +export type VerificationMethodRelationship = + | SignatureVerificationMethodRelationship + | EncryptionMethodRelationship -/** - * Type of a new key material to add under a DID. - */ -export type BaseNewDidKey = { - publicKey: Uint8Array - type: string -} +type Base58BtcMultibaseString = `z${string}` -/** - * Type of a new verification key to add under a DID. - */ -export type NewDidVerificationKey = BaseNewDidKey & { - type: VerificationKeyType -} -/** - * A new public key specified when creating a new light DID. +/* + * The verification method map MUST include the id, type, controller, and specific verification material properties that are determined by the value of type and are defined in 5.2.1 Verification Material. A verification method MAY include additional properties. Verification methods SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ -export type NewLightDidVerificationKey = NewDidVerificationKey & { - type: LightDidSupportedVerificationKeyType -} -/** - * Type of a new encryption key to add under a DID. - */ -export type NewDidEncryptionKey = BaseNewDidKey & { type: EncryptionKeyType } - -/** - * The SDK-specific base details of a DID key. - */ -export type BaseDidKey = { - /** - * Relative key URI: `#` sign followed by fragment part of URI. +export type VerificationMethod = { + /* + * The value of the id property for a verification method MUST be a string that conforms to the rules in Section 3.2 DID URL Syntax. */ id: UriFragment - /** - * The public key material. + /* + * The value of the type property MUST be a string that references exactly one verification method type. In order to maximize global interoperability, the verification method type SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ - publicKey: Uint8Array - /** - * The inclusion block of the key, if stored on chain. + type: 'MultiKey' + /* + * The value of the controller property MUST be a string that conforms to the rules in 3.1 DID Syntax. */ - includedAt?: BN - /** - * The type of the key. + controller: DidUri + /* + * The publicKeyMultibase property is OPTIONAL. This feature is non-normative. If present, the value MUST be a string representation of a [MULTIBASE] encoded public key. */ - type: string + publicKeyMultibase: Base58BtcMultibaseString } -/** - * The SDK-specific details of a DID verification key. - */ -export type DidVerificationKey = BaseDidKey & { type: VerificationKeyType } -/** - * The SDK-specific details of a DID encryption key. +/* + * Each service map MUST contain id, type, and serviceEndpoint properties. Each service extension MAY include additional properties and MAY further restrict the properties associated with the extension. */ -export type DidEncryptionKey = BaseDidKey & { type: EncryptionKeyType } -/** - * The SDK-specific details of a DID key. - */ -export type DidKey = DidVerificationKey | DidEncryptionKey - -/** - * The SDK-specific details of a new DID service endpoint. - */ -export type DidServiceEndpoint = { - /** - * Relative endpoint URI: `#` sign followed by fragment part of URI. +export type Service = { + /* + * The value of the id property MUST be a URI conforming to [RFC3986]. A conforming producer MUST NOT produce multiple service entries with the same id. A conforming consumer MUST produce an error if it detects multiple service entries with the same id. */ id: UriFragment - /** - * A list of service types the endpoint exposes. + /* + * The value of the type property MUST be a string or a set of strings. In order to maximize interoperability, the service type and its associated properties SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. */ type: string[] - /** - * A list of URIs the endpoint exposes its services at. + /* + * The value of the serviceEndpoint property MUST be a string, a map, or a set composed of one or more strings and/or maps. All string values MUST be valid URIs conforming to [RFC3986] and normalized according to the Normalization and Comparison rules in RFC3986 and to any normalization rules in its applicable URI scheme specification. */ serviceEndpoint: string[] } -/** - * A signature issued with a DID associated key, indicating which key was used to sign. - */ -export type DidSignature = { - keyUri: DidResourceUri - signature: string +export type DidDocument = { + /* + * The value of id MUST be a string that conforms to the rules in 3.1 DID Syntax and MUST exist in the root map of the data model for the DID document. + */ + id: DidUri + /* + * The alsoKnownAs property is OPTIONAL. If present, the value MUST be a set where each item in the set is a URI conforming to [RFC3986]. + */ + alsoKnownAs?: string[] + /* + * The verificationMethod property is OPTIONAL. If present, the value MUST be a set of verification methods, where each verification method is expressed using a map. + */ + verificationMethod?: VerificationMethod[] + /* + * The authentication property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + authentication?: UriFragment[] + /* + * The assertionMethod property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + assertionMethod?: UriFragment[] + /* + * The keyAgreement property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + keyAgreement?: UriFragment[] + /* + * The capabilityDelegation property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. + */ + capabilityDelegation?: UriFragment[] + /* + * The service property is OPTIONAL. If present, the associated value MUST be a set of services, where each service is described by a map. + */ + service?: Service[] } -export interface DidDocument { - uri: DidUri - - authentication: [DidVerificationKey] - assertionMethod?: [DidVerificationKey] - capabilityDelegation?: [DidVerificationKey] - keyAgreement?: DidEncryptionKey[] - - service?: DidServiceEndpoint[] -} +export type JsonLd = T & { '@context': string[] } diff --git a/packages/types/src/DidDocumentExporter.ts b/packages/types/src/DidDocumentExporter.ts deleted file mode 100644 index 9b675e23e7..0000000000 --- a/packages/types/src/DidDocumentExporter.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { - DidResourceUri, - DidServiceEndpoint, - DidUri, - EncryptionKeyType, - VerificationKeyType, -} from './DidDocument.js' -import { DidResolutionDocumentMetadata } from './DidResolver.js' - -export type ConformingDidDocumentKeyType = - | 'Ed25519VerificationKey2018' - | 'Sr25519VerificationKey2020' - | 'EcdsaSecp256k1VerificationKey2019' - | 'X25519KeyAgreementKey2019' - -export const verificationKeyTypesMap: Record< - VerificationKeyType, - ConformingDidDocumentKeyType -> = { - // proposed and used by dock.io, e.g. https://github.com/w3c-ccg/security-vocab/issues/32, https://github.com/docknetwork/sdk/blob/9c818b03bfb4fdf144c20678169c7aad3935ad96/src/utils/vc/contexts/security_context.js - sr25519: 'Sr25519VerificationKey2020', - // these are part of current w3 security vocab, see e.g. https://www.w3.org/ns/did/v1 - ed25519: 'Ed25519VerificationKey2018', - ecdsa: 'EcdsaSecp256k1VerificationKey2019', -} - -export const encryptionKeyTypesMap: Record< - EncryptionKeyType, - ConformingDidDocumentKeyType -> = { - x25519: 'X25519KeyAgreementKey2019', -} - -/** - * A spec-compliant description of a DID key. - */ -export type ConformingDidKey = { - /** - * The full key URI, in the form of #. - */ - id: DidResourceUri - /** - * The key controller, in the form of . - */ - controller: DidUri - /** - * The base58-encoded public component of the key. - */ - publicKeyBase58: string - /** - * The key type signalling the intended signing/encryption algorithm for the use of this key. - */ - type: ConformingDidDocumentKeyType -} - -/** - * A spec-compliant description of a DID endpoint. - */ -export type ConformingDidServiceEndpoint = Omit & { - /** - * The full service URI, in the form of #. - */ - id: DidResourceUri -} - -/** - * A DID Document according to the [W3C DID Core specification](https://www.w3.org/TR/did-core/). - */ -export type ConformingDidDocument = { - id: DidUri - verificationMethod: ConformingDidKey[] - authentication: [ConformingDidKey['id']] - assertionMethod?: [ConformingDidKey['id']] - keyAgreement?: [ConformingDidKey['id']] - capabilityDelegation?: [ConformingDidKey['id']] - service?: ConformingDidServiceEndpoint[] - alsoKnownAs?: [`w3n:${string}`] -} - -/** - * A JSON+LD DID Document that extends a traditional DID Document with additional semantic information. - */ -export type JsonLDDidDocument = ConformingDidDocument & { '@context': string[] } - -/** - * DID Resolution Metadata returned by the DID `resolve` function as described by DID specifications (https://www.w3.org/TR/did-core/#did-resolution-metadata). - */ -export interface DidResolutionMetadata { - error?: 'notFound' | 'invalidDid' - errorMessage?: string -} - -/** - * Object containing the return values of the DID `resolve` function as described by DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - */ -export interface ConformingDidResolutionResult { - didDocumentMetadata: Partial - didResolutionMetadata: DidResolutionMetadata - didDocument?: Partial & - Pick -} diff --git a/packages/types/src/DidDocumentV2.ts b/packages/types/src/DidDocumentV2.ts deleted file mode 100644 index 1fb635ac38..0000000000 --- a/packages/types/src/DidDocumentV2.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { KiltAddress } from './Address' - -type AuthenticationKeyType = '00' | '01' -type DidUriVersion = '' | `v${string}:` -type LightDidEncodedData = '' | `:${string}` - -/** - * A string containing a KILT DID Uri. - */ -export type DidUri = - | `did:kilt:${DidUriVersion}${KiltAddress}` - | `did:kilt:light:${DidUriVersion}${AuthenticationKeyType}${KiltAddress}${LightDidEncodedData}` - -/** - * The fragment part of the DID URI including the `#` character. - */ -export type UriFragment = `#${string}` -/** - * URI for DID resources like keys or service endpoints. - */ -export type DidUrl = `${DidUri}${UriFragment}` - -export type SignatureVerificationMethodRelationship = - | 'authentication' - | 'capabilityDelegation' - | 'assertionMethod' -export type EncryptionMethodRelationship = 'keyAgreement' - -export type VerificationMethodRelationship = - | SignatureVerificationMethodRelationship - | EncryptionMethodRelationship - -type Base58BtcMultibaseString = `z${string}` - -/* - * The verification method map MUST include the id, type, controller, and specific verification material properties that are determined by the value of type and are defined in 5.2.1 Verification Material. A verification method MAY include additional properties. Verification methods SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. - */ -export type VerificationMethod = { - /* - * The value of the id property for a verification method MUST be a string that conforms to the rules in Section 3.2 DID URL Syntax. - */ - id: UriFragment - /* - * The value of the type property MUST be a string that references exactly one verification method type. In order to maximize global interoperability, the verification method type SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. - */ - type: 'MultiKey' - /* - * The value of the controller property MUST be a string that conforms to the rules in 3.1 DID Syntax. - */ - controller: DidUri - /* - * The publicKeyMultibase property is OPTIONAL. This feature is non-normative. If present, the value MUST be a string representation of a [MULTIBASE] encoded public key. - */ - publicKeyMultibase: Base58BtcMultibaseString -} - -/* - * Each service map MUST contain id, type, and serviceEndpoint properties. Each service extension MAY include additional properties and MAY further restrict the properties associated with the extension. - */ -export type Service = { - /* - * The value of the id property MUST be a URI conforming to [RFC3986]. A conforming producer MUST NOT produce multiple service entries with the same id. A conforming consumer MUST produce an error if it detects multiple service entries with the same id. - */ - id: UriFragment - /* - * The value of the type property MUST be a string or a set of strings. In order to maximize interoperability, the service type and its associated properties SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. - */ - type: string[] - /* - * The value of the serviceEndpoint property MUST be a string, a map, or a set composed of one or more strings and/or maps. All string values MUST be valid URIs conforming to [RFC3986] and normalized according to the Normalization and Comparison rules in RFC3986 and to any normalization rules in its applicable URI scheme specification. - */ - serviceEndpoint: string[] -} - -export type DidDocument = { - /* - * The value of id MUST be a string that conforms to the rules in 3.1 DID Syntax and MUST exist in the root map of the data model for the DID document. - */ - id: DidUri - /* - * The alsoKnownAs property is OPTIONAL. If present, the value MUST be a set where each item in the set is a URI conforming to [RFC3986]. - */ - alsoKnownAs?: string[] - /* - * The verificationMethod property is OPTIONAL. If present, the value MUST be a set of verification methods, where each verification method is expressed using a map. - */ - verificationMethod?: VerificationMethod[] - /* - * The authentication property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ - authentication?: UriFragment[] - /* - * The assertionMethod property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ - assertionMethod?: UriFragment[] - /* - * The keyAgreement property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ - keyAgreement?: UriFragment[] - /* - * The capabilityDelegation property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ - capabilityDelegation?: UriFragment[] - /* - * The service property is OPTIONAL. If present, the associated value MUST be a set of services, where each service is described by a map. - */ - service?: Service[] -} - -export type JsonLd = T & { '@context': string[] } diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index fd74dc2373..dcf0d038b8 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -5,74 +5,247 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { - ConformingDidKey, - ConformingDidServiceEndpoint, -} from './DidDocumentExporter.js' import type { - DidDocument, - DidKey, - DidResourceUri, DidUri, - KeyRelationship, -} from './DidDocument.js' + DidDocument, + DidUrl, + VerificationMethod, + Service, + JsonLd, +} from './DidDocument' -/** - * DID resolution metadata that includes a subset of the properties defined in the [W3C proposed standard](https://www.w3.org/TR/did-core/#did-resolution). +/* + * The `accept` header must not be used for the regular `resolve` function, so we enforce that statically. + * For more info, please refer to https://www.w3.org/TR/did-core/#did-resolution-options. */ -export type DidResolutionDocumentMetadata = { - /** - * If present, it indicates that the resolved by DID should be treated as if it were the DID as specified in this property. +export type ResolutionOptions = Record + +export type ResolutionMetadata = { + /* + * The error code from the resolution process. + * This property is REQUIRED when there is an error in the resolution process. + * The value of this property MUST be a single keyword ASCII string. + * The possible property values of this field SHOULD be registered in the DID Specification Registries. + * This specification defines the following common error values: + * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + */ + error?: 'invalidDid' | 'notFound' +} + +export type ResolutionDocumentMetadata = { + /* + * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. + * If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. + */ + deactivated?: true + /* + * DID document metadata MAY include a canonicalId property. + * If present, the value MUST be a string that conforms to the rules in Section 3.1 DID Syntax. + * The relationship is a statement that the canonicalId value is logically equivalent to the id property value and that the canonicalId value is defined by the DID method to be the canonical ID for the DID subject in the scope of the containing DID document. + * A canonicalId value MUST be produced by, and a form of, the same DID method as the id property value. (e.g., did:example:abc == did:example:ABC). */ canonicalId?: DidUri - /** - * A boolean flag indicating whether the resolved DID has been deactivated. +} + +export type ResolutionResult = { + /* + * A metadata structure consisting of values relating to the results of the DID resolution process which typically changes between invocations of the resolve and resolveRepresentation functions, as it represents data about the resolution process itself. + * This structure is REQUIRED, and in the case of an error in the resolution process, this MUST NOT be empty. + * If resolveRepresentation was called, this structure MUST contain a contentType property containing the Media Type of the representation found in the didDocumentStream. + * If the resolution is not successful, this structure MUST contain an error property describing the error. + * The possible properties within this structure and their possible values are registered in the DID Specification Registries. + */ + didResolutionMetadata: ResolutionMetadata + /* + * If the resolution is successful, and if the resolve function was called, this MUST be a DID document abstract data model (a map) as described in 4. Data Model that is capable of being transformed into a conforming DID Document (representation), using the production rules specified by the representation. + * The value of id in the resolved DID document MUST match the DID that was resolved. + * If the resolution is unsuccessful, this value MUST be empty. + */ + didDocument?: DidDocument + /* + * If the resolution is successful, this MUST be a metadata structure. + * This structure contains metadata about the DID document contained in the didDocument property. + * This metadata typically does not change between invocations of the resolve and resolveRepresentation functions unless the DID document changes, as it represents metadata about the DID document. + * If the resolution is unsuccessful, this output MUST be an empty metadata structure. + * The possible properties within this structure and their possible values SHOULD be registered in the DID Specification Registries. */ - deactivated: boolean + didDocumentMetadata: ResolutionDocumentMetadata } -/** - * The result of a DID resolution. - * - * It includes the DID Document, and optional document resolution metadata. - */ -export type DidResolutionResult = { - /** - * The resolved DID document. It is undefined if the DID has been upgraded or deleted. +export type RepresentationResolutionOptions = { + /* + * The Media Type of the caller's preferred representation of the DID document. + * The Media Type MUST be expressed as an ASCII string. + * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. + * This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. */ - document?: DidDocument - /** - * The DID resolution metadata. + accept?: Accept +} + +export type SuccessfulRepresentationResolutionMetadata< + ContentType extends string +> = { + /* + * The Media Type of the returned didDocumentStream. + * This property is REQUIRED if resolution is successful and if the resolveRepresentation function was called. + * This property MUST NOT be present if the resolve function was called. + * The value of this property MUST be an ASCII string that is the Media Type of the conformant representations. + * The caller of the resolveRepresentation function MUST use this value when determining how to parse and process the didDocumentStream returned by this function into the data model. */ - metadata: DidResolutionDocumentMetadata - /** - * The DID's web3Name, if any. + contentType: ContentType +} +export type FailedRepresentationResolutionMetadata = { + /* + * The error code from the resolution process. + * This property is REQUIRED when there is an error in the resolution process. + * The value of this property MUST be a single keyword ASCII string. + * The possible property values of this field SHOULD be registered in the DID Specification Registries. + * This specification defines the following common error values: + * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. */ - web3Name?: string + error: 'invalidDid' | 'notFound' | 'representationNotSupported' } -export type ResolvedDidKey = Pick & - Pick +// Either success with `contentType` or failure with `error` +export type RepresentationResolutionMetadata = + | SuccessfulRepresentationResolutionMetadata + | FailedRepresentationResolutionMetadata -export type ResolvedDidServiceEndpoint = ConformingDidServiceEndpoint +export type RepresentationResolutionDocumentMetadata = + ResolutionDocumentMetadata -/** - * Resolves a DID URI, returning the full contents of the DID document. - * - * @param did A DID URI identifying a DID document. All additional parameters and fragments are ignored. - * @returns A promise of a [[DidResolutionResult]] object representing the DID document or null if the DID - * cannot be resolved. - */ -export type DidResolve = (did: DidUri) => Promise +export type RepresentationResolutionResult = Pick< + ResolutionResult, + 'didDocumentMetadata' +> & { + /* + * If the resolution is successful, and if the resolveRepresentation function was called, this MUST be a byte stream of the resolved DID document in one of the conformant representations. + * The byte stream might then be parsed by the caller of the resolveRepresentation function into a data model, which can in turn be validated and processed. + * If the resolution is unsuccessful, this value MUST be an empty stream. + */ + didDocumentStream?: Buffer + didResolutionMetadata: RepresentationResolutionMetadata +} -/** - * Resolves a DID URI identifying a public key associated with a DID. - * - * @param didUri A DID URI identifying a public key associated with a DID through the DID document. - * @returns A promise of a [[ResolvedDidKey]] object representing the DID public key or null if - * the DID or key URI cannot be resolved. +/* + * The resolve function returns the DID document in its abstract form (a map). */ -export type DidResolveKey = ( - didUri: DidResourceUri, - expectedVerificationMethod?: KeyRelationship -) => Promise +export interface ResolveDid { + resolve: ( + /* + * This is the DID to resolve. + * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. + */ + did: DidUri, + /* + * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. + * This input is REQUIRED, but the structure MAY be empty. + */ + resolutionOptions: ResolutionOptions + ) => Promise + + resolveRepresentation: ( + /* + * This is the DID to resolve. + * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. + */ + did: DidUri, + /* + * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. + * This input is REQUIRED, but the structure MAY be empty. + */ + resolutionOptions: RepresentationResolutionOptions + ) => Promise> +} + +export type DereferenceOptions = { + /* + * The Media Type that the caller prefers for contentStream. + * The Media Type MUST be expressed as an ASCII string. + * The DID URL dereferencing implementation SHOULD use this value to determine the contentType of the representation contained in the returned value if such a representation is supported and available. + */ + accept?: Accept +} + +export type SuccessfulDereferenceMetadata = { + /* + * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. + * The Media Type value MUST be expressed as an ASCII string. + */ + contentType: ContentType +} +export type FailedDereferenceMetadata = { + /* + * The error code from the dereferencing process. + * This property is REQUIRED when there is an error in the dereferencing process. + * The value of this property MUST be a single keyword expressed as an ASCII string. + * The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + * This specification defines the following common error values: + * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) + * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. + */ + error: 'invalidDidUrl' | 'notFound' +} + +// Either success with `contentType` or failure with `error` +export type DereferenceMetadata = + | SuccessfulDereferenceMetadata + | FailedDereferenceMetadata + +export type DereferenceContentStream = + | DidDocument + | JsonLd + | VerificationMethod + | JsonLd + | Service + | JsonLd + | Buffer + +export type DereferenceContentMetadata = ResolutionDocumentMetadata + +export type DereferenceResult = { + /* + * A metadata structure consisting of values relating to the results of the DID URL dereferencing process. + * This structure is REQUIRED, and in the case of an error in the dereferencing process, this MUST NOT be empty. + * Properties defined by this specification are in 7.2.2 DID URL Dereferencing Metadata. + * If the dereferencing is not successful, this structure MUST contain an error property describing the error. + */ + dereferencingMetadata: DereferenceMetadata + /* + * If the dereferencing function was called and successful, this MUST contain a resource corresponding to the DID URL. + * The contentStream MAY be a resource such as a DID document that is serializable in one of the conformant representations, a Verification Method, a service, or any other resource format that can be identified via a Media Type and obtained through the resolution process. + * If the dereferencing is unsuccessful, this value MUST be empty. + */ + contentStream?: DereferenceContentStream + /* + * If the dereferencing is successful, this MUST be a metadata structure, but the structure MAY be empty. + * This structure contains metadata about the contentStream. + * If the contentStream is a DID document, this MUST be a didDocumentMetadata structure as described in DID Resolution. + * If the dereferencing is unsuccessful, this output MUST be an empty metadata structure. + */ + contentMetadata: DereferenceContentMetadata +} + +export interface DereferenceDidUrl { + dereference: ( + /* + * A conformant DID URL as a single string. + * This is the DID URL to dereference. + * To dereference a DID fragment, the complete DID URL including the DID fragment MUST be used. This input is REQUIRED. + */ + didUrl: DidUri | DidUrl, + /* + * A metadata structure consisting of input options to the dereference function in addition to the didUrl itself. + * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. + * This input is REQUIRED, but the structure MAY be empty. + */ + dereferenceOptions: DereferenceOptions + ) => Promise> +} + +export interface DidResolver + extends ResolveDid, + DereferenceDidUrl {} diff --git a/packages/types/src/DidResolverV2.ts b/packages/types/src/DidResolverV2.ts deleted file mode 100644 index 4f63697a77..0000000000 --- a/packages/types/src/DidResolverV2.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { - DidUri, - DidDocument, - DidUrl, - VerificationMethod, - Service, - JsonLd, -} from './DidDocumentV2' - -/* - * The `accept` header must not be used for the regular `resolve` function, so we enforce that statically. - * For more info, please refer to https://www.w3.org/TR/did-core/#did-resolution-options. - */ -export type ResolutionOptions = Record - -export type ResolutionMetadata = { - /* - * The error code from the resolution process. - * This property is REQUIRED when there is an error in the resolution process. - * The value of this property MUST be a single keyword ASCII string. - * The possible property values of this field SHOULD be registered in the DID Specification Registries. - * This specification defines the following common error values: - * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. - * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. - */ - error?: 'invalidDid' | 'notFound' -} - -export type ResolutionDocumentMetadata = { - /* - * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. - * If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. - */ - deactivated?: true - /* - * DID document metadata MAY include a canonicalId property. - * If present, the value MUST be a string that conforms to the rules in Section 3.1 DID Syntax. - * The relationship is a statement that the canonicalId value is logically equivalent to the id property value and that the canonicalId value is defined by the DID method to be the canonical ID for the DID subject in the scope of the containing DID document. - * A canonicalId value MUST be produced by, and a form of, the same DID method as the id property value. (e.g., did:example:abc == did:example:ABC). - */ - canonicalId?: DidUri -} - -export type ResolutionResult = { - /* - * A metadata structure consisting of values relating to the results of the DID resolution process which typically changes between invocations of the resolve and resolveRepresentation functions, as it represents data about the resolution process itself. - * This structure is REQUIRED, and in the case of an error in the resolution process, this MUST NOT be empty. - * If resolveRepresentation was called, this structure MUST contain a contentType property containing the Media Type of the representation found in the didDocumentStream. - * If the resolution is not successful, this structure MUST contain an error property describing the error. - * The possible properties within this structure and their possible values are registered in the DID Specification Registries. - */ - didResolutionMetadata: ResolutionMetadata - /* - * If the resolution is successful, and if the resolve function was called, this MUST be a DID document abstract data model (a map) as described in 4. Data Model that is capable of being transformed into a conforming DID Document (representation), using the production rules specified by the representation. - * The value of id in the resolved DID document MUST match the DID that was resolved. - * If the resolution is unsuccessful, this value MUST be empty. - */ - didDocument?: DidDocument - /* - * If the resolution is successful, this MUST be a metadata structure. - * This structure contains metadata about the DID document contained in the didDocument property. - * This metadata typically does not change between invocations of the resolve and resolveRepresentation functions unless the DID document changes, as it represents metadata about the DID document. - * If the resolution is unsuccessful, this output MUST be an empty metadata structure. - * The possible properties within this structure and their possible values SHOULD be registered in the DID Specification Registries. - */ - didDocumentMetadata: ResolutionDocumentMetadata -} - -export type RepresentationResolutionOptions = { - /* - * The Media Type of the caller's preferred representation of the DID document. - * The Media Type MUST be expressed as an ASCII string. - * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. - * This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. - */ - accept?: Accept -} - -export type SuccessfulRepresentationResolutionMetadata< - ContentType extends string -> = { - /* - * The Media Type of the returned didDocumentStream. - * This property is REQUIRED if resolution is successful and if the resolveRepresentation function was called. - * This property MUST NOT be present if the resolve function was called. - * The value of this property MUST be an ASCII string that is the Media Type of the conformant representations. - * The caller of the resolveRepresentation function MUST use this value when determining how to parse and process the didDocumentStream returned by this function into the data model. - */ - contentType: ContentType -} -export type FailedRepresentationResolutionMetadata = { - /* - * The error code from the resolution process. - * This property is REQUIRED when there is an error in the resolution process. - * The value of this property MUST be a single keyword ASCII string. - * The possible property values of this field SHOULD be registered in the DID Specification Registries. - * This specification defines the following common error values: - * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. - * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. - * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. - */ - error: 'invalidDid' | 'notFound' | 'representationNotSupported' -} - -// Either success with `contentType` or failure with `error` -export type RepresentationResolutionMetadata = - | SuccessfulRepresentationResolutionMetadata - | FailedRepresentationResolutionMetadata - -export type RepresentationResolutionDocumentMetadata = - ResolutionDocumentMetadata - -export type RepresentationResolutionResult = Pick< - ResolutionResult, - 'didDocumentMetadata' -> & { - /* - * If the resolution is successful, and if the resolveRepresentation function was called, this MUST be a byte stream of the resolved DID document in one of the conformant representations. - * The byte stream might then be parsed by the caller of the resolveRepresentation function into a data model, which can in turn be validated and processed. - * If the resolution is unsuccessful, this value MUST be an empty stream. - */ - didDocumentStream?: Buffer - didResolutionMetadata: RepresentationResolutionMetadata -} - -/* - * The resolve function returns the DID document in its abstract form (a map). - */ -export interface ResolveDid { - resolve: ( - /* - * This is the DID to resolve. - * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. - */ - did: DidUri, - /* - * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. - * This input is REQUIRED, but the structure MAY be empty. - */ - resolutionOptions: ResolutionOptions - ) => Promise - - resolveRepresentation: ( - /* - * This is the DID to resolve. - * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. - */ - did: DidUri, - /* - * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. - * This input is REQUIRED, but the structure MAY be empty. - */ - resolutionOptions: RepresentationResolutionOptions - ) => Promise> -} - -export type DereferenceOptions = { - /* - * The Media Type that the caller prefers for contentStream. - * The Media Type MUST be expressed as an ASCII string. - * The DID URL dereferencing implementation SHOULD use this value to determine the contentType of the representation contained in the returned value if such a representation is supported and available. - */ - accept?: Accept -} - -export type SuccessfulDereferenceMetadata = { - /* - * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. - * The Media Type value MUST be expressed as an ASCII string. - */ - contentType: ContentType -} -export type FailedDereferenceMetadata = { - /* - * The error code from the dereferencing process. - * This property is REQUIRED when there is an error in the dereferencing process. - * The value of this property MUST be a single keyword expressed as an ASCII string. - * The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. - * This specification defines the following common error values: - * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) - * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. - */ - error: 'invalidDidUrl' | 'notFound' -} - -// Either success with `contentType` or failure with `error` -export type DereferenceMetadata = - | SuccessfulDereferenceMetadata - | FailedDereferenceMetadata - -export type DereferenceContentStream = - | DidDocument - | JsonLd - | VerificationMethod - | JsonLd - | Service - | JsonLd - | Buffer - -export type DereferenceContentMetadata = ResolutionDocumentMetadata - -export type DereferenceResult = { - /* - * A metadata structure consisting of values relating to the results of the DID URL dereferencing process. - * This structure is REQUIRED, and in the case of an error in the dereferencing process, this MUST NOT be empty. - * Properties defined by this specification are in 7.2.2 DID URL Dereferencing Metadata. - * If the dereferencing is not successful, this structure MUST contain an error property describing the error. - */ - dereferencingMetadata: DereferenceMetadata - /* - * If the dereferencing function was called and successful, this MUST contain a resource corresponding to the DID URL. - * The contentStream MAY be a resource such as a DID document that is serializable in one of the conformant representations, a Verification Method, a service, or any other resource format that can be identified via a Media Type and obtained through the resolution process. - * If the dereferencing is unsuccessful, this value MUST be empty. - */ - contentStream?: DereferenceContentStream - /* - * If the dereferencing is successful, this MUST be a metadata structure, but the structure MAY be empty. - * This structure contains metadata about the contentStream. - * If the contentStream is a DID document, this MUST be a didDocumentMetadata structure as described in DID Resolution. - * If the dereferencing is unsuccessful, this output MUST be an empty metadata structure. - */ - contentMetadata: DereferenceContentMetadata -} - -export interface DereferenceDidUrl { - dereference: ( - /* - * A conformant DID URL as a single string. - * This is the DID URL to dereference. - * To dereference a DID fragment, the complete DID URL including the DID fragment MUST be used. This input is REQUIRED. - */ - didUrl: DidUri | DidUrl, - /* - * A metadata structure consisting of input options to the dereference function in addition to the didUrl itself. - * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. - * This input is REQUIRED, but the structure MAY be empty. - */ - dereferenceOptions: DereferenceOptions - ) => Promise> -} - -export interface DidResolver - extends ResolveDid, - DereferenceDidUrl {} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 50445a8be2..0e5db8b307 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -24,10 +24,5 @@ export * from './Credential.js' export * from './DidDocument.js' export * from './CryptoCallbacks.js' export * from './DidResolver.js' -export * from './DidDocumentExporter.js' export * from './PublicCredential.js' export * from './Imported.js' - -export * as DidDocumentV2 from './DidDocumentV2.js' -export * as DidResolverV2 from './DidResolverV2.js' -export * as CryptoCallbacksV2 from './CryptoCallbacksV2.js' From c5caaeefd1da0108cdddccf5452154a05a050e86 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 10 Oct 2023 11:45:33 +0100 Subject: [PATCH 34/78] Fix DID dereferencing and signature resolution --- packages/did/src/Did.signature.ts | 35 +++++++++--------- packages/did/src/DidResolver/DidResolver.ts | 40 ++++++++++++++++++--- packages/types/src/DidResolver.ts | 20 ++++++++++- 3 files changed, 70 insertions(+), 25 deletions(-) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 888f2a5f33..24340917ac 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -6,18 +6,19 @@ */ import type { + DereferenceDidUrl, DidUri, DidUrl, - ResolveDid, SignatureVerificationMethodRelationship, SignResponseData, + VerificationMethod, } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { isHex } from '@polkadot/util' import { multibaseKeyToDidKey, parse, validateUri } from './Did.utils.js' -import { resolve } from './DidResolver/DidResolver.js' +import { dereference } from './DidResolver/DidResolver.js' export type DidSignatureVerificationInput = { message: string | Uint8Array @@ -26,7 +27,7 @@ export type DidSignatureVerificationInput = { expectedSigner?: DidUri allowUpgraded?: boolean expectedVerificationMethodRelationship?: SignatureVerificationMethodRelationship - resolveDid?: ResolveDid['resolve'] + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } export type DidSignature = { @@ -76,7 +77,7 @@ function verifyDidSignatureDataStructure( * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. - * @param input.resolveDid Allows specifying a custom DID resolve. Defaults to the built-in [[resolve]]. + * @param input.dereferenceDidUrl Allows specifying a custom DID dereference. Defaults to the built-in [[dereferenceDidUrl]]. */ export async function verifyDidSignature({ message, @@ -85,7 +86,7 @@ export async function verifyDidSignature({ expectedSigner, allowUpgraded = false, expectedVerificationMethodRelationship, - resolveDid = resolve, + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: DidSignatureVerificationInput): Promise { // checks if key uri points to the right did; alternatively we could check the key's controller const signer = parse(signerUrl) @@ -107,30 +108,26 @@ export async function verifyDidSignature({ } } - const { didDocument } = await resolveDid(signerUrl, {}) - if (didDocument === undefined) { - throw new SDKErrors.SignatureUnverifiableError( - `Error validating the DID signature. Cannot fetch DID Document for "${signerUrl}".` - ) - } - const verificationMethod = didDocument.verificationMethod?.find( - (vm) => vm.id === signer.fragment + const { contentStream, contentMetadata } = await dereferenceDidUrl( + signerUrl, + {} ) - if (verificationMethod === undefined) { + if (contentStream === undefined) { throw new SDKErrors.SignatureUnverifiableError( - `Cannot find verification method with ID "${signer.fragment} in the DID Document for "${signerUrl}".` + `Error validating the DID signature. Cannot fetch DID Document or the verification method for "${signerUrl}".` ) } if ( expectedVerificationMethodRelationship !== undefined && - didDocument[expectedVerificationMethodRelationship]?.find( - (vm) => vm === signer.fragment - ) === undefined + !contentMetadata?.verificationRelationship?.includes( + expectedVerificationMethodRelationship + ) ) { throw new SDKErrors.SignatureUnverifiableError( - `Cannot find verification method with ID "${signer.fragment} for the relationship "${expectedVerificationMethodRelationship}".` + `Cannot find verification "${signerUrl} for the relationship "${expectedVerificationMethodRelationship}".` ) } + const verificationMethod = contentStream as VerificationMethod const { publicKey } = multibaseKeyToDidKey( verificationMethod.publicKeyMultibase diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 4efc93229a..b09a66315b 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -19,6 +19,7 @@ import type { ResolutionDocumentMetadata, ResolutionOptions, ResolutionResult, + VerificationMethodRelationship, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' @@ -228,21 +229,50 @@ async function dereferenceInternal( contentMetadata: didDocumentMetadata, } } - const dereferencedResource = (() => { + // Return the dereferenced resource and its set of relationships with the controlling DID Document. + const [dereferencedResource, verificationRelationship] = (() => { const verificationMethod = didDocument?.verificationMethod?.find( - (vm) => vm.id === fragment + (vm) => vm.controller === didDocument.id && vm.id === fragment ) if (verificationMethod !== undefined) { - return verificationMethod + const verificationRelationships: VerificationMethodRelationship[] = [] + if ( + didDocument?.authentication?.find((a) => a === verificationMethod.id) + ) { + verificationRelationships.push('authentication') + } + if ( + didDocument?.assertionMethod?.find((a) => a === verificationMethod.id) + ) { + verificationRelationships.push('assertionMethod') + } + if ( + didDocument?.capabilityDelegation?.find( + (a) => a === verificationMethod.id + ) + ) { + verificationRelationships.push('capabilityDelegation') + } + if (didDocument?.keyAgreement?.find((a) => a === verificationMethod.id)) { + verificationRelationships.push('keyAgreement') + } + return [ + verificationMethod, + verificationRelationships.length > 0 + ? verificationRelationships + : undefined, + ] } const service = didDocument?.service?.find((s) => s.id === fragment) - return service + return [service, undefined] })() return { contentStream: dereferencedResource, - contentMetadata: {}, + contentMetadata: { + verificationRelationship, + }, } } diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index dcf0d038b8..69079062d8 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -12,6 +12,7 @@ import type { VerificationMethod, Service, JsonLd, + VerificationMethodRelationship, } from './DidDocument' /* @@ -170,6 +171,16 @@ export type DereferenceOptions = { accept?: Accept } +export type SignatureVerificationRelationship = + | 'authentication' + | 'capabilityDelegation' + | 'assertionMethod' +export type EncryptionRelationship = 'keyAgreement' + +export type VerificationRelationship = + | SignatureVerificationRelationship + | EncryptionRelationship + export type SuccessfulDereferenceMetadata = { /* * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. @@ -204,7 +215,14 @@ export type DereferenceContentStream = | JsonLd | Buffer -export type DereferenceContentMetadata = ResolutionDocumentMetadata +export type DereferenceContentMetadata = ResolutionDocumentMetadata & { + /* + * NOT YET DRAFTED. DRAFTING WORK WILL START SOON. + * This field is optional and is set only if the dereferenced object is a verification method and it belongs to one of the verification methods of the DID Document. + * This field is empty if the dereferenced object is a full DID Document or a service, or if the dereferences verification method is not linked to the DID Document by any specific relationship. + */ + verificationRelationship?: VerificationRelationship[] +} export type DereferenceResult = { /* From 0fb7c57a738b3d88aeae200ee6a22be561e6c9f7 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 10 Oct 2023 11:56:10 +0100 Subject: [PATCH 35/78] Compiling! --- .../credentialsV1/KiltRevocationStatusV1.ts | 2 +- packages/did/src/Did.chain.ts | 4 ++-- packages/did/src/Did.signature.ts | 4 ++-- packages/did/src/DidDetails/DidDetails.ts | 6 ++--- packages/did/src/DidDetails/FullDidDetails.ts | 10 ++++---- packages/did/src/DidResolver/DidResolver.ts | 4 ++-- packages/legacy-credentials/src/Credential.ts | 24 +++++++++++-------- packages/legacy-credentials/src/vcInterop.ts | 2 +- packages/types/src/CryptoCallbacks.ts | 4 ++-- packages/types/src/DidDocument.ts | 10 ++++---- packages/types/src/DidResolver.ts | 12 +--------- 11 files changed, 38 insertions(+), 44 deletions(-) diff --git a/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts b/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts index ce303a74a9..f2d8525a79 100644 --- a/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts +++ b/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts @@ -49,7 +49,7 @@ export async function check( `Cannot handle revocation status checks for asset type ${assetNamespace}:${assetReference}` ) } - if (!assetInstance) { + if (assetInstance === undefined) { throw new SDKErrors.CredentialMalformedError( "The attestation record's CAIP-19 identifier must contain an asset index ('token_id') decoding to the credential root hash" ) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index fdd54f2b43..dce8b5e774 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -22,7 +22,7 @@ import type { DidUri, KiltAddress, Service, - SignatureVerificationMethodRelationship, + SignatureVerificationRelationship, SignExtrinsicCallback, SignRequestData, SignResponseData, @@ -606,7 +606,7 @@ export async function getStoreTxFromDidDocument( export interface SigningOptions { sign: SignExtrinsicCallback - verificationMethodRelationship: SignatureVerificationMethodRelationship + verificationMethodRelationship: SignatureVerificationRelationship } /** diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 24340917ac..1dc8628ecc 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -9,7 +9,7 @@ import type { DereferenceDidUrl, DidUri, DidUrl, - SignatureVerificationMethodRelationship, + SignatureVerificationRelationship, SignResponseData, VerificationMethod, } from '@kiltprotocol/types' @@ -26,7 +26,7 @@ export type DidSignatureVerificationInput = { signerUrl: DidUrl expectedSigner?: DidUri allowUpgraded?: boolean - expectedVerificationMethodRelationship?: SignatureVerificationMethodRelationship + expectedVerificationMethodRelationship?: SignatureVerificationRelationship dereferenceDidUrl?: DereferenceDidUrl['dereference'] } diff --git a/packages/did/src/DidDetails/DidDetails.ts b/packages/did/src/DidDetails/DidDetails.ts index 8f4aecd50b..13e1848136 100644 --- a/packages/did/src/DidDetails/DidDetails.ts +++ b/packages/did/src/DidDetails/DidDetails.ts @@ -10,7 +10,7 @@ import type { Service, UriFragment, VerificationMethod, - VerificationMethodRelationship, + VerificationRelationship, } from '@kiltprotocol/types' import { didKeyToVerificationMethod } from '../Did.utils.js' @@ -85,7 +85,7 @@ function doesVerificationMethodExist( function addVerificationMethod( didDocument: DidDocument, verificationMethod: VerificationMethod, - relationship: VerificationMethodRelationship + relationship: VerificationRelationship ): void { const existingRelationship = didDocument[relationship] ?? [] existingRelationship.push(verificationMethod.id) @@ -113,7 +113,7 @@ function addVerificationMethod( export function addKeypairAsVerificationMethod( didDocument: DidDocument, { id, publicKey, type: keyType }: BaseNewDidKey & { id: UriFragment }, - relationship: VerificationMethodRelationship + relationship: VerificationRelationship ): void { const verificationMethod = didKeyToVerificationMethod(didDocument.id, id, { keyType: keyType as DidKeyType, diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 202df522d0..824dece014 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -10,7 +10,7 @@ import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' import type { DidUri, KiltAddress, - SignatureVerificationMethodRelationship, + SignatureVerificationRelationship, SignExtrinsicCallback, SubmittableExtrinsic, } from '@kiltprotocol/types' @@ -32,7 +32,7 @@ import { // TODO: Should have an RPC or something similar to avoid inconsistencies in the future. const methodMapping: Record< string, - SignatureVerificationMethodRelationship | undefined + SignatureVerificationRelationship | undefined > = { attestation: 'assertionMethod', ctype: 'assertionMethod', @@ -48,7 +48,7 @@ const methodMapping: Record< function getVerificationMethodRelationshipForRuntimeCall( call: Extrinsic['method'] -): SignatureVerificationMethodRelationship | undefined { +): SignatureVerificationRelationship | undefined { const { section, method } = call // get the VerificationKeyRelationship of a batched call @@ -79,7 +79,7 @@ function getVerificationMethodRelationshipForRuntimeCall( */ export function getVerificationMethodRelationshipForTx( extrinsic: Extrinsic -): SignatureVerificationMethodRelationship | undefined { +): SignatureVerificationRelationship | undefined { return getVerificationMethodRelationshipForRuntimeCall(extrinsic.method) } @@ -156,7 +156,7 @@ export async function authorizeTx( type GroupedExtrinsics = Array<{ extrinsics: Extrinsic[] - verificationMethodRelationship: SignatureVerificationMethodRelationship + verificationMethodRelationship: SignatureVerificationRelationship }> function groupExtrinsicsByKeyRelationship( diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index b09a66315b..211f5e2466 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -19,7 +19,7 @@ import type { ResolutionDocumentMetadata, ResolutionOptions, ResolutionResult, - VerificationMethodRelationship, + VerificationRelationship, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' @@ -235,7 +235,7 @@ async function dereferenceInternal( (vm) => vm.controller === didDocument.id && vm.id === fragment ) if (verificationMethod !== undefined) { - const verificationRelationships: VerificationMethodRelationship[] = [] + const verificationRelationships: VerificationRelationship[] = [] if ( didDocument?.authentication?.find((a) => a === verificationMethod.id) ) { diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index d9a5073526..fa9aef8e02 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -21,13 +21,12 @@ import { ConfigService } from '@kiltprotocol/config' import { Attestation, CType } from '@kiltprotocol/core' import { isDidSignature, - resolve, + dereference, signatureFromJson, signatureToJson, verifyDidSignature, } from '@kiltprotocol/did' import type { - ResolveDid, DidUri, Hash, IAttestation, @@ -37,6 +36,7 @@ import type { ICredentialPresentation, IDelegationNode, SignCallback, + DereferenceDidUrl, } from '@kiltprotocol/types' import { Crypto, DataUtils, SDKErrors } from '@kiltprotocol/utils' import * as Claim from './Claim.js' @@ -205,17 +205,17 @@ export function verifyDataStructure(input: ICredential): void { * * @param input - The [[ICredentialPresentation]]. * @param verificationOpts Additional verification options. - * @param verificationOpts.resolveDid - The function used to resolve the claimer's DID Document and verification method. Defaults to [[resolve]]. + * @param verificationOpts.dereferenceDidUrl - The function used to resolve the claimer's DID Document and verification method. Defaults to [[dereferenceDidUrl]]. * @param verificationOpts.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. */ export async function verifySignature( input: ICredentialPresentation, { challenge, - resolveDid = resolve, + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: { challenge?: string - resolveDid?: ResolveDid['resolve'] + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } = {} ): Promise { const { claimerSignature } = input @@ -234,7 +234,7 @@ export async function verifySignature( allowUpgraded: true, expectedVerificationMethodRelationship: 'authentication', signerUrl: claimerSignature.signerUrl, - resolveDid, + dereferenceDidUrl, }) } @@ -280,7 +280,7 @@ export function fromClaim( type VerifyOptions = { ctype?: ICType challenge?: string - resolveDid?: ResolveDid['resolve'] + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } /** @@ -433,17 +433,21 @@ export async function verifyCredential( * @param options - Additional parameter for more verification steps. * @param options.ctype - CType which the included claim should be checked against. * @param options.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. - * @param options.resolveDid - The function used to resolve the claimer's key. Defaults to [[resolveKey]]. + * @param options.dereferenceDidUrl - The function used to resolve the claimer's verification method. Defaults to [[dereference]]. * @returns A [[VerifiedCredential]] object, which is the orignal credential presentation with two additional properties: * a boolean `revoked` status flag and the `attester` DID. */ export async function verifyPresentation( presentation: ICredentialPresentation, - { ctype, challenge, resolveDid = resolve }: VerifyOptions = {} + { + ctype, + challenge, + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + }: VerifyOptions = {} ): Promise { await verifySignature(presentation, { challenge, - resolveDid, + dereferenceDidUrl, }) return verifyCredential(presentation, { ctype }) } diff --git a/packages/legacy-credentials/src/vcInterop.ts b/packages/legacy-credentials/src/vcInterop.ts index 0e30e6280b..6bcdd82ca7 100644 --- a/packages/legacy-credentials/src/vcInterop.ts +++ b/packages/legacy-credentials/src/vcInterop.ts @@ -167,7 +167,7 @@ export function fromVC(input: Types.KiltCredentialV1): ICredential { ) const legitimations = (legitimationVcs ?? []).map( ({ id, verifiableCredential }) => - verifiableCredential + verifiableCredential !== undefined ? fromVC(verifiableCredential) : ({ rootHash: u8aToHex(KiltCredentialV1.idToRootHash(id)), diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 03e2114d1c..69bdad24ad 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -7,7 +7,7 @@ import type { DidUri, - SignatureVerificationMethodRelationship, + SignatureVerificationRelationship, VerificationMethod, } from './DidDocument' @@ -23,7 +23,7 @@ export interface SignRequestData { /** * The did key relationship to be used. */ - verificationMethodRelationship: SignatureVerificationMethodRelationship + verificationMethodRelationship: SignatureVerificationRelationship /** * The DID to be used for signing. diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 1fb635ac38..e98a9743c5 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -27,15 +27,15 @@ export type UriFragment = `#${string}` */ export type DidUrl = `${DidUri}${UriFragment}` -export type SignatureVerificationMethodRelationship = +export type SignatureVerificationRelationship = | 'authentication' | 'capabilityDelegation' | 'assertionMethod' -export type EncryptionMethodRelationship = 'keyAgreement' +export type EncryptionRelationship = 'keyAgreement' -export type VerificationMethodRelationship = - | SignatureVerificationMethodRelationship - | EncryptionMethodRelationship +export type VerificationRelationship = + | SignatureVerificationRelationship + | EncryptionRelationship type Base58BtcMultibaseString = `z${string}` diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index 69079062d8..a2842af5e1 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -12,7 +12,7 @@ import type { VerificationMethod, Service, JsonLd, - VerificationMethodRelationship, + VerificationRelationship, } from './DidDocument' /* @@ -171,16 +171,6 @@ export type DereferenceOptions = { accept?: Accept } -export type SignatureVerificationRelationship = - | 'authentication' - | 'capabilityDelegation' - | 'assertionMethod' -export type EncryptionRelationship = 'keyAgreement' - -export type VerificationRelationship = - | SignatureVerificationRelationship - | EncryptionRelationship - export type SuccessfulDereferenceMetadata = { /* * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. From 66a0e5d9ec61a94fffdd57967e8cabb10dc9c99f Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 10 Oct 2023 12:00:19 +0100 Subject: [PATCH 36/78] Compiling after applying stash --- packages/did/src/Did.signature.ts | 6 +- packages/types/src/DidResolver.ts | 2 +- tests/integration/AccountLinking.spec.ts | 36 +- tests/integration/Attestation.spec.ts | 64 +- tests/integration/Did.spec.ts | 557 +++++--- tests/integration/Did2.spec.ts | 1478 ---------------------- tests/testUtils/TestUtils.ts | 207 ++- tests/testUtils/TestUtils2.ts | 489 ------- tests/testUtils/index.ts | 2 - 9 files changed, 556 insertions(+), 2285 deletions(-) delete mode 100644 tests/integration/Did2.spec.ts delete mode 100644 tests/testUtils/TestUtils2.ts diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 1dc8628ecc..a2c9074667 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -185,9 +185,9 @@ export function signatureToJson({ export function signatureFromJson( input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 ): Pick & { - verificationMethodUri: DidUrl + signerUrl: DidUrl } { - const verificationMethodUri = (() => { + const signerUrl = (() => { if ('keyUri' in input) { return input.keyUri } @@ -197,5 +197,5 @@ export function signatureFromJson( return input.signerUrl })() const signature = Crypto.coToUInt8(input.signature) - return { signature, verificationMethodUri } + return { signature, signerUrl } } diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index a2842af5e1..b356b58b11 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -254,6 +254,6 @@ export interface DereferenceDidUrl { ) => Promise> } -export interface DidResolver +export interface DidResolver extends ResolveDid, DereferenceDidUrl {} diff --git a/tests/integration/AccountLinking.spec.ts b/tests/integration/AccountLinking.spec.ts index 51b9da0ae7..967f263135 100644 --- a/tests/integration/AccountLinking.spec.ts +++ b/tests/integration/AccountLinking.spec.ts @@ -65,7 +65,7 @@ describe('When there is an on-chain DID', () => { const associateSenderTx = api.tx.didLookup.associateSender() const signedTx = await Did.authorizeTx( - did.uri, + did.id, associateSenderTx, didKey.getSignCallback(did), paymentAccount.address @@ -92,12 +92,12 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([paymentAccount.address]) - expect(queryByAccount.document.uri).toStrictEqual(did.uri) + expect(queryByAccount.document.id).toStrictEqual(did.id) }, 30_000) it('should be possible to associate the tx sender to a new DID', async () => { const associateSenderTx = api.tx.didLookup.associateSender() const signedTx = await Did.authorizeTx( - newDid.uri, + newDid.id, associateSenderTx, newDidKey.getSignCallback(newDid), paymentAccount.address @@ -120,7 +120,7 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([paymentAccount.address]) - expect(queryByAccount.document.uri).toStrictEqual(newDid.uri) + expect(queryByAccount.document.id).toStrictEqual(newDid.id) }, 30_000) it('should be possible for the sender to remove the link', async () => { const removeSenderTx = api.tx.didLookup.removeSenderAssociation() @@ -174,11 +174,11 @@ describe('When there is an on-chain DID', () => { } const args = await Did.associateAccountToChainArgs( keypair.address, - did.uri, + did.id, async (payload) => keypair.sign(payload, { withType: false }) ) const signedTx = await Did.authorizeTx( - did.uri, + did.id, api.tx.didLookup.associateAccount(...args), didKey.getSignCallback(did), paymentAccount.address @@ -204,7 +204,7 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([keypair.address]) - expect(queryByAccount.document.uri).toStrictEqual(did.uri) + expect(queryByAccount.document.id).toStrictEqual(did.id) }) it('should be possible to associate the account to a new DID while the sender pays the deposit', async () => { if (skip) { @@ -212,11 +212,11 @@ describe('When there is an on-chain DID', () => { } const args = await Did.associateAccountToChainArgs( keypair.address, - newDid.uri, + newDid.id, async (payload) => keypair.sign(payload, { withType: false }) ) const signedTx = await Did.authorizeTx( - newDid.uri, + newDid.id, api.tx.didLookup.associateAccount(...args), newDidKey.getSignCallback(newDid), paymentAccount.address @@ -239,7 +239,7 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([keypair.address]) - expect(queryByAccount.document.uri).toStrictEqual(newDid.uri) + expect(queryByAccount.document.id).toStrictEqual(newDid.id) }) it('should be possible for the DID to remove the link', async () => { if (skip) { @@ -248,7 +248,7 @@ describe('When there is an on-chain DID', () => { const removeLinkTx = api.tx.didLookup.removeAccountAssociation(keypairChain) const signedTx = await Did.authorizeTx( - newDid.uri, + newDid.id, removeLinkTx, newDidKey.getSignCallback(newDid), paymentAccount.address @@ -271,7 +271,7 @@ describe('When there is an on-chain DID', () => { ) expect(encodedQueryByAccount.isNone).toBe(true) const encodedQueryByDid = await api.call.did.query( - Did.toChain(newDid.uri) + Did.toChain(newDid.id) ) const queryByDid = Did.linkedInfoFromChain(encodedQueryByDid) expect(queryByDid.accounts).toStrictEqual([]) @@ -299,11 +299,11 @@ describe('When there is an on-chain DID', () => { it('should be possible to associate the account while the sender pays the deposit', async () => { const args = await Did.associateAccountToChainArgs( genericAccount.address, - did.uri, + did.id, async (payload) => genericAccount.sign(payload, { withType: true }) ) const signedTx = await Did.authorizeTx( - did.uri, + did.id, api.tx.didLookup.associateAccount(...args), didKey.getSignCallback(did), paymentAccount.address @@ -330,13 +330,13 @@ describe('When there is an on-chain DID', () => { // Use generic substrate address prefix const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount, 42) expect(queryByAccount.accounts).toStrictEqual([genericAccount.address]) - expect(queryByAccount.document.uri).toStrictEqual(did.uri) + expect(queryByAccount.document.id).toStrictEqual(did.id) }) it('should be possible to add a Web3 name for the linked DID and retrieve it starting from the linked account', async () => { const web3NameClaimTx = api.tx.web3Names.claim('test-name') const signedTx = await Did.authorizeTx( - did.uri, + did.id, web3NameClaimTx, didKey.getSignCallback(did), paymentAccount.address @@ -346,13 +346,13 @@ describe('When there is an on-chain DID', () => { // Check that the Web3 name has been linked to the DID const encodedQueryByW3n = await api.call.did.queryByWeb3Name('test-name') const queryByW3n = Did.linkedInfoFromChain(encodedQueryByW3n) - expect(queryByW3n.document.uri).toStrictEqual(did.uri) + expect(queryByW3n.document.id).toStrictEqual(did.id) // Check that it is possible to retrieve the web3 name from the account linked to the DID const encodedQueryByAccount = await api.call.did.queryByAccount( Did.accountToChain(genericAccount.address) ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) - expect(queryByAccount.web3Name).toStrictEqual('test-name') + expect(queryByAccount.document.alsoKnownAs).toStrictEqual(['test-name']) }) it('should be possible for the sender to remove the link', async () => { diff --git a/tests/integration/Attestation.spec.ts b/tests/integration/Attestation.spec.ts index 67690ea491..ed23405582 100644 --- a/tests/integration/Attestation.spec.ts +++ b/tests/integration/Attestation.spec.ts @@ -77,7 +77,7 @@ describe('handling attestations that do not exist', () => { it('Attestation.getRevokeTx', async () => { const draft = api.tx.attestation.revoke(claimHash, null) const authorized = await Did.authorizeTx( - attester.uri, + attester.id, draft, attesterKey.getSignCallback(attester), tokenHolder.address @@ -91,7 +91,7 @@ describe('handling attestations that do not exist', () => { it('Attestation.getRemoveTx', async () => { const draft = api.tx.attestation.remove(claimHash, null) const authorized = await Did.authorizeTx( - attester.uri, + attester.id, draft, attesterKey.getSignCallback(attester), tokenHolder.address @@ -108,7 +108,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const ctypeExists = await isCtypeOnChain(driversLicenseCType) if (ctypeExists) return const tx = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.ctype.add(CType.toChain(driversLicenseCType)), attesterKey.getSignCallback(attester), tokenHolder.address @@ -121,7 +121,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim) const presentation = await Credential.createPresentation({ @@ -141,7 +141,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim) expect(() => Credential.verifyDataIntegrity(credential)).not.toThrow() @@ -157,7 +157,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const attestation = Attestation.fromCredentialAndDid( presentation, - attester.uri + attester.id ) const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -165,7 +165,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -180,7 +180,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { await expect( Credential.verifyPresentation(presentation) - ).resolves.toMatchObject({ attester: attester.uri, revoked: false }) + ).resolves.toMatchObject({ attester: attester.id, revoked: false }) // Claim the deposit back by submitting the reclaimDeposit extrinsic with the deposit payer's account. const reclaimTx = api.tx.attestation.reclaimDeposit(attestation.claimHash) @@ -202,7 +202,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim) expect(() => Credential.verifyDataIntegrity(credential)).not.toThrow() @@ -217,7 +217,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const attestation = Attestation.fromCredentialAndDid( presentation, - attester.uri + attester.id ) const { keypair, getSignCallback } = makeSigningKeyTool() @@ -227,7 +227,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, getSignCallback(attester), keypair.address @@ -254,15 +254,11 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const content = { name: 'Ralph', weight: 120 } - const claim = Claim.fromCTypeAndClaimContents( - badCtype, - content, - claimer.uri - ) + const claim = Claim.fromCTypeAndClaimContents(badCtype, content, claimer.id) const credential = Credential.fromClaim(claim) const attestation = Attestation.fromCredentialAndDid( credential, - attester.uri + attester.id ) const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -270,7 +266,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -293,21 +289,21 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) credential = Credential.fromClaim(claim) const presentation = await Credential.createPresentation({ credential, signCallback: claimerKey.getSignCallback(claimer), }) - attestation = Attestation.fromCredentialAndDid(credential, attester.uri) + attestation = Attestation.fromCredentialAndDid(credential, attester.id) const storeTx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -330,7 +326,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -349,7 +345,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const fakeCredential = Credential.fromClaim(claim) await Credential.createPresentation({ @@ -365,7 +361,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { it('should not be possible for the claimer to revoke an attestation', async () => { const revokeTx = api.tx.attestation.revoke(attestation.claimHash, null) const authorizedRevokeTx = await Did.authorizeTx( - claimer.uri, + claimer.id, revokeTx, claimerKey.getSignCallback(claimer), tokenHolder.address @@ -395,7 +391,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const revokeTx = api.tx.attestation.revoke(attestation.claimHash, null) const authorizedRevokeTx = await Did.authorizeTx( - attester.uri, + attester.id, revokeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -411,13 +407,13 @@ describe('When there is an attester, claimer and ctype drivers license', () => { await expect( Credential.verifyCredential(credential) - ).resolves.toMatchObject({ attester: attester.uri, revoked: true }) + ).resolves.toMatchObject({ attester: attester.id, revoked: true }) }, 40_000) it('should be possible for the deposit payer to remove an attestation', async () => { const removeTx = api.tx.attestation.remove(attestation.claimHash, null) const authorizedRemoveTx = await Did.authorizeTx( - attester.uri, + attester.id, removeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -446,7 +442,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { CType.toChain(officialLicenseAuthorityCType) ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -464,7 +460,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { LicenseType: "Driver's License", LicenseSubtypes: 'sports cars, tanks', }, - attester.uri + attester.id ) const credential1 = Credential.fromClaim(licenseAuthorization) await Credential.createPresentation({ @@ -473,7 +469,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const licenseAuthorizationGranted = Attestation.fromCredentialAndDid( credential1, - anotherAttester.uri + anotherAttester.id ) const storeTx = api.tx.attestation.add( licenseAuthorizationGranted.claimHash, @@ -481,7 +477,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - anotherAttester.uri, + anotherAttester.id, storeTx, anotherAttesterKey.getSignCallback(anotherAttester), tokenHolder.address @@ -492,7 +488,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const iBelieveICanDrive = Claim.fromCTypeAndClaimContents( driversLicenseCType, { name: 'Dominic Toretto', age: 52 }, - claimer.uri + claimer.id ) const credential2 = Credential.fromClaim(iBelieveICanDrive, { legitimations: [credential1], @@ -503,7 +499,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const licenseGranted = Attestation.fromCredentialAndDid( credential2, - attester.uri + attester.id ) const storeTx2 = api.tx.attestation.add( licenseGranted.claimHash, @@ -511,7 +507,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx2 = await Did.authorizeTx( - attester.uri, + attester.id, storeTx2, attesterKey.getSignCallback(attester), tokenHolder.address diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index c81635f47f..f5cb939564 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -6,27 +6,26 @@ */ import type { ApiPromise } from '@polkadot/api' -import { BN } from '@polkadot/util' - -import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' -import * as Did from '@kiltprotocol/did' -import { +import type { DidDocument, - DidResolutionResult, - DidServiceEndpoint, KiltKeyringPair, - NewDidEncryptionKey, - NewDidVerificationKey, - NewLightDidVerificationKey, - Permission, + ResolutionResult, + Service, SignCallback, + VerificationMethod, } from '@kiltprotocol/types' + +import { BN } from '@polkadot/util' +import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' +import { Permission } from '@kiltprotocol/types' import { UUID } from '@kiltprotocol/utils' +import * as Did from '@kiltprotocol/did' + +import type { KeyTool } from '../testUtils/index.js' import { createFullDidFromSeed, createMinimalLightDidFromKeypair, - KeyTool, makeEncryptionKeyTool, makeSigningKeyTool, } from '../testUtils/index.js' @@ -61,7 +60,7 @@ describe('write and didDeleteTx', () => { it('fails to create a new DID on chain with a different submitter than the one in the creation operation', async () => { const otherAccount = devBob - const tx = await Did.getStoreTx( + const tx = await Did.getStoreTxFromDidDocument( did, otherAccount.address, key.storeDidCallback @@ -73,8 +72,15 @@ describe('write and didDeleteTx', () => { }, 60_000) it('writes a new DID record to chain', async () => { + const { publicKeyMultibase } = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] + ) as VerificationMethod + const { keyType, publicKey: authPublicKey } = + Did.multibaseKeyToDidKey(publicKeyMultibase) const newDid = Did.createLightDidDocument({ - authentication: did.authentication as [NewLightDidVerificationKey], + authentication: [{ publicKey: authPublicKey, type: keyType }] as [ + Did.NewDidVerificationKey + ], service: [ { id: '#test-id-1', @@ -89,7 +95,7 @@ describe('write and didDeleteTx', () => { ], }) - const tx = await Did.getStoreTx( + const tx = await Did.getStoreTxFromDidDocument( newDid, paymentAccount.address, key.storeDidCallback @@ -97,19 +103,12 @@ describe('write and didDeleteTx', () => { await submitTx(tx, paymentAccount) - const fullDidUri = Did.getFullDidUri(newDid.uri) + const fullDidUri = Did.getFullDidUri(newDid.id) const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDidUri)) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - expect(fullDid).toMatchObject({ - uri: fullDidUri, - authentication: [ - expect.objectContaining({ - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKey: newDid.authentication[0].publicKey, - type: 'sr25519', - }), - ], + expect(fullDid).toMatchObject(>{ + id: fullDidUri, service: [ { id: '#test-id-1', @@ -122,13 +121,26 @@ describe('write and didDeleteTx', () => { type: ['test-type-2'], }, ], + verificationMethod: [ + expect.objectContaining(>{ + controller: fullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: authPublicKey, + }), + }), + ], }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.keyAgreement).toBe(undefined) + expect(fullDid.assertionMethod).toBe(undefined) + expect(fullDid.capabilityDelegation).toBe(undefined) }, 60_000) it('should return no results for empty accounts', async () => { - const emptyDid = Did.getFullDidUriFromKey( - makeSigningKeyTool().authentication[0] - ) + const emptyDid = Did.getFullDidUri(makeSigningKeyTool().keypair.address) const encodedDid = Did.toChain(emptyDid) expect((await api.call.did.query(encodedDid)).isSome).toBe(false) @@ -137,7 +149,7 @@ describe('write and didDeleteTx', () => { it('fails to delete the DID using a different submitter than the one specified in the DID operation or using a services count that is too low', async () => { // We verify that the DID to delete is on chain. const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(did.uri)) + Did.toChain(Did.getFullDidUri(did.id)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() @@ -148,7 +160,7 @@ describe('write and didDeleteTx', () => { let call = api.tx.did.delete(new BN(10)) let submittable = await Did.authorizeTx( - fullDid.uri, + fullDid.id, call, signCallback, // Use a different account than the submitter one @@ -164,7 +176,7 @@ describe('write and didDeleteTx', () => { call = api.tx.did.delete(new BN(1)) submittable = await Did.authorizeTx( - fullDid.uri, + fullDid.id, call, signCallback, paymentAccount.address @@ -182,12 +194,12 @@ describe('write and didDeleteTx', () => { it('deletes DID from previous step', async () => { // We verify that the DID to delete is on chain. const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(did.uri)) + Did.toChain(Did.getFullDidUri(did.id)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - const encodedDid = Did.toChain(fullDid.uri) + const encodedDid = Did.toChain(fullDid.id) const linkedInfo = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) @@ -195,7 +207,7 @@ describe('write and didDeleteTx', () => { const call = api.tx.did.delete(storedEndpointsCount) const submittable = await Did.authorizeTx( - fullDid.uri, + fullDid.id, call, signCallback, paymentAccount.address @@ -217,7 +229,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const { keypair, getSignCallback, storeDidCallback } = makeSigningKeyTool() const newDid = await createMinimalLightDidFromKeypair(keypair) - const tx = await Did.getStoreTx( + const tx = await Did.getStoreTxFromDidDocument( newDid, paymentAccount.address, storeDidCallback @@ -227,7 +239,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { // This will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. let fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(newDid.uri)) + Did.toChain(Did.getFullDidUri(newDid.id)) ) let { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -237,7 +249,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { Did.publicKeyToChain(newKey.authentication[0]) ) const tx2 = await Did.authorizeTx( - fullDid.uri, + fullDid.id, updateAuthenticationKeyCall, getSignCallback(fullDid), paymentAccount.address @@ -247,12 +259,12 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { // Authentication key changed, so did must be updated. // Also this will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(newDid.uri)) + Did.toChain(Did.getFullDidUri(newDid.id)) ) fullDid = Did.linkedInfoFromChain(fullDidLinkedInfo).document // Add a new service endpoint - const newEndpoint: DidServiceEndpoint = { + const newEndpoint: Did.NewService = { id: '#new-endpoint', type: ['new-type'], serviceEndpoint: ['x:new-url'], @@ -262,27 +274,27 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { ) const tx3 = await Did.authorizeTx( - fullDid.uri, + fullDid.id, updateEndpointCall, newKey.getSignCallback(fullDid), paymentAccount.address ) await submitTx(tx3, paymentAccount) - const encodedDid = Did.toChain(fullDid.uri) + const encodedDid = Did.toChain(fullDid.id) const linkedInfo = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) - expect(Did.getService(linkedInfo.document, newEndpoint.id)).toStrictEqual( - newEndpoint - ) + expect( + linkedInfo.document.service?.find((s) => s.id === newEndpoint.id) + ).toStrictEqual(newEndpoint) // Delete the added service endpoint const removeEndpointCall = api.tx.did.removeServiceEndpoint( - Did.resourceIdToChain(newEndpoint.id) + Did.fragmentIdToChain(newEndpoint.id) ) const tx4 = await Did.authorizeTx( - fullDid.uri, + fullDid.id, removeEndpointCall, newKey.getSignCallback(fullDid), paymentAccount.address @@ -293,7 +305,9 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const linkedInfo2 = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) - expect(Did.getService(linkedInfo2.document, newEndpoint.id)).toBe(undefined) + expect( + linkedInfo2.document.service?.find((s) => s.id === newEndpoint.id) + ).toBe(undefined) // Claim the deposit back const storedEndpointsCount = linkedInfo2.document.service?.length ?? 0 @@ -317,14 +331,14 @@ describe('DID migration', () => { keyAgreement, }) - const storeTx = await Did.getStoreTx( + const storeTx = await Did.getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) + const migratedFullDidUri = Did.getFullDidUri(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( Did.toChain(migratedFullDidUri) ) @@ -332,32 +346,38 @@ describe('DID migration', () => { migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject({ - uri: migratedFullDidUri, - authentication: [ - expect.objectContaining({ - publicKey: lightDid.authentication[0].publicKey, - type: 'ed25519', + expect(migratedFullDid).toMatchObject(>{ + id: migratedFullDidUri, + verificationMethod: [ + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), - ], - keyAgreement: [ - expect.objectContaining({ - publicKey: lightDid.keyAgreement?.[0].publicKey, - type: 'x25519', + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), }), ], }) + expect(migratedFullDid.authentication).toHaveLength(1) + expect(migratedFullDid.keyAgreement).toHaveLength(1) + expect(migratedFullDid.assertionMethod).toBe(undefined) + expect(migratedFullDid.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome + (await api.call.did.query(Did.toChain(migratedFullDid.id))).isSome ).toBe(true) - const { metadata } = (await Did.resolve( - lightDid.uri - )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.resolve( + lightDid.id + )) as ResolutionResult - expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) - expect(metadata.deactivated).toBe(false) + expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.deactivated).toBe(undefined) }) it('migrates light DID with sr25519 auth key', async () => { @@ -366,14 +386,14 @@ describe('DID migration', () => { authentication, }) - const storeTx = await Did.getStoreTx( + const storeTx = await Did.getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) + const migratedFullDidUri = Did.getFullDidUri(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( Did.toChain(migratedFullDidUri) ) @@ -381,26 +401,32 @@ describe('DID migration', () => { migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject({ - uri: migratedFullDidUri, - authentication: [ - expect.objectContaining({ - publicKey: lightDid.authentication[0].publicKey, - type: 'sr25519', + expect(migratedFullDid).toMatchObject(>{ + id: migratedFullDidUri, + verificationMethod: [ + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), ], }) + expect(migratedFullDid.authentication).toHaveLength(1) + expect(migratedFullDid.keyAgreement).toBe(undefined) + expect(migratedFullDid.assertionMethod).toBe(undefined) + expect(migratedFullDid.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome + (await api.call.did.query(Did.toChain(migratedFullDid.id))).isSome ).toBe(true) - const { metadata } = (await Did.resolve( - lightDid.uri - )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.resolve( + lightDid.id + )) as ResolutionResult - expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) - expect(metadata.deactivated).toBe(false) + expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.deactivated).toBe(undefined) }) it('migrates light DID with ed25519 auth key, encryption key, and service endpoints', async () => { @@ -408,7 +434,7 @@ describe('DID migration', () => { const { keyAgreement } = makeEncryptionKeyTool( '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' ) - const service: DidServiceEndpoint[] = [ + const service: Did.NewService[] = [ { id: '#id-1', type: ['type-1'], @@ -421,14 +447,14 @@ describe('DID migration', () => { service, }) - const storeTx = await Did.getStoreTx( + const storeTx = await Did.getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) + const migratedFullDidUri = Did.getFullDidUri(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( Did.toChain(migratedFullDidUri) ) @@ -436,18 +462,20 @@ describe('DID migration', () => { migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject({ - uri: migratedFullDidUri, - authentication: [ - expect.objectContaining({ - publicKey: lightDid.authentication[0].publicKey, - type: 'ed25519', + expect(migratedFullDid).toMatchObject(>{ + id: migratedFullDidUri, + verificationMethod: [ + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), - ], - keyAgreement: [ - expect.objectContaining({ - publicKey: lightDid.keyAgreement?.[0].publicKey, - type: 'x25519', + expect.objectContaining(>{ + controller: migratedFullDidUri, + type: 'MultiKey', + // We cannot match the ID of the key because it will be defined by the blockchain while saving + publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), }), ], service: [ @@ -458,16 +486,20 @@ describe('DID migration', () => { }, ], }) + expect(migratedFullDid.authentication).toHaveLength(1) + expect(migratedFullDid.keyAgreement).toHaveLength(1) + expect(migratedFullDid.assertionMethod).toBe(undefined) + expect(migratedFullDid.capabilityDelegation).toBe(undefined) - const encodedDid = Did.toChain(migratedFullDid.uri) + const encodedDid = Did.toChain(migratedFullDid.id) expect((await api.call.did.query(encodedDid)).isSome).toBe(true) - const { metadata } = (await Did.resolve( - lightDid.uri - )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.resolve( + lightDid.id + )) as ResolutionResult - expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) - expect(metadata.deactivated).toBe(false) + expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.deactivated).toBe(undefined) // Remove and claim the deposit back const linkedInfo = Did.linkedInfoFromChain( @@ -492,7 +524,7 @@ describe('DID authorization', () => { makeSigningKeyTool('ed25519') beforeAll(async () => { - const createTx = await Did.getStoreTx( + const createTx = await Did.getStoreTxFromInput( { authentication, assertionMethod: authentication, @@ -503,7 +535,11 @@ describe('DID authorization', () => { ) await submitTx(createTx, paymentAccount) const didLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) did = Did.linkedInfoFromChain(didLinkedInfo).document }, 60_000) @@ -512,7 +548,7 @@ describe('DID authorization', () => { const cType = CType.fromProperties(UUID.generate(), {}) const call = api.tx.ctype.add(CType.toChain(cType)) const tx = await Did.authorizeTx( - did.uri, + did.id, call, getSignCallback(did), paymentAccount.address @@ -524,12 +560,12 @@ describe('DID authorization', () => { it('no longer authorizes ctype creation after DID deletion', async () => { const linkedInfo = Did.linkedInfoFromChain( - await api.call.did.query(Did.toChain(did.uri)) + await api.call.did.query(Did.toChain(did.id)) ) const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 const deleteCall = api.tx.did.delete(storedEndpointsCount) const tx = await Did.authorizeTx( - did.uri, + did.id, deleteCall, getSignCallback(did), paymentAccount.address @@ -539,7 +575,7 @@ describe('DID authorization', () => { const cType = CType.fromProperties(UUID.generate(), {}) const call = api.tx.ctype.add(CType.toChain(cType)) const tx2 = await Did.authorizeTx( - did.uri, + did.id, call, getSignCallback(did), paymentAccount.address @@ -556,8 +592,8 @@ describe('DID authorization', () => { describe('DID management batching', () => { describe('FullDidCreationBuilder', () => { it('Build a complete full DID', async () => { - const { keypair, storeDidCallback, authentication } = makeSigningKeyTool() - const extrinsic = await Did.getStoreTx( + const { storeDidCallback, authentication } = makeSigningKeyTool() + const extrinsic = await Did.getStoreTxFromInput( { authentication, assertionMethod: [ @@ -609,44 +645,71 @@ describe('DID management batching', () => { ) await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - expect(fullDid).toMatchObject({ - authentication: [ + expect(fullDid.verificationMethod).toEqual>( + expect.arrayContaining([ expect.objectContaining({ - publicKey: keypair.publicKey, - type: 'sr25519', + // Authentication + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), - ], - assertionMethod: [ + // Assertion method expect.objectContaining({ - publicKey: new Uint8Array(32).fill(1), - type: 'sr25519', + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: new Uint8Array(32).fill(1), + }), }), - ], - capabilityDelegation: [ + // Capability delegation expect.objectContaining({ - publicKey: new Uint8Array(33).fill(1), - type: 'ecdsa', + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ecdsa', + publicKey: new Uint8Array(33).fill(1), + }), }), - ], - keyAgreement: [ + // Key agreement 1 expect.objectContaining({ - publicKey: new Uint8Array(32).fill(3), - type: 'x25519', + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(1), + }), }), + // Key agreement 2 expect.objectContaining({ - publicKey: new Uint8Array(32).fill(2), - type: 'x25519', + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(2), + }), }), + // Key agreement 3 expect.objectContaining({ - publicKey: new Uint8Array(32).fill(1), - type: 'x25519', + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(3), + }), }), - ], + ]) + ) + expect(fullDid).toMatchObject(>{ service: [ { id: '#id-3', @@ -665,16 +728,20 @@ describe('DID management batching', () => { }, ], }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.assertionMethod).toHaveLength(1) + expect(fullDid.capabilityDelegation).toHaveLength(1) + expect(fullDid.keyAgreement).toHaveLength(3) }) it('Build a minimal full DID with an Ecdsa key', async () => { const { keypair, storeDidCallback } = makeSigningKeyTool('ecdsa') - const didAuthKey: NewDidVerificationKey = { + const didAuthKey: Did.NewDidVerificationKey = { publicKey: keypair.publicKey, type: 'ecdsa', } - const extrinsic = await Did.getStoreTx( + const extrinsic = await Did.getStoreTxFromInput( { authentication: [didAuthKey] }, paymentAccount.address, storeDidCallback @@ -682,17 +749,29 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(didAuthKey)) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - expect(fullDid?.authentication).toMatchObject([ - { - publicKey: keypair.publicKey, - type: 'ecdsa', - }, - ]) + expect(fullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), + }), + ], + }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.assertionMethod).toBe(undefined) + expect(fullDid.capabilityDelegation).toBe(undefined) + expect(fullDid.keyAgreement).toBe(undefined) }) }) @@ -701,7 +780,7 @@ describe('DID management batching', () => { const { keypair, getSignCallback, storeDidCallback, authentication } = makeSigningKeyTool() - const createTx = await Did.getStoreTx( + const createTx = await Did.getStoreTxFromInput( { authentication, keyAgreement: [ @@ -745,7 +824,11 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const initialFullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: initialFullDid } = Did.linkedInfoFromChain( initialFullDidLinkedInfo @@ -756,13 +839,21 @@ describe('DID management batching', () => { const extrinsic = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: initialFullDid.uri, + did: initialFullDid.id, extrinsics: [ api.tx.did.removeKeyAgreementKey( - Did.resourceIdToChain(encryptionKeys[0].id) + Did.fragmentIdToChain( + initialFullDid.verificationMethod!.find( + (vm) => vm.id === encryptionKeys[0] + )!.id + ) ), api.tx.did.removeKeyAgreementKey( - Did.resourceIdToChain(encryptionKeys[1].id) + Did.fragmentIdToChain( + initialFullDid.verificationMethod!.find( + (vm) => vm.id === encryptionKeys[1] + )!.id + ) ), api.tx.did.removeAttestationKey(), api.tx.did.removeDelegationKey(), @@ -775,7 +866,7 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const finalFullDidLinkedInfo = await api.call.did.query( - Did.toChain(initialFullDid.uri) + Did.toChain(initialFullDid.id) ) const { document: finalFullDid } = Did.linkedInfoFromChain( finalFullDidLinkedInfo @@ -783,17 +874,23 @@ describe('DID management batching', () => { expect(finalFullDid).not.toBeNull() - expect( - finalFullDid.authentication[0] - ).toMatchObject({ - publicKey: keypair.publicKey, - type: 'sr25519', + expect(finalFullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: finalFullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: keypair.publicKey, + type: 'sr25519', + }), + }), + ], }) - - expect(finalFullDid.keyAgreement).toBeUndefined() - expect(finalFullDid.assertionMethod).toBeUndefined() - expect(finalFullDid.capabilityDelegation).toBeUndefined() - expect(finalFullDid.service).toBeUndefined() + expect(finalFullDid.authentication).toHaveLength(1) + expect(finalFullDid.assertionMethod).toBe(undefined) + expect(finalFullDid.capabilityDelegation).toBe(undefined) + expect(finalFullDid.keyAgreement).toBe(undefined) }, 40_000) it('Correctly handles rotation of the authentication key', async () => { @@ -801,9 +898,9 @@ describe('DID management batching', () => { makeSigningKeyTool() const { authentication: [newAuthKey], - } = makeSigningKeyTool('ed25519') + } = TestUtils.makeSigningKeyTool('ed25519') - const createTx = await Did.getStoreTx( + const createTx = await Did.getStoreTxFromInput( { authentication }, paymentAccount.address, storeDidCallback @@ -811,7 +908,11 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const initialFullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: initialFullDid } = Did.linkedInfoFromChain( initialFullDidLinkedInfo @@ -819,7 +920,7 @@ describe('DID management batching', () => { const extrinsic = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: initialFullDid.uri, + did: initialFullDid.id, extrinsics: [ api.tx.did.addServiceEndpoint( Did.serviceToChain({ @@ -844,29 +945,49 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const finalFullDidLinkedInfo = await api.call.did.query( - Did.toChain(initialFullDid.uri) + Did.toChain(initialFullDid.id) ) const { document: finalFullDid } = Did.linkedInfoFromChain( finalFullDidLinkedInfo ) expect(finalFullDid).not.toBeNull() - - expect(finalFullDid.authentication[0]).toMatchObject({ - publicKey: newAuthKey.publicKey, - type: newAuthKey.type, + expect(finalFullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: finalFullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: newAuthKey.publicKey, + type: 'ed25519', + }), + }), + ], + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#id-2', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }, + ], }) + expect(finalFullDid.authentication).toHaveLength(1) expect(finalFullDid.keyAgreement).toBeUndefined() expect(finalFullDid.assertionMethod).toBeUndefined() expect(finalFullDid.capabilityDelegation).toBeUndefined() - expect(finalFullDid.service).toHaveLength(2) }, 40_000) it('simple `batch` succeeds despite failures of some extrinsics', async () => { const { authentication, getSignCallback, storeDidCallback } = makeSigningKeyTool() - const tx = await Did.getStoreTx( + const tx = await Did.getStoreTxFromInput( { authentication, service: [ @@ -883,7 +1004,11 @@ describe('DID management batching', () => { // Create the full DID with a service endpoint await submitTx(tx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -892,7 +1017,7 @@ describe('DID management batching', () => { // Try to set a new attestation key and a duplicate service endpoint const updateTx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), api.tx.did.addServiceEndpoint( @@ -910,28 +1035,44 @@ describe('DID management batching', () => { await submitTx(updateTx, paymentAccount) const updatedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) const { document: updatedFullDid } = Did.linkedInfoFromChain( updatedFullDidLinkedInfo ) - // .setAttestationKey() extrinsic went through in the batch - expect(updatedFullDid.assertionMethod?.[0]).toBeDefined() - // The service endpoint will match the one manually added, and not the one set in the batch - expect( - Did.getService(updatedFullDid, '#id-1') - ).toStrictEqual({ - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], + expect(updatedFullDid).toMatchObject>({ + verificationMethod: [ + expect.objectContaining({ + // Authentication and assertionMethod + controller: fullDid.id, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }), + ], + // Old service maintained + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + ], }) + + expect(updatedFullDid.authentication).toHaveLength(1) + expect(updatedFullDid.keyAgreement).toBeUndefined() + // .setAttestationKey() extrinsic went through in the batch + expect(updatedFullDid.assertionMethod).toStrictEqual( + updatedFullDid.authentication + ) + expect(updatedFullDid.capabilityDelegation).toBeUndefined() }, 60_000) it('batchAll fails if any extrinsics fails', async () => { const { authentication, getSignCallback, storeDidCallback } = makeSigningKeyTool() - const createTx = await Did.getStoreTx( + const createTx = await Did.getStoreTxFromInput( { authentication, service: [ @@ -947,7 +1088,11 @@ describe('DID management batching', () => { ) await submitTx(createTx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -956,7 +1101,7 @@ describe('DID management batching', () => { // Use batchAll to set a new attestation key and a duplicate service endpoint const updateTx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), api.tx.did.addServiceEndpoint( @@ -978,7 +1123,7 @@ describe('DID management batching', () => { }) const updatedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) const { document: updatedFullDid } = Did.linkedInfoFromChain( updatedFullDidLinkedInfo @@ -987,8 +1132,8 @@ describe('DID management batching', () => { expect(updatedFullDid.assertionMethod).toBeUndefined() // The service endpoint will match the one manually added, and not the one set in the builder. expect( - Did.getService(updatedFullDid, '#id-1') - ).toStrictEqual({ + updatedFullDid.service?.find((s) => s.id === '#id-1') + ).toStrictEqual({ id: '#id-1', type: ['type-1'], serviceEndpoint: ['x:url-1'], @@ -1010,15 +1155,15 @@ describe('DID extrinsics batching', () => { const cType = CType.fromProperties(UUID.generate(), {}) const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) const rootNode = DelegationNode.newRoot({ - account: fullDid.uri, + account: fullDid.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cType.$id), }) const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ ctypeStoreTx, // Will fail since the delegation cannot be revoked before it is added @@ -1040,15 +1185,15 @@ describe('DID extrinsics batching', () => { const cType = CType.fromProperties(UUID.generate(), {}) const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) const rootNode = DelegationNode.newRoot({ - account: fullDid.uri, + account: fullDid.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cType.$id), }) const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ ctypeStoreTx, // Will fail since the delegation cannot be revoked before it is added @@ -1072,7 +1217,7 @@ describe('DID extrinsics batching', () => { it('can batch extrinsics for the same required key type', async () => { const web3NameClaimTx = api.tx.web3Names.claim('test-1') const authorizedTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, web3NameClaimTx, key.getSignCallback(fullDid), paymentAccount.address @@ -1083,7 +1228,7 @@ describe('DID extrinsics batching', () => { const web3Name2ClaimExt = api.tx.web3Names.claim('test-2') const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, - did: fullDid.uri, + did: fullDid.id, extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], sign: key.getSignCallback(fullDid), submitter: paymentAccount.address, @@ -1095,8 +1240,8 @@ describe('DID extrinsics batching', () => { expect(encoded1.isSome).toBe(false) // Test for correct creation of second web3 name const encoded2 = await api.call.did.queryByWeb3Name('test-2') - expect(Did.linkedInfoFromChain(encoded2).document.uri).toStrictEqual( - fullDid.uri + expect(Did.linkedInfoFromChain(encoded2).document.id).toStrictEqual( + fullDid.id ) }, 30_000) @@ -1108,7 +1253,7 @@ describe('DID extrinsics batching', () => { const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) // Delegation key const rootNode = DelegationNode.newRoot({ - account: fullDid.uri, + account: fullDid.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(ctype1.$id), }) @@ -1120,11 +1265,11 @@ describe('DID extrinsics batching', () => { const ctype2 = CType.fromProperties(UUID.generate(), {}) const ctype2Creation = api.tx.ctype.add(CType.toChain(ctype2)) // Delegation key - const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.uri) + const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.id) const batchedExtrinsics = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ web3NameReleaseExt, ctype1Creation, @@ -1144,9 +1289,9 @@ describe('DID extrinsics batching', () => { expect(encoded.isSome).toBe(false) const { - document: { uri }, + document: { id }, } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name('test-2')) - expect(uri).toStrictEqual(fullDid.uri) + expect(id).toStrictEqual(fullDid.id) // Test correct use of attestation keys await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() @@ -1159,7 +1304,7 @@ describe('DID extrinsics batching', () => { }) describe('Runtime constraints', () => { - let testAuthKey: NewDidVerificationKey + let testAuthKey: Did.NewDidVerificationKey const { keypair, storeDidCallback } = makeSigningKeyTool('ed25519') beforeAll(async () => { @@ -1172,12 +1317,12 @@ describe('Runtime constraints', () => { it('should not be possible to create a DID with too many encryption keys', async () => { // Maximum is 10 const newKeyAgreementKeys = Array(10).map( - (_, index): NewDidEncryptionKey => ({ + (_, index): Did.NewDidEncryptionKey => ({ publicKey: Uint8Array.from(new Array(32).fill(index)), type: 'x25519', }) ) - await Did.getStoreTx( + await Did.getStoreTxFromInput( { authentication: [testAuthKey], keyAgreement: newKeyAgreementKeys, @@ -1191,7 +1336,7 @@ describe('Runtime constraints', () => { type: 'x25519', }) await expect( - Did.getStoreTx( + Did.getStoreTxFromInput( { authentication: [testAuthKey], keyAgreement: newKeyAgreementKeys, @@ -1208,13 +1353,13 @@ describe('Runtime constraints', () => { it('should not be possible to create a DID with too many service endpoints', async () => { // Maximum is 25 const newServiceEndpoints = Array(25).map( - (_, index): DidServiceEndpoint => ({ + (_, index): Did.NewService => ({ id: `#service-${index}`, type: [`type-${index}`], serviceEndpoint: [`x:url-${index}`], }) ) - await Did.getStoreTx( + await Did.getStoreTxFromInput( { authentication: [testAuthKey], service: newServiceEndpoints, @@ -1229,7 +1374,7 @@ describe('Runtime constraints', () => { serviceEndpoint: ['x:url-100'], }) await expect( - Did.getStoreTx( + Did.getStoreTxFromInput( { authentication: [testAuthKey], service: newServiceEndpoints, diff --git a/tests/integration/Did2.spec.ts b/tests/integration/Did2.spec.ts deleted file mode 100644 index 6a7f156b77..0000000000 --- a/tests/integration/Did2.spec.ts +++ /dev/null @@ -1,1478 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import type { ApiPromise } from '@polkadot/api' -import type { - CryptoCallbacksV2, - DidDocumentV2, - DidResolverV2, - KiltKeyringPair, -} from '@kiltprotocol/types' - -import { BN } from '@polkadot/util' -import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' -import { Permission } from '@kiltprotocol/types' -import { UUID } from '@kiltprotocol/utils' -import * as Did from '@kiltprotocol/did' - -import { TestUtilsV2 } from '../testUtils/index.js' -import { - createEndowedTestAccount, - devBob, - initializeApi, - submitTx, -} from './utils.js' - -let paymentAccount: KiltKeyringPair -let api: ApiPromise - -beforeAll(async () => { - api = await initializeApi() -}, 30_000) - -beforeAll(async () => { - paymentAccount = await createEndowedTestAccount() -}, 30_000) - -describe('write and didDeleteTx', () => { - let did: DidDocumentV2.DidDocument - let key: TestUtilsV2.KeyTool - let signCallback: CryptoCallbacksV2.SignCallback - - beforeAll(async () => { - key = TestUtilsV2.makeSigningKeyTool() - did = await TestUtilsV2.createMinimalLightDidFromKeypair(key.keypair) - signCallback = key.getSignCallback(did) - }) - - it('fails to create a new DID on chain with a different submitter than the one in the creation operation', async () => { - const otherAccount = devBob - const tx = await Did.DidChainV2.getStoreTxFromDidDocument( - did, - otherAccount.address, - key.storeDidCallback - ) - - await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ - isBadOrigin: true, - }) - }, 60_000) - - it('writes a new DID record to chain', async () => { - const { publicKeyMultibase } = did.verificationMethod?.find( - (vm) => vm.id === did.authentication?.[0] - ) as DidDocumentV2.VerificationMethod - const { keyType, publicKey: authPublicKey } = - Did.DidUtilsV2.multibaseKeyToDidKey(publicKeyMultibase) - const newDid = Did.DidDetailsV2.createLightDidDocument({ - authentication: [{ publicKey: authPublicKey, type: keyType }] as [ - Did.DidDetailsV2.NewDidVerificationKey - ], - service: [ - { - id: '#test-id-1', - type: ['test-type-1'], - serviceEndpoint: ['x:test-url-1'], - }, - { - id: '#test-id-2', - type: ['test-type-2'], - serviceEndpoint: ['x:test-url-2'], - }, - ], - }) - - const tx = await Did.DidChainV2.getStoreTxFromDidDocument( - newDid, - paymentAccount.address, - key.storeDidCallback - ) - - await submitTx(tx, paymentAccount) - - const fullDidUri = Did.DidUtilsV2.getFullDidUri(newDid.id) - const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(fullDidUri) - ) - const { document: fullDid } = - Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - - expect(fullDid).toMatchObject(>{ - id: fullDidUri, - service: [ - { - id: '#test-id-1', - serviceEndpoint: ['x:test-url-1'], - type: ['test-type-1'], - }, - { - id: '#test-id-2', - serviceEndpoint: ['x:test-url-2'], - type: ['test-type-2'], - }, - ], - verificationMethod: [ - expect.objectContaining(>{ - controller: fullDidUri, - type: 'MultiKey', - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'sr25519', - publicKey: authPublicKey, - }), - }), - ], - }) - expect(fullDid.authentication).toHaveLength(1) - expect(fullDid.keyAgreement).toBe(undefined) - expect(fullDid.assertionMethod).toBe(undefined) - expect(fullDid.capabilityDelegation).toBe(undefined) - }, 60_000) - - it('should return no results for empty accounts', async () => { - const emptyDid = Did.DidUtilsV2.getFullDidUri( - TestUtilsV2.makeSigningKeyTool().keypair.address - ) - - const encodedDid = Did.DidChainV2.toChain(emptyDid) - expect((await api.call.did.query(encodedDid)).isSome).toBe(false) - }) - - it('fails to delete the DID using a different submitter than the one specified in the DID operation or using a services count that is too low', async () => { - // We verify that the DID to delete is on chain. - const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(did.id)) - ) - const { document: fullDid } = - Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - expect(fullDid).not.toBeNull() - - const otherAccount = devBob - - // 10 is an example value. It is not used here since we are testing another error - let call = api.tx.did.delete(new BN(10)) - - let submittable = await Did.DidDetailsV2.authorizeTx( - fullDid.id, - call, - signCallback, - // Use a different account than the submitter one - otherAccount.address - ) - - await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ - section: 'did', - name: 'BadDidOrigin', - }) - - // We use 1 here and this should fail as there are two service endpoints stored. - call = api.tx.did.delete(new BN(1)) - - submittable = await Did.DidDetailsV2.authorizeTx( - fullDid.id, - call, - signCallback, - paymentAccount.address - ) - - // Will fail because count provided is too low - await expect(submitTx(submittable, paymentAccount)).rejects.toMatchObject({ - section: 'did', - name: expect.stringMatching( - /^(StoredEndpointsCountTooLarge|MaxStoredEndpointsCountExceeded)$/ - ), - }) - }, 60_000) - - it('deletes DID from previous step', async () => { - // We verify that the DID to delete is on chain. - const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(did.id)) - ) - const { document: fullDid } = - Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - expect(fullDid).not.toBeNull() - - const encodedDid = Did.DidChainV2.toChain(fullDid.id) - const linkedInfo = Did.DidRpc2.linkedInfoFromChain( - await api.call.did.query(encodedDid) - ) - const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 - const call = api.tx.did.delete(storedEndpointsCount) - - const submittable = await Did.DidDetailsV2.authorizeTx( - fullDid.id, - call, - signCallback, - paymentAccount.address - ) - - // Check that DID is not blacklisted. - expect((await api.query.did.didBlacklist(encodedDid)).isNone).toBe(true) - - await submitTx(submittable, paymentAccount) - - expect((await api.call.did.query(encodedDid)).isNone).toBe(true) - - // Check that DID is now blacklisted. - expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) - }, 60_000) -}) - -it('creates and updates DID, and then reclaims the deposit back', async () => { - const { keypair, getSignCallback, storeDidCallback } = - TestUtilsV2.makeSigningKeyTool() - const newDid = await TestUtilsV2.createMinimalLightDidFromKeypair(keypair) - - const tx = await Did.DidChainV2.getStoreTxFromDidDocument( - newDid, - paymentAccount.address, - storeDidCallback - ) - - await submitTx(tx, paymentAccount) - - // This will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. - let fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(newDid.id)) - ) - let { document: fullDid } = Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - - const newKey = TestUtilsV2.makeSigningKeyTool() - - const updateAuthenticationKeyCall = api.tx.did.setAuthenticationKey( - Did.DidChainV2.publicKeyToChain(newKey.authentication[0]) - ) - const tx2 = await Did.DidDetailsV2.authorizeTx( - fullDid.id, - updateAuthenticationKeyCall, - getSignCallback(fullDid), - paymentAccount.address - ) - await submitTx(tx2, paymentAccount) - - // Authentication key changed, so did must be updated. - // Also this will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. - fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(Did.DidUtilsV2.getFullDidUri(newDid.id)) - ) - fullDid = Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo).document - - // Add a new service endpoint - const newEndpoint: Did.DidDetailsV2.NewService = { - id: '#new-endpoint', - type: ['new-type'], - serviceEndpoint: ['x:new-url'], - } - const updateEndpointCall = api.tx.did.addServiceEndpoint( - Did.DidChainV2.serviceToChain(newEndpoint) - ) - - const tx3 = await Did.DidDetailsV2.authorizeTx( - fullDid.id, - updateEndpointCall, - newKey.getSignCallback(fullDid), - paymentAccount.address - ) - await submitTx(tx3, paymentAccount) - - const encodedDid = Did.DidChainV2.toChain(fullDid.id) - const linkedInfo = Did.DidRpc2.linkedInfoFromChain( - await api.call.did.query(encodedDid) - ) - expect( - linkedInfo.document.service?.find((s) => s.id === newEndpoint.id) - ).toStrictEqual(newEndpoint) - - // Delete the added service endpoint - const removeEndpointCall = api.tx.did.removeServiceEndpoint( - Did.DidChainV2.fragmentIdToChain(newEndpoint.id) - ) - const tx4 = await Did.DidDetailsV2.authorizeTx( - fullDid.id, - removeEndpointCall, - newKey.getSignCallback(fullDid), - paymentAccount.address - ) - await submitTx(tx4, paymentAccount) - - // There should not be any endpoint with the given ID now. - const linkedInfo2 = Did.DidRpc2.linkedInfoFromChain( - await api.call.did.query(encodedDid) - ) - expect( - linkedInfo2.document.service?.find((s) => s.id === newEndpoint.id) - ).toBe(undefined) - - // Claim the deposit back - const storedEndpointsCount = linkedInfo2.document.service?.length ?? 0 - const reclaimDepositTx = api.tx.did.reclaimDeposit( - encodedDid, - storedEndpointsCount - ) - await submitTx(reclaimDepositTx, paymentAccount) - // Verify that the DID has been deleted - expect((await api.call.did.query(encodedDid)).isNone).toBe(true) -}, 80_000) - -describe('DID migration', () => { - it('migrates light DID with ed25519 auth key and encryption key', async () => { - const { storeDidCallback, authentication } = - TestUtilsV2.makeSigningKeyTool('ed25519') - const { keyAgreement } = TestUtilsV2.makeEncryptionKeyTool( - '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' - ) - const lightDid = Did.DidDetailsV2.createLightDidDocument({ - authentication, - keyAgreement, - }) - - const storeTx = await Did.DidChainV2.getStoreTxFromDidDocument( - lightDid, - paymentAccount.address, - storeDidCallback - ) - - await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.DidUtilsV2.getFullDidUri(lightDid.id) - const migratedFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(migratedFullDidUri) - ) - const { document: migratedFullDid } = Did.DidRpc2.linkedInfoFromChain( - migratedFullDidLinkedInfo - ) - - expect(migratedFullDid).toMatchObject(>{ - id: migratedFullDidUri, - verificationMethod: [ - expect.objectContaining(>{ - controller: migratedFullDidUri, - type: 'MultiKey', - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }), - expect.objectContaining(>{ - controller: migratedFullDidUri, - type: 'MultiKey', - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - keyAgreement[0] - ), - }), - ], - }) - expect(migratedFullDid.authentication).toHaveLength(1) - expect(migratedFullDid.keyAgreement).toHaveLength(1) - expect(migratedFullDid.assertionMethod).toBe(undefined) - expect(migratedFullDid.capabilityDelegation).toBe(undefined) - - expect( - (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))) - .isSome - ).toBe(true) - - const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( - lightDid.id - )) as DidResolverV2.ResolutionResult - - expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) - expect(didDocumentMetadata.deactivated).toBe(undefined) - }) - - it('migrates light DID with sr25519 auth key', async () => { - const { authentication, storeDidCallback } = - TestUtilsV2.makeSigningKeyTool() - const lightDid = Did.DidDetailsV2.createLightDidDocument({ - authentication, - }) - - const storeTx = await Did.DidChainV2.getStoreTxFromDidDocument( - lightDid, - paymentAccount.address, - storeDidCallback - ) - - await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.DidUtilsV2.getFullDidUri(lightDid.id) - const migratedFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(migratedFullDidUri) - ) - const { document: migratedFullDid } = Did.DidRpc2.linkedInfoFromChain( - migratedFullDidLinkedInfo - ) - - expect(migratedFullDid).toMatchObject(>{ - id: migratedFullDidUri, - verificationMethod: [ - expect.objectContaining(>{ - controller: migratedFullDidUri, - type: 'MultiKey', - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }), - ], - }) - expect(migratedFullDid.authentication).toHaveLength(1) - expect(migratedFullDid.keyAgreement).toBe(undefined) - expect(migratedFullDid.assertionMethod).toBe(undefined) - expect(migratedFullDid.capabilityDelegation).toBe(undefined) - - expect( - (await api.call.did.query(Did.DidChainV2.toChain(migratedFullDid.id))) - .isSome - ).toBe(true) - - const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( - lightDid.id - )) as DidResolverV2.ResolutionResult - - expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) - expect(didDocumentMetadata.deactivated).toBe(undefined) - }) - - it('migrates light DID with ed25519 auth key, encryption key, and service endpoints', async () => { - const { storeDidCallback, authentication } = - TestUtilsV2.makeSigningKeyTool('ed25519') - const { keyAgreement } = TestUtilsV2.makeEncryptionKeyTool( - '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' - ) - const service: Did.DidDetailsV2.NewService[] = [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - ] - const lightDid = Did.DidDetailsV2.createLightDidDocument({ - authentication, - keyAgreement, - service, - }) - - const storeTx = await Did.DidChainV2.getStoreTxFromDidDocument( - lightDid, - paymentAccount.address, - storeDidCallback - ) - - await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.DidUtilsV2.getFullDidUri(lightDid.id) - const migratedFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(migratedFullDidUri) - ) - const { document: migratedFullDid } = Did.DidRpc2.linkedInfoFromChain( - migratedFullDidLinkedInfo - ) - - expect(migratedFullDid).toMatchObject(>{ - id: migratedFullDidUri, - verificationMethod: [ - expect.objectContaining(>{ - controller: migratedFullDidUri, - type: 'MultiKey', - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }), - expect.objectContaining(>{ - controller: migratedFullDidUri, - type: 'MultiKey', - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - keyAgreement[0] - ), - }), - ], - service: [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - ], - }) - expect(migratedFullDid.authentication).toHaveLength(1) - expect(migratedFullDid.keyAgreement).toHaveLength(1) - expect(migratedFullDid.assertionMethod).toBe(undefined) - expect(migratedFullDid.capabilityDelegation).toBe(undefined) - - const encodedDid = Did.DidChainV2.toChain(migratedFullDid.id) - expect((await api.call.did.query(encodedDid)).isSome).toBe(true) - - const { didDocumentMetadata } = (await Did.DidResolverV2.resolve( - lightDid.id - )) as DidResolverV2.ResolutionResult - - expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) - expect(didDocumentMetadata.deactivated).toBe(undefined) - - // Remove and claim the deposit back - const linkedInfo = Did.DidRpc2.linkedInfoFromChain( - await api.call.did.query(encodedDid) - ) - const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 - const reclaimDepositTx = api.tx.did.reclaimDeposit( - encodedDid, - storedEndpointsCount - ) - await submitTx(reclaimDepositTx, paymentAccount) - - expect((await api.call.did.query(encodedDid)).isNone).toBe(true) - expect((await api.query.did.didBlacklist(encodedDid)).isSome).toBe(true) - }, 60_000) -}) - -describe('DID authorization', () => { - // Light DIDs cannot authorize extrinsics - let did: DidDocumentV2.DidDocument - const { getSignCallback, storeDidCallback, authentication } = - TestUtilsV2.makeSigningKeyTool('ed25519') - - beforeAll(async () => { - const createTx = await Did.DidChainV2.getStoreTxFromInput( - { - authentication, - assertionMethod: authentication, - capabilityDelegation: authentication, - }, - paymentAccount.address, - storeDidCallback - ) - await submitTx(createTx, paymentAccount) - const didLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain( - Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }) - ) - ) - did = Did.DidRpc2.linkedInfoFromChain(didLinkedInfo).document - }, 60_000) - - it('authorizes ctype creation with DID signature', async () => { - const cType = CType.fromProperties(UUID.generate(), {}) - const call = api.tx.ctype.add(CType.toChain(cType)) - const tx = await Did.DidDetailsV2.authorizeTx( - did.id, - call, - getSignCallback(did), - paymentAccount.address - ) - await submitTx(tx, paymentAccount) - - await expect(CType.verifyStored(cType)).resolves.not.toThrow() - }, 60_000) - - it('no longer authorizes ctype creation after DID deletion', async () => { - const linkedInfo = Did.DidRpc2.linkedInfoFromChain( - await api.call.did.query(Did.DidChainV2.toChain(did.id)) - ) - const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 - const deleteCall = api.tx.did.delete(storedEndpointsCount) - const tx = await Did.DidDetailsV2.authorizeTx( - did.id, - deleteCall, - getSignCallback(did), - paymentAccount.address - ) - await submitTx(tx, paymentAccount) - - const cType = CType.fromProperties(UUID.generate(), {}) - const call = api.tx.ctype.add(CType.toChain(cType)) - const tx2 = await Did.DidDetailsV2.authorizeTx( - did.id, - call, - getSignCallback(did), - paymentAccount.address - ) - await expect(submitTx(tx2, paymentAccount)).rejects.toMatchObject({ - section: 'did', - name: expect.stringMatching(/^(DidNotPresent|NotFound)$/), - }) - - await expect(CType.verifyStored(cType)).rejects.toThrow() - }, 60_000) -}) - -describe('DID management batching', () => { - describe('FullDidCreationBuilder', () => { - it('Build a complete full DID', async () => { - const { storeDidCallback, authentication } = - TestUtilsV2.makeSigningKeyTool() - const extrinsic = await Did.DidChainV2.getStoreTxFromInput( - { - authentication, - assertionMethod: [ - { - publicKey: new Uint8Array(32).fill(1), - type: 'sr25519', - }, - ], - capabilityDelegation: [ - { - publicKey: new Uint8Array(33).fill(1), - type: 'ecdsa', - }, - ], - keyAgreement: [ - { - publicKey: new Uint8Array(32).fill(1), - type: 'x25519', - }, - { - publicKey: new Uint8Array(32).fill(2), - type: 'x25519', - }, - { - publicKey: new Uint8Array(32).fill(3), - type: 'x25519', - }, - ], - service: [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#id-2', - type: ['type-2'], - serviceEndpoint: ['x:url-2'], - }, - { - id: '#id-3', - type: ['type-3'], - serviceEndpoint: ['x:url-3'], - }, - ], - }, - paymentAccount.address, - storeDidCallback - ) - await submitTx(extrinsic, paymentAccount) - const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain( - Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }) - ) - ) - const { document: fullDid } = - Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - - expect(fullDid).not.toBeNull() - expect(fullDid.verificationMethod).toEqual< - Partial - >( - expect.arrayContaining([ - expect.objectContaining({ - // Authentication - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }), - // Assertion method - expect.objectContaining({ - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'sr25519', - publicKey: new Uint8Array(32).fill(1), - }), - }), - // Capability delegation - expect.objectContaining({ - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'ecdsa', - publicKey: new Uint8Array(33).fill(1), - }), - }), - // Key agreement 1 - expect.objectContaining({ - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'x25519', - publicKey: new Uint8Array(32).fill(1), - }), - }), - // Key agreement 2 - expect.objectContaining({ - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'x25519', - publicKey: new Uint8Array(32).fill(2), - }), - }), - // Key agreement 3 - expect.objectContaining({ - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: 'x25519', - publicKey: new Uint8Array(32).fill(3), - }), - }), - ]) - ) - expect(fullDid).toMatchObject(>{ - service: [ - { - id: '#id-3', - type: ['type-3'], - serviceEndpoint: ['x:url-3'], - }, - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#id-2', - type: ['type-2'], - serviceEndpoint: ['x:url-2'], - }, - ], - }) - expect(fullDid.authentication).toHaveLength(1) - expect(fullDid.assertionMethod).toHaveLength(1) - expect(fullDid.capabilityDelegation).toHaveLength(1) - expect(fullDid.keyAgreement).toHaveLength(3) - }) - - it('Build a minimal full DID with an Ecdsa key', async () => { - const { keypair, storeDidCallback } = - TestUtilsV2.makeSigningKeyTool('ecdsa') - const didAuthKey: Did.DidDetailsV2.NewDidVerificationKey = { - publicKey: keypair.publicKey, - type: 'ecdsa', - } - - const extrinsic = await Did.DidChainV2.getStoreTxFromInput( - { authentication: [didAuthKey] }, - paymentAccount.address, - storeDidCallback - ) - await submitTx(extrinsic, paymentAccount) - - const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain( - Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: - Did.DidUtilsV2.keypairToMultibaseKey(didAuthKey), - }) - ) - ) - const { document: fullDid } = - Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - - expect(fullDid).not.toBeNull() - expect(fullDid).toMatchObject(>{ - verificationMethod: [ - // Authentication - expect.objectContaining(>{ - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: - Did.DidUtilsV2.keypairToMultibaseKey(didAuthKey), - }), - ], - }) - expect(fullDid.authentication).toHaveLength(1) - expect(fullDid.assertionMethod).toBe(undefined) - expect(fullDid.capabilityDelegation).toBe(undefined) - expect(fullDid.keyAgreement).toBe(undefined) - }) - }) - - describe('FullDidUpdateBuilder', () => { - it('Build from a complete full DID and remove everything but the authentication key', async () => { - const { keypair, getSignCallback, storeDidCallback, authentication } = - TestUtilsV2.makeSigningKeyTool() - - const createTx = await Did.DidChainV2.getStoreTxFromInput( - { - authentication, - keyAgreement: [ - { - publicKey: new Uint8Array(32).fill(1), - type: 'x25519', - }, - { - publicKey: new Uint8Array(32).fill(2), - type: 'x25519', - }, - ], - assertionMethod: [ - { - publicKey: new Uint8Array(32).fill(1), - type: 'sr25519', - }, - ], - capabilityDelegation: [ - { - publicKey: new Uint8Array(33).fill(1), - type: 'ecdsa', - }, - ], - service: [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#id-2', - type: ['type-2'], - serviceEndpoint: ['x:url-2'], - }, - ], - }, - paymentAccount.address, - storeDidCallback - ) - await submitTx(createTx, paymentAccount) - - const initialFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain( - Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }) - ) - ) - const { document: initialFullDid } = Did.DidRpc2.linkedInfoFromChain( - initialFullDidLinkedInfo - ) - - const encryptionKeys = initialFullDid.keyAgreement - if (!encryptionKeys) throw new Error('No key agreement keys') - - const extrinsic = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batchAll, - did: initialFullDid.id, - extrinsics: [ - api.tx.did.removeKeyAgreementKey( - Did.DidChainV2.fragmentIdToChain( - initialFullDid.verificationMethod!.find( - (vm) => vm.id === encryptionKeys[0] - )!.id - ) - ), - api.tx.did.removeKeyAgreementKey( - Did.DidChainV2.fragmentIdToChain( - initialFullDid.verificationMethod!.find( - (vm) => vm.id === encryptionKeys[1] - )!.id - ) - ), - api.tx.did.removeAttestationKey(), - api.tx.did.removeDelegationKey(), - api.tx.did.removeServiceEndpoint('id-1'), - api.tx.did.removeServiceEndpoint('id-2'), - ], - sign: getSignCallback(initialFullDid), - submitter: paymentAccount.address, - }) - await submitTx(extrinsic, paymentAccount) - - const finalFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(initialFullDid.id) - ) - const { document: finalFullDid } = Did.DidRpc2.linkedInfoFromChain( - finalFullDidLinkedInfo - ) - - expect(finalFullDid).not.toBeNull() - - expect(finalFullDid).toMatchObject(>{ - verificationMethod: [ - // Authentication - expect.objectContaining(>{ - controller: finalFullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - publicKey: keypair.publicKey, - type: 'sr25519', - }), - }), - ], - }) - expect(finalFullDid.authentication).toHaveLength(1) - expect(finalFullDid.assertionMethod).toBe(undefined) - expect(finalFullDid.capabilityDelegation).toBe(undefined) - expect(finalFullDid.keyAgreement).toBe(undefined) - }, 40_000) - - it('Correctly handles rotation of the authentication key', async () => { - const { authentication, getSignCallback, storeDidCallback } = - TestUtilsV2.makeSigningKeyTool() - const { - authentication: [newAuthKey], - } = TestUtilsV2.makeSigningKeyTool('ed25519') - - const createTx = await Did.DidChainV2.getStoreTxFromInput( - { authentication }, - paymentAccount.address, - storeDidCallback - ) - await submitTx(createTx, paymentAccount) - - const initialFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain( - Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }) - ) - ) - const { document: initialFullDid } = Did.DidRpc2.linkedInfoFromChain( - initialFullDidLinkedInfo - ) - - const extrinsic = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batchAll, - did: initialFullDid.id, - extrinsics: [ - api.tx.did.addServiceEndpoint( - Did.DidChainV2.serviceToChain({ - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }) - ), - api.tx.did.setAuthenticationKey( - Did.DidChainV2.publicKeyToChain(newAuthKey) - ), - api.tx.did.addServiceEndpoint( - Did.DidChainV2.serviceToChain({ - id: '#id-2', - type: ['type-2'], - serviceEndpoint: ['x:url-2'], - }) - ), - ], - sign: getSignCallback(initialFullDid), - submitter: paymentAccount.address, - }) - - await submitTx(extrinsic, paymentAccount) - - const finalFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(initialFullDid.id) - ) - const { document: finalFullDid } = Did.DidRpc2.linkedInfoFromChain( - finalFullDidLinkedInfo - ) - - expect(finalFullDid).not.toBeNull() - expect(finalFullDid).toMatchObject(>{ - verificationMethod: [ - // Authentication - expect.objectContaining(>{ - controller: finalFullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - publicKey: newAuthKey.publicKey, - type: 'ed25519', - }), - }), - ], - service: [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - { - id: '#id-2', - type: ['type-2'], - serviceEndpoint: ['x:url-2'], - }, - ], - }) - - expect(finalFullDid.authentication).toHaveLength(1) - expect(finalFullDid.keyAgreement).toBeUndefined() - expect(finalFullDid.assertionMethod).toBeUndefined() - expect(finalFullDid.capabilityDelegation).toBeUndefined() - }, 40_000) - - it('simple `batch` succeeds despite failures of some extrinsics', async () => { - const { authentication, getSignCallback, storeDidCallback } = - TestUtilsV2.makeSigningKeyTool() - const tx = await Did.DidChainV2.getStoreTxFromInput( - { - authentication, - service: [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - ], - }, - paymentAccount.address, - storeDidCallback - ) - // Create the full DID with a service endpoint - await submitTx(tx, paymentAccount) - const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain( - Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }) - ) - ) - const { document: fullDid } = - Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - - expect(fullDid.assertionMethod).toBeUndefined() - - // Try to set a new attestation key and a duplicate service endpoint - const updateTx = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batch, - did: fullDid.id, - extrinsics: [ - api.tx.did.setAttestationKey( - Did.DidChainV2.publicKeyToChain(authentication[0]) - ), - api.tx.did.addServiceEndpoint( - Did.DidChainV2.serviceToChain({ - id: '#id-1', - type: ['type-2'], - serviceEndpoint: ['x:url-2'], - }) - ), - ], - sign: getSignCallback(fullDid), - submitter: paymentAccount.address, - }) - // Now the second operation fails but the batch succeeds - await submitTx(updateTx, paymentAccount) - - const updatedFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(fullDid.id) - ) - const { document: updatedFullDid } = Did.DidRpc2.linkedInfoFromChain( - updatedFullDidLinkedInfo - ) - - expect(updatedFullDid).toMatchObject>({ - verificationMethod: [ - expect.objectContaining({ - // Authentication and assertionMethod - controller: fullDid.id, - type: 'MultiKey', - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }), - ], - // Old service maintained - service: [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - ], - }) - - expect(updatedFullDid.authentication).toHaveLength(1) - expect(updatedFullDid.keyAgreement).toBeUndefined() - // .setAttestationKey() extrinsic went through in the batch - expect(updatedFullDid.assertionMethod).toStrictEqual( - updatedFullDid.authentication - ) - expect(updatedFullDid.capabilityDelegation).toBeUndefined() - }, 60_000) - - it('batchAll fails if any extrinsics fails', async () => { - const { authentication, getSignCallback, storeDidCallback } = - TestUtilsV2.makeSigningKeyTool() - const createTx = await Did.DidChainV2.getStoreTxFromInput( - { - authentication, - service: [ - { - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }, - ], - }, - paymentAccount.address, - storeDidCallback - ) - await submitTx(createTx, paymentAccount) - const fullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain( - Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey( - authentication[0] - ), - }) - ) - ) - const { document: fullDid } = - Did.DidRpc2.linkedInfoFromChain(fullDidLinkedInfo) - - expect(fullDid.assertionMethod).toBeUndefined() - - // Use batchAll to set a new attestation key and a duplicate service endpoint - const updateTx = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batchAll, - did: fullDid.id, - extrinsics: [ - api.tx.did.setAttestationKey( - Did.DidChainV2.publicKeyToChain(authentication[0]) - ), - api.tx.did.addServiceEndpoint( - Did.DidChainV2.serviceToChain({ - id: '#id-1', - type: ['type-2'], - serviceEndpoint: ['x:url-2'], - }) - ), - ], - sign: getSignCallback(fullDid), - submitter: paymentAccount.address, - }) - - // Now, submitting will result in the second operation to fail AND the batch to fail, so we can test the atomic flag. - await expect(submitTx(updateTx, paymentAccount)).rejects.toMatchObject({ - section: 'did', - name: expect.stringMatching(/^ServiceAlready(Exists|Present)$/), - }) - - const updatedFullDidLinkedInfo = await api.call.did.query( - Did.DidChainV2.toChain(fullDid.id) - ) - const { document: updatedFullDid } = Did.DidRpc2.linkedInfoFromChain( - updatedFullDidLinkedInfo - ) - // .setAttestationKey() extrinsic went through but it was then reverted - expect(updatedFullDid.assertionMethod).toBeUndefined() - // The service endpoint will match the one manually added, and not the one set in the builder. - expect( - updatedFullDid.service?.find((s) => s.id === '#id-1') - ).toStrictEqual({ - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], - }) - }, 60_000) - }) -}) - -describe('DID extrinsics batching', () => { - let fullDid: DidDocumentV2.DidDocument - let key: TestUtilsV2.KeyTool - - beforeAll(async () => { - key = TestUtilsV2.makeSigningKeyTool() - fullDid = await TestUtilsV2.createFullDidFromSeed( - paymentAccount, - key.keypair - ) - }, 50_000) - - it('simple batch succeeds despite failures of some extrinsics', async () => { - const cType = CType.fromProperties(UUID.generate(), {}) - const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) - const rootNode = DelegationNode.newRoot({ - account: fullDid.id, - permissions: [Permission.DELEGATE], - cTypeHash: CType.idToHash(cType.$id), - }) - const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) - const tx = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batch, - did: fullDid.id, - extrinsics: [ - ctypeStoreTx, - // Will fail since the delegation cannot be revoked before it is added - delegationRevocationTx, - delegationStoreTx, - ], - sign: key.getSignCallback(fullDid), - submitter: paymentAccount.address, - }) - - // The entire submission promise is resolves and does not throw - await submitTx(tx, paymentAccount) - - // The ctype has been created, even though the delegation operations failed. - await expect(CType.verifyStored(cType)).resolves.not.toThrow() - }) - - it('batchAll fails if any extrinsics fail', async () => { - const cType = CType.fromProperties(UUID.generate(), {}) - const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) - const rootNode = DelegationNode.newRoot({ - account: fullDid.id, - permissions: [Permission.DELEGATE], - cTypeHash: CType.idToHash(cType.$id), - }) - const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) - const tx = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batchAll, - did: fullDid.id, - extrinsics: [ - ctypeStoreTx, - // Will fail since the delegation cannot be revoked before it is added - delegationRevocationTx, - delegationStoreTx, - ], - sign: key.getSignCallback(fullDid), - submitter: paymentAccount.address, - }) - - // The entire submission promise is rejected and throws. - await expect(submitTx(tx, paymentAccount)).rejects.toMatchObject({ - section: 'delegation', - name: 'DelegationNotFound', - }) - - // The ctype has not been created, since atomicity ensures the whole batch is reverted in case of failure. - await expect(CType.verifyStored(cType)).rejects.toThrow() - }) - - it('can batch extrinsics for the same required key type', async () => { - const web3NameClaimTx = api.tx.web3Names.claim('test-1') - const authorizedTx = await Did.DidDetailsV2.authorizeTx( - fullDid.id, - web3NameClaimTx, - key.getSignCallback(fullDid), - paymentAccount.address - ) - await submitTx(authorizedTx, paymentAccount) - - const web3Name1ReleaseExt = api.tx.web3Names.releaseByOwner() - const web3Name2ClaimExt = api.tx.web3Names.claim('test-2') - const tx = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batch, - did: fullDid.id, - extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], - sign: key.getSignCallback(fullDid), - submitter: paymentAccount.address, - }) - await submitTx(tx, paymentAccount) - - // Test for correct creation and deletion - const encoded1 = await api.call.did.queryByWeb3Name('test-1') - expect(encoded1.isSome).toBe(false) - // Test for correct creation of second web3 name - const encoded2 = await api.call.did.queryByWeb3Name('test-2') - expect(Did.DidRpc2.linkedInfoFromChain(encoded2).document.id).toStrictEqual( - fullDid.id - ) - }, 30_000) - - it('can batch extrinsics for different required key types', async () => { - // Authentication key - const web3NameReleaseExt = api.tx.web3Names.releaseByOwner() - // Attestation key - const ctype1 = CType.fromProperties(UUID.generate(), {}) - const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) - // Delegation key - const rootNode = DelegationNode.newRoot({ - account: fullDid.id, - permissions: [Permission.DELEGATE], - cTypeHash: CType.idToHash(ctype1.$id), - }) - const delegationHierarchyCreation = await rootNode.getStoreTx() - - // Authentication key - const web3NameNewClaimExt = api.tx.web3Names.claim('test-2') - // Attestation key - const ctype2 = CType.fromProperties(UUID.generate(), {}) - const ctype2Creation = api.tx.ctype.add(CType.toChain(ctype2)) - // Delegation key - const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.id) - - const batchedExtrinsics = await Did.DidDetailsV2.authorizeBatch({ - batchFunction: api.tx.utility.batchAll, - did: fullDid.id, - extrinsics: [ - web3NameReleaseExt, - ctype1Creation, - delegationHierarchyCreation, - web3NameNewClaimExt, - ctype2Creation, - delegationHierarchyRemoval, - ], - sign: key.getSignCallback(fullDid), - submitter: paymentAccount.address, - }) - - await submitTx(batchedExtrinsics, paymentAccount) - - // Test correct use of authentication keys - const encoded = await api.call.did.queryByWeb3Name('test') - expect(encoded.isSome).toBe(false) - - const { - document: { id }, - } = Did.DidRpc2.linkedInfoFromChain( - await api.call.did.queryByWeb3Name('test-2') - ) - expect(id).toStrictEqual(fullDid.id) - - // Test correct use of attestation keys - await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() - await expect(CType.verifyStored(ctype2)).resolves.not.toThrow() - - // Test correct use of delegation keys - const node = await DelegationNode.fetch(rootNode.id) - expect(node.revoked).toBe(true) - }) -}) - -describe('Runtime constraints', () => { - let testAuthKey: Did.DidDetailsV2.NewDidVerificationKey - const { keypair, storeDidCallback } = - TestUtilsV2.makeSigningKeyTool('ed25519') - - beforeAll(async () => { - testAuthKey = { - publicKey: keypair.publicKey, - type: 'ed25519', - } - }) - describe('DID creation', () => { - it('should not be possible to create a DID with too many encryption keys', async () => { - // Maximum is 10 - const newKeyAgreementKeys = Array(10).map( - (_, index): Did.DidDetailsV2.NewDidEncryptionKey => ({ - publicKey: Uint8Array.from(new Array(32).fill(index)), - type: 'x25519', - }) - ) - await Did.DidChainV2.getStoreTxFromInput( - { - authentication: [testAuthKey], - keyAgreement: newKeyAgreementKeys, - }, - paymentAccount.address, - storeDidCallback - ) - // One more than the maximum - newKeyAgreementKeys.push({ - publicKey: Uint8Array.from(new Array(32).fill(100)), - type: 'x25519', - }) - await expect( - Did.DidChainV2.getStoreTxFromInput( - { - authentication: [testAuthKey], - keyAgreement: newKeyAgreementKeys, - }, - - paymentAccount.address, - storeDidCallback - ) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The number of key agreement keys in the creation operation is greater than the maximum allowed, which is 10"` - ) - }, 30_000) - - it('should not be possible to create a DID with too many service endpoints', async () => { - // Maximum is 25 - const newServiceEndpoints = Array(25).map( - (_, index): Did.DidDetailsV2.NewService => ({ - id: `#service-${index}`, - type: [`type-${index}`], - serviceEndpoint: [`x:url-${index}`], - }) - ) - await Did.DidChainV2.getStoreTxFromInput( - { - authentication: [testAuthKey], - service: newServiceEndpoints, - }, - paymentAccount.address, - storeDidCallback - ) - // One more than the maximum - newServiceEndpoints.push({ - id: '#service-100', - type: ['type-100'], - serviceEndpoint: ['x:url-100'], - }) - await expect( - Did.DidChainV2.getStoreTxFromInput( - { - authentication: [testAuthKey], - service: newServiceEndpoints, - }, - - paymentAccount.address, - storeDidCallback - ) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot store more than 25 service endpoints per DID"` - ) - }, 30_000) - - it('should not be possible to create a DID with a service endpoint that is too long', async () => { - const serviceId = '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - const limit = api.consts.did.maxServiceIdLength.toNumber() - expect(serviceId.length).toBeGreaterThan(limit) - }) - - it('should not be possible to create a DID with a service endpoint that has too many types', async () => { - const types = ['type-1', 'type-2'] - const limit = api.consts.did.maxNumberOfTypesPerService.toNumber() - expect(types.length).toBeGreaterThan(limit) - }) - - it('should not be possible to create a DID with a service endpoint that has too many URIs', async () => { - const uris = ['x:url-1', 'x:url-2', 'x:url-3'] - const limit = api.consts.did.maxNumberOfUrlsPerService.toNumber() - expect(uris.length).toBeGreaterThan(limit) - }) - - it('should not be possible to create a DID with a service endpoint that has a type that is too long', async () => { - const type = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - const limit = api.consts.did.maxServiceTypeLength.toNumber() - expect(type.length).toBeGreaterThan(limit) - }) - - it('should not be possible to create a DID with a service endpoint that has a URI that is too long', async () => { - const uri = - 'a:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - const limit = api.consts.did.maxServiceUrlLength.toNumber() - expect(uri.length).toBeGreaterThan(limit) - }) - }) -}) - -afterAll(async () => { - await disconnect() -}) diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 10b14efa23..54b95b646f 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ /** * Copyright (c) 2018-2023, BOTLabs GmbH. * @@ -10,24 +11,28 @@ import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' import type { DecryptCallback, DidDocument, - DidKey, - DidServiceEndpoint, - DidVerificationKey, EncryptCallback, - KeyRelationship, KeyringPair, KiltEncryptionKeypair, KiltKeyringPair, - LightDidSupportedVerificationKeyType, - NewLightDidVerificationKey, SignCallback, + UriFragment, + VerificationMethod, + VerificationMethodRelationship, } from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' -import * as Did from '@kiltprotocol/did' +import type { + BaseNewDidKey, + ChainDidKey, + DidKeyType, + LightDidSupportedVerificationKeyType, + NewLightDidVerificationKey, + NewService, +} from '@kiltprotocol/did' +import { Crypto } from '@kiltprotocol/utils' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' -import { linkedInfoFromChain, toChain } from '@kiltprotocol/did' +import * as Did from '@kiltprotocol/did' export type EncryptionKeyToolCallback = ( didDocument: DidDocument @@ -45,10 +50,13 @@ export function makeEncryptCallback({ }: KiltEncryptionKeypair): EncryptionKeyToolCallback { return (didDocument) => { return async function encryptCallback({ data, peerPublicKey }) { - const keyId = didDocument.keyAgreement?.[0].id - if (!keyId) { - throw new Error(`Encryption key not found in did "${didDocument.uri}"`) + const keyId = didDocument.keyAgreement?.[0] + if (keyId === undefined) { + throw new Error(`Encryption key not found in did "${didDocument.id}"`) } + const verificationMethod = didDocument.verificationMethod?.find( + (v) => v.id === keyId + ) as VerificationMethod const { box, nonce } = Crypto.encryptAsymmetric( data, peerPublicKey, @@ -57,7 +65,7 @@ export function makeEncryptCallback({ return { nonce, data: box, - keyUri: `${didDocument.uri}${keyId}`, + verificationMethod, } } } @@ -119,25 +127,31 @@ export type KeyToolSignCallback = (didDocument: DidDocument) => SignCallback */ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { return (didDocument) => - async function sign({ data, keyRelationship }) { - const keyId = didDocument[keyRelationship]?.[0].id - const keyType = didDocument[keyRelationship]?.[0].type - if (keyId === undefined || keyType === undefined) { + async function sign({ data, verificationMethodRelationship }) { + const keyId = didDocument[verificationMethodRelationship]?.[0] + if (keyId === undefined) { throw new Error( - `Key for purpose "${keyRelationship}" not found in did "${didDocument.uri}"` + `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` + ) + } + const verificationMethod = didDocument.verificationMethod?.find( + (vm) => vm.id === keyId + ) + if (verificationMethod === undefined) { + throw new Error( + `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` ) } const signature = keypair.sign(data, { withType: false }) return { signature, - keyUri: `${didDocument.uri}${keyId}`, - keyType, + verificationMethod, } } } -type StoreDidCallback = Parameters['2'] +type StoreDidCallback = Parameters['2'] /** * Generates a callback that can be used for signing. @@ -152,7 +166,9 @@ export function makeStoreDidCallback( const signature = keypair.sign(data, { withType: false }) return { signature, - keyType: keypair.type, + verificationMethod: { + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), + }, } } } @@ -185,6 +201,48 @@ export function makeSigningKeyTool( } } +function doesVerificationMethodExist( + didDocument: DidDocument, + { id }: Pick +): boolean { + return ( + didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined + ) +} + +function addVerificationMethod( + didDocument: DidDocument, + verificationMethod: VerificationMethod, + relationship: VerificationMethodRelationship +): void { + const existingRelationship = didDocument[relationship] ?? [] + existingRelationship.push(verificationMethod.id) + // eslint-disable-next-line no-param-reassign + didDocument[relationship] = existingRelationship + if (!doesVerificationMethodExist(didDocument, verificationMethod)) { + const existingVerificationMethod = didDocument.verificationMethod ?? [] + existingVerificationMethod.push(verificationMethod) + // eslint-disable-next-line no-param-reassign + didDocument.verificationMethod = existingVerificationMethod + } +} + +function addKeypairAsVerificationMethod( + didDocument: DidDocument, + { id, publicKey, type: keyType }: BaseNewDidKey & { id: UriFragment }, + relationship: VerificationMethodRelationship +): void { + const verificationMethod = Did.didKeyToVerificationMethod( + didDocument.id, + id, + { + keyType: keyType as DidKeyType, + publicKey, + } + ) + addVerificationMethod(didDocument, verificationMethod, relationship) +} + /** * Given a keypair, creates a light DID with an authentication and an encryption key. * @@ -203,14 +261,14 @@ export async function createMinimalLightDidFromKeypair( } // Mock function to generate a key ID without having to rely on a real chain metadata. -export function computeKeyId(key: DidKey['publicKey']): DidKey['id'] { +export function computeKeyId(key: ChainDidKey['publicKey']): ChainDidKey['id'] { return `#${blake2AsHex(key, 256)}` } function makeDidKeyFromKeypair({ publicKey, type, -}: KiltKeyringPair): DidVerificationKey { +}: KiltKeyringPair): ChainDidKey { return { id: computeKeyId(publicKey), publicKey, @@ -238,39 +296,77 @@ export async function createLocalDemoFullDidFromKeypair( ]), endpoints = [], }: { - keyRelationships?: Set> - endpoints?: DidServiceEndpoint[] + keyRelationships?: Set< + Omit + > + endpoints?: NewService[] } = {} ): Promise { - const authKey = makeDidKeyFromKeypair(keypair) - const uri = Did.getFullDidUriFromKey(authKey) + const { + type: keyType, + publicKey, + id: authKeyId, + } = makeDidKeyFromKeypair(keypair) + const id = Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: keyType, + publicKey, + }), + }) const result: DidDocument = { - uri, - authentication: [authKey], + id, + authentication: [authKeyId], + verificationMethod: [ + Did.didKeyToVerificationMethod(id, authKeyId, { + keyType, + publicKey, + }), + ], service: endpoints, } if (keyRelationships.has('keyAgreement')) { - const encryptionKeypair = makeEncryptionKeyTool(`${keypair.publicKey}//enc`) - .keyAgreement[0] - const encKey = { - ...encryptionKeypair, - id: computeKeyId(encryptionKeypair.publicKey), - } - result.keyAgreement = [encKey] + const { publicKey: encPublicKey, type } = makeEncryptionKeyTool( + `${keypair.publicKey}//enc` + ).keyAgreement[0] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'keyAgreement' + ) } if (keyRelationships.has('assertionMethod')) { - const attKey = makeDidKeyFromKeypair( + const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//att') as KiltKeyringPair ) - result.assertionMethod = [attKey] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'assertionMethod' + ) } if (keyRelationships.has('capabilityDelegation')) { - const delKey = makeDidKeyFromKeypair( + const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//del') as KiltKeyringPair ) - result.capabilityDelegation = [delKey] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'capabilityDelegation' + ) } return result @@ -286,10 +382,10 @@ export async function createLocalDemoFullDidFromKeypair( export async function createLocalDemoFullDidFromLightDid( lightDid: DidDocument ): Promise { - const { uri, authentication } = lightDid + const { id, authentication } = lightDid return { - uri: Did.getFullDidUri(uri), + id, authentication, assertionMethod: authentication, capabilityDelegation: authentication, @@ -304,22 +400,25 @@ export async function createFullDidFromLightDid( sign: StoreDidCallback ): Promise { const api = ConfigService.get('api') - const { authentication, uri } = lightDidForId - const tx = await Did.getStoreTx( - { - authentication, - assertionMethod: authentication, - capabilityDelegation: authentication, - keyAgreement: lightDidForId.keyAgreement, - service: lightDidForId.service, - }, + const fullDidDocumentToBeCreated = lightDidForId + fullDidDocumentToBeCreated.assertionMethod = [ + fullDidDocumentToBeCreated.authentication![0], + ] + fullDidDocumentToBeCreated.capabilityDelegation = [ + fullDidDocumentToBeCreated.authentication![0], + ] + const tx = await Did.getStoreTxFromDidDocument( + fullDidDocumentToBeCreated, payer.address, sign ) await Blockchain.signAndSubmitTx(tx, payer) const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid - const encodedDidDetails = await queryFunction(toChain(Did.getFullDidUri(uri))) - return linkedInfoFromChain(encodedDidDetails).document + const encodedDidDetails = await queryFunction( + Did.toChain(fullDidDocumentToBeCreated.id) + ) + const { document } = await Did.linkedInfoFromChain(encodedDidDetails) + return document } export async function createFullDidFromSeed( diff --git a/tests/testUtils/TestUtils2.ts b/tests/testUtils/TestUtils2.ts deleted file mode 100644 index 6b42ecac4d..0000000000 --- a/tests/testUtils/TestUtils2.ts +++ /dev/null @@ -1,489 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' - -import type { - CryptoCallbacksV2, - DidDocument, - DidDocumentV2, - DidKey, - DidServiceEndpoint, - DidVerificationKey, - KeyRelationship, - KeyringPair, - KiltEncryptionKeypair, - KiltKeyringPair, - LightDidSupportedVerificationKeyType, - NewLightDidVerificationKey, -} from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' -import * as Did from '@kiltprotocol/did' - -import { Blockchain } from '@kiltprotocol/chain-helpers' -import { ConfigService } from '@kiltprotocol/config' - -export type EncryptionKeyToolCallback = ( - didDocument: DidDocumentV2.DidDocument -) => CryptoCallbacksV2.EncryptCallback - -/** - * Generates a callback that can be used for encryption. - * - * @param secretKey The options parameter. - * @param secretKey.secretKey The key to use for encryption. - * @returns The callback. - */ -export function makeEncryptCallback({ - secretKey, -}: KiltEncryptionKeypair): EncryptionKeyToolCallback { - return (didDocument) => { - return async function encryptCallback({ data, peerPublicKey }) { - const keyId = didDocument.keyAgreement?.[0] - if (keyId === undefined) { - throw new Error(`Encryption key not found in did "${didDocument.id}"`) - } - const verificationMethod = didDocument.verificationMethod?.find( - (v) => v.id === keyId - ) as DidDocumentV2.VerificationMethod - const { box, nonce } = Crypto.encryptAsymmetric( - data, - peerPublicKey, - secretKey - ) - return { - nonce, - data: box, - verificationMethod, - } - } - } -} - -/** - * Generates a callback that can be used for decryption. - * - * @param secretKey The options parameter. - * @param secretKey.secretKey The key to use for decryption. - * @returns The callback. - */ -export function makeDecryptCallback({ - secretKey, -}: KiltEncryptionKeypair): CryptoCallbacksV2.DecryptCallback { - return async function decryptCallback({ data, nonce, peerPublicKey }) { - const decrypted = Crypto.decryptAsymmetric( - { box: data, nonce }, - peerPublicKey, - secretKey - ) - if (decrypted === false) throw new Error('Decryption failed') - return { data: decrypted } - } -} - -export interface EncryptionKeyTool { - keyAgreement: [KiltEncryptionKeypair] - encrypt: EncryptionKeyToolCallback - decrypt: CryptoCallbacksV2.DecryptCallback -} - -/** - * Generates a keypair suitable for encryption. - * - * @param seed {string} Input to generate the keypair from. - * @returns Object with secret and public key and the key type. - */ -export function makeEncryptionKeyTool(seed: string): EncryptionKeyTool { - const keypair = Crypto.makeEncryptionKeypairFromSeed(blake2AsU8a(seed, 256)) - - const encrypt = makeEncryptCallback(keypair) - const decrypt = makeDecryptCallback(keypair) - - return { - keyAgreement: [keypair], - encrypt, - decrypt, - } -} - -export type KeyToolSignCallback = ( - didDocument: DidDocumentV2.DidDocument -) => CryptoCallbacksV2.SignCallback - -/** - * Generates a callback that can be used for signing. - * - * @param keypair The keypair to use for signing. - * @returns The callback. - */ -export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { - return (didDocument) => - async function sign({ data, verificationMethodRelationship }) { - const keyId = didDocument[verificationMethodRelationship]?.[0] - if (keyId === undefined) { - throw new Error( - `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` - ) - } - const verificationMethod = didDocument.verificationMethod?.find( - (vm) => vm.id === keyId - ) - if (verificationMethod === undefined) { - throw new Error( - `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` - ) - } - const signature = keypair.sign(data, { withType: false }) - - return { - signature, - verificationMethod, - } - } -} - -type StoreDidCallback = Parameters< - typeof Did.DidChainV2.getStoreTxFromInput ->['2'] - -/** - * Generates a callback that can be used for signing. - * - * @param keypair The keypair to use for signing. - * @returns The callback. - */ -export function makeStoreDidCallback( - keypair: KiltKeyringPair -): StoreDidCallback { - return async function sign({ data }) { - const signature = keypair.sign(data, { withType: false }) - return { - signature, - verificationMethod: { - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey(keypair), - }, - } - } -} - -export interface KeyTool { - keypair: KiltKeyringPair - getSignCallback: KeyToolSignCallback - storeDidCallback: StoreDidCallback - authentication: [NewLightDidVerificationKey] -} - -/** - * Generates a keypair usable for signing and a few related values. - * - * @param type The type to use for the keypair. - * @returns The keypair, matching sign callback, a key usable as DID authentication key. - */ -export function makeSigningKeyTool( - type: KiltKeyringPair['type'] = 'sr25519' -): KeyTool { - const keypair = Crypto.makeKeypairFromSeed(undefined, type) - const getSignCallback = makeSignCallback(keypair) - const storeDidCallback = makeStoreDidCallback(keypair) - - return { - keypair, - getSignCallback, - storeDidCallback, - authentication: [keypair as NewLightDidVerificationKey], - } -} - -function doesVerificationMethodExist( - didDocument: DidDocumentV2.DidDocument, - { id }: Pick -): boolean { - return ( - didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined - ) -} - -function addVerificationMethod( - didDocument: DidDocumentV2.DidDocument, - verificationMethod: DidDocumentV2.VerificationMethod, - relationship: DidDocumentV2.VerificationMethodRelationship -): void { - const existingRelationship = didDocument[relationship] ?? [] - existingRelationship.push(verificationMethod.id) - // eslint-disable-next-line no-param-reassign - didDocument[relationship] = existingRelationship - if (!doesVerificationMethodExist(didDocument, verificationMethod)) { - const existingVerificationMethod = didDocument.verificationMethod ?? [] - existingVerificationMethod.push(verificationMethod) - // eslint-disable-next-line no-param-reassign - didDocument.verificationMethod = existingVerificationMethod - } -} - -function addKeypairAsVerificationMethod( - didDocument: DidDocumentV2.DidDocument, - { - id, - publicKey, - type: keyType, - }: Did.DidDetailsV2.BaseNewDidKey & { id: DidDocumentV2.UriFragment }, - relationship: DidDocumentV2.VerificationMethodRelationship -): void { - const verificationMethod = Did.DidUtilsV2.didKeyToVerificationMethod( - didDocument.id, - id, - { - keyType: keyType as Did.DidDetailsV2.DidKeyType, - publicKey, - } - ) - addVerificationMethod(didDocument, verificationMethod, relationship) -} - -/** - * Given a keypair, creates a light DID with an authentication and an encryption key. - * - * @param keypair KeyringPair instance for authentication key. - * @returns DidDocument. - */ -export async function createMinimalLightDidFromKeypair( - keypair: KeyringPair -): Promise { - const type = keypair.type as LightDidSupportedVerificationKeyType - return Did.DidDetailsV2.createLightDidDocument({ - authentication: [{ publicKey: keypair.publicKey, type }], - keyAgreement: makeEncryptionKeyTool(`${keypair.publicKey}//enc`) - .keyAgreement, - }) -} - -// Mock function to generate a key ID without having to rely on a real chain metadata. -export function computeKeyId(key: DidKey['publicKey']): DidKey['id'] { - return `#${blake2AsHex(key, 256)}` -} - -function makeDidKeyFromKeypair({ - publicKey, - type, -}: KiltKeyringPair): DidVerificationKey { - return { - id: computeKeyId(publicKey), - publicKey, - type, - } -} - -/** - * Creates [[DidDocument]] for local use, e.g., in testing. Will not work on-chain because key IDs are generated ad-hoc. - * - * @param keypair The KeyringPair for authentication key, other keys derived from it. - * @param generationOptions The additional options for generation. - * @param generationOptions.keyRelationships The set of key relationships to indicate which keys must be added to the DID. - * @param generationOptions.endpoints The set of service endpoints that must be added to the DID. - * - * @returns A promise resolving to a [[DidDocument]] object. The resulting object is NOT stored on chain. - */ -export async function createLocalDemoFullDidFromKeypair( - keypair: KiltKeyringPair, - { - keyRelationships = new Set([ - 'assertionMethod', - 'capabilityDelegation', - 'keyAgreement', - ]), - endpoints = [], - }: { - keyRelationships?: Set> - endpoints?: DidServiceEndpoint[] - } = {} -): Promise { - const { - type: keyType, - publicKey, - id: authKeyId, - } = makeDidKeyFromKeypair(keypair) - const id = Did.DidUtilsV2.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: Did.DidUtilsV2.keypairToMultibaseKey({ - type: keyType, - publicKey, - }), - }) - - const result: DidDocumentV2.DidDocument = { - id, - authentication: [authKeyId], - verificationMethod: [ - Did.DidUtilsV2.didKeyToVerificationMethod(id, authKeyId, { - keyType, - publicKey, - }), - ], - service: endpoints, - } - - if (keyRelationships.has('keyAgreement')) { - const { publicKey: encPublicKey, type } = makeEncryptionKeyTool( - `${keypair.publicKey}//enc` - ).keyAgreement[0] - addKeypairAsVerificationMethod( - result, - { - id: computeKeyId(encPublicKey), - publicKey: encPublicKey, - type, - }, - 'keyAgreement' - ) - } - if (keyRelationships.has('assertionMethod')) { - const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( - keypair.derive('//att') as KiltKeyringPair - ) - addKeypairAsVerificationMethod( - result, - { - id: computeKeyId(encPublicKey), - publicKey: encPublicKey, - type, - }, - 'assertionMethod' - ) - } - if (keyRelationships.has('capabilityDelegation')) { - const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( - keypair.derive('//del') as KiltKeyringPair - ) - addKeypairAsVerificationMethod( - result, - { - id: computeKeyId(encPublicKey), - publicKey: encPublicKey, - type, - }, - 'capabilityDelegation' - ) - } - - return result -} - -/** - * Creates a full DID from a light DID where the verification keypair is enabled for all verification purposes (authentication, assertionMethod, capabilityDelegation). - * This is not recommended, use for demo purposes only! - * - * @param lightDid The light DID whose keys will be used on the full DID. - * @returns A full DID instance that is not yet written to the blockchain. - */ -export async function createLocalDemoFullDidFromLightDid( - lightDid: DidDocument -): Promise { - const { uri, authentication } = lightDid - - return { - uri: Did.getFullDidUri(uri), - authentication, - assertionMethod: authentication, - capabilityDelegation: authentication, - keyAgreement: lightDid.keyAgreement, - } -} - -// It takes the auth key from the light DID and use it as attestation and delegation key as well. -export async function createFullDidFromLightDid( - payer: KiltKeyringPair, - lightDidForId: DidDocumentV2.DidDocument, - sign: StoreDidCallback -): Promise { - const api = ConfigService.get('api') - const fullDidDocumentToBeCreated = lightDidForId - fullDidDocumentToBeCreated.assertionMethod = [ - fullDidDocumentToBeCreated.authentication![0], - ] - fullDidDocumentToBeCreated.capabilityDelegation = [ - fullDidDocumentToBeCreated.authentication![0], - ] - const tx = await Did.DidChainV2.getStoreTxFromDidDocument( - fullDidDocumentToBeCreated, - payer.address, - sign - ) - await Blockchain.signAndSubmitTx(tx, payer) - const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid - const encodedDidDetails = await queryFunction( - Did.DidChainV2.toChain(Did.getFullDidUri(fullDidDocumentToBeCreated.id)) - ) - const { - document: { - authentication, - uri, - assertionMethod, - capabilityDelegation, - keyAgreement, - service, - }, - } = await Did.linkedInfoFromChain(encodedDidDetails) - const didDocument: DidDocumentV2.DidDocument = { - id: uri, - authentication: [authentication[0].id], - verificationMethod: [ - Did.DidUtilsV2.didKeyToVerificationMethod(uri, authentication[0].id, { - keyType: authentication[0].type, - publicKey: authentication[0].publicKey, - }), - ], - service, - } - if (assertionMethod !== undefined) { - const { id, publicKey, type } = assertionMethod[0] - addKeypairAsVerificationMethod( - didDocument, - { - id, - publicKey, - type, - }, - 'assertionMethod' - ) - } - if (capabilityDelegation !== undefined) { - const { id, publicKey, type } = capabilityDelegation[0] - addKeypairAsVerificationMethod( - didDocument, - { - id, - publicKey, - type, - }, - 'capabilityDelegation' - ) - } - if (keyAgreement !== undefined && keyAgreement.length > 0) { - keyAgreement.forEach(({ id, type, publicKey }) => { - addKeypairAsVerificationMethod( - didDocument, - { - id, - publicKey, - type, - }, - 'keyAgreement' - ) - }) - } - - return didDocument -} - -export async function createFullDidFromSeed( - payer: KiltKeyringPair, - keypair: KiltKeyringPair -): Promise { - const lightDid = await createMinimalLightDidFromKeypair(keypair) - const sign = makeStoreDidCallback(keypair) - return createFullDidFromLightDid(payer, lightDid, sign) -} diff --git a/tests/testUtils/index.ts b/tests/testUtils/index.ts index 73ad2d773a..093a7233d5 100644 --- a/tests/testUtils/index.ts +++ b/tests/testUtils/index.ts @@ -7,5 +7,3 @@ export * as ApiMocks from './mocks/index.js' export * from './TestUtils.js' - -export * as TestUtilsV2 from './TestUtils2.js' From 6bedabedeb819ad84ed663e8c74101aac76416b8 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 10 Oct 2023 12:02:58 +0100 Subject: [PATCH 37/78] Fix TestUtils --- tests/testUtils/TestUtils.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 54b95b646f..9b9fe5a540 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ /** * Copyright (c) 2018-2023, BOTLabs GmbH. * @@ -18,7 +17,7 @@ import type { SignCallback, UriFragment, VerificationMethod, - VerificationMethodRelationship, + VerificationRelationship, } from '@kiltprotocol/types' import type { BaseNewDidKey, @@ -213,7 +212,7 @@ function doesVerificationMethodExist( function addVerificationMethod( didDocument: DidDocument, verificationMethod: VerificationMethod, - relationship: VerificationMethodRelationship + relationship: VerificationRelationship ): void { const existingRelationship = didDocument[relationship] ?? [] existingRelationship.push(verificationMethod.id) @@ -230,7 +229,7 @@ function addVerificationMethod( function addKeypairAsVerificationMethod( didDocument: DidDocument, { id, publicKey, type: keyType }: BaseNewDidKey & { id: UriFragment }, - relationship: VerificationMethodRelationship + relationship: VerificationRelationship ): void { const verificationMethod = Did.didKeyToVerificationMethod( didDocument.id, @@ -296,9 +295,7 @@ export async function createLocalDemoFullDidFromKeypair( ]), endpoints = [], }: { - keyRelationships?: Set< - Omit - > + keyRelationships?: Set> endpoints?: NewService[] } = {} ): Promise { @@ -402,9 +399,11 @@ export async function createFullDidFromLightDid( const api = ConfigService.get('api') const fullDidDocumentToBeCreated = lightDidForId fullDidDocumentToBeCreated.assertionMethod = [ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fullDidDocumentToBeCreated.authentication![0], ] fullDidDocumentToBeCreated.capabilityDelegation = [ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fullDidDocumentToBeCreated.authentication![0], ] const tx = await Did.getStoreTxFromDidDocument( From 6698cbc5198c34bcbce62c666dcce2135aad32fb Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 10 Oct 2023 13:43:19 +0100 Subject: [PATCH 38/78] verificationRelationship -> verificationRelationships --- packages/did/src/Did.signature.ts | 2 +- packages/did/src/DidResolver/DidResolver.ts | 2 +- packages/types/src/DidResolver.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index a2c9074667..c7ad11463c 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -119,7 +119,7 @@ export async function verifyDidSignature({ } if ( expectedVerificationMethodRelationship !== undefined && - !contentMetadata?.verificationRelationship?.includes( + !contentMetadata?.verificationRelationships?.includes( expectedVerificationMethodRelationship ) ) { diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 211f5e2466..f26779fa7d 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -271,7 +271,7 @@ async function dereferenceInternal( return { contentStream: dereferencedResource, contentMetadata: { - verificationRelationship, + verificationRelationships: verificationRelationship, }, } } diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index b356b58b11..2785103a4c 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -211,7 +211,7 @@ export type DereferenceContentMetadata = ResolutionDocumentMetadata & { * This field is optional and is set only if the dereferenced object is a verification method and it belongs to one of the verification methods of the DID Document. * This field is empty if the dereferenced object is a full DID Document or a service, or if the dereferences verification method is not linked to the DID Document by any specific relationship. */ - verificationRelationship?: VerificationRelationship[] + verificationRelationships?: VerificationRelationship[] } export type DereferenceResult = { From 0b2b4618caa540ab59f2f489c6d91aa06f0e6bd2 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 10 Oct 2023 15:15:40 +0100 Subject: [PATCH 39/78] Prepare ground for proper DID dereferencing --- packages/types/src/DidDocument.ts | 24 +++++++++++++++++++++++- packages/types/src/DidResolver.ts | 10 +--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index e98a9743c5..50bad52873 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -18,14 +18,36 @@ export type DidUri = | `did:kilt:${DidUriVersion}${KiltAddress}` | `did:kilt:light:${DidUriVersion}${AuthenticationKeyType}${KiltAddress}${LightDidEncodedData}` +export type UriQuery = `${string}=${string}` +type MAXIMUM_ALLOWED_BOUNDARY = 50 +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow +type Last = T extends [...infer _, infer Last] + ? Last + : never +type ConcatPrevious = Last extends string + ? `${Last}&${UriQuery}` + : never + +type Mapped< + N extends number, + Result extends unknown[] = [UriQuery] +> = Result['length'] extends N + ? Result + : Mapped]> + +export type UriQueryParams = Mapped[number] + /** * The fragment part of the DID URI including the `#` character. */ export type UriFragment = `#${string}` + /** * URI for DID resources like keys or service endpoints. */ -export type DidUrl = `${DidUri}${UriFragment}` +export type DidUrl = + | `${DidUri}${UriFragment}` + | `${DidUri}?${UriQueryParams}${UriFragment}` export type SignatureVerificationRelationship = | 'authentication' diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index 2785103a4c..360fdf0b0e 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -12,7 +12,6 @@ import type { VerificationMethod, Service, JsonLd, - VerificationRelationship, } from './DidDocument' /* @@ -205,14 +204,7 @@ export type DereferenceContentStream = | JsonLd | Buffer -export type DereferenceContentMetadata = ResolutionDocumentMetadata & { - /* - * NOT YET DRAFTED. DRAFTING WORK WILL START SOON. - * This field is optional and is set only if the dereferenced object is a verification method and it belongs to one of the verification methods of the DID Document. - * This field is empty if the dereferenced object is a full DID Document or a service, or if the dereferences verification method is not linked to the DID Document by any specific relationship. - */ - verificationRelationships?: VerificationRelationship[] -} +export type DereferenceContentMetadata = ResolutionDocumentMetadata export type DereferenceResult = { /* From 9878f4e8c91e358a6e66647f9e8ec0f29f9459bc Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 11 Oct 2023 10:22:49 +0100 Subject: [PATCH 40/78] Add support for query params in DID URLs --- packages/did/src/Did.signature.ts | 28 +++-- packages/did/src/Did.utils.ts | 55 ++++++++-- packages/did/src/DidDetails/DidDetails.ts | 14 +++ packages/did/src/DidResolver/DidResolver.ts | 116 ++++++++++++-------- packages/types/src/DidDocument.ts | 7 +- packages/types/src/DidResolver.ts | 3 +- 6 files changed, 148 insertions(+), 75 deletions(-) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index c7ad11463c..59c2013c43 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -11,6 +11,7 @@ import type { DidUrl, SignatureVerificationRelationship, SignResponseData, + UriFragment, VerificationMethod, } from '@kiltprotocol/types' @@ -63,7 +64,7 @@ function verifyDidSignatureDataStructure( `Expected signature as a hex string, got ${input.signature}` ) } - validateUri(verificationMethodUri, 'ResourceUri') + validateUri(verificationMethodUri, 'Url') } /** @@ -108,25 +109,22 @@ export async function verifyDidSignature({ } } - const { contentStream, contentMetadata } = await dereferenceDidUrl( - signerUrl, - {} - ) + // Add the expectedVerificationMethodRelationship to the DID URL before dereferencing. + const urlForDereference: DidUrl = + expectedVerificationMethodRelationship !== undefined + ? `${ + signer.did + }?requiredVerificationRelationship=${expectedVerificationMethodRelationship}${ + signer.fragment as UriFragment + }` + : signerUrl + + const { contentStream } = await dereferenceDidUrl(urlForDereference, {}) if (contentStream === undefined) { throw new SDKErrors.SignatureUnverifiableError( `Error validating the DID signature. Cannot fetch DID Document or the verification method for "${signerUrl}".` ) } - if ( - expectedVerificationMethodRelationship !== undefined && - !contentMetadata?.verificationRelationships?.includes( - expectedVerificationMethodRelationship - ) - ) { - throw new SDKErrors.SignatureUnverifiableError( - `Cannot find verification "${signerUrl} for the relationship "${expectedVerificationMethodRelationship}".` - ) - } const verificationMethod = contentStream as VerificationMethod const { publicKey } = multibaseKeyToDidKey( diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 3d384e2967..d91243f613 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -30,28 +30,45 @@ const FULL_DID_LATEST_VERSION = 1 // Matches the following full DIDs // - did:kilt: +// - did:kilt:? // - did:kilt:# +// - did:kilt:?# const FULL_KILT_DID_REGEX = - /^did:kilt:(?
4[1-9a-km-zA-HJ-NP-Z]{47})(?#[^#\n]+)?$/ + /^did:kilt:(?
4[1-9a-km-zA-HJ-NP-Z]{47})(?\?[^#]+)?(?#[^#\n]+)?$/ // Matches the following light DIDs // - did:kilt:light:00 // - did:kilt:light:01: +// - did:kilt:light:10? // - did:kilt:light:10# +// - did:kilt:light:01:? // - did:kilt:light:99:# +// - did:kilt:light:01:?# const LIGHT_KILT_DID_REGEX = - /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?#[^#\n]+)?$/ + /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?\?[^#\n]+)?(?#[^#\n]+)?$/ type IDidParsingResult = { did: DidUri version: number type: 'light' | 'full' address: KiltAddress + queryParameters?: Record fragment?: UriFragment authKeyTypeEncoding?: string encodedDetails?: string } +function exportQueryParamsFromUri(didUri: DidUrl): Record { + const urlified = new URL(didUri) + const params: Record = {} + urlified.searchParams.forEach((value, key) => { + if (params[value] === undefined) { + params[value] = key + } + }) + return params +} + /** * Parses a KILT DID uri and returns the information contained within in a structured form. * @@ -59,6 +76,7 @@ type IDidParsingResult = { * @returns Object containing information extracted from the DID uri. */ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { + // Then we check if it conforms to either a full or a light DID URI. let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups if (matches) { const { version: versionString, fragment } = matches @@ -66,11 +84,19 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { const version = versionString ? parseInt(versionString, 10) : FULL_DID_LATEST_VERSION + const queryParameters = (() => { + try { + return exportQueryParamsFromUri(didUri as DidUrl) + } catch { + throw new SDKErrors.InvalidDidFormatError(didUri) + } + })() return { did: didUri.replace(fragment || '', '') as DidUri, version, type: 'full', address, + queryParameters, fragment: fragment === '#' ? undefined : (fragment as UriFragment), } } @@ -88,11 +114,19 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { const version = versionString ? parseInt(versionString, 10) : LIGHT_DID_LATEST_VERSION + const queryParameters = (() => { + try { + return exportQueryParamsFromUri(didUri as DidUrl) + } catch { + throw new SDKErrors.InvalidDidFormatError(didUri) + } + })() return { did: didUri.replace(fragment || '', '') as DidUri, version, type: 'light', address, + queryParameters, fragment: fragment === '#' ? undefined : (fragment as UriFragment), encodedDetails, authKeyTypeEncoding: authKeyType, @@ -239,29 +273,26 @@ export function isSameSubject(didA: DidUri, didB: DidUri): boolean { * @param input Arbitrary input. * @param expectType `ResourceUri` if the URI is expected to have a fragment (following '#'), `Did` if it is expected not to have one. Default allows both. */ -export function validateUri( - input: unknown, - expectType?: 'Did' | 'ResourceUri' -): void { +export function validateUri(input: unknown, expectType?: 'Uri' | 'Url'): void { if (typeof input !== 'string') { throw new TypeError(`DID string expected, got ${typeof input}`) } - const { address, fragment } = parse(input as DidUri) + const { address, queryParameters, fragment } = parse(input as DidUri) if ( - fragment !== undefined && - (expectType === 'Did' || + (fragment !== undefined || queryParameters !== undefined) && + (expectType === 'Uri' || // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. (typeof expectType === 'boolean' && expectType === false)) ) { throw new SDKErrors.DidError( - 'Expected a Kilt DidUri but got a DidResourceUri (containing a #fragment)' + 'Expected a Kilt DidUri but got a DidUrl (containing a #fragment or a ?query_parameter)' ) } - if (fragment === undefined && expectType === 'ResourceUri') { + if (fragment === undefined && expectType === 'Url') { throw new SDKErrors.DidError( - 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' + 'Expected a Kilt DidUrl (containing a #fragment) but got a DidUri' ) } diff --git a/packages/did/src/DidDetails/DidDetails.ts b/packages/did/src/DidDetails/DidDetails.ts index 13e1848136..c531b43ef5 100644 --- a/packages/did/src/DidDetails/DidDetails.ts +++ b/packages/did/src/DidDetails/DidDetails.ts @@ -51,6 +51,20 @@ export function isValidDidKeyType(input: string): input is DidKeyType { export type NewVerificationMethod = Omit export type NewService = Service +export function isValidVerificationRelationship( + input: unknown +): input is VerificationRelationship { + switch (input as VerificationRelationship) { + case 'assertionMethod': + case 'authentication': + case 'capabilityDelegation': + case 'keyAgreement': + return true + default: + return false + } +} + /** * Type of a new key material to add under a DID. */ diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index f26779fa7d..4a8ea3a5a0 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -14,6 +14,7 @@ import type { DidResolver, DidUri, DidUrl, + FailedDereferenceMetadata, JsonLd, RepresentationResolutionResult, ResolutionDocumentMetadata, @@ -30,6 +31,7 @@ import { linkedInfoFromChain } from '../Did.rpc.js' import { toChain } from '../Did.chain.js' import { getFullDidUri, parse, validateUri } from '../Did.utils.js' import { parseDocumentFromLightDid } from '../DidDetails/LightDidDetails.js' +import { isValidVerificationRelationship } from '../DidDetails/DidDetails.js' const DID_JSON = 'application/did+json' const DID_JSON_LD = 'application/did+ld+json' @@ -121,7 +123,7 @@ export async function resolve( resolutionOptions: ResolutionOptions = {} ): Promise { try { - validateUri(did, 'Did') + validateUri(did, 'Uri') } catch (error) { return { didResolutionMetadata: { @@ -209,9 +211,17 @@ export async function resolveRepresentation( } as RepresentationResolutionResult } -type InternalDereferenceResult = { - contentStream?: DereferenceContentStream - contentMetadata: DereferenceContentMetadata +type InternalDereferenceResult = + | FailedDereferenceMetadata + | { + contentMetadata: DereferenceContentMetadata + contentStream: DereferenceContentStream + } + +function isFailedDereferenceMetadata( + input: InternalDereferenceResult +): input is FailedDereferenceMetadata { + return (input as FailedDereferenceMetadata)?.error !== undefined } async function dereferenceInternal( @@ -219,60 +229,82 @@ async function dereferenceInternal( // eslint-disable-next-line @typescript-eslint/no-unused-vars dereferenceOptions: DereferenceOptions ): Promise { - const { did, fragment } = parse(didUrl) + const { did, queryParameters, fragment } = parse(didUrl) const { didDocument, didDocumentMetadata } = await resolve(did) + if (didDocument === undefined) { + return { + error: 'notFound', + } + } + if (fragment === undefined) { return { - contentStream: didDocument, contentMetadata: didDocumentMetadata, + contentStream: didDocument, } } - // Return the dereferenced resource and its set of relationships with the controlling DID Document. - const [dereferencedResource, verificationRelationship] = (() => { + + const [dereferencedResource, dereferencingError] = (() => { const verificationMethod = didDocument?.verificationMethod?.find( - (vm) => vm.controller === didDocument.id && vm.id === fragment + ({ controller, id }) => controller === didDocument.id && id === fragment ) + if (verificationMethod !== undefined) { - const verificationRelationships: VerificationRelationship[] = [] - if ( - didDocument?.authentication?.find((a) => a === verificationMethod.id) - ) { - verificationRelationships.push('authentication') + const requiredVerificationRelationship = + queryParameters?.requiredVerificationRelationship + + // If a verification method is found and no filter is applied, return the retrieved verification method. + if (requiredVerificationRelationship === undefined) { + return [verificationMethod, null] } - if ( - didDocument?.assertionMethod?.find((a) => a === verificationMethod.id) - ) { - verificationRelationships.push('assertionMethod') + // If a verification method is found and the applied filter is invalid, return the dereferencing error. + if (!isValidVerificationRelationship(requiredVerificationRelationship)) { + return [ + null, + { + error: 'invalidVerificationRelationship', + } as FailedDereferenceMetadata, + ] } + // If a verification method is found and it matches the applied filter, return the retrieved verification method. if ( - didDocument?.capabilityDelegation?.find( - (a) => a === verificationMethod.id + didDocument[requiredVerificationRelationship]?.includes( + verificationMethod.id ) ) { - verificationRelationships.push('capabilityDelegation') - } - if (didDocument?.keyAgreement?.find((a) => a === verificationMethod.id)) { - verificationRelationships.push('keyAgreement') + return [verificationMethod, null] } + // Finally, if the above condition fails and the verification method does not pass the applied filter, the `notFound` error is returned. return [ - verificationMethod, - verificationRelationships.length > 0 - ? verificationRelationships - : undefined, + null, + { + error: 'notFound', + } as FailedDereferenceMetadata, ] } + // If no verification method is found, try to retrieve a service with the provided ID, ignoring any query parameters. const service = didDocument?.service?.find((s) => s.id === fragment) - return [service, undefined] + if (service === undefined) { + return [ + null, + { + error: 'notFound', + } as FailedDereferenceMetadata, + ] + } + return [service, null] })() + if (dereferencingError !== null) { + return dereferencingError + } + return { contentStream: dereferencedResource, - contentMetadata: { - verificationRelationships: verificationRelationship, - }, + contentMetadata: {}, } } @@ -306,40 +338,36 @@ export async function dereference( } } - const resolutionResult = await dereferenceInternal(didUrl, { + const dereferenceResult = await dereferenceInternal(didUrl, { accept: contentType, }) - const { contentMetadata, contentStream } = resolutionResult - - if (contentStream === undefined) { + if (isFailedDereferenceMetadata(dereferenceResult)) { return { - dereferencingMetadata: { - error: 'notFound', - }, - contentMetadata, + contentMetadata: {}, + dereferencingMetadata: dereferenceResult, } } const stream = (() => { if (contentType === 'application/did+json') { - return contentStream + return dereferenceResult.contentStream } if (contentType === 'application/did+ld+json') { return { - ...contentStream, + ...dereferenceResult.contentStream, '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], } } // contentType === 'application/did+cbor' - return Buffer.from(cbor.encode(contentStream)) + return Buffer.from(cbor.encode(dereferenceResult.contentStream)) })() return { dereferencingMetadata: { contentType, }, - contentMetadata, + contentMetadata: dereferenceResult.contentMetadata, contentStream: stream, } } diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 50bad52873..213310261a 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -19,7 +19,8 @@ export type DidUri = | `did:kilt:light:${DidUriVersion}${AuthenticationKeyType}${KiltAddress}${LightDidEncodedData}` export type UriQuery = `${string}=${string}` -type MAXIMUM_ALLOWED_BOUNDARY = 50 +// Only one query parameter is allowed +type MAXIMUM_ALLOWED_BOUNDARY = 1 // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow type Last = T extends [...infer _, infer Last] ? Last @@ -35,7 +36,7 @@ type Mapped< ? Result : Mapped]> -export type UriQueryParams = Mapped[number] +export type UriQueryParams = `?${Mapped[number]}` /** * The fragment part of the DID URI including the `#` character. @@ -47,7 +48,7 @@ export type UriFragment = `#${string}` */ export type DidUrl = | `${DidUri}${UriFragment}` - | `${DidUri}?${UriQueryParams}${UriFragment}` + | `${DidUri}${UriQueryParams}${UriFragment}` export type SignatureVerificationRelationship = | 'authentication' diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index 360fdf0b0e..e8e6dd6b63 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -186,8 +186,9 @@ export type FailedDereferenceMetadata = { * This specification defines the following common error values: * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. + * invalidVerificationRelationship: https://github.com/decentralized-identity/did-spec-extensions/pull/21 */ - error: 'invalidDidUrl' | 'notFound' + error: 'invalidDidUrl' | 'notFound' | 'invalidVerificationRelationship' } // Either success with `contentType` or failure with `error` From 8c592b45909a516b7be4f9c948fdefcaa1c9c721 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 11 Oct 2023 10:50:36 +0100 Subject: [PATCH 41/78] Compiling again --- packages/core/src/attestation/Attestation.ts | 2 +- .../src/credentialsV1/KiltAttestationProofV1.ts | 2 +- packages/did/src/Did.signature.ts | 14 ++++++-------- packages/did/src/DidResolver/DidResolver.ts | 1 - packages/legacy-credentials/src/Claim.ts | 2 +- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/core/src/attestation/Attestation.ts b/packages/core/src/attestation/Attestation.ts index 59311723ee..ec46fb2452 100644 --- a/packages/core/src/attestation/Attestation.ts +++ b/packages/core/src/attestation/Attestation.ts @@ -49,7 +49,7 @@ export function verifyDataStructure(input: IAttestation): void { if (!input.owner) { throw new SDKErrors.OwnerMissingError() } - Did.validateUri(input.owner, 'Did') + Did.validateUri(input.owner, 'Uri') if (typeof input.revoked !== 'boolean') { throw new SDKErrors.RevokedTypeError() diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index 1861eb0367..c6ecebe2cd 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -347,7 +347,7 @@ export async function verify( validateCredentialStructure(credential) const { nonTransferable, credentialStatus, credentialSubject, issuer } = credential - validateUri(issuer, 'Did') + validateUri(issuer, 'Uri') await validateSubject(credential, opts) // 4. check nonTransferable if (nonTransferable !== true) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 59c2013c43..17a43a0117 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -11,7 +11,6 @@ import type { DidUrl, SignatureVerificationRelationship, SignResponseData, - UriFragment, VerificationMethod, } from '@kiltprotocol/types' @@ -110,14 +109,13 @@ export async function verifyDidSignature({ } // Add the expectedVerificationMethodRelationship to the DID URL before dereferencing. - const urlForDereference: DidUrl = + const queryParams = expectedVerificationMethodRelationship !== undefined - ? `${ - signer.did - }?requiredVerificationRelationship=${expectedVerificationMethodRelationship}${ - signer.fragment as UriFragment - }` - : signerUrl + ? `?requiredVerificationRelationship=${expectedVerificationMethodRelationship}` + : '' + const uriFragment = signer.fragment !== undefined ? `${signer.fragment}` : '' + const urlForDereference = + `${signer.did}${queryParams}${uriFragment}` as DidUrl const { contentStream } = await dereferenceDidUrl(urlForDereference, {}) if (contentStream === undefined) { diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 4a8ea3a5a0..f553720f2b 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -20,7 +20,6 @@ import type { ResolutionDocumentMetadata, ResolutionOptions, ResolutionResult, - VerificationRelationship, } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' diff --git a/packages/legacy-credentials/src/Claim.ts b/packages/legacy-credentials/src/Claim.ts index cf21a4b6f1..a39ce8dbed 100644 --- a/packages/legacy-credentials/src/Claim.ts +++ b/packages/legacy-credentials/src/Claim.ts @@ -143,7 +143,7 @@ export function verifyDataStructure(input: IClaim | PartialClaim): void { throw new SDKErrors.CTypeHashMissingError() } if ('owner' in input) { - Did.validateUri(input.owner, 'Did') + Did.validateUri(input.owner, 'Uri') } if (input.contents !== undefined) { Object.entries(input.contents).forEach(([key, value]) => { From 845e319aa4c0b0a9d86996f6fb48f70acb3ba5d5 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 11 Oct 2023 14:41:27 +0100 Subject: [PATCH 42/78] WIP dereferencing in tests --- packages/did/src/Did.signature.spec.ts | 101 ++++++++++++------ packages/did/src/Did.signature.ts | 10 +- packages/did/src/Did.utils.ts | 10 +- .../did/src/DidDetails/FullDidDetails.spec.ts | 14 +-- .../src/suites/KiltAttestationProofV1.spec.ts | 2 +- 5 files changed, 91 insertions(+), 46 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 6093d4b148..16dba490e7 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -19,28 +19,28 @@ import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' import type { DidSignature } from './Did.signature' import type { NewLightDidVerificationKey } from './DidDetails' -import { TestUtilsV2 } from '../../../tests/testUtils' +import { makeSigningKeyTool } from '../../../tests/testUtils' import { isDidSignature, signatureFromJson, signatureToJson, verifyDidSignature, } from './Did.signature' -import { resolve } from './DidResolver' +import { dereference } from './DidResolver/DidResolver' import { keypairToMultibaseKey, multibaseKeyToDidKey, parse } from './Did.utils' import { createLightDidDocument } from './DidDetails' -jest.mock('./DidResolver') +jest.mock('./DidResolver/DidResolver') jest - .mocked(resolve) - .mockImplementation(jest.requireActual('./DidResolver').resolve) + .mocked(dereference) + .mockImplementation(jest.requireActual('./DidResolver').dereference) describe('light DID', () => { let keypair: KiltKeyringPair let did: DidDocument let sign: SignCallback beforeAll(() => { - const keyTool = TestUtilsV2.makeSigningKeyTool() + const keyTool = makeSigningKeyTool() keypair = keyTool.keypair did = createLightDidDocument({ authentication: keyTool.authentication, @@ -50,18 +50,32 @@ describe('light DID', () => { beforeEach(() => { jest - .mocked(resolve) + .mocked(dereference) .mockReset() .mockImplementation(async (didUrl) => { - const { address } = parse(didUrl) - if (address === keypair.address) { + const { address, queryParameters } = parse(didUrl) + if ( + address === keypair.address && + (queryParameters === undefined || + queryParameters.requiredVerificationRelationship === + 'authentication') + ) { return { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - didDocument: did, + contentMetadata: {}, + dereferencingMetadata: { + contentType: 'application/did+json', + }, + contentStream: did.verificationMethod?.find( + ({ id }) => id === did.authentication![0] + ), } } - return Promise.reject() + return { + contentMetadata: {}, + dereferencingMetadata: { + error: 'notFound', + }, + } }) }) @@ -99,7 +113,7 @@ describe('light DID', () => { const deserialized = signatureFromJson(oldSignature) expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.verificationMethodUri).toStrictEqual(signerUrl) + expect(deserialized.signerUrl).toStrictEqual(signerUrl) expect(deserialized).not.toHaveProperty('keyId') }) @@ -146,7 +160,10 @@ describe('light DID', () => { verificationMethodRelationship: 'authentication', }) const wrongVerificationMethodId = `${verificationMethod.id}1a` - jest.mocked(resolve).mockRejectedValue(new Error('DID not found')) + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) await expect( verifyDidSignature({ message: SIGNED_STRING, @@ -175,7 +192,7 @@ describe('light DID', () => { }) it('fails if key id malformed', async () => { - jest.mocked(resolve).mockRestore() + jest.mocked(dereference).mockRestore() const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const let { signature, verificationMethod } = await sign({ @@ -188,15 +205,19 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: - `${did.id}${malformedVerificationId}` as DidUrl, + signerUrl: `${did.id}${malformedVerificationId}` as DidUrl, expectedVerificationMethodRelationship: 'authentication', }) ).rejects.toThrow() }) it('does not verify if migrated to Full DID', async () => { - jest.mocked(resolve).mockRejectedValue(new Error('Migrated')) + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: { + canonicalId: did.id, + }, + dereferencingMetadata: { contentType: 'application/did+json' }, + }) const SIGNED_STRING = 'signed string' const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), @@ -230,7 +251,7 @@ describe('light DID', () => { }) const expectedSigner = createLightDidDocument({ - authentication: TestUtilsV2.makeSigningKeyTool().authentication, + authentication: makeSigningKeyTool().authentication, }).id await expect( @@ -316,18 +337,32 @@ describe('full DID', () => { beforeEach(() => { jest - .mocked(resolve) + .mocked(dereference) .mockReset() - .mockImplementation(async (didUri) => { - const { address } = parse(didUri) - if (address === keypair.address) { + .mockImplementation(async (didUrl) => { + const { address, queryParameters } = parse(didUrl) + if ( + address === keypair.address && + (queryParameters === undefined || + queryParameters.requiredVerificationRelationship === + 'authentication') + ) { return { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - didDocument: did, + contentMetadata: {}, + dereferencingMetadata: { + contentType: 'application/did+json', + }, + contentStream: did.verificationMethod?.find( + ({ id }) => id === did.authentication![0] + ), } } - return Promise.reject() + return { + contentMetadata: {}, + dereferencingMetadata: { + error: 'notFound', + }, + } }) }) @@ -366,7 +401,10 @@ describe('full DID', () => { }) it('does not verify if deactivated', async () => { - jest.mocked(resolve).mockRejectedValue(new Error('Deactivated')) + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: { deactivated: true }, + dereferencingMetadata: { contentType: 'application/did+json' }, + }) const SIGNED_STRING = 'signed string' const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), @@ -384,7 +422,10 @@ describe('full DID', () => { }) it('does not verify if not on chain', async () => { - jest.mocked(resolve).mockRejectedValue(new Error('Not on chain')) + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) const SIGNED_STRING = 'signed string' const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 17a43a0117..7310be9c15 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -6,9 +6,9 @@ */ import type { - DereferenceDidUrl, DidUri, DidUrl, + ResolveDid, SignatureVerificationRelationship, SignResponseData, VerificationMethod, @@ -18,7 +18,7 @@ import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { isHex } from '@polkadot/util' import { multibaseKeyToDidKey, parse, validateUri } from './Did.utils.js' -import { dereference } from './DidResolver/DidResolver.js' +import { resolve } from './DidResolver/DidResolver.js' export type DidSignatureVerificationInput = { message: string | Uint8Array @@ -27,7 +27,7 @@ export type DidSignatureVerificationInput = { expectedSigner?: DidUri allowUpgraded?: boolean expectedVerificationMethodRelationship?: SignatureVerificationRelationship - dereferenceDidUrl?: DereferenceDidUrl['dereference'] + resolveDid?: ResolveDid['resolve'] } export type DidSignature = { @@ -77,7 +77,7 @@ function verifyDidSignatureDataStructure( * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. - * @param input.dereferenceDidUrl Allows specifying a custom DID dereference. Defaults to the built-in [[dereferenceDidUrl]]. + * @param input.resolveDid Allows specifying a custom DID resolver. Defaults to the built-in [[resolve]]. */ export async function verifyDidSignature({ message, @@ -86,7 +86,7 @@ export async function verifyDidSignature({ expectedSigner, allowUpgraded = false, expectedVerificationMethodRelationship, - dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + resolveDid = resolve, }: DidSignatureVerificationInput): Promise { // checks if key uri points to the right did; alternatively we could check the key's controller const signer = parse(signerUrl) diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index d91243f613..caf531371f 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -62,8 +62,8 @@ function exportQueryParamsFromUri(didUri: DidUrl): Record { const urlified = new URL(didUri) const params: Record = {} urlified.searchParams.forEach((value, key) => { - if (params[value] === undefined) { - params[value] = key + if (params[key] === undefined) { + params[key] = value } }) return params @@ -86,7 +86,8 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { : FULL_DID_LATEST_VERSION const queryParameters = (() => { try { - return exportQueryParamsFromUri(didUri as DidUrl) + const queryParams = exportQueryParamsFromUri(didUri as DidUrl) + return Object.keys(queryParams).length > 0 ? queryParams : undefined } catch { throw new SDKErrors.InvalidDidFormatError(didUri) } @@ -116,7 +117,8 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { : LIGHT_DID_LATEST_VERSION const queryParameters = (() => { try { - return exportQueryParamsFromUri(didUri as DidUrl) + const queryParams = exportQueryParamsFromUri(didUri as DidUrl) + return Object.keys(queryParams).length > 0 ? queryParams : undefined } catch { throw new SDKErrors.InvalidDidFormatError(didUri) } diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 8378ea6f99..585a80ab83 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -16,7 +16,11 @@ import { BN } from '@polkadot/util' import { randomAsHex } from '@polkadot/util-crypto' import { ConfigService } from '@kiltprotocol/config' -import { ApiMocks, TestUtilsV2 } from '../../../../tests/testUtils' +import { + ApiMocks, + createLocalDemoFullDidFromKeypair, + makeSigningKeyTool, +} from '../../../../tests/testUtils' import { generateDidAuthenticatedTx } from '../Did.chain.js' import { authorizeBatch, @@ -27,7 +31,7 @@ const augmentedApi = ApiMocks.createAugmentedApi() const mockedApi: any = ApiMocks.getMockedApi() ConfigService.set({ api: mockedApi }) -jest.mock('../Did2.chain') +jest.mock('../Did.chain') jest .mocked(generateDidAuthenticatedTx) .mockResolvedValue({} as SubmittableExtrinsic) @@ -45,11 +49,9 @@ describe('When creating an instance from the chain', () => { let fullDid: DidDocument beforeAll(async () => { - const keyTool = TestUtilsV2.makeSigningKeyTool() + const keyTool = makeSigningKeyTool() keypair = keyTool.keypair - fullDid = await TestUtilsV2.createLocalDemoFullDidFromKeypair( - keyTool.keypair - ) + fullDid = await createLocalDemoFullDidFromKeypair(keyTool.keypair) sign = keyTool.getSignCallback(fullDid) }) diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index 99feaceffa..98c30e4b90 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -59,7 +59,7 @@ import { makeFakeDid } from './Sr25519Signature2020.spec' jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - resolveCompliant: jest.fn(), + resolve: jest.fn(), authorizeTx: jest.fn(), })) From 7739f4e442b6c70be033a8cf2ccaa9a58a4c343d Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 11 Oct 2023 15:49:54 +0100 Subject: [PATCH 43/78] Credential.spec.ts for legacy credential almost passing --- packages/did/src/Did.signature.ts | 58 +++++--- packages/did/src/Did.utils.ts | 9 +- .../legacy-credentials/src/Credential.spec.ts | 136 +++++++++++++----- packages/types/src/DidDocument.ts | 24 +--- 4 files changed, 141 insertions(+), 86 deletions(-) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 7310be9c15..0c7fc66454 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -6,19 +6,19 @@ */ import type { + DereferenceDidUrl, + DidDocument, DidUri, DidUrl, - ResolveDid, SignatureVerificationRelationship, SignResponseData, - VerificationMethod, } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { isHex } from '@polkadot/util' import { multibaseKeyToDidKey, parse, validateUri } from './Did.utils.js' -import { resolve } from './DidResolver/DidResolver.js' +import { dereference } from './DidResolver/DidResolver.js' export type DidSignatureVerificationInput = { message: string | Uint8Array @@ -27,7 +27,7 @@ export type DidSignatureVerificationInput = { expectedSigner?: DidUri allowUpgraded?: boolean expectedVerificationMethodRelationship?: SignatureVerificationRelationship - resolveDid?: ResolveDid['resolve'] + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } export type DidSignature = { @@ -77,7 +77,7 @@ function verifyDidSignatureDataStructure( * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. - * @param input.resolveDid Allows specifying a custom DID resolver. Defaults to the built-in [[resolve]]. + * @param input.dereferenceDidUrl Allows specifying a custom DID dereferenced. Defaults to the built-in [[dereference]]. */ export async function verifyDidSignature({ message, @@ -86,7 +86,7 @@ export async function verifyDidSignature({ expectedSigner, allowUpgraded = false, expectedVerificationMethodRelationship, - resolveDid = resolve, + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: DidSignatureVerificationInput): Promise { // checks if key uri points to the right did; alternatively we could check the key's controller const signer = parse(signerUrl) @@ -107,23 +107,47 @@ export async function verifyDidSignature({ throw new SDKErrors.DidSubjectMismatchError(signer.did, expected.did) } } + if (signer.fragment === undefined) { + throw new SDKErrors.DidError( + `Key URI "${signer}" is not a valid DID resource.` + ) + } - // Add the expectedVerificationMethodRelationship to the DID URL before dereferencing. - const queryParams = - expectedVerificationMethodRelationship !== undefined - ? `?requiredVerificationRelationship=${expectedVerificationMethodRelationship}` - : '' - const uriFragment = signer.fragment !== undefined ? `${signer.fragment}` : '' - const urlForDereference = - `${signer.did}${queryParams}${uriFragment}` as DidUrl - - const { contentStream } = await dereferenceDidUrl(urlForDereference, {}) + const { contentStream, contentMetadata } = await dereferenceDidUrl( + signer.did, + {} + ) if (contentStream === undefined) { throw new SDKErrors.SignatureUnverifiableError( `Error validating the DID signature. Cannot fetch DID Document or the verification method for "${signerUrl}".` ) } - const verificationMethod = contentStream as VerificationMethod + // If the light DID has been upgraded we consider the old key URI invalid, the full DID URI should be used instead. + if (contentMetadata.canonicalId !== undefined) { + throw new SDKErrors.DidResolveUpgradedDidError() + } + if (contentMetadata.deactivated) { + throw new SDKErrors.DidDeactivatedError() + } + const didDocument = contentStream as DidDocument + const verificationMethod = didDocument.verificationMethod?.find( + ({ controller, id }) => + controller === didDocument.id && id === signer.fragment + ) + if (verificationMethod === undefined) { + throw new SDKErrors.DidNotFoundError('Verification method not found in DID') + } + // Check whether the provided key ID is within the keys for a given verification relationship, if provided. + if ( + expectedVerificationMethodRelationship && + !didDocument[expectedVerificationMethodRelationship]?.some( + (id) => id === verificationMethod.id + ) + ) { + throw new SDKErrors.DidError( + `No key "${signer.fragment}" for the verification method "${expectedVerificationMethodRelationship}"` + ) + } const { publicKey } = multibaseKeyToDidKey( verificationMethod.publicKeyMultibase diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index caf531371f..1ed499bc5b 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -30,22 +30,17 @@ const FULL_DID_LATEST_VERSION = 1 // Matches the following full DIDs // - did:kilt: -// - did:kilt:? // - did:kilt:# -// - did:kilt:?# const FULL_KILT_DID_REGEX = - /^did:kilt:(?
4[1-9a-km-zA-HJ-NP-Z]{47})(?\?[^#]+)?(?#[^#\n]+)?$/ + /^did:kilt:(?
4[1-9a-km-zA-HJ-NP-Z]{47})(?#[^#\n]+)?$/ // Matches the following light DIDs // - did:kilt:light:00 // - did:kilt:light:01: -// - did:kilt:light:10? // - did:kilt:light:10# -// - did:kilt:light:01:? // - did:kilt:light:99:# -// - did:kilt:light:01:?# const LIGHT_KILT_DID_REGEX = - /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?\?[^#\n]+)?(?#[^#\n]+)?$/ + /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?#[^#\n]+)?$/ type IDidParsingResult = { did: DidUri diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index f051ec340d..2946a0b950 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -13,6 +13,7 @@ import { ConfigService } from '@kiltprotocol/config' import { Attestation, CType, init } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import type { + DereferenceResult, DidDocument, DidUri, DidUrl, @@ -21,13 +22,19 @@ import type { IClaimContents, ICredential, ICredentialPresentation, - ResolutionResult, SignCallback, + VerificationMethod, } from '@kiltprotocol/types' +import { + didKeyToVerificationMethod, + NewDidVerificationKey, + SupportedContentType, +} from '@kiltprotocol/did' import { Crypto, SDKErrors, UUID } from '@kiltprotocol/utils' import { ApiMocks, + computeKeyId, createLocalDemoFullDidFromKeypair, KeyTool, makeSigningKeyTool, @@ -435,16 +442,26 @@ describe('Presentations', () => { let identityDave: DidDocument let migratedAndDeletedLightDid: DidDocument - async function resolveDid(keyUri: DidUrl): Promise { - const { did } = Did.parse(keyUri) + async function dereferenceDidUrl( + didUrl: DidUrl | DidUri + ): Promise> { + const { did } = Did.parse(didUrl) const didDocument = [ identityAlice, identityBob, identityCharlie, identityDave, ].find(({ id }) => id === did) - if (!didDocument) throw new Error('Cannot resolve mocked DID') - return { didDocumentMetadata: {}, didResolutionMetadata: {}, didDocument } + if (!didDocument) + return { + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + } + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: didDocument, + } } // TODO: Cleanup file by migrating setup functions and removing duplicate tests. @@ -524,7 +541,7 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - resolveDid, + dereferenceDidUrl, }) ).resolves.toMatchObject({ revoked: false, attester: identityBob.id }) }) @@ -550,7 +567,7 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).resolves.toMatchObject({ revoked: false, attester: identityBob.id }) }) @@ -568,7 +585,7 @@ describe('Presentations', () => { await expect( Credential.verifyPresentation(credential as ICredentialPresentation, { ctype: testCType, - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow() }) @@ -596,7 +613,7 @@ describe('Presentations', () => { await expect( Credential.verifySignature(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) @@ -623,12 +640,14 @@ describe('Presentations', () => { signCallback: keyAlice.getSignCallback(identityAlice), }) // but replace signer key reference with authentication key of light did - presentation.claimerSignature.keyUri = `${identityDave.id}${identityDave.authentication[0].id}` + presentation.claimerSignature.signerUrl = `${identityDave.id}${ + identityDave.authentication![0] + }` // signature would check out but mismatch should be detected await expect( Credential.verifySignature(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) @@ -655,7 +674,7 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrowError() }) @@ -726,29 +745,71 @@ describe('create presentation', () => { // Returns a full DID that has the same subject of the first light DID, but the same key authentication key as the second one, if provided, or as the first one otherwise. function createMinimalFullDidFromLightDid( lightDidForId: DidDocument, - newAuthenticationKey?: DidVerificationKey + newAuthenticationKey?: NewDidVerificationKey ): DidDocument { - const uri = Did.getFullDidUri(lightDidForId.id) - const authKey = newAuthenticationKey || lightDidForId.authentication[0] + const id = Did.getFullDidUri(lightDidForId.id) + const authMethod = (() => { + if (newAuthenticationKey !== undefined) { + return didKeyToVerificationMethod( + id, + computeKeyId(newAuthenticationKey.publicKey), + { + keyType: newAuthenticationKey?.type, + publicKey: newAuthenticationKey.publicKey, + } + ) + } + const lightDidAuth = lightDidForId.authentication![0] + const lightDidVerificationMethod = lightDidForId.verificationMethod?.find( + ({ id: vmId }) => vmId === lightDidAuth + ) as VerificationMethod + const { publicKey } = Did.multibaseKeyToDidKey( + lightDidVerificationMethod.publicKeyMultibase + ) + lightDidVerificationMethod.id = computeKeyId(publicKey) + return lightDidVerificationMethod + })() return { - uri, - authentication: [authKey], + id, + authentication: [authMethod.id], + verificationMethod: [authMethod], } } - async function didResolveKey( - keyUri: DidResourceUri - ): Promise { - const { did } = Did.parse(keyUri) - const document = [ - migratedClaimerLightDid, - unmigratedClaimerLightDid, - migratedClaimerFullDid, - attester, - ].find(({ uri }) => uri === did) - if (!document) throw new Error('Cannot resolve mocked DID') - return Did.keyToResolvedKey(document.authentication[0], did) + async function dereferenceDidUrl( + didUrl: DidUrl | DidUri + ): Promise> { + const { did } = Did.parse(didUrl) + if (did === migratedClaimerLightDid.id) { + return { + contentMetadata: { canonicalId: migratedClaimerFullDid.id }, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: migratedClaimerLightDid, + } + } + if (did === unmigratedClaimerLightDid.id) { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: unmigratedClaimerLightDid, + } + } + if (did === migratedClaimerFullDid.id) { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: unmigratedClaimerLightDid, + } + } + if (did === attester.id) { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: attester, + } + } + return { contentMetadata: {}, dereferencingMetadata: { error: 'notFound' } } } beforeAll(async () => { @@ -768,10 +829,7 @@ describe('create presentation', () => { newKeyForMigratedClaimerDid = makeSigningKeyTool() migratedClaimerFullDid = createMinimalFullDidFromLightDid( migratedClaimerLightDid, - { - ...newKeyForMigratedClaimerDid.authentication[0], - id: '#new-auth', - } + { ...newKeyForMigratedClaimerDid.keypair } ) migratedThenDeletedKey = makeSigningKeyTool('ed25519') migratedThenDeletedClaimerLightDid = Did.createLightDidDocument({ @@ -813,7 +871,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).resolves.toMatchObject({ revoked: false, attester: attester.id }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) @@ -842,12 +900,12 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).resolves.toMatchObject({ revoked: false, attester: attester.id }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) - it('should create presentation and exclude specific attributes using a migrated DID', async () => { + it.only('should create presentation and exclude specific attributes using a migrated DID', async () => { // cannot be used since the variable needs to be established in the outer scope credential = Credential.fromClaim( Claim.fromCTypeAndClaimContents( @@ -873,7 +931,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).resolves.toMatchObject({ revoked: false, attester: attester.id }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) @@ -905,7 +963,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(att, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow() }) @@ -936,7 +994,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow() }) diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 213310261a..586c17d415 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -18,26 +18,6 @@ export type DidUri = | `did:kilt:${DidUriVersion}${KiltAddress}` | `did:kilt:light:${DidUriVersion}${AuthenticationKeyType}${KiltAddress}${LightDidEncodedData}` -export type UriQuery = `${string}=${string}` -// Only one query parameter is allowed -type MAXIMUM_ALLOWED_BOUNDARY = 1 -// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-shadow -type Last = T extends [...infer _, infer Last] - ? Last - : never -type ConcatPrevious = Last extends string - ? `${Last}&${UriQuery}` - : never - -type Mapped< - N extends number, - Result extends unknown[] = [UriQuery] -> = Result['length'] extends N - ? Result - : Mapped]> - -export type UriQueryParams = `?${Mapped[number]}` - /** * The fragment part of the DID URI including the `#` character. */ @@ -46,9 +26,7 @@ export type UriFragment = `#${string}` /** * URI for DID resources like keys or service endpoints. */ -export type DidUrl = - | `${DidUri}${UriFragment}` - | `${DidUri}${UriQueryParams}${UriFragment}` +export type DidUrl = `${DidUri}${UriFragment}` | `${DidUri}${UriFragment}` export type SignatureVerificationRelationship = | 'authentication' From 78bb06bf6f1adab20a52675596adcd3163c11688 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 11 Oct 2023 16:34:03 +0100 Subject: [PATCH 44/78] Did.signature.spec.ts working again --- packages/did/src/Did.signature.spec.ts | 98 +++++++++---------- packages/did/src/Did.signature.ts | 15 +-- .../legacy-credentials/src/Credential.spec.ts | 4 +- packages/legacy-credentials/src/Credential.ts | 4 +- packages/types/src/CryptoCallbacks.ts | 2 +- .../src/suites/KiltAttestationProofV1.spec.ts | 7 +- 6 files changed, 67 insertions(+), 63 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 16dba490e7..2cf9a8e549 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -11,6 +11,7 @@ import type { DidDocument, SignCallback, DidUrl, + DereferenceResult, } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' @@ -26,7 +27,7 @@ import { signatureToJson, verifyDidSignature, } from './Did.signature' -import { dereference } from './DidResolver/DidResolver' +import { dereference, SupportedContentType } from './DidResolver/DidResolver' import { keypairToMultibaseKey, multibaseKeyToDidKey, parse } from './Did.utils' import { createLightDidDocument } from './DidDetails' @@ -52,31 +53,22 @@ describe('light DID', () => { jest .mocked(dereference) .mockReset() - .mockImplementation(async (didUrl) => { - const { address, queryParameters } = parse(didUrl) - if ( - address === keypair.address && - (queryParameters === undefined || - queryParameters.requiredVerificationRelationship === - 'authentication') - ) { + .mockImplementation( + async (didUrl): Promise> => { + const { address } = parse(didUrl) + if (address === keypair.address) { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: did, + } + } return { contentMetadata: {}, - dereferencingMetadata: { - contentType: 'application/did+json', - }, - contentStream: did.verificationMethod?.find( - ({ id }) => id === did.authentication![0] - ), + dereferencingMetadata: { error: 'notFound' }, } } - return { - contentMetadata: {}, - dereferencingMetadata: { - error: 'notFound', - }, - } - }) + ) }) it('verifies did signature over string', async () => { @@ -90,7 +82,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -98,14 +90,13 @@ describe('light DID', () => { it('deserializes old did signature (with `keyId` property) to new format', async () => { const SIGNED_STRING = 'signed string' - const { signature, signerUrl } = signatureToJson({ - did: did.id, - ...(await sign({ + const { signature, signerUrl } = signatureToJson( + await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, verificationMethodRelationship: 'authentication', - })), - }) + }) + ) const oldSignature = { signature, keyId: signerUrl, @@ -128,7 +119,7 @@ describe('light DID', () => { verifyDidSignature({ message: SIGNED_BYTES, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, expectedVerificationMethodRelationship: 'authentication', }) ).resolves.not.toThrow() @@ -295,12 +286,14 @@ describe('light DID', () => { }, ], }).id + console.log('expectedSigner', expectedSigner) + console.log('signer', verificationMethod) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, expectedSigner, expectedVerificationMethodRelationship: 'authentication', }) @@ -326,10 +319,12 @@ describe('full DID', () => { }, ], } - sign = async ({ data }) => ({ + sign = async ({ data, did: signingDid }) => ({ signature: keypair.sign(data), verificationMethod: { id: '#0x12345', + controller: signingDid, + type: 'MultiKey', publicKeyMultibase: keypairToMultibaseKey(keypair), }, }) @@ -339,31 +334,26 @@ describe('full DID', () => { jest .mocked(dereference) .mockReset() - .mockImplementation(async (didUrl) => { - const { address, queryParameters } = parse(didUrl) - if ( - address === keypair.address && - (queryParameters === undefined || - queryParameters.requiredVerificationRelationship === - 'authentication') - ) { + .mockImplementation( + async (didUrl): Promise> => { + const { address } = parse(didUrl) + console.log('address', address) + console.log('didUrl', didUrl) + console.log(address === keypair.address) + console.log('did', did) + if (address === keypair.address) { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: did, + } + } return { contentMetadata: {}, - dereferencingMetadata: { - contentType: 'application/did+json', - }, - contentStream: did.verificationMethod?.find( - ({ id }) => id === did.authentication![0] - ), + dereferencingMetadata: { error: 'notFound' }, } } - return { - contentMetadata: {}, - dereferencingMetadata: { - error: 'notFound', - }, - } - }) + ) }) it('verifies did signature over string', async () => { @@ -390,6 +380,8 @@ describe('full DID', () => { did: did.id, verificationMethodRelationship: 'authentication', }) + console.log('verificationMethod', verificationMethod) + console.log('signerUrl', `${did.id}${verificationMethod.id}`) await expect( verifyDidSignature({ message: SIGNED_BYTES, @@ -464,6 +456,8 @@ describe('full DID', () => { }, ] as [NewLightDidVerificationKey], }).id + console.log(expectedSigner) + console.log(did) await expect( verifyDidSignature({ diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 0c7fc66454..4baf261b2f 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -113,10 +113,15 @@ export async function verifyDidSignature({ ) } + console.log('before') const { contentStream, contentMetadata } = await dereferenceDidUrl( signer.did, {} ) + console.log('signer.did', signer.did) + console.log('signerUrl', signerUrl) + console.log('contentStream', contentStream) + console.log('contentMetadata', contentMetadata) if (contentStream === undefined) { throw new SDKErrors.SignatureUnverifiableError( `Error validating the DID signature. Cannot fetch DID Document or the verification method for "${signerUrl}".` @@ -130,10 +135,12 @@ export async function verifyDidSignature({ throw new SDKErrors.DidDeactivatedError() } const didDocument = contentStream as DidDocument + console.log('didDocument', didDocument) const verificationMethod = didDocument.verificationMethod?.find( ({ controller, id }) => controller === didDocument.id && id === signer.fragment ) + console.log('verificationMethod', verificationMethod) if (verificationMethod === undefined) { throw new SDKErrors.DidNotFoundError('Verification method not found in DID') } @@ -178,20 +185,16 @@ export function isDidSignature( * * @param input Signature data returned from the [[SignCallback]]. * @param input.signature Signature bytes. - * @param input.did The DID URI of the signer. * @param input.verificationMethod The verification method used to generate the signature. * @returns A [[DidSignature]] object where signature is hex-encoded. */ export function signatureToJson({ - did, signature, verificationMethod, -}: SignResponseData & { - did: DidUri -}): DidSignature { +}: SignResponseData): DidSignature { return { signature: Crypto.u8aToHex(signature), - signerUrl: `${did}${verificationMethod.id}`, + signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, } } diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 2946a0b950..4e273b689e 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -799,7 +799,7 @@ describe('create presentation', () => { return { contentMetadata: {}, dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: unmigratedClaimerLightDid, + contentStream: migratedClaimerFullDid, } } if (did === attester.id) { @@ -905,7 +905,7 @@ describe('create presentation', () => { ).resolves.toMatchObject({ revoked: false, attester: attester.id }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) - it.only('should create presentation and exclude specific attributes using a migrated DID', async () => { + it('should create presentation and exclude specific attributes using a migrated DID', async () => { // cannot be used since the variable needs to be established in the outer scope credential = Credential.fromClaim( Claim.fromCTypeAndClaimContents( diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index fa9aef8e02..b05e3ea3f2 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -550,7 +550,9 @@ export async function createPresentation({ return { ...presentation, claimerSignature: { - ...signatureToJson({ did: credential.claim.owner, ...signature }), + ...signatureToJson({ + ...signature, + }), ...(challenge && { challenge }), }, } diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 69bdad24ad..4d7d11a9d3 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -42,7 +42,7 @@ export interface SignResponseData { /** * The did key uri used for signing. */ - verificationMethod: Pick + verificationMethod: VerificationMethod } /** diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index 98c30e4b90..532eed0d60 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -449,7 +449,12 @@ describe('issuance', () => { const didSigner: KiltAttestationProofV1.DidSigner = { did: attestedVc.issuer, signer: async () => ({ - verificationMethod: { id: '#test', publicKeyMultibase: 'zasd' }, + verificationMethod: { + controller: attestedVc.issuer, + type: 'MultiKey', + id: '#test', + publicKeyMultibase: 'zasd', + }, signature: new Uint8Array(32), }), } From 26565562b9e321736b1f1ec61479b9376238ce1b Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 12 Oct 2023 08:22:52 +0100 Subject: [PATCH 45/78] Return minimal DID document when DID is deactivated or migrated --- packages/did/src/DidResolver/DidResolver.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index f553720f2b..d4df575aa4 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -75,10 +75,13 @@ async function resolveInternal( .isSome if (isFullDidDeleted) { return { - // No canonicalId and no details are returned as we consider this DID deactivated/deleted. + // No canonicalId is returned as we consider this DID deactivated/deleted. documentMetadata: { deactivated: true, }, + document: { + id: did, + }, } } @@ -94,13 +97,12 @@ async function resolveInternal( canonicalId: getFullDidUri(did), }, document: { - id: did, + ...lightDocument, }, } } // If no full DID details nor deletion info is found, the light DID is un-migrated. - // Metadata will simply contain `deactivated: false`. return { document: lightDocument, documentMetadata: {}, @@ -133,7 +135,7 @@ export async function resolve( } const resolutionResult = await resolveInternal(did) - if (!resolutionResult) { + if (resolutionResult === null) { return { didResolutionMetadata: { error: 'notFound', From 0de4cd56babb563690fd739d7c9e80650fd67d42 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 12 Oct 2023 08:39:20 +0100 Subject: [PATCH 46/78] Add test for legacy signature support --- packages/did/src/Did.signature.spec.ts | 20 ++++++++++++++++++++ packages/did/src/Did.signature.ts | 7 ------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 2cf9a8e549..46ac589ec0 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -108,6 +108,26 @@ describe('light DID', () => { expect(deserialized).not.toHaveProperty('keyId') }) + it('deserializes old did signature (with `keyUri` property) to new format', async () => { + const SIGNED_STRING = 'signed string' + const { signature, signerUrl } = signatureToJson( + await sign({ + data: Crypto.coToUInt8(SIGNED_STRING), + did: did.id, + verificationMethodRelationship: 'authentication', + }) + ) + const oldSignature = { + signature, + keyUri: signerUrl, + } + + const deserialized = signatureFromJson(oldSignature) + expect(deserialized.signature).toBeInstanceOf(Uint8Array) + expect(deserialized.signerUrl).toStrictEqual(signerUrl) + expect(deserialized).not.toHaveProperty('keyUri') + }) + it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) const { signature, verificationMethod } = await sign({ diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 4baf261b2f..2565a295bc 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -113,15 +113,10 @@ export async function verifyDidSignature({ ) } - console.log('before') const { contentStream, contentMetadata } = await dereferenceDidUrl( signer.did, {} ) - console.log('signer.did', signer.did) - console.log('signerUrl', signerUrl) - console.log('contentStream', contentStream) - console.log('contentMetadata', contentMetadata) if (contentStream === undefined) { throw new SDKErrors.SignatureUnverifiableError( `Error validating the DID signature. Cannot fetch DID Document or the verification method for "${signerUrl}".` @@ -135,12 +130,10 @@ export async function verifyDidSignature({ throw new SDKErrors.DidDeactivatedError() } const didDocument = contentStream as DidDocument - console.log('didDocument', didDocument) const verificationMethod = didDocument.verificationMethod?.find( ({ controller, id }) => controller === didDocument.id && id === signer.fragment ) - console.log('verificationMethod', verificationMethod) if (verificationMethod === undefined) { throw new SDKErrors.DidNotFoundError('Verification method not found in DID') } From 491b446ed9624bf4bc8fba3394dcccbea6e55faf Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Thu, 12 Oct 2023 11:05:10 +0100 Subject: [PATCH 47/78] Sr25519Signature2020.spec.ts test failing --- packages/did/src/Did.signature.spec.ts | 10 -------- packages/did/src/DidResolver/DidResolver.ts | 4 +-- packages/vc-export/src/documentLoader.ts | 13 ++++++---- .../src/suites/Sr25519Signature2020.spec.ts | 25 ++++++++++--------- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 46ac589ec0..cc8730e01a 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -306,8 +306,6 @@ describe('light DID', () => { }, ], }).id - console.log('expectedSigner', expectedSigner) - console.log('signer', verificationMethod) await expect( verifyDidSignature({ @@ -357,10 +355,6 @@ describe('full DID', () => { .mockImplementation( async (didUrl): Promise> => { const { address } = parse(didUrl) - console.log('address', address) - console.log('didUrl', didUrl) - console.log(address === keypair.address) - console.log('did', did) if (address === keypair.address) { return { contentMetadata: {}, @@ -400,8 +394,6 @@ describe('full DID', () => { did: did.id, verificationMethodRelationship: 'authentication', }) - console.log('verificationMethod', verificationMethod) - console.log('signerUrl', `${did.id}${verificationMethod.id}`) await expect( verifyDidSignature({ message: SIGNED_BYTES, @@ -476,8 +468,6 @@ describe('full DID', () => { }, ] as [NewLightDidVerificationKey], }).id - console.log(expectedSigner) - console.log(did) await expect( verifyDidSignature({ diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index d4df575aa4..41a16139bb 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -219,8 +219,8 @@ type InternalDereferenceResult = contentStream: DereferenceContentStream } -function isFailedDereferenceMetadata( - input: InternalDereferenceResult +export function isFailedDereferenceMetadata( + input: unknown ): input is FailedDereferenceMetadata { return (input as FailedDereferenceMetadata)?.error !== undefined } diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index d507b7fce5..88542117f0 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -19,8 +19,9 @@ import { DID_CONTEXTS, KILT_DID_CONTEXT_URL, parse, - resolve as resolveDid, + dereference as dereferenceDid, W3C_DID_CONTEXT_URL, + isFailedDereferenceMetadata, } from '@kiltprotocol/did' import { CType } from '@kiltprotocol/core' @@ -81,13 +82,15 @@ export const kiltContextsLoader: DocumentLoader = async (url) => { export const kiltDidLoader: DocumentLoader = async (url) => { const { did } = parse(url as DidUri) - const { didDocument, didResolutionMetadata } = await resolveDid(did) - if (didResolutionMetadata.error) { - throw new Error(didResolutionMetadata.error) + const { dereferencingMetadata, contentStream } = await dereferenceDid(did, { + accept: 'application/did+ld+json', + }) + if (isFailedDereferenceMetadata(dereferencingMetadata)) { + throw new Error(dereferencingMetadata.error) } // Framing can help us resolve to the requested resource (did or did uri). This way we return either a key or the full DID document, depending on what was requested. const document = (await jsonld.frame( - didDocument ?? {}, + contentStream ?? {}, { // add did contexts to make sure we get a compacted representation '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], diff --git a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts index 35f208d0bd..2de5f64979 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts @@ -27,7 +27,7 @@ jest.mock('@digitalbazaar/http-client', () => ({})) jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - resolveCompliant: jest.fn(), + dereference: jest.fn(), })) const documentLoader = combineDocumentLoaders([ @@ -41,12 +41,12 @@ export async function makeFakeDid() { const keypair = Crypto.makeKeypairFromUri('//Ingo', 'sr25519') const didDocument: DidDocument = { id: ingosCredential.credentialSubject.id as DidUri, - authentication: ['#autentication'], + authentication: ['#authentication'], assertionMethod: ['#assertion'], verificationMethod: [ Did.didKeyToVerificationMethod( ingosCredential.credentialSubject.id as DidUri, - '#autentication', + '#authentication', { ...keypair, keyType: keypair.type } ), Did.didKeyToVerificationMethod( @@ -56,24 +56,25 @@ export async function makeFakeDid() { ), ], } - jest.mocked(Did.resolve).mockImplementation(async (did) => { + + jest.mocked(Did.dereference).mockImplementation(async (did) => { if (did.includes('light')) { return { - didDocument: Did.parseDocumentFromLightDid(did, false), - didDocumentMetadata: {}, - didResolutionMetadata: {}, + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: Did.parseDocumentFromLightDid(did, false), } } if (did.startsWith(didDocument.id)) { return { - didDocument, - didDocumentMetadata: {}, - didResolutionMetadata: {}, + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: didDocument, } } return { - didDocumentMetadata: {}, - didResolutionMetadata: { error: 'notFound' }, + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, } }) return { didDocument, keypair } From 04dcfed0a62304d7f15eb89fd1303c132161cc87 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 09:51:26 +0100 Subject: [PATCH 48/78] Key -> verification method renaming --- .../core/src/delegation/DelegationNode.ts | 11 +-- packages/did/src/Did.chain.ts | 22 +++--- packages/did/src/Did.signature.spec.ts | 6 +- packages/did/src/Did.signature.ts | 14 ++-- packages/did/src/Did.utils.ts | 25 +++---- packages/did/src/DidDetails/DidDetails.ts | 47 +++++++------ .../did/src/DidDetails/FullDidDetails.spec.ts | 30 ++++---- packages/did/src/DidDetails/FullDidDetails.ts | 12 ++-- .../did/src/DidDetails/LightDidDetails.ts | 26 +++---- packages/did/src/DidDetails/index.ts | 6 +- .../legacy-credentials/src/Credential.spec.ts | 4 +- packages/types/src/CryptoCallbacks.ts | 8 +-- packages/types/src/DidDocument.ts | 2 +- tests/bundle/bundle-test.ts | 69 +++++++++++-------- tests/testUtils/TestUtils.ts | 22 +++--- 15 files changed, 167 insertions(+), 137 deletions(-) diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index 9e31c4caaf..f977cbbdff 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -279,15 +279,18 @@ export class DelegationNode implements IDelegationNode { `DID verification method URL "${signerUrl}" couldn't be parsed` ) } - const key = delegateDid.verificationMethod?.find( + const verificationMethod = delegateDid.verificationMethod?.find( ({ id }) => id === fragment ) - if (!key) { + if (!verificationMethod) { throw new SDKErrors.DidError( - `Key with fragment "${fragment}" was not found on DID: "${delegateDid.id}"` + `Verification method with ID "${fragment}" was not found on DID: "${delegateDid.id}"` ) } - return Did.didSignatureToChain(key, delegateSignature.signature) + return Did.didSignatureToChain( + verificationMethod, + delegateSignature.signature + ) } /** diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index dce8b5e774..e79aa0f7d3 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -35,16 +35,16 @@ import { ConfigService } from '@kiltprotocol/config' import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' import type { - DidEncryptionKeyType, + DidEncryptionMethodType, NewService, - DidVerificationKeyType, + DidSigningMethodType, NewDidVerificationKey, NewDidEncryptionKey, } from './DidDetails/DidDetails.js' import { - isValidVerificationKeyType, - isValidEncryptionKeyType, + isValidVerificationMethodType, + isValidEncryptionMethodType, } from './DidDetails/DidDetails.js' import { multibaseKeyToDidKey, @@ -114,10 +114,10 @@ export type ChainDidBaseKey = { type: string } export type ChainDidVerificationKey = ChainDidBaseKey & { - type: DidVerificationKeyType + type: DidSigningMethodType } export type ChainDidEncryptionKey = ChainDidBaseKey & { - type: DidEncryptionKeyType + type: DidEncryptionMethodType } export type ChainDidKey = ChainDidVerificationKey | ChainDidEncryptionKey export type ChainDidService = { @@ -502,7 +502,7 @@ export async function getStoreTxFromDidDocument( const { keyType, publicKey } = multibaseKeyToDidKey( authVerificationMethod.publicKeyMultibase ) - if (!isValidVerificationKeyType(keyType)) { + if (!isValidVerificationMethodType(keyType)) { throw new SDKErrors.DidError( `Provided authentication key has an unsupported key type "${keyType}".` ) @@ -525,7 +525,7 @@ export async function getStoreTxFromDidDocument( ) } const { keyType, publicKey } = multibaseKeyToDidKey(vm.publicKeyMultibase) - if (!isValidEncryptionKeyType(keyType)) { + if (!isValidEncryptionMethodType(keyType)) { throw new SDKErrors.DidError( `The key agreement key with ID "${k}" has an unsupported key type ${keyType}.` ) @@ -553,7 +553,7 @@ export async function getStoreTxFromDidDocument( const { keyType, publicKey } = multibaseKeyToDidKey( assertionVerificationMethod.publicKeyMultibase ) - if (!isValidVerificationKeyType(keyType)) { + if (!isValidVerificationMethodType(keyType)) { throw new SDKErrors.DidError( `The assertion method key with ID "${assertionMethodId}" has an unsupported key type ${keyType}.` ) @@ -580,7 +580,7 @@ export async function getStoreTxFromDidDocument( const { keyType, publicKey } = multibaseKeyToDidKey( capabilityDelegationVerificationMethod.publicKeyMultibase ) - if (!isValidVerificationKeyType(keyType)) { + if (!isValidVerificationMethodType(keyType)) { throw new SDKErrors.DidError( `The capability delegation method key with ID "${capabilityDelegationId}" has an unsupported key type ${keyType}.` ) @@ -671,7 +671,7 @@ export function didSignatureToChain( signature: Uint8Array ): EncodedSignature { const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) - if (!isValidVerificationKeyType(keyType)) { + if (!isValidVerificationMethodType(keyType)) { throw new SDKErrors.DidError( `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` ) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index cc8730e01a..2f76ce60ae 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -162,7 +162,7 @@ describe('light DID', () => { ).rejects.toThrow() }) - it('fails if key id does not match', async () => { + it('fails if verification method id does not match', async () => { const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const let { signature, verificationMethod } = await sign({ @@ -202,7 +202,7 @@ describe('light DID', () => { ).rejects.toThrow() }) - it('fails if key id malformed', async () => { + it('fails if verification method id malformed', async () => { jest.mocked(dereference).mockRestore() const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const @@ -506,7 +506,7 @@ describe('type guard', () => { keypair = Crypto.makeKeypairFromSeed() }) - it('rejects malformed key uri', () => { + it('rejects malformed signer URL', () => { let signature: DidSignature = { // @ts-expect-error signerUrl: `did:kilt:${keypair.address}?mykey`, diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 2565a295bc..55b0b2d401 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -74,7 +74,7 @@ function verifyDidSignatureDataStructure( * @param input.message The message that was signed. * @param input.signature Signature bytes. * @param input.signerUrl DID URL of the verification method used for signing. - * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. + * @param input.expectedSigner If given, verification fails if the controller of the signing verification method is not the expectedSigner. * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. * @param input.dereferenceDidUrl Allows specifying a custom DID dereferenced. Defaults to the built-in [[dereference]]. @@ -88,7 +88,7 @@ export async function verifyDidSignature({ expectedVerificationMethodRelationship, dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: DidSignatureVerificationInput): Promise { - // checks if key uri points to the right did; alternatively we could check the key's controller + // checks if signer URL points to the right did; alternatively we could check the verification method's controller const signer = parse(signerUrl) if (expectedSigner && expectedSigner !== signer.did) { // check for allowable exceptions @@ -98,7 +98,7 @@ export async function verifyDidSignature({ expected.address === signer.address && expected.version === signer.version // EITHER: signer is a full did and we allow signatures by corresponding full did const allowedUpgrade = allowUpgraded && signer.type === 'full' - // OR: both are light dids and their auth key type matches + // OR: both are light dids and their auth verification method key type matches const keyTypeMatch = signer.type === 'light' && expected.type === 'light' && @@ -109,7 +109,7 @@ export async function verifyDidSignature({ } if (signer.fragment === undefined) { throw new SDKErrors.DidError( - `Key URI "${signer}" is not a valid DID resource.` + `Signer DID URL "${signerUrl}" is not a valid DID resource.` ) } @@ -137,7 +137,7 @@ export async function verifyDidSignature({ if (verificationMethod === undefined) { throw new SDKErrors.DidNotFoundError('Verification method not found in DID') } - // Check whether the provided key ID is within the keys for a given verification relationship, if provided. + // Check whether the provided verification method ID is included in the given verification relationship, if provided. if ( expectedVerificationMethodRelationship && !didDocument[expectedVerificationMethodRelationship]?.some( @@ -145,7 +145,7 @@ export async function verifyDidSignature({ ) ) { throw new SDKErrors.DidError( - `No key "${signer.fragment}" for the verification method "${expectedVerificationMethodRelationship}"` + `No verification method "${signer.fragment}" for the verification method "${expectedVerificationMethodRelationship}"` ) } @@ -156,7 +156,7 @@ export async function verifyDidSignature({ } /** - * Type guard assuring that the input is a valid DidSignature object, consisting of a signature as hex and the uri of the signing key. + * Type guard assuring that the input is a valid DidSignature object, consisting of a signature as hex and the DID URL of the signer's verification method. * Does not cryptographically verify the signature itself! * * @param input Arbitrary input. diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 1ed499bc5b..125142a6f5 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -18,7 +18,7 @@ import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' -import type { DidKeyType } from './DidDetails/DidDetails.js' +import type { DidVerificationMethodType } from './DidDetails/DidDetails.js' // The latest version for KILT light DIDs. const LIGHT_DID_LATEST_VERSION = 1 @@ -135,16 +135,17 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { type DecodedVerificationMethod = { publicKey: Uint8Array - keyType: DidKeyType + keyType: DidVerificationMethodType } -const multicodecPrefixes: Record = { - 0xe7: ['ecdsa', 33], - 0xec: ['x25519', 32], - 0xed: ['ed25519', 32], - 0xef: ['sr25519', 32], -} -const multicodecReversePrefixes: Record = { +const multicodecPrefixes: Record = + { + 0xe7: ['ecdsa', 33], + 0xec: ['x25519', 32], + 0xed: ['ed25519', 32], + 0xef: ['sr25519', 32], + } +const multicodecReversePrefixes: Record = { ecdsa: 0xe7, ed25519: 0xed, sr25519: 0xef, @@ -194,7 +195,7 @@ export function keypairToMultibaseKey({ type, publicKey, }: Pick & { - type: DidKeyType + type: DidVerificationMethodType }): VerificationMethod['publicKeyMultibase'] { const multiCodecPublicKeyPrefix = multicodecReversePrefixes[type] if (multiCodecPublicKeyPrefix === undefined) { @@ -215,7 +216,7 @@ export function keypairToMultibaseKey({ } /** - * Export a DID key to a `MultiKey` verification method. + * Convert a DID key to a `MultiKey` verification method. * * @param controller The verification method controller's DID URI. * @param id The verification method ID. @@ -336,7 +337,7 @@ export function getFullDidUri( } /** - * Builds the URI of a full DID if it is created with the authentication key provided. + * Builds the URI of a full DID if it is created with the authentication verification method derived from the provided public key. * * @param verificationMethod The DID verification method. * @returns The expected full DID URI. diff --git a/packages/did/src/DidDetails/DidDetails.ts b/packages/did/src/DidDetails/DidDetails.ts index c531b43ef5..b94ea94cb7 100644 --- a/packages/did/src/DidDetails/DidDetails.ts +++ b/packages/did/src/DidDetails/DidDetails.ts @@ -16,36 +16,43 @@ import type { import { didKeyToVerificationMethod } from '../Did.utils.js' /** - * Possible types for a DID verification key. + * Possible types for a DID verification method. */ -const verificationKeyTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const -export const verificationKeyTypes = verificationKeyTypesC as unknown as string[] -export type DidVerificationKeyType = typeof verificationKeyTypesC[number] +const signingMethodTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const +export const signingMethodTypes = signingMethodTypesC as unknown as string[] +export type DidSigningMethodType = typeof signingMethodTypesC[number] // `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 -export function isValidVerificationKeyType( +export function isValidVerificationMethodType( input: string -): input is DidVerificationKeyType { - return verificationKeyTypes.includes(input) +): input is DidSigningMethodType { + return signingMethodTypes.includes(input) } /** - * Possible types for a DID encryption key. + * Possible types for a DID encryption verification method. */ -const encryptionKeyTypesC = ['x25519'] as const -export const encryptionKeyTypes = encryptionKeyTypesC as unknown as string[] -export type DidEncryptionKeyType = typeof encryptionKeyTypesC[number] +const encryptionMethodTypesC = ['x25519'] as const +export const encryptionMethodTypes = + encryptionMethodTypesC as unknown as string[] +export type DidEncryptionMethodType = typeof encryptionMethodTypesC[number] -export function isValidEncryptionKeyType( +export function isValidEncryptionMethodType( input: string -): input is DidEncryptionKeyType { - return encryptionKeyTypes.includes(input) +): input is DidEncryptionMethodType { + return encryptionMethodTypes.includes(input) } -export type DidKeyType = DidVerificationKeyType | DidEncryptionKeyType +export type DidVerificationMethodType = + | DidSigningMethodType + | DidEncryptionMethodType -export function isValidDidKeyType(input: string): input is DidKeyType { - return isValidVerificationKeyType(input) || isValidEncryptionKeyType(input) +export function isValidDidVerificationType( + input: string +): input is DidSigningMethodType { + return ( + isValidVerificationMethodType(input) || isValidEncryptionMethodType(input) + ) } export type NewVerificationMethod = Omit @@ -77,14 +84,14 @@ export type BaseNewDidKey = { * Type of a new verification key to add under a DID. */ export type NewDidVerificationKey = BaseNewDidKey & { - type: DidVerificationKeyType + type: DidSigningMethodType } /** * Type of a new encryption key to add under a DID. */ export type NewDidEncryptionKey = BaseNewDidKey & { - type: DidEncryptionKeyType + type: DidEncryptionMethodType } function doesVerificationMethodExist( @@ -130,7 +137,7 @@ export function addKeypairAsVerificationMethod( relationship: VerificationRelationship ): void { const verificationMethod = didKeyToVerificationMethod(didDocument.id, id, { - keyType: keyType as DidKeyType, + keyType: keyType as DidSigningMethodType, publicKey, }) addVerificationMethod(didDocument, verificationMethod, relationship) diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 585a80ab83..7796eb0692 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -166,14 +166,14 @@ describe('When creating an instance from the chain', () => { const mockApi = ApiMocks.createAugmentedApi() describe('When creating an instance from the chain', () => { - it('Should return correct KeyRelationship for single valid call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( + it('Should return correct VerificationRelationship for single valid call', () => { + const verificationRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return correct KeyRelationship for batched call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( + it('Should return correct VerificationRelationship for batched call', () => { + const verificationRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.batch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -187,10 +187,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return correct KeyRelationship for batchAll call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( + it('Should return correct VerificationRelationship for batchAll call', () => { + const verificationRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.batchAll([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -204,10 +204,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return correct KeyRelationship for forceBatch call', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( + it('Should return correct VerificationRelationship for forceBatch call', () => { + const verificationRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -221,10 +221,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return undefined for batch with mixed KeyRelationship calls', () => { - const keyRelationship = getVerificationMethodRelationshipForTx( + it('Should return undefined for batch with mixed VerificationRelationship calls', () => { + const verificationRelationship = getVerificationMethodRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -234,6 +234,6 @@ describe('When creating an instance from the chain', () => { mockApi.tx.web3Names.claim('awesomename'), ]) ) - expect(keyRelationship).toBeUndefined() + expect(verificationRelationship).toBeUndefined() }) }) diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 824dece014..073ba502ba 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -51,13 +51,13 @@ function getVerificationMethodRelationshipForRuntimeCall( ): SignatureVerificationRelationship | undefined { const { section, method } = call - // get the VerificationKeyRelationship of a batched call + // get the VerificationRelationship of a batched call if ( section === 'utility' && ['batch', 'batchAll', 'forceBatch'].includes(method) && call.args[0].toRawType() === 'Vec' ) { - // map all calls to their VerificationKeyRelationship and deduplicate the items + // map all calls to their VerificationRelationship and deduplicate the items return (call.args[0] as unknown as Array) .map(getVerificationMethodRelationshipForRuntimeCall) .reduce((prev, value) => (prev === value ? prev : undefined)) @@ -141,7 +141,9 @@ export async function authorizeTx( const verificationMethodRelationship = getVerificationMethodRelationshipForTx(extrinsic) if (verificationMethodRelationship === undefined) { - throw new SDKErrors.SDKError('No key relationship found for extrinsic') + throw new SDKErrors.SDKError( + 'No verification relationship found for extrinsic' + ) } return generateDidAuthenticatedTx({ @@ -159,7 +161,7 @@ type GroupedExtrinsics = Array<{ verificationMethodRelationship: SignatureVerificationRelationship }> -function groupExtrinsicsByKeyRelationship( +function groupExtrinsicsByVerificationRelationship( extrinsics: Extrinsic[] ): GroupedExtrinsics { const [first, ...rest] = extrinsics.map((extrinsic) => { @@ -243,7 +245,7 @@ export async function authorizeBatch({ }) } - const groups = groupExtrinsicsByKeyRelationship(extrinsics) + const groups = groupExtrinsicsByVerificationRelationship(extrinsics) const firstNonce = nonce || (await getNextNonce(did)) const promises = groups.map(async (group, batchIndex) => { diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 049c504d94..64899d8027 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -18,7 +18,7 @@ import type { NewDidEncryptionKey, NewDidVerificationKey, NewService, - DidVerificationKeyType, + DidSigningMethodType, } from './DidDetails.js' import { @@ -30,14 +30,14 @@ import { import { fragmentIdToChain, validateNewService } from '../Did.chain.js' import { addKeypairAsVerificationMethod, - encryptionKeyTypes, + encryptionMethodTypes, } from './DidDetails.js' /** - * Currently, a light DID does not support the use of an ECDSA key as its authentication key. + * Currently, a light DID does not support the use of an ECDSA key as its authentication verification method. */ export type LightDidSupportedVerificationKeyType = Extract< - DidVerificationKeyType, + DidSigningMethodType, 'ed25519' | 'sr25519' > /** @@ -103,7 +103,7 @@ function validateCreateDocumentInput({ } if ( keyAgreement?.[0].type && - !encryptionKeyTypes.includes(keyAgreement[0].type) + !encryptionMethodTypes.includes(keyAgreement[0].type) ) { throw new SDKErrors.DidError( `Encryption key type "${keyAgreement[0].type}" is not supported` @@ -137,12 +137,12 @@ interface SerializableStructure { } /** - * Serialize the optional key agreement key and service endpoints of a light DID using the CBOR serialization algorithm + * Serialize the optional key agreement verification method and services of a light DID using the CBOR serialization algorithm * and encoding the result in Base58 format with a multibase prefix. * * @param details The light DID details to encode. - * @param details.keyAgreement The DID key agreement key. - * @param details.service The DID service endpoints. + * @param details.keyAgreement The DID key agreement verification method. + * @param details.service The DID services. * @returns The Base58-encoded and CBOR-serialized light DID optional details. */ function serializeAdditionalLightDidDetails({ @@ -203,14 +203,14 @@ function deserializeAdditionalLightDidDetails( } /** - * Create [[DidDocument]] of a light DID using the provided keys and endpoints. - * Sets proper key IDs, builds light DID URI. - * Private keys are assumed to already live in another storage, as it contains reference only to public keys. + * Create [[DidDocument]] of a light DID using the provided verification methods and services. + * Sets proper verification method IDs, builds light DID URI. + * Private keys are assumed to already live in another storage, as it contains reference only to public keys as verification methods. * * @param input The input. * @param input.authentication The array containing the public keys to be used as the light DID authentication verification method. * @param input.keyAgreement The optional array containing the public keys to be used as the light DID key agreement verification methods. - * @param input.service The optional light DID service endpoints. + * @param input.service The optional light DID services. * * @returns The resulting [[DidDocument]]. */ @@ -269,7 +269,7 @@ export function createLightDidDocument({ * For the DIDs you have received from external sources use [[resolve]] etc. * * Parsing is possible because of the self-describing and self-containing nature of light DIDs. - * Private keys are assumed to already live in another storage, as it contains reference only to public keys. + * Private keys are assumed to already live in another storage, as it contains reference only to public keys as verification methods. * * @param uri The DID URI to parse. * @param failIfFragmentPresent Whether to fail when parsing the URI in case a fragment is present or not, which is not relevant to the creation of the DID. It defaults to true. diff --git a/packages/did/src/DidDetails/index.ts b/packages/did/src/DidDetails/index.ts index 25cd6e329c..093402ef37 100644 --- a/packages/did/src/DidDetails/index.ts +++ b/packages/did/src/DidDetails/index.ts @@ -8,9 +8,9 @@ // We don't export the `add*VerificationMethod` functions, they are meant to be used internally export { BaseNewDidKey, - DidEncryptionKeyType, - DidKeyType, - DidVerificationKeyType, + DidEncryptionMethodType, + DidSigningMethodType, + DidVerificationMethodType, NewDidEncryptionKey, NewDidVerificationKey, NewService, diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 4e273b689e..0f9b2378c3 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -634,12 +634,12 @@ describe('Presentations', () => { [legitimation] ) - // sign presentation using Alice's authenication key + // sign presentation using Alice's authenication verification method const presentation = await Credential.createPresentation({ credential, signCallback: keyAlice.getSignCallback(identityAlice), }) - // but replace signer key reference with authentication key of light did + // but replace signer key reference with authentication verification method of light did presentation.claimerSignature.signerUrl = `${identityDave.id}${ identityDave.authentication![0] }` diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 4d7d11a9d3..246e9dcee2 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -21,7 +21,7 @@ export interface SignRequestData { data: Uint8Array /** - * The did key relationship to be used. + * The DID verification method relationship to be used. */ verificationMethodRelationship: SignatureVerificationRelationship @@ -40,7 +40,7 @@ export interface SignResponseData { */ signature: Uint8Array /** - * The did key uri used for signing. + * The DID verification method used for signing. */ verificationMethod: VerificationMethod } @@ -90,7 +90,7 @@ export interface EncryptResponseData { */ nonce: Uint8Array /** - * The did key uri used for the encryption. + * The DID verification method used for the encryption. */ verificationMethod: VerificationMethod } @@ -119,7 +119,7 @@ export interface DecryptRequestData { */ nonce: Uint8Array /** - * The did key uri, which should be used for decryption. + * The DID verification method, which should be used for decryption. */ verificationMethod: VerificationMethod } diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 586c17d415..5998f3701b 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -24,7 +24,7 @@ export type DidUri = export type UriFragment = `#${string}` /** - * URI for DID resources like keys or service endpoints. + * URL for DID resources like keys or service endpoints. */ export type DidUrl = `${DidUri}${UriFragment}` | `${DidUri}${UriFragment}` diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index bbb852e163..69ad3f509c 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,12 +7,12 @@ /// +import { keypairToMultibaseKey, NewDidEncryptionKey } from '@kiltprotocol/did' import type { DidDocument, KeyringPair, KiltEncryptionKeypair, KiltKeyringPair, - NewDidEncryptionKey, SignCallback, } from '@kiltprotocol/types' @@ -37,28 +37,32 @@ function makeSignCallback( keypair: KeyringPair ): (didDocument: DidDocument) => SignCallback { return (didDocument) => { - return async function sign({ data, keyRelationship }) { - const keyId = didDocument[keyRelationship]?.[0].id - const keyType = didDocument[keyRelationship]?.[0].type - if (keyId === undefined || keyType === undefined) { + return async function sign({ data, verificationMethodRelationship }) { + const authKeyId = didDocument[verificationMethodRelationship]?.[0] + const authKey = didDocument.verificationMethod?.find( + ({ id }) => id === authKeyId + ) + if (authKeyId === undefined || authKey === undefined) { throw new Error( - `Key for purpose "${keyRelationship}" not found in did "${didDocument.uri}"` + `No verification method for purpose "${verificationMethodRelationship}" found in DID "${didDocument.id}"` ) } const signature = keypair.sign(data, { withType: false }) - return { signature, keyUri: `${didDocument.uri}${keyId}`, keyType } + return { signature, verificationMethod: authKey } } } } -type StoreDidCallback = Parameters['2'] +type StoreDidCallback = Parameters['2'] function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { return async function sign({ data }) { const signature = keypair.sign(data, { withType: false }) return { signature, - keyType: keypair.type, + verificationMethod: { + publicKeyMultibase: keypairToMultibaseKey(keypair), + }, } } } @@ -101,7 +105,7 @@ async function createFullDidFromKeypair( const api = ConfigService.get('api') const sign = makeStoreDidCallback(keypair) - const storeTx = await Did.getStoreTx( + const storeTx = await Did.getStoreTxFromInput( { authentication: [keypair], assertionMethod: [keypair], @@ -115,7 +119,11 @@ async function createFullDidFromKeypair( const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( - Did.toChain(Did.getFullDidUriFromKey(keypair)) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(keypair), + }) + ) ) return Did.linkedInfoFromChain(encodedDidDetails).document } @@ -166,7 +174,7 @@ async function runAll() { keyAgreement: [{ publicKey: encPublicKey, type: 'x25519' }], }) if ( - testDid.uri !== + testDid.id !== `did:kilt:light:01${address}:z1Ac9CMtYCTRWjetJfJqJoV7FcPDD9nHPHDHry7t3KZmvYe1HQP1tgnBuoG3enuGaowpF8V88sCxytDPDy6ZxhW` ) { throw new Error('DID Test Unsuccessful') @@ -179,7 +187,7 @@ async function runAll() { 'ed25519' ) - const didStoreTx = await Did.getStoreTx( + const didStoreTx = await Did.getStoreTxFromInput( { authentication: [keypair] }, payer.address, storeDidCallback @@ -188,15 +196,19 @@ async function runAll() { const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( - Did.toChain(Did.getFullDidUriFromKey(keypair)) + Did.toChain( + Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(keypair), + }) + ) ) const fullDid = Did.linkedInfoFromChain(encodedDidDetails).document - const resolved = await Did.resolve(fullDid.uri) + const resolved = await Did.resolve(fullDid.id) if ( - resolved && - !resolved.metadata.deactivated && - resolved.document?.uri === fullDid.uri + resolved !== undefined && + !resolved.didDocumentMetadata.deactivated && + resolved.didDocument?.id === fullDid.id ) { console.info('DID matches') } else { @@ -204,15 +216,18 @@ async function runAll() { } const deleteTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, api.tx.did.delete(BalanceUtils.toFemtoKilt(0)), getSignCallback(fullDid), payer.address ) await Blockchain.signAndSubmitTx(deleteTx, payer) - const resolvedAgain = await Did.resolve(fullDid.uri) - if (!resolvedAgain || resolvedAgain.metadata.deactivated) { + const resolvedAgain = await Did.resolve(fullDid.id) + if ( + resolvedAgain === undefined || + resolvedAgain.didDocumentMetadata.deactivated + ) { console.info('DID successfully deleted') } else { throw new Error('DID was not deleted') @@ -230,7 +245,7 @@ async function runAll() { }) const cTypeStoreTx = await Did.authorizeTx( - alice.uri, + alice.id, api.tx.ctype.add(CType.toChain(DriversLicense)), aliceSign(alice), payer.address @@ -247,8 +262,8 @@ async function runAll() { const credential = KiltCredentialV1.fromInput({ cType: DriversLicense.$id, claims: content, - subject: bob.uri, - issuer: alice.uri, + subject: bob.id, + issuer: alice.id, }) await KiltCredentialV1.validateSubject(credential, { @@ -259,13 +274,13 @@ async function runAll() { if ( credential.credentialSubject.name !== content.name || credential.credentialSubject.age !== content.age || - credential.credentialSubject.id !== bob.uri + credential.credentialSubject.id !== bob.id ) { throw new Error('Claim content inside Credential mismatching') } const issued = await KiltAttestationProofV1.issue(credential, { - didSigner: { did: alice.uri, signer: aliceSign(alice) }, + didSigner: { did: alice.id, signer: aliceSign(alice) }, transactionHandler: { account: payer.address, signAndSubmit: async (tx) => { @@ -291,7 +306,7 @@ async function runAll() { await KiltRevocationStatusV1.check(issued) console.info('Credential status verified') - const presentation = Presentation.create([issued], bob.uri) + const presentation = Presentation.create([issued], bob.id) console.info('Presentation created') Presentation.validateStructure(presentation) diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 9b9fe5a540..75aa6da69f 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -22,7 +22,7 @@ import type { import type { BaseNewDidKey, ChainDidKey, - DidKeyType, + DidVerificationMethodType, LightDidSupportedVerificationKeyType, NewLightDidVerificationKey, NewService, @@ -130,7 +130,7 @@ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { const keyId = didDocument[verificationMethodRelationship]?.[0] if (keyId === undefined) { throw new Error( - `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` + `Verification method for relationship "${verificationMethodRelationship}" not found in DID "${didDocument.id}"` ) } const verificationMethod = didDocument.verificationMethod?.find( @@ -138,7 +138,7 @@ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { ) if (verificationMethod === undefined) { throw new Error( - `Key for purpose "${verificationMethodRelationship}" not found in did "${didDocument.id}"` + `Verification method for relationship "${verificationMethodRelationship}" not found in DID "${didDocument.id}"` ) } const signature = keypair.sign(data, { withType: false }) @@ -235,7 +235,7 @@ function addKeypairAsVerificationMethod( didDocument.id, id, { - keyType: keyType as DidKeyType, + keyType: keyType as DidVerificationMethodType, publicKey, } ) @@ -280,7 +280,7 @@ function makeDidKeyFromKeypair({ * * @param keypair The KeyringPair for authentication key, other keys derived from it. * @param generationOptions The additional options for generation. - * @param generationOptions.keyRelationships The set of key relationships to indicate which keys must be added to the DID. + * @param generationOptions.verificationRelationships The set of verification relationships to indicate which keys must be added to the DID. * @param generationOptions.endpoints The set of service endpoints that must be added to the DID. * * @returns A promise resolving to a [[DidDocument]] object. The resulting object is NOT stored on chain. @@ -288,14 +288,16 @@ function makeDidKeyFromKeypair({ export async function createLocalDemoFullDidFromKeypair( keypair: KiltKeyringPair, { - keyRelationships = new Set([ + verificationRelationships = new Set([ 'assertionMethod', 'capabilityDelegation', 'keyAgreement', ]), endpoints = [], }: { - keyRelationships?: Set> + verificationRelationships?: Set< + Omit + > endpoints?: NewService[] } = {} ): Promise { @@ -323,7 +325,7 @@ export async function createLocalDemoFullDidFromKeypair( service: endpoints, } - if (keyRelationships.has('keyAgreement')) { + if (verificationRelationships.has('keyAgreement')) { const { publicKey: encPublicKey, type } = makeEncryptionKeyTool( `${keypair.publicKey}//enc` ).keyAgreement[0] @@ -337,7 +339,7 @@ export async function createLocalDemoFullDidFromKeypair( 'keyAgreement' ) } - if (keyRelationships.has('assertionMethod')) { + if (verificationRelationships.has('assertionMethod')) { const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//att') as KiltKeyringPair ) @@ -351,7 +353,7 @@ export async function createLocalDemoFullDidFromKeypair( 'assertionMethod' ) } - if (keyRelationships.has('capabilityDelegation')) { + if (verificationRelationships.has('capabilityDelegation')) { const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//del') as KiltKeyringPair ) From 87ba0bec18b1bfe8c2be817992ee68a469a7f7a1 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 10:46:13 +0100 Subject: [PATCH 49/78] Key/service ID to URL --- packages/did/src/Did.chain.ts | 32 +++++++++---------- .../src/DidDetails/LightDidDetails.spec.ts | 4 +-- .../did/src/DidDetails/LightDidDetails.ts | 4 +-- .../did/src/DidResolver/_DidResolver.spec.ts | 12 +++---- packages/types/src/DidDocument.ts | 2 +- tests/integration/Did.spec.ts | 30 ++++++++--------- tests/testUtils/TestUtils.ts | 2 +- 7 files changed, 43 insertions(+), 43 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index e79aa0f7d3..a3693bcb63 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -258,17 +258,17 @@ function isUriFragment(str: string): boolean { } /** - * Performs sanity checks on service endpoint data, making sure that the following conditions are met: + * Performs sanity checks on service data, making sure that the following conditions are met: * - The `id` property is a string containing a valid URI fragment according to RFC#3986, not a complete DID URI. * - If the `uris` property contains one or more strings, they must be valid URIs according to RFC#3986. * - * @param endpoint A service endpoint object to check. + * @param endpoint A service object to check. */ export function validateNewService(endpoint: NewService): void { const { id, serviceEndpoint } = endpoint if ((id as string).startsWith('did:kilt')) { throw new SDKErrors.DidError( - `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` + `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URL, which is violated by id "${id}"` ) } if (!isUriFragment(fragmentIdToChain(id))) { @@ -346,13 +346,13 @@ export type GetStoreTxSignCallback = ( /** * Create a DID creation operation which includes the information provided. * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys as verification methods and service endpoints. + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys as verification methods and services. * - * A DID creation operation can contain at most 25 new service endpoints. - * Additionally, each service endpoint must respect the following conditions: - * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. - * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. - * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * A DID creation operation can contain at most 25 new services. + * Additionally, each service must respect the following conditions: + * - The service ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service has at most 1 service type, with a value that is at most 50 bytes long. + * - The service has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. * * @param input The DID keys and services to store. * @param submitter The KILT address authorized to submit the creation operation. @@ -406,7 +406,7 @@ export async function getStoreTxFromInput( api.consts.did.maxNumberOfServicesPerDid.toNumber() if (service.length > maxNumberOfServicesPerDid) { throw new SDKErrors.DidError( - `Cannot store more than ${maxNumberOfServicesPerDid} service endpoints per DID` + `Cannot store more than ${maxNumberOfServicesPerDid} services per DID` ) } @@ -456,13 +456,13 @@ export async function getStoreTxFromInput( * Only the first authentication, assertion, and capability delegation verification methods are considered from the input DID Document. * All the input DID Document key agreement verification methods are considered. * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and service endpoints. + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and services. * - * A DID creation operation can contain at most 25 new service endpoints. - * Additionally, each service endpoint must respect the following conditions: - * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. - * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. - * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * A DID creation operation can contain at most 25 new services. + * Additionally, each service must respect the following conditions: + * - The service ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service has at most 1 service type, with a value that is at most 50 bytes long. + * - The service has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. * * @param input The DID Document to store. * @param submitter The KILT address authorized to submit the creation operation. diff --git a/packages/did/src/DidDetails/LightDidDetails.spec.ts b/packages/did/src/DidDetails/LightDidDetails.spec.ts index db0cb858cb..3c50411dd2 100644 --- a/packages/did/src/DidDetails/LightDidDetails.spec.ts +++ b/packages/did/src/DidDetails/LightDidDetails.spec.ts @@ -29,7 +29,7 @@ import { */ describe('When creating an instance from the details', () => { - it('correctly assign the right sr25519 authentication key, x25519 encryption key, and service endpoints', () => { + it('correctly assign the right sr25519 authentication key, x25519 encryption key, and services', () => { const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') const encKey = Crypto.makeEncryptionKeypairFromSeed( new Uint8Array(32).fill(1) @@ -158,7 +158,7 @@ describe('When creating an instance from the details', () => { }) describe('When creating an instance from a URI', () => { - it('correctly assign the right authentication key, encryption key, and service endpoints', () => { + it('correctly assign the right authentication key, encryption key, and services', () => { const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') const encKey = Crypto.makeEncryptionKeypairFromSeed( new Uint8Array(32).fill(1) diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 64899d8027..53a4e32bf0 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -83,7 +83,7 @@ export type CreateDocumentInput = { */ keyAgreement?: [NewDidEncryptionKey] /** - * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. + * The set of services associated with this DID. Each service ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ service?: NewService[] @@ -132,7 +132,7 @@ interface SerializableStructure { [SERVICES_MAP_KEY]?: Array< Partial> & { id: string - } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their service endpoints. + } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their services. > } diff --git a/packages/did/src/DidResolver/_DidResolver.spec.ts b/packages/did/src/DidResolver/_DidResolver.spec.ts index 4a1c212804..88cdf9d417 100644 --- a/packages/did/src/DidResolver/_DidResolver.spec.ts +++ b/packages/did/src/DidResolver/_DidResolver.spec.ts @@ -189,7 +189,7 @@ describe('When resolving a key', () => { }) }) -describe('When resolving a service endpoint', () => { +describe('When resolving a service', () => { it('correctly resolves it for a full DID if both the DID and the endpoint exist', async () => { const fullDid = didWithServiceEndpoints const serviceIdUri: DidResourceUri = `${fullDid}#service-1` @@ -324,8 +324,8 @@ describe('When resolving a full DID', () => { ]) }) - it('correctly resolves the document with service endpoints', async () => { - // Mock transform function changed to return two service endpoints. + it('correctly resolves the document with services', async () => { + // Mock transform function changed to return two services. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() @@ -366,7 +366,7 @@ describe('When resolving a full DID', () => { }) it('correctly resolves the document with web3Name', async () => { - // Mock transform function changed to return two service endpoints. + // Mock transform function changed to return two services. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() @@ -474,7 +474,7 @@ describe('When resolving a light DID', () => { ]) }) - it('correctly resolves the document with authentication key, encryption key, and two service endpoints', async () => { + it('correctly resolves the document with authentication key, encryption key, and two services', async () => { const lightDid = Did.createLightDidDocument({ authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], keyAgreement: [{ publicKey: encryptionKey.publicKey, type: 'x25519' }], @@ -590,7 +590,7 @@ describe('When resolving with the spec compliant resolver', () => { identifier, }) }) - // Mock transform function changed to return two service endpoints. + // Mock transform function changed to return two services. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 5998f3701b..437f723396 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -24,7 +24,7 @@ export type DidUri = export type UriFragment = `#${string}` /** - * URL for DID resources like keys or service endpoints. + * URL for DID resources like keys or services. */ export type DidUrl = `${DidUri}${UriFragment}` | `${DidUri}${UriFragment}` diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index f5cb939564..38e5672a94 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -172,7 +172,7 @@ describe('write and didDeleteTx', () => { name: 'BadDidOrigin', }) - // We use 1 here and this should fail as there are two service endpoints stored. + // We use 1 here and this should fail as there are two services stored. call = api.tx.did.delete(new BN(1)) submittable = await Did.authorizeTx( @@ -263,7 +263,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { ) fullDid = Did.linkedInfoFromChain(fullDidLinkedInfo).document - // Add a new service endpoint + // Add a new service const newEndpoint: Did.NewService = { id: '#new-endpoint', type: ['new-type'], @@ -289,7 +289,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { linkedInfo.document.service?.find((s) => s.id === newEndpoint.id) ).toStrictEqual(newEndpoint) - // Delete the added service endpoint + // Delete the added service const removeEndpointCall = api.tx.did.removeServiceEndpoint( Did.fragmentIdToChain(newEndpoint.id) ) @@ -429,7 +429,7 @@ describe('DID migration', () => { expect(didDocumentMetadata.deactivated).toBe(undefined) }) - it('migrates light DID with ed25519 auth key, encryption key, and service endpoints', async () => { + it('migrates light DID with ed25519 auth key, encryption key, and services', async () => { const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') const { keyAgreement } = makeEncryptionKeyTool( '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' @@ -1001,7 +1001,7 @@ describe('DID management batching', () => { paymentAccount.address, storeDidCallback ) - // Create the full DID with a service endpoint + // Create the full DID with a service await submitTx(tx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( @@ -1014,7 +1014,7 @@ describe('DID management batching', () => { expect(fullDid.assertionMethod).toBeUndefined() - // Try to set a new attestation key and a duplicate service endpoint + // Try to set a new attestation key and a duplicate service const updateTx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, did: fullDid.id, @@ -1098,7 +1098,7 @@ describe('DID management batching', () => { expect(fullDid.assertionMethod).toBeUndefined() - // Use batchAll to set a new attestation key and a duplicate service endpoint + // Use batchAll to set a new attestation key and a duplicate service const updateTx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, did: fullDid.id, @@ -1130,7 +1130,7 @@ describe('DID management batching', () => { ) // .setAttestationKey() extrinsic went through but it was then reverted expect(updatedFullDid.assertionMethod).toBeUndefined() - // The service endpoint will match the one manually added, and not the one set in the builder. + // The service will match the one manually added, and not the one set in the builder. expect( updatedFullDid.service?.find((s) => s.id === '#id-1') ).toStrictEqual({ @@ -1350,7 +1350,7 @@ describe('Runtime constraints', () => { ) }, 30_000) - it('should not be possible to create a DID with too many service endpoints', async () => { + it('should not be possible to create a DID with too many services', async () => { // Maximum is 25 const newServiceEndpoints = Array(25).map( (_, index): Did.NewService => ({ @@ -1384,35 +1384,35 @@ describe('Runtime constraints', () => { storeDidCallback ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot store more than 25 service endpoints per DID"` + `"Cannot store more than 25 services per DID"` ) }, 30_000) - it('should not be possible to create a DID with a service endpoint that is too long', async () => { + it('should not be possible to create a DID with a service that is too long', async () => { const serviceId = '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const limit = api.consts.did.maxServiceIdLength.toNumber() expect(serviceId.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has too many types', async () => { + it('should not be possible to create a DID with a service that has too many types', async () => { const types = ['type-1', 'type-2'] const limit = api.consts.did.maxNumberOfTypesPerService.toNumber() expect(types.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has too many URIs', async () => { + it('should not be possible to create a DID with a service that has too many URIs', async () => { const uris = ['x:url-1', 'x:url-2', 'x:url-3'] const limit = api.consts.did.maxNumberOfUrlsPerService.toNumber() expect(uris.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has a type that is too long', async () => { + it('should not be possible to create a DID with a service that has a type that is too long', async () => { const type = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const limit = api.consts.did.maxServiceTypeLength.toNumber() expect(type.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has a URI that is too long', async () => { + it('should not be possible to create a DID with a service that has a URI that is too long', async () => { const uri = 'a:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const limit = api.consts.did.maxServiceUrlLength.toNumber() diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 75aa6da69f..65ec835d15 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -281,7 +281,7 @@ function makeDidKeyFromKeypair({ * @param keypair The KeyringPair for authentication key, other keys derived from it. * @param generationOptions The additional options for generation. * @param generationOptions.verificationRelationships The set of verification relationships to indicate which keys must be added to the DID. - * @param generationOptions.endpoints The set of service endpoints that must be added to the DID. + * @param generationOptions.endpoints The set of services that must be added to the DID. * * @returns A promise resolving to a [[DidDocument]] object. The resulting object is NOT stored on chain. */ From a279263138055287c529da09e1f88f268c868163 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 11:20:07 +0100 Subject: [PATCH 50/78] More constants --- packages/core/src/attestation/Attestation.ts | 2 +- .../credentialsV1/KiltAttestationProofV1.ts | 4 +- .../credentialsV1/KiltRevocationStatusV1.ts | 2 +- .../core/src/delegation/DelegationNode.ts | 4 +- packages/did/src/Did.chain.ts | 10 +-- packages/did/src/Did.signature.spec.ts | 62 +++++++++---------- packages/did/src/Did.signature.ts | 18 +++--- packages/did/src/Did.utils.ts | 30 +++++---- .../did/src/DidDetails/FullDidDetails.spec.ts | 12 ++-- packages/did/src/DidDetails/FullDidDetails.ts | 39 ++++++------ .../did/src/DidDetails/LightDidDetails.ts | 6 +- .../did/src/DidLinks/AccountLinks.chain.ts | 3 +- packages/did/src/DidResolver/DidResolver.ts | 6 +- packages/legacy-credentials/src/Claim.ts | 2 +- packages/legacy-credentials/src/Credential.ts | 4 +- packages/legacy-credentials/src/vcInterop.ts | 2 +- packages/types/src/CryptoCallbacks.ts | 4 +- .../src/suites/KiltAttestationProofV1.ts | 4 +- tests/bundle/bundle-test.ts | 14 ++--- tests/testUtils/TestUtils.ts | 8 +-- 20 files changed, 121 insertions(+), 115 deletions(-) diff --git a/packages/core/src/attestation/Attestation.ts b/packages/core/src/attestation/Attestation.ts index ec46fb2452..906782d91b 100644 --- a/packages/core/src/attestation/Attestation.ts +++ b/packages/core/src/attestation/Attestation.ts @@ -49,7 +49,7 @@ export function verifyDataStructure(input: IAttestation): void { if (!input.owner) { throw new SDKErrors.OwnerMissingError() } - Did.validateUri(input.owner, 'Uri') + Did.validateIdentifier(input.owner, 'Uri') if (typeof input.revoked !== 'boolean') { throw new SDKErrors.RevokedTypeError() diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index c6ecebe2cd..2117209b67 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -32,7 +32,7 @@ import type { IEventData, Signer } from '@polkadot/types/types' import { authorizeTx, getFullDidUri, - validateUri, + validateIdentifier, fromChain as didFromChain, } from '@kiltprotocol/did' import { JsonSchema, SDKErrors, Caip19 } from '@kiltprotocol/utils' @@ -347,7 +347,7 @@ export async function verify( validateCredentialStructure(credential) const { nonTransferable, credentialStatus, credentialSubject, issuer } = credential - validateUri(issuer, 'Uri') + validateIdentifier(issuer, 'Uri') await validateSubject(credential, opts) // 4. check nonTransferable if (nonTransferable !== true) diff --git a/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts b/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts index f2d8525a79..ce303a74a9 100644 --- a/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts +++ b/packages/core/src/credentialsV1/KiltRevocationStatusV1.ts @@ -49,7 +49,7 @@ export async function check( `Cannot handle revocation status checks for asset type ${assetNamespace}:${assetReference}` ) } - if (assetInstance === undefined) { + if (!assetInstance) { throw new SDKErrors.CredentialMalformedError( "The attestation record's CAIP-19 identifier must contain an asset index ('token_id') decoding to the credential root hash" ) diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index f977cbbdff..bf801196cc 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -269,7 +269,7 @@ export class DelegationNode implements IDelegationNode { const delegateSignature = await sign({ data: this.generateHash(), did: delegateDid.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) const signerUrl = `${delegateDid.id}${delegateSignature.verificationMethod.id}` as DidUrl @@ -284,7 +284,7 @@ export class DelegationNode implements IDelegationNode { ) if (!verificationMethod) { throw new SDKErrors.DidError( - `Verification method with ID "${fragment}" was not found on DID: "${delegateDid.id}"` + `Verification method "${signerUrl}" was not found on DID: "${delegateDid.id}"` ) } return Did.didSignatureToChain( diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index a3693bcb63..1a4b054507 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -443,7 +443,7 @@ export async function getStoreTxFromInput( const { signature } = await sign({ data: encoded, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) const encodedSignature = { [authenticationKey.type]: signature, @@ -606,7 +606,7 @@ export async function getStoreTxFromDidDocument( export interface SigningOptions { sign: SignExtrinsicCallback - verificationMethodRelationship: SignatureVerificationRelationship + verificationRelationship: SignatureVerificationRelationship } /** @@ -615,7 +615,7 @@ export interface SigningOptions { * * @param params Object wrapping all input to the function. * @param params.did Full DID. - * @param params.verificationMethodRelationship DID verification relationship to be used for authorization. + * @param params.verificationRelationship DID verification relationship to be used for authorization. * @param params.sign The callback to interface with the key store managing the private key to be used. * @param params.call The call or extrinsic to be authorized. * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. @@ -625,7 +625,7 @@ export interface SigningOptions { */ export async function generateDidAuthenticatedTx({ did, - verificationMethodRelationship, + verificationRelationship, sign, call, txCounter, @@ -646,7 +646,7 @@ export async function generateDidAuthenticatedTx({ ) const { signature, verificationMethod } = await sign({ data: signableCall.toU8a(), - verificationMethodRelationship, + verificationRelationship, did, }) const { keyType } = multibaseKeyToDidKey( diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 2f76ce60ae..cb63940b13 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -76,14 +76,14 @@ describe('light DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -94,7 +94,7 @@ describe('light DID', () => { await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) ) const oldSignature = { @@ -114,7 +114,7 @@ describe('light DID', () => { await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) ) const oldSignature = { @@ -133,14 +133,14 @@ describe('light DID', () => { const { signature, verificationMethod } = await sign({ data: SIGNED_BYTES, did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -150,14 +150,14 @@ describe('light DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'assertionMethod', + expectedVerificationRelationship: 'assertionMethod', }) ).rejects.toThrow() }) @@ -168,7 +168,7 @@ describe('light DID', () => { let { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) const wrongVerificationMethodId = `${verificationMethod.id}1a` jest.mocked(dereference).mockResolvedValue({ @@ -180,7 +180,7 @@ describe('light DID', () => { message: SIGNED_STRING, signature, signerUrl: `${did.id}${wrongVerificationMethodId}` as DidUrl, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) @@ -190,14 +190,14 @@ describe('light DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING.substring(1), signature, signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) @@ -209,7 +209,7 @@ describe('light DID', () => { let { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) const malformedVerificationId = verificationMethod.id.replace('#', '?') await expect( @@ -217,7 +217,7 @@ describe('light DID', () => { message: SIGNED_STRING, signature, signerUrl: `${did.id}${malformedVerificationId}` as DidUrl, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) @@ -233,14 +233,14 @@ describe('light DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) @@ -258,7 +258,7 @@ describe('light DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) const expectedSigner = createLightDidDocument({ @@ -271,7 +271,7 @@ describe('light DID', () => { signature, signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) @@ -281,7 +281,7 @@ describe('light DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) const authKey = did.verificationMethod?.find( @@ -313,7 +313,7 @@ describe('light DID', () => { signature, signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -375,14 +375,14 @@ describe('full DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -392,14 +392,14 @@ describe('full DID', () => { const { signature, verificationMethod } = await sign({ data: SIGNED_BYTES, did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -413,14 +413,14 @@ describe('full DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) @@ -434,14 +434,14 @@ describe('full DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) @@ -451,7 +451,7 @@ describe('full DID', () => { const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) const authKey = did.verificationMethod?.find( @@ -475,7 +475,7 @@ describe('full DID', () => { signature, signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) @@ -486,7 +486,7 @@ describe('full DID', () => { signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, allowUpgraded: true, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 55b0b2d401..880e4f24bc 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -17,7 +17,7 @@ import type { import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { isHex } from '@polkadot/util' -import { multibaseKeyToDidKey, parse, validateUri } from './Did.utils.js' +import { multibaseKeyToDidKey, parse, validateIdentifier } from './Did.utils.js' import { dereference } from './DidResolver/DidResolver.js' export type DidSignatureVerificationInput = { @@ -26,7 +26,7 @@ export type DidSignatureVerificationInput = { signerUrl: DidUrl expectedSigner?: DidUri allowUpgraded?: boolean - expectedVerificationMethodRelationship?: SignatureVerificationRelationship + expectedVerificationRelationship?: SignatureVerificationRelationship dereferenceDidUrl?: DereferenceDidUrl['dereference'] } @@ -49,7 +49,7 @@ type OldDidSignatureV2 = { function verifyDidSignatureDataStructure( input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 ): void { - const verificationMethodUri = (() => { + const verificationMethodUrl = (() => { if ('keyUri' in input) { return input.keyUri } @@ -63,7 +63,7 @@ function verifyDidSignatureDataStructure( `Expected signature as a hex string, got ${input.signature}` ) } - validateUri(verificationMethodUri, 'Url') + validateIdentifier(verificationMethodUrl, 'Url') } /** @@ -76,7 +76,7 @@ function verifyDidSignatureDataStructure( * @param input.signerUrl DID URL of the verification method used for signing. * @param input.expectedSigner If given, verification fails if the controller of the signing verification method is not the expectedSigner. * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. - * @param input.expectedVerificationMethodRelationship Which relationship to the signer DID the verification method must have. + * @param input.expectedVerificationRelationship Which relationship to the signer DID the verification method must have. * @param input.dereferenceDidUrl Allows specifying a custom DID dereferenced. Defaults to the built-in [[dereference]]. */ export async function verifyDidSignature({ @@ -85,7 +85,7 @@ export async function verifyDidSignature({ signerUrl, expectedSigner, allowUpgraded = false, - expectedVerificationMethodRelationship, + expectedVerificationRelationship, dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: DidSignatureVerificationInput): Promise { // checks if signer URL points to the right did; alternatively we could check the verification method's controller @@ -139,13 +139,13 @@ export async function verifyDidSignature({ } // Check whether the provided verification method ID is included in the given verification relationship, if provided. if ( - expectedVerificationMethodRelationship && - !didDocument[expectedVerificationMethodRelationship]?.some( + expectedVerificationRelationship && + !didDocument[expectedVerificationRelationship]?.some( (id) => id === verificationMethod.id ) ) { throw new SDKErrors.DidError( - `No verification method "${signer.fragment}" for the verification method "${expectedVerificationMethodRelationship}"` + `No verification method "${signer.fragment}" for the verification method "${expectedVerificationRelationship}"` ) } diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 125142a6f5..3865b06b67 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -71,7 +71,7 @@ function exportQueryParamsFromUri(didUri: DidUrl): Record { * @returns Object containing information extracted from the DID uri. */ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { - // Then we check if it conforms to either a full or a light DID URI. + // Then we check if it conforms to either a full or a light DID URL. let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups if (matches) { const { version: versionString, fragment } = matches @@ -138,18 +138,23 @@ type DecodedVerificationMethod = { keyType: DidVerificationMethodType } +const MULTICODEC_ECDSA_PREFIX = 0xe7 +const MULTICODEC_X25519_PREFIX = 0xec +const MULTICODEC_ED25519_PREFIX = 0xed +const MULTICODEC_SR25519_PREFIX = 0xef + const multicodecPrefixes: Record = { - 0xe7: ['ecdsa', 33], - 0xec: ['x25519', 32], - 0xed: ['ed25519', 32], - 0xef: ['sr25519', 32], + [MULTICODEC_ECDSA_PREFIX]: ['ecdsa', 33], + [MULTICODEC_X25519_PREFIX]: ['x25519', 32], + [MULTICODEC_ED25519_PREFIX]: ['ed25519', 32], + [MULTICODEC_SR25519_PREFIX]: ['sr25519', 32], } const multicodecReversePrefixes: Record = { - ecdsa: 0xe7, - ed25519: 0xed, - sr25519: 0xef, - x25519: 0xec, + ecdsa: MULTICODEC_ECDSA_PREFIX, + x25519: MULTICODEC_X25519_PREFIX, + ed25519: MULTICODEC_ED25519_PREFIX, + sr25519: MULTICODEC_SR25519_PREFIX, } /** @@ -271,7 +276,10 @@ export function isSameSubject(didA: DidUri, didB: DidUri): boolean { * @param input Arbitrary input. * @param expectType `ResourceUri` if the URI is expected to have a fragment (following '#'), `Did` if it is expected not to have one. Default allows both. */ -export function validateUri(input: unknown, expectType?: 'Uri' | 'Url'): void { +export function validateIdentifier( + input: unknown, + expectType?: 'Uri' | 'Url' +): void { if (typeof input !== 'string') { throw new TypeError(`DID string expected, got ${typeof input}`) } @@ -288,7 +296,7 @@ export function validateUri(input: unknown, expectType?: 'Uri' | 'Url'): void { ) } - if (fragment === undefined && expectType === 'Url') { + if (!fragment && expectType === 'Url') { throw new SDKErrors.DidError( 'Expected a Kilt DidUrl (containing a #fragment) but got a DidUri' ) diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 7796eb0692..f108e36887 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -24,7 +24,7 @@ import { import { generateDidAuthenticatedTx } from '../Did.chain.js' import { authorizeBatch, - getVerificationMethodRelationshipForTx, + getVerificationRelationshipForTx, } from './FullDidDetails.js' const augmentedApi = ApiMocks.createAugmentedApi() @@ -167,13 +167,13 @@ const mockApi = ApiMocks.createAugmentedApi() describe('When creating an instance from the chain', () => { it('Should return correct VerificationRelationship for single valid call', () => { - const verificationRelationship = getVerificationMethodRelationshipForTx( + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) ) expect(verificationRelationship).toBe('assertionMethod') }) it('Should return correct VerificationRelationship for batched call', () => { - const verificationRelationship = getVerificationMethodRelationshipForTx( + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.batch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -190,7 +190,7 @@ describe('When creating an instance from the chain', () => { expect(verificationRelationship).toBe('assertionMethod') }) it('Should return correct VerificationRelationship for batchAll call', () => { - const verificationRelationship = getVerificationMethodRelationshipForTx( + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.batchAll([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -207,7 +207,7 @@ describe('When creating an instance from the chain', () => { expect(verificationRelationship).toBe('assertionMethod') }) it('Should return correct VerificationRelationship for forceBatch call', () => { - const verificationRelationship = getVerificationMethodRelationshipForTx( + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -224,7 +224,7 @@ describe('When creating an instance from the chain', () => { expect(verificationRelationship).toBe('assertionMethod') }) it('Should return undefined for batch with mixed VerificationRelationship calls', () => { - const verificationRelationship = getVerificationMethodRelationshipForTx( + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 073ba502ba..d9ed59e84e 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -46,7 +46,7 @@ const methodMapping: Record< web3Names: 'authentication', } -function getVerificationMethodRelationshipForRuntimeCall( +function getVerificationRelationshipForRuntimeCall( call: Extrinsic['method'] ): SignatureVerificationRelationship | undefined { const { section, method } = call @@ -59,7 +59,7 @@ function getVerificationMethodRelationshipForRuntimeCall( ) { // map all calls to their VerificationRelationship and deduplicate the items return (call.args[0] as unknown as Array) - .map(getVerificationMethodRelationshipForRuntimeCall) + .map(getVerificationRelationshipForRuntimeCall) .reduce((prev, value) => (prev === value ? prev : undefined)) } @@ -72,15 +72,15 @@ function getVerificationMethodRelationshipForRuntimeCall( } /** - * Detect the verification relationship for a verification method which should be used to DID-authorize the provided extrinsic. + * Detect the relationship for a verification method which should be used to DID-authorize the provided extrinsic. * * @param extrinsic The unsigned extrinsic to inspect. * @returns The verification relationship. */ -export function getVerificationMethodRelationshipForTx( +export function getVerificationRelationshipForTx( extrinsic: Extrinsic ): SignatureVerificationRelationship | undefined { - return getVerificationMethodRelationshipForRuntimeCall(extrinsic.method) + return getVerificationRelationshipForRuntimeCall(extrinsic.method) } // Max nonce value is (2^64) - 1 @@ -138,9 +138,8 @@ export async function authorizeTx( ) } - const verificationMethodRelationship = - getVerificationMethodRelationshipForTx(extrinsic) - if (verificationMethodRelationship === undefined) { + const verificationRelationship = getVerificationRelationshipForTx(extrinsic) + if (verificationRelationship === undefined) { throw new SDKErrors.SDKError( 'No verification relationship found for extrinsic' ) @@ -148,7 +147,7 @@ export async function authorizeTx( return generateDidAuthenticatedTx({ did, - verificationMethodRelationship, + verificationRelationship, sign, call: extrinsic, txCounter: txCounter || (await getNextNonce(did)), @@ -158,41 +157,39 @@ export async function authorizeTx( type GroupedExtrinsics = Array<{ extrinsics: Extrinsic[] - verificationMethodRelationship: SignatureVerificationRelationship + verificationRelationship: SignatureVerificationRelationship }> function groupExtrinsicsByVerificationRelationship( extrinsics: Extrinsic[] ): GroupedExtrinsics { const [first, ...rest] = extrinsics.map((extrinsic) => { - const verificationMethodRelationship = - getVerificationMethodRelationshipForTx(extrinsic) - if (verificationMethodRelationship === undefined) { + const verificationRelationship = getVerificationRelationshipForTx(extrinsic) + if (verificationRelationship === undefined) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) } - return { extrinsic, verificationMethodRelationship } + return { extrinsic, verificationRelationship } }) const groups: GroupedExtrinsics = [ { extrinsics: [first.extrinsic], - verificationMethodRelationship: first.verificationMethodRelationship, + verificationRelationship: first.verificationRelationship, }, ] - rest.forEach(({ extrinsic, verificationMethodRelationship }) => { + rest.forEach(({ extrinsic, verificationRelationship }) => { const currentGroup = groups[groups.length - 1] const useCurrentGroup = - verificationMethodRelationship === - currentGroup.verificationMethodRelationship + verificationRelationship === currentGroup.verificationRelationship if (useCurrentGroup) { currentGroup.extrinsics.push(extrinsic) } else { groups.push({ extrinsics: [extrinsic], - verificationMethodRelationship, + verificationRelationship, }) } }) @@ -253,11 +250,11 @@ export async function authorizeBatch({ const call = list.length === 1 ? list[0] : batchFunction(list) const txCounter = increaseNonce(firstNonce, batchIndex) - const { verificationMethodRelationship } = group + const { verificationRelationship } = group return generateDidAuthenticatedTx({ did, - verificationMethodRelationship, + verificationRelationship, sign, call, txCounter, diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 53a4e32bf0..20c976f11e 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -76,7 +76,7 @@ export type CreateDocumentInput = { * The key to be used as the DID authentication verification method. This is mandatory and will be used as the first authentication verification method * of the full DID upon migration. */ - authentication: [NewDidVerificationKey] + authentication: [NewLightDidVerificationKey] /** * The optional encryption key to be used as the DID key agreement verification method. If present, it will be used as the first key agreement verification method * of the full DID upon migration. @@ -98,7 +98,7 @@ function validateCreateDocumentInput({ const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[authentication[0].type] - if (authenticationKeyTypeEncoding === undefined) { + if (!authenticationKeyTypeEncoding) { throw new SDKErrors.UnsupportedKeyError(authentication[0].type) } if ( @@ -294,7 +294,7 @@ export function parseDocumentFromLightDid( `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` ) } - if (fragment !== undefined && failIfFragmentPresent) { + if (fragment && failIfFragmentPresent) { throw new SDKErrors.DidError( `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` ) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index e25f4de136..38aefe164f 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -27,7 +27,8 @@ import { import { SDKErrors } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' -import { toChain, EncodedSignature } from '../Did.chain.js' +import type { EncodedSignature } from '../Did.chain.js' +import { toChain } from '../Did.chain.js' /** * A chain-agnostic address, which can be encoded using any network prefix. diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 41a16139bb..55f0ab5d25 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -28,7 +28,7 @@ import { cbor } from '@kiltprotocol/utils' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' import { linkedInfoFromChain } from '../Did.rpc.js' import { toChain } from '../Did.chain.js' -import { getFullDidUri, parse, validateUri } from '../Did.utils.js' +import { getFullDidUri, parse, validateIdentifier } from '../Did.utils.js' import { parseDocumentFromLightDid } from '../DidDetails/LightDidDetails.js' import { isValidVerificationRelationship } from '../DidDetails/DidDetails.js' @@ -124,7 +124,7 @@ export async function resolve( resolutionOptions: ResolutionOptions = {} ): Promise { try { - validateUri(did, 'Uri') + validateIdentifier(did, 'Uri') } catch (error) { return { didResolutionMetadata: { @@ -329,7 +329,7 @@ export async function dereference( const contentType = isValidContentType(accept) ? accept : DID_JSON try { - validateUri(didUrl) + validateIdentifier(didUrl) } catch (error) { return { dereferencingMetadata: { diff --git a/packages/legacy-credentials/src/Claim.ts b/packages/legacy-credentials/src/Claim.ts index a39ce8dbed..c9273b5b94 100644 --- a/packages/legacy-credentials/src/Claim.ts +++ b/packages/legacy-credentials/src/Claim.ts @@ -143,7 +143,7 @@ export function verifyDataStructure(input: IClaim | PartialClaim): void { throw new SDKErrors.CTypeHashMissingError() } if ('owner' in input) { - Did.validateUri(input.owner, 'Uri') + Did.validateIdentifier(input.owner, 'Uri') } if (input.contents !== undefined) { Object.entries(input.contents).forEach(([key, value]) => { diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index b05e3ea3f2..9cc4147a12 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -232,7 +232,7 @@ export async function verifySignature( expectedSigner: input.claim.owner, // allow full did to sign presentation if owned by corresponding light did allowUpgraded: true, - expectedVerificationMethodRelationship: 'authentication', + expectedVerificationRelationship: 'authentication', signerUrl: claimerSignature.signerUrl, dereferenceDidUrl, }) @@ -544,7 +544,7 @@ export async function createPresentation({ const signature = await signCallback({ data: makeSigningData(presentation, challenge), did: credential.claim.owner, - verificationMethodRelationship: 'authentication', + verificationRelationship: 'authentication', }) return { diff --git a/packages/legacy-credentials/src/vcInterop.ts b/packages/legacy-credentials/src/vcInterop.ts index 6bcdd82ca7..0e30e6280b 100644 --- a/packages/legacy-credentials/src/vcInterop.ts +++ b/packages/legacy-credentials/src/vcInterop.ts @@ -167,7 +167,7 @@ export function fromVC(input: Types.KiltCredentialV1): ICredential { ) const legitimations = (legitimationVcs ?? []).map( ({ id, verifiableCredential }) => - verifiableCredential !== undefined + verifiableCredential ? fromVC(verifiableCredential) : ({ rootHash: u8aToHex(KiltCredentialV1.idToRootHash(id)), diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 246e9dcee2..da2ccd7e89 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -9,7 +9,7 @@ import type { DidUri, SignatureVerificationRelationship, VerificationMethod, -} from './DidDocument' +} from './DidDocument.js' /** * Base interface for all signing requests. @@ -23,7 +23,7 @@ export interface SignRequestData { /** * The DID verification method relationship to be used. */ - verificationMethodRelationship: SignatureVerificationRelationship + verificationRelationship: SignatureVerificationRelationship /** * The DID to be used for signing. diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.ts index 8909c09f7e..5dab087a1b 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.ts @@ -184,7 +184,7 @@ export class KiltAttestationV1Suite extends LinkedDataProof { KiltCredentialV1.validateStructure(credential) const { id } = credential const proof = this.attestationInfo.get(id) - if (proof === undefined) { + if (!proof) { throw new Error( `No attestation information available for the credential ${id}. Make sure you have called anchorCredential on the same instance of this class.` ) @@ -213,7 +213,7 @@ export class KiltAttestationV1Suite extends LinkedDataProof { let cType = type?.find((str): str is ICType['$id'] => str.startsWith('kilt:ctype:') ) - if (cType === undefined) { + if (!cType) { cType = credentialSubject['@context']['@vocab'].slice( 0, -1 diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 69ad3f509c..762f27ecc8 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,7 +7,7 @@ /// -import { keypairToMultibaseKey, NewDidEncryptionKey } from '@kiltprotocol/did' +import type { NewDidEncryptionKey } from '@kiltprotocol/did' import type { DidDocument, KeyringPair, @@ -37,14 +37,14 @@ function makeSignCallback( keypair: KeyringPair ): (didDocument: DidDocument) => SignCallback { return (didDocument) => { - return async function sign({ data, verificationMethodRelationship }) { - const authKeyId = didDocument[verificationMethodRelationship]?.[0] + return async function sign({ data, verificationRelationship }) { + const authKeyId = didDocument[verificationRelationship]?.[0] const authKey = didDocument.verificationMethod?.find( ({ id }) => id === authKeyId ) if (authKeyId === undefined || authKey === undefined) { throw new Error( - `No verification method for purpose "${verificationMethodRelationship}" found in DID "${didDocument.id}"` + `No verification method for purpose "${verificationRelationship}" found in DID "${didDocument.id}"` ) } const signature = keypair.sign(data, { withType: false }) @@ -61,7 +61,7 @@ function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { return { signature, verificationMethod: { - publicKeyMultibase: keypairToMultibaseKey(keypair), + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), }, } } @@ -121,7 +121,7 @@ async function createFullDidFromKeypair( const encodedDidDetails = await queryFunction( Did.toChain( Did.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(keypair), + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), }) ) ) @@ -198,7 +198,7 @@ async function runAll() { const encodedDidDetails = await queryFunction( Did.toChain( Did.getFullDidUriFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(keypair), + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), }) ) ) diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 65ec835d15..556fb4db43 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -126,11 +126,11 @@ export type KeyToolSignCallback = (didDocument: DidDocument) => SignCallback */ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { return (didDocument) => - async function sign({ data, verificationMethodRelationship }) { - const keyId = didDocument[verificationMethodRelationship]?.[0] + async function sign({ data, verificationRelationship }) { + const keyId = didDocument[verificationRelationship]?.[0] if (keyId === undefined) { throw new Error( - `Verification method for relationship "${verificationMethodRelationship}" not found in DID "${didDocument.id}"` + `Verification method for relationship "${verificationRelationship}" not found in DID "${didDocument.id}"` ) } const verificationMethod = didDocument.verificationMethod?.find( @@ -138,7 +138,7 @@ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { ) if (verificationMethod === undefined) { throw new Error( - `Verification method for relationship "${verificationMethodRelationship}" not found in DID "${didDocument.id}"` + `Verification method for relationship "${verificationRelationship}" not found in DID "${didDocument.id}"` ) } const signature = keypair.sign(data, { withType: false }) From 998cdba58430347d6ce88af4f2a2d8ec743010b7 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 11:27:10 +0100 Subject: [PATCH 51/78] Fix yarn check --- tests/breakingChanges/BreakingChanges.spec.ts | 2 +- tests/integration/Ctypes.spec.ts | 12 ++--- tests/integration/Delegation.spec.ts | 36 +++++++-------- tests/integration/Deposit.spec.ts | 44 +++++++++---------- tests/integration/Did.spec.ts | 4 +- tests/integration/ErrorHandler.spec.ts | 4 +- tests/integration/PublicCredentials.spec.ts | 34 +++++++------- tests/integration/Web3Names.spec.ts | 24 +++++----- 8 files changed, 80 insertions(+), 80 deletions(-) diff --git a/tests/breakingChanges/BreakingChanges.spec.ts b/tests/breakingChanges/BreakingChanges.spec.ts index 0f97a60256..15a4c4d223 100644 --- a/tests/breakingChanges/BreakingChanges.spec.ts +++ b/tests/breakingChanges/BreakingChanges.spec.ts @@ -45,7 +45,7 @@ describe('Breaking Changes', () => { '0x127f2375faf3472c2f94ffcdd5424590b27294631f2cb8041407e501bc97c44c' ) - expect(did.uri).toMatchInlineSnapshot( + expect(did.id).toMatchInlineSnapshot( `"did:kilt:light:004quk8nu1MLvzdoT4fE6SJsLS4fFpyvuGz7sQpMF7ZAWTDoF5:z1msTRicERqs59nwMvp3yzMRBhUYGmkum7ehY7rtKQc8HzfEx4b4eyRhrc37ZShT3oG7E89x89vaG9W4hRxPS23EAFnCSeVbVRrKGJmFQvYhjgKSMmrGC7gSxgHe1a3g41uamhD49AEi13YVMkgeHpyEQJBy7N7gGyW7jTWFcwzAnws4wSazBVG1qHmVJrhmusoJoTfKTPKXkExKyur8Z341EkcRkHteY8dV3VjLXHnfhRW2yU9oM2cRm5ozgaufxrXsQBx33ygTW2wvrfzzXsYw4Bs6Vf2tC3ipBTDcKyCk6G88LYnzBosRM15W3KmDRciJ2iPjqiQkhYm77EQyaw"` ) diff --git a/tests/integration/Ctypes.spec.ts b/tests/integration/Ctypes.spec.ts index 253a76ef82..52fdea9b2c 100644 --- a/tests/integration/Ctypes.spec.ts +++ b/tests/integration/Ctypes.spec.ts @@ -55,7 +55,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const { keypair, getSignCallback } = makeSigningKeyTool() const storeTx = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx, getSignCallback(ctypeCreator), keypair.address @@ -71,7 +71,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const cType = makeCType() const storeTx = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx, key.getSignCallback(ctypeCreator), paymentAccount.address @@ -83,7 +83,7 @@ describe('When there is an CtypeCreator and a verifier', () => { cType.$id ) expect(originalCtype).toStrictEqual(cType) - expect(creator).toBe(ctypeCreator.uri) + expect(creator).toBe(ctypeCreator.id) await expect(CType.verifyStored(originalCtype)).resolves.not.toThrow() } }, 40_000) @@ -92,7 +92,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const cType = makeCType() const storeTx = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx, key.getSignCallback(ctypeCreator), paymentAccount.address @@ -101,7 +101,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const storeTx2 = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx2 = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx2, key.getSignCallback(ctypeCreator), paymentAccount.address @@ -115,7 +115,7 @@ describe('When there is an CtypeCreator and a verifier', () => { if (hasBlockNumbers) { const retrievedCType = await CType.fetchFromChain(cType.$id) - expect(retrievedCType.creator).toBe(ctypeCreator.uri) + expect(retrievedCType.creator).toBe(ctypeCreator.id) } }, 45_000) diff --git a/tests/integration/Delegation.spec.ts b/tests/integration/Delegation.spec.ts index 51485ba0de..ecc1efd7d1 100644 --- a/tests/integration/Delegation.spec.ts +++ b/tests/integration/Delegation.spec.ts @@ -57,14 +57,14 @@ async function writeHierarchy( sign: SignCallback ): Promise { const rootNode = DelegationNode.newRoot({ - account: delegator.uri, + account: delegator.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cTypeId), }) const storeTx = await rootNode.getStoreTx() const authorizedStoreTx = await Did.authorizeTx( - delegator.uri, + delegator.id, storeTx, sign, paymentAccount.address @@ -86,13 +86,13 @@ async function addDelegation( const delegationNode = DelegationNode.newNode({ hierarchyId, parentId, - account: delegate.uri, + account: delegate.id, permissions, }) const signature = await delegationNode.delegateSign(delegate, delegateSign) const storeTx = await delegationNode.getStoreTx(signature) const authorizedStoreTx = await Did.authorizeTx( - delegator.uri, + delegator.id, storeTx, delegatorSign, paymentAccount.address @@ -118,7 +118,7 @@ beforeAll(async () => { const storeTx = api.tx.ctype.add(CType.toChain(driversLicenseCType)) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), paymentAccount.address @@ -180,7 +180,7 @@ describe('and attestation rights have been delegated', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim, { delegationId: delegatedNode.id, @@ -197,7 +197,7 @@ describe('and attestation rights have been delegated', () => { const attestation = Attestation.fromCredentialAndDid( credential, - attester.uri + attester.id ) const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -205,7 +205,7 @@ describe('and attestation rights have been delegated', () => { { Delegation: { subjectNodeId: delegatedNode.id } } ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), paymentAccount.address @@ -224,7 +224,7 @@ describe('and attestation rights have been delegated', () => { Delegation: { maxChecks: 1 }, }) const authorizedStoreTx2 = await Did.authorizeTx( - root.uri, + root.id, revokeTx, rootKey.getSignCallback(root), paymentAccount.address @@ -273,9 +273,9 @@ describe('revocation', () => { ) // Test revocation - const revokeTx = await delegationA.getRevokeTx(delegator.uri) + const revokeTx = await delegationA.getRevokeTx(delegator.id) const authorizedRevokeTx = await Did.authorizeTx( - delegator.uri, + delegator.id, revokeTx, delegatorSign, paymentAccount.address @@ -288,7 +288,7 @@ describe('revocation', () => { // Change introduced in https://github.com/KILTprotocol/mashnet-node/pull/304 const removeTx = await delegationA.getRemoveTx() const authorizedRemoveTx = await Did.authorizeTx( - delegator.uri, + delegator.id, removeTx, delegatorSign, paymentAccount.address @@ -321,7 +321,7 @@ describe('revocation', () => { ) const revokeTx = api.tx.delegation.revokeDelegation(delegationRoot.id, 1, 1) const authorizedRevokeTx = await Did.authorizeTx( - firstDelegate.uri, + firstDelegate.id, revokeTx, firstDelegateSign, paymentAccount.address @@ -334,9 +334,9 @@ describe('revocation', () => { }) await expect(delegationRoot.verify()).resolves.not.toThrow() - const revokeTx2 = await delegationA.getRevokeTx(firstDelegate.uri) + const revokeTx2 = await delegationA.getRevokeTx(firstDelegate.id) const authorizedRevokeTx2 = await Did.authorizeTx( - firstDelegate.uri, + firstDelegate.id, revokeTx2, firstDelegateSign, paymentAccount.address @@ -368,9 +368,9 @@ describe('revocation', () => { secondDelegateSign ) delegationRoot = await delegationRoot.getLatestState() - const revokeTx = await delegationRoot.getRevokeTx(delegator.uri) + const revokeTx = await delegationRoot.getRevokeTx(delegator.id) const authorizedRevokeTx = await Did.authorizeTx( - delegator.uri, + delegator.id, revokeTx, delegatorSign, paymentAccount.address @@ -438,7 +438,7 @@ describe('handling queries to data not on chain', () => { permissions: [0], hierarchyId: randomAsHex(32), parentId: randomAsHex(32), - account: attester.uri, + account: attester.id, }).getAttestationHashes() ).toEqual([]) }) diff --git a/tests/integration/Deposit.spec.ts b/tests/integration/Deposit.spec.ts index 236fbea25e..fbfa20d60a 100644 --- a/tests/integration/Deposit.spec.ts +++ b/tests/integration/Deposit.spec.ts @@ -48,18 +48,18 @@ async function checkDeleteFullDid( sign: SignCallback ): Promise { storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) const deleteDid = api.tx.did.delete(storedEndpointsCount) - tx = await Did.authorizeTx(fullDid.uri, deleteDid, sign, identity.address) + tx = await Did.authorizeTx(fullDid.id, deleteDid, sign, identity.address) const balanceBeforeDeleting = ( await api.query.system.account(identity.address) ).data const didResult = Did.documentFromChain( - await api.query.did.did(Did.toChain(fullDid.uri)) + await api.query.did.did(Did.toChain(fullDid.id)) ) const didDeposit = didResult.deposit @@ -79,16 +79,16 @@ async function checkReclaimFullDid( fullDid: DidDocument ): Promise { storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) - tx = api.tx.did.reclaimDeposit(Did.toChain(fullDid.uri), storedEndpointsCount) + tx = api.tx.did.reclaimDeposit(Did.toChain(fullDid.id), storedEndpointsCount) const balanceBeforeRevoking = ( await api.query.system.account(identity.address) ).data const didResult = Did.documentFromChain( - await api.query.did.did(Did.toChain(fullDid.uri)) + await api.query.did.did(Did.toChain(fullDid.id)) ) const didDeposit = didResult.deposit @@ -109,14 +109,14 @@ async function checkRemoveFullDidAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) @@ -130,10 +130,10 @@ async function checkRemoveFullDidAttestation( const balanceBeforeRemoving = ( await api.query.system.account(identity.address) ).data - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.remove(attestation.claimHash, null) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) @@ -152,21 +152,21 @@ async function checkReclaimFullDidAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) const balanceBeforeReclaiming = ( await api.query.system.account(identity.address) ).data - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.reclaimDeposit(attestation.claimHash) @@ -194,25 +194,25 @@ async function checkDeletedDidReclaimAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) const deleteDid = api.tx.did.delete(storedEndpointsCount) - tx = await Did.authorizeTx(fullDid.uri, deleteDid, sign, identity.address) + tx = await Did.authorizeTx(fullDid.id, deleteDid, sign, identity.address) await submitTx(tx, identity) @@ -234,7 +234,7 @@ async function checkWeb3Deposit( const depositAmount = api.consts.web3Names.deposit.toBn() const claimTx = api.tx.web3Names.claim(web3Name) let didAuthorizedTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, claimTx, sign, identity.address @@ -253,7 +253,7 @@ async function checkWeb3Deposit( const releaseTx = api.tx.web3Names.releaseByOwner() didAuthorizedTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, releaseTx, sign, identity.address @@ -295,7 +295,7 @@ beforeAll(async () => { const ctypeExists = await isCtypeOnChain(driversLicenseCType) if (!ctypeExists) { const extrinsic = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.ctype.add(CType.toChain(driversLicenseCType)), attesterKey.getSignCallback(attester), devFaucet.address @@ -311,7 +311,7 @@ beforeAll(async () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, rawClaim, - claimerLightDid.uri + claimerLightDid.id ) credential = Credential.fromClaim(claim) diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index 38e5672a94..403b8a7e46 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -79,7 +79,7 @@ describe('write and didDeleteTx', () => { Did.multibaseKeyToDidKey(publicKeyMultibase) const newDid = Did.createLightDidDocument({ authentication: [{ publicKey: authPublicKey, type: keyType }] as [ - Did.NewDidVerificationKey + Did.NewLightDidVerificationKey ], service: [ { @@ -898,7 +898,7 @@ describe('DID management batching', () => { makeSigningKeyTool() const { authentication: [newAuthKey], - } = TestUtils.makeSigningKeyTool('ed25519') + } = makeSigningKeyTool('ed25519') const createTx = await Did.getStoreTxFromInput( { authentication }, diff --git a/tests/integration/ErrorHandler.spec.ts b/tests/integration/ErrorHandler.spec.ts index 26f7a6e9ea..b440c7821d 100644 --- a/tests/integration/ErrorHandler.spec.ts +++ b/tests/integration/ErrorHandler.spec.ts @@ -76,7 +76,7 @@ it('records an extrinsic error when ctype does not exist', async () => { cTypeHash: '0x103752ecd8e284b1c9677337ccc91ea255ac8e6651dc65d90f0504f31d7e54f0', delegationId: null, - owner: someDid.uri, + owner: someDid.id, revoked: false, } const storeTx = api.tx.attestation.add( @@ -85,7 +85,7 @@ it('records an extrinsic error when ctype does not exist', async () => { null ) const tx = await Did.authorizeTx( - someDid.uri, + someDid.id, storeTx, key.getSignCallback(someDid), paymentAccount.address diff --git a/tests/integration/PublicCredentials.spec.ts b/tests/integration/PublicCredentials.spec.ts index b6c93c9491..b4989f53ab 100644 --- a/tests/integration/PublicCredentials.spec.ts +++ b/tests/integration/PublicCredentials.spec.ts @@ -49,7 +49,7 @@ async function issueCredential( credential: IPublicCredentialInput ): Promise { const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential)), attesterKey.getSignCallback(attester), tokenHolder.address @@ -66,7 +66,7 @@ beforeAll(async () => { const ctypeExists = await isCtypeOnChain(nftNameCType) if (ctypeExists) return const tx = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.ctype.add(CType.toChain(nftNameCType)), attesterKey.getSignCallback(attester), tokenHolder.address @@ -87,7 +87,7 @@ describe('When there is an attester and ctype NFT name', () => { await issueCredential(latestCredential) const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) const publicCredentialEntry = await api.call.publicCredentials.getById( @@ -104,7 +104,7 @@ describe('When there is an attester and ctype NFT name', () => { expect.objectContaining({ ...latestCredential, id: credentialId, - attester: attester.uri, + attester: attester.id, revoked: false, }) ) @@ -147,7 +147,7 @@ describe('When there is an attester and ctype NFT name', () => { }) const authorizedBatch = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: attester.uri, + did: attester.id, extrinsics: credentialCreationTxs, sign: attesterKey.getSignCallback(attester), submitter: tokenHolder.address, @@ -165,7 +165,7 @@ describe('When there is an attester and ctype NFT name', () => { it('should be possible to revoke a credential', async () => { const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) let assetCredential = await PublicCredentials.fetchCredentialFromChain( credentialId @@ -176,7 +176,7 @@ describe('When there is an attester and ctype NFT name', () => { expect(assetCredential.revoked).toBe(false) const revocationTx = api.tx.publicCredentials.revoke(credentialId, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, revocationTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -199,7 +199,7 @@ describe('When there is an attester and ctype NFT name', () => { it('should be possible to unrevoke a credential', async () => { const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) let assetCredential = await PublicCredentials.fetchCredentialFromChain( credentialId @@ -211,7 +211,7 @@ describe('When there is an attester and ctype NFT name', () => { const unrevocationTx = api.tx.publicCredentials.unrevoke(credentialId, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, unrevocationTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -234,7 +234,7 @@ describe('When there is an attester and ctype NFT name', () => { it('should be possible to remove a credential', async () => { const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) let encodedAssetCredential = await api.call.publicCredentials.getById( credentialId @@ -246,7 +246,7 @@ describe('When there is an attester and ctype NFT name', () => { const removalTx = api.tx.publicCredentials.remove(credentialId, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, removalTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -284,7 +284,7 @@ describe('When there is an issued public credential', () => { await issueCredential(latestCredential) const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) credential = await PublicCredentials.fetchCredentialFromChain(credentialId) }) @@ -433,7 +433,7 @@ describe('When there is an issued public credential', () => { // Revoke first const revocationTx = api.tx.publicCredentials.revoke(credential.id, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, revocationTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -485,11 +485,11 @@ describe('When there is a batch which contains a credential creation', () => { } // A batchAll with a DID call, and a nested batch with a second DID call and a nested forceBatch batch with a third DID call. const currentAttesterNonce = Did.documentFromChain( - await api.query.did.did(Did.toChain(attester.uri)) + await api.query.did.did(Did.toChain(attester.id)) ).lastTxCounter const batchTx = api.tx.utility.batchAll([ await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential1)), attesterKey.getSignCallback(attester), tokenHolder.address, @@ -497,7 +497,7 @@ describe('When there is a batch which contains a credential creation', () => { ), api.tx.utility.batch([ await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential2)), attesterKey.getSignCallback(attester), tokenHolder.address, @@ -505,7 +505,7 @@ describe('When there is a batch which contains a credential creation', () => { ), api.tx.utility.forceBatch([ await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add( PublicCredentials.toChain(credential3) ), diff --git a/tests/integration/Web3Names.spec.ts b/tests/integration/Web3Names.spec.ts index 1c00908e54..9e586154eb 100644 --- a/tests/integration/Web3Names.spec.ts +++ b/tests/integration/Web3Names.spec.ts @@ -37,8 +37,8 @@ describe('When there is an Web3NameCreator and a payer', () => { let otherWeb3NameCreator: DidDocument let paymentAccount: KiltKeyringPair let otherPaymentAccount: KeyringPair - let nick: Did.Web3Name - let differentNick: Did.Web3Name + let nick: string + let differentNick: string beforeAll(async () => { nick = `nick_${randomAsHex(2)}` @@ -68,7 +68,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const tx = api.tx.web3Names.claim(nick) const bobbyBroke = makeSigningKeyTool().keypair const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), bobbyBroke.address @@ -82,7 +82,7 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should be possible to create a w3n name with enough tokens', async () => { const tx = api.tx.web3Names.claim(nick) const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address @@ -93,21 +93,21 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should be possible to lookup the DID uri with the given nick', async () => { const { - document: { uri }, + document: { id }, } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name(nick)) - expect(uri).toStrictEqual(w3nCreator.uri) + expect(id).toStrictEqual(w3nCreator.id) }, 30_000) it('should be possible to lookup the nick with the given DID uri', async () => { - const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.uri)) + const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.id)) const didInfo = Did.linkedInfoFromChain(encodedDidInfo) - expect(didInfo.web3Name).toBe(nick) + expect(didInfo.document.alsoKnownAs).toBe([nick]) }, 30_000) it('should not be possible to create the same w3n twice', async () => { const tx = api.tx.web3Names.claim(nick) const authorizedTx = await Did.authorizeTx( - otherWeb3NameCreator.uri, + otherWeb3NameCreator.id, tx, otherW3NCreatorKey.getSignCallback(otherWeb3NameCreator), paymentAccount.address @@ -124,7 +124,7 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should not be possible to create a second w3n for the same did', async () => { const tx = api.tx.web3Names.claim('nick2') const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address @@ -156,7 +156,7 @@ describe('When there is an Web3NameCreator and a payer', () => { // prepare the w3n on chain const prepareTx = api.tx.web3Names.claim(differentNick) const prepareAuthorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, prepareTx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address @@ -165,7 +165,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const tx = api.tx.web3Names.releaseByOwner() const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address From fc79afe0d3bfefeb62ef5d90d1509a5f355edc26 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 12:03:23 +0100 Subject: [PATCH 52/78] Fix JSDocs --- packages/did/src/Did.chain.ts | 8 +-- packages/did/src/Did.signature.ts | 2 +- packages/did/src/Did.utils.ts | 16 +++-- packages/did/src/DidDetails/DidDetails.ts | 28 +++++++- packages/did/src/DidResolver/DidResolver.ts | 10 ++- packages/legacy-credentials/src/Credential.ts | 4 +- packages/types/src/CryptoCallbacks.ts | 2 +- packages/types/src/DidDocument.ts | 52 ++++----------- packages/types/src/DidResolver.ts | 64 +++++++++---------- 9 files changed, 97 insertions(+), 89 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 1a4b054507..549a8f2e1e 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -335,13 +335,13 @@ interface GetStoreTxInput { service?: NewService[] } -type GetStoreTxSignCallbacResponse = Pick & { +type GetStoreTxSignCallbackResponse = Pick & { // We don't need the key ID to dispatch the tx. verificationMethod: Pick } export type GetStoreTxSignCallback = ( signData: Omit -) => Promise +) => Promise /** * Create a DID creation operation which includes the information provided. @@ -382,14 +382,14 @@ export async function getStoreTxFromInput( } // For now, it only takes the first attestation key, if present. - if (assertionMethod !== undefined && assertionMethod.length > 1) { + if (assertionMethod && assertionMethod.length > 1) { throw new SDKErrors.DidError( `More than one attestation key (${assertionMethod.length}) specified. The chain can only store one.` ) } // For now, it only takes the first delegation key, if present. - if (capabilityDelegation !== undefined && capabilityDelegation.length > 1) { + if (capabilityDelegation && capabilityDelegation.length > 1) { throw new SDKErrors.DidError( `More than one delegation key (${capabilityDelegation.length}) specified. The chain can only store one.` ) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 880e4f24bc..d6d12b3f8f 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -67,7 +67,7 @@ function verifyDidSignatureDataStructure( } /** - * Verify a DID signature given the signer's DID URL. + * Verify a DID signature given the signer's DID URL (i.e., DID URI + verification method ID). * A signature verification returns false if a migrated and then deleted DID is used. * * @param input Object wrapping all input. diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 3865b06b67..aaa5cf7bf5 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -53,6 +53,8 @@ type IDidParsingResult = { encodedDetails?: string } +// Exports the params section of a DID URL as a map. +// If multiple keys are present, only the first one is returned. function exportQueryParamsFromUri(didUri: DidUrl): Record { const urlified = new URL(didUri) const params: Record = {} @@ -158,9 +160,9 @@ const multicodecReversePrefixes: Record = { } /** - * Decode a multibase, multicodec representation of a verification method into its fundamental components: the public key and the key type. + * Decode a Multikey representation of a verification method into its fundamental components: the public key and the key type. * - * @param publicKeyMultibase The verification method's public key multibase. + * @param publicKeyMultibase The verification method's public key in Multikey format (i.e., multicodec-prefixed, then multibase encoded). * @returns The decoded public key and [[DidKeyType]]. */ export function multibaseKeyToDidKey( @@ -189,12 +191,12 @@ export function multibaseKeyToDidKey( } /** - * Calculate the multibase, multicodec representation of a keypair given its type and public key. + * Calculate the Multikey representation of a keypair given its type and public key. * - * @param keypair The input keypair to encode as multibase, multicodec. + * @param keypair The input keypair to encode as Multikey. * @param keypair.type The keypair [[DidKeyType]]. * @param keypair.publicKey The keypair public key. - * @returns The multicodec, multibase encoding of the provided keypair. + * @returns The Multikey representation (i.e., multicodec-prefixed, then multibase encoded) of the provided keypair. */ export function keypairToMultibaseKey({ type, @@ -228,7 +230,7 @@ export function keypairToMultibaseKey({ * @param key The DID key to export as a verification method. * @param key.keyType The key type. * @param key.publicKey The public component of the key. - * @returns The provided key encoded as a [[ DidDocumentV2.VerificationMethod]]. + * @returns The provided key encoded as a [[VerificationMethod]]. */ export function didKeyToVerificationMethod( controller: VerificationMethod['controller'], @@ -286,7 +288,7 @@ export function validateIdentifier( const { address, queryParameters, fragment } = parse(input as DidUri) if ( - (fragment !== undefined || queryParameters !== undefined) && + (fragment || queryParameters) && (expectType === 'Uri' || // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. (typeof expectType === 'boolean' && expectType === false)) diff --git a/packages/did/src/DidDetails/DidDetails.ts b/packages/did/src/DidDetails/DidDetails.ts index b94ea94cb7..7b985b9930 100644 --- a/packages/did/src/DidDetails/DidDetails.ts +++ b/packages/did/src/DidDetails/DidDetails.ts @@ -16,13 +16,19 @@ import type { import { didKeyToVerificationMethod } from '../Did.utils.js' /** - * Possible types for a DID verification method. + * Possible types for a DID verification method used in digital signatures. */ const signingMethodTypesC = ['sr25519', 'ed25519', 'ecdsa'] as const export const signingMethodTypes = signingMethodTypesC as unknown as string[] export type DidSigningMethodType = typeof signingMethodTypesC[number] // `as unknown as string[]` is a workaround for https://github.com/microsoft/TypeScript/issues/26255 +/** + * Type guard checking whether the provided input string represents one of the supported signing verification types. + * + * @param input The input string. + * @returns Whether the input string is an instance of [[DidSigningMethodType]]. + */ export function isValidVerificationMethodType( input: string ): input is DidSigningMethodType { @@ -30,13 +36,19 @@ export function isValidVerificationMethodType( } /** - * Possible types for a DID encryption verification method. + * Possible types for a DID verification method used in encryption. */ const encryptionMethodTypesC = ['x25519'] as const export const encryptionMethodTypes = encryptionMethodTypesC as unknown as string[] export type DidEncryptionMethodType = typeof encryptionMethodTypesC[number] +/** + * Type guard checking whether the provided input string represents one of the supported encryption verification types. + * + * @param input The input string. + * @returns Whether the input string is an instance of [[DidEncryptionMethodType]]. + */ export function isValidEncryptionMethodType( input: string ): input is DidEncryptionMethodType { @@ -47,6 +59,12 @@ export type DidVerificationMethodType = | DidSigningMethodType | DidEncryptionMethodType +/** + * Type guard checking whether the provided input string represents one of the supported signing or encryption verification types. + * + * @param input The input string. + * @returns Whether the input string is an instance of [[DidSigningMethodType]]. + */ export function isValidDidVerificationType( input: string ): input is DidSigningMethodType { @@ -58,6 +76,12 @@ export function isValidDidVerificationType( export type NewVerificationMethod = Omit export type NewService = Service +/** + * Type guard checking whether the provided input represents one of the supported verification relationships. + * + * @param input The input. + * @returns Whether the input is an instance of [[VerificationRelationship]]. + */ export function isValidVerificationRelationship( input: unknown ): input is VerificationRelationship { diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 55f0ab5d25..0e5ca7ae69 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -37,7 +37,7 @@ const DID_JSON_LD = 'application/did+ld+json' const DID_CBOR = 'application/did+cbor' /** - * Supported types for DID resolution and dereferencing. + * Supported content types for DID resolution and dereferencing. */ export type SupportedContentType = | typeof DID_JSON @@ -97,7 +97,7 @@ async function resolveInternal( canonicalId: getFullDidUri(did), }, document: { - ...lightDocument, + id: lightDocument.id, }, } } @@ -219,6 +219,12 @@ type InternalDereferenceResult = contentStream: DereferenceContentStream } +/** + * Type guard checking whether the provided input is a [[FailedDereferenceMetadata]]. + * + * @param input The input to check. + * @returns Whether the input is a [[FailedDereferenceMetadata]]. + */ export function isFailedDereferenceMetadata( input: unknown ): input is FailedDereferenceMetadata { diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 9cc4147a12..d23244e9cb 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -205,7 +205,7 @@ export function verifyDataStructure(input: ICredential): void { * * @param input - The [[ICredentialPresentation]]. * @param verificationOpts Additional verification options. - * @param verificationOpts.dereferenceDidUrl - The function used to resolve the claimer's DID Document and verification method. Defaults to [[dereferenceDidUrl]]. + * @param verificationOpts.dereferenceDidUrl - The function used to dereference the claimer's DID Document and verification method. Defaults to [[dereferenceDidUrl]]. * @param verificationOpts.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. */ export async function verifySignature( @@ -433,7 +433,7 @@ export async function verifyCredential( * @param options - Additional parameter for more verification steps. * @param options.ctype - CType which the included claim should be checked against. * @param options.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. - * @param options.dereferenceDidUrl - The function used to resolve the claimer's verification method. Defaults to [[dereference]]. + * @param options.dereferenceDidUrl - The function used to dereference the claimer's DID and verification method. Defaults to [[dereference]]. * @returns A [[VerifiedCredential]] object, which is the orignal credential presentation with two additional properties: * a boolean `revoked` status flag and the `attester` DID. */ diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index da2ccd7e89..8ed89cb866 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -21,7 +21,7 @@ export interface SignRequestData { data: Uint8Array /** - * The DID verification method relationship to be used. + * The DID verification relationship to be used. */ verificationRelationship: SignatureVerificationRelationship diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 437f723396..310aa8d939 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -26,7 +26,7 @@ export type UriFragment = `#${string}` /** * URL for DID resources like keys or services. */ -export type DidUrl = `${DidUri}${UriFragment}` | `${DidUri}${UriFragment}` +export type DidUrl = `${DidUri}${UriFragment}` export type SignatureVerificationRelationship = | 'authentication' @@ -40,78 +40,54 @@ export type VerificationRelationship = type Base58BtcMultibaseString = `z${string}` -/* - * The verification method map MUST include the id, type, controller, and specific verification material properties that are determined by the value of type and are defined in 5.2.1 Verification Material. A verification method MAY include additional properties. Verification methods SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. +/** + * The verification method of a DID. */ export type VerificationMethod = { - /* - * The value of the id property for a verification method MUST be a string that conforms to the rules in Section 3.2 DID URL Syntax. + /** + * The relative identifier (i.e., `#`) of the verification method. */ id: UriFragment - /* - * The value of the type property MUST be a string that references exactly one verification method type. In order to maximize global interoperability, the verification method type SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + /** + * The type of the verification method. This is fixed for KILT DIDs. */ type: 'MultiKey' - /* - * The value of the controller property MUST be a string that conforms to the rules in 3.1 DID Syntax. + /** + * The controller of the verification method. */ controller: DidUri /* - * The publicKeyMultibase property is OPTIONAL. This feature is non-normative. If present, the value MUST be a string representation of a [MULTIBASE] encoded public key. + * The multicodec-prefixed, multibase-encoded verification method's public key. */ publicKeyMultibase: Base58BtcMultibaseString } /* - * Each service map MUST contain id, type, and serviceEndpoint properties. Each service extension MAY include additional properties and MAY further restrict the properties associated with the extension. + * The service of a KILT DID. */ export type Service = { /* - * The value of the id property MUST be a URI conforming to [RFC3986]. A conforming producer MUST NOT produce multiple service entries with the same id. A conforming consumer MUST produce an error if it detects multiple service entries with the same id. + * The relative identifier (i.e., `#`) of the verification method. */ id: UriFragment /* - * The value of the type property MUST be a string or a set of strings. In order to maximize interoperability, the service type and its associated properties SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. + * The set of service types. */ type: string[] /* - * The value of the serviceEndpoint property MUST be a string, a map, or a set composed of one or more strings and/or maps. All string values MUST be valid URIs conforming to [RFC3986] and normalized according to the Normalization and Comparison rules in RFC3986 and to any normalization rules in its applicable URI scheme specification. + * A list of URIs the endpoint exposes its services at. */ serviceEndpoint: string[] } export type DidDocument = { - /* - * The value of id MUST be a string that conforms to the rules in 3.1 DID Syntax and MUST exist in the root map of the data model for the DID document. - */ id: DidUri - /* - * The alsoKnownAs property is OPTIONAL. If present, the value MUST be a set where each item in the set is a URI conforming to [RFC3986]. - */ alsoKnownAs?: string[] - /* - * The verificationMethod property is OPTIONAL. If present, the value MUST be a set of verification methods, where each verification method is expressed using a map. - */ verificationMethod?: VerificationMethod[] - /* - * The authentication property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ authentication?: UriFragment[] - /* - * The assertionMethod property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ assertionMethod?: UriFragment[] - /* - * The keyAgreement property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ keyAgreement?: UriFragment[] - /* - * The capabilityDelegation property is OPTIONAL. If present, the associated value MUST be a set of one or more verification methods. Each verification method MAY be embedded or referenced. - */ capabilityDelegation?: UriFragment[] - /* - * The service property is OPTIONAL. If present, the associated value MUST be a set of services, where each service is described by a map. - */ service?: Service[] } diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index e8e6dd6b63..1bd0d4de54 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -14,32 +14,32 @@ import type { JsonLd, } from './DidDocument' -/* +/** * The `accept` header must not be used for the regular `resolve` function, so we enforce that statically. * For more info, please refer to https://www.w3.org/TR/did-core/#did-resolution-options. */ export type ResolutionOptions = Record export type ResolutionMetadata = { - /* + /** * The error code from the resolution process. * This property is REQUIRED when there is an error in the resolution process. * The value of this property MUST be a single keyword ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries. * This specification defines the following common error values: - * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. - * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + * * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. */ error?: 'invalidDid' | 'notFound' } export type ResolutionDocumentMetadata = { - /* + /** * If a DID has been deactivated, DID document metadata MUST include this property with the boolean value true. * If a DID has not been deactivated, this property is OPTIONAL, but if included, MUST have the boolean value false. */ deactivated?: true - /* + /** * DID document metadata MAY include a canonicalId property. * If present, the value MUST be a string that conforms to the rules in Section 3.1 DID Syntax. * The relationship is a statement that the canonicalId value is logically equivalent to the id property value and that the canonicalId value is defined by the DID method to be the canonical ID for the DID subject in the scope of the containing DID document. @@ -49,7 +49,7 @@ export type ResolutionDocumentMetadata = { } export type ResolutionResult = { - /* + /** * A metadata structure consisting of values relating to the results of the DID resolution process which typically changes between invocations of the resolve and resolveRepresentation functions, as it represents data about the resolution process itself. * This structure is REQUIRED, and in the case of an error in the resolution process, this MUST NOT be empty. * If resolveRepresentation was called, this structure MUST contain a contentType property containing the Media Type of the representation found in the didDocumentStream. @@ -57,13 +57,13 @@ export type ResolutionResult = { * The possible properties within this structure and their possible values are registered in the DID Specification Registries. */ didResolutionMetadata: ResolutionMetadata - /* + /** * If the resolution is successful, and if the resolve function was called, this MUST be a DID document abstract data model (a map) as described in 4. Data Model that is capable of being transformed into a conforming DID Document (representation), using the production rules specified by the representation. * The value of id in the resolved DID document MUST match the DID that was resolved. * If the resolution is unsuccessful, this value MUST be empty. */ didDocument?: DidDocument - /* + /** * If the resolution is successful, this MUST be a metadata structure. * This structure contains metadata about the DID document contained in the didDocument property. * This metadata typically does not change between invocations of the resolve and resolveRepresentation functions unless the DID document changes, as it represents metadata about the DID document. @@ -74,7 +74,7 @@ export type ResolutionResult = { } export type RepresentationResolutionOptions = { - /* + /** * The Media Type of the caller's preferred representation of the DID document. * The Media Type MUST be expressed as an ASCII string. * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. @@ -86,7 +86,7 @@ export type RepresentationResolutionOptions = { export type SuccessfulRepresentationResolutionMetadata< ContentType extends string > = { - /* + /** * The Media Type of the returned didDocumentStream. * This property is REQUIRED if resolution is successful and if the resolveRepresentation function was called. * This property MUST NOT be present if the resolve function was called. @@ -96,15 +96,15 @@ export type SuccessfulRepresentationResolutionMetadata< contentType: ContentType } export type FailedRepresentationResolutionMetadata = { - /* + /** * The error code from the resolution process. * This property is REQUIRED when there is an error in the resolution process. * The value of this property MUST be a single keyword ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries. * This specification defines the following common error values: - * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. - * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. - * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. + * * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + * * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. */ error: 'invalidDid' | 'notFound' | 'representationNotSupported' } @@ -121,7 +121,7 @@ export type RepresentationResolutionResult = Pick< ResolutionResult, 'didDocumentMetadata' > & { - /* + /** * If the resolution is successful, and if the resolveRepresentation function was called, this MUST be a byte stream of the resolved DID document in one of the conformant representations. * The byte stream might then be parsed by the caller of the resolveRepresentation function into a data model, which can in turn be validated and processed. * If the resolution is unsuccessful, this value MUST be an empty stream. @@ -130,17 +130,17 @@ export type RepresentationResolutionResult = Pick< didResolutionMetadata: RepresentationResolutionMetadata } -/* +/** * The resolve function returns the DID document in its abstract form (a map). */ export interface ResolveDid { resolve: ( - /* + /** * This is the DID to resolve. * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. */ did: DidUri, - /* + /** * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. * This input is REQUIRED, but the structure MAY be empty. */ @@ -148,12 +148,12 @@ export interface ResolveDid { ) => Promise resolveRepresentation: ( - /* + /** * This is the DID to resolve. * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. */ did: DidUri, - /* + /** * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. * This input is REQUIRED, but the structure MAY be empty. */ @@ -162,7 +162,7 @@ export interface ResolveDid { } export type DereferenceOptions = { - /* + /** * The Media Type that the caller prefers for contentStream. * The Media Type MUST be expressed as an ASCII string. * The DID URL dereferencing implementation SHOULD use this value to determine the contentType of the representation contained in the returned value if such a representation is supported and available. @@ -171,22 +171,22 @@ export type DereferenceOptions = { } export type SuccessfulDereferenceMetadata = { - /* + /** * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. * The Media Type value MUST be expressed as an ASCII string. */ contentType: ContentType } export type FailedDereferenceMetadata = { - /* + /** * The error code from the dereferencing process. * This property is REQUIRED when there is an error in the dereferencing process. * The value of this property MUST be a single keyword expressed as an ASCII string. * The possible property values of this field SHOULD be registered in the DID Specification Registries [DID-SPEC-REGISTRIES]. * This specification defines the following common error values: - * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.) - * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. - * invalidVerificationRelationship: https://github.com/decentralized-identity/did-spec-extensions/pull/21 + * * invalidDidUrl: The DID URL supplied to the DID URL dereferencing function does not conform to valid syntax. (See 3.2 DID URL Syntax.). + * * notFound: The DID URL dereferencer was unable to find the contentStream resulting from this dereferencing request. + * * invalidVerificationRelationship: https://github.com/decentralized-identity/did-spec-extensions/pull/21. */ error: 'invalidDidUrl' | 'notFound' | 'invalidVerificationRelationship' } @@ -208,20 +208,20 @@ export type DereferenceContentStream = export type DereferenceContentMetadata = ResolutionDocumentMetadata export type DereferenceResult = { - /* + /** * A metadata structure consisting of values relating to the results of the DID URL dereferencing process. * This structure is REQUIRED, and in the case of an error in the dereferencing process, this MUST NOT be empty. * Properties defined by this specification are in 7.2.2 DID URL Dereferencing Metadata. * If the dereferencing is not successful, this structure MUST contain an error property describing the error. */ dereferencingMetadata: DereferenceMetadata - /* + /** * If the dereferencing function was called and successful, this MUST contain a resource corresponding to the DID URL. * The contentStream MAY be a resource such as a DID document that is serializable in one of the conformant representations, a Verification Method, a service, or any other resource format that can be identified via a Media Type and obtained through the resolution process. * If the dereferencing is unsuccessful, this value MUST be empty. */ contentStream?: DereferenceContentStream - /* + /** * If the dereferencing is successful, this MUST be a metadata structure, but the structure MAY be empty. * This structure contains metadata about the contentStream. * If the contentStream is a DID document, this MUST be a didDocumentMetadata structure as described in DID Resolution. @@ -232,13 +232,13 @@ export type DereferenceResult = { export interface DereferenceDidUrl { dereference: ( - /* + /** * A conformant DID URL as a single string. * This is the DID URL to dereference. * To dereference a DID fragment, the complete DID URL including the DID fragment MUST be used. This input is REQUIRED. */ didUrl: DidUri | DidUrl, - /* + /** * A metadata structure consisting of input options to the dereference function in addition to the didUrl itself. * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. * This input is REQUIRED, but the structure MAY be empty. From 6f1dd3c29a7517edde7849601f5e158c196f2428 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 14:03:01 +0100 Subject: [PATCH 53/78] Update tests --- packages/did/src/Did.signature.spec.ts | 4 +- .../legacy-credentials/src/Credential.spec.ts | 60 +++++++++++-------- tests/bundle/bundle-test.ts | 6 +- tests/integration/Did.spec.ts | 12 ++-- tests/integration/Web3Names.spec.ts | 2 +- tests/testUtils/TestUtils.ts | 2 +- 6 files changed, 47 insertions(+), 39 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index cb63940b13..00aab5914d 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -228,6 +228,7 @@ describe('light DID', () => { canonicalId: did.id, }, dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: { id: did.id }, }) const SIGNED_STRING = 'signed string' const { signature, verificationMethod } = await sign({ @@ -408,6 +409,7 @@ describe('full DID', () => { jest.mocked(dereference).mockResolvedValue({ contentMetadata: { deactivated: true }, dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: { id: did.id }, }) const SIGNED_STRING = 'signed string' const { signature, verificationMethod } = await sign({ @@ -521,7 +523,7 @@ describe('type guard', () => { expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - signerUrl: `did:kilt:${keypair.address}`, + signerUrl: `kilt:did:${keypair.address}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 0f9b2378c3..266ec12433 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -634,7 +634,7 @@ describe('Presentations', () => { [legitimation] ) - // sign presentation using Alice's authenication verification method + // sign presentation using Alice's authentication verification method const presentation = await Credential.createPresentation({ credential, signCallback: keyAlice.getSignCallback(identityAlice), @@ -754,7 +754,7 @@ describe('create presentation', () => { id, computeKeyId(newAuthenticationKey.publicKey), { - keyType: newAuthenticationKey?.type, + keyType: newAuthenticationKey.type, publicKey: newAuthenticationKey.publicKey, } ) @@ -766,6 +766,7 @@ describe('create presentation', () => { const { publicKey } = Did.multibaseKeyToDidKey( lightDidVerificationMethod.publicKeyMultibase ) + // Override the verification method ID to the computed one lightDidVerificationMethod.id = computeKeyId(publicKey) return lightDidVerificationMethod })() @@ -781,35 +782,42 @@ describe('create presentation', () => { didUrl: DidUrl | DidUri ): Promise> { const { did } = Did.parse(didUrl) - if (did === migratedClaimerLightDid.id) { - return { - contentMetadata: { canonicalId: migratedClaimerFullDid.id }, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: migratedClaimerLightDid, + switch (did) { + case migratedClaimerLightDid.id: { + return { + contentMetadata: { canonicalId: migratedClaimerFullDid.id }, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: { id: migratedClaimerLightDid.id }, + } } - } - if (did === unmigratedClaimerLightDid.id) { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: unmigratedClaimerLightDid, + case unmigratedClaimerLightDid.id: { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: unmigratedClaimerLightDid, + } } - } - if (did === migratedClaimerFullDid.id) { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: migratedClaimerFullDid, + case migratedClaimerFullDid.id: { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: migratedClaimerFullDid, + } } - } - if (did === attester.id) { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: attester, + case attester.id: { + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: attester, + } + } + default: { + return { + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + } } } - return { contentMetadata: {}, dereferencingMetadata: { error: 'notFound' } } } beforeAll(async () => { diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 762f27ecc8..8b70820a10 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -206,7 +206,6 @@ async function runAll() { const resolved = await Did.resolve(fullDid.id) if ( - resolved !== undefined && !resolved.didDocumentMetadata.deactivated && resolved.didDocument?.id === fullDid.id ) { @@ -224,10 +223,7 @@ async function runAll() { await Blockchain.signAndSubmitTx(deleteTx, payer) const resolvedAgain = await Did.resolve(fullDid.id) - if ( - resolvedAgain === undefined || - resolvedAgain.didDocumentMetadata.deactivated - ) { + if (resolvedAgain.didDocumentMetadata.deactivated) { console.info('DID successfully deleted') } else { throw new Error('DID was not deleted') diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index 403b8a7e46..29ce021be2 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -77,7 +77,7 @@ describe('write and didDeleteTx', () => { ) as VerificationMethod const { keyType, publicKey: authPublicKey } = Did.multibaseKeyToDidKey(publicKeyMultibase) - const newDid = Did.createLightDidDocument({ + const input: Did.CreateDocumentInput = { authentication: [{ publicKey: authPublicKey, type: keyType }] as [ Did.NewLightDidVerificationKey ], @@ -93,17 +93,19 @@ describe('write and didDeleteTx', () => { serviceEndpoint: ['x:test-url-2'], }, ], - }) + } - const tx = await Did.getStoreTxFromDidDocument( - newDid, + const tx = await Did.getStoreTxFromInput( + input, paymentAccount.address, key.storeDidCallback ) await submitTx(tx, paymentAccount) - const fullDidUri = Did.getFullDidUri(newDid.id) + const fullDidUri = Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase, + }) const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDidUri)) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) diff --git a/tests/integration/Web3Names.spec.ts b/tests/integration/Web3Names.spec.ts index 9e586154eb..25ffdf95b4 100644 --- a/tests/integration/Web3Names.spec.ts +++ b/tests/integration/Web3Names.spec.ts @@ -101,7 +101,7 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should be possible to lookup the nick with the given DID uri', async () => { const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.id)) const didInfo = Did.linkedInfoFromChain(encodedDidInfo) - expect(didInfo.document.alsoKnownAs).toBe([nick]) + expect(didInfo.document.alsoKnownAs).toBe([`w3n:${nick}`]) }, 30_000) it('should not be possible to create the same w3n twice', async () => { diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 556fb4db43..30c251c5e5 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -50,7 +50,7 @@ export function makeEncryptCallback({ return (didDocument) => { return async function encryptCallback({ data, peerPublicKey }) { const keyId = didDocument.keyAgreement?.[0] - if (keyId === undefined) { + if (!keyId) { throw new Error(`Encryption key not found in did "${didDocument.id}"`) } const verificationMethod = didDocument.verificationMethod?.find( From 3076a8a2b21cb0aa1b5e823be2b84ac56b2480cb Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 14:20:18 +0100 Subject: [PATCH 54/78] Minor refinements --- packages/did/src/Did.chain.ts | 36 +++++++++---------- packages/did/src/Did.signature.ts | 3 +- packages/did/src/Did.utils.ts | 15 ++++---- .../did/src/DidDetails/FullDidDetails.spec.ts | 8 ++--- packages/did/src/DidDetails/FullDidDetails.ts | 10 +++--- .../did/src/DidLinks/AccountLinks.chain.ts | 2 +- packages/legacy-credentials/src/Credential.ts | 4 +-- packages/types/src/DidDocument.ts | 5 ++- packages/vc-export/src/documentLoader.ts | 13 ++++--- 9 files changed, 48 insertions(+), 48 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 549a8f2e1e..6cf9a57bf3 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -214,24 +214,6 @@ export function documentFromChain( return didRecord } -export function publicKeyToChain( - key: NewDidVerificationKey -): EncodedVerificationKey -export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey - -/** - * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. - * - * @param key Object describing data associated with a public key. - * @returns Data restructured to allow SCALE encoding by polkadot api. - */ -export function publicKeyToChain( - key: NewDidVerificationKey | NewDidEncryptionKey -): EncodedDidKey { - // TypeScript can't infer type here, so we have to add a type assertion. - return { [key.type]: key.publicKey } as EncodedDidKey -} - function isUri(str: string): boolean { try { const url = new URL(str) // this actually accepts any URI but throws if it can't be parsed @@ -326,6 +308,24 @@ export type AuthorizeCallInput = { blockNumber?: AnyNumber } +export function publicKeyToChain( + key: NewDidVerificationKey +): EncodedVerificationKey +export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey + +/** + * Transforms a DID public key record to an enum-type key-value pair required in many key-related extrinsics. + * + * @param key Object describing data associated with a public key. + * @returns Data restructured to allow SCALE encoding by polkadot api. + */ +export function publicKeyToChain( + key: NewDidVerificationKey | NewDidEncryptionKey +): EncodedDidKey { + // TypeScript can't infer type here, so we have to add a type assertion. + return { [key.type]: key.publicKey } as EncodedDidKey +} + interface GetStoreTxInput { authentication: [NewDidVerificationKey] assertionMethod?: [NewDidVerificationKey] diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index d6d12b3f8f..430d54a583 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -5,6 +5,8 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { isHex } from '@polkadot/util' + import type { DereferenceDidUrl, DidDocument, @@ -15,7 +17,6 @@ import type { } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { isHex } from '@polkadot/util' import { multibaseKeyToDidKey, parse, validateIdentifier } from './Did.utils.js' import { dereference } from './DidResolver/DidResolver.js' diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index aaa5cf7bf5..5293fbc0d8 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -5,6 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import type { DidUri, DidUrl, @@ -13,10 +14,8 @@ import type { UriFragment, VerificationMethod, } from '@kiltprotocol/types' - -import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' -import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' import type { DidVerificationMethodType } from './DidDetails/DidDetails.js' @@ -272,11 +271,11 @@ export function isSameSubject(didA: DidUri, didB: DidUri): boolean { } /** - * Checks that a string (or other input) is a valid KILT DID uri with or without a URI fragment. + * Checks that a string (or other input) is a valid KILT DID with or without a trailing fragment. * Throws otherwise. * * @param input Arbitrary input. - * @param expectType `ResourceUri` if the URI is expected to have a fragment (following '#'), `Did` if it is expected not to have one. Default allows both. + * @param expectType `Uri` if the URI is expected to have a fragment (following '#'), `Url` if it is expected not to have one. Default allows both. */ export function validateIdentifier( input: unknown, @@ -285,16 +284,16 @@ export function validateIdentifier( if (typeof input !== 'string') { throw new TypeError(`DID string expected, got ${typeof input}`) } - const { address, queryParameters, fragment } = parse(input as DidUri) + const { address, fragment } = parse(input as DidUri) if ( - (fragment || queryParameters) && + fragment && (expectType === 'Uri' || // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. (typeof expectType === 'boolean' && expectType === false)) ) { throw new SDKErrors.DidError( - 'Expected a Kilt DidUri but got a DidUrl (containing a #fragment or a ?query_parameter)' + 'Expected a Kilt DidUri but got a DidUrl (containing a #fragment)' ) } diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index f108e36887..10e39ebdc1 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -5,6 +5,10 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { BN } from '@polkadot/util' +import { randomAsHex } from '@polkadot/util-crypto' + +import { ConfigService } from '@kiltprotocol/config' import type { DidDocument, KiltKeyringPair, @@ -12,10 +16,6 @@ import type { SubmittableExtrinsic, } from '@kiltprotocol/types' -import { BN } from '@polkadot/util' -import { randomAsHex } from '@polkadot/util-crypto' -import { ConfigService } from '@kiltprotocol/config' - import { ApiMocks, createLocalDemoFullDidFromKeypair, diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index d9ed59e84e..c4243744b8 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -7,6 +7,8 @@ import type { Extrinsic } from '@polkadot/types/interfaces' import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' +import { BN } from '@polkadot/util' + import type { DidUri, KiltAddress, @@ -15,17 +17,15 @@ import type { SubmittableExtrinsic, } from '@kiltprotocol/types' -import { BN } from '@polkadot/util' - -import { ConfigService } from '@kiltprotocol/config' import { SDKErrors } from '@kiltprotocol/utils' +import { ConfigService } from '@kiltprotocol/config' -import { parse } from '../Did.utils.js' import { documentFromChain, generateDidAuthenticatedTx, toChain, } from '../Did.chain.js' +import { parse } from '../Did.utils.js' // Must be in sync with what's implemented in impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for Call // in https://github.com/KILTprotocol/mashnet-node/blob/develop/runtimes/spiritnet/src/lib.rs @@ -165,7 +165,7 @@ function groupExtrinsicsByVerificationRelationship( ): GroupedExtrinsics { const [first, ...rest] = extrinsics.map((extrinsic) => { const verificationRelationship = getVerificationRelationshipForTx(extrinsic) - if (verificationRelationship === undefined) { + if (!verificationRelationship) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 38aefe164f..fc903f64f9 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -5,6 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' import type { TypeDef } from '@polkadot/types/types' import type { KeypairType } from '@polkadot/util-crypto/types' import type { ApiPromise } from '@polkadot/api' @@ -16,7 +17,6 @@ import type { KiltAddress, } from '@kiltprotocol/types' -import { decodeAddress, signatureVerify } from '@polkadot/util-crypto' import { stringToU8a, U8A_WRAP_ETHEREUM, diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index d23244e9cb..753e233d8a 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -550,9 +550,7 @@ export async function createPresentation({ return { ...presentation, claimerSignature: { - ...signatureToJson({ - ...signature, - }), + ...signatureToJson(signature), ...(challenge && { challenge }), }, } diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts index 310aa8d939..8210f16ebb 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/DidDocument.ts @@ -26,7 +26,10 @@ export type UriFragment = `#${string}` /** * URL for DID resources like keys or services. */ -export type DidUrl = `${DidUri}${UriFragment}` +export type DidUrl = + | `${DidUri}${UriFragment}` + // Very broad type definition, mostly for the compiler. Actual regex matching for query params is done where needed. + | `${DidUri}?{string}${UriFragment}` export type SignatureVerificationRelationship = | 'authentication' diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index 88542117f0..afe9b03fd5 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -8,13 +8,6 @@ // @ts-expect-error not a typescript module import jsonld from 'jsonld' // cjs module -import type { - DidDocument, - DidUri, - ICType, - VerificationMethod, -} from '@kiltprotocol/types' - import { DID_CONTEXTS, KILT_DID_CONTEXT_URL, @@ -23,6 +16,12 @@ import { W3C_DID_CONTEXT_URL, isFailedDereferenceMetadata, } from '@kiltprotocol/did' +import type { + DidDocument, + DidUri, + ICType, + VerificationMethod, +} from '@kiltprotocol/types' import { CType } from '@kiltprotocol/core' import { validationContexts } from './context/index.js' From 82c5db9138de64eeff846b531d17d935d5d4fa6f Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 14:36:09 +0100 Subject: [PATCH 55/78] Minor fixes --- packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index 532eed0d60..3af326646c 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -59,7 +59,7 @@ import { makeFakeDid } from './Sr25519Signature2020.spec' jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - resolve: jest.fn(), + dereference: jest.fn(), authorizeTx: jest.fn(), })) From 79a75c1c45f69673cf3f8aca60f549c629bf0d16 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 14:49:01 +0100 Subject: [PATCH 56/78] Apply suggestion --- packages/did/src/DidResolver/DidResolver.ts | 41 ++++++++++++--------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 0e5ca7ae69..15b17a9560 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -170,7 +170,29 @@ export async function resolveRepresentation( accept: DID_JSON, } ): Promise> { - if (!isValidContentType(accept)) { + const inputTransform = (() => { + switch (accept) { + case 'application/did+json': { + return (didDoc: DidDocument) => JSON.stringify(didDoc) + } + case 'application/did+ld+json': { + return (didDoc: DidDocument) => { + const jsonLdDoc: JsonLd = { + ...didDoc, + '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + } + return JSON.stringify(jsonLdDoc) + } + } + case 'application/did+cbor': { + return (didDoc: DidDocument) => cbor.encode(didDoc) + } + default: { + return null + } + } + })() + if (inputTransform === null) { return { didResolutionMetadata: { error: 'representationNotSupported', @@ -190,25 +212,10 @@ export async function resolveRepresentation( } as RepresentationResolutionResult } - const bufferInput = (() => { - if (accept === 'application/did+json') { - return JSON.stringify(didDocument) - } - if (accept === 'application/did+ld+json') { - const jsonLdDoc: JsonLd = { - ...didDocument, - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], - } - return JSON.stringify(jsonLdDoc) - } - // contentType === 'application/did+cbor - return cbor.encode(didDocument) - })() - return { didDocumentMetadata, didResolutionMetadata, - didDocumentStream: Buffer.from(bufferInput), + didDocumentStream: Buffer.from(inputTransform(didDocument)), } as RepresentationResolutionResult } From 303abc9ac123bf964fa861bbd368ec0a52421acd Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 14:51:37 +0100 Subject: [PATCH 57/78] Push different yarn.lock --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5719189fb0..37fd734bf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9227,11 +9227,11 @@ typescript@^4.8.3: "typescript@patch:typescript@^4.8.3#~builtin": version: 4.8.3 - resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=3b564f" + resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=aae4e6" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: dfe2ee3b9da1d74b9e06784ae90c20c435db9ad6ab23172911f6cdbfd7ab7213ae3611c4254c5a2c6dc2e89f05a658b95493890bf62d218267033b3d8a2e4dd6 + checksum: 2222d2382fb3146089b1d27ce2b55e9d1f99cc64118f1aba75809b693b856c5d3c324f052f60c75b577947fc538bc1c27bad0eb76cbdba9a63a253489504ba7e languageName: node linkType: hard From 94c3bb513b7e257067850918c2bb2d2ad8005c36 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 15:18:14 +0100 Subject: [PATCH 58/78] Fix type import --- packages/did/src/Did.signature.spec.ts | 2 +- packages/did/src/Did.signature.ts | 6 +----- packages/legacy-credentials/src/Credential.spec.ts | 3 ++- packages/types/src/Attestation.ts | 2 +- packages/types/src/Claim.ts | 2 +- packages/types/src/Credential.ts | 3 +-- packages/types/src/CryptoCallbacks.ts | 2 +- packages/types/src/Delegation.ts | 2 +- packages/types/src/{DidDocument.ts => Did.ts} | 5 +++++ packages/types/src/DidResolver.ts | 2 +- packages/types/src/PublicCredential.ts | 2 +- packages/types/src/index.ts | 2 +- 12 files changed, 17 insertions(+), 16 deletions(-) rename packages/types/src/{DidDocument.ts => Did.ts} (97%) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 00aab5914d..1b2a4ccc9d 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -11,13 +11,13 @@ import type { DidDocument, SignCallback, DidUrl, + DidSignature, DereferenceResult, } from '@kiltprotocol/types' import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' -import type { DidSignature } from './Did.signature' import type { NewLightDidVerificationKey } from './DidDetails' import { makeSigningKeyTool } from '../../../tests/testUtils' diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 430d54a583..abdfdc4d73 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -10,6 +10,7 @@ import { isHex } from '@polkadot/util' import type { DereferenceDidUrl, DidDocument, + DidSignature, DidUri, DidUrl, SignatureVerificationRelationship, @@ -31,11 +32,6 @@ export type DidSignatureVerificationInput = { dereferenceDidUrl?: DereferenceDidUrl['dereference'] } -export type DidSignature = { - signerUrl: DidUrl - signature: string -} - // Used solely for retro-compatibility with previously-generated DID signatures. // It is reasonable to think that it will be removed at some point in the future. type OldDidSignatureV1 = { diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 266ec12433..3456988226 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -15,6 +15,7 @@ import * as Did from '@kiltprotocol/did' import type { DereferenceResult, DidDocument, + DidSignature, DidUri, DidUrl, IAttestation, @@ -231,7 +232,7 @@ describe('Credential', () => { } as ICredentialPresentation builtCredentialMalformedSignature.claimerSignature = { signature: Crypto.hashStr('aaa'), - } as Did.DidSignature + } as DidSignature builtCredentialMalformedSignature.rootHash = Credential.calculateRootHash( builtCredentialMalformedSignature ) diff --git a/packages/types/src/Attestation.ts b/packages/types/src/Attestation.ts index 0f42f89749..e39fd72503 100644 --- a/packages/types/src/Attestation.ts +++ b/packages/types/src/Attestation.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from './DidDocument' +import type { DidUri } from './Did' import type { IDelegationNode } from './Delegation' import type { ICredential } from './Credential' import type { CTypeHash } from './CType' diff --git a/packages/types/src/Claim.ts b/packages/types/src/Claim.ts index 5e8691644b..3ebbd808da 100644 --- a/packages/types/src/Claim.ts +++ b/packages/types/src/Claim.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { DidUri } from './DidDocument' +import type { DidUri } from './Did' type ClaimPrimitives = string | number | boolean diff --git a/packages/types/src/Credential.ts b/packages/types/src/Credential.ts index 006b6099b8..cd63793efb 100644 --- a/packages/types/src/Credential.ts +++ b/packages/types/src/Credential.ts @@ -5,10 +5,9 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidSignature } from '@kiltprotocol/did' - import type { IClaim } from './Claim' import type { IDelegationNode } from './Delegation' +import type { DidSignature } from './Did' import type { HexString } from './Imported' export type Hash = HexString diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index 8ed89cb866..eb25667394 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -9,7 +9,7 @@ import type { DidUri, SignatureVerificationRelationship, VerificationMethod, -} from './DidDocument.js' +} from './Did' /** * Base interface for all signing requests. diff --git a/packages/types/src/Delegation.ts b/packages/types/src/Delegation.ts index 7049e33618..1bc057e5de 100644 --- a/packages/types/src/Delegation.ts +++ b/packages/types/src/Delegation.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { DidUri } from './DidDocument' +import type { DidUri } from './Did' /* eslint-disable no-bitwise */ export const Permission = { diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/Did.ts similarity index 97% rename from packages/types/src/DidDocument.ts rename to packages/types/src/Did.ts index 8210f16ebb..32329ffb17 100644 --- a/packages/types/src/DidDocument.ts +++ b/packages/types/src/Did.ts @@ -41,6 +41,11 @@ export type VerificationRelationship = | SignatureVerificationRelationship | EncryptionRelationship +export type DidSignature = { + signerUrl: DidUrl + signature: string +} + type Base58BtcMultibaseString = `z${string}` /** diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index 1bd0d4de54..339dc590a0 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -12,7 +12,7 @@ import type { VerificationMethod, Service, JsonLd, -} from './DidDocument' +} from './Did' /** * The `accept` header must not be used for the regular `resolve` function, so we enforce that statically. diff --git a/packages/types/src/PublicCredential.ts b/packages/types/src/PublicCredential.ts index 0b3ba75dbd..b9aca0f014 100644 --- a/packages/types/src/PublicCredential.ts +++ b/packages/types/src/PublicCredential.ts @@ -9,7 +9,7 @@ import type { HexString, BN } from './Imported' import type { CTypeHash } from './CType' import type { IDelegationNode } from './Delegation' import type { IClaimContents } from './Claim' -import type { DidUri } from './DidDocument' +import type { DidUri } from './Did' import type { AssetDidUri } from './AssetDid' /* diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0e5db8b307..83fde92b10 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -21,7 +21,7 @@ export * from './Deposit.js' export * from './Delegation.js' export * from './Address.js' export * from './Credential.js' -export * from './DidDocument.js' +export * from './Did.js' export * from './CryptoCallbacks.js' export * from './DidResolver.js' export * from './PublicCredential.js' From e015e03d0017eae9295e0e6f31e2ba3f2c08dbcc Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 15:30:31 +0100 Subject: [PATCH 59/78] Fix integration test --- tests/integration/AccountLinking.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/AccountLinking.spec.ts b/tests/integration/AccountLinking.spec.ts index 967f263135..61badec164 100644 --- a/tests/integration/AccountLinking.spec.ts +++ b/tests/integration/AccountLinking.spec.ts @@ -352,7 +352,9 @@ describe('When there is an on-chain DID', () => { Did.accountToChain(genericAccount.address) ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) - expect(queryByAccount.document.alsoKnownAs).toStrictEqual(['test-name']) + expect(queryByAccount.document.alsoKnownAs).toStrictEqual([ + 'w3n:test-name', + ]) }) it('should be possible for the sender to remove the link', async () => { From a8c5f51f3ec721b29420c3a9f467e60df4e972cb Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 16 Oct 2023 15:47:29 +0100 Subject: [PATCH 60/78] Fix integration test pt.2 --- tests/integration/Web3Names.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/Web3Names.spec.ts b/tests/integration/Web3Names.spec.ts index 25ffdf95b4..0cfc576c78 100644 --- a/tests/integration/Web3Names.spec.ts +++ b/tests/integration/Web3Names.spec.ts @@ -101,7 +101,7 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should be possible to lookup the nick with the given DID uri', async () => { const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.id)) const didInfo = Did.linkedInfoFromChain(encodedDidInfo) - expect(didInfo.document.alsoKnownAs).toBe([`w3n:${nick}`]) + expect(didInfo.document.alsoKnownAs).toStrictEqual([`w3n:${nick}`]) }, 30_000) it('should not be possible to create the same w3n twice', async () => { From 0f267dbb162656a81686d9bfe03c228c250059aa Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 17 Oct 2023 09:25:03 +0100 Subject: [PATCH 61/78] Update unit tests for DidResolver.spec.ts --- packages/did/src/Did.signature.ts | 4 +- .../did/src/DidResolver/DidResolver.spec.ts | 647 ++++++++++++++++ .../did/src/DidResolver/_DidResolver.spec.ts | 712 ------------------ packages/legacy-credentials/src/Credential.ts | 8 +- packages/types/src/DidResolver.ts | 40 +- yarn.lock | 4 +- 6 files changed, 676 insertions(+), 739 deletions(-) create mode 100644 packages/did/src/DidResolver/DidResolver.spec.ts delete mode 100644 packages/did/src/DidResolver/_DidResolver.spec.ts diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index abdfdc4d73..7191c17916 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -29,7 +29,7 @@ export type DidSignatureVerificationInput = { expectedSigner?: DidUri allowUpgraded?: boolean expectedVerificationRelationship?: SignatureVerificationRelationship - dereferenceDidUrl?: DereferenceDidUrl['dereference'] + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } // Used solely for retro-compatibility with previously-generated DID signatures. @@ -83,7 +83,7 @@ export async function verifyDidSignature({ expectedSigner, allowUpgraded = false, expectedVerificationRelationship, - dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: DidSignatureVerificationInput): Promise { // checks if signer URL points to the right did; alternatively we could check the verification method's controller const signer = parse(signerUrl) diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts new file mode 100644 index 0000000000..743353a19f --- /dev/null +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -0,0 +1,647 @@ +/** + * Copyright (c) 2018-2023, BOTLabs GmbH. + * + * This source code is licensed under the BSD 4-Clause "Original" license + * found in the LICENSE file in the root directory of this source tree. + */ + +import { ConfigService } from '@kiltprotocol/config' +import { + DereferenceResult, + DidUri, + DidUrl, + KiltAddress, + ResolutionResult, + Service, + UriFragment, + VerificationMethod, +} from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' + +import { ApiMocks, makeSigningKeyTool } from '../../../../tests/testUtils' +import { linkedInfoFromChain } from '../Did.rpc.js' + +import * as Did from '../index.js' + +const addressWithAuthenticationKey = + '4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' +const didWithAuthenticationKey: DidUri = `did:kilt:${addressWithAuthenticationKey}` +const addressWithAllKeys = `4sDxAgw86PFvC6TQbvZzo19WoYF6T4HcLd2i9wzvojkLXLvp` +const didWithAllKeys: DidUri = `did:kilt:${addressWithAllKeys}` +const addressWithServiceEndpoints = `4q4DHavMdesaSMH3g32xH3fhxYPt5pmoP9oSwgTr73dQLrkN` +const didWithServiceEndpoints: DidUri = `did:kilt:${addressWithServiceEndpoints}` +const deletedAddress = '4rrVTLAXgeoE8jo8si571HnqHtd5WmvLuzfH6e1xBsVXsRo7' +const deletedDid: DidUri = `did:kilt:${deletedAddress}` + +const didIsBlacklisted = ApiMocks.mockChainQueryReturn( + 'did', + 'didBlacklist', + 'true' +) + +const augmentedApi = ApiMocks.createAugmentedApi() + +let mockedApi: any +beforeAll(() => { + mockedApi = ApiMocks.getMockedApi() + ConfigService.set({ api: mockedApi }) + + // Mock `api.call.did.query(didUri)` + // By default it returns a simple LinkedDidInfo with no web3name and no accounts linked. + jest + .spyOn(mockedApi.call.did, 'query') + .mockImplementation(async (identifier) => { + return augmentedApi.createType('Option', { + identifier, + accounts: [], + w3n: null, + serviceEndpoints: [ + { + id: 'foo', + serviceTypes: ['type-service-1'], + urls: ['x:url-service-1'], + }, + ], + details: { + authenticationKey: '01234567890123456789012345678901', + keyAgreementKeys: [], + delegationKey: null, + attestationKey: null, + publicKeys: [], + lastTxCounter: 123, + deposit: { + owner: addressWithAuthenticationKey, + amount: 0, + }, + }, + }) + }) +}) + +function generateAuthenticationVerificationMethod( + controller: DidUri +): VerificationMethod { + return { + id: '#auth', + controller, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + } +} + +function generateEncryptionVerificationMethod( + controller: DidUri +): VerificationMethod { + return { + id: '#enc', + controller, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(1), + type: 'x25519', + }), + } +} + +function generateAssertionVerificationMethod( + controller: DidUri +): VerificationMethod { + return { + id: '#att', + controller, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(2), + type: 'sr25519', + }), + } +} + +function generateCapabilityDelegationVerificationMethod( + controller: DidUri +): VerificationMethod { + return { + id: '#del', + controller, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(33).fill(3), + type: 'ecdsa', + }), + } +} + +function generateServiceEndpoint(serviceId: UriFragment): Service { + const fragment = serviceId.substring(1) + return { + id: serviceId, + type: [`type-${fragment}`], + serviceEndpoint: [`x:url-${fragment}`], + } +} + +jest.mock('../Did.rpc.js') +// By default its mock returns a DIDDocument with the test authentication key, test service, and the URI derived from the identifier provided in the resolution. +jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + service: [generateServiceEndpoint('#service-1')], + }, + } +}) + +describe('When dereferencing a verification method', () => { + it('correctly dereference it for a full DID if both the DID and the verification method exist', async () => { + const fullDid = didWithAuthenticationKey + const verificationMethodUrl: DidUrl = `${fullDid}#auth` + + expect( + await Did.dereference(verificationMethodUrl, { + accept: 'application/did+json', + }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: generateAuthenticationVerificationMethod(fullDid), + }) + }) + + it('returns error if either the DID or the verification method do not exist', async () => { + let verificationMethodUrl: DidUrl = `${deletedDid}#enc` + + expect( + await Did.dereference(verificationMethodUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) + + const didWithNoEncryptionKey = didWithAuthenticationKey + verificationMethodUrl = `${didWithNoEncryptionKey}#enc` + + expect( + await Did.dereference(verificationMethodUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) + }) + + it('throws for invalid URLs', async () => { + const invalidUrl = 'invalid-url' as DidUrl + expect(await Did.dereference(invalidUrl)).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'invalidDidUrl' }, + }) + }) +}) + +describe('When resolving a service', () => { + it('correctly resolves it for a full DID if both the DID and the endpoint exist', async () => { + const fullDid = didWithServiceEndpoints + const serviceIdUrl: DidUrl = `${fullDid}#service-1` + + expect( + await Did.dereference(serviceIdUrl, { accept: 'application/did+json' }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: { + id: '#service-1', + type: [`type-service-1`], + serviceEndpoint: [`x:url-service-1`], + }, + }) + }) + + it('returns error if either the DID or the service do not exist', async () => { + // Mock transform function changed to not return any services (twice). + jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + }, + } + }) + jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + }, + } + }) + + let serviceIdUrl: DidUrl = `${deletedDid}#service-1` + + expect( + await Did.dereference(serviceIdUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) + + const didWithNoServiceEndpoints = didWithAuthenticationKey + serviceIdUrl = `${didWithNoServiceEndpoints}#service-1` + + expect( + await Did.dereference(serviceIdUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) + }) +}) + +describe('When resolving a full DID', () => { + it('correctly resolves the document with an authentication verification method', async () => { + const fullDidWithAuthenticationKey = didWithAuthenticationKey + expect( + await Did.resolve(fullDidWithAuthenticationKey) + ).toMatchObject({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: fullDidWithAuthenticationKey, + authentication: ['#auth'], + verificationMethod: [ + { + controller: fullDidWithAuthenticationKey, + id: '#auth', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: new Uint8Array(32).fill(0), + }), + }, + ], + }, + }) + }) + + it('correctly resolves the document with all keys', async () => { + // Mock transform function changed to return all keys for the DIDDocument. + jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + const encMethod = generateEncryptionVerificationMethod(did) + const attMethod = generateAssertionVerificationMethod(did) + const delMethod = generateCapabilityDelegationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + keyAgreement: [encMethod.id], + assertionMethod: [attMethod.id], + capabilityDelegation: [delMethod.id], + verificationMethod: [authMethod, encMethod, attMethod, delMethod], + }, + } + }) + const fullDidWithAllKeys = didWithAllKeys + expect( + await Did.resolve(fullDidWithAllKeys) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: fullDidWithAllKeys, + authentication: ['#auth'], + keyAgreement: ['#enc'], + assertionMethod: ['#att'], + capabilityDelegation: ['#del'], + verificationMethod: [ + { + controller: fullDidWithAllKeys, + id: '#auth', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: new Uint8Array(32).fill(0), + }), + }, + { + controller: fullDidWithAllKeys, + id: '#enc', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(1), + }), + }, + { + controller: fullDidWithAllKeys, + id: '#att', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: new Uint8Array(32).fill(2), + }), + }, + { + controller: fullDidWithAllKeys, + id: '#del', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ecdsa', + publicKey: new Uint8Array(33).fill(3), + }), + }, + ], + }, + }) + }) + + it('correctly resolves the document with services', async () => { + // Mock transform function changed to return two services. + jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + service: [ + generateServiceEndpoint('#id-1'), + generateServiceEndpoint('#id-2'), + ], + }, + } + }) + const fullDidWithServiceEndpoints = didWithServiceEndpoints + expect( + await Did.resolve(fullDidWithServiceEndpoints) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: fullDidWithServiceEndpoints, + authentication: ['#auth'], + verificationMethod: [ + { + controller: fullDidWithServiceEndpoints, + id: '#auth', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: new Uint8Array(32).fill(0), + }), + }, + ], + service: [ + { + id: '#id-1', + type: ['type-id-1'], + serviceEndpoint: ['x:url-id-1'], + }, + { + id: '#id-2', + type: ['type-id-2'], + serviceEndpoint: ['x:url-id-2'], + }, + ], + }, + }) + }) + + it('correctly resolves the document with web3Name', async () => { + // Mock transform function changed to return two services. + jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + alsoKnownAs: ['w3n:w3nick'], + }, + } + }) + expect( + await Did.resolve(didWithAuthenticationKey) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: didWithAuthenticationKey, + authentication: ['#auth'], + verificationMethod: [ + { + controller: didWithAuthenticationKey, + id: '#auth', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: new Uint8Array(32).fill(0), + }), + }, + ], + alsoKnownAs: ['w3n:w3nick'], + }, + }) + }) + + it('correctly resolves a non-existing DID', async () => { + // RPC call changed to not return anything. + jest + .spyOn(mockedApi.call.did, 'query') + .mockResolvedValueOnce( + augmentedApi.createType('Option', null) + ) + const randomKeypair = makeSigningKeyTool().authentication[0] + const randomDid = Did.getFullDidUriFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(randomKeypair), + }) + expect(await Did.resolve(randomDid)).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'notFound' }, + }) + }) + + it('correctly resolves a deleted DID', async () => { + // RPC call changed to not return anything. + jest + .spyOn(mockedApi.call.did, 'query') + .mockResolvedValueOnce( + augmentedApi.createType('Option', null) + ) + mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) + + expect(await Did.resolve(deletedDid)).toStrictEqual({ + didDocumentMetadata: { deactivated: true }, + didResolutionMetadata: {}, + didDocument: { id: deletedDid }, + }) + }) +}) + +describe('When resolving a light DID', () => { + const authKey = Crypto.makeKeypairFromSeed() + const encryptionKey = Crypto.makeEncryptionKeypairFromSeed() + + beforeEach(() => { + // RPC call changed to not return anything by default. + jest + .spyOn(mockedApi.call.did, 'query') + .mockResolvedValue( + augmentedApi.createType('Option', null) + ) + }) + + it('correctly resolves the document with an authentication key', async () => { + const lightDidWithAuthenticationKey = Did.createLightDidDocument({ + authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], + }) + expect( + await Did.resolve(lightDidWithAuthenticationKey.id) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: lightDidWithAuthenticationKey.id, + authentication: ['#authentication'], + verificationMethod: [ + { + controller: lightDidWithAuthenticationKey.id, + id: '#authentication', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + ...authKey, + type: 'sr25519', + }), + }, + ], + service: undefined, + }, + }) + }) + + it('correctly resolves the document with authentication key, encryption key, and two services', async () => { + const lightDid = Did.createLightDidDocument({ + authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], + keyAgreement: [{ publicKey: encryptionKey.publicKey, type: 'x25519' }], + service: [ + generateServiceEndpoint('#service-1'), + generateServiceEndpoint('#service-2'), + ], + }) + expect(await Did.resolve(lightDid.id)).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: lightDid.id, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ + { + controller: lightDid.id, + id: '#authentication', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + ...authKey, + type: 'sr25519', + }), + }, + { + controller: lightDid.id, + id: '#encryption', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey(encryptionKey), + }, + ], + service: [ + { + id: '#service-1', + type: ['type-service-1'], + serviceEndpoint: ['x:url-service-1'], + }, + { + id: '#service-2', + type: ['type-service-2'], + serviceEndpoint: ['x:url-service-2'], + }, + ], + }, + }) + }) + + it('correctly resolves a migrated and not deleted DID', async () => { + // RPC call changed to return something. + jest.spyOn(mockedApi.call.did, 'query').mockResolvedValueOnce( + augmentedApi.createType('Option', { + addressWithAuthenticationKey, + accounts: [], + w3n: null, + details: { + authenticationKey: '01234567890123456789012345678901', + keyAgreementKeys: [], + delegationKey: null, + attestationKey: null, + publicKeys: [], + lastTxCounter: 123, + deposit: { + owner: addressWithAuthenticationKey, + amount: 0, + }, + }, + }) + ) + const migratedDid: DidUri = `did:kilt:light:00${addressWithAuthenticationKey}` + expect(await Did.resolve(migratedDid)).toStrictEqual({ + didDocumentMetadata: { canonicalId: didWithAuthenticationKey }, + didResolutionMetadata: {}, + didDocument: { + id: migratedDid, + }, + }) + }) + + it('correctly resolves a migrated and deleted DID', async () => { + // Mock the resolved DID as deleted. + mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) + + const migratedDid: DidUri = `did:kilt:light:00${deletedAddress}` + expect(await Did.resolve(migratedDid)).toStrictEqual({ + didDocumentMetadata: { deactivated: true }, + didResolutionMetadata: {}, + didDocument: { + id: migratedDid, + }, + }) + }) +}) diff --git a/packages/did/src/DidResolver/_DidResolver.spec.ts b/packages/did/src/DidResolver/_DidResolver.spec.ts deleted file mode 100644 index 88cdf9d417..0000000000 --- a/packages/did/src/DidResolver/_DidResolver.spec.ts +++ /dev/null @@ -1,712 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { BN } from '@polkadot/util' -import { base58Encode } from '@polkadot/util-crypto' - -import { ConfigService } from '@kiltprotocol/config' -import type { - ConformingDidKey, - ConformingDidServiceEndpoint, - DidEncryptionKey, - DidKey, - DidResolutionDocumentMetadata, - DidResolutionMetadata, - DidResolutionResult, - DidResourceUri, - DidServiceEndpoint, - DidUri, - DidVerificationKey, - KiltAddress, - ResolvedDidKey, - ResolvedDidServiceEndpoint, - UriFragment, -} from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' - -import { ApiMocks, makeSigningKeyTool } from '../../../../tests/testUtils' -import { linkedInfoFromChain } from '../Did.rpc.js' -import { getFullDidUriFromKey } from '../Did.utils' - -import * as Did from '../index.js' -import { - resolve, - resolveCompliant, - resolveKey, - resolveService, -} from './index.js' - -const addressWithAuthenticationKey = - '4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' -const didWithAuthenticationKey: DidUri = `did:kilt:${addressWithAuthenticationKey}` -const addressWithAllKeys = `4sDxAgw86PFvC6TQbvZzo19WoYF6T4HcLd2i9wzvojkLXLvp` -const didWithAllKeys: DidUri = `did:kilt:${addressWithAllKeys}` -const addressWithServiceEndpoints = `4q4DHavMdesaSMH3g32xH3fhxYPt5pmoP9oSwgTr73dQLrkN` -const didWithServiceEndpoints: DidUri = `did:kilt:${addressWithServiceEndpoints}` -const deletedAddress = '4rrVTLAXgeoE8jo8si571HnqHtd5WmvLuzfH6e1xBsVXsRo7' -const deletedDid: DidUri = `did:kilt:${deletedAddress}` - -const didIsBlacklisted = ApiMocks.mockChainQueryReturn( - 'did', - 'didBlacklist', - 'true' -) - -const augmentedApi = ApiMocks.createAugmentedApi() - -let mockedApi: any -beforeAll(() => { - mockedApi = ApiMocks.getMockedApi() - ConfigService.set({ api: mockedApi }) - - // Mock `api.call.did.query(didUri)` - // By default it returns a simple LinkedDidInfo with no web3name and no accounts linked. - jest - .spyOn(mockedApi.call.did, 'query') - .mockImplementation(async (identifier) => { - return augmentedApi.createType('Option', { - identifier, - accounts: [], - w3n: null, - serviceEndpoints: [ - { - id: 'foo', - serviceTypes: ['type-service-1'], - urls: ['x:url-service-1'], - }, - ], - details: { - authenticationKey: '01234567890123456789012345678901', - keyAgreementKeys: [], - delegationKey: null, - attestationKey: null, - publicKeys: [], - lastTxCounter: 123, - deposit: { - owner: addressWithAuthenticationKey, - amount: 0, - }, - }, - }) - }) -}) - -function generateAuthenticationKey(): DidVerificationKey { - return { - id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), - } -} - -function generateEncryptionKey(): DidEncryptionKey { - return { - id: '#enc', - type: 'x25519', - publicKey: new Uint8Array(32).fill(1), - includedAt: new BN(15), - } -} - -function generateAttestationKey(): DidVerificationKey { - return { - id: '#att', - type: 'sr25519', - publicKey: new Uint8Array(32).fill(2), - includedAt: new BN(20), - } -} - -function generateDelegationKey(): DidVerificationKey { - return { - id: '#del', - type: 'ecdsa', - publicKey: new Uint8Array(32).fill(3), - includedAt: new BN(25), - } -} - -function generateServiceEndpoint(serviceId: UriFragment): DidServiceEndpoint { - const fragment = serviceId.substring(1) - return { - id: serviceId, - type: [`type-${fragment}`], - serviceEndpoint: [`x:url-${fragment}`], - } -} - -jest.mock('../Did.rpc.js') -// By default its mock returns a DIDDocument with the test authentication key, test service, and the URI derived from the identifier provided in the resolution. -jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - - return { - accounts: [], - document: { - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - authentication: [generateAuthenticationKey()], - service: [generateServiceEndpoint('#service-1')], - }, - } -}) - -describe('When resolving a key', () => { - it('correctly resolves it for a full DID if both the DID and the key exist', async () => { - const fullDid = didWithAuthenticationKey - const keyIdUri: DidResourceUri = `${fullDid}#auth` - - expect(await resolveKey(keyIdUri)).toStrictEqual({ - controller: fullDid, - publicKey: new Uint8Array(32).fill(0), - id: keyIdUri, - type: 'ed25519', - }) - }) - - it('returns null if either the DID or the key do not exist', async () => { - let keyIdUri: DidResourceUri = `${deletedDid}#enc` - - await expect(resolveKey(keyIdUri)).rejects.toThrow() - - const didWithNoEncryptionKey = didWithAuthenticationKey - keyIdUri = `${didWithNoEncryptionKey}#enc` - - await expect(resolveKey(keyIdUri)).rejects.toThrow() - }) - - it('throws for invalid URIs', async () => { - const uriWithoutFragment = deletedDid - await expect( - resolveKey(uriWithoutFragment as DidResourceUri) - ).rejects.toThrow() - - const invalidUri = 'invalid-uri' as DidResourceUri - await expect(resolveKey(invalidUri)).rejects.toThrow() - }) -}) - -describe('When resolving a service', () => { - it('correctly resolves it for a full DID if both the DID and the endpoint exist', async () => { - const fullDid = didWithServiceEndpoints - const serviceIdUri: DidResourceUri = `${fullDid}#service-1` - - expect( - await resolveService(serviceIdUri) - ).toStrictEqual({ - id: serviceIdUri, - type: [`type-service-1`], - serviceEndpoint: [`x:url-service-1`], - }) - }) - - it('returns null if either the DID or the service do not exist', async () => { - // Mock transform function changed to not return any services (twice). - jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - - return { - accounts: [], - document: { - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - authentication: [generateAuthenticationKey()], - }, - } - }) - jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - - return { - accounts: [], - document: { - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - authentication: [generateAuthenticationKey()], - }, - } - }) - - let serviceIdUri: DidResourceUri = `${deletedDid}#service-1` - - await expect(resolveService(serviceIdUri)).rejects.toThrow() - - const didWithNoServiceEndpoints = didWithAuthenticationKey - serviceIdUri = `${didWithNoServiceEndpoints}#service-1` - - await expect(resolveService(serviceIdUri)).rejects.toThrow() - }) - - it('throws for invalid URIs', async () => { - const uriWithoutFragment = deletedDid - await expect( - resolveService(uriWithoutFragment as DidResourceUri) - ).rejects.toThrow() - - const invalidUri = 'invalid-uri' as DidResourceUri - await expect(resolveService(invalidUri)).rejects.toThrow() - }) -}) - -describe('When resolving a full DID', () => { - it('correctly resolves the document with an authentication key', async () => { - const fullDidWithAuthenticationKey = didWithAuthenticationKey - const { document, metadata, web3Name } = (await resolve( - fullDidWithAuthenticationKey - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document.uri).toStrictEqual(fullDidWithAuthenticationKey) - expect(Did.getKeys(document)).toStrictEqual([ - { - id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), - }, - ]) - expect(web3Name).toBeUndefined() - }) - - it('correctly resolves the document with all keys', async () => { - // Mock transform function changed to return all keys for the DIDDocument. - jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - - return { - accounts: [], - document: { - authentication: [generateAuthenticationKey()], - keyAgreement: [generateEncryptionKey()], - assertionMethod: [generateAttestationKey()], - capabilityDelegation: [generateDelegationKey()], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - }, - } - }) - const fullDidWithAllKeys = didWithAllKeys - const { document, metadata } = (await resolve( - fullDidWithAllKeys - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document.uri).toStrictEqual(fullDidWithAllKeys) - expect(Did.getKeys(document)).toStrictEqual([ - { - id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), - }, - { - id: '#att', - type: 'sr25519', - publicKey: new Uint8Array(32).fill(2), - includedAt: new BN(20), - }, - { - id: '#del', - type: 'ecdsa', - publicKey: new Uint8Array(32).fill(3), - includedAt: new BN(25), - }, - { - id: '#enc', - type: 'x25519', - publicKey: new Uint8Array(32).fill(1), - includedAt: new BN(15), - }, - ]) - }) - - it('correctly resolves the document with services', async () => { - // Mock transform function changed to return two services. - jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - - return { - accounts: [], - document: { - authentication: [generateAuthenticationKey()], - service: [ - generateServiceEndpoint('#id-1'), - generateServiceEndpoint('#id-2'), - ], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - }, - } - }) - const fullDidWithServiceEndpoints = didWithServiceEndpoints - const { document, metadata } = (await resolve( - fullDidWithServiceEndpoints - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document.uri).toStrictEqual(fullDidWithServiceEndpoints) - expect(document.service).toStrictEqual([ - { - id: '#id-1', - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: '#id-2', - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], - }, - ]) - }) - - it('correctly resolves the document with web3Name', async () => { - // Mock transform function changed to return two services. - jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - - return { - accounts: [], - document: { - authentication: [generateAuthenticationKey()], - service: [], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - }, - web3Name: 'w3nick', - } - }) - const { document, metadata, web3Name } = (await resolve( - didWithAuthenticationKey - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document.uri).toStrictEqual(didWithAuthenticationKey) - expect(web3Name).toStrictEqual('w3nick') - }) - - it('correctly resolves a non-existing DID', async () => { - // RPC call changed to not return anything. - jest - .spyOn(mockedApi.call.did, 'query') - .mockResolvedValueOnce( - augmentedApi.createType('Option', null) - ) - const randomDid = getFullDidUriFromKey( - makeSigningKeyTool().authentication[0] - ) - expect(await resolve(randomDid)).toBeNull() - }) - - it('correctly resolves a deleted DID', async () => { - // RPC call changed to not return anything. - jest - .spyOn(mockedApi.call.did, 'query') - .mockResolvedValueOnce( - augmentedApi.createType('Option', null) - ) - mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) - - const { document, metadata } = (await resolve( - deletedDid - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: true, - }) - expect(document).toBeUndefined() - }) - - it('correctly resolves DID document given a fragment', async () => { - const fullDidWithAuthenticationKey = didWithAuthenticationKey - const keyIdUri: DidUri = `${fullDidWithAuthenticationKey}#auth` - const { document, metadata } = (await resolve( - keyIdUri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document?.uri).toStrictEqual(fullDidWithAuthenticationKey) - }) -}) - -describe('When resolving a light DID', () => { - const authKey = Crypto.makeKeypairFromSeed() - const encryptionKey = Crypto.makeEncryptionKeypairFromSeed() - - beforeEach(() => { - // RPC call changed to not return anything by default. - jest - .spyOn(mockedApi.call.did, 'query') - .mockResolvedValue( - augmentedApi.createType('Option', null) - ) - }) - - it('correctly resolves the document with an authentication key', async () => { - const lightDidWithAuthenticationKey = Did.createLightDidDocument({ - authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], - }) - const { document, metadata } = (await resolve( - lightDidWithAuthenticationKey.uri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document?.uri).toStrictEqual( - lightDidWithAuthenticationKey.uri - ) - expect(Did.getKeys(lightDidWithAuthenticationKey)).toStrictEqual([ - { - id: '#authentication', - type: 'sr25519', - publicKey: authKey.publicKey, - }, - ]) - }) - - it('correctly resolves the document with authentication key, encryption key, and two services', async () => { - const lightDid = Did.createLightDidDocument({ - authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], - keyAgreement: [{ publicKey: encryptionKey.publicKey, type: 'x25519' }], - service: [ - generateServiceEndpoint('#service-1'), - generateServiceEndpoint('#service-2'), - ], - }) - const { document, metadata } = (await resolve( - lightDid.uri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document?.uri).toStrictEqual(lightDid.uri) - expect(Did.getKeys(lightDid)).toStrictEqual([ - { - id: '#authentication', - type: 'sr25519', - publicKey: authKey.publicKey, - }, - { - id: '#encryption', - type: 'x25519', - publicKey: encryptionKey.publicKey, - }, - ]) - expect(lightDid.service).toStrictEqual([ - { - id: '#service-1', - type: ['type-service-1'], - serviceEndpoint: ['x:url-service-1'], - }, - { - id: '#service-2', - type: ['type-service-2'], - serviceEndpoint: ['x:url-service-2'], - }, - ]) - }) - - it('correctly resolves a migrated and not deleted DID', async () => { - // RPC call changed to return something. - jest.spyOn(mockedApi.call.did, 'query').mockResolvedValueOnce( - augmentedApi.createType('Option', { - addressWithAuthenticationKey, - accounts: [], - w3n: null, - details: { - authenticationKey: '01234567890123456789012345678901', - keyAgreementKeys: [], - delegationKey: null, - attestationKey: null, - publicKeys: [], - lastTxCounter: 123, - deposit: { - owner: addressWithAuthenticationKey, - amount: 0, - }, - }, - }) - ) - const migratedDid: DidUri = `did:kilt:light:00${addressWithAuthenticationKey}` - const { document, metadata } = (await resolve( - migratedDid - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - canonicalId: didWithAuthenticationKey, - }) - expect(document).toBe(undefined) - }) - - it('correctly resolves a migrated and deleted DID', async () => { - // Mock the resolved DID as deleted. - mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) - - const migratedDid: DidUri = `did:kilt:light:00${deletedAddress}` - const { document, metadata } = (await resolve( - migratedDid - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: true, - }) - expect(document).toBeUndefined() - }) - - it('correctly resolves DID document given a fragment', async () => { - const lightDid = Did.createLightDidDocument({ - authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], - }) - const keyIdUri: DidUri = `${lightDid.uri}#auth` - const { document, metadata } = (await resolve( - keyIdUri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document?.uri).toStrictEqual(lightDid.uri) - }) -}) - -describe('When resolving with the spec compliant resolver', () => { - beforeAll(() => { - jest - .spyOn(mockedApi.call.did, 'query') - .mockImplementation(async (identifier) => { - return augmentedApi.createType('Option', { - identifier, - }) - }) - // Mock transform function changed to return two services. - jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - - return { - accounts: [], - document: { - authentication: [generateAuthenticationKey()], - service: [ - generateServiceEndpoint('#id-1'), - generateServiceEndpoint('#id-2'), - ], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - }, - web3Name: 'w3nick', - } - }) - }) - - it('returns a spec-compliant DID document', async () => { - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(didWithAuthenticationKey) - if (didDocument === undefined) throw new Error('Document unresolved') - - expect(didDocumentMetadata).toStrictEqual({ - deactivated: false, - }) - - expect(didResolutionMetadata).toStrictEqual({}) - - expect(didDocument.id).toStrictEqual(didWithAuthenticationKey) - expect(didDocument.authentication).toStrictEqual([`${didDocument.id}#auth`]) - expect(didDocument.verificationMethod).toContainEqual({ - id: `${didWithAuthenticationKey}${'#auth'}`, - controller: didWithAuthenticationKey, - type: 'Ed25519VerificationKey2018', - publicKeyBase58: base58Encode(new Uint8Array(32).fill(0)), - }) - expect(didDocument.service).toStrictEqual([ - { - id: `${didWithAuthenticationKey}#id-1`, - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: `${didWithAuthenticationKey}#id-2`, - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], - }, - ]) - expect(didDocument).toHaveProperty('alsoKnownAs', ['w3n:w3nick']) - }) - - it('correctly resolves a non-existing DID', async () => { - // RPC call changed to not return anything. - jest - .spyOn(mockedApi.call.did, 'query') - .mockResolvedValueOnce( - augmentedApi.createType('Option', null) - ) - const randomDid = getFullDidUriFromKey( - makeSigningKeyTool().authentication[0] - ) - - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(randomDid) - - expect(didDocumentMetadata).toStrictEqual({}) - expect(didResolutionMetadata).toHaveProperty('error', 'notFound') - expect(didDocument).toBeUndefined() - }) - - it('correctly resolves a deleted DID', async () => { - // RPC call changed to not return anything. - jest - .spyOn(mockedApi.call.did, 'query') - .mockResolvedValueOnce( - augmentedApi.createType('Option', null) - ) - mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) - - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(deletedDid) - - expect(didDocumentMetadata).toStrictEqual({ - deactivated: true, - }) - expect(didResolutionMetadata).toStrictEqual({}) - expect(didDocument).toStrictEqual({ id: deletedDid }) - }) - - it('correctly resolves an upgraded light DID', async () => { - const key = makeSigningKeyTool().authentication[0] - const lightDid = Did.createLightDidDocument({ authentication: [key] }).uri - const fullDid = getFullDidUriFromKey(key) - - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(lightDid) - - expect(didDocumentMetadata).toStrictEqual({ - deactivated: false, - canonicalId: fullDid, - }) - expect(didResolutionMetadata).toStrictEqual({}) - expect(didDocument).toStrictEqual({ id: lightDid }) - }) - - it('does not dereference a DID URL (with fragment)', async () => { - const fullDidWithAuthenticationKey = didWithAuthenticationKey - const keyIdUri: DidUri = `${fullDidWithAuthenticationKey}#auth` - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(keyIdUri) - - expect(didDocumentMetadata).toStrictEqual({}) - expect(didResolutionMetadata).toHaveProperty< - DidResolutionMetadata['error'] - >('error', 'invalidDid') - expect(didDocument).toBeUndefined() - }) -}) diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 753e233d8a..18f3afc199 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -212,10 +212,10 @@ export async function verifySignature( input: ICredentialPresentation, { challenge, - dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: { challenge?: string - dereferenceDidUrl?: DereferenceDidUrl['dereference'] + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } = {} ): Promise { const { claimerSignature } = input @@ -280,7 +280,7 @@ export function fromClaim( type VerifyOptions = { ctype?: ICType challenge?: string - dereferenceDidUrl?: DereferenceDidUrl['dereference'] + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } /** @@ -442,7 +442,7 @@ export async function verifyPresentation( { ctype, challenge, - dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: VerifyOptions = {} ): Promise { await verifySignature(presentation, { diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index 339dc590a0..07cf5e2083 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -73,7 +73,7 @@ export type ResolutionResult = { didDocumentMetadata: ResolutionDocumentMetadata } -export type RepresentationResolutionOptions = { +export type RepresentationResolutionOptions = { /** * The Media Type of the caller's preferred representation of the DID document. * The Media Type MUST be expressed as an ASCII string. @@ -84,7 +84,7 @@ export type RepresentationResolutionOptions = { } export type SuccessfulRepresentationResolutionMetadata< - ContentType extends string + ContentType extends string = string > = { /** * The Media Type of the returned didDocumentStream. @@ -110,17 +110,18 @@ export type FailedRepresentationResolutionMetadata = { } // Either success with `contentType` or failure with `error` -export type RepresentationResolutionMetadata = +export type RepresentationResolutionMetadata< + ContentType extends string = string +> = | SuccessfulRepresentationResolutionMetadata | FailedRepresentationResolutionMetadata export type RepresentationResolutionDocumentMetadata = ResolutionDocumentMetadata -export type RepresentationResolutionResult = Pick< - ResolutionResult, - 'didDocumentMetadata' -> & { +export type RepresentationResolutionResult< + ContentType extends string = string +> = Pick & { /** * If the resolution is successful, and if the resolveRepresentation function was called, this MUST be a byte stream of the resolved DID document in one of the conformant representations. * The byte stream might then be parsed by the caller of the resolveRepresentation function into a data model, which can in turn be validated and processed. @@ -133,7 +134,7 @@ export type RepresentationResolutionResult = Pick< /** * The resolve function returns the DID document in its abstract form (a map). */ -export interface ResolveDid { +export interface ResolveDid { resolve: ( /** * This is the DID to resolve. @@ -161,7 +162,7 @@ export interface ResolveDid { ) => Promise> } -export type DereferenceOptions = { +export type DereferenceOptions = { /** * The Media Type that the caller prefers for contentStream. * The Media Type MUST be expressed as an ASCII string. @@ -170,13 +171,14 @@ export type DereferenceOptions = { accept?: Accept } -export type SuccessfulDereferenceMetadata = { - /** - * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. - * The Media Type value MUST be expressed as an ASCII string. - */ - contentType: ContentType -} +export type SuccessfulDereferenceMetadata = + { + /** + * The Media Type of the returned contentStream SHOULD be expressed using this property if dereferencing is successful. + * The Media Type value MUST be expressed as an ASCII string. + */ + contentType: ContentType + } export type FailedDereferenceMetadata = { /** * The error code from the dereferencing process. @@ -192,7 +194,7 @@ export type FailedDereferenceMetadata = { } // Either success with `contentType` or failure with `error` -export type DereferenceMetadata = +export type DereferenceMetadata = | SuccessfulDereferenceMetadata | FailedDereferenceMetadata @@ -207,7 +209,7 @@ export type DereferenceContentStream = export type DereferenceContentMetadata = ResolutionDocumentMetadata -export type DereferenceResult = { +export type DereferenceResult = { /** * A metadata structure consisting of values relating to the results of the DID URL dereferencing process. * This structure is REQUIRED, and in the case of an error in the dereferencing process, this MUST NOT be empty. @@ -230,7 +232,7 @@ export type DereferenceResult = { contentMetadata: DereferenceContentMetadata } -export interface DereferenceDidUrl { +export interface DereferenceDidUrl { dereference: ( /** * A conformant DID URL as a single string. diff --git a/yarn.lock b/yarn.lock index 37fd734bf1..5719189fb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9227,11 +9227,11 @@ typescript@^4.8.3: "typescript@patch:typescript@^4.8.3#~builtin": version: 4.8.3 - resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=aae4e6" + resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=3b564f" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 2222d2382fb3146089b1d27ce2b55e9d1f99cc64118f1aba75809b693b856c5d3c324f052f60c75b577947fc538bc1c27bad0eb76cbdba9a63a253489504ba7e + checksum: dfe2ee3b9da1d74b9e06784ae90c20c435db9ad6ab23172911f6cdbfd7ab7213ae3611c4254c5a2c6dc2e89f05a658b95493890bf62d218267033b3d8a2e4dd6 languageName: node linkType: hard From 28d068f0b11236cb1242c6025ad7c37d4c7f0833 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 17 Oct 2023 12:25:49 +0100 Subject: [PATCH 62/78] Half-way through new DidResolver unit tests --- .../did/src/DidResolver/DidResolver.spec.ts | 356 +++++++++++++++++- packages/did/src/DidResolver/DidResolver.ts | 58 ++- 2 files changed, 394 insertions(+), 20 deletions(-) diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index 743353a19f..4cc8eb8ca8 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -11,12 +11,13 @@ import { DidUri, DidUrl, KiltAddress, + RepresentationResolutionResult, ResolutionResult, Service, UriFragment, VerificationMethod, } from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' +import { Crypto, cbor } from '@kiltprotocol/utils' import { ApiMocks, makeSigningKeyTool } from '../../../../tests/testUtils' import { linkedInfoFromChain } from '../Did.rpc.js' @@ -645,3 +646,356 @@ describe('When resolving a light DID', () => { }) }) }) + +describe('DID Resolution compliance', () => { + beforeAll(() => { + jest + .spyOn(mockedApi.call.did, 'query') + .mockImplementation(async (identifier) => { + return augmentedApi.createType('Option', { + identifier, + accounts: [], + w3n: null, + serviceEndpoints: [ + { + id: 'foo', + serviceTypes: ['type-service-1'], + urls: ['x:url-service-1'], + }, + ], + details: { + authenticationKey: '01234567890123456789012345678901', + keyAgreementKeys: [], + delegationKey: null, + attestationKey: null, + publicKeys: [], + lastTxCounter: 123, + deposit: { + owner: addressWithAuthenticationKey, + amount: 0, + }, + }, + }) + }) + jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + }, + } + }) + }) + describe('resolve(did, resolutionOptions) → « didResolutionMetadata, didDocument, didDocumentMetadata »', () => { + it('returns empty `didDocumentMetadata` and `didResolutionMetadata` when successfully returning a DID Document that has not been deleted nor migrated', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect(await Did.resolve(did)).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + }, + }) + }) + it('returns the right `didResolutionMetadata.error` when the DID does not exist', async () => { + jest + .spyOn(mockedApi.call.did, 'query') + .mockResolvedValueOnce( + augmentedApi.createType('Option', null) + ) + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect(await Did.resolve(did)).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'notFound' }, + }) + }) + it('returns the right `didResolutionMetadata.error` when the input DID is invalid', async () => { + const did = 'did:kilt:test-did' as unknown as DidUri + expect(await Did.resolve(did)).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'invalidDid' }, + }) + }) + }) + + describe('resolveRepresentation(did, resolutionOptions) → « didResolutionMetadata, didDocumentStream, didDocumentMetadata »', () => { + it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.resolveRepresentation(did) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: Did.DID_JSON_CONTENT_TYPE }, + didDocumentStream: Buffer.from( + JSON.stringify({ + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + }) + ), + }) + }) + it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+ld+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.resolveRepresentation(did, { + accept: 'application/did+ld+json', + }) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: Did.DID_JSON_LD_CONTENT_TYPE }, + didDocumentStream: Buffer.from( + JSON.stringify({ + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + '@context': [Did.W3C_DID_CONTEXT_URL, Did.KILT_DID_CONTEXT_URL], + }) + ), + }) + }) + it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+cbor` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.resolveRepresentation(did, { + accept: 'application/did+cbor', + }) + ).toMatchObject({ + didDocumentMetadata: {}, + didResolutionMetadata: { contentType: Did.DID_CBOR_CONTENT_TYPE }, + didDocumentStream: Buffer.from( + cbor.encode({ + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + }) + ), + }) + }) + it('returns the right `didResolutionMetadata.error` when the DID does not exist', async () => { + jest + .spyOn(mockedApi.call.did, 'query') + .mockResolvedValueOnce( + augmentedApi.createType('Option', null) + ) + + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.resolveRepresentation(did) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'notFound' }, + }) + }) + it('returns the right `didResolutionMetadata.error` when the input DID is invalid', async () => { + const did = 'did:kilt:test-did' as unknown as DidUri + expect( + await Did.resolveRepresentation(did) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'invalidDid' }, + }) + }) + it('returns the right `didResolutionMetadata.error` when the requested content type is not supported', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.resolveRepresentation(did, { + accept: 'application/json' as Did.SupportedContentType, + }) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'representationNotSupported' }, + }) + }) + }) + + describe('dereference(didUrl, dereferenceOptions) → « dereferencingMetadata, contentStream, contentMetadata »', () => { + it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect(await Did.dereference(did)).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: Did.DID_JSON_CONTENT_TYPE }, + contentStream: { + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + }, + }) + }) + it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+ld+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.dereference(did, { accept: 'application/did+ld+json' }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: Did.DID_JSON_LD_CONTENT_TYPE }, + contentStream: { + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + '@context': [Did.W3C_DID_CONTEXT_URL, Did.KILT_DID_CONTEXT_URL], + }, + }) + }) + it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+cbor` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.dereference(did, { accept: 'application/did+cbor' }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: Did.DID_CBOR_CONTENT_TYPE }, + contentStream: Buffer.from( + cbor.encode({ + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + }) + ), + }) + }) + it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+json` (ignoring the provided `accept` option) representation when successfully returning a verification method for a DID that has not been deleted nor migrated', async () => { + const didUrl: DidUrl = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth' + expect( + await Did.dereference(didUrl, { accept: 'application/did+cbor' }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: Did.DID_JSON_CONTENT_TYPE }, + contentStream: { + id: '#auth', + controller: + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + }) + }) + it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+json` (ignoring the provided `accept` option) representation when successfully returning a service for a DID that has not been deleted nor migrated', async () => { + jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { + const { identifier } = linkedInfo.unwrap() + const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) + + return { + accounts: [], + document: { + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + service: [ + { + id: '#id-1', + type: ['type'], + serviceEndpoint: ['x:url'], + }, + ], + }, + } + }) + const didUrl: DidUrl = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-1' + expect( + await Did.dereference(didUrl, { accept: 'application/did+cbor' }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: Did.DID_JSON_CONTENT_TYPE }, + contentStream: { + id: '#id-1', + type: ['type'], + serviceEndpoint: ['x:url'], + }, + }) + }) + }) +}) diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 15b17a9560..608839046f 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -32,20 +32,24 @@ import { getFullDidUri, parse, validateIdentifier } from '../Did.utils.js' import { parseDocumentFromLightDid } from '../DidDetails/LightDidDetails.js' import { isValidVerificationRelationship } from '../DidDetails/DidDetails.js' -const DID_JSON = 'application/did+json' -const DID_JSON_LD = 'application/did+ld+json' -const DID_CBOR = 'application/did+cbor' +export const DID_JSON_CONTENT_TYPE = 'application/did+json' +export const DID_JSON_LD_CONTENT_TYPE = 'application/did+ld+json' +export const DID_CBOR_CONTENT_TYPE = 'application/did+cbor' /** * Supported content types for DID resolution and dereferencing. */ export type SupportedContentType = - | typeof DID_JSON - | typeof DID_JSON_LD - | typeof DID_CBOR + | typeof DID_JSON_CONTENT_TYPE + | typeof DID_JSON_LD_CONTENT_TYPE + | typeof DID_CBOR_CONTENT_TYPE function isValidContentType(input: unknown): input is SupportedContentType { - return input === DID_JSON || input === DID_JSON_LD || input === DID_CBOR + return ( + input === DID_JSON_CONTENT_TYPE || + input === DID_JSON_LD_CONTENT_TYPE || + input === DID_CBOR_CONTENT_TYPE + ) } type InternalResolutionResult = { @@ -167,7 +171,7 @@ export async function resolve( export async function resolveRepresentation( did: DidUri, { accept }: DereferenceOptions = { - accept: DID_JSON, + accept: DID_JSON_CONTENT_TYPE, } ): Promise> { const inputTransform = (() => { @@ -214,7 +218,10 @@ export async function resolveRepresentation( return { didDocumentMetadata, - didResolutionMetadata, + didResolutionMetadata: { + ...didResolutionMetadata, + contentType: accept, + }, didDocumentStream: Buffer.from(inputTransform(didDocument)), } as RepresentationResolutionResult } @@ -335,11 +342,13 @@ export async function dereference( didUrl: DidUri | DidUrl, // eslint-disable-next-line @typescript-eslint/no-unused-vars { accept }: DereferenceOptions = { - accept: DID_JSON, + accept: DID_JSON_CONTENT_TYPE, } ): Promise> { // The spec does not include an error for unsupported content types for dereferences - const contentType = isValidContentType(accept) ? accept : DID_JSON + const contentType = isValidContentType(accept) + ? accept + : DID_JSON_CONTENT_TYPE try { validateIdentifier(didUrl) @@ -363,23 +372,34 @@ export async function dereference( } } - const stream = (() => { + const [stream, contentTypeValue] = (() => { + const s = dereferenceResult.contentStream as any + // Stream is a not DID Document, ignore the `contentType`. + if (s.type !== undefined) { + return [dereferenceResult.contentStream, DID_JSON_CONTENT_TYPE] + } if (contentType === 'application/did+json') { - return dereferenceResult.contentStream + return [dereferenceResult.contentStream, contentType] } if (contentType === 'application/did+ld+json') { - return { - ...dereferenceResult.contentStream, - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], - } + return [ + { + ...dereferenceResult.contentStream, + '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], + }, + contentType, + ] } // contentType === 'application/did+cbor' - return Buffer.from(cbor.encode(dereferenceResult.contentStream)) + return [ + Buffer.from(cbor.encode(dereferenceResult.contentStream)), + contentType, + ] })() return { dereferencingMetadata: { - contentType, + contentType: contentTypeValue as SupportedContentType, }, contentMetadata: dereferenceResult.contentMetadata, contentStream: stream, From 226b08653346175dbab37fa920b9f4120c328e0b Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 17 Oct 2023 15:07:43 +0100 Subject: [PATCH 63/78] Unit tests for DidResolver complete --- .../did/src/DidResolver/DidResolver.spec.ts | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index 4cc8eb8ca8..b41a652b17 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -962,7 +962,7 @@ describe('DID Resolution compliance', () => { }) }) it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+json` (ignoring the provided `accept` option) representation when successfully returning a service for a DID that has not been deleted nor migrated', async () => { - jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { + jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) @@ -998,4 +998,52 @@ describe('DID Resolution compliance', () => { }) }) }) + it('returns the right `dereferencingMetadata.error` when the DID does not exist', async () => { + jest + .spyOn(mockedApi.call.did, 'query') + .mockResolvedValueOnce( + augmentedApi.createType('Option', null) + ) + + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect(await Did.dereference(did)).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) + }) + it('returns the right `didResolutionMetadata.error` when the input DID is invalid', async () => { + const did = 'did:kilt:test-did' as unknown as DidUri + expect(await Did.dereference(did)).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'invalidDidUrl' }, + }) + }) + it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+json` (the default value) when the `options.accept` value is invalid', async () => { + const did: DidUri = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect( + await Did.dereference(did, { + accept: 'application/json' as unknown as Did.SupportedContentType, + }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: Did.DID_JSON_CONTENT_TYPE }, + contentStream: { + id: did, + authentication: ['#auth'], + verificationMethod: [ + { + id: '#auth', + controller: did, + type: 'MultiKey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), + }, + ], + }, + }) + }) }) From a1d67284bd815f9d9c867479eed24f92277e44fc Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 17 Oct 2023 15:47:13 +0100 Subject: [PATCH 64/78] Replace buffer --- packages/did/src/Did.utils.ts | 2 +- .../did/src/DidResolver/DidResolver.spec.ts | 2 +- packages/did/src/DidResolver/DidResolver.ts | 2 +- packages/utils/package.json | 1 + packages/utils/src/Crypto.spec.ts | 1 + packages/utils/src/DataUtils.spec.ts | 2 +- packages/utils/src/index.ts | 1 + yarn.lock | 17 ++++++++++++++--- 8 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 5293fbc0d8..e21a3e84e8 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -14,7 +14,7 @@ import type { UriFragment, VerificationMethod, } from '@kiltprotocol/types' -import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import { Buffer, DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' import type { DidVerificationMethodType } from './DidDetails/DidDetails.js' diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index b41a652b17..5c02a154a7 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -17,7 +17,7 @@ import { UriFragment, VerificationMethod, } from '@kiltprotocol/types' -import { Crypto, cbor } from '@kiltprotocol/utils' +import { Crypto, cbor, Buffer } from '@kiltprotocol/utils' import { ApiMocks, makeSigningKeyTool } from '../../../../tests/testUtils' import { linkedInfoFromChain } from '../Did.rpc.js' diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 608839046f..8f12f14227 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -23,7 +23,7 @@ import type { } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' -import { cbor } from '@kiltprotocol/utils' +import { Buffer, cbor } from '@kiltprotocol/utils' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' import { linkedInfoFromChain } from '../Did.rpc.js' diff --git a/packages/utils/package.json b/packages/utils/package.json index 3759f261bf..9b44e9de30 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -39,6 +39,7 @@ "@polkadot/keyring": "^12.0.0", "@polkadot/util": "^12.0.0", "@polkadot/util-crypto": "^12.0.0", + "buffer": "^6.0.3", "cbor-web": "^9.0.0", "tweetnacl": "^1.0.3", "uuid": "^9.0.0" diff --git a/packages/utils/src/Crypto.spec.ts b/packages/utils/src/Crypto.spec.ts index fd08e586aa..dda4349904 100644 --- a/packages/utils/src/Crypto.spec.ts +++ b/packages/utils/src/Crypto.spec.ts @@ -8,6 +8,7 @@ import * as string from '@polkadot/util/string' import nacl from 'tweetnacl' import * as Crypto from './Crypto' +import { Buffer } from './index.js' const messageStr = 'This is a test' const message = new Uint8Array(string.stringToU8a(messageStr)) diff --git a/packages/utils/src/DataUtils.spec.ts b/packages/utils/src/DataUtils.spec.ts index c53cfc4c52..242d3852c1 100644 --- a/packages/utils/src/DataUtils.spec.ts +++ b/packages/utils/src/DataUtils.spec.ts @@ -7,7 +7,7 @@ import { encodeAddress } from '@polkadot/keyring' import type { KiltAddress } from '@kiltprotocol/types' -import { SDKErrors, ss58Format } from './index' +import { Buffer, SDKErrors, ss58Format } from './index' import { verifyKiltAddress, verifyIsHex } from './DataUtils' import * as Crypto from './Crypto' diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index ba73308f91..73c823aa2d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -19,4 +19,5 @@ export * as JsonSchema from './json-schema/index.js' export { Caip19, Caip2 } from './CAIP/index.js' export { ss58Format } from './ss58Format.js' export { cbor } from './cbor.js' +export { Buffer } from 'buffer' export { Keyring } from '@polkadot/keyring' diff --git a/yarn.lock b/yarn.lock index 5719189fb0..f2e95cb182 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2095,6 +2095,7 @@ __metadata: "@polkadot/keyring": ^12.0.0 "@polkadot/util": ^12.0.0 "@polkadot/util-crypto": ^12.0.0 + buffer: ^6.0.3 cbor-web: ^9.0.0 rimraf: ^3.0.2 tweetnacl: ^1.0.3 @@ -3925,6 +3926,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: ^1.3.1 + ieee754: ^1.2.1 + checksum: 5ad23293d9a731e4318e420025800b42bf0d264004c0286c8cc010af7a270c7a0f6522e84f54b9ad65cbd6db20b8badbfd8d2ebf4f80fa03dab093b89e68c3f9 + languageName: node + linkType: hard + "buildcheck@npm:0.0.3": version: 0.0.3 resolution: "buildcheck@npm:0.0.3" @@ -5788,7 +5799,7 @@ fsevents@^2.3.2: languageName: node linkType: hard -"ieee754@npm:^1.1.13": +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e @@ -9227,11 +9238,11 @@ typescript@^4.8.3: "typescript@patch:typescript@^4.8.3#~builtin": version: 4.8.3 - resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=3b564f" + resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=aae4e6" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: dfe2ee3b9da1d74b9e06784ae90c20c435db9ad6ab23172911f6cdbfd7ab7213ae3611c4254c5a2c6dc2e89f05a658b95493890bf62d218267033b3d8a2e4dd6 + checksum: 2222d2382fb3146089b1d27ce2b55e9d1f99cc64118f1aba75809b693b856c5d3c324f052f60c75b577947fc538bc1c27bad0eb76cbdba9a63a253489504ba7e languageName: node linkType: hard From eb9c0721890e9add7efdaacdfa64c53234527871 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 20 Oct 2023 09:29:11 +0100 Subject: [PATCH 65/78] Apply suggestion --- packages/did/src/Did.chain.ts | 128 ++++++++-------------------------- 1 file changed, 30 insertions(+), 98 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 6cf9a57bf3..06895185eb 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -40,6 +40,7 @@ import type { DidSigningMethodType, NewDidVerificationKey, NewDidEncryptionKey, + BaseNewDidKey, } from './DidDetails/DidDetails.js' import { @@ -484,120 +485,51 @@ export async function getStoreTxFromDidDocument( verificationMethod, } = input - const authKey = (() => { - const authenticationMethodId = authentication?.[0] - if (authenticationMethodId === undefined) { - throw new SDKErrors.DidError( - 'Cannot create a DID without an authentication method.' - ) - } - const authVerificationMethod = verificationMethod?.find( - (vm) => vm.id === authenticationMethodId - ) - if (authVerificationMethod === undefined) { - throw new SDKErrors.DidError( - `Cannot find the authentication method with ID "${authenticationMethodId}" in the \`verificationMethod\` property.` - ) - } - const { keyType, publicKey } = multibaseKeyToDidKey( - authVerificationMethod.publicKeyMultibase - ) - if (!isValidVerificationMethodType(keyType)) { - throw new SDKErrors.DidError( - `Provided authentication key has an unsupported key type "${keyType}".` - ) - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() - - const keyAgreementKeys = (() => { - if (keyAgreement === undefined || keyAgreement.length === 0) { - return undefined - } - return keyAgreement.map((k) => { - const vm = verificationMethod?.find((_vm) => _vm.id === k) - if (vm === undefined) { - throw new SDKErrors.DidError( - `Cannot find the key agreement method with ID "${k}" in the \`verificationMethod\` property.` - ) - } - const { keyType, publicKey } = multibaseKeyToDidKey(vm.publicKeyMultibase) - if (!isValidEncryptionMethodType(keyType)) { - throw new SDKErrors.DidError( - `The key agreement key with ID "${k}" has an unsupported key type ${keyType}.` - ) - } - return { - type: keyType, - publicKey, - } as NewDidEncryptionKey - }) - })() - - const assertionMethodKey = (() => { - const assertionMethodId = assertionMethod?.[0] - if (assertionMethodId === undefined) { + const [authKey, assertKey, delKey, ...encKeys] = [ + authentication?.[0], + assertionMethod?.[0], + capabilityDelegation?.[0], + ...(keyAgreement ?? []), + ].map((keyId): BaseNewDidKey | undefined => { + if (!keyId) { return undefined } - const assertionVerificationMethod = verificationMethod?.find( - (vm) => vm.id === assertionMethodId - ) - if (assertionVerificationMethod === undefined) { + const key = verificationMethod?.find((vm) => vm.id === keyId) + if (key === undefined) { throw new SDKErrors.DidError( - `Cannot find the assertion method with ID "${assertionMethodId}" in the \`verificationMethod\` property.` + `A verification method with ID "${keyId}" was not found in the \`verificationMethod\` property of the provided DID Document.` ) } - const { keyType, publicKey } = multibaseKeyToDidKey( - assertionVerificationMethod.publicKeyMultibase - ) - if (!isValidVerificationMethodType(keyType)) { + const { keyType, publicKey } = multibaseKeyToDidKey(key.publicKeyMultibase) + if ( + !isValidVerificationMethodType(keyType) && + !isValidEncryptionMethodType(keyType) + ) { throw new SDKErrors.DidError( - `The assertion method key with ID "${assertionMethodId}" has an unsupported key type ${keyType}.` + `Verification method with ID "${keyId}" has an unsupported type "${keyType}".` ) } return { type: keyType, publicKey, - } as NewDidVerificationKey - })() - - const capabilityDelegationKey = (() => { - const capabilityDelegationId = capabilityDelegation?.[0] - if (capabilityDelegationId === undefined) { - return undefined - } - const capabilityDelegationVerificationMethod = verificationMethod?.find( - (vm) => vm.id === capabilityDelegationId - ) - if (capabilityDelegationVerificationMethod === undefined) { - throw new SDKErrors.DidError( - `Cannot find the capability delegation method with ID "${capabilityDelegationId}" in the \`verificationMethod\` property.` - ) } - const { keyType, publicKey } = multibaseKeyToDidKey( - capabilityDelegationVerificationMethod.publicKeyMultibase + }) + + if (authKey === undefined) { + throw new SDKErrors.DidError( + 'Cannot create a DID without an authentication method.' ) - if (!isValidVerificationMethodType(keyType)) { - throw new SDKErrors.DidError( - `The capability delegation method key with ID "${capabilityDelegationId}" has an unsupported key type ${keyType}.` - ) - } - return { - type: keyType, - publicKey, - } as NewDidVerificationKey - })() + } const storeTxInput: GetStoreTxInput = { - authentication: [authKey], - assertionMethod: assertionMethodKey ? [assertionMethodKey] : undefined, - capabilityDelegation: capabilityDelegationKey - ? [capabilityDelegationKey] + authentication: [authKey as NewDidVerificationKey], + assertionMethod: assertKey + ? [assertKey as NewDidVerificationKey] + : undefined, + capabilityDelegation: delKey + ? [delKey as NewDidVerificationKey] : undefined, - keyAgreement: keyAgreementKeys, + keyAgreement: encKeys as NewDidEncryptionKey[], service, } From 2274c27128ef1dd9b93583c212db66784859570e Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 20 Oct 2023 09:30:05 +0100 Subject: [PATCH 66/78] Apply w3n suggestion --- packages/did/src/Did.rpc.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/did/src/Did.rpc.ts b/packages/did/src/Did.rpc.ts index cceba661e8..272a74e997 100644 --- a/packages/did/src/Did.rpc.ts +++ b/packages/did/src/Did.rpc.ts @@ -204,9 +204,8 @@ export function linkedInfoFromChain( did.service = services } - const web3Name = w3n.isNone ? undefined : w3n.unwrap().toHuman() - if (web3Name !== undefined) { - did.alsoKnownAs = [`w3n:${web3Name}`] + if (w3n.isSome) { + did.alsoKnownAs = [`w3n:${w3n.unwrap().toHuman()}`] } const linkedAccounts = connectedAccountsFromChain(accounts, networkPrefix) From a98f360394db4fabe3935051d89682a070aebe76 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 20 Oct 2023 09:38:07 +0100 Subject: [PATCH 67/78] export type --- packages/did/src/DidDetails/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/did/src/DidDetails/index.ts b/packages/did/src/DidDetails/index.ts index 093402ef37..a11e3504d1 100644 --- a/packages/did/src/DidDetails/index.ts +++ b/packages/did/src/DidDetails/index.ts @@ -6,7 +6,7 @@ */ // We don't export the `add*VerificationMethod` functions, they are meant to be used internally -export { +export type { BaseNewDidKey, DidEncryptionMethodType, DidSigningMethodType, From 447e64ba692de3e215c90556ecc9a4f12daf9d33 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 20 Oct 2023 09:50:43 +0100 Subject: [PATCH 68/78] Replace Buffer with Uint8Array --- packages/did/src/Did.utils.ts | 15 ++++++++------- .../did/src/DidResolver/DidResolver.spec.ts | 11 ++++++----- packages/did/src/DidResolver/DidResolver.ts | 13 +++++++------ packages/types/src/DidResolver.ts | 4 ++-- packages/utils/package.json | 1 - packages/utils/src/Crypto.spec.ts | 1 - packages/utils/src/DataUtils.spec.ts | 2 +- packages/utils/src/index.ts | 1 - yarn.lock | 17 +++-------------- 9 files changed, 27 insertions(+), 38 deletions(-) diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index e21a3e84e8..5476dc1451 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -5,6 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { u8aToString } from '@polkadot/util' import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import type { DidUri, @@ -14,7 +15,7 @@ import type { UriFragment, VerificationMethod, } from '@kiltprotocol/types' -import { Buffer, DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' +import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' import type { DidVerificationMethodType } from './DidDetails/DidDetails.js' @@ -216,9 +217,9 @@ export function keypairToMultibaseKey({ ) } const multiCodecPublicKey = [multiCodecPublicKeyPrefix, ...publicKey] - return Buffer.from( - multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) - ).toString() as `z${string}` + return u8aToString( + multibaseEncode('base58btc', Uint8Array.from(multiCodecPublicKey)) + ) as `z${string}` } /** @@ -253,9 +254,9 @@ export function didKeyToVerificationMethod( controller, id, type: 'MultiKey', - publicKeyMultibase: Buffer.from( - multibaseEncode('base58btc', Buffer.from(multiCodecPublicKey)) - ).toString() as `z${string}`, + publicKeyMultibase: u8aToString( + multibaseEncode('base58btc', Uint8Array.from(multiCodecPublicKey)) + ) as `z${string}`, } } diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index 5c02a154a7..6d4a66fc3e 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -17,7 +17,8 @@ import { UriFragment, VerificationMethod, } from '@kiltprotocol/types' -import { Crypto, cbor, Buffer } from '@kiltprotocol/utils' +import { Crypto, cbor } from '@kiltprotocol/utils' +import { stringToU8a } from '@polkadot/util' import { ApiMocks, makeSigningKeyTool } from '../../../../tests/testUtils' import { linkedInfoFromChain } from '../Did.rpc.js' @@ -747,7 +748,7 @@ describe('DID Resolution compliance', () => { ).toStrictEqual({ didDocumentMetadata: {}, didResolutionMetadata: { contentType: Did.DID_JSON_CONTENT_TYPE }, - didDocumentStream: Buffer.from( + didDocumentStream: stringToU8a( JSON.stringify({ id: did, authentication: ['#auth'], @@ -776,7 +777,7 @@ describe('DID Resolution compliance', () => { ).toStrictEqual({ didDocumentMetadata: {}, didResolutionMetadata: { contentType: Did.DID_JSON_LD_CONTENT_TYPE }, - didDocumentStream: Buffer.from( + didDocumentStream: stringToU8a( JSON.stringify({ id: did, authentication: ['#auth'], @@ -806,7 +807,7 @@ describe('DID Resolution compliance', () => { ).toMatchObject({ didDocumentMetadata: {}, didResolutionMetadata: { contentType: Did.DID_CBOR_CONTENT_TYPE }, - didDocumentStream: Buffer.from( + didDocumentStream: Uint8Array.from( cbor.encode({ id: did, authentication: ['#auth'], @@ -922,7 +923,7 @@ describe('DID Resolution compliance', () => { ).toStrictEqual({ contentMetadata: {}, dereferencingMetadata: { contentType: Did.DID_CBOR_CONTENT_TYPE }, - contentStream: Buffer.from( + contentStream: Uint8Array.from( cbor.encode({ id: did, authentication: ['#auth'], diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 8f12f14227..55fc7e80ff 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -22,8 +22,9 @@ import type { ResolutionResult, } from '@kiltprotocol/types' +import { stringToU8a } from '@polkadot/util' import { ConfigService } from '@kiltprotocol/config' -import { Buffer, cbor } from '@kiltprotocol/utils' +import { cbor } from '@kiltprotocol/utils' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' import { linkedInfoFromChain } from '../Did.rpc.js' @@ -177,7 +178,7 @@ export async function resolveRepresentation( const inputTransform = (() => { switch (accept) { case 'application/did+json': { - return (didDoc: DidDocument) => JSON.stringify(didDoc) + return (didDoc: DidDocument) => stringToU8a(JSON.stringify(didDoc)) } case 'application/did+ld+json': { return (didDoc: DidDocument) => { @@ -185,11 +186,11 @@ export async function resolveRepresentation( ...didDoc, '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], } - return JSON.stringify(jsonLdDoc) + return stringToU8a(JSON.stringify(jsonLdDoc)) } } case 'application/did+cbor': { - return (didDoc: DidDocument) => cbor.encode(didDoc) + return (didDoc: DidDocument) => Uint8Array.from(cbor.encode(didDoc)) } default: { return null @@ -222,7 +223,7 @@ export async function resolveRepresentation( ...didResolutionMetadata, contentType: accept, }, - didDocumentStream: Buffer.from(inputTransform(didDocument)), + didDocumentStream: inputTransform(didDocument), } as RepresentationResolutionResult } @@ -392,7 +393,7 @@ export async function dereference( } // contentType === 'application/did+cbor' return [ - Buffer.from(cbor.encode(dereferenceResult.contentStream)), + Uint8Array.from(cbor.encode(dereferenceResult.contentStream)), contentType, ] })() diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index 07cf5e2083..975d81672b 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -127,7 +127,7 @@ export type RepresentationResolutionResult< * The byte stream might then be parsed by the caller of the resolveRepresentation function into a data model, which can in turn be validated and processed. * If the resolution is unsuccessful, this value MUST be an empty stream. */ - didDocumentStream?: Buffer + didDocumentStream?: Uint8Array didResolutionMetadata: RepresentationResolutionMetadata } @@ -205,7 +205,7 @@ export type DereferenceContentStream = | JsonLd | Service | JsonLd - | Buffer + | Uint8Array export type DereferenceContentMetadata = ResolutionDocumentMetadata diff --git a/packages/utils/package.json b/packages/utils/package.json index 9b44e9de30..3759f261bf 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -39,7 +39,6 @@ "@polkadot/keyring": "^12.0.0", "@polkadot/util": "^12.0.0", "@polkadot/util-crypto": "^12.0.0", - "buffer": "^6.0.3", "cbor-web": "^9.0.0", "tweetnacl": "^1.0.3", "uuid": "^9.0.0" diff --git a/packages/utils/src/Crypto.spec.ts b/packages/utils/src/Crypto.spec.ts index dda4349904..fd08e586aa 100644 --- a/packages/utils/src/Crypto.spec.ts +++ b/packages/utils/src/Crypto.spec.ts @@ -8,7 +8,6 @@ import * as string from '@polkadot/util/string' import nacl from 'tweetnacl' import * as Crypto from './Crypto' -import { Buffer } from './index.js' const messageStr = 'This is a test' const message = new Uint8Array(string.stringToU8a(messageStr)) diff --git a/packages/utils/src/DataUtils.spec.ts b/packages/utils/src/DataUtils.spec.ts index 242d3852c1..c53cfc4c52 100644 --- a/packages/utils/src/DataUtils.spec.ts +++ b/packages/utils/src/DataUtils.spec.ts @@ -7,7 +7,7 @@ import { encodeAddress } from '@polkadot/keyring' import type { KiltAddress } from '@kiltprotocol/types' -import { Buffer, SDKErrors, ss58Format } from './index' +import { SDKErrors, ss58Format } from './index' import { verifyKiltAddress, verifyIsHex } from './DataUtils' import * as Crypto from './Crypto' diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 73c823aa2d..ba73308f91 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -19,5 +19,4 @@ export * as JsonSchema from './json-schema/index.js' export { Caip19, Caip2 } from './CAIP/index.js' export { ss58Format } from './ss58Format.js' export { cbor } from './cbor.js' -export { Buffer } from 'buffer' export { Keyring } from '@polkadot/keyring' diff --git a/yarn.lock b/yarn.lock index f2e95cb182..5719189fb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2095,7 +2095,6 @@ __metadata: "@polkadot/keyring": ^12.0.0 "@polkadot/util": ^12.0.0 "@polkadot/util-crypto": ^12.0.0 - buffer: ^6.0.3 cbor-web: ^9.0.0 rimraf: ^3.0.2 tweetnacl: ^1.0.3 @@ -3926,16 +3925,6 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^6.0.3": - version: 6.0.3 - resolution: "buffer@npm:6.0.3" - dependencies: - base64-js: ^1.3.1 - ieee754: ^1.2.1 - checksum: 5ad23293d9a731e4318e420025800b42bf0d264004c0286c8cc010af7a270c7a0f6522e84f54b9ad65cbd6db20b8badbfd8d2ebf4f80fa03dab093b89e68c3f9 - languageName: node - linkType: hard - "buildcheck@npm:0.0.3": version: 0.0.3 resolution: "buildcheck@npm:0.0.3" @@ -5799,7 +5788,7 @@ fsevents@^2.3.2: languageName: node linkType: hard -"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": +"ieee754@npm:^1.1.13": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e @@ -9238,11 +9227,11 @@ typescript@^4.8.3: "typescript@patch:typescript@^4.8.3#~builtin": version: 4.8.3 - resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=aae4e6" + resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=3b564f" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 2222d2382fb3146089b1d27ce2b55e9d1f99cc64118f1aba75809b693b856c5d3c324f052f60c75b577947fc538bc1c27bad0eb76cbdba9a63a253489504ba7e + checksum: dfe2ee3b9da1d74b9e06784ae90c20c435db9ad6ab23172911f6cdbfd7ab7213ae3611c4254c5a2c6dc2e89f05a658b95493890bf62d218267033b3d8a2e4dd6 languageName: node linkType: hard From caf9e926498a334cd3bddb3eac5c39baf98ef146 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Fri, 20 Oct 2023 11:26:31 +0100 Subject: [PATCH 69/78] Add multikey context --- packages/did/package.json | 1 + packages/did/src/DidResolver/DidContexts.ts | 17 +++++++++++++++-- yarn.lock | 12 ++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/did/package.json b/packages/did/package.json index 00f41c2f1a..95bdd77c81 100644 --- a/packages/did/package.json +++ b/packages/did/package.json @@ -34,6 +34,7 @@ "typescript": "^4.8.3" }, "dependencies": { + "@digitalbazaar/multikey-context": "^1.0.0", "@digitalbazaar/security-context": "^1.0.0", "@kiltprotocol/augment-api": "workspace:*", "@kiltprotocol/config": "workspace:*", diff --git a/packages/did/src/DidResolver/DidContexts.ts b/packages/did/src/DidResolver/DidContexts.ts index 196e35a2df..50249c96d4 100644 --- a/packages/did/src/DidResolver/DidContexts.ts +++ b/packages/did/src/DidResolver/DidContexts.ts @@ -7,18 +7,24 @@ // @ts-expect-error not a TS package import securityContexts from '@digitalbazaar/security-context' +// @ts-expect-error not a TS package +import multikeyContexts from '@digitalbazaar/multikey-context' const securityContextsMap: Map< string, Record > = securityContexts.contexts +const multikeyContextsMap: Map< + string, + Record +> = multikeyContexts.contexts /** * IPFS URL identifying a JSON-LD context file describing terms used in DID documents of the KILT method that are not defined in the W3C DID core context. - * Should be the second entry in the ordered set of contexts after [[W3C_DID_CONTEXT_URL]] in the JSON-LD representation of a KILT DID document. + * Should be the third entry in the ordered set of contexts after [[W3C_DID_CONTEXT_URL]] and [[W3C_MULTIKEY_CONTEXT_URL]] in the JSON-LD representation of a KILT DID document. */ export const KILT_DID_CONTEXT_URL = - 'ipfs://QmU7QkuTCPz7NmD5bD7Z7mQVz2UsSPaEK58B5sYnjnPRNW' + 'ipfs://QmPtQ7wbdxbTuGugx4nFAyrhspcqXKrnriuGr7x4NYaZYN' /** * URL identifying the JSON-LD context file that is part of the W3C DID core specifications describing the terms defined by the core data model. * Must be the first entry in the ordered set of contexts in a JSON-LD representation of a DID document. @@ -31,6 +37,11 @@ export const W3C_DID_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' * This document is extended by the context file available under the [[KILT_DID_CONTEXT_URL]]. */ export const W3C_SECURITY_CONTEXT_URL = securityContexts.SECURITY_CONTEXT_V2_URL +/** + * URL identifying a JSON-LD context file proposed by the W3C Credentials Community Group defining the `Multikey` verification method type, used in verification methods on KILT DID documents. + * This document is extended by the context file available under the [[KILT_DID_CONTEXT_URL]]. + */ +export const W3C_MULTIKEY_CONTEXT_URL = multikeyContexts.CONTEXT_URL /** * An object containing static copies of JSON-LD context files relevant to KILT DID documents, of the form -> context. * These context definitions are not supposed to change; therefore, a cached version can (and should) be used to avoid unexpected changes in definitions. @@ -39,6 +50,7 @@ export const DID_CONTEXTS = { [KILT_DID_CONTEXT_URL]: { '@context': [ W3C_SECURITY_CONTEXT_URL, + W3C_MULTIKEY_CONTEXT_URL, { '@protected': true, KiltPublishedCredentialCollectionV1: @@ -107,4 +119,5 @@ export const DID_CONTEXTS = { }, }, ...Object.fromEntries(securityContextsMap), + ...Object.fromEntries(multikeyContextsMap), } diff --git a/yarn.lock b/yarn.lock index 5719189fb0..1ac7b6ba2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1533,6 +1533,13 @@ __metadata: languageName: node linkType: hard +"@digitalbazaar/multikey-context@npm:^1.0.0": + version: 1.0.0 + resolution: "@digitalbazaar/multikey-context@npm:1.0.0" + checksum: e651253ce101a0a5de0e46de5c6f7c936aa07fd14e5b14d38ba57c105eaa2490c256245482bc1f5173269a992cab2ebe5eec6c26523f0e61aae03d3a6788cc6b + languageName: node + linkType: hard + "@digitalbazaar/security-context@npm:^1.0.0": version: 1.0.0 resolution: "@digitalbazaar/security-context@npm:1.0.0" @@ -2012,6 +2019,7 @@ __metadata: version: 0.0.0-use.local resolution: "@kiltprotocol/did@workspace:packages/did" dependencies: + "@digitalbazaar/multikey-context": ^1.0.0 "@digitalbazaar/security-context": ^1.0.0 "@kiltprotocol/augment-api": "workspace:*" "@kiltprotocol/config": "workspace:*" @@ -9227,11 +9235,11 @@ typescript@^4.8.3: "typescript@patch:typescript@^4.8.3#~builtin": version: 4.8.3 - resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=3b564f" + resolution: "typescript@patch:typescript@npm%3A4.8.3#~builtin::version=4.8.3&hash=aae4e6" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: dfe2ee3b9da1d74b9e06784ae90c20c435db9ad6ab23172911f6cdbfd7ab7213ae3611c4254c5a2c6dc2e89f05a658b95493890bf62d218267033b3d8a2e4dd6 + checksum: 2222d2382fb3146089b1d27ce2b55e9d1f99cc64118f1aba75809b693b856c5d3c324f052f60c75b577947fc538bc1c27bad0eb76cbdba9a63a253489504ba7e languageName: node linkType: hard From 4e6c3dfc1f60fb93cb3ed0529dfdbf051f69e875 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 23 Oct 2023 11:35:12 +0100 Subject: [PATCH 70/78] Fix unit tests for linked signature suites --- packages/did/src/Did.signature.spec.ts | 4 +- packages/did/src/Did.utils.ts | 2 +- .../src/DidDetails/LightDidDetails.spec.ts | 12 ++--- .../did/src/DidResolver/DidResolver.spec.ts | 46 +++++++++---------- packages/types/src/Did.ts | 2 +- packages/vc-export/src/documentLoader.ts | 4 +- .../src/suites/KiltAttestationProofV1.spec.ts | 5 +- .../src/suites/Sr25519Signature2020.spec.ts | 10 +++- .../src/suites/Sr25519Signature2020.ts | 8 ++++ .../src/suites/Sr25519VerificationKey.ts | 3 ++ tests/integration/Did.spec.ts | 32 ++++++------- 11 files changed, 72 insertions(+), 56 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 1b2a4ccc9d..f13f8af874 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -334,7 +334,7 @@ describe('full DID', () => { controller: `did:kilt:${keypair.address}`, id: '#0x12345', publicKeyMultibase: keypairToMultibaseKey(keypair), - type: 'MultiKey', + type: 'Multikey', }, ], } @@ -343,7 +343,7 @@ describe('full DID', () => { verificationMethod: { id: '#0x12345', controller: signingDid, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: keypairToMultibaseKey(keypair), }, }) diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 5476dc1451..92036fef79 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -253,7 +253,7 @@ export function didKeyToVerificationMethod( return { controller, id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: u8aToString( multibaseEncode('base58btc', Uint8Array.from(multiCodecPublicKey)) ) as `z${string}`, diff --git a/packages/did/src/DidDetails/LightDidDetails.spec.ts b/packages/did/src/DidDetails/LightDidDetails.spec.ts index 3c50411dd2..45954b4483 100644 --- a/packages/did/src/DidDetails/LightDidDetails.spec.ts +++ b/packages/did/src/DidDetails/LightDidDetails.spec.ts @@ -65,7 +65,7 @@ describe('When creating an instance from the details', () => { publicKey: authKey.publicKey, type: 'sr25519', }), - type: 'MultiKey', + type: 'Multikey', }, { controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, @@ -74,7 +74,7 @@ describe('When creating an instance from the details', () => { publicKey: encKey.publicKey, type: 'x25519', }), - type: 'MultiKey', + type: 'Multikey', }, ], service: [ @@ -117,7 +117,7 @@ describe('When creating an instance from the details', () => { publicKey: authKey.publicKey, type: 'ed25519', }), - type: 'MultiKey', + type: 'Multikey', }, { controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, @@ -126,7 +126,7 @@ describe('When creating an instance from the details', () => { publicKey: encKey.publicKey, type: 'x25519', }), - type: 'MultiKey', + type: 'Multikey', }, ], }) @@ -198,7 +198,7 @@ describe('When creating an instance from a URI', () => { publicKey: authKey.publicKey, type: 'sr25519', }), - type: 'MultiKey', + type: 'Multikey', }, { controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, @@ -207,7 +207,7 @@ describe('When creating an instance from a URI', () => { publicKey: encKey.publicKey, type: 'x25519', }), - type: 'MultiKey', + type: 'Multikey', }, ], service: [ diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index 6d4a66fc3e..0fd9a377d7 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -86,7 +86,7 @@ function generateAuthenticationVerificationMethod( return { id: '#auth', controller, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -100,7 +100,7 @@ function generateEncryptionVerificationMethod( return { id: '#enc', controller, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(1), type: 'x25519', @@ -114,7 +114,7 @@ function generateAssertionVerificationMethod( return { id: '#att', controller, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(2), type: 'sr25519', @@ -128,7 +128,7 @@ function generateCapabilityDelegationVerificationMethod( return { id: '#del', controller, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(33).fill(3), type: 'ecdsa', @@ -294,7 +294,7 @@ describe('When resolving a full DID', () => { { controller: fullDidWithAuthenticationKey, id: '#auth', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), @@ -343,7 +343,7 @@ describe('When resolving a full DID', () => { { controller: fullDidWithAllKeys, id: '#auth', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), @@ -352,7 +352,7 @@ describe('When resolving a full DID', () => { { controller: fullDidWithAllKeys, id: '#enc', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'x25519', publicKey: new Uint8Array(32).fill(1), @@ -361,7 +361,7 @@ describe('When resolving a full DID', () => { { controller: fullDidWithAllKeys, id: '#att', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'sr25519', publicKey: new Uint8Array(32).fill(2), @@ -370,7 +370,7 @@ describe('When resolving a full DID', () => { { controller: fullDidWithAllKeys, id: '#del', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'ecdsa', publicKey: new Uint8Array(33).fill(3), @@ -414,7 +414,7 @@ describe('When resolving a full DID', () => { { controller: fullDidWithServiceEndpoints, id: '#auth', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), @@ -466,7 +466,7 @@ describe('When resolving a full DID', () => { { controller: didWithAuthenticationKey, id: '#auth', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'ed25519', publicKey: new Uint8Array(32).fill(0), @@ -541,7 +541,7 @@ describe('When resolving a light DID', () => { { controller: lightDidWithAuthenticationKey.id, id: '#authentication', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ ...authKey, type: 'sr25519', @@ -573,7 +573,7 @@ describe('When resolving a light DID', () => { { controller: lightDid.id, id: '#authentication', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ ...authKey, type: 'sr25519', @@ -582,7 +582,7 @@ describe('When resolving a light DID', () => { { controller: lightDid.id, id: '#encryption', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey(encryptionKey), }, ], @@ -707,7 +707,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -756,7 +756,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -785,7 +785,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -815,7 +815,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -879,7 +879,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -904,7 +904,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -931,7 +931,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -954,7 +954,7 @@ describe('DID Resolution compliance', () => { id: '#auth', controller: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', @@ -1037,7 +1037,7 @@ describe('DID Resolution compliance', () => { { id: '#auth', controller: did, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: new Uint8Array(32).fill(0), type: 'ed25519', diff --git a/packages/types/src/Did.ts b/packages/types/src/Did.ts index 32329ffb17..131dc0f3e4 100644 --- a/packages/types/src/Did.ts +++ b/packages/types/src/Did.ts @@ -59,7 +59,7 @@ export type VerificationMethod = { /** * The type of the verification method. This is fixed for KILT DIDs. */ - type: 'MultiKey' + type: 'Multikey' /** * The controller of the verification method. */ diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index afe9b03fd5..7a4bddb91e 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -81,9 +81,7 @@ export const kiltContextsLoader: DocumentLoader = async (url) => { export const kiltDidLoader: DocumentLoader = async (url) => { const { did } = parse(url as DidUri) - const { dereferencingMetadata, contentStream } = await dereferenceDid(did, { - accept: 'application/did+ld+json', - }) + const { dereferencingMetadata, contentStream } = await dereferenceDid(did) if (isFailedDereferenceMetadata(dereferencingMetadata)) { throw new Error(dereferencingMetadata.error) } diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index 3af326646c..b06e64852d 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -313,7 +313,8 @@ describe('vc-js', () => { it('creates and verifies a signed presentation (sr25519)', async () => { const signer = { sign: async ({ data }: { data: Uint8Array }) => keypair.sign(data), - id: didDocument.authentication?.[0], + // TODO: This goes against the signer interface of the rest of the SDK, where `id` is supposed to be only the verification method ID. Change this. + id: `${didDocument.id}${didDocument.authentication![0]}`, } const signingSuite = new Sr25519Signature2020({ signer }) @@ -451,7 +452,7 @@ describe('issuance', () => { signer: async () => ({ verificationMethod: { controller: attestedVc.issuer, - type: 'MultiKey', + type: 'Multikey', id: '#test', publicKeyMultibase: 'zasd', }, diff --git a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts index 2de5f64979..621a66fd2c 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts @@ -8,6 +8,7 @@ // @ts-expect-error not a typescript module import * as vcjs from '@digitalbazaar/vc' +import { base58Encode } from '@polkadot/util-crypto' import { Types, init, W3C_CREDENTIAL_CONTEXT_URL } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import { Crypto } from '@kiltprotocol/utils' @@ -90,7 +91,8 @@ beforeAll(async () => { it('issues and verifies a signed credential', async () => { const signer = { sign: async ({ data }: { data: Uint8Array }) => keypair.sign(data), - id: didDocument.assertionMethod![0], + // TODO: This goes against the signer interface of the rest of the SDK, where `id` is supposed to be only the verification method ID. Change this. + id: `${didDocument.id}${didDocument.assertionMethod![0]}`, } const attestationSigner = new Sr25519Signature2020({ signer }) @@ -121,13 +123,17 @@ it('issues and verifies a signed credential', async () => { const assertionMethod = didDocument.verificationMethod?.find(({ id }) => id.includes('assertion') ) + const { publicKey } = Did.multibaseKeyToDidKey( + assertionMethod!.publicKeyMultibase + ) + const publicKeyBase58 = base58Encode(publicKey) result = await vcjs.verifyCredential({ credential: verifiableCredential, suite: new Sr25519Signature2020({ key: new Sr25519VerificationKey2020({ ...assertionMethod, - publicKeyBase58: assertionMethod!.publicKeyMultibase.substring(1), + publicKeyBase58, }), }), documentLoader, diff --git a/packages/vc-export/src/suites/Sr25519Signature2020.ts b/packages/vc-export/src/suites/Sr25519Signature2020.ts index 19584ed0cf..0d6205984b 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.ts @@ -11,6 +11,7 @@ import { base58Decode, base58Encode } from '@polkadot/util-crypto' import jsigs from 'jsonld-signatures' // cjs module import { KiltCredentialV1, Types } from '@kiltprotocol/core' +import { KILT_DID_CONTEXT_URL, multibaseKeyToDidKey } from '@kiltprotocol/did' import { context } from '../context/context.js' import { Sr25519VerificationKey2020 } from './Sr25519VerificationKey.js' import { includesContext } from './utils.js' @@ -184,6 +185,8 @@ export class Sr25519Signature2020 extends LinkedDataSignature { let contextUrl if (verificationMethod.type === 'Sr25519VerificationKey2020') { contextUrl = SUITE_CONTEXT_URL + } else if (verificationMethod.type === 'Multikey') { + contextUrl = KILT_DID_CONTEXT_URL } else { throw new Error(`Unsupported key type "${verificationMethod.type}".`) } @@ -230,11 +233,16 @@ export class Sr25519Signature2020 extends LinkedDataSignature { const verificationMethod = typeof document === 'string' ? JSON.parse(document) : document + const { publicKey } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) + const publicKeyBase58 = base58Encode(publicKey) await this.assertVerificationMethod({ verificationMethod }) const verificationKey = ( await Sr25519VerificationKey2020.from({ ...verificationMethod, + publicKeyBase58, }) ).export({ publicKey: true, includeContext: true }) return verificationKey diff --git a/packages/vc-export/src/suites/Sr25519VerificationKey.ts b/packages/vc-export/src/suites/Sr25519VerificationKey.ts index 58e53837b2..c9d7aa545d 100644 --- a/packages/vc-export/src/suites/Sr25519VerificationKey.ts +++ b/packages/vc-export/src/suites/Sr25519VerificationKey.ts @@ -88,6 +88,9 @@ export class Sr25519VerificationKey2020 extends LDKeyPair { throw new TypeError('The "publicKeyBase58" property is required.') } this.privateKeyBase58 = options.privateKeyBase58 + if (options.id !== undefined && options.id.startsWith('#')) { + this.id = `${this.controller}${options.id}` + } } /** diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index 29ce021be2..6f22cca312 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -126,7 +126,7 @@ describe('write and didDeleteTx', () => { verificationMethod: [ expect.objectContaining(>{ controller: fullDidUri, - type: 'MultiKey', + type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'sr25519', @@ -353,13 +353,13 @@ describe('DID migration', () => { verificationMethod: [ expect.objectContaining(>{ controller: migratedFullDidUri, - type: 'MultiKey', + type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), expect.objectContaining(>{ controller: migratedFullDidUri, - type: 'MultiKey', + type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), }), @@ -408,7 +408,7 @@ describe('DID migration', () => { verificationMethod: [ expect.objectContaining(>{ controller: migratedFullDidUri, - type: 'MultiKey', + type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), @@ -469,13 +469,13 @@ describe('DID migration', () => { verificationMethod: [ expect.objectContaining(>{ controller: migratedFullDidUri, - type: 'MultiKey', + type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), expect.objectContaining(>{ controller: migratedFullDidUri, - type: 'MultiKey', + type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), }), @@ -661,13 +661,13 @@ describe('DID management batching', () => { expect.objectContaining({ // Authentication controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), // Assertion method expect.objectContaining({ controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'sr25519', publicKey: new Uint8Array(32).fill(1), @@ -676,7 +676,7 @@ describe('DID management batching', () => { // Capability delegation expect.objectContaining({ controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'ecdsa', publicKey: new Uint8Array(33).fill(1), @@ -685,7 +685,7 @@ describe('DID management batching', () => { // Key agreement 1 expect.objectContaining({ controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'x25519', publicKey: new Uint8Array(32).fill(1), @@ -694,7 +694,7 @@ describe('DID management batching', () => { // Key agreement 2 expect.objectContaining({ controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'x25519', publicKey: new Uint8Array(32).fill(2), @@ -703,7 +703,7 @@ describe('DID management batching', () => { // Key agreement 3 expect.objectContaining({ controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ type: 'x25519', publicKey: new Uint8Array(32).fill(3), @@ -765,7 +765,7 @@ describe('DID management batching', () => { // Authentication expect.objectContaining(>{ controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), }), ], @@ -881,7 +881,7 @@ describe('DID management batching', () => { // Authentication expect.objectContaining(>{ controller: finalFullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: keypair.publicKey, type: 'sr25519', @@ -959,7 +959,7 @@ describe('DID management batching', () => { // Authentication expect.objectContaining(>{ controller: finalFullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey({ publicKey: newAuthKey.publicKey, type: 'ed25519', @@ -1048,7 +1048,7 @@ describe('DID management batching', () => { expect.objectContaining({ // Authentication and assertionMethod controller: fullDid.id, - type: 'MultiKey', + type: 'Multikey', publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), ], From 00f2b27826b1c33c9e29bbad0d62bc30e9e44438 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 23 Oct 2023 15:59:05 +0100 Subject: [PATCH 71/78] Legacy support for document loader --- packages/vc-export/src/documentLoader.ts | 54 ++++++++++++++++++- .../src/suites/KiltAttestationProofV1.spec.ts | 5 +- .../src/suites/Sr25519Signature2020.spec.ts | 39 +++++++++----- .../src/suites/Sr25519Signature2020.ts | 8 --- .../src/suites/Sr25519VerificationKey.ts | 3 -- 5 files changed, 80 insertions(+), 29 deletions(-) diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index 7a4bddb91e..ff1c25e4a6 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -8,6 +8,7 @@ // @ts-expect-error not a typescript module import jsonld from 'jsonld' // cjs module +import { base58Encode } from '@polkadot/util-crypto' import { DID_CONTEXTS, KILT_DID_CONTEXT_URL, @@ -15,6 +16,7 @@ import { dereference as dereferenceDid, W3C_DID_CONTEXT_URL, isFailedDereferenceMetadata, + multibaseKeyToDidKey, } from '@kiltprotocol/did' import type { DidDocument, @@ -79,15 +81,65 @@ export const kiltContextsLoader: DocumentLoader = async (url) => { throw new Error(`not a known Kilt context: ${url}`) } +type LegacyVerificationMethodType = + | 'Sr25519VerificationKey2020' + | 'Ed25519VerificationKey2018' + | 'EcdsaSecp256k1VerificationKey2019' + | 'X25519KeyAgreementKey2019' +type LegacyVerificationMethod = Pick< + VerificationMethod, + 'id' | 'controller' +> & { publicKeyBase58: string; type: LegacyVerificationMethodType } + +// Returns legacy representations of a KILT DID verification method. export const kiltDidLoader: DocumentLoader = async (url) => { const { did } = parse(url as DidUri) const { dereferencingMetadata, contentStream } = await dereferenceDid(did) if (isFailedDereferenceMetadata(dereferencingMetadata)) { throw new Error(dereferencingMetadata.error) } + const didDocument = (() => { + if (contentStream === undefined) { + return {} + } + const doc = { ...contentStream } as DidDocument + doc.verificationMethod = doc.verificationMethod?.map( + (vm): LegacyVerificationMethod => { + // Bail early if the returned document is already in legacy format + if (vm.type !== 'Multikey') { + return vm as unknown as LegacyVerificationMethod + } + const { controller, id, publicKeyMultibase } = vm + const { keyType, publicKey } = multibaseKeyToDidKey(publicKeyMultibase) + const publicKeyBase58 = base58Encode(publicKey) + const verificationMethodType: LegacyVerificationMethodType = (() => { + switch (keyType) { + case 'ed25519': + return 'Ed25519VerificationKey2018' + case 'sr25519': + return 'Sr25519VerificationKey2020' + case 'ecdsa': + return 'EcdsaSecp256k1VerificationKey2019' + case 'x25519': + return 'X25519KeyAgreementKey2019' + default: + throw new Error(`Unsupported key type "${keyType}"`) + } + })() + return { + controller, + id, + publicKeyBase58, + type: verificationMethodType, + } + } + ) as unknown as VerificationMethod[] + return doc + })() + // Framing can help us resolve to the requested resource (did or did uri). This way we return either a key or the full DID document, depending on what was requested. const document = (await jsonld.frame( - contentStream ?? {}, + didDocument, { // add did contexts to make sure we get a compacted representation '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index b06e64852d..d5bfd41780 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -313,8 +313,7 @@ describe('vc-js', () => { it('creates and verifies a signed presentation (sr25519)', async () => { const signer = { sign: async ({ data }: { data: Uint8Array }) => keypair.sign(data), - // TODO: This goes against the signer interface of the rest of the SDK, where `id` is supposed to be only the verification method ID. Change this. - id: `${didDocument.id}${didDocument.authentication![0]}`, + id: didDocument.id + didDocument.authentication![0], } const signingSuite = new Sr25519Signature2020({ signer }) @@ -450,13 +449,13 @@ describe('issuance', () => { const didSigner: KiltAttestationProofV1.DidSigner = { did: attestedVc.issuer, signer: async () => ({ + signature: new Uint8Array(32), verificationMethod: { controller: attestedVc.issuer, type: 'Multikey', id: '#test', publicKeyMultibase: 'zasd', }, - signature: new Uint8Array(32), }), } const transactionHandler: KiltAttestationProofV1.TxHandler = { diff --git a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts index 621a66fd2c..343db4bab9 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts @@ -91,8 +91,7 @@ beforeAll(async () => { it('issues and verifies a signed credential', async () => { const signer = { sign: async ({ data }: { data: Uint8Array }) => keypair.sign(data), - // TODO: This goes against the signer interface of the rest of the SDK, where `id` is supposed to be only the verification method ID. Change this. - id: `${didDocument.id}${didDocument.assertionMethod![0]}`, + id: didDocument.id + didDocument.assertionMethod![0], } const attestationSigner = new Sr25519Signature2020({ signer }) @@ -117,23 +116,36 @@ it('issues and verifies a signed credential', async () => { expect(result).not.toHaveProperty('error') expect(result).toHaveProperty('verified', true) - const authenticationMethod = didDocument.verificationMethod?.find(({ id }) => - id.includes('authentication') - ) - const assertionMethod = didDocument.verificationMethod?.find(({ id }) => - id.includes('assertion') - ) - const { publicKey } = Did.multibaseKeyToDidKey( - assertionMethod!.publicKeyMultibase - ) - const publicKeyBase58 = base58Encode(publicKey) + const authenticationMethod = (() => { + const m = didDocument.verificationMethod?.find(({ id }) => + id.includes('authentication') + ) + const { publicKey } = Did.multibaseKeyToDidKey(m!.publicKeyMultibase) + const publicKeyBase58 = base58Encode(publicKey) + return { + ...m, + id: didDocument.id + m!.id, + publicKeyBase58, + } + })() + const assertionMethod = (() => { + const m = didDocument.verificationMethod?.find(({ id }) => + id.includes('assertion') + ) + const { publicKey } = Did.multibaseKeyToDidKey(m!.publicKeyMultibase) + const publicKeyBase58 = base58Encode(publicKey) + return { + ...m, + id: didDocument.id + m!.id, + publicKeyBase58, + } + })() result = await vcjs.verifyCredential({ credential: verifiableCredential, suite: new Sr25519Signature2020({ key: new Sr25519VerificationKey2020({ ...assertionMethod, - publicKeyBase58, }), }), documentLoader, @@ -146,7 +158,6 @@ it('issues and verifies a signed credential', async () => { suite: new Sr25519Signature2020({ key: new Sr25519VerificationKey2020({ ...authenticationMethod, - publicKeyBase58: authenticationMethod!.publicKeyMultibase.substring(1), }), }), documentLoader, diff --git a/packages/vc-export/src/suites/Sr25519Signature2020.ts b/packages/vc-export/src/suites/Sr25519Signature2020.ts index 0d6205984b..19584ed0cf 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.ts @@ -11,7 +11,6 @@ import { base58Decode, base58Encode } from '@polkadot/util-crypto' import jsigs from 'jsonld-signatures' // cjs module import { KiltCredentialV1, Types } from '@kiltprotocol/core' -import { KILT_DID_CONTEXT_URL, multibaseKeyToDidKey } from '@kiltprotocol/did' import { context } from '../context/context.js' import { Sr25519VerificationKey2020 } from './Sr25519VerificationKey.js' import { includesContext } from './utils.js' @@ -185,8 +184,6 @@ export class Sr25519Signature2020 extends LinkedDataSignature { let contextUrl if (verificationMethod.type === 'Sr25519VerificationKey2020') { contextUrl = SUITE_CONTEXT_URL - } else if (verificationMethod.type === 'Multikey') { - contextUrl = KILT_DID_CONTEXT_URL } else { throw new Error(`Unsupported key type "${verificationMethod.type}".`) } @@ -233,16 +230,11 @@ export class Sr25519Signature2020 extends LinkedDataSignature { const verificationMethod = typeof document === 'string' ? JSON.parse(document) : document - const { publicKey } = multibaseKeyToDidKey( - verificationMethod.publicKeyMultibase - ) - const publicKeyBase58 = base58Encode(publicKey) await this.assertVerificationMethod({ verificationMethod }) const verificationKey = ( await Sr25519VerificationKey2020.from({ ...verificationMethod, - publicKeyBase58, }) ).export({ publicKey: true, includeContext: true }) return verificationKey diff --git a/packages/vc-export/src/suites/Sr25519VerificationKey.ts b/packages/vc-export/src/suites/Sr25519VerificationKey.ts index c9d7aa545d..58e53837b2 100644 --- a/packages/vc-export/src/suites/Sr25519VerificationKey.ts +++ b/packages/vc-export/src/suites/Sr25519VerificationKey.ts @@ -88,9 +88,6 @@ export class Sr25519VerificationKey2020 extends LDKeyPair { throw new TypeError('The "publicKeyBase58" property is required.') } this.privateKeyBase58 = options.privateKeyBase58 - if (options.id !== undefined && options.id.startsWith('#')) { - this.id = `${this.controller}${options.id}` - } } /** From 6e42e192e71f2acbdd0b4aec2f6a3c7d8cdef535 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 24 Oct 2023 11:27:45 +0100 Subject: [PATCH 72/78] Make getStoreTxFromDidDocument only used in tests --- packages/did/src/Did.chain.ts | 93 +------------------------- packages/did/src/DidDetails/index.ts | 4 ++ tests/bundle/bundle-test.ts | 6 +- tests/integration/Did.spec.ts | 41 ++++++------ tests/testUtils/TestUtils.ts | 97 +++++++++++++++++++++++++++- 5 files changed, 124 insertions(+), 117 deletions(-) diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 06895185eb..f77a9cffce 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -18,7 +18,6 @@ import type { import type { BN, Deposit, - DidDocument, DidUri, KiltAddress, Service, @@ -40,13 +39,9 @@ import type { DidSigningMethodType, NewDidVerificationKey, NewDidEncryptionKey, - BaseNewDidKey, } from './DidDetails/DidDetails.js' -import { - isValidVerificationMethodType, - isValidEncryptionMethodType, -} from './DidDetails/DidDetails.js' +import { isValidVerificationMethodType } from './DidDetails/DidDetails.js' import { multibaseKeyToDidKey, keypairToMultibaseKey, @@ -361,7 +356,7 @@ export type GetStoreTxSignCallback = ( * * @returns The SubmittableExtrinsic for the DID creation operation. */ -export async function getStoreTxFromInput( +export async function getStoreTx( input: GetStoreTxInput, submitter: KiltAddress, sign: GetStoreTxSignCallback @@ -452,90 +447,6 @@ export async function getStoreTxFromInput( return api.tx.did.create(encoded, encodedSignature) } -/** - * Create a DID creation operation which would write to chain the DID Document provided as input. - * Only the first authentication, assertion, and capability delegation verification methods are considered from the input DID Document. - * All the input DID Document key agreement verification methods are considered. - * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and services. - * - * A DID creation operation can contain at most 25 new services. - * Additionally, each service must respect the following conditions: - * - The service ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. - * - The service has at most 1 service type, with a value that is at most 50 bytes long. - * - The service has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. - * - * @param input The DID Document to store. - * @param submitter The KILT address authorized to submit the creation operation. - * @param sign The sign callback. The authentication key has to be used. - * - * @returns The SubmittableExtrinsic for the DID creation operation. - */ -export async function getStoreTxFromDidDocument( - input: DidDocument, - submitter: KiltAddress, - sign: GetStoreTxSignCallback -): Promise { - const { - authentication, - assertionMethod, - keyAgreement, - capabilityDelegation, - service, - verificationMethod, - } = input - - const [authKey, assertKey, delKey, ...encKeys] = [ - authentication?.[0], - assertionMethod?.[0], - capabilityDelegation?.[0], - ...(keyAgreement ?? []), - ].map((keyId): BaseNewDidKey | undefined => { - if (!keyId) { - return undefined - } - const key = verificationMethod?.find((vm) => vm.id === keyId) - if (key === undefined) { - throw new SDKErrors.DidError( - `A verification method with ID "${keyId}" was not found in the \`verificationMethod\` property of the provided DID Document.` - ) - } - const { keyType, publicKey } = multibaseKeyToDidKey(key.publicKeyMultibase) - if ( - !isValidVerificationMethodType(keyType) && - !isValidEncryptionMethodType(keyType) - ) { - throw new SDKErrors.DidError( - `Verification method with ID "${keyId}" has an unsupported type "${keyType}".` - ) - } - return { - type: keyType, - publicKey, - } - }) - - if (authKey === undefined) { - throw new SDKErrors.DidError( - 'Cannot create a DID without an authentication method.' - ) - } - - const storeTxInput: GetStoreTxInput = { - authentication: [authKey as NewDidVerificationKey], - assertionMethod: assertKey - ? [assertKey as NewDidVerificationKey] - : undefined, - capabilityDelegation: delKey - ? [delKey as NewDidVerificationKey] - : undefined, - keyAgreement: encKeys as NewDidEncryptionKey[], - service, - } - - return getStoreTxFromInput(storeTxInput, submitter, sign) -} - export interface SigningOptions { sign: SignExtrinsicCallback verificationRelationship: SignatureVerificationRelationship diff --git a/packages/did/src/DidDetails/index.ts b/packages/did/src/DidDetails/index.ts index a11e3504d1..daceeda2f3 100644 --- a/packages/did/src/DidDetails/index.ts +++ b/packages/did/src/DidDetails/index.ts @@ -16,5 +16,9 @@ export type { NewService, NewVerificationMethod, } from './DidDetails.js' +export { + isValidDidVerificationType, + isValidEncryptionMethodType, +} from './DidDetails.js' export * from './LightDidDetails.js' export * from './FullDidDetails.js' diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 8b70820a10..028accdd59 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -53,7 +53,7 @@ function makeSignCallback( } } -type StoreDidCallback = Parameters['2'] +type StoreDidCallback = Parameters['2'] function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { return async function sign({ data }) { @@ -105,7 +105,7 @@ async function createFullDidFromKeypair( const api = ConfigService.get('api') const sign = makeStoreDidCallback(keypair) - const storeTx = await Did.getStoreTxFromInput( + const storeTx = await Did.getStoreTx( { authentication: [keypair], assertionMethod: [keypair], @@ -187,7 +187,7 @@ async function runAll() { 'ed25519' ) - const didStoreTx = await Did.getStoreTxFromInput( + const didStoreTx = await Did.getStoreTx( { authentication: [keypair] }, payer.address, storeDidCallback diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index 6f22cca312..b0d039160d 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -28,6 +28,7 @@ import { createMinimalLightDidFromKeypair, makeEncryptionKeyTool, makeSigningKeyTool, + getStoreTxFromDidDocument, } from '../testUtils/index.js' import { createEndowedTestAccount, @@ -60,7 +61,7 @@ describe('write and didDeleteTx', () => { it('fails to create a new DID on chain with a different submitter than the one in the creation operation', async () => { const otherAccount = devBob - const tx = await Did.getStoreTxFromDidDocument( + const tx = await getStoreTxFromDidDocument( did, otherAccount.address, key.storeDidCallback @@ -95,7 +96,7 @@ describe('write and didDeleteTx', () => { ], } - const tx = await Did.getStoreTxFromInput( + const tx = await Did.getStoreTx( input, paymentAccount.address, key.storeDidCallback @@ -231,7 +232,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const { keypair, getSignCallback, storeDidCallback } = makeSigningKeyTool() const newDid = await createMinimalLightDidFromKeypair(keypair) - const tx = await Did.getStoreTxFromDidDocument( + const tx = await getStoreTxFromDidDocument( newDid, paymentAccount.address, storeDidCallback @@ -333,7 +334,7 @@ describe('DID migration', () => { keyAgreement, }) - const storeTx = await Did.getStoreTxFromDidDocument( + const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback @@ -388,7 +389,7 @@ describe('DID migration', () => { authentication, }) - const storeTx = await Did.getStoreTxFromDidDocument( + const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback @@ -449,7 +450,7 @@ describe('DID migration', () => { service, }) - const storeTx = await Did.getStoreTxFromDidDocument( + const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback @@ -526,7 +527,7 @@ describe('DID authorization', () => { makeSigningKeyTool('ed25519') beforeAll(async () => { - const createTx = await Did.getStoreTxFromInput( + const createTx = await Did.getStoreTx( { authentication, assertionMethod: authentication, @@ -595,7 +596,7 @@ describe('DID management batching', () => { describe('FullDidCreationBuilder', () => { it('Build a complete full DID', async () => { const { storeDidCallback, authentication } = makeSigningKeyTool() - const extrinsic = await Did.getStoreTxFromInput( + const extrinsic = await Did.getStoreTx( { authentication, assertionMethod: [ @@ -743,7 +744,7 @@ describe('DID management batching', () => { type: 'ecdsa', } - const extrinsic = await Did.getStoreTxFromInput( + const extrinsic = await Did.getStoreTx( { authentication: [didAuthKey] }, paymentAccount.address, storeDidCallback @@ -782,7 +783,7 @@ describe('DID management batching', () => { const { keypair, getSignCallback, storeDidCallback, authentication } = makeSigningKeyTool() - const createTx = await Did.getStoreTxFromInput( + const createTx = await Did.getStoreTx( { authentication, keyAgreement: [ @@ -902,7 +903,7 @@ describe('DID management batching', () => { authentication: [newAuthKey], } = makeSigningKeyTool('ed25519') - const createTx = await Did.getStoreTxFromInput( + const createTx = await Did.getStoreTx( { authentication }, paymentAccount.address, storeDidCallback @@ -989,7 +990,7 @@ describe('DID management batching', () => { it('simple `batch` succeeds despite failures of some extrinsics', async () => { const { authentication, getSignCallback, storeDidCallback } = makeSigningKeyTool() - const tx = await Did.getStoreTxFromInput( + const tx = await Did.getStoreTx( { authentication, service: [ @@ -1003,7 +1004,7 @@ describe('DID management batching', () => { paymentAccount.address, storeDidCallback ) - // Create the full DID with a service + // Create the full DIgetStoreTx await submitTx(tx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( @@ -1074,7 +1075,7 @@ describe('DID management batching', () => { it('batchAll fails if any extrinsics fails', async () => { const { authentication, getSignCallback, storeDidCallback } = makeSigningKeyTool() - const createTx = await Did.getStoreTxFromInput( + const createTx = await Did.getStoreTx( { authentication, service: [ @@ -1324,7 +1325,7 @@ describe('Runtime constraints', () => { type: 'x25519', }) ) - await Did.getStoreTxFromInput( + await Did.getStoreTx( { authentication: [testAuthKey], keyAgreement: newKeyAgreementKeys, @@ -1338,7 +1339,7 @@ describe('Runtime constraints', () => { type: 'x25519', }) await expect( - Did.getStoreTxFromInput( + Did.getStoreTx( { authentication: [testAuthKey], keyAgreement: newKeyAgreementKeys, @@ -1353,7 +1354,7 @@ describe('Runtime constraints', () => { }, 30_000) it('should not be possible to create a DID with too many services', async () => { - // Maximum is 25 + // MaxgetStoreTx const newServiceEndpoints = Array(25).map( (_, index): Did.NewService => ({ id: `#service-${index}`, @@ -1361,7 +1362,7 @@ describe('Runtime constraints', () => { serviceEndpoint: [`x:url-${index}`], }) ) - await Did.getStoreTxFromInput( + await Did.getStoreTx( { authentication: [testAuthKey], service: newServiceEndpoints, @@ -1376,7 +1377,7 @@ describe('Runtime constraints', () => { serviceEndpoint: ['x:url-100'], }) await expect( - Did.getStoreTxFromInput( + Did.getStoreTx( { authentication: [testAuthKey], service: newServiceEndpoints, @@ -1391,7 +1392,7 @@ describe('Runtime constraints', () => { }, 30_000) it('should not be possible to create a DID with a service that is too long', async () => { - const serviceId = '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const serviceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const limit = api.consts.did.maxServiceIdLength.toNumber() expect(serviceId.length).toBeGreaterThan(limit) }) diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 30c251c5e5..33bd219b44 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -12,9 +12,11 @@ import type { DidDocument, EncryptCallback, KeyringPair, + KiltAddress, KiltEncryptionKeypair, KiltKeyringPair, SignCallback, + SubmittableExtrinsic, UriFragment, VerificationMethod, VerificationRelationship, @@ -23,12 +25,15 @@ import type { BaseNewDidKey, ChainDidKey, DidVerificationMethodType, + GetStoreTxSignCallback, LightDidSupportedVerificationKeyType, NewLightDidVerificationKey, + NewDidVerificationKey, + NewDidEncryptionKey, NewService, } from '@kiltprotocol/did' -import { Crypto } from '@kiltprotocol/utils' +import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' import * as Did from '@kiltprotocol/did' @@ -150,7 +155,7 @@ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { } } -type StoreDidCallback = Parameters['2'] +type StoreDidCallback = Parameters['2'] /** * Generates a callback that can be used for signing. @@ -392,6 +397,92 @@ export async function createLocalDemoFullDidFromLightDid( } } +/** + * Create a DID creation operation which would write to chain the DID Document provided as input. + * Only the first authentication, assertion, and capability delegation verification methods are considered from the input DID Document. + * All the input DID Document key agreement verification methods are considered. + * + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and services. + * + * A DID creation operation can contain at most 25 new services. + * Additionally, each service must respect the following conditions: + * - The service ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service has at most 1 service type, with a value that is at most 50 bytes long. + * - The service has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * + * @param input The DID Document to store. + * @param submitter The KILT address authorized to submit the creation operation. + * @param sign The sign callback. The authentication key has to be used. + * + * @returns The SubmittableExtrinsic for the DID creation operation. + */ +export async function getStoreTxFromDidDocument( + input: DidDocument, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const { + authentication, + assertionMethod, + keyAgreement, + capabilityDelegation, + service, + verificationMethod, + } = input + + const [authKey, assertKey, delKey, ...encKeys] = [ + authentication?.[0], + assertionMethod?.[0], + capabilityDelegation?.[0], + ...(keyAgreement ?? []), + ].map((keyId): BaseNewDidKey | undefined => { + if (!keyId) { + return undefined + } + const key = verificationMethod?.find((vm) => vm.id === keyId) + if (key === undefined) { + throw new SDKErrors.DidError( + `A verification method with ID "${keyId}" was not found in the \`verificationMethod\` property of the provided DID Document.` + ) + } + const { keyType, publicKey } = Did.multibaseKeyToDidKey( + key.publicKeyMultibase + ) + if ( + !Did.isValidDidVerificationType(keyType) && + !Did.isValidEncryptionMethodType(keyType) + ) { + throw new SDKErrors.DidError( + `Verification method with ID "${keyId}" has an unsupported type "${keyType}".` + ) + } + return { + type: keyType, + publicKey, + } + }) + + if (authKey === undefined) { + throw new SDKErrors.DidError( + 'Cannot create a DID without an authentication method.' + ) + } + + const storeTxInput: Parameters[0] = { + authentication: [authKey as NewDidVerificationKey], + assertionMethod: assertKey + ? [assertKey as NewDidVerificationKey] + : undefined, + capabilityDelegation: delKey + ? [delKey as NewDidVerificationKey] + : undefined, + keyAgreement: encKeys as NewDidEncryptionKey[], + service, + } + + return Did.getStoreTx(storeTxInput, submitter, sign) +} + // It takes the auth key from the light DID and use it as attestation and delegation key as well. export async function createFullDidFromLightDid( payer: KiltKeyringPair, @@ -408,7 +499,7 @@ export async function createFullDidFromLightDid( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion fullDidDocumentToBeCreated.authentication![0], ] - const tx = await Did.getStoreTxFromDidDocument( + const tx = await getStoreTxFromDidDocument( fullDidDocumentToBeCreated, payer.address, sign From 1cfe243e301e47b7601be5abd4a8ae6314aea1b1 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 24 Oct 2023 11:32:47 +0100 Subject: [PATCH 73/78] Use Object.fromEntries --- packages/did/src/Did.utils.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 92036fef79..52f2f9d1fe 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -57,13 +57,7 @@ type IDidParsingResult = { // If multiple keys are present, only the first one is returned. function exportQueryParamsFromUri(didUri: DidUrl): Record { const urlified = new URL(didUri) - const params: Record = {} - urlified.searchParams.forEach((value, key) => { - if (params[key] === undefined) { - params[key] = value - } - }) - return params + return Object.fromEntries(urlified.searchParams) } /** From 27f03a28be369c8d03c9f871d79bdfbd5b08a861 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 24 Oct 2023 11:38:48 +0100 Subject: [PATCH 74/78] Refactor exportQueryParamsFromUri --- packages/did/src/Did.utils.ts | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 52f2f9d1fe..473038594f 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -55,9 +55,18 @@ type IDidParsingResult = { // Exports the params section of a DID URL as a map. // If multiple keys are present, only the first one is returned. -function exportQueryParamsFromUri(didUri: DidUrl): Record { - const urlified = new URL(didUri) - return Object.fromEntries(urlified.searchParams) +// If no query params are present, returns null. +function exportQueryParamsFromUri( + didUri: DidUrl +): Record | undefined { + try { + const urlified = new URL(didUri) + return urlified.searchParams.size > 0 + ? Object.fromEntries(urlified.searchParams) + : undefined + } catch { + throw new SDKErrors.InvalidDidFormatError(didUri) + } } /** @@ -75,14 +84,7 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { const version = versionString ? parseInt(versionString, 10) : FULL_DID_LATEST_VERSION - const queryParameters = (() => { - try { - const queryParams = exportQueryParamsFromUri(didUri as DidUrl) - return Object.keys(queryParams).length > 0 ? queryParams : undefined - } catch { - throw new SDKErrors.InvalidDidFormatError(didUri) - } - })() + const queryParameters = exportQueryParamsFromUri(didUri as DidUrl) return { did: didUri.replace(fragment || '', '') as DidUri, version, @@ -106,14 +108,7 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { const version = versionString ? parseInt(versionString, 10) : LIGHT_DID_LATEST_VERSION - const queryParameters = (() => { - try { - const queryParams = exportQueryParamsFromUri(didUri as DidUrl) - return Object.keys(queryParams).length > 0 ? queryParams : undefined - } catch { - throw new SDKErrors.InvalidDidFormatError(didUri) - } - })() + const queryParameters = exportQueryParamsFromUri(didUri as DidUrl) return { did: didUri.replace(fragment || '', '') as DidUri, version, From 573373ddd52d93d4d85878d0fd12f770408ffc49 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 24 Oct 2023 11:48:09 +0100 Subject: [PATCH 75/78] Use resolve instead of dereference --- packages/vc-export/src/documentLoader.ts | 26 ++++++++----------- .../src/suites/KiltAttestationProofV1.spec.ts | 2 +- .../src/suites/Sr25519Signature2020.spec.ts | 20 +++++++------- 3 files changed, 22 insertions(+), 26 deletions(-) diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index ff1c25e4a6..ef1244e788 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -13,9 +13,8 @@ import { DID_CONTEXTS, KILT_DID_CONTEXT_URL, parse, - dereference as dereferenceDid, + resolve as resolveDid, W3C_DID_CONTEXT_URL, - isFailedDereferenceMetadata, multibaseKeyToDidKey, } from '@kiltprotocol/did' import type { @@ -94,15 +93,12 @@ type LegacyVerificationMethod = Pick< // Returns legacy representations of a KILT DID verification method. export const kiltDidLoader: DocumentLoader = async (url) => { const { did } = parse(url as DidUri) - const { dereferencingMetadata, contentStream } = await dereferenceDid(did) - if (isFailedDereferenceMetadata(dereferencingMetadata)) { - throw new Error(dereferencingMetadata.error) - } + const { didDocument: resolvedDidDocument } = await resolveDid(did) const didDocument = (() => { - if (contentStream === undefined) { + if (resolvedDidDocument === undefined) { return {} } - const doc = { ...contentStream } as DidDocument + const doc: DidDocument = { ...resolvedDidDocument } doc.verificationMethod = doc.verificationMethod?.map( (vm): LegacyVerificationMethod => { // Bail early if the returned document is already in legacy format @@ -138,7 +134,7 @@ export const kiltDidLoader: DocumentLoader = async (url) => { })() // Framing can help us resolve to the requested resource (did or did uri). This way we return either a key or the full DID document, depending on what was requested. - const document = (await jsonld.frame( + const jsonLdDocument = (await jsonld.frame( didDocument, { // add did contexts to make sure we get a compacted representation @@ -157,28 +153,28 @@ export const kiltDidLoader: DocumentLoader = async (url) => { } as jsonld.Options.Frame )) as DidDocument | VerificationMethod // The signature suites expect key-related json-LD contexts; we add them here - switch ((document as { type: string }).type) { + switch ((jsonLdDocument as { type: string }).type) { // these 4 are currently used case Sr25519VerificationKey2020.suite: - document['@context'].push(Sr25519VerificationKey2020.SUITE_CONTEXT) + jsonLdDocument['@context'].push(Sr25519VerificationKey2020.SUITE_CONTEXT) break case 'Ed25519VerificationKey2018': - document['@context'].push( + jsonLdDocument['@context'].push( 'https://w3id.org/security/suites/ed25519-2018/v1' ) break case 'EcdsaSecp256k1VerificationKey2019': - document['@context'].push('https://w3id.org/security/v1') + jsonLdDocument['@context'].push('https://w3id.org/security/v1') break case 'X25519KeyAgreementKey2019': - document['@context'].push( + jsonLdDocument['@context'].push( 'https://w3id.org/security/suites/x25519-2019/v1' ) break default: break } - return { contextUrl: undefined, documentUrl: url, document } + return { contextUrl: undefined, documentUrl: url, document: jsonLdDocument } } const loader = CType.newCachingCTypeLoader() diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index d5bfd41780..704732e70b 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -59,7 +59,7 @@ import { makeFakeDid } from './Sr25519Signature2020.spec' jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - dereference: jest.fn(), + resolve: jest.fn(), authorizeTx: jest.fn(), })) diff --git a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts index 343db4bab9..49e5e68a24 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts @@ -28,7 +28,7 @@ jest.mock('@digitalbazaar/http-client', () => ({})) jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - dereference: jest.fn(), + resolve: jest.fn(), })) const documentLoader = combineDocumentLoaders([ @@ -58,24 +58,24 @@ export async function makeFakeDid() { ], } - jest.mocked(Did.dereference).mockImplementation(async (did) => { + jest.mocked(Did.resolve).mockImplementation(async (did) => { if (did.includes('light')) { return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: Did.parseDocumentFromLightDid(did, false), + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: Did.parseDocumentFromLightDid(did, false), } } if (did.startsWith(didDocument.id)) { return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: didDocument, + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument, } } return { - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'notFound' }, } }) return { didDocument, keypair } From a80347abf68f697907967b681124299f4bbe4d52 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 24 Oct 2023 11:51:10 +0100 Subject: [PATCH 76/78] Fixes --- packages/did/src/Did.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 473038594f..37a21a3d2d 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -55,7 +55,7 @@ type IDidParsingResult = { // Exports the params section of a DID URL as a map. // If multiple keys are present, only the first one is returned. -// If no query params are present, returns null. +// If no query params are present, returns undefined. function exportQueryParamsFromUri( didUri: DidUrl ): Record | undefined { From b2f12c5a010b77c5e70bbfe6f5069683b691d7e5 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 25 Oct 2023 14:06:37 +0100 Subject: [PATCH 77/78] Revert DidSignature renaming --- packages/did/src/Did.signature.spec.ts | 50 ++++++------------- packages/did/src/Did.signature.ts | 24 +++------ .../legacy-credentials/src/Credential.spec.ts | 2 +- packages/legacy-credentials/src/Credential.ts | 2 +- packages/types/src/Did.ts | 3 +- 5 files changed, 26 insertions(+), 55 deletions(-) diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index f13f8af874..88a5d497f3 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -90,7 +90,7 @@ describe('light DID', () => { it('deserializes old did signature (with `keyId` property) to new format', async () => { const SIGNED_STRING = 'signed string' - const { signature, signerUrl } = signatureToJson( + const { signature, keyUri } = signatureToJson( await sign({ data: Crypto.coToUInt8(SIGNED_STRING), did: did.id, @@ -99,35 +99,15 @@ describe('light DID', () => { ) const oldSignature = { signature, - keyId: signerUrl, + keyId: keyUri, } const deserialized = signatureFromJson(oldSignature) expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.signerUrl).toStrictEqual(signerUrl) + expect(deserialized.signerUrl).toStrictEqual(keyUri) expect(deserialized).not.toHaveProperty('keyId') }) - it('deserializes old did signature (with `keyUri` property) to new format', async () => { - const SIGNED_STRING = 'signed string' - const { signature, signerUrl } = signatureToJson( - await sign({ - data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', - }) - ) - const oldSignature = { - signature, - keyUri: signerUrl, - } - - const deserialized = signatureFromJson(oldSignature) - expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.signerUrl).toStrictEqual(signerUrl) - expect(deserialized).not.toHaveProperty('keyUri') - }) - it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) const { signature, verificationMethod } = await sign({ @@ -248,7 +228,7 @@ describe('light DID', () => { it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - signerUrl: `${did.id}${did.authentication![0]}`, + keyUri: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -495,7 +475,7 @@ describe('full DID', () => { it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - signerUrl: `${did.id}${did.authentication![0]}`, + keyUri: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -511,31 +491,31 @@ describe('type guard', () => { it('rejects malformed signer URL', () => { let signature: DidSignature = { // @ts-expect-error - signerUrl: `did:kilt:${keypair.address}?mykey`, + keyUri: `did:kilt:${keypair.address}?mykey`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - signerUrl: `kilt:did:${keypair.address}#mykey`, + keyUri: `kilt:did:${keypair.address}#mykey`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - signerUrl: `kilt:did:${keypair.address}`, + keyUri: `kilt:did:${keypair.address}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - signerUrl: keypair.address, + keyUri: keypair.address, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - signerUrl: '', + keyUri: '', signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) @@ -543,7 +523,7 @@ describe('type guard', () => { it('rejects unexpected signature type', () => { const signature: DidSignature = { - signerUrl: `did:kilt:${keypair.address}#mykey` as DidUrl, + keyUri: `did:kilt:${keypair.address}#mykey` as DidUrl, signature: '', } expect(isDidSignature(signature)).toBe(false) @@ -556,14 +536,14 @@ describe('type guard', () => { it('rejects incomplete objects', () => { let signature: DidSignature = { - signerUrl: `did:kilt:${keypair.address}#mykey` as DidUrl, + keyUri: `did:kilt:${keypair.address}#mykey` as DidUrl, // @ts-expect-error signature: undefined, } expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - signerUrl: undefined, + keyUri: undefined, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) @@ -574,14 +554,14 @@ describe('type guard', () => { expect(isDidSignature(signature)).toBe(false) signature = { // @ts-expect-error - signerUrl: `did:kilt:${keypair.address}#mykey`, + keyUri: `did:kilt:${keypair.address}#mykey`, } expect(isDidSignature(signature)).toBe(false) // @ts-expect-error signature = {} expect(isDidSignature(signature)).toBe(false) // @ts-expect-error - signature = { signerUrl: null, signature: null } + signature = { keyUri: null, signature: null } expect(isDidSignature(signature)).toBe(false) }) }) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index 7191c17916..d4b5361ef6 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -38,22 +38,15 @@ type OldDidSignatureV1 = { signature: string keyId: DidUrl } -type OldDidSignatureV2 = { - signature: string - keyUri: DidUrl -} function verifyDidSignatureDataStructure( - input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 + input: DidSignature | OldDidSignatureV1 ): void { const verificationMethodUrl = (() => { - if ('keyUri' in input) { - return input.keyUri - } if ('keyId' in input) { return input.keyId } - return input.signerUrl + return input.keyUri })() if (!isHex(input.signature)) { throw new SDKErrors.SignatureMalformedError( @@ -161,7 +154,7 @@ export async function verifyDidSignature({ */ export function isDidSignature( input: unknown -): input is DidSignature | OldDidSignatureV1 | OldDidSignatureV2 { +): input is DidSignature | OldDidSignatureV1 { try { verifyDidSignatureDataStructure(input as DidSignature) return true @@ -184,30 +177,27 @@ export function signatureToJson({ }: SignResponseData): DidSignature { return { signature: Crypto.u8aToHex(signature), - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + keyUri: `${verificationMethod.controller}${verificationMethod.id}`, } } /** * Deserializes a [[DidSignature]] for signature verification. - * Handles backwards compatibility to an older version of the interface where the `verificationMethodUri` property was called either `keyUri` or `keyId`. + * Handles backwards compatibility to an older version of the interface where the `verificationMethodUri` property was called `keyId`. * * @param input A [[DidSignature]] object. * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. */ export function signatureFromJson( - input: DidSignature | OldDidSignatureV1 | OldDidSignatureV2 + input: DidSignature | OldDidSignatureV1 ): Pick & { signerUrl: DidUrl } { const signerUrl = (() => { - if ('keyUri' in input) { - return input.keyUri - } if ('keyId' in input) { return input.keyId } - return input.signerUrl + return input.keyUri })() const signature = Crypto.coToUInt8(input.signature) return { signature, signerUrl } diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 3456988226..30cce1e861 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -641,7 +641,7 @@ describe('Presentations', () => { signCallback: keyAlice.getSignCallback(identityAlice), }) // but replace signer key reference with authentication verification method of light did - presentation.claimerSignature.signerUrl = `${identityDave.id}${ + presentation.claimerSignature.keyUri = `${identityDave.id}${ identityDave.authentication![0] }` diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 18f3afc199..1f6a7810ab 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -233,7 +233,7 @@ export async function verifySignature( // allow full did to sign presentation if owned by corresponding light did allowUpgraded: true, expectedVerificationRelationship: 'authentication', - signerUrl: claimerSignature.signerUrl, + signerUrl: claimerSignature.keyUri, dereferenceDidUrl, }) } diff --git a/packages/types/src/Did.ts b/packages/types/src/Did.ts index 131dc0f3e4..e7b63fc4a3 100644 --- a/packages/types/src/Did.ts +++ b/packages/types/src/Did.ts @@ -42,7 +42,8 @@ export type VerificationRelationship = | EncryptionRelationship export type DidSignature = { - signerUrl: DidUrl + // Name `keyUri` kept for retro-compatibility + keyUri: DidUrl signature: string } From 2fda0d66c1fa5dc0b0655a5ca98035e6921417fc Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 25 Oct 2023 15:19:43 +0100 Subject: [PATCH 78/78] Rename URI to DID and keep DID URL --- .../src/credentials/PublicCredential.chain.ts | 20 +-- .../src/credentials/PublicCredential.spec.ts | 8 +- .../src/credentials/PublicCredential.ts | 8 +- packages/asset-credentials/src/dids/index.ts | 28 ++-- .../core/src/attestation/Attestation.spec.ts | 4 +- packages/core/src/attestation/Attestation.ts | 6 +- .../KiltAttestationProofV1.spec.ts | 4 +- .../credentialsV1/KiltAttestationProofV1.ts | 16 +-- .../src/credentialsV1/KiltCredentialV1.ts | 6 +- packages/core/src/credentialsV1/types.ts | 10 +- packages/core/src/ctype/CType.chain.ts | 4 +- .../src/delegation/DelegationNode.spec.ts | 8 +- .../core/src/delegation/DelegationNode.ts | 8 +- .../src/delegation/DelegationNode.utils.ts | 4 +- .../core/src/presentation/Presentation.ts | 6 +- packages/did/src/Did.chain.ts | 18 +-- packages/did/src/Did.signature.spec.ts | 2 +- packages/did/src/Did.signature.ts | 22 +-- packages/did/src/Did.utils.ts | 90 ++++++------- packages/did/src/DidDetails/FullDidDetails.ts | 8 +- .../src/DidDetails/LightDidDetails.spec.ts | 20 +-- .../did/src/DidDetails/LightDidDetails.ts | 34 ++--- .../did/src/DidLinks/AccountLinks.chain.ts | 12 +- .../did/src/DidResolver/DidResolver.spec.ts | 86 ++++++------ packages/did/src/DidResolver/DidResolver.ts | 20 +-- packages/legacy-credentials/src/Claim.spec.ts | 4 +- packages/legacy-credentials/src/Claim.ts | 10 +- .../legacy-credentials/src/Credential.spec.ts | 12 +- packages/legacy-credentials/src/Credential.ts | 6 +- packages/types/src/AssetDid.ts | 2 +- packages/types/src/Attestation.ts | 4 +- packages/types/src/Claim.ts | 4 +- packages/types/src/CryptoCallbacks.ts | 6 +- packages/types/src/Delegation.ts | 4 +- packages/types/src/Did.ts | 22 +-- packages/types/src/DidResolver.ts | 10 +- packages/types/src/PublicCredential.ts | 18 +-- packages/utils/src/SDKErrors.ts | 2 +- packages/vc-export/src/documentLoader.ts | 6 +- .../src/suites/Sr25519Signature2020.spec.ts | 12 +- packages/vc-export/src/suites/types.ts | 4 +- tests/breakingChanges/BreakingChanges.spec.ts | 2 +- tests/bundle/bundle-test.ts | 4 +- tests/integration/Did.spec.ts | 127 +++++++++--------- tests/integration/PublicCredentials.spec.ts | 8 +- tests/integration/Web3Names.spec.ts | 4 +- tests/testUtils/TestUtils.ts | 2 +- 47 files changed, 371 insertions(+), 354 deletions(-) diff --git a/packages/asset-credentials/src/credentials/PublicCredential.chain.ts b/packages/asset-credentials/src/credentials/PublicCredential.chain.ts index 9f96a76060..6309ea71fc 100644 --- a/packages/asset-credentials/src/credentials/PublicCredential.chain.ts +++ b/packages/asset-credentials/src/credentials/PublicCredential.chain.ts @@ -6,12 +6,12 @@ */ import type { - AssetDidUri, + AssetDid, CTypeHash, IDelegationNode, IPublicCredentialInput, IPublicCredential, - DidUri, + Did, HexString, } from '@kiltprotocol/types' import type { ApiPromise } from '@polkadot/api' @@ -29,11 +29,11 @@ import { fromChain as didFromChain } from '@kiltprotocol/did' import { SDKErrors, cbor } from '@kiltprotocol/utils' import { getIdForCredential } from './PublicCredential.js' -import { validateUri } from '../dids/index.js' +import { validateDid } from '../dids/index.js' export interface EncodedPublicCredential { ctypeHash: CTypeHash - subject: AssetDidUri + subject: AssetDid claims: HexString authorization: IDelegationNode['id'] | null } @@ -68,12 +68,12 @@ function credentialInputFromChain({ subject, }: PublicCredentialsCredentialsCredential): IPublicCredentialInput { const credentialSubject = subject.toUtf8() - validateUri(credentialSubject) + validateDid(credentialSubject) return { claims: cbor.decode(claims), cTypeHash: ctypeHash.toHex(), delegationId: authorization.unwrapOr(undefined)?.toHex() ?? null, - subject: credentialSubject as AssetDidUri, + subject: credentialSubject as AssetDid, } } @@ -86,9 +86,9 @@ export interface PublicCredentialEntry { */ ctypeHash: HexString /** - * DID URI of the attester. + * DID of the attester. */ - attester: DidUri + attester: Did /** * Flag indicating whether the credential is currently revoked. */ @@ -244,13 +244,13 @@ export async function fetchCredentialFromChain( /** * Retrieves from the blockchain the [[IPublicCredential]]s that have been issued to the provided AssetDID. * - * This is the **only** secure way for users to retrieve and verify all the credentials issued to a given [[AssetDidUri]]. + * This is the **only** secure way for users to retrieve and verify all the credentials issued to a given [[AssetDid]]. * * @param subject The AssetDID of the subject. * @returns An array of [[IPublicCredential]] as the result of combining the on-chain information and the information present in the tx history. */ export async function fetchCredentialsFromChain( - subject: AssetDidUri + subject: AssetDid ): Promise { const api = ConfigService.get('api') diff --git a/packages/asset-credentials/src/credentials/PublicCredential.spec.ts b/packages/asset-credentials/src/credentials/PublicCredential.spec.ts index 29f3d53e9f..b93a3ebb38 100644 --- a/packages/asset-credentials/src/credentials/PublicCredential.spec.ts +++ b/packages/asset-credentials/src/credentials/PublicCredential.spec.ts @@ -11,8 +11,8 @@ import { ConfigService } from '@kiltprotocol/config' import { CType } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import type { - AssetDidUri, - DidUri, + AssetDid, + Did as KiltDid, IAssetClaim, IClaimContents, IPublicCredential, @@ -39,7 +39,7 @@ const assetIdentifier = // Build a public credential with fake attestation (i.e., attester, block number, revocation status) information. function buildCredential( - assetDid: AssetDidUri, + assetDid: AssetDid, contents: IClaimContents ): IPublicCredential { const claim: IAssetClaim = { @@ -48,7 +48,7 @@ function buildCredential( subject: assetDid, } const credential = PublicCredential.fromClaim(claim) - const attester: DidUri = Did.getFullDidUri(devAlice.address) + const attester: KiltDid = Did.getFullDid(devAlice.address) return { ...credential, attester, diff --git a/packages/asset-credentials/src/credentials/PublicCredential.ts b/packages/asset-credentials/src/credentials/PublicCredential.ts index 9972b5aaa3..404b19c32a 100644 --- a/packages/asset-credentials/src/credentials/PublicCredential.ts +++ b/packages/asset-credentials/src/credentials/PublicCredential.ts @@ -9,7 +9,7 @@ import type { AccountId } from '@polkadot/types/interfaces' import type { PublicCredentialsCredentialsCredential } from '@kiltprotocol/augment-api' import type { HexString, - DidUri, + Did as KiltDid, IAssetClaim, ICType, IDelegationNode, @@ -30,7 +30,7 @@ import { toChain as publicCredentialToChain } from './PublicCredential.chain.js' /** * Calculates the ID of a [[IPublicCredentialInput]], to be used to retrieve the full credential content from the blockchain. * - * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[DidUri]] and then Blake2b hashing the result. + * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[Did]] and then Blake2b hashing the result. * * @param credential The input credential object. * @param attester The DID of the credential attester. @@ -38,7 +38,7 @@ import { toChain as publicCredentialToChain } from './PublicCredential.chain.js' */ export function getIdForCredential( credential: IPublicCredentialInput, - attester: DidUri + attester: KiltDid ): HexString { const api = ConfigService.get('api') @@ -62,7 +62,7 @@ function verifyClaimStructure(input: IAssetClaim | PartialAssetClaim): void { throw new SDKErrors.CTypeHashMissingError() } if (input.subject) { - AssetDid.validateUri(input.subject) + AssetDid.validateDid(input.subject) } if (input.contents) { Object.entries(input.contents).forEach(([key, value]) => { diff --git a/packages/asset-credentials/src/dids/index.ts b/packages/asset-credentials/src/dids/index.ts index 395c2067b4..2a86128c77 100644 --- a/packages/asset-credentials/src/dids/index.ts +++ b/packages/asset-credentials/src/dids/index.ts @@ -6,7 +6,7 @@ */ import type { - AssetDidUri, + AssetDid, Caip19AssetId, Caip19AssetInstance, Caip19AssetNamespace, @@ -22,7 +22,7 @@ const ASSET_DID_REGEX = /^did:asset:(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32}))\.(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,64})(:(?[-a-zA-Z0-9]{1,78}))?)$/ type IAssetDidParsingResult = { - uri: AssetDidUri + did: AssetDid chainId: Caip2ChainId chainNamespace: Caip2ChainNamespace chainReference: Caip2ChainReference @@ -33,35 +33,35 @@ type IAssetDidParsingResult = { } /** - * Parses an AssetDID uri and returns the information contained within in a structured form. + * Parses an AssetDID and returns the information contained within in a structured form. - * @param assetDidUri An AssetDID uri as a string. -* @returns Object containing information extracted from the AssetDID uri. + * @param assetDid An AssetDID as a string. +* @returns Object containing information extracted from the AssetDID. */ -export function parse(assetDidUri: AssetDidUri): IAssetDidParsingResult { - const matches = ASSET_DID_REGEX.exec(assetDidUri)?.groups +export function parse(assetDid: AssetDid): IAssetDidParsingResult { + const matches = ASSET_DID_REGEX.exec(assetDid)?.groups if (!matches) { - throw new SDKErrors.InvalidDidFormatError(assetDidUri) + throw new SDKErrors.InvalidDidFormatError(assetDid) } - const { chainId, assetId } = matches as Omit + const { chainId, assetId } = matches as Omit return { - ...(matches as Omit), - uri: `did:asset:${chainId}.${assetId}`, + ...(matches as Omit), + did: `did:asset:${chainId}.${assetId}`, } } /** - * Checks that a string (or other input) is a valid AssetDID uri. + * Checks that a string (or other input) is a valid AssetDID. * Throws otherwise. * * @param input Arbitrary input. */ -export function validateUri(input: unknown): void { +export function validateDid(input: unknown): void { if (typeof input !== 'string') { throw new TypeError(`Asset DID string expected, got ${typeof input}`) } - parse(input as AssetDidUri) + parse(input as AssetDid) } diff --git a/packages/core/src/attestation/Attestation.spec.ts b/packages/core/src/attestation/Attestation.spec.ts index 855ec2e28e..792623d054 100644 --- a/packages/core/src/attestation/Attestation.spec.ts +++ b/packages/core/src/attestation/Attestation.spec.ts @@ -8,7 +8,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { ConfigService } from '@kiltprotocol/config' -import type { CTypeHash, DidUri, IAttestation } from '@kiltprotocol/types' +import type { CTypeHash, Did, IAttestation } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' import { ApiMocks } from '../../../../tests/testUtils' @@ -22,7 +22,7 @@ beforeAll(() => { }) describe('Attestation', () => { - const identityAlice: DidUri = + const identityAlice: Did = 'did:kilt:4nwPAmtsK5toZfBM9WvmAe4Fa3LyZ3X3JHt7EUFfrcPPAZAm' const cTypeHash: CTypeHash = diff --git a/packages/core/src/attestation/Attestation.ts b/packages/core/src/attestation/Attestation.ts index 906782d91b..76c339775a 100644 --- a/packages/core/src/attestation/Attestation.ts +++ b/packages/core/src/attestation/Attestation.ts @@ -9,7 +9,7 @@ import type { IAttestation, IDelegationHierarchyDetails, ICredential, - DidUri, + Did as KiltDid, } from '@kiltprotocol/types' import { DataUtils, SDKErrors } from '@kiltprotocol/utils' import * as Did from '@kiltprotocol/did' @@ -49,7 +49,7 @@ export function verifyDataStructure(input: IAttestation): void { if (!input.owner) { throw new SDKErrors.OwnerMissingError() } - Did.validateIdentifier(input.owner, 'Uri') + Did.validateDid(input.owner, 'Did') if (typeof input.revoked !== 'boolean') { throw new SDKErrors.RevokedTypeError() @@ -65,7 +65,7 @@ export function verifyDataStructure(input: IAttestation): void { */ export function fromCredentialAndDid( credential: ICredential, - attesterDid: DidUri + attesterDid: KiltDid ): IAttestation { const attestation = { claimHash: credential.rootHash, diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts index 10d221880c..b0cbc04ecb 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts @@ -9,7 +9,7 @@ import { encodeAddress, randomAsHex, randomAsU8a } from '@polkadot/util-crypto' import { u8aToHex, u8aToU8a } from '@polkadot/util' import { parse } from '@kiltprotocol/did' -import type { DidUri } from '@kiltprotocol/types' +import type { Did } from '@kiltprotocol/types' import { attestation, @@ -77,7 +77,7 @@ describe('proofs', () => { }) it('checks delegation node owners', async () => { - const delegator: DidUri = `did:kilt:${encodeAddress(randomAsU8a(32), 38)}` + const delegator: Did = `did:kilt:${encodeAddress(randomAsU8a(32), 38)}` const credentialWithDelegators: KiltCredentialV1 = { ...VC, federatedTrustModel: VC.federatedTrustModel?.map((i) => { diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index 2117209b67..f4942a1168 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -31,8 +31,8 @@ import type { IEventData, Signer } from '@polkadot/types/types' import { authorizeTx, - getFullDidUri, - validateIdentifier, + getFullDid, + validateDid, fromChain as didFromChain, } from '@kiltprotocol/did' import { JsonSchema, SDKErrors, Caip19 } from '@kiltprotocol/utils' @@ -43,7 +43,7 @@ import type { RuntimeCommonAuthorizationAuthorizationId, } from '@kiltprotocol/augment-api' import type { - DidUri, + Did, ICType, IDelegationNode, KiltAddress, @@ -220,7 +220,7 @@ async function verifyAttestedAt( ): Promise<{ verified: boolean timestamp: number - attester: DidUri + attester: Did cTypeId: ICType['$id'] delegationId: IDelegationNode['id'] | null }> { @@ -256,7 +256,7 @@ async function verifyAttestedAt( Option | Option ] & IEventData - const attester = getFullDidUri(encodeAddress(att.toU8a(), 38)) + const attester = getFullDid(encodeAddress(att.toU8a(), 38)) const cTypeId = CType.hashToId(cTypeHash.toHex()) const delegationId = authorization.isSome ? ( @@ -276,7 +276,7 @@ async function verifyAttestedAt( async function verifyAuthoritiesInHierarchy( api: ApiPromise, nodeId: Uint8Array | string, - delegators: Set + delegators: Set ): Promise { const node = (await api.query.delegation.delegationNodes(nodeId)).unwrapOr( null @@ -347,7 +347,7 @@ export async function verify( validateCredentialStructure(credential) const { nonTransferable, credentialStatus, credentialSubject, issuer } = credential - validateIdentifier(issuer, 'Uri') + validateDid(issuer, 'Did') await validateSubject(credential, opts) // 4. check nonTransferable if (nonTransferable !== true) @@ -660,7 +660,7 @@ export type AttestationHandler = ( }> export interface DidSigner { - did: DidUri + did: Did signer: SignExtrinsicCallback } diff --git a/packages/core/src/credentialsV1/KiltCredentialV1.ts b/packages/core/src/credentialsV1/KiltCredentialV1.ts index 1babbae66d..de5861f4d9 100644 --- a/packages/core/src/credentialsV1/KiltCredentialV1.ts +++ b/packages/core/src/credentialsV1/KiltCredentialV1.ts @@ -12,7 +12,7 @@ import { JsonSchema, SDKErrors } from '@kiltprotocol/utils' import type { ICType, ICredential, - DidUri, + Did, IDelegationNode, } from '@kiltprotocol/types' @@ -215,10 +215,10 @@ export function validateStructure( } interface CredentialInput { - subject: DidUri + subject: Did claims: ICredential['claim']['contents'] cType: ICType['$id'] - issuer: DidUri + issuer: Did timestamp?: number chainGenesisHash?: Uint8Array claimHash?: ICredential['rootHash'] diff --git a/packages/core/src/credentialsV1/types.ts b/packages/core/src/credentialsV1/types.ts index 0d76d1c770..1be6826b1e 100644 --- a/packages/core/src/credentialsV1/types.ts +++ b/packages/core/src/credentialsV1/types.ts @@ -9,7 +9,7 @@ import type { VerificationMethod, - DidUri, + Did, Caip2ChainId, IClaimContents, ICType, @@ -96,7 +96,7 @@ export interface VerifiablePresentation { '@context': [typeof W3C_CREDENTIAL_CONTEXT_URL, ...string[]] type: [typeof W3C_PRESENTATION_TYPE, ...string[]] verifiableCredential: VerifiableCredential | VerifiableCredential[] - holder: DidUri + holder: Did proof?: Proof | Proof[] expirationDate?: string issuanceDate?: string @@ -134,14 +134,14 @@ export interface KiltAttesterLegitimationV1 extends IssuerBacking { export interface KiltAttesterDelegationV1 extends IssuerBacking { id: `kilt:delegation/${string}` type: typeof KILT_ATTESTER_DELEGATION_V1_TYPE - delegators?: DidUri[] + delegators?: Did[] } export interface CredentialSubject extends IClaimContents { '@context': { '@vocab': string } - id: DidUri + id: Did } export interface KiltCredentialV1 extends VerifiableCredential { @@ -166,7 +166,7 @@ export interface KiltCredentialV1 extends VerifiableCredential { /** * The entity that issued the credential. */ - issuer: DidUri + issuer: Did /** * If true, this credential can only be presented and used by its subject. */ diff --git a/packages/core/src/ctype/CType.chain.ts b/packages/core/src/ctype/CType.chain.ts index 94fff97e96..17a9768477 100644 --- a/packages/core/src/ctype/CType.chain.ts +++ b/packages/core/src/ctype/CType.chain.ts @@ -11,7 +11,7 @@ import type { AccountId, Call } from '@polkadot/types/interfaces' import type { BN } from '@polkadot/util' import type { CtypeCtypeEntry } from '@kiltprotocol/augment-api' -import type { CTypeHash, DidUri, ICType } from '@kiltprotocol/types' +import type { CTypeHash, Did as KiltDid, ICType } from '@kiltprotocol/types' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' @@ -76,7 +76,7 @@ export interface CTypeChainDetails { /** * The DID of the CType's creator. */ - creator: DidUri + creator: KiltDid /** * The block number in which the CType was created. */ diff --git a/packages/core/src/delegation/DelegationNode.spec.ts b/packages/core/src/delegation/DelegationNode.spec.ts index dbf3a84e8e..a56d97c55a 100644 --- a/packages/core/src/delegation/DelegationNode.spec.ts +++ b/packages/core/src/delegation/DelegationNode.spec.ts @@ -10,7 +10,7 @@ import { encodeAddress } from '@polkadot/keyring' import { ConfigService } from '@kiltprotocol/config' import { CTypeHash, - DidUri, + Did, IDelegationHierarchyDetails, IDelegationNode, Permission, @@ -54,7 +54,7 @@ describe('DelegationNode', () => { let hierarchyId: string let parentId: string let hashList: string[] - let addresses: DidUri[] + let addresses: Did[] beforeAll(() => { jest @@ -85,7 +85,7 @@ describe('DelegationNode', () => { .map((_val, index) => Crypto.hashStr(`${index + 1}`)) addresses = Array(10002) .fill('') - .map( + .map( (_val, index) => `did:kilt:${encodeAddress(Crypto.hash(`${index}`, 256), ss58Format)}` ) @@ -448,7 +448,7 @@ describe('DelegationNode', () => { }) it('returns null if looking for non-existent account', async () => { - const noOnesAddress: DidUri = `did:kilt:${encodeAddress( + const noOnesAddress: Did = `did:kilt:${encodeAddress( Crypto.hash('-1', 256), ss58Format )}` diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index bf801196cc..e63b156e4b 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -8,7 +8,7 @@ import type { CTypeHash, DidDocument, - DidUri, + Did as KiltDid, IAttestation, IDelegationHierarchyDetails, IDelegationNode, @@ -58,7 +58,7 @@ export class DelegationNode implements IDelegationNode { public readonly hierarchyId: IDelegationNode['hierarchyId'] public readonly parentId?: IDelegationNode['parentId'] private childrenIdentifiers: Array = [] - public readonly account: DidUri + public readonly account: KiltDid public readonly permissions: IDelegationNode['permissions'] private hierarchyDetails?: IDelegationHierarchyDetails public readonly revoked: boolean @@ -350,7 +350,7 @@ export class DelegationNode implements IDelegationNode { * @returns An object containing a `node` owned by the identity if it is delegating, plus the number of `steps` traversed. `steps` is 0 if the DID is owner of the current node. */ public async findAncestorOwnedBy( - dids: DidUri | DidUri[] + dids: KiltDid | KiltDid[] ): Promise<{ steps: number; node: DelegationNode | null }> { const acceptedDids = Array.isArray(dids) ? dids : [dids] if (acceptedDids.includes(this.account)) { @@ -403,7 +403,7 @@ export class DelegationNode implements IDelegationNode { * @param did The address of the identity used to revoke the delegation. * @returns Promise containing an unsigned SubmittableExtrinsic. */ - public async getRevokeTx(did: DidUri): Promise { + public async getRevokeTx(did: KiltDid): Promise { const { steps, node } = await this.findAncestorOwnedBy(did) if (!node) { throw new SDKErrors.UnauthorizedError( diff --git a/packages/core/src/delegation/DelegationNode.utils.ts b/packages/core/src/delegation/DelegationNode.utils.ts index 3c70e3bc60..b34a3a7645 100644 --- a/packages/core/src/delegation/DelegationNode.utils.ts +++ b/packages/core/src/delegation/DelegationNode.utils.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri, IAttestation, IDelegationNode } from '@kiltprotocol/types' +import type { Did, IAttestation, IDelegationNode } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' import { isHex } from '@polkadot/util' import { DelegationNode } from './DelegationNode.js' @@ -39,7 +39,7 @@ export function permissionsAsBitset(delegation: IDelegationNode): Uint8Array { * @returns 0 if `attester` is the owner of `attestation`, the number of delegation nodes traversed otherwise. */ export async function countNodeDepth( - attester: DidUri, + attester: Did, attestation: IAttestation ): Promise { let delegationTreeTraversalSteps = 0 diff --git a/packages/core/src/presentation/Presentation.ts b/packages/core/src/presentation/Presentation.ts index 3e8e346c87..5d56cd96a3 100644 --- a/packages/core/src/presentation/Presentation.ts +++ b/packages/core/src/presentation/Presentation.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from '@kiltprotocol/types' +import type { Did } from '@kiltprotocol/types' import { JsonSchema, SDKErrors } from '@kiltprotocol/utils' import { @@ -134,7 +134,7 @@ export function assertHolderCanPresentCredentials({ holder, verifiableCredential, }: { - holder: DidUri + holder: Did verifiableCredential: VerifiableCredential[] | VerifiableCredential }): void { const credentials = Array.isArray(verifiableCredential) @@ -164,7 +164,7 @@ export function assertHolderCanPresentCredentials({ */ export function create( VCs: VerifiableCredential[], - holder: DidUri, + holder: Did, { validFrom, validUntil, diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index f77a9cffce..d8ebc0f1ee 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -18,7 +18,7 @@ import type { import type { BN, Deposit, - DidUri, + Did, KiltAddress, Service, SignatureVerificationRelationship, @@ -46,7 +46,7 @@ import { multibaseKeyToDidKey, keypairToMultibaseKey, getAddressFromVerificationMethod, - getFullDidUri, + getFullDid, parse, } from './Did.utils.js' @@ -66,7 +66,7 @@ export type EncodedSignature = EncodedVerificationKey * @param did The DID to format. * @returns The blockchain-formatted DID. */ -export function toChain(did: DidUri): ChainDidIdentifier { +export function toChain(did: Did): ChainDidIdentifier { return parse(did).address } @@ -81,13 +81,13 @@ export function fragmentIdToChain(id: UriFragment): string { } /** - * Convert the DID data from blockchain format to the DID URI. + * Convert the DID data from blockchain format to the DID. * * @param encoded The chain-formatted DID. - * @returns The DID URI. + * @returns The DID. */ -export function fromChain(encoded: AccountId32): DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) +export function fromChain(encoded: AccountId32): Did { + return getFullDid(Crypto.encodeAddress(encoded, ss58Format)) } /** @@ -237,7 +237,7 @@ function isUriFragment(str: string): boolean { /** * Performs sanity checks on service data, making sure that the following conditions are met: - * - The `id` property is a string containing a valid URI fragment according to RFC#3986, not a complete DID URI. + * - The `id` property is a string containing a valid URI fragment according to RFC#3986, not a complete DID URL. * - If the `uris` property contains one or more strings, they must be valid URIs according to RFC#3986. * * @param endpoint A service object to check. @@ -297,7 +297,7 @@ export function serviceFromChain( } export type AuthorizeCallInput = { - did: DidUri + did: Did txCounter: AnyNumber call: Extrinsic submitter: KiltAddress diff --git a/packages/did/src/Did.signature.spec.ts b/packages/did/src/Did.signature.spec.ts index 88a5d497f3..e5672ca8f8 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -104,7 +104,7 @@ describe('light DID', () => { const deserialized = signatureFromJson(oldSignature) expect(deserialized.signature).toBeInstanceOf(Uint8Array) - expect(deserialized.signerUrl).toStrictEqual(keyUri) + expect(deserialized.keyUri).toStrictEqual(keyUri) expect(deserialized).not.toHaveProperty('keyId') }) diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index d4b5361ef6..f7d5a94c2b 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -11,7 +11,7 @@ import type { DereferenceDidUrl, DidDocument, DidSignature, - DidUri, + Did, DidUrl, SignatureVerificationRelationship, SignResponseData, @@ -19,14 +19,14 @@ import type { import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { multibaseKeyToDidKey, parse, validateIdentifier } from './Did.utils.js' +import { multibaseKeyToDidKey, parse, validateDid } from './Did.utils.js' import { dereference } from './DidResolver/DidResolver.js' export type DidSignatureVerificationInput = { message: string | Uint8Array signature: Uint8Array signerUrl: DidUrl - expectedSigner?: DidUri + expectedSigner?: Did allowUpgraded?: boolean expectedVerificationRelationship?: SignatureVerificationRelationship dereferenceDidUrl?: DereferenceDidUrl['dereference'] @@ -53,11 +53,11 @@ function verifyDidSignatureDataStructure( `Expected signature as a hex string, got ${input.signature}` ) } - validateIdentifier(verificationMethodUrl, 'Url') + validateDid(verificationMethodUrl, 'DidUrl') } /** - * Verify a DID signature given the signer's DID URL (i.e., DID URI + verification method ID). + * Verify a DID signature given the signer's DID URL (i.e., DID + verification method ID). * A signature verification returns false if a migrated and then deleted DID is used. * * @param input Object wrapping all input. @@ -99,7 +99,7 @@ export async function verifyDidSignature({ } if (signer.fragment === undefined) { throw new SDKErrors.DidError( - `Signer DID URL "${signerUrl}" is not a valid DID resource.` + `Signer DID URL "${signerUrl}" does not point to a valid resource under the signer's DID Document.` ) } @@ -112,7 +112,7 @@ export async function verifyDidSignature({ `Error validating the DID signature. Cannot fetch DID Document or the verification method for "${signerUrl}".` ) } - // If the light DID has been upgraded we consider the old key URI invalid, the full DID URI should be used instead. + // If the light DID has been upgraded we consider the old key ID invalid, the full DID should be used instead. if (contentMetadata.canonicalId !== undefined) { throw new SDKErrors.DidResolveUpgradedDidError() } @@ -183,7 +183,7 @@ export function signatureToJson({ /** * Deserializes a [[DidSignature]] for signature verification. - * Handles backwards compatibility to an older version of the interface where the `verificationMethodUri` property was called `keyId`. + * Handles backwards compatibility to an older version of the interface where the `keyUri` property was called `keyId`. * * @param input A [[DidSignature]] object. * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. @@ -191,14 +191,14 @@ export function signatureToJson({ export function signatureFromJson( input: DidSignature | OldDidSignatureV1 ): Pick & { - signerUrl: DidUrl + keyUri: DidUrl } { - const signerUrl = (() => { + const keyUri = (() => { if ('keyId' in input) { return input.keyId } return input.keyUri })() const signature = Crypto.coToUInt8(input.signature) - return { signature, signerUrl } + return { signature, keyUri } } diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index 37a21a3d2d..bf44a0d404 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -8,7 +8,7 @@ import { u8aToString } from '@polkadot/util' import { blake2AsU8a, encodeAddress } from '@polkadot/util-crypto' import type { - DidUri, + Did, DidUrl, KeyringPair, KiltAddress, @@ -26,7 +26,7 @@ const LIGHT_DID_LATEST_VERSION = 1 // The latest version for KILT full DIDs. const FULL_DID_LATEST_VERSION = 1 -// NOTICE: The following regex patterns must be kept in sync with DidUri type in @kiltprotocol/types +// NOTICE: The following regex patterns must be kept in sync with `Did` type in @kiltprotocol/types // Matches the following full DIDs // - did:kilt: @@ -43,7 +43,7 @@ const LIGHT_KILT_DID_REGEX = /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?#[^#\n]+)?$/ type IDidParsingResult = { - did: DidUri + did: Did version: number type: 'light' | 'full' address: KiltAddress @@ -56,37 +56,37 @@ type IDidParsingResult = { // Exports the params section of a DID URL as a map. // If multiple keys are present, only the first one is returned. // If no query params are present, returns undefined. -function exportQueryParamsFromUri( - didUri: DidUrl +function exportQueryParamsFromDidUrl( + did: DidUrl ): Record | undefined { try { - const urlified = new URL(didUri) + const urlified = new URL(did) return urlified.searchParams.size > 0 ? Object.fromEntries(urlified.searchParams) : undefined } catch { - throw new SDKErrors.InvalidDidFormatError(didUri) + throw new SDKErrors.InvalidDidFormatError(did) } } /** - * Parses a KILT DID uri and returns the information contained within in a structured form. + * Parses a KILT DID or a DID URL and returns the information contained within in a structured form. * - * @param didUri A KILT DID uri as a string. - * @returns Object containing information extracted from the DID uri. + * @param did A KILT DID or a DID URL as a string. + * @returns Object containing information extracted from the input string. */ -export function parse(didUri: DidUri | DidUrl): IDidParsingResult { - // Then we check if it conforms to either a full or a light DID URL. - let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups +export function parse(did: Did | DidUrl): IDidParsingResult { + // Then we check if it conforms to either a full or a light DID. + let matches = FULL_KILT_DID_REGEX.exec(did)?.groups if (matches) { const { version: versionString, fragment } = matches const address = matches.address as KiltAddress const version = versionString ? parseInt(versionString, 10) : FULL_DID_LATEST_VERSION - const queryParameters = exportQueryParamsFromUri(didUri as DidUrl) + const queryParameters = exportQueryParamsFromDidUrl(did as DidUrl) return { - did: didUri.replace(fragment || '', '') as DidUri, + did: did.replace(fragment || '', '') as Did, version, type: 'full', address, @@ -96,7 +96,7 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { } // If it fails to parse full DID, try with light DID - matches = LIGHT_KILT_DID_REGEX.exec(didUri)?.groups + matches = LIGHT_KILT_DID_REGEX.exec(did)?.groups if (matches) { const { authKeyType, @@ -108,9 +108,9 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { const version = versionString ? parseInt(versionString, 10) : LIGHT_DID_LATEST_VERSION - const queryParameters = exportQueryParamsFromUri(didUri as DidUrl) + const queryParameters = exportQueryParamsFromDidUrl(did as DidUrl) return { - did: didUri.replace(fragment || '', '') as DidUri, + did: did.replace(fragment || '', '') as Did, version, type: 'light', address, @@ -121,7 +121,7 @@ export function parse(didUri: DidUri | DidUrl): IDidParsingResult { } } - throw new SDKErrors.InvalidDidFormatError(didUri) + throw new SDKErrors.InvalidDidFormatError(did) } type DecodedVerificationMethod = { @@ -214,7 +214,7 @@ export function keypairToMultibaseKey({ /** * Convert a DID key to a `MultiKey` verification method. * - * @param controller The verification method controller's DID URI. + * @param controller The verification method controller's DID. * @param id The verification method ID. * @param key The DID key to export as a verification method. * @param key.keyType The key type. @@ -252,11 +252,11 @@ export function didKeyToVerificationMethod( /** * Returns true if both didA and didB refer to the same DID subject, i.e., whether they have the same identifier as specified in the method spec. * - * @param didA A KILT DID uri as a string. - * @param didB A second KILT DID uri as a string. + * @param didA A KILT DID as a string. + * @param didB A second KILT DID as a string. * @returns Whether didA and didB refer to the same DID subject. */ -export function isSameSubject(didA: DidUri, didB: DidUri): boolean { +export function isSameSubject(didA: Did, didB: Did): boolean { return parse(didA).address === parse(didB).address } @@ -265,31 +265,31 @@ export function isSameSubject(didA: DidUri, didB: DidUri): boolean { * Throws otherwise. * * @param input Arbitrary input. - * @param expectType `Uri` if the URI is expected to have a fragment (following '#'), `Url` if it is expected not to have one. Default allows both. + * @param expectType `Did` if the the input is expected to have a fragment (following '#'), `DidUrl` if it is expected not to have one. Default allows both. */ -export function validateIdentifier( +export function validateDid( input: unknown, - expectType?: 'Uri' | 'Url' + expectType?: 'Did' | 'DidUrl' ): void { if (typeof input !== 'string') { throw new TypeError(`DID string expected, got ${typeof input}`) } - const { address, fragment } = parse(input as DidUri) + const { address, fragment } = parse(input as DidUrl) if ( fragment && - (expectType === 'Uri' || + (expectType === 'Did' || // for backwards compatibility with previous implementations, `false` maps to `Did` while `true` maps to `undefined`. (typeof expectType === 'boolean' && expectType === false)) ) { throw new SDKErrors.DidError( - 'Expected a Kilt DidUri but got a DidUrl (containing a #fragment)' + 'Expected a Kilt Did but got a DidUrl (containing a #fragment)' ) } - if (!fragment && expectType === 'Url') { + if (!fragment && expectType === 'DidUrl') { throw new SDKErrors.DidError( - 'Expected a Kilt DidUrl (containing a #fragment) but got a DidUri' + 'Expected a Kilt DidUrl (containing a #fragment) but got a Did' ) } @@ -318,32 +318,32 @@ export function getAddressFromVerificationMethod({ } /** - * Builds the URI a light DID will have after it’s stored on the blockchain. + * Builds the full DID a light DID will have after it’s stored on the blockchain. * - * @param didOrAddress The URI of the light DID. Internally it’s used with the DID "address" as well. - * @param version The version of the DID URI to use. - * @returns The expected full DID URI. + * @param didOrAddress The light DID. Internally it’s used with the DID "address" as well. + * @param version The version of the DID to use. + * @returns The expected full DID. */ -export function getFullDidUri( - didOrAddress: DidUri | KiltAddress, +export function getFullDid( + didOrAddress: Did | KiltAddress, version = FULL_DID_LATEST_VERSION -): DidUri { +): Did { const address = DataUtils.isKiltAddress(didOrAddress) ? didOrAddress - : parse(didOrAddress as DidUri).address + : parse(didOrAddress as Did).address const versionString = version === 1 ? '' : `v${version}` - return `did:kilt:${versionString}${address}` as DidUri + return `did:kilt:${versionString}${address}` as Did } /** - * Builds the URI of a full DID if it is created with the authentication verification method derived from the provided public key. + * Builds the of a full DID if it is created with the authentication verification method derived from the provided public key. * * @param verificationMethod The DID verification method. - * @returns The expected full DID URI. + * @returns The expected full DID . */ -export function getFullDidUriFromVerificationMethod( +export function getFullDidFromVerificationMethod( verificationMethod: Pick -): DidUri { +): Did { const address = getAddressFromVerificationMethod(verificationMethod) - return getFullDidUri(address) + return getFullDid(address) } diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index c4243744b8..5303e6fb06 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -10,7 +10,7 @@ import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' import { BN } from '@polkadot/util' import type { - DidUri, + Did, KiltAddress, SignatureVerificationRelationship, SignExtrinsicCallback, @@ -101,7 +101,7 @@ function increaseNonce(currentNonce: BN, increment = 1): BN { * @param did The DID data. * @returns The next valid nonce, i.e., the nonce currently stored on the blockchain + 1, wrapping around the max value when reached. */ -async function getNextNonce(did: DidUri): Promise { +async function getNextNonce(did: Did): Promise { const api = ConfigService.get('api') const queried = await api.query.did.did(toChain(did)) const currentNonce = queried.isSome @@ -122,7 +122,7 @@ async function getNextNonce(did: DidUri): Promise { * @returns The DID-signed submittable extrinsic. */ export async function authorizeTx( - did: DidUri, + did: Did, extrinsic: Extrinsic, sign: SignExtrinsicCallback, submitterAccount: KiltAddress, @@ -218,7 +218,7 @@ export async function authorizeBatch({ submitter, }: { batchFunction: SubmittableExtrinsicFunction<'promise'> - did: DidUri + did: Did extrinsics: Extrinsic[] nonce?: BN sign: SignExtrinsicCallback diff --git a/packages/did/src/DidDetails/LightDidDetails.spec.ts b/packages/did/src/DidDetails/LightDidDetails.spec.ts index 45954b4483..a4e6089934 100644 --- a/packages/did/src/DidDetails/LightDidDetails.spec.ts +++ b/packages/did/src/DidDetails/LightDidDetails.spec.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocument, DidUri } from '@kiltprotocol/types' +import type { DidDocument, Did, DidUrl } from '@kiltprotocol/types' import { Crypto } from '@kiltprotocol/utils' @@ -157,7 +157,7 @@ describe('When creating an instance from the details', () => { }) }) -describe('When creating an instance from a URI', () => { +describe('When creating an instance from a light DID', () => { it('correctly assign the right authentication key, encryption key, and services', () => { const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') const encKey = Crypto.makeEncryptionKeypairFromSeed( @@ -248,19 +248,19 @@ describe('When creating an instance from a URI', () => { service, }) - const uriWithFragment: DidUri = `${expectedLightDid.id}#authentication` + const didWithFragment: DidUrl = `${expectedLightDid.id}#authentication` - expect(() => parseDocumentFromLightDid(uriWithFragment, true)).toThrow() + expect(() => parseDocumentFromLightDid(didWithFragment, true)).toThrow() expect(() => - parseDocumentFromLightDid(uriWithFragment, false) + parseDocumentFromLightDid(didWithFragment, false) ).not.toThrow() }) - it('fail if the URI is not correct', () => { + it('fail if the DID is not correct', () => { const validKiltAddress = Crypto.makeKeypairFromSeed() - const incorrectURIs = [ + const incorrectDIDs = [ 'did:kilt:light:sdasdsadas', - // @ts-ignore not a valid DID uri + // @ts-ignore not a valid DID 'random-uri', 'did:kilt:light', 'did:kilt:light:', @@ -271,8 +271,8 @@ describe('When creating an instance from a URI', () => { // Random encoded details `did:kilt:light:00${validKiltAddress}:randomdetails`, ] - incorrectURIs.forEach((uri) => { - expect(() => parseDocumentFromLightDid(uri as DidUri)).toThrow() + incorrectDIDs.forEach((did) => { + expect(() => parseDocumentFromLightDid(did as Did)).toThrow() }) }) }) diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 20c976f11e..cc23bb98e1 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocument, DidUri } from '@kiltprotocol/types' +import type { DidDocument, Did } from '@kiltprotocol/types' import { base58Decode, @@ -203,8 +203,8 @@ function deserializeAdditionalLightDidDetails( } /** - * Create [[DidDocument]] of a light DID using the provided verification methods and services. - * Sets proper verification method IDs, builds light DID URI. + * Create a light [[DidDocument]] using the provided verification methods and services. + * Sets proper verification method IDs, builds light DID Document. * Private keys are assumed to already live in another storage, as it contains reference only to public keys as verification methods. * * @param input The input. @@ -236,14 +236,14 @@ export function createLightDidDocument({ }) const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' - const uri = - `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as DidUri + const did = + `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as Did - const did: DidDocument = { - id: uri, + const didDocument: DidDocument = { + id: did, authentication: [authenticationKeyId], verificationMethod: [ - didKeyToVerificationMethod(uri, authenticationKeyId, { + didKeyToVerificationMethod(did, authenticationKeyId, { keyType: authentication[0].type, publicKey: authentication[0].publicKey, }), @@ -254,30 +254,30 @@ export function createLightDidDocument({ if (keyAgreement !== undefined) { const { publicKey, type } = keyAgreement[0] addKeypairAsVerificationMethod( - did, + didDocument, { id: encryptionKeyId, publicKey, type }, 'keyAgreement' ) } - return did + return didDocument } /** - * Create [[DidDocument]] of a light DID by parsing the provided input URI. + * Create a light [[DidDocument]] by parsing the provided input DID. * Only use for DIDs you control, when you are certain they have not been upgraded to on-chain full DIDs. * For the DIDs you have received from external sources use [[resolve]] etc. * * Parsing is possible because of the self-describing and self-containing nature of light DIDs. * Private keys are assumed to already live in another storage, as it contains reference only to public keys as verification methods. * - * @param uri The DID URI to parse. - * @param failIfFragmentPresent Whether to fail when parsing the URI in case a fragment is present or not, which is not relevant to the creation of the DID. It defaults to true. + * @param did The DID to parse. + * @param failIfFragmentPresent Whether to fail when parsing the DID in case a fragment is present or not, which is not relevant to the creation of the DID. It defaults to true. * * @returns The resulting [[DidDocument]]. */ export function parseDocumentFromLightDid( - uri: DidUri, + did: Did, failIfFragmentPresent = true ): DidDocument { const { @@ -287,16 +287,16 @@ export function parseDocumentFromLightDid( fragment, type, authKeyTypeEncoding, - } = parse(uri) + } = parse(did) if (type !== 'light') { throw new SDKErrors.DidError( - `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` + `Cannot build a light DID Document from the provided DID "${did}" because it does not refer to a light DID` ) } if (fragment && failIfFragmentPresent) { throw new SDKErrors.DidError( - `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` + `Cannot build a light DID Document from the provided DID "${did}" because it has a fragment` ) } const keyType = diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index fc903f64f9..745e6777d5 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -11,7 +11,7 @@ import type { KeypairType } from '@polkadot/util-crypto/types' import type { ApiPromise } from '@polkadot/api' import type { BN } from '@polkadot/util' import type { - DidUri, + Did, HexString, KeyringPair, KiltAddress, @@ -134,7 +134,7 @@ function getUnprefixedSignature( } async function getLinkingChallengeV1( - did: DidUri, + did: Did, validUntil: BN ): Promise { const api = ConfigService.get('api') @@ -156,7 +156,7 @@ async function getLinkingChallengeV1( .toU8a() } -function getLinkingChallengeV2(did: DidUri, validUntil: BN): Uint8Array { +function getLinkingChallengeV2(did: Did, validUntil: BN): Uint8Array { return stringToU8a( `Publicly link the signing address to ${did} before block number ${validUntil}` ) @@ -167,12 +167,12 @@ function getLinkingChallengeV2(did: DidUri, validUntil: BN): Uint8Array { * The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will * link the account to the DID. * - * @param did The URI of the DID that that should be linked to an account. + * @param did The DID that should be linked to an account. * @param validUntil Last blocknumber that this challenge is valid for. * @returns The encoded challenge. */ export async function getLinkingChallenge( - did: DidUri, + did: Did, validUntil: BN ): Promise { const api = ConfigService.get('api') @@ -261,7 +261,7 @@ export function getWrappedChallenge( */ export async function associateAccountToChainArgs( accountAddress: Address, - did: DidUri, + did: Did, sign: (encodedLinkingDetails: HexString) => Promise, nBlocksValid = 10 ): Promise { diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index 0fd9a377d7..622d2d2729 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -8,7 +8,7 @@ import { ConfigService } from '@kiltprotocol/config' import { DereferenceResult, - DidUri, + Did as KiltDid, DidUrl, KiltAddress, RepresentationResolutionResult, @@ -27,13 +27,13 @@ import * as Did from '../index.js' const addressWithAuthenticationKey = '4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' -const didWithAuthenticationKey: DidUri = `did:kilt:${addressWithAuthenticationKey}` +const didWithAuthenticationKey: KiltDid = `did:kilt:${addressWithAuthenticationKey}` const addressWithAllKeys = `4sDxAgw86PFvC6TQbvZzo19WoYF6T4HcLd2i9wzvojkLXLvp` -const didWithAllKeys: DidUri = `did:kilt:${addressWithAllKeys}` +const didWithAllKeys: KiltDid = `did:kilt:${addressWithAllKeys}` const addressWithServiceEndpoints = `4q4DHavMdesaSMH3g32xH3fhxYPt5pmoP9oSwgTr73dQLrkN` -const didWithServiceEndpoints: DidUri = `did:kilt:${addressWithServiceEndpoints}` +const didWithServiceEndpoints: KiltDid = `did:kilt:${addressWithServiceEndpoints}` const deletedAddress = '4rrVTLAXgeoE8jo8si571HnqHtd5WmvLuzfH6e1xBsVXsRo7' -const deletedDid: DidUri = `did:kilt:${deletedAddress}` +const deletedDid: KiltDid = `did:kilt:${deletedAddress}` const didIsBlacklisted = ApiMocks.mockChainQueryReturn( 'did', @@ -48,7 +48,7 @@ beforeAll(() => { mockedApi = ApiMocks.getMockedApi() ConfigService.set({ api: mockedApi }) - // Mock `api.call.did.query(didUri)` + // Mock `api.call.did.query(did)` // By default it returns a simple LinkedDidInfo with no web3name and no accounts linked. jest .spyOn(mockedApi.call.did, 'query') @@ -81,7 +81,7 @@ beforeAll(() => { }) function generateAuthenticationVerificationMethod( - controller: DidUri + controller: KiltDid ): VerificationMethod { return { id: '#auth', @@ -95,7 +95,7 @@ function generateAuthenticationVerificationMethod( } function generateEncryptionVerificationMethod( - controller: DidUri + controller: KiltDid ): VerificationMethod { return { id: '#enc', @@ -109,7 +109,7 @@ function generateEncryptionVerificationMethod( } function generateAssertionVerificationMethod( - controller: DidUri + controller: KiltDid ): VerificationMethod { return { id: '#att', @@ -123,7 +123,7 @@ function generateAssertionVerificationMethod( } function generateCapabilityDelegationVerificationMethod( - controller: DidUri + controller: KiltDid ): VerificationMethod { return { id: '#del', @@ -146,10 +146,10 @@ function generateServiceEndpoint(serviceId: UriFragment): Service { } jest.mock('../Did.rpc.js') -// By default its mock returns a DIDDocument with the test authentication key, test service, and the URI derived from the identifier provided in the resolution. +// By default its mock returns a DIDDocument with the test authentication key, test service, and the DID derived from the identifier provided in the resolution. jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) return { @@ -215,7 +215,9 @@ describe('When resolving a service', () => { const serviceIdUrl: DidUrl = `${fullDid}#service-1` expect( - await Did.dereference(serviceIdUrl, { accept: 'application/did+json' }) + await Did.dereference(serviceIdUrl, { + accept: 'application/did+json', + }) ).toStrictEqual({ contentMetadata: {}, dereferencingMetadata: { contentType: 'application/did+json' }, @@ -231,7 +233,7 @@ describe('When resolving a service', () => { // Mock transform function changed to not return any services (twice). jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) return { @@ -245,7 +247,7 @@ describe('When resolving a service', () => { }) jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) return { @@ -309,7 +311,7 @@ describe('When resolving a full DID', () => { // Mock transform function changed to return all keys for the DIDDocument. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) const encMethod = generateEncryptionVerificationMethod(did) const attMethod = generateAssertionVerificationMethod(did) @@ -385,7 +387,7 @@ describe('When resolving a full DID', () => { // Mock transform function changed to return two services. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) return { @@ -441,7 +443,7 @@ describe('When resolving a full DID', () => { // Mock transform function changed to return two services. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) return { @@ -486,7 +488,7 @@ describe('When resolving a full DID', () => { augmentedApi.createType('Option', null) ) const randomKeypair = makeSigningKeyTool().authentication[0] - const randomDid = Did.getFullDidUriFromVerificationMethod({ + const randomDid = Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(randomKeypair), }) expect(await Did.resolve(randomDid)).toStrictEqual({ @@ -623,7 +625,7 @@ describe('When resolving a light DID', () => { }, }) ) - const migratedDid: DidUri = `did:kilt:light:00${addressWithAuthenticationKey}` + const migratedDid: KiltDid = `did:kilt:light:00${addressWithAuthenticationKey}` expect(await Did.resolve(migratedDid)).toStrictEqual({ didDocumentMetadata: { canonicalId: didWithAuthenticationKey }, didResolutionMetadata: {}, @@ -637,7 +639,7 @@ describe('When resolving a light DID', () => { // Mock the resolved DID as deleted. mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) - const migratedDid: DidUri = `did:kilt:light:00${deletedAddress}` + const migratedDid: KiltDid = `did:kilt:light:00${deletedAddress}` expect(await Did.resolve(migratedDid)).toStrictEqual({ didDocumentMetadata: { deactivated: true }, didResolutionMetadata: {}, @@ -680,7 +682,7 @@ describe('DID Resolution compliance', () => { }) jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) return { @@ -695,7 +697,7 @@ describe('DID Resolution compliance', () => { }) describe('resolve(did, resolutionOptions) → « didResolutionMetadata, didDocument, didDocumentMetadata »', () => { it('returns empty `didDocumentMetadata` and `didResolutionMetadata` when successfully returning a DID Document that has not been deleted nor migrated', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect(await Did.resolve(did)).toStrictEqual({ didDocumentMetadata: {}, @@ -723,7 +725,7 @@ describe('DID Resolution compliance', () => { .mockResolvedValueOnce( augmentedApi.createType('Option', null) ) - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect(await Did.resolve(did)).toStrictEqual({ didDocumentMetadata: {}, @@ -731,7 +733,7 @@ describe('DID Resolution compliance', () => { }) }) it('returns the right `didResolutionMetadata.error` when the input DID is invalid', async () => { - const did = 'did:kilt:test-did' as unknown as DidUri + const did = 'did:kilt:test-did' as unknown as KiltDid expect(await Did.resolve(did)).toStrictEqual({ didDocumentMetadata: {}, didResolutionMetadata: { error: 'invalidDid' }, @@ -741,7 +743,7 @@ describe('DID Resolution compliance', () => { describe('resolveRepresentation(did, resolutionOptions) → « didResolutionMetadata, didDocumentStream, didDocumentMetadata »', () => { it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.resolveRepresentation(did) @@ -768,7 +770,7 @@ describe('DID Resolution compliance', () => { }) }) it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+ld+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.resolveRepresentation(did, { @@ -776,7 +778,9 @@ describe('DID Resolution compliance', () => { }) ).toStrictEqual({ didDocumentMetadata: {}, - didResolutionMetadata: { contentType: Did.DID_JSON_LD_CONTENT_TYPE }, + didResolutionMetadata: { + contentType: Did.DID_JSON_LD_CONTENT_TYPE, + }, didDocumentStream: stringToU8a( JSON.stringify({ id: did, @@ -798,7 +802,7 @@ describe('DID Resolution compliance', () => { }) }) it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+cbor` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.resolveRepresentation(did, { @@ -833,7 +837,7 @@ describe('DID Resolution compliance', () => { augmentedApi.createType('Option', null) ) - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.resolveRepresentation(did) @@ -843,7 +847,7 @@ describe('DID Resolution compliance', () => { }) }) it('returns the right `didResolutionMetadata.error` when the input DID is invalid', async () => { - const did = 'did:kilt:test-did' as unknown as DidUri + const did = 'did:kilt:test-did' as unknown as KiltDid expect( await Did.resolveRepresentation(did) ).toStrictEqual({ @@ -852,7 +856,7 @@ describe('DID Resolution compliance', () => { }) }) it('returns the right `didResolutionMetadata.error` when the requested content type is not supported', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.resolveRepresentation(did, { @@ -867,7 +871,7 @@ describe('DID Resolution compliance', () => { describe('dereference(didUrl, dereferenceOptions) → « dereferencingMetadata, contentStream, contentMetadata »', () => { it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect(await Did.dereference(did)).toStrictEqual({ contentMetadata: {}, @@ -890,13 +894,15 @@ describe('DID Resolution compliance', () => { }) }) it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+ld+json` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.dereference(did, { accept: 'application/did+ld+json' }) ).toStrictEqual({ contentMetadata: {}, - dereferencingMetadata: { contentType: Did.DID_JSON_LD_CONTENT_TYPE }, + dereferencingMetadata: { + contentType: Did.DID_JSON_LD_CONTENT_TYPE, + }, contentStream: { id: did, authentication: ['#auth'], @@ -916,7 +922,7 @@ describe('DID Resolution compliance', () => { }) }) it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+cbor` representation when successfully returning a DID Document that has not been deleted nor migrated', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.dereference(did, { accept: 'application/did+cbor' }) @@ -965,7 +971,7 @@ describe('DID Resolution compliance', () => { it('returns empty `didDocumentMetadata` and `didResolutionMetadata.contentType: application/did+json` (ignoring the provided `accept` option) representation when successfully returning a service for a DID that has not been deleted nor migrated', async () => { jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: DidUri = `did:kilt:${identifier as unknown as KiltAddress}` + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` const authMethod = generateAuthenticationVerificationMethod(did) return { @@ -1006,7 +1012,7 @@ describe('DID Resolution compliance', () => { augmentedApi.createType('Option', null) ) - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect(await Did.dereference(did)).toStrictEqual({ contentMetadata: {}, @@ -1014,14 +1020,14 @@ describe('DID Resolution compliance', () => { }) }) it('returns the right `didResolutionMetadata.error` when the input DID is invalid', async () => { - const did = 'did:kilt:test-did' as unknown as DidUri + const did = 'did:kilt:test-did' as unknown as KiltDid expect(await Did.dereference(did)).toStrictEqual({ contentMetadata: {}, dereferencingMetadata: { error: 'invalidDidUrl' }, }) }) it('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+json` (the default value) when the `options.accept` value is invalid', async () => { - const did: DidUri = + const did: KiltDid = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' expect( await Did.dereference(did, { diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 55fc7e80ff..d3659f9a81 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -12,7 +12,7 @@ import type { DereferenceResult, DidDocument, DidResolver, - DidUri, + Did, DidUrl, FailedDereferenceMetadata, JsonLd, @@ -29,7 +29,7 @@ import { cbor } from '@kiltprotocol/utils' import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' import { linkedInfoFromChain } from '../Did.rpc.js' import { toChain } from '../Did.chain.js' -import { getFullDidUri, parse, validateIdentifier } from '../Did.utils.js' +import { getFullDid, parse, validateDid } from '../Did.utils.js' import { parseDocumentFromLightDid } from '../DidDetails/LightDidDetails.js' import { isValidVerificationRelationship } from '../DidDetails/DidDetails.js' @@ -59,7 +59,7 @@ type InternalResolutionResult = { } async function resolveInternal( - did: DidUri + did: Did ): Promise { const { type } = parse(did) const api = ConfigService.get('api') @@ -99,7 +99,7 @@ async function resolveInternal( if (document !== undefined) { return { documentMetadata: { - canonicalId: getFullDidUri(did), + canonicalId: getFullDid(did), }, document: { id: lightDocument.id, @@ -124,12 +124,12 @@ async function resolveInternal( * @returns The resolution result for the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). */ export async function resolve( - did: DidUri, + did: Did, // eslint-disable-next-line @typescript-eslint/no-unused-vars resolutionOptions: ResolutionOptions = {} ): Promise { try { - validateIdentifier(did, 'Uri') + validateDid(did, 'Did') } catch (error) { return { didResolutionMetadata: { @@ -170,7 +170,7 @@ export async function resolve( * @returns The resolution result for the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). */ export async function resolveRepresentation( - did: DidUri, + did: Did, { accept }: DereferenceOptions = { accept: DID_JSON_CONTENT_TYPE, } @@ -247,7 +247,7 @@ export function isFailedDereferenceMetadata( } async function dereferenceInternal( - didUrl: DidUri | DidUrl, + didUrl: Did | DidUrl, // eslint-disable-next-line @typescript-eslint/no-unused-vars dereferenceOptions: DereferenceOptions ): Promise { @@ -340,7 +340,7 @@ async function dereferenceInternal( * @returns The resolution result for the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). */ export async function dereference( - didUrl: DidUri | DidUrl, + didUrl: Did | DidUrl, // eslint-disable-next-line @typescript-eslint/no-unused-vars { accept }: DereferenceOptions = { accept: DID_JSON_CONTENT_TYPE, @@ -352,7 +352,7 @@ export async function dereference( : DID_JSON_CONTENT_TYPE try { - validateIdentifier(didUrl) + validateDid(didUrl) } catch (error) { return { dereferencingMetadata: { diff --git a/packages/legacy-credentials/src/Claim.spec.ts b/packages/legacy-credentials/src/Claim.spec.ts index ba8b7f991c..796f33c146 100644 --- a/packages/legacy-credentials/src/Claim.spec.ts +++ b/packages/legacy-credentials/src/Claim.spec.ts @@ -6,7 +6,7 @@ */ import { CType } from '@kiltprotocol/core' -import type { DidUri, ICType, IClaim } from '@kiltprotocol/types' +import type { Did, ICType, IClaim } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' import * as Claim from './Claim' @@ -104,7 +104,7 @@ describe('compute hashes & validate by reproducing them', () => { }) describe('Claim', () => { - let did: DidUri + let did: Did let claimContents: any let testCType: ICType let claim: IClaim diff --git a/packages/legacy-credentials/src/Claim.ts b/packages/legacy-credentials/src/Claim.ts index c9273b5b94..4f515394fc 100644 --- a/packages/legacy-credentials/src/Claim.ts +++ b/packages/legacy-credentials/src/Claim.ts @@ -20,7 +20,7 @@ import { CType } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import type { - DidUri, + Did as KiltDid, HexString, ICType, IClaim, @@ -143,7 +143,7 @@ export function verifyDataStructure(input: IClaim | PartialClaim): void { throw new SDKErrors.CTypeHashMissingError() } if ('owner' in input) { - Did.validateIdentifier(input.owner, 'Uri') + Did.validateDid(input.owner, 'Did') } if (input.contents !== undefined) { Object.entries(input.contents).forEach(([key, value]) => { @@ -184,7 +184,7 @@ export function fromNestedCTypeClaim( cTypeInput: ICType, nestedCType: ICType[], claimContents: IClaim['contents'], - claimOwner: DidUri + claimOwner: KiltDid ): IClaim { CType.verifyClaimAgainstNestedSchemas(cTypeInput, nestedCType, claimContents) @@ -198,7 +198,7 @@ export function fromNestedCTypeClaim( } /** - * Constructs a new Claim from the given [[ICType]], IClaim['contents'] and [[DidUri]]. + * Constructs a new Claim from the given [[ICType]], IClaim['contents'] and [[Did]]. * * @param cType [[ICType]] for which the Claim will be built. * @param claimContents IClaim['contents'] to be used as the pure contents of the instantiated Claim. @@ -208,7 +208,7 @@ export function fromNestedCTypeClaim( export function fromCTypeAndClaimContents( cType: ICType, claimContents: IClaim['contents'], - claimOwner: DidUri + claimOwner: KiltDid ): IClaim { CType.verifyDataStructure(cType) CType.verifyClaimAgainstSchema(claimContents, cType) diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 30cce1e861..c5bc9f12ef 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -16,7 +16,7 @@ import type { DereferenceResult, DidDocument, DidSignature, - DidUri, + Did as KiltDid, DidUrl, IAttestation, IClaim, @@ -50,7 +50,7 @@ const testCType = CType.fromProperties('Credential', { }) function buildCredential( - claimerDid: DidUri, + claimerDid: KiltDid, contents: IClaimContents, legitimations: ICredential[] ): ICredential { @@ -444,7 +444,7 @@ describe('Presentations', () => { let migratedAndDeletedLightDid: DidDocument async function dereferenceDidUrl( - didUrl: DidUrl | DidUri + didUrl: DidUrl | KiltDid ): Promise> { const { did } = Did.parse(didUrl) const didDocument = [ @@ -468,7 +468,7 @@ describe('Presentations', () => { // TODO: Cleanup file by migrating setup functions and removing duplicate tests. async function buildPresentation( claimer: DidDocument, - attesterDid: DidUri, + attesterDid: KiltDid, contents: IClaim['contents'], legitimations: ICredential[], sign: SignCallback @@ -748,7 +748,7 @@ describe('create presentation', () => { lightDidForId: DidDocument, newAuthenticationKey?: NewDidVerificationKey ): DidDocument { - const id = Did.getFullDidUri(lightDidForId.id) + const id = Did.getFullDid(lightDidForId.id) const authMethod = (() => { if (newAuthenticationKey !== undefined) { return didKeyToVerificationMethod( @@ -780,7 +780,7 @@ describe('create presentation', () => { } async function dereferenceDidUrl( - didUrl: DidUrl | DidUri + didUrl: DidUrl | KiltDid ): Promise> { const { did } = Did.parse(didUrl) switch (did) { diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 1f6a7810ab..54ed56cead 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -27,7 +27,7 @@ import { verifyDidSignature, } from '@kiltprotocol/did' import type { - DidUri, + Did, Hash, IAttestation, ICType, @@ -346,7 +346,7 @@ export function verifyAgainstAttestation( * @returns An object containing the `attester` DID and `revoked` status of the on-chain attestation. */ export async function verifyAttested(credential: ICredential): Promise<{ - attester: DidUri + attester: Did revoked: boolean }> { const api = ConfigService.get('api') @@ -366,7 +366,7 @@ export async function verifyAttested(credential: ICredential): Promise<{ export interface VerifiedCredential extends ICredential { revoked: boolean - attester: DidUri + attester: Did } /** diff --git a/packages/types/src/AssetDid.ts b/packages/types/src/AssetDid.ts index ecb9a52ef1..5f5c35439c 100644 --- a/packages/types/src/AssetDid.ts +++ b/packages/types/src/AssetDid.ts @@ -40,4 +40,4 @@ export type Caip19AssetId = /** * A string containing an AssetDID as per the [AssetDID specification](https://github.com/KILTprotocol/spec-asset-did). */ -export type AssetDidUri = `did:asset:${Caip2ChainId}.${Caip19AssetId}` +export type AssetDid = `did:asset:${Caip2ChainId}.${Caip19AssetId}` diff --git a/packages/types/src/Attestation.ts b/packages/types/src/Attestation.ts index e39fd72503..2b4d582702 100644 --- a/packages/types/src/Attestation.ts +++ b/packages/types/src/Attestation.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from './Did' +import type { Did } from './Did' import type { IDelegationNode } from './Delegation' import type { ICredential } from './Credential' import type { CTypeHash } from './CType' @@ -13,7 +13,7 @@ import type { CTypeHash } from './CType' export interface IAttestation { claimHash: ICredential['rootHash'] cTypeHash: CTypeHash - owner: DidUri + owner: Did delegationId: IDelegationNode['id'] | null revoked: boolean } diff --git a/packages/types/src/Claim.ts b/packages/types/src/Claim.ts index 3ebbd808da..6a7b341ffe 100644 --- a/packages/types/src/Claim.ts +++ b/packages/types/src/Claim.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { DidUri } from './Did' +import type { Did } from './Did' type ClaimPrimitives = string | number | boolean @@ -20,7 +20,7 @@ export interface IClaimContents { export interface IClaim { cTypeHash: CTypeHash contents: IClaimContents - owner: DidUri + owner: Did } /** diff --git a/packages/types/src/CryptoCallbacks.ts b/packages/types/src/CryptoCallbacks.ts index eb25667394..a48916696c 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -6,7 +6,7 @@ */ import type { - DidUri, + Did, SignatureVerificationRelationship, VerificationMethod, } from './Did' @@ -28,7 +28,7 @@ export interface SignRequestData { /** * The DID to be used for signing. */ - did: DidUri + did: Did } /** @@ -74,7 +74,7 @@ export interface EncryptRequestData { /** * The DID to be used for encryption. */ - did: DidUri + did: Did } /** diff --git a/packages/types/src/Delegation.ts b/packages/types/src/Delegation.ts index 1bc057e5de..2139eb6115 100644 --- a/packages/types/src/Delegation.ts +++ b/packages/types/src/Delegation.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { DidUri } from './Did' +import type { Did } from './Did' /* eslint-disable no-bitwise */ export const Permission = { @@ -20,7 +20,7 @@ export interface IDelegationNode { hierarchyId: IDelegationNode['id'] parentId?: IDelegationNode['id'] childrenIds: Array - account: DidUri + account: Did permissions: PermissionType[] revoked: boolean } diff --git a/packages/types/src/Did.ts b/packages/types/src/Did.ts index e7b63fc4a3..fc0d458a0a 100644 --- a/packages/types/src/Did.ts +++ b/packages/types/src/Did.ts @@ -8,18 +8,18 @@ import type { KiltAddress } from './Address' type AuthenticationKeyType = '00' | '01' -type DidUriVersion = '' | `v${string}:` -type LightDidEncodedData = '' | `:${string}` +type DidVersion = '' | `v${string}:` +type LightDidDocumentEncodedData = '' | `:${string}` /** - * A string containing a KILT DID Uri. + * A string containing a KILT DID. */ -export type DidUri = - | `did:kilt:${DidUriVersion}${KiltAddress}` - | `did:kilt:light:${DidUriVersion}${AuthenticationKeyType}${KiltAddress}${LightDidEncodedData}` +export type Did = + | `did:kilt:${DidVersion}${KiltAddress}` + | `did:kilt:light:${DidVersion}${AuthenticationKeyType}${KiltAddress}${LightDidDocumentEncodedData}` /** - * The fragment part of the DID URI including the `#` character. + * The fragment part of the DID including the `#` character. */ export type UriFragment = `#${string}` @@ -27,9 +27,9 @@ export type UriFragment = `#${string}` * URL for DID resources like keys or services. */ export type DidUrl = - | `${DidUri}${UriFragment}` + | `${Did}${UriFragment}` // Very broad type definition, mostly for the compiler. Actual regex matching for query params is done where needed. - | `${DidUri}?{string}${UriFragment}` + | `${Did}?{string}${UriFragment}` export type SignatureVerificationRelationship = | 'authentication' @@ -64,7 +64,7 @@ export type VerificationMethod = { /** * The controller of the verification method. */ - controller: DidUri + controller: Did /* * The multicodec-prefixed, multibase-encoded verification method's public key. */ @@ -90,7 +90,7 @@ export type Service = { } export type DidDocument = { - id: DidUri + id: Did alsoKnownAs?: string[] verificationMethod?: VerificationMethod[] authentication?: UriFragment[] diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index 975d81672b..9fc56d9b64 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -6,7 +6,7 @@ */ import type { - DidUri, + Did, DidDocument, DidUrl, VerificationMethod, @@ -45,7 +45,7 @@ export type ResolutionDocumentMetadata = { * The relationship is a statement that the canonicalId value is logically equivalent to the id property value and that the canonicalId value is defined by the DID method to be the canonical ID for the DID subject in the scope of the containing DID document. * A canonicalId value MUST be produced by, and a form of, the same DID method as the id property value. (e.g., did:example:abc == did:example:ABC). */ - canonicalId?: DidUri + canonicalId?: Did } export type ResolutionResult = { @@ -140,7 +140,7 @@ export interface ResolveDid { * This is the DID to resolve. * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. */ - did: DidUri, + did: Did, /** * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. * This input is REQUIRED, but the structure MAY be empty. @@ -153,7 +153,7 @@ export interface ResolveDid { * This is the DID to resolve. * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. */ - did: DidUri, + did: Did, /** * A metadata structure containing properties defined in 7.1.1 DID Resolution Options. * This input is REQUIRED, but the structure MAY be empty. @@ -239,7 +239,7 @@ export interface DereferenceDidUrl { * This is the DID URL to dereference. * To dereference a DID fragment, the complete DID URL including the DID fragment MUST be used. This input is REQUIRED. */ - didUrl: DidUri | DidUrl, + didUrl: Did | DidUrl, /** * A metadata structure consisting of input options to the dereference function in addition to the didUrl itself. * Properties defined by this specification are in 7.2.1 DID URL Dereferencing Options. diff --git a/packages/types/src/PublicCredential.ts b/packages/types/src/PublicCredential.ts index b9aca0f014..25f8d67144 100644 --- a/packages/types/src/PublicCredential.ts +++ b/packages/types/src/PublicCredential.ts @@ -9,11 +9,11 @@ import type { HexString, BN } from './Imported' import type { CTypeHash } from './CType' import type { IDelegationNode } from './Delegation' import type { IClaimContents } from './Claim' -import type { DidUri } from './Did' -import type { AssetDidUri } from './AssetDid' +import type { Did } from './Did' +import type { AssetDid } from './AssetDid' /* - * The minimal information required to issue a public credential to a given [[AssetDidUri]]. + * The minimal information required to issue a public credential to a given [[AssetDid]]. */ export interface IPublicCredentialInput { /* @@ -27,7 +27,7 @@ export interface IPublicCredentialInput { /* * The subject of the credential. */ - subject: AssetDidUri + subject: AssetDid /* * The content of the credential. The structure must match what the CType specifies. */ @@ -41,13 +41,13 @@ export interface IPublicCredential extends IPublicCredentialInput { /* * The unique ID of the credential. It is cryptographically derived from the credential content. * - * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[DidUri]] and then Blake2b hashing the result. + * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[Did]] and then Blake2b hashing the result. */ id: HexString /* - * The KILT DID uri of the credential attester. + * The KILT DID of the credential attester. */ - attester: DidUri + attester: Did /* * The block number at which the credential was issued. */ @@ -63,12 +63,12 @@ export interface IPublicCredential extends IPublicCredentialInput { /* * A claim for a public credential. * - * Like an [[IClaim]], but with a [[AssetDidUri]] `subject` instead of an [[IClaim]] `owner`. + * Like an [[IClaim]], but with a [[AssetDid]] `subject` instead of an [[IClaim]] `owner`. */ export interface IAssetClaim { cTypeHash: CTypeHash contents: IClaimContents - subject: AssetDidUri + subject: AssetDid } /** diff --git a/packages/utils/src/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index c9673eba54..cf9bec1697 100644 --- a/packages/utils/src/SDKErrors.ts +++ b/packages/utils/src/SDKErrors.ts @@ -116,7 +116,7 @@ export class SignatureMalformedError extends SDKError {} export class DidSubjectMismatchError extends SDKError { constructor(actual: string, expected: string) { super( - `The DID "${actual}" doesn't match the DID Document's URI "${expected}"` + `The DID "${actual}" doesn't match the DID Document's id "${expected}"` ) } } diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index ef1244e788..ef2db0cbd2 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -19,7 +19,7 @@ import { } from '@kiltprotocol/did' import type { DidDocument, - DidUri, + Did, ICType, VerificationMethod, } from '@kiltprotocol/types' @@ -92,7 +92,7 @@ type LegacyVerificationMethod = Pick< // Returns legacy representations of a KILT DID verification method. export const kiltDidLoader: DocumentLoader = async (url) => { - const { did } = parse(url as DidUri) + const { did } = parse(url as Did) const { didDocument: resolvedDidDocument } = await resolveDid(did) const didDocument = (() => { if (resolvedDidDocument === undefined) { @@ -133,7 +133,7 @@ export const kiltDidLoader: DocumentLoader = async (url) => { return doc })() - // Framing can help us resolve to the requested resource (did or did uri). This way we return either a key or the full DID document, depending on what was requested. + // Framing can help us resolve to the requested resource (did or did url). This way we return either a key or the full DID document, depending on what was requested. const jsonLdDocument = (await jsonld.frame( didDocument, { diff --git a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts index 49e5e68a24..fe7050687b 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts @@ -12,7 +12,11 @@ import { base58Encode } from '@polkadot/util-crypto' import { Types, init, W3C_CREDENTIAL_CONTEXT_URL } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import { Crypto } from '@kiltprotocol/utils' -import type { DidDocument, DidUri, KiltKeyringPair } from '@kiltprotocol/types' +import type { + DidDocument, + Did as KiltDid, + KiltKeyringPair, +} from '@kiltprotocol/types' import { combineDocumentLoaders, @@ -41,17 +45,17 @@ export async function makeFakeDid() { await init() const keypair = Crypto.makeKeypairFromUri('//Ingo', 'sr25519') const didDocument: DidDocument = { - id: ingosCredential.credentialSubject.id as DidUri, + id: ingosCredential.credentialSubject.id as KiltDid, authentication: ['#authentication'], assertionMethod: ['#assertion'], verificationMethod: [ Did.didKeyToVerificationMethod( - ingosCredential.credentialSubject.id as DidUri, + ingosCredential.credentialSubject.id as KiltDid, '#authentication', { ...keypair, keyType: keypair.type } ), Did.didKeyToVerificationMethod( - ingosCredential.credentialSubject.id as DidUri, + ingosCredential.credentialSubject.id as KiltDid, '#assertion', { ...keypair, keyType: keypair.type } ), diff --git a/packages/vc-export/src/suites/types.ts b/packages/vc-export/src/suites/types.ts index d14ae644d4..7e5362515e 100644 --- a/packages/vc-export/src/suites/types.ts +++ b/packages/vc-export/src/suites/types.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from '@kiltprotocol/types' +import type { Did } from '@kiltprotocol/types' export interface JSigsSigner { sign: (data: { data: Uint8Array }) => Promise @@ -24,5 +24,5 @@ export interface JSigsVerificationResult { verified: boolean error?: Error purposeResult?: { verified: boolean; error?: Error } - verificationMethod?: { id: string; type: string; controller: DidUri } + verificationMethod?: { id: string; type: string; controller: Did } } diff --git a/tests/breakingChanges/BreakingChanges.spec.ts b/tests/breakingChanges/BreakingChanges.spec.ts index 15a4c4d223..7c473b709b 100644 --- a/tests/breakingChanges/BreakingChanges.spec.ts +++ b/tests/breakingChanges/BreakingChanges.spec.ts @@ -40,7 +40,7 @@ function makeLightDidFromSeed(seed: string) { describe('Breaking Changes', () => { describe('Light DID', () => { - it('does not break the light did uri generation', () => { + it('does not break the light did generation', () => { const { did } = makeLightDidFromSeed( '0x127f2375faf3472c2f94ffcdd5424590b27294631f2cb8041407e501bc97c44c' ) diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 028accdd59..7312b3193f 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -120,7 +120,7 @@ async function createFullDidFromKeypair( const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(keypair), }) ) @@ -197,7 +197,7 @@ async function runAll() { const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(keypair), }) ) diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index b0d039160d..f4499c3188 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -104,14 +104,15 @@ describe('write and didDeleteTx', () => { await submitTx(tx, paymentAccount) - const fullDidUri = Did.getFullDidUriFromVerificationMethod({ + const fullDid = Did.getFullDidFromVerificationMethod({ publicKeyMultibase, }) - const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDidUri)) - const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDid)) + const { document: fullDidDocument } = + Did.linkedInfoFromChain(fullDidLinkedInfo) - expect(fullDid).toMatchObject(>{ - id: fullDidUri, + expect(fullDidDocument).toMatchObject(>{ + id: fullDid, service: [ { id: '#test-id-1', @@ -126,7 +127,7 @@ describe('write and didDeleteTx', () => { ], verificationMethod: [ expect.objectContaining(>{ - controller: fullDidUri, + controller: fullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey({ @@ -136,14 +137,14 @@ describe('write and didDeleteTx', () => { }), ], }) - expect(fullDid.authentication).toHaveLength(1) - expect(fullDid.keyAgreement).toBe(undefined) - expect(fullDid.assertionMethod).toBe(undefined) - expect(fullDid.capabilityDelegation).toBe(undefined) + expect(fullDidDocument.authentication).toHaveLength(1) + expect(fullDidDocument.keyAgreement).toBe(undefined) + expect(fullDidDocument.assertionMethod).toBe(undefined) + expect(fullDidDocument.capabilityDelegation).toBe(undefined) }, 60_000) it('should return no results for empty accounts', async () => { - const emptyDid = Did.getFullDidUri(makeSigningKeyTool().keypair.address) + const emptyDid = Did.getFullDid(makeSigningKeyTool().keypair.address) const encodedDid = Did.toChain(emptyDid) expect((await api.call.did.query(encodedDid)).isSome).toBe(false) @@ -152,7 +153,7 @@ describe('write and didDeleteTx', () => { it('fails to delete the DID using a different submitter than the one specified in the DID operation or using a services count that is too low', async () => { // We verify that the DID to delete is on chain. const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(did.id)) + Did.toChain(Did.getFullDid(did.id)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() @@ -197,7 +198,7 @@ describe('write and didDeleteTx', () => { it('deletes DID from previous step', async () => { // We verify that the DID to delete is on chain. const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(did.id)) + Did.toChain(Did.getFullDid(did.id)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() @@ -242,7 +243,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { // This will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. let fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(newDid.id)) + Did.toChain(Did.getFullDid(newDid.id)) ) let { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -262,7 +263,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { // Authentication key changed, so did must be updated. // Also this will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(newDid.id)) + Did.toChain(Did.getFullDid(newDid.id)) ) fullDid = Did.linkedInfoFromChain(fullDidLinkedInfo).document @@ -341,45 +342,47 @@ describe('DID migration', () => { ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.id) + const migratedFullDid = Did.getFullDid(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDidUri) + Did.toChain(migratedFullDid) ) - const { document: migratedFullDid } = Did.linkedInfoFromChain( + const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject(>{ - id: migratedFullDidUri, + expect(migratedFullDidDocument).toMatchObject(>{ + id: migratedFullDid, verificationMethod: [ expect.objectContaining(>{ - controller: migratedFullDidUri, + controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), expect.objectContaining(>{ - controller: migratedFullDidUri, + controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), }), ], }) - expect(migratedFullDid.authentication).toHaveLength(1) - expect(migratedFullDid.keyAgreement).toHaveLength(1) - expect(migratedFullDid.assertionMethod).toBe(undefined) - expect(migratedFullDid.capabilityDelegation).toBe(undefined) + expect(migratedFullDidDocument.authentication).toHaveLength(1) + expect(migratedFullDidDocument.keyAgreement).toHaveLength(1) + expect(migratedFullDidDocument.assertionMethod).toBe(undefined) + expect(migratedFullDidDocument.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.toChain(migratedFullDid.id))).isSome + (await api.call.did.query(Did.toChain(migratedFullDidDocument.id))).isSome ).toBe(true) const { didDocumentMetadata } = (await Did.resolve( lightDid.id )) as ResolutionResult - expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.canonicalId).toStrictEqual( + migratedFullDidDocument.id + ) expect(didDocumentMetadata.deactivated).toBe(undefined) }) @@ -396,39 +399,41 @@ describe('DID migration', () => { ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.id) + const migratedFullDid = Did.getFullDid(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDidUri) + Did.toChain(migratedFullDid) ) - const { document: migratedFullDid } = Did.linkedInfoFromChain( + const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject(>{ - id: migratedFullDidUri, + expect(migratedFullDidDocument).toMatchObject(>{ + id: migratedFullDid, verificationMethod: [ expect.objectContaining(>{ - controller: migratedFullDidUri, + controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), ], }) - expect(migratedFullDid.authentication).toHaveLength(1) - expect(migratedFullDid.keyAgreement).toBe(undefined) - expect(migratedFullDid.assertionMethod).toBe(undefined) - expect(migratedFullDid.capabilityDelegation).toBe(undefined) + expect(migratedFullDidDocument.authentication).toHaveLength(1) + expect(migratedFullDidDocument.keyAgreement).toBe(undefined) + expect(migratedFullDidDocument.assertionMethod).toBe(undefined) + expect(migratedFullDidDocument.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.toChain(migratedFullDid.id))).isSome + (await api.call.did.query(Did.toChain(migratedFullDidDocument.id))).isSome ).toBe(true) const { didDocumentMetadata } = (await Did.resolve( lightDid.id )) as ResolutionResult - expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.canonicalId).toStrictEqual( + migratedFullDidDocument.id + ) expect(didDocumentMetadata.deactivated).toBe(undefined) }) @@ -457,25 +462,25 @@ describe('DID migration', () => { ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.id) + const migratedFullDid = Did.getFullDid(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDidUri) + Did.toChain(migratedFullDid) ) - const { document: migratedFullDid } = Did.linkedInfoFromChain( + const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject(>{ - id: migratedFullDidUri, + expect(migratedFullDidDocument).toMatchObject(>{ + id: migratedFullDid, verificationMethod: [ expect.objectContaining(>{ - controller: migratedFullDidUri, + controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), expect.objectContaining(>{ - controller: migratedFullDidUri, + controller: migratedFullDid, type: 'Multikey', // We cannot match the ID of the key because it will be defined by the blockchain while saving publicKeyMultibase: Did.keypairToMultibaseKey(keyAgreement[0]), @@ -489,19 +494,21 @@ describe('DID migration', () => { }, ], }) - expect(migratedFullDid.authentication).toHaveLength(1) - expect(migratedFullDid.keyAgreement).toHaveLength(1) - expect(migratedFullDid.assertionMethod).toBe(undefined) - expect(migratedFullDid.capabilityDelegation).toBe(undefined) + expect(migratedFullDidDocument.authentication).toHaveLength(1) + expect(migratedFullDidDocument.keyAgreement).toHaveLength(1) + expect(migratedFullDidDocument.assertionMethod).toBe(undefined) + expect(migratedFullDidDocument.capabilityDelegation).toBe(undefined) - const encodedDid = Did.toChain(migratedFullDid.id) + const encodedDid = Did.toChain(migratedFullDidDocument.id) expect((await api.call.did.query(encodedDid)).isSome).toBe(true) const { didDocumentMetadata } = (await Did.resolve( lightDid.id )) as ResolutionResult - expect(didDocumentMetadata.canonicalId).toStrictEqual(migratedFullDid.id) + expect(didDocumentMetadata.canonicalId).toStrictEqual( + migratedFullDidDocument.id + ) expect(didDocumentMetadata.deactivated).toBe(undefined) // Remove and claim the deposit back @@ -539,7 +546,7 @@ describe('DID authorization', () => { await submitTx(createTx, paymentAccount) const didLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }) ) @@ -649,7 +656,7 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }) ) @@ -753,7 +760,7 @@ describe('DID management batching', () => { const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), }) ) @@ -828,7 +835,7 @@ describe('DID management batching', () => { const initialFullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }) ) @@ -912,7 +919,7 @@ describe('DID management batching', () => { const initialFullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }) ) @@ -1008,7 +1015,7 @@ describe('DID management batching', () => { await submitTx(tx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }) ) @@ -1092,7 +1099,7 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( Did.toChain( - Did.getFullDidUriFromVerificationMethod({ + Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }) ) diff --git a/tests/integration/PublicCredentials.spec.ts b/tests/integration/PublicCredentials.spec.ts index b4989f53ab..4de6374a65 100644 --- a/tests/integration/PublicCredentials.spec.ts +++ b/tests/integration/PublicCredentials.spec.ts @@ -6,7 +6,7 @@ */ import type { - AssetDidUri, + AssetDid, DidDocument, HexString, IPublicCredential, @@ -42,7 +42,7 @@ let attesterKey: KeyTool let api: ApiPromise // Generate a random asset ID -let assetId: AssetDidUri = `did:asset:eip155:1.erc20:${randomAsHex(20)}` +let assetId: AssetDid = `did:asset:eip155:1.erc20:${randomAsHex(20)}` let latestCredential: IPublicCredentialInput async function issueCredential( @@ -345,7 +345,7 @@ describe('When there is an issued public credential', () => { const credentialWithDifferentSubject = { ...credential, subject: - 'did:asset:eip155:1.erc721:0x6d19295A5E47199D823D8793942b21a256ef1A4d' as AssetDidUri, + 'did:asset:eip155:1.erc721:0x6d19295A5E47199D823D8793942b21a256ef1A4d' as AssetDid, } await expect( PublicCredentials.verifyCredential(credentialWithDifferentSubject) @@ -383,7 +383,7 @@ describe('When there is an issued public credential', () => { it('should not be verified when another party receives it if it has different attester info', async () => { const credentialWithDifferentAttester = { ...credential, - attester: Did.getFullDidUri(devAlice.address), + attester: Did.getFullDid(devAlice.address), } await expect( PublicCredentials.verifyCredential(credentialWithDifferentAttester) diff --git a/tests/integration/Web3Names.spec.ts b/tests/integration/Web3Names.spec.ts index 0cfc576c78..2904bab2ca 100644 --- a/tests/integration/Web3Names.spec.ts +++ b/tests/integration/Web3Names.spec.ts @@ -91,14 +91,14 @@ describe('When there is an Web3NameCreator and a payer', () => { await submitTx(authorizedTx, paymentAccount) }, 30_000) - it('should be possible to lookup the DID uri with the given nick', async () => { + it('should be possible to lookup the DID with the given nick', async () => { const { document: { id }, } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name(nick)) expect(id).toStrictEqual(w3nCreator.id) }, 30_000) - it('should be possible to lookup the nick with the given DID uri', async () => { + it('should be possible to lookup the nick with the given DID', async () => { const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.id)) const didInfo = Did.linkedInfoFromChain(encodedDidInfo) expect(didInfo.document.alsoKnownAs).toStrictEqual([`w3n:${nick}`]) diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 33bd219b44..074dc89215 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -311,7 +311,7 @@ export async function createLocalDemoFullDidFromKeypair( publicKey, id: authKeyId, } = makeDidKeyFromKeypair(keypair) - const id = Did.getFullDidUriFromVerificationMethod({ + const id = Did.getFullDidFromVerificationMethod({ publicKeyMultibase: Did.keypairToMultibaseKey({ type: keyType, publicKey,