Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding web vc create #269

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
5 changes: 5 additions & 0 deletions packages/agent/src/test-managed-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();

Expand All @@ -146,6 +150,7 @@ export class TestManagedAgent {
dwnManager,
identityManager,
keyManager,
vcManager,
rpcClient,
syncManager
});
Expand Down
20 changes: 18 additions & 2 deletions packages/agent/src/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
110 changes: 110 additions & 0 deletions packages/agent/src/vc-manager.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Check warning on line 41 in packages/agent/src/vc-manager.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/vc-manager.ts#L36-L41

Added lines #L36 - L41 were not covered by tests

set agent(agent: Web5ManagedAgent) {
this._agent = agent;
}

Check warning on line 45 in packages/agent/src/vc-manager.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/vc-manager.ts#L44-L45

Added lines #L44 - L45 were not covered by tests

/**
* 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<VcResponse> {
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};
}

Check warning on line 67 in packages/agent/src/vc-manager.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/vc-manager.ts#L53-L67

Added lines #L53 - L67 were not covered by tests

private async constructVcSigner(author: string): Promise<Signer> {
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<Uint8Array> => {
return await this.agent.keyManager.sign({
algorithm : signingKey.privateKey.algorithm,
data : content,
keyRef : normalizedSigningKeyId
});
}
};
}

Check warning on line 109 in packages/agent/src/vc-manager.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/vc-manager.ts#L70-L109

Added lines #L70 - L109 were not covered by tests
}
23 changes: 19 additions & 4 deletions packages/api/src/vc-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,25 @@
}

/**
* Issues a VC (Not implemented yet)
* Issues a VC to the subject did
*
* @param issuer The issuer URI of the credential, as a [String].

Check warning on line 20 in packages/api/src/vc-api.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

docs: tsdoc-param-tag-missing-hyphen

The `@param` block should be followed by a parameter name and then a hyphen
* @param subject The subject URI of the credential, as a [String].

Check warning on line 21 in packages/api/src/vc-api.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

docs: tsdoc-param-tag-missing-hyphen

The `@param` block should be followed by a parameter name and then a hyphen
* @param dataType The type of the credential, as a [String].

Check warning on line 22 in packages/api/src/vc-api.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

docs: tsdoc-param-tag-missing-hyphen

The `@param` block should be followed by a parameter name and then a hyphen
* @param data The credential data, as a generic type [T].

Check warning on line 23 in packages/api/src/vc-api.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

docs: tsdoc-param-tag-missing-hyphen

The `@param` block should be followed by a parameter name and then a hyphen
* @param expirationDate The expiration date.

Check warning on line 24 in packages/api/src/vc-api.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

docs: tsdoc-param-tag-missing-hyphen

The `@param` block should be followed by a parameter name and then a hyphen
* @return A VerifiableCredential JWT.

Check warning on line 25 in packages/api/src/vc-api.ts

View workflow job for this annotation

GitHub Actions / tbdocs-reporter

docs: tsdoc-undefined-tag

The TSDoc tag "`@return`" is not defined in this configuration
*/
async create() {
// TODO: implement
throw new Error('Not implemented.');
async create(issuer: string, subject: string, dataType: string, data: any, expirationDate?: string): Promise<string> {
const agentResponse = await this.agent.processVcRequest({
issuer : issuer,
subject : subject,
dataType : dataType,
data : data,
expirationDate : expirationDate
});

const { vcJwt } = agentResponse;
return vcJwt;
}
}
45 changes: 41 additions & 4 deletions packages/api/tests/utils/test-user-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +32,7 @@ import {
DidMessage,
DwnManager,
KeyManager,
VcManager,
AppDataVault,
Web5RpcClient,
IdentityManager,
Expand All @@ -43,6 +50,7 @@ type TestUserAgentOptions = {
dwnManager: DwnManager;
identityManager: IdentityManager;
keyManager: KeyManager;
vcManager: VcManager,
rpcClient: Web5Rpc;
syncManager: SyncManager;

Expand All @@ -60,6 +68,7 @@ export class TestUserAgent implements Web5ManagedAgent {
dwnManager: DwnManager;
identityManager: IdentityManager;
keyManager: KeyManager;
vcManager: VcManager;
rpcClient: Web5Rpc;
syncManager: SyncManager;

Expand All @@ -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;

Expand All @@ -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.
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -160,6 +172,7 @@ export class TestUserAgent implements Web5ManagedAgent {
dwnManager,
identityManager,
keyManager,
vcManager,
rpcClient,
syncManager,
});
Expand Down Expand Up @@ -191,8 +204,32 @@ export class TestUserAgent implements Web5ManagedAgent {
return this.dwnManager.processRequest(request);
}

async processVcRequest(_request: ProcessVcRequest): Promise<VcResponse> {
throw new Error('Not implemented');
async processVcRequest(request: ProcessVcRequest): Promise<VcResponse> {
const vcResponse = await this.vcManager.processRequest(request);

// TODO: Write to DWN and Update VcResponse Object with optional dwnResponse
// const messageOptions: Partial<RecordsWriteOptions> = {
// 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<DidResponse> {
Expand Down
19 changes: 11 additions & 8 deletions packages/api/tests/vc-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 () => {
Expand All @@ -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;
});
});
});
7 changes: 4 additions & 3 deletions packages/credentials/src/verifiable-credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/credentials/tests/presentation-exchange.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('PresentationExchange', () => {
issuerDid : alice.did,
subjectDid : alice.did,
kid : alice.did + '#' + alice.did.split(':')[2],
alg : 'EdDSA',
signer : signer
};

Expand Down
Loading
Loading