Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
nitro-neal committed Jul 31, 2023
1 parent 7c3de88 commit 002985e
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 43 deletions.
8 changes: 7 additions & 1 deletion packages/credentials/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { ICredential, IIssuer, ICredentialStatus, ICredentialSubject} from '@sphereon/ssi-types';
import { ICredential, IIssuer, ICredentialStatus, ICredentialSubject, ICredentialSchemaType} from '@sphereon/ssi-types';

/**
* @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC data model}
*/
export type { ICredential as VerifiableCredential };


/**
* @see {@link https://www.w3.org/TR/vc-data-model/#data-schemas | Data schemas}
*/
export type { ICredentialSchemaType as CredentialSchemaType };

/**
* The issuer of a {@link VerifiableCredential}.
* The value of the issuer property must be either a URI or an object containing an `id` property.
Expand Down
17 changes: 13 additions & 4 deletions packages/credentials/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@ export function getCurrentXmlSchema112Timestamp() : string {
}

export function getFutureXmlSchema112Timestamp(secondsInFuture: number): string {
// Create a new Date object for the current time plus the specified number of seconds
const futureDate = new Date(Date.now() + secondsInFuture * 1000); // convert seconds to milliseconds

// Omit the milliseconds part from toISOString() output
const futureDate = new Date(Date.now() + secondsInFuture * 1000);
return futureDate.toISOString().replace(/\.\d+Z$/, 'Z');
}

export function isValidXmlSchema112Timestamp(timestamp: string): boolean {
// Format: yyyy-MM-ddTHH:mm:ssZ
const regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
if (!regex.test(timestamp)) {
return false;
}

const date = new Date(timestamp);

return !isNaN(date.getTime());
}
48 changes: 39 additions & 9 deletions packages/credentials/tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
import { expect } from 'chai';
import {getCurrentXmlSchema112Timestamp, getFutureXmlSchema112Timestamp} from '../src/utils.js';
import {getCurrentXmlSchema112Timestamp, getFutureXmlSchema112Timestamp, isValidXmlSchema112Timestamp} from '../src/utils.js';

describe('credentials utils', () => {
it('gets correct time', () => {
const timestamp = getCurrentXmlSchema112Timestamp();
expect(timestamp).to.not.be.undefined;
expect(timestamp).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);

describe('getCurrentXmlSchema112Timestamp', () => {
it('gets correct time', () => {
const timestamp = getCurrentXmlSchema112Timestamp();
expect(timestamp).to.not.be.undefined;
expect(timestamp).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);

});
});
describe('getFutureXmlSchema112Timestamp', () => {
it('gets correct time', () => {
const timestamp = getFutureXmlSchema112Timestamp(123);
expect(timestamp).to.not.be.undefined;
expect(timestamp).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
});
});

it('gets correct time', () => {
const timestamp = getFutureXmlSchema112Timestamp(123);
expect(timestamp).to.not.be.undefined;
expect(timestamp).to.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/);
describe('validateXmlSchema112Timestamp', () => {
it('validates correctly formatted timestamps', () => {
const timestamp = '2023-07-31T12:34:56Z';
const result = isValidXmlSchema112Timestamp(timestamp);
expect(result).to.be.true;
});

it('rejects incorrectly formatted timestamps', () => {
const badTimestamp = '2023-07-31T12:34:56.789Z'; // includes milliseconds
const result = isValidXmlSchema112Timestamp(badTimestamp);
expect(result).to.be.false;
});

it('rejects non-timestamps', () => {
const notATimestamp = 'This is definitely not a timestamp';
const result = isValidXmlSchema112Timestamp(notATimestamp);
expect(result).to.be.false;
});

it('rejects empty string', () => {
const emptyString = '';
const result = isValidXmlSchema112Timestamp(emptyString);
expect(result).to.be.false;
});
});
});
41 changes: 37 additions & 4 deletions packages/web5-user-agent/src/web5-user-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,12 @@ export class Web5UserAgent implements Web5Agent {
}

async processVcRequest(request: ProcessVcRequest): Promise<VcResponse> {
if (!request.vc) {
throw new Error(`must have vc to process vc request`);
}

let kid = request.kid;

if (!kid) {
const didResolution = await this.didResolver.resolve(request.author);

Expand All @@ -229,8 +233,39 @@ export class Web5UserAgent implements Web5Agent {

const vcJwt = await this.#sign(request.vc as VerifiableCredential, kid);

const messageOptions: Partial<RecordsWriteOptions> = { ...{ schema: 'vc/vc', dataFormat: 'application/vc+jwt' } };
const dataBlob = new Blob([vcJwt], { type: 'text/plain' });
let schema;

if (request.vc.credentialSchema) {
let credentialSchema = request.vc.credentialSchema;
if (typeof credentialSchema === 'string') {
schema = credentialSchema;
} else if (Array.isArray(credentialSchema)) {
for (let item of credentialSchema) {
if (typeof item !== 'string' && item.id) {
schema = item.id;
break;
}
}
} else if (typeof credentialSchema === 'object' && credentialSchema.id) {
schema = credentialSchema.id;
}
} else if (!schema && request.vc['@context']) {
let context = request.vc['@context'];

if (typeof context === 'string' && context !== 'https://www.w3.org/2018/credentials/v1') {
schema = context;
} else if (Array.isArray(context)) {
let filteredContext = context.filter(e => typeof e === 'string' && e !== 'https://www.w3.org/2018/credentials/v1');
if (filteredContext.length > 0) {
schema = filteredContext[0];
}
} else if (typeof context === 'object' && context.name && context.name !== 'https://www.w3.org/2018/credentials/v1') {
schema = context.name;
}
}

const messageOptions: Partial<RecordsWriteOptions> = { ...{ schema, dataFormat: 'application/vc+jwt' } };
const dataBlob = new Blob([vcJwt], { type: 'application/vc+jwt' });

const dwnResponse = await this.processDwnRequest({
author : request.author,
Expand Down Expand Up @@ -343,8 +378,6 @@ export class Web5UserAgent implements Web5Agent {
return { message: dwnMessage.toJSON(), dataStream: readableStream };
}


// TODO: have issuer did key already stored in key manager and use that instead of generating a new one
async #sign(vc: VerifiableCredential, kid: string): Promise<string> {
const keyPair = await this.keyManager.getKey({keyRef: kid}) as ManagedKeyPair;

Expand Down
25 changes: 25 additions & 0 deletions packages/web5-user-agent/tests/common/web5-user-agent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,29 @@ describe('Web5UserAgent', () => {
expect(dataBytes).to.eql(value);
});
});

describe('sendVcRequest', () => {
it('throws an exception method not implemented.', async () => {
try {
const vc = {
id : '123',
'@context' : ['https://www.w3.org/2018/credentials/v1'],
credentialSubject : {firstName: 'Bob'},
type : ['VerifiableCredential'],
issuer : did,
issuanceDate : '2023-07-31T12:34:56Z',
};

await testAgent.agent.sendVcRequest({
author : did,
target : did,
vc : vc
});

expect.fail();
} catch(e) {
expect(e.message).to.include(`Method not implemented.`);
}
});
});
});
4 changes: 4 additions & 0 deletions packages/web5-user-agent/tests/node/web5-user-agent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ describe('[Node only] Web5UserAgent', () => {
describe('processDwnRequest', () => {
xit('can accept Blobs');
});

describe('processVcRequest', () => {
xit('can accept Vcs');
});
});
33 changes: 23 additions & 10 deletions packages/web5/src/vc-api.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { Web5Agent, VcResponse } from '@tbd54566975/web5-agent';
import type { VerifiableCredential } from '../../credentials/src/types.js';

import { getCurrentXmlSchema112Timestamp } from '../../credentials/src/utils.js';
import { v4 as uuidv4 } from 'uuid';

import { Web5Agent, VcResponse } from '@tbd54566975/web5-agent';
import { UnionMessageReply, RecordsWriteMessage } from '@tbd54566975/dwn-sdk-js';

import type { VerifiableCredential, CredentialSchemaType } from '@tbd54566975/credentials/';
import { getCurrentXmlSchema112Timestamp, isValidXmlSchema112Timestamp } from '@tbd54566975/credentials';

import { Record } from './record.js';

export type VcCreateRequest = {
credentialSubject: any;
kid?: string;
credentialSchema?: CredentialSchemaType | CredentialSchemaType[];
expirationDate?: string;
}

export type VcCreateResponse = {
status: UnionMessageReply['status'];
record?: Record
Expand All @@ -22,26 +30,31 @@ export class VcApi {
this.#connectedDid = connectedDid;
}

// TODO: Add CreateOptions for more robust VC creation
async create(credentialSubject: any, kid?: string): Promise<VcCreateResponse> {
if (!credentialSubject || typeof credentialSubject !== 'object') {
async create(request: VcCreateRequest): Promise<VcCreateResponse> {
if (!request.credentialSubject || typeof request.credentialSubject !== 'object') {
throw new Error('credentialSubject not valid');
}

if (request?.expirationDate && !isValidXmlSchema112Timestamp(request?.expirationDate)) {
throw new Error('expirationDate not valid');
}

const vc: VerifiableCredential = {
id : uuidv4(),
'@context' : ['https://www.w3.org/2018/credentials/v1'],
credentialSubject : credentialSubject,
credentialSubject : request.credentialSubject,
type : ['VerifiableCredential'],
issuer : this.#connectedDid ,
issuanceDate : getCurrentXmlSchema112Timestamp(),
credentialSchema : request?.credentialSchema,
expirationDate : request?.expirationDate,
};

const agentResponse: VcResponse = await this.#web5Agent.processVcRequest({
author : this.#connectedDid,
target : this.#connectedDid,
vc : vc,
kid : kid
kid : request.kid
});

const { message, reply: { status } } = agentResponse;
Expand All @@ -51,7 +64,7 @@ export class VcApi {
if (200 <= status.code && status.code <= 299) {
const recordOptions = {
author : this.#connectedDid,
encodedData : new Blob([agentResponse.vcJwt], { type: 'text/plain' }),
encodedData : new Blob([agentResponse.vcJwt], { type: 'application/vc+jwt' }),
target : this.#connectedDid,
...responseMessage,
};
Expand Down
2 changes: 2 additions & 0 deletions packages/web5/tests/test-utils/test-user-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ export class TestAgent {
connections : [appDidState.id],
});

// TODO: Import did auth key into key manager (but will have to convert format from jwk to key material)

return { did: profile.did.id };
}

Expand Down
64 changes: 49 additions & 15 deletions packages/web5/tests/web5-vc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { expect } from 'chai';

import * as testProfile from './fixtures/test-profiles.js';

import { VcApi } from '../src/vc-api.js';
import { VcApi, VcCreateRequest } from '../src/vc-api.js';
import { TestAgent, TestProfileOptions } from './test-utils/test-user-agent.js';

// import jwt from 'jsonwebtoken';
import jwt from 'jsonwebtoken';

let did: string;
let vcApi: VcApi;
Expand All @@ -21,9 +21,6 @@ describe('web5.vc', () => {
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);
Expand All @@ -38,27 +35,64 @@ describe('web5.vc', () => {
describe('create', () => {
it('valid vc', async () => {
const credentialSubject = {firstName: 'alice'};
const result = await vcApi.create(credentialSubject, testAgent.signKeyPair.privateKey.id);

// const resultRecord = await result.record?.data.text();
// console.log({resultRecord});
const vcCreateRequest: VcCreateRequest = { credentialSubject: credentialSubject, kid: testAgent.signKeyPair.privateKey.id};
const result = await vcApi.create(vcCreateRequest);

// const decoded = jwt.decode(resultRecord, { complete: true });
// console.log(decoded);
const resultRecord = await result.record?.data.text();
const decodedVc = jwt.decode(resultRecord, { complete: true });

expect(result.status.code).to.equal(202);
expect(result.status.detail).to.equal('Accepted');
expect(result.record).to.exist;
// expect(resultRecord).to.deep.equal(result.vcJwt);
// expect(decoded.payload.id).to.equal(result.vc.id);
// expect(decoded.payload.credentialSubject).to.deep.equal(result.vc.credentialSubject);
// expect(decoded.payload.issuer).to.deep.equal(result.vc.issuer);
expect(resultRecord).to.equal(result.vcJwt);
expect(decodedVc.payload.vc.credentialSubject).to.deep.equal(credentialSubject);
});

it('simple schema string', async () => {
const credentialSubject = {firstName: 'alice'};

const vcCreateRequest: VcCreateRequest = { credentialSubject: credentialSubject, kid: testAgent.signKeyPair.privateKey.id, credentialSchema: 'https://schema.org/Person'};
const result = await vcApi.create(vcCreateRequest);

const resultRecord = await result.record?.data.text();
const decodedVc = jwt.decode(resultRecord, { complete: true });

expect(result.status.code).to.equal(202);
expect(decodedVc.payload.vc.credentialSchema).to.equal('https://schema.org/Person');
expect(result.record?.schema).to.equal('https://schema.org/Person');
});

it('simple schema type', async () => {
const credentialSubject = {firstName: 'alice'};

const vcCreateRequest: VcCreateRequest = { credentialSubject: credentialSubject, kid: testAgent.signKeyPair.privateKey.id, credentialSchema: {id: 'https://schema.org/Person', type: 'JsonSchemaValidator2018'}};
const result = await vcApi.create(vcCreateRequest);

const resultRecord = await result.record?.data.text();
const decodedVc = jwt.decode(resultRecord, { complete: true });

expect(result.status.code).to.equal(202);
expect(decodedVc.payload.vc.credentialSchema).to.deep.equal({id: 'https://schema.org/Person', type: 'JsonSchemaValidator2018'});
expect(result.record?.schema).to.equal('https://schema.org/Person');
});

it('invalid expiration date', async () => {
const credentialSubject = {firstName: 'alice'};
try {
const vcCreateRequest: VcCreateRequest = { credentialSubject: credentialSubject, kid: testAgent.signKeyPair.privateKey.id, expirationDate: 'badexpirationdate'};
await vcApi.create(vcCreateRequest);
expect.fail();
} catch(e) {
expect(e.message).to.include('expirationDate not valid');
}
});

it('invalid credential subject', async () => {
const credentialSubject = 'badcredsubject';
try {
await vcApi.create(credentialSubject, testAgent.signKeyPair.privateKey.id);
const vcCreateRequest: VcCreateRequest = { credentialSubject: credentialSubject, kid: testAgent.signKeyPair.privateKey.id};
await vcApi.create(vcCreateRequest);
expect.fail();
} catch(e) {
expect(e.message).to.include('credentialSubject not valid');
Expand Down

0 comments on commit 002985e

Please sign in to comment.