Skip to content

Commit

Permalink
Added the WebCrypt conversion functions
Browse files Browse the repository at this point in the history
  • Loading branch information
iherman committed Aug 15, 2024
1 parent 2131378 commit 8ba9763
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 32 deletions.
153 changes: 146 additions & 7 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
/**
* Conversion to and from [Multikey format](https://www.w3.org/TR/controller-document/#multikey) from
* JWK for the three EC curves that are defined for Verifiable Credentials: [ECDSA with P-256 and P-384](https://www.w3.org/TR/vc-di-ecdsa/#multikey)
* JWK or WebCrypto for the three EC curves that are defined for Verifiable Credentials: [ECDSA with P-256 and P-384](https://www.w3.org/TR/vc-di-ecdsa/#multikey)
* and [EDDSA](https://www.w3.org/TR/vc-di-eddsa/#multikey).
*
* @package
*/

import * as convert from './lib/convert';
import { JWKKeyPair, MultikeyPair, Multikey, isJWKKeyPair, isMultikeyPair } from './lib/common';
export type { JWKKeyPair, MultikeyPair, Multikey } from './lib/common';
import * as convert from './lib/convert';
import { JWKKeyPair, MultikeyPair, Multikey } from './lib/common';
export type { JWKKeyPair, MultikeyPair, Multikey } from './lib/common';

// This type guard function is reused at two different places, better factor it out...
function isMultikeyPair(obj: any): obj is MultikeyPair {
return (obj as MultikeyPair).publicKeyMultibase !== undefined;
}

/* =========================================================================================
Converting multikeys to JWK
========================================================================================= */

/**
* Generic function to convert a multikey pair to JWK. This function decodes the multikey data
* Convert a multikey pair to JWK. This function decodes the multikey data
* into a binary buffer, checks the preambles and invokes the crypto specific converter functions
* (depending on the preamble values) that do the final conversion from the binary data to JWK.
*
Expand All @@ -21,12 +30,14 @@ export type { JWKKeyPair, MultikeyPair, Multikey } from
* @throws - exceptions if something is incorrect in the incoming data
*/
export function multikeyToJWK(keys: MultikeyPair): JWKKeyPair;

/**
* Variant of the conversion function for a single (public) key in Multikey, returning the generated JWK.
* Overloaded version of the conversion function for a single (public) key in Multikey, returning the generated JWK.
* @param keys
* @throws - exceptions if something is incorrect in the incoming data
*/
export function multikeyToJWK(keys: Multikey): JsonWebKey;

export function multikeyToJWK(keys: MultikeyPair | Multikey): JWKKeyPair | JsonWebKey {
const input: MultikeyPair = isMultikeyPair(keys) ? keys as MultikeyPair : { publicKeyMultibase: keys };
const jwk_keys = convert.multikeyToJWK(input);
Expand All @@ -37,6 +48,74 @@ export function multikeyToJWK(keys: MultikeyPair | Multikey): JWKKeyPair | JsonW
}
}

/* =========================================================================================
Converting multikeys to WebCrypto
========================================================================================= */

/**
* Convert a multikey pair to Web Crypto. This function decodes the multikey data into JWK using the
* `multikeyToJWK` function, and imports the resulting keys into Web Crypto.
*
* Works for `ecdsa` (both `P-384` and `P-256`), and `eddsa`.
*
* Note that, because WebCrypto methods are asynchronous, so is this function.
*
* @param keys
* @throws - exceptions if something is incorrect in the incoming data
* @async
*/
export async function multikeyToCrypto(keys: MultikeyPair): Promise<CryptoKeyPair>;

/**
* Overloaded version of the conversion function for a single (public) key in Multikey, returning the generated Crypto Key.
* @param keys
* @throws - exceptions if something is incorrect in the incoming data
*/
export async function multikeyToCrypto(keys: Multikey): Promise<CryptoKey>;

