From 7c3de8816ea8bf45693894bec7e29ceeede5de8c Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 26 Jul 2023 15:18:48 -0700 Subject: [PATCH] signing with kid --- package.json | 2 +- packages/web5-agent/src/web5-agent.ts | 3 +- packages/web5-user-agent/src/utils.ts | 30 -------- .../web5-user-agent/src/web5-user-agent.ts | 70 +++++++++++-------- packages/web5/src/vc-api.ts | 12 ++-- packages/web5/src/web5.ts | 2 +- .../web5/tests/test-utils/test-user-agent.ts | 18 +++-- packages/web5/tests/web5-vc.spec.ts | 12 +++- 8 files changed, 72 insertions(+), 77 deletions(-) diff --git a/package.json b/package.json index f48b7308a..aa425d60e 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "workspaces": [ "packages/common", "packages/crypto", + "packages/credentials", "packages/web5-agent", "packages/dids", - "packages/credentials", "packages/web5-user-agent", "packages/web5-proxy-agent", "packages/web5" diff --git a/packages/web5-agent/src/web5-agent.ts b/packages/web5-agent/src/web5-agent.ts index b66f76e78..39f09d916 100644 --- a/packages/web5-agent/src/web5-agent.ts +++ b/packages/web5-agent/src/web5-agent.ts @@ -65,6 +65,7 @@ export type ProcessVcRequest = { author: string; target: string; vc: VerifiableCredential; + kid?: string }; export type SendVcRequest = { @@ -75,7 +76,7 @@ export type SendVcRequest = { }; export type VcResponse = { - vcDataBlob?: Blob; + vcJwt?: string; message?: unknown; messageCid?: string; reply: UnionMessageReply; diff --git a/packages/web5-user-agent/src/utils.ts b/packages/web5-user-agent/src/utils.ts index 4bbda2ad7..5659fedf1 100644 --- a/packages/web5-user-agent/src/utils.ts +++ b/packages/web5-user-agent/src/utils.ts @@ -1,6 +1,5 @@ import type { Readable } from 'readable-stream'; import { ReadableWebToNodeStream } from 'readable-web-to-node-stream'; -import { Encoder} from '@tbd54566975/dwn-sdk-js'; export function blobToIsomorphicNodeReadable(blob: Blob): Readable { return webReadableToIsomorphicNodeReadable(blob.stream()); @@ -8,33 +7,4 @@ export function blobToIsomorphicNodeReadable(blob: Blob): Readable { export function webReadableToIsomorphicNodeReadable(webReadable: ReadableStream) { return new ReadableWebToNodeStream(webReadable); -} - -export function dataToBlob(data, dataFormat){ - let dataBlob; - // Check for Object or String, and if neither, assume bytes. - const detectedType = toType(data); - if (dataFormat === 'text/plain' || detectedType === 'string') { - dataBlob = new Blob([data], { type: 'text/plain' }); - } - else if (dataFormat === 'application/json' || detectedType === 'object') { - const dataBytes = Encoder.objectToBytes(data); - dataBlob = new Blob([dataBytes], { type: 'application/json' }); - } - else if (data instanceof Uint8Array || data instanceof ArrayBuffer) { - dataBlob = new Blob([data], { type: 'application/octet-stream' }); - } - else if (data instanceof Blob) { - dataBlob = data; - } - else { - throw new Error('data type not supported.'); - } - dataFormat = dataFormat || dataBlob.type || 'application/octet-stream'; - return { dataBlob, dataFormat }; -} - - -function toType(obj){ - return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); } \ No newline at end of file diff --git a/packages/web5-user-agent/src/web5-user-agent.ts b/packages/web5-user-agent/src/web5-user-agent.ts index 9227793bc..f3ad56970 100644 --- a/packages/web5-user-agent/src/web5-user-agent.ts +++ b/packages/web5-user-agent/src/web5-user-agent.ts @@ -50,11 +50,12 @@ import { import { ProfileApi } from './profile-api.js'; import { DwnRpcClient } from './dwn-rpc-client.js'; import { blobToIsomorphicNodeReadable, webReadableToIsomorphicNodeReadable } from './utils.js'; -import { dataToBlob } from './utils.js'; -import { KeyManager } from '@tbd54566975/crypto'; +import { KeyManager, ManagedKeyPair } from '@tbd54566975/crypto'; import { VerifiableCredential } from '@tbd54566975/credentials'; +import { Convert } from '@tbd54566975/common'; + // TODO: allow user to provide optional array of DwnRpc implementations once DwnRpc has been moved out of this package export type Web5UserAgentOptions = { dwn: Dwn; @@ -204,11 +205,32 @@ export class Web5UserAgent implements Web5Agent { } async processVcRequest(request: ProcessVcRequest): Promise { - const signedJwt = await this.#sign(request.vc as VerifiableCredential); + + let kid = request.kid; + if (!kid) { + const didResolution = await this.didResolver.resolve(request.author); + + if (!didResolution.didDocument) { + if (didResolution.didResolutionMetadata?.error) { + throw new Error(`DID resolution error: ${didResolution.didResolutionMetadata.error}`); + } else { + throw new Error('DID resolution error: other'); + } + } + + const [ service ] = didUtils.getServices(didResolution.didDocument, { id: '#dwn' }); + if (!service) { + throw new Error(`${request.target} has no '#dwn' service endpoints`); + } + + const serviceEndpoint = service.serviceEndpoint as DwnServiceEndpoint; + kid = serviceEndpoint.messageAuthorizationKeys[0]; + } + + const vcJwt = await this.#sign(request.vc as VerifiableCredential, kid); const messageOptions: Partial = { ...{ schema: 'vc/vc', dataFormat: 'application/vc+jwt' } }; - const { dataBlob, dataFormat } = dataToBlob(signedJwt, messageOptions.dataFormat); - messageOptions.dataFormat = dataFormat; + const dataBlob = new Blob([vcJwt], { type: 'text/plain' }); const dwnResponse = await this.processDwnRequest({ author : request.author, @@ -220,16 +242,15 @@ export class Web5UserAgent implements Web5Agent { }); const vcResponse: VcResponse = { - vcDataBlob: dataBlob, + vcJwt: vcJwt, ...dwnResponse, }; return vcResponse; } - async sendVcRequest(request: SendVcRequest): Promise { - console.log(request); - return {} as VcResponse; + async sendVcRequest(_request: SendVcRequest): Promise { + throw new Error('Method not implemented.'); } async #getDwnMessage(author: string, messageType: string, messageCid: string): Promise { @@ -324,34 +345,24 @@ export class Web5UserAgent implements Web5Agent { // TODO: have issuer did key already stored in key manager and use that instead of generating a new one - async #sign(obj: any): Promise { - const vc = obj as VerifiableCredential; - - const keyPair = await this.keyManager.generateKey({ - algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, - extractable : false, - keyUsages : ['sign', 'verify'] - }); - - let subjectId; + async #sign(vc: VerifiableCredential, kid: string): Promise { + const keyPair = await this.keyManager.getKey({keyRef: kid}) as ManagedKeyPair; - if (Array.isArray(vc.credentialSubject)) { - subjectId = vc.credentialSubject[0].id; - } - else { - subjectId = vc.credentialSubject.id; - } + const now = Math.floor(Date.now() / 1000); const jwtPayload = { + iat : now, iss : vc.issuer, - sub : subjectId, - ...vc + jti : vc.id, + nbf : now, + sub : vc.issuer, + vc : vc, }; const payloadBytes = Encoder.objectToBytes(jwtPayload); const payloadBase64url = Encoder.bytesToBase64Url(payloadBytes); - const protectedHeader = {alg: 'ECDSA', kid: keyPair.privateKey.id}; + const protectedHeader = {alg: 'ECDSA', kid: keyPair.privateKey.id, typ: 'JWT'}; const headerBytes = Encoder.objectToBytes(protectedHeader); const headerBase64url = Encoder.bytesToBase64Url(headerBytes); @@ -364,8 +375,7 @@ export class Web5UserAgent implements Web5Agent { data : signatureInputBytes, }); - let uint8ArraySignature = new Uint8Array(signatureArrayBuffer); - const signatureBase64url = Encoder.bytesToBase64Url(uint8ArraySignature); + const signatureBase64url = Convert.arrayBuffer(signatureArrayBuffer).toBase64Url(); return `${headerBase64url}.${payloadBase64url}.${signatureBase64url}`; } diff --git a/packages/web5/src/vc-api.ts b/packages/web5/src/vc-api.ts index 3ace129d1..52d3719d7 100644 --- a/packages/web5/src/vc-api.ts +++ b/packages/web5/src/vc-api.ts @@ -10,6 +10,7 @@ import { Record } from './record.js'; export type VcCreateResponse = { status: UnionMessageReply['status']; record?: Record + vcJwt?: string; }; export class VcApi { @@ -22,7 +23,7 @@ export class VcApi { } // TODO: Add CreateOptions for more robust VC creation - async create(credentialSubject: any): Promise { + async create(credentialSubject: any, kid?: string): Promise { if (!credentialSubject || typeof credentialSubject !== 'object') { throw new Error('credentialSubject not valid'); } @@ -32,14 +33,15 @@ export class VcApi { '@context' : ['https://www.w3.org/2018/credentials/v1'], credentialSubject : credentialSubject, type : ['VerifiableCredential'], - issuer : { id: this.#connectedDid }, + issuer : this.#connectedDid , issuanceDate : getCurrentXmlSchema112Timestamp(), }; const agentResponse: VcResponse = await this.#web5Agent.processVcRequest({ author : this.#connectedDid, target : this.#connectedDid, - vc : vc + vc : vc, + kid : kid }); const { message, reply: { status } } = agentResponse; @@ -49,7 +51,7 @@ export class VcApi { if (200 <= status.code && status.code <= 299) { const recordOptions = { author : this.#connectedDid, - encodedData : agentResponse.vcDataBlob, + encodedData : new Blob([agentResponse.vcJwt], { type: 'text/plain' }), target : this.#connectedDid, ...responseMessage, }; @@ -57,6 +59,6 @@ export class VcApi { record = new Record(this.#web5Agent, recordOptions); } - return { record, status }; + return { record: record, status: status, vcJwt: agentResponse.vcJwt }; } } \ No newline at end of file diff --git a/packages/web5/src/web5.ts b/packages/web5/src/web5.ts index 764063cb6..ce3db5266 100644 --- a/packages/web5/src/web5.ts +++ b/packages/web5/src/web5.ts @@ -5,7 +5,7 @@ import type { DidState, DidMethodApi, DidResolverCache, DwnServiceEndpoint } fro import ms from 'ms'; // import { Web5ProxyAgent } from '@tbd54566975/web5-proxy-agent'; -import { Dwn} from '@tbd54566975/dwn-sdk-js'; +import { Dwn } from '@tbd54566975/dwn-sdk-js'; import { Web5UserAgent, ProfileApi, SyncApi } from '@tbd54566975/web5-user-agent'; import { DidIonApi, DidKeyApi, utils as didUtils } from '@tbd54566975/dids'; diff --git a/packages/web5/tests/test-utils/test-user-agent.ts b/packages/web5/tests/test-utils/test-user-agent.ts index cdd316d1b..67f3442c2 100644 --- a/packages/web5/tests/test-utils/test-user-agent.ts +++ b/packages/web5/tests/test-utils/test-user-agent.ts @@ -24,6 +24,7 @@ export type TestAgentOptions = { didResolver: DidResolver; didIon: DidIonApi; didKey: DidKeyApi; + signKeyPair: ManagedKeyPair; } export type TestProfile = { @@ -46,6 +47,7 @@ export class TestAgent { didResolver: DidResolver; didIon: DidIonApi; didKey: DidKeyApi; + signKeyPair: ManagedKeyPair; constructor(options: TestAgentOptions) { this.agent = options.agent; @@ -59,6 +61,7 @@ export class TestAgent { this.didResolver = options.didResolver; this.didIon = options.didIon; this.didKey = options.didKey; + this.signKeyPair = options.signKeyPair; } async clearStorage(): Promise { @@ -105,18 +108,14 @@ export class TestAgent { const profileApi = new ProfileApi(profileStore); - // Instantiate in-memory store for KMS key metadata and public keys. const kmsMemoryStore = new MemoryStore(); const kmsKeyStore = new KmsKeyStore(kmsMemoryStore); - // Instantiate in-memory store for KMS private keys. const memoryPrivateKeyStore = new MemoryStore(); const kmsPrivateKeyStore = new KmsPrivateKeyStore(memoryPrivateKeyStore); - // Instantiate local KMS using key stores. const localKms = new LocalKms('local', kmsKeyStore, kmsPrivateKeyStore); - // Instantiate in-memory store for KeyManager key metadata. const kmMemoryStore = new MemoryStore(); const keyManagerStore = new KeyManagerStore({ store: kmMemoryStore }); @@ -127,6 +126,12 @@ export class TestAgent { const keyManager = new KeyManager(keyManagerOptions); + const keyPair = await keyManager.generateKey({ + algorithm : { name: 'ECDSA', namedCurve: 'secp256k1' }, + extractable : false, + keyUsages : ['sign', 'verify'] + }); + const agent = new Web5UserAgent({ profileManager : new ProfileApi(profileStore), dwn : dwn, @@ -144,8 +149,9 @@ export class TestAgent { profileApi, profileStore, didResolver, - didIon : DidIon, - didKey : DidKey, + didIon : DidIon, + didKey : DidKey, + signKeyPair : keyPair }); } diff --git a/packages/web5/tests/web5-vc.spec.ts b/packages/web5/tests/web5-vc.spec.ts index 09556f481..28a9ff72c 100644 --- a/packages/web5/tests/web5-vc.spec.ts +++ b/packages/web5/tests/web5-vc.spec.ts @@ -19,8 +19,11 @@ describe('web5.vc', () => { beforeEach(async () => { await testAgent.clearStorage(); - testProfileOptions = await testProfile.ion.with.dwn.service.and.authorization.keys(); + + // TODO: Store this key in keymanager + // console.log(testProfileOptions.profileDidOptions.keys[0]); + ({ did } = await testAgent.createProfile(testProfileOptions)); vcApi = new VcApi(testAgent.agent, did); @@ -35,10 +38,13 @@ describe('web5.vc', () => { describe('create', () => { it('valid vc', async () => { const credentialSubject = {firstName: 'alice'}; - const result = await vcApi.create(credentialSubject); + const result = await vcApi.create(credentialSubject, testAgent.signKeyPair.privateKey.id); // const resultRecord = await result.record?.data.text(); + // console.log({resultRecord}); + // const decoded = jwt.decode(resultRecord, { complete: true }); + // console.log(decoded); expect(result.status.code).to.equal(202); expect(result.status.detail).to.equal('Accepted'); @@ -52,7 +58,7 @@ describe('web5.vc', () => { it('invalid credential subject', async () => { const credentialSubject = 'badcredsubject'; try { - await vcApi.create(credentialSubject); + await vcApi.create(credentialSubject, testAgent.signKeyPair.privateKey.id); expect.fail(); } catch(e) { expect(e.message).to.include('credentialSubject not valid');