diff --git a/packages/asset-credentials/src/credentials/PublicCredential.chain.ts b/packages/asset-credentials/src/credentials/PublicCredential.chain.ts index 6309ea71fc..9f96a76060 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 { - AssetDid, + AssetDidUri, CTypeHash, IDelegationNode, IPublicCredentialInput, IPublicCredential, - Did, + DidUri, 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 { validateDid } from '../dids/index.js' +import { validateUri } from '../dids/index.js' export interface EncodedPublicCredential { ctypeHash: CTypeHash - subject: AssetDid + subject: AssetDidUri claims: HexString authorization: IDelegationNode['id'] | null } @@ -68,12 +68,12 @@ function credentialInputFromChain({ subject, }: PublicCredentialsCredentialsCredential): IPublicCredentialInput { const credentialSubject = subject.toUtf8() - validateDid(credentialSubject) + validateUri(credentialSubject) return { claims: cbor.decode(claims), cTypeHash: ctypeHash.toHex(), delegationId: authorization.unwrapOr(undefined)?.toHex() ?? null, - subject: credentialSubject as AssetDid, + subject: credentialSubject as AssetDidUri, } } @@ -86,9 +86,9 @@ export interface PublicCredentialEntry { */ ctypeHash: HexString /** - * DID of the attester. + * DID URI of the attester. */ - attester: Did + attester: DidUri /** * 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 [[AssetDid]]. + * This is the **only** secure way for users to retrieve and verify all the credentials issued to a given [[AssetDidUri]]. * * @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: AssetDid + subject: AssetDidUri ): 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 b93a3ebb38..29f3d53e9f 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 { - AssetDid, - Did as KiltDid, + AssetDidUri, + DidUri, 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: AssetDid, + assetDid: AssetDidUri, contents: IClaimContents ): IPublicCredential { const claim: IAssetClaim = { @@ -48,7 +48,7 @@ function buildCredential( subject: assetDid, } const credential = PublicCredential.fromClaim(claim) - const attester: KiltDid = Did.getFullDid(devAlice.address) + const attester: DidUri = Did.getFullDidUri(devAlice.address) return { ...credential, attester, diff --git a/packages/asset-credentials/src/credentials/PublicCredential.ts b/packages/asset-credentials/src/credentials/PublicCredential.ts index 404b19c32a..9972b5aaa3 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, - Did as KiltDid, + DidUri, 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 [[Did]] and then Blake2b hashing the result. + * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[DidUri]] 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: KiltDid + attester: DidUri ): HexString { const api = ConfigService.get('api') @@ -62,7 +62,7 @@ function verifyClaimStructure(input: IAssetClaim | PartialAssetClaim): void { throw new SDKErrors.CTypeHashMissingError() } if (input.subject) { - AssetDid.validateDid(input.subject) + AssetDid.validateUri(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 2a86128c77..395c2067b4 100644 --- a/packages/asset-credentials/src/dids/index.ts +++ b/packages/asset-credentials/src/dids/index.ts @@ -6,7 +6,7 @@ */ import type { - AssetDid, + AssetDidUri, 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 = { - did: AssetDid + uri: AssetDidUri chainId: Caip2ChainId chainNamespace: Caip2ChainNamespace chainReference: Caip2ChainReference @@ -33,35 +33,35 @@ type IAssetDidParsingResult = { } /** - * Parses an AssetDID and returns the information contained within in a structured form. + * Parses an AssetDID uri and returns the information contained within in a structured form. - * @param assetDid An AssetDID as a string. -* @returns Object containing information extracted from the AssetDID. + * @param assetDidUri An AssetDID uri as a string. +* @returns Object containing information extracted from the AssetDID uri. */ -export function parse(assetDid: AssetDid): IAssetDidParsingResult { - const matches = ASSET_DID_REGEX.exec(assetDid)?.groups +export function parse(assetDidUri: AssetDidUri): IAssetDidParsingResult { + const matches = ASSET_DID_REGEX.exec(assetDidUri)?.groups if (!matches) { - throw new SDKErrors.InvalidDidFormatError(assetDid) + throw new SDKErrors.InvalidDidFormatError(assetDidUri) } - const { chainId, assetId } = matches as Omit + const { chainId, assetId } = matches as Omit return { - ...(matches as Omit), - did: `did:asset:${chainId}.${assetId}`, + ...(matches as Omit), + uri: `did:asset:${chainId}.${assetId}`, } } /** - * Checks that a string (or other input) is a valid AssetDID. + * Checks that a string (or other input) is a valid AssetDID uri. * Throws otherwise. * * @param input Arbitrary input. */ -export function validateDid(input: unknown): void { +export function validateUri(input: unknown): void { if (typeof input !== 'string') { throw new TypeError(`Asset DID string expected, got ${typeof input}`) } - parse(input as AssetDid) + parse(input as AssetDidUri) } diff --git a/packages/core/src/attestation/Attestation.spec.ts b/packages/core/src/attestation/Attestation.spec.ts index 792623d054..855ec2e28e 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, Did, IAttestation } from '@kiltprotocol/types' +import type { CTypeHash, DidUri, IAttestation } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' import { ApiMocks } from '../../../../tests/testUtils' @@ -22,7 +22,7 @@ beforeAll(() => { }) describe('Attestation', () => { - const identityAlice: Did = + const identityAlice: DidUri = 'did:kilt:4nwPAmtsK5toZfBM9WvmAe4Fa3LyZ3X3JHt7EUFfrcPPAZAm' const cTypeHash: CTypeHash = diff --git a/packages/core/src/attestation/Attestation.ts b/packages/core/src/attestation/Attestation.ts index 76c339775a..59311723ee 100644 --- a/packages/core/src/attestation/Attestation.ts +++ b/packages/core/src/attestation/Attestation.ts @@ -9,7 +9,7 @@ import type { IAttestation, IDelegationHierarchyDetails, ICredential, - Did as KiltDid, + DidUri, } 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.validateDid(input.owner, 'Did') + Did.validateUri(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: KiltDid + attesterDid: DidUri ): 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 b0cbc04ecb..10d221880c 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 { Did } from '@kiltprotocol/types' +import type { DidUri } from '@kiltprotocol/types' import { attestation, @@ -77,7 +77,7 @@ describe('proofs', () => { }) it('checks delegation node owners', async () => { - const delegator: Did = `did:kilt:${encodeAddress(randomAsU8a(32), 38)}` + const delegator: DidUri = `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 f4942a1168..1861eb0367 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, - getFullDid, - validateDid, + getFullDidUri, + validateUri, 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 { - Did, + DidUri, ICType, IDelegationNode, KiltAddress, @@ -220,7 +220,7 @@ async function verifyAttestedAt( ): Promise<{ verified: boolean timestamp: number - attester: Did + attester: DidUri cTypeId: ICType['$id'] delegationId: IDelegationNode['id'] | null }> { @@ -256,7 +256,7 @@ async function verifyAttestedAt( Option | Option ] & IEventData - const attester = getFullDid(encodeAddress(att.toU8a(), 38)) + const attester = getFullDidUri(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 - validateDid(issuer, 'Did') + validateUri(issuer, 'Did') await validateSubject(credential, opts) // 4. check nonTransferable if (nonTransferable !== true) @@ -660,7 +660,7 @@ export type AttestationHandler = ( }> export interface DidSigner { - did: Did + did: DidUri signer: SignExtrinsicCallback } diff --git a/packages/core/src/credentialsV1/KiltCredentialV1.ts b/packages/core/src/credentialsV1/KiltCredentialV1.ts index de5861f4d9..1babbae66d 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, - Did, + DidUri, IDelegationNode, } from '@kiltprotocol/types' @@ -215,10 +215,10 @@ export function validateStructure( } interface CredentialInput { - subject: Did + subject: DidUri claims: ICredential['claim']['contents'] cType: ICType['$id'] - issuer: Did + issuer: DidUri timestamp?: number chainGenesisHash?: Uint8Array claimHash?: ICredential['rootHash'] diff --git a/packages/core/src/credentialsV1/types.ts b/packages/core/src/credentialsV1/types.ts index 1be6826b1e..88c534ebb7 100644 --- a/packages/core/src/credentialsV1/types.ts +++ b/packages/core/src/credentialsV1/types.ts @@ -8,8 +8,8 @@ /* eslint-disable no-use-before-define */ import type { - VerificationMethod, - Did, + ConformingDidKey, + DidUri, Caip2ChainId, IClaimContents, ICType, @@ -33,7 +33,7 @@ import type { KILT_ATTESTER_LEGITIMATION_V1_TYPE, } from './common.js' -export type IPublicKeyRecord = VerificationMethod +export type IPublicKeyRecord = ConformingDidKey export interface Proof { type: string @@ -96,7 +96,7 @@ export interface VerifiablePresentation { '@context': [typeof W3C_CREDENTIAL_CONTEXT_URL, ...string[]] type: [typeof W3C_PRESENTATION_TYPE, ...string[]] verifiableCredential: VerifiableCredential | VerifiableCredential[] - holder: Did + holder: DidUri 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?: Did[] + delegators?: DidUri[] } export interface CredentialSubject extends IClaimContents { '@context': { '@vocab': string } - id: Did + id: DidUri } export interface KiltCredentialV1 extends VerifiableCredential { @@ -166,7 +166,7 @@ export interface KiltCredentialV1 extends VerifiableCredential { /** * The entity that issued the credential. */ - issuer: Did + issuer: DidUri /** * 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 17a9768477..94fff97e96 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, Did as KiltDid, ICType } from '@kiltprotocol/types' +import type { CTypeHash, DidUri, 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: KiltDid + creator: DidUri /** * 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 a56d97c55a..dbf3a84e8e 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, - Did, + DidUri, IDelegationHierarchyDetails, IDelegationNode, Permission, @@ -54,7 +54,7 @@ describe('DelegationNode', () => { let hierarchyId: string let parentId: string let hashList: string[] - let addresses: Did[] + let addresses: DidUri[] 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: Did = `did:kilt:${encodeAddress( + const noOnesAddress: DidUri = `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 e63b156e4b..60d50a96dc 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -8,13 +8,13 @@ import type { CTypeHash, DidDocument, - Did as KiltDid, + DidUri, + DidVerificationKey, IAttestation, IDelegationHierarchyDetails, IDelegationNode, SignCallback, SubmittableExtrinsic, - DidUrl, } from '@kiltprotocol/types' import { Crypto, SDKErrors, UUID } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' @@ -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: KiltDid + public readonly account: DidUri public readonly permissions: IDelegationNode['permissions'] private hierarchyDetails?: IDelegationHierarchyDetails public readonly revoked: boolean @@ -268,27 +268,23 @@ export class DelegationNode implements IDelegationNode { ): Promise { const delegateSignature = await sign({ data: this.generateHash(), - did: delegateDid.id, - verificationRelationship: 'authentication', + did: delegateDid.uri, + keyRelationship: 'authentication', }) - const signerUrl = - `${delegateDid.id}${delegateSignature.verificationMethod.id}` as DidUrl - const { fragment } = Did.parse(signerUrl) + const { fragment } = Did.parse(delegateSignature.keyUri) if (!fragment) { throw new SDKErrors.DidError( - `DID verification method URL "${signerUrl}" couldn't be parsed` + `DID key uri "${delegateSignature.keyUri}" couldn't be parsed` ) } - const verificationMethod = delegateDid.verificationMethod?.find( - ({ id }) => id === fragment - ) - if (!verificationMethod) { + const key = Did.getKey(delegateDid, fragment) + if (!key) { throw new SDKErrors.DidError( - `Verification method "${signerUrl}" was not found on DID: "${delegateDid.id}"` + `Key with fragment "${fragment}" was not found on DID: "${delegateDid.uri}"` ) } return Did.didSignatureToChain( - verificationMethod, + key as DidVerificationKey, delegateSignature.signature ) } @@ -350,7 +346,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: KiltDid | KiltDid[] + dids: DidUri | DidUri[] ): Promise<{ steps: number; node: DelegationNode | null }> { const acceptedDids = Array.isArray(dids) ? dids : [dids] if (acceptedDids.includes(this.account)) { @@ -403,7 +399,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: KiltDid): Promise { + public async getRevokeTx(did: DidUri): 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 b34a3a7645..3c70e3bc60 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 { Did, IAttestation, IDelegationNode } from '@kiltprotocol/types' +import type { DidUri, 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: Did, + attester: DidUri, attestation: IAttestation ): Promise { let delegationTreeTraversalSteps = 0 diff --git a/packages/core/src/presentation/Presentation.ts b/packages/core/src/presentation/Presentation.ts index 5d56cd96a3..3e8e346c87 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 { Did } from '@kiltprotocol/types' +import type { DidUri } from '@kiltprotocol/types' import { JsonSchema, SDKErrors } from '@kiltprotocol/utils' import { @@ -134,7 +134,7 @@ export function assertHolderCanPresentCredentials({ holder, verifiableCredential, }: { - holder: Did + holder: DidUri verifiableCredential: VerifiableCredential[] | VerifiableCredential }): void { const credentials = Array.isArray(verifiableCredential) @@ -164,7 +164,7 @@ export function assertHolderCanPresentCredentials({ */ export function create( VCs: VerifiableCredential[], - holder: Did, + holder: DidUri, { validFrom, validUntil, diff --git a/packages/did/package.json b/packages/did/package.json index 95bdd77c81..05c3a631c6 100644 --- a/packages/did/package.json +++ b/packages/did/package.json @@ -34,7 +34,6 @@ "typescript": "^4.8.3" }, "dependencies": { - "@digitalbazaar/multikey-context": "^1.0.0", "@digitalbazaar/security-context": "^1.0.0", "@kiltprotocol/augment-api": "workspace:*", "@kiltprotocol/config": "workspace:*", @@ -45,7 +44,6 @@ "@polkadot/types": "^10.4.0", "@polkadot/types-codec": "^10.4.0", "@polkadot/util": "^12.0.0", - "@polkadot/util-crypto": "^12.0.0", - "multibase": "^4.0.6" + "@polkadot/util-crypto": "^12.0.0" } } diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index d8ebc0f1ee..09daf45e41 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -8,57 +8,54 @@ 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, - Did, + DidDocument, + DidEncryptionKey, + DidKey, + DidServiceEndpoint, + DidUri, + DidVerificationKey, KiltAddress, - Service, - SignatureVerificationRelationship, + NewDidEncryptionKey, + NewDidVerificationKey, SignExtrinsicCallback, SignRequestData, SignResponseData, SubmittableExtrinsic, UriFragment, - VerificationMethod, + VerificationKeyRelationship, + BN, } from '@kiltprotocol/types' - -import { ConfigService } from '@kiltprotocol/config' +import { verificationKeyTypes } from '@kiltprotocol/types' import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' - +import { ConfigService } from '@kiltprotocol/config' import type { - DidEncryptionMethodType, - NewService, - DidSigningMethodType, - NewDidVerificationKey, - NewDidEncryptionKey, -} from './DidDetails/DidDetails.js' + DidDidDetails, + DidDidDetailsDidAuthorizedCallOperation, + DidDidDetailsDidPublicKey, + DidDidDetailsDidPublicKeyDetails, + DidServiceEndpointsDidEndpoint, + KiltSupportDeposit, +} from '@kiltprotocol/augment-api' -import { isValidVerificationMethodType } from './DidDetails/DidDetails.js' import { - multibaseKeyToDidKey, - keypairToMultibaseKey, - getAddressFromVerificationMethod, - getFullDid, + EncodedEncryptionKey, + EncodedKey, + EncodedSignature, + EncodedVerificationKey, + getAddressByKey, + getFullDidUri, parse, } from './Did.utils.js' -export type ChainDidIdentifier = KiltAddress +// ### Chain type definitions + +export type ChainDidPublicKey = DidDidDetailsDidPublicKey +export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails -export type EncodedVerificationKey = - | { sr25519: Uint8Array } - | { ed25519: Uint8Array } - | { ecdsa: Uint8Array } -export type EncodedEncryptionKey = { x25519: Uint8Array } -export type EncodedDidKey = EncodedVerificationKey | EncodedEncryptionKey -export type EncodedSignature = EncodedVerificationKey +// ### RAW QUERYING (lowest layer) /** * Format a DID to be used as a parameter for the blockchain API functions. @@ -66,30 +63,20 @@ export type EncodedSignature = EncodedVerificationKey * @param did The DID to format. * @returns The blockchain-formatted DID. */ -export function toChain(did: Did): ChainDidIdentifier { +export function toChain(did: DidUri): KiltAddress { return parse(did).address } /** - * Format a DID fragment to be used as a parameter for the blockchain API functions. + * Format a DID resource ID to be used as a parameter for the blockchain API functions. - * @param id The DID fragment to format. + * @param id The DID resource ID to format. * @returns The blockchain-formatted ID. */ -export function fragmentIdToChain(id: UriFragment): string { +export function resourceIdToChain(id: UriFragment): string { return id.replace(/^#/, '') } -/** - * Convert the DID data from blockchain format to the DID. - * - * @param encoded The chain-formatted DID. - * @returns The DID. - */ -export function fromChain(encoded: AccountId32): Did { - return getFullDid(Crypto.encodeAddress(encoded, ss58Format)) -} - /** * Convert the deposit data coming from the blockchain to JS object. * @@ -103,57 +90,42 @@ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { } } -export type ChainDidBaseKey = { - id: UriFragment - publicKey: Uint8Array - includedAt?: BN - type: string -} -export type ChainDidVerificationKey = ChainDidBaseKey & { - type: DidSigningMethodType -} -export type ChainDidEncryptionKey = ChainDidBaseKey & { - type: DidEncryptionMethodType -} -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[] +// ### DECODED QUERYING types +type ChainDocument = Pick< + DidDocument, + 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'keyAgreement' +> & { 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( +// ### DECODED QUERYING (builds on top of raw querying) + +function didPublicKeyDetailsFromChain( keyId: Hash, - keyDetails: DidDidDetailsDidPublicKeyDetails -): ChainDidKey { + keyDetails: ChainDidPublicKeyDetails +): 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(), - 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. * @@ -162,7 +134,7 @@ export function publicKeyFromChain( */ export function documentFromChain( encoded: Option -): ChainDidDetails { +): ChainDocument { const { publicKeys, authenticationKey, @@ -173,28 +145,28 @@ export function documentFromChain( deposit, } = encoded.unwrap() - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => + didPublicKeyDetailsFromChain(keyId, keyDetails) + ) .reduce((res, key) => { - res[fragmentIdToChain(key.id)] = key + res[resourceIdToChain(key.id)] = key return res }, {}) - const authentication = keys[ - authenticationKey.toHex() - ] as ChainDidVerificationKey + const authentication = keys[authenticationKey.toHex()] as DidVerificationKey - const didRecord: ChainDidDetails = { + const didRecord: ChainDocument = { authentication: [authentication], lastTxCounter: lastTxCounter.toBn(), deposit: depositFromChain(deposit), } if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey + const key = keys[attestationKey.unwrap().toHex()] as DidVerificationKey didRecord.assertionMethod = [key] } if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey + const key = keys[delegationKey.unwrap().toHex()] as DidVerificationKey didRecord.capabilityDelegation = [key] } @@ -203,13 +175,25 @@ export function documentFromChain( ) if (keyAgreementKeyIds.length > 0) { didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as ChainDidEncryptionKey + (id) => keys[id] as DidEncryptionKey ) } return didRecord } +interface ChainEndpoint { + id: string + serviceTypes: DidServiceEndpoint['type'] + urls: DidServiceEndpoint['serviceEndpoint'] +} + +/** + * 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 @@ -219,7 +203,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. @@ -229,27 +213,27 @@ 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 } } /** - * 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 URL. + * 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 object to check. + * @param endpoint A service endpoint object to check. */ -export function validateNewService(endpoint: NewService): void { +export function validateService(endpoint: DidServiceEndpoint): void { const { id, serviceEndpoint } = endpoint - if ((id as string).startsWith('did:kilt')) { + 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 URL, which is violated by id "${id}"` + `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))) { + 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.` ) @@ -269,11 +253,11 @@ export function validateNewService(endpoint: NewService): void { * @param service The DID service to format. * @returns The blockchain-formatted DID service. */ -export function serviceToChain(service: NewService): ChainDidService { - validateNewService(service) +export function serviceToChain(service: DidServiceEndpoint): ChainEndpoint { + validateService(service) const { id, type, serviceEndpoint } = service return { - id: fragmentIdToChain(id), + id: resourceIdToChain(id), serviceTypes: type, urls: serviceEndpoint, } @@ -287,7 +271,7 @@ export function serviceToChain(service: NewService): ChainDidService { */ export function serviceFromChain( encoded: Option -): Service { +): DidServiceEndpoint { const { id, serviceTypes, urls } = encoded.unwrap() return { id: `#${id.toUtf8()}`, @@ -296,14 +280,18 @@ export function serviceFromChain( } } +// ### EXTRINSICS types + export type AuthorizeCallInput = { - did: Did + did: DidUri txCounter: AnyNumber call: Extrinsic submitter: KiltAddress blockNumber?: AnyNumber } +// ### EXTRINSICS + export function publicKeyToChain( key: NewDidVerificationKey ): EncodedVerificationKey @@ -317,9 +305,9 @@ export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey */ export function publicKeyToChain( key: NewDidVerificationKey | NewDidEncryptionKey -): EncodedDidKey { +): EncodedKey { // TypeScript can't infer type here, so we have to add a type assertion. - return { [key.type]: key.publicKey } as EncodedDidKey + return { [key.type]: key.publicKey } as EncodedKey } interface GetStoreTxInput { @@ -328,36 +316,32 @@ interface GetStoreTxInput { capabilityDelegation?: [NewDidVerificationKey] keyAgreement?: NewDidEncryptionKey[] - service?: NewService[] + service?: DidServiceEndpoint[] } -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. * - * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys as verification methods and services. + * 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 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. + * 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 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, + input: GetStoreTxInput | DidDocument, submitter: KiltAddress, sign: GetStoreTxSignCallback ): Promise { @@ -402,14 +386,12 @@ export async function getStoreTx( api.consts.did.maxNumberOfServicesPerDid.toNumber() if (service.length > maxNumberOfServicesPerDid) { throw new SDKErrors.DidError( - `Cannot store more than ${maxNumberOfServicesPerDid} services per DID` + `Cannot store more than ${maxNumberOfServicesPerDid} service endpoints per DID` ) } const [authenticationKey] = authentication - const did = getAddressFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(authenticationKey), - }) + const did = getAddressByKey(authenticationKey) const newAttestationKey = assertionMethod && @@ -437,19 +419,19 @@ 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, - verificationRelationship: 'authentication', + keyRelationship: 'authentication', }) const encodedSignature = { - [authenticationKey.type]: signature, + [signature.keyType]: signature.signature, } as EncodedSignature return api.tx.did.create(encoded, encodedSignature) } export interface SigningOptions { sign: SignExtrinsicCallback - verificationRelationship: SignatureVerificationRelationship + keyRelationship: VerificationKeyRelationship } /** @@ -458,7 +440,7 @@ export interface SigningOptions { * * @param params Object wrapping all input to the function. * @param params.did Full DID. - * @param params.verificationRelationship DID verification relationship to be used for authorization. + * @param params.keyRelationship DID key 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. @@ -468,7 +450,7 @@ export interface SigningOptions { */ export async function generateDidAuthenticatedTx({ did, - verificationRelationship, + keyRelationship, sign, call, txCounter, @@ -487,38 +469,34 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const { signature, verificationMethod } = await sign({ + const signature = await sign({ data: signableCall.toU8a(), - verificationRelationship, + keyRelationship, did, }) - const { keyType } = multibaseKeyToDidKey( - verificationMethod.publicKeyMultibase - ) const encodedSignature = { - [keyType]: signature, + [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 key.publicKeyMultibase The multibase, multicodec representation of the signing public key. - * @param signature The signature generated with the full DID associated 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( - { publicKeyMultibase }: VerificationMethod, + key: DidVerificationKey, signature: Uint8Array ): EncodedSignature { - const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) - if (!isValidVerificationMethodType(keyType)) { + if (!verificationKeyTypes.includes(key.type)) { throw new SDKErrors.DidError( - `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` + `encodedDidSignature requires a verification key. A key of type "${key.type}" was used instead` ) } - return { [keyType]: signature } as EncodedSignature + return { [key.type]: signature } as EncodedSignature } diff --git a/packages/did/src/Did.rpc.ts b/packages/did/src/Did.rpc.ts index 272a74e997..da77219d65 100644 --- a/packages/did/src/Did.rpc.ts +++ b/packages/did/src/Did.rpc.ts @@ -7,42 +7,74 @@ 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 { DidDocument, KiltAddress, Service } from '@kiltprotocol/types' +import type { + Deposit, + DidDocument, + DidEncryptionKey, + DidKey, + DidServiceEndpoint, + DidUri, + DidVerificationKey, + KiltAddress, + UriFragment, + BN, +} 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 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 { +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 { const { publicKeys, authenticationKey, @@ -53,28 +85,29 @@ function documentFromChain( deposit, } = encoded - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => + didPublicKeyDetailsFromChain(keyId, keyDetails) + ) .reduce((res, key) => { - res[fragmentIdToChain(key.id)] = key + res[resourceIdToChain(key.id)] = key return res }, {}) - const authentication = keys[ - authenticationKey.toHex() - ] as ChainDidVerificationKey + const authentication = keys[authenticationKey.toHex()] as DidVerificationKey - const didRecord: ChainDidDetails = { + const didRecord: RpcDocument = { authentication: [authentication], lastTxCounter: lastTxCounter.toBn(), deposit: depositFromChain(deposit), } + if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey + const key = keys[attestationKey.unwrap().toHex()] as DidVerificationKey didRecord.assertionMethod = [key] } if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey + const key = keys[delegationKey.unwrap().toHex()] as DidVerificationKey didRecord.capabilityDelegation = [key] } @@ -83,25 +116,27 @@ function documentFromChain( ) if (keyAgreementKeyIds.length > 0) { didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as ChainDidEncryptionKey + (id) => keys[id] as DidEncryptionKey ) } return didRecord } -function serviceFromChain(encoded: DidServiceEndpointsDidEndpoint): Service { +function serviceFromChain( + encoded: DidServiceEndpointsDidEndpoint +): DidServiceEndpoint { const { id, serviceTypes, urls } = encoded return { - id: `#${id.toUtf8()}`, - type: serviceTypes.map((type) => type.toUtf8()), - serviceEndpoint: urls.map((url) => url.toUtf8()), + id: `#${u8aToString(id)}`, + type: serviceTypes.map(u8aToString), + serviceEndpoint: urls.map(u8aToString), } } function servicesFromChain( encoded: DidServiceEndpointsDidEndpoint[] -): Service[] { +): DidServiceEndpoint[] { return encoded.map((encodedValue) => serviceFromChain(encodedValue)) } @@ -135,8 +170,14 @@ function connectedAccountsFromChain( ) } -export interface LinkedDidInfo { +/** + * Web3Name is the type of nickname for a DID. + */ +export type Web3Name = string + +export interface DidInfo { document: DidDocument + web3Name?: Web3Name accounts: Address[] } @@ -150,67 +191,30 @@ export interface LinkedDidInfo { export function linkedInfoFromChain( encoded: Option, networkPrefix = ss58Format -): LinkedDidInfo { +): DidInfo { const { identifier, accounts, w3n, serviceEndpoints, details } = encoded.unwrap() - const { - authentication, - keyAgreement, - capabilityDelegation, - assertionMethod, - } = documentFromChain(details) + const didRec = documentFromChain(details) const did: 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' - ) + uri: fromChain(identifier), + authentication: didRec.authentication, + assertionMethod: didRec.assertionMethod, + capabilityDelegation: didRec.capabilityDelegation, + keyAgreement: didRec.keyAgreement, } - 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 service = servicesFromChain(serviceEndpoints) + if (service.length > 0) { + did.service = service } - if (w3n.isSome) { - did.alsoKnownAs = [`w3n:${w3n.unwrap().toHuman()}`] - } + const web3Name = w3n.isNone ? undefined : w3n.unwrap().toHuman() 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 e5672ca8f8..449e698136 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -5,20 +5,18 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' + import type { - KiltKeyringPair, - KeyringPair, DidDocument, - SignCallback, - DidUrl, + DidResourceUri, DidSignature, - DereferenceResult, + KeyringPair, + KiltKeyringPair, + NewLightDidVerificationKey, + SignCallback, } from '@kiltprotocol/types' - import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' - -import type { NewLightDidVerificationKey } from './DidDetails' import { makeSigningKeyTool } from '../../../tests/testUtils' import { @@ -27,14 +25,13 @@ import { signatureToJson, verifyDidSignature, } from './Did.signature' -import { dereference, SupportedContentType } from './DidResolver/DidResolver' -import { keypairToMultibaseKey, multibaseKeyToDidKey, parse } from './Did.utils' -import { createLightDidDocument } from './DidDetails' +import { keyToResolvedKey, resolveKey } from './DidResolver' +import * as Did from './index.js' -jest.mock('./DidResolver/DidResolver') +jest.mock('./DidResolver') jest - .mocked(dereference) - .mockImplementation(jest.requireActual('./DidResolver').dereference) + .mocked(keyToResolvedKey) + .mockImplementation(jest.requireActual('./DidResolver').keyToResolvedKey) describe('light DID', () => { let keypair: KiltKeyringPair @@ -43,7 +40,7 @@ describe('light DID', () => { beforeAll(() => { const keyTool = makeSigningKeyTool() keypair = keyTool.keypair - did = createLightDidDocument({ + did = Did.createLightDidDocument({ authentication: keyTool.authentication, }) sign = keyTool.getSignCallback(did) @@ -51,39 +48,28 @@ describe('light DID', () => { beforeEach(() => { jest - .mocked(dereference) + .mocked(resolveKey) .mockReset() - .mockImplementation( - async (didUrl): Promise> => { - const { address } = parse(didUrl) - if (address === keypair.address) { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: did, - } - } - return { - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - } - } + .mockImplementation(async (didUri, keyRelationship = 'authentication') => + didUri.includes(keypair.address) + ? Did.keyToResolvedKey(did[keyRelationship]![0], did.uri) + : Promise.reject() ) }) it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() }) @@ -93,8 +79,8 @@ describe('light DID', () => { const { signature, keyUri } = signatureToJson( await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) ) const oldSignature = { @@ -110,125 +96,117 @@ describe('light DID', () => { it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: SIGNED_BYTES, - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() }) it('fails if relationship does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationRelationship: 'assertionMethod', + keyUri, + expectedVerificationMethod: 'assertionMethod', }) ).rejects.toThrow() }) - it('fails if verification method id does not match', async () => { + 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({ + let { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', - }) - const wrongVerificationMethodId = `${verificationMethod.id}1a` - jest.mocked(dereference).mockResolvedValue({ - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, + did: did.uri, + keyRelationship: 'authentication', }) + keyUri = `${keyUri}1a` + jest.mocked(resolveKey).mockRejectedValue(new Error('Key not found')) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${wrongVerificationMethodId}` as DidUrl, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() }) it('fails if signature does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING.substring(1), signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() }) - it('fails if verification method id malformed', async () => { - jest.mocked(dereference).mockRestore() + it('fails if key id malformed', async () => { + jest.mocked(resolveKey).mockRestore() const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, verificationMethod } = await sign({ + let { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) - const malformedVerificationId = verificationMethod.id.replace('#', '?') + // @ts-expect-error + keyUri = keyUri.replace('#', '?') await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${malformedVerificationId}` as DidUrl, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() }) it('does not verify if migrated to Full DID', async () => { - jest.mocked(dereference).mockResolvedValue({ - contentMetadata: { - canonicalId: did.id, - }, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: { id: did.id }, - }) + jest.mocked(resolveKey).mockRejectedValue(new Error('Migrated')) const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() }) it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - keyUri: `${did.id}${did.authentication![0]}`, + keyUri: `${did.uri}${did.authentication[0].id}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -236,48 +214,37 @@ describe('light DID', () => { it('detects signer expectation mismatch if signature is by unrelated did', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) - const expectedSigner = createLightDidDocument({ + const expectedSigner = Did.createLightDidDocument({ authentication: makeSigningKeyTool().authentication, - }).id + }).uri await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + keyUri, expectedSigner, - expectedVerificationRelationship: 'authentication', + expectedVerificationMethod: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) it('allows variations of the same light did', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: '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], + const expectedSigner = Did.createLightDidDocument({ + authentication: did.authentication as [NewLightDidVerificationKey], keyAgreement: [{ type: 'x25519', publicKey: new Uint8Array(32).fill(1) }], service: [ { @@ -286,15 +253,15 @@ describe('light DID', () => { serviceEndpoint: ['http://example.com'], }, ], - }).id + }).uri await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + keyUri, expectedSigner, - expectedVerificationRelationship: 'authentication', + expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() }) @@ -307,157 +274,122 @@ describe('full DID', () => { beforeAll(() => { keypair = Crypto.makeKeypairFromSeed() did = { - id: `did:kilt:${keypair.address}`, - authentication: ['#0x12345'], - verificationMethod: [ + uri: `did:kilt:${keypair.address}`, + authentication: [ { - controller: `did:kilt:${keypair.address}`, id: '#0x12345', - publicKeyMultibase: keypairToMultibaseKey(keypair), - type: 'Multikey', + type: 'sr25519', + publicKey: keypair.publicKey, }, ], } - sign = async ({ data, did: signingDid }) => ({ + sign = async ({ data }) => ({ signature: keypair.sign(data), - verificationMethod: { - id: '#0x12345', - controller: signingDid, - type: 'Multikey', - publicKeyMultibase: keypairToMultibaseKey(keypair), - }, + keyUri: `${did.uri}#0x12345`, + keyType: 'sr25519', }) }) beforeEach(() => { jest - .mocked(dereference) + .mocked(resolveKey) .mockReset() - .mockImplementation( - async (didUrl): Promise> => { - const { address } = parse(didUrl) - if (address === keypair.address) { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: did, - } - } - return { - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - } - } + .mockImplementation(async (didUri) => + didUri.includes(keypair.address) + ? Did.keyToResolvedKey(did.authentication[0], did.uri) + : Promise.reject() ) }) it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: '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({ + const { signature, keyUri } = await sign({ data: SIGNED_BYTES, - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() }) it('does not verify if deactivated', async () => { - jest.mocked(dereference).mockResolvedValue({ - contentMetadata: { deactivated: true }, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: { id: did.id }, - }) + jest.mocked(resolveKey).mockRejectedValue(new Error('Deactivated')) const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: 'authentication', }) ).rejects.toThrow() }) it('does not verify if not on chain', async () => { - jest.mocked(dereference).mockResolvedValue({ - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - }) + jest.mocked(resolveKey).mockRejectedValue(new Error('Not on chain')) const SIGNED_STRING = 'signed string' - const { signature, verificationMethod } = await sign({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, - expectedVerificationRelationship: 'authentication', + keyUri, + expectedVerificationMethod: '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({ + const { signature, keyUri } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.id, - verificationRelationship: 'authentication', + did: did.uri, + keyRelationship: '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 + const expectedSigner = Did.createLightDidDocument({ + authentication: did.authentication as [NewLightDidVerificationKey], + }).uri await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + keyUri, expectedSigner, - expectedVerificationRelationship: 'authentication', + expectedVerificationMethod: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) @@ -465,17 +397,17 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - signerUrl: `${did.id}${verificationMethod.id}`, + keyUri, expectedSigner, allowUpgraded: true, - expectedVerificationRelationship: 'authentication', + expectedVerificationMethod: 'authentication', }) ).resolves.not.toThrow() }) it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - keyUri: `${did.id}${did.authentication![0]}`, + keyUri: `${did.uri}${did.authentication[0].id}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -488,7 +420,7 @@ describe('type guard', () => { keypair = Crypto.makeKeypairFromSeed() }) - it('rejects malformed signer URL', () => { + it('rejects malformed key uri', () => { let signature: DidSignature = { // @ts-expect-error keyUri: `did:kilt:${keypair.address}?mykey`, @@ -523,7 +455,7 @@ describe('type guard', () => { it('rejects unexpected signature type', () => { const signature: DidSignature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidUrl, + keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, signature: '', } expect(isDidSignature(signature)).toBe(false) @@ -536,7 +468,7 @@ describe('type guard', () => { it('rejects incomplete objects', () => { let signature: DidSignature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidUrl, + keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, // @ts-expect-error signature: undefined, } @@ -552,9 +484,9 @@ describe('type guard', () => { signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) + // @ts-expect-error signature = { - // @ts-expect-error - keyUri: `did:kilt:${keypair.address}#mykey`, + keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, } expect(isDidSignature(signature)).toBe(false) // @ts-expect-error diff --git a/packages/did/src/Did.signature.ts b/packages/did/src/Did.signature.ts index f7d5a94c2b..07a853d79a 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -7,79 +7,77 @@ import { isHex } from '@polkadot/util' -import type { - DereferenceDidUrl, - DidDocument, +import { + DidResolveKey, + DidResourceUri, DidSignature, - Did, - DidUrl, - SignatureVerificationRelationship, + DidUri, SignResponseData, + VerificationKeyRelationship, } from '@kiltprotocol/types' - import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { multibaseKeyToDidKey, parse, validateDid } from './Did.utils.js' -import { dereference } from './DidResolver/DidResolver.js' +import { resolveKey } from './DidResolver/index.js' +import { parse, validateUri } from './Did.utils.js' export type DidSignatureVerificationInput = { message: string | Uint8Array signature: Uint8Array - signerUrl: DidUrl - expectedSigner?: Did + keyUri: DidResourceUri + expectedSigner?: DidUri allowUpgraded?: boolean - expectedVerificationRelationship?: SignatureVerificationRelationship - dereferenceDidUrl?: DereferenceDidUrl['dereference'] + expectedVerificationMethod?: VerificationKeyRelationship + didResolveKey?: DidResolveKey } // 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: DidUrl +type OldDidSignature = Pick & { + keyId: DidSignature['keyUri'] } +/** + * 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 | OldDidSignatureV1 + input: DidSignature | OldDidSignature ): void { - const verificationMethodUrl = (() => { - if ('keyId' in input) { - return input.keyId - } - return input.keyUri - })() + const keyUri = 'keyUri' in input ? input.keyUri : input.keyId if (!isHex(input.signature)) { throw new SDKErrors.SignatureMalformedError( `Expected signature as a hex string, got ${input.signature}` ) } - validateDid(verificationMethodUrl, 'DidUrl') + validateUri(keyUri, 'ResourceUri') } /** - * Verify a DID signature given the signer's DID URL (i.e., DID + verification method ID). + * Verify a DID signature given the key URI of the signature. * 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 verification method is not the expectedSigner. + * @param input.keyUri DID URI of the key 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.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]]. + * @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]]. */ export async function verifyDidSignature({ message, signature, - signerUrl, + keyUri, expectedSigner, allowUpgraded = false, - expectedVerificationRelationship, - dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + expectedVerificationMethod, + didResolveKey = resolveKey, }: DidSignatureVerificationInput): Promise { - // checks if signer URL points to the right did; alternatively we could check the verification method's controller - const signer = parse(signerUrl) + // checks if key uri points to the right did; alternatively we could check the key's controller + const signer = parse(keyUri) if (expectedSigner && expectedSigner !== signer.did) { // check for allowable exceptions const expected = parse(expectedSigner) @@ -88,7 +86,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 verification method key type matches + // OR: both are light dids and their auth key type matches const keyTypeMatch = signer.type === 'light' && expected.type === 'light' && @@ -97,56 +95,14 @@ export async function verifyDidSignature({ throw new SDKErrors.DidSubjectMismatchError(signer.did, expected.did) } } - if (signer.fragment === undefined) { - throw new SDKErrors.DidError( - `Signer DID URL "${signerUrl}" does not point to a valid resource under the signer's DID Document.` - ) - } - 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}".` - ) - } - // 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() - } - 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 verification method ID is included in the given verification relationship, if provided. - if ( - expectedVerificationRelationship && - !didDocument[expectedVerificationRelationship]?.some( - (id) => id === verificationMethod.id - ) - ) { - throw new SDKErrors.DidError( - `No verification method "${signer.fragment}" for the verification method "${expectedVerificationRelationship}"` - ) - } + const { publicKey } = await didResolveKey(keyUri, expectedVerificationMethod) - 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 DID URL of the signer's verification method. + * 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. @@ -154,7 +110,7 @@ export async function verifyDidSignature({ */ export function isDidSignature( input: unknown -): input is DidSignature | OldDidSignatureV1 { +): input is DidSignature | OldDidSignature { try { verifyDidSignatureDataStructure(input as DidSignature) return true @@ -168,17 +124,14 @@ export function isDidSignature( * * @param input Signature data returned from the [[SignCallback]]. * @param input.signature Signature bytes. - * @param input.verificationMethod The verification method used to generate the signature. + * @param input.keyUri DID URI of the key used for signing. * @returns A [[DidSignature]] object where signature is hex-encoded. */ export function signatureToJson({ signature, - verificationMethod, + keyUri, }: SignResponseData): DidSignature { - return { - signature: Crypto.u8aToHex(signature), - keyUri: `${verificationMethod.controller}${verificationMethod.id}`, - } + return { signature: Crypto.u8aToHex(signature), keyUri } } /** @@ -189,16 +142,9 @@ export function signatureToJson({ * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. */ export function signatureFromJson( - input: DidSignature | OldDidSignatureV1 -): Pick & { - keyUri: DidUrl -} { - const keyUri = (() => { - if ('keyId' in input) { - return input.keyId - } - return input.keyUri - })() + input: DidSignature | OldDidSignature +): Pick { + const keyUri = 'keyUri' in input ? input.keyUri : input.keyId const signature = Crypto.coToUInt8(input.signature) return { signature, keyUri } } diff --git a/packages/did/src/Did.utils.ts b/packages/did/src/Did.utils.ts index bf44a0d404..2cc1eaf631 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -5,20 +5,16 @@ * 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 { - Did, - DidUrl, - KeyringPair, + +import { + DidResourceUri, + DidUri, + DidVerificationKey, KiltAddress, UriFragment, - VerificationMethod, } from '@kiltprotocol/types' import { DataUtils, SDKErrors, ss58Format } from '@kiltprotocol/utils' -import { decode as multibaseDecode, encode as multibaseEncode } from 'multibase' - -import type { DidVerificationMethodType } from './DidDetails/DidDetails.js' // The latest version for KILT light DIDs. const LIGHT_DID_LATEST_VERSION = 1 @@ -26,7 +22,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 `Did` type in @kiltprotocol/types +// NOTICE: The following regex patterns must be kept in sync with DidUri type in @kiltprotocol/types // Matches the following full DIDs // - did:kilt: @@ -43,60 +39,40 @@ 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: Did + did: DidUri version: number type: 'light' | 'full' address: KiltAddress - queryParameters?: Record fragment?: UriFragment authKeyTypeEncoding?: string encodedDetails?: string } -// 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 exportQueryParamsFromDidUrl( - did: DidUrl -): Record | undefined { - try { - const urlified = new URL(did) - return urlified.searchParams.size > 0 - ? Object.fromEntries(urlified.searchParams) - : undefined - } catch { - throw new SDKErrors.InvalidDidFormatError(did) - } -} - /** - * Parses a KILT DID or a DID URL and returns the information contained within in a structured form. + * Parses a KILT DID uri and returns the information contained within in a structured form. * - * @param did A KILT DID or a DID URL as a string. - * @returns Object containing information extracted from the input string. + * @param didUri A KILT DID uri as a string. + * @returns Object containing information extracted from the DID uri. */ -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 +export function parse(didUri: DidUri | 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 - const queryParameters = exportQueryParamsFromDidUrl(did as DidUrl) return { - did: did.replace(fragment || '', '') as Did, + did: didUri.replace(fragment || '', '') as DidUri, version, type: 'full', address, - queryParameters, fragment: fragment === '#' ? undefined : (fragment as UriFragment), } } // If it fails to parse full DID, try with light DID - matches = LIGHT_KILT_DID_REGEX.exec(did)?.groups + matches = LIGHT_KILT_DID_REGEX.exec(didUri)?.groups if (matches) { const { authKeyType, @@ -108,173 +84,57 @@ export function parse(did: Did | DidUrl): IDidParsingResult { const version = versionString ? parseInt(versionString, 10) : LIGHT_DID_LATEST_VERSION - const queryParameters = exportQueryParamsFromDidUrl(did as DidUrl) return { - did: did.replace(fragment || '', '') as Did, + did: didUri.replace(fragment || '', '') as DidUri, version, type: 'light', address, - queryParameters, fragment: fragment === '#' ? undefined : (fragment as UriFragment), encodedDetails, authKeyTypeEncoding: authKeyType, } } - throw new SDKErrors.InvalidDidFormatError(did) -} - -type DecodedVerificationMethod = { - publicKey: Uint8Array - 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 = - { - [MULTICODEC_ECDSA_PREFIX]: ['ecdsa', 33], - [MULTICODEC_X25519_PREFIX]: ['x25519', 32], - [MULTICODEC_ED25519_PREFIX]: ['ed25519', 32], - [MULTICODEC_SR25519_PREFIX]: ['sr25519', 32], - } -const multicodecReversePrefixes: Record = { - ecdsa: MULTICODEC_ECDSA_PREFIX, - x25519: MULTICODEC_X25519_PREFIX, - ed25519: MULTICODEC_ED25519_PREFIX, - sr25519: MULTICODEC_SR25519_PREFIX, -} - -/** - * 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 in Multikey format (i.e., multicodec-prefixed, then multibase encoded). - * @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 Multikey representation of a keypair given its type and public key. - * - * @param keypair The input keypair to encode as Multikey. - * @param keypair.type The keypair [[DidKeyType]]. - * @param keypair.publicKey The keypair public key. - * @returns The Multikey representation (i.e., multicodec-prefixed, then multibase encoded) of the provided keypair. - */ -export function keypairToMultibaseKey({ - type, - publicKey, -}: Pick & { - type: DidVerificationMethodType -}): 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 u8aToString( - multibaseEncode('base58btc', Uint8Array.from(multiCodecPublicKey)) - ) as `z${string}` -} - -/** - * Convert a DID key to a `MultiKey` verification method. - * - * @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. - * @param key.publicKey The public component of the key. - * @returns The provided key encoded as a [[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: u8aToString( - multibaseEncode('base58btc', Uint8Array.from(multiCodecPublicKey)) - ) as `z${string}`, - } + 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 as a string. - * @param didB A second KILT DID as a string. + * @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: Did, didB: Did): boolean { +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 with or without a trailing fragment. + * 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 `Did` if the the input is expected to have a fragment (following '#'), `DidUrl` if it is expected not to have one. Default allows both. + * @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 validateDid( +export function validateUri( input: unknown, - expectType?: 'Did' | 'DidUrl' + expectType?: 'Did' | 'ResourceUri' ): void { if (typeof input !== 'string') { throw new TypeError(`DID string expected, got ${typeof input}`) } - const { address, fragment } = parse(input as DidUrl) + const { address, fragment } = parse(input as DidUri) if ( fragment && @@ -283,13 +143,13 @@ export function validateDid( (typeof expectType === 'boolean' && expectType === false)) ) { throw new SDKErrors.DidError( - 'Expected a Kilt Did but got a DidUrl (containing a #fragment)' + 'Expected a Kilt DidUri but got a DidResourceUri (containing a #fragment)' ) } - if (!fragment && expectType === 'DidUrl') { + if (!fragment && expectType === 'ResourceUri') { throw new SDKErrors.DidError( - 'Expected a Kilt DidUrl (containing a #fragment) but got a Did' + 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' ) } @@ -297,16 +157,17 @@ export function validateDid( } /** - * Internal: derive the address part of the DID when it is created from the provided authentication verification method. + * Internal: derive the address part of the DID when it is created from authentication key. * - * @param input The authentication verification method. - * @param input.publicKeyMultibase The `publicKeyMultibase` value of the verification method. + * @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 getAddressFromVerificationMethod({ - publicKeyMultibase, -}: Pick): KiltAddress { - const { keyType: type, publicKey } = multibaseKeyToDidKey(publicKeyMultibase) +export function getAddressByKey({ + publicKey, + type, +}: Pick): KiltAddress { if (type === 'ed25519' || type === 'sr25519') { return encodeAddress(publicKey, ss58Format) } @@ -318,32 +179,32 @@ export function getAddressFromVerificationMethod({ } /** - * Builds the full DID a light DID will have after it’s stored on the blockchain. + * Builds the URI a light DID will have after it’s stored on the blockchain. * - * @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. + * @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 getFullDid( - didOrAddress: Did | KiltAddress, +export function getFullDidUri( + didOrAddress: DidUri | KiltAddress, version = FULL_DID_LATEST_VERSION -): Did { +): DidUri { const address = DataUtils.isKiltAddress(didOrAddress) ? didOrAddress - : parse(didOrAddress as Did).address + : parse(didOrAddress as DidUri).address const versionString = version === 1 ? '' : `v${version}` - return `did:kilt:${versionString}${address}` as Did + return `did:kilt:${versionString}${address}` as DidUri } /** - * Builds the of a full DID if it is created with the authentication verification method derived from the provided public key. + * 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 . + * @param key The key that will be used as DID authentication key. + * @returns The expected full DID URI. */ -export function getFullDidFromVerificationMethod( - verificationMethod: Pick -): Did { - const address = getAddressFromVerificationMethod(verificationMethod) - return getFullDid(address) +export function getFullDidUriFromKey( + key: Pick +): DidUri { + const address = getAddressByKey(key) + return getFullDidUri(address) } diff --git a/packages/did/src/DidDetails/DidDetails.spec.ts b/packages/did/src/DidDetails/DidDetails.spec.ts new file mode 100644 index 0000000000..26302a1f63 --- /dev/null +++ b/packages/did/src/DidDetails/DidDetails.spec.ts @@ -0,0 +1,122 @@ +/** + * 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 7b985b9930..0af5565641 100644 --- a/packages/did/src/DidDetails/DidDetails.ts +++ b/packages/did/src/DidDetails/DidDetails.ts @@ -7,162 +7,51 @@ import type { DidDocument, - Service, - UriFragment, - VerificationMethod, - VerificationRelationship, + DidKey, + DidServiceEndpoint, } from '@kiltprotocol/types' -import { didKeyToVerificationMethod } from '../Did.utils.js' - -/** - * 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 { - return signingMethodTypes.includes(input) -} - -/** - * 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 { - return encryptionMethodTypes.includes(input) -} - -export type DidVerificationMethodType = - | DidSigningMethodType - | DidEncryptionMethodType - /** - * Type guard checking whether the provided input string represents one of the supported signing or encryption verification types. + * Gets all public keys associated with this DID. * - * @param input The input string. - * @returns Whether the input string is an instance of [[DidSigningMethodType]]. + * @param did The DID data. + * @returns Array of public keys. */ -export function isValidDidVerificationType( - input: string -): input is DidSigningMethodType { - return ( - isValidVerificationMethodType(input) || isValidEncryptionMethodType(input) - ) +export function getKeys( + did: Partial & Pick +): DidKey[] { + return [ + ...did.authentication, + ...(did.assertionMethod || []), + ...(did.capabilityDelegation || []), + ...(did.keyAgreement || []), + ] } -export type NewVerificationMethod = Omit -export type NewService = Service - /** - * Type guard checking whether the provided input represents one of the supported verification relationships. + * Returns a key with a given id, if associated with this DID. * - * @param input The input. - * @returns Whether the input is an instance of [[VerificationRelationship]]. - */ -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. - */ -export type BaseNewDidKey = { - publicKey: Uint8Array - type: string -} - -/** - * Type of a new verification key to add under a DID. + * @param did The DID data. + * @param id Key id (not the full key uri). + * @returns The respective public key data or undefined. */ -export type NewDidVerificationKey = BaseNewDidKey & { - type: DidSigningMethodType +export function getKey( + did: Partial & Pick, + id: DidKey['id'] +): DidKey | undefined { + return getKeys(did).find((key) => key.id === id) } /** - * Type of a new encryption key to add under a DID. - */ -export type NewDidEncryptionKey = BaseNewDidKey & { - type: DidEncryptionMethodType -} - -function doesVerificationMethodExist( - didDocument: DidDocument, - { id }: Pick -): boolean { - return ( - didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined - ) -} - -function addVerificationMethod( - didDocument: DidDocument, - verificationMethod: VerificationMethod, - relationship: VerificationRelationship -): 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. + * Returns a service endpoint with a given id, if associated with this DID. * - * @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: DidDocument, - { id, publicKey, type: keyType }: BaseNewDidKey & { id: UriFragment }, - relationship: VerificationRelationship -): void { - const verificationMethod = didKeyToVerificationMethod(didDocument.id, id, { - keyType: keyType as DidSigningMethodType, - publicKey, - }) - addVerificationMethod(didDocument, verificationMethod, relationship) + * @param did The DID data. + * @param id Endpoint id (not the full endpoint uri). + * @returns The respective endpoint data or undefined. + */ +export function getService( + did: Pick, + id: DidServiceEndpoint['id'] +): DidServiceEndpoint | undefined { + return did.service?.find((endpoint) => endpoint.id === id) } diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 10e39ebdc1..3f80049315 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -22,10 +22,7 @@ import { makeSigningKeyTool, } from '../../../../tests/testUtils' import { generateDidAuthenticatedTx } from '../Did.chain.js' -import { - authorizeBatch, - getVerificationRelationshipForTx, -} from './FullDidDetails.js' +import * as Did from './index.js' const augmentedApi = ApiMocks.createAugmentedApi() const mockedApi: any = ApiMocks.getMockedApi() @@ -59,8 +56,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 () => - authorizeBatch({ - did: fullDid.id, + Did.authorizeBatch({ + did: fullDid.uri, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [extrinsic, extrinsic], sign, @@ -77,8 +74,8 @@ describe('When creating an instance from the chain', () => { ]) const batchFunction = jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll - await authorizeBatch({ - did: fullDid.id, + await Did.authorizeBatch({ + did: fullDid.uri, batchFunction, extrinsics: [extrinsic, extrinsic], sign, @@ -114,8 +111,8 @@ describe('When creating an instance from the chain', () => { ctype3Extrinsic, ctype4Extrinsic, ] - await authorizeBatch({ - did: fullDid.id, + await Did.authorizeBatch({ + did: fullDid.uri, batchFunction, extrinsics, nonce: new BN(0), @@ -142,8 +139,8 @@ describe('When creating an instance from the chain', () => { describe('.build()', () => { it('throws if batch is empty', async () => { await expect(async () => - authorizeBatch({ - did: fullDid.id, + Did.authorizeBatch({ + did: fullDid.uri, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [], sign, @@ -166,14 +163,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 VerificationRelationship for single valid call', () => { - const verificationRelationship = getVerificationRelationshipForTx( + it('Should return correct KeyRelationship for single valid call', () => { + const keyRelationship = Did.getKeyRelationshipForTx( mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) ) - expect(verificationRelationship).toBe('assertionMethod') + expect(keyRelationship).toBe('assertionMethod') }) - it('Should return correct VerificationRelationship for batched call', () => { - const verificationRelationship = getVerificationRelationshipForTx( + it('Should return correct KeyRelationship for batched call', () => { + const keyRelationship = Did.getKeyRelationshipForTx( mockApi.tx.utility.batch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -187,10 +184,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(verificationRelationship).toBe('assertionMethod') + expect(keyRelationship).toBe('assertionMethod') }) - it('Should return correct VerificationRelationship for batchAll call', () => { - const verificationRelationship = getVerificationRelationshipForTx( + it('Should return correct KeyRelationship for batchAll call', () => { + const keyRelationship = Did.getKeyRelationshipForTx( mockApi.tx.utility.batchAll([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -204,10 +201,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(verificationRelationship).toBe('assertionMethod') + expect(keyRelationship).toBe('assertionMethod') }) - it('Should return correct VerificationRelationship for forceBatch call', () => { - const verificationRelationship = getVerificationRelationshipForTx( + it('Should return correct KeyRelationship for forceBatch call', () => { + const keyRelationship = Did.getKeyRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -221,10 +218,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(verificationRelationship).toBe('assertionMethod') + expect(keyRelationship).toBe('assertionMethod') }) - it('Should return undefined for batch with mixed VerificationRelationship calls', () => { - const verificationRelationship = getVerificationRelationshipForTx( + it('Should return undefined for batch with mixed KeyRelationship calls', () => { + const keyRelationship = Did.getKeyRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -234,6 +231,6 @@ describe('When creating an instance from the chain', () => { mockApi.tx.web3Names.claim('awesomename'), ]) ) - expect(verificationRelationship).toBeUndefined() + expect(keyRelationship).toBeUndefined() }) }) diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index 5303e6fb06..b473a67c05 100644 --- a/packages/did/src/DidDetails/FullDidDetails.ts +++ b/packages/did/src/DidDetails/FullDidDetails.ts @@ -10,11 +10,11 @@ import type { SubmittableExtrinsicFunction } from '@polkadot/api/types' import { BN } from '@polkadot/util' import type { - Did, + DidUri, KiltAddress, - SignatureVerificationRelationship, SignExtrinsicCallback, SubmittableExtrinsic, + VerificationKeyRelationship, } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' @@ -30,10 +30,7 @@ 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< - string, - SignatureVerificationRelationship | undefined -> = { +const methodMapping: Record = { attestation: 'assertionMethod', ctype: 'assertionMethod', delegation: 'capabilityDelegation', @@ -46,20 +43,20 @@ const methodMapping: Record< web3Names: 'authentication', } -function getVerificationRelationshipForRuntimeCall( +function getKeyRelationshipForMethod( call: Extrinsic['method'] -): SignatureVerificationRelationship | undefined { +): VerificationKeyRelationship | undefined { const { section, method } = call - // get the VerificationRelationship of a batched 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 VerificationRelationship and deduplicate the items + // map all calls to their VerificationKeyRelationship and deduplicate the items return (call.args[0] as unknown as Array) - .map(getVerificationRelationshipForRuntimeCall) + .map(getKeyRelationshipForMethod) .reduce((prev, value) => (prev === value ? prev : undefined)) } @@ -72,15 +69,15 @@ function getVerificationRelationshipForRuntimeCall( } /** - * Detect the relationship for a verification method which should be used to DID-authorize the provided extrinsic. + * 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 verification relationship. + * @returns The key relationship. */ -export function getVerificationRelationshipForTx( +export function getKeyRelationshipForTx( extrinsic: Extrinsic -): SignatureVerificationRelationship | undefined { - return getVerificationRelationshipForRuntimeCall(extrinsic.method) +): VerificationKeyRelationship | undefined { + return getKeyRelationshipForMethod(extrinsic.method) } // Max nonce value is (2^64) - 1 @@ -101,7 +98,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: Did): Promise { +async function getNextNonce(did: DidUri): Promise { const api = ConfigService.get('api') const queried = await api.query.did.did(toChain(did)) const currentNonce = queried.isSome @@ -111,7 +108,7 @@ async function getNextNonce(did: Did): Promise { } /** - * Signs and returns the provided unsigned extrinsic with the right DID verification method, if present. Otherwise, it will throw an error. + * 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. @@ -122,7 +119,7 @@ async function getNextNonce(did: Did): Promise { * @returns The DID-signed submittable extrinsic. */ export async function authorizeTx( - did: Did, + did: DidUri, extrinsic: Extrinsic, sign: SignExtrinsicCallback, submitterAccount: KiltAddress, @@ -138,16 +135,14 @@ export async function authorizeTx( ) } - const verificationRelationship = getVerificationRelationshipForTx(extrinsic) - if (verificationRelationship === undefined) { - throw new SDKErrors.SDKError( - 'No verification relationship found for extrinsic' - ) + const keyRelationship = getKeyRelationshipForTx(extrinsic) + if (keyRelationship === undefined) { + throw new SDKErrors.SDKError('No key relationship found for extrinsic') } return generateDidAuthenticatedTx({ did, - verificationRelationship, + keyRelationship, sign, call: extrinsic, txCounter: txCounter || (await getNextNonce(did)), @@ -157,39 +152,38 @@ export async function authorizeTx( type GroupedExtrinsics = Array<{ extrinsics: Extrinsic[] - verificationRelationship: SignatureVerificationRelationship + keyRelationship: VerificationKeyRelationship }> -function groupExtrinsicsByVerificationRelationship( +function groupExtrinsicsByKeyRelationship( extrinsics: Extrinsic[] ): GroupedExtrinsics { const [first, ...rest] = extrinsics.map((extrinsic) => { - const verificationRelationship = getVerificationRelationshipForTx(extrinsic) - if (!verificationRelationship) { + const keyRelationship = getKeyRelationshipForTx(extrinsic) + if (!keyRelationship) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) } - return { extrinsic, verificationRelationship } + return { extrinsic, keyRelationship } }) const groups: GroupedExtrinsics = [ { extrinsics: [first.extrinsic], - verificationRelationship: first.verificationRelationship, + keyRelationship: first.keyRelationship, }, ] - rest.forEach(({ extrinsic, verificationRelationship }) => { + rest.forEach(({ extrinsic, keyRelationship }) => { const currentGroup = groups[groups.length - 1] - const useCurrentGroup = - verificationRelationship === currentGroup.verificationRelationship + const useCurrentGroup = keyRelationship === currentGroup.keyRelationship if (useCurrentGroup) { currentGroup.extrinsics.push(extrinsic) } else { groups.push({ extrinsics: [extrinsic], - verificationRelationship, + keyRelationship, }) } }) @@ -198,7 +192,7 @@ function groupExtrinsicsByVerificationRelationship( } /** - * Authorizes/signs a list of extrinsics grouping them in batches by required verification relationship. + * 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`. @@ -218,7 +212,7 @@ export async function authorizeBatch({ submitter, }: { batchFunction: SubmittableExtrinsicFunction<'promise'> - did: Did + did: DidUri extrinsics: Extrinsic[] nonce?: BN sign: SignExtrinsicCallback @@ -242,7 +236,7 @@ export async function authorizeBatch({ }) } - const groups = groupExtrinsicsByVerificationRelationship(extrinsics) + const groups = groupExtrinsicsByKeyRelationship(extrinsics) const firstNonce = nonce || (await getNextNonce(did)) const promises = groups.map(async (group, batchIndex) => { @@ -250,11 +244,11 @@ export async function authorizeBatch({ const call = list.length === 1 ? list[0] : batchFunction(list) const txCounter = increaseNonce(firstNonce, batchIndex) - const { verificationRelationship } = group + const { keyRelationship } = group return generateDidAuthenticatedTx({ did, - verificationRelationship, + keyRelationship, sign, call, txCounter, diff --git a/packages/did/src/DidDetails/LightDidDetails.spec.ts b/packages/did/src/DidDetails/LightDidDetails.spec.ts index a4e6089934..2a7a23a926 100644 --- a/packages/did/src/DidDetails/LightDidDetails.spec.ts +++ b/packages/did/src/DidDetails/LightDidDetails.spec.ts @@ -5,18 +5,10 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocument, Did, DidUrl } from '@kiltprotocol/types' - +import { DidDocument, DidServiceEndpoint, DidUri } from '@kiltprotocol/types' import { Crypto } from '@kiltprotocol/utils' -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' +import * as Did from '../index.js' /* * Functions tested: @@ -29,12 +21,12 @@ import { */ describe('When creating an instance from the details', () => { - it('correctly assign the right sr25519 authentication key, x25519 encryption key, and services', () => { + 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[] = [ + const service: DidServiceEndpoint[] = [ { id: '#service-1', type: ['type-1'], @@ -47,34 +39,26 @@ describe('When creating an instance from the details', () => { }, ] - const lightDid = createLightDidDocument({ + const lightDid = Did.createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, }) expect(lightDid).toEqual({ - id: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - authentication: ['#authentication'], - keyAgreement: ['#encryption'], - verificationMethod: [ + uri: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + authentication: [ { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#authentication', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: authKey.publicKey, - type: 'sr25519', - }), - type: 'Multikey', + publicKey: authKey.publicKey, + type: 'sr25519', }, + ], + keyAgreement: [ { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#encryption', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: encKey.publicKey, - type: 'x25519', - }), - type: 'Multikey', + publicKey: encKey.publicKey, + type: 'x25519', }, ], service: [ @@ -98,35 +82,27 @@ describe('When creating an instance from the details', () => { new Uint8Array(32).fill(1) ) - const lightDid = createLightDidDocument({ + const lightDid = Did.createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], }) - expect(parse(lightDid.id).address).toStrictEqual(authKey.address) + expect(Did.parse(lightDid.uri).address).toStrictEqual(authKey.address) - expect(lightDid).toEqual({ - id: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, - authentication: ['#authentication'], - keyAgreement: ['#encryption'], - verificationMethod: [ + expect(lightDid).toEqual({ + uri: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, + authentication: [ { - controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, id: '#authentication', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: authKey.publicKey, - type: 'ed25519', - }), - type: 'Multikey', + publicKey: authKey.publicKey, + type: 'ed25519', }, + ], + keyAgreement: [ { - controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, id: '#encryption', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: encKey.publicKey, - type: 'x25519', - }), - type: 'Multikey', + publicKey: encKey.publicKey, + type: 'x25519', }, ], }) @@ -139,7 +115,9 @@ describe('When creating an instance from the details', () => { authentication: [authKey], } expect(() => - createLightDidDocument(invalidInput as unknown as CreateDocumentInput) + Did.createLightDidDocument( + invalidInput as unknown as Did.CreateDocumentInput + ) ).toThrowError() }) @@ -152,18 +130,20 @@ describe('When creating an instance from the details', () => { keyAgreement: [{ publicKey: encKey.publicKey, type: 'bls' }], } expect(() => - createLightDidDocument(invalidInput as unknown as CreateDocumentInput) + Did.createLightDidDocument( + invalidInput as unknown as Did.CreateDocumentInput + ) ).toThrowError() }) }) -describe('When creating an instance from a light DID', () => { - it('correctly assign the right authentication key, encryption key, and services', () => { +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[] = [ + const endpoints: DidServiceEndpoint[] = [ { id: '#service-1', type: ['type-1'], @@ -176,38 +156,30 @@ describe('When creating an instance from a light DID', () => { }, ] // We are sure this is correct because of the described case above - const expectedLightDid = createLightDidDocument({ + const expectedLightDid = Did.createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service: endpoints, }) - const { address } = parse(expectedLightDid.id) - const builtLightDid = parseDocumentFromLightDid(expectedLightDid.id) + const { address } = Did.parse(expectedLightDid.uri) + const builtLightDid = Did.parseDocumentFromLightDid(expectedLightDid.uri) expect(builtLightDid).toStrictEqual(expectedLightDid) expect(builtLightDid).toStrictEqual({ - id: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - authentication: ['#authentication'], - keyAgreement: ['#encryption'], - verificationMethod: [ + uri: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7` as DidUri, + authentication: [ { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#authentication', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: authKey.publicKey, - type: 'sr25519', - }), - type: 'Multikey', + publicKey: authKey.publicKey, + type: 'sr25519', }, + ], + keyAgreement: [ { - controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#encryption', - publicKeyMultibase: keypairToMultibaseKey({ - publicKey: encKey.publicKey, - type: 'x25519', - }), - type: 'Multikey', + publicKey: encKey.publicKey, + type: 'x25519', }, ], service: [ @@ -228,7 +200,7 @@ describe('When creating an instance from a light DID', () => { it('fail if a fragment is present according to the options', () => { const authKey = Crypto.makeKeypairFromSeed() const encKey = Crypto.makeEncryptionKeypairFromSeed() - const service: NewService[] = [ + const service: DidServiceEndpoint[] = [ { id: '#service-1', type: ['type-1'], @@ -242,25 +214,25 @@ describe('When creating an instance from a light DID', () => { ] // We are sure this is correct because of the described case above - const expectedLightDid = createLightDidDocument({ + const expectedLightDid = Did.createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, }) - const didWithFragment: DidUrl = `${expectedLightDid.id}#authentication` + const uriWithFragment: DidUri = `${expectedLightDid.uri}#authentication` - expect(() => parseDocumentFromLightDid(didWithFragment, true)).toThrow() + expect(() => Did.parseDocumentFromLightDid(uriWithFragment, true)).toThrow() expect(() => - parseDocumentFromLightDid(didWithFragment, false) + Did.parseDocumentFromLightDid(uriWithFragment, false) ).not.toThrow() }) - it('fail if the DID is not correct', () => { + it('fail if the URI is not correct', () => { const validKiltAddress = Crypto.makeKeypairFromSeed() - const incorrectDIDs = [ + const incorrectURIs = [ 'did:kilt:light:sdasdsadas', - // @ts-ignore not a valid DID + // @ts-ignore not a valid DID uri 'random-uri', 'did:kilt:light', 'did:kilt:light:', @@ -271,8 +243,8 @@ describe('When creating an instance from a light DID', () => { // Random encoded details `did:kilt:light:00${validKiltAddress}:randomdetails`, ] - incorrectDIDs.forEach((did) => { - expect(() => parseDocumentFromLightDid(did as Did)).toThrow() + incorrectURIs.forEach((uri) => { + expect(() => Did.parseDocumentFromLightDid(uri as DidUri)).toThrow() }) }) }) diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index cc23bb98e1..30a752f6bd 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -5,53 +5,32 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidDocument, Did } 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, - NewDidVerificationKey, - NewService, - DidSigningMethodType, -} from './DidDetails.js' + NewLightDidVerificationKey, +} from '@kiltprotocol/types' +import { encryptionKeyTypes } from '@kiltprotocol/types' -import { - keypairToMultibaseKey, - didKeyToVerificationMethod, - getAddressFromVerificationMethod, - parse, -} from '../Did.utils.js' -import { fragmentIdToChain, validateNewService } from '../Did.chain.js' -import { - addKeypairAsVerificationMethod, - encryptionMethodTypes, -} from './DidDetails.js' +import { SDKErrors, ss58Format, cbor } from '@kiltprotocol/utils' -/** - * Currently, a light DID does not support the use of an ECDSA key as its authentication verification method. - */ -export type LightDidSupportedVerificationKeyType = Extract< - DidSigningMethodType, - 'ed25519' | 'sr25519' -> -/** - * A new public key specified when creating a new light DID. - */ -export type NewLightDidVerificationKey = NewDidVerificationKey & { - type: LightDidSupportedVerificationKeyType -} - -type LightDidEncoding = '00' | '01' +import { getAddressByKey, parse } from '../Did.utils.js' +import { resourceIdToChain, validateService } from '../Did.chain.js' const authenticationKeyId = '#authentication' const encryptionKeyId = '#encryption' +type LightDidEncoding = '00' | '01' + const verificationKeyTypeToLightDidEncoding: Record< LightDidSupportedVerificationKeyType, LightDidEncoding @@ -73,26 +52,26 @@ const lightDidEncodingToVerificationKeyType: Record< */ 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 + * The DID authentication key. This is mandatory and will be used as the first authentication key * of the full DID upon migration. */ 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 + * The optional DID encryption key. If present, it will be used as the first key agreement key * of the full DID upon migration. */ keyAgreement?: [NewDidEncryptionKey] /** - * The set of services associated with this DID. Each service ID must be unique. + * 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({ authentication, keyAgreement, - service, + service: services, }: CreateDocumentInput): void { // Check authentication key type const authenticationKeyTypeEncoding = @@ -101,9 +80,10 @@ function validateCreateDocumentInput({ if (!authenticationKeyTypeEncoding) { throw new SDKErrors.UnsupportedKeyError(authentication[0].type) } + if ( keyAgreement?.[0].type && - !encryptionMethodTypes.includes(keyAgreement[0].type) + !encryptionKeyTypes.includes(keyAgreement[0].type) ) { throw new SDKErrors.DidError( `Encryption key type "${keyAgreement[0].type}" is not supported` @@ -113,14 +93,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. - service?.forEach((s) => { + services?.forEach((service) => { // A service ID cannot have a reserved ID that is used for key IDs. - if (s.id === '#authentication' || s.id === '#encryption') { + if (service.id === '#authentication' || service.id === '#encryption') { throw new SDKErrors.DidError( - `Cannot specify a service ID with the name "${s.id}" as it is a reserved keyword` + `Cannot specify a service ID with the name "${service.id}" as it is a reserved keyword` ) } - validateNewService(s) + validateService(service) }) } @@ -130,20 +110,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 services. + } & { 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 verification method and services of a light DID using the CBOR serialization algorithm + * Serialize the optional encryption key and service endpoints of an off-chain 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 verification method. - * @param details.service The DID services. - * @returns The Base58-encoded and CBOR-serialized light DID optional details. + * @param details.keyAgreement The DID encryption key. + * @param details.service The DID service endpoints. + * @returns The Base58-encoded and CBOR-serialized off-chain DID optional details. */ function serializeAdditionalLightDidDetails({ keyAgreement, @@ -156,7 +136,7 @@ function serializeAdditionalLightDidDetails({ } if (service && service.length > 0) { objectToSerialize[SERVICES_MAP_KEY] = service.map(({ id, ...rest }) => ({ - id: fragmentIdToChain(id), + id: resourceIdToChain(id), ...rest, })) } @@ -203,14 +183,14 @@ function deserializeAdditionalLightDidDetails( } /** - * 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. + * 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 services. + * @param input.authentication The array containing light DID authentication key. + * @param input.keyAgreement The optional array containing light DID encryption key. + * @param input.service The optional light DID service endpoints. * * @returns The resulting [[DidDocument]]. */ @@ -231,53 +211,52 @@ export function createLightDidDocument({ // Validity is checked in validateCreateDocumentInput const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[authentication[0].type] - const address = getAddressFromVerificationMethod({ - publicKeyMultibase: keypairToMultibaseKey(authentication[0]), - }) + const address = getAddressByKey(authentication[0]) const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' - const did = - `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as Did + const uri = + `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as DidUri - const didDocument: DidDocument = { - id: did, - authentication: [authenticationKeyId], - verificationMethod: [ - didKeyToVerificationMethod(did, authenticationKeyId, { - keyType: authentication[0].type, + const did: DidDocument = { + uri, + authentication: [ + { + id: authenticationKeyId, // Authentication key always has the #authentication ID. + type: authentication[0].type, publicKey: authentication[0].publicKey, - }), + }, ], service, } if (keyAgreement !== undefined) { - const { publicKey, type } = keyAgreement[0] - addKeypairAsVerificationMethod( - didDocument, - { id: encryptionKeyId, publicKey, type }, - 'keyAgreement' - ) + did.keyAgreement = [ + { + id: encryptionKeyId, // Encryption key always has the #encryption ID. + type: keyAgreement[0].type, + publicKey: keyAgreement[0].publicKey, + }, + ] } - return didDocument + return did } /** - * Create a light [[DidDocument]] by parsing the provided input 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 as verification methods. + * Private keys are assumed to already live in another storage, as it contains reference only to public keys. * - * @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. + * @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( - did: Did, + uri: DidUri, failIfFragmentPresent = true ): DidDocument { const { @@ -287,16 +266,16 @@ export function parseDocumentFromLightDid( fragment, type, authKeyTypeEncoding, - } = parse(did) + } = parse(uri) if (type !== 'light') { throw new SDKErrors.DidError( - `Cannot build a light DID Document from the provided DID "${did}" because it does not refer to a light DID` + `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 Document from the provided DID "${did}" because it has a fragment` + `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` ) } const keyType = diff --git a/packages/did/src/DidDetails/index.ts b/packages/did/src/DidDetails/index.ts index daceeda2f3..99c4484aa9 100644 --- a/packages/did/src/DidDetails/index.ts +++ b/packages/did/src/DidDetails/index.ts @@ -5,20 +5,6 @@ * 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 type { - BaseNewDidKey, - DidEncryptionMethodType, - DidSigningMethodType, - DidVerificationMethodType, - NewDidEncryptionKey, - NewDidVerificationKey, - NewService, - NewVerificationMethod, -} from './DidDetails.js' -export { - isValidDidVerificationType, - isValidEncryptionMethodType, -} from './DidDetails.js' +export * from './DidDetails.js' export * from './LightDidDetails.js' export * from './FullDidDetails.js' diff --git a/packages/did/src/DidResolver/DidContexts.ts b/packages/did/src/DidDocumentExporter/DidContexts.ts similarity index 80% rename from packages/did/src/DidResolver/DidContexts.ts rename to packages/did/src/DidDocumentExporter/DidContexts.ts index 50249c96d4..196e35a2df 100644 --- a/packages/did/src/DidResolver/DidContexts.ts +++ b/packages/did/src/DidDocumentExporter/DidContexts.ts @@ -7,24 +7,18 @@ // @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 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. + * 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://QmPtQ7wbdxbTuGugx4nFAyrhspcqXKrnriuGr7x4NYaZYN' + '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. @@ -37,11 +31,6 @@ 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. @@ -50,7 +39,6 @@ export const DID_CONTEXTS = { [KILT_DID_CONTEXT_URL]: { '@context': [ W3C_SECURITY_CONTEXT_URL, - W3C_MULTIKEY_CONTEXT_URL, { '@protected': true, KiltPublishedCredentialCollectionV1: @@ -119,5 +107,4 @@ export const DID_CONTEXTS = { }, }, ...Object.fromEntries(securityContextsMap), - ...Object.fromEntries(multikeyContextsMap), } diff --git a/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts b/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts new file mode 100644 index 0000000000..4a957c40d3 --- /dev/null +++ b/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts @@ -0,0 +1,336 @@ +/** + * 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 new file mode 100644 index 0000000000..bfaebeb439 --- /dev/null +++ b/packages/did/src/DidDocumentExporter/DidDocumentExporter.ts @@ -0,0 +1,114 @@ +/** + * 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 new file mode 100644 index 0000000000..e9c90965e0 --- /dev/null +++ b/packages/did/src/DidDocumentExporter/README.md @@ -0,0 +1,5 @@ +# 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 new file mode 100644 index 0000000000..3243aecb8a --- /dev/null +++ b/packages/did/src/DidDocumentExporter/index.ts @@ -0,0 +1,9 @@ +/** + * 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 745e6777d5..882568d3bd 100644 --- a/packages/did/src/DidLinks/AccountLinks.chain.ts +++ b/packages/did/src/DidLinks/AccountLinks.chain.ts @@ -8,26 +8,26 @@ 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 { - Did, - HexString, - KeyringPair, - KiltAddress, -} from '@kiltprotocol/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 { + DidUri, + HexString, + KeyringPair, + KiltAddress, +} from '@kiltprotocol/types' -import type { EncodedSignature } from '../Did.chain.js' +import { EncodedSignature } from '../Did.utils.js' import { toChain } from '../Did.chain.js' /** @@ -134,7 +134,7 @@ function getUnprefixedSignature( } async function getLinkingChallengeV1( - did: Did, + did: DidUri, validUntil: BN ): Promise { const api = ConfigService.get('api') @@ -156,7 +156,7 @@ async function getLinkingChallengeV1( .toU8a() } -function getLinkingChallengeV2(did: Did, validUntil: BN): Uint8Array { +function getLinkingChallengeV2(did: DidUri, validUntil: BN): Uint8Array { return stringToU8a( `Publicly link the signing address to ${did} before block number ${validUntil}` ) @@ -167,12 +167,12 @@ function getLinkingChallengeV2(did: Did, 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 DID that should be linked to an account. + * @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: Did, + did: DidUri, validUntil: BN ): Promise { const api = ConfigService.get('api') @@ -261,7 +261,7 @@ export function getWrappedChallenge( */ export async function associateAccountToChainArgs( accountAddress: Address, - did: Did, + did: DidUri, 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 622d2d2729..4a1c212804 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -5,35 +5,50 @@ * 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 { - DereferenceResult, - Did as KiltDid, - DidUrl, +import type { + ConformingDidKey, + ConformingDidServiceEndpoint, + DidEncryptionKey, + DidKey, + DidResolutionDocumentMetadata, + DidResolutionMetadata, + DidResolutionResult, + DidResourceUri, + DidServiceEndpoint, + DidUri, + DidVerificationKey, KiltAddress, - RepresentationResolutionResult, - ResolutionResult, - Service, + ResolvedDidKey, + ResolvedDidServiceEndpoint, UriFragment, - VerificationMethod, } from '@kiltprotocol/types' -import { Crypto, cbor } from '@kiltprotocol/utils' -import { stringToU8a } from '@polkadot/util' +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: KiltDid = `did:kilt:${addressWithAuthenticationKey}` +const didWithAuthenticationKey: DidUri = `did:kilt:${addressWithAuthenticationKey}` const addressWithAllKeys = `4sDxAgw86PFvC6TQbvZzo19WoYF6T4HcLd2i9wzvojkLXLvp` -const didWithAllKeys: KiltDid = `did:kilt:${addressWithAllKeys}` +const didWithAllKeys: DidUri = `did:kilt:${addressWithAllKeys}` const addressWithServiceEndpoints = `4q4DHavMdesaSMH3g32xH3fhxYPt5pmoP9oSwgTr73dQLrkN` -const didWithServiceEndpoints: KiltDid = `did:kilt:${addressWithServiceEndpoints}` +const didWithServiceEndpoints: DidUri = `did:kilt:${addressWithServiceEndpoints}` const deletedAddress = '4rrVTLAXgeoE8jo8si571HnqHtd5WmvLuzfH6e1xBsVXsRo7' -const deletedDid: KiltDid = `did:kilt:${deletedAddress}` +const deletedDid: DidUri = `did:kilt:${deletedAddress}` const didIsBlacklisted = ApiMocks.mockChainQueryReturn( 'did', @@ -48,7 +63,7 @@ beforeAll(() => { mockedApi = ApiMocks.getMockedApi() ConfigService.set({ api: mockedApi }) - // Mock `api.call.did.query(did)` + // 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') @@ -80,63 +95,42 @@ beforeAll(() => { }) }) -function generateAuthenticationVerificationMethod( - controller: KiltDid -): VerificationMethod { +function generateAuthenticationKey(): DidVerificationKey { return { id: '#auth', - controller, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - publicKey: new Uint8Array(32).fill(0), - type: 'ed25519', - }), + type: 'ed25519', + publicKey: new Uint8Array(32).fill(0), } } -function generateEncryptionVerificationMethod( - controller: KiltDid -): VerificationMethod { +function generateEncryptionKey(): DidEncryptionKey { return { id: '#enc', - controller, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - publicKey: new Uint8Array(32).fill(1), - type: 'x25519', - }), + type: 'x25519', + publicKey: new Uint8Array(32).fill(1), + includedAt: new BN(15), } } -function generateAssertionVerificationMethod( - controller: KiltDid -): VerificationMethod { +function generateAttestationKey(): DidVerificationKey { return { id: '#att', - controller, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - publicKey: new Uint8Array(32).fill(2), - type: 'sr25519', - }), + type: 'sr25519', + publicKey: new Uint8Array(32).fill(2), + includedAt: new BN(20), } } -function generateCapabilityDelegationVerificationMethod( - controller: KiltDid -): VerificationMethod { +function generateDelegationKey(): DidVerificationKey { return { id: '#del', - controller, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - publicKey: new Uint8Array(33).fill(3), - type: 'ecdsa', - }), + type: 'ecdsa', + publicKey: new Uint8Array(32).fill(3), + includedAt: new BN(25), } } -function generateServiceEndpoint(serviceId: UriFragment): Service { +function generateServiceEndpoint(serviceId: UriFragment): DidServiceEndpoint { const fragment = serviceId.substring(1) return { id: serviceId, @@ -146,338 +140,256 @@ 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 DID derived from the identifier provided in the resolution. +// 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: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` - const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - id: did, - authentication: [authMethod.id], - verificationMethod: [authMethod], + uri: `did:kilt:${identifier as unknown as KiltAddress}`, + authentication: [generateAuthenticationKey()], 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 () => { +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 verificationMethodUrl: DidUrl = `${fullDid}#auth` + const keyIdUri: DidResourceUri = `${fullDid}#auth` - expect( - await Did.dereference(verificationMethodUrl, { - accept: 'application/did+json', - }) - ).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: generateAuthenticationVerificationMethod(fullDid), + expect(await resolveKey(keyIdUri)).toStrictEqual({ + controller: fullDid, + publicKey: new Uint8Array(32).fill(0), + id: keyIdUri, + type: 'ed25519', }) }) - it('returns error if either the DID or the verification method do not exist', async () => { - let verificationMethodUrl: DidUrl = `${deletedDid}#enc` + it('returns null if either the DID or the key do not exist', async () => { + let keyIdUri: DidResourceUri = `${deletedDid}#enc` - expect( - await Did.dereference(verificationMethodUrl) - ).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - }) + await expect(resolveKey(keyIdUri)).rejects.toThrow() const didWithNoEncryptionKey = didWithAuthenticationKey - verificationMethodUrl = `${didWithNoEncryptionKey}#enc` + keyIdUri = `${didWithNoEncryptionKey}#enc` - expect( - await Did.dereference(verificationMethodUrl) - ).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - }) + await expect(resolveKey(keyIdUri)).rejects.toThrow() }) - it('throws for invalid URLs', async () => { - const invalidUrl = 'invalid-url' as DidUrl - expect(await Did.dereference(invalidUrl)).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { error: 'invalidDidUrl' }, - }) + 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', () => { +describe('When resolving a service endpoint', () => { 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` + const serviceIdUri: DidResourceUri = `${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`], - }, + await resolveService(serviceIdUri) + ).toStrictEqual({ + id: serviceIdUri, + type: [`type-service-1`], + serviceEndpoint: [`x:url-service-1`], }) }) - it('returns error if either the DID or the service do not exist', async () => { + 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() - const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` - const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - id: did, - authentication: [authMethod.id], - verificationMethod: [authMethod], + uri: `did:kilt:${identifier as unknown as KiltAddress}`, + authentication: [generateAuthenticationKey()], }, } }) jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` - const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - id: did, - authentication: [authMethod.id], - verificationMethod: [authMethod], + uri: `did:kilt:${identifier as unknown as KiltAddress}`, + authentication: [generateAuthenticationKey()], }, } }) - let serviceIdUrl: DidUrl = `${deletedDid}#service-1` + let serviceIdUri: DidResourceUri = `${deletedDid}#service-1` - expect( - await Did.dereference(serviceIdUrl) - ).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - }) + await expect(resolveService(serviceIdUri)).rejects.toThrow() const didWithNoServiceEndpoints = didWithAuthenticationKey - serviceIdUrl = `${didWithNoServiceEndpoints}#service-1` + serviceIdUri = `${didWithNoServiceEndpoints}#service-1` - expect( - await Did.dereference(serviceIdUrl) - ).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - }) + 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 verification method', async () => { + it('correctly resolves the document with an authentication key', 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), - }), - }, - ], + 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() - const did: KiltDid = `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], + authentication: [generateAuthenticationKey()], + keyAgreement: [generateEncryptionKey()], + assertionMethod: [generateAttestationKey()], + capabilityDelegation: [generateDelegationKey()], + uri: `did:kilt:${identifier as unknown as KiltAddress}`, }, } }) 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), - }), - }, - ], + 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. + it('correctly resolves the document with service endpoints', async () => { + // Mock transform function changed to return two service endpoints. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` - const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - id: did, - authentication: [authMethod.id], - verificationMethod: [authMethod], + authentication: [generateAuthenticationKey()], service: [ generateServiceEndpoint('#id-1'), generateServiceEndpoint('#id-2'), ], + uri: `did:kilt:${identifier as unknown as KiltAddress}`, }, } }) 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'], - }, - ], + 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. + // Mock transform function changed to return two service endpoints. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` - const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - id: did, - authentication: [authMethod.id], - verificationMethod: [authMethod], - alsoKnownAs: ['w3n:w3nick'], + authentication: [generateAuthenticationKey()], + service: [], + uri: `did:kilt:${identifier as unknown as KiltAddress}`, }, + web3Name: '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'], - }, + 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 () => { @@ -487,14 +399,10 @@ describe('When resolving a full DID', () => { .mockResolvedValueOnce( augmentedApi.createType('Option', null) ) - const randomKeypair = makeSigningKeyTool().authentication[0] - const randomDid = Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(randomKeypair), - }) - expect(await Did.resolve(randomDid)).toStrictEqual({ - didDocumentMetadata: {}, - didResolutionMetadata: { error: 'notFound' }, - }) + const randomDid = getFullDidUriFromKey( + makeSigningKeyTool().authentication[0] + ) + expect(await resolve(randomDid)).toBeNull() }) it('correctly resolves a deleted DID', async () => { @@ -506,11 +414,27 @@ describe('When resolving a full DID', () => { ) mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) - expect(await Did.resolve(deletedDid)).toStrictEqual({ - didDocumentMetadata: { deactivated: true }, - didResolutionMetadata: {}, - didDocument: { id: deletedDid }, + 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) }) }) @@ -531,31 +455,26 @@ describe('When resolving a light DID', () => { 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, - }, + 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 () => { + it('correctly resolves the document with authentication key, encryption key, and two service endpoints', async () => { const lightDid = Did.createLightDidDocument({ authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], keyAgreement: [{ publicKey: encryptionKey.publicKey, type: 'x25519' }], @@ -564,44 +483,38 @@ describe('When resolving a light DID', () => { 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'], - }, - ], - }, + 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 () => { @@ -625,432 +538,175 @@ describe('When resolving a light DID', () => { }, }) ) - const migratedDid: KiltDid = `did:kilt:light:00${addressWithAuthenticationKey}` - expect(await Did.resolve(migratedDid)).toStrictEqual({ - didDocumentMetadata: { canonicalId: didWithAuthenticationKey }, - didResolutionMetadata: {}, - didDocument: { - id: migratedDid, - }, + 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: KiltDid = `did:kilt:light:00${deletedAddress}` - expect(await Did.resolve(migratedDid)).toStrictEqual({ - didDocumentMetadata: { deactivated: true }, - didResolutionMetadata: {}, - didDocument: { - id: migratedDid, - }, + 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('DID Resolution compliance', () => { +describe('When resolving with the spec compliant resolver', () => { 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) => { + // Mock transform function changed to return two service endpoints. + jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() - const did: KiltDid = `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: KiltDid = - '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', - }), - }, + authentication: [generateAuthenticationKey()], + service: [ + generateServiceEndpoint('#id-1'), + generateServiceEndpoint('#id-2'), ], + uri: `did:kilt:${identifier as unknown as KiltAddress}`, }, - }) - }) - 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: KiltDid = - '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 KiltDid - expect(await Did.resolve(did)).toStrictEqual({ - didDocumentMetadata: {}, - didResolutionMetadata: { error: 'invalidDid' }, - }) + web3Name: 'w3nick', + } }) }) - 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: KiltDid = - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - expect( - await Did.resolveRepresentation(did) - ).toStrictEqual({ - didDocumentMetadata: {}, - didResolutionMetadata: { contentType: Did.DID_JSON_CONTENT_TYPE }, - didDocumentStream: stringToU8a( - 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: KiltDid = - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - expect( - await Did.resolveRepresentation(did, { - accept: 'application/did+ld+json', - }) - ).toStrictEqual({ - didDocumentMetadata: {}, - didResolutionMetadata: { - contentType: Did.DID_JSON_LD_CONTENT_TYPE, - }, - didDocumentStream: stringToU8a( - 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: KiltDid = - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - expect( - await Did.resolveRepresentation(did, { - accept: 'application/did+cbor', - }) - ).toMatchObject({ - didDocumentMetadata: {}, - didResolutionMetadata: { contentType: Did.DID_CBOR_CONTENT_TYPE }, - didDocumentStream: Uint8Array.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: KiltDid = - '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 KiltDid - expect( - await Did.resolveRepresentation(did) - ).toStrictEqual({ - didDocumentMetadata: {}, - didResolutionMetadata: { error: 'invalidDid' }, - }) + 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, }) - it('returns the right `didResolutionMetadata.error` when the requested content type is not supported', async () => { - const did: KiltDid = - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - expect( - await Did.resolveRepresentation(did, { - accept: 'application/json' as Did.SupportedContentType, - }) - ).toStrictEqual({ - didDocumentMetadata: {}, - didResolutionMetadata: { error: 'representationNotSupported' }, - }) + + 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']) }) - 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: KiltDid = - '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: KiltDid = - '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: KiltDid = - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - expect( - await Did.dereference(did, { accept: 'application/did+cbor' }) - ).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { contentType: Did.DID_CBOR_CONTENT_TYPE }, - contentStream: Uint8Array.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).mockImplementationOnce((linkedInfo) => { - const { identifier } = linkedInfo.unwrap() - const did: KiltDid = `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'], - }, - }) - }) + 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('returns the right `dereferencingMetadata.error` when the DID does not exist', async () => { + + 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) + augmentedApi.createType('Option', null) ) + mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) + + const { didDocument, didDocumentMetadata, didResolutionMetadata } = + await resolveCompliant(deletedDid) - const did: KiltDid = - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - expect(await Did.dereference(did)).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, + expect(didDocumentMetadata).toStrictEqual({ + deactivated: true, }) + expect(didResolutionMetadata).toStrictEqual({}) + expect(didDocument).toStrictEqual({ id: deletedDid }) }) - it('returns the right `didResolutionMetadata.error` when the input DID is invalid', async () => { - const did = 'did:kilt:test-did' as unknown as KiltDid - expect(await Did.dereference(did)).toStrictEqual({ - contentMetadata: {}, - dereferencingMetadata: { error: 'invalidDidUrl' }, + + 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('returns empty `contentMetadata` and `dereferencingMetadata.contentType: application/did+json` (the default value) when the `options.accept` value is invalid', async () => { - const did: KiltDid = - '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', - }), - }, - ], - }, - }) + + 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/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index d3659f9a81..16ad075122 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -6,87 +6,68 @@ */ import type { - DereferenceContentMetadata, - DereferenceContentStream, - DereferenceOptions, - DereferenceResult, - DidDocument, - DidResolver, - Did, - DidUrl, - FailedDereferenceMetadata, - JsonLd, - RepresentationResolutionResult, - ResolutionDocumentMetadata, - ResolutionOptions, - ResolutionResult, + ConformingDidResolutionResult, + DidKey, + DidResolutionResult, + DidResourceUri, + DidUri, + KeyRelationship, + ResolvedDidKey, + ResolvedDidServiceEndpoint, + UriFragment, } from '@kiltprotocol/types' - -import { stringToU8a } from '@polkadot/util' +import { SDKErrors } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' -import { cbor } from '@kiltprotocol/utils' -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' -import { linkedInfoFromChain } from '../Did.rpc.js' +import * as Did from '../index.js' import { toChain } from '../Did.chain.js' -import { getFullDid, parse, validateDid } from '../Did.utils.js' -import { parseDocumentFromLightDid } from '../DidDetails/LightDidDetails.js' -import { isValidVerificationRelationship } from '../DidDetails/DidDetails.js' - -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' +import { linkedInfoFromChain } from '../Did.rpc.js' +import { getFullDidUri, parse } from '../Did.utils.js' +import { exportToDidDocument } from '../DidDocumentExporter/DidDocumentExporter.js' /** - * Supported content types for DID resolution and dereferencing. + * 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. */ -export type SupportedContentType = - | 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_CONTENT_TYPE || - input === DID_JSON_LD_CONTENT_TYPE || - input === DID_CBOR_CONTENT_TYPE - ) -} - -type InternalResolutionResult = { - document?: DidDocument - documentMetadata: ResolutionDocumentMetadata -} - -async function resolveInternal( - did: Did -): Promise { +export async function resolve( + did: DidUri +): Promise { const { type } = parse(did) const api = ConfigService.get('api') - - const { document } = await api.call.did - .query(toChain(did)) + 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)) .then(linkedInfoFromChain) - .catch(() => ({ document: undefined })) + .catch(() => ({ document: undefined, web3Name: undefined })) - if (type === 'full' && document !== undefined) { + if (type === 'full' && document) { return { document, - documentMetadata: {}, + metadata: { + deactivated: false, + }, + ...(web3Name && { web3Name }), } } + // 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 is returned as we consider this DID deactivated/deleted. - documentMetadata: { + // No canonicalId and no details are returned as we consider this DID deactivated/deleted. + metadata: { deactivated: true, }, - document: { - id: did, - }, } } @@ -94,324 +75,205 @@ async function resolveInternal( return null } - const lightDocument = parseDocumentFromLightDid(did, false) + const lightDocument = Did.parseDocumentFromLightDid(did, false) // If a full DID with same subject is present, return the resolution metadata accordingly. - if (document !== undefined) { + if (document) { return { - documentMetadata: { - canonicalId: getFullDid(did), - }, - document: { - id: lightDocument.id, + metadata: { + canonicalId: getFullDidUri(did), + deactivated: false, }, } } // 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: {}, + metadata: { + deactivated: false, + }, } } /** * 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. - * @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). + * @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). */ -export async function resolve( - did: Did, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolutionOptions: ResolutionOptions = {} -): Promise { +export async function resolveCompliant( + did: DidUri +): Promise { + const result: ConformingDidResolutionResult = { + didDocumentMetadata: {}, + didResolutionMetadata: {}, + } try { - validateDid(did, 'Did') + Did.validateUri(did, 'Did') } catch (error) { - return { - didResolutionMetadata: { - error: 'invalidDid', - }, - didDocumentMetadata: {}, + result.didResolutionMetadata.error = 'invalidDid' + if (error instanceof Error) { + result.didResolutionMetadata.errorMessage = + error.name + error.message ? `: ${error.message}` : '' } + return result } - - const resolutionResult = await resolveInternal(did) - if (resolutionResult === null) { - return { - didResolutionMetadata: { - error: 'notFound', - }, - didDocumentMetadata: {}, - } + const resolutionResult = await resolve(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}`] } - const { documentMetadata: didDocumentMetadata, document: didDocument } = - resolutionResult + return result +} +/** + * 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 { - didResolutionMetadata: {}, - didDocumentMetadata, - didDocument, + controller: did, + id: `${did}${id}`, + publicKey, + type, + ...(includedAt && { includedAt }), } } /** - * 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`. + * Converts the DID key returned by the `resolveKey()` into the format used in the SDK. * - * @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). + * @param key The key in the resolveKey-format. + * @returns The key in the SDK format. */ -export async function resolveRepresentation( - did: Did, - { accept }: DereferenceOptions = { - accept: DID_JSON_CONTENT_TYPE, - } -): Promise> { - const inputTransform = (() => { - switch (accept) { - case 'application/did+json': { - return (didDoc: DidDocument) => stringToU8a(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 stringToU8a(JSON.stringify(jsonLdDoc)) - } - } - case 'application/did+cbor': { - return (didDoc: DidDocument) => Uint8Array.from(cbor.encode(didDoc)) - } - default: { - return null - } - } - })() - if (inputTransform === null) { - 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 RepresentationResolutionResult - } - +export function resolvedKeyToKey(key: ResolvedDidKey): DidKey { + const { id, publicKey, includedAt, type } = key return { - didDocumentMetadata, - didResolutionMetadata: { - ...didResolutionMetadata, - contentType: accept, - }, - didDocumentStream: inputTransform(didDocument), - } as RepresentationResolutionResult + id: Did.parse(id).fragment as UriFragment, + publicKey, + type, + ...(includedAt && { includedAt }), + } } -type InternalDereferenceResult = - | FailedDereferenceMetadata - | { - contentMetadata: DereferenceContentMetadata - contentStream: DereferenceContentStream - } - /** - * Type guard checking whether the provided input is a [[FailedDereferenceMetadata]]. + * Resolve a DID key URI to the key details. * - * @param input The input to check. - * @returns Whether the input is a [[FailedDereferenceMetadata]]. + * @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 function isFailedDereferenceMetadata( - input: unknown -): input is FailedDereferenceMetadata { - return (input as FailedDereferenceMetadata)?.error !== undefined -} +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` + ) + } -async function dereferenceInternal( - didUrl: Did | DidUrl, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - dereferenceOptions: DereferenceOptions -): Promise { - const { did, queryParameters, fragment } = parse(didUrl) + const resolved = await resolve(did) + if (!resolved) { + throw new SDKErrors.DidNotFoundError() + } - const { didDocument, didDocumentMetadata } = await resolve(did) + const { + document, + metadata: { canonicalId }, + } = resolved - if (didDocument === undefined) { - return { - error: 'notFound', - } + // 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() } - if (fragment === undefined) { - return { - contentMetadata: didDocumentMetadata, - contentStream: didDocument, - } + const key = Did.getKey(document, keyId) + if (!key) { + throw new SDKErrors.DidNotFoundError('Key not found in DID') } - const [dereferencedResource, dereferencingError] = (() => { - const verificationMethod = didDocument?.verificationMethod?.find( - ({ controller, id }) => controller === didDocument.id && id === fragment + // 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}"` ) - - if (verificationMethod !== undefined) { - 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 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[requiredVerificationRelationship]?.includes( - verificationMethod.id - ) - ) { - 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 [ - 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) - if (service === undefined) { - return [ - null, - { - error: 'notFound', - } as FailedDereferenceMetadata, - ] - } - return [service, null] - })() - - if (dereferencingError !== null) { - return dereferencingError } - return { - contentStream: dereferencedResource, - contentMetadata: {}, - } + return keyToResolvedKey(key, did) } /** - * 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`. + * Resolve a DID service URI to the service details. * - * @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). + * @param serviceUri The DID service URI. + * @returns The details associated with the service endpoint. */ -export async function dereference( - didUrl: Did | DidUrl, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - { accept }: DereferenceOptions = { - accept: DID_JSON_CONTENT_TYPE, +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` + ) } -): Promise> { - // The spec does not include an error for unsupported content types for dereferences - const contentType = isValidContentType(accept) - ? accept - : DID_JSON_CONTENT_TYPE - try { - validateDid(didUrl) - } catch (error) { - return { - dereferencingMetadata: { - error: 'invalidDidUrl', - }, - contentMetadata: {}, - } + const resolved = await resolve(did) + if (!resolved) { + throw new SDKErrors.DidNotFoundError() } - const dereferenceResult = await dereferenceInternal(didUrl, { - accept: contentType, - }) + const { + document, + metadata: { canonicalId }, + } = resolved - if (isFailedDereferenceMetadata(dereferenceResult)) { - return { - contentMetadata: {}, - dereferencingMetadata: dereferenceResult, - } + // 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 [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, contentType] - } - if (contentType === 'application/did+ld+json') { - return [ - { - ...dereferenceResult.contentStream, - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], - }, - contentType, - ] - } - // contentType === 'application/did+cbor' - return [ - Uint8Array.from(cbor.encode(dereferenceResult.contentStream)), - contentType, - ] - })() + const service = Did.getService(document, serviceId) + if (!service) { + throw new SDKErrors.DidNotFoundError('Service not found in DID') + } return { - dereferencingMetadata: { - contentType: contentTypeValue as SupportedContentType, - }, - contentMetadata: dereferenceResult.contentMetadata, - contentStream: stream, + ...service, + id: `${did}${serviceId}`, } } - -/** - * 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/index.ts b/packages/did/src/DidResolver/index.ts index 4c7d68c721..64023f7e73 100644 --- a/packages/did/src/DidResolver/index.ts +++ b/packages/did/src/DidResolver/index.ts @@ -5,5 +5,4 @@ * found in the LICENSE file in the root directory of this source tree. */ -export * from './DidContexts.js' export * from './DidResolver.js' diff --git a/packages/did/src/index.ts b/packages/did/src/index.ts index f9aa46843f..38320500db 100644 --- a/packages/did/src/index.ts +++ b/packages/did/src/index.ts @@ -10,6 +10,7 @@ */ export * from './DidDetails/index.js' +export * from './DidDocumentExporter/index.js' export * from './DidResolver/index.js' export * from './Did.chain.js' export * from './Did.rpc.js' diff --git a/packages/legacy-credentials/src/Claim.spec.ts b/packages/legacy-credentials/src/Claim.spec.ts index 796f33c146..ba8b7f991c 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 { Did, ICType, IClaim } from '@kiltprotocol/types' +import type { DidUri, 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: Did + let did: DidUri 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 4f515394fc..cf21a4b6f1 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 { - Did as KiltDid, + DidUri, HexString, ICType, IClaim, @@ -143,7 +143,7 @@ export function verifyDataStructure(input: IClaim | PartialClaim): void { throw new SDKErrors.CTypeHashMissingError() } if ('owner' in input) { - Did.validateDid(input.owner, 'Did') + Did.validateUri(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: KiltDid + claimOwner: DidUri ): IClaim { CType.verifyClaimAgainstNestedSchemas(cTypeInput, nestedCType, claimContents) @@ -198,7 +198,7 @@ export function fromNestedCTypeClaim( } /** - * Constructs a new Claim from the given [[ICType]], IClaim['contents'] and [[Did]]. + * Constructs a new Claim from the given [[ICType]], IClaim['contents'] and [[DidUri]]. * * @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: KiltDid + claimOwner: DidUri ): 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 c5bc9f12ef..2d4d5dbfb5 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -13,29 +13,23 @@ import { ConfigService } from '@kiltprotocol/config' import { Attestation, CType, init } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import type { - DereferenceResult, DidDocument, + DidResourceUri, DidSignature, - Did as KiltDid, - DidUrl, + DidUri, + DidVerificationKey, IAttestation, IClaim, IClaimContents, ICredential, ICredentialPresentation, + ResolvedDidKey, 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, @@ -50,7 +44,7 @@ const testCType = CType.fromProperties('Credential', { }) function buildCredential( - claimerDid: KiltDid, + claimerDid: DidUri, contents: IClaimContents, legitimations: ICredential[] ): ICredential { @@ -443,32 +437,24 @@ describe('Presentations', () => { let identityDave: DidDocument let migratedAndDeletedLightDid: DidDocument - async function dereferenceDidUrl( - didUrl: DidUrl | KiltDid - ): Promise> { - const { did } = Did.parse(didUrl) - const didDocument = [ + async function didResolveKey( + keyUri: DidResourceUri + ): Promise { + const { did } = Did.parse(keyUri) + const document = [ identityAlice, identityBob, identityCharlie, identityDave, - ].find(({ id }) => id === did) - if (!didDocument) - return { - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - } - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: didDocument, - } + ].find(({ uri }) => uri === did) + if (!document) throw new Error('Cannot resolve mocked DID') + return Did.keyToResolvedKey(document.authentication[0], did) } // TODO: Cleanup file by migrating setup functions and removing duplicate tests. async function buildPresentation( claimer: DidDocument, - attesterDid: KiltDid, + attesterDid: DidUri, contents: IClaim['contents'], legitimations: ICredential[], sign: SignCallback @@ -477,7 +463,7 @@ describe('Presentations', () => { const claim = Claim.fromCTypeAndClaimContents( testCType, contents, - claimer.id + claimer.uri ) // build credential with legitimations const credential = Credential.fromClaim(claim, { @@ -508,7 +494,7 @@ describe('Presentations', () => { ) ;[legitimation] = await buildPresentation( identityAlice, - identityBob.id, + identityBob.uri, {}, [], keyAlice.getSignCallback(identityAlice) @@ -519,7 +505,7 @@ describe('Presentations', () => { .mockResolvedValue( ApiMocks.mockChainQueryReturn('attestation', 'attestations', { revoked: false, - attester: Did.toChain(identityBob.id), + attester: Did.toChain(identityBob.uri), ctypeHash: CType.idToHash(testCType.$id), } as any) as any ) @@ -528,7 +514,7 @@ describe('Presentations', () => { it('verify credentials signed by a full DID', async () => { const [presentation] = await buildPresentation( identityCharlie, - identityAlice.id, + identityAlice.uri, { a: 'a', b: 'b', @@ -542,9 +528,9 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - dereferenceDidUrl, + didResolveKey, }) - ).resolves.toMatchObject({ revoked: false, attester: identityBob.id }) + ).resolves.toMatchObject({ revoked: false, attester: identityBob.uri }) }) it('verify credentials signed by a light DID', async () => { const { getSignCallback, authentication } = makeSigningKeyTool('ed25519') @@ -554,7 +540,7 @@ describe('Presentations', () => { const [presentation] = await buildPresentation( identityDave, - identityAlice.id, + identityAlice.uri, { a: 'a', b: 'b', @@ -568,14 +554,14 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - dereferenceDidUrl, + didResolveKey, }) - ).resolves.toMatchObject({ revoked: false, attester: identityBob.id }) + ).resolves.toMatchObject({ revoked: false, attester: identityBob.uri }) }) it('throws if signature is missing on credential presentation', async () => { const credential = buildCredential( - identityBob.id, + identityBob.uri, { a: 'a', b: 'b', @@ -586,7 +572,7 @@ describe('Presentations', () => { await expect( Credential.verifyPresentation(credential as ICredentialPresentation, { ctype: testCType, - dereferenceDidUrl, + didResolveKey, }) ).rejects.toThrow() }) @@ -598,7 +584,7 @@ describe('Presentations', () => { }) const credential = buildCredential( - identityBob.id, + identityBob.uri, { a: 'a', b: 'b', @@ -614,7 +600,7 @@ describe('Presentations', () => { await expect( Credential.verifySignature(presentation, { - dereferenceDidUrl, + didResolveKey, }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) @@ -626,7 +612,7 @@ describe('Presentations', () => { }) const credential = buildCredential( - identityAlice.id, + identityAlice.uri, { a: 'a', b: 'b', @@ -635,20 +621,18 @@ describe('Presentations', () => { [legitimation] ) - // sign presentation using Alice's authentication verification method + // sign presentation using Alice's authenication key const presentation = await Credential.createPresentation({ credential, signCallback: keyAlice.getSignCallback(identityAlice), }) - // but replace signer key reference with authentication verification method of light did - presentation.claimerSignature.keyUri = `${identityDave.id}${ - identityDave.authentication![0] - }` + // but replace signer key reference with authentication key of light did + presentation.claimerSignature.keyUri = `${identityDave.uri}${identityDave.authentication[0].id}` // signature would check out but mismatch should be detected await expect( Credential.verifySignature(presentation, { - dereferenceDidUrl, + didResolveKey, }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) @@ -661,7 +645,7 @@ describe('Presentations', () => { const [presentation] = await buildPresentation( migratedAndDeletedLightDid, - identityAlice.id, + identityAlice.uri, { a: 'a', b: 'b', @@ -675,7 +659,7 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - dereferenceDidUrl, + didResolveKey, }) ).rejects.toThrowError() }) @@ -683,7 +667,7 @@ describe('Presentations', () => { it('Typeguard should return true on complete Credentials', async () => { const [presentation] = await buildPresentation( identityAlice, - identityBob.id, + identityBob.uri, {}, [], keyAlice.getSignCallback(identityAlice) @@ -696,7 +680,7 @@ describe('Presentations', () => { it('Should throw error when attestation is from different credential', async () => { const [credential, attestation] = await buildPresentation( identityAlice, - identityBob.id, + identityBob.uri, {}, [], keyAlice.getSignCallback(identityAlice) @@ -718,7 +702,7 @@ describe('Presentations', () => { it('returns Claim Hash of the attestation', async () => { const [credential, attestation] = await buildPresentation( identityAlice, - identityBob.id, + identityBob.uri, {}, [], keyAlice.getSignCallback(identityAlice) @@ -746,79 +730,29 @@ 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?: NewDidVerificationKey + newAuthenticationKey?: DidVerificationKey ): DidDocument { - const id = Did.getFullDid(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 - ) - // Override the verification method ID to the computed one - lightDidVerificationMethod.id = computeKeyId(publicKey) - return lightDidVerificationMethod - })() + const uri = Did.getFullDidUri(lightDidForId.uri) + const authKey = newAuthenticationKey || lightDidForId.authentication[0] return { - id, - authentication: [authMethod.id], - verificationMethod: [authMethod], + uri, + authentication: [authKey], } } - async function dereferenceDidUrl( - didUrl: DidUrl | KiltDid - ): Promise> { - const { did } = Did.parse(didUrl) - switch (did) { - case migratedClaimerLightDid.id: { - return { - contentMetadata: { canonicalId: migratedClaimerFullDid.id }, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: { id: migratedClaimerLightDid.id }, - } - } - case unmigratedClaimerLightDid.id: { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: unmigratedClaimerLightDid, - } - } - case migratedClaimerFullDid.id: { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: migratedClaimerFullDid, - } - } - case attester.id: { - return { - contentMetadata: {}, - dereferencingMetadata: { contentType: 'application/did+json' }, - contentStream: attester, - } - } - default: { - return { - contentMetadata: {}, - dereferencingMetadata: { error: 'notFound' }, - } - } - } + 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) } beforeAll(async () => { @@ -838,7 +772,10 @@ describe('create presentation', () => { newKeyForMigratedClaimerDid = makeSigningKeyTool() migratedClaimerFullDid = createMinimalFullDidFromLightDid( migratedClaimerLightDid, - { ...newKeyForMigratedClaimerDid.keypair } + { + ...newKeyForMigratedClaimerDid.authentication[0], + id: '#new-auth', + } ) migratedThenDeletedKey = makeSigningKeyTool('ed25519') migratedThenDeletedClaimerLightDid = Did.createLightDidDocument({ @@ -853,7 +790,7 @@ describe('create presentation', () => { name: 'Peter', age: 12, }, - migratedClaimerFullDid.id + migratedClaimerFullDid.uri ) ) @@ -862,7 +799,7 @@ describe('create presentation', () => { .mockResolvedValue( ApiMocks.mockChainQueryReturn('attestation', 'attestations', { revoked: false, - attester: Did.toChain(attester.id), + attester: Did.toChain(attester.uri), ctypeHash: CType.idToHash(ctype.$id), } as any) as any ) @@ -880,9 +817,9 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - dereferenceDidUrl, + didResolveKey, }) - ).resolves.toMatchObject({ revoked: false, attester: attester.id }) + ).resolves.toMatchObject({ revoked: false, attester: attester.uri }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) it('should create presentation and exclude specific attributes using a light DID', async () => { @@ -894,7 +831,7 @@ describe('create presentation', () => { name: 'Peter', age: 12, }, - unmigratedClaimerLightDid.id + unmigratedClaimerLightDid.uri ) ) @@ -909,9 +846,9 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - dereferenceDidUrl, + didResolveKey, }) - ).resolves.toMatchObject({ revoked: false, attester: attester.id }) + ).resolves.toMatchObject({ revoked: false, attester: attester.uri }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) it('should create presentation and exclude specific attributes using a migrated DID', async () => { @@ -924,7 +861,7 @@ describe('create presentation', () => { age: 12, }, // Use of light DID in the claim. - migratedClaimerLightDid.id + migratedClaimerLightDid.uri ) ) @@ -940,9 +877,9 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - dereferenceDidUrl, + didResolveKey, }) - ).resolves.toMatchObject({ revoked: false, attester: attester.id }) + ).resolves.toMatchObject({ revoked: false, attester: attester.uri }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) @@ -956,7 +893,7 @@ describe('create presentation', () => { age: 12, }, // Use of light DID in the claim. - migratedClaimerLightDid.id + migratedClaimerLightDid.uri ) ) @@ -972,7 +909,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(att, { - dereferenceDidUrl, + didResolveKey, }) ).rejects.toThrow() }) @@ -987,7 +924,7 @@ describe('create presentation', () => { age: 12, }, // Use of light DID in the claim. - migratedThenDeletedClaimerLightDid.id + migratedThenDeletedClaimerLightDid.uri ) ) @@ -1003,7 +940,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - dereferenceDidUrl, + didResolveKey, }) ).rejects.toThrow() }) diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index 54ed56cead..e6f0c0dd54 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -21,13 +21,14 @@ import { ConfigService } from '@kiltprotocol/config' import { Attestation, CType } from '@kiltprotocol/core' import { isDidSignature, - dereference, + resolveKey, signatureFromJson, signatureToJson, verifyDidSignature, } from '@kiltprotocol/did' import type { - Did, + DidResolveKey, + DidUri, Hash, IAttestation, ICType, @@ -36,7 +37,6 @@ 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.dereferenceDidUrl - The function used to dereference the claimer's DID Document and verification method. Defaults to [[dereferenceDidUrl]]. + * @param verificationOpts.didResolveKey - The function used to resolve the claimer's key. Defaults to [[resolveKey]]. * @param verificationOpts.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. */ export async function verifySignature( input: ICredentialPresentation, { challenge, - dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + didResolveKey = resolveKey, }: { challenge?: string - dereferenceDidUrl?: DereferenceDidUrl['dereference'] + didResolveKey?: DidResolveKey } = {} ): Promise { const { claimerSignature } = input @@ -232,9 +232,8 @@ export async function verifySignature( expectedSigner: input.claim.owner, // allow full did to sign presentation if owned by corresponding light did allowUpgraded: true, - expectedVerificationRelationship: 'authentication', - signerUrl: claimerSignature.keyUri, - dereferenceDidUrl, + expectedVerificationMethod: 'authentication', + didResolveKey, }) } @@ -280,7 +279,7 @@ export function fromClaim( type VerifyOptions = { ctype?: ICType challenge?: string - dereferenceDidUrl?: DereferenceDidUrl['dereference'] + didResolveKey?: DidResolveKey } /** @@ -346,7 +345,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: Did + attester: DidUri revoked: boolean }> { const api = ConfigService.get('api') @@ -366,7 +365,7 @@ export async function verifyAttested(credential: ICredential): Promise<{ export interface VerifiedCredential extends ICredential { revoked: boolean - attester: Did + attester: DidUri } /** @@ -433,21 +432,17 @@ 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 dereference the claimer's DID and verification method. Defaults to [[dereference]]. + * @param options.didResolveKey - The function used to resolve the claimer's key. Defaults to [[resolveKey]]. * @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, - dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], - }: VerifyOptions = {} + { ctype, challenge, didResolveKey = resolveKey }: VerifyOptions = {} ): Promise { await verifySignature(presentation, { challenge, - dereferenceDidUrl, + didResolveKey, }) return verifyCredential(presentation, { ctype }) } @@ -544,7 +539,7 @@ export async function createPresentation({ const signature = await signCallback({ data: makeSigningData(presentation, challenge), did: credential.claim.owner, - verificationRelationship: 'authentication', + keyRelationship: 'authentication', }) return { diff --git a/packages/types/src/AssetDid.ts b/packages/types/src/AssetDid.ts index 5f5c35439c..ecb9a52ef1 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 AssetDid = `did:asset:${Caip2ChainId}.${Caip19AssetId}` +export type AssetDidUri = `did:asset:${Caip2ChainId}.${Caip19AssetId}` diff --git a/packages/types/src/Attestation.ts b/packages/types/src/Attestation.ts index 2b4d582702..0f42f89749 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 { Did } from './Did' +import type { DidUri } from './DidDocument' 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: Did + owner: DidUri delegationId: IDelegationNode['id'] | null revoked: boolean } diff --git a/packages/types/src/Claim.ts b/packages/types/src/Claim.ts index 6a7b341ffe..5e8691644b 100644 --- a/packages/types/src/Claim.ts +++ b/packages/types/src/Claim.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { Did } from './Did' +import type { DidUri } from './DidDocument' type ClaimPrimitives = string | number | boolean @@ -20,7 +20,7 @@ export interface IClaimContents { export interface IClaim { cTypeHash: CTypeHash contents: IClaimContents - owner: Did + owner: DidUri } /** diff --git a/packages/types/src/Credential.ts b/packages/types/src/Credential.ts index cd63793efb..ec2c0d467b 100644 --- a/packages/types/src/Credential.ts +++ b/packages/types/src/Credential.ts @@ -5,9 +5,9 @@ * found in the LICENSE file in the root directory of this source tree. */ +import type { DidSignature } from './DidDocument' 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 a48916696c..3bb8225b11 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -6,10 +6,11 @@ */ import type { - Did, - SignatureVerificationRelationship, - VerificationMethod, -} from './Did' + DidResourceUri, + DidUri, + DidVerificationKey, + VerificationKeyRelationship, +} from './DidDocument.js' /** * Base interface for all signing requests. @@ -21,14 +22,14 @@ export interface SignRequestData { data: Uint8Array /** - * The DID verification relationship to be used. + * The did key relationship to be used. */ - verificationRelationship: SignatureVerificationRelationship + keyRelationship: VerificationKeyRelationship /** * The DID to be used for signing. */ - did: Did + did: DidUri } /** @@ -40,9 +41,13 @@ export interface SignResponseData { */ signature: Uint8Array /** - * The DID verification method used for signing. + * The did key uri used for signing. */ - verificationMethod: VerificationMethod + keyUri: DidResourceUri + /** + * The did key type used for signing. + */ + keyType: DidVerificationKey['type'] } /** @@ -57,7 +62,7 @@ export type SignCallback = ( */ export type SignExtrinsicCallback = ( signData: SignRequestData -) => Promise +) => Promise> /** * Base interface for encryption requests. @@ -74,7 +79,7 @@ export interface EncryptRequestData { /** * The DID to be used for encryption. */ - did: Did + did: DidUri } /** @@ -90,9 +95,9 @@ export interface EncryptResponseData { */ nonce: Uint8Array /** - * The DID verification method used for the encryption. + * The did key uri used for the encryption. */ - verificationMethod: VerificationMethod + keyUri: DidResourceUri } /** @@ -119,9 +124,9 @@ export interface DecryptRequestData { */ nonce: Uint8Array /** - * The DID verification method, which should be used for decryption. + * The did key uri, which should be used for decryption. */ - verificationMethod: VerificationMethod + keyUri: DidResourceUri } export interface DecryptResponseData { diff --git a/packages/types/src/Delegation.ts b/packages/types/src/Delegation.ts index 2139eb6115..7049e33618 100644 --- a/packages/types/src/Delegation.ts +++ b/packages/types/src/Delegation.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { Did } from './Did' +import type { DidUri } from './DidDocument' /* eslint-disable no-bitwise */ export const Permission = { @@ -20,7 +20,7 @@ export interface IDelegationNode { hierarchyId: IDelegationNode['id'] parentId?: IDelegationNode['id'] childrenIds: Array - account: Did + account: DidUri permissions: PermissionType[] revoked: boolean } diff --git a/packages/types/src/Did.ts b/packages/types/src/Did.ts deleted file mode 100644 index fc0d458a0a..0000000000 --- a/packages/types/src/Did.ts +++ /dev/null @@ -1,103 +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 DidVersion = '' | `v${string}:` -type LightDidDocumentEncodedData = '' | `:${string}` - -/** - * A string containing a KILT DID. - */ -export type Did = - | `did:kilt:${DidVersion}${KiltAddress}` - | `did:kilt:light:${DidVersion}${AuthenticationKeyType}${KiltAddress}${LightDidDocumentEncodedData}` - -/** - * The fragment part of the DID including the `#` character. - */ -export type UriFragment = `#${string}` - -/** - * URL for DID resources like keys or services. - */ -export type DidUrl = - | `${Did}${UriFragment}` - // Very broad type definition, mostly for the compiler. Actual regex matching for query params is done where needed. - | `${Did}?{string}${UriFragment}` - -export type SignatureVerificationRelationship = - | 'authentication' - | 'capabilityDelegation' - | 'assertionMethod' -export type EncryptionRelationship = 'keyAgreement' - -export type VerificationRelationship = - | SignatureVerificationRelationship - | EncryptionRelationship - -export type DidSignature = { - // Name `keyUri` kept for retro-compatibility - keyUri: DidUrl - signature: string -} - -type Base58BtcMultibaseString = `z${string}` - -/** - * The verification method of a DID. - */ -export type VerificationMethod = { - /** - * The relative identifier (i.e., `#`) of the verification method. - */ - id: UriFragment - /** - * The type of the verification method. This is fixed for KILT DIDs. - */ - type: 'Multikey' - /** - * The controller of the verification method. - */ - controller: Did - /* - * The multicodec-prefixed, multibase-encoded verification method's public key. - */ - publicKeyMultibase: Base58BtcMultibaseString -} - -/* - * The service of a KILT DID. - */ -export type Service = { - /* - * The relative identifier (i.e., `#`) of the verification method. - */ - id: UriFragment - /* - * The set of service types. - */ - type: string[] - /* - * A list of URIs the endpoint exposes its services at. - */ - serviceEndpoint: string[] -} - -export type DidDocument = { - id: Did - alsoKnownAs?: string[] - verificationMethod?: VerificationMethod[] - authentication?: UriFragment[] - assertionMethod?: UriFragment[] - keyAgreement?: UriFragment[] - capabilityDelegation?: UriFragment[] - service?: Service[] -} - -export type JsonLd = T & { '@context': string[] } diff --git a/packages/types/src/DidDocument.ts b/packages/types/src/DidDocument.ts new file mode 100644 index 0000000000..9bf9a75025 --- /dev/null +++ b/packages/types/src/DidDocument.ts @@ -0,0 +1,176 @@ +/** + * 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 { 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. + */ +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 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' +> + +/** + * 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] + +/** + * 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 base details of a DID key. + */ +export type BaseDidKey = { + /** + * Relative key URI: `#` sign followed by fragment part of URI. + */ + id: 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 +} + +/** + * 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 + +/** + * The SDK-specific details of a new DID service endpoint. + */ +export type DidServiceEndpoint = { + /** + * Relative endpoint URI: `#` sign followed by fragment part of URI. + */ + id: UriFragment + /** + * A list of service types the endpoint exposes. + */ + type: string[] + /** + * A list of URIs the endpoint exposes its services at. + */ + 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 interface DidDocument { + uri: DidUri + + authentication: [DidVerificationKey] + assertionMethod?: [DidVerificationKey] + capabilityDelegation?: [DidVerificationKey] + keyAgreement?: DidEncryptionKey[] + + service?: DidServiceEndpoint[] +} diff --git a/packages/types/src/DidDocumentExporter.ts b/packages/types/src/DidDocumentExporter.ts new file mode 100644 index 0000000000..9b675e23e7 --- /dev/null +++ b/packages/types/src/DidDocumentExporter.ts @@ -0,0 +1,108 @@ +/** + * 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/DidResolver.ts b/packages/types/src/DidResolver.ts index 9fc56d9b64..fd74dc2373 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -5,250 +5,74 @@ * found in the LICENSE file in the root directory of this source tree. */ +import { + ConformingDidKey, + ConformingDidServiceEndpoint, +} from './DidDocumentExporter.js' import type { - Did, DidDocument, - DidUrl, - VerificationMethod, - Service, - JsonLd, -} from './Did' + DidKey, + DidResourceUri, + DidUri, + KeyRelationship, +} from './DidDocument.js' /** - * 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. + * 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). */ -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?: Did -} - -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 +export type DidResolutionDocumentMetadata = { /** - * 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. + * If present, it indicates that the resolved by DID should be treated as if it were the DID as specified in this property. */ - didDocument?: DidDocument + canonicalId?: DidUri /** - * 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. + * A boolean flag indicating whether the resolved DID has been deactivated. */ - didDocumentMetadata: ResolutionDocumentMetadata + deactivated: boolean } -export type RepresentationResolutionOptions = { +/** + * The result of a DID resolution. + * + * It includes the DID Document, and optional document resolution metadata. + */ +export type DidResolutionResult = { /** - * 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 resolved DID document. It is undefined if the DID has been upgraded or deleted. */ - accept?: Accept -} - -export type SuccessfulRepresentationResolutionMetadata< - ContentType extends string = string -> = { + document?: DidDocument /** - * 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. + * The DID resolution metadata. */ - contentType: ContentType -} -export type FailedRepresentationResolutionMetadata = { + metadata: DidResolutionDocumentMetadata /** - * 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. + * The DID's web3Name, if any. */ - error: 'invalidDid' | 'notFound' | 'representationNotSupported' + web3Name?: string } -// Either success with `contentType` or failure with `error` -export type RepresentationResolutionMetadata< - ContentType extends string = string -> = - | SuccessfulRepresentationResolutionMetadata - | FailedRepresentationResolutionMetadata +export type ResolvedDidKey = Pick & + Pick -export type RepresentationResolutionDocumentMetadata = - ResolutionDocumentMetadata - -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. - * If the resolution is unsuccessful, this value MUST be an empty stream. - */ - didDocumentStream?: Uint8Array - didResolutionMetadata: RepresentationResolutionMetadata -} +export type ResolvedDidServiceEndpoint = ConformingDidServiceEndpoint /** - * The resolve function returns the DID document in its abstract form (a map). + * 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 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: Did, - /** - * 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: Did, - /** - * 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 DidResolve = (did: DidUri) => 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. - * * invalidVerificationRelationship: https://github.com/decentralized-identity/did-spec-extensions/pull/21. - */ - error: 'invalidDidUrl' | 'notFound' | 'invalidVerificationRelationship' -} - -// Either success with `contentType` or failure with `error` -export type DereferenceMetadata = - | SuccessfulDereferenceMetadata - | FailedDereferenceMetadata - -export type DereferenceContentStream = - | DidDocument - | JsonLd - | VerificationMethod - | JsonLd - | Service - | JsonLd - | Uint8Array - -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: 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. - * This input is REQUIRED, but the structure MAY be empty. - */ - dereferenceOptions: DereferenceOptions - ) => Promise> -} - -export interface DidResolver - extends ResolveDid, - DereferenceDidUrl {} +/** + * 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. + */ +export type DidResolveKey = ( + didUri: DidResourceUri, + expectedVerificationMethod?: KeyRelationship +) => Promise diff --git a/packages/types/src/PublicCredential.ts b/packages/types/src/PublicCredential.ts index 25f8d67144..0b3ba75dbd 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 { Did } from './Did' -import type { AssetDid } from './AssetDid' +import type { DidUri } from './DidDocument' +import type { AssetDidUri } from './AssetDid' /* - * The minimal information required to issue a public credential to a given [[AssetDid]]. + * The minimal information required to issue a public credential to a given [[AssetDidUri]]. */ export interface IPublicCredentialInput { /* @@ -27,7 +27,7 @@ export interface IPublicCredentialInput { /* * The subject of the credential. */ - subject: AssetDid + subject: AssetDidUri /* * 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 [[Did]] and then Blake2b hashing the result. + * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[DidUri]] and then Blake2b hashing the result. */ id: HexString /* - * The KILT DID of the credential attester. + * The KILT DID uri of the credential attester. */ - attester: Did + attester: DidUri /* * 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 [[AssetDid]] `subject` instead of an [[IClaim]] `owner`. + * Like an [[IClaim]], but with a [[AssetDidUri]] `subject` instead of an [[IClaim]] `owner`. */ export interface IAssetClaim { cTypeHash: CTypeHash contents: IClaimContents - subject: AssetDid + subject: AssetDidUri } /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 83fde92b10..be510ee7af 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -21,8 +21,9 @@ export * from './Deposit.js' export * from './Delegation.js' export * from './Address.js' export * from './Credential.js' -export * from './Did.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' diff --git a/packages/utils/src/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index cf9bec1697..c9673eba54 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 id "${expected}"` + `The DID "${actual}" doesn't match the DID Document's URI "${expected}"` ) } } diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index ef2db0cbd2..b66cf0f90d 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -8,23 +8,21 @@ // @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, parse, - resolve as resolveDid, + resolveCompliant, W3C_DID_CONTEXT_URL, - multibaseKeyToDidKey, } from '@kiltprotocol/did' import type { - DidDocument, - Did, + ConformingDidDocument, + ConformingDidKey, + DidUri, ICType, - VerificationMethod, } from '@kiltprotocol/types' -import { CType } from '@kiltprotocol/core' +import { CType } from '@kiltprotocol/core' import { validationContexts } from './context/index.js' import { Sr25519VerificationKey2020 } from './suites/Sr25519VerificationKey.js' @@ -80,62 +78,17 @@ 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 Did) - const { didDocument: resolvedDidDocument } = await resolveDid(did) - const didDocument = (() => { - if (resolvedDidDocument === undefined) { - return {} - } - const doc: DidDocument = { ...resolvedDidDocument } - 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 url). This way we return either a key or the full DID document, depending on what was requested. - const jsonLdDocument = (await jsonld.frame( - didDocument, + const { did } = parse(url as DidUri) + const { didDocument, didResolutionMetadata } = await resolveCompliant(did) + if (didResolutionMetadata.error) { + throw new Error( + `${didResolutionMetadata.error}:${didResolutionMetadata.errorMessage}` + ) + } + // 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 ?? {}, { // add did contexts to make sure we get a compacted representation '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], @@ -151,30 +104,30 @@ export const kiltDidLoader: DocumentLoader = async (url) => { }, // forced because 'base' is not defined in the types we're using; these are for v1.5 bc no more recent types exist } as jsonld.Options.Frame - )) as DidDocument | VerificationMethod + )) as ConformingDidDocument | ConformingDidKey // The signature suites expect key-related json-LD contexts; we add them here - switch ((jsonLdDocument as { type: string }).type) { + switch ((document as { type: string }).type) { // these 4 are currently used case Sr25519VerificationKey2020.suite: - jsonLdDocument['@context'].push(Sr25519VerificationKey2020.SUITE_CONTEXT) + document['@context'].push(Sr25519VerificationKey2020.SUITE_CONTEXT) break case 'Ed25519VerificationKey2018': - jsonLdDocument['@context'].push( + document['@context'].push( 'https://w3id.org/security/suites/ed25519-2018/v1' ) break case 'EcdsaSecp256k1VerificationKey2019': - jsonLdDocument['@context'].push('https://w3id.org/security/v1') + document['@context'].push('https://w3id.org/security/v1') break case 'X25519KeyAgreementKey2019': - jsonLdDocument['@context'].push( + document['@context'].push( 'https://w3id.org/security/suites/x25519-2019/v1' ) break default: break } - return { contextUrl: undefined, documentUrl: url, document: jsonLdDocument } + return { contextUrl: undefined, documentUrl: url, document } } 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 704732e70b..783e0ff4e2 100644 --- a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts +++ b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts @@ -22,7 +22,7 @@ import jsonld from 'jsonld' // cjs module import { ConfigService } from '@kiltprotocol/config' import * as Did from '@kiltprotocol/did' import type { - DidDocument, + ConformingDidDocument, HexString, ICType, KiltAddress, @@ -59,7 +59,7 @@ import { makeFakeDid } from './Sr25519Signature2020.spec' jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - resolve: jest.fn(), + resolveCompliant: jest.fn(), authorizeTx: jest.fn(), })) @@ -154,7 +154,7 @@ let suite: KiltAttestationV1Suite let purpose: KiltAttestationProofV1Purpose let proof: Types.KiltAttestationProofV1 let keypair: KiltKeyringPair -let didDocument: DidDocument +let didDocument: ConformingDidDocument beforeAll(async () => { suite = new KiltAttestationV1Suite({ @@ -313,7 +313,7 @@ describe('vc-js', () => { it('creates and verifies a signed presentation (sr25519)', async () => { const signer = { sign: async ({ data }: { data: Uint8Array }) => keypair.sign(data), - id: didDocument.id + didDocument.authentication![0], + id: didDocument.authentication[0], } const signingSuite = new Sr25519Signature2020({ signer }) @@ -357,7 +357,7 @@ describe('vc-js', () => { }) const edSigner = { sign: async ({ data }: { data: Uint8Array }) => edKeypair.sign(data), - id: lightDid.id + lightDid.authentication?.[0], + id: lightDid.uri + lightDid.authentication[0].id, } const signingSuite = new Ed25519Signature2020({ signer: edSigner }) @@ -368,7 +368,7 @@ describe('vc-js', () => { let presentation = vcjs.createPresentation({ verifiableCredential: attestedVc, - holder: lightDid.id, + holder: lightDid.uri, }) presentation = await vcjs.signPresentation({ @@ -450,12 +450,7 @@ describe('issuance', () => { did: attestedVc.issuer, signer: async () => ({ signature: new Uint8Array(32), - verificationMethod: { - controller: attestedVc.issuer, - type: 'Multikey', - id: '#test', - publicKeyMultibase: 'zasd', - }, + keyType: 'sr25519' as const, }), } 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 fe7050687b..8916a54171 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts @@ -8,13 +8,12 @@ // @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' import type { - DidDocument, - Did as KiltDid, + ConformingDidDocument, + DidUri, KiltKeyringPair, } from '@kiltprotocol/types' @@ -32,7 +31,7 @@ jest.mock('@digitalbazaar/http-client', () => ({})) jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - resolve: jest.fn(), + resolveCompliant: jest.fn(), })) const documentLoader = combineDocumentLoaders([ @@ -44,37 +43,35 @@ const documentLoader = combineDocumentLoaders([ export async function makeFakeDid() { await init() const keypair = Crypto.makeKeypairFromUri('//Ingo', 'sr25519') - const didDocument: DidDocument = { - id: ingosCredential.credentialSubject.id as KiltDid, - authentication: ['#authentication'], - assertionMethod: ['#assertion'], - verificationMethod: [ - Did.didKeyToVerificationMethod( - ingosCredential.credentialSubject.id as KiltDid, - '#authentication', - { ...keypair, keyType: keypair.type } - ), - Did.didKeyToVerificationMethod( - ingosCredential.credentialSubject.id as KiltDid, - '#assertion', - { ...keypair, keyType: keypair.type } - ), - ], - } - - jest.mocked(Did.resolve).mockImplementation(async (did) => { + const didDocument = Did.exportToDidDocument( + { + uri: ingosCredential.credentialSubject.id as DidUri, + authentication: [ + { + ...keypair, + id: '#authentication', + }, + ], + assertionMethod: [{ ...keypair, id: '#assertion' }], + }, + 'application/json' + ) + jest.mocked(Did.resolveCompliant).mockImplementation(async (did) => { if (did.includes('light')) { return { + didDocument: Did.exportToDidDocument( + Did.parseDocumentFromLightDid(did, false), + 'application/json' + ), didDocumentMetadata: {}, didResolutionMetadata: {}, - didDocument: Did.parseDocumentFromLightDid(did, false), } } if (did.startsWith(didDocument.id)) { return { + didDocument, didDocumentMetadata: {}, didResolutionMetadata: {}, - didDocument, } } return { @@ -85,7 +82,7 @@ export async function makeFakeDid() { return { didDocument, keypair } } -let didDocument: DidDocument +let didDocument: ConformingDidDocument let keypair: KiltKeyringPair beforeAll(async () => { @@ -95,7 +92,7 @@ beforeAll(async () => { it('issues and verifies a signed credential', async () => { const signer = { sign: async ({ data }: { data: Uint8Array }) => keypair.sign(data), - id: didDocument.id + didDocument.assertionMethod![0], + id: didDocument.assertionMethod![0], } const attestationSigner = new Sr25519Signature2020({ signer }) @@ -120,36 +117,13 @@ it('issues and verifies a signed credential', async () => { expect(result).not.toHaveProperty('error') expect(result).toHaveProperty('verified', true) - 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, + ...didDocument.verificationMethod.find(({ id }) => + id.includes('assertion') + )!, }), }), documentLoader, @@ -161,7 +135,9 @@ it('issues and verifies a signed credential', async () => { credential: verifiableCredential, suite: new Sr25519Signature2020({ key: new Sr25519VerificationKey2020({ - ...authenticationMethod, + ...didDocument.verificationMethod.find(({ id }) => + id.includes('authentication') + )!, }), }), documentLoader, diff --git a/packages/vc-export/src/suites/types.ts b/packages/vc-export/src/suites/types.ts index 7e5362515e..d14ae644d4 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 { Did } from '@kiltprotocol/types' +import type { DidUri } 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: Did } + verificationMethod?: { id: string; type: string; controller: DidUri } } diff --git a/tests/breakingChanges/BreakingChanges.spec.ts b/tests/breakingChanges/BreakingChanges.spec.ts index 7c473b709b..0f97a60256 100644 --- a/tests/breakingChanges/BreakingChanges.spec.ts +++ b/tests/breakingChanges/BreakingChanges.spec.ts @@ -40,12 +40,12 @@ function makeLightDidFromSeed(seed: string) { describe('Breaking Changes', () => { describe('Light DID', () => { - it('does not break the light did generation', () => { + it('does not break the light did uri generation', () => { const { did } = makeLightDidFromSeed( '0x127f2375faf3472c2f94ffcdd5424590b27294631f2cb8041407e501bc97c44c' ) - expect(did.id).toMatchInlineSnapshot( + expect(did.uri).toMatchInlineSnapshot( `"did:kilt:light:004quk8nu1MLvzdoT4fE6SJsLS4fFpyvuGz7sQpMF7ZAWTDoF5:z1msTRicERqs59nwMvp3yzMRBhUYGmkum7ehY7rtKQc8HzfEx4b4eyRhrc37ZShT3oG7E89x89vaG9W4hRxPS23EAFnCSeVbVRrKGJmFQvYhjgKSMmrGC7gSxgHe1a3g41uamhD49AEi13YVMkgeHpyEQJBy7N7gGyW7jTWFcwzAnws4wSazBVG1qHmVJrhmusoJoTfKTPKXkExKyur8Z341EkcRkHteY8dV3VjLXHnfhRW2yU9oM2cRm5ozgaufxrXsQBx33ygTW2wvrfzzXsYw4Bs6Vf2tC3ipBTDcKyCk6G88LYnzBosRM15W3KmDRciJ2iPjqiQkhYm77EQyaw"` ) diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index 7312b3193f..bbb852e163 100644 --- a/tests/bundle/bundle-test.ts +++ b/tests/bundle/bundle-test.ts @@ -7,12 +7,12 @@ /// -import type { NewDidEncryptionKey } from '@kiltprotocol/did' import type { DidDocument, KeyringPair, KiltEncryptionKeypair, KiltKeyringPair, + NewDidEncryptionKey, SignCallback, } from '@kiltprotocol/types' @@ -37,18 +37,16 @@ function makeSignCallback( keypair: KeyringPair ): (didDocument: DidDocument) => SignCallback { return (didDocument) => { - return async function sign({ data, verificationRelationship }) { - const authKeyId = didDocument[verificationRelationship]?.[0] - const authKey = didDocument.verificationMethod?.find( - ({ id }) => id === authKeyId - ) - if (authKeyId === undefined || authKey === undefined) { + return async function sign({ data, keyRelationship }) { + const keyId = didDocument[keyRelationship]?.[0].id + const keyType = didDocument[keyRelationship]?.[0].type + if (keyId === undefined || keyType === undefined) { throw new Error( - `No verification method for purpose "${verificationRelationship}" found in DID "${didDocument.id}"` + `Key for purpose "${keyRelationship}" not found in did "${didDocument.uri}"` ) } const signature = keypair.sign(data, { withType: false }) - return { signature, verificationMethod: authKey } + return { signature, keyUri: `${didDocument.uri}${keyId}`, keyType } } } } @@ -60,9 +58,7 @@ function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { const signature = keypair.sign(data, { withType: false }) return { signature, - verificationMethod: { - publicKeyMultibase: Did.keypairToMultibaseKey(keypair), - }, + keyType: keypair.type, } } } @@ -119,11 +115,7 @@ async function createFullDidFromKeypair( const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(keypair), - }) - ) + Did.toChain(Did.getFullDidUriFromKey(keypair)) ) return Did.linkedInfoFromChain(encodedDidDetails).document } @@ -174,7 +166,7 @@ async function runAll() { keyAgreement: [{ publicKey: encPublicKey, type: 'x25519' }], }) if ( - testDid.id !== + testDid.uri !== `did:kilt:light:01${address}:z1Ac9CMtYCTRWjetJfJqJoV7FcPDD9nHPHDHry7t3KZmvYe1HQP1tgnBuoG3enuGaowpF8V88sCxytDPDy6ZxhW` ) { throw new Error('DID Test Unsuccessful') @@ -196,18 +188,15 @@ async function runAll() { const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(keypair), - }) - ) + Did.toChain(Did.getFullDidUriFromKey(keypair)) ) const fullDid = Did.linkedInfoFromChain(encodedDidDetails).document - const resolved = await Did.resolve(fullDid.id) + const resolved = await Did.resolve(fullDid.uri) if ( - !resolved.didDocumentMetadata.deactivated && - resolved.didDocument?.id === fullDid.id + resolved && + !resolved.metadata.deactivated && + resolved.document?.uri === fullDid.uri ) { console.info('DID matches') } else { @@ -215,15 +204,15 @@ async function runAll() { } const deleteTx = await Did.authorizeTx( - fullDid.id, + fullDid.uri, api.tx.did.delete(BalanceUtils.toFemtoKilt(0)), getSignCallback(fullDid), payer.address ) await Blockchain.signAndSubmitTx(deleteTx, payer) - const resolvedAgain = await Did.resolve(fullDid.id) - if (resolvedAgain.didDocumentMetadata.deactivated) { + const resolvedAgain = await Did.resolve(fullDid.uri) + if (!resolvedAgain || resolvedAgain.metadata.deactivated) { console.info('DID successfully deleted') } else { throw new Error('DID was not deleted') @@ -241,7 +230,7 @@ async function runAll() { }) const cTypeStoreTx = await Did.authorizeTx( - alice.id, + alice.uri, api.tx.ctype.add(CType.toChain(DriversLicense)), aliceSign(alice), payer.address @@ -258,8 +247,8 @@ async function runAll() { const credential = KiltCredentialV1.fromInput({ cType: DriversLicense.$id, claims: content, - subject: bob.id, - issuer: alice.id, + subject: bob.uri, + issuer: alice.uri, }) await KiltCredentialV1.validateSubject(credential, { @@ -270,13 +259,13 @@ async function runAll() { if ( credential.credentialSubject.name !== content.name || credential.credentialSubject.age !== content.age || - credential.credentialSubject.id !== bob.id + credential.credentialSubject.id !== bob.uri ) { throw new Error('Claim content inside Credential mismatching') } const issued = await KiltAttestationProofV1.issue(credential, { - didSigner: { did: alice.id, signer: aliceSign(alice) }, + didSigner: { did: alice.uri, signer: aliceSign(alice) }, transactionHandler: { account: payer.address, signAndSubmit: async (tx) => { @@ -302,7 +291,7 @@ async function runAll() { await KiltRevocationStatusV1.check(issued) console.info('Credential status verified') - const presentation = Presentation.create([issued], bob.id) + const presentation = Presentation.create([issued], bob.uri) console.info('Presentation created') Presentation.validateStructure(presentation) diff --git a/tests/integration/AccountLinking.spec.ts b/tests/integration/AccountLinking.spec.ts index 61badec164..51b9da0ae7 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.id, + did.uri, 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.id).toStrictEqual(did.id) + expect(queryByAccount.document.uri).toStrictEqual(did.uri) }, 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.id, + newDid.uri, 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.id).toStrictEqual(newDid.id) + expect(queryByAccount.document.uri).toStrictEqual(newDid.uri) }, 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.id, + did.uri, async (payload) => keypair.sign(payload, { withType: false }) ) const signedTx = await Did.authorizeTx( - did.id, + did.uri, 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.id).toStrictEqual(did.id) + expect(queryByAccount.document.uri).toStrictEqual(did.uri) }) 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.id, + newDid.uri, async (payload) => keypair.sign(payload, { withType: false }) ) const signedTx = await Did.authorizeTx( - newDid.id, + newDid.uri, 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.id).toStrictEqual(newDid.id) + expect(queryByAccount.document.uri).toStrictEqual(newDid.uri) }) 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.id, + newDid.uri, 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.id) + Did.toChain(newDid.uri) ) 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.id, + did.uri, async (payload) => genericAccount.sign(payload, { withType: true }) ) const signedTx = await Did.authorizeTx( - did.id, + did.uri, 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.id).toStrictEqual(did.id) + expect(queryByAccount.document.uri).toStrictEqual(did.uri) }) 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.id, + did.uri, web3NameClaimTx, didKey.getSignCallback(did), paymentAccount.address @@ -346,15 +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.id).toStrictEqual(did.id) + expect(queryByW3n.document.uri).toStrictEqual(did.uri) // 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.document.alsoKnownAs).toStrictEqual([ - 'w3n:test-name', - ]) + expect(queryByAccount.web3Name).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 ed23405582..67690ea491 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.id, + attester.uri, 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.id, + attester.uri, 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.id, + attester.uri, 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.id + claimer.uri ) 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.id + claimer.uri ) 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.id + attester.uri ) 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.id, + attester.uri, 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.id, revoked: false }) + ).resolves.toMatchObject({ attester: attester.uri, 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.id + claimer.uri ) 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.id + attester.uri ) 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.id, + attester.uri, storeTx, getSignCallback(attester), keypair.address @@ -254,11 +254,15 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const content = { name: 'Ralph', weight: 120 } - const claim = Claim.fromCTypeAndClaimContents(badCtype, content, claimer.id) + const claim = Claim.fromCTypeAndClaimContents( + badCtype, + content, + claimer.uri + ) const credential = Credential.fromClaim(claim) const attestation = Attestation.fromCredentialAndDid( credential, - attester.id + attester.uri ) const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -266,7 +270,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.id, + attester.uri, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -289,21 +293,21 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.id + claimer.uri ) credential = Credential.fromClaim(claim) const presentation = await Credential.createPresentation({ credential, signCallback: claimerKey.getSignCallback(claimer), }) - attestation = Attestation.fromCredentialAndDid(credential, attester.id) + attestation = Attestation.fromCredentialAndDid(credential, attester.uri) const storeTx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) const authorizedStoreTx = await Did.authorizeTx( - attester.id, + attester.uri, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -326,7 +330,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.id, + attester.uri, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -345,7 +349,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.id + claimer.uri ) const fakeCredential = Credential.fromClaim(claim) await Credential.createPresentation({ @@ -361,7 +365,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.id, + claimer.uri, revokeTx, claimerKey.getSignCallback(claimer), tokenHolder.address @@ -391,7 +395,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.id, + attester.uri, revokeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -407,13 +411,13 @@ describe('When there is an attester, claimer and ctype drivers license', () => { await expect( Credential.verifyCredential(credential) - ).resolves.toMatchObject({ attester: attester.id, revoked: true }) + ).resolves.toMatchObject({ attester: attester.uri, 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.id, + attester.uri, removeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -442,7 +446,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { CType.toChain(officialLicenseAuthorityCType) ) const authorizedStoreTx = await Did.authorizeTx( - attester.id, + attester.uri, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -460,7 +464,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { LicenseType: "Driver's License", LicenseSubtypes: 'sports cars, tanks', }, - attester.id + attester.uri ) const credential1 = Credential.fromClaim(licenseAuthorization) await Credential.createPresentation({ @@ -469,7 +473,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const licenseAuthorizationGranted = Attestation.fromCredentialAndDid( credential1, - anotherAttester.id + anotherAttester.uri ) const storeTx = api.tx.attestation.add( licenseAuthorizationGranted.claimHash, @@ -477,7 +481,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - anotherAttester.id, + anotherAttester.uri, storeTx, anotherAttesterKey.getSignCallback(anotherAttester), tokenHolder.address @@ -488,7 +492,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const iBelieveICanDrive = Claim.fromCTypeAndClaimContents( driversLicenseCType, { name: 'Dominic Toretto', age: 52 }, - claimer.id + claimer.uri ) const credential2 = Credential.fromClaim(iBelieveICanDrive, { legitimations: [credential1], @@ -499,7 +503,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const licenseGranted = Attestation.fromCredentialAndDid( credential2, - attester.id + attester.uri ) const storeTx2 = api.tx.attestation.add( licenseGranted.claimHash, @@ -507,7 +511,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx2 = await Did.authorizeTx( - attester.id, + attester.uri, storeTx2, attesterKey.getSignCallback(attester), tokenHolder.address diff --git a/tests/integration/Ctypes.spec.ts b/tests/integration/Ctypes.spec.ts index 52fdea9b2c..253a76ef82 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.id, + ctypeCreator.uri, 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.id, + ctypeCreator.uri, 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.id) + expect(creator).toBe(ctypeCreator.uri) 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.id, + ctypeCreator.uri, 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.id, + ctypeCreator.uri, 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.id) + expect(retrievedCType.creator).toBe(ctypeCreator.uri) } }, 45_000) diff --git a/tests/integration/Delegation.spec.ts b/tests/integration/Delegation.spec.ts index ecc1efd7d1..51485ba0de 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.id, + account: delegator.uri, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cTypeId), }) const storeTx = await rootNode.getStoreTx() const authorizedStoreTx = await Did.authorizeTx( - delegator.id, + delegator.uri, storeTx, sign, paymentAccount.address @@ -86,13 +86,13 @@ async function addDelegation( const delegationNode = DelegationNode.newNode({ hierarchyId, parentId, - account: delegate.id, + account: delegate.uri, permissions, }) const signature = await delegationNode.delegateSign(delegate, delegateSign) const storeTx = await delegationNode.getStoreTx(signature) const authorizedStoreTx = await Did.authorizeTx( - delegator.id, + delegator.uri, 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.id, + attester.uri, storeTx, attesterKey.getSignCallback(attester), paymentAccount.address @@ -180,7 +180,7 @@ describe('and attestation rights have been delegated', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.id + claimer.uri ) 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.id + attester.uri ) 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.id, + attester.uri, 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.id, + root.uri, revokeTx, rootKey.getSignCallback(root), paymentAccount.address @@ -273,9 +273,9 @@ describe('revocation', () => { ) // Test revocation - const revokeTx = await delegationA.getRevokeTx(delegator.id) + const revokeTx = await delegationA.getRevokeTx(delegator.uri) const authorizedRevokeTx = await Did.authorizeTx( - delegator.id, + delegator.uri, 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.id, + delegator.uri, 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.id, + firstDelegate.uri, revokeTx, firstDelegateSign, paymentAccount.address @@ -334,9 +334,9 @@ describe('revocation', () => { }) await expect(delegationRoot.verify()).resolves.not.toThrow() - const revokeTx2 = await delegationA.getRevokeTx(firstDelegate.id) + const revokeTx2 = await delegationA.getRevokeTx(firstDelegate.uri) const authorizedRevokeTx2 = await Did.authorizeTx( - firstDelegate.id, + firstDelegate.uri, revokeTx2, firstDelegateSign, paymentAccount.address @@ -368,9 +368,9 @@ describe('revocation', () => { secondDelegateSign ) delegationRoot = await delegationRoot.getLatestState() - const revokeTx = await delegationRoot.getRevokeTx(delegator.id) + const revokeTx = await delegationRoot.getRevokeTx(delegator.uri) const authorizedRevokeTx = await Did.authorizeTx( - delegator.id, + delegator.uri, 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.id, + account: attester.uri, }).getAttestationHashes() ).toEqual([]) }) diff --git a/tests/integration/Deposit.spec.ts b/tests/integration/Deposit.spec.ts index fbfa20d60a..236fbea25e 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.id) + Did.toChain(fullDid.uri) ) const deleteDid = api.tx.did.delete(storedEndpointsCount) - tx = await Did.authorizeTx(fullDid.id, deleteDid, sign, identity.address) + tx = await Did.authorizeTx(fullDid.uri, 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.id)) + await api.query.did.did(Did.toChain(fullDid.uri)) ) const didDeposit = didResult.deposit @@ -79,16 +79,16 @@ async function checkReclaimFullDid( fullDid: DidDocument ): Promise { storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.id) + Did.toChain(fullDid.uri) ) - tx = api.tx.did.reclaimDeposit(Did.toChain(fullDid.id), storedEndpointsCount) + tx = api.tx.did.reclaimDeposit(Did.toChain(fullDid.uri), storedEndpointsCount) const balanceBeforeRevoking = ( await api.query.system.account(identity.address) ).data const didResult = Did.documentFromChain( - await api.query.did.did(Did.toChain(fullDid.id)) + await api.query.did.did(Did.toChain(fullDid.uri)) ) const didDeposit = didResult.deposit @@ -109,14 +109,14 @@ async function checkRemoveFullDidAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.uri, 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.id) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) tx = api.tx.attestation.remove(attestation.claimHash, null) - authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.uri, 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.id) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) await submitTx(authorizedTx, identity) const balanceBeforeReclaiming = ( await api.query.system.account(identity.address) ).data - attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) tx = api.tx.attestation.reclaimDeposit(attestation.claimHash) @@ -194,25 +194,25 @@ async function checkDeletedDidReclaimAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) await submitTx(authorizedTx, identity) storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.id) + Did.toChain(fullDid.uri) ) - attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) const deleteDid = api.tx.did.delete(storedEndpointsCount) - tx = await Did.authorizeTx(fullDid.id, deleteDid, sign, identity.address) + tx = await Did.authorizeTx(fullDid.uri, 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.id, + fullDid.uri, claimTx, sign, identity.address @@ -253,7 +253,7 @@ async function checkWeb3Deposit( const releaseTx = api.tx.web3Names.releaseByOwner() didAuthorizedTx = await Did.authorizeTx( - fullDid.id, + fullDid.uri, releaseTx, sign, identity.address @@ -295,7 +295,7 @@ beforeAll(async () => { const ctypeExists = await isCtypeOnChain(driversLicenseCType) if (!ctypeExists) { const extrinsic = await Did.authorizeTx( - attester.id, + attester.uri, 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.id + claimerLightDid.uri ) credential = Credential.fromClaim(claim) diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index f4499c3188..c81635f47f 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -6,29 +6,29 @@ */ import type { ApiPromise } from '@polkadot/api' -import type { +import { BN } from '@polkadot/util' + +import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' +import * as Did from '@kiltprotocol/did' +import { DidDocument, + DidResolutionResult, + DidServiceEndpoint, KiltKeyringPair, - ResolutionResult, - Service, + NewDidEncryptionKey, + NewDidVerificationKey, + NewLightDidVerificationKey, + Permission, 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, - getStoreTxFromDidDocument, } from '../testUtils/index.js' import { createEndowedTestAccount, @@ -61,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 getStoreTxFromDidDocument( + const tx = await Did.getStoreTx( did, otherAccount.address, key.storeDidCallback @@ -73,15 +73,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] - ) as VerificationMethod - const { keyType, publicKey: authPublicKey } = - Did.multibaseKeyToDidKey(publicKeyMultibase) - const input: Did.CreateDocumentInput = { - authentication: [{ publicKey: authPublicKey, type: keyType }] as [ - Did.NewLightDidVerificationKey - ], + const newDid = Did.createLightDidDocument({ + authentication: did.authentication as [NewLightDidVerificationKey], service: [ { id: '#test-id-1', @@ -94,25 +87,29 @@ describe('write and didDeleteTx', () => { serviceEndpoint: ['x:test-url-2'], }, ], - } + }) const tx = await Did.getStoreTx( - input, + newDid, paymentAccount.address, key.storeDidCallback ) await submitTx(tx, paymentAccount) - const fullDid = Did.getFullDidFromVerificationMethod({ - publicKeyMultibase, - }) - const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDid)) - const { document: fullDidDocument } = - Did.linkedInfoFromChain(fullDidLinkedInfo) + const fullDidUri = Did.getFullDidUri(newDid.uri) + const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDidUri)) + const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) - expect(fullDidDocument).toMatchObject(>{ - id: fullDid, + 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', + }), + ], service: [ { id: '#test-id-1', @@ -125,26 +122,13 @@ describe('write and didDeleteTx', () => { type: ['test-type-2'], }, ], - verificationMethod: [ - expect.objectContaining(>{ - 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({ - type: 'sr25519', - publicKey: authPublicKey, - }), - }), - ], }) - 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.getFullDid(makeSigningKeyTool().keypair.address) + const emptyDid = Did.getFullDidUriFromKey( + makeSigningKeyTool().authentication[0] + ) const encodedDid = Did.toChain(emptyDid) expect((await api.call.did.query(encodedDid)).isSome).toBe(false) @@ -153,7 +137,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.getFullDid(did.id)) + Did.toChain(Did.getFullDidUri(did.uri)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() @@ -164,7 +148,7 @@ describe('write and didDeleteTx', () => { let call = api.tx.did.delete(new BN(10)) let submittable = await Did.authorizeTx( - fullDid.id, + fullDid.uri, call, signCallback, // Use a different account than the submitter one @@ -176,11 +160,11 @@ describe('write and didDeleteTx', () => { name: 'BadDidOrigin', }) - // We use 1 here and this should fail as there are two services stored. + // 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.id, + fullDid.uri, call, signCallback, paymentAccount.address @@ -198,12 +182,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.getFullDid(did.id)) + Did.toChain(Did.getFullDidUri(did.uri)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - const encodedDid = Did.toChain(fullDid.id) + const encodedDid = Did.toChain(fullDid.uri) const linkedInfo = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) @@ -211,7 +195,7 @@ describe('write and didDeleteTx', () => { const call = api.tx.did.delete(storedEndpointsCount) const submittable = await Did.authorizeTx( - fullDid.id, + fullDid.uri, call, signCallback, paymentAccount.address @@ -233,7 +217,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 getStoreTxFromDidDocument( + const tx = await Did.getStoreTx( newDid, paymentAccount.address, storeDidCallback @@ -243,7 +227,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.getFullDid(newDid.id)) + Did.toChain(Did.getFullDidUri(newDid.uri)) ) let { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -253,7 +237,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { Did.publicKeyToChain(newKey.authentication[0]) ) const tx2 = await Did.authorizeTx( - fullDid.id, + fullDid.uri, updateAuthenticationKeyCall, getSignCallback(fullDid), paymentAccount.address @@ -263,12 +247,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.getFullDid(newDid.id)) + Did.toChain(Did.getFullDidUri(newDid.uri)) ) fullDid = Did.linkedInfoFromChain(fullDidLinkedInfo).document - // Add a new service - const newEndpoint: Did.NewService = { + // Add a new service endpoint + const newEndpoint: DidServiceEndpoint = { id: '#new-endpoint', type: ['new-type'], serviceEndpoint: ['x:new-url'], @@ -278,27 +262,27 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { ) const tx3 = await Did.authorizeTx( - fullDid.id, + fullDid.uri, updateEndpointCall, newKey.getSignCallback(fullDid), paymentAccount.address ) await submitTx(tx3, paymentAccount) - const encodedDid = Did.toChain(fullDid.id) + const encodedDid = Did.toChain(fullDid.uri) const linkedInfo = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) - expect( - linkedInfo.document.service?.find((s) => s.id === newEndpoint.id) - ).toStrictEqual(newEndpoint) + expect(Did.getService(linkedInfo.document, newEndpoint.id)).toStrictEqual( + newEndpoint + ) - // Delete the added service + // Delete the added service endpoint const removeEndpointCall = api.tx.did.removeServiceEndpoint( - Did.fragmentIdToChain(newEndpoint.id) + Did.resourceIdToChain(newEndpoint.id) ) const tx4 = await Did.authorizeTx( - fullDid.id, + fullDid.uri, removeEndpointCall, newKey.getSignCallback(fullDid), paymentAccount.address @@ -309,9 +293,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const linkedInfo2 = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) - expect( - linkedInfo2.document.service?.find((s) => s.id === newEndpoint.id) - ).toBe(undefined) + expect(Did.getService(linkedInfo2.document, newEndpoint.id)).toBe(undefined) // Claim the deposit back const storedEndpointsCount = linkedInfo2.document.service?.length ?? 0 @@ -335,55 +317,47 @@ describe('DID migration', () => { keyAgreement, }) - const storeTx = await getStoreTxFromDidDocument( + const storeTx = await Did.getStoreTx( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDid = Did.getFullDid(lightDid.id) + const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDid) + Did.toChain(migratedFullDidUri) ) - const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( + const { document: migratedFullDid } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDidDocument).toMatchObject(>{ - id: migratedFullDid, - verificationMethod: [ - expect.objectContaining(>{ - 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).toMatchObject({ + uri: migratedFullDidUri, + authentication: [ + expect.objectContaining({ + publicKey: lightDid.authentication[0].publicKey, + type: 'ed25519', }), - expect.objectContaining(>{ - 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]), + ], + keyAgreement: [ + expect.objectContaining({ + publicKey: lightDid.keyAgreement?.[0].publicKey, + type: 'x25519', }), ], }) - 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(migratedFullDidDocument.id))).isSome + (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome ).toBe(true) - const { didDocumentMetadata } = (await Did.resolve( - lightDid.id - )) as ResolutionResult + const { metadata } = (await Did.resolve( + lightDid.uri + )) as DidResolutionResult - expect(didDocumentMetadata.canonicalId).toStrictEqual( - migratedFullDidDocument.id - ) - expect(didDocumentMetadata.deactivated).toBe(undefined) + expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) + expect(metadata.deactivated).toBe(false) }) it('migrates light DID with sr25519 auth key', async () => { @@ -392,57 +366,49 @@ describe('DID migration', () => { authentication, }) - const storeTx = await getStoreTxFromDidDocument( + const storeTx = await Did.getStoreTx( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDid = Did.getFullDid(lightDid.id) + const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDid) + Did.toChain(migratedFullDidUri) ) - const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( + const { document: migratedFullDid } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDidDocument).toMatchObject(>{ - id: migratedFullDid, - verificationMethod: [ - expect.objectContaining(>{ - 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).toMatchObject({ + uri: migratedFullDidUri, + authentication: [ + expect.objectContaining({ + publicKey: lightDid.authentication[0].publicKey, + type: 'sr25519', }), ], }) - 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(migratedFullDidDocument.id))).isSome + (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome ).toBe(true) - const { didDocumentMetadata } = (await Did.resolve( - lightDid.id - )) as ResolutionResult + const { metadata } = (await Did.resolve( + lightDid.uri + )) as DidResolutionResult - expect(didDocumentMetadata.canonicalId).toStrictEqual( - migratedFullDidDocument.id - ) - expect(didDocumentMetadata.deactivated).toBe(undefined) + expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) + expect(metadata.deactivated).toBe(false) }) - it('migrates light DID with ed25519 auth key, encryption key, and services', async () => { + 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: Did.NewService[] = [ + const service: DidServiceEndpoint[] = [ { id: '#id-1', type: ['type-1'], @@ -455,35 +421,33 @@ describe('DID migration', () => { service, }) - const storeTx = await getStoreTxFromDidDocument( + const storeTx = await Did.getStoreTx( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDid = Did.getFullDid(lightDid.id) + const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDid) + Did.toChain(migratedFullDidUri) ) - const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( + const { document: migratedFullDid } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDidDocument).toMatchObject(>{ - id: migratedFullDid, - verificationMethod: [ - expect.objectContaining(>{ - 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).toMatchObject({ + uri: migratedFullDidUri, + authentication: [ + expect.objectContaining({ + publicKey: lightDid.authentication[0].publicKey, + type: 'ed25519', }), - expect.objectContaining(>{ - 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]), + ], + keyAgreement: [ + expect.objectContaining({ + publicKey: lightDid.keyAgreement?.[0].publicKey, + type: 'x25519', }), ], service: [ @@ -494,22 +458,16 @@ describe('DID migration', () => { }, ], }) - expect(migratedFullDidDocument.authentication).toHaveLength(1) - expect(migratedFullDidDocument.keyAgreement).toHaveLength(1) - expect(migratedFullDidDocument.assertionMethod).toBe(undefined) - expect(migratedFullDidDocument.capabilityDelegation).toBe(undefined) - const encodedDid = Did.toChain(migratedFullDidDocument.id) + const encodedDid = Did.toChain(migratedFullDid.uri) expect((await api.call.did.query(encodedDid)).isSome).toBe(true) - const { didDocumentMetadata } = (await Did.resolve( - lightDid.id - )) as ResolutionResult + const { metadata } = (await Did.resolve( + lightDid.uri + )) as DidResolutionResult - expect(didDocumentMetadata.canonicalId).toStrictEqual( - migratedFullDidDocument.id - ) - expect(didDocumentMetadata.deactivated).toBe(undefined) + expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) + expect(metadata.deactivated).toBe(false) // Remove and claim the deposit back const linkedInfo = Did.linkedInfoFromChain( @@ -545,11 +503,7 @@ describe('DID authorization', () => { ) await submitTx(createTx, paymentAccount) const didLinkedInfo = await api.call.did.query( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) - ) + Did.toChain(Did.getFullDidUriFromKey(authentication[0])) ) did = Did.linkedInfoFromChain(didLinkedInfo).document }, 60_000) @@ -558,7 +512,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.id, + did.uri, call, getSignCallback(did), paymentAccount.address @@ -570,12 +524,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.id)) + 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.id, + did.uri, deleteCall, getSignCallback(did), paymentAccount.address @@ -585,7 +539,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.id, + did.uri, call, getSignCallback(did), paymentAccount.address @@ -602,7 +556,7 @@ describe('DID authorization', () => { describe('DID management batching', () => { describe('FullDidCreationBuilder', () => { it('Build a complete full DID', async () => { - const { storeDidCallback, authentication } = makeSigningKeyTool() + const { keypair, storeDidCallback, authentication } = makeSigningKeyTool() const extrinsic = await Did.getStoreTx( { authentication, @@ -655,71 +609,44 @@ describe('DID management batching', () => { ) await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) - ) + Did.toChain(Did.getFullDidUriFromKey(authentication[0])) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - expect(fullDid.verificationMethod).toEqual>( - expect.arrayContaining([ + expect(fullDid).toMatchObject({ + authentication: [ expect.objectContaining({ - // Authentication - controller: fullDid.id, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + publicKey: keypair.publicKey, + type: 'sr25519', }), - // Assertion method + ], + assertionMethod: [ expect.objectContaining({ - controller: fullDid.id, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - type: 'sr25519', - publicKey: new Uint8Array(32).fill(1), - }), + publicKey: new Uint8Array(32).fill(1), + type: 'sr25519', }), - // Capability delegation + ], + capabilityDelegation: [ expect.objectContaining({ - controller: fullDid.id, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - type: 'ecdsa', - publicKey: new Uint8Array(33).fill(1), - }), + publicKey: new Uint8Array(33).fill(1), + type: 'ecdsa', }), - // Key agreement 1 + ], + keyAgreement: [ expect.objectContaining({ - controller: fullDid.id, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - type: 'x25519', - publicKey: new Uint8Array(32).fill(1), - }), + publicKey: new Uint8Array(32).fill(3), + type: 'x25519', }), - // Key agreement 2 expect.objectContaining({ - controller: fullDid.id, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - type: 'x25519', - publicKey: new Uint8Array(32).fill(2), - }), + publicKey: new Uint8Array(32).fill(2), + type: 'x25519', }), - // Key agreement 3 expect.objectContaining({ - controller: fullDid.id, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - type: 'x25519', - publicKey: new Uint8Array(32).fill(3), - }), + publicKey: new Uint8Array(32).fill(1), + type: 'x25519', }), - ]) - ) - expect(fullDid).toMatchObject(>{ + ], service: [ { id: '#id-3', @@ -738,15 +665,11 @@ 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: Did.NewDidVerificationKey = { + const didAuthKey: NewDidVerificationKey = { publicKey: keypair.publicKey, type: 'ecdsa', } @@ -759,29 +682,17 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), - }) - ) + Did.toChain(Did.getFullDidUriFromKey(didAuthKey)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - 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) + expect(fullDid?.authentication).toMatchObject([ + { + publicKey: keypair.publicKey, + type: 'ecdsa', + }, + ]) }) }) @@ -834,11 +745,7 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const initialFullDidLinkedInfo = await api.call.did.query( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) - ) + Did.toChain(Did.getFullDidUriFromKey(authentication[0])) ) const { document: initialFullDid } = Did.linkedInfoFromChain( initialFullDidLinkedInfo @@ -849,21 +756,13 @@ describe('DID management batching', () => { const extrinsic = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: initialFullDid.id, + did: initialFullDid.uri, extrinsics: [ api.tx.did.removeKeyAgreementKey( - Did.fragmentIdToChain( - initialFullDid.verificationMethod!.find( - (vm) => vm.id === encryptionKeys[0] - )!.id - ) + Did.resourceIdToChain(encryptionKeys[0].id) ), api.tx.did.removeKeyAgreementKey( - Did.fragmentIdToChain( - initialFullDid.verificationMethod!.find( - (vm) => vm.id === encryptionKeys[1] - )!.id - ) + Did.resourceIdToChain(encryptionKeys[1].id) ), api.tx.did.removeAttestationKey(), api.tx.did.removeDelegationKey(), @@ -876,7 +775,7 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const finalFullDidLinkedInfo = await api.call.did.query( - Did.toChain(initialFullDid.id) + Did.toChain(initialFullDid.uri) ) const { document: finalFullDid } = Did.linkedInfoFromChain( finalFullDidLinkedInfo @@ -884,23 +783,17 @@ describe('DID management batching', () => { expect(finalFullDid).not.toBeNull() - expect(finalFullDid).toMatchObject(>{ - verificationMethod: [ - // Authentication - expect.objectContaining(>{ - controller: finalFullDid.id, - type: 'Multikey', - publicKeyMultibase: Did.keypairToMultibaseKey({ - publicKey: keypair.publicKey, - type: 'sr25519', - }), - }), - ], + expect( + finalFullDid.authentication[0] + ).toMatchObject({ + 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) + + 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 () => { @@ -918,11 +811,7 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const initialFullDidLinkedInfo = await api.call.did.query( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) - ) + Did.toChain(Did.getFullDidUriFromKey(authentication[0])) ) const { document: initialFullDid } = Did.linkedInfoFromChain( initialFullDidLinkedInfo @@ -930,7 +819,7 @@ describe('DID management batching', () => { const extrinsic = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: initialFullDid.id, + did: initialFullDid.uri, extrinsics: [ api.tx.did.addServiceEndpoint( Did.serviceToChain({ @@ -955,43 +844,23 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const finalFullDidLinkedInfo = await api.call.did.query( - Did.toChain(initialFullDid.id) + Did.toChain(initialFullDid.uri) ) const { document: finalFullDid } = Did.linkedInfoFromChain( finalFullDidLinkedInfo ) expect(finalFullDid).not.toBeNull() - 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[0]).toMatchObject({ + publicKey: newAuthKey.publicKey, + type: newAuthKey.type, }) - 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 () => { @@ -1011,23 +880,19 @@ describe('DID management batching', () => { paymentAccount.address, storeDidCallback ) - // Create the full DIgetStoreTx + // Create the full DID with a service endpoint await submitTx(tx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) - ) + 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 + // 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, + did: fullDid.uri, extrinsics: [ api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), api.tx.did.addServiceEndpoint( @@ -1045,38 +910,22 @@ describe('DID management batching', () => { await submitTx(updateTx, paymentAccount) const updatedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(fullDid.id) + Did.toChain(fullDid.uri) ) const { document: updatedFullDid } = Did.linkedInfoFromChain( updatedFullDidLinkedInfo ) - 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() + 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 () => { @@ -1098,20 +947,16 @@ describe('DID management batching', () => { ) await submitTx(createTx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain( - Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), - }) - ) + 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 + // 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, + did: fullDid.uri, extrinsics: [ api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), api.tx.did.addServiceEndpoint( @@ -1133,17 +978,17 @@ describe('DID management batching', () => { }) const updatedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(fullDid.id) + 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 will match the one manually added, and not the one set in the builder. + // 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({ + Did.getService(updatedFullDid, '#id-1') + ).toStrictEqual({ id: '#id-1', type: ['type-1'], serviceEndpoint: ['x:url-1'], @@ -1165,15 +1010,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.id, + account: fullDid.uri, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cType.$id), }) const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, - did: fullDid.id, + did: fullDid.uri, extrinsics: [ ctypeStoreTx, // Will fail since the delegation cannot be revoked before it is added @@ -1195,15 +1040,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.id, + account: fullDid.uri, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cType.$id), }) const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.id, + did: fullDid.uri, extrinsics: [ ctypeStoreTx, // Will fail since the delegation cannot be revoked before it is added @@ -1227,7 +1072,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.id, + fullDid.uri, web3NameClaimTx, key.getSignCallback(fullDid), paymentAccount.address @@ -1238,7 +1083,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.id, + did: fullDid.uri, extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], sign: key.getSignCallback(fullDid), submitter: paymentAccount.address, @@ -1250,8 +1095,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.id).toStrictEqual( - fullDid.id + expect(Did.linkedInfoFromChain(encoded2).document.uri).toStrictEqual( + fullDid.uri ) }, 30_000) @@ -1263,7 +1108,7 @@ describe('DID extrinsics batching', () => { const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) // Delegation key const rootNode = DelegationNode.newRoot({ - account: fullDid.id, + account: fullDid.uri, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(ctype1.$id), }) @@ -1275,11 +1120,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.id) + const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.uri) const batchedExtrinsics = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.id, + did: fullDid.uri, extrinsics: [ web3NameReleaseExt, ctype1Creation, @@ -1299,9 +1144,9 @@ describe('DID extrinsics batching', () => { expect(encoded.isSome).toBe(false) const { - document: { id }, + document: { uri }, } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name('test-2')) - expect(id).toStrictEqual(fullDid.id) + expect(uri).toStrictEqual(fullDid.uri) // Test correct use of attestation keys await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() @@ -1314,7 +1159,7 @@ describe('DID extrinsics batching', () => { }) describe('Runtime constraints', () => { - let testAuthKey: Did.NewDidVerificationKey + let testAuthKey: NewDidVerificationKey const { keypair, storeDidCallback } = makeSigningKeyTool('ed25519') beforeAll(async () => { @@ -1327,7 +1172,7 @@ 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): Did.NewDidEncryptionKey => ({ + (_, index): NewDidEncryptionKey => ({ publicKey: Uint8Array.from(new Array(32).fill(index)), type: 'x25519', }) @@ -1360,10 +1205,10 @@ describe('Runtime constraints', () => { ) }, 30_000) - it('should not be possible to create a DID with too many services', async () => { - // MaxgetStoreTx + 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.NewService => ({ + (_, index): DidServiceEndpoint => ({ id: `#service-${index}`, type: [`type-${index}`], serviceEndpoint: [`x:url-${index}`], @@ -1394,35 +1239,35 @@ describe('Runtime constraints', () => { storeDidCallback ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot store more than 25 services per DID"` + `"Cannot store more than 25 service endpoints per DID"` ) }, 30_000) - it('should not be possible to create a DID with a service that is too long', async () => { - const serviceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + 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 that has too many types', async () => { + 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 that has too many URIs', async () => { + 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 that has a type that is too long', async () => { + 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 that has a URI that is too long', async () => { + 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() diff --git a/tests/integration/ErrorHandler.spec.ts b/tests/integration/ErrorHandler.spec.ts index b440c7821d..26f7a6e9ea 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.id, + owner: someDid.uri, 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.id, + someDid.uri, storeTx, key.getSignCallback(someDid), paymentAccount.address diff --git a/tests/integration/PublicCredentials.spec.ts b/tests/integration/PublicCredentials.spec.ts index 4de6374a65..b6c93c9491 100644 --- a/tests/integration/PublicCredentials.spec.ts +++ b/tests/integration/PublicCredentials.spec.ts @@ -6,7 +6,7 @@ */ import type { - AssetDid, + AssetDidUri, DidDocument, HexString, IPublicCredential, @@ -42,14 +42,14 @@ let attesterKey: KeyTool let api: ApiPromise // Generate a random asset ID -let assetId: AssetDid = `did:asset:eip155:1.erc20:${randomAsHex(20)}` +let assetId: AssetDidUri = `did:asset:eip155:1.erc20:${randomAsHex(20)}` let latestCredential: IPublicCredentialInput async function issueCredential( credential: IPublicCredentialInput ): Promise { const authorizedStoreTx = await Did.authorizeTx( - attester.id, + attester.uri, 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.id, + attester.uri, 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.id + attester.uri ) 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.id, + attester: attester.uri, 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.id, + did: attester.uri, 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.id + attester.uri ) 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.id, + attester.uri, 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.id + attester.uri ) 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.id, + attester.uri, 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.id + attester.uri ) 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.id, + attester.uri, 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.id + attester.uri ) credential = await PublicCredentials.fetchCredentialFromChain(credentialId) }) @@ -345,7 +345,7 @@ describe('When there is an issued public credential', () => { const credentialWithDifferentSubject = { ...credential, subject: - 'did:asset:eip155:1.erc721:0x6d19295A5E47199D823D8793942b21a256ef1A4d' as AssetDid, + 'did:asset:eip155:1.erc721:0x6d19295A5E47199D823D8793942b21a256ef1A4d' as AssetDidUri, } 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.getFullDid(devAlice.address), + attester: Did.getFullDidUri(devAlice.address), } await expect( PublicCredentials.verifyCredential(credentialWithDifferentAttester) @@ -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.id, + attester.uri, 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.id)) + await api.query.did.did(Did.toChain(attester.uri)) ).lastTxCounter const batchTx = api.tx.utility.batchAll([ await Did.authorizeTx( - attester.id, + attester.uri, 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.id, + attester.uri, 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.id, + attester.uri, api.tx.publicCredentials.add( PublicCredentials.toChain(credential3) ), diff --git a/tests/integration/Web3Names.spec.ts b/tests/integration/Web3Names.spec.ts index 2904bab2ca..1c00908e54 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: string - let differentNick: string + let nick: Did.Web3Name + let differentNick: Did.Web3Name 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.id, + w3nCreator.uri, 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.id, + w3nCreator.uri, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address @@ -91,23 +91,23 @@ describe('When there is an Web3NameCreator and a payer', () => { await submitTx(authorizedTx, paymentAccount) }, 30_000) - it('should be possible to lookup the DID with the given nick', async () => { + it('should be possible to lookup the DID uri with the given nick', async () => { const { - document: { id }, + document: { uri }, } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name(nick)) - expect(id).toStrictEqual(w3nCreator.id) + expect(uri).toStrictEqual(w3nCreator.uri) }, 30_000) - it('should be possible to lookup the nick with the given DID', async () => { - const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.id)) + 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 didInfo = Did.linkedInfoFromChain(encodedDidInfo) - expect(didInfo.document.alsoKnownAs).toStrictEqual([`w3n:${nick}`]) + expect(didInfo.web3Name).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.id, + otherWeb3NameCreator.uri, 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.id, + w3nCreator.uri, 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.id, + w3nCreator.uri, 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.id, + w3nCreator.uri, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 074dc89215..10b14efa23 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -10,33 +10,24 @@ import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' import type { DecryptCallback, DidDocument, + DidKey, + DidServiceEndpoint, + DidVerificationKey, EncryptCallback, + KeyRelationship, KeyringPair, - KiltAddress, KiltEncryptionKeypair, KiltKeyringPair, - SignCallback, - SubmittableExtrinsic, - UriFragment, - VerificationMethod, - VerificationRelationship, -} from '@kiltprotocol/types' -import type { - BaseNewDidKey, - ChainDidKey, - DidVerificationMethodType, - GetStoreTxSignCallback, LightDidSupportedVerificationKeyType, NewLightDidVerificationKey, - NewDidVerificationKey, - NewDidEncryptionKey, - NewService, -} from '@kiltprotocol/did' + SignCallback, +} from '@kiltprotocol/types' +import { Crypto } from '@kiltprotocol/utils' +import * as Did from '@kiltprotocol/did' -import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' -import * as Did from '@kiltprotocol/did' +import { linkedInfoFromChain, toChain } from '@kiltprotocol/did' export type EncryptionKeyToolCallback = ( didDocument: DidDocument @@ -54,13 +45,10 @@ export function makeEncryptCallback({ }: KiltEncryptionKeypair): EncryptionKeyToolCallback { return (didDocument) => { return async function encryptCallback({ data, peerPublicKey }) { - const keyId = didDocument.keyAgreement?.[0] + const keyId = didDocument.keyAgreement?.[0].id if (!keyId) { - throw new Error(`Encryption key not found in did "${didDocument.id}"`) + throw new Error(`Encryption key not found in did "${didDocument.uri}"`) } - const verificationMethod = didDocument.verificationMethod?.find( - (v) => v.id === keyId - ) as VerificationMethod const { box, nonce } = Crypto.encryptAsymmetric( data, peerPublicKey, @@ -69,7 +57,7 @@ export function makeEncryptCallback({ return { nonce, data: box, - verificationMethod, + keyUri: `${didDocument.uri}${keyId}`, } } } @@ -131,26 +119,20 @@ export type KeyToolSignCallback = (didDocument: DidDocument) => SignCallback */ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { return (didDocument) => - async function sign({ data, verificationRelationship }) { - const keyId = didDocument[verificationRelationship]?.[0] - if (keyId === undefined) { + async function sign({ data, keyRelationship }) { + const keyId = didDocument[keyRelationship]?.[0].id + const keyType = didDocument[keyRelationship]?.[0].type + if (keyId === undefined || keyType === undefined) { throw new Error( - `Verification method for relationship "${verificationRelationship}" not found in DID "${didDocument.id}"` - ) - } - const verificationMethod = didDocument.verificationMethod?.find( - (vm) => vm.id === keyId - ) - if (verificationMethod === undefined) { - throw new Error( - `Verification method for relationship "${verificationRelationship}" not found in DID "${didDocument.id}"` + `Key for purpose "${keyRelationship}" not found in did "${didDocument.uri}"` ) } const signature = keypair.sign(data, { withType: false }) return { signature, - verificationMethod, + keyUri: `${didDocument.uri}${keyId}`, + keyType, } } } @@ -170,9 +152,7 @@ export function makeStoreDidCallback( const signature = keypair.sign(data, { withType: false }) return { signature, - verificationMethod: { - publicKeyMultibase: Did.keypairToMultibaseKey(keypair), - }, + keyType: keypair.type, } } } @@ -205,48 +185,6 @@ 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: VerificationRelationship -): 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: VerificationRelationship -): void { - const verificationMethod = Did.didKeyToVerificationMethod( - didDocument.id, - id, - { - keyType: keyType as DidVerificationMethodType, - publicKey, - } - ) - addVerificationMethod(didDocument, verificationMethod, relationship) -} - /** * Given a keypair, creates a light DID with an authentication and an encryption key. * @@ -265,14 +203,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: ChainDidKey['publicKey']): ChainDidKey['id'] { +export function computeKeyId(key: DidKey['publicKey']): DidKey['id'] { return `#${blake2AsHex(key, 256)}` } function makeDidKeyFromKeypair({ publicKey, type, -}: KiltKeyringPair): ChainDidKey { +}: KiltKeyringPair): DidVerificationKey { return { id: computeKeyId(publicKey), publicKey, @@ -285,92 +223,54 @@ 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 services that must be added to the DID. + * @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, { - verificationRelationships = new Set([ + keyRelationships = new Set([ 'assertionMethod', 'capabilityDelegation', 'keyAgreement', ]), endpoints = [], }: { - verificationRelationships?: Set< - Omit - > - endpoints?: NewService[] + keyRelationships?: Set> + endpoints?: DidServiceEndpoint[] } = {} ): Promise { - const { - type: keyType, - publicKey, - id: authKeyId, - } = makeDidKeyFromKeypair(keypair) - const id = Did.getFullDidFromVerificationMethod({ - publicKeyMultibase: Did.keypairToMultibaseKey({ - type: keyType, - publicKey, - }), - }) + const authKey = makeDidKeyFromKeypair(keypair) + const uri = Did.getFullDidUriFromKey(authKey) const result: DidDocument = { - id, - authentication: [authKeyId], - verificationMethod: [ - Did.didKeyToVerificationMethod(id, authKeyId, { - keyType, - publicKey, - }), - ], + uri, + authentication: [authKey], service: endpoints, } - if (verificationRelationships.has('keyAgreement')) { - const { publicKey: encPublicKey, type } = makeEncryptionKeyTool( - `${keypair.publicKey}//enc` - ).keyAgreement[0] - addKeypairAsVerificationMethod( - result, - { - id: computeKeyId(encPublicKey), - publicKey: encPublicKey, - type, - }, - 'keyAgreement' - ) + if (keyRelationships.has('keyAgreement')) { + const encryptionKeypair = makeEncryptionKeyTool(`${keypair.publicKey}//enc`) + .keyAgreement[0] + const encKey = { + ...encryptionKeypair, + id: computeKeyId(encryptionKeypair.publicKey), + } + result.keyAgreement = [encKey] } - if (verificationRelationships.has('assertionMethod')) { - const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( + if (keyRelationships.has('assertionMethod')) { + const attKey = makeDidKeyFromKeypair( keypair.derive('//att') as KiltKeyringPair ) - addKeypairAsVerificationMethod( - result, - { - id: computeKeyId(encPublicKey), - publicKey: encPublicKey, - type, - }, - 'assertionMethod' - ) + result.assertionMethod = [attKey] } - if (verificationRelationships.has('capabilityDelegation')) { - const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( + if (keyRelationships.has('capabilityDelegation')) { + const delKey = makeDidKeyFromKeypair( keypair.derive('//del') as KiltKeyringPair ) - addKeypairAsVerificationMethod( - result, - { - id: computeKeyId(encPublicKey), - publicKey: encPublicKey, - type, - }, - 'capabilityDelegation' - ) + result.capabilityDelegation = [delKey] } return result @@ -386,10 +286,10 @@ export async function createLocalDemoFullDidFromKeypair( export async function createLocalDemoFullDidFromLightDid( lightDid: DidDocument ): Promise { - const { id, authentication } = lightDid + const { uri, authentication } = lightDid return { - id, + uri: Did.getFullDidUri(uri), authentication, assertionMethod: authentication, capabilityDelegation: authentication, @@ -397,92 +297,6 @@ 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, @@ -490,27 +304,22 @@ export async function createFullDidFromLightDid( sign: StoreDidCallback ): Promise { 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 getStoreTxFromDidDocument( - fullDidDocumentToBeCreated, + const { authentication, uri } = lightDidForId + const tx = await Did.getStoreTx( + { + authentication, + assertionMethod: authentication, + capabilityDelegation: authentication, + keyAgreement: lightDidForId.keyAgreement, + service: lightDidForId.service, + }, payer.address, sign ) await Blockchain.signAndSubmitTx(tx, payer) const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid - const encodedDidDetails = await queryFunction( - Did.toChain(fullDidDocumentToBeCreated.id) - ) - const { document } = await Did.linkedInfoFromChain(encodedDidDetails) - return document + const encodedDidDetails = await queryFunction(toChain(Did.getFullDidUri(uri))) + return linkedInfoFromChain(encodedDidDetails).document } export async function createFullDidFromSeed( diff --git a/yarn.lock b/yarn.lock index 1ac7b6ba2a..20bdd665d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1533,13 +1533,6 @@ __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" @@ -2019,7 +2012,6 @@ __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:*" @@ -2031,7 +2023,6 @@ __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 @@ -2133,13 +2124,6 @@ __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": version: 1.0.0 resolution: "@noble/curves@npm:1.0.0" @@ -7284,15 +7268,6 @@ 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 - "nan@npm:^2.15.0": version: 2.15.0 resolution: "nan@npm:2.15.0"