// Implementation of the overloaded functions
export async function multikeyToCrypto(keys: MultikeyPair | Multikey): Promise<CryptoKeyPair | CryptoKey> {
const input: MultikeyPair = isMultikeyPair(keys) ? keys as MultikeyPair : { publicKeyMultibase: keys };
const jwkPair: JWKKeyPair = multikeyToJWK(input);

const algorithm: { name: string, namedCurve ?: string } = { name : "" };

// We have to establish what the algorithm type is from the public jwk
switch (jwkPair.public.kty) {
case 'EC':
algorithm.name = "ECDSA";
algorithm.namedCurve = jwkPair.public.crv;
break;
case 'OKP':
algorithm.name = "Ed25519";
break;
default:
// In fact, this does not happen; the JWK comes from our own
// generation, that raises an error earlier in this case.
// But this keeps typescript happy...
throw new Error("Unknown kty value for the JWK key");
}

const output: CryptoKeyPair = {
publicKey : await crypto.subtle.importKey("jwk", jwkPair.public, algorithm, true, ["verify"]),
privateKey : undefined,
}
if (jwkPair.secret != undefined) {
output.privateKey = await crypto.subtle.importKey("jwk", jwkPair.secret, algorithm, true, ["sign"])
}

// Got the return, the type depends on the overloaded input type
if (isMultikeyPair(keys)) {
return output;
} else {
return output.publicKey;
}
}

/* =========================================================================================
Converting JWK to multikeys
========================================================================================= */

/**
* Convert a JWK Key pair to Multikeys. This function decodes the JWK keys, finds out which binary key it encodes
* and, converts the key to the multikey versions depending on the exact curve.
Expand All @@ -51,13 +130,19 @@ export function multikeyToJWK(keys: MultikeyPair | Multikey): JWKKeyPair | JsonW
* @throws - exceptions if something is incorrect in the incoming data
*/
export function JWKToMultikey(keys: JWKKeyPair): MultikeyPair;

/**
* Variant of the conversion function for a single (public) key in JWK, returning the generated Multikey.
* Overloaded version of the conversion function for a single (public) key in JWK, returning the generated Multikey.
* @param keys
* @throws - exceptions if something is incorrect in the incoming data
*/
export function JWKToMultikey(keys: JsonWebKey): Multikey;

// Implementation of the overloaded functions
export function JWKToMultikey(keys: JWKKeyPair | JsonWebKey): MultikeyPair | Multikey {
function isJWKKeyPair(obj: any): obj is JWKKeyPair {
return (obj as JWKKeyPair).public !== undefined;
}
const input: JWKKeyPair = isJWKKeyPair(keys) ? keys : {public: keys};
const m_keys = convert.JWKToMultikey(input);
if (isJWKKeyPair(keys)) {
Expand All @@ -66,3 +151,57 @@ export function JWKToMultikey(keys: JWKKeyPair | JsonWebKey): MultikeyPair | Mul
return m_keys.publicKeyMultibase;
}
}

/* =========================================================================================
Converting WebCrypto to multikeys
========================================================================================= */

/**
* Convert a Crypto Key pair to Multikeys. This function exports the Cryptokeys into a JWK Key pair,
* and uses the `JWKToMultikey` function.
*
* Works for `ecdsa` (both `P-384` and `P-256`), and `eddsa`.
*
* Note that, because WebCrypto methods are asynchronous, so is this function.
*
* @param keys
* @throws - exceptions if something is incorrect in the incoming data
* @async
*/
export async function cryptoToMultikey(keys: CryptoKeyPair): Promise<MultikeyPair>;

/**
* Overloaded version of the conversion function for a single (public) key in JWK, returning the generated Multikey.
* @param keys
* @throws - exceptions if something is incorrect in the incoming data
*/
export async function cryptoToMultikey(keys: CryptoKey): Promise<Multikey>;

// Implementation of the overloaded functions
export async function cryptoToMultikey(keys: CryptoKeyPair | CryptoKey): Promise<MultikeyPair | Multikey> {
function isCryptoKeyPair(obj: any): obj is CryptoKeyPair {
return (obj as CryptoKeyPair).publicKey !== undefined;
}
const isPair = isCryptoKeyPair(keys);

const input: CryptoKeyPair = isPair ? keys : { publicKey: keys, privateKey: undefined };

// Generate the JWK version of the cryptokeys:
const jwkKeyPair: JWKKeyPair = {
public: await crypto.subtle.exportKey("jwk", input.publicKey),
}
if (isPair && input.privateKey !== undefined) {
jwkKeyPair.secret = await crypto.subtle.exportKey("jwk", input.privateKey);
}

// Ready for conversion
const output: MultikeyPair = JWKToMultikey(jwkKeyPair);

// Return the right version
if (isPair) {
return output;
} else {
return output.publicKeyMultibase;
}
}

24 changes: 0 additions & 24 deletions lib/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ export interface JWKKeyPair {
secret?: JsonWebKey;
}

/**
* Typeguard for JWK Key Pair.
* It is not really elaborate, it only tries to differentiate between a JWK Single Key and a Key Pair.
*
* @param obj
* @returns is it a JWKKeyPair?
*/
// deno-lint-ignore no-explicit-any
export function isJWKKeyPair(obj: any): obj is JWKKeyPair {
return (obj as JWKKeyPair).public !== undefined;
}

/**
* Type for a Multikey
*
Expand All @@ -40,18 +28,6 @@ export interface MultikeyPair {
secretKeyMultibase?: Multikey;
}

/**
* Typeguard for a Multikey Pair.
* It is not really elaborate, it only tries to differentiate between a single Multikey and a Key Pair.
*
* @param obj
* @returns is it a MultikeyPair?
*/
// deno-lint-ignore no-explicit-any
export function isMultikeyPair(obj: any): obj is MultikeyPair {
return (obj as MultikeyPair).publicKeyMultibase !== undefined;
}

/**
* Same as the Multikey Pair, but decoded and without the preambles. Just the bare key values.
*/
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"scripts": {
"dist": "tsc -d",
"docs": "./node_modules/.bin/typedoc index.ts lib/*",
"test": "./node_modules/.bin/ts-node tests/roundtrip.ts"
"test_jwk": "./node_modules/.bin/ts-node tests/roundtrip_jwk.ts",
"test_cry": "./node_modules/.bin/ts-node tests/roundtrip_cry.ts"
},
"repository": {
"type": "git",
Expand Down
60 changes: 60 additions & 0 deletions tests/roundtrip_cry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
JWKKeyPair, MultikeyPair,
cryptoToMultikey, multikeyToCrypto
} from "../index";

/** ----------------------------- */

/************* Debugging help ***********/
// deno-lint-ignore no-explicit-any
export function str(inp: any): void {
console.log(JSON.stringify(inp, null, 4));
}

/**
* Convert a CryptoKey Pair into a JWK Pair. Not really used by these tools, but handy to have it to help debugging.
* @param newPair
* @returns
*/

async function toJWK(newPair: CryptoKeyPair): Promise<JWKKeyPair> {
const publicKey: JsonWebKey = await crypto.subtle.exportKey("jwk", newPair.publicKey);
const privateKey: JsonWebKey = await crypto.subtle.exportKey("jwk", newPair.privateKey);
return { public: publicKey, secret: privateKey };
}

async function main(): Promise<void> {
const onePair = async (label: string, pair: CryptoKeyPair): Promise<void> => {
// Do a round-trip
const mk: MultikeyPair = await cryptoToMultikey(pair);
const newPair: CryptoKeyPair = await multikeyToCrypto(mk);

// For debugging, both keypairs are converted into JWK
const keyPair = await toJWK(pair);
const mkPair = await toJWK(newPair);

console.log(`----\n${label}:`);
console.log(`Original key in JWK:`)
str(keyPair);
console.log(`Generated key in JWK:`)
str(mkPair);

if (label === "EDDSA") {
console.log(`Values are equal? ${keyPair.secret?.x === mkPair.secret?.x && keyPair?.secret?.d === keyPair?.secret?.d}`)
} else {
console.log(`Values are equal? ${keyPair.secret?.x === mkPair.secret?.x && keyPair.secret?.y === mkPair.secret?.y && keyPair?.secret?.d === keyPair?.secret?.d}`)
}
}

const eddsaPair: CryptoKeyPair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]) as CryptoKeyPair;
await onePair("EDDSA", eddsaPair);

const p256: CryptoKeyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]) as CryptoKeyPair;
await onePair("ECDSA P-256", p256);

const p384: CryptoKeyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-384" }, true, ["sign", "verify"]) as CryptoKeyPair;
await onePair("ECDSA P-384", p384);
}

main()

2 changes: 2 additions & 0 deletions tests/roundtrip.ts → tests/roundtrip_jwk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ async function main(): Promise<void> {
const mk: MultikeyPair = JWKToMultikey(keyPair);
const mkPair: JWKKeyPair = multikeyToJWK(mk);
console.log(`----\n${label}:`);
console.log("Original:")
str(keyPair);
console.log("Generated:")
str(mkPair);

if (label === "EDDSA") {
Expand Down

0 comments on commit 8ba9763

Please sign in to comment.