Skip to content

Commit

Permalink
signing with kid
Browse files Browse the repository at this point in the history
  • Loading branch information
nitro-neal committed Jul 26, 2023
1 parent a843df2 commit 7c3de88
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 77 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion packages/web5-agent/src/web5-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export type ProcessVcRequest = {
author: string;
target: string;
vc: VerifiableCredential;
kid?: string
};

export type SendVcRequest = {
Expand All @@ -75,7 +76,7 @@ export type SendVcRequest = {
};

export type VcResponse = {
vcDataBlob?: Blob;
vcJwt?: string;
message?: unknown;
messageCid?: string;
reply: UnionMessageReply;
Expand Down
30 changes: 0 additions & 30 deletions packages/web5-user-agent/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,10 @@
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());
}

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();
}
70 changes: 40 additions & 30 deletions packages/web5-user-agent/src/web5-user-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -204,11 +205,32 @@ export class Web5UserAgent implements Web5Agent {
}

async processVcRequest(request: ProcessVcRequest): Promise<VcResponse> {
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<RecordsWriteOptions> = { ...{ 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,
Expand All @@ -220,16 +242,15 @@ export class Web5UserAgent implements Web5Agent {
});

const vcResponse: VcResponse = {
vcDataBlob: dataBlob,
vcJwt: vcJwt,
...dwnResponse,
};

return vcResponse;
}

Check warning on line 250 in packages/web5-user-agent/src/web5-user-agent.ts

View check run for this annotation

Codecov / codecov/patch

packages/web5-user-agent/src/web5-user-agent.ts#L208-L250

Added lines #L208 - L250 were not covered by tests

async sendVcRequest(request: SendVcRequest): Promise<VcResponse> {
console.log(request);
return {} as VcResponse;
async sendVcRequest(_request: SendVcRequest): Promise<VcResponse> {
throw new Error('Method not implemented.');
}

Check warning on line 254 in packages/web5-user-agent/src/web5-user-agent.ts

View check run for this annotation

Codecov / codecov/patch

packages/web5-user-agent/src/web5-user-agent.ts#L253-L254

Added lines #L253 - L254 were not covered by tests

async #getDwnMessage(author: string, messageType: string, messageCid: string): Promise<DwnMessage> {
Expand Down Expand Up @@ -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<string> {
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<string> {
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);

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

Check warning on line 381 in packages/web5-user-agent/src/web5-user-agent.ts

View check run for this annotation

Codecov / codecov/patch

packages/web5-user-agent/src/web5-user-agent.ts#L349-L381

Added lines #L349 - L381 were not covered by tests
Expand Down
12 changes: 7 additions & 5 deletions packages/web5/src/vc-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Record } from './record.js';
export type VcCreateResponse = {
status: UnionMessageReply['status'];
record?: Record
vcJwt?: string;
};

export class VcApi {
Expand All @@ -22,7 +23,7 @@ export class VcApi {
}

// TODO: Add CreateOptions for more robust VC creation
async create(credentialSubject: any): Promise<VcCreateResponse> {
async create(credentialSubject: any, kid?: string): Promise<VcCreateResponse> {
if (!credentialSubject || typeof credentialSubject !== 'object') {
throw new Error('credentialSubject not valid');
}
Expand All @@ -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;
Expand All @@ -49,14 +51,14 @@ 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,
};

record = new Record(this.#web5Agent, recordOptions);
}

return { record, status };
return { record: record, status: status, vcJwt: agentResponse.vcJwt };
}
}
2 changes: 1 addition & 1 deletion packages/web5/src/web5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
18 changes: 12 additions & 6 deletions packages/web5/tests/test-utils/test-user-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type TestAgentOptions = {
didResolver: DidResolver;
didIon: DidIonApi;
didKey: DidKeyApi;
signKeyPair: ManagedKeyPair;
}

export type TestProfile = {
Expand All @@ -46,6 +47,7 @@ export class TestAgent {
didResolver: DidResolver;
didIon: DidIonApi;
didKey: DidKeyApi;
signKeyPair: ManagedKeyPair;

constructor(options: TestAgentOptions) {
this.agent = options.agent;
Expand All @@ -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<void> {
Expand Down Expand Up @@ -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<string, ManagedKey | ManagedKeyPair>();
const kmsKeyStore = new KmsKeyStore(kmsMemoryStore);

// Instantiate in-memory store for KMS private keys.
const memoryPrivateKeyStore = new MemoryStore<string, ManagedPrivateKey>();
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<string, ManagedKey | ManagedKeyPair>();
const keyManagerStore = new KeyManagerStore({ store: kmMemoryStore });

Expand All @@ -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,
Expand All @@ -144,8 +149,9 @@ export class TestAgent {
profileApi,
profileStore,
didResolver,
didIon : DidIon,
didKey : DidKey,
didIon : DidIon,
didKey : DidKey,
signKeyPair : keyPair
});
}

Expand Down
12 changes: 9 additions & 3 deletions packages/web5/tests/web5-vc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Expand All @@ -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');
Expand Down

0 comments on commit 7c3de88

Please sign in to comment.