From 56cfaf82ed26413c8218eb58cadf07efe4a8a16f Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Sun, 3 Mar 2024 16:21:14 -0800 Subject: [PATCH 01/11] Add an `exportable` option to EcdsaKeyPair Addresses issue #108 --- packages/default-plugins/src/p256/crypto.ts | 18 ++++++++++-------- packages/default-plugins/src/p256/keypair.ts | 12 ++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/default-plugins/src/p256/crypto.ts b/packages/default-plugins/src/p256/crypto.ts index ef8dcff..cfec24d 100644 --- a/packages/default-plugins/src/p256/crypto.ts +++ b/packages/default-plugins/src/p256/crypto.ts @@ -9,14 +9,16 @@ export const ALG = "ECDSA" export const DEFAULT_CURVE = "P-256" export const DEFAULT_HASH_ALG = "SHA-256" -export const generateKeypair = async (): Promise => { +export const generateKeypair = async ( + exportable = false +): Promise => { return await webcrypto.subtle.generateKey( { name: ALG, namedCurve: DEFAULT_CURVE, }, - false, - [ "sign", "verify" ] + exportable, + ["sign", "verify"] ) } @@ -32,10 +34,10 @@ export const importKeypairJwk = async ( namedCurve: DEFAULT_CURVE, }, exportable, - ["sign" ] + ["sign"] ) - const { kty, crv, x, y} = privKeyJwk - const pubKeyJwk = { kty, crv, x, y} + const { kty, crv, x, y } = privKeyJwk + const pubKeyJwk = { kty, crv, x, y } const publicKey = await webcrypto.subtle.importKey( "jwk", pubKeyJwk, @@ -44,7 +46,7 @@ export const importKeypairJwk = async ( namedCurve: DEFAULT_CURVE, }, true, - [ "verify" ] + ["verify"] ) return { privateKey, publicKey } } @@ -62,7 +64,7 @@ export const importKey = async ( key, { name: ALG, namedCurve: DEFAULT_CURVE }, true, - [ "verify" ] + ["verify"] ) } diff --git a/packages/default-plugins/src/p256/keypair.ts b/packages/default-plugins/src/p256/keypair.ts index 76b8e2f..b3bbdee 100644 --- a/packages/default-plugins/src/p256/keypair.ts +++ b/packages/default-plugins/src/p256/keypair.ts @@ -32,7 +32,7 @@ export class EcdsaKeypair implements DidableKey, ExportableKey { exportable?: boolean }): Promise { const { exportable = false } = params || {} - const keypair = await crypto.generateKeypair() + const keypair = await crypto.generateKeypair(exportable) if (!isAvailableCryptoKeyPair(keypair)) { throw new Error(`Couldn't generate valid keypair`) @@ -47,12 +47,12 @@ export class EcdsaKeypair implements DidableKey, ExportableKey { params?: { exportable?: boolean }): Promise { - const { exportable = false } = params || {} - const keypair = await crypto.importKeypairJwk(jwk, exportable) + const { exportable = false } = params || {} + const keypair = await crypto.importKeypairJwk(jwk, exportable) - if (!isAvailableCryptoKeyPair(keypair)) { - throw new Error(`Couldn't generate valid keypair`) - } + if (!isAvailableCryptoKeyPair(keypair)) { + throw new Error(`Couldn't generate valid keypair`) + } const publicKey = await crypto.exportKey(keypair.publicKey) return new EcdsaKeypair(keypair, publicKey, exportable) From 05b636adaeb234b16bf8363f2cc2211d2689da92 Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Wed, 6 Mar 2024 13:02:27 -0800 Subject: [PATCH 02/11] Implement and test import / export for ECDSA Keypair --- packages/default-plugins/src/p256/crypto.ts | 6 +++ packages/default-plugins/src/p256/keypair.ts | 21 ++++++--- packages/default-plugins/tests/ecdsa.test.ts | 45 +++++++++++++++++++- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/packages/default-plugins/src/p256/crypto.ts b/packages/default-plugins/src/p256/crypto.ts index cfec24d..8b53b66 100644 --- a/packages/default-plugins/src/p256/crypto.ts +++ b/packages/default-plugins/src/p256/crypto.ts @@ -51,6 +51,12 @@ export const importKeypairJwk = async ( return { privateKey, publicKey } } +export const exportPrivateKeyJwk = async ( + keyPair: AvailableCryptoKeyPair +): Promise => { + return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey) +} + export const exportKey = async (key: CryptoKey): Promise => { const buf = await webcrypto.subtle.exportKey("raw", key) return new Uint8Array(buf) diff --git a/packages/default-plugins/src/p256/keypair.ts b/packages/default-plugins/src/p256/keypair.ts index b3bbdee..03efb40 100644 --- a/packages/default-plugins/src/p256/keypair.ts +++ b/packages/default-plugins/src/p256/keypair.ts @@ -1,6 +1,6 @@ import { webcrypto } from "one-webcrypto" import * as uint8arrays from "uint8arrays" -import { DidableKey, Encodings, ExportableKey } from "@ucans/core" +import { DidableKey, Encodings, ExportableKey, ImportableKey } from "@ucans/core" import * as crypto from "./crypto.js" import { @@ -70,11 +70,20 @@ export class EcdsaKeypair implements DidableKey, ExportableKey { if (!this.exportable) { throw new Error("Key is not exportable") } - const arrayBuffer = await webcrypto.subtle.exportKey( - "pkcs8", - this.keypair.privateKey - ) - return uint8arrays.toString(new Uint8Array(arrayBuffer), format) + return JSON.stringify(await crypto.exportPrivateKeyJwk(this.keypair)) + } + + /** + * Convenience function on the Keypair class to allow for keys to be exported / persisted. + * This is most useful for situations where you want to have consistent keys between restarts. + * A Developer can export a key, save it in a vault, and rehydrate it for use in a later run. + * @param jwk + * @returns + */ + static async import(jwk: PrivateKeyJwk): Promise { + const keypair = await crypto.importKeypairJwk(jwk, true) + const publickey = await crypto.exportKey(keypair.publicKey) + return new EcdsaKeypair(keypair, publickey, true) } } diff --git a/packages/default-plugins/tests/ecdsa.test.ts b/packages/default-plugins/tests/ecdsa.test.ts index 214c4c3..59cdf41 100644 --- a/packages/default-plugins/tests/ecdsa.test.ts +++ b/packages/default-plugins/tests/ecdsa.test.ts @@ -1,7 +1,6 @@ import { p256Plugin } from "../src/p256/plugin.js" import EcdsaKeypair from "../src/p256/keypair.js" - describe("ecdsa", () => { let keypair: EcdsaKeypair @@ -54,10 +53,52 @@ const testVectors = [ describe("ecdsa did:key", () => { it("derives the correct DID from the JWK", async () => { - for(const vector of testVectors) { + for (const vector of testVectors) { const keypair = await EcdsaKeypair.importFromJwk(vector.jwk) const did = keypair.did() expect(did).toEqual(vector.id) } }) +}) + +describe("import and exporting a key", () => { + let exportableKeypair: EcdsaKeypair; + let nonExportableKeypair: EcdsaKeypair; + + beforeAll(async () => { + exportableKeypair = await EcdsaKeypair.create({ exportable: true }) + nonExportableKeypair = await EcdsaKeypair.create({ exportable: false }) + }) + + it("can export a key using jwk", async () => { + const exported = await exportableKeypair.export() + expect(exported.length).toBeGreaterThan(0) + }) + + it("won't export a non exportable keypar", async () => { + await expect(nonExportableKeypair.export()) + .rejects + .toThrow('Key is not exportable') + }) + + it('Can export a key and re-import from it', async () => { + const exported = await exportableKeypair.export() + + const jwk = JSON.parse(exported) + const newKey = await EcdsaKeypair.import(jwk) + + const input = new Uint8Array(Buffer.from("test", "utf-8")); + const msg = new Uint8Array(Buffer.from("test message", "utf-8")) + + + // Expect the public keys to match + expect(exportableKeypair.did()).toEqual(newKey.did()) + + // Verify old and new keys are compatible + let signedMessage = await exportableKeypair.sign(msg) + expect(await p256Plugin.verifySignature(newKey.did(), msg, signedMessage)).toBe(true) + + signedMessage = await newKey.sign(msg) + expect(await p256Plugin.verifySignature(exportableKeypair.did(), msg, signedMessage)).toBe(true) + }) }) \ No newline at end of file From a49d5c9466d269d88bd8ea1824099f008806ebfc Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Wed, 6 Mar 2024 16:27:17 -0800 Subject: [PATCH 03/11] Import and Export for RSA keys --- packages/default-plugins/src/p256/keypair.ts | 4 +- packages/default-plugins/src/rsa/crypto.ts | 66 +++++++++++++++----- packages/default-plugins/src/rsa/keypair.ts | 24 +++++-- packages/default-plugins/tests/rsa.test.ts | 39 +++++++++++- 4 files changed, 109 insertions(+), 24 deletions(-) diff --git a/packages/default-plugins/src/p256/keypair.ts b/packages/default-plugins/src/p256/keypair.ts index 03efb40..90d4be1 100644 --- a/packages/default-plugins/src/p256/keypair.ts +++ b/packages/default-plugins/src/p256/keypair.ts @@ -81,9 +81,7 @@ export class EcdsaKeypair implements DidableKey, ExportableKey { * @returns */ static async import(jwk: PrivateKeyJwk): Promise { - const keypair = await crypto.importKeypairJwk(jwk, true) - const publickey = await crypto.exportKey(keypair.publicKey) - return new EcdsaKeypair(keypair, publickey, true) + return EcdsaKeypair.importFromJwk(jwk, { exportable: true }) } } diff --git a/packages/default-plugins/src/rsa/crypto.ts b/packages/default-plugins/src/rsa/crypto.ts index 339d38b..9851f6c 100644 --- a/packages/default-plugins/src/rsa/crypto.ts +++ b/packages/default-plugins/src/rsa/crypto.ts @@ -2,6 +2,7 @@ import { webcrypto } from "one-webcrypto" import * as uint8arrays from "uint8arrays" import { RSA_DID_PREFIX, RSA_DID_PREFIX_OLD } from "../prefixes.js" import { didFromKeyBytes, keyBytesFromDid } from "../util.js" +import { AvailableCryptoKeyPair, PrivateKeyJwk } from "../types.js" export const RSA_ALG = "RSASSA-PKCS1-v1_5" export const DEFAULT_KEY_SIZE = 2048 @@ -9,16 +10,16 @@ export const DEFAULT_HASH_ALG = "SHA-256" export const SALT_LEGNTH = 128 -export const generateKeypair = async (size: number = DEFAULT_KEY_SIZE): Promise => { +export const generateKeypair = async (size: number = DEFAULT_KEY_SIZE, exportable: boolean = false): Promise => { return await webcrypto.subtle.generateKey( { name: RSA_ALG, modulusLength: size, - publicExponent: new Uint8Array([ 0x01, 0x00, 0x01 ]), + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: { name: DEFAULT_HASH_ALG } }, - false, - [ "sign", "verify" ] + exportable, + ["sign", "verify"] ) } @@ -27,14 +28,47 @@ export const exportKey = async (key: CryptoKey): Promise => { return new Uint8Array(buf) } +export const exportPrivateKeyJwk = async (keyPair: AvailableCryptoKeyPair): Promise => { + return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey) +} + export const importKey = async (key: Uint8Array): Promise => { return await webcrypto.subtle.importKey( "spki", key, { name: RSA_ALG, hash: { name: DEFAULT_HASH_ALG } }, true, - [ "verify" ] + ["verify"] + ) +} + +export const importKeypairJwk = async ( + privKeyJwk: JsonWebKey, + exportable = false +): Promise => { + const privateKey = await webcrypto.subtle.importKey( + "jwk", + privKeyJwk, + { + name: RSA_ALG, + hash: { name: DEFAULT_HASH_ALG }, + }, + exportable, + ["sign"] + ) + const { kty, n, e } = privKeyJwk + const pubKeyJwk = { kty, n, e } + const publicKey = await webcrypto.subtle.importKey( + "jwk", + pubKeyJwk, + { + name: RSA_ALG, + hash: { name: DEFAULT_HASH_ALG }, + }, + true, + ["verify"] ) + return { privateKey, publicKey } } export const sign = async (msg: Uint8Array, privateKey: CryptoKey): Promise => { @@ -100,16 +134,16 @@ export const publicKeyToOldDid = (pubkey: Uint8Array): string => { * * See https://github.com/ucan-wg/ts-ucan/issues/30 */ -const SPKI_PARAMS_ENCODED = new Uint8Array([ 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0 ]) -const ASN_SEQUENCE_TAG = new Uint8Array([ 0x30 ]) -const ASN_BITSTRING_TAG = new Uint8Array([ 0x03 ]) +const SPKI_PARAMS_ENCODED = new Uint8Array([48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]) +const ASN_SEQUENCE_TAG = new Uint8Array([0x30]) +const ASN_BITSTRING_TAG = new Uint8Array([0x03]) export const convertRSAPublicKeyToSubjectPublicKeyInfo = (rsaPublicKey: Uint8Array): Uint8Array => { // More info on bitstring encoding: https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-bit-string const bitStringEncoded = uint8arrays.concat([ ASN_BITSTRING_TAG, asn1DERLengthEncode(rsaPublicKey.length + 1), - new Uint8Array([ 0x00 ]), // amount of unused bits at the end of our bitstring (counts into length?!) + new Uint8Array([0x00]), // amount of unused bits at the end of our bitstring (counts into length?!) rsaPublicKey ]) return uint8arrays.concat([ @@ -129,7 +163,7 @@ export const convertSubjectPublicKeyInfoToRSAPublicKey = (subjectPublicKeyInfo: // we expect the bitstring next const bitstringParams = asn1Into(subjectPublicKeyInfo, ASN_BITSTRING_TAG, position) const bitstring = subjectPublicKeyInfo.subarray(bitstringParams.position, bitstringParams.position + bitstringParams.length) - const unusedBitPadding = bitstring[ 0 ] + const unusedBitPadding = bitstring[0] if (unusedBitPadding !== 0) { throw new Error(`Can't convert SPKI to PKCS: Expected bitstring length to be multiple of 8, but got ${unusedBitPadding} unused bits in last byte.`) } @@ -145,7 +179,7 @@ export function asn1DERLengthEncode(length: number): Uint8Array { } if (length <= 127) { - return new Uint8Array([ length ]) + return new Uint8Array([length]) } const octets: number[] = [] @@ -154,15 +188,15 @@ export function asn1DERLengthEncode(length: number): Uint8Array { length = length >>> 8 } octets.reverse() - return new Uint8Array([ 0x80 | (octets.length & 0xFF), ...octets ]) + return new Uint8Array([0x80 | (octets.length & 0xFF), ...octets]) } function asn1DERLengthDecodeWithConsumed(bytes: Uint8Array): { number: number; consumed: number } { - if ((bytes[ 0 ] & 0x80) === 0) { - return { number: bytes[ 0 ], consumed: 1 } + if ((bytes[0] & 0x80) === 0) { + return { number: bytes[0], consumed: 1 } } - const numberBytes = bytes[ 0 ] & 0x7F + const numberBytes = bytes[0] & 0x7F if (bytes.length < numberBytes + 1) { throw new Error(`ASN parsing error: Too few bytes. Expected encoded length's length to be at least ${numberBytes}`) } @@ -170,7 +204,7 @@ function asn1DERLengthDecodeWithConsumed(bytes: Uint8Array): { number: number; c let length = 0 for (let i = 0; i < numberBytes; i++) { length = length << 8 - length = length | bytes[ i + 1 ] + length = length | bytes[i + 1] } return { number: length, consumed: numberBytes + 1 } } diff --git a/packages/default-plugins/src/rsa/keypair.ts b/packages/default-plugins/src/rsa/keypair.ts index 0c5dff1..ba4ed27 100644 --- a/packages/default-plugins/src/rsa/keypair.ts +++ b/packages/default-plugins/src/rsa/keypair.ts @@ -2,8 +2,9 @@ import { webcrypto } from "one-webcrypto" import * as uint8arrays from "uint8arrays" import * as crypto from "./crypto.js" -import { AvailableCryptoKeyPair, isAvailableCryptoKeyPair } from "../types.js" +import { AvailableCryptoKeyPair, PrivateKeyJwk, isAvailableCryptoKeyPair } from "../types.js" import { DidableKey, Encodings, ExportableKey } from "@ucans/core" +import { PrivateKeyInput } from "crypto" export class RsaKeypair implements DidableKey, ExportableKey { @@ -25,7 +26,7 @@ export class RsaKeypair implements DidableKey, ExportableKey { exportable?: boolean }): Promise { const { size = 2048, exportable = false } = params || {} - const keypair = await crypto.generateKeypair(size) + const keypair = await crypto.generateKeypair(size, exportable) if (!isAvailableCryptoKeyPair(keypair)) { throw new Error(`Couldn't generate valid keypair`) } @@ -45,10 +46,25 @@ export class RsaKeypair implements DidableKey, ExportableKey { if (!this.exportable) { throw new Error("Key is not exportable") } - const arrayBuffer = await webcrypto.subtle.exportKey("pkcs8", this.keypair.privateKey) - return uint8arrays.toString(new Uint8Array(arrayBuffer), format) + const exported = await crypto.exportPrivateKeyJwk(this.keypair) + return JSON.stringify(exported) } + static async importFromJwk(jwk: JsonWebKey, params: { exportable: true }): Promise { + const { exportable = false } = params || {} + const keypair = await crypto.importKeypairJwk(jwk, exportable) + + if (!isAvailableCryptoKeyPair(keypair)) { + throw new Error(`Couldn't generate valid keypair`) + } + + const publicKey = await crypto.exportKey(keypair.publicKey) + return new RsaKeypair(keypair, publicKey, exportable) + } + + static async import(jwk: PrivateKeyJwk): Promise { + return RsaKeypair.importFromJwk(jwk, { exportable: true }) + } } export default RsaKeypair diff --git a/packages/default-plugins/tests/rsa.test.ts b/packages/default-plugins/tests/rsa.test.ts index e18f279..033ba6e 100644 --- a/packages/default-plugins/tests/rsa.test.ts +++ b/packages/default-plugins/tests/rsa.test.ts @@ -2,7 +2,7 @@ import * as fc from "fast-check" import * as uint8arrays from "uint8arrays" import { rsaPlugin } from "../src/rsa/plugin.js" import * as rsaCrypto from "../src/rsa/crypto.js" -import RSAKeypair from "../src/rsa/keypair.js" +import RSAKeypair, { RsaKeypair } from "../src/rsa/keypair.js" describe("rsa", () => { @@ -97,4 +97,41 @@ describe("ASN", () => { }) }) + + describe("Import / Export", () => { + let exportableKey: RSAKeypair + let nonExportableKey: RSAKeypair + + beforeAll(async () => { + exportableKey = await RSAKeypair.create({ exportable: true }) + nonExportableKey = await RSAKeypair.create({ exportable: false }) + + }) + + it("can export a key if marked exportable", async () => { + const exported = await exportableKey.export() + expect(exported).not.toBeNull() + }) + + it("cannont export a key if not marked exportable", async () => { + await expect(nonExportableKey.export()) + .rejects + .toThrow("Key is not exportable") + }) + + it("can import an exported key", async () => { + const exported = await exportableKey.export() + const newKey = await RsaKeypair.import(JSON.parse(exported)) + + expect(newKey.did()).toEqual(exportableKey.did()) + + // Cross sign and verify + const msg = new Uint8Array(Buffer.from("test string", "utf-8")) + let signed = await exportableKey.sign(msg) + expect(await rsaPlugin.verifySignature(newKey.did(), msg, signed)).toBe(true) + + signed = await newKey.sign(msg) + expect(await rsaPlugin.verifySignature(exportableKey.did(), msg, signed)).toBe(true) + }) + }) }) From 9f447bc4280aef0d2943866333bb5eb1e563dbf5 Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Wed, 6 Mar 2024 16:42:06 -0800 Subject: [PATCH 04/11] Import / Export for EdKeypair --- .../default-plugins/src/ed25519/keypair.ts | 4 ++ packages/default-plugins/tests/ecdsa.test.ts | 2 +- .../default-plugins/tests/ed25519.test.ts | 38 ++++++++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/default-plugins/src/ed25519/keypair.ts b/packages/default-plugins/src/ed25519/keypair.ts index d217546..bfae692 100644 --- a/packages/default-plugins/src/ed25519/keypair.ts +++ b/packages/default-plugins/src/ed25519/keypair.ts @@ -52,6 +52,10 @@ export class EdKeypair implements DidableKey, ExportableKey { return uint8arrays.toString(this.secretKey, format) } + static async import(secretKey: string, params?: { exportable: boolean }): Promise { + const { exportable = false } = params || {} + return EdKeypair.fromSecretKey(secretKey, { exportable }) + } } diff --git a/packages/default-plugins/tests/ecdsa.test.ts b/packages/default-plugins/tests/ecdsa.test.ts index 59cdf41..95eee6e 100644 --- a/packages/default-plugins/tests/ecdsa.test.ts +++ b/packages/default-plugins/tests/ecdsa.test.ts @@ -101,4 +101,4 @@ describe("import and exporting a key", () => { signedMessage = await newKey.sign(msg) expect(await p256Plugin.verifySignature(exportableKeypair.did(), msg, signedMessage)).toBe(true) }) -}) \ No newline at end of file +}) diff --git a/packages/default-plugins/tests/ed25519.test.ts b/packages/default-plugins/tests/ed25519.test.ts index 72ee25c..f15d3d4 100644 --- a/packages/default-plugins/tests/ed25519.test.ts +++ b/packages/default-plugins/tests/ed25519.test.ts @@ -1,5 +1,5 @@ import { ed25519Plugin } from "../src/ed25519/plugin.js" -import EdwardsKey from "../src/ed25519/keypair.js" +import EdwardsKey, { EdKeypair } from "../src/ed25519/keypair.js" describe("ed25519", () => { @@ -25,3 +25,39 @@ describe("ed25519", () => { }) }) + +describe("Import / Export", () => { + let exportableKey: EdKeypair + let nonExportableKey: EdKeypair + + beforeAll(async () => { + exportableKey = await EdKeypair.create({ exportable: true }) + nonExportableKey = await EdKeypair.create({ exportable: false }) + }) + + it("Will export a key that is exportable", async () => { + const exported = exportableKey.export() + expect(exported).not.toBe(null) + }) + + it("Will not export a key that is not exportable", async () => { + await expect(nonExportableKey.export()) + .rejects + .toThrow("Key is not exportable") + }) + + it("Will import an exported key", async () => { + const exported = await exportableKey.export() + const newKey = await EdKeypair.import(exported) + + expect(newKey.did()).toEqual(exportableKey.did()) + + // Sign and verify + const msg = new Uint8Array(Buffer.from("test signing", "utf-8")) + let signed = await exportableKey.sign(msg) + expect(await ed25519Plugin.verifySignature(await newKey.did(), msg, signed)).toBe(true) + + signed = await newKey.sign(msg) + expect(await ed25519Plugin.verifySignature(await exportableKey.did(), msg, signed)).toBe(true) + }) +}) \ No newline at end of file From 6e55ec43ccfa06e412e54cfd5146aa94a1e507ae Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Thu, 7 Mar 2024 06:46:14 -0800 Subject: [PATCH 05/11] [WIP] Remove unit8array as a dependency --- packages/default-plugins/src/ed25519/keypair.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/default-plugins/src/ed25519/keypair.ts b/packages/default-plugins/src/ed25519/keypair.ts index bfae692..91c60d2 100644 --- a/packages/default-plugins/src/ed25519/keypair.ts +++ b/packages/default-plugins/src/ed25519/keypair.ts @@ -1,4 +1,3 @@ -import * as uint8arrays from "uint8arrays" import * as ed25519 from "@stablelib/ed25519" import * as crypto from "./crypto.js" @@ -28,11 +27,11 @@ export class EdKeypair implements DidableKey, ExportableKey { } static fromSecretKey(key: string, params?: { - format?: Encodings exportable?: boolean }): EdKeypair { - const { format = "base64pad", exportable = false } = params || {} - const secretKey = uint8arrays.fromString(key, format) + const { exportable = false } = params || {} + + const secretKey = new Uint8Array(Buffer.from(key, 'base64')) const publicKey = ed25519.extractPublicKeyFromSecretKey(secretKey) return new EdKeypair(secretKey, publicKey, exportable) } @@ -45,11 +44,12 @@ export class EdKeypair implements DidableKey, ExportableKey { return ed25519.sign(this.secretKey, msg) } - async export(format: Encodings = "base64pad"): Promise { + async export(): Promise { if (!this.exportable) { throw new Error("Key is not exportable") } - return uint8arrays.toString(this.secretKey, format) + const buf = Buffer.from(this.secretKey) + return buf.toString('base64') } static async import(secretKey: string, params?: { exportable: boolean }): Promise { From 3530226531fbabe401bbff0f67e48d582cb3e7b3 Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Thu, 7 Mar 2024 06:47:14 -0800 Subject: [PATCH 06/11] [bug] remove unused dependency --- packages/default-plugins/src/p256/keypair.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/default-plugins/src/p256/keypair.ts b/packages/default-plugins/src/p256/keypair.ts index 90d4be1..64aeeae 100644 --- a/packages/default-plugins/src/p256/keypair.ts +++ b/packages/default-plugins/src/p256/keypair.ts @@ -1,6 +1,6 @@ import { webcrypto } from "one-webcrypto" import * as uint8arrays from "uint8arrays" -import { DidableKey, Encodings, ExportableKey, ImportableKey } from "@ucans/core" +import { DidableKey, Encodings, ExportableKey } from "@ucans/core" import * as crypto from "./crypto.js" import { From 76f3b950d2b02fe1e3c47c157e8e3f1b4a0f42be Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Thu, 7 Mar 2024 17:32:59 -0800 Subject: [PATCH 07/11] [fix] Normalize back to uintarrays [Fix] Normalize on uint8arrays --- packages/default-plugins/src/ed25519/keypair.ts | 13 +++++++------ packages/default-plugins/tests/ecdsa.test.ts | 5 ++--- packages/default-plugins/tests/ed25519.test.ts | 3 ++- packages/default-plugins/tests/rsa.test.ts | 2 +- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/default-plugins/src/ed25519/keypair.ts b/packages/default-plugins/src/ed25519/keypair.ts index 91c60d2..4e8c343 100644 --- a/packages/default-plugins/src/ed25519/keypair.ts +++ b/packages/default-plugins/src/ed25519/keypair.ts @@ -1,3 +1,4 @@ +import * as uint8arrays from "uint8arrays" import * as ed25519 from "@stablelib/ed25519" import * as crypto from "./crypto.js" @@ -27,11 +28,11 @@ export class EdKeypair implements DidableKey, ExportableKey { } static fromSecretKey(key: string, params?: { + format?: Encodings, exportable?: boolean }): EdKeypair { - const { exportable = false } = params || {} - - const secretKey = new Uint8Array(Buffer.from(key, 'base64')) + const { format = "base64pad", exportable = false } = params || {} + const secretKey = uint8arrays.fromString(key, format) const publicKey = ed25519.extractPublicKeyFromSecretKey(secretKey) return new EdKeypair(secretKey, publicKey, exportable) } @@ -44,12 +45,12 @@ export class EdKeypair implements DidableKey, ExportableKey { return ed25519.sign(this.secretKey, msg) } - async export(): Promise { + async export(format: Encodings = "base64pad"): Promise { if (!this.exportable) { throw new Error("Key is not exportable") } - const buf = Buffer.from(this.secretKey) - return buf.toString('base64') + + return uint8arrays.toString(this.secretKey, format) } static async import(secretKey: string, params?: { exportable: boolean }): Promise { diff --git a/packages/default-plugins/tests/ecdsa.test.ts b/packages/default-plugins/tests/ecdsa.test.ts index 95eee6e..70d39ac 100644 --- a/packages/default-plugins/tests/ecdsa.test.ts +++ b/packages/default-plugins/tests/ecdsa.test.ts @@ -1,3 +1,4 @@ +import * as uint8arrays from 'uint8arrays' import { p256Plugin } from "../src/p256/plugin.js" import EcdsaKeypair from "../src/p256/keypair.js" @@ -87,9 +88,7 @@ describe("import and exporting a key", () => { const jwk = JSON.parse(exported) const newKey = await EcdsaKeypair.import(jwk) - const input = new Uint8Array(Buffer.from("test", "utf-8")); - const msg = new Uint8Array(Buffer.from("test message", "utf-8")) - + const msg = uint8arrays.fromString("test message", "utf-8") // Expect the public keys to match expect(exportableKeypair.did()).toEqual(newKey.did()) diff --git a/packages/default-plugins/tests/ed25519.test.ts b/packages/default-plugins/tests/ed25519.test.ts index f15d3d4..45859df 100644 --- a/packages/default-plugins/tests/ed25519.test.ts +++ b/packages/default-plugins/tests/ed25519.test.ts @@ -1,3 +1,4 @@ +import * as uint8arrays from "uint8arrays" import { ed25519Plugin } from "../src/ed25519/plugin.js" import EdwardsKey, { EdKeypair } from "../src/ed25519/keypair.js" @@ -53,7 +54,7 @@ describe("Import / Export", () => { expect(newKey.did()).toEqual(exportableKey.did()) // Sign and verify - const msg = new Uint8Array(Buffer.from("test signing", "utf-8")) + const msg = uint8arrays.fromString("test signing", "utf-8") let signed = await exportableKey.sign(msg) expect(await ed25519Plugin.verifySignature(await newKey.did(), msg, signed)).toBe(true) diff --git a/packages/default-plugins/tests/rsa.test.ts b/packages/default-plugins/tests/rsa.test.ts index 033ba6e..9d7f6ad 100644 --- a/packages/default-plugins/tests/rsa.test.ts +++ b/packages/default-plugins/tests/rsa.test.ts @@ -126,7 +126,7 @@ describe("ASN", () => { expect(newKey.did()).toEqual(exportableKey.did()) // Cross sign and verify - const msg = new Uint8Array(Buffer.from("test string", "utf-8")) + const msg = uint8arrays.fromString("test signing", "utf-8") let signed = await exportableKey.sign(msg) expect(await rsaPlugin.verifySignature(newKey.did(), msg, signed)).toBe(true) From 552581d31a9b7a1d6c045b2caec4862d963b6b80 Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Fri, 8 Mar 2024 14:39:26 -0800 Subject: [PATCH 08/11] [feat] Normalize `export` to use JWK types --- .../default-plugins/src/ed25519/keypair.ts | 21 ++++++++++++++----- packages/default-plugins/src/p256/crypto.ts | 4 ++-- packages/default-plugins/src/p256/keypair.ts | 8 +++---- packages/default-plugins/src/rsa/crypto.ts | 2 +- packages/default-plugins/src/rsa/keypair.ts | 13 ++++-------- packages/default-plugins/src/types.ts | 11 ++++++++-- packages/default-plugins/tests/ecdsa.test.ts | 15 ++++++------- packages/default-plugins/tests/rsa.test.ts | 2 +- 8 files changed, 44 insertions(+), 32 deletions(-) diff --git a/packages/default-plugins/src/ed25519/keypair.ts b/packages/default-plugins/src/ed25519/keypair.ts index 4e8c343..515b9f4 100644 --- a/packages/default-plugins/src/ed25519/keypair.ts +++ b/packages/default-plugins/src/ed25519/keypair.ts @@ -3,6 +3,7 @@ import * as ed25519 from "@stablelib/ed25519" import * as crypto from "./crypto.js" import { DidableKey, Encodings, ExportableKey } from "@ucans/core" +import { PrivateKeyJwk } from "../types.js" export class EdKeypair implements DidableKey, ExportableKey { @@ -28,7 +29,7 @@ export class EdKeypair implements DidableKey, ExportableKey { } static fromSecretKey(key: string, params?: { - format?: Encodings, + format?: Encodings exportable?: boolean }): EdKeypair { const { format = "base64pad", exportable = false } = params || {} @@ -45,17 +46,27 @@ export class EdKeypair implements DidableKey, ExportableKey { return ed25519.sign(this.secretKey, msg) } - async export(format: Encodings = "base64pad"): Promise { + async export(): Promise { if (!this.exportable) { throw new Error("Key is not exportable") } - return uint8arrays.toString(this.secretKey, format) + const jwk: PrivateKeyJwk = { + kty: "EC", + crv: "Ed25519", + d: uint8arrays.toString(this.secretKey, "base64pad"), + } + return jwk } - static async import(secretKey: string, params?: { exportable: boolean }): Promise { + static async import(jwk: PrivateKeyJwk, params?: { exportable: boolean }): Promise { const { exportable = false } = params || {} - return EdKeypair.fromSecretKey(secretKey, { exportable }) + + if (jwk.kty !== "EC" || jwk.crv !== "Ed25519") { + throw new Error("Cannot import key of type: ${jwk.kty} curve: ${jwk.crv} into ED25519 key") + } + + return EdKeypair.fromSecretKey(jwk.d, { exportable }) } } diff --git a/packages/default-plugins/src/p256/crypto.ts b/packages/default-plugins/src/p256/crypto.ts index 8b53b66..81e035e 100644 --- a/packages/default-plugins/src/p256/crypto.ts +++ b/packages/default-plugins/src/p256/crypto.ts @@ -53,8 +53,8 @@ export const importKeypairJwk = async ( export const exportPrivateKeyJwk = async ( keyPair: AvailableCryptoKeyPair -): Promise => { - return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey) +): Promise => { + return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey) as PrivateKeyJwk } export const exportKey = async (key: CryptoKey): Promise => { diff --git a/packages/default-plugins/src/p256/keypair.ts b/packages/default-plugins/src/p256/keypair.ts index 64aeeae..46d931a 100644 --- a/packages/default-plugins/src/p256/keypair.ts +++ b/packages/default-plugins/src/p256/keypair.ts @@ -1,6 +1,4 @@ -import { webcrypto } from "one-webcrypto" -import * as uint8arrays from "uint8arrays" -import { DidableKey, Encodings, ExportableKey } from "@ucans/core" +import { DidableKey, ExportableKey } from "@ucans/core" import * as crypto from "./crypto.js" import { @@ -66,11 +64,11 @@ export class EcdsaKeypair implements DidableKey, ExportableKey { return await crypto.sign(msg, this.keypair.privateKey) } - async export(format: Encodings = "base64pad"): Promise { + async export(): Promise { if (!this.exportable) { throw new Error("Key is not exportable") } - return JSON.stringify(await crypto.exportPrivateKeyJwk(this.keypair)) + return await crypto.exportPrivateKeyJwk(this.keypair) } /** diff --git a/packages/default-plugins/src/rsa/crypto.ts b/packages/default-plugins/src/rsa/crypto.ts index 9851f6c..da120a2 100644 --- a/packages/default-plugins/src/rsa/crypto.ts +++ b/packages/default-plugins/src/rsa/crypto.ts @@ -43,7 +43,7 @@ export const importKey = async (key: Uint8Array): Promise => { } export const importKeypairJwk = async ( - privKeyJwk: JsonWebKey, + privKeyJwk: PrivateKeyJwk, exportable = false ): Promise => { const privateKey = await webcrypto.subtle.importKey( diff --git a/packages/default-plugins/src/rsa/keypair.ts b/packages/default-plugins/src/rsa/keypair.ts index ba4ed27..8e445d2 100644 --- a/packages/default-plugins/src/rsa/keypair.ts +++ b/packages/default-plugins/src/rsa/keypair.ts @@ -1,10 +1,6 @@ -import { webcrypto } from "one-webcrypto" -import * as uint8arrays from "uint8arrays" - import * as crypto from "./crypto.js" import { AvailableCryptoKeyPair, PrivateKeyJwk, isAvailableCryptoKeyPair } from "../types.js" -import { DidableKey, Encodings, ExportableKey } from "@ucans/core" -import { PrivateKeyInput } from "crypto" +import { DidableKey, ExportableKey } from "@ucans/core" export class RsaKeypair implements DidableKey, ExportableKey { @@ -42,15 +38,14 @@ export class RsaKeypair implements DidableKey, ExportableKey { return await crypto.sign(msg, this.keypair.privateKey) } - async export(format: Encodings = "base64pad"): Promise { + async export(): Promise { if (!this.exportable) { throw new Error("Key is not exportable") } - const exported = await crypto.exportPrivateKeyJwk(this.keypair) - return JSON.stringify(exported) + return await crypto.exportPrivateKeyJwk(this.keypair) as PrivateKeyJwk } - static async importFromJwk(jwk: JsonWebKey, params: { exportable: true }): Promise { + static async importFromJwk(jwk: PrivateKeyJwk, params: { exportable: true }): Promise { const { exportable = false } = params || {} const keypair = await crypto.importKeypairJwk(jwk, exportable) diff --git a/packages/default-plugins/src/types.ts b/packages/default-plugins/src/types.ts index 23489be..aeca589 100644 --- a/packages/default-plugins/src/types.ts +++ b/packages/default-plugins/src/types.ts @@ -6,8 +6,15 @@ export interface AvailableCryptoKeyPair { export type PublicKeyJwk = { kty: string crv: string - x: string - y: string + + // For P256 curves + x?: string + y?: string + + // For RSA curves + n?: string + e?: string + } export type PrivateKeyJwk = PublicKeyJwk & { d: string } diff --git a/packages/default-plugins/tests/ecdsa.test.ts b/packages/default-plugins/tests/ecdsa.test.ts index 70d39ac..d45b9aa 100644 --- a/packages/default-plugins/tests/ecdsa.test.ts +++ b/packages/default-plugins/tests/ecdsa.test.ts @@ -1,4 +1,4 @@ -import * as uint8arrays from 'uint8arrays' +import * as uint8arrays from "uint8arrays" import { p256Plugin } from "../src/p256/plugin.js" import EcdsaKeypair from "../src/p256/keypair.js" @@ -63,8 +63,8 @@ describe("ecdsa did:key", () => { }) describe("import and exporting a key", () => { - let exportableKeypair: EcdsaKeypair; - let nonExportableKeypair: EcdsaKeypair; + let exportableKeypair: EcdsaKeypair + let nonExportableKeypair: EcdsaKeypair beforeAll(async () => { exportableKeypair = await EcdsaKeypair.create({ exportable: true }) @@ -73,19 +73,20 @@ describe("import and exporting a key", () => { it("can export a key using jwk", async () => { const exported = await exportableKeypair.export() - expect(exported.length).toBeGreaterThan(0) + expect(exported.kty).toBe("EC") + expect(exported.crv).toBe("P-256") }) it("won't export a non exportable keypar", async () => { await expect(nonExportableKeypair.export()) .rejects - .toThrow('Key is not exportable') + .toThrow("Key is not exportable") }) - it('Can export a key and re-import from it', async () => { + it("Can export a key and re-import from it", async () => { const exported = await exportableKeypair.export() - const jwk = JSON.parse(exported) + const jwk = exported const newKey = await EcdsaKeypair.import(jwk) const msg = uint8arrays.fromString("test message", "utf-8") diff --git a/packages/default-plugins/tests/rsa.test.ts b/packages/default-plugins/tests/rsa.test.ts index 9d7f6ad..5b2a00d 100644 --- a/packages/default-plugins/tests/rsa.test.ts +++ b/packages/default-plugins/tests/rsa.test.ts @@ -121,7 +121,7 @@ describe("ASN", () => { it("can import an exported key", async () => { const exported = await exportableKey.export() - const newKey = await RsaKeypair.import(JSON.parse(exported)) + const newKey = await RsaKeypair.import(exported) expect(newKey.did()).toEqual(exportableKey.did()) From 3bda0d63422341226c8d9a65b3ae44edab3e3568 Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Fri, 15 Mar 2024 07:56:15 -0700 Subject: [PATCH 09/11] [fix] added notes about ed25519 export --- packages/default-plugins/src/ed25519/keypair.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/default-plugins/src/ed25519/keypair.ts b/packages/default-plugins/src/ed25519/keypair.ts index 515b9f4..1129aff 100644 --- a/packages/default-plugins/src/ed25519/keypair.ts +++ b/packages/default-plugins/src/ed25519/keypair.ts @@ -51,6 +51,18 @@ export class EdKeypair implements DidableKey, ExportableKey { throw new Error("Key is not exportable") } + /* + * EdDSA is relatively new and not supported everywhere. There's no good documentation + * within the JWK spec or parameter export to be able to reconstruct the key via parameters + * Example, there's no good documentation on parameterizing like other curves: (x, y, n, e) + * + * In an effort to remain compatible with other tooling in the space, the following article + * describes a way of encoding JWK that is at least consistent with other tooling. As our current + * libraries are only able to reconstruct a key via importing a secret key, encoding the secret + * as the `d` parameter seems to make sense and have some compatibility with other tools. + * + * [Link](https://gist.github.com/kousu/f3174af57e1fc42a0a88586b5a5ffdc9) + */ const jwk: PrivateKeyJwk = { kty: "EC", crv: "Ed25519", From 2c5daca0f5957e6d075180f3f30d92f0ff2f0b50 Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Fri, 15 Mar 2024 07:58:47 -0700 Subject: [PATCH 10/11] [fix] Revised comment about export to include other parameters --- packages/default-plugins/src/ed25519/keypair.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/default-plugins/src/ed25519/keypair.ts b/packages/default-plugins/src/ed25519/keypair.ts index 1129aff..84a229c 100644 --- a/packages/default-plugins/src/ed25519/keypair.ts +++ b/packages/default-plugins/src/ed25519/keypair.ts @@ -62,6 +62,10 @@ export class EdKeypair implements DidableKey, ExportableKey { * as the `d` parameter seems to make sense and have some compatibility with other tools. * * [Link](https://gist.github.com/kousu/f3174af57e1fc42a0a88586b5a5ffdc9) + * + * While `kty` and `crv` are not absolutely required for this to work within the library, + * including them is an attempt to be closer to the [JWK Spec](https://datatracker.ietf.org/doc/html/rfc7517) + * since we are hand rolling this export. */ const jwk: PrivateKeyJwk = { kty: "EC", From 2e7a5cc0983b08038dc954d73a1fef3a0f6c350f Mon Sep 17 00:00:00 2001 From: Kris Shinn Date: Fri, 15 Mar 2024 08:03:42 -0700 Subject: [PATCH 11/11] [fix] Generify ExportableKey type to allow for PrivateKeyJwk return types --- packages/core/src/types.ts | 16 ++++++++-------- packages/default-plugins/src/ed25519/keypair.ts | 2 +- packages/default-plugins/src/p256/keypair.ts | 2 +- packages/default-plugins/src/rsa/keypair.ts | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 63044a5..54f7cf4 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -59,8 +59,8 @@ export interface Didable { did: () => string } -export interface ExportableKey { - export: (format?: Encodings) => Promise +export interface ExportableKey { + export: () => Promise } export interface Keypair { @@ -68,7 +68,7 @@ export interface Keypair { sign: (msg: Uint8Array) => Promise } -export interface DidableKey extends Didable, Keypair {} +export interface DidableKey extends Didable, Keypair { } // MISC @@ -80,21 +80,21 @@ export type Encodings = SupportedEncodings export interface IndexByAudience { - [ audienceDID: string ]: Array<{ + [audienceDID: string]: Array<{ processedUcan: Ucan capabilities: DelegationChain[] }> } export interface StoreI { - add(ucan: Ucan): Promise - getByAudience(audience: string): Ucan[] - findByAudience(audience: string, predicate: (ucan: Ucan) => boolean): Ucan | null + add(ucan: Ucan): Promise + getByAudience(audience: string): Ucan[] + findByAudience(audience: string, predicate: (ucan: Ucan) => boolean): Ucan | null findWithCapability( audience: string, requiredCapability: Capability, requiredIssuer: string, - ): Iterable + ): Iterable } // BUILDER diff --git a/packages/default-plugins/src/ed25519/keypair.ts b/packages/default-plugins/src/ed25519/keypair.ts index 84a229c..ae8810d 100644 --- a/packages/default-plugins/src/ed25519/keypair.ts +++ b/packages/default-plugins/src/ed25519/keypair.ts @@ -6,7 +6,7 @@ import { DidableKey, Encodings, ExportableKey } from "@ucans/core" import { PrivateKeyJwk } from "../types.js" -export class EdKeypair implements DidableKey, ExportableKey { +export class EdKeypair implements DidableKey, ExportableKey { public jwtAlg = "EdDSA" diff --git a/packages/default-plugins/src/p256/keypair.ts b/packages/default-plugins/src/p256/keypair.ts index 46d931a..f612dd7 100644 --- a/packages/default-plugins/src/p256/keypair.ts +++ b/packages/default-plugins/src/p256/keypair.ts @@ -8,7 +8,7 @@ import { } from "../types.js" -export class EcdsaKeypair implements DidableKey, ExportableKey { +export class EcdsaKeypair implements DidableKey, ExportableKey { public jwtAlg = "ES256" diff --git a/packages/default-plugins/src/rsa/keypair.ts b/packages/default-plugins/src/rsa/keypair.ts index 8e445d2..a75396a 100644 --- a/packages/default-plugins/src/rsa/keypair.ts +++ b/packages/default-plugins/src/rsa/keypair.ts @@ -3,7 +3,7 @@ import { AvailableCryptoKeyPair, PrivateKeyJwk, isAvailableCryptoKeyPair } from import { DidableKey, ExportableKey } from "@ucans/core" -export class RsaKeypair implements DidableKey, ExportableKey { +export class RsaKeypair implements DidableKey, ExportableKey{ public jwtAlg = "RS256"