From 0a22d9c2426c69c95263b2f0b36617794b59be62 Mon Sep 17 00:00:00 2001 From: tadejp8 <48256808+tadejpodrekar@users.noreply.github.com> Date: Fri, 24 Feb 2023 19:11:27 +0100 Subject: [PATCH] feat(did-provider-jwk): add did:jwk method support (#1128) --- __tests__/localAgent.test.ts | 5 + __tests__/localJsonStoreAgent.test.ts | 5 + __tests__/localMemoryStoreAgent.test.ts | 5 + __tests__/restAgent.test.ts | 5 + __tests__/shared/didManager.ts | 71 ++++++++++ __tests__/shared/resolveDid.ts | 15 ++ __tests__/shared/verifiableDataJWT.ts | 29 ++++ packages/did-provider-jwk/README.md | 4 + packages/did-provider-jwk/api-extractor.json | 18 +++ packages/did-provider-jwk/package.json | 39 +++++ packages/did-provider-jwk/src/index.ts | 9 ++ .../did-provider-jwk/src/jwk-did-provider.ts | 134 ++++++++++++++++++ packages/did-provider-jwk/src/jwkDidUtils.ts | 107 ++++++++++++++ packages/did-provider-jwk/src/resolver.ts | 100 +++++++++++++ .../src/types/jwk-provider-types.ts | 26 ++++ packages/did-provider-jwk/tsconfig.json | 14 ++ packages/test-react-app/package.json | 1 + packages/test-react-app/src/veramo/setup.ts | 5 + pnpm-lock.yaml | 25 ++++ 19 files changed, 617 insertions(+) create mode 100644 packages/did-provider-jwk/README.md create mode 100644 packages/did-provider-jwk/api-extractor.json create mode 100644 packages/did-provider-jwk/package.json create mode 100644 packages/did-provider-jwk/src/index.ts create mode 100644 packages/did-provider-jwk/src/jwk-did-provider.ts create mode 100644 packages/did-provider-jwk/src/jwkDidUtils.ts create mode 100644 packages/did-provider-jwk/src/resolver.ts create mode 100644 packages/did-provider-jwk/src/types/jwk-provider-types.ts create mode 100644 packages/did-provider-jwk/tsconfig.json diff --git a/__tests__/localAgent.test.ts b/__tests__/localAgent.test.ts index f3711eb9a..09f5f8030 100644 --- a/__tests__/localAgent.test.ts +++ b/__tests__/localAgent.test.ts @@ -39,6 +39,7 @@ import { EthrDIDProvider } from '../packages/did-provider-ethr/src' import { WebDIDProvider } from '../packages/did-provider-web/src' import { getDidKeyResolver, KeyDIDProvider } from '../packages/did-provider-key/src' import { getDidPkhResolver, PkhDIDProvider } from '../packages/did-provider-pkh/src' +import { getDidJwkResolver, JwkDIDProvider } from '../packages/did-provider-jwk/src' import { DIDComm, DIDCommHttpTransport, DIDCommMessageHandler, IDIDComm } from '../packages/did-comm/src' import { ISelectiveDisclosure, @@ -201,6 +202,9 @@ const setup = async (options?: IAgentOptions): Promise => { 'did:pkh': new PkhDIDProvider({ defaultKms: 'local', }), + 'did:jwk': new JwkDIDProvider({ + defaultKms: 'local', + }), 'did:fake': new FakeDidProvider(), }, }), @@ -219,6 +223,7 @@ const setup = async (options?: IAgentOptions): Promise => { ...webDidResolver(), ...getDidKeyResolver(), ...getDidPkhResolver(), + ...getDidJwkResolver(), ...new FakeDidResolver(() => agent).getDidFakeResolver(), }), new DataStore(dbConnection), diff --git a/__tests__/localJsonStoreAgent.test.ts b/__tests__/localJsonStoreAgent.test.ts index 7018a329c..d3c7cff35 100644 --- a/__tests__/localJsonStoreAgent.test.ts +++ b/__tests__/localJsonStoreAgent.test.ts @@ -37,6 +37,7 @@ import { EthrDIDProvider } from '../packages/did-provider-ethr/src' import { WebDIDProvider } from '../packages/did-provider-web/src' import { getDidKeyResolver, KeyDIDProvider } from '../packages/did-provider-key/src' import { getDidPkhResolver, PkhDIDProvider } from '../packages/did-provider-pkh/src' +import { getDidJwkResolver, JwkDIDProvider } from '../packages/did-provider-jwk/src' import { DIDComm, DIDCommMessageHandler, IDIDComm } from '../packages/did-comm/src' import { ISelectiveDisclosure, @@ -164,6 +165,9 @@ const setup = async (options?: IAgentOptions): Promise => { 'did:pkh': new PkhDIDProvider({ defaultKms: 'local', }), + 'did:jwk': new JwkDIDProvider({ + defaultKms: 'local', + }), 'did:fake': new FakeDidProvider(), }, }), @@ -173,6 +177,7 @@ const setup = async (options?: IAgentOptions): Promise => { ...webDidResolver(), ...getDidKeyResolver(), ...getDidPkhResolver(), + ...getDidJwkResolver(), ...new FakeDidResolver(() => agent).getDidFakeResolver(), }), }), diff --git a/__tests__/localMemoryStoreAgent.test.ts b/__tests__/localMemoryStoreAgent.test.ts index 7a6dc3a8a..04afddef4 100644 --- a/__tests__/localMemoryStoreAgent.test.ts +++ b/__tests__/localMemoryStoreAgent.test.ts @@ -36,6 +36,7 @@ import { EthrDIDProvider } from '../packages/did-provider-ethr/src' import { WebDIDProvider } from '../packages/did-provider-web/src' import { getDidKeyResolver, KeyDIDProvider } from '../packages/did-provider-key/src' import { getDidPkhResolver, PkhDIDProvider } from '../packages/did-provider-pkh/src' +import { getDidJwkResolver, JwkDIDProvider } from '../packages/did-provider-jwk/src' import { DIDComm, DIDCommMessageHandler, IDIDComm } from '../packages/did-comm/src' import { ISelectiveDisclosure, @@ -161,6 +162,9 @@ const setup = async (options?: IAgentOptions): Promise => { 'did:pkh': new PkhDIDProvider({ defaultKms: 'local', }), + 'did:jwk': new JwkDIDProvider({ + defaultKms: 'local', + }), 'did:fake': new FakeDidProvider(), }, }), @@ -169,6 +173,7 @@ const setup = async (options?: IAgentOptions): Promise => { ...webDidResolver(), ...getDidKeyResolver(), ...getDidPkhResolver(), + ...getDidJwkResolver(), ...new FakeDidResolver(() => agent).getDidFakeResolver(), }), new DataStore(dbConnection), diff --git a/__tests__/restAgent.test.ts b/__tests__/restAgent.test.ts index c085bc5c2..048306e01 100644 --- a/__tests__/restAgent.test.ts +++ b/__tests__/restAgent.test.ts @@ -46,6 +46,7 @@ import { EthrDIDProvider } from '../packages/did-provider-ethr/src' import { WebDIDProvider } from '../packages/did-provider-web/src' import { getDidKeyResolver, KeyDIDProvider } from '../packages/did-provider-key/src' import { getDidPkhResolver, PkhDIDProvider } from '../packages/did-provider-pkh/src' +import { getDidJwkResolver, JwkDIDProvider } from '../packages/did-provider-jwk/src' import { DIDComm, DIDCommHttpTransport, DIDCommMessageHandler, IDIDComm } from '../packages/did-comm/src' import { ISelectiveDisclosure, @@ -191,6 +192,9 @@ const setup = async (options?: IAgentOptions): Promise => { 'did:pkh': new PkhDIDProvider({ defaultKms: 'local', }), + 'did:jwk': new JwkDIDProvider({ + defaultKms: 'local', + }), 'did:fake': new FakeDidProvider(), }, }), @@ -201,6 +205,7 @@ const setup = async (options?: IAgentOptions): Promise => { // key: getUniversalResolver(), // resolve using remote resolver... when uniresolver becomes more stable, ...getDidKeyResolver(), ...getDidPkhResolver(), + ...getDidJwkResolver(), ...new FakeDidResolver(() => serverAgent as TAgent).getDidFakeResolver(), }), }), diff --git a/__tests__/shared/didManager.ts b/__tests__/shared/didManager.ts index a075a1002..781c8fdc3 100644 --- a/__tests__/shared/didManager.ts +++ b/__tests__/shared/didManager.ts @@ -74,6 +74,77 @@ export default (testContext: { expect(identifier.controllerKeyId).toEqual(identifier.keys[0].kid) }) + it('should create identifier using did:jwk', async () => { + // keyType supports 'Secp256k1', 'Secp256r1', 'Ed25519', 'X25519' + const keyType = 'Ed25519' + identifier = await agent.didManagerCreate({ + provider: 'did:jwk', + options: { + keyType, + } + }) + expect(identifier.provider).toEqual('did:jwk') + expect(identifier.keys[0].type).toEqual(keyType) + expect(identifier.controllerKeyId).toEqual(identifier.keys[0].kid) + }) + it('should create identifier using did:jwk with an imported key', async () => { + // keyType supports 'Secp256k1', 'Secp256r1', 'Ed25519', 'X25519' + const keyType = 'Ed25519' + identifier = await agent.didManagerCreate({ + provider: 'did:jwk', + options: { + keyType, + privateKeyHex: 'f3157fbbb356a0d56a84a1a9752f81d0638cce4153168bd1b46f68a6e62b82b0f3157fbbb356a0d56a84a1a9752f81d0638cce4153168bd1b46f68a6e62b82b0', + } + }) + expect(identifier.provider).toEqual('did:jwk') + expect(identifier.keys[0].type).toEqual(keyType) + expect(identifier.controllerKeyId).toEqual(identifier.keys[0].kid) + }) + it('should create identifier using did:jwk with a default imported key', async () => { + // keyType supports 'Secp256k1', 'Secp256r1', 'Ed25519', 'X25519' + const keyType = 'Secp256k1' + identifier = await agent.didManagerCreate({ + provider: 'did:jwk', + options: { + privateKeyHex: 'f3157fbbb356a0d56a84a1a9752f81d0638cce4153168bd1b46f68a6e62b82b0', + } + }) + expect(identifier.provider).toEqual('did:jwk') + expect(identifier.keys[0].type).toEqual(keyType) + expect(identifier.controllerKeyId).toEqual(identifier.keys[0].kid) + }) + it('should throw error for invalid privateKEyHex', async () => { + await expect( agent.didManagerCreate({ + provider: 'did:jwk', + options: { + privateKeyHex: '1234', + } + })).rejects.toThrow() + expect(identifier.provider).toEqual('did:jwk') + }) + it('should throw error for invalid keyUse parameter', async () => { + await expect( agent.didManagerCreate({ + provider: 'did:jwk', + options: { + keyType: 'Secp256k1', + keyUse: 'signing', + } + })).rejects.toThrow('illegal_argument: Key use must be sig or enc') + expect(identifier.provider).toEqual('did:jwk') + }) + it('should throw error for invalid Ed25519 key use', async () => { + await expect( agent.didManagerCreate({ + provider: 'did:jwk', + alias: 'test1', + options: { + keyType: 'Ed25519', + keyUse: 'enc', + } + })).rejects.toThrow('illegal_argument: Ed25519 keys cannot be used for encryption') + expect(identifier.provider).toEqual('did:jwk') + }) + it('should throw error for existing alias provider combo', async () => { await expect( agent.didManagerCreate({ diff --git a/__tests__/shared/resolveDid.ts b/__tests__/shared/resolveDid.ts index 45d05ad44..6bcb7fc74 100644 --- a/__tests__/shared/resolveDid.ts +++ b/__tests__/shared/resolveDid.ts @@ -51,6 +51,21 @@ export default (testContext: { //let cred = await agent.createVerifiableCredential() }); + it('should resolve did:jwk', async () => { + let identifier: IIdentifier = await agent.didManagerCreate({ + provider: 'did:jwk', + // keyType supports 'Secp256k1', 'Secp256r1', 'Ed25519', 'X25519' + options: { + keyType: "Ed25519" + } + }) + const result = await agent.resolveDid({ didUrl: identifier.did}) + const didDoc = result.didDocument + expect(didDoc?.id).toEqual(identifier.did) + expect(result).toHaveProperty('didDocumentMetadata') + expect(result).toHaveProperty('didResolutionMetadata') + }); + it('should resolve imported fake did', async () => { const did = 'did:fake:myfakedid' await agent.didManagerImport({ diff --git a/__tests__/shared/verifiableDataJWT.ts b/__tests__/shared/verifiableDataJWT.ts index cd4e99c65..3c900effb 100644 --- a/__tests__/shared/verifiableDataJWT.ts +++ b/__tests__/shared/verifiableDataJWT.ts @@ -84,6 +84,35 @@ export default (testContext: { expect(payload.vc.credentialSubject.id).not.toBeDefined() }) + it('should create verifiable credential (simple) using did:jwk identifier', async () => { + const ident = await agent.didManagerCreate({ + kms: 'local', + provider: 'did:jwk', + }) + const verifiableCredential = await agent.createVerifiableCredential({ + credential: { + issuer: { id: ident.did }, + type: ['Example'], + credentialSubject: { + id: 'did:web:example.com', + you: 'Rock', + }, + }, + proofFormat: 'jwt', + }) + const verifyResult = await agent.verifyCredential({credential: verifiableCredential}) + + expect(verifyResult.verified).toBe(true) + expect(verifiableCredential).toHaveProperty('proof.jwt') + expect(verifiableCredential).toHaveProperty('issuanceDate') + expect(verifiableCredential['@context']).toEqual(['https://www.w3.org/2018/credentials/v1']) + expect(verifiableCredential['type']).toEqual(['VerifiableCredential', 'Example']) + + const token = verifiableCredential.proof.jwt + const { payload } = decodeJWT(token) + expect(payload.vc.credentialSubject.id).not.toBeDefined() + }) + it('should create verifiable credential keeping original fields', async () => { expect.assertions(5) const verifiableCredential = await agent.createVerifiableCredential({ diff --git a/packages/did-provider-jwk/README.md b/packages/did-provider-jwk/README.md new file mode 100644 index 000000000..f0689fb90 --- /dev/null +++ b/packages/did-provider-jwk/README.md @@ -0,0 +1,4 @@ +# Veramo did:jwk provider + +This package contains an implementation of `AbstractIdentifierProvider` for the `did:jwk` method, according to the [specification](https://github.com/quartzjer/did-jwk/blob/main/spec.md). +This enables creation and control of `did:jwk` entities. diff --git a/packages/did-provider-jwk/api-extractor.json b/packages/did-provider-jwk/api-extractor.json new file mode 100644 index 000000000..409d7f16c --- /dev/null +++ b/packages/did-provider-jwk/api-extractor.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "apiReport": { + "enabled": true, + "reportFolder": "./api", + "reportTempFolder": "./api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "./api/.api.json" + }, + + "dtsRollup": { + "enabled": false + }, + "mainEntryPointFilePath": "/build/index.d.ts" +} diff --git a/packages/did-provider-jwk/package.json b/packages/did-provider-jwk/package.json new file mode 100644 index 000000000..d28a78ed9 --- /dev/null +++ b/packages/did-provider-jwk/package.json @@ -0,0 +1,39 @@ +{ + "name": "@veramo/did-provider-jwk", + "version": "5.0.0", + "description": "Veramo plugin that can enable creation and control of did:jwk identifiers.", + "main": "build/index.js", + "types": "build/index.d.ts", + "exports": "./build/index.js", + "scripts": { + "build": "tsc", + "extract-api": "node ../cli/bin/veramo.js dev extract-api" + }, + "dependencies": { + "@veramo/core-types": "^5.0.0", + "@veramo/did-manager": "^5.0.0", + "@veramo/utils": "^5.0.0", + "debug": "^4.3.3", + "did-resolver": "^4.0.1", + "elliptic": "^6.5.4" + }, + "devDependencies": { + "@types/debug": "4.1.7", + "@types/elliptic": "6.4.14", + "typescript": "4.9.4" + }, + "author": "Tadej Podrekar", + "license": "Apache-2.0", + "keywords": [], + "files": [ + "build/**/*", + "src/**/*", + "README.md", + "LICENSE" + ], + "type": "module", + "moduleDirectories": [ + "node_modules", + "src" + ] +} diff --git a/packages/did-provider-jwk/src/index.ts b/packages/did-provider-jwk/src/index.ts new file mode 100644 index 000000000..cc99ca72b --- /dev/null +++ b/packages/did-provider-jwk/src/index.ts @@ -0,0 +1,9 @@ +/** + * Provides `did:jwk` {@link @veramo/did-provider-jwk#JwkDIDProvider | identifier provider } for the + * {@link @veramo/did-manager#DIDManager} + * + * @packageDocumentation + */ +export { JwkDIDProvider } from './jwk-did-provider.js' +export { getDidJwkResolver } from './resolver.js' +export * from './types/jwk-provider-types.js' diff --git a/packages/did-provider-jwk/src/jwk-did-provider.ts b/packages/did-provider-jwk/src/jwk-did-provider.ts new file mode 100644 index 000000000..335e419b7 --- /dev/null +++ b/packages/did-provider-jwk/src/jwk-did-provider.ts @@ -0,0 +1,134 @@ +import { IIdentifier, IKey, IService, IAgentContext, IKeyManager } from '@veramo/core-types' +import { AbstractIdentifierProvider } from '@veramo/did-manager' +import { encodeJoseBlob } from '@veramo/utils' +import { VerificationMethod } from 'did-resolver' +import type { JwkCreateIdentifierOptions, JwkDidImportOrGenerateKeyArgs, JwkDidSupportedKeyTypes } from './types/jwk-provider-types.js' +import { generateJWKfromVerificationMethod } from './jwkDidUtils.js' + +import Debug from 'debug' +const debug = Debug('veramo:did-jwk:identifier-provider') + +type IContext = IAgentContext + +/** + * {@link @veramo/did-manager#DIDManager} identifier provider for `did:jwk` identifiers + * + * @beta This API may change without a BREAKING CHANGE notice. + */ +export class JwkDIDProvider extends AbstractIdentifierProvider { + private defaultKms: string + + constructor(options: { defaultKms: string }) { + super() + this.defaultKms = options.defaultKms + } + + async createIdentifier( + { kms, options }: { kms?: string; options?: JwkCreateIdentifierOptions }, + context: IContext, + ): Promise> { + const keyType: JwkDidSupportedKeyTypes = options?.keyType || 'Secp256k1' + const key = await this.importOrGenerateKey( + { + kms: kms || this.defaultKms, + options: { + keyType, + ...(options?.privateKeyHex && { privateKeyHex: options.privateKeyHex }), + }, + }, + context, + ) + + const jwk = generateJWKfromVerificationMethod( + keyType, + { + publicKeyHex: key.publicKeyHex, + } as VerificationMethod, + options?.keyUse + ) + const jwkBase64url = encodeJoseBlob(jwk as {}) + + const identifier: Omit = { + did: `did:jwk:${jwkBase64url}`, + controllerKeyId: key.kid, + keys: [key], + services: [], + } + debug('Created', identifier.did) + return identifier + } + + async updateIdentifier( + args: { + did: string + kms?: string + alias?: string + options?: any + }, + context: IAgentContext, + ): Promise { + throw new Error('not_supported: JwkDIDProvider updateIdentifier not possible') + } + + async deleteIdentifier( + identifier: IIdentifier, + context: IContext + ): Promise { + for (const { kid } of identifier.keys) { + await context.agent.keyManagerDelete({ kid }) + } + return true + } + + async addKey({ + identifier, + key, + options, + }: { identifier: IIdentifier; key: IKey; options?: any }, + context: IContext + ): Promise { + throw Error('not_supported: JwkDIDProvider addKey not possible') + } + + async addService({ + identifier, + service, + options, + }: { identifier: IIdentifier; service: IService; options?: any }, + context: IContext + ): Promise { + throw Error('not_supported: JwkDIDProvider addService not possible') + } + + async removeKey( + args: { identifier: IIdentifier; kid: string; options?: any }, + context: IContext + ): Promise { + throw Error('not_supported: JwkDIDProvider removeKey not possible') + + } + + async removeService( + args: { identifier: IIdentifier; id: string; options?: any }, + context: IContext + ): Promise { + throw Error('not_supported: JwkDIDProvider removeService not possible') + } + + private async importOrGenerateKey( + args: JwkDidImportOrGenerateKeyArgs, + context: IContext + ): Promise { + if (args.options.privateKeyHex) { + return context.agent.keyManagerImport({ + kms: args.kms || this.defaultKms, + type: args.options.keyType, + privateKeyHex: args.options.privateKeyHex, + }) + } + return context.agent.keyManagerCreate({ + kms: args.kms || this.defaultKms, + type: args.options.keyType, + }) + } +} diff --git a/packages/did-provider-jwk/src/jwkDidUtils.ts b/packages/did-provider-jwk/src/jwkDidUtils.ts new file mode 100644 index 000000000..3ed1f75e5 --- /dev/null +++ b/packages/did-provider-jwk/src/jwkDidUtils.ts @@ -0,0 +1,107 @@ +import { JwkDidSupportedKeyTypes, KeyUse, SupportedKeyTypes } from './types/jwk-provider-types.js' +import { VerificationMethod, type JsonWebKey } from 'did-resolver' +import { hexToBytes, bytesToBase64url, extractPublicKeyHex } from '@veramo/utils' +import elliptic from 'elliptic' + +export function getKeyUse(keyType: JwkDidSupportedKeyTypes, passedKeyUse?: KeyUse): KeyUse { + if (passedKeyUse) { + if (passedKeyUse !== 'sig' && passedKeyUse !== 'enc') { + throw new Error('illegal_argument: Key use must be sig or enc') + } + if (passedKeyUse === 'sig' && keyType === 'X25519') { + throw new Error('illegal_argument: X25519 keys cannot be used for signing') + } + if (passedKeyUse === 'enc' && keyType === 'Ed25519') { + throw new Error('illegal_argument: Ed25519 keys cannot be used for encryption') + } + return passedKeyUse + } + switch (keyType) { + case 'Secp256k1': + case 'Secp256r1': + case 'Ed25519': + return 'sig' + case 'X25519': + return 'enc' + default: + throw new Error('illegal_argument: Unknown key type') + } +} + +export function isJWK(data: unknown): data is JsonWebKey { + if ( + typeof data === 'object' && + data && + 'crv' in data && + typeof data.crv === 'string' && + 'kty' in data && + 'x' in data && + typeof data.x === 'string' && + ((data.kty === 'EC' && 'y' in data && typeof data.y === 'string') || + (data.kty === 'OKP' && !('y' in data))) + ) { + return true + } + return false +} + +function createJWK(keyType: JwkDidSupportedKeyTypes, pubKey: string | Uint8Array, passedKeyUse?: KeyUse): JsonWebKey | undefined { + try { + const keyUse = getKeyUse(keyType, passedKeyUse) + switch (keyType) { + case SupportedKeyTypes.Secp256k1: + {const EC = new elliptic.ec('secp256k1') + const pubPoint = EC.keyFromPublic(pubKey, 'hex').getPublic() + const x = pubPoint.getX() + const y = pubPoint.getY() + + return { + alg: 'ES256K', + crv: 'secp256k1', + kty: 'EC', + ...(keyUse && { use: keyUse }), + x: bytesToBase64url(hexToBytes(x.toString('hex'))), + y: bytesToBase64url(hexToBytes(y.toString('hex'))), + } as JsonWebKey} + case SupportedKeyTypes.Secp256r1: + {const EC = new elliptic.ec('p256') + // add '03' prefix to public key + const pubPoint = EC.keyFromPublic(`03${pubKey}`, 'hex').getPublic() + const x = pubPoint.getX() + const y = pubPoint.getY() + + return { + alg: 'ES256', + crv: 'P-256', + kty: 'EC', + ...(keyUse && { use: keyUse }), + x: bytesToBase64url(hexToBytes(x.toString('hex'))), + y: bytesToBase64url(hexToBytes(y.toString('hex'))), + } as JsonWebKey} + case SupportedKeyTypes.Ed25519: + return { + alg: 'EdDSA', + crv: 'Ed25519', + kty: 'OKP', + ...(keyUse && { use: keyUse }), + x: bytesToBase64url(typeof pubKey === 'string' ? hexToBytes(pubKey) : pubKey), + } as JsonWebKey + case SupportedKeyTypes.X25519: + return { + alg: 'ECDH-ES', + crv: 'X25519', + kty: 'OKP', + ...(keyUse && { use: keyUse }), + x: bytesToBase64url(typeof pubKey === 'string' ? hexToBytes(pubKey) : pubKey), + } as JsonWebKey + default: + throw new Error(`not_supported: Failed to create JWK using ${keyType}`) + } + } catch (error) { + throw error; + } +} + +export function generateJWKfromVerificationMethod(keyType: JwkDidSupportedKeyTypes, key: VerificationMethod, keyUse?: KeyUse) { + return createJWK(keyType, extractPublicKeyHex(key), keyUse) +} diff --git a/packages/did-provider-jwk/src/resolver.ts b/packages/did-provider-jwk/src/resolver.ts new file mode 100644 index 000000000..336394995 --- /dev/null +++ b/packages/did-provider-jwk/src/resolver.ts @@ -0,0 +1,100 @@ +import { + DIDDocument, + DIDResolutionOptions, + DIDResolutionResult, + DIDResolver, + ParsedDID, + Resolvable, + JsonWebKey, +} from 'did-resolver' +import { encodeBase64url, decodeBase64url } from '@veramo/utils' +import { isJWK } from './jwkDidUtils.js' + +function generateDidResolution(jwk: JsonWebKey, parsed: ParsedDID): Promise { + return new Promise((resolve, reject) => { + try { + const sig = jwk.use === 'sig' + const enc = jwk.use === 'enc' + const did = `did:jwk:${encodeBase64url(JSON.stringify(jwk))}` + const didDocument: DIDDocument = { + id: did, + '@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/jws-2020/v1'], + verificationMethod: [ + { + id: `${did}#0`, + type: 'JsonWebKey2020', + controller: did, + publicKeyJwk: jwk, + }, + ], + ...(sig && { assertionMethod: [`${did}#0`] }), + ...(sig && { authentication: [`${did}#0`] }), + ...(sig && { capabilityInvocation: [`${did}#0`] }), + ...(sig && { capabilityDelegation: [`${did}#0`] }), + ...(enc && { keyAgreement: [`${did}#0`] }), + } + resolve({ + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + pattern: '^(did:jwk:.+)$', + did: { + didString: did, + methodSpecificId: parsed.id, + method: 'jwk', + }, + }, + didDocument, + } as DIDResolutionResult) + } catch (error: any) { + reject(error) + } + }) +} + +function parseDidJwkIdentifier(didIdentifier: string): JsonWebKey { + try { + const jwk = JSON.parse(decodeBase64url(didIdentifier)) as unknown + if (!isJWK(jwk)) { + throw new Error("illegal_argument: DID identifier doesn't contain a valid JWK") + } + return jwk + } catch (error: any) { + throw new Error('illegal_argument: Invalid DID identifier') + } +} + +export const resolveDidJwk: DIDResolver = async ( + did: string, + parsed: ParsedDID, + resolver: Resolvable, + options: DIDResolutionOptions, +): Promise => { + try { + if (parsed.method !== 'jwk') throw Error('illegal_argument: Invalid DID method') + + const didIdentifier = did.split('did:jwk:')[1] + if (!didIdentifier) throw Error('illegal_argument: Invalid DID') + + const jwk = parseDidJwkIdentifier(didIdentifier) + const didResolution = await generateDidResolution(jwk, parsed) + return didResolution + } catch (err: any) { + return { + didDocumentMetadata: {}, + didResolutionMetadata: { + error: err.message, + }, + didDocument: null, + } as DIDResolutionResult + } +} + +/** + * Provides a mapping to a did:jwk resolver, usable by {@link did-resolver#Resolver}. + * + * @public + */ +export function getDidJwkResolver() { + return { jwk: resolveDidJwk } +} diff --git a/packages/did-provider-jwk/src/types/jwk-provider-types.ts b/packages/did-provider-jwk/src/types/jwk-provider-types.ts new file mode 100644 index 000000000..514c71ead --- /dev/null +++ b/packages/did-provider-jwk/src/types/jwk-provider-types.ts @@ -0,0 +1,26 @@ +export type JwkCreateIdentifierOptions = { + keyType?: JwkDidSupportedKeyTypes + privateKeyHex?: string + keyUse?: KeyUse +} + +export type JwkDidImportOrGenerateKeyArgs = { + kms: string + options: ImportOrGenerateKeyOpts +} + +type ImportOrGenerateKeyOpts = { + keyType: JwkDidSupportedKeyTypes + privateKeyHex?: string +} + +export type JwkDidSupportedKeyTypes = 'Secp256r1' | 'Secp256k1' | 'Ed25519' | 'X25519' + +export enum SupportedKeyTypes { + Secp256r1 = 'Secp256r1', + Secp256k1 = 'Secp256k1', + Ed25519 = 'Ed25519', + X25519 = 'X25519', +} + +export type KeyUse = 'sig' | 'enc' diff --git a/packages/did-provider-jwk/tsconfig.json b/packages/did-provider-jwk/tsconfig.json new file mode 100644 index 000000000..246308a66 --- /dev/null +++ b/packages/did-provider-jwk/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "declarationDir": "build", + "skipLibCheck": true + }, + "references": [ + {"path": "../core-types"}, + {"path": "../did-manager"}, + {"path": "../utils"} + ] +} diff --git a/packages/test-react-app/package.json b/packages/test-react-app/package.json index aad90617a..5488229d9 100644 --- a/packages/test-react-app/package.json +++ b/packages/test-react-app/package.json @@ -16,6 +16,7 @@ "@veramo/did-provider-ethr": "^5.0.0", "@veramo/did-provider-key": "^5.0.0", "@veramo/did-provider-pkh": "^5.0.0", + "@veramo/did-provider-jwk": "^5.0.0", "@veramo/did-provider-web": "^5.0.0", "@veramo/did-resolver": "^5.0.0", "@veramo/key-manager": "^5.0.0", diff --git a/packages/test-react-app/src/veramo/setup.ts b/packages/test-react-app/src/veramo/setup.ts index 983275942..e2557a391 100644 --- a/packages/test-react-app/src/veramo/setup.ts +++ b/packages/test-react-app/src/veramo/setup.ts @@ -30,6 +30,7 @@ import { } from '@veramo/credential-ld' import { getDidKeyResolver, KeyDIDProvider } from '@veramo/did-provider-key' import { getDidPkhResolver, PkhDIDProvider } from '@veramo/did-provider-pkh' +import { getDidJwkResolver, JwkDIDProvider } from '@veramo/did-provider-jwk' import { DIDComm, DIDCommMessageHandler, IDIDComm } from '@veramo/did-comm' import { ISelectiveDisclosure, SdrMessageHandler, SelectiveDisclosure } from '@veramo/selective-disclosure' import { KeyManagementSystem, SecretBox } from '@veramo/kms-local' @@ -67,6 +68,7 @@ export function getAgent(options?: IAgentOptions): TAgent { ...webDidResolver(), ...getDidKeyResolver(), ...getDidPkhResolver(), + ...getDidJwkResolver(), ...new FakeDidResolver(() => agent as TAgent).getDidFakeResolver(), }), }), @@ -112,6 +114,9 @@ export function getAgent(options?: IAgentOptions): TAgent { 'did:pkh': new PkhDIDProvider({ defaultKms: 'local', }), + 'did:jwk': new JwkDIDProvider({ + defaultKms: 'local', + }), 'did:fake': new FakeDidProvider(), }, }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23dc00afc..5a5b1deee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -612,6 +612,29 @@ importers: '@types/debug': 4.1.7 typescript: 4.9.4 + packages/did-provider-jwk: + specifiers: + '@types/debug': 4.1.7 + '@types/elliptic': 6.4.14 + '@veramo/core-types': ^5.0.0 + '@veramo/did-manager': ^5.0.0 + '@veramo/utils': ^5.0.0 + debug: ^4.3.3 + did-resolver: ^4.0.1 + elliptic: ^6.5.4 + typescript: 4.9.4 + dependencies: + '@veramo/core-types': link:../core-types + '@veramo/did-manager': link:../did-manager + '@veramo/utils': link:../utils + debug: 4.3.4 + did-resolver: 4.0.1 + elliptic: 6.5.4 + devDependencies: + '@types/debug': 4.1.7 + '@types/elliptic': 6.4.14 + typescript: 4.9.4 + packages/did-provider-key: specifiers: '@transmute/did-key-ed25519': ^0.3.0-unstable.10 @@ -912,6 +935,7 @@ importers: '@veramo/did-jwt': ^5.0.0 '@veramo/did-manager': ^5.0.0 '@veramo/did-provider-ethr': ^5.0.0 + '@veramo/did-provider-jwk': ^5.0.0 '@veramo/did-provider-key': ^5.0.0 '@veramo/did-provider-pkh': ^5.0.0 '@veramo/did-provider-web': ^5.0.0 @@ -959,6 +983,7 @@ importers: '@veramo/did-jwt': link:../did-jwt '@veramo/did-manager': link:../did-manager '@veramo/did-provider-ethr': link:../did-provider-ethr + '@veramo/did-provider-jwk': link:../did-provider-jwk '@veramo/did-provider-key': link:../did-provider-key '@veramo/did-provider-pkh': link:../did-provider-pkh '@veramo/did-provider-web': link:../did-provider-web