Skip to content
Merged
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
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^20.11.24",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.1.0",
Expand All @@ -34,6 +35,7 @@
"typescript": "^5.3.3"
},
"dependencies": {
"jose": "^6.0.10",
"jsonwebtoken": "^9.0.2",
"uuid": "^9.0.1"
}
Expand Down
41 changes: 39 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Encoding = 'base64' | 'base64url' | 'none';
export type Encoding = 'base64' | 'base64url' | 'json' | 'none';

export interface Attachment {
type: string;
Expand Down Expand Up @@ -72,6 +72,27 @@ export interface Analysis {
extra?: Record<string, any>;
}

/**
* Interface for JWS signature components according to the vCon specification.
* Used in the signatures array of a signed vCon.
*/
export interface Signature {
/**
* The protected header in base64url encoding
*/
protected: string;

/**
* The JWS signature in base64url encoding
*/
signature: string;

/**
* Optional unprotected header
*/
header?: Record<string, any>;
}

export interface VconData {
uuid?: string;
vcon?: string;
Expand All @@ -87,8 +108,24 @@ export interface VconData {
attachments?: Attachment[];
analysis?: Analysis[];
tags?: Record<string, any>;

/**
* Original signature property - kept for backward compatibility
*/
signature?: {
alg: string;
signature: string;
};
}

/**
* JWS signature array according to the JWS JSON Serialization
* Added when a vCon is signed using the sign() method
*/
signatures?: Signature[];

/**
* Base64url encoded payload containing the original vCon data
* Added when a vCon is signed using the sign() method
*/
payload?: string;
}
197 changes: 193 additions & 4 deletions src/vcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { VconData, Attachment, Party, Dialog, Analysis, Encoding } from './types
import { Attachment as AttachmentClass } from './attachment';
import { Party as PartyClass } from './party';
import { Dialog as DialogClass } from './dialog';
import * as crypto from 'crypto';

export class Vcon {
private data: VconData;
data: VconData;

constructor(vconDict: Partial<VconData> = {}) {
this.data = {
Expand Down Expand Up @@ -82,10 +83,13 @@ export class Vcon {
dialog: params.dialog,
vendor: params.vendor,
body: params.body,
encoding: params.encoding || 'none',
extra: params.extra || {}
encoding: params.encoding || 'none'
};

if (params.extra) {
analysis.extra = params.extra;
}

if (!this.data.analysis) {
this.data.analysis = [];
}
Expand Down Expand Up @@ -126,6 +130,191 @@ export class Vcon {
return { ...this.data };
}

/**
* Helper method to encode a string in base64url format
*
* @param input - The string to encode
* @returns The base64url encoded string
*/
private base64UrlEncode(input: string): string {
return Buffer.from(input)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}

/**
* Sign the vCon using JWS (JSON Web Signature).
*
* This method signs the vCon using the provided private key, adding the signature
* information to the vCon. The signature can later be verified using the
* corresponding public key.
*
* @param privateKey - The RSA private key in PEM format or as a crypto.KeyObject
* @throws Error - If there is an error during the signing process
*
* @example
* ```typescript
* import * as crypto from 'crypto';
* const { privateKey } = crypto.generateKeyPairSync('rsa', {
* modulusLength: 2048,
* publicKeyEncoding: { type: 'spki', format: 'pem' },
* privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
* });
* const vcon = Vcon.buildNew();
* vcon.sign(privateKey);
* ```
*/
sign(privateKeyInput: string | crypto.KeyObject): void {
try {
console.log("Signing vCon with JWS");

// Convert the vCon to a JSON string for signing
const payload = this.toJson();

// Convert private key to PEM format if it's a KeyObject
const privateKey = typeof privateKeyInput === 'string'
? privateKeyInput
: privateKeyInput.export({ type: 'pkcs8', format: 'pem' }).toString();

// Create a header for JWS
const header = {
alg: 'RS256',
typ: 'JWS'
};

// Create base64url encoded versions
const headerBase64 = this.base64UrlEncode(JSON.stringify(header));
const payloadBase64 = this.base64UrlEncode(payload);

// Create the signature input
const signatureInput = `${headerBase64}.${payloadBase64}`;

// Create signature
const signer = crypto.createSign('RSA-SHA256');
signer.update(signatureInput);
const signature = signer.sign(privateKey, 'base64');

// Convert to base64url format
const signatureBase64Url = signature
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');

// Update the vCon with the signature information
this.data.signatures = [{ protected: headerBase64, signature: signatureBase64Url }];
this.data.payload = payloadBase64;

// Remove the original vCon properties that are now in the payload
// to match the signed vCon format
const keysToKeep = ['signatures', 'payload'];
Object.keys(this.data).forEach(key => {
if (!keysToKeep.includes(key)) {
delete this.data[key as keyof VconData];
}
});

console.log("Successfully signed vCon");
} catch (error) {
console.error("Failed to sign vCon:", error);
throw error;
}
}

/**
* Verify the JWS signature of the vCon.
*
* This method verifies the vCon's signature using the provided public key.
* The vCon must have been previously signed using the corresponding private key.
*
* @param publicKey - The RSA public key in PEM format or as a crypto.KeyObject
* @returns true if the signature is valid, false otherwise
* @throws Error - If the vCon is not signed
*
* @example
* ```typescript
* import * as crypto from 'crypto';
* const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
* modulusLength: 2048,
* publicKeyEncoding: { type: 'spki', format: 'pem' },
* privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
* });
* const vcon = Vcon.buildNew();
* vcon.sign(privateKey);
* const isValid = vcon.verify(publicKey);
* console.log(isValid); // Prints true
* ```
*/
verify(publicKeyInput: string | crypto.KeyObject): boolean {
if (!this.data.signatures || !this.data.payload) {
console.error("Cannot verify: vCon is not signed");
throw new Error("vCon is not signed");
}

try {
console.log("Verifying vCon signature");

// Extract components
const { protected: protectedHeader, signature } = this.data.signatures[0];
const payload = this.data.payload;

// Convert public key to appropriate format
const publicKey = typeof publicKeyInput === 'string'
? publicKeyInput
: publicKeyInput.export({ type: 'spki', format: 'pem' }).toString();

// Create signature input
const signatureInput = `${protectedHeader}.${payload}`;

// Convert base64url signature to base64
const signatureBase64 = signature
.replace(/-/g, '+')
.replace(/_/g, '/');

// Verify the signature
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(signatureInput);
const isValid = verifier.verify(publicKey, signatureBase64, 'base64');

console.log("Signature verification result:", isValid);
return isValid;
} catch (error) {
console.warn("Invalid signature detected:", error);
return false;
}
}

/**
* Generate a new RSA key pair for signing vCons.
*
* This method generates a new RSA key pair that can be used for signing
* and verifying vCons.
*
* @returns A tuple containing the private key and public key as PEM strings
*
* @example
* ```typescript
* const [privateKey, publicKey] = Vcon.generateKeyPair();
* const vcon = Vcon.buildNew();
* vcon.sign(privateKey);
* const isValid = vcon.verify(publicKey);
* console.log(isValid); // Prints true
* ```
*/
static generateKeyPair(): [string, string] {
console.log("Generating new RSA key pair");

const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});

console.log("Successfully generated RSA key pair");
return [privateKey, publicKey];
}

get parties(): Party[] {
return this.data.parties || [];
}
Expand Down Expand Up @@ -177,4 +366,4 @@ export class Vcon {
get meta(): Record<string, any> | undefined {
return this.data.meta;
}
}
}