diff --git a/packages/crypto/src/jose.ts b/packages/crypto/src/jose.ts index bc9671f31..2175df80a 100644 --- a/packages/crypto/src/jose.ts +++ b/packages/crypto/src/jose.ts @@ -477,6 +477,41 @@ const joseToMulticodecMapping: { [key: string]: string } = { export class Jose { + /** + * Canonicalizes a given object according to RFC 8785 (https://tools.ietf.org/html/rfc8785), + * which describes JSON Canonicalization Scheme (JCS). This function sorts the keys of the + * object and its nested objects alphabetically and then returns a stringified version of it. + * This method handles nested objects, array values, and null values appropriately. + * + * @param obj - The object to canonicalize. + * @returns The stringified version of the input object with its keys sorted alphabetically + * per RFC 8785. + */ + public static canonicalize(obj: { [key: string]: any }): string { + /** + * Recursively sorts the keys of an object. + * + * @param obj - The object whose keys are to be sorted. + * @returns A new object with sorted keys. + */ + const sortObjKeys = (obj: { [key: string]: any }): { [key: string]: any } => { + if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) { + const sortedKeys = Object.keys(obj).sort(); + const sortedObj: { [key: string]: any } = {}; + for (const key of sortedKeys) { + // Recursively sort keys of nested objects. + sortedObj[key] = sortObjKeys(obj[key]); + } + return sortedObj; + } + return obj; + }; + + // Stringify and return the final sorted object. + const sortedObj = sortObjKeys(obj); + return JSON.stringify(sortedObj); + } + public static async cryptoKeyToJwk(options: { key: Web5Crypto.CryptoKey, }): Promise { @@ -897,7 +932,7 @@ export class Jose { params.push(options.namedCurve); /** - * All symmetric encryption (AES) WebCrypto algorithms + * AES symmetric encryption WebCrypto algorithms * set a value for the "length" parameter. */ } else if ('length' in options && options.length !== undefined) { @@ -923,15 +958,6 @@ export class Jose { return { ...jose }; } - - private static canonicalize(obj: { [key: string]: any }): string { - const sortedKeys = Object.keys(obj).sort(); - const sortedObj = sortedKeys.reduce<{ [key: string]: any }>((acc, key) => { - acc[key] = obj[key]; - return acc; - }, {}); - return JSON.stringify(sortedObj); - } } type Constructable = new (...args: any[]) => object; diff --git a/packages/dids/src/did-jwk.ts b/packages/dids/src/did-jwk.ts new file mode 100644 index 000000000..977bd8c53 --- /dev/null +++ b/packages/dids/src/did-jwk.ts @@ -0,0 +1,355 @@ +import type { JwkUse, PrivateKeyJwk,PublicKeyJwk, Web5Crypto } from '@web5/crypto'; + +import { Convert } from '@web5/common'; +import { EcdhAlgorithm, EcdsaAlgorithm, EdDsaAlgorithm, Jose } from '@web5/crypto'; + +import type { + DidMethod, + DidDocument, + PortableDid, + DidResolutionResult, + DidResolutionOptions, + VerificationRelationship, + DidKeySetVerificationMethodKey, +} from './types.js'; + +import { parseDid } from './utils.js'; + +const SupportedCryptoAlgorithms = [ + 'Ed25519', + 'secp256k1', + 'X25519' +] as const; + +export type DidJwkCreateOptions = { + keyAlgorithm?: typeof SupportedCryptoAlgorithms[number]; + keySet?: DidJwkKeySet; +} + +export type DidJwkCreateDocumentOptions = { + publicKeyJwk: PublicKeyJwk; +} + +export type DidJwkKeySet = { + verificationMethodKeys?: DidKeySetVerificationMethodKey[]; +} + +/** + * The `DidJwkMethod` class provides an implementation of a Decentralized Identifier (DID) method + * based on JSON Web Keys (JWK) that deterministically transforms a JWK into a DID Document. This + * class supports the generation, encoding, decoding, and resolution of DIDs and DID Documents + * using the `did:jwk` method. + * + * See the {@link https://github.com/quartzjer/did-jwk/blob/main/spec.md | did:jwk} spec for more + * information. + * + * Example usage: + * + * ```ts + * const portableDid = await DidJwkMethod.create({ keyAlgorithm: 'Ed25519' }); + * const didDocument = await DidJwkMethod.createDocument({ publicKeyJwk: portableDid.keySet.verificationMethodKeys[0].publicKeyJwk }); + * const resolvedDid = await DidJwkMethod.resolve({ didUrl: portableDid.did }); + * ``` + */ +export class DidJwkMethod implements DidMethod { + + /** + * Name of the DID method + */ + public static methodName = 'jwk'; + + /** + * Creates a new DID using the `did:jwk` method. + * + * @param options - Optional parameters for creating the DID. + * @returns A Promise resolving to a `PortableDid` object representing the new DID. + */ + public static async create(options?: DidJwkCreateOptions): Promise { + let { keyAlgorithm, keySet } = options ?? {}; + + // Begin constructing a PortableDid. + const portableDid: Partial = {}; + + // If keySet not given, generate a default key set. + portableDid.keySet = keySet ?? await DidJwkMethod.generateKeySet({ keyAlgorithm }); + + // Verify that the key set contains one public key. + const publicKeyJwk = portableDid.keySet.verificationMethodKeys?.[0]?.publicKeyJwk; + if (!publicKeyJwk) { + throw new Error('DidJwkMethod: Failed to create DID with given input.'); + } + + // Encode the public key JWK to a DID string. + portableDid.did = await DidJwkMethod.encodeJwk({ publicKeyJwk }); + + // Expand the DID string to a DID document. + portableDid.document = await DidJwkMethod.createDocument({ publicKeyJwk }); + + return portableDid as PortableDid; + } + + /** + * Expands a did:jwk identifier to a DID Document. + * + * @param options + * @returns - A DID document. + */ + public static async createDocument(options: DidJwkCreateDocumentOptions): Promise { + const { publicKeyJwk } = options; + + // Initialize document to an empty object. + const document: Partial = {}; + + // Set the @context property. + document['@context'] = [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1' + ]; + + // Encode the public key JWK to a DID string and set as the document identifier. + document.id = await DidJwkMethod.encodeJwk({ publicKeyJwk }); + + const keyId = `${document.id}#0`; + + // Set the verificationMethod property. + document.verificationMethod = [{ + id : keyId, + type : 'JsonWebKey2020', + controller : document.id, + publicKeyJwk : publicKeyJwk + }]; + + document.authentication = [keyId]; + document.assertionMethod = [keyId]; + document.capabilityInvocation = [keyId]; + document.capabilityDelegation = [keyId]; + document.keyAgreement = [keyId]; + + /** If the JWK contains a `use` property with the value "sig" then the `keyAgreement` property + * is not included in the DID Document. If the `use` value is "enc" then only the `keyAgreement` + * property is included in the DID Document. */ + switch (publicKeyJwk.use) { + case 'sig': { + delete document.keyAgreement; + break; + } + + case 'enc': { + delete document.authentication; + delete document.assertionMethod; + delete document.capabilityInvocation; + delete document.capabilityDelegation; + break; + } + } + + return document as DidDocument; + } + + /** + * Decodes a `did:jwk` identifier to a public key in JWK format. + * + * @param options - The options for the operation. + * @returns A Promise resolving to a `PublicKeyJwk` object representing the public key. + */ + public static async decodeJwk(options: { + didUrl: string + }): Promise { + const { didUrl } = options; + + let publicKeyJwk: PublicKeyJwk; + + try { + const parsedDid = parseDid({ didUrl }); + if (parsedDid?.method !== 'jwk') throw new Error('Failed to parse DID.'); + + publicKeyJwk = Convert.base64Url(parsedDid.id).toObject() as PublicKeyJwk; + + } catch (error: any) { + throw new Error(`DidJwkMethod: Unable to decode DID: ${didUrl}.`); + } + + return publicKeyJwk; + } + + /** + * Encodes a public key in JWK format to a `did:jwk` identifier. + * + * @param options - The options for the operation. + * @returns A Promise resolving to a string representing the `did:jwk` identifier. + */ + public static async encodeJwk(options: { + publicKeyJwk: PublicKeyJwk + }): Promise { + const { publicKeyJwk } = options; + + let did: string; + + try { + // Serialize the public key JWK to a UTF-8 string. + const publicKeyJwkString = Jose.canonicalize(publicKeyJwk); + + // Encode to Base64Url format. + const publicKeyJwkBase64Url = Convert.string(publicKeyJwkString).toBase64Url(); + + // Attach the prefix `did:jwk`. + did = `did:jwk:${publicKeyJwkBase64Url}`; + + } catch (error: any) { + throw new Error(`DidJwkMethod: Unable to encode JWK.`); + } + + return did; + } + + /** + * Generates a key set for use with the `did:jwk` method. + * + * @param options - Optional parameters for generating the key set. + * @returns A Promise resolving to a `DidJwkKeySet` object representing the key set. + */ + public static async generateKeySet(options?: { + keyAlgorithm?: typeof SupportedCryptoAlgorithms[number] + }): Promise { + // Generate Ed25519 keys, by default. + const { keyAlgorithm = 'Ed25519' } = options ?? {}; + + let keyUse: JwkUse; + let keyPair: Web5Crypto.CryptoKeyPair; + let verificationRelationships: VerificationRelationship[]; + + switch (keyAlgorithm) { + case 'Ed25519': { + keyPair = await new EdDsaAlgorithm().generateKey({ + algorithm : { name: 'EdDSA', namedCurve: 'Ed25519' }, + extractable : true, + keyUsages : ['sign', 'verify'] + }); + keyUse = 'sig'; + verificationRelationships = ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation']; + break; + } + + case 'secp256k1': { + keyPair = await new EcdsaAlgorithm().generateKey({ + algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, + extractable : true, + keyUsages : ['sign', 'verify'] + }); + keyUse = 'sig'; + verificationRelationships = ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation']; + break; + } + + case 'X25519': { + keyPair = await new EcdhAlgorithm().generateKey({ + algorithm : { name: 'ECDH', namedCurve: 'X25519' }, + extractable : true, + keyUsages : ['deriveBits', 'deriveKey'] + }); + keyUse = 'enc'; + verificationRelationships = ['keyAgreement']; + break; + } + + default: { + throw new Error(`Unsupported crypto algorithm: '${keyAlgorithm}'`); + } + } + + // Convert the key pair to JWK format. + const publicKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.publicKey }) as PublicKeyJwk; + const privateKeyJwk = await Jose.cryptoKeyToJwk({ key: keyPair.privateKey }) as PrivateKeyJwk; + + // Add the `use` property to each JWK. + publicKeyJwk.use = keyUse; + privateKeyJwk.use = keyUse; + + // Create the key set. + const keySet: DidJwkKeySet = { + verificationMethodKeys: [{ + publicKeyJwk, + privateKeyJwk, + relationships: verificationRelationships + }] + }; + + return keySet; + } + + /** + * Given the W3C DID Document of a `did:jwk` DID, return the identifier of + * the verification method key that will be used for signing messages and + * credentials, by default. + * + * @param document = DID Document to get the default signing key from. + * @returns Verification method identifier for the default signing key. + */ + public static async getDefaultSigningKey(options: { + didDocument: DidDocument + }): Promise { + const { didDocument } = options; + + const signingKeyId = `${didDocument.id}#0`; + + return signingKeyId; + } + + /** + * Resolves a `did:jwk` identifier to a DID Document. + * + * @param options - The options for the operation. + * @returns A Promise resolving to a `DidResolutionResult` object representing the result of the resolution. + */ + public static async resolve(options: { + didUrl: string, + resolutionOptions?: DidResolutionOptions + }): Promise { + const { didUrl, resolutionOptions: _ } = options; + // TODO: Implement resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution + + const parsedDid = parseDid({ didUrl }); + if (!parsedDid) { + return { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : undefined, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + error : 'invalidDid', + errorMessage : `Cannot parse DID: ${didUrl}` + } + }; + } + + if (parsedDid.method !== 'jwk') { + return { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : undefined, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + error : 'methodNotSupported', + errorMessage : `Method not supported: ${parsedDid.method}` + } + }; + } + + const publicKeyJwk = await DidJwkMethod.decodeJwk({ didUrl }); + const didDocument = await DidJwkMethod.createDocument({ publicKeyJwk }); + + return { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + did : { + didString : parsedDid.did, + methodSpecificId : parsedDid.id, + method : parsedDid.method + } + } + }; + } +} \ No newline at end of file diff --git a/packages/dids/tests/did-jwk.spec.ts b/packages/dids/tests/did-jwk.spec.ts new file mode 100644 index 000000000..461ce160c --- /dev/null +++ b/packages/dids/tests/did-jwk.spec.ts @@ -0,0 +1,233 @@ +import type { PublicKeyJwk } from '@web5/crypto'; + +import { expect } from 'chai'; + +import type { DidJwkCreateOptions } from '../src/did-jwk.js'; +import type { DidDocument, PortableDid } from '../src/types.js'; + +import { DidJwkMethod } from '../src/did-jwk.js'; +import { DidResolver } from '../src/did-resolver.js'; +import { didJwkCreateTestVectors, didJwkResolveTestVectors } from './fixtures/test-vectors/did-jwk.js'; + +describe('DidJwkMethod', () => { + describe('create()', () => { + it('creates a DID with Ed25519 keys, by default', async () => { + const portableDid = await DidJwkMethod.create(); + + // Verify expected result. + expect(portableDid).to.have.property('did'); + expect(portableDid).to.have.property('document'); + expect(portableDid).to.have.property('keySet'); + expect(portableDid.keySet).to.have.property('verificationMethodKeys'); + expect(portableDid.keySet.verificationMethodKeys).to.have.length(1); + expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); + expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); + expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('alg', 'EdDSA'); + expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('crv', 'Ed25519'); + }); + + it('creates a DID with secp256k1 keys, if specified', async () => { + const portableDid = await DidJwkMethod.create({ keyAlgorithm: 'secp256k1' }); + + // Verify expected result. + expect(portableDid).to.have.property('did'); + expect(portableDid).to.have.property('document'); + expect(portableDid).to.have.property('keySet'); + expect(portableDid.keySet).to.have.property('verificationMethodKeys'); + expect(portableDid.keySet.verificationMethodKeys).to.have.length(1); + expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); + expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); + expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('alg', 'ES256K'); + expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('crv', 'secp256k1'); + }); + + it('creates a DID with X25519 keys, if specified', async () => { + const portableDid = await DidJwkMethod.create({ keyAlgorithm: 'X25519' }); + + // Verify expected result. + expect(portableDid).to.have.property('did'); + expect(portableDid).to.have.property('document'); + expect(portableDid).to.have.property('keySet'); + expect(portableDid.keySet).to.have.property('verificationMethodKeys'); + expect(portableDid.keySet.verificationMethodKeys).to.have.length(1); + expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); + expect(portableDid.keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); + expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.not.have.property('alg'); + expect(portableDid.keySet.verificationMethodKeys?.[0].publicKeyJwk).to.have.property('crv', 'X25519'); + }); + + it(`does not include the 'keyAgreement' relationship for Ed25519 and secp256k1 keys`, async () => { + let portableDid: PortableDid; + + // Verify for Ed25519. + portableDid = await DidJwkMethod.create({ keyAlgorithm: 'Ed25519' }); + expect(portableDid.document).to.not.have.property('keyAgreement'); + + // Verify for secp256k1. + portableDid = await DidJwkMethod.create({ keyAlgorithm: 'secp256k1' }); + expect(portableDid.document).to.not.have.property('keyAgreement'); + }); + + it(`only specifies 'keyAgreement' relationship for X25519 keys`, async () => { + const portableDid = await DidJwkMethod.create({ keyAlgorithm: 'X25519' }); + + expect(portableDid.document).to.have.property('keyAgreement'); + expect(portableDid.document).to.not.have.property('assertionMethod'); + expect(portableDid.document).to.not.have.property('authentication'); + expect(portableDid.document).to.not.have.property('capabilityDelegation'); + expect(portableDid.document).to.not.have.property('capabilityInvocation'); + }); + + it('throws an error if no public key is found', async () => { + await expect(DidJwkMethod.create({ keySet: {} })).to.be.rejectedWith('Failed to create DID with given input.'); + }); + + for (const vector of didJwkCreateTestVectors ) { + it(`passes test vector ${vector.id}`, async () => { + const portableDid = await DidJwkMethod.create(vector.input as DidJwkCreateOptions); + + expect(portableDid.did).to.deep.equal(vector.output.did); + expect(portableDid.document).to.deep.equal(vector.output.document); + expect(portableDid.keySet).to.deep.equal(vector.output.keySet); + }); + } + }); + + describe('createDocument()', () => { + it('creates a DidDocument from a public key', async () => { + const publicKeyJwk: PublicKeyJwk = { + alg : 'EdDSA', + crv : 'Ed25519', + kty : 'OKP', + ext : 'true', + key_ops : [ 'verify' ], + x : 'Tg1q17C1km4-YYDg-1z5JB_hbvc2vapyXihGi1dxZ7s', + use : 'sig' + }; + const document = await DidJwkMethod.createDocument({ publicKeyJwk }); + + expect(document).to.have.property('id'); + expect(document).to.have.property('verificationMethod'); + expect(document).to.have.property('authentication'); + }); + }); + + describe('decodeJwk()', () => { + it('decodes a didUrl to a public key JWK', async () => { + const portableDid = await DidJwkMethod.create(); + const publicKeyJwk = await DidJwkMethod.decodeJwk({ didUrl: portableDid.did }); + + expect(publicKeyJwk).to.be.an('object'); + expect(publicKeyJwk).to.have.property('alg'); + expect(publicKeyJwk).to.have.property('crv'); + expect(publicKeyJwk).to.have.property('kty'); + expect(publicKeyJwk).to.have.property('use'); + expect(publicKeyJwk).to.have.property('x'); + }); + + it('throws an error for invalid didUrl', async () => { + await expect(DidJwkMethod.decodeJwk({ didUrl: 'invalid' })).to.be.rejectedWith('Unable to decode DID: invalid'); + }); + }); + + describe('encodeJwk()', () => { + it('encodes a PublicKeyJwk to a DID string', async () => { + const { keySet } = await DidJwkMethod.create(); + const publicKeyJwk = keySet.verificationMethodKeys?.[0]?.publicKeyJwk; + const did = await DidJwkMethod.encodeJwk({ publicKeyJwk }); + expect(did).to.be.a('string').and.to.match(/^did:jwk:/); + }); + + it('throws an error for invalid JWK', async () => { + const circularReference: any = {}; + circularReference.self = circularReference; + await expect(DidJwkMethod.encodeJwk({ publicKeyJwk: circularReference })).to.be.rejectedWith('Unable to encode JWK'); + }); + }); + + describe('generateKeySet()', () => { + it('generates a DidJwkKeySet with default algorithm', async () => { + const keySet = await DidJwkMethod.generateKeySet(); + expect(keySet).to.have.property('verificationMethodKeys'); + expect(keySet.verificationMethodKeys?.[0]).to.have.property('publicKeyJwk'); + expect(keySet.verificationMethodKeys?.[0]).to.have.property('privateKeyJwk'); + }); + + it('throws an error for unsupported algorithm', async () => { + await expect(DidJwkMethod.generateKeySet({ keyAlgorithm: 'unsupported' as any })).to.be.rejectedWith('Unsupported crypto algorithm'); + }); + }); + + describe('getDefaultSigningKey()', () => { + it('should return the default signing key ID constructed from the DID document ID', async () => { + const didDocument: DidDocument = { + id: 'did:jwk:example', + // ... other properties + }; + const expectedSigningKeyId = 'did:jwk:example#0'; + + const signingKeyId = await DidJwkMethod.getDefaultSigningKey({ didDocument }); + + expect(signingKeyId).to.equal(expectedSigningKeyId); + }); + }); + + describe('resolve()', () => { + it('resolves a didUrl to a DidResolutionResult', async () => { + const portableDid = await DidJwkMethod.create(); + const result = await DidJwkMethod.resolve({ didUrl: portableDid.did }); + expect(result).to.have.property('didDocument'); + expect(result).to.have.property('didResolutionMetadata').which.has.property('did'); + }); + + it('resolves to alternate DID identifier but matching JWK due to canonicalization', async () => { + const didResolutionResult = await DidJwkMethod.resolve({ didUrl: 'did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9' }); + + // JWK should match the did:jwk spec test vector. + expect(didResolutionResult.didDocument.verificationMethod?.[0].publicKeyJwk).to.deep.equal({ + crv : 'X25519', + kty : 'OKP', + use : 'enc', + x : '3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08' + }); + + // But the DID identifier should be different due to canonicalization. + expect(didResolutionResult.didDocument.id).to.not.equal('did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9'); + expect(didResolutionResult.didDocument.id).to.equal('did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9'); + }); + + it('returns an error for invalid didUrl', async () => { + const result = await DidJwkMethod.resolve({ didUrl: 'invalid' }); + expect(result).to.have.property('didResolutionMetadata').which.has.property('error', 'invalidDid'); + }); + + it('returns an error for unsupported method', async () => { + const result = await DidJwkMethod.resolve({ didUrl: 'did:unsupported:xyz' }); + expect(result).to.have.property('didResolutionMetadata').which.has.property('error', 'methodNotSupported'); + }); + + for (const vector of didJwkResolveTestVectors ) { + it(`passes test vector ${vector.id}`, async () => { + const didResolutionResult = await DidJwkMethod.resolve(vector.input); + + expect(didResolutionResult).to.deep.equal(vector.output); + }); + } + }); + + describe('Integration with DidResolver', () => { + it('DidResolver resolves a did:jwk DID', async () => { + // Create a DID using the DidJwkMethod. + const { did, document: createdDocument } = await DidJwkMethod.create(); + + // Instantiate a DidResolver with the DidJwkMethod. + const didResolver = new DidResolver({ didResolvers: [DidJwkMethod] }); + + // Resolve the DID using the DidResolver. + const { didDocument: resolvedDocument } = await didResolver.resolve(did); + + // Verify that the resolved document matches the created document. + expect(resolvedDocument).to.deep.equal(createdDocument); + }); + }); +}); \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-jwk.ts b/packages/dids/tests/fixtures/test-vectors/did-jwk.ts new file mode 100644 index 000000000..0acb0ca41 --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/did-jwk.ts @@ -0,0 +1,110 @@ +export const didJwkCreateTestVectors = [ + { + id : 'did:jwk.create.1', + input : { + keySet: { + verificationMethodKeys: [{ + 'publicKeyJwk': { + 'alg' : 'EdDSA', + 'crv' : 'Ed25519', + 'kty' : 'OKP', + 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' + }, + relationships: ['authentication'] + }], + }, + keyAlgorithm: 'Ed25519', + }, + output: { + did : 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9', + document : { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1' + ], + id : 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9', + verificationMethod : [ + { + id : 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9#0', + type : 'JsonWebKey2020', + controller : 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9', + publicKeyJwk : { + alg : 'EdDSA', + crv : 'Ed25519', + kty : 'OKP', + x : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' + } + } + ], + assertionMethod: [ + 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9#0' + ], + authentication: [ + 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9#0' + ], + capabilityDelegation: [ + 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9#0' + ], + capabilityInvocation: [ + 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9#0' + ], + keyAgreement: [ + 'did:jwk:eyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoicnBLbkRQOEY0X2p3dlE3eERra3VLeDE2NU9Td2N5clF2bUVXbDJlaWdJVSJ9#0' + ], + }, + keySet: { + verificationMethodKeys: [{ + 'publicKeyJwk': { + 'alg' : 'EdDSA', + 'crv' : 'Ed25519', + 'kty' : 'OKP', + 'x' : 'rpKnDP8F4_jwvQ7xDkkuKx165OSwcyrQvmEWl2eigIU' + }, + relationships: ['authentication'] + }], + }, + } + }, +]; + +export const didJwkResolveTestVectors = [ + { + id : 'did:jwk.resolve.1', + input : { + didUrl: 'did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9' + }, + output: { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1' + ], + id : 'did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + verificationMethod : [ + { + id : 'did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0', + type : 'JsonWebKey2020', + controller : 'did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + publicKeyJwk : { + kty : 'OKP', + crv : 'X25519', + use : 'enc', + x : '3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08' + } + } + ], + keyAgreement: ['did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0'] + }, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + did : { + didString : 'did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + methodSpecificId : 'eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9', + method : 'jwk' + } + } + } + }, +]; \ No newline at end of file