Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an exportable option to default-plugins #109

Merged
merged 11 commits into from
Mar 15, 2024
4 changes: 4 additions & 0 deletions packages/default-plugins/src/ed25519/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EdKeypair> {
const { exportable = false } = params || {}
return EdKeypair.fromSecretKey(secretKey, { exportable })
}
}


Expand Down
6 changes: 6 additions & 0 deletions packages/default-plugins/src/p256/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export const importKeypairJwk = async (
return { privateKey, publicKey }
}

export const exportPrivateKeyJwk = async (
keyPair: AvailableCryptoKeyPair
): Promise<JsonWebKey> => {
return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey)
}

export const exportKey = async (key: CryptoKey): Promise<Uint8Array> => {
const buf = await webcrypto.subtle.exportKey("raw", key)
return new Uint8Array(buf)
Expand Down
19 changes: 13 additions & 6 deletions packages/default-plugins/src/p256/keypair.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -70,11 +70,18 @@ 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<EcdsaKeypair> {
return EcdsaKeypair.importFromJwk(jwk, { exportable: true })
}
}

Expand Down
66 changes: 50 additions & 16 deletions packages/default-plugins/src/rsa/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ 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
export const DEFAULT_HASH_ALG = "SHA-256"
export const SALT_LEGNTH = 128


export const generateKeypair = async (size: number = DEFAULT_KEY_SIZE): Promise<CryptoKeyPair> => {
export const generateKeypair = async (size: number = DEFAULT_KEY_SIZE, exportable: boolean = false): Promise<CryptoKeyPair> => {
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"]
)
}

Expand All @@ -27,14 +28,47 @@ export const exportKey = async (key: CryptoKey): Promise<Uint8Array> => {
return new Uint8Array(buf)
}

export const exportPrivateKeyJwk = async (keyPair: AvailableCryptoKeyPair): Promise<JsonWebKey> => {
return await webcrypto.subtle.exportKey("jwk", keyPair.privateKey)
}

export const importKey = async (key: Uint8Array): Promise<CryptoKey> => {
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<AvailableCryptoKeyPair> => {
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<Uint8Array> => {
Expand Down Expand Up @@ -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([
Expand All @@ -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.`)
}
Expand All @@ -145,7 +179,7 @@ export function asn1DERLengthEncode(length: number): Uint8Array {
}

if (length <= 127) {
return new Uint8Array([ length ])
return new Uint8Array([length])
}

const octets: number[] = []
Expand All @@ -154,23 +188,23 @@ 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}`)
}

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 }
}
Expand Down
24 changes: 20 additions & 4 deletions packages/default-plugins/src/rsa/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -25,7 +26,7 @@ export class RsaKeypair implements DidableKey, ExportableKey {
exportable?: boolean
}): Promise<RsaKeypair> {
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`)
}
Expand All @@ -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<RsaKeypair> {
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<RsaKeypair> {
return RsaKeypair.importFromJwk(jwk, { exportable: true })
}
}

export default RsaKeypair
47 changes: 44 additions & 3 deletions packages/default-plugins/tests/ecdsa.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { p256Plugin } from "../src/p256/plugin.js"
import EcdsaKeypair from "../src/p256/keypair.js"


describe("ecdsa", () => {

let keypair: EcdsaKeypair
Expand Down Expand Up @@ -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)
matheus23 marked this conversation as resolved.
Show resolved Hide resolved
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)
})
})
38 changes: 37 additions & 1 deletion packages/default-plugins/tests/ed25519.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {

Expand All @@ -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)
})
})
Loading
Loading