diff --git a/packages/asset-credentials/src/credentials/PublicCredential.chain.ts b/packages/asset-credentials/src/credentials/PublicCredential.chain.ts index 9f96a7606..6309ea71f 100644 --- a/packages/asset-credentials/src/credentials/PublicCredential.chain.ts +++ b/packages/asset-credentials/src/credentials/PublicCredential.chain.ts @@ -6,12 +6,12 @@ */ import type { - AssetDidUri, + AssetDid, CTypeHash, IDelegationNode, IPublicCredentialInput, IPublicCredential, - DidUri, + Did, HexString, } from '@kiltprotocol/types' import type { ApiPromise } from '@polkadot/api' @@ -29,11 +29,11 @@ import { fromChain as didFromChain } from '@kiltprotocol/did' import { SDKErrors, cbor } from '@kiltprotocol/utils' import { getIdForCredential } from './PublicCredential.js' -import { validateUri } from '../dids/index.js' +import { validateDid } from '../dids/index.js' export interface EncodedPublicCredential { ctypeHash: CTypeHash - subject: AssetDidUri + subject: AssetDid claims: HexString authorization: IDelegationNode['id'] | null } @@ -68,12 +68,12 @@ function credentialInputFromChain({ subject, }: PublicCredentialsCredentialsCredential): IPublicCredentialInput { const credentialSubject = subject.toUtf8() - validateUri(credentialSubject) + validateDid(credentialSubject) return { claims: cbor.decode(claims), cTypeHash: ctypeHash.toHex(), delegationId: authorization.unwrapOr(undefined)?.toHex() ?? null, - subject: credentialSubject as AssetDidUri, + subject: credentialSubject as AssetDid, } } @@ -86,9 +86,9 @@ export interface PublicCredentialEntry { */ ctypeHash: HexString /** - * DID URI of the attester. + * DID of the attester. */ - attester: DidUri + attester: Did /** * Flag indicating whether the credential is currently revoked. */ @@ -244,13 +244,13 @@ export async function fetchCredentialFromChain( /** * Retrieves from the blockchain the [[IPublicCredential]]s that have been issued to the provided AssetDID. * - * This is the **only** secure way for users to retrieve and verify all the credentials issued to a given [[AssetDidUri]]. + * This is the **only** secure way for users to retrieve and verify all the credentials issued to a given [[AssetDid]]. * * @param subject The AssetDID of the subject. * @returns An array of [[IPublicCredential]] as the result of combining the on-chain information and the information present in the tx history. */ export async function fetchCredentialsFromChain( - subject: AssetDidUri + subject: AssetDid ): Promise { const api = ConfigService.get('api') diff --git a/packages/asset-credentials/src/credentials/PublicCredential.spec.ts b/packages/asset-credentials/src/credentials/PublicCredential.spec.ts index 29f3d53e9..b93a3ebb3 100644 --- a/packages/asset-credentials/src/credentials/PublicCredential.spec.ts +++ b/packages/asset-credentials/src/credentials/PublicCredential.spec.ts @@ -11,8 +11,8 @@ import { ConfigService } from '@kiltprotocol/config' import { CType } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import type { - AssetDidUri, - DidUri, + AssetDid, + Did as KiltDid, IAssetClaim, IClaimContents, IPublicCredential, @@ -39,7 +39,7 @@ const assetIdentifier = // Build a public credential with fake attestation (i.e., attester, block number, revocation status) information. function buildCredential( - assetDid: AssetDidUri, + assetDid: AssetDid, contents: IClaimContents ): IPublicCredential { const claim: IAssetClaim = { @@ -48,7 +48,7 @@ function buildCredential( subject: assetDid, } const credential = PublicCredential.fromClaim(claim) - const attester: DidUri = Did.getFullDidUri(devAlice.address) + const attester: KiltDid = Did.getFullDid(devAlice.address) return { ...credential, attester, diff --git a/packages/asset-credentials/src/credentials/PublicCredential.ts b/packages/asset-credentials/src/credentials/PublicCredential.ts index 9972b5aaa..404b19c32 100644 --- a/packages/asset-credentials/src/credentials/PublicCredential.ts +++ b/packages/asset-credentials/src/credentials/PublicCredential.ts @@ -9,7 +9,7 @@ import type { AccountId } from '@polkadot/types/interfaces' import type { PublicCredentialsCredentialsCredential } from '@kiltprotocol/augment-api' import type { HexString, - DidUri, + Did as KiltDid, IAssetClaim, ICType, IDelegationNode, @@ -30,7 +30,7 @@ import { toChain as publicCredentialToChain } from './PublicCredential.chain.js' /** * Calculates the ID of a [[IPublicCredentialInput]], to be used to retrieve the full credential content from the blockchain. * - * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[DidUri]] and then Blake2b hashing the result. + * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[Did]] and then Blake2b hashing the result. * * @param credential The input credential object. * @param attester The DID of the credential attester. @@ -38,7 +38,7 @@ import { toChain as publicCredentialToChain } from './PublicCredential.chain.js' */ export function getIdForCredential( credential: IPublicCredentialInput, - attester: DidUri + attester: KiltDid ): HexString { const api = ConfigService.get('api') @@ -62,7 +62,7 @@ function verifyClaimStructure(input: IAssetClaim | PartialAssetClaim): void { throw new SDKErrors.CTypeHashMissingError() } if (input.subject) { - AssetDid.validateUri(input.subject) + AssetDid.validateDid(input.subject) } if (input.contents) { Object.entries(input.contents).forEach(([key, value]) => { diff --git a/packages/asset-credentials/src/dids/index.ts b/packages/asset-credentials/src/dids/index.ts index 395c2067b..2a86128c7 100644 --- a/packages/asset-credentials/src/dids/index.ts +++ b/packages/asset-credentials/src/dids/index.ts @@ -6,7 +6,7 @@ */ import type { - AssetDidUri, + AssetDid, Caip19AssetId, Caip19AssetInstance, Caip19AssetNamespace, @@ -22,7 +22,7 @@ const ASSET_DID_REGEX = /^did:asset:(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,32}))\.(?(?[-a-z0-9]{3,8}):(?[-a-zA-Z0-9]{1,64})(:(?[-a-zA-Z0-9]{1,78}))?)$/ type IAssetDidParsingResult = { - uri: AssetDidUri + did: AssetDid chainId: Caip2ChainId chainNamespace: Caip2ChainNamespace chainReference: Caip2ChainReference @@ -33,35 +33,35 @@ type IAssetDidParsingResult = { } /** - * Parses an AssetDID uri and returns the information contained within in a structured form. + * Parses an AssetDID and returns the information contained within in a structured form. - * @param assetDidUri An AssetDID uri as a string. -* @returns Object containing information extracted from the AssetDID uri. + * @param assetDid An AssetDID as a string. +* @returns Object containing information extracted from the AssetDID. */ -export function parse(assetDidUri: AssetDidUri): IAssetDidParsingResult { - const matches = ASSET_DID_REGEX.exec(assetDidUri)?.groups +export function parse(assetDid: AssetDid): IAssetDidParsingResult { + const matches = ASSET_DID_REGEX.exec(assetDid)?.groups if (!matches) { - throw new SDKErrors.InvalidDidFormatError(assetDidUri) + throw new SDKErrors.InvalidDidFormatError(assetDid) } - const { chainId, assetId } = matches as Omit + const { chainId, assetId } = matches as Omit return { - ...(matches as Omit), - uri: `did:asset:${chainId}.${assetId}`, + ...(matches as Omit), + did: `did:asset:${chainId}.${assetId}`, } } /** - * Checks that a string (or other input) is a valid AssetDID uri. + * Checks that a string (or other input) is a valid AssetDID. * Throws otherwise. * * @param input Arbitrary input. */ -export function validateUri(input: unknown): void { +export function validateDid(input: unknown): void { if (typeof input !== 'string') { throw new TypeError(`Asset DID string expected, got ${typeof input}`) } - parse(input as AssetDidUri) + parse(input as AssetDid) } diff --git a/packages/core/src/attestation/Attestation.spec.ts b/packages/core/src/attestation/Attestation.spec.ts index 855ec2e28..792623d05 100644 --- a/packages/core/src/attestation/Attestation.spec.ts +++ b/packages/core/src/attestation/Attestation.spec.ts @@ -8,7 +8,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { ConfigService } from '@kiltprotocol/config' -import type { CTypeHash, DidUri, IAttestation } from '@kiltprotocol/types' +import type { CTypeHash, Did, IAttestation } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' import { ApiMocks } from '../../../../tests/testUtils' @@ -22,7 +22,7 @@ beforeAll(() => { }) describe('Attestation', () => { - const identityAlice: DidUri = + const identityAlice: Did = 'did:kilt:4nwPAmtsK5toZfBM9WvmAe4Fa3LyZ3X3JHt7EUFfrcPPAZAm' const cTypeHash: CTypeHash = diff --git a/packages/core/src/attestation/Attestation.ts b/packages/core/src/attestation/Attestation.ts index 59311723e..76c339775 100644 --- a/packages/core/src/attestation/Attestation.ts +++ b/packages/core/src/attestation/Attestation.ts @@ -9,7 +9,7 @@ import type { IAttestation, IDelegationHierarchyDetails, ICredential, - DidUri, + Did as KiltDid, } from '@kiltprotocol/types' import { DataUtils, SDKErrors } from '@kiltprotocol/utils' import * as Did from '@kiltprotocol/did' @@ -49,7 +49,7 @@ export function verifyDataStructure(input: IAttestation): void { if (!input.owner) { throw new SDKErrors.OwnerMissingError() } - Did.validateUri(input.owner, 'Did') + Did.validateDid(input.owner, 'Did') if (typeof input.revoked !== 'boolean') { throw new SDKErrors.RevokedTypeError() @@ -65,7 +65,7 @@ export function verifyDataStructure(input: IAttestation): void { */ export function fromCredentialAndDid( credential: ICredential, - attesterDid: DidUri + attesterDid: KiltDid ): IAttestation { const attestation = { claimHash: credential.rootHash, diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts index 10d221880..b0cbc04ec 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.spec.ts @@ -9,7 +9,7 @@ import { encodeAddress, randomAsHex, randomAsU8a } from '@polkadot/util-crypto' import { u8aToHex, u8aToU8a } from '@polkadot/util' import { parse } from '@kiltprotocol/did' -import type { DidUri } from '@kiltprotocol/types' +import type { Did } from '@kiltprotocol/types' import { attestation, @@ -77,7 +77,7 @@ describe('proofs', () => { }) it('checks delegation node owners', async () => { - const delegator: DidUri = `did:kilt:${encodeAddress(randomAsU8a(32), 38)}` + const delegator: Did = `did:kilt:${encodeAddress(randomAsU8a(32), 38)}` const credentialWithDelegators: KiltCredentialV1 = { ...VC, federatedTrustModel: VC.federatedTrustModel?.map((i) => { diff --git a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts index 1861eb036..f4942a116 100644 --- a/packages/core/src/credentialsV1/KiltAttestationProofV1.ts +++ b/packages/core/src/credentialsV1/KiltAttestationProofV1.ts @@ -31,8 +31,8 @@ import type { IEventData, Signer } from '@polkadot/types/types' import { authorizeTx, - getFullDidUri, - validateUri, + getFullDid, + validateDid, fromChain as didFromChain, } from '@kiltprotocol/did' import { JsonSchema, SDKErrors, Caip19 } from '@kiltprotocol/utils' @@ -43,7 +43,7 @@ import type { RuntimeCommonAuthorizationAuthorizationId, } from '@kiltprotocol/augment-api' import type { - DidUri, + Did, ICType, IDelegationNode, KiltAddress, @@ -220,7 +220,7 @@ async function verifyAttestedAt( ): Promise<{ verified: boolean timestamp: number - attester: DidUri + attester: Did cTypeId: ICType['$id'] delegationId: IDelegationNode['id'] | null }> { @@ -256,7 +256,7 @@ async function verifyAttestedAt( Option | Option ] & IEventData - const attester = getFullDidUri(encodeAddress(att.toU8a(), 38)) + const attester = getFullDid(encodeAddress(att.toU8a(), 38)) const cTypeId = CType.hashToId(cTypeHash.toHex()) const delegationId = authorization.isSome ? ( @@ -276,7 +276,7 @@ async function verifyAttestedAt( async function verifyAuthoritiesInHierarchy( api: ApiPromise, nodeId: Uint8Array | string, - delegators: Set + delegators: Set ): Promise { const node = (await api.query.delegation.delegationNodes(nodeId)).unwrapOr( null @@ -347,7 +347,7 @@ export async function verify( validateCredentialStructure(credential) const { nonTransferable, credentialStatus, credentialSubject, issuer } = credential - validateUri(issuer, 'Did') + validateDid(issuer, 'Did') await validateSubject(credential, opts) // 4. check nonTransferable if (nonTransferable !== true) @@ -660,7 +660,7 @@ export type AttestationHandler = ( }> export interface DidSigner { - did: DidUri + did: Did signer: SignExtrinsicCallback } diff --git a/packages/core/src/credentialsV1/KiltCredentialV1.ts b/packages/core/src/credentialsV1/KiltCredentialV1.ts index 1babbae66..de5861f4d 100644 --- a/packages/core/src/credentialsV1/KiltCredentialV1.ts +++ b/packages/core/src/credentialsV1/KiltCredentialV1.ts @@ -12,7 +12,7 @@ import { JsonSchema, SDKErrors } from '@kiltprotocol/utils' import type { ICType, ICredential, - DidUri, + Did, IDelegationNode, } from '@kiltprotocol/types' @@ -215,10 +215,10 @@ export function validateStructure( } interface CredentialInput { - subject: DidUri + subject: Did claims: ICredential['claim']['contents'] cType: ICType['$id'] - issuer: DidUri + issuer: Did timestamp?: number chainGenesisHash?: Uint8Array claimHash?: ICredential['rootHash'] diff --git a/packages/core/src/credentialsV1/types.ts b/packages/core/src/credentialsV1/types.ts index 88c534ebb..1be6826b1 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 { - ConformingDidKey, - DidUri, + VerificationMethod, + Did, Caip2ChainId, IClaimContents, ICType, @@ -33,7 +33,7 @@ import type { KILT_ATTESTER_LEGITIMATION_V1_TYPE, } from './common.js' -export type IPublicKeyRecord = ConformingDidKey +export type IPublicKeyRecord = VerificationMethod 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: DidUri + holder: Did proof?: Proof | Proof[] expirationDate?: string issuanceDate?: string @@ -134,14 +134,14 @@ export interface KiltAttesterLegitimationV1 extends IssuerBacking { export interface KiltAttesterDelegationV1 extends IssuerBacking { id: `kilt:delegation/${string}` type: typeof KILT_ATTESTER_DELEGATION_V1_TYPE - delegators?: DidUri[] + delegators?: Did[] } export interface CredentialSubject extends IClaimContents { '@context': { '@vocab': string } - id: DidUri + id: Did } export interface KiltCredentialV1 extends VerifiableCredential { @@ -166,7 +166,7 @@ export interface KiltCredentialV1 extends VerifiableCredential { /** * The entity that issued the credential. */ - issuer: DidUri + issuer: Did /** * If true, this credential can only be presented and used by its subject. */ diff --git a/packages/core/src/ctype/CType.chain.ts b/packages/core/src/ctype/CType.chain.ts index 94fff97e9..17a976847 100644 --- a/packages/core/src/ctype/CType.chain.ts +++ b/packages/core/src/ctype/CType.chain.ts @@ -11,7 +11,7 @@ import type { AccountId, Call } from '@polkadot/types/interfaces' import type { BN } from '@polkadot/util' import type { CtypeCtypeEntry } from '@kiltprotocol/augment-api' -import type { CTypeHash, DidUri, ICType } from '@kiltprotocol/types' +import type { CTypeHash, Did as KiltDid, ICType } from '@kiltprotocol/types' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' @@ -76,7 +76,7 @@ export interface CTypeChainDetails { /** * The DID of the CType's creator. */ - creator: DidUri + creator: KiltDid /** * The block number in which the CType was created. */ diff --git a/packages/core/src/delegation/DelegationNode.spec.ts b/packages/core/src/delegation/DelegationNode.spec.ts index dbf3a84e8..a56d97c55 100644 --- a/packages/core/src/delegation/DelegationNode.spec.ts +++ b/packages/core/src/delegation/DelegationNode.spec.ts @@ -10,7 +10,7 @@ import { encodeAddress } from '@polkadot/keyring' import { ConfigService } from '@kiltprotocol/config' import { CTypeHash, - DidUri, + Did, IDelegationHierarchyDetails, IDelegationNode, Permission, @@ -54,7 +54,7 @@ describe('DelegationNode', () => { let hierarchyId: string let parentId: string let hashList: string[] - let addresses: DidUri[] + let addresses: Did[] beforeAll(() => { jest @@ -85,7 +85,7 @@ describe('DelegationNode', () => { .map((_val, index) => Crypto.hashStr(`${index + 1}`)) addresses = Array(10002) .fill('') - .map( + .map( (_val, index) => `did:kilt:${encodeAddress(Crypto.hash(`${index}`, 256), ss58Format)}` ) @@ -448,7 +448,7 @@ describe('DelegationNode', () => { }) it('returns null if looking for non-existent account', async () => { - const noOnesAddress: DidUri = `did:kilt:${encodeAddress( + const noOnesAddress: Did = `did:kilt:${encodeAddress( Crypto.hash('-1', 256), ss58Format )}` diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index 60d50a96d..e63b156e4 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -8,13 +8,13 @@ import type { CTypeHash, DidDocument, - DidUri, - DidVerificationKey, + Did as KiltDid, 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: DidUri + public readonly account: KiltDid public readonly permissions: IDelegationNode['permissions'] private hierarchyDetails?: IDelegationHierarchyDetails public readonly revoked: boolean @@ -268,23 +268,27 @@ export class DelegationNode implements IDelegationNode { ): Promise { const delegateSignature = await sign({ data: this.generateHash(), - did: delegateDid.uri, - keyRelationship: 'authentication', + did: delegateDid.id, + verificationRelationship: 'authentication', }) - const { fragment } = Did.parse(delegateSignature.keyUri) + const signerUrl = + `${delegateDid.id}${delegateSignature.verificationMethod.id}` as DidUrl + const { fragment } = Did.parse(signerUrl) if (!fragment) { throw new SDKErrors.DidError( - `DID key uri "${delegateSignature.keyUri}" couldn't be parsed` + `DID verification method URL "${signerUrl}" couldn't be parsed` ) } - const key = Did.getKey(delegateDid, fragment) - if (!key) { + const verificationMethod = delegateDid.verificationMethod?.find( + ({ id }) => id === fragment + ) + if (!verificationMethod) { throw new SDKErrors.DidError( - `Key with fragment "${fragment}" was not found on DID: "${delegateDid.uri}"` + `Verification method "${signerUrl}" was not found on DID: "${delegateDid.id}"` ) } return Did.didSignatureToChain( - key as DidVerificationKey, + verificationMethod, delegateSignature.signature ) } @@ -346,7 +350,7 @@ export class DelegationNode implements IDelegationNode { * @returns An object containing a `node` owned by the identity if it is delegating, plus the number of `steps` traversed. `steps` is 0 if the DID is owner of the current node. */ public async findAncestorOwnedBy( - dids: DidUri | DidUri[] + dids: KiltDid | KiltDid[] ): Promise<{ steps: number; node: DelegationNode | null }> { const acceptedDids = Array.isArray(dids) ? dids : [dids] if (acceptedDids.includes(this.account)) { @@ -399,7 +403,7 @@ export class DelegationNode implements IDelegationNode { * @param did The address of the identity used to revoke the delegation. * @returns Promise containing an unsigned SubmittableExtrinsic. */ - public async getRevokeTx(did: DidUri): Promise { + public async getRevokeTx(did: KiltDid): Promise { const { steps, node } = await this.findAncestorOwnedBy(did) if (!node) { throw new SDKErrors.UnauthorizedError( diff --git a/packages/core/src/delegation/DelegationNode.utils.ts b/packages/core/src/delegation/DelegationNode.utils.ts index 3c70e3bc6..b34a3a764 100644 --- a/packages/core/src/delegation/DelegationNode.utils.ts +++ b/packages/core/src/delegation/DelegationNode.utils.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri, IAttestation, IDelegationNode } from '@kiltprotocol/types' +import type { Did, IAttestation, IDelegationNode } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' import { isHex } from '@polkadot/util' import { DelegationNode } from './DelegationNode.js' @@ -39,7 +39,7 @@ export function permissionsAsBitset(delegation: IDelegationNode): Uint8Array { * @returns 0 if `attester` is the owner of `attestation`, the number of delegation nodes traversed otherwise. */ export async function countNodeDepth( - attester: DidUri, + attester: Did, attestation: IAttestation ): Promise { let delegationTreeTraversalSteps = 0 diff --git a/packages/core/src/presentation/Presentation.ts b/packages/core/src/presentation/Presentation.ts index 3e8e346c8..5d56cd96a 100644 --- a/packages/core/src/presentation/Presentation.ts +++ b/packages/core/src/presentation/Presentation.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from '@kiltprotocol/types' +import type { Did } from '@kiltprotocol/types' import { JsonSchema, SDKErrors } from '@kiltprotocol/utils' import { @@ -134,7 +134,7 @@ export function assertHolderCanPresentCredentials({ holder, verifiableCredential, }: { - holder: DidUri + holder: Did verifiableCredential: VerifiableCredential[] | VerifiableCredential }): void { const credentials = Array.isArray(verifiableCredential) @@ -164,7 +164,7 @@ export function assertHolderCanPresentCredentials({ */ export function create( VCs: VerifiableCredential[], - holder: DidUri, + holder: Did, { validFrom, validUntil, diff --git a/packages/did/package.json b/packages/did/package.json index 05c3a631c..95bdd77c8 100644 --- a/packages/did/package.json +++ b/packages/did/package.json @@ -34,6 +34,7 @@ "typescript": "^4.8.3" }, "dependencies": { + "@digitalbazaar/multikey-context": "^1.0.0", "@digitalbazaar/security-context": "^1.0.0", "@kiltprotocol/augment-api": "workspace:*", "@kiltprotocol/config": "workspace:*", @@ -44,6 +45,7 @@ "@polkadot/types": "^10.4.0", "@polkadot/types-codec": "^10.4.0", "@polkadot/util": "^12.0.0", - "@polkadot/util-crypto": "^12.0.0" + "@polkadot/util-crypto": "^12.0.0", + "multibase": "^4.0.6" } } diff --git a/packages/did/src/Did.chain.ts b/packages/did/src/Did.chain.ts index 09daf45e4..d8ebc0f1e 100644 --- a/packages/did/src/Did.chain.ts +++ b/packages/did/src/Did.chain.ts @@ -8,54 +8,57 @@ import type { Option } from '@polkadot/types' import type { AccountId32, Extrinsic, Hash } from '@polkadot/types/interfaces' import type { AnyNumber } from '@polkadot/types/types' - import type { + DidDidDetails, + DidDidDetailsDidAuthorizedCallOperation, + DidDidDetailsDidPublicKeyDetails, + DidServiceEndpointsDidEndpoint, + KiltSupportDeposit, +} from '@kiltprotocol/augment-api' +import type { + BN, Deposit, - DidDocument, - DidEncryptionKey, - DidKey, - DidServiceEndpoint, - DidUri, - DidVerificationKey, + Did, KiltAddress, - NewDidEncryptionKey, - NewDidVerificationKey, + Service, + SignatureVerificationRelationship, SignExtrinsicCallback, SignRequestData, SignResponseData, SubmittableExtrinsic, UriFragment, - VerificationKeyRelationship, - BN, + VerificationMethod, } from '@kiltprotocol/types' -import { verificationKeyTypes } from '@kiltprotocol/types' -import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' + import { ConfigService } from '@kiltprotocol/config' +import { Crypto, SDKErrors, ss58Format } from '@kiltprotocol/utils' + import type { - DidDidDetails, - DidDidDetailsDidAuthorizedCallOperation, - DidDidDetailsDidPublicKey, - DidDidDetailsDidPublicKeyDetails, - DidServiceEndpointsDidEndpoint, - KiltSupportDeposit, -} from '@kiltprotocol/augment-api' + DidEncryptionMethodType, + NewService, + DidSigningMethodType, + NewDidVerificationKey, + NewDidEncryptionKey, +} from './DidDetails/DidDetails.js' +import { isValidVerificationMethodType } from './DidDetails/DidDetails.js' import { - EncodedEncryptionKey, - EncodedKey, - EncodedSignature, - EncodedVerificationKey, - getAddressByKey, - getFullDidUri, + multibaseKeyToDidKey, + keypairToMultibaseKey, + getAddressFromVerificationMethod, + getFullDid, parse, } from './Did.utils.js' -// ### Chain type definitions - -export type ChainDidPublicKey = DidDidDetailsDidPublicKey -export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails +export type ChainDidIdentifier = KiltAddress -// ### RAW QUERYING (lowest layer) +export type EncodedVerificationKey = + | { sr25519: Uint8Array } + | { ed25519: Uint8Array } + | { ecdsa: Uint8Array } +export type EncodedEncryptionKey = { x25519: Uint8Array } +export type EncodedDidKey = EncodedVerificationKey | EncodedEncryptionKey +export type EncodedSignature = EncodedVerificationKey /** * Format a DID to be used as a parameter for the blockchain API functions. @@ -63,20 +66,30 @@ export type ChainDidPublicKeyDetails = DidDidDetailsDidPublicKeyDetails * @param did The DID to format. * @returns The blockchain-formatted DID. */ -export function toChain(did: DidUri): KiltAddress { +export function toChain(did: Did): ChainDidIdentifier { return parse(did).address } /** - * Format a DID resource ID to be used as a parameter for the blockchain API functions. + * Format a DID fragment to be used as a parameter for the blockchain API functions. - * @param id The DID resource ID to format. + * @param id The DID fragment to format. * @returns The blockchain-formatted ID. */ -export function resourceIdToChain(id: UriFragment): string { +export function fragmentIdToChain(id: UriFragment): string { return id.replace(/^#/, '') } +/** + * Convert the DID data from blockchain format to the DID. + * + * @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. * @@ -90,42 +103,57 @@ export function depositFromChain(deposit: KiltSupportDeposit): Deposit { } } -// ### DECODED QUERYING types +export type ChainDidBaseKey = { + id: UriFragment + publicKey: Uint8Array + includedAt?: BN + type: string +} +export type ChainDidVerificationKey = ChainDidBaseKey & { + type: 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[] -type ChainDocument = Pick< - DidDocument, - 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'keyAgreement' -> & { lastTxCounter: BN deposit: Deposit } -// ### DECODED QUERYING (builds on top of raw querying) - -function didPublicKeyDetailsFromChain( +/** + * Convert a DID public key from the blockchain format to a JS object. + * + * @param keyId The key ID. + * @param keyDetails The associated public key blockchain-formatted details. + * @returns The JS-formatted DID key. + */ +export function publicKeyFromChain( keyId: Hash, - keyDetails: ChainDidPublicKeyDetails -): DidKey { + keyDetails: DidDidDetailsDidPublicKeyDetails +): ChainDidKey { const key = keyDetails.key.isPublicEncryptionKey ? keyDetails.key.asPublicEncryptionKey : keyDetails.key.asPublicVerificationKey return { id: `#${keyId.toHex()}`, - type: key.type.toLowerCase() as DidKey['type'], publicKey: key.value.toU8a(), + type: key.type.toLowerCase() as ChainDidKey['type'], } } -/** - * Convert the DID data from blockchain format to the DID URI. - * - * @param encoded The chain-formatted DID. - * @returns The DID URI. - */ -export function fromChain(encoded: AccountId32): DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) -} - /** * Convert the DID Document data from the blockchain format to a JS object. * @@ -134,7 +162,7 @@ export function fromChain(encoded: AccountId32): DidUri { */ export function documentFromChain( encoded: Option -): ChainDocument { +): ChainDidDetails { const { publicKeys, authenticationKey, @@ -145,28 +173,28 @@ export function documentFromChain( deposit, } = encoded.unwrap() - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => - didPublicKeyDetailsFromChain(keyId, keyDetails) - ) + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) .reduce((res, key) => { - res[resourceIdToChain(key.id)] = key + res[fragmentIdToChain(key.id)] = key return res }, {}) - const authentication = keys[authenticationKey.toHex()] as DidVerificationKey + const authentication = keys[ + authenticationKey.toHex() + ] as ChainDidVerificationKey - const didRecord: ChainDocument = { + const didRecord: ChainDidDetails = { authentication: [authentication], lastTxCounter: lastTxCounter.toBn(), deposit: depositFromChain(deposit), } if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.assertionMethod = [key] } if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.capabilityDelegation = [key] } @@ -175,25 +203,13 @@ export function documentFromChain( ) if (keyAgreementKeyIds.length > 0) { didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as DidEncryptionKey + (id) => keys[id] as ChainDidEncryptionKey ) } return didRecord } -interface ChainEndpoint { - id: string - serviceTypes: DidServiceEndpoint['type'] - urls: DidServiceEndpoint['serviceEndpoint'] -} - -/** - * 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 @@ -203,7 +219,7 @@ function isUri(str: string): boolean { } } -const UriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ +const uriFragmentRegex = /^[a-zA-Z0-9._~%+,;=*()'&$!@:/?-]+$/ /** * Checks if a string is a valid URI fragment according to RFC#3986. @@ -213,27 +229,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 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. + * 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. * - If the `uris` property contains one or more strings, they must be valid URIs according to RFC#3986. * - * @param endpoint A service endpoint object to check. + * @param endpoint A service object to check. */ -export function validateService(endpoint: DidServiceEndpoint): void { +export function validateNewService(endpoint: NewService): void { const { id, serviceEndpoint } = endpoint - if (id.startsWith('did:kilt')) { + if ((id as string).startsWith('did:kilt')) { throw new SDKErrors.DidError( - `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URI, which is violated by id "${id}"` + `This function requires only the URI fragment part (following '#') of the service ID, not the full DID URL, which is violated by id "${id}"` ) } - if (!isUriFragment(resourceIdToChain(id))) { + if (!isUriFragment(fragmentIdToChain(id))) { throw new SDKErrors.DidError( `The service ID must be valid as a URI fragment according to RFC#3986, which "${id}" is not. Make sure not to use disallowed characters (e.g. whitespace) or consider URL-encoding the desired id.` ) @@ -253,11 +269,11 @@ export function validateService(endpoint: DidServiceEndpoint): void { * @param service The DID service to format. * @returns The blockchain-formatted DID service. */ -export function serviceToChain(service: DidServiceEndpoint): ChainEndpoint { - validateService(service) +export function serviceToChain(service: NewService): ChainDidService { + validateNewService(service) const { id, type, serviceEndpoint } = service return { - id: resourceIdToChain(id), + id: fragmentIdToChain(id), serviceTypes: type, urls: serviceEndpoint, } @@ -271,7 +287,7 @@ export function serviceToChain(service: DidServiceEndpoint): ChainEndpoint { */ export function serviceFromChain( encoded: Option -): DidServiceEndpoint { +): Service { const { id, serviceTypes, urls } = encoded.unwrap() return { id: `#${id.toUtf8()}`, @@ -280,18 +296,14 @@ export function serviceFromChain( } } -// ### EXTRINSICS types - export type AuthorizeCallInput = { - did: DidUri + did: Did txCounter: AnyNumber call: Extrinsic submitter: KiltAddress blockNumber?: AnyNumber } -// ### EXTRINSICS - export function publicKeyToChain( key: NewDidVerificationKey ): EncodedVerificationKey @@ -305,9 +317,9 @@ export function publicKeyToChain(key: NewDidEncryptionKey): EncodedEncryptionKey */ export function publicKeyToChain( key: NewDidVerificationKey | NewDidEncryptionKey -): EncodedKey { +): EncodedDidKey { // TypeScript can't infer type here, so we have to add a type assertion. - return { [key.type]: key.publicKey } as EncodedKey + return { [key.type]: key.publicKey } as EncodedDidKey } interface GetStoreTxInput { @@ -316,32 +328,36 @@ interface GetStoreTxInput { capabilityDelegation?: [NewDidVerificationKey] keyAgreement?: NewDidEncryptionKey[] - service?: DidServiceEndpoint[] + service?: NewService[] } +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 and service endpoints. + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided keys as verification methods and services. * - * A DID creation operation can contain at most 25 new service endpoints. - * Additionally, each service endpoint must respect the following conditions: - * - The service endpoint ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. - * - The service endpoint has at most 1 service type, with a value that is at most 50 bytes long. - * - The service endpoint has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * A DID creation operation can contain at most 25 new services. + * Additionally, each service must respect the following conditions: + * - The service ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service has at most 1 service type, with a value that is at most 50 bytes long. + * - The service has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. * - * @param input The DID keys and services to store, also accepts DidDocument, so you can store a light DID for example. + * @param input The DID keys and services to store. * @param submitter The KILT address authorized to submit the creation operation. * @param sign The sign callback. The authentication key has to be used. * * @returns The SubmittableExtrinsic for the DID creation operation. */ export async function getStoreTx( - input: GetStoreTxInput | DidDocument, + input: GetStoreTxInput, submitter: KiltAddress, sign: GetStoreTxSignCallback ): Promise { @@ -386,12 +402,14 @@ export async function getStoreTx( api.consts.did.maxNumberOfServicesPerDid.toNumber() if (service.length > maxNumberOfServicesPerDid) { throw new SDKErrors.DidError( - `Cannot store more than ${maxNumberOfServicesPerDid} service endpoints per DID` + `Cannot store more than ${maxNumberOfServicesPerDid} services per DID` ) } const [authenticationKey] = authentication - const did = getAddressByKey(authenticationKey) + const did = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authenticationKey), + }) const newAttestationKey = assertionMethod && @@ -419,19 +437,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, - keyRelationship: 'authentication', + verificationRelationship: 'authentication', }) const encodedSignature = { - [signature.keyType]: signature.signature, + [authenticationKey.type]: signature, } as EncodedSignature return api.tx.did.create(encoded, encodedSignature) } export interface SigningOptions { sign: SignExtrinsicCallback - keyRelationship: VerificationKeyRelationship + verificationRelationship: SignatureVerificationRelationship } /** @@ -440,7 +458,7 @@ export interface SigningOptions { * * @param params Object wrapping all input to the function. * @param params.did Full DID. - * @param params.keyRelationship DID key relationship to be used for authorization. + * @param params.verificationRelationship DID verification relationship to be used for authorization. * @param params.sign The callback to interface with the key store managing the private key to be used. * @param params.call The call or extrinsic to be authorized. * @param params.txCounter The nonce or txCounter value for this extrinsic, which must be on larger than the current txCounter value of the authorizing full DID. @@ -450,7 +468,7 @@ export interface SigningOptions { */ export async function generateDidAuthenticatedTx({ did, - keyRelationship, + verificationRelationship, sign, call, txCounter, @@ -469,34 +487,38 @@ export async function generateDidAuthenticatedTx({ blockNumber: blockNumber ?? (await api.query.system.number()), } ) - const signature = await sign({ + const { signature, verificationMethod } = await sign({ data: signableCall.toU8a(), - keyRelationship, + verificationRelationship, did, }) + const { keyType } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) const encodedSignature = { - [signature.keyType]: signature.signature, + [keyType]: signature, } as EncodedSignature return api.tx.did.submitDidCall(signableCall, encodedSignature) } -// ### Chain utils /** * Compiles an enum-type key-value pair representation of a signature created with a full DID verification method. Required for creating full DID signed extrinsics. * * @param key Object describing data associated with a public key. - * @param signature Object containing a signature generated with a full DID associated public key. + * @param key.publicKeyMultibase The multibase, multicodec representation of the signing public key. + * @param signature The signature generated with the full DID associated public key. * @returns Data restructured to allow SCALE encoding by polkadot api. */ export function didSignatureToChain( - key: DidVerificationKey, + { publicKeyMultibase }: VerificationMethod, signature: Uint8Array ): EncodedSignature { - if (!verificationKeyTypes.includes(key.type)) { + const { keyType } = multibaseKeyToDidKey(publicKeyMultibase) + if (!isValidVerificationMethodType(keyType)) { throw new SDKErrors.DidError( - `encodedDidSignature requires a verification key. A key of type "${key.type}" was used instead` + `encodedDidSignature requires a verification key. A key of type "${keyType}" was used instead` ) } - return { [key.type]: signature } as EncodedSignature + return { [keyType]: signature } as EncodedSignature } diff --git a/packages/did/src/Did.rpc.ts b/packages/did/src/Did.rpc.ts index da77219d6..272a74e99 100644 --- a/packages/did/src/Did.rpc.ts +++ b/packages/did/src/Did.rpc.ts @@ -7,74 +7,42 @@ import type { Option, Vec } from '@polkadot/types' import type { Codec } from '@polkadot/types/types' -import type { AccountId32, Hash } from '@polkadot/types/interfaces' import type { RawDidLinkedInfo, - KiltSupportDeposit, - DidDidDetailsDidPublicKeyDetails, DidDidDetails, DidServiceEndpointsDidEndpoint, PalletDidLookupLinkableAccountLinkableAccountId, } from '@kiltprotocol/augment-api' -import type { - Deposit, - DidDocument, - DidEncryptionKey, - DidKey, - DidServiceEndpoint, - DidUri, - DidVerificationKey, - KiltAddress, - UriFragment, - BN, -} from '@kiltprotocol/types' +import type { DidDocument, KiltAddress, Service } from '@kiltprotocol/types' +import { ss58Format } from '@kiltprotocol/utils' import { encodeAddress } from '@polkadot/keyring' import { ethereumEncode } from '@polkadot/util-crypto' -import { u8aToString } from '@polkadot/util' -import { Crypto, ss58Format } from '@kiltprotocol/utils' - -import { Address, SubstrateAddress } from './DidLinks/AccountLinks.chain.js' -import { getFullDidUri } from './Did.utils.js' -function fromChain(encoded: AccountId32): DidUri { - return getFullDidUri(Crypto.encodeAddress(encoded, ss58Format)) -} - -type RpcDocument = Pick< - DidDocument, - 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'keyAgreement' -> & { - lastTxCounter: BN - deposit: Deposit -} - -function depositFromChain(deposit: KiltSupportDeposit): Deposit { - return { - owner: Crypto.encodeAddress(deposit.owner, ss58Format), - amount: deposit.amount.toBn(), - } -} - -function didPublicKeyDetailsFromChain( - keyId: Hash, - keyDetails: DidDidDetailsDidPublicKeyDetails -): DidKey { - const key = keyDetails.key.isPublicEncryptionKey - ? keyDetails.key.asPublicEncryptionKey - : keyDetails.key.asPublicVerificationKey - return { - id: `#${keyId.toHex()}`, - type: key.type.toLowerCase() as DidKey['type'], - publicKey: key.value.toU8a(), - } -} - -function resourceIdToChain(id: UriFragment): string { - return id.replace(/^#/, '') -} - -function documentFromChain(encoded: DidDidDetails): RpcDocument { +import type { + Address, + SubstrateAddress, +} from './DidLinks/AccountLinks.chain.js' +import type { + ChainDidDetails, + ChainDidEncryptionKey, + ChainDidKey, + ChainDidVerificationKey, +} from './Did.chain.js' + +import { + depositFromChain, + fragmentIdToChain, + fromChain, + publicKeyFromChain, +} from './Did.chain.js' + +import { didKeyToVerificationMethod } from './Did.utils.js' +import { addKeypairAsVerificationMethod } from './DidDetails/DidDetails.js' + +function documentFromChain( + encoded: DidDidDetails +): Omit { const { publicKeys, authenticationKey, @@ -85,29 +53,28 @@ function documentFromChain(encoded: DidDidDetails): RpcDocument { deposit, } = encoded - const keys: Record = [...publicKeys.entries()] - .map(([keyId, keyDetails]) => - didPublicKeyDetailsFromChain(keyId, keyDetails) - ) + const keys: Record = [...publicKeys.entries()] + .map(([keyId, keyDetails]) => publicKeyFromChain(keyId, keyDetails)) .reduce((res, key) => { - res[resourceIdToChain(key.id)] = key + res[fragmentIdToChain(key.id)] = key return res }, {}) - const authentication = keys[authenticationKey.toHex()] as DidVerificationKey + const authentication = keys[ + authenticationKey.toHex() + ] as ChainDidVerificationKey - const didRecord: RpcDocument = { + const didRecord: ChainDidDetails = { authentication: [authentication], lastTxCounter: lastTxCounter.toBn(), deposit: depositFromChain(deposit), } - if (attestationKey.isSome) { - const key = keys[attestationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[attestationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.assertionMethod = [key] } if (delegationKey.isSome) { - const key = keys[delegationKey.unwrap().toHex()] as DidVerificationKey + const key = keys[delegationKey.unwrap().toHex()] as ChainDidVerificationKey didRecord.capabilityDelegation = [key] } @@ -116,27 +83,25 @@ function documentFromChain(encoded: DidDidDetails): RpcDocument { ) if (keyAgreementKeyIds.length > 0) { didRecord.keyAgreement = keyAgreementKeyIds.map( - (id) => keys[id] as DidEncryptionKey + (id) => keys[id] as ChainDidEncryptionKey ) } return didRecord } -function serviceFromChain( - encoded: DidServiceEndpointsDidEndpoint -): DidServiceEndpoint { +function serviceFromChain(encoded: DidServiceEndpointsDidEndpoint): Service { const { id, serviceTypes, urls } = encoded return { - id: `#${u8aToString(id)}`, - type: serviceTypes.map(u8aToString), - serviceEndpoint: urls.map(u8aToString), + id: `#${id.toUtf8()}`, + type: serviceTypes.map((type) => type.toUtf8()), + serviceEndpoint: urls.map((url) => url.toUtf8()), } } function servicesFromChain( encoded: DidServiceEndpointsDidEndpoint[] -): DidServiceEndpoint[] { +): Service[] { return encoded.map((encodedValue) => serviceFromChain(encodedValue)) } @@ -170,14 +135,8 @@ function connectedAccountsFromChain( ) } -/** - * Web3Name is the type of nickname for a DID. - */ -export type Web3Name = string - -export interface DidInfo { +export interface LinkedDidInfo { document: DidDocument - web3Name?: Web3Name accounts: Address[] } @@ -191,30 +150,67 @@ export interface DidInfo { export function linkedInfoFromChain( encoded: Option, networkPrefix = ss58Format -): DidInfo { +): LinkedDidInfo { const { identifier, accounts, w3n, serviceEndpoints, details } = encoded.unwrap() - const didRec = documentFromChain(details) + const { + authentication, + keyAgreement, + capabilityDelegation, + assertionMethod, + } = documentFromChain(details) const did: DidDocument = { - uri: fromChain(identifier), - authentication: didRec.authentication, - assertionMethod: didRec.assertionMethod, - capabilityDelegation: didRec.capabilityDelegation, - keyAgreement: didRec.keyAgreement, + id: fromChain(identifier), + authentication: [authentication[0].id], + verificationMethod: [ + didKeyToVerificationMethod(fromChain(identifier), authentication[0].id, { + keyType: authentication[0].type, + publicKey: authentication[0].publicKey, + }), + ], + } + + if (keyAgreement !== undefined && keyAgreement.length > 0) { + keyAgreement.forEach(({ id, publicKey, type }) => { + addKeypairAsVerificationMethod( + did, + { id, publicKey, type }, + 'keyAgreement' + ) + }) + } + + if (assertionMethod !== undefined) { + const { id, type, publicKey } = assertionMethod[0] + addKeypairAsVerificationMethod( + did, + { id, publicKey, type }, + 'assertionMethod' + ) } - const service = servicesFromChain(serviceEndpoints) - if (service.length > 0) { - did.service = service + if (capabilityDelegation !== undefined) { + const { id, type, publicKey } = capabilityDelegation[0] + addKeypairAsVerificationMethod( + did, + { id, publicKey, type }, + 'capabilityDelegation' + ) + } + + const services = servicesFromChain(serviceEndpoints) + if (services.length > 0) { + did.service = services } - const web3Name = w3n.isNone ? undefined : w3n.unwrap().toHuman() + if (w3n.isSome) { + did.alsoKnownAs = [`w3n:${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 449e69813..e5672ca8f 100644 --- a/packages/did/src/Did.signature.spec.ts +++ b/packages/did/src/Did.signature.spec.ts @@ -5,18 +5,20 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { randomAsHex, randomAsU8a } from '@polkadot/util-crypto' - import type { - DidDocument, - DidResourceUri, - DidSignature, - KeyringPair, KiltKeyringPair, - NewLightDidVerificationKey, + KeyringPair, + DidDocument, SignCallback, + DidUrl, + DidSignature, + DereferenceResult, } 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 { @@ -25,13 +27,14 @@ import { signatureToJson, verifyDidSignature, } from './Did.signature' -import { keyToResolvedKey, resolveKey } from './DidResolver' -import * as Did from './index.js' +import { dereference, SupportedContentType } from './DidResolver/DidResolver' +import { keypairToMultibaseKey, multibaseKeyToDidKey, parse } from './Did.utils' +import { createLightDidDocument } from './DidDetails' -jest.mock('./DidResolver') +jest.mock('./DidResolver/DidResolver') jest - .mocked(keyToResolvedKey) - .mockImplementation(jest.requireActual('./DidResolver').keyToResolvedKey) + .mocked(dereference) + .mockImplementation(jest.requireActual('./DidResolver').dereference) describe('light DID', () => { let keypair: KiltKeyringPair @@ -40,7 +43,7 @@ describe('light DID', () => { beforeAll(() => { const keyTool = makeSigningKeyTool() keypair = keyTool.keypair - did = Did.createLightDidDocument({ + did = createLightDidDocument({ authentication: keyTool.authentication, }) sign = keyTool.getSignCallback(did) @@ -48,28 +51,39 @@ describe('light DID', () => { beforeEach(() => { jest - .mocked(resolveKey) + .mocked(dereference) .mockReset() - .mockImplementation(async (didUri, keyRelationship = 'authentication') => - didUri.includes(keypair.address) - ? Did.keyToResolvedKey(did[keyRelationship]![0], did.uri) - : Promise.reject() + .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' }, + } + } ) }) it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -79,8 +93,8 @@ describe('light DID', () => { const { signature, keyUri } = signatureToJson( await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) ) const oldSignature = { @@ -96,117 +110,125 @@ describe('light DID', () => { it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: SIGNED_BYTES, - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('fails if relationship does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'assertionMethod', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationRelationship: 'assertionMethod', }) ).rejects.toThrow() }) - it('fails if key id does not match', async () => { + it('fails if verification method id does not match', async () => { const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, keyUri } = await sign({ + let { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', + }) + const wrongVerificationMethodId = `${verificationMethod.id}1a` + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, }) - keyUri = `${keyUri}1a` - jest.mocked(resolveKey).mockRejectedValue(new Error('Key not found')) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${wrongVerificationMethodId}` as DidUrl, + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) it('fails if signature does not match', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING.substring(1), signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) - it('fails if key id malformed', async () => { - jest.mocked(resolveKey).mockRestore() + it('fails if verification method id malformed', async () => { + jest.mocked(dereference).mockRestore() const SIGNED_STRING = 'signed string' // eslint-disable-next-line prefer-const - let { signature, keyUri } = await sign({ + let { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) - // @ts-expect-error - keyUri = keyUri.replace('#', '?') + const malformedVerificationId = verificationMethod.id.replace('#', '?') await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${malformedVerificationId}` as DidUrl, + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) it('does not verify if migrated to Full DID', async () => { - jest.mocked(resolveKey).mockRejectedValue(new Error('Migrated')) + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: { + canonicalId: did.id, + }, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: { id: did.id }, + }) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - keyUri: `${did.uri}${did.authentication[0].id}`, + keyUri: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -214,37 +236,48 @@ describe('light DID', () => { it('detects signer expectation mismatch if signature is by unrelated did', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) - const expectedSigner = Did.createLightDidDocument({ + const expectedSigner = createLightDidDocument({ authentication: makeSigningKeyTool().authentication, - }).uri + }).id await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethod: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) it('allows variations of the same light did', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) - const expectedSigner = Did.createLightDidDocument({ - authentication: did.authentication as [NewLightDidVerificationKey], + const authKey = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] + ) + const expectedSignerAuthKey = multibaseKeyToDidKey( + authKey!.publicKeyMultibase + ) + const expectedSigner = createLightDidDocument({ + authentication: [ + { + publicKey: expectedSignerAuthKey.publicKey, + type: expectedSignerAuthKey.keyType, + }, + ] as [NewLightDidVerificationKey], keyAgreement: [{ type: 'x25519', publicKey: new Uint8Array(32).fill(1) }], service: [ { @@ -253,15 +286,15 @@ describe('light DID', () => { serviceEndpoint: ['http://example.com'], }, ], - }).uri + }).id await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${verificationMethod.controller}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethod: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) @@ -274,122 +307,157 @@ describe('full DID', () => { beforeAll(() => { keypair = Crypto.makeKeypairFromSeed() did = { - uri: `did:kilt:${keypair.address}`, - authentication: [ + id: `did:kilt:${keypair.address}`, + authentication: ['#0x12345'], + verificationMethod: [ { + controller: `did:kilt:${keypair.address}`, id: '#0x12345', - type: 'sr25519', - publicKey: keypair.publicKey, + publicKeyMultibase: keypairToMultibaseKey(keypair), + type: 'Multikey', }, ], } - sign = async ({ data }) => ({ + sign = async ({ data, did: signingDid }) => ({ signature: keypair.sign(data), - keyUri: `${did.uri}#0x12345`, - keyType: 'sr25519', + verificationMethod: { + id: '#0x12345', + controller: signingDid, + type: 'Multikey', + publicKeyMultibase: keypairToMultibaseKey(keypair), + }, }) }) beforeEach(() => { jest - .mocked(resolveKey) + .mocked(dereference) .mockReset() - .mockImplementation(async (didUri) => - didUri.includes(keypair.address) - ? Did.keyToResolvedKey(did.authentication[0], did.uri) - : Promise.reject() + .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' }, + } + } ) }) it('verifies did signature over string', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('verifies did signature over bytes', async () => { const SIGNED_BYTES = Uint8Array.from([1, 2, 3, 4, 5]) - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: SIGNED_BYTES, - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_BYTES, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('does not verify if deactivated', async () => { - jest.mocked(resolveKey).mockRejectedValue(new Error('Deactivated')) + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: { deactivated: true }, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: { id: did.id }, + }) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) it('does not verify if not on chain', async () => { - jest.mocked(resolveKey).mockRejectedValue(new Error('Not on chain')) + jest.mocked(dereference).mockResolvedValue({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, - expectedVerificationMethod: 'authentication', + signerUrl: `${did.id}${verificationMethod.id}`, + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow() }) it('accepts signature of full did for light did if enabled', async () => { const SIGNED_STRING = 'signed string' - const { signature, keyUri } = await sign({ + const { signature, verificationMethod } = await sign({ data: Crypto.coToUInt8(SIGNED_STRING), - did: did.uri, - keyRelationship: 'authentication', + did: did.id, + verificationRelationship: 'authentication', }) - const expectedSigner = Did.createLightDidDocument({ - authentication: did.authentication as [NewLightDidVerificationKey], - }).uri + const authKey = did.verificationMethod?.find( + (vm) => vm.id === did.authentication?.[0] + ) + const expectedSignerAuthKey = multibaseKeyToDidKey( + authKey!.publicKeyMultibase + ) + const expectedSigner = createLightDidDocument({ + authentication: [ + { + publicKey: expectedSignerAuthKey.publicKey, + type: expectedSignerAuthKey.keyType, + }, + ] as [NewLightDidVerificationKey], + }).id await expect( verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, - expectedVerificationMethod: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) @@ -397,17 +465,17 @@ describe('full DID', () => { verifyDidSignature({ message: SIGNED_STRING, signature, - keyUri, + signerUrl: `${did.id}${verificationMethod.id}`, expectedSigner, allowUpgraded: true, - expectedVerificationMethod: 'authentication', + expectedVerificationRelationship: 'authentication', }) ).resolves.not.toThrow() }) it('typeguard accepts legal signature objects', () => { const signature: DidSignature = { - keyUri: `${did.uri}${did.authentication[0].id}`, + keyUri: `${did.id}${did.authentication![0]}`, signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(true) @@ -420,7 +488,7 @@ describe('type guard', () => { keypair = Crypto.makeKeypairFromSeed() }) - it('rejects malformed key uri', () => { + it('rejects malformed signer URL', () => { let signature: DidSignature = { // @ts-expect-error keyUri: `did:kilt:${keypair.address}?mykey`, @@ -455,7 +523,7 @@ describe('type guard', () => { it('rejects unexpected signature type', () => { const signature: DidSignature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, + keyUri: `did:kilt:${keypair.address}#mykey` as DidUrl, signature: '', } expect(isDidSignature(signature)).toBe(false) @@ -468,7 +536,7 @@ describe('type guard', () => { it('rejects incomplete objects', () => { let signature: DidSignature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, + keyUri: `did:kilt:${keypair.address}#mykey` as DidUrl, // @ts-expect-error signature: undefined, } @@ -484,9 +552,9 @@ describe('type guard', () => { signature: randomAsHex(32), } expect(isDidSignature(signature)).toBe(false) - // @ts-expect-error signature = { - keyUri: `did:kilt:${keypair.address}#mykey` as DidResourceUri, + // @ts-expect-error + keyUri: `did:kilt:${keypair.address}#mykey`, } 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 07a853d79..f7d5a94c2 100644 --- a/packages/did/src/Did.signature.ts +++ b/packages/did/src/Did.signature.ts @@ -7,77 +7,79 @@ import { isHex } from '@polkadot/util' -import { - DidResolveKey, - DidResourceUri, +import type { + DereferenceDidUrl, + DidDocument, DidSignature, - DidUri, + Did, + DidUrl, + SignatureVerificationRelationship, SignResponseData, - VerificationKeyRelationship, } from '@kiltprotocol/types' + import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { resolveKey } from './DidResolver/index.js' -import { parse, validateUri } from './Did.utils.js' +import { multibaseKeyToDidKey, parse, validateDid } from './Did.utils.js' +import { dereference } from './DidResolver/DidResolver.js' export type DidSignatureVerificationInput = { message: string | Uint8Array signature: Uint8Array - keyUri: DidResourceUri - expectedSigner?: DidUri + signerUrl: DidUrl + expectedSigner?: Did allowUpgraded?: boolean - expectedVerificationMethod?: VerificationKeyRelationship - didResolveKey?: DidResolveKey + expectedVerificationRelationship?: SignatureVerificationRelationship + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } // Used solely for retro-compatibility with previously-generated DID signatures. // It is reasonable to think that it will be removed at some point in the future. -type OldDidSignature = Pick & { - keyId: DidSignature['keyUri'] +type OldDidSignatureV1 = { + signature: string + keyId: DidUrl } -/** - * Checks whether the input is a valid DidSignature object, consisting of a signature as hex and the uri of the signing key. - * Does not cryptographically verify the signature itself! - * - * @param input Arbitrary input. - */ function verifyDidSignatureDataStructure( - input: DidSignature | OldDidSignature + input: DidSignature | OldDidSignatureV1 ): void { - const keyUri = 'keyUri' in input ? input.keyUri : input.keyId + const verificationMethodUrl = (() => { + if ('keyId' in input) { + return input.keyId + } + return input.keyUri + })() if (!isHex(input.signature)) { throw new SDKErrors.SignatureMalformedError( `Expected signature as a hex string, got ${input.signature}` ) } - validateUri(keyUri, 'ResourceUri') + validateDid(verificationMethodUrl, 'DidUrl') } /** - * Verify a DID signature given the key URI of the signature. + * Verify a DID signature given the signer's DID URL (i.e., DID + verification method ID). * A signature verification returns false if a migrated and then deleted DID is used. * * @param input Object wrapping all input. * @param input.message The message that was signed. * @param input.signature Signature bytes. - * @param input.keyUri DID URI of the key used for signing. - * @param input.expectedSigner If given, verification fails if the controller of the signing key is not the expectedSigner. + * @param input.signerUrl DID URL of the verification method used for signing. + * @param input.expectedSigner If given, verification fails if the controller of the signing verification method is not the expectedSigner. * @param input.allowUpgraded If `expectedSigner` is a light DID, setting this flag to `true` will accept signatures by the corresponding full DID. - * @param input.expectedVerificationMethod Which relationship to the signer DID the key must have. - * @param input.didResolveKey Allows specifying a custom DID key resolve. Defaults to the built-in [[resolveKey]]. + * @param input.expectedVerificationRelationship Which relationship to the signer DID the verification method must have. + * @param input.dereferenceDidUrl Allows specifying a custom DID dereferenced. Defaults to the built-in [[dereference]]. */ export async function verifyDidSignature({ message, signature, - keyUri, + signerUrl, expectedSigner, allowUpgraded = false, - expectedVerificationMethod, - didResolveKey = resolveKey, + expectedVerificationRelationship, + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: DidSignatureVerificationInput): Promise { - // checks if key uri points to the right did; alternatively we could check the key's controller - const signer = parse(keyUri) + // checks if signer URL points to the right did; alternatively we could check the verification method's controller + const signer = parse(signerUrl) if (expectedSigner && expectedSigner !== signer.did) { // check for allowable exceptions const expected = parse(expectedSigner) @@ -86,7 +88,7 @@ export async function verifyDidSignature({ expected.address === signer.address && expected.version === signer.version // EITHER: signer is a full did and we allow signatures by corresponding full did const allowedUpgrade = allowUpgraded && signer.type === 'full' - // OR: both are light dids and their auth key type matches + // OR: both are light dids and their auth verification method key type matches const keyTypeMatch = signer.type === 'light' && expected.type === 'light' && @@ -95,14 +97,56 @@ 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 { publicKey } = await didResolveKey(keyUri, expectedVerificationMethod) + 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 } = multibaseKeyToDidKey( + verificationMethod.publicKeyMultibase + ) Crypto.verify(message, signature, publicKey) } /** - * Type guard assuring that the input is a valid DidSignature object, consisting of a signature as hex and the uri of the signing key. + * Type guard assuring that the input is a valid DidSignature object, consisting of a signature as hex and the DID URL of the signer's verification method. * Does not cryptographically verify the signature itself! * * @param input Arbitrary input. @@ -110,7 +154,7 @@ export async function verifyDidSignature({ */ export function isDidSignature( input: unknown -): input is DidSignature | OldDidSignature { +): input is DidSignature | OldDidSignatureV1 { try { verifyDidSignatureDataStructure(input as DidSignature) return true @@ -124,14 +168,17 @@ export function isDidSignature( * * @param input Signature data returned from the [[SignCallback]]. * @param input.signature Signature bytes. - * @param input.keyUri DID URI of the key used for signing. + * @param input.verificationMethod The verification method used to generate the signature. * @returns A [[DidSignature]] object where signature is hex-encoded. */ export function signatureToJson({ signature, - keyUri, + verificationMethod, }: SignResponseData): DidSignature { - return { signature: Crypto.u8aToHex(signature), keyUri } + return { + signature: Crypto.u8aToHex(signature), + keyUri: `${verificationMethod.controller}${verificationMethod.id}`, + } } /** @@ -142,9 +189,16 @@ export function signatureToJson({ * @returns The deserialized DidSignature where the signature is represented as a Uint8Array. */ export function signatureFromJson( - input: DidSignature | OldDidSignature -): Pick { - const keyUri = 'keyUri' in input ? input.keyUri : input.keyId + input: DidSignature | OldDidSignatureV1 +): Pick & { + keyUri: DidUrl +} { + const keyUri = (() => { + if ('keyId' in input) { + return input.keyId + } + return input.keyUri + })() 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 2cc1eaf63..bf44a0d40 100644 --- a/packages/did/src/Did.utils.ts +++ b/packages/did/src/Did.utils.ts @@ -5,16 +5,20 @@ * 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 { - DidResourceUri, - DidUri, - DidVerificationKey, +import type { + Did, + DidUrl, + KeyringPair, 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 @@ -22,7 +26,7 @@ const LIGHT_DID_LATEST_VERSION = 1 // The latest version for KILT full DIDs. const FULL_DID_LATEST_VERSION = 1 -// NOTICE: The following regex patterns must be kept in sync with DidUri type in @kiltprotocol/types +// NOTICE: The following regex patterns must be kept in sync with `Did` type in @kiltprotocol/types // Matches the following full DIDs // - did:kilt: @@ -39,40 +43,60 @@ const LIGHT_KILT_DID_REGEX = /^did:kilt:light:(?[0-9]{2})(?
4[1-9a-km-zA-HJ-NP-Z]{47,48})(:(?.+?))?(?#[^#\n]+)?$/ type IDidParsingResult = { - did: DidUri + did: Did version: number type: 'light' | 'full' address: KiltAddress + 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 uri and returns the information contained within in a structured form. + * Parses a KILT DID or a DID URL and returns the information contained within in a structured form. * - * @param didUri A KILT DID uri as a string. - * @returns Object containing information extracted from the DID uri. + * @param did A KILT DID or a DID URL as a string. + * @returns Object containing information extracted from the input string. */ -export function parse(didUri: DidUri | DidResourceUri): IDidParsingResult { - let matches = FULL_KILT_DID_REGEX.exec(didUri)?.groups +export function parse(did: Did | DidUrl): IDidParsingResult { + // Then we check if it conforms to either a full or a light DID. + let matches = FULL_KILT_DID_REGEX.exec(did)?.groups if (matches) { const { version: versionString, fragment } = matches const address = matches.address as KiltAddress const version = versionString ? parseInt(versionString, 10) : FULL_DID_LATEST_VERSION + const queryParameters = exportQueryParamsFromDidUrl(did as DidUrl) return { - did: didUri.replace(fragment || '', '') as DidUri, + did: did.replace(fragment || '', '') as Did, 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(didUri)?.groups + matches = LIGHT_KILT_DID_REGEX.exec(did)?.groups if (matches) { const { authKeyType, @@ -84,57 +108,173 @@ export function parse(didUri: DidUri | DidResourceUri): IDidParsingResult { const version = versionString ? parseInt(versionString, 10) : LIGHT_DID_LATEST_VERSION + const queryParameters = exportQueryParamsFromDidUrl(did as DidUrl) return { - did: didUri.replace(fragment || '', '') as DidUri, + did: did.replace(fragment || '', '') as Did, version, type: 'light', address, + queryParameters, fragment: fragment === '#' ? undefined : (fragment as UriFragment), encodedDetails, authKeyTypeEncoding: authKeyType, } } - throw new SDKErrors.InvalidDidFormatError(didUri) + 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, } /** - * 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. + * Decode a Multikey representation of a verification method into its fundamental components: the public key and the key type. * - * @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. + * @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 isSameSubject(didA: DidUri, didB: DidUri): boolean { - return parse(didA).address === parse(didB).address +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, + } } -export type EncodedVerificationKey = - | { sr25519: Uint8Array } - | { ed25519: Uint8Array } - | { ecdsa: Uint8Array } - -export type EncodedEncryptionKey = { x25519: Uint8Array } +/** + * 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}` +} -export type EncodedKey = EncodedVerificationKey | EncodedEncryptionKey +/** + * 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}`, + } +} -export type EncodedSignature = EncodedVerificationKey +/** + * 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. + * @returns Whether didA and didB refer to the same DID subject. + */ +export function isSameSubject(didA: Did, didB: Did): boolean { + return parse(didA).address === parse(didB).address +} /** - * Checks that a string (or other input) is a valid KILT DID uri with or without a URI fragment. + * Checks that a string (or other input) is a valid KILT DID with or without a trailing fragment. * Throws otherwise. * * @param input Arbitrary input. - * @param expectType `ResourceUri` if the URI is expected to have a fragment (following '#'), `Did` if it is expected not to have one. Default allows both. + * @param expectType `Did` if the the input is expected to have a fragment (following '#'), `DidUrl` if it is expected not to have one. Default allows both. */ -export function validateUri( +export function validateDid( input: unknown, - expectType?: 'Did' | 'ResourceUri' + expectType?: 'Did' | 'DidUrl' ): void { if (typeof input !== 'string') { throw new TypeError(`DID string expected, got ${typeof input}`) } - const { address, fragment } = parse(input as DidUri) + const { address, fragment } = parse(input as DidUrl) if ( fragment && @@ -143,13 +283,13 @@ export function validateUri( (typeof expectType === 'boolean' && expectType === false)) ) { throw new SDKErrors.DidError( - 'Expected a Kilt DidUri but got a DidResourceUri (containing a #fragment)' + 'Expected a Kilt Did but got a DidUrl (containing a #fragment)' ) } - if (!fragment && expectType === 'ResourceUri') { + if (!fragment && expectType === 'DidUrl') { throw new SDKErrors.DidError( - 'Expected a Kilt DidResourceUri (containing a #fragment) but got a DidUri' + 'Expected a Kilt DidUrl (containing a #fragment) but got a Did' ) } @@ -157,17 +297,16 @@ export function validateUri( } /** - * Internal: derive the address part of the DID when it is created from authentication key. + * Internal: derive the address part of the DID when it is created from the provided authentication verification method. * - * @param input The authentication key. - * @param input.publicKey The public key. - * @param input.type The type of the key. + * @param input The authentication verification method. + * @param input.publicKeyMultibase The `publicKeyMultibase` value of the verification method. * @returns The expected address of the DID. */ -export function getAddressByKey({ - publicKey, - type, -}: Pick): KiltAddress { +export function getAddressFromVerificationMethod({ + publicKeyMultibase, +}: Pick): KiltAddress { + const { keyType: type, publicKey } = multibaseKeyToDidKey(publicKeyMultibase) if (type === 'ed25519' || type === 'sr25519') { return encodeAddress(publicKey, ss58Format) } @@ -179,32 +318,32 @@ export function getAddressByKey({ } /** - * Builds the URI a light DID will have after it’s stored on the blockchain. + * Builds the full DID a light DID will have after it’s stored on the blockchain. * - * @param didOrAddress The URI of the light DID. Internally it’s used with the DID "address" as well. - * @param version The version of the DID URI to use. - * @returns The expected full DID URI. + * @param didOrAddress The light DID. Internally it’s used with the DID "address" as well. + * @param version The version of the DID to use. + * @returns The expected full DID. */ -export function getFullDidUri( - didOrAddress: DidUri | KiltAddress, +export function getFullDid( + didOrAddress: Did | KiltAddress, version = FULL_DID_LATEST_VERSION -): DidUri { +): Did { const address = DataUtils.isKiltAddress(didOrAddress) ? didOrAddress - : parse(didOrAddress as DidUri).address + : parse(didOrAddress as Did).address const versionString = version === 1 ? '' : `v${version}` - return `did:kilt:${versionString}${address}` as DidUri + return `did:kilt:${versionString}${address}` as Did } /** - * Builds the URI of a full DID if it is created with the authentication key provided. + * Builds the of a full DID if it is created with the authentication verification method derived from the provided public key. * - * @param key The key that will be used as DID authentication key. - * @returns The expected full DID URI. + * @param verificationMethod The DID verification method. + * @returns The expected full DID . */ -export function getFullDidUriFromKey( - key: Pick -): DidUri { - const address = getAddressByKey(key) - return getFullDidUri(address) +export function getFullDidFromVerificationMethod( + verificationMethod: Pick +): Did { + const address = getAddressFromVerificationMethod(verificationMethod) + return getFullDid(address) } diff --git a/packages/did/src/DidDetails/DidDetails.spec.ts b/packages/did/src/DidDetails/DidDetails.spec.ts deleted file mode 100644 index 26302a1f6..000000000 --- a/packages/did/src/DidDetails/DidDetails.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { DidDocument, DidKey, DidServiceEndpoint } from '@kiltprotocol/types' - -import { getService, getKey, getKeys } from './DidDetails' - -const minimalDid: DidDocument = { - uri: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - authentication: [ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - ], -} - -const maximalDid: DidDocument = { - uri: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - authentication: [ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - ], - assertionMethod: [ - { - id: '#assertionMethod', - publicKey: new Uint8Array(0), - type: 'ed25519', - }, - ], - capabilityDelegation: [ - { - id: '#capabilityDelegation', - publicKey: new Uint8Array(0), - type: 'ecdsa', - }, - ], - keyAgreement: [ - { - id: '#keyAgreement', - publicKey: new Uint8Array(0), - type: 'x25519', - }, - ], - service: [ - { - id: '#service', - type: ['foo'], - serviceEndpoint: ['https://example.com/'], - }, - ], -} - -describe('DidDetais', () => { - describe('getKeys', () => { - it('should get keys of a minimal DID', async () => { - expect(getKeys(minimalDid)).toEqual([ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - ]) - }) - it('should get keys of a maximal DID', async () => { - expect(getKeys(maximalDid)).toEqual([ - { - id: '#authentication', - publicKey: new Uint8Array(0), - type: 'sr25519', - }, - { - id: '#assertionMethod', - publicKey: new Uint8Array(0), - type: 'ed25519', - }, - { - id: '#capabilityDelegation', - publicKey: new Uint8Array(0), - type: 'ecdsa', - }, - { - id: '#keyAgreement', - publicKey: new Uint8Array(0), - type: 'x25519', - }, - ]) - }) - }) - describe('getKey', () => { - it('should get key by ID', async () => { - expect(getKey(maximalDid, '#capabilityDelegation')).toEqual({ - id: '#capabilityDelegation', - publicKey: new Uint8Array(0), - type: 'ecdsa', - }) - }) - it('should return undefined when key not found', async () => { - expect(getKey(minimalDid, '#capabilityDelegation')).toEqual(undefined) - }) - }) - describe('getService', () => { - it('should get endpoint by ID', async () => { - expect(getService(maximalDid, '#service')).toEqual({ - id: '#service', - serviceEndpoint: ['https://example.com/'], - type: ['foo'], - }) - }) - it('should return undefined when key not found', async () => { - expect(getService(minimalDid, '#service')).toEqual(undefined) - }) - }) -}) diff --git a/packages/did/src/DidDetails/DidDetails.ts b/packages/did/src/DidDetails/DidDetails.ts index 0af556564..7b985b993 100644 --- a/packages/did/src/DidDetails/DidDetails.ts +++ b/packages/did/src/DidDetails/DidDetails.ts @@ -7,51 +7,162 @@ import type { DidDocument, - DidKey, - DidServiceEndpoint, + Service, + UriFragment, + VerificationMethod, + VerificationRelationship, } 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 + /** - * Gets all public keys associated with this DID. + * Type guard checking whether the provided input string represents one of the supported signing or encryption verification types. * - * @param did The DID data. - * @returns Array of public keys. + * @param input The input string. + * @returns Whether the input string is an instance of [[DidSigningMethodType]]. */ -export function getKeys( - did: Partial & Pick -): DidKey[] { - return [ - ...did.authentication, - ...(did.assertionMethod || []), - ...(did.capabilityDelegation || []), - ...(did.keyAgreement || []), - ] +export function isValidDidVerificationType( + input: string +): input is DidSigningMethodType { + return ( + isValidVerificationMethodType(input) || isValidEncryptionMethodType(input) + ) } +export type NewVerificationMethod = Omit +export type NewService = Service + /** - * Returns a key with a given id, if associated with this DID. + * Type guard checking whether the provided input represents one of the supported verification relationships. * - * @param did The DID data. - * @param id Key id (not the full key uri). - * @returns The respective public key data or undefined. + * @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. */ -export function getKey( - did: Partial & Pick, - id: DidKey['id'] -): DidKey | undefined { - return getKeys(did).find((key) => key.id === id) +export type NewDidVerificationKey = BaseNewDidKey & { + type: DidSigningMethodType } /** - * Returns a service endpoint with a given id, if associated with this DID. + * 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. * - * @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) + * @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) } diff --git a/packages/did/src/DidDetails/FullDidDetails.spec.ts b/packages/did/src/DidDetails/FullDidDetails.spec.ts index 3f8004931..10e39ebdc 100644 --- a/packages/did/src/DidDetails/FullDidDetails.spec.ts +++ b/packages/did/src/DidDetails/FullDidDetails.spec.ts @@ -22,7 +22,10 @@ import { makeSigningKeyTool, } from '../../../../tests/testUtils' import { generateDidAuthenticatedTx } from '../Did.chain.js' -import * as Did from './index.js' +import { + authorizeBatch, + getVerificationRelationshipForTx, +} from './FullDidDetails.js' const augmentedApi = ApiMocks.createAugmentedApi() const mockedApi: any = ApiMocks.getMockedApi() @@ -56,8 +59,8 @@ describe('When creating an instance from the chain', () => { it('fails if the extrinsic does not require a DID', async () => { const extrinsic = augmentedApi.tx.indices.claim(1) await expect(async () => - Did.authorizeBatch({ - did: fullDid.uri, + authorizeBatch({ + did: fullDid.id, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [extrinsic, extrinsic], sign, @@ -74,8 +77,8 @@ describe('When creating an instance from the chain', () => { ]) const batchFunction = jest.fn() as unknown as typeof mockedApi.tx.utility.batchAll - await Did.authorizeBatch({ - did: fullDid.uri, + await authorizeBatch({ + did: fullDid.id, batchFunction, extrinsics: [extrinsic, extrinsic], sign, @@ -111,8 +114,8 @@ describe('When creating an instance from the chain', () => { ctype3Extrinsic, ctype4Extrinsic, ] - await Did.authorizeBatch({ - did: fullDid.uri, + await authorizeBatch({ + did: fullDid.id, batchFunction, extrinsics, nonce: new BN(0), @@ -139,8 +142,8 @@ describe('When creating an instance from the chain', () => { describe('.build()', () => { it('throws if batch is empty', async () => { await expect(async () => - Did.authorizeBatch({ - did: fullDid.uri, + authorizeBatch({ + did: fullDid.id, batchFunction: augmentedApi.tx.utility.batchAll, extrinsics: [], sign, @@ -163,14 +166,14 @@ describe('When creating an instance from the chain', () => { const mockApi = ApiMocks.createAugmentedApi() describe('When creating an instance from the chain', () => { - it('Should return correct KeyRelationship for single valid call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + it('Should return correct VerificationRelationship for single valid call', () => { + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.attestation.add(new Uint8Array(32), new Uint8Array(32), null) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return correct KeyRelationship for batched call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + it('Should return correct VerificationRelationship for batched call', () => { + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.batch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -184,10 +187,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return correct KeyRelationship for batchAll call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + it('Should return correct VerificationRelationship for batchAll call', () => { + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.batchAll([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -201,10 +204,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return correct KeyRelationship for forceBatch call', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + it('Should return correct VerificationRelationship for forceBatch call', () => { + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -218,10 +221,10 @@ describe('When creating an instance from the chain', () => { ), ]) ) - expect(keyRelationship).toBe('assertionMethod') + expect(verificationRelationship).toBe('assertionMethod') }) - it('Should return undefined for batch with mixed KeyRelationship calls', () => { - const keyRelationship = Did.getKeyRelationshipForTx( + it('Should return undefined for batch with mixed VerificationRelationship calls', () => { + const verificationRelationship = getVerificationRelationshipForTx( mockApi.tx.utility.forceBatch([ mockApi.tx.attestation.add( new Uint8Array(32), @@ -231,6 +234,6 @@ describe('When creating an instance from the chain', () => { mockApi.tx.web3Names.claim('awesomename'), ]) ) - expect(keyRelationship).toBeUndefined() + expect(verificationRelationship).toBeUndefined() }) }) diff --git a/packages/did/src/DidDetails/FullDidDetails.ts b/packages/did/src/DidDetails/FullDidDetails.ts index b473a67c0..5303e6fb0 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 { - DidUri, + Did, KiltAddress, + SignatureVerificationRelationship, SignExtrinsicCallback, SubmittableExtrinsic, - VerificationKeyRelationship, } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' @@ -30,7 +30,10 @@ import { parse } from '../Did.utils.js' // Must be in sync with what's implemented in impl did::DeriveDidCallAuthorizationVerificationKeyRelationship for Call // in https://github.com/KILTprotocol/mashnet-node/blob/develop/runtimes/spiritnet/src/lib.rs // TODO: Should have an RPC or something similar to avoid inconsistencies in the future. -const methodMapping: Record = { +const methodMapping: Record< + string, + SignatureVerificationRelationship | undefined +> = { attestation: 'assertionMethod', ctype: 'assertionMethod', delegation: 'capabilityDelegation', @@ -43,20 +46,20 @@ const methodMapping: Record = { web3Names: 'authentication', } -function getKeyRelationshipForMethod( +function getVerificationRelationshipForRuntimeCall( call: Extrinsic['method'] -): VerificationKeyRelationship | undefined { +): SignatureVerificationRelationship | undefined { const { section, method } = call - // get the VerificationKeyRelationship of a batched call + // get the VerificationRelationship of a batched call if ( section === 'utility' && ['batch', 'batchAll', 'forceBatch'].includes(method) && call.args[0].toRawType() === 'Vec' ) { - // map all calls to their VerificationKeyRelationship and deduplicate the items + // map all calls to their VerificationRelationship and deduplicate the items return (call.args[0] as unknown as Array) - .map(getKeyRelationshipForMethod) + .map(getVerificationRelationshipForRuntimeCall) .reduce((prev, value) => (prev === value ? prev : undefined)) } @@ -69,15 +72,15 @@ function getKeyRelationshipForMethod( } /** - * Detect the key relationship for a key which should be used to DID-authorize the provided extrinsic. + * Detect the relationship for a verification method which should be used to DID-authorize the provided extrinsic. * * @param extrinsic The unsigned extrinsic to inspect. - * @returns The key relationship. + * @returns The verification relationship. */ -export function getKeyRelationshipForTx( +export function getVerificationRelationshipForTx( extrinsic: Extrinsic -): VerificationKeyRelationship | undefined { - return getKeyRelationshipForMethod(extrinsic.method) +): SignatureVerificationRelationship | undefined { + return getVerificationRelationshipForRuntimeCall(extrinsic.method) } // Max nonce value is (2^64) - 1 @@ -98,7 +101,7 @@ function increaseNonce(currentNonce: BN, increment = 1): BN { * @param did The DID data. * @returns The next valid nonce, i.e., the nonce currently stored on the blockchain + 1, wrapping around the max value when reached. */ -async function getNextNonce(did: DidUri): Promise { +async function getNextNonce(did: Did): Promise { const api = ConfigService.get('api') const queried = await api.query.did.did(toChain(did)) const currentNonce = queried.isSome @@ -108,7 +111,7 @@ async function getNextNonce(did: DidUri): Promise { } /** - * Signs and returns the provided unsigned extrinsic with the right DID key, if present. Otherwise, it will throw an error. + * Signs and returns the provided unsigned extrinsic with the right DID verification method, if present. Otherwise, it will throw an error. * * @param did The DID data. * @param extrinsic The unsigned extrinsic to sign. @@ -119,7 +122,7 @@ async function getNextNonce(did: DidUri): Promise { * @returns The DID-signed submittable extrinsic. */ export async function authorizeTx( - did: DidUri, + did: Did, extrinsic: Extrinsic, sign: SignExtrinsicCallback, submitterAccount: KiltAddress, @@ -135,14 +138,16 @@ export async function authorizeTx( ) } - const keyRelationship = getKeyRelationshipForTx(extrinsic) - if (keyRelationship === undefined) { - throw new SDKErrors.SDKError('No key relationship found for extrinsic') + const verificationRelationship = getVerificationRelationshipForTx(extrinsic) + if (verificationRelationship === undefined) { + throw new SDKErrors.SDKError( + 'No verification relationship found for extrinsic' + ) } return generateDidAuthenticatedTx({ did, - keyRelationship, + verificationRelationship, sign, call: extrinsic, txCounter: txCounter || (await getNextNonce(did)), @@ -152,38 +157,39 @@ export async function authorizeTx( type GroupedExtrinsics = Array<{ extrinsics: Extrinsic[] - keyRelationship: VerificationKeyRelationship + verificationRelationship: SignatureVerificationRelationship }> -function groupExtrinsicsByKeyRelationship( +function groupExtrinsicsByVerificationRelationship( extrinsics: Extrinsic[] ): GroupedExtrinsics { const [first, ...rest] = extrinsics.map((extrinsic) => { - const keyRelationship = getKeyRelationshipForTx(extrinsic) - if (!keyRelationship) { + const verificationRelationship = getVerificationRelationshipForTx(extrinsic) + if (!verificationRelationship) { throw new SDKErrors.DidBatchError( 'Can only batch extrinsics that require a DID signature' ) } - return { extrinsic, keyRelationship } + return { extrinsic, verificationRelationship } }) const groups: GroupedExtrinsics = [ { extrinsics: [first.extrinsic], - keyRelationship: first.keyRelationship, + verificationRelationship: first.verificationRelationship, }, ] - rest.forEach(({ extrinsic, keyRelationship }) => { + rest.forEach(({ extrinsic, verificationRelationship }) => { const currentGroup = groups[groups.length - 1] - const useCurrentGroup = keyRelationship === currentGroup.keyRelationship + const useCurrentGroup = + verificationRelationship === currentGroup.verificationRelationship if (useCurrentGroup) { currentGroup.extrinsics.push(extrinsic) } else { groups.push({ extrinsics: [extrinsic], - keyRelationship, + verificationRelationship, }) } }) @@ -192,7 +198,7 @@ function groupExtrinsicsByKeyRelationship( } /** - * Authorizes/signs a list of extrinsics grouping them in batches by required key type. + * Authorizes/signs a list of extrinsics grouping them in batches by required verification relationship. * * @param input The object with named parameters. * @param input.batchFunction The batch function to use, for example `api.tx.utility.batchAll`. @@ -212,7 +218,7 @@ export async function authorizeBatch({ submitter, }: { batchFunction: SubmittableExtrinsicFunction<'promise'> - did: DidUri + did: Did extrinsics: Extrinsic[] nonce?: BN sign: SignExtrinsicCallback @@ -236,7 +242,7 @@ export async function authorizeBatch({ }) } - const groups = groupExtrinsicsByKeyRelationship(extrinsics) + const groups = groupExtrinsicsByVerificationRelationship(extrinsics) const firstNonce = nonce || (await getNextNonce(did)) const promises = groups.map(async (group, batchIndex) => { @@ -244,11 +250,11 @@ export async function authorizeBatch({ const call = list.length === 1 ? list[0] : batchFunction(list) const txCounter = increaseNonce(firstNonce, batchIndex) - const { keyRelationship } = group + const { verificationRelationship } = group return generateDidAuthenticatedTx({ did, - keyRelationship, + verificationRelationship, sign, call, txCounter, diff --git a/packages/did/src/DidDetails/LightDidDetails.spec.ts b/packages/did/src/DidDetails/LightDidDetails.spec.ts index 2a7a23a92..a4e608993 100644 --- a/packages/did/src/DidDetails/LightDidDetails.spec.ts +++ b/packages/did/src/DidDetails/LightDidDetails.spec.ts @@ -5,10 +5,18 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { DidDocument, DidServiceEndpoint, DidUri } from '@kiltprotocol/types' +import type { DidDocument, Did, DidUrl } from '@kiltprotocol/types' + import { Crypto } from '@kiltprotocol/utils' -import * as Did from '../index.js' +import type { NewService } from './DidDetails.js' +import type { CreateDocumentInput } from './LightDidDetails.js' + +import { keypairToMultibaseKey, parse } from '../Did.utils.js' +import { + createLightDidDocument, + parseDocumentFromLightDid, +} from './LightDidDetails.js' /* * Functions tested: @@ -21,12 +29,12 @@ import * as Did from '../index.js' */ describe('When creating an instance from the details', () => { - it('correctly assign the right sr25519 authentication key, x25519 encryption key, and service endpoints', () => { + it('correctly assign the right sr25519 authentication key, x25519 encryption key, and services', () => { const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') const encKey = Crypto.makeEncryptionKeypairFromSeed( new Uint8Array(32).fill(1) ) - const service: DidServiceEndpoint[] = [ + const service: NewService[] = [ { id: '#service-1', type: ['type-1'], @@ -39,26 +47,34 @@ describe('When creating an instance from the details', () => { }, ] - const lightDid = Did.createLightDidDocument({ + const lightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, }) expect(lightDid).toEqual({ - uri: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, - authentication: [ + id: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#authentication', - publicKey: authKey.publicKey, - type: 'sr25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'sr25519', + }), + type: 'Multikey', }, - ], - keyAgreement: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#encryption', - publicKey: encKey.publicKey, - type: 'x25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'Multikey', }, ], service: [ @@ -82,27 +98,35 @@ describe('When creating an instance from the details', () => { new Uint8Array(32).fill(1) ) - const lightDid = Did.createLightDidDocument({ + const lightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], }) - expect(Did.parse(lightDid.uri).address).toStrictEqual(authKey.address) + expect(parse(lightDid.id).address).toStrictEqual(authKey.address) - expect(lightDid).toEqual({ - uri: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, - authentication: [ + expect(lightDid).toEqual({ + id: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ { + controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, id: '#authentication', - publicKey: authKey.publicKey, - type: 'ed25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'ed25519', + }), + type: 'Multikey', }, - ], - keyAgreement: [ { + controller: `did:kilt:light:01${authKey.address}:z15dZSRuzEPTFnBErPxqJie4CmmQH1gYKSQYxmwW5Qhgz5Sr7EYJA3J65KoC5YbgF3NGoBsTY2v6zwj1uDnZzgXzLy8R72Fhjmp8ujY81y2AJc8uQ6s2pVbAMZ6bnvaZ3GVe8bMjY5MiKFySS27qRi`, id: '#encryption', - publicKey: encKey.publicKey, - type: 'x25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'Multikey', }, ], }) @@ -115,9 +139,7 @@ describe('When creating an instance from the details', () => { authentication: [authKey], } expect(() => - Did.createLightDidDocument( - invalidInput as unknown as Did.CreateDocumentInput - ) + createLightDidDocument(invalidInput as unknown as CreateDocumentInput) ).toThrowError() }) @@ -130,20 +152,18 @@ describe('When creating an instance from the details', () => { keyAgreement: [{ publicKey: encKey.publicKey, type: 'bls' }], } expect(() => - Did.createLightDidDocument( - invalidInput as unknown as Did.CreateDocumentInput - ) + createLightDidDocument(invalidInput as unknown as CreateDocumentInput) ).toThrowError() }) }) -describe('When creating an instance from a URI', () => { - it('correctly assign the right authentication key, encryption key, and service endpoints', () => { +describe('When creating an instance from a light DID', () => { + it('correctly assign the right authentication key, encryption key, and services', () => { const authKey = Crypto.makeKeypairFromSeed(undefined, 'sr25519') const encKey = Crypto.makeEncryptionKeypairFromSeed( new Uint8Array(32).fill(1) ) - const endpoints: DidServiceEndpoint[] = [ + const endpoints: NewService[] = [ { id: '#service-1', type: ['type-1'], @@ -156,30 +176,38 @@ describe('When creating an instance from a URI', () => { }, ] // We are sure this is correct because of the described case above - const expectedLightDid = Did.createLightDidDocument({ + const expectedLightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service: endpoints, }) - const { address } = Did.parse(expectedLightDid.uri) - const builtLightDid = Did.parseDocumentFromLightDid(expectedLightDid.uri) + const { address } = parse(expectedLightDid.id) + const builtLightDid = parseDocumentFromLightDid(expectedLightDid.id) expect(builtLightDid).toStrictEqual(expectedLightDid) expect(builtLightDid).toStrictEqual({ - uri: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7` as DidUri, - authentication: [ + id: `did:kilt:light:00${address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#authentication', - publicKey: authKey.publicKey, - type: 'sr25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: authKey.publicKey, + type: 'sr25519', + }), + type: 'Multikey', }, - ], - keyAgreement: [ { + controller: `did:kilt:light:00${authKey.address}:z17GNCdxLqMYTMC5pnnDrPZGxLEFcXvDamtGNXeNkfSaFf8cktX6erFJiQy8S3ugL981NNys7Rz8DJiaNPZi98v1oeFVL7PjUGNTz1g3jgZo4VgQri2SYHBifZFX9foHZH4DreZXFN66k5dPrvAtBpFXaiG2WZkkxsnxNWxYpqWPPcxvbTE6pJbXxWKjRUd7rog1h9vjA93QA9jMDxm6BSGJHACFgSPUU3UTLk2kjNwT2bjZVvihVFu1zibxwHjowb7N6UQfieJ7ny9HnaQy64qJvGqh4NNtpwkhwm5DTYUoAeAhjt3a6TWyxmBgbFdZF7`, id: '#encryption', - publicKey: encKey.publicKey, - type: 'x25519', + publicKeyMultibase: keypairToMultibaseKey({ + publicKey: encKey.publicKey, + type: 'x25519', + }), + type: 'Multikey', }, ], service: [ @@ -200,7 +228,7 @@ describe('When creating an instance from a URI', () => { it('fail if a fragment is present according to the options', () => { const authKey = Crypto.makeKeypairFromSeed() const encKey = Crypto.makeEncryptionKeypairFromSeed() - const service: DidServiceEndpoint[] = [ + const service: NewService[] = [ { id: '#service-1', type: ['type-1'], @@ -214,25 +242,25 @@ describe('When creating an instance from a URI', () => { ] // We are sure this is correct because of the described case above - const expectedLightDid = Did.createLightDidDocument({ + const expectedLightDid = createLightDidDocument({ authentication: [authKey], keyAgreement: [encKey], service, }) - const uriWithFragment: DidUri = `${expectedLightDid.uri}#authentication` + const didWithFragment: DidUrl = `${expectedLightDid.id}#authentication` - expect(() => Did.parseDocumentFromLightDid(uriWithFragment, true)).toThrow() + expect(() => parseDocumentFromLightDid(didWithFragment, true)).toThrow() expect(() => - Did.parseDocumentFromLightDid(uriWithFragment, false) + parseDocumentFromLightDid(didWithFragment, false) ).not.toThrow() }) - it('fail if the URI is not correct', () => { + it('fail if the DID is not correct', () => { const validKiltAddress = Crypto.makeKeypairFromSeed() - const incorrectURIs = [ + const incorrectDIDs = [ 'did:kilt:light:sdasdsadas', - // @ts-ignore not a valid DID uri + // @ts-ignore not a valid DID 'random-uri', 'did:kilt:light', 'did:kilt:light:', @@ -243,8 +271,8 @@ describe('When creating an instance from a URI', () => { // Random encoded details `did:kilt:light:00${validKiltAddress}:randomdetails`, ] - incorrectURIs.forEach((uri) => { - expect(() => Did.parseDocumentFromLightDid(uri as DidUri)).toThrow() + incorrectDIDs.forEach((did) => { + expect(() => parseDocumentFromLightDid(did as Did)).toThrow() }) }) }) diff --git a/packages/did/src/DidDetails/LightDidDetails.ts b/packages/did/src/DidDetails/LightDidDetails.ts index 30a752f6b..cc23bb98e 100644 --- a/packages/did/src/DidDetails/LightDidDetails.ts +++ b/packages/did/src/DidDetails/LightDidDetails.ts @@ -5,32 +5,53 @@ * found in the LICENSE file in the root directory of this source tree. */ +import type { DidDocument, 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, - NewLightDidVerificationKey, -} from '@kiltprotocol/types' -import { encryptionKeyTypes } from '@kiltprotocol/types' + NewDidVerificationKey, + NewService, + DidSigningMethodType, +} from './DidDetails.js' -import { SDKErrors, ss58Format, cbor } from '@kiltprotocol/utils' +import { + keypairToMultibaseKey, + didKeyToVerificationMethod, + getAddressFromVerificationMethod, + parse, +} from '../Did.utils.js' +import { fragmentIdToChain, validateNewService } from '../Did.chain.js' +import { + addKeypairAsVerificationMethod, + encryptionMethodTypes, +} from './DidDetails.js' -import { getAddressByKey, parse } from '../Did.utils.js' -import { resourceIdToChain, validateService } from '../Did.chain.js' +/** + * Currently, a light DID does not support the use of an ECDSA key as its authentication 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' const authenticationKeyId = '#authentication' const encryptionKeyId = '#encryption' -type LightDidEncoding = '00' | '01' - const verificationKeyTypeToLightDidEncoding: Record< LightDidSupportedVerificationKeyType, LightDidEncoding @@ -52,26 +73,26 @@ const lightDidEncodingToVerificationKeyType: Record< */ export type CreateDocumentInput = { /** - * The DID authentication key. This is mandatory and will be used as the first authentication key + * The key to be used as the DID authentication verification method. This is mandatory and will be used as the first authentication verification method * of the full DID upon migration. */ authentication: [NewLightDidVerificationKey] /** - * The optional DID encryption key. If present, it will be used as the first key agreement key + * The optional encryption key to be used as the DID key agreement verification method. If present, it will be used as the first key agreement verification method * of the full DID upon migration. */ keyAgreement?: [NewDidEncryptionKey] /** - * The set of service endpoints associated with this DID. Each service endpoint ID must be unique. + * The set of services associated with this DID. Each service ID must be unique. * The service ID must not contain the DID prefix when used to create a new DID. */ - service?: DidServiceEndpoint[] + service?: NewService[] } function validateCreateDocumentInput({ authentication, keyAgreement, - service: services, + service, }: CreateDocumentInput): void { // Check authentication key type const authenticationKeyTypeEncoding = @@ -80,10 +101,9 @@ function validateCreateDocumentInput({ if (!authenticationKeyTypeEncoding) { throw new SDKErrors.UnsupportedKeyError(authentication[0].type) } - if ( keyAgreement?.[0].type && - !encryptionKeyTypes.includes(keyAgreement[0].type) + !encryptionMethodTypes.includes(keyAgreement[0].type) ) { throw new SDKErrors.DidError( `Encryption key type "${keyAgreement[0].type}" is not supported` @@ -93,14 +113,14 @@ function validateCreateDocumentInput({ // Checks that for all service IDs have regular strings as their ID and not a full DID. // Plus, we forbid a service ID to be `authentication` or `encryption` as that would create confusion // when upgrading to a full DID. - services?.forEach((service) => { + service?.forEach((s) => { // A service ID cannot have a reserved ID that is used for key IDs. - if (service.id === '#authentication' || service.id === '#encryption') { + if (s.id === '#authentication' || s.id === '#encryption') { throw new SDKErrors.DidError( - `Cannot specify a service ID with the name "${service.id}" as it is a reserved keyword` + `Cannot specify a service ID with the name "${s.id}" as it is a reserved keyword` ) } - validateService(service) + validateNewService(s) }) } @@ -110,20 +130,20 @@ const SERVICES_MAP_KEY = 's' interface SerializableStructure { [KEY_AGREEMENT_MAP_KEY]?: NewDidEncryptionKey [SERVICES_MAP_KEY]?: Array< - Partial> & { + Partial> & { id: string - } & { types?: string[]; urls?: string[] } // This below was mistakenly not accounted for during the SDK refactor, meaning there are light DIDs that contain these keys in their service endpoints. + } & { 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. > } /** - * Serialize the optional encryption key and service endpoints of an off-chain DID using the CBOR serialization algorithm + * Serialize the optional key agreement verification method and services of a light DID using the CBOR serialization algorithm * and encoding the result in Base58 format with a multibase prefix. * * @param details The light DID details to encode. - * @param details.keyAgreement The DID encryption key. - * @param details.service The DID service endpoints. - * @returns The Base58-encoded and CBOR-serialized off-chain DID optional details. + * @param details.keyAgreement The DID key agreement verification method. + * @param details.service The DID services. + * @returns The Base58-encoded and CBOR-serialized light DID optional details. */ function serializeAdditionalLightDidDetails({ keyAgreement, @@ -136,7 +156,7 @@ function serializeAdditionalLightDidDetails({ } if (service && service.length > 0) { objectToSerialize[SERVICES_MAP_KEY] = service.map(({ id, ...rest }) => ({ - id: resourceIdToChain(id), + id: fragmentIdToChain(id), ...rest, })) } @@ -183,14 +203,14 @@ function deserializeAdditionalLightDidDetails( } /** - * Create [[DidDocument]] of a light DID using the provided keys and endpoints. - * Sets proper key IDs, builds light DID URI. - * Private keys are assumed to already live in another storage, as it contains reference only to public keys. + * Create a light [[DidDocument]] using the provided verification methods and services. + * Sets proper verification method IDs, builds light DID Document. + * Private keys are assumed to already live in another storage, as it contains reference only to public keys as verification methods. * * @param input The input. - * @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. + * @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. * * @returns The resulting [[DidDocument]]. */ @@ -211,52 +231,53 @@ export function createLightDidDocument({ // Validity is checked in validateCreateDocumentInput const authenticationKeyTypeEncoding = verificationKeyTypeToLightDidEncoding[authentication[0].type] - const address = getAddressByKey(authentication[0]) + const address = getAddressFromVerificationMethod({ + publicKeyMultibase: keypairToMultibaseKey(authentication[0]), + }) const encodedDetailsString = encodedDetails ? `:${encodedDetails}` : '' - const uri = - `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as DidUri + const did = + `did:kilt:light:${authenticationKeyTypeEncoding}${address}${encodedDetailsString}` as Did - const did: DidDocument = { - uri, - authentication: [ - { - id: authenticationKeyId, // Authentication key always has the #authentication ID. - type: authentication[0].type, + const didDocument: DidDocument = { + id: did, + authentication: [authenticationKeyId], + verificationMethod: [ + didKeyToVerificationMethod(did, authenticationKeyId, { + keyType: authentication[0].type, publicKey: authentication[0].publicKey, - }, + }), ], service, } if (keyAgreement !== undefined) { - did.keyAgreement = [ - { - id: encryptionKeyId, // Encryption key always has the #encryption ID. - type: keyAgreement[0].type, - publicKey: keyAgreement[0].publicKey, - }, - ] + const { publicKey, type } = keyAgreement[0] + addKeypairAsVerificationMethod( + didDocument, + { id: encryptionKeyId, publicKey, type }, + 'keyAgreement' + ) } - return did + return didDocument } /** - * Create [[DidDocument]] of a light DID by parsing the provided input URI. + * Create a light [[DidDocument]] by parsing the provided input DID. * Only use for DIDs you control, when you are certain they have not been upgraded to on-chain full DIDs. * For the DIDs you have received from external sources use [[resolve]] etc. * * Parsing is possible because of the self-describing and self-containing nature of light DIDs. - * Private keys are assumed to already live in another storage, as it contains reference only to public keys. + * Private keys are assumed to already live in another storage, as it contains reference only to public keys as verification methods. * - * @param uri The DID URI to parse. - * @param failIfFragmentPresent Whether to fail when parsing the URI in case a fragment is present or not, which is not relevant to the creation of the DID. It defaults to true. + * @param did The DID to parse. + * @param failIfFragmentPresent Whether to fail when parsing the DID in case a fragment is present or not, which is not relevant to the creation of the DID. It defaults to true. * * @returns The resulting [[DidDocument]]. */ export function parseDocumentFromLightDid( - uri: DidUri, + did: Did, failIfFragmentPresent = true ): DidDocument { const { @@ -266,16 +287,16 @@ export function parseDocumentFromLightDid( fragment, type, authKeyTypeEncoding, - } = parse(uri) + } = parse(did) if (type !== 'light') { throw new SDKErrors.DidError( - `Cannot build a light DID from the provided URI "${uri}" because it does not refer to a light DID` + `Cannot build a light DID Document from the provided DID "${did}" because it does not refer to a light DID` ) } if (fragment && failIfFragmentPresent) { throw new SDKErrors.DidError( - `Cannot build a light DID from the provided URI "${uri}" because it has a fragment` + `Cannot build a light DID Document from the provided DID "${did}" because it has a fragment` ) } const keyType = diff --git a/packages/did/src/DidDetails/index.ts b/packages/did/src/DidDetails/index.ts index 99c4484aa..daceeda2f 100644 --- a/packages/did/src/DidDetails/index.ts +++ b/packages/did/src/DidDetails/index.ts @@ -5,6 +5,20 @@ * found in the LICENSE file in the root directory of this source tree. */ -export * from './DidDetails.js' +// We don't export the `add*VerificationMethod` functions, they are meant to be used internally +export type { + BaseNewDidKey, + DidEncryptionMethodType, + DidSigningMethodType, + DidVerificationMethodType, + NewDidEncryptionKey, + NewDidVerificationKey, + NewService, + NewVerificationMethod, +} from './DidDetails.js' +export { + isValidDidVerificationType, + isValidEncryptionMethodType, +} from './DidDetails.js' export * from './LightDidDetails.js' export * from './FullDidDetails.js' diff --git a/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts b/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts deleted file mode 100644 index 4a957c40d..000000000 --- a/packages/did/src/DidDocumentExporter/DidDocumentExporter.spec.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { BN } from '@polkadot/util' - -import type { - DidServiceEndpoint, - NewDidVerificationKey, - DidDocument, - DidVerificationKey, - DidEncryptionKey, - UriFragment, - DidUri, -} from '@kiltprotocol/types' - -import { exportToDidDocument } from './DidDocumentExporter.js' -import * as Did from '../index.js' -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from '../index.js' - -const did: DidUri = 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' - -function generateAuthenticationKey(): DidVerificationKey { - return { - id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), - } -} - -function generateEncryptionKey(): DidEncryptionKey { - return { - id: '#enc', - type: 'x25519', - publicKey: new Uint8Array(32).fill(0), - includedAt: new BN(15), - } -} - -function generateAttestationKey(): DidVerificationKey { - return { - id: '#att', - type: 'sr25519', - publicKey: new Uint8Array(32).fill(0), - includedAt: new BN(20), - } -} - -function generateDelegationKey(): DidVerificationKey { - return { - id: '#del', - type: 'ecdsa', - publicKey: new Uint8Array(32).fill(0), - includedAt: new BN(25), - } -} - -function generateServiceEndpoint(serviceId: UriFragment): DidServiceEndpoint { - const fragment = Did.resourceIdToChain(serviceId) - return { - id: serviceId, - type: [`type-${fragment}`], - serviceEndpoint: [`x:url-${fragment}`], - } -} - -const fullDid: DidDocument = { - uri: did, - authentication: [generateAuthenticationKey()], - keyAgreement: [generateEncryptionKey()], - assertionMethod: [generateAttestationKey()], - capabilityDelegation: [generateDelegationKey()], - service: [generateServiceEndpoint('#id-1'), generateServiceEndpoint('#id-2')], -} - -describe('When exporting a DID Document from a full DID', () => { - it('exports the expected application/json W3C DID Document with an Ed25519 authentication key, one x25519 encryption key, an Sr25519 assertion key, an Ecdsa delegation key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(fullDid, 'application/json') - - expect(didDoc).toStrictEqual({ - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - verificationMethod: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Ed25519VerificationKey2018', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Sr25519VerificationKey2020', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'EcdsaSecp256k1VerificationKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'X25519KeyAgreementKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - ], - authentication: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - ], - keyAgreement: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - ], - assertionMethod: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - ], - capabilityDelegation: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - ], - service: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-1', - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-2', - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], - }, - ], - }) - }) - - it('exports the expected application/ld+json W3C DID Document with an Ed25519 authentication key, two x25519 encryption keys, an Sr25519 assertion key, an Ecdsa delegation key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(fullDid, 'application/ld+json') - - expect(didDoc).toStrictEqual({ - '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - verificationMethod: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Ed25519VerificationKey2018', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'Sr25519VerificationKey2020', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'EcdsaSecp256k1VerificationKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - controller: - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', - type: 'X25519KeyAgreementKey2019', - publicKeyBase58: '11111111111111111111111111111111', - }, - ], - authentication: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#auth', - ], - keyAgreement: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#enc', - ], - assertionMethod: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#att', - ], - capabilityDelegation: [ - 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#del', - ], - service: [ - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-1', - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs#id-2', - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], - }, - ], - }) - }) - - it('fails to export to an unsupported mimetype', async () => { - expect(() => - // @ts-ignore - exportToDidDocument(fullDid, 'random-mime-type') - ).toThrow() - }) -}) - -describe('When exporting a DID Document from a light DID', () => { - const authKey = generateAuthenticationKey() as NewDidVerificationKey - const encKey = generateEncryptionKey() - const service = [ - generateServiceEndpoint('#id-1'), - generateServiceEndpoint('#id-2'), - ] - const lightDid = Did.createLightDidDocument({ - authentication: [{ publicKey: authKey.publicKey, type: 'ed25519' }], - keyAgreement: [{ publicKey: encKey.publicKey, type: 'x25519' }], - service, - }) - - it('exports the expected application/json W3C DID Document with an Ed25519 authentication key, one x25519 encryption key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(lightDid, 'application/json') - - expect(didDoc).toMatchInlineSnapshot(` - { - "authentication": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - ], - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "keyAgreement": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - ], - "service": [ - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-1", - "serviceEndpoint": [ - "x:url-id-1", - ], - "type": [ - "type-id-1", - ], - }, - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-2", - "serviceEndpoint": [ - "x:url-id-2", - ], - "type": [ - "type-id-2", - ], - }, - ], - "verificationMethod": [ - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "Ed25519VerificationKey2018", - }, - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "X25519KeyAgreementKey2019", - }, - ], - } - `) - }) - - it('exports the expected application/json+ld W3C DID Document with an Ed25519 authentication key, one x25519 encryption key, and two service endpoints', async () => { - const didDoc = exportToDidDocument(lightDid, 'application/ld+json') - - expect(didDoc).toMatchInlineSnapshot(` - { - "@context": [ - "https://www.w3.org/ns/did/v1", - "ipfs://QmU7QkuTCPz7NmD5bD7Z7mQVz2UsSPaEK58B5sYnjnPRNW", - ], - "authentication": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - ], - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "keyAgreement": [ - "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - ], - "service": [ - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-1", - "serviceEndpoint": [ - "x:url-id-1", - ], - "type": [ - "type-id-1", - ], - }, - { - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#id-2", - "serviceEndpoint": [ - "x:url-id-2", - ], - "type": [ - "type-id-2", - ], - }, - ], - "verificationMethod": [ - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#authentication", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "Ed25519VerificationKey2018", - }, - { - "controller": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf", - "id": "did:kilt:light:014nv4phaKc4EcwENdRERuMF79ZSSB5xvnAk3zNySSbVbXhSwS:z16QMTH1Pc4A99Und9RZvzyikFR73Aepx9exPZPgXJX18upeuSpgXeat2LsjEQpXUBUtaRtdpSXpv42KitoFqySLjiuXVcghuoWviPci3QrnQMeD161howeWdF5GTbBFRHSVXpEu9PWbtUEsnLfDf2NQgu4LmktN8Ti6CAmdQtQiVNbJkB7TnyzLiJJ27rYayWj15mjJ9EoNyyu3rDJGomi2vUgt2DiSUXaJbnSzuuFf#encryption", - "publicKeyBase58": "11111111111111111111111111111111", - "type": "X25519KeyAgreementKey2019", - }, - ], - } - `) - }) - - it('fails to export to an unsupported mimetype', async () => { - expect(() => - // @ts-ignore - exportToDidDocument(lightDid, 'random-mime-type') - ).toThrow() - }) -}) diff --git a/packages/did/src/DidDocumentExporter/DidDocumentExporter.ts b/packages/did/src/DidDocumentExporter/DidDocumentExporter.ts deleted file mode 100644 index bfaebeb43..000000000 --- a/packages/did/src/DidDocumentExporter/DidDocumentExporter.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { base58Encode } from '@polkadot/util-crypto' - -import type { - DidDocument, - ConformingDidDocument, - DidResourceUri, - JsonLDDidDocument, - UriFragment, -} from '@kiltprotocol/types' -import { - encryptionKeyTypesMap, - verificationKeyTypesMap, -} from '@kiltprotocol/types' -import { SDKErrors } from '@kiltprotocol/utils' -import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' - -function exportToJsonDidDocument(did: DidDocument): ConformingDidDocument { - const { - uri: controller, - authentication, - assertionMethod = [], - capabilityDelegation = [], - keyAgreement = [], - service = [], - } = did - - function toAbsoluteUri(keyId: UriFragment): DidResourceUri { - if (keyId.startsWith(controller)) { - return keyId as DidResourceUri - } - return `${controller}${keyId}` - } - - const verificationMethod: ConformingDidDocument['verificationMethod'] = [ - ...authentication, - ...assertionMethod, - ...capabilityDelegation, - ] - .map((key) => ({ ...key, type: verificationKeyTypesMap[key.type] })) - .concat( - keyAgreement.map((key) => ({ - ...key, - type: encryptionKeyTypesMap[key.type], - })) - ) - .map(({ id, type, publicKey }) => ({ - id: toAbsoluteUri(id), - controller, - type, - publicKeyBase58: base58Encode(publicKey), - })) - .filter( - // remove duplicates - ({ id }, index, array) => - index === array.findIndex((key) => key.id === id) - ) - - return { - id: controller, - verificationMethod, - authentication: [toAbsoluteUri(authentication[0].id)], - ...(assertionMethod[0] && { - assertionMethod: [toAbsoluteUri(assertionMethod[0].id)], - }), - ...(capabilityDelegation[0] && { - capabilityDelegation: [toAbsoluteUri(capabilityDelegation[0].id)], - }), - ...(keyAgreement.length > 0 && { - keyAgreement: [toAbsoluteUri(keyAgreement[0].id)], - }), - ...(service.length > 0 && { - service: service.map((endpoint) => ({ - ...endpoint, - id: `${controller}${endpoint.id}`, - })), - }), - } -} - -function exportToJsonLdDidDocument(did: DidDocument): JsonLDDidDocument { - const document = exportToJsonDidDocument(did) - document['@context'] = [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL] - return document as JsonLDDidDocument -} - -/** - * Export a [[DidDocument]] to a W3C-spec conforming DID Document in the format provided. - * - * @param did The [[DidDocument]]. - * @param mimeType The format for the output DID Document. Accepted values are `application/json` and `application/ld+json`. - * @returns The DID Document formatted according to the mime type provided, or an error if the format specified is not supported. - */ -export function exportToDidDocument( - did: DidDocument, - mimeType: 'application/json' | 'application/ld+json' -): ConformingDidDocument { - switch (mimeType) { - case 'application/json': - return exportToJsonDidDocument(did) - case 'application/ld+json': - return exportToJsonLdDidDocument(did) - default: - throw new SDKErrors.DidExporterError( - `The MIME type "${mimeType}" not supported by any of the available exporters` - ) - } -} diff --git a/packages/did/src/DidDocumentExporter/README.md b/packages/did/src/DidDocumentExporter/README.md deleted file mode 100644 index e9c90965e..000000000 --- a/packages/did/src/DidDocumentExporter/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# DID Document exporter - -The DID Document exporter provides the functionality needed to convert an instance of a generic `DidDocument` into a document that is compliant with the [W3C specification](https://www.w3.org/TR/did-core/). This component is required for the KILT plugin for the [DIF Universal Resolver](https://dev.uniresolver.io/). - -For a list of examples and code snippets, please refer to our [official documentation](https://docs.kilt.io/docs/develop/sdk/cookbook/dids/did-export). diff --git a/packages/did/src/DidDocumentExporter/index.ts b/packages/did/src/DidDocumentExporter/index.ts deleted file mode 100644 index 3243aecb8..000000000 --- a/packages/did/src/DidDocumentExporter/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -export * from './DidDocumentExporter.js' -export * from './DidContexts.js' diff --git a/packages/did/src/DidLinks/AccountLinks.chain.ts b/packages/did/src/DidLinks/AccountLinks.chain.ts index 882568d3b..745e6777d 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 { EncodedSignature } from '../Did.utils.js' +import type { EncodedSignature } from '../Did.chain.js' import { toChain } from '../Did.chain.js' /** @@ -134,7 +134,7 @@ function getUnprefixedSignature( } async function getLinkingChallengeV1( - did: DidUri, + did: Did, validUntil: BN ): Promise { const api = ConfigService.get('api') @@ -156,7 +156,7 @@ async function getLinkingChallengeV1( .toU8a() } -function getLinkingChallengeV2(did: DidUri, validUntil: BN): Uint8Array { +function getLinkingChallengeV2(did: Did, validUntil: BN): Uint8Array { return stringToU8a( `Publicly link the signing address to ${did} before block number ${validUntil}` ) @@ -167,12 +167,12 @@ function getLinkingChallengeV2(did: DidUri, validUntil: BN): Uint8Array { * The account has to sign the challenge, while the DID will sign the extrinsic that contains the challenge and will * link the account to the DID. * - * @param did The URI of the DID that that should be linked to an account. + * @param did The DID that should be linked to an account. * @param validUntil Last blocknumber that this challenge is valid for. * @returns The encoded challenge. */ export async function getLinkingChallenge( - did: DidUri, + did: Did, validUntil: BN ): Promise { const api = ConfigService.get('api') @@ -261,7 +261,7 @@ export function getWrappedChallenge( */ export async function associateAccountToChainArgs( accountAddress: Address, - did: DidUri, + did: Did, sign: (encodedLinkingDetails: HexString) => Promise, nBlocksValid = 10 ): Promise { diff --git a/packages/did/src/DidDocumentExporter/DidContexts.ts b/packages/did/src/DidResolver/DidContexts.ts similarity index 80% rename from packages/did/src/DidDocumentExporter/DidContexts.ts rename to packages/did/src/DidResolver/DidContexts.ts index 196e35a2d..50249c96d 100644 --- a/packages/did/src/DidDocumentExporter/DidContexts.ts +++ b/packages/did/src/DidResolver/DidContexts.ts @@ -7,18 +7,24 @@ // @ts-expect-error not a TS package import securityContexts from '@digitalbazaar/security-context' +// @ts-expect-error not a TS package +import multikeyContexts from '@digitalbazaar/multikey-context' const securityContextsMap: Map< string, Record > = securityContexts.contexts +const multikeyContextsMap: Map< + string, + Record +> = multikeyContexts.contexts /** * IPFS URL identifying a JSON-LD context file describing terms used in DID documents of the KILT method that are not defined in the W3C DID core context. - * Should be the second entry in the ordered set of contexts after [[W3C_DID_CONTEXT_URL]] in the JSON-LD representation of a KILT DID document. + * Should be the third entry in the ordered set of contexts after [[W3C_DID_CONTEXT_URL]] and [[W3C_MULTIKEY_CONTEXT_URL]] in the JSON-LD representation of a KILT DID document. */ export const KILT_DID_CONTEXT_URL = - 'ipfs://QmU7QkuTCPz7NmD5bD7Z7mQVz2UsSPaEK58B5sYnjnPRNW' + 'ipfs://QmPtQ7wbdxbTuGugx4nFAyrhspcqXKrnriuGr7x4NYaZYN' /** * URL identifying the JSON-LD context file that is part of the W3C DID core specifications describing the terms defined by the core data model. * Must be the first entry in the ordered set of contexts in a JSON-LD representation of a DID document. @@ -31,6 +37,11 @@ export const W3C_DID_CONTEXT_URL = 'https://www.w3.org/ns/did/v1' * This document is extended by the context file available under the [[KILT_DID_CONTEXT_URL]]. */ export const W3C_SECURITY_CONTEXT_URL = securityContexts.SECURITY_CONTEXT_V2_URL +/** + * URL identifying a JSON-LD context file proposed by the W3C Credentials Community Group defining the `Multikey` verification method type, used in verification methods on KILT DID documents. + * This document is extended by the context file available under the [[KILT_DID_CONTEXT_URL]]. + */ +export const W3C_MULTIKEY_CONTEXT_URL = multikeyContexts.CONTEXT_URL /** * An object containing static copies of JSON-LD context files relevant to KILT DID documents, of the form -> context. * These context definitions are not supposed to change; therefore, a cached version can (and should) be used to avoid unexpected changes in definitions. @@ -39,6 +50,7 @@ export const DID_CONTEXTS = { [KILT_DID_CONTEXT_URL]: { '@context': [ W3C_SECURITY_CONTEXT_URL, + W3C_MULTIKEY_CONTEXT_URL, { '@protected': true, KiltPublishedCredentialCollectionV1: @@ -107,4 +119,5 @@ export const DID_CONTEXTS = { }, }, ...Object.fromEntries(securityContextsMap), + ...Object.fromEntries(multikeyContextsMap), } diff --git a/packages/did/src/DidResolver/DidResolver.spec.ts b/packages/did/src/DidResolver/DidResolver.spec.ts index 4a1c21280..622d2d272 100644 --- a/packages/did/src/DidResolver/DidResolver.spec.ts +++ b/packages/did/src/DidResolver/DidResolver.spec.ts @@ -5,50 +5,35 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { BN } from '@polkadot/util' -import { base58Encode } from '@polkadot/util-crypto' - import { ConfigService } from '@kiltprotocol/config' -import type { - ConformingDidKey, - ConformingDidServiceEndpoint, - DidEncryptionKey, - DidKey, - DidResolutionDocumentMetadata, - DidResolutionMetadata, - DidResolutionResult, - DidResourceUri, - DidServiceEndpoint, - DidUri, - DidVerificationKey, +import { + DereferenceResult, + Did as KiltDid, + DidUrl, KiltAddress, - ResolvedDidKey, - ResolvedDidServiceEndpoint, + RepresentationResolutionResult, + ResolutionResult, + Service, UriFragment, + VerificationMethod, } from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' +import { Crypto, cbor } from '@kiltprotocol/utils' +import { stringToU8a } from '@polkadot/util' import { ApiMocks, makeSigningKeyTool } from '../../../../tests/testUtils' import { linkedInfoFromChain } from '../Did.rpc.js' -import { getFullDidUriFromKey } from '../Did.utils' import * as Did from '../index.js' -import { - resolve, - resolveCompliant, - resolveKey, - resolveService, -} from './index.js' const addressWithAuthenticationKey = '4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' -const didWithAuthenticationKey: DidUri = `did:kilt:${addressWithAuthenticationKey}` +const didWithAuthenticationKey: KiltDid = `did:kilt:${addressWithAuthenticationKey}` const addressWithAllKeys = `4sDxAgw86PFvC6TQbvZzo19WoYF6T4HcLd2i9wzvojkLXLvp` -const didWithAllKeys: DidUri = `did:kilt:${addressWithAllKeys}` +const didWithAllKeys: KiltDid = `did:kilt:${addressWithAllKeys}` const addressWithServiceEndpoints = `4q4DHavMdesaSMH3g32xH3fhxYPt5pmoP9oSwgTr73dQLrkN` -const didWithServiceEndpoints: DidUri = `did:kilt:${addressWithServiceEndpoints}` +const didWithServiceEndpoints: KiltDid = `did:kilt:${addressWithServiceEndpoints}` const deletedAddress = '4rrVTLAXgeoE8jo8si571HnqHtd5WmvLuzfH6e1xBsVXsRo7' -const deletedDid: DidUri = `did:kilt:${deletedAddress}` +const deletedDid: KiltDid = `did:kilt:${deletedAddress}` const didIsBlacklisted = ApiMocks.mockChainQueryReturn( 'did', @@ -63,7 +48,7 @@ beforeAll(() => { mockedApi = ApiMocks.getMockedApi() ConfigService.set({ api: mockedApi }) - // Mock `api.call.did.query(didUri)` + // Mock `api.call.did.query(did)` // By default it returns a simple LinkedDidInfo with no web3name and no accounts linked. jest .spyOn(mockedApi.call.did, 'query') @@ -95,42 +80,63 @@ beforeAll(() => { }) }) -function generateAuthenticationKey(): DidVerificationKey { +function generateAuthenticationVerificationMethod( + controller: KiltDid +): VerificationMethod { return { id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), + controller, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(0), + type: 'ed25519', + }), } } -function generateEncryptionKey(): DidEncryptionKey { +function generateEncryptionVerificationMethod( + controller: KiltDid +): VerificationMethod { return { id: '#enc', - type: 'x25519', - publicKey: new Uint8Array(32).fill(1), - includedAt: new BN(15), + controller, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(1), + type: 'x25519', + }), } } -function generateAttestationKey(): DidVerificationKey { +function generateAssertionVerificationMethod( + controller: KiltDid +): VerificationMethod { return { id: '#att', - type: 'sr25519', - publicKey: new Uint8Array(32).fill(2), - includedAt: new BN(20), + controller, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(32).fill(2), + type: 'sr25519', + }), } } -function generateDelegationKey(): DidVerificationKey { +function generateCapabilityDelegationVerificationMethod( + controller: KiltDid +): VerificationMethod { return { id: '#del', - type: 'ecdsa', - publicKey: new Uint8Array(32).fill(3), - includedAt: new BN(25), + controller, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: new Uint8Array(33).fill(3), + type: 'ecdsa', + }), } } -function generateServiceEndpoint(serviceId: UriFragment): DidServiceEndpoint { +function generateServiceEndpoint(serviceId: UriFragment): Service { const fragment = serviceId.substring(1) return { id: serviceId, @@ -140,256 +146,338 @@ function generateServiceEndpoint(serviceId: UriFragment): DidServiceEndpoint { } jest.mock('../Did.rpc.js') -// By default its mock returns a DIDDocument with the test authentication key, test service, and the URI derived from the identifier provided in the resolution. +// By default its mock returns a DIDDocument with the test authentication key, test service, and the DID derived from the identifier provided in the resolution. jest.mocked(linkedInfoFromChain).mockImplementation((linkedInfo) => { const { identifier } = linkedInfo.unwrap() + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - authentication: [generateAuthenticationKey()], + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], service: [generateServiceEndpoint('#service-1')], }, } }) -describe('When resolving a key', () => { - it('correctly resolves it for a full DID if both the DID and the key exist', async () => { +describe('When dereferencing a verification method', () => { + it('correctly dereference it for a full DID if both the DID and the verification method exist', async () => { const fullDid = didWithAuthenticationKey - const keyIdUri: DidResourceUri = `${fullDid}#auth` + const verificationMethodUrl: DidUrl = `${fullDid}#auth` - expect(await resolveKey(keyIdUri)).toStrictEqual({ - controller: fullDid, - publicKey: new Uint8Array(32).fill(0), - id: keyIdUri, - type: 'ed25519', + expect( + await Did.dereference(verificationMethodUrl, { + accept: 'application/did+json', + }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: generateAuthenticationVerificationMethod(fullDid), }) }) - it('returns null if either the DID or the key do not exist', async () => { - let keyIdUri: DidResourceUri = `${deletedDid}#enc` + it('returns error if either the DID or the verification method do not exist', async () => { + let verificationMethodUrl: DidUrl = `${deletedDid}#enc` - await expect(resolveKey(keyIdUri)).rejects.toThrow() + expect( + await Did.dereference(verificationMethodUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) const didWithNoEncryptionKey = didWithAuthenticationKey - keyIdUri = `${didWithNoEncryptionKey}#enc` + verificationMethodUrl = `${didWithNoEncryptionKey}#enc` - await expect(resolveKey(keyIdUri)).rejects.toThrow() + expect( + await Did.dereference(verificationMethodUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) }) - 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() + it('throws for invalid URLs', async () => { + const invalidUrl = 'invalid-url' as DidUrl + expect(await Did.dereference(invalidUrl)).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'invalidDidUrl' }, + }) }) }) -describe('When resolving a service endpoint', () => { +describe('When resolving a service', () => { it('correctly resolves it for a full DID if both the DID and the endpoint exist', async () => { const fullDid = didWithServiceEndpoints - const serviceIdUri: DidResourceUri = `${fullDid}#service-1` + const serviceIdUrl: DidUrl = `${fullDid}#service-1` expect( - await resolveService(serviceIdUri) - ).toStrictEqual({ - id: serviceIdUri, - type: [`type-service-1`], - serviceEndpoint: [`x:url-service-1`], + await Did.dereference(serviceIdUrl, { + accept: 'application/did+json', + }) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: { + id: '#service-1', + type: [`type-service-1`], + serviceEndpoint: [`x:url-service-1`], + }, }) }) - it('returns null if either the DID or the service do not exist', async () => { + it('returns error if either the DID or the service do not exist', async () => { // Mock transform function changed to not return any services (twice). jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - authentication: [generateAuthenticationKey()], + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], }, } }) 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: { - uri: `did:kilt:${identifier as unknown as KiltAddress}`, - authentication: [generateAuthenticationKey()], + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], }, } }) - let serviceIdUri: DidResourceUri = `${deletedDid}#service-1` + let serviceIdUrl: DidUrl = `${deletedDid}#service-1` - await expect(resolveService(serviceIdUri)).rejects.toThrow() + expect( + await Did.dereference(serviceIdUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) const didWithNoServiceEndpoints = didWithAuthenticationKey - serviceIdUri = `${didWithNoServiceEndpoints}#service-1` - - await expect(resolveService(serviceIdUri)).rejects.toThrow() - }) - - it('throws for invalid URIs', async () => { - const uriWithoutFragment = deletedDid - await expect( - resolveService(uriWithoutFragment as DidResourceUri) - ).rejects.toThrow() + serviceIdUrl = `${didWithNoServiceEndpoints}#service-1` - const invalidUri = 'invalid-uri' as DidResourceUri - await expect(resolveService(invalidUri)).rejects.toThrow() + expect( + await Did.dereference(serviceIdUrl) + ).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + }) }) }) describe('When resolving a full DID', () => { - it('correctly resolves the document with an authentication key', async () => { + it('correctly resolves the document with an authentication verification method', async () => { const fullDidWithAuthenticationKey = didWithAuthenticationKey - const { document, metadata, web3Name } = (await resolve( - fullDidWithAuthenticationKey - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document.uri).toStrictEqual(fullDidWithAuthenticationKey) - expect(Did.getKeys(document)).toStrictEqual([ - { - id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), + expect( + 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), + }), + }, + ], }, - ]) - 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: { - authentication: [generateAuthenticationKey()], - keyAgreement: [generateEncryptionKey()], - assertionMethod: [generateAttestationKey()], - capabilityDelegation: [generateDelegationKey()], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, + id: did, + authentication: [authMethod.id], + keyAgreement: [encMethod.id], + assertionMethod: [attMethod.id], + capabilityDelegation: [delMethod.id], + verificationMethod: [authMethod, encMethod, attMethod, delMethod], }, } }) const fullDidWithAllKeys = didWithAllKeys - const { document, metadata } = (await resolve( - fullDidWithAllKeys - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document.uri).toStrictEqual(fullDidWithAllKeys) - expect(Did.getKeys(document)).toStrictEqual([ - { - id: '#auth', - type: 'ed25519', - publicKey: new Uint8Array(32).fill(0), - }, - { - id: '#att', - type: 'sr25519', - publicKey: new Uint8Array(32).fill(2), - includedAt: new BN(20), - }, - { - id: '#del', - type: 'ecdsa', - publicKey: new Uint8Array(32).fill(3), - includedAt: new BN(25), - }, - { - id: '#enc', - type: 'x25519', - publicKey: new Uint8Array(32).fill(1), - includedAt: new BN(15), + expect( + await Did.resolve(fullDidWithAllKeys) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: fullDidWithAllKeys, + authentication: ['#auth'], + keyAgreement: ['#enc'], + assertionMethod: ['#att'], + capabilityDelegation: ['#del'], + verificationMethod: [ + { + controller: fullDidWithAllKeys, + id: '#auth', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: new Uint8Array(32).fill(0), + }), + }, + { + controller: fullDidWithAllKeys, + id: '#enc', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(1), + }), + }, + { + controller: fullDidWithAllKeys, + id: '#att', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: new Uint8Array(32).fill(2), + }), + }, + { + controller: fullDidWithAllKeys, + id: '#del', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ecdsa', + publicKey: new Uint8Array(33).fill(3), + }), + }, + ], }, - ]) + }) }) - it('correctly resolves the document with service endpoints', async () => { - // Mock transform function changed to return two service endpoints. + it('correctly resolves the document with services', async () => { + // Mock transform function changed to return two services. jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { const { identifier } = linkedInfo.unwrap() + const did: KiltDid = `did:kilt:${identifier as unknown as KiltAddress}` + const authMethod = generateAuthenticationVerificationMethod(did) return { accounts: [], document: { - authentication: [generateAuthenticationKey()], + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], service: [ generateServiceEndpoint('#id-1'), generateServiceEndpoint('#id-2'), ], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, }, } }) const fullDidWithServiceEndpoints = didWithServiceEndpoints - const { document, metadata } = (await resolve( - fullDidWithServiceEndpoints - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document.uri).toStrictEqual(fullDidWithServiceEndpoints) - expect(document.service).toStrictEqual([ - { - id: '#id-1', - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: '#id-2', - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], + expect( + await Did.resolve(fullDidWithServiceEndpoints) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: fullDidWithServiceEndpoints, + authentication: ['#auth'], + verificationMethod: [ + { + controller: fullDidWithServiceEndpoints, + id: '#auth', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ed25519', + publicKey: new Uint8Array(32).fill(0), + }), + }, + ], + service: [ + { + id: '#id-1', + type: ['type-id-1'], + serviceEndpoint: ['x:url-id-1'], + }, + { + id: '#id-2', + type: ['type-id-2'], + serviceEndpoint: ['x:url-id-2'], + }, + ], }, - ]) + }) }) it('correctly resolves the document with web3Name', async () => { - // Mock transform function changed to return two service endpoints. + // Mock transform function changed to return two services. 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: { - authentication: [generateAuthenticationKey()], - service: [], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], + alsoKnownAs: ['w3n:w3nick'], }, - web3Name: 'w3nick', } }) - const { document, metadata, web3Name } = (await resolve( - didWithAuthenticationKey - )) as DidResolutionResult - if (document === undefined) throw new Error('Document unresolved') - - expect(metadata).toStrictEqual({ - deactivated: false, + expect( + 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'], + }, }) - expect(document.uri).toStrictEqual(didWithAuthenticationKey) - expect(web3Name).toStrictEqual('w3nick') }) it('correctly resolves a non-existing DID', async () => { @@ -399,10 +487,14 @@ describe('When resolving a full DID', () => { .mockResolvedValueOnce( augmentedApi.createType('Option', null) ) - const randomDid = getFullDidUriFromKey( - makeSigningKeyTool().authentication[0] - ) - expect(await resolve(randomDid)).toBeNull() + const randomKeypair = makeSigningKeyTool().authentication[0] + const randomDid = Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(randomKeypair), + }) + expect(await Did.resolve(randomDid)).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: { error: 'notFound' }, + }) }) it('correctly resolves a deleted DID', async () => { @@ -414,27 +506,11 @@ describe('When resolving a full DID', () => { ) mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) - const { document, metadata } = (await resolve( - deletedDid - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: true, - }) - expect(document).toBeUndefined() - }) - - it('correctly resolves DID document given a fragment', async () => { - const fullDidWithAuthenticationKey = didWithAuthenticationKey - const keyIdUri: DidUri = `${fullDidWithAuthenticationKey}#auth` - const { document, metadata } = (await resolve( - keyIdUri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, + expect(await Did.resolve(deletedDid)).toStrictEqual({ + didDocumentMetadata: { deactivated: true }, + didResolutionMetadata: {}, + didDocument: { id: deletedDid }, }) - expect(document?.uri).toStrictEqual(fullDidWithAuthenticationKey) }) }) @@ -455,26 +531,31 @@ describe('When resolving a light DID', () => { const lightDidWithAuthenticationKey = Did.createLightDidDocument({ authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], }) - const { document, metadata } = (await resolve( - lightDidWithAuthenticationKey.uri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document?.uri).toStrictEqual( - lightDidWithAuthenticationKey.uri - ) - expect(Did.getKeys(lightDidWithAuthenticationKey)).toStrictEqual([ - { - id: '#authentication', - type: 'sr25519', - publicKey: authKey.publicKey, + expect( + await Did.resolve(lightDidWithAuthenticationKey.id) + ).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: lightDidWithAuthenticationKey.id, + authentication: ['#authentication'], + verificationMethod: [ + { + controller: lightDidWithAuthenticationKey.id, + id: '#authentication', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + ...authKey, + type: 'sr25519', + }), + }, + ], + service: undefined, }, - ]) + }) }) - it('correctly resolves the document with authentication key, encryption key, and two service endpoints', async () => { + it('correctly resolves the document with authentication key, encryption key, and two services', async () => { const lightDid = Did.createLightDidDocument({ authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], keyAgreement: [{ publicKey: encryptionKey.publicKey, type: 'x25519' }], @@ -483,38 +564,44 @@ describe('When resolving a light DID', () => { generateServiceEndpoint('#service-2'), ], }) - const { document, metadata } = (await resolve( - lightDid.uri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - }) - expect(document?.uri).toStrictEqual(lightDid.uri) - expect(Did.getKeys(lightDid)).toStrictEqual([ - { - id: '#authentication', - type: 'sr25519', - publicKey: authKey.publicKey, - }, - { - id: '#encryption', - type: 'x25519', - publicKey: encryptionKey.publicKey, - }, - ]) - expect(lightDid.service).toStrictEqual([ - { - id: '#service-1', - type: ['type-service-1'], - serviceEndpoint: ['x:url-service-1'], - }, - { - id: '#service-2', - type: ['type-service-2'], - serviceEndpoint: ['x:url-service-2'], + expect(await Did.resolve(lightDid.id)).toStrictEqual({ + didDocumentMetadata: {}, + didResolutionMetadata: {}, + didDocument: { + id: lightDid.id, + authentication: ['#authentication'], + keyAgreement: ['#encryption'], + verificationMethod: [ + { + controller: lightDid.id, + id: '#authentication', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + ...authKey, + type: 'sr25519', + }), + }, + { + controller: lightDid.id, + id: '#encryption', + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey(encryptionKey), + }, + ], + service: [ + { + id: '#service-1', + type: ['type-service-1'], + serviceEndpoint: ['x:url-service-1'], + }, + { + id: '#service-2', + type: ['type-service-2'], + serviceEndpoint: ['x:url-service-2'], + }, + ], }, - ]) + }) }) it('correctly resolves a migrated and not deleted DID', async () => { @@ -538,175 +625,432 @@ describe('When resolving a light DID', () => { }, }) ) - const migratedDid: DidUri = `did:kilt:light:00${addressWithAuthenticationKey}` - const { document, metadata } = (await resolve( - migratedDid - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, - canonicalId: didWithAuthenticationKey, + const migratedDid: KiltDid = `did:kilt:light:00${addressWithAuthenticationKey}` + expect(await Did.resolve(migratedDid)).toStrictEqual({ + didDocumentMetadata: { canonicalId: didWithAuthenticationKey }, + didResolutionMetadata: {}, + didDocument: { + id: migratedDid, + }, }) - expect(document).toBe(undefined) }) it('correctly resolves a migrated and deleted DID', async () => { // Mock the resolved DID as deleted. mockedApi.query.did.didBlacklist.mockReturnValueOnce(didIsBlacklisted) - const migratedDid: DidUri = `did:kilt:light:00${deletedAddress}` - const { document, metadata } = (await resolve( - migratedDid - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: true, - }) - expect(document).toBeUndefined() - }) - - it('correctly resolves DID document given a fragment', async () => { - const lightDid = Did.createLightDidDocument({ - authentication: [{ publicKey: authKey.publicKey, type: 'sr25519' }], - }) - const keyIdUri: DidUri = `${lightDid.uri}#auth` - const { document, metadata } = (await resolve( - keyIdUri - )) as DidResolutionResult - - expect(metadata).toStrictEqual({ - deactivated: false, + const migratedDid: KiltDid = `did:kilt:light:00${deletedAddress}` + expect(await Did.resolve(migratedDid)).toStrictEqual({ + didDocumentMetadata: { deactivated: true }, + didResolutionMetadata: {}, + didDocument: { + id: migratedDid, + }, }) - expect(document?.uri).toStrictEqual(lightDid.uri) }) }) -describe('When resolving with the spec compliant resolver', () => { +describe('DID Resolution compliance', () => { beforeAll(() => { jest .spyOn(mockedApi.call.did, 'query') .mockImplementation(async (identifier) => { return augmentedApi.createType('Option', { identifier, + accounts: [], + w3n: null, + serviceEndpoints: [ + { + id: 'foo', + serviceTypes: ['type-service-1'], + urls: ['x:url-service-1'], + }, + ], + details: { + authenticationKey: '01234567890123456789012345678901', + keyAgreementKeys: [], + delegationKey: null, + attestationKey: null, + publicKeys: [], + lastTxCounter: 123, + deposit: { + owner: addressWithAuthenticationKey, + amount: 0, + }, + }, }) }) - // Mock transform function changed to return two service endpoints. - jest.mocked(linkedInfoFromChain).mockImplementationOnce((linkedInfo) => { + 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: { - authentication: [generateAuthenticationKey()], - service: [ - generateServiceEndpoint('#id-1'), - generateServiceEndpoint('#id-2'), - ], - uri: `did:kilt:${identifier as unknown as KiltAddress}`, + id: did, + authentication: [authMethod.id], + verificationMethod: [authMethod], }, - web3Name: 'w3nick', } }) }) - - it('returns a spec-compliant DID document', async () => { - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(didWithAuthenticationKey) - if (didDocument === undefined) throw new Error('Document unresolved') - - expect(didDocumentMetadata).toStrictEqual({ - deactivated: false, + 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', + }), + }, + ], + }, + }) }) - - 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)), + 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' }, + }) }) - expect(didDocument.service).toStrictEqual([ - { - id: `${didWithAuthenticationKey}#id-1`, - type: ['type-id-1'], - serviceEndpoint: ['x:url-id-1'], - }, - { - id: `${didWithAuthenticationKey}#id-2`, - type: ['type-id-2'], - serviceEndpoint: ['x:url-id-2'], - }, - ]) - expect(didDocument).toHaveProperty('alsoKnownAs', ['w3n:w3nick']) }) - it('correctly resolves a non-existing DID', async () => { - // RPC call changed to not return anything. - jest - .spyOn(mockedApi.call.did, 'query') - .mockResolvedValueOnce( - augmentedApi.createType('Option', null) - ) - const randomDid = getFullDidUriFromKey( - makeSigningKeyTool().authentication[0] - ) - - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(randomDid) - - expect(didDocumentMetadata).toStrictEqual({}) - expect(didResolutionMetadata).toHaveProperty('error', 'notFound') - expect(didDocument).toBeUndefined() + 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 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' }, + }) + }) }) - it('correctly resolves a deleted DID', async () => { - // RPC call changed to not return anything. + 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('returns the right `dereferencingMetadata.error` when the DID does not exist', async () => { 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) - expect(didDocumentMetadata).toStrictEqual({ - deactivated: true, + const did: KiltDid = + 'did:kilt:4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs' + expect(await Did.dereference(did)).toStrictEqual({ + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, }) - expect(didResolutionMetadata).toStrictEqual({}) - expect(didDocument).toStrictEqual({ id: deletedDid }) }) - - it('correctly resolves an upgraded light DID', async () => { - const key = makeSigningKeyTool().authentication[0] - const lightDid = Did.createLightDidDocument({ authentication: [key] }).uri - const fullDid = getFullDidUriFromKey(key) - - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(lightDid) - - expect(didDocumentMetadata).toStrictEqual({ - deactivated: false, - canonicalId: fullDid, + 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' }, }) - expect(didResolutionMetadata).toStrictEqual({}) - expect(didDocument).toStrictEqual({ id: lightDid }) }) - - it('does not dereference a DID URL (with fragment)', async () => { - const fullDidWithAuthenticationKey = didWithAuthenticationKey - const keyIdUri: DidUri = `${fullDidWithAuthenticationKey}#auth` - const { didDocument, didDocumentMetadata, didResolutionMetadata } = - await resolveCompliant(keyIdUri) - - expect(didDocumentMetadata).toStrictEqual({}) - expect(didResolutionMetadata).toHaveProperty< - DidResolutionMetadata['error'] - >('error', 'invalidDid') - expect(didDocument).toBeUndefined() + 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', + }), + }, + ], + }, + }) }) }) diff --git a/packages/did/src/DidResolver/DidResolver.ts b/packages/did/src/DidResolver/DidResolver.ts index 16ad07512..d3659f9a8 100644 --- a/packages/did/src/DidResolver/DidResolver.ts +++ b/packages/did/src/DidResolver/DidResolver.ts @@ -6,68 +6,87 @@ */ import type { - ConformingDidResolutionResult, - DidKey, - DidResolutionResult, - DidResourceUri, - DidUri, - KeyRelationship, - ResolvedDidKey, - ResolvedDidServiceEndpoint, - UriFragment, + DereferenceContentMetadata, + DereferenceContentStream, + DereferenceOptions, + DereferenceResult, + DidDocument, + DidResolver, + Did, + DidUrl, + FailedDereferenceMetadata, + JsonLd, + RepresentationResolutionResult, + ResolutionDocumentMetadata, + ResolutionOptions, + ResolutionResult, } from '@kiltprotocol/types' -import { SDKErrors } from '@kiltprotocol/utils' + +import { stringToU8a } from '@polkadot/util' import { ConfigService } from '@kiltprotocol/config' +import { cbor } from '@kiltprotocol/utils' -import * as Did from '../index.js' -import { toChain } from '../Did.chain.js' +import { KILT_DID_CONTEXT_URL, W3C_DID_CONTEXT_URL } from './DidContexts.js' import { linkedInfoFromChain } from '../Did.rpc.js' -import { getFullDidUri, parse } from '../Did.utils.js' -import { exportToDidDocument } from '../DidDocumentExporter/DidDocumentExporter.js' +import { toChain } from '../Did.chain.js' +import { 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' /** - * Resolve a DID URI to the DID document and its metadata. - * - * The URI can also identify a key or a service, but it will be ignored during resolution. - * - * @param did The subject's DID. - * @returns The details associated with the DID subject. + * Supported content types for DID resolution and dereferencing. */ -export async function resolve( - did: DidUri -): Promise { +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 { const { type } = parse(did) const api = ConfigService.get('api') - const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid - const { section, version } = queryFunction?.meta ?? {} - if (version > 2) - throw new Error( - `This version of the KILT sdk supports runtime api '${section}' <=v2 , but the blockchain runtime implements ${version}. Please upgrade!` - ) - const { document, web3Name } = await queryFunction(toChain(did)) + + const { document } = await api.call.did + .query(toChain(did)) .then(linkedInfoFromChain) - .catch(() => ({ document: undefined, web3Name: undefined })) + .catch(() => ({ document: undefined })) - if (type === 'full' && document) { + if (type === 'full' && document !== undefined) { return { document, - metadata: { - deactivated: false, - }, - ...(web3Name && { web3Name }), + documentMetadata: {}, } } - // If the full DID has been deleted (or the light DID was upgraded and deleted), - // return the info in the resolution metadata. const isFullDidDeleted = (await api.query.did.didBlacklist(toChain(did))) .isSome if (isFullDidDeleted) { return { - // No canonicalId and no details are returned as we consider this DID deactivated/deleted. - metadata: { + // No canonicalId is returned as we consider this DID deactivated/deleted. + documentMetadata: { deactivated: true, }, + document: { + id: did, + }, } } @@ -75,205 +94,324 @@ export async function resolve( return null } - const lightDocument = Did.parseDocumentFromLightDid(did, false) + const lightDocument = parseDocumentFromLightDid(did, false) // If a full DID with same subject is present, return the resolution metadata accordingly. - if (document) { + if (document !== undefined) { return { - metadata: { - canonicalId: getFullDidUri(did), - deactivated: false, + documentMetadata: { + canonicalId: getFullDid(did), + }, + document: { + id: lightDocument.id, }, } } // 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, - metadata: { - deactivated: false, - }, + documentMetadata: {}, } } /** * Implementation of `resolve` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - * As opposed to `resolve`, which takes a more pragmatic approach, the `didDocument` property contains a fully compliant DID document abstract data model. * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. * * @param did The DID to resolve. - * @returns An object with the properties `didDocument` (a spec-conforming DID document or `undefined`), `didDocumentMetadata` (equivalent to `metadata` returned by [[resolve]]), as well as `didResolutionMetadata` (indicating an `error` if any). + * @param resolutionOptions The resolution options accepted by the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * @returns The resolution result for the `resolve` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). */ -export async function resolveCompliant( - did: DidUri -): Promise { - const result: ConformingDidResolutionResult = { - didDocumentMetadata: {}, - didResolutionMetadata: {}, - } +export async function resolve( + did: Did, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + resolutionOptions: ResolutionOptions = {} +): Promise { try { - Did.validateUri(did, 'Did') + validateDid(did, 'Did') } catch (error) { - result.didResolutionMetadata.error = 'invalidDid' - if (error instanceof Error) { - result.didResolutionMetadata.errorMessage = - error.name + error.message ? `: ${error.message}` : '' + return { + didResolutionMetadata: { + error: 'invalidDid', + }, + didDocumentMetadata: {}, } - return result - } - const resolutionResult = await resolve(did) - 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 resolutionResult = await resolveInternal(did) + if (resolutionResult === null) { + return { + didResolutionMetadata: { + error: 'notFound', + }, + didDocumentMetadata: {}, + } } - return result -} + const { documentMetadata: didDocumentMetadata, document: didDocument } = + resolutionResult -/** - * Converts the DID key in the format returned by `resolveKey()`, useful for own implementations of `resolveKey`. - * - * @param key The DID key in the SDK format. - * @param did The DID the key belongs to. - * @returns The key in the resolveKey-format. - */ -export function keyToResolvedKey(key: DidKey, did: DidUri): ResolvedDidKey { - const { id, publicKey, includedAt, type } = key return { - controller: did, - id: `${did}${id}`, - publicKey, - type, - ...(includedAt && { includedAt }), + didResolutionMetadata: {}, + didDocumentMetadata, + didDocument, } } /** - * Converts the DID key returned by the `resolveKey()` into the format used in the SDK. + * Implementation of `resolveRepresentation` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * Additionally, this function returns an id-only DID document in the case where a DID has been deleted or upgraded. + * If a DID is invalid or has not been registered, this is indicated by the `error` property on the `didResolutionMetadata`. * - * @param key The key in the resolveKey-format. - * @returns The key in the SDK format. + * @param did The DID to resolve. + * @param resolutionOptions The resolution options accepted by the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). + * @param resolutionOptions.accept The content type accepted by the requesting client. + * @returns The resolution result for the `resolveRepresentation` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-resolution). */ -export function resolvedKeyToKey(key: ResolvedDidKey): DidKey { - const { id, publicKey, includedAt, type } = key - return { - id: Did.parse(id).fragment as UriFragment, - publicKey, - type, - ...(includedAt && { includedAt }), +export async function resolveRepresentation( + did: 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 } + + return { + didDocumentMetadata, + didResolutionMetadata: { + ...didResolutionMetadata, + contentType: accept, + }, + didDocumentStream: inputTransform(didDocument), + } as RepresentationResolutionResult } +type InternalDereferenceResult = + | FailedDereferenceMetadata + | { + contentMetadata: DereferenceContentMetadata + contentStream: DereferenceContentStream + } + /** - * Resolve a DID key URI to the key details. + * Type guard checking whether the provided 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. + * @param input The input to check. + * @returns Whether the input is a [[FailedDereferenceMetadata]]. */ -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` - ) - } +export function isFailedDereferenceMetadata( + input: unknown +): input is FailedDereferenceMetadata { + return (input as FailedDereferenceMetadata)?.error !== undefined +} - const resolved = await resolve(did) - if (!resolved) { - throw new SDKErrors.DidNotFoundError() - } +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 { - document, - metadata: { canonicalId }, - } = resolved + const { didDocument, didDocumentMetadata } = await resolve(did) - // 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 (didDocument === undefined) { + return { + error: 'notFound', + } } - const key = Did.getKey(document, keyId) - if (!key) { - throw new SDKErrors.DidNotFoundError('Key not found in DID') + if (fragment === undefined) { + return { + contentMetadata: didDocumentMetadata, + contentStream: didDocument, + } } - // 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}"` + const [dereferencedResource, dereferencingError] = (() => { + const verificationMethod = didDocument?.verificationMethod?.find( + ({ controller, id }) => controller === didDocument.id && id === fragment ) + + 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 keyToResolvedKey(key, did) + return { + contentStream: dereferencedResource, + contentMetadata: {}, + } } /** - * Resolve a DID service URI to the service details. + * Implementation of `dereference` compliant with W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). + * If a DID URL is invalid or has not been registered, this is indicated by the `error` property on the `dereferencingMetadata`. * - * @param serviceUri The DID service URI. - * @returns The details associated with the service endpoint. + * @param didUrl The DID URL to dereference. + * @param resolutionOptions The resolution options accepted by the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). + * @param resolutionOptions.accept The content type accepted by the requesting client. + * @returns The resolution result for the `dereference` function as specified in the W3C DID specifications (https://www.w3.org/TR/did-core/#did-url-dereferencing). */ -export async function resolveService( - serviceUri: DidResourceUri -): Promise { - const { did, fragment: serviceId } = parse(serviceUri) - - // A fragment (serviceId) IS expected to resolve a key. - if (!serviceId) { - throw new SDKErrors.DidError( - `Service URI "${serviceUri}" is not a valid DID resource` - ) +export async function dereference( + didUrl: Did | DidUrl, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + { accept }: DereferenceOptions = { + accept: DID_JSON_CONTENT_TYPE, } +): Promise> { + // The spec does not include an error for unsupported content types for dereferences + const contentType = isValidContentType(accept) + ? accept + : DID_JSON_CONTENT_TYPE - const resolved = await resolve(did) - if (!resolved) { - throw new SDKErrors.DidNotFoundError() + try { + validateDid(didUrl) + } catch (error) { + return { + dereferencingMetadata: { + error: 'invalidDidUrl', + }, + contentMetadata: {}, + } } - const { - document, - metadata: { canonicalId }, - } = resolved + const dereferenceResult = await dereferenceInternal(didUrl, { + accept: contentType, + }) - // If the light DID has been upgraded we consider the old service URI invalid, the full DID URI should be used instead. - if (canonicalId) { - throw new SDKErrors.DidResolveUpgradedDidError() - } - if (!document) { - throw new SDKErrors.DidDeactivatedError() + if (isFailedDereferenceMetadata(dereferenceResult)) { + return { + contentMetadata: {}, + dereferencingMetadata: dereferenceResult, + } } - const service = Did.getService(document, serviceId) - if (!service) { - throw new SDKErrors.DidNotFoundError('Service not found in DID') - } + 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, + ] + })() return { - ...service, - id: `${did}${serviceId}`, + dereferencingMetadata: { + contentType: contentTypeValue as SupportedContentType, + }, + contentMetadata: dereferenceResult.contentMetadata, + contentStream: stream, } } + +/** + * Fully-fledged default resolver capable of resolving DIDs in their canonical form, encoded for a specific content type, and of dereferencing parts of a DID Document according to the dereferencing specification. + */ +export const resolver: DidResolver = { + resolve, + resolveRepresentation, + dereference, +} diff --git a/packages/did/src/DidResolver/index.ts b/packages/did/src/DidResolver/index.ts index 64023f7e7..4c7d68c72 100644 --- a/packages/did/src/DidResolver/index.ts +++ b/packages/did/src/DidResolver/index.ts @@ -5,4 +5,5 @@ * 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 38320500d..f9aa46843 100644 --- a/packages/did/src/index.ts +++ b/packages/did/src/index.ts @@ -10,7 +10,6 @@ */ 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 ba8b7f991..796f33c14 100644 --- a/packages/legacy-credentials/src/Claim.spec.ts +++ b/packages/legacy-credentials/src/Claim.spec.ts @@ -6,7 +6,7 @@ */ import { CType } from '@kiltprotocol/core' -import type { DidUri, ICType, IClaim } from '@kiltprotocol/types' +import type { Did, ICType, IClaim } from '@kiltprotocol/types' import { SDKErrors } from '@kiltprotocol/utils' import * as Claim from './Claim' @@ -104,7 +104,7 @@ describe('compute hashes & validate by reproducing them', () => { }) describe('Claim', () => { - let did: DidUri + let did: Did let claimContents: any let testCType: ICType let claim: IClaim diff --git a/packages/legacy-credentials/src/Claim.ts b/packages/legacy-credentials/src/Claim.ts index cf21a4b6f..4f515394f 100644 --- a/packages/legacy-credentials/src/Claim.ts +++ b/packages/legacy-credentials/src/Claim.ts @@ -20,7 +20,7 @@ import { CType } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import type { - DidUri, + Did as KiltDid, HexString, ICType, IClaim, @@ -143,7 +143,7 @@ export function verifyDataStructure(input: IClaim | PartialClaim): void { throw new SDKErrors.CTypeHashMissingError() } if ('owner' in input) { - Did.validateUri(input.owner, 'Did') + Did.validateDid(input.owner, 'Did') } if (input.contents !== undefined) { Object.entries(input.contents).forEach(([key, value]) => { @@ -184,7 +184,7 @@ export function fromNestedCTypeClaim( cTypeInput: ICType, nestedCType: ICType[], claimContents: IClaim['contents'], - claimOwner: DidUri + claimOwner: KiltDid ): IClaim { CType.verifyClaimAgainstNestedSchemas(cTypeInput, nestedCType, claimContents) @@ -198,7 +198,7 @@ export function fromNestedCTypeClaim( } /** - * Constructs a new Claim from the given [[ICType]], IClaim['contents'] and [[DidUri]]. + * Constructs a new Claim from the given [[ICType]], IClaim['contents'] and [[Did]]. * * @param cType [[ICType]] for which the Claim will be built. * @param claimContents IClaim['contents'] to be used as the pure contents of the instantiated Claim. @@ -208,7 +208,7 @@ export function fromNestedCTypeClaim( export function fromCTypeAndClaimContents( cType: ICType, claimContents: IClaim['contents'], - claimOwner: DidUri + claimOwner: KiltDid ): IClaim { CType.verifyDataStructure(cType) CType.verifyClaimAgainstSchema(claimContents, cType) diff --git a/packages/legacy-credentials/src/Credential.spec.ts b/packages/legacy-credentials/src/Credential.spec.ts index 2d4d5dbfb..c5bc9f12e 100644 --- a/packages/legacy-credentials/src/Credential.spec.ts +++ b/packages/legacy-credentials/src/Credential.spec.ts @@ -13,23 +13,29 @@ import { ConfigService } from '@kiltprotocol/config' import { Attestation, CType, init } from '@kiltprotocol/core' import * as Did from '@kiltprotocol/did' import type { + DereferenceResult, DidDocument, - DidResourceUri, DidSignature, - DidUri, - DidVerificationKey, + Did as KiltDid, + DidUrl, 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, @@ -44,7 +50,7 @@ const testCType = CType.fromProperties('Credential', { }) function buildCredential( - claimerDid: DidUri, + claimerDid: KiltDid, contents: IClaimContents, legitimations: ICredential[] ): ICredential { @@ -437,24 +443,32 @@ describe('Presentations', () => { let identityDave: DidDocument let migratedAndDeletedLightDid: DidDocument - async function didResolveKey( - keyUri: DidResourceUri - ): Promise { - const { did } = Did.parse(keyUri) - const document = [ + async function dereferenceDidUrl( + didUrl: DidUrl | KiltDid + ): Promise> { + const { did } = Did.parse(didUrl) + const didDocument = [ identityAlice, identityBob, identityCharlie, identityDave, - ].find(({ uri }) => uri === did) - if (!document) throw new Error('Cannot resolve mocked DID') - return Did.keyToResolvedKey(document.authentication[0], did) + ].find(({ id }) => id === did) + if (!didDocument) + return { + contentMetadata: {}, + dereferencingMetadata: { error: 'notFound' }, + } + return { + contentMetadata: {}, + dereferencingMetadata: { contentType: 'application/did+json' }, + contentStream: didDocument, + } } // TODO: Cleanup file by migrating setup functions and removing duplicate tests. async function buildPresentation( claimer: DidDocument, - attesterDid: DidUri, + attesterDid: KiltDid, contents: IClaim['contents'], legitimations: ICredential[], sign: SignCallback @@ -463,7 +477,7 @@ describe('Presentations', () => { const claim = Claim.fromCTypeAndClaimContents( testCType, contents, - claimer.uri + claimer.id ) // build credential with legitimations const credential = Credential.fromClaim(claim, { @@ -494,7 +508,7 @@ describe('Presentations', () => { ) ;[legitimation] = await buildPresentation( identityAlice, - identityBob.uri, + identityBob.id, {}, [], keyAlice.getSignCallback(identityAlice) @@ -505,7 +519,7 @@ describe('Presentations', () => { .mockResolvedValue( ApiMocks.mockChainQueryReturn('attestation', 'attestations', { revoked: false, - attester: Did.toChain(identityBob.uri), + attester: Did.toChain(identityBob.id), ctypeHash: CType.idToHash(testCType.$id), } as any) as any ) @@ -514,7 +528,7 @@ describe('Presentations', () => { it('verify credentials signed by a full DID', async () => { const [presentation] = await buildPresentation( identityCharlie, - identityAlice.uri, + identityAlice.id, { a: 'a', b: 'b', @@ -528,9 +542,9 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) - ).resolves.toMatchObject({ revoked: false, attester: identityBob.uri }) + ).resolves.toMatchObject({ revoked: false, attester: identityBob.id }) }) it('verify credentials signed by a light DID', async () => { const { getSignCallback, authentication } = makeSigningKeyTool('ed25519') @@ -540,7 +554,7 @@ describe('Presentations', () => { const [presentation] = await buildPresentation( identityDave, - identityAlice.uri, + identityAlice.id, { a: 'a', b: 'b', @@ -554,14 +568,14 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) - ).resolves.toMatchObject({ revoked: false, attester: identityBob.uri }) + ).resolves.toMatchObject({ revoked: false, attester: identityBob.id }) }) it('throws if signature is missing on credential presentation', async () => { const credential = buildCredential( - identityBob.uri, + identityBob.id, { a: 'a', b: 'b', @@ -572,7 +586,7 @@ describe('Presentations', () => { await expect( Credential.verifyPresentation(credential as ICredentialPresentation, { ctype: testCType, - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow() }) @@ -584,7 +598,7 @@ describe('Presentations', () => { }) const credential = buildCredential( - identityBob.uri, + identityBob.id, { a: 'a', b: 'b', @@ -600,7 +614,7 @@ describe('Presentations', () => { await expect( Credential.verifySignature(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) @@ -612,7 +626,7 @@ describe('Presentations', () => { }) const credential = buildCredential( - identityAlice.uri, + identityAlice.id, { a: 'a', b: 'b', @@ -621,18 +635,20 @@ describe('Presentations', () => { [legitimation] ) - // sign presentation using Alice's authenication key + // sign presentation using Alice's authentication verification method const presentation = await Credential.createPresentation({ credential, signCallback: keyAlice.getSignCallback(identityAlice), }) - // but replace signer key reference with authentication key of light did - presentation.claimerSignature.keyUri = `${identityDave.uri}${identityDave.authentication[0].id}` + // but replace signer key reference with authentication verification method of light did + presentation.claimerSignature.keyUri = `${identityDave.id}${ + identityDave.authentication![0] + }` // signature would check out but mismatch should be detected await expect( Credential.verifySignature(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow(SDKErrors.DidSubjectMismatchError) }) @@ -645,7 +661,7 @@ describe('Presentations', () => { const [presentation] = await buildPresentation( migratedAndDeletedLightDid, - identityAlice.uri, + identityAlice.id, { a: 'a', b: 'b', @@ -659,7 +675,7 @@ describe('Presentations', () => { expect(() => Credential.verifyDataIntegrity(presentation)).not.toThrow() await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrowError() }) @@ -667,7 +683,7 @@ describe('Presentations', () => { it('Typeguard should return true on complete Credentials', async () => { const [presentation] = await buildPresentation( identityAlice, - identityBob.uri, + identityBob.id, {}, [], keyAlice.getSignCallback(identityAlice) @@ -680,7 +696,7 @@ describe('Presentations', () => { it('Should throw error when attestation is from different credential', async () => { const [credential, attestation] = await buildPresentation( identityAlice, - identityBob.uri, + identityBob.id, {}, [], keyAlice.getSignCallback(identityAlice) @@ -702,7 +718,7 @@ describe('Presentations', () => { it('returns Claim Hash of the attestation', async () => { const [credential, attestation] = await buildPresentation( identityAlice, - identityBob.uri, + identityBob.id, {}, [], keyAlice.getSignCallback(identityAlice) @@ -730,29 +746,79 @@ describe('create presentation', () => { // Returns a full DID that has the same subject of the first light DID, but the same key authentication key as the second one, if provided, or as the first one otherwise. function createMinimalFullDidFromLightDid( lightDidForId: DidDocument, - newAuthenticationKey?: DidVerificationKey + newAuthenticationKey?: NewDidVerificationKey ): DidDocument { - const uri = Did.getFullDidUri(lightDidForId.uri) - const authKey = newAuthenticationKey || lightDidForId.authentication[0] + 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 + })() return { - uri, - authentication: [authKey], + id, + authentication: [authMethod.id], + verificationMethod: [authMethod], } } - async function didResolveKey( - keyUri: DidResourceUri - ): Promise { - const { did } = Did.parse(keyUri) - const document = [ - migratedClaimerLightDid, - unmigratedClaimerLightDid, - migratedClaimerFullDid, - attester, - ].find(({ uri }) => uri === did) - if (!document) throw new Error('Cannot resolve mocked DID') - return Did.keyToResolvedKey(document.authentication[0], did) + async function dereferenceDidUrl( + didUrl: DidUrl | 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' }, + } + } + } } beforeAll(async () => { @@ -772,10 +838,7 @@ describe('create presentation', () => { newKeyForMigratedClaimerDid = makeSigningKeyTool() migratedClaimerFullDid = createMinimalFullDidFromLightDid( migratedClaimerLightDid, - { - ...newKeyForMigratedClaimerDid.authentication[0], - id: '#new-auth', - } + { ...newKeyForMigratedClaimerDid.keypair } ) migratedThenDeletedKey = makeSigningKeyTool('ed25519') migratedThenDeletedClaimerLightDid = Did.createLightDidDocument({ @@ -790,7 +853,7 @@ describe('create presentation', () => { name: 'Peter', age: 12, }, - migratedClaimerFullDid.uri + migratedClaimerFullDid.id ) ) @@ -799,7 +862,7 @@ describe('create presentation', () => { .mockResolvedValue( ApiMocks.mockChainQueryReturn('attestation', 'attestations', { revoked: false, - attester: Did.toChain(attester.uri), + attester: Did.toChain(attester.id), ctypeHash: CType.idToHash(ctype.$id), } as any) as any ) @@ -817,9 +880,9 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) - ).resolves.toMatchObject({ revoked: false, attester: attester.uri }) + ).resolves.toMatchObject({ revoked: false, attester: attester.id }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) it('should create presentation and exclude specific attributes using a light DID', async () => { @@ -831,7 +894,7 @@ describe('create presentation', () => { name: 'Peter', age: 12, }, - unmigratedClaimerLightDid.uri + unmigratedClaimerLightDid.id ) ) @@ -846,9 +909,9 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) - ).resolves.toMatchObject({ revoked: false, attester: attester.uri }) + ).resolves.toMatchObject({ revoked: false, attester: attester.id }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) it('should create presentation and exclude specific attributes using a migrated DID', async () => { @@ -861,7 +924,7 @@ describe('create presentation', () => { age: 12, }, // Use of light DID in the claim. - migratedClaimerLightDid.uri + migratedClaimerLightDid.id ) ) @@ -877,9 +940,9 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) - ).resolves.toMatchObject({ revoked: false, attester: attester.uri }) + ).resolves.toMatchObject({ revoked: false, attester: attester.id }) expect(presentation.claimerSignature?.challenge).toEqual(challenge) }) @@ -893,7 +956,7 @@ describe('create presentation', () => { age: 12, }, // Use of light DID in the claim. - migratedClaimerLightDid.uri + migratedClaimerLightDid.id ) ) @@ -909,7 +972,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(att, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow() }) @@ -924,7 +987,7 @@ describe('create presentation', () => { age: 12, }, // Use of light DID in the claim. - migratedThenDeletedClaimerLightDid.uri + migratedThenDeletedClaimerLightDid.id ) ) @@ -940,7 +1003,7 @@ describe('create presentation', () => { }) await expect( Credential.verifyPresentation(presentation, { - didResolveKey, + dereferenceDidUrl, }) ).rejects.toThrow() }) diff --git a/packages/legacy-credentials/src/Credential.ts b/packages/legacy-credentials/src/Credential.ts index e6f0c0dd5..54ed56cea 100644 --- a/packages/legacy-credentials/src/Credential.ts +++ b/packages/legacy-credentials/src/Credential.ts @@ -21,14 +21,13 @@ import { ConfigService } from '@kiltprotocol/config' import { Attestation, CType } from '@kiltprotocol/core' import { isDidSignature, - resolveKey, + dereference, signatureFromJson, signatureToJson, verifyDidSignature, } from '@kiltprotocol/did' import type { - DidResolveKey, - DidUri, + Did, Hash, IAttestation, ICType, @@ -37,6 +36,7 @@ import type { ICredentialPresentation, IDelegationNode, SignCallback, + DereferenceDidUrl, } from '@kiltprotocol/types' import { Crypto, DataUtils, SDKErrors } from '@kiltprotocol/utils' import * as Claim from './Claim.js' @@ -205,17 +205,17 @@ export function verifyDataStructure(input: ICredential): void { * * @param input - The [[ICredentialPresentation]]. * @param verificationOpts Additional verification options. - * @param verificationOpts.didResolveKey - The function used to resolve the claimer's key. Defaults to [[resolveKey]]. + * @param verificationOpts.dereferenceDidUrl - The function used to dereference the claimer's DID Document and verification method. Defaults to [[dereferenceDidUrl]]. * @param verificationOpts.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. */ export async function verifySignature( input: ICredentialPresentation, { challenge, - didResolveKey = resolveKey, + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], }: { challenge?: string - didResolveKey?: DidResolveKey + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } = {} ): Promise { const { claimerSignature } = input @@ -232,8 +232,9 @@ export async function verifySignature( expectedSigner: input.claim.owner, // allow full did to sign presentation if owned by corresponding light did allowUpgraded: true, - expectedVerificationMethod: 'authentication', - didResolveKey, + expectedVerificationRelationship: 'authentication', + signerUrl: claimerSignature.keyUri, + dereferenceDidUrl, }) } @@ -279,7 +280,7 @@ export function fromClaim( type VerifyOptions = { ctype?: ICType challenge?: string - didResolveKey?: DidResolveKey + dereferenceDidUrl?: DereferenceDidUrl['dereference'] } /** @@ -345,7 +346,7 @@ export function verifyAgainstAttestation( * @returns An object containing the `attester` DID and `revoked` status of the on-chain attestation. */ export async function verifyAttested(credential: ICredential): Promise<{ - attester: DidUri + attester: Did revoked: boolean }> { const api = ConfigService.get('api') @@ -365,7 +366,7 @@ export async function verifyAttested(credential: ICredential): Promise<{ export interface VerifiedCredential extends ICredential { revoked: boolean - attester: DidUri + attester: Did } /** @@ -432,17 +433,21 @@ export async function verifyCredential( * @param options - Additional parameter for more verification steps. * @param options.ctype - CType which the included claim should be checked against. * @param options.challenge - The expected value of the challenge. Verification will fail in case of a mismatch. - * @param options.didResolveKey - The function used to resolve the claimer's key. Defaults to [[resolveKey]]. + * @param options.dereferenceDidUrl - The function used to dereference the claimer's DID and verification method. Defaults to [[dereference]]. * @returns A [[VerifiedCredential]] object, which is the orignal credential presentation with two additional properties: * a boolean `revoked` status flag and the `attester` DID. */ export async function verifyPresentation( presentation: ICredentialPresentation, - { ctype, challenge, didResolveKey = resolveKey }: VerifyOptions = {} + { + ctype, + challenge, + dereferenceDidUrl = dereference as DereferenceDidUrl['dereference'], + }: VerifyOptions = {} ): Promise { await verifySignature(presentation, { challenge, - didResolveKey, + dereferenceDidUrl, }) return verifyCredential(presentation, { ctype }) } @@ -539,7 +544,7 @@ export async function createPresentation({ const signature = await signCallback({ data: makeSigningData(presentation, challenge), did: credential.claim.owner, - keyRelationship: 'authentication', + verificationRelationship: 'authentication', }) return { diff --git a/packages/types/src/AssetDid.ts b/packages/types/src/AssetDid.ts index ecb9a52ef..5f5c35439 100644 --- a/packages/types/src/AssetDid.ts +++ b/packages/types/src/AssetDid.ts @@ -40,4 +40,4 @@ export type Caip19AssetId = /** * A string containing an AssetDID as per the [AssetDID specification](https://github.com/KILTprotocol/spec-asset-did). */ -export type AssetDidUri = `did:asset:${Caip2ChainId}.${Caip19AssetId}` +export type AssetDid = `did:asset:${Caip2ChainId}.${Caip19AssetId}` diff --git a/packages/types/src/Attestation.ts b/packages/types/src/Attestation.ts index 0f42f8974..2b4d58270 100644 --- a/packages/types/src/Attestation.ts +++ b/packages/types/src/Attestation.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from './DidDocument' +import type { Did } from './Did' import type { IDelegationNode } from './Delegation' import type { ICredential } from './Credential' import type { CTypeHash } from './CType' @@ -13,7 +13,7 @@ import type { CTypeHash } from './CType' export interface IAttestation { claimHash: ICredential['rootHash'] cTypeHash: CTypeHash - owner: DidUri + owner: Did delegationId: IDelegationNode['id'] | null revoked: boolean } diff --git a/packages/types/src/Claim.ts b/packages/types/src/Claim.ts index 5e8691644..6a7b341ff 100644 --- a/packages/types/src/Claim.ts +++ b/packages/types/src/Claim.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { DidUri } from './DidDocument' +import type { Did } from './Did' type ClaimPrimitives = string | number | boolean @@ -20,7 +20,7 @@ export interface IClaimContents { export interface IClaim { cTypeHash: CTypeHash contents: IClaimContents - owner: DidUri + owner: Did } /** diff --git a/packages/types/src/Credential.ts b/packages/types/src/Credential.ts index ec2c0d467..cd63793ef 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 3bb8225b1..a48916696 100644 --- a/packages/types/src/CryptoCallbacks.ts +++ b/packages/types/src/CryptoCallbacks.ts @@ -6,11 +6,10 @@ */ import type { - DidResourceUri, - DidUri, - DidVerificationKey, - VerificationKeyRelationship, -} from './DidDocument.js' + Did, + SignatureVerificationRelationship, + VerificationMethod, +} from './Did' /** * Base interface for all signing requests. @@ -22,14 +21,14 @@ export interface SignRequestData { data: Uint8Array /** - * The did key relationship to be used. + * The DID verification relationship to be used. */ - keyRelationship: VerificationKeyRelationship + verificationRelationship: SignatureVerificationRelationship /** * The DID to be used for signing. */ - did: DidUri + did: Did } /** @@ -41,13 +40,9 @@ export interface SignResponseData { */ signature: Uint8Array /** - * The did key uri used for signing. + * The DID verification method used for signing. */ - keyUri: DidResourceUri - /** - * The did key type used for signing. - */ - keyType: DidVerificationKey['type'] + verificationMethod: VerificationMethod } /** @@ -62,7 +57,7 @@ export type SignCallback = ( */ export type SignExtrinsicCallback = ( signData: SignRequestData -) => Promise> +) => Promise /** * Base interface for encryption requests. @@ -79,7 +74,7 @@ export interface EncryptRequestData { /** * The DID to be used for encryption. */ - did: DidUri + did: Did } /** @@ -95,9 +90,9 @@ export interface EncryptResponseData { */ nonce: Uint8Array /** - * The did key uri used for the encryption. + * The DID verification method used for the encryption. */ - keyUri: DidResourceUri + verificationMethod: VerificationMethod } /** @@ -124,9 +119,9 @@ export interface DecryptRequestData { */ nonce: Uint8Array /** - * The did key uri, which should be used for decryption. + * The DID verification method, which should be used for decryption. */ - keyUri: DidResourceUri + verificationMethod: VerificationMethod } export interface DecryptResponseData { diff --git a/packages/types/src/Delegation.ts b/packages/types/src/Delegation.ts index 7049e3361..2139eb611 100644 --- a/packages/types/src/Delegation.ts +++ b/packages/types/src/Delegation.ts @@ -6,7 +6,7 @@ */ import type { CTypeHash } from './CType' -import type { DidUri } from './DidDocument' +import type { Did } from './Did' /* eslint-disable no-bitwise */ export const Permission = { @@ -20,7 +20,7 @@ export interface IDelegationNode { hierarchyId: IDelegationNode['id'] parentId?: IDelegationNode['id'] childrenIds: Array - account: DidUri + account: Did permissions: PermissionType[] revoked: boolean } diff --git a/packages/types/src/Did.ts b/packages/types/src/Did.ts new file mode 100644 index 000000000..fc0d458a0 --- /dev/null +++ b/packages/types/src/Did.ts @@ -0,0 +1,103 @@ +/** + * 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 deleted file mode 100644 index 9bf9a7502..000000000 --- a/packages/types/src/DidDocument.ts +++ /dev/null @@ -1,176 +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 { 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 deleted file mode 100644 index 9b675e23e..000000000 --- a/packages/types/src/DidDocumentExporter.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2018-2023, BOTLabs GmbH. - * - * This source code is licensed under the BSD 4-Clause "Original" license - * found in the LICENSE file in the root directory of this source tree. - */ - -import { - DidResourceUri, - DidServiceEndpoint, - DidUri, - EncryptionKeyType, - VerificationKeyType, -} from './DidDocument.js' -import { DidResolutionDocumentMetadata } from './DidResolver.js' - -export type ConformingDidDocumentKeyType = - | 'Ed25519VerificationKey2018' - | 'Sr25519VerificationKey2020' - | 'EcdsaSecp256k1VerificationKey2019' - | 'X25519KeyAgreementKey2019' - -export const verificationKeyTypesMap: Record< - VerificationKeyType, - ConformingDidDocumentKeyType -> = { - // proposed and used by dock.io, e.g. https://github.com/w3c-ccg/security-vocab/issues/32, https://github.com/docknetwork/sdk/blob/9c818b03bfb4fdf144c20678169c7aad3935ad96/src/utils/vc/contexts/security_context.js - sr25519: 'Sr25519VerificationKey2020', - // these are part of current w3 security vocab, see e.g. https://www.w3.org/ns/did/v1 - ed25519: 'Ed25519VerificationKey2018', - ecdsa: 'EcdsaSecp256k1VerificationKey2019', -} - -export const encryptionKeyTypesMap: Record< - EncryptionKeyType, - ConformingDidDocumentKeyType -> = { - x25519: 'X25519KeyAgreementKey2019', -} - -/** - * A spec-compliant description of a DID key. - */ -export type ConformingDidKey = { - /** - * The full key URI, in the form of #. - */ - id: DidResourceUri - /** - * The key controller, in the form of . - */ - controller: DidUri - /** - * The base58-encoded public component of the key. - */ - publicKeyBase58: string - /** - * The key type signalling the intended signing/encryption algorithm for the use of this key. - */ - type: ConformingDidDocumentKeyType -} - -/** - * A spec-compliant description of a DID endpoint. - */ -export type ConformingDidServiceEndpoint = Omit & { - /** - * The full service URI, in the form of #. - */ - id: DidResourceUri -} - -/** - * A DID Document according to the [W3C DID Core specification](https://www.w3.org/TR/did-core/). - */ -export type ConformingDidDocument = { - id: DidUri - verificationMethod: ConformingDidKey[] - authentication: [ConformingDidKey['id']] - assertionMethod?: [ConformingDidKey['id']] - keyAgreement?: [ConformingDidKey['id']] - capabilityDelegation?: [ConformingDidKey['id']] - service?: ConformingDidServiceEndpoint[] - alsoKnownAs?: [`w3n:${string}`] -} - -/** - * A JSON+LD DID Document that extends a traditional DID Document with additional semantic information. - */ -export type JsonLDDidDocument = ConformingDidDocument & { '@context': string[] } - -/** - * DID Resolution Metadata returned by the DID `resolve` function as described by DID specifications (https://www.w3.org/TR/did-core/#did-resolution-metadata). - */ -export interface DidResolutionMetadata { - error?: 'notFound' | 'invalidDid' - errorMessage?: string -} - -/** - * Object containing the return values of the DID `resolve` function as described by DID specifications (https://www.w3.org/TR/did-core/#did-resolution). - */ -export interface ConformingDidResolutionResult { - didDocumentMetadata: Partial - didResolutionMetadata: DidResolutionMetadata - didDocument?: Partial & - Pick -} diff --git a/packages/types/src/DidResolver.ts b/packages/types/src/DidResolver.ts index fd74dc237..9fc56d9b6 100644 --- a/packages/types/src/DidResolver.ts +++ b/packages/types/src/DidResolver.ts @@ -5,74 +5,250 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { - ConformingDidKey, - ConformingDidServiceEndpoint, -} from './DidDocumentExporter.js' import type { + Did, DidDocument, - DidKey, - DidResourceUri, - DidUri, - KeyRelationship, -} from './DidDocument.js' + DidUrl, + VerificationMethod, + Service, + JsonLd, +} from './Did' /** - * DID resolution metadata that includes a subset of the properties defined in the [W3C proposed standard](https://www.w3.org/TR/did-core/#did-resolution). + * The `accept` header must not be used for the regular `resolve` function, so we enforce that statically. + * For more info, please refer to https://www.w3.org/TR/did-core/#did-resolution-options. */ -export type DidResolutionDocumentMetadata = { +export type ResolutionOptions = Record + +export type ResolutionMetadata = { /** - * If present, it indicates that the resolved by DID should be treated as if it were the DID as specified in this property. + * 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. */ - canonicalId?: DidUri + error?: 'invalidDid' | 'notFound' +} + +export type ResolutionDocumentMetadata = { /** - * A boolean flag indicating whether the resolved DID has been deactivated. + * 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: boolean + 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 } -/** - * The result of a DID resolution. - * - * It includes the DID Document, and optional document resolution metadata. - */ -export type DidResolutionResult = { +export type ResolutionResult = { /** - * The resolved DID document. It is undefined if the DID has been upgraded or deleted. + * 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. */ - document?: DidDocument + didResolutionMetadata: ResolutionMetadata /** - * The DID resolution metadata. + * 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. */ - metadata: DidResolutionDocumentMetadata + didDocument?: DidDocument /** - * The DID's web3Name, if any. + * 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. */ - web3Name?: string + didDocumentMetadata: ResolutionDocumentMetadata } -export type ResolvedDidKey = Pick & - Pick +export type RepresentationResolutionOptions = { + /** + * The Media Type of the caller's preferred representation of the DID document. + * The Media Type MUST be expressed as an ASCII string. + * The DID resolver implementation SHOULD use this value to determine the representation contained in the returned didDocumentStream if such a representation is supported and available. + * This property is OPTIONAL for the resolveRepresentation function and MUST NOT be used with the resolve function. + */ + accept?: Accept +} -export type ResolvedDidServiceEndpoint = ConformingDidServiceEndpoint +export type SuccessfulRepresentationResolutionMetadata< + ContentType extends string = string +> = { + /** + * The Media Type of the returned didDocumentStream. + * This property is REQUIRED if resolution is successful and if the resolveRepresentation function was called. + * This property MUST NOT be present if the resolve function was called. + * The value of this property MUST be an ASCII string that is the Media Type of the conformant representations. + * The caller of the resolveRepresentation function MUST use this value when determining how to parse and process the didDocumentStream returned by this function into the data model. + */ + contentType: ContentType +} +export type FailedRepresentationResolutionMetadata = { + /** + * The error code from the resolution process. + * This property is REQUIRED when there is an error in the resolution process. + * The value of this property MUST be a single keyword ASCII string. + * The possible property values of this field SHOULD be registered in the DID Specification Registries. + * This specification defines the following common error values: + * * invalidDid: The DID supplied to the DID resolution function does not conform to valid syntax. + * * notFound: The DID resolver was unable to find the DID document resulting from this resolution request. + * * representationNotSupported: This error code is returned if the representation requested via the accept input metadata property is not supported by the DID method and/or DID resolver implementation. + */ + error: 'invalidDid' | 'notFound' | 'representationNotSupported' +} -/** - * Resolves a DID URI, returning the full contents of the DID document. - * - * @param did A DID URI identifying a DID document. All additional parameters and fragments are ignored. - * @returns A promise of a [[DidResolutionResult]] object representing the DID document or null if the DID - * cannot be resolved. - */ -export type DidResolve = (did: DidUri) => Promise +// Either success with `contentType` or failure with `error` +export type RepresentationResolutionMetadata< + ContentType extends string = string +> = + | SuccessfulRepresentationResolutionMetadata + | FailedRepresentationResolutionMetadata + +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 +} /** - * Resolves a DID URI identifying a public key associated with a DID. - * - * @param didUri A DID URI identifying a public key associated with a DID through the DID document. - * @returns A promise of a [[ResolvedDidKey]] object representing the DID public key or null if - * the DID or key URI cannot be resolved. + * The resolve function returns the DID document in its abstract form (a map). */ -export type DidResolveKey = ( - didUri: DidResourceUri, - expectedVerificationMethod?: KeyRelationship -) => Promise +export interface ResolveDid { + resolve: ( + /** + * This is the DID to resolve. + * This input is REQUIRED and the value MUST be a conformant DID as defined in 3.1 DID Syntax. + */ + did: 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 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 {} diff --git a/packages/types/src/PublicCredential.ts b/packages/types/src/PublicCredential.ts index 0b3ba75db..25f8d6714 100644 --- a/packages/types/src/PublicCredential.ts +++ b/packages/types/src/PublicCredential.ts @@ -9,11 +9,11 @@ import type { HexString, BN } from './Imported' import type { CTypeHash } from './CType' import type { IDelegationNode } from './Delegation' import type { IClaimContents } from './Claim' -import type { DidUri } from './DidDocument' -import type { AssetDidUri } from './AssetDid' +import type { Did } from './Did' +import type { AssetDid } from './AssetDid' /* - * The minimal information required to issue a public credential to a given [[AssetDidUri]]. + * The minimal information required to issue a public credential to a given [[AssetDid]]. */ export interface IPublicCredentialInput { /* @@ -27,7 +27,7 @@ export interface IPublicCredentialInput { /* * The subject of the credential. */ - subject: AssetDidUri + subject: AssetDid /* * The content of the credential. The structure must match what the CType specifies. */ @@ -41,13 +41,13 @@ export interface IPublicCredential extends IPublicCredentialInput { /* * The unique ID of the credential. It is cryptographically derived from the credential content. * - * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[DidUri]] and then Blake2b hashing the result. + * The ID is formed by first concatenating the SCALE-encoded [[IPublicCredentialInput]] with the SCALE-encoded [[Did]] and then Blake2b hashing the result. */ id: HexString /* - * The KILT DID uri of the credential attester. + * The KILT DID of the credential attester. */ - attester: DidUri + attester: Did /* * The block number at which the credential was issued. */ @@ -63,12 +63,12 @@ export interface IPublicCredential extends IPublicCredentialInput { /* * A claim for a public credential. * - * Like an [[IClaim]], but with a [[AssetDidUri]] `subject` instead of an [[IClaim]] `owner`. + * Like an [[IClaim]], but with a [[AssetDid]] `subject` instead of an [[IClaim]] `owner`. */ export interface IAssetClaim { cTypeHash: CTypeHash contents: IClaimContents - subject: AssetDidUri + subject: AssetDid } /** diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index be510ee7a..83fde92b1 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -21,9 +21,8 @@ export * from './Deposit.js' export * from './Delegation.js' export * from './Address.js' export * from './Credential.js' -export * from './DidDocument.js' +export * from './Did.js' export * from './CryptoCallbacks.js' export * from './DidResolver.js' -export * from './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 c9673eba5..cf9bec169 100644 --- a/packages/utils/src/SDKErrors.ts +++ b/packages/utils/src/SDKErrors.ts @@ -116,7 +116,7 @@ export class SignatureMalformedError extends SDKError {} export class DidSubjectMismatchError extends SDKError { constructor(actual: string, expected: string) { super( - `The DID "${actual}" doesn't match the DID Document's URI "${expected}"` + `The DID "${actual}" doesn't match the DID Document's id "${expected}"` ) } } diff --git a/packages/vc-export/src/documentLoader.ts b/packages/vc-export/src/documentLoader.ts index b66cf0f90..ef2db0cbd 100644 --- a/packages/vc-export/src/documentLoader.ts +++ b/packages/vc-export/src/documentLoader.ts @@ -8,21 +8,23 @@ // @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, - resolveCompliant, + resolve as resolveDid, W3C_DID_CONTEXT_URL, + multibaseKeyToDidKey, } from '@kiltprotocol/did' import type { - ConformingDidDocument, - ConformingDidKey, - DidUri, + DidDocument, + Did, ICType, + VerificationMethod, } from '@kiltprotocol/types' - import { CType } from '@kiltprotocol/core' + import { validationContexts } from './context/index.js' import { Sr25519VerificationKey2020 } from './suites/Sr25519VerificationKey.js' @@ -78,17 +80,62 @@ export const kiltContextsLoader: DocumentLoader = async (url) => { throw new Error(`not a known Kilt context: ${url}`) } +type LegacyVerificationMethodType = + | 'Sr25519VerificationKey2020' + | 'Ed25519VerificationKey2018' + | 'EcdsaSecp256k1VerificationKey2019' + | 'X25519KeyAgreementKey2019' +type LegacyVerificationMethod = Pick< + VerificationMethod, + 'id' | 'controller' +> & { publicKeyBase58: string; type: LegacyVerificationMethodType } + +// Returns legacy representations of a KILT DID verification method. export const kiltDidLoader: DocumentLoader = async (url) => { - const { did } = parse(url as DidUri) - const { 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 ?? {}, + 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, { // add did contexts to make sure we get a compacted representation '@context': [W3C_DID_CONTEXT_URL, KILT_DID_CONTEXT_URL], @@ -104,30 +151,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 ConformingDidDocument | ConformingDidKey + )) as DidDocument | VerificationMethod // The signature suites expect key-related json-LD contexts; we add them here - switch ((document as { type: string }).type) { + switch ((jsonLdDocument as { type: string }).type) { // these 4 are currently used case Sr25519VerificationKey2020.suite: - document['@context'].push(Sr25519VerificationKey2020.SUITE_CONTEXT) + jsonLdDocument['@context'].push(Sr25519VerificationKey2020.SUITE_CONTEXT) break case 'Ed25519VerificationKey2018': - document['@context'].push( + jsonLdDocument['@context'].push( 'https://w3id.org/security/suites/ed25519-2018/v1' ) break case 'EcdsaSecp256k1VerificationKey2019': - document['@context'].push('https://w3id.org/security/v1') + jsonLdDocument['@context'].push('https://w3id.org/security/v1') break case 'X25519KeyAgreementKey2019': - document['@context'].push( + jsonLdDocument['@context'].push( 'https://w3id.org/security/suites/x25519-2019/v1' ) break default: break } - return { contextUrl: undefined, documentUrl: url, document } + return { contextUrl: undefined, documentUrl: url, document: jsonLdDocument } } const loader = CType.newCachingCTypeLoader() diff --git a/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts b/packages/vc-export/src/suites/KiltAttestationProofV1.spec.ts index 783e0ff4e..704732e70 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 { - ConformingDidDocument, + DidDocument, HexString, ICType, KiltAddress, @@ -59,7 +59,7 @@ import { makeFakeDid } from './Sr25519Signature2020.spec' jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - resolveCompliant: jest.fn(), + resolve: jest.fn(), authorizeTx: jest.fn(), })) @@ -154,7 +154,7 @@ let suite: KiltAttestationV1Suite let purpose: KiltAttestationProofV1Purpose let proof: Types.KiltAttestationProofV1 let keypair: KiltKeyringPair -let didDocument: ConformingDidDocument +let didDocument: DidDocument 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.authentication[0], + id: didDocument.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.uri + lightDid.authentication[0].id, + id: lightDid.id + lightDid.authentication?.[0], } const signingSuite = new Ed25519Signature2020({ signer: edSigner }) @@ -368,7 +368,7 @@ describe('vc-js', () => { let presentation = vcjs.createPresentation({ verifiableCredential: attestedVc, - holder: lightDid.uri, + holder: lightDid.id, }) presentation = await vcjs.signPresentation({ @@ -450,7 +450,12 @@ describe('issuance', () => { did: attestedVc.issuer, signer: async () => ({ signature: new Uint8Array(32), - keyType: 'sr25519' as const, + verificationMethod: { + controller: attestedVc.issuer, + type: 'Multikey', + id: '#test', + publicKeyMultibase: 'zasd', + }, }), } 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 8916a5417..fe7050687 100644 --- a/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts +++ b/packages/vc-export/src/suites/Sr25519Signature2020.spec.ts @@ -8,12 +8,13 @@ // @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 { - ConformingDidDocument, - DidUri, + DidDocument, + Did as KiltDid, KiltKeyringPair, } from '@kiltprotocol/types' @@ -31,7 +32,7 @@ jest.mock('@digitalbazaar/http-client', () => ({})) jest.mock('@kiltprotocol/did', () => ({ ...jest.requireActual('@kiltprotocol/did'), - resolveCompliant: jest.fn(), + resolve: jest.fn(), })) const documentLoader = combineDocumentLoaders([ @@ -43,35 +44,37 @@ const documentLoader = combineDocumentLoaders([ export async function makeFakeDid() { await init() const keypair = Crypto.makeKeypairFromUri('//Ingo', 'sr25519') - 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) => { + 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) => { 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 { @@ -82,7 +85,7 @@ export async function makeFakeDid() { return { didDocument, keypair } } -let didDocument: ConformingDidDocument +let didDocument: DidDocument let keypair: KiltKeyringPair beforeAll(async () => { @@ -92,7 +95,7 @@ beforeAll(async () => { it('issues and verifies a signed credential', async () => { const signer = { sign: async ({ data }: { data: Uint8Array }) => keypair.sign(data), - id: didDocument.assertionMethod![0], + id: didDocument.id + didDocument.assertionMethod![0], } const attestationSigner = new Sr25519Signature2020({ signer }) @@ -117,13 +120,36 @@ 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({ - ...didDocument.verificationMethod.find(({ id }) => - id.includes('assertion') - )!, + ...assertionMethod, }), }), documentLoader, @@ -135,9 +161,7 @@ it('issues and verifies a signed credential', async () => { credential: verifiableCredential, suite: new Sr25519Signature2020({ key: new Sr25519VerificationKey2020({ - ...didDocument.verificationMethod.find(({ id }) => - id.includes('authentication') - )!, + ...authenticationMethod, }), }), documentLoader, diff --git a/packages/vc-export/src/suites/types.ts b/packages/vc-export/src/suites/types.ts index d14ae644d..7e5362515 100644 --- a/packages/vc-export/src/suites/types.ts +++ b/packages/vc-export/src/suites/types.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import type { DidUri } from '@kiltprotocol/types' +import type { Did } from '@kiltprotocol/types' export interface JSigsSigner { sign: (data: { data: Uint8Array }) => Promise @@ -24,5 +24,5 @@ export interface JSigsVerificationResult { verified: boolean error?: Error purposeResult?: { verified: boolean; error?: Error } - verificationMethod?: { id: string; type: string; controller: DidUri } + verificationMethod?: { id: string; type: string; controller: Did } } diff --git a/tests/breakingChanges/BreakingChanges.spec.ts b/tests/breakingChanges/BreakingChanges.spec.ts index 0f97a6025..7c473b709 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 uri generation', () => { + it('does not break the light did generation', () => { const { did } = makeLightDidFromSeed( '0x127f2375faf3472c2f94ffcdd5424590b27294631f2cb8041407e501bc97c44c' ) - expect(did.uri).toMatchInlineSnapshot( + expect(did.id).toMatchInlineSnapshot( `"did:kilt:light:004quk8nu1MLvzdoT4fE6SJsLS4fFpyvuGz7sQpMF7ZAWTDoF5:z1msTRicERqs59nwMvp3yzMRBhUYGmkum7ehY7rtKQc8HzfEx4b4eyRhrc37ZShT3oG7E89x89vaG9W4hRxPS23EAFnCSeVbVRrKGJmFQvYhjgKSMmrGC7gSxgHe1a3g41uamhD49AEi13YVMkgeHpyEQJBy7N7gGyW7jTWFcwzAnws4wSazBVG1qHmVJrhmusoJoTfKTPKXkExKyur8Z341EkcRkHteY8dV3VjLXHnfhRW2yU9oM2cRm5ozgaufxrXsQBx33ygTW2wvrfzzXsYw4Bs6Vf2tC3ipBTDcKyCk6G88LYnzBosRM15W3KmDRciJ2iPjqiQkhYm77EQyaw"` ) diff --git a/tests/bundle/bundle-test.ts b/tests/bundle/bundle-test.ts index bbb852e16..7312b3193 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,16 +37,18 @@ function makeSignCallback( keypair: KeyringPair ): (didDocument: DidDocument) => SignCallback { return (didDocument) => { - return async function sign({ data, keyRelationship }) { - const keyId = didDocument[keyRelationship]?.[0].id - const keyType = didDocument[keyRelationship]?.[0].type - if (keyId === undefined || keyType === undefined) { + return async function sign({ data, verificationRelationship }) { + const authKeyId = didDocument[verificationRelationship]?.[0] + const authKey = didDocument.verificationMethod?.find( + ({ id }) => id === authKeyId + ) + if (authKeyId === undefined || authKey === undefined) { throw new Error( - `Key for purpose "${keyRelationship}" not found in did "${didDocument.uri}"` + `No verification method for purpose "${verificationRelationship}" found in DID "${didDocument.id}"` ) } const signature = keypair.sign(data, { withType: false }) - return { signature, keyUri: `${didDocument.uri}${keyId}`, keyType } + return { signature, verificationMethod: authKey } } } } @@ -58,7 +60,9 @@ function makeStoreDidCallback(keypair: KiltKeyringPair): StoreDidCallback { const signature = keypair.sign(data, { withType: false }) return { signature, - keyType: keypair.type, + verificationMethod: { + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), + }, } } } @@ -115,7 +119,11 @@ async function createFullDidFromKeypair( const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( - Did.toChain(Did.getFullDidUriFromKey(keypair)) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), + }) + ) ) return Did.linkedInfoFromChain(encodedDidDetails).document } @@ -166,7 +174,7 @@ async function runAll() { keyAgreement: [{ publicKey: encPublicKey, type: 'x25519' }], }) if ( - testDid.uri !== + testDid.id !== `did:kilt:light:01${address}:z1Ac9CMtYCTRWjetJfJqJoV7FcPDD9nHPHDHry7t3KZmvYe1HQP1tgnBuoG3enuGaowpF8V88sCxytDPDy6ZxhW` ) { throw new Error('DID Test Unsuccessful') @@ -188,15 +196,18 @@ async function runAll() { const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid const encodedDidDetails = await queryFunction( - Did.toChain(Did.getFullDidUriFromKey(keypair)) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), + }) + ) ) const fullDid = Did.linkedInfoFromChain(encodedDidDetails).document - const resolved = await Did.resolve(fullDid.uri) + const resolved = await Did.resolve(fullDid.id) if ( - resolved && - !resolved.metadata.deactivated && - resolved.document?.uri === fullDid.uri + !resolved.didDocumentMetadata.deactivated && + resolved.didDocument?.id === fullDid.id ) { console.info('DID matches') } else { @@ -204,15 +215,15 @@ async function runAll() { } const deleteTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, api.tx.did.delete(BalanceUtils.toFemtoKilt(0)), getSignCallback(fullDid), payer.address ) await Blockchain.signAndSubmitTx(deleteTx, payer) - const resolvedAgain = await Did.resolve(fullDid.uri) - if (!resolvedAgain || resolvedAgain.metadata.deactivated) { + const resolvedAgain = await Did.resolve(fullDid.id) + if (resolvedAgain.didDocumentMetadata.deactivated) { console.info('DID successfully deleted') } else { throw new Error('DID was not deleted') @@ -230,7 +241,7 @@ async function runAll() { }) const cTypeStoreTx = await Did.authorizeTx( - alice.uri, + alice.id, api.tx.ctype.add(CType.toChain(DriversLicense)), aliceSign(alice), payer.address @@ -247,8 +258,8 @@ async function runAll() { const credential = KiltCredentialV1.fromInput({ cType: DriversLicense.$id, claims: content, - subject: bob.uri, - issuer: alice.uri, + subject: bob.id, + issuer: alice.id, }) await KiltCredentialV1.validateSubject(credential, { @@ -259,13 +270,13 @@ async function runAll() { if ( credential.credentialSubject.name !== content.name || credential.credentialSubject.age !== content.age || - credential.credentialSubject.id !== bob.uri + credential.credentialSubject.id !== bob.id ) { throw new Error('Claim content inside Credential mismatching') } const issued = await KiltAttestationProofV1.issue(credential, { - didSigner: { did: alice.uri, signer: aliceSign(alice) }, + didSigner: { did: alice.id, signer: aliceSign(alice) }, transactionHandler: { account: payer.address, signAndSubmit: async (tx) => { @@ -291,7 +302,7 @@ async function runAll() { await KiltRevocationStatusV1.check(issued) console.info('Credential status verified') - const presentation = Presentation.create([issued], bob.uri) + const presentation = Presentation.create([issued], bob.id) console.info('Presentation created') Presentation.validateStructure(presentation) diff --git a/tests/integration/AccountLinking.spec.ts b/tests/integration/AccountLinking.spec.ts index 51b9da0ae..61badec16 100644 --- a/tests/integration/AccountLinking.spec.ts +++ b/tests/integration/AccountLinking.spec.ts @@ -65,7 +65,7 @@ describe('When there is an on-chain DID', () => { const associateSenderTx = api.tx.didLookup.associateSender() const signedTx = await Did.authorizeTx( - did.uri, + did.id, associateSenderTx, didKey.getSignCallback(did), paymentAccount.address @@ -92,12 +92,12 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([paymentAccount.address]) - expect(queryByAccount.document.uri).toStrictEqual(did.uri) + expect(queryByAccount.document.id).toStrictEqual(did.id) }, 30_000) it('should be possible to associate the tx sender to a new DID', async () => { const associateSenderTx = api.tx.didLookup.associateSender() const signedTx = await Did.authorizeTx( - newDid.uri, + newDid.id, associateSenderTx, newDidKey.getSignCallback(newDid), paymentAccount.address @@ -120,7 +120,7 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([paymentAccount.address]) - expect(queryByAccount.document.uri).toStrictEqual(newDid.uri) + expect(queryByAccount.document.id).toStrictEqual(newDid.id) }, 30_000) it('should be possible for the sender to remove the link', async () => { const removeSenderTx = api.tx.didLookup.removeSenderAssociation() @@ -174,11 +174,11 @@ describe('When there is an on-chain DID', () => { } const args = await Did.associateAccountToChainArgs( keypair.address, - did.uri, + did.id, async (payload) => keypair.sign(payload, { withType: false }) ) const signedTx = await Did.authorizeTx( - did.uri, + did.id, api.tx.didLookup.associateAccount(...args), didKey.getSignCallback(did), paymentAccount.address @@ -204,7 +204,7 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([keypair.address]) - expect(queryByAccount.document.uri).toStrictEqual(did.uri) + expect(queryByAccount.document.id).toStrictEqual(did.id) }) it('should be possible to associate the account to a new DID while the sender pays the deposit', async () => { if (skip) { @@ -212,11 +212,11 @@ describe('When there is an on-chain DID', () => { } const args = await Did.associateAccountToChainArgs( keypair.address, - newDid.uri, + newDid.id, async (payload) => keypair.sign(payload, { withType: false }) ) const signedTx = await Did.authorizeTx( - newDid.uri, + newDid.id, api.tx.didLookup.associateAccount(...args), newDidKey.getSignCallback(newDid), paymentAccount.address @@ -239,7 +239,7 @@ describe('When there is an on-chain DID', () => { ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) expect(queryByAccount.accounts).toStrictEqual([keypair.address]) - expect(queryByAccount.document.uri).toStrictEqual(newDid.uri) + expect(queryByAccount.document.id).toStrictEqual(newDid.id) }) it('should be possible for the DID to remove the link', async () => { if (skip) { @@ -248,7 +248,7 @@ describe('When there is an on-chain DID', () => { const removeLinkTx = api.tx.didLookup.removeAccountAssociation(keypairChain) const signedTx = await Did.authorizeTx( - newDid.uri, + newDid.id, removeLinkTx, newDidKey.getSignCallback(newDid), paymentAccount.address @@ -271,7 +271,7 @@ describe('When there is an on-chain DID', () => { ) expect(encodedQueryByAccount.isNone).toBe(true) const encodedQueryByDid = await api.call.did.query( - Did.toChain(newDid.uri) + Did.toChain(newDid.id) ) const queryByDid = Did.linkedInfoFromChain(encodedQueryByDid) expect(queryByDid.accounts).toStrictEqual([]) @@ -299,11 +299,11 @@ describe('When there is an on-chain DID', () => { it('should be possible to associate the account while the sender pays the deposit', async () => { const args = await Did.associateAccountToChainArgs( genericAccount.address, - did.uri, + did.id, async (payload) => genericAccount.sign(payload, { withType: true }) ) const signedTx = await Did.authorizeTx( - did.uri, + did.id, api.tx.didLookup.associateAccount(...args), didKey.getSignCallback(did), paymentAccount.address @@ -330,13 +330,13 @@ describe('When there is an on-chain DID', () => { // Use generic substrate address prefix const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount, 42) expect(queryByAccount.accounts).toStrictEqual([genericAccount.address]) - expect(queryByAccount.document.uri).toStrictEqual(did.uri) + expect(queryByAccount.document.id).toStrictEqual(did.id) }) it('should be possible to add a Web3 name for the linked DID and retrieve it starting from the linked account', async () => { const web3NameClaimTx = api.tx.web3Names.claim('test-name') const signedTx = await Did.authorizeTx( - did.uri, + did.id, web3NameClaimTx, didKey.getSignCallback(did), paymentAccount.address @@ -346,13 +346,15 @@ describe('When there is an on-chain DID', () => { // Check that the Web3 name has been linked to the DID const encodedQueryByW3n = await api.call.did.queryByWeb3Name('test-name') const queryByW3n = Did.linkedInfoFromChain(encodedQueryByW3n) - expect(queryByW3n.document.uri).toStrictEqual(did.uri) + expect(queryByW3n.document.id).toStrictEqual(did.id) // Check that it is possible to retrieve the web3 name from the account linked to the DID const encodedQueryByAccount = await api.call.did.queryByAccount( Did.accountToChain(genericAccount.address) ) const queryByAccount = Did.linkedInfoFromChain(encodedQueryByAccount) - expect(queryByAccount.web3Name).toStrictEqual('test-name') + expect(queryByAccount.document.alsoKnownAs).toStrictEqual([ + 'w3n: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 67690ea49..ed2340558 100644 --- a/tests/integration/Attestation.spec.ts +++ b/tests/integration/Attestation.spec.ts @@ -77,7 +77,7 @@ describe('handling attestations that do not exist', () => { it('Attestation.getRevokeTx', async () => { const draft = api.tx.attestation.revoke(claimHash, null) const authorized = await Did.authorizeTx( - attester.uri, + attester.id, draft, attesterKey.getSignCallback(attester), tokenHolder.address @@ -91,7 +91,7 @@ describe('handling attestations that do not exist', () => { it('Attestation.getRemoveTx', async () => { const draft = api.tx.attestation.remove(claimHash, null) const authorized = await Did.authorizeTx( - attester.uri, + attester.id, draft, attesterKey.getSignCallback(attester), tokenHolder.address @@ -108,7 +108,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const ctypeExists = await isCtypeOnChain(driversLicenseCType) if (ctypeExists) return const tx = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.ctype.add(CType.toChain(driversLicenseCType)), attesterKey.getSignCallback(attester), tokenHolder.address @@ -121,7 +121,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim) const presentation = await Credential.createPresentation({ @@ -141,7 +141,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim) expect(() => Credential.verifyDataIntegrity(credential)).not.toThrow() @@ -157,7 +157,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const attestation = Attestation.fromCredentialAndDid( presentation, - attester.uri + attester.id ) const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -165,7 +165,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -180,7 +180,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { await expect( Credential.verifyPresentation(presentation) - ).resolves.toMatchObject({ attester: attester.uri, revoked: false }) + ).resolves.toMatchObject({ attester: attester.id, revoked: false }) // Claim the deposit back by submitting the reclaimDeposit extrinsic with the deposit payer's account. const reclaimTx = api.tx.attestation.reclaimDeposit(attestation.claimHash) @@ -202,7 +202,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim) expect(() => Credential.verifyDataIntegrity(credential)).not.toThrow() @@ -217,7 +217,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const attestation = Attestation.fromCredentialAndDid( presentation, - attester.uri + attester.id ) const { keypair, getSignCallback } = makeSigningKeyTool() @@ -227,7 +227,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, getSignCallback(attester), keypair.address @@ -254,15 +254,11 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const content = { name: 'Ralph', weight: 120 } - const claim = Claim.fromCTypeAndClaimContents( - badCtype, - content, - claimer.uri - ) + const claim = Claim.fromCTypeAndClaimContents(badCtype, content, claimer.id) const credential = Credential.fromClaim(claim) const attestation = Attestation.fromCredentialAndDid( credential, - attester.uri + attester.id ) const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -270,7 +266,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -293,21 +289,21 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) credential = Credential.fromClaim(claim) const presentation = await Credential.createPresentation({ credential, signCallback: claimerKey.getSignCallback(claimer), }) - attestation = Attestation.fromCredentialAndDid(credential, attester.uri) + attestation = Attestation.fromCredentialAndDid(credential, attester.id) const storeTx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -330,7 +326,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -349,7 +345,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const fakeCredential = Credential.fromClaim(claim) await Credential.createPresentation({ @@ -365,7 +361,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { it('should not be possible for the claimer to revoke an attestation', async () => { const revokeTx = api.tx.attestation.revoke(attestation.claimHash, null) const authorizedRevokeTx = await Did.authorizeTx( - claimer.uri, + claimer.id, revokeTx, claimerKey.getSignCallback(claimer), tokenHolder.address @@ -395,7 +391,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const revokeTx = api.tx.attestation.revoke(attestation.claimHash, null) const authorizedRevokeTx = await Did.authorizeTx( - attester.uri, + attester.id, revokeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -411,13 +407,13 @@ describe('When there is an attester, claimer and ctype drivers license', () => { await expect( Credential.verifyCredential(credential) - ).resolves.toMatchObject({ attester: attester.uri, revoked: true }) + ).resolves.toMatchObject({ attester: attester.id, revoked: true }) }, 40_000) it('should be possible for the deposit payer to remove an attestation', async () => { const removeTx = api.tx.attestation.remove(attestation.claimHash, null) const authorizedRemoveTx = await Did.authorizeTx( - attester.uri, + attester.id, removeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -446,7 +442,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { CType.toChain(officialLicenseAuthorityCType) ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -464,7 +460,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { LicenseType: "Driver's License", LicenseSubtypes: 'sports cars, tanks', }, - attester.uri + attester.id ) const credential1 = Credential.fromClaim(licenseAuthorization) await Credential.createPresentation({ @@ -473,7 +469,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const licenseAuthorizationGranted = Attestation.fromCredentialAndDid( credential1, - anotherAttester.uri + anotherAttester.id ) const storeTx = api.tx.attestation.add( licenseAuthorizationGranted.claimHash, @@ -481,7 +477,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx = await Did.authorizeTx( - anotherAttester.uri, + anotherAttester.id, storeTx, anotherAttesterKey.getSignCallback(anotherAttester), tokenHolder.address @@ -492,7 +488,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { const iBelieveICanDrive = Claim.fromCTypeAndClaimContents( driversLicenseCType, { name: 'Dominic Toretto', age: 52 }, - claimer.uri + claimer.id ) const credential2 = Credential.fromClaim(iBelieveICanDrive, { legitimations: [credential1], @@ -503,7 +499,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { }) const licenseGranted = Attestation.fromCredentialAndDid( credential2, - attester.uri + attester.id ) const storeTx2 = api.tx.attestation.add( licenseGranted.claimHash, @@ -511,7 +507,7 @@ describe('When there is an attester, claimer and ctype drivers license', () => { null ) const authorizedStoreTx2 = await Did.authorizeTx( - attester.uri, + attester.id, storeTx2, attesterKey.getSignCallback(attester), tokenHolder.address diff --git a/tests/integration/Ctypes.spec.ts b/tests/integration/Ctypes.spec.ts index 253a76ef8..52fdea9b2 100644 --- a/tests/integration/Ctypes.spec.ts +++ b/tests/integration/Ctypes.spec.ts @@ -55,7 +55,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const { keypair, getSignCallback } = makeSigningKeyTool() const storeTx = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx, getSignCallback(ctypeCreator), keypair.address @@ -71,7 +71,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const cType = makeCType() const storeTx = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx, key.getSignCallback(ctypeCreator), paymentAccount.address @@ -83,7 +83,7 @@ describe('When there is an CtypeCreator and a verifier', () => { cType.$id ) expect(originalCtype).toStrictEqual(cType) - expect(creator).toBe(ctypeCreator.uri) + expect(creator).toBe(ctypeCreator.id) await expect(CType.verifyStored(originalCtype)).resolves.not.toThrow() } }, 40_000) @@ -92,7 +92,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const cType = makeCType() const storeTx = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx, key.getSignCallback(ctypeCreator), paymentAccount.address @@ -101,7 +101,7 @@ describe('When there is an CtypeCreator and a verifier', () => { const storeTx2 = api.tx.ctype.add(CType.toChain(cType)) const authorizedStoreTx2 = await Did.authorizeTx( - ctypeCreator.uri, + ctypeCreator.id, storeTx2, key.getSignCallback(ctypeCreator), paymentAccount.address @@ -115,7 +115,7 @@ describe('When there is an CtypeCreator and a verifier', () => { if (hasBlockNumbers) { const retrievedCType = await CType.fetchFromChain(cType.$id) - expect(retrievedCType.creator).toBe(ctypeCreator.uri) + expect(retrievedCType.creator).toBe(ctypeCreator.id) } }, 45_000) diff --git a/tests/integration/Delegation.spec.ts b/tests/integration/Delegation.spec.ts index 51485ba0d..ecc1efd7d 100644 --- a/tests/integration/Delegation.spec.ts +++ b/tests/integration/Delegation.spec.ts @@ -57,14 +57,14 @@ async function writeHierarchy( sign: SignCallback ): Promise { const rootNode = DelegationNode.newRoot({ - account: delegator.uri, + account: delegator.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cTypeId), }) const storeTx = await rootNode.getStoreTx() const authorizedStoreTx = await Did.authorizeTx( - delegator.uri, + delegator.id, storeTx, sign, paymentAccount.address @@ -86,13 +86,13 @@ async function addDelegation( const delegationNode = DelegationNode.newNode({ hierarchyId, parentId, - account: delegate.uri, + account: delegate.id, permissions, }) const signature = await delegationNode.delegateSign(delegate, delegateSign) const storeTx = await delegationNode.getStoreTx(signature) const authorizedStoreTx = await Did.authorizeTx( - delegator.uri, + delegator.id, storeTx, delegatorSign, paymentAccount.address @@ -118,7 +118,7 @@ beforeAll(async () => { const storeTx = api.tx.ctype.add(CType.toChain(driversLicenseCType)) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), paymentAccount.address @@ -180,7 +180,7 @@ describe('and attestation rights have been delegated', () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, content, - claimer.uri + claimer.id ) const credential = Credential.fromClaim(claim, { delegationId: delegatedNode.id, @@ -197,7 +197,7 @@ describe('and attestation rights have been delegated', () => { const attestation = Attestation.fromCredentialAndDid( credential, - attester.uri + attester.id ) const storeTx = api.tx.attestation.add( attestation.claimHash, @@ -205,7 +205,7 @@ describe('and attestation rights have been delegated', () => { { Delegation: { subjectNodeId: delegatedNode.id } } ) const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, storeTx, attesterKey.getSignCallback(attester), paymentAccount.address @@ -224,7 +224,7 @@ describe('and attestation rights have been delegated', () => { Delegation: { maxChecks: 1 }, }) const authorizedStoreTx2 = await Did.authorizeTx( - root.uri, + root.id, revokeTx, rootKey.getSignCallback(root), paymentAccount.address @@ -273,9 +273,9 @@ describe('revocation', () => { ) // Test revocation - const revokeTx = await delegationA.getRevokeTx(delegator.uri) + const revokeTx = await delegationA.getRevokeTx(delegator.id) const authorizedRevokeTx = await Did.authorizeTx( - delegator.uri, + delegator.id, revokeTx, delegatorSign, paymentAccount.address @@ -288,7 +288,7 @@ describe('revocation', () => { // Change introduced in https://github.com/KILTprotocol/mashnet-node/pull/304 const removeTx = await delegationA.getRemoveTx() const authorizedRemoveTx = await Did.authorizeTx( - delegator.uri, + delegator.id, removeTx, delegatorSign, paymentAccount.address @@ -321,7 +321,7 @@ describe('revocation', () => { ) const revokeTx = api.tx.delegation.revokeDelegation(delegationRoot.id, 1, 1) const authorizedRevokeTx = await Did.authorizeTx( - firstDelegate.uri, + firstDelegate.id, revokeTx, firstDelegateSign, paymentAccount.address @@ -334,9 +334,9 @@ describe('revocation', () => { }) await expect(delegationRoot.verify()).resolves.not.toThrow() - const revokeTx2 = await delegationA.getRevokeTx(firstDelegate.uri) + const revokeTx2 = await delegationA.getRevokeTx(firstDelegate.id) const authorizedRevokeTx2 = await Did.authorizeTx( - firstDelegate.uri, + firstDelegate.id, revokeTx2, firstDelegateSign, paymentAccount.address @@ -368,9 +368,9 @@ describe('revocation', () => { secondDelegateSign ) delegationRoot = await delegationRoot.getLatestState() - const revokeTx = await delegationRoot.getRevokeTx(delegator.uri) + const revokeTx = await delegationRoot.getRevokeTx(delegator.id) const authorizedRevokeTx = await Did.authorizeTx( - delegator.uri, + delegator.id, revokeTx, delegatorSign, paymentAccount.address @@ -438,7 +438,7 @@ describe('handling queries to data not on chain', () => { permissions: [0], hierarchyId: randomAsHex(32), parentId: randomAsHex(32), - account: attester.uri, + account: attester.id, }).getAttestationHashes() ).toEqual([]) }) diff --git a/tests/integration/Deposit.spec.ts b/tests/integration/Deposit.spec.ts index 236fbea25..fbfa20d60 100644 --- a/tests/integration/Deposit.spec.ts +++ b/tests/integration/Deposit.spec.ts @@ -48,18 +48,18 @@ async function checkDeleteFullDid( sign: SignCallback ): Promise { storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) const deleteDid = api.tx.did.delete(storedEndpointsCount) - tx = await Did.authorizeTx(fullDid.uri, deleteDid, sign, identity.address) + tx = await Did.authorizeTx(fullDid.id, deleteDid, sign, identity.address) const balanceBeforeDeleting = ( await api.query.system.account(identity.address) ).data const didResult = Did.documentFromChain( - await api.query.did.did(Did.toChain(fullDid.uri)) + await api.query.did.did(Did.toChain(fullDid.id)) ) const didDeposit = didResult.deposit @@ -79,16 +79,16 @@ async function checkReclaimFullDid( fullDid: DidDocument ): Promise { storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) - tx = api.tx.did.reclaimDeposit(Did.toChain(fullDid.uri), storedEndpointsCount) + tx = api.tx.did.reclaimDeposit(Did.toChain(fullDid.id), storedEndpointsCount) const balanceBeforeRevoking = ( await api.query.system.account(identity.address) ).data const didResult = Did.documentFromChain( - await api.query.did.did(Did.toChain(fullDid.uri)) + await api.query.did.did(Did.toChain(fullDid.id)) ) const didDeposit = didResult.deposit @@ -109,14 +109,14 @@ async function checkRemoveFullDidAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) @@ -130,10 +130,10 @@ async function checkRemoveFullDidAttestation( const balanceBeforeRemoving = ( await api.query.system.account(identity.address) ).data - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.remove(attestation.claimHash, null) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) @@ -152,21 +152,21 @@ async function checkReclaimFullDidAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) const balanceBeforeReclaiming = ( await api.query.system.account(identity.address) ).data - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.reclaimDeposit(attestation.claimHash) @@ -194,25 +194,25 @@ async function checkDeletedDidReclaimAttestation( sign: SignCallback, credential: ICredential ): Promise { - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) tx = api.tx.attestation.add( attestation.claimHash, attestation.cTypeHash, null ) - authorizedTx = await Did.authorizeTx(fullDid.uri, tx, sign, identity.address) + authorizedTx = await Did.authorizeTx(fullDid.id, tx, sign, identity.address) await submitTx(authorizedTx, identity) storedEndpointsCount = await api.query.did.didEndpointsCount( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) - attestation = Attestation.fromCredentialAndDid(credential, fullDid.uri) + attestation = Attestation.fromCredentialAndDid(credential, fullDid.id) const deleteDid = api.tx.did.delete(storedEndpointsCount) - tx = await Did.authorizeTx(fullDid.uri, deleteDid, sign, identity.address) + tx = await Did.authorizeTx(fullDid.id, deleteDid, sign, identity.address) await submitTx(tx, identity) @@ -234,7 +234,7 @@ async function checkWeb3Deposit( const depositAmount = api.consts.web3Names.deposit.toBn() const claimTx = api.tx.web3Names.claim(web3Name) let didAuthorizedTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, claimTx, sign, identity.address @@ -253,7 +253,7 @@ async function checkWeb3Deposit( const releaseTx = api.tx.web3Names.releaseByOwner() didAuthorizedTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, releaseTx, sign, identity.address @@ -295,7 +295,7 @@ beforeAll(async () => { const ctypeExists = await isCtypeOnChain(driversLicenseCType) if (!ctypeExists) { const extrinsic = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.ctype.add(CType.toChain(driversLicenseCType)), attesterKey.getSignCallback(attester), devFaucet.address @@ -311,7 +311,7 @@ beforeAll(async () => { const claim = Claim.fromCTypeAndClaimContents( driversLicenseCType, rawClaim, - claimerLightDid.uri + claimerLightDid.id ) credential = Credential.fromClaim(claim) diff --git a/tests/integration/Did.spec.ts b/tests/integration/Did.spec.ts index c81635f47..f4499c318 100644 --- a/tests/integration/Did.spec.ts +++ b/tests/integration/Did.spec.ts @@ -6,29 +6,29 @@ */ import type { ApiPromise } from '@polkadot/api' -import { BN } from '@polkadot/util' - -import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' -import * as Did from '@kiltprotocol/did' -import { +import type { DidDocument, - DidResolutionResult, - DidServiceEndpoint, KiltKeyringPair, - NewDidEncryptionKey, - NewDidVerificationKey, - NewLightDidVerificationKey, - Permission, + ResolutionResult, + Service, SignCallback, + VerificationMethod, } from '@kiltprotocol/types' + +import { BN } from '@polkadot/util' +import { CType, DelegationNode, disconnect } from '@kiltprotocol/core' +import { Permission } from '@kiltprotocol/types' import { UUID } from '@kiltprotocol/utils' +import * as Did from '@kiltprotocol/did' + +import type { KeyTool } from '../testUtils/index.js' import { createFullDidFromSeed, createMinimalLightDidFromKeypair, - KeyTool, makeEncryptionKeyTool, makeSigningKeyTool, + 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 Did.getStoreTx( + const tx = await getStoreTxFromDidDocument( did, otherAccount.address, key.storeDidCallback @@ -73,8 +73,15 @@ describe('write and didDeleteTx', () => { }, 60_000) it('writes a new DID record to chain', async () => { - const newDid = Did.createLightDidDocument({ - authentication: did.authentication as [NewLightDidVerificationKey], + 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 + ], service: [ { id: '#test-id-1', @@ -87,29 +94,25 @@ describe('write and didDeleteTx', () => { serviceEndpoint: ['x:test-url-2'], }, ], - }) + } const tx = await Did.getStoreTx( - newDid, + input, paymentAccount.address, key.storeDidCallback ) await submitTx(tx, paymentAccount) - const fullDidUri = Did.getFullDidUri(newDid.uri) - const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDidUri)) - const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) + const fullDid = Did.getFullDidFromVerificationMethod({ + publicKeyMultibase, + }) + const fullDidLinkedInfo = await api.call.did.query(Did.toChain(fullDid)) + const { document: fullDidDocument } = + Did.linkedInfoFromChain(fullDidLinkedInfo) - expect(fullDid).toMatchObject({ - uri: fullDidUri, - authentication: [ - expect.objectContaining({ - // We cannot match the ID of the key because it will be defined by the blockchain while saving - publicKey: newDid.authentication[0].publicKey, - type: 'sr25519', - }), - ], + expect(fullDidDocument).toMatchObject(>{ + id: fullDid, service: [ { id: '#test-id-1', @@ -122,13 +125,26 @@ 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.getFullDidUriFromKey( - makeSigningKeyTool().authentication[0] - ) + const emptyDid = Did.getFullDid(makeSigningKeyTool().keypair.address) const encodedDid = Did.toChain(emptyDid) expect((await api.call.did.query(encodedDid)).isSome).toBe(false) @@ -137,7 +153,7 @@ describe('write and didDeleteTx', () => { it('fails to delete the DID using a different submitter than the one specified in the DID operation or using a services count that is too low', async () => { // We verify that the DID to delete is on chain. const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(did.uri)) + Did.toChain(Did.getFullDid(did.id)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() @@ -148,7 +164,7 @@ describe('write and didDeleteTx', () => { let call = api.tx.did.delete(new BN(10)) let submittable = await Did.authorizeTx( - fullDid.uri, + fullDid.id, call, signCallback, // Use a different account than the submitter one @@ -160,11 +176,11 @@ describe('write and didDeleteTx', () => { name: 'BadDidOrigin', }) - // We use 1 here and this should fail as there are two service endpoints stored. + // We use 1 here and this should fail as there are two services stored. call = api.tx.did.delete(new BN(1)) submittable = await Did.authorizeTx( - fullDid.uri, + fullDid.id, call, signCallback, paymentAccount.address @@ -182,12 +198,12 @@ describe('write and didDeleteTx', () => { it('deletes DID from previous step', async () => { // We verify that the DID to delete is on chain. const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(did.uri)) + Did.toChain(Did.getFullDid(did.id)) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - const encodedDid = Did.toChain(fullDid.uri) + const encodedDid = Did.toChain(fullDid.id) const linkedInfo = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) @@ -195,7 +211,7 @@ describe('write and didDeleteTx', () => { const call = api.tx.did.delete(storedEndpointsCount) const submittable = await Did.authorizeTx( - fullDid.uri, + fullDid.id, call, signCallback, paymentAccount.address @@ -217,7 +233,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const { keypair, getSignCallback, storeDidCallback } = makeSigningKeyTool() const newDid = await createMinimalLightDidFromKeypair(keypair) - const tx = await Did.getStoreTx( + const tx = await getStoreTxFromDidDocument( newDid, paymentAccount.address, storeDidCallback @@ -227,7 +243,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { // This will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. let fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(newDid.uri)) + Did.toChain(Did.getFullDid(newDid.id)) ) let { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) @@ -237,7 +253,7 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { Did.publicKeyToChain(newKey.authentication[0]) ) const tx2 = await Did.authorizeTx( - fullDid.uri, + fullDid.id, updateAuthenticationKeyCall, getSignCallback(fullDid), paymentAccount.address @@ -247,12 +263,12 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { // Authentication key changed, so did must be updated. // Also this will better be handled once we have the UpdateBuilder class, which encapsulates all the logic. fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUri(newDid.uri)) + Did.toChain(Did.getFullDid(newDid.id)) ) fullDid = Did.linkedInfoFromChain(fullDidLinkedInfo).document - // Add a new service endpoint - const newEndpoint: DidServiceEndpoint = { + // Add a new service + const newEndpoint: Did.NewService = { id: '#new-endpoint', type: ['new-type'], serviceEndpoint: ['x:new-url'], @@ -262,27 +278,27 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { ) const tx3 = await Did.authorizeTx( - fullDid.uri, + fullDid.id, updateEndpointCall, newKey.getSignCallback(fullDid), paymentAccount.address ) await submitTx(tx3, paymentAccount) - const encodedDid = Did.toChain(fullDid.uri) + const encodedDid = Did.toChain(fullDid.id) const linkedInfo = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) - expect(Did.getService(linkedInfo.document, newEndpoint.id)).toStrictEqual( - newEndpoint - ) + expect( + linkedInfo.document.service?.find((s) => s.id === newEndpoint.id) + ).toStrictEqual(newEndpoint) - // Delete the added service endpoint + // Delete the added service const removeEndpointCall = api.tx.did.removeServiceEndpoint( - Did.resourceIdToChain(newEndpoint.id) + Did.fragmentIdToChain(newEndpoint.id) ) const tx4 = await Did.authorizeTx( - fullDid.uri, + fullDid.id, removeEndpointCall, newKey.getSignCallback(fullDid), paymentAccount.address @@ -293,7 +309,9 @@ it('creates and updates DID, and then reclaims the deposit back', async () => { const linkedInfo2 = Did.linkedInfoFromChain( await api.call.did.query(encodedDid) ) - expect(Did.getService(linkedInfo2.document, newEndpoint.id)).toBe(undefined) + expect( + linkedInfo2.document.service?.find((s) => s.id === newEndpoint.id) + ).toBe(undefined) // Claim the deposit back const storedEndpointsCount = linkedInfo2.document.service?.length ?? 0 @@ -317,47 +335,55 @@ describe('DID migration', () => { keyAgreement, }) - const storeTx = await Did.getStoreTx( + const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) + const migratedFullDid = Did.getFullDid(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDidUri) + Did.toChain(migratedFullDid) ) - const { document: migratedFullDid } = Did.linkedInfoFromChain( + const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject({ - uri: migratedFullDidUri, - authentication: [ - expect.objectContaining({ - publicKey: lightDid.authentication[0].publicKey, - type: 'ed25519', + 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]), }), - ], - keyAgreement: [ - expect.objectContaining({ - publicKey: lightDid.keyAgreement?.[0].publicKey, - type: 'x25519', + 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]), }), ], }) + expect(migratedFullDidDocument.authentication).toHaveLength(1) + expect(migratedFullDidDocument.keyAgreement).toHaveLength(1) + expect(migratedFullDidDocument.assertionMethod).toBe(undefined) + expect(migratedFullDidDocument.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome + (await api.call.did.query(Did.toChain(migratedFullDidDocument.id))).isSome ).toBe(true) - const { metadata } = (await Did.resolve( - lightDid.uri - )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.resolve( + lightDid.id + )) as ResolutionResult - expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) - expect(metadata.deactivated).toBe(false) + expect(didDocumentMetadata.canonicalId).toStrictEqual( + migratedFullDidDocument.id + ) + expect(didDocumentMetadata.deactivated).toBe(undefined) }) it('migrates light DID with sr25519 auth key', async () => { @@ -366,49 +392,57 @@ describe('DID migration', () => { authentication, }) - const storeTx = await Did.getStoreTx( + const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) + const migratedFullDid = Did.getFullDid(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDidUri) + Did.toChain(migratedFullDid) ) - const { document: migratedFullDid } = Did.linkedInfoFromChain( + const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject({ - uri: migratedFullDidUri, - authentication: [ - expect.objectContaining({ - publicKey: lightDid.authentication[0].publicKey, - type: 'sr25519', + 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(migratedFullDidDocument.authentication).toHaveLength(1) + expect(migratedFullDidDocument.keyAgreement).toBe(undefined) + expect(migratedFullDidDocument.assertionMethod).toBe(undefined) + expect(migratedFullDidDocument.capabilityDelegation).toBe(undefined) expect( - (await api.call.did.query(Did.toChain(migratedFullDid.uri))).isSome + (await api.call.did.query(Did.toChain(migratedFullDidDocument.id))).isSome ).toBe(true) - const { metadata } = (await Did.resolve( - lightDid.uri - )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.resolve( + lightDid.id + )) as ResolutionResult - expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) - expect(metadata.deactivated).toBe(false) + expect(didDocumentMetadata.canonicalId).toStrictEqual( + migratedFullDidDocument.id + ) + expect(didDocumentMetadata.deactivated).toBe(undefined) }) - it('migrates light DID with ed25519 auth key, encryption key, and service endpoints', async () => { + it('migrates light DID with ed25519 auth key, encryption key, and services', async () => { const { storeDidCallback, authentication } = makeSigningKeyTool('ed25519') const { keyAgreement } = makeEncryptionKeyTool( '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' ) - const service: DidServiceEndpoint[] = [ + const service: Did.NewService[] = [ { id: '#id-1', type: ['type-1'], @@ -421,33 +455,35 @@ describe('DID migration', () => { service, }) - const storeTx = await Did.getStoreTx( + const storeTx = await getStoreTxFromDidDocument( lightDid, paymentAccount.address, storeDidCallback ) await submitTx(storeTx, paymentAccount) - const migratedFullDidUri = Did.getFullDidUri(lightDid.uri) + const migratedFullDid = Did.getFullDid(lightDid.id) const migratedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(migratedFullDidUri) + Did.toChain(migratedFullDid) ) - const { document: migratedFullDid } = Did.linkedInfoFromChain( + const { document: migratedFullDidDocument } = Did.linkedInfoFromChain( migratedFullDidLinkedInfo ) - expect(migratedFullDid).toMatchObject({ - uri: migratedFullDidUri, - authentication: [ - expect.objectContaining({ - publicKey: lightDid.authentication[0].publicKey, - type: 'ed25519', + 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]), }), - ], - keyAgreement: [ - expect.objectContaining({ - publicKey: lightDid.keyAgreement?.[0].publicKey, - type: 'x25519', + 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]), }), ], service: [ @@ -458,16 +494,22 @@ 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(migratedFullDid.uri) + const encodedDid = Did.toChain(migratedFullDidDocument.id) expect((await api.call.did.query(encodedDid)).isSome).toBe(true) - const { metadata } = (await Did.resolve( - lightDid.uri - )) as DidResolutionResult + const { didDocumentMetadata } = (await Did.resolve( + lightDid.id + )) as ResolutionResult - expect(metadata.canonicalId).toStrictEqual(migratedFullDid.uri) - expect(metadata.deactivated).toBe(false) + expect(didDocumentMetadata.canonicalId).toStrictEqual( + migratedFullDidDocument.id + ) + expect(didDocumentMetadata.deactivated).toBe(undefined) // Remove and claim the deposit back const linkedInfo = Did.linkedInfoFromChain( @@ -503,7 +545,11 @@ describe('DID authorization', () => { ) await submitTx(createTx, paymentAccount) const didLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) did = Did.linkedInfoFromChain(didLinkedInfo).document }, 60_000) @@ -512,7 +558,7 @@ describe('DID authorization', () => { const cType = CType.fromProperties(UUID.generate(), {}) const call = api.tx.ctype.add(CType.toChain(cType)) const tx = await Did.authorizeTx( - did.uri, + did.id, call, getSignCallback(did), paymentAccount.address @@ -524,12 +570,12 @@ describe('DID authorization', () => { it('no longer authorizes ctype creation after DID deletion', async () => { const linkedInfo = Did.linkedInfoFromChain( - await api.call.did.query(Did.toChain(did.uri)) + await api.call.did.query(Did.toChain(did.id)) ) const storedEndpointsCount = linkedInfo.document.service?.length ?? 0 const deleteCall = api.tx.did.delete(storedEndpointsCount) const tx = await Did.authorizeTx( - did.uri, + did.id, deleteCall, getSignCallback(did), paymentAccount.address @@ -539,7 +585,7 @@ describe('DID authorization', () => { const cType = CType.fromProperties(UUID.generate(), {}) const call = api.tx.ctype.add(CType.toChain(cType)) const tx2 = await Did.authorizeTx( - did.uri, + did.id, call, getSignCallback(did), paymentAccount.address @@ -556,7 +602,7 @@ describe('DID authorization', () => { describe('DID management batching', () => { describe('FullDidCreationBuilder', () => { it('Build a complete full DID', async () => { - const { keypair, storeDidCallback, authentication } = makeSigningKeyTool() + const { storeDidCallback, authentication } = makeSigningKeyTool() const extrinsic = await Did.getStoreTx( { authentication, @@ -609,44 +655,71 @@ describe('DID management batching', () => { ) await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - expect(fullDid).toMatchObject({ - authentication: [ + expect(fullDid.verificationMethod).toEqual>( + expect.arrayContaining([ expect.objectContaining({ - publicKey: keypair.publicKey, - type: 'sr25519', + // Authentication + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), }), - ], - assertionMethod: [ + // Assertion method expect.objectContaining({ - publicKey: new Uint8Array(32).fill(1), - type: 'sr25519', + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'sr25519', + publicKey: new Uint8Array(32).fill(1), + }), }), - ], - capabilityDelegation: [ + // Capability delegation expect.objectContaining({ - publicKey: new Uint8Array(33).fill(1), - type: 'ecdsa', + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'ecdsa', + publicKey: new Uint8Array(33).fill(1), + }), }), - ], - keyAgreement: [ + // Key agreement 1 expect.objectContaining({ - publicKey: new Uint8Array(32).fill(3), - type: 'x25519', + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(1), + }), }), + // Key agreement 2 expect.objectContaining({ - publicKey: new Uint8Array(32).fill(2), - type: 'x25519', + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(2), + }), }), + // Key agreement 3 expect.objectContaining({ - publicKey: new Uint8Array(32).fill(1), - type: 'x25519', + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: 'x25519', + publicKey: new Uint8Array(32).fill(3), + }), }), - ], + ]) + ) + expect(fullDid).toMatchObject(>{ service: [ { id: '#id-3', @@ -665,11 +738,15 @@ describe('DID management batching', () => { }, ], }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.assertionMethod).toHaveLength(1) + expect(fullDid.capabilityDelegation).toHaveLength(1) + expect(fullDid.keyAgreement).toHaveLength(3) }) it('Build a minimal full DID with an Ecdsa key', async () => { const { keypair, storeDidCallback } = makeSigningKeyTool('ecdsa') - const didAuthKey: NewDidVerificationKey = { + const didAuthKey: Did.NewDidVerificationKey = { publicKey: keypair.publicKey, type: 'ecdsa', } @@ -682,17 +759,29 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(didAuthKey)) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid).not.toBeNull() - expect(fullDid?.authentication).toMatchObject([ - { - publicKey: keypair.publicKey, - type: 'ecdsa', - }, - ]) + expect(fullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey(didAuthKey), + }), + ], + }) + expect(fullDid.authentication).toHaveLength(1) + expect(fullDid.assertionMethod).toBe(undefined) + expect(fullDid.capabilityDelegation).toBe(undefined) + expect(fullDid.keyAgreement).toBe(undefined) }) }) @@ -745,7 +834,11 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const initialFullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: initialFullDid } = Did.linkedInfoFromChain( initialFullDidLinkedInfo @@ -756,13 +849,21 @@ describe('DID management batching', () => { const extrinsic = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: initialFullDid.uri, + did: initialFullDid.id, extrinsics: [ api.tx.did.removeKeyAgreementKey( - Did.resourceIdToChain(encryptionKeys[0].id) + Did.fragmentIdToChain( + initialFullDid.verificationMethod!.find( + (vm) => vm.id === encryptionKeys[0] + )!.id + ) ), api.tx.did.removeKeyAgreementKey( - Did.resourceIdToChain(encryptionKeys[1].id) + Did.fragmentIdToChain( + initialFullDid.verificationMethod!.find( + (vm) => vm.id === encryptionKeys[1] + )!.id + ) ), api.tx.did.removeAttestationKey(), api.tx.did.removeDelegationKey(), @@ -775,7 +876,7 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const finalFullDidLinkedInfo = await api.call.did.query( - Did.toChain(initialFullDid.uri) + Did.toChain(initialFullDid.id) ) const { document: finalFullDid } = Did.linkedInfoFromChain( finalFullDidLinkedInfo @@ -783,17 +884,23 @@ describe('DID management batching', () => { expect(finalFullDid).not.toBeNull() - expect( - finalFullDid.authentication[0] - ).toMatchObject({ - publicKey: keypair.publicKey, - type: 'sr25519', + expect(finalFullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: finalFullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: keypair.publicKey, + type: 'sr25519', + }), + }), + ], }) - - expect(finalFullDid.keyAgreement).toBeUndefined() - expect(finalFullDid.assertionMethod).toBeUndefined() - expect(finalFullDid.capabilityDelegation).toBeUndefined() - expect(finalFullDid.service).toBeUndefined() + expect(finalFullDid.authentication).toHaveLength(1) + expect(finalFullDid.assertionMethod).toBe(undefined) + expect(finalFullDid.capabilityDelegation).toBe(undefined) + expect(finalFullDid.keyAgreement).toBe(undefined) }, 40_000) it('Correctly handles rotation of the authentication key', async () => { @@ -811,7 +918,11 @@ describe('DID management batching', () => { await submitTx(createTx, paymentAccount) const initialFullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: initialFullDid } = Did.linkedInfoFromChain( initialFullDidLinkedInfo @@ -819,7 +930,7 @@ describe('DID management batching', () => { const extrinsic = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: initialFullDid.uri, + did: initialFullDid.id, extrinsics: [ api.tx.did.addServiceEndpoint( Did.serviceToChain({ @@ -844,23 +955,43 @@ describe('DID management batching', () => { await submitTx(extrinsic, paymentAccount) const finalFullDidLinkedInfo = await api.call.did.query( - Did.toChain(initialFullDid.uri) + Did.toChain(initialFullDid.id) ) const { document: finalFullDid } = Did.linkedInfoFromChain( finalFullDidLinkedInfo ) expect(finalFullDid).not.toBeNull() - - expect(finalFullDid.authentication[0]).toMatchObject({ - publicKey: newAuthKey.publicKey, - type: newAuthKey.type, + expect(finalFullDid).toMatchObject(>{ + verificationMethod: [ + // Authentication + expect.objectContaining(>{ + controller: finalFullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey({ + publicKey: newAuthKey.publicKey, + type: 'ed25519', + }), + }), + ], + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + { + id: '#id-2', + type: ['type-2'], + serviceEndpoint: ['x:url-2'], + }, + ], }) + expect(finalFullDid.authentication).toHaveLength(1) expect(finalFullDid.keyAgreement).toBeUndefined() expect(finalFullDid.assertionMethod).toBeUndefined() expect(finalFullDid.capabilityDelegation).toBeUndefined() - expect(finalFullDid.service).toHaveLength(2) }, 40_000) it('simple `batch` succeeds despite failures of some extrinsics', async () => { @@ -880,19 +1011,23 @@ describe('DID management batching', () => { paymentAccount.address, storeDidCallback ) - // Create the full DID with a service endpoint + // Create the full DIgetStoreTx await submitTx(tx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid.assertionMethod).toBeUndefined() - // Try to set a new attestation key and a duplicate service endpoint + // Try to set a new attestation key and a duplicate service const updateTx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), api.tx.did.addServiceEndpoint( @@ -910,22 +1045,38 @@ describe('DID management batching', () => { await submitTx(updateTx, paymentAccount) const updatedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) const { document: updatedFullDid } = Did.linkedInfoFromChain( updatedFullDidLinkedInfo ) - // .setAttestationKey() extrinsic went through in the batch - expect(updatedFullDid.assertionMethod?.[0]).toBeDefined() - // The service endpoint will match the one manually added, and not the one set in the batch - expect( - Did.getService(updatedFullDid, '#id-1') - ).toStrictEqual({ - id: '#id-1', - type: ['type-1'], - serviceEndpoint: ['x:url-1'], + expect(updatedFullDid).toMatchObject>({ + verificationMethod: [ + expect.objectContaining({ + // Authentication and assertionMethod + controller: fullDid.id, + type: 'Multikey', + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }), + ], + // Old service maintained + service: [ + { + id: '#id-1', + type: ['type-1'], + serviceEndpoint: ['x:url-1'], + }, + ], }) + + expect(updatedFullDid.authentication).toHaveLength(1) + expect(updatedFullDid.keyAgreement).toBeUndefined() + // .setAttestationKey() extrinsic went through in the batch + expect(updatedFullDid.assertionMethod).toStrictEqual( + updatedFullDid.authentication + ) + expect(updatedFullDid.capabilityDelegation).toBeUndefined() }, 60_000) it('batchAll fails if any extrinsics fails', async () => { @@ -947,16 +1098,20 @@ describe('DID management batching', () => { ) await submitTx(createTx, paymentAccount) const fullDidLinkedInfo = await api.call.did.query( - Did.toChain(Did.getFullDidUriFromKey(authentication[0])) + Did.toChain( + Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey(authentication[0]), + }) + ) ) const { document: fullDid } = Did.linkedInfoFromChain(fullDidLinkedInfo) expect(fullDid.assertionMethod).toBeUndefined() - // Use batchAll to set a new attestation key and a duplicate service endpoint + // Use batchAll to set a new attestation key and a duplicate service const updateTx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ api.tx.did.setAttestationKey(Did.publicKeyToChain(authentication[0])), api.tx.did.addServiceEndpoint( @@ -978,17 +1133,17 @@ describe('DID management batching', () => { }) const updatedFullDidLinkedInfo = await api.call.did.query( - Did.toChain(fullDid.uri) + Did.toChain(fullDid.id) ) const { document: updatedFullDid } = Did.linkedInfoFromChain( updatedFullDidLinkedInfo ) // .setAttestationKey() extrinsic went through but it was then reverted expect(updatedFullDid.assertionMethod).toBeUndefined() - // The service endpoint will match the one manually added, and not the one set in the builder. + // The service will match the one manually added, and not the one set in the builder. expect( - Did.getService(updatedFullDid, '#id-1') - ).toStrictEqual({ + updatedFullDid.service?.find((s) => s.id === '#id-1') + ).toStrictEqual({ id: '#id-1', type: ['type-1'], serviceEndpoint: ['x:url-1'], @@ -1010,15 +1165,15 @@ describe('DID extrinsics batching', () => { const cType = CType.fromProperties(UUID.generate(), {}) const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) const rootNode = DelegationNode.newRoot({ - account: fullDid.uri, + account: fullDid.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cType.$id), }) const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ ctypeStoreTx, // Will fail since the delegation cannot be revoked before it is added @@ -1040,15 +1195,15 @@ describe('DID extrinsics batching', () => { const cType = CType.fromProperties(UUID.generate(), {}) const ctypeStoreTx = api.tx.ctype.add(CType.toChain(cType)) const rootNode = DelegationNode.newRoot({ - account: fullDid.uri, + account: fullDid.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(cType.$id), }) const delegationStoreTx = await rootNode.getStoreTx() - const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.uri) + const delegationRevocationTx = await rootNode.getRevokeTx(fullDid.id) const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ ctypeStoreTx, // Will fail since the delegation cannot be revoked before it is added @@ -1072,7 +1227,7 @@ describe('DID extrinsics batching', () => { it('can batch extrinsics for the same required key type', async () => { const web3NameClaimTx = api.tx.web3Names.claim('test-1') const authorizedTx = await Did.authorizeTx( - fullDid.uri, + fullDid.id, web3NameClaimTx, key.getSignCallback(fullDid), paymentAccount.address @@ -1083,7 +1238,7 @@ describe('DID extrinsics batching', () => { const web3Name2ClaimExt = api.tx.web3Names.claim('test-2') const tx = await Did.authorizeBatch({ batchFunction: api.tx.utility.batch, - did: fullDid.uri, + did: fullDid.id, extrinsics: [web3Name1ReleaseExt, web3Name2ClaimExt], sign: key.getSignCallback(fullDid), submitter: paymentAccount.address, @@ -1095,8 +1250,8 @@ describe('DID extrinsics batching', () => { expect(encoded1.isSome).toBe(false) // Test for correct creation of second web3 name const encoded2 = await api.call.did.queryByWeb3Name('test-2') - expect(Did.linkedInfoFromChain(encoded2).document.uri).toStrictEqual( - fullDid.uri + expect(Did.linkedInfoFromChain(encoded2).document.id).toStrictEqual( + fullDid.id ) }, 30_000) @@ -1108,7 +1263,7 @@ describe('DID extrinsics batching', () => { const ctype1Creation = api.tx.ctype.add(CType.toChain(ctype1)) // Delegation key const rootNode = DelegationNode.newRoot({ - account: fullDid.uri, + account: fullDid.id, permissions: [Permission.DELEGATE], cTypeHash: CType.idToHash(ctype1.$id), }) @@ -1120,11 +1275,11 @@ describe('DID extrinsics batching', () => { const ctype2 = CType.fromProperties(UUID.generate(), {}) const ctype2Creation = api.tx.ctype.add(CType.toChain(ctype2)) // Delegation key - const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.uri) + const delegationHierarchyRemoval = await rootNode.getRevokeTx(fullDid.id) const batchedExtrinsics = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: fullDid.uri, + did: fullDid.id, extrinsics: [ web3NameReleaseExt, ctype1Creation, @@ -1144,9 +1299,9 @@ describe('DID extrinsics batching', () => { expect(encoded.isSome).toBe(false) const { - document: { uri }, + document: { id }, } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name('test-2')) - expect(uri).toStrictEqual(fullDid.uri) + expect(id).toStrictEqual(fullDid.id) // Test correct use of attestation keys await expect(CType.verifyStored(ctype1)).resolves.not.toThrow() @@ -1159,7 +1314,7 @@ describe('DID extrinsics batching', () => { }) describe('Runtime constraints', () => { - let testAuthKey: NewDidVerificationKey + let testAuthKey: Did.NewDidVerificationKey const { keypair, storeDidCallback } = makeSigningKeyTool('ed25519') beforeAll(async () => { @@ -1172,7 +1327,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): NewDidEncryptionKey => ({ + (_, index): Did.NewDidEncryptionKey => ({ publicKey: Uint8Array.from(new Array(32).fill(index)), type: 'x25519', }) @@ -1205,10 +1360,10 @@ describe('Runtime constraints', () => { ) }, 30_000) - it('should not be possible to create a DID with too many service endpoints', async () => { - // Maximum is 25 + it('should not be possible to create a DID with too many services', async () => { + // MaxgetStoreTx const newServiceEndpoints = Array(25).map( - (_, index): DidServiceEndpoint => ({ + (_, index): Did.NewService => ({ id: `#service-${index}`, type: [`type-${index}`], serviceEndpoint: [`x:url-${index}`], @@ -1239,35 +1394,35 @@ describe('Runtime constraints', () => { storeDidCallback ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot store more than 25 service endpoints per DID"` + `"Cannot store more than 25 services per DID"` ) }, 30_000) - it('should not be possible to create a DID with a service endpoint that is too long', async () => { - const serviceId = '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + it('should not be possible to create a DID with a service that is too long', async () => { + const serviceId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const limit = api.consts.did.maxServiceIdLength.toNumber() expect(serviceId.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has too many types', async () => { + it('should not be possible to create a DID with a service that has too many types', async () => { const types = ['type-1', 'type-2'] const limit = api.consts.did.maxNumberOfTypesPerService.toNumber() expect(types.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has too many URIs', async () => { + it('should not be possible to create a DID with a service that has too many URIs', async () => { const uris = ['x:url-1', 'x:url-2', 'x:url-3'] const limit = api.consts.did.maxNumberOfUrlsPerService.toNumber() expect(uris.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has a type that is too long', async () => { + it('should not be possible to create a DID with a service that has a type that is too long', async () => { const type = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const limit = api.consts.did.maxServiceTypeLength.toNumber() expect(type.length).toBeGreaterThan(limit) }) - it('should not be possible to create a DID with a service endpoint that has a URI that is too long', async () => { + it('should not be possible to create a DID with a service that has a URI that is too long', async () => { const uri = 'a:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const limit = api.consts.did.maxServiceUrlLength.toNumber() diff --git a/tests/integration/ErrorHandler.spec.ts b/tests/integration/ErrorHandler.spec.ts index 26f7a6e9e..b440c7821 100644 --- a/tests/integration/ErrorHandler.spec.ts +++ b/tests/integration/ErrorHandler.spec.ts @@ -76,7 +76,7 @@ it('records an extrinsic error when ctype does not exist', async () => { cTypeHash: '0x103752ecd8e284b1c9677337ccc91ea255ac8e6651dc65d90f0504f31d7e54f0', delegationId: null, - owner: someDid.uri, + owner: someDid.id, revoked: false, } const storeTx = api.tx.attestation.add( @@ -85,7 +85,7 @@ it('records an extrinsic error when ctype does not exist', async () => { null ) const tx = await Did.authorizeTx( - someDid.uri, + someDid.id, storeTx, key.getSignCallback(someDid), paymentAccount.address diff --git a/tests/integration/PublicCredentials.spec.ts b/tests/integration/PublicCredentials.spec.ts index b6c93c949..4de6374a6 100644 --- a/tests/integration/PublicCredentials.spec.ts +++ b/tests/integration/PublicCredentials.spec.ts @@ -6,7 +6,7 @@ */ import type { - AssetDidUri, + AssetDid, DidDocument, HexString, IPublicCredential, @@ -42,14 +42,14 @@ let attesterKey: KeyTool let api: ApiPromise // Generate a random asset ID -let assetId: AssetDidUri = `did:asset:eip155:1.erc20:${randomAsHex(20)}` +let assetId: AssetDid = `did:asset:eip155:1.erc20:${randomAsHex(20)}` let latestCredential: IPublicCredentialInput async function issueCredential( credential: IPublicCredentialInput ): Promise { const authorizedStoreTx = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential)), attesterKey.getSignCallback(attester), tokenHolder.address @@ -66,7 +66,7 @@ beforeAll(async () => { const ctypeExists = await isCtypeOnChain(nftNameCType) if (ctypeExists) return const tx = await Did.authorizeTx( - attester.uri, + attester.id, api.tx.ctype.add(CType.toChain(nftNameCType)), attesterKey.getSignCallback(attester), tokenHolder.address @@ -87,7 +87,7 @@ describe('When there is an attester and ctype NFT name', () => { await issueCredential(latestCredential) const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) const publicCredentialEntry = await api.call.publicCredentials.getById( @@ -104,7 +104,7 @@ describe('When there is an attester and ctype NFT name', () => { expect.objectContaining({ ...latestCredential, id: credentialId, - attester: attester.uri, + attester: attester.id, revoked: false, }) ) @@ -147,7 +147,7 @@ describe('When there is an attester and ctype NFT name', () => { }) const authorizedBatch = await Did.authorizeBatch({ batchFunction: api.tx.utility.batchAll, - did: attester.uri, + did: attester.id, extrinsics: credentialCreationTxs, sign: attesterKey.getSignCallback(attester), submitter: tokenHolder.address, @@ -165,7 +165,7 @@ describe('When there is an attester and ctype NFT name', () => { it('should be possible to revoke a credential', async () => { const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) let assetCredential = await PublicCredentials.fetchCredentialFromChain( credentialId @@ -176,7 +176,7 @@ describe('When there is an attester and ctype NFT name', () => { expect(assetCredential.revoked).toBe(false) const revocationTx = api.tx.publicCredentials.revoke(credentialId, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, revocationTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -199,7 +199,7 @@ describe('When there is an attester and ctype NFT name', () => { it('should be possible to unrevoke a credential', async () => { const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) let assetCredential = await PublicCredentials.fetchCredentialFromChain( credentialId @@ -211,7 +211,7 @@ describe('When there is an attester and ctype NFT name', () => { const unrevocationTx = api.tx.publicCredentials.unrevoke(credentialId, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, unrevocationTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -234,7 +234,7 @@ describe('When there is an attester and ctype NFT name', () => { it('should be possible to remove a credential', async () => { const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) let encodedAssetCredential = await api.call.publicCredentials.getById( credentialId @@ -246,7 +246,7 @@ describe('When there is an attester and ctype NFT name', () => { const removalTx = api.tx.publicCredentials.remove(credentialId, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, removalTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -284,7 +284,7 @@ describe('When there is an issued public credential', () => { await issueCredential(latestCredential) const credentialId = PublicCredentials.getIdForCredential( latestCredential, - attester.uri + attester.id ) credential = await PublicCredentials.fetchCredentialFromChain(credentialId) }) @@ -345,7 +345,7 @@ describe('When there is an issued public credential', () => { const credentialWithDifferentSubject = { ...credential, subject: - 'did:asset:eip155:1.erc721:0x6d19295A5E47199D823D8793942b21a256ef1A4d' as AssetDidUri, + 'did:asset:eip155:1.erc721:0x6d19295A5E47199D823D8793942b21a256ef1A4d' as AssetDid, } await expect( PublicCredentials.verifyCredential(credentialWithDifferentSubject) @@ -383,7 +383,7 @@ describe('When there is an issued public credential', () => { it('should not be verified when another party receives it if it has different attester info', async () => { const credentialWithDifferentAttester = { ...credential, - attester: Did.getFullDidUri(devAlice.address), + attester: Did.getFullDid(devAlice.address), } await expect( PublicCredentials.verifyCredential(credentialWithDifferentAttester) @@ -433,7 +433,7 @@ describe('When there is an issued public credential', () => { // Revoke first const revocationTx = api.tx.publicCredentials.revoke(credential.id, null) const authorizedTx = await Did.authorizeTx( - attester.uri, + attester.id, revocationTx, attesterKey.getSignCallback(attester), tokenHolder.address @@ -485,11 +485,11 @@ describe('When there is a batch which contains a credential creation', () => { } // A batchAll with a DID call, and a nested batch with a second DID call and a nested forceBatch batch with a third DID call. const currentAttesterNonce = Did.documentFromChain( - await api.query.did.did(Did.toChain(attester.uri)) + await api.query.did.did(Did.toChain(attester.id)) ).lastTxCounter const batchTx = api.tx.utility.batchAll([ await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential1)), attesterKey.getSignCallback(attester), tokenHolder.address, @@ -497,7 +497,7 @@ describe('When there is a batch which contains a credential creation', () => { ), api.tx.utility.batch([ await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add(PublicCredentials.toChain(credential2)), attesterKey.getSignCallback(attester), tokenHolder.address, @@ -505,7 +505,7 @@ describe('When there is a batch which contains a credential creation', () => { ), api.tx.utility.forceBatch([ await Did.authorizeTx( - attester.uri, + attester.id, api.tx.publicCredentials.add( PublicCredentials.toChain(credential3) ), diff --git a/tests/integration/Web3Names.spec.ts b/tests/integration/Web3Names.spec.ts index 1c00908e5..2904bab2c 100644 --- a/tests/integration/Web3Names.spec.ts +++ b/tests/integration/Web3Names.spec.ts @@ -37,8 +37,8 @@ describe('When there is an Web3NameCreator and a payer', () => { let otherWeb3NameCreator: DidDocument let paymentAccount: KiltKeyringPair let otherPaymentAccount: KeyringPair - let nick: Did.Web3Name - let differentNick: Did.Web3Name + let nick: string + let differentNick: string beforeAll(async () => { nick = `nick_${randomAsHex(2)}` @@ -68,7 +68,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const tx = api.tx.web3Names.claim(nick) const bobbyBroke = makeSigningKeyTool().keypair const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), bobbyBroke.address @@ -82,7 +82,7 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should be possible to create a w3n name with enough tokens', async () => { const tx = api.tx.web3Names.claim(nick) const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address @@ -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 uri with the given nick', async () => { + it('should be possible to lookup the DID with the given nick', async () => { const { - document: { uri }, + document: { id }, } = Did.linkedInfoFromChain(await api.call.did.queryByWeb3Name(nick)) - expect(uri).toStrictEqual(w3nCreator.uri) + expect(id).toStrictEqual(w3nCreator.id) }, 30_000) - it('should be possible to lookup the nick with the given DID uri', async () => { - const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.uri)) + it('should be possible to lookup the nick with the given DID', async () => { + const encodedDidInfo = await api.call.did.query(Did.toChain(w3nCreator.id)) const didInfo = Did.linkedInfoFromChain(encodedDidInfo) - expect(didInfo.web3Name).toBe(nick) + expect(didInfo.document.alsoKnownAs).toStrictEqual([`w3n:${nick}`]) }, 30_000) it('should not be possible to create the same w3n twice', async () => { const tx = api.tx.web3Names.claim(nick) const authorizedTx = await Did.authorizeTx( - otherWeb3NameCreator.uri, + otherWeb3NameCreator.id, tx, otherW3NCreatorKey.getSignCallback(otherWeb3NameCreator), paymentAccount.address @@ -124,7 +124,7 @@ describe('When there is an Web3NameCreator and a payer', () => { it('should not be possible to create a second w3n for the same did', async () => { const tx = api.tx.web3Names.claim('nick2') const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address @@ -156,7 +156,7 @@ describe('When there is an Web3NameCreator and a payer', () => { // prepare the w3n on chain const prepareTx = api.tx.web3Names.claim(differentNick) const prepareAuthorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, prepareTx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address @@ -165,7 +165,7 @@ describe('When there is an Web3NameCreator and a payer', () => { const tx = api.tx.web3Names.releaseByOwner() const authorizedTx = await Did.authorizeTx( - w3nCreator.uri, + w3nCreator.id, tx, w3nCreatorKey.getSignCallback(w3nCreator), paymentAccount.address diff --git a/tests/testUtils/TestUtils.ts b/tests/testUtils/TestUtils.ts index 10b14efa2..074dc8921 100644 --- a/tests/testUtils/TestUtils.ts +++ b/tests/testUtils/TestUtils.ts @@ -10,24 +10,33 @@ import { blake2AsHex, blake2AsU8a } from '@polkadot/util-crypto' import type { DecryptCallback, DidDocument, - DidKey, - DidServiceEndpoint, - DidVerificationKey, EncryptCallback, - KeyRelationship, KeyringPair, + KiltAddress, KiltEncryptionKeypair, KiltKeyringPair, - LightDidSupportedVerificationKeyType, - NewLightDidVerificationKey, SignCallback, + SubmittableExtrinsic, + UriFragment, + VerificationMethod, + VerificationRelationship, } from '@kiltprotocol/types' -import { Crypto } from '@kiltprotocol/utils' -import * as Did from '@kiltprotocol/did' +import type { + BaseNewDidKey, + ChainDidKey, + DidVerificationMethodType, + GetStoreTxSignCallback, + LightDidSupportedVerificationKeyType, + NewLightDidVerificationKey, + NewDidVerificationKey, + NewDidEncryptionKey, + NewService, +} from '@kiltprotocol/did' +import { Crypto, SDKErrors } from '@kiltprotocol/utils' import { Blockchain } from '@kiltprotocol/chain-helpers' import { ConfigService } from '@kiltprotocol/config' -import { linkedInfoFromChain, toChain } from '@kiltprotocol/did' +import * as Did from '@kiltprotocol/did' export type EncryptionKeyToolCallback = ( didDocument: DidDocument @@ -45,10 +54,13 @@ export function makeEncryptCallback({ }: KiltEncryptionKeypair): EncryptionKeyToolCallback { return (didDocument) => { return async function encryptCallback({ data, peerPublicKey }) { - const keyId = didDocument.keyAgreement?.[0].id + const keyId = didDocument.keyAgreement?.[0] if (!keyId) { - throw new Error(`Encryption key not found in did "${didDocument.uri}"`) + throw new Error(`Encryption key not found in did "${didDocument.id}"`) } + const verificationMethod = didDocument.verificationMethod?.find( + (v) => v.id === keyId + ) as VerificationMethod const { box, nonce } = Crypto.encryptAsymmetric( data, peerPublicKey, @@ -57,7 +69,7 @@ export function makeEncryptCallback({ return { nonce, data: box, - keyUri: `${didDocument.uri}${keyId}`, + verificationMethod, } } } @@ -119,20 +131,26 @@ export type KeyToolSignCallback = (didDocument: DidDocument) => SignCallback */ export function makeSignCallback(keypair: KeyringPair): KeyToolSignCallback { return (didDocument) => - async function sign({ data, keyRelationship }) { - const keyId = didDocument[keyRelationship]?.[0].id - const keyType = didDocument[keyRelationship]?.[0].type - if (keyId === undefined || keyType === undefined) { + async function sign({ data, verificationRelationship }) { + const keyId = didDocument[verificationRelationship]?.[0] + if (keyId === undefined) { throw new Error( - `Key for purpose "${keyRelationship}" not found in did "${didDocument.uri}"` + `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}"` ) } const signature = keypair.sign(data, { withType: false }) return { signature, - keyUri: `${didDocument.uri}${keyId}`, - keyType, + verificationMethod, } } } @@ -152,7 +170,9 @@ export function makeStoreDidCallback( const signature = keypair.sign(data, { withType: false }) return { signature, - keyType: keypair.type, + verificationMethod: { + publicKeyMultibase: Did.keypairToMultibaseKey(keypair), + }, } } } @@ -185,6 +205,48 @@ export function makeSigningKeyTool( } } +function doesVerificationMethodExist( + didDocument: DidDocument, + { id }: Pick +): boolean { + return ( + didDocument.verificationMethod?.find((vm) => vm.id === id) !== undefined + ) +} + +function addVerificationMethod( + didDocument: DidDocument, + verificationMethod: VerificationMethod, + relationship: 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. * @@ -203,14 +265,14 @@ export async function createMinimalLightDidFromKeypair( } // Mock function to generate a key ID without having to rely on a real chain metadata. -export function computeKeyId(key: DidKey['publicKey']): DidKey['id'] { +export function computeKeyId(key: ChainDidKey['publicKey']): ChainDidKey['id'] { return `#${blake2AsHex(key, 256)}` } function makeDidKeyFromKeypair({ publicKey, type, -}: KiltKeyringPair): DidVerificationKey { +}: KiltKeyringPair): ChainDidKey { return { id: computeKeyId(publicKey), publicKey, @@ -223,54 +285,92 @@ function makeDidKeyFromKeypair({ * * @param keypair The KeyringPair for authentication key, other keys derived from it. * @param generationOptions The additional options for generation. - * @param generationOptions.keyRelationships The set of key relationships to indicate which keys must be added to the DID. - * @param generationOptions.endpoints The set of service endpoints that must be added to the DID. + * @param generationOptions.verificationRelationships The set of verification relationships to indicate which keys must be added to the DID. + * @param generationOptions.endpoints The set of services that must be added to the DID. * * @returns A promise resolving to a [[DidDocument]] object. The resulting object is NOT stored on chain. */ export async function createLocalDemoFullDidFromKeypair( keypair: KiltKeyringPair, { - keyRelationships = new Set([ + verificationRelationships = new Set([ 'assertionMethod', 'capabilityDelegation', 'keyAgreement', ]), endpoints = [], }: { - keyRelationships?: Set> - endpoints?: DidServiceEndpoint[] + verificationRelationships?: Set< + Omit + > + endpoints?: NewService[] } = {} ): Promise { - const authKey = makeDidKeyFromKeypair(keypair) - const uri = Did.getFullDidUriFromKey(authKey) + const { + type: keyType, + publicKey, + id: authKeyId, + } = makeDidKeyFromKeypair(keypair) + const id = Did.getFullDidFromVerificationMethod({ + publicKeyMultibase: Did.keypairToMultibaseKey({ + type: keyType, + publicKey, + }), + }) const result: DidDocument = { - uri, - authentication: [authKey], + id, + authentication: [authKeyId], + verificationMethod: [ + Did.didKeyToVerificationMethod(id, authKeyId, { + keyType, + publicKey, + }), + ], service: endpoints, } - if (keyRelationships.has('keyAgreement')) { - const encryptionKeypair = makeEncryptionKeyTool(`${keypair.publicKey}//enc`) - .keyAgreement[0] - const encKey = { - ...encryptionKeypair, - id: computeKeyId(encryptionKeypair.publicKey), - } - result.keyAgreement = [encKey] + 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('assertionMethod')) { - const attKey = makeDidKeyFromKeypair( + if (verificationRelationships.has('assertionMethod')) { + const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//att') as KiltKeyringPair ) - result.assertionMethod = [attKey] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'assertionMethod' + ) } - if (keyRelationships.has('capabilityDelegation')) { - const delKey = makeDidKeyFromKeypair( + if (verificationRelationships.has('capabilityDelegation')) { + const { publicKey: encPublicKey, type } = makeDidKeyFromKeypair( keypair.derive('//del') as KiltKeyringPair ) - result.capabilityDelegation = [delKey] + addKeypairAsVerificationMethod( + result, + { + id: computeKeyId(encPublicKey), + publicKey: encPublicKey, + type, + }, + 'capabilityDelegation' + ) } return result @@ -286,10 +386,10 @@ export async function createLocalDemoFullDidFromKeypair( export async function createLocalDemoFullDidFromLightDid( lightDid: DidDocument ): Promise { - const { uri, authentication } = lightDid + const { id, authentication } = lightDid return { - uri: Did.getFullDidUri(uri), + id, authentication, assertionMethod: authentication, capabilityDelegation: authentication, @@ -297,6 +397,92 @@ export async function createLocalDemoFullDidFromLightDid( } } +/** + * Create a DID creation operation which would write to chain the DID Document provided as input. + * Only the first authentication, assertion, and capability delegation verification methods are considered from the input DID Document. + * All the input DID Document key agreement verification methods are considered. + * + * The resulting extrinsic can be submitted to create an on-chain DID that has the provided verification methods and services. + * + * A DID creation operation can contain at most 25 new services. + * Additionally, each service must respect the following conditions: + * - The service ID is at most 50 bytes long and is a valid URI fragment according to RFC#3986. + * - The service has at most 1 service type, with a value that is at most 50 bytes long. + * - The service has at most 1 URI, with a value that is at most 200 bytes long, and which is a valid URI according to RFC#3986. + * + * @param input The DID Document to store. + * @param submitter The KILT address authorized to submit the creation operation. + * @param sign The sign callback. The authentication key has to be used. + * + * @returns The SubmittableExtrinsic for the DID creation operation. + */ +export async function getStoreTxFromDidDocument( + input: DidDocument, + submitter: KiltAddress, + sign: GetStoreTxSignCallback +): Promise { + const { + authentication, + assertionMethod, + keyAgreement, + capabilityDelegation, + service, + verificationMethod, + } = input + + const [authKey, assertKey, delKey, ...encKeys] = [ + authentication?.[0], + assertionMethod?.[0], + capabilityDelegation?.[0], + ...(keyAgreement ?? []), + ].map((keyId): BaseNewDidKey | undefined => { + if (!keyId) { + return undefined + } + const key = verificationMethod?.find((vm) => vm.id === keyId) + if (key === undefined) { + throw new SDKErrors.DidError( + `A verification method with ID "${keyId}" was not found in the \`verificationMethod\` property of the provided DID Document.` + ) + } + const { keyType, publicKey } = Did.multibaseKeyToDidKey( + key.publicKeyMultibase + ) + if ( + !Did.isValidDidVerificationType(keyType) && + !Did.isValidEncryptionMethodType(keyType) + ) { + throw new SDKErrors.DidError( + `Verification method with ID "${keyId}" has an unsupported type "${keyType}".` + ) + } + return { + type: keyType, + publicKey, + } + }) + + if (authKey === undefined) { + throw new SDKErrors.DidError( + 'Cannot create a DID without an authentication method.' + ) + } + + const storeTxInput: Parameters[0] = { + authentication: [authKey as NewDidVerificationKey], + assertionMethod: assertKey + ? [assertKey as NewDidVerificationKey] + : undefined, + capabilityDelegation: delKey + ? [delKey as NewDidVerificationKey] + : undefined, + keyAgreement: encKeys as NewDidEncryptionKey[], + service, + } + + return Did.getStoreTx(storeTxInput, submitter, sign) +} + // It takes the auth key from the light DID and use it as attestation and delegation key as well. export async function createFullDidFromLightDid( payer: KiltKeyringPair, @@ -304,22 +490,27 @@ export async function createFullDidFromLightDid( sign: StoreDidCallback ): Promise { const api = ConfigService.get('api') - const { authentication, uri } = lightDidForId - const tx = await Did.getStoreTx( - { - authentication, - assertionMethod: authentication, - capabilityDelegation: authentication, - keyAgreement: lightDidForId.keyAgreement, - service: lightDidForId.service, - }, + const fullDidDocumentToBeCreated = lightDidForId + fullDidDocumentToBeCreated.assertionMethod = [ + // 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, payer.address, sign ) await Blockchain.signAndSubmitTx(tx, payer) const queryFunction = api.call.did?.query ?? api.call.didApi.queryDid - const encodedDidDetails = await queryFunction(toChain(Did.getFullDidUri(uri))) - return linkedInfoFromChain(encodedDidDetails).document + const encodedDidDetails = await queryFunction( + Did.toChain(fullDidDocumentToBeCreated.id) + ) + const { document } = await Did.linkedInfoFromChain(encodedDidDetails) + return document } export async function createFullDidFromSeed( diff --git a/yarn.lock b/yarn.lock index 20bdd665d..1ac7b6ba2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1533,6 +1533,13 @@ __metadata: languageName: node linkType: hard +"@digitalbazaar/multikey-context@npm:^1.0.0": + version: 1.0.0 + resolution: "@digitalbazaar/multikey-context@npm:1.0.0" + checksum: e651253ce101a0a5de0e46de5c6f7c936aa07fd14e5b14d38ba57c105eaa2490c256245482bc1f5173269a992cab2ebe5eec6c26523f0e61aae03d3a6788cc6b + languageName: node + linkType: hard + "@digitalbazaar/security-context@npm:^1.0.0": version: 1.0.0 resolution: "@digitalbazaar/security-context@npm:1.0.0" @@ -2012,6 +2019,7 @@ __metadata: version: 0.0.0-use.local resolution: "@kiltprotocol/did@workspace:packages/did" dependencies: + "@digitalbazaar/multikey-context": ^1.0.0 "@digitalbazaar/security-context": ^1.0.0 "@kiltprotocol/augment-api": "workspace:*" "@kiltprotocol/config": "workspace:*" @@ -2023,6 +2031,7 @@ __metadata: "@polkadot/types-codec": ^10.4.0 "@polkadot/util": ^12.0.0 "@polkadot/util-crypto": ^12.0.0 + multibase: ^4.0.6 rimraf: ^3.0.2 typescript: ^4.8.3 languageName: unknown @@ -2124,6 +2133,13 @@ __metadata: languageName: unknown linkType: soft +"@multiformats/base-x@npm:^4.0.1": + version: 4.0.1 + resolution: "@multiformats/base-x@npm:4.0.1" + checksum: ecbf84bdd7613fd795e4a41f20f3e8cc7df8bbee84690b7feed383d45a638ed228a80ff6f5c930373cbf24539f64857b66023ee3c1e914f6bac9995c76414a87 + languageName: node + linkType: hard + "@noble/curves@npm:1.0.0": version: 1.0.0 resolution: "@noble/curves@npm:1.0.0" @@ -7268,6 +7284,15 @@ fsevents@^2.3.2: languageName: node linkType: hard +"multibase@npm:^4.0.6": + version: 4.0.6 + resolution: "multibase@npm:4.0.6" + dependencies: + "@multiformats/base-x": ^4.0.1 + checksum: 891ce47f509c6070d2306e7e00aef3ef41fbb50a848a1e1bec5e75ca63c5032015a436cf09e9e3939b5b2ca81e74804151eb410a388f10e9aabf7a2f5a35d272 + languageName: node + linkType: hard + "nan@npm:^2.15.0": version: 2.15.0 resolution: "nan@npm:2.15.0"