diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index ed39e790e..68c9c81f5 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -14,5 +14,6 @@ export * from './store-managed-key.js'; export * from './store-managed-identity.js'; export * from './sync-manager.js'; export * from './utils.js'; +export * from './vc-manager.js'; export * from './test-managed-agent.js'; \ No newline at end of file diff --git a/packages/agent/src/test-managed-agent.ts b/packages/agent/src/test-managed-agent.ts index c5eda8b77..0260cba2c 100644 --- a/packages/agent/src/test-managed-agent.ts +++ b/packages/agent/src/test-managed-agent.ts @@ -21,6 +21,7 @@ import { DidStoreDwn, DidStoreMemory } from './store-managed-did.js'; import { IdentityManager, ManagedIdentity } from './identity-manager.js'; import { IdentityStoreDwn, IdentityStoreMemory } from './store-managed-identity.js'; import { KeyStoreDwn, KeyStoreMemory, PrivateKeyStoreDwn, PrivateKeyStoreMemory } from './store-managed-key.js'; +import {VcManager} from './vc-manager.js'; type CreateMethodOptions = { agentClass: new (options: any) => Web5ManagedAgent @@ -131,6 +132,9 @@ export class TestManagedAgent { // Instantiate a DwnManager using the custom DWN instance. const dwnManager = new DwnManager({ dwn }); + // Instantiate a VcManager. + const vcManager = new VcManager({}); + // Instantiate an RPC Client. const rpcClient = new Web5RpcClient(); @@ -146,6 +150,7 @@ export class TestManagedAgent { dwnManager, identityManager, keyManager, + vcManager, rpcClient, syncManager }); diff --git a/packages/agent/src/types/agent.ts b/packages/agent/src/types/agent.ts index 78f2c1db0..182a32da8 100644 --- a/packages/agent/src/types/agent.ts +++ b/packages/agent/src/types/agent.ts @@ -102,9 +102,25 @@ export interface SerializableDwnMessage { * Verifiable Credential Types */ -export type ProcessVcRequest = { /** empty */ } +/** + * Type definition for a request to process a Verifiable Credential (VC). + * + * @param issuer The issuer URI of the credential, as a [String]. + * @param subject The subject URI of the credential, as a [String]. + * @param dataType The type of the credential, as a [String]. + * @param data The credential data, as a generic type [T]. + * @param expirationDate The expiration date. + */ +export type ProcessVcRequest = { + issuer: string; + subject: string; + dataType: string, + data: any, + expirationDate?: string +} + export type SendVcRequest = { /** empty */ } -export type VcResponse = { /** empty */ } +export type VcResponse = { vcJwt: string } /** * Web5 Agent Types diff --git a/packages/agent/src/vc-manager.ts b/packages/agent/src/vc-manager.ts new file mode 100644 index 000000000..ec6b6de25 --- /dev/null +++ b/packages/agent/src/vc-manager.ts @@ -0,0 +1,110 @@ +import { Jose } from '@web5/crypto'; +import { utils as didUtils } from '@web5/dids'; +import { VerifiableCredential } from '@web5/credentials'; +import { Signer } from '@tbd54566975/dwn-sdk-js'; +import { isManagedKeyPair } from './utils.js'; + +import type { VcResponse, ProcessVcRequest, Web5ManagedAgent } from './types/agent.js'; + +export type VcManagerOptions = { agent?: Web5ManagedAgent; } + +export class VcManager { + /** + * Holds the instance of a `Web5ManagedAgent` that represents the current + * execution context for the `VcManager`. This agent is utilized + * to interact with other Web5 agent components. It's vital + * to ensure this instance is set to correctly contextualize + * operations within the broader Web5 agent framework. + */ + private _agent?: Web5ManagedAgent; + + constructor(options: VcManagerOptions) { + const { agent } = options; + this._agent = agent; + } + + /** + * Retrieves the `Web5ManagedAgent` execution context. + * If the `agent` instance proprety is undefined, it will throw an error. + * + * @returns The `Web5ManagedAgent` instance that represents the current execution + * context. + * + * @throws Will throw an error if the `agent` instance property is undefined. + */ + get agent(): Web5ManagedAgent { + if (this._agent === undefined) { + throw new Error('VcManager: Unable to determine agent execution context.'); + } + + return this._agent; + } + + set agent(agent: Web5ManagedAgent) { + this._agent = agent; + } + + /** + * Processes a request to create and sign a verifiable credential. + * The process involves creating a VC object with the provided data, constructing a signer, + * and signing the VC with the signer's sign function. The resultant VC is a JWT (JSON Web Token). + */ + async processRequest(request: ProcessVcRequest): Promise { + const { dataType, issuer, subject, data } = request; + const vc = VerifiableCredential.create(dataType, issuer, subject, data); + + const vcSigner = await this.constructVcSigner(issuer); + const vcSignOptions = { + issuerDid : issuer, + subjectDid : subject, + kid : vcSigner.keyId, + alg : vcSigner.algorithm, + signer : vcSigner.sign + }; + + const vcJwt = await vc.sign(vcSignOptions); + return {vcJwt: vcJwt}; + } + + private async constructVcSigner(author: string): Promise { + const signingKeyId = await this.agent.didManager.getDefaultSigningKey({ did: author }); + + if (!signingKeyId) { + throw new Error (`VcManager: Unable to determine signing key id for author: '${author}'`); + } + + /** + * DID keys stored in KeyManager use the canonicalId as an alias, so + * normalize the signing key ID before attempting to retrieve the key. + */ + const parsedDid = didUtils.parseDid({ didUrl: signingKeyId }); + if (!parsedDid) { + throw new Error(`DidIonMethod: Unable to parse DID: ${signingKeyId}`); + } + + const normalizedDid = parsedDid.did.split(':', 3).join(':'); + const normalizedSigningKeyId = `${normalizedDid}#${parsedDid.fragment}`; + const signingKey = await this.agent.keyManager.getKey({ keyRef: normalizedSigningKeyId }); + + if (!isManagedKeyPair(signingKey)) { + throw new Error(`VcManager: Signing key not found for author: '${author}'`); + } + + const { alg } = Jose.webCryptoToJose(signingKey.privateKey.algorithm); + if (alg === undefined) { + throw Error(`No algorithm provided to sign with key ID ${signingKeyId}`); + } + + return { + keyId : signingKeyId, + algorithm : alg, + sign : async (content: Uint8Array): Promise => { + return await this.agent.keyManager.sign({ + algorithm : signingKey.privateKey.algorithm, + data : content, + keyRef : normalizedSigningKeyId + }); + } + }; + } +} \ No newline at end of file diff --git a/packages/api/src/vc-api.ts b/packages/api/src/vc-api.ts index ccf346be2..dc7617daf 100644 --- a/packages/api/src/vc-api.ts +++ b/packages/api/src/vc-api.ts @@ -15,10 +15,25 @@ export class VcApi { } /** - * Issues a VC (Not implemented yet) + * Issues a VC to the subject did + * + * @param issuer The issuer URI of the credential, as a [String]. + * @param subject The subject URI of the credential, as a [String]. + * @param dataType The type of the credential, as a [String]. + * @param data The credential data, as a generic type [T]. + * @param expirationDate The expiration date. + * @return A VerifiableCredential JWT. */ - async create() { - // TODO: implement - throw new Error('Not implemented.'); + async create(issuer: string, subject: string, dataType: string, data: any, expirationDate?: string): Promise { + const agentResponse = await this.agent.processVcRequest({ + issuer : issuer, + subject : subject, + dataType : dataType, + data : data, + expirationDate : expirationDate + }); + + const { vcJwt } = agentResponse; + return vcJwt; } } \ No newline at end of file diff --git a/packages/api/tests/utils/test-user-agent.ts b/packages/api/tests/utils/test-user-agent.ts index cf487db33..985787ad1 100644 --- a/packages/api/tests/utils/test-user-agent.ts +++ b/packages/api/tests/utils/test-user-agent.ts @@ -13,9 +13,15 @@ import type { SyncManager, } from '@web5/agent'; -import { Dwn, EventLogLevel, +import { + Dwn, + EventLogLevel, DataStoreLevel, - MessageStoreLevel, } from '@tbd54566975/dwn-sdk-js'; + MessageStoreLevel, + // RecordsWriteOptions, + // DwnInterfaceName, + // DwnMethodName, +} from '@tbd54566975/dwn-sdk-js'; import { DidResolver, DidKeyMethod, @@ -26,6 +32,7 @@ import { DidMessage, DwnManager, KeyManager, + VcManager, AppDataVault, Web5RpcClient, IdentityManager, @@ -43,6 +50,7 @@ type TestUserAgentOptions = { dwnManager: DwnManager; identityManager: IdentityManager; keyManager: KeyManager; + vcManager: VcManager, rpcClient: Web5Rpc; syncManager: SyncManager; @@ -60,6 +68,7 @@ export class TestUserAgent implements Web5ManagedAgent { dwnManager: DwnManager; identityManager: IdentityManager; keyManager: KeyManager; + vcManager: VcManager; rpcClient: Web5Rpc; syncManager: SyncManager; @@ -78,6 +87,7 @@ export class TestUserAgent implements Web5ManagedAgent { this.dwnManager = options.dwnManager; this.identityManager = options.identityManager; this.keyManager = options.keyManager; + this.vcManager = options.vcManager; this.rpcClient = options.rpcClient; this.syncManager = options.syncManager; @@ -86,6 +96,7 @@ export class TestUserAgent implements Web5ManagedAgent { this.dwnManager.agent = this; this.identityManager.agent = this; this.keyManager.agent = this; + this.vcManager.agent = this; this.syncManager.agent = this; // TestUserAgent-specific properties. @@ -128,6 +139,7 @@ export class TestUserAgent implements Web5ManagedAgent { memory: new LocalKms({ kmsName: 'memory' }) }; const keyManager = new KeyManager({ kms }); + const vcManager = new VcManager({}); // Instantiate DID resolver. const didMethodApis = [DidKeyMethod]; @@ -160,6 +172,7 @@ export class TestUserAgent implements Web5ManagedAgent { dwnManager, identityManager, keyManager, + vcManager, rpcClient, syncManager, }); @@ -191,8 +204,32 @@ export class TestUserAgent implements Web5ManagedAgent { return this.dwnManager.processRequest(request); } - async processVcRequest(_request: ProcessVcRequest): Promise { - throw new Error('Not implemented'); + async processVcRequest(request: ProcessVcRequest): Promise { + const vcResponse = await this.vcManager.processRequest(request); + + // TODO: Write to DWN and Update VcResponse Object with optional dwnResponse + // const messageOptions: Partial = { + // schema : request.dataType, + // dataFormat : 'application/vc+jwt', + // }; + // + // const vcDataBlob = new Blob([vcResponse.vcJwt], { type: 'text/plain' }); + // + // const dwnProcessOptions = { + // author : request.issuer, + // dataStream : vcDataBlob, + // messageOptions, + // messageType : DwnInterfaceName.Records + DwnMethodName.Write, + // store : true, + // target : request.issuer + // }; + // + // const dwnResponse = await this.processDwnRequest(dwnProcessOptions); + // + // console.log('DWN RESPONSE:'); + // console.log(dwnResponse); + + return vcResponse; } async sendDidRequest(_request: DidRequest): Promise { diff --git a/packages/api/tests/vc-api.spec.ts b/packages/api/tests/vc-api.spec.ts index dcfd9b03d..f4e8c6ba4 100644 --- a/packages/api/tests/vc-api.spec.ts +++ b/packages/api/tests/vc-api.spec.ts @@ -3,10 +3,12 @@ import { TestManagedAgent } from '@web5/agent'; import { VcApi } from '../src/vc-api.js'; import { TestUserAgent } from './utils/test-user-agent.js'; +import { VerifiableCredential } from '@web5/credentials'; describe('VcApi', () => { let vc: VcApi; let testAgent: TestManagedAgent; + let identityDid: string; before(async () => { testAgent = await TestManagedAgent.create({ @@ -28,8 +30,9 @@ describe('VcApi', () => { kms : 'local' }); + identityDid = identity.did; // Instantiate VcApi. - vc = new VcApi({ agent: testAgent.agent, connectedDid: identity.did }); + vc = new VcApi({ agent: testAgent.agent, connectedDid: identityDid }); }); after(async () => { @@ -38,13 +41,13 @@ describe('VcApi', () => { }); describe('create()', () => { - it('is not implemented', async () => { - try { - await vc.create(); - expect.fail('Expected method to throw, but it did not.'); - } catch(e) { - expect(e.message).to.include('Not implemented.'); - } + it('returns a self signed vc', async () => { + const vcJwt = await vc.create(identityDid, identityDid, 'ExampleDataType', {example: 'goodStuff'}); + + expect(vcJwt).to.not.be.null; + expect(vcJwt.split('.').length).to.equal(3); + + await expect(VerifiableCredential.verify(vcJwt)).to.be.fulfilled; }); }); }); \ No newline at end of file diff --git a/packages/credentials/src/verifiable-credential.ts b/packages/credentials/src/verifiable-credential.ts index 2da6086bf..4d62afd78 100644 --- a/packages/credentials/src/verifiable-credential.ts +++ b/packages/credentials/src/verifiable-credential.ts @@ -22,9 +22,10 @@ export const DEFAULT_VC_TYPE = 'VerifiableCredential'; export type VcDataModel = ICredential; export type SignOptions = { - kid: string; issuerDid: string; subjectDid: string; + kid: string; + alg: string; signer: Signer, } @@ -262,9 +263,9 @@ function decode(jwt: string): DecodedVcJwt { } async function createJwt(payload: any, signOptions: SignOptions) { - const { issuerDid, subjectDid, signer, kid } = signOptions; + const { issuerDid, subjectDid, signer, kid, alg } = signOptions; - const header: JwtHeaderParams = { alg: 'EdDSA', typ: 'JWT', kid: kid }; + const header: JwtHeaderParams = { alg: alg, typ: 'JWT', kid: kid }; const jwtPayload = { iss : issuerDid, diff --git a/packages/credentials/tests/presentation-exchange.spec.ts b/packages/credentials/tests/presentation-exchange.spec.ts index 881614b85..ea70a3116 100644 --- a/packages/credentials/tests/presentation-exchange.spec.ts +++ b/packages/credentials/tests/presentation-exchange.spec.ts @@ -33,6 +33,7 @@ describe('PresentationExchange', () => { issuerDid : alice.did, subjectDid : alice.did, kid : alice.did + '#' + alice.did.split(':')[2], + alg : 'EdDSA', signer : signer }; diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts index 13a9476dc..f8bcd472b 100644 --- a/packages/credentials/tests/verifiable-credential.spec.ts +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -26,6 +26,7 @@ describe('Verifiable Credential Tests', () => { issuerDid : alice.did, subjectDid : alice.did, kid : alice.did + '#' + alice.did.split(':')[2], + alg : 'EdDSA', signer : signer }; }); @@ -128,6 +129,7 @@ describe('Verifiable Credential Tests', () => { issuerDid : 'bad:did: invalidDid', subjectDid : signOptions.subjectDid, kid : signOptions.issuerDid + '#' + signOptions.issuerDid.split(':')[2], + alg : 'EdDSA', signer : signer }; diff --git a/packages/identity-agent/src/identity-agent.ts b/packages/identity-agent/src/identity-agent.ts index 38a2c1565..e8ddecd6d 100644 --- a/packages/identity-agent/src/identity-agent.ts +++ b/packages/identity-agent/src/identity-agent.ts @@ -22,6 +22,7 @@ import { DidMessage, DwnManager, KeyManager, + VcManager, DidStoreDwn, KeyStoreDwn, AppDataVault, @@ -41,6 +42,7 @@ export type IdentityAgentOptions = { dwnManager: DwnManager; identityManager: IdentityManager; keyManager: KeyManager; + vcManager: VcManager; rpcClient: Web5Rpc; syncManager: SyncManager; } @@ -53,6 +55,7 @@ export class IdentityAgent implements Web5ManagedAgent { dwnManager: DwnManager; identityManager: IdentityManager; keyManager: KeyManager; + vcManager: VcManager; rpcClient: Web5Rpc; syncManager: SyncManager; @@ -64,6 +67,7 @@ export class IdentityAgent implements Web5ManagedAgent { this.dwnManager = options.dwnManager; this.identityManager = options.identityManager; this.keyManager = options.keyManager; + this.vcManager = options.vcManager; this.rpcClient = options.rpcClient; this.syncManager = options.syncManager; @@ -72,13 +76,14 @@ export class IdentityAgent implements Web5ManagedAgent { this.dwnManager.agent = this; this.identityManager.agent = this; this.keyManager.agent = this; + this.vcManager.agent = this; this.syncManager.agent = this; } static async create(options: Partial = {}): Promise { let { agentDid, appData, didManager, didResolver, dwnManager, - identityManager, keyManager, rpcClient, syncManager + identityManager, keyManager, vcManager, rpcClient, syncManager } = options; if (agentDid === undefined) { @@ -143,6 +148,12 @@ export class IdentityAgent implements Web5ManagedAgent { }); } + if (vcManager === undefined) { + // A custom VcManager implementation was not specified, so + // instantiate a default. + vcManager = new VcManager({}); + } + if (rpcClient === undefined) { // A custom RPC Client implementation was not specified, so // instantiate a default. @@ -164,6 +175,7 @@ export class IdentityAgent implements Web5ManagedAgent { dwnManager, identityManager, keyManager, + vcManager, rpcClient, syncManager }); @@ -217,8 +229,8 @@ export class IdentityAgent implements Web5ManagedAgent { return this.dwnManager.processRequest(request); } - async processVcRequest(_request: ProcessVcRequest): Promise { - throw new Error('Not implemented'); + async processVcRequest(request: ProcessVcRequest): Promise { + return this.vcManager.processRequest(request); } async sendDidRequest(_request: DidRequest): Promise { diff --git a/packages/user-agent/src/user-agent.ts b/packages/user-agent/src/user-agent.ts index 816233b06..d5017bd08 100644 --- a/packages/user-agent/src/user-agent.ts +++ b/packages/user-agent/src/user-agent.ts @@ -21,6 +21,7 @@ import { DidManager, DwnManager, KeyManager, + VcManager, DidStoreDwn, KeyStoreDwn, AppDataVault, @@ -41,6 +42,7 @@ export type Web5UserAgentOptions = { dwnManager: DwnManager; identityManager: IdentityManager; keyManager: KeyManager; + vcManager: VcManager; rpcClient: Web5Rpc; syncManager: SyncManager; } @@ -53,6 +55,7 @@ export class Web5UserAgent implements Web5ManagedAgent { dwnManager: DwnManager; identityManager: IdentityManager; keyManager: KeyManager; + vcManager: VcManager; rpcClient: Web5Rpc; syncManager: SyncManager; @@ -60,6 +63,7 @@ export class Web5UserAgent implements Web5ManagedAgent { this.agentDid = options.agentDid; this.appData = options.appData; this.keyManager = options.keyManager; + this.vcManager = options.vcManager; this.didManager = options.didManager; this.didResolver = options.didResolver; this.dwnManager = options.dwnManager; @@ -72,13 +76,14 @@ export class Web5UserAgent implements Web5ManagedAgent { this.dwnManager.agent = this; this.identityManager.agent = this; this.keyManager.agent = this; + this.vcManager.agent = this; this.syncManager.agent = this; } static async create(options: Partial = {}): Promise { let { agentDid, appData, didManager, didResolver, dwnManager, - identityManager, keyManager, rpcClient, syncManager + identityManager, keyManager, vcManager, rpcClient, syncManager } = options; if (agentDid === undefined) { @@ -147,6 +152,13 @@ export class Web5UserAgent implements Web5ManagedAgent { }); } + + if (vcManager === undefined) { + /** A custom VcManager implementation was not specified, so + * instantiate a default. */ + vcManager = new VcManager({}); + } + if (rpcClient === undefined) { // A custom RPC Client implementation was not specified, so // instantiate a default. @@ -167,6 +179,7 @@ export class Web5UserAgent implements Web5ManagedAgent { didResolver, dwnManager, keyManager, + vcManager, identityManager, rpcClient, syncManager @@ -219,8 +232,8 @@ export class Web5UserAgent implements Web5ManagedAgent { return this.dwnManager.processRequest(request); } - async processVcRequest(_request: ProcessVcRequest): Promise { - throw new Error('Not implemented'); + async processVcRequest(request: ProcessVcRequest): Promise { + return this.vcManager.processRequest(request); } async sendDidRequest(_request: DidRequest): Promise {