From 9b21d8d04193454aefeff65988c9e10e6801ab01 Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Mon, 1 Apr 2024 15:23:56 -0400 Subject: [PATCH 1/8] remove commented out verification code. move into separate function --- export/index.html | 40 ++++++++++++++++++++++++++++++++++++-- import/index.html | 44 ++++++++++++++++++++++++++++++------------ import/standalone.html | 44 ++++++++++++++++++++++++++++++------------ 3 files changed, 102 insertions(+), 26 deletions(-) diff --git a/export/index.html b/export/index.html index b87bc78..d106c10 100644 --- a/export/index.html +++ b/export/index.html @@ -121,6 +121,8 @@

Message log

const TURNKEY_EMBEDDED_KEY = "TURNKEY_EMBEDDED_KEY" /** 48 hours in milliseconds */ const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 48; + /** Turnkey Signer enclave's public key */ + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /** * Creates a new public/private key pair and persists it in localStorage @@ -240,6 +242,26 @@

Message log

return new Uint8Array([...s, ...r]); } + /** + * Function to verify enclave signature on import bundle received from the server. + */ + const verifyEnclaveSignature = async (enclaveQuorumPublic, publicKey, publicSignature) => { + // Second half is the public key for the enclave quorum encryption key + const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); + + // todo(olivia): throw error if enclave quorum public is null once server changes are deployed + if (enclaveQuorumPublic) { + if (enclaveQuorumPublic !== encryptionQuorumPublic) { + throw new Error("enclave quorum public keys from client and bundle do not match") + } + } + + const publicKeyBuf = uint8arrayFromHexString(publicKey); + const publicSignatureBuf = uint8arrayFromHexString(publicSignature); + const quorumKey = importTargetKey(new Uint8Array(encryptionQuorumPublic)); + return await crypto.subtle.verify( { name: "ECDSA", namedCurve: "P-256" }, quorumKey, publicSignatureBuf, publicKeyBuf); + } + /** * Function to send a message. If this page is embedded as an iframe we'll use window.top.postMessage. Otherwise we'll display it in the DOM. * @param type message type. Can be "PUBLIC_KEY_CREATED" or "BUNDLE_INJECTED" @@ -440,7 +462,8 @@

Message log

logMessage, uint8arrayFromHexString, uint8arrayToHexString, - additionalAssociatedData + additionalAssociatedData, + verifyEnclaveSignature } }(); @@ -556,11 +579,23 @@

Message log

* Parse and decrypt the export bundle. * The `bundle` param is a JSON string of the encapsulated public * key, encapsulated public key signature, and the ciphertext. - * Example: {"encappedPublic":"04912cb4200c40f04ae4a162f4c870c78cb4498a8efda0b94f4a9cb848d611bd40e9acccab2bf73cee1e269d8350a02f4df71864921097838f05c288d944fa2f8b","encappedPublicSignature":"304502200cd19a3c5892f1eeab88fe0cdd7cca63736a7d15fc364186fb3c913e1e01568b022100dea49557c176f6ca052b27ad164f077cf64d2aa55fbdc4757a14767f8b8c6b48","ciphertext":"0e5d5503f43721135818051e4c5b77b3365b66ec4020b0051d59ea9fc773c67bd4b61ed34a97b07a3074a85546721ae4"} + * Example: {"encappedPublic":"04912cb4200c40f04ae4a162f4c870c78cb4498a8efda0b94f4a9cb848d611bd40e9acccab2bf73cee1e269d8350a02f4df71864921097838f05c288d944fa2f8b","encappedPublicSignature":"304502200cd19a3c5892f1eeab88fe0cdd7cca63736a7d15fc364186fb3c913e1e01568b022100dea49557c176f6ca052b27ad164f077cf64d2aa55fbdc4757a14767f8b8c6b48","ciphertext":"0e5d5503f43721135818051e4c5b77b3365b66ec4020b0051d59ea9fc773c67bd4b61ed34a97b07a3074a85546721ae4","enclaveQuorumPublic":"04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"} * @param {string} bundle */ const decryptBundle = async bundle => { + // Second half is the public key for the enclave quorum encryption key + const encryptionQuorumPublicBuf = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); + + // Parse the import bundle const bundleObj = JSON.parse(bundle); + + // Verify enclave signature + const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.encappedPublic, bundleObj.encappedPublicSignature); + if (!verified) { + throw new Error("failed to verify enclave signature"); + } + + // Decrypt the ciphertext const encappedKeyBuf = TKHQ.uint8arrayFromHexString(bundleObj.encappedPublic); const ciphertextBuf = TKHQ.uint8arrayFromHexString(bundleObj.ciphertext); const embeddedKeyJwk = await TKHQ.getEmbeddedKey(); @@ -593,6 +628,7 @@

Message log

} else { key = await TKHQ.encodeKey(privateKeyBytes, keyFormat); } + // Display only the key displayKey(key); diff --git a/import/index.html b/import/index.html index ac45e70..9cbb0fb 100644 --- a/import/index.html +++ b/import/index.html @@ -43,6 +43,8 @@ window.TKHQ = function() { /** constants for LocalStorage */ const TURNKEY_TARGET_EMBEDDED_KEY = "TURNKEY_TARGET_EMBEDDED_KEY" + /** Turnkey Signer enclave's quorum public key */ + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Import a key to encrypt to as a CryptoKey and export it as a JSON Web Key. @@ -183,6 +185,26 @@ return new Uint8Array([...s, ...r]); } + /** + * Function to verify enclave signature on import bundle received from the server. + */ + const verifyEnclaveSignature = async (enclaveQuorumPublic, publicKey, publicSignature) => { + // Second half is the public key for the enclave quorum encryption key + const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); + + // todo(olivia): throw error if enclave quorum public is null once server changes are deployed + if (enclaveQuorumPublic) { + if (enclaveQuorumPublic !== encryptionQuorumPublic) { + throw new Error("enclave quorum public keys from client and bundle do not match") + } + } + + const publicKeyBuf = uint8arrayFromHexString(publicKey); + const publicSignatureBuf = uint8arrayFromHexString(publicSignature); + const quorumKey = importTargetKey(new Uint8Array(encryptionQuorumPublic)); + return await crypto.subtle.verify( { name: "ECDSA", namedCurve: "P-256" }, quorumKey, publicSignatureBuf, publicKeyBuf); + } + /** * Function to send a message. If this page is embedded as an iframe we'll use window.top.postMessage. Otherwise we'll display it in the DOM. * @param type message type. Can be "PUBLIC_KEY_CREATED" or "BUNDLE_INJECTED" @@ -207,7 +229,8 @@ uint8arrayToHexString, base58Decode, decodeKey, - additionalAssociatedData + additionalAssociatedData, + verifyEnclaveSignature } }(); @@ -261,26 +284,23 @@ * Parses the `import_bundle` and stores the target public key as a JWK * in local storage. Sends true upon success. * @param {string} bundle - * Example bundle: {"targetPublic":"0491ccb68758b822a6549257f87769eeed37c6cb68a6c6255c5f238e2b6e6e61838c8ac857f2e305970a6435715f84e5a2e4b02a4d1e5289ba7ec7910e47d2d50f","targetPublicSignature":"3045022100cefc333c330c9fa300d1aa10a439a76539b4d6967301638ab9edc9fd9468bfdb0220339bba7e2b00b45d52e941d068ecd3bfd16fd1926da69dd7769893268990d62f"} + * Example bundle: {"targetPublic":"0491ccb68758b822a6549257f87769eeed37c6cb68a6c6255c5f238e2b6e6e61838c8ac857f2e305970a6435715f84e5a2e4b02a4d1e5289ba7ec7910e47d2d50f","targetPublicSignature":"3045022100cefc333c330c9fa300d1aa10a439a76539b4d6967301638ab9edc9fd9468bfdb0220339bba7e2b00b45d52e941d068ecd3bfd16fd1926da69dd7769893268990d62f","enclaveQuorumPublic":"04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"} */ const onInjectImportBundle = async bundle => { // Parse the import bundle const bundleObj = JSON.parse(bundle); - const targetPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublic); + + // Verify enclave signature + const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.targetPublic, bundleObj.targetPublicSignature); + if (!verified) { + throw new Error("failed to verify enclave signature"); + } // Import target public key generated from enclave and set in local storage + const targetPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublic); const targetPublicKeyJwk = await TKHQ.importTargetKey(new Uint8Array(targetPublicBuf)); TKHQ.setTargetEmbeddedKey(targetPublicKeyJwk); - // todo(olivia): verify the signature with the enclave quorum public key once returned in server messages - // const targetSignatureBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublicSignature); - // const quorumPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.enclaveQuorumPublic); - // const quorumKey = TKHQ.importTargetKey(new Uint8Array(quorumPublicBuf)); - // const verified = await crypto.subtle.verify(quorumKey, targetSignatureBuf, targetPublicBuf); - // if (verified === false) { - // throw new Error("verification failed"); - // } - // Send up BUNDLE_INJECTED message TKHQ.sendMessageUp("BUNDLE_INJECTED", true) } diff --git a/import/standalone.html b/import/standalone.html index 2b9f49d..d5d8b46 100644 --- a/import/standalone.html +++ b/import/standalone.html @@ -106,6 +106,8 @@

Message log

window.TKHQ = function() { /** constants for LocalStorage */ const TURNKEY_TARGET_EMBEDDED_KEY = "TURNKEY_TARGET_EMBEDDED_KEY" + /** Turnkey Signer enclave's public key */ + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Import a key to encrypt to as a CryptoKey and export it as a JSON Web Key. @@ -246,6 +248,26 @@

Message log

return new Uint8Array([...s, ...r]); } + /** + * Function to verify enclave signature on import bundle received from the server. + */ + const verifyEnclaveSignature = async (enclaveQuorumPublic, publicKey, publicSignature) => { + // Second half is the public key for the enclave quorum encryption key + const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); + + // todo(olivia): throw error if enclave quorum public is null once server changes are deployed + if (enclaveQuorumPublic) { + if (enclaveQuorumPublic !== encryptionQuorumPublic) { + throw new Error("enclave quorum public keys from client and bundle do not match") + } + } + + const publicKeyBuf = uint8arrayFromHexString(publicKey); + const publicSignatureBuf = uint8arrayFromHexString(publicSignature); + const quorumKey = importTargetKey(new Uint8Array(encryptionQuorumPublic)); + return await crypto.subtle.verify( { name: "ECDSA", namedCurve: "P-256" }, quorumKey, publicSignatureBuf, publicKeyBuf); + } + /** * Function to send a message. If this page is embedded as an iframe we'll use window.top.postMessage. Otherwise we'll display it in the DOM. * @param type message type. Can be "PUBLIC_KEY_CREATED" or "BUNDLE_INJECTED" @@ -282,7 +304,8 @@

Message log

uint8arrayToHexString, base58Decode, decodeKey, - additionalAssociatedData + additionalAssociatedData, + verifyEnclaveSignature } }(); @@ -367,26 +390,23 @@

Message log

* Parses the `import_bundle` and stores the target public key as a JWK * in local storage. Sends true upon success. * @param {string} bundle - * Example bundle: {"targetPublic":"0491ccb68758b822a6549257f87769eeed37c6cb68a6c6255c5f238e2b6e6e61838c8ac857f2e305970a6435715f84e5a2e4b02a4d1e5289ba7ec7910e47d2d50f","targetPublicSignature":"3045022100cefc333c330c9fa300d1aa10a439a76539b4d6967301638ab9edc9fd9468bfdb0220339bba7e2b00b45d52e941d068ecd3bfd16fd1926da69dd7769893268990d62f"} + * Example bundle: {"targetPublic":"0491ccb68758b822a6549257f87769eeed37c6cb68a6c6255c5f238e2b6e6e61838c8ac857f2e305970a6435715f84e5a2e4b02a4d1e5289ba7ec7910e47d2d50f","targetPublicSignature":"3045022100cefc333c330c9fa300d1aa10a439a76539b4d6967301638ab9edc9fd9468bfdb0220339bba7e2b00b45d52e941d068ecd3bfd16fd1926da69dd7769893268990d62f","enclaveQuorumPublic":"04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"} */ const onInjectImportBundle = async bundle => { // Parse the import bundle const bundleObj = JSON.parse(bundle); - const targetPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublic); + + // Verify enclave signature + const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.targetPublic, bundleObj.targetPublicSignature); + if (!verified) { + throw new Error("failed to verify enclave signature"); + } // Import target public key generated from enclave and set in local storage + const targetPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublic); const targetPublicKeyJwk = await TKHQ.importTargetKey(new Uint8Array(targetPublicBuf)); TKHQ.setTargetEmbeddedKey(targetPublicKeyJwk); - // todo(olivia): verify the signature with the enclave quorum public key once returned in server messages - // const targetSignatureBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublicSignature); - // const quorumPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.enclaveQuorumPublic); - // const quorumKey = TKHQ.importTargetKey(new Uint8Array(quorumPublicBuf)); - // const verified = await crypto.subtle.verify(quorumKey, targetSignatureBuf, targetPublicBuf); - // if (verified === false) { - // throw new Error("verification failed"); - // } - // Send up BUNDLE_INJECTED message TKHQ.sendMessageUp("BUNDLE_INJECTED", true) } From 6e1cb978c3236b34ae53e8f0f276e52ebf53b5a8 Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Mon, 1 Apr 2024 22:35:10 -0400 Subject: [PATCH 2/8] add der encoding helper fns and tests to export --- export/index.html | 88 ++++++++++++++++++++++++++++++++++++++++---- export/index.test.js | 35 ++++++++++++++++++ 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/export/index.html b/export/index.html index d106c10..5a391af 100644 --- a/export/index.html +++ b/export/index.html @@ -124,6 +124,28 @@

Message log

/** Turnkey Signer enclave's public key */ const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" + /* + * Import a key to encrypt to as a CryptoKey and export it as a JSON Web Key. + */ + const importTargetKey = async targetPublic => { + const targetKey = await crypto.subtle.importKey("raw", targetPublic, { + name: 'ECDH', + namedCurve: 'P-256', + }, true, []); + + return await crypto.subtle.exportKey("jwk", targetKey); + } + + /* + * Imports the quorum public key as a CryptoKey and returns it. + */ + const importQuorumKey = async quorumPublic => { + return await crypto.subtle.importKey("raw", quorumPublic, { + name: 'ECDSA', + namedCurve: 'P-256' + }, true, ['verify']); + } + /** * Creates a new public/private key pair and persists it in localStorage */ @@ -190,7 +212,7 @@

Message log

* expiry time is missing. * @param {string} key */ - const getItemWithExpiry = (key) => { + const getItemWithExpiry = key => { const itemStr = window.localStorage.getItem(key); if (!itemStr) { return null; @@ -214,7 +236,7 @@

Message log

* @param {string} hexString * @returns {Uint8Array} */ - const uint8arrayFromHexString = function(hexString) { + const uint8arrayFromHexString = hexString => { var hexRegex = /^[0-9A-Fa-f]+$/; if (!hexString || hexString.length % 2 != 0 || !hexRegex.test(hexString)) { throw new Error('cannot create uint8array from invalid hex string: "' + hexString + '"'); @@ -245,7 +267,7 @@

Message log

/** * Function to verify enclave signature on import bundle received from the server. */ - const verifyEnclaveSignature = async (enclaveQuorumPublic, publicKey, publicSignature) => { + const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { // Second half is the public key for the enclave quorum encryption key const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); @@ -255,11 +277,61 @@

Message log

throw new Error("enclave quorum public keys from client and bundle do not match") } } - + + // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); - const publicSignatureBuf = uint8arrayFromHexString(publicSignature); - const quorumKey = importTargetKey(new Uint8Array(encryptionQuorumPublic)); - return await crypto.subtle.verify( { name: "ECDSA", namedCurve: "P-256" }, quorumKey, publicSignatureBuf, publicKeyBuf); + const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); + const quorumKey = await importQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); + return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); + } + + /** + * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. + */ + const fromDerSignature = (derSignature) => { + const derSignatureBuf = uint8arrayFromHexString(derSignature); + + // Skip the sequence tag (0x30) + let index = 2; + + // Parse 'r' and check for integer tag (0x02) + if (derSignatureBuf[index] !== 0x02) { + throw new Error("failed to convert DER-encoded signature: invalid tag for r"); + } + index++; // Move past the INTEGER tag + const rLength = derSignatureBuf[index]; + index++; // Move past the length byte + const r = derSignatureBuf.slice(index, index + rLength); + index += rLength; // Move to the start of s + + // Parse 's' and check for integer tag (0x02) + if (derSignatureBuf[index] !== 0x02) { + throw new Error("failed to convert DER-encoded signature: invalid tag for s"); + } + index++; // Move past the INTEGER tag + const sLength = derSignatureBuf[index]; + index++; // Move past the length byte + const s = derSignatureBuf.slice(index, index + sLength); + + // Normalize 'r' and 's' to 32 bytes each + const rPadded = padStartWithZeroes(r, 32); + const sPadded = padStartWithZeroes(s, 32); + + // Concatenate and return the raw signature + return new Uint8Array([...rPadded, ...sPadded]); + } + + /** + * Function to pad byte array with 0's + */ + const padStartWithZeroes = (byteArray, targetLength) => { + const paddingLength = targetLength - byteArray.length; + if (paddingLength > 0) { + const padding = new Uint8Array(paddingLength).fill(0); + return new Uint8Array([...padding, ...byteArray]); + } + return byteArray; } /** @@ -590,7 +662,7 @@

Message log

const bundleObj = JSON.parse(bundle); // Verify enclave signature - const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.encappedPublic, bundleObj.encappedPublicSignature); + const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.encappedPublicSignature, bundleObj.encappedPublic); if (!verified) { throw new Error("failed to verify enclave signature"); } diff --git a/export/index.test.js b/export/index.test.js index 0664660..0b895dd 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -3,6 +3,7 @@ import { JSDOM } from "jsdom" import fs from "fs" import path from "path" import * as crypto from "crypto"; +import { rejects } from "assert"; const html = fs.readFileSync(path.resolve(__dirname, "./index.html"), "utf8"); @@ -160,4 +161,38 @@ describe("TKHQ", () => { // TODO: test logMessage / sendMessageUp expect(true).toBe(true); }) + + it("verifies enclave signature", async () => { + // No "enclaveQuorumPublic" field in the export bundle. Valid signature + let verified = await TKHQ.verifyEnclaveSignature(null, "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5"); + expect(verified).toBe(true); + + // "enclaveQuorumPublic" field present in the export bundle. Valid signature + verified = await TKHQ.verifyEnclaveSignature("04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5"); + expect(verified).toBe(true); + + // "enclaveQuorumPublic" field present in the export bundle but doesn't match what's pinned on export.turnkey.com + await expect( + TKHQ.verifyEnclaveSignature("04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5") + ).rejects.toThrow("enclave quorum public keys from client and bundle do not match"); + + // Invalid signature + verified = await TKHQ.verifyEnclaveSignature("04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4"); + expect(verified).toBe(false); + + // Invalid DER-encoding for signature + await expect( + TKHQ.verifyEnclaveSignature(null, "300220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4") + ).rejects.toThrow("failed to convert DER-encoded signature: invalid tag for r"); + + // Invalid hex-encoding for signature + await expect( + TKHQ.verifyEnclaveSignature(null, "", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4") + ).rejects.toThrow('cannot create uint8array from invalid hex string: ""'); + + // Invalid hex-encoding for public key + await expect( + TKHQ.verifyEnclaveSignature(null, "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "") + ).rejects.toThrow('cannot create uint8array from invalid hex string: ""'); + }) }) From b38826b2f8f5b8dfa9c17511835e0e8162974221 Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Mon, 1 Apr 2024 22:48:09 -0400 Subject: [PATCH 3/8] harden uint8arrayFromHex for import. add verify enclave signature helpers and tests to import --- export/index.html | 12 ------- import/index.html | 81 +++++++++++++++++++++++++++++++++++++----- import/index.test.js | 34 ++++++++++++++++++ import/standalone.html | 81 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 180 insertions(+), 28 deletions(-) diff --git a/export/index.html b/export/index.html index 5a391af..745072c 100644 --- a/export/index.html +++ b/export/index.html @@ -124,18 +124,6 @@

Message log

/** Turnkey Signer enclave's public key */ const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" - /* - * Import a key to encrypt to as a CryptoKey and export it as a JSON Web Key. - */ - const importTargetKey = async targetPublic => { - const targetKey = await crypto.subtle.importKey("raw", targetPublic, { - name: 'ECDH', - namedCurve: 'P-256', - }, true, []); - - return await crypto.subtle.exportKey("jwk", targetKey); - } - /* * Imports the quorum public key as a CryptoKey and returns it. */ diff --git a/import/index.html b/import/index.html index 9cbb0fb..a9ccfce 100644 --- a/import/index.html +++ b/import/index.html @@ -58,6 +58,16 @@ return await crypto.subtle.exportKey("jwk", targetKey); } + /* + * Imports the quorum public key as a CryptoKey and returns it. + */ + const importQuorumKey = async quorumPublic => { + return await crypto.subtle.importKey("raw", quorumPublic, { + name: 'ECDSA', + namedCurve: 'P-256' + }, true, ['verify']); + } + /** * Gets the current target embedded private key JWK. Returns `null` if not found. */ @@ -84,8 +94,13 @@ * @param {string} hexString * @returns {Uint8Array} */ - const uint8arrayFromHexString = hexString => - new Uint8Array(hexString.match(/../g).map(h=>parseInt(h,16))); + const uint8arrayFromHexString = hexString => { + var hexRegex = /^[0-9A-Fa-f]+$/; + if (!hexString || hexString.length % 2 != 0 || !hexRegex.test(hexString)) { + throw new Error('cannot create uint8array from invalid hex string: "' + hexString + '"'); + } + return new Uint8Array(hexString.match(/../g).map(h=>parseInt(h,16))); + } /** * Takes a Uint8Array and returns a hex string @@ -188,7 +203,7 @@ /** * Function to verify enclave signature on import bundle received from the server. */ - const verifyEnclaveSignature = async (enclaveQuorumPublic, publicKey, publicSignature) => { + const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { // Second half is the public key for the enclave quorum encryption key const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); @@ -198,11 +213,61 @@ throw new Error("enclave quorum public keys from client and bundle do not match") } } - + + // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); - const publicSignatureBuf = uint8arrayFromHexString(publicSignature); - const quorumKey = importTargetKey(new Uint8Array(encryptionQuorumPublic)); - return await crypto.subtle.verify( { name: "ECDSA", namedCurve: "P-256" }, quorumKey, publicSignatureBuf, publicKeyBuf); + const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); + const quorumKey = await importQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); + return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); + } + + /** + * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. + */ + const fromDerSignature = (derSignature) => { + const derSignatureBuf = uint8arrayFromHexString(derSignature); + + // Skip the sequence tag (0x30) + let index = 2; + + // Parse 'r' and check for integer tag (0x02) + if (derSignatureBuf[index] !== 0x02) { + throw new Error("failed to convert DER-encoded signature: invalid tag for r"); + } + index++; // Move past the INTEGER tag + const rLength = derSignatureBuf[index]; + index++; // Move past the length byte + const r = derSignatureBuf.slice(index, index + rLength); + index += rLength; // Move to the start of s + + // Parse 's' and check for integer tag (0x02) + if (derSignatureBuf[index] !== 0x02) { + throw new Error("failed to convert DER-encoded signature: invalid tag for s"); + } + index++; // Move past the INTEGER tag + const sLength = derSignatureBuf[index]; + index++; // Move past the length byte + const s = derSignatureBuf.slice(index, index + sLength); + + // Normalize 'r' and 's' to 32 bytes each + const rPadded = padStartWithZeroes(r, 32); + const sPadded = padStartWithZeroes(s, 32); + + // Concatenate and return the raw signature + return new Uint8Array([...rPadded, ...sPadded]); + } + + /** + * Function to pad byte array with 0's + */ + const padStartWithZeroes = (byteArray, targetLength) => { + const paddingLength = targetLength - byteArray.length; + if (paddingLength > 0) { + const padding = new Uint8Array(paddingLength).fill(0); + return new Uint8Array([...padding, ...byteArray]); + } + return byteArray; } /** @@ -291,7 +356,7 @@ const bundleObj = JSON.parse(bundle); // Verify enclave signature - const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.targetPublic, bundleObj.targetPublicSignature); + const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.targetPublicSignature, bundleObj.targetPublic); if (!verified) { throw new Error("failed to verify enclave signature"); } diff --git a/import/index.test.js b/import/index.test.js index 9629499..b4f4cbd 100644 --- a/import/index.test.js +++ b/import/index.test.js @@ -100,4 +100,38 @@ describe("TKHQ", () => { // TODO: test logMessage / sendMessageUp expect(true).toBe(true); }) + + it("verifies enclave signature", async () => { + // No "enclaveQuorumPublic" field in the export bundle. Valid signature + let verified = await TKHQ.verifyEnclaveSignature(null, "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5"); + expect(verified).toBe(true); + + // "enclaveQuorumPublic" field present in the export bundle. Valid signature + verified = await TKHQ.verifyEnclaveSignature("04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5"); + expect(verified).toBe(true); + + // "enclaveQuorumPublic" field present in the export bundle but doesn't match what's pinned on export.turnkey.com + await expect( + TKHQ.verifyEnclaveSignature("04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5") + ).rejects.toThrow("enclave quorum public keys from client and bundle do not match"); + + // Invalid signature + verified = await TKHQ.verifyEnclaveSignature("04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4"); + expect(verified).toBe(false); + + // Invalid DER-encoding for signature + await expect( + TKHQ.verifyEnclaveSignature(null, "300220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4") + ).rejects.toThrow("failed to convert DER-encoded signature: invalid tag for r"); + + // Invalid hex-encoding for signature + await expect( + TKHQ.verifyEnclaveSignature(null, "", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4") + ).rejects.toThrow('cannot create uint8array from invalid hex string: ""'); + + // Invalid hex-encoding for public key + await expect( + TKHQ.verifyEnclaveSignature(null, "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "") + ).rejects.toThrow('cannot create uint8array from invalid hex string: ""'); + }) }) diff --git a/import/standalone.html b/import/standalone.html index d5d8b46..369d232 100644 --- a/import/standalone.html +++ b/import/standalone.html @@ -121,6 +121,16 @@

Message log

return await crypto.subtle.exportKey("jwk", targetKey); } + /* + * Imports the quorum public key as a CryptoKey and returns it. + */ + const importQuorumKey = async quorumPublic => { + return await crypto.subtle.importKey("raw", quorumPublic, { + name: 'ECDSA', + namedCurve: 'P-256' + }, true, ['verify']); + } + /** * Gets the current target embedded private key JWK. Returns `null` if not found. */ @@ -147,8 +157,13 @@

Message log

* @param {string} hexString * @returns {Uint8Array} */ - const uint8arrayFromHexString = hexString => - new Uint8Array(hexString.match(/../g).map(h=>parseInt(h,16))); + const uint8arrayFromHexString = hexString => { + var hexRegex = /^[0-9A-Fa-f]+$/; + if (!hexString || hexString.length % 2 != 0 || !hexRegex.test(hexString)) { + throw new Error('cannot create uint8array from invalid hex string: "' + hexString + '"'); + } + return new Uint8Array(hexString.match(/../g).map(h=>parseInt(h,16))); + } /** * Takes a Uint8Array and returns a hex string @@ -251,7 +266,7 @@

Message log

/** * Function to verify enclave signature on import bundle received from the server. */ - const verifyEnclaveSignature = async (enclaveQuorumPublic, publicKey, publicSignature) => { + const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { // Second half is the public key for the enclave quorum encryption key const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); @@ -261,11 +276,61 @@

Message log

throw new Error("enclave quorum public keys from client and bundle do not match") } } - + + // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); - const publicSignatureBuf = uint8arrayFromHexString(publicSignature); - const quorumKey = importTargetKey(new Uint8Array(encryptionQuorumPublic)); - return await crypto.subtle.verify( { name: "ECDSA", namedCurve: "P-256" }, quorumKey, publicSignatureBuf, publicKeyBuf); + const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); + const quorumKey = await importQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); + return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); + } + + /** + * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. + */ + const fromDerSignature = (derSignature) => { + const derSignatureBuf = uint8arrayFromHexString(derSignature); + + // Skip the sequence tag (0x30) + let index = 2; + + // Parse 'r' and check for integer tag (0x02) + if (derSignatureBuf[index] !== 0x02) { + throw new Error("failed to convert DER-encoded signature: invalid tag for r"); + } + index++; // Move past the INTEGER tag + const rLength = derSignatureBuf[index]; + index++; // Move past the length byte + const r = derSignatureBuf.slice(index, index + rLength); + index += rLength; // Move to the start of s + + // Parse 's' and check for integer tag (0x02) + if (derSignatureBuf[index] !== 0x02) { + throw new Error("failed to convert DER-encoded signature: invalid tag for s"); + } + index++; // Move past the INTEGER tag + const sLength = derSignatureBuf[index]; + index++; // Move past the length byte + const s = derSignatureBuf.slice(index, index + sLength); + + // Normalize 'r' and 's' to 32 bytes each + const rPadded = padStartWithZeroes(r, 32); + const sPadded = padStartWithZeroes(s, 32); + + // Concatenate and return the raw signature + return new Uint8Array([...rPadded, ...sPadded]); + } + + /** + * Function to pad byte array with 0's + */ + const padStartWithZeroes = (byteArray, targetLength) => { + const paddingLength = targetLength - byteArray.length; + if (paddingLength > 0) { + const padding = new Uint8Array(paddingLength).fill(0); + return new Uint8Array([...padding, ...byteArray]); + } + return byteArray; } /** @@ -397,7 +462,7 @@

Message log

const bundleObj = JSON.parse(bundle); // Verify enclave signature - const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.targetPublic, bundleObj.targetPublicSignature); + const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.targetPublicSignature, bundleObj.targetPublic); if (!verified) { throw new Error("failed to verify enclave signature"); } From c4edb47bde10b060e3681187c8884ad1ecd8047b Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Mon, 1 Apr 2024 22:48:36 -0400 Subject: [PATCH 4/8] add import install deps and run tests for improt --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48497ad..769c99b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - directory: ["auth", "export"] + directory: ["auth", "export", "import"] steps: - name: Checkout From 86da7fb19b42bde3c239b797523e88642293f499 Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Tue, 2 Apr 2024 13:21:50 -0400 Subject: [PATCH 5/8] rename import key to load key to disambiguate from feature --- export/index.html | 6 +++--- import/index.html | 16 ++++++++-------- import/index.test.js | 2 +- import/standalone.html | 16 ++++++++-------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/export/index.html b/export/index.html index 745072c..df7259e 100644 --- a/export/index.html +++ b/export/index.html @@ -125,9 +125,9 @@

Message log

const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* - * Imports the quorum public key as a CryptoKey and returns it. + * Loads the quorum public key as a CryptoKey. */ - const importQuorumKey = async quorumPublic => { + const loadQuorumKey = async quorumPublic => { return await crypto.subtle.importKey("raw", quorumPublic, { name: 'ECDSA', namedCurve: 'P-256' @@ -270,7 +270,7 @@

Message log

const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); - const quorumKey = await importQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); + const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } diff --git a/import/index.html b/import/index.html index a9ccfce..4fa7361 100644 --- a/import/index.html +++ b/import/index.html @@ -47,9 +47,9 @@ const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* - * Import a key to encrypt to as a CryptoKey and export it as a JSON Web Key. + * Load a key to encrypt to as a CryptoKey and return it as a JSON Web Key. */ - const importTargetKey = async (targetPublic) => { + const loadTargetKey = async (targetPublic) => { const targetKey = await crypto.subtle.importKey("raw", targetPublic, { name: 'ECDH', namedCurve: 'P-256', @@ -59,9 +59,9 @@ } /* - * Imports the quorum public key as a CryptoKey and returns it. + * Loads the quorum public key as a CryptoKey. */ - const importQuorumKey = async quorumPublic => { + const loadQuorumKey = async quorumPublic => { return await crypto.subtle.importKey("raw", quorumPublic, { name: 'ECDSA', namedCurve: 'P-256' @@ -218,7 +218,7 @@ const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); - const quorumKey = await importQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); + const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } @@ -285,7 +285,7 @@ } return { - importTargetKey, + loadTargetKey, getTargetEmbeddedKey, setTargetEmbeddedKey, resetTargetEmbeddedKey, @@ -361,9 +361,9 @@ throw new Error("failed to verify enclave signature"); } - // Import target public key generated from enclave and set in local storage + // Load target public key generated from enclave and set in local storage const targetPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublic); - const targetPublicKeyJwk = await TKHQ.importTargetKey(new Uint8Array(targetPublicBuf)); + const targetPublicKeyJwk = await TKHQ.loadTargetKey(new Uint8Array(targetPublicBuf)); TKHQ.setTargetEmbeddedKey(targetPublicKeyJwk); // Send up BUNDLE_INJECTED message diff --git a/import/index.test.js b/import/index.test.js index b4f4cbd..e172773 100644 --- a/import/index.test.js +++ b/import/index.test.js @@ -44,7 +44,7 @@ describe("TKHQ", () => { it("imports P256 keys", async () => { const targetPubHex = "0491ccb68758b822a6549257f87769eeed37c6cb68a6c6255c5f238e2b6e6e61838c8ac857f2e305970a6435715f84e5a2e4b02a4d1e5289ba7ec7910e47d2d50f"; const targetPublicBuf = TKHQ.uint8arrayFromHexString(targetPubHex); - const key = await TKHQ.importTargetKey(new Uint8Array(targetPublicBuf)); + const key = await TKHQ.loadTargetKey(new Uint8Array(targetPublicBuf)); expect(key.kty).toEqual("EC"); expect(key.ext).toBe(true); expect(key.crv).toBe("P-256"); diff --git a/import/standalone.html b/import/standalone.html index 369d232..dd91cc2 100644 --- a/import/standalone.html +++ b/import/standalone.html @@ -110,9 +110,9 @@

Message log

const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* - * Import a key to encrypt to as a CryptoKey and export it as a JSON Web Key. + * Load a key to encrypt to as a CryptoKey and return it as a JSON Web Key. */ - const importTargetKey = async (targetPublic) => { + const loadTargetKey = async (targetPublic) => { const targetKey = await crypto.subtle.importKey("raw", targetPublic, { name: 'ECDH', namedCurve: 'P-256', @@ -122,9 +122,9 @@

Message log

} /* - * Imports the quorum public key as a CryptoKey and returns it. + * Loads the quorum public key as a CryptoKey. */ - const importQuorumKey = async quorumPublic => { + const loadQuorumKey = async quorumPublic => { return await crypto.subtle.importKey("raw", quorumPublic, { name: 'ECDSA', namedCurve: 'P-256' @@ -281,7 +281,7 @@

Message log

const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); - const quorumKey = await importQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); + const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } @@ -359,7 +359,7 @@

Message log

} return { - importTargetKey, + loadTargetKey, getTargetEmbeddedKey, setTargetEmbeddedKey, resetTargetEmbeddedKey, @@ -467,9 +467,9 @@

Message log

throw new Error("failed to verify enclave signature"); } - // Import target public key generated from enclave and set in local storage + // Load target public key generated from enclave and set in local storage const targetPublicBuf = TKHQ.uint8arrayFromHexString(bundleObj.targetPublic); - const targetPublicKeyJwk = await TKHQ.importTargetKey(new Uint8Array(targetPublicBuf)); + const targetPublicKeyJwk = await TKHQ.loadTargetKey(new Uint8Array(targetPublicBuf)); TKHQ.setTargetEmbeddedKey(targetPublicKeyJwk); // Send up BUNDLE_INJECTED message From f167dd855e28547a271dfee2e695de094f646822 Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Tue, 2 Apr 2024 13:33:34 -0400 Subject: [PATCH 6/8] pin only enclave quorum encryption pubkey. update pubkey mismatch error msg --- export/index.html | 14 ++++---------- export/index.test.js | 2 +- import/index.html | 11 ++++------- import/index.test.js | 2 +- import/standalone.html | 11 ++++------- 5 files changed, 14 insertions(+), 26 deletions(-) diff --git a/export/index.html b/export/index.html index df7259e..c523090 100644 --- a/export/index.html +++ b/export/index.html @@ -122,7 +122,7 @@

Message log

/** 48 hours in milliseconds */ const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 48; /** Turnkey Signer enclave's public key */ - const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Loads the quorum public key as a CryptoKey. @@ -256,20 +256,17 @@

Message log

* Function to verify enclave signature on import bundle received from the server. */ const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { - // Second half is the public key for the enclave quorum encryption key - const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); - // todo(olivia): throw error if enclave quorum public is null once server changes are deployed if (enclaveQuorumPublic) { - if (enclaveQuorumPublic !== encryptionQuorumPublic) { - throw new Error("enclave quorum public keys from client and bundle do not match") + if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { + throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); } } // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); - const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); + const encryptionQuorumPublicBuf = uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY); const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } @@ -643,9 +640,6 @@

Message log

* @param {string} bundle */ const decryptBundle = async bundle => { - // Second half is the public key for the enclave quorum encryption key - const encryptionQuorumPublicBuf = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); - // Parse the import bundle const bundleObj = JSON.parse(bundle); diff --git a/export/index.test.js b/export/index.test.js index 0b895dd..02c1c63 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -174,7 +174,7 @@ describe("TKHQ", () => { // "enclaveQuorumPublic" field present in the export bundle but doesn't match what's pinned on export.turnkey.com await expect( TKHQ.verifyEnclaveSignature("04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5") - ).rejects.toThrow("enclave quorum public keys from client and bundle do not match"); + ).rejects.toThrow("enclave quorum public keys from client and bundle do not match. Client: 04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569. Bundle: 04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb."); // Invalid signature verified = await TKHQ.verifyEnclaveSignature("04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4"); diff --git a/import/index.html b/import/index.html index 4fa7361..fb4f65e 100644 --- a/import/index.html +++ b/import/index.html @@ -44,7 +44,7 @@ /** constants for LocalStorage */ const TURNKEY_TARGET_EMBEDDED_KEY = "TURNKEY_TARGET_EMBEDDED_KEY" /** Turnkey Signer enclave's quorum public key */ - const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Load a key to encrypt to as a CryptoKey and return it as a JSON Web Key. @@ -204,20 +204,17 @@ * Function to verify enclave signature on import bundle received from the server. */ const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { - // Second half is the public key for the enclave quorum encryption key - const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); - // todo(olivia): throw error if enclave quorum public is null once server changes are deployed if (enclaveQuorumPublic) { - if (enclaveQuorumPublic !== encryptionQuorumPublic) { - throw new Error("enclave quorum public keys from client and bundle do not match") + if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { + throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); } } // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); - const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); + const encryptionQuorumPublicBuf = uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY); const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } diff --git a/import/index.test.js b/import/index.test.js index e172773..af0cfe0 100644 --- a/import/index.test.js +++ b/import/index.test.js @@ -113,7 +113,7 @@ describe("TKHQ", () => { // "enclaveQuorumPublic" field present in the export bundle but doesn't match what's pinned on export.turnkey.com await expect( TKHQ.verifyEnclaveSignature("04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5") - ).rejects.toThrow("enclave quorum public keys from client and bundle do not match"); + ).rejects.toThrow("enclave quorum public keys from client and bundle do not match. Client: 04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569. Bundle: 04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb."); // Invalid signature verified = await TKHQ.verifyEnclaveSignature("04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569", "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04d32d8e0fe5a401a717971fabfabe02ddb6bea39b72a18a415fc0273579b394650aae97f75b0462ffa8880a1899c7a930569974519685a995d2e74e372e105bf4"); diff --git a/import/standalone.html b/import/standalone.html index dd91cc2..b704df4 100644 --- a/import/standalone.html +++ b/import/standalone.html @@ -107,7 +107,7 @@

Message log

/** constants for LocalStorage */ const TURNKEY_TARGET_EMBEDDED_KEY = "TURNKEY_TARGET_EMBEDDED_KEY" /** Turnkey Signer enclave's public key */ - const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04ca7c0d624c75de6f34af342e87a21e0d8c83efd1bd5b5da0c0177c147f744fba6f01f9f37356f9c617659aafa55f6e0af8d169a8f054d153ab3201901fb63ecb04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Load a key to encrypt to as a CryptoKey and return it as a JSON Web Key. @@ -267,20 +267,17 @@

Message log

* Function to verify enclave signature on import bundle received from the server. */ const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { - // Second half is the public key for the enclave quorum encryption key - const encryptionQuorumPublic = TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY.slice(130, 260); - // todo(olivia): throw error if enclave quorum public is null once server changes are deployed if (enclaveQuorumPublic) { - if (enclaveQuorumPublic !== encryptionQuorumPublic) { - throw new Error("enclave quorum public keys from client and bundle do not match") + if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { + throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); } } // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); - const encryptionQuorumPublicBuf = uint8arrayFromHexString(encryptionQuorumPublic); + const encryptionQuorumPublicBuf = uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY); const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } From 3013681815a34fede1a0564d74b0b59eac4f464a Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Tue, 2 Apr 2024 17:46:41 -0400 Subject: [PATCH 7/8] fix der encoding and padding for export. add tests for helper functions. convert arrow fns to declarative fns to avoid ordering issues. --- export/index.html | 147 +++++++++++++++++++++++++------------------ export/index.test.js | 41 ++++++++++++ 2 files changed, 128 insertions(+), 60 deletions(-) diff --git a/export/index.html b/export/index.html index c523090..77f05a5 100644 --- a/export/index.html +++ b/export/index.html @@ -121,23 +121,21 @@

Message log

const TURNKEY_EMBEDDED_KEY = "TURNKEY_EMBEDDED_KEY" /** 48 hours in milliseconds */ const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 48; - /** Turnkey Signer enclave's public key */ - const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Loads the quorum public key as a CryptoKey. */ - const loadQuorumKey = async quorumPublic => { + async function loadQuorumKey(quorumPublic) { return await crypto.subtle.importKey("raw", quorumPublic, { - name: 'ECDSA', - namedCurve: 'P-256' - }, true, ['verify']); + name: "ECDSA", + namedCurve: "P-256" + }, true, ["verify"]); } /** * Creates a new public/private key pair and persists it in localStorage */ - const initEmbeddedKey = async () => { + async function initEmbeddedKey() { const retrievedKey = await getEmbeddedKey(); if (retrievedKey === null) { const targetKey = await generateTargetKey(); @@ -149,7 +147,7 @@

Message log

/* * Generate a key to encrypt to and export it as a JSON Web Key. */ - const generateTargetKey = async () => { + async function generateTargetKey() { const p256key = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256', @@ -161,7 +159,7 @@

Message log

/** * Gets the current embedded private key JWK. Returns `null` if not found. */ - const getEmbeddedKey = () => { + function getEmbeddedKey() { const jwtKey = TKHQ.getItemWithExpiry(TURNKEY_EMBEDDED_KEY) return jwtKey ? JSON.parse(jwtKey) : null; } @@ -170,14 +168,16 @@

Message log

* Sets the embedded private key JWK with the default expiration time. * @param {JsonWebKey} targetKey */ - const setEmbeddedKey = targetKey => + function setEmbeddedKey(targetKey) { setItemWithExpiry(TURNKEY_EMBEDDED_KEY, JSON.stringify(targetKey), TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS); + } /** * Resets the current embedded private key JWK. */ - const onResetEmbeddedKey = () => + function onResetEmbeddedKey() { window.localStorage.removeItem(TURNKEY_EMBEDDED_KEY); + } /** * Set an item in localStorage with an expiration time @@ -185,7 +185,7 @@

Message log

* @param {string} value * @param {number} ttl expiration time in milliseconds */ - const setItemWithExpiry = (key, value, ttl) => { + function setItemWithExpiry(key, value, ttl) { const now = new Date(); const item = { value: value, @@ -200,7 +200,7 @@

Message log

* expiry time is missing. * @param {string} key */ - const getItemWithExpiry = key => { + function getItemWithExpiry(key) { const itemStr = window.localStorage.getItem(key); if (!itemStr) { return null; @@ -218,13 +218,12 @@

Message log

return item.value; }; - /** * Takes a hex string (e.g. "e4567ab") and returns an array buffer (Uint8Array) * @param {string} hexString * @returns {Uint8Array} */ - const uint8arrayFromHexString = hexString => { + function uint8arrayFromHexString(hexString) { var hexRegex = /^[0-9A-Fa-f]+$/; if (!hexString || hexString.length % 2 != 0 || !hexRegex.test(hexString)) { throw new Error('cannot create uint8array from invalid hex string: "' + hexString + '"'); @@ -237,47 +236,57 @@

Message log

* @param {Uint8Array} buffer * @return {string} */ - const uint8arrayToHexString = buffer => { + function uint8arrayToHexString(buffer) { return [...buffer] .map(x => x.toString(16).padStart(2, '0')) .join(''); } /** - * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. + * Function to normalize padding of byte array with 0's to a target length */ - const additionalAssociatedData = (senderPubBuf, receiverPubBuf) => { - const s = Array.from(new Uint8Array(senderPubBuf)); - const r = Array.from(new Uint8Array(receiverPubBuf)); - return new Uint8Array([...s, ...r]); - } + function normalizePadding(byteArray, targetLength) { + const paddingLength = targetLength - byteArray.length; - /** - * Function to verify enclave signature on import bundle received from the server. - */ - const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { - // todo(olivia): throw error if enclave quorum public is null once server changes are deployed - if (enclaveQuorumPublic) { - if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { - throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); + // Add leading 0's to array + if (paddingLength > 0) { + const padding = new Uint8Array(paddingLength).fill(0); + return new Uint8Array([...padding, ...byteArray]); + } + + // Remove leading 0's from array + if (paddingLength < 0) { + const expectedZeroCount = paddingLength * -1; + let zeroCount = 0; + for (let i = 0; i < expectedZeroCount && i < byteArray.length; i++) { + if (byteArray[i] === 0) { + zeroCount++; + } + } + // Check if the number of zeros found equals the number of zeroes expected + if (zeroCount !== expectedZeroCount) { + throw new Error(`invalid number of starting zeroes. Expected number of zeroes: ${expectedZeroCount}. Found: ${zeroCount}.`); } + return byteArray.slice(expectedZeroCount, expectedZeroCount + targetLength); } - - // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format - const publicSignatureBuf = fromDerSignature(publicSignature); - const publicKeyBuf = uint8arrayFromHexString(publicKey); - const encryptionQuorumPublicBuf = uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY); - const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); - return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); + return byteArray; } + /** + * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. + */ + function additionalAssociatedData(senderPubBuf, receiverPubBuf) { + const s = Array.from(new Uint8Array(senderPubBuf)); + const r = Array.from(new Uint8Array(receiverPubBuf)); + return new Uint8Array([...s, ...r]); + } /** * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. */ - const fromDerSignature = (derSignature) => { + function fromDerSignature(derSignature) { const derSignatureBuf = uint8arrayFromHexString(derSignature); - // Skip the sequence tag (0x30) + // Check and skip the sequence tag (0x30) let index = 2; // Parse 'r' and check for integer tag (0x02) @@ -300,23 +309,37 @@

Message log

const s = derSignatureBuf.slice(index, index + sLength); // Normalize 'r' and 's' to 32 bytes each - const rPadded = padStartWithZeroes(r, 32); - const sPadded = padStartWithZeroes(s, 32); + const rPadded = normalizePadding(r, 32); + const sPadded = normalizePadding(s, 32); // Concatenate and return the raw signature return new Uint8Array([...rPadded, ...sPadded]); } /** - * Function to pad byte array with 0's + * Function to verify enclave signature on import bundle received from the server. */ - const padStartWithZeroes = (byteArray, targetLength) => { - const paddingLength = targetLength - byteArray.length; - if (paddingLength > 0) { - const padding = new Uint8Array(paddingLength).fill(0); - return new Uint8Array([...padding, ...byteArray]); + async function verifyEnclaveSignature(enclaveQuorumPublic, publicSignature, publicKey) { + /** Turnkey Signer enclave's public key */ + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"; + + // todo(olivia): throw error if enclave quorum public is null once server changes are deployed + if (enclaveQuorumPublic) { + if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { + throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); + } } - return byteArray; + + const encryptionQuorumPublicBuf = new Uint8Array(uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY)); + const quorumKey = await loadQuorumKey(encryptionQuorumPublicBuf); + if (!quorumKey) { + throw new Error("failed to load quorum key"); + } + + // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + const publicSignatureBuf = fromDerSignature(publicSignature); + const publicKeyBuf = uint8arrayFromHexString(publicKey); + return await crypto.subtle.verify({ name: "ECDSA", namedCurve: "P-256", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } /** @@ -324,7 +347,7 @@

Message log

* @param type message type. Can be "PUBLIC_KEY_CREATED" or "BUNDLE_INJECTED" * @param value message value */ - const sendMessageUp = (type, value) => { + function sendMessageUp(type, value) { if (window.top !== null) { window.top.postMessage({ "type": type, @@ -337,7 +360,7 @@

Message log

/** * Function to log a message and persist it in the page's DOM. */ - const logMessage = content => { + function logMessage(content) { const messageLog = document.getElementById("message-log"); const message = document.createElement("p") message.innerText = content; @@ -349,7 +372,7 @@

Message log

* key in raw format. * @return {Uint8array} */ - const p256JWKPrivateToPublic = async jwkPrivate => { + async function p256JWKPrivateToPublic(jwkPrivate) { // make a copy so we don't modify the underlying object const jwkPrivateCopy = { ... jwkPrivate } // change jwk so it will be imported as a public key @@ -453,7 +476,7 @@

Message log

* @param {Uint8Array} privateKeyBytes * @param {string} keyFormat Can be "HEXADECIMAL" or "SOLANA" */ - const encodeKey = async (privateKeyBytes, keyFormat, publicKeyBytes) => { + async function encodeKey(privateKeyBytes, keyFormat, publicKeyBytes) { switch (keyFormat) { case "SOLANA": if (!publicKeyBytes) { @@ -482,7 +505,7 @@

Message log

* from wallet bytes. * @param {Uint8Array} walletBytes */ - const encodeWallet = walletBytes => { + function encodeWallet(walletBytes) { const decoder = new TextDecoder("utf-8"); const wallet = decoder.decode(walletBytes); let mnemonic; @@ -519,6 +542,8 @@

Message log

logMessage, uint8arrayFromHexString, uint8arrayToHexString, + normalizePadding, + fromDerSignature, additionalAssociatedData, verifyEnclaveSignature } @@ -606,7 +631,7 @@

Message log

* Then append an element containing the hex-encoded raw private key. * @param {string} key */ - const displayKey = key => { + function displayKey(key) { Array.from(document.body.children).forEach(child => { if (child.tagName !== "SCRIPT") { child.style.display = 'none'; @@ -639,14 +664,17 @@

Message log

* Example: {"encappedPublic":"04912cb4200c40f04ae4a162f4c870c78cb4498a8efda0b94f4a9cb848d611bd40e9acccab2bf73cee1e269d8350a02f4df71864921097838f05c288d944fa2f8b","encappedPublicSignature":"304502200cd19a3c5892f1eeab88fe0cdd7cca63736a7d15fc364186fb3c913e1e01568b022100dea49557c176f6ca052b27ad164f077cf64d2aa55fbdc4757a14767f8b8c6b48","ciphertext":"0e5d5503f43721135818051e4c5b77b3365b66ec4020b0051d59ea9fc773c67bd4b61ed34a97b07a3074a85546721ae4","enclaveQuorumPublic":"04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"} * @param {string} bundle */ - const decryptBundle = async bundle => { + async function decryptBundle(bundle) { // Parse the import bundle const bundleObj = JSON.parse(bundle); // Verify enclave signature + if (!TKHQ.verifyEnclaveSignature) { + throw new Error("method not loaded"); + } const verified = await TKHQ.verifyEnclaveSignature(bundleObj.enclaveQuorumPublic, bundleObj.encappedPublicSignature, bundleObj.encappedPublic); if (!verified) { - throw new Error("failed to verify enclave signature"); + throw new Error(`failed to verify enclave signature: ${bundle}`); } // Decrypt the ciphertext @@ -665,7 +693,7 @@

Message log

* Function triggered when INJECT_KEY_EXPORT_BUNDLE event is received. * @param {string} bundle */ - const onInjectKeyBundle = async (bundle, keyFormat) => { + async function onInjectKeyBundle(bundle, keyFormat) { // Decrypt the export bundle const keyBytes = await decryptBundle(bundle); @@ -683,7 +711,6 @@

Message log

key = await TKHQ.encodeKey(privateKeyBytes, keyFormat); } - // Display only the key displayKey(key); @@ -695,7 +722,7 @@

Message log

* Function triggered when INJECT_WALLET_EXPORT_BUNDLE event is received. * @param {string} bundle */ - const onInjectWalletBundle = async bundle => { + async function onInjectWalletBundle(bundle) { // Decrypt the export bundle const walletBytes = await decryptBundle(bundle); @@ -716,7 +743,7 @@

Message log

* Decrypt the ciphertext (ArrayBuffer) given an encapsulation key (ArrayBuffer) * and the receivers private key (JSON Web Key). */ - const HpkeDecrypt = async ({ ciphertextBuf, encappedKeyBuf, receiverPrivJwk }) => { + async function HpkeDecrypt({ ciphertextBuf, encappedKeyBuf, receiverPrivJwk }) { const kemContext = new hpke.DhkemP256HkdfSha256(); var receiverPriv = await kemContext.importKey("jwk", {...receiverPrivJwk}, false); diff --git a/export/index.test.js b/export/index.test.js index 02c1c63..cee535b 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -162,6 +162,47 @@ describe("TKHQ", () => { expect(true).toBe(true); }) + it("normalizes padding in a byte array", () => { + // Array with no leading 0's and a valid target length + const arr = new Uint8Array(32).fill(1); + expect(TKHQ.normalizePadding(arr, 32).length).toBe(32); + expect(TKHQ.normalizePadding(arr, 32)).toBe(arr); + + // Array with an extra leading 0 and valid target length + const zeroesArr = new Uint8Array(1).fill(0); + const zeroesLeadingArr = new Uint8Array([...zeroesArr, ...arr]); + expect(TKHQ.normalizePadding(zeroesLeadingArr, 32).length).toBe(32); + expect(TKHQ.normalizePadding(zeroesLeadingArr, 32)).toStrictEqual(arr); + + // Array with a missing leading 0 and valid target length + const zeroesMissingArr = new Uint8Array(31).fill(1); + const paddedArr = new Uint8Array(32); + paddedArr.fill(1, 1); + expect(TKHQ.normalizePadding(zeroesMissingArr, 32).length).toBe(32); + expect(Array.from(TKHQ.normalizePadding(zeroesMissingArr, 32))).toStrictEqual(Array.from(paddedArr)); + + // Array with an extra leading 0 and invalid zero count + expect(() => TKHQ.normalizePadding(zeroesLeadingArr, 31)).toThrow("invalid number of starting zeroes. Expected number of zeroes: 2. Found: 1."); + }) + + it("decodes a ASN.1 DER-encoded signature to raw format", () => { + // Valid signature where r and s don't need padding + expect(TKHQ.fromDerSignature("304402202b769b6dd410ff8a1cbcd5dd7fb2733e80f11922443b1eb629e6e538d1054c3b022020b9715d140f079190123411370971cc6daba8e61b6b58d36321c31ae331799b").length).toBe(64); + + // Valid signature where r and s have extra padding + expect(TKHQ.fromDerSignature("3046022100b71f5a377a7ae6d245d1aa22145f52f7c7d87fcaf7c68c60f43fecf3817b22cf022100cdea30eb54c099a8c86b14c3d2c4accd59c21fbeacd878842d5e9bdd39d19d55").length).toBe(64); + + // Valid signature where r has extra padding + expect(TKHQ.fromDerSignature("304502210088f4f3b59e277f30cb16c05541551eca702ce925002dbc3de3a7c0a7f76b23f902202a0f272c3e5724848dc5232c3409918277d65fd7e8c6eb1630bf6eb2eeb472e3").length).toBe(64); + + // Invalid signature. Wrong integer tag for r + expect(() => TKHQ.fromDerSignature("304503210088f4f3b59e277f30cb16c05541551eca702ce925002dbc3de3a7c0a7f76b23f902202a0f272c3e5724848dc5232c3409918277d65fd7e8c6eb1630bf6eb2eeb472e3")).toThrow("failed to convert DER-encoded signature: invalid tag for r"); + + // Invalid signature. Wrong integer tag for s + expect(() => TKHQ.fromDerSignature("304502210088f4f3b59e277f30cb16c05541551eca702ce925002dbc3de3a7c0a7f76b23f903202a0f272c3e5724848dc5232c3409918277d65fd7e8c6eb1630bf6eb2eeb472e3")).toThrow("failed to convert DER-encoded signature: invalid tag for s"); + + }) + it("verifies enclave signature", async () => { // No "enclaveQuorumPublic" field in the export bundle. Valid signature let verified = await TKHQ.verifyEnclaveSignature(null, "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5"); From 45fca84c052e6facb7aa46dbca33966525d010f5 Mon Sep 17 00:00:00 2001 From: Olivia Thet Date: Tue, 2 Apr 2024 18:08:40 -0400 Subject: [PATCH 8/8] fix der encoding and padding for export. add tests for helper functions. convert arrow fns to declarative fns to avoid ordering issues. --- export/index.html | 5 +- export/index.test.js | 1 - import/index.html | 127 +++++++++++++++++++++++---------------- import/index.test.js | 40 +++++++++++++ import/standalone.html | 131 +++++++++++++++++++++++++---------------- 5 files changed, 199 insertions(+), 105 deletions(-) diff --git a/export/index.html b/export/index.html index 77f05a5..5e4e32e 100644 --- a/export/index.html +++ b/export/index.html @@ -280,6 +280,7 @@

Message log

const r = Array.from(new Uint8Array(receiverPubBuf)); return new Uint8Array([...s, ...r]); } + /** * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. */ @@ -335,7 +336,7 @@

Message log

if (!quorumKey) { throw new Error("failed to load quorum key"); } - + // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format const publicSignatureBuf = fromDerSignature(publicSignature); const publicKeyBuf = uint8arrayFromHexString(publicKey); @@ -710,7 +711,7 @@

Message log

} else { key = await TKHQ.encodeKey(privateKeyBytes, keyFormat); } - + // Display only the key displayKey(key); diff --git a/export/index.test.js b/export/index.test.js index cee535b..4caf612 100644 --- a/export/index.test.js +++ b/export/index.test.js @@ -200,7 +200,6 @@ describe("TKHQ", () => { // Invalid signature. Wrong integer tag for s expect(() => TKHQ.fromDerSignature("304502210088f4f3b59e277f30cb16c05541551eca702ce925002dbc3de3a7c0a7f76b23f903202a0f272c3e5724848dc5232c3409918277d65fd7e8c6eb1630bf6eb2eeb472e3")).toThrow("failed to convert DER-encoded signature: invalid tag for s"); - }) it("verifies enclave signature", async () => { diff --git a/import/index.html b/import/index.html index fb4f65e..2d86ec3 100644 --- a/import/index.html +++ b/import/index.html @@ -43,13 +43,11 @@ window.TKHQ = function() { /** constants for LocalStorage */ const TURNKEY_TARGET_EMBEDDED_KEY = "TURNKEY_TARGET_EMBEDDED_KEY" - /** Turnkey Signer enclave's quorum public key */ - const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Load a key to encrypt to as a CryptoKey and return it as a JSON Web Key. */ - const loadTargetKey = async (targetPublic) => { + async function loadTargetKey(targetPublic) { const targetKey = await crypto.subtle.importKey("raw", targetPublic, { name: 'ECDH', namedCurve: 'P-256', @@ -61,7 +59,7 @@ /* * Loads the quorum public key as a CryptoKey. */ - const loadQuorumKey = async quorumPublic => { + async function loadQuorumKey(quorumPublic) { return await crypto.subtle.importKey("raw", quorumPublic, { name: 'ECDSA', namedCurve: 'P-256' @@ -71,7 +69,7 @@ /** * Gets the current target embedded private key JWK. Returns `null` if not found. */ - const getTargetEmbeddedKey = () => { + function getTargetEmbeddedKey() { const jwtKey = window.localStorage.getItem(TURNKEY_TARGET_EMBEDDED_KEY); return jwtKey ? JSON.parse(jwtKey) : null; } @@ -80,21 +78,23 @@ * Sets the target embedded public key JWK. * @param {JsonWebKey} targetKey */ - const setTargetEmbeddedKey = targetKey => + function setTargetEmbeddedKey(targetKey) { window.localStorage.setItem(TURNKEY_TARGET_EMBEDDED_KEY, JSON.stringify(targetKey)); + } /** * Resets the current target embedded private key JWK. */ - const resetTargetEmbeddedKey = () => + function resetTargetEmbeddedKey() { window.localStorage.removeItem(TURNKEY_TARGET_EMBEDDED_KEY); + } /** * Takes a hex string (e.g. "e4567ab") and returns an array buffer (Uint8Array) * @param {string} hexString * @returns {Uint8Array} */ - const uint8arrayFromHexString = hexString => { + function uint8arrayFromHexString(hexString) { var hexRegex = /^[0-9A-Fa-f]+$/; if (!hexString || hexString.length % 2 != 0 || !hexRegex.test(hexString)) { throw new Error('cannot create uint8array from invalid hex string: "' + hexString + '"'); @@ -103,11 +103,11 @@ } /** - * Takes a Uint8Array and returns a hex string - * @param {Uint8Array} buffer - * @return {string} - */ - const uint8arrayToHexString = buffer => { + * Takes a Uint8Array and returns a hex string + * @param {Uint8Array} buffer + * @return {string} + */ + function uint8arrayToHexString(buffer) { return [...buffer] .map(x => x.toString(16).padStart(2, '0')) .join(''); @@ -169,7 +169,7 @@ * @param {string} privateKey * @param {string} keyFormat Can be "HEXADECIMAL" or "SOLANA" */ - const decodeKey = (privateKey, keyFormat) => { + function decodeKey(privateKey, keyFormat) { switch (keyFormat) { case "SOLANA": const decodedKeyBytes = base58Decode(privateKey); @@ -192,40 +192,51 @@ } /** - * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. + * Function to normalize padding of byte array with 0's to a target length */ - const additionalAssociatedData = (senderPubBuf, receiverPubBuf) => { - const s = Array.from(new Uint8Array(senderPubBuf)); - const r = Array.from(new Uint8Array(receiverPubBuf)); - return new Uint8Array([...s, ...r]); + function normalizePadding(byteArray, targetLength) { + const paddingLength = targetLength - byteArray.length; + + // Add leading 0's to array + if (paddingLength > 0) { + const padding = new Uint8Array(paddingLength).fill(0); + return new Uint8Array([...padding, ...byteArray]); + } + + // Remove leading 0's from array + if (paddingLength < 0) { + const expectedZeroCount = paddingLength * -1; + let zeroCount = 0; + for (let i = 0; i < expectedZeroCount && i < byteArray.length; i++) { + if (byteArray[i] === 0) { + zeroCount++; + } + } + // Check if the number of zeros found equals the number of zeroes expected + if (zeroCount !== expectedZeroCount) { + throw new Error(`invalid number of starting zeroes. Expected number of zeroes: ${expectedZeroCount}. Found: ${zeroCount}.`); + } + return byteArray.slice(expectedZeroCount, expectedZeroCount + targetLength); + } + return byteArray; } /** - * Function to verify enclave signature on import bundle received from the server. + * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. */ - const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { - // todo(olivia): throw error if enclave quorum public is null once server changes are deployed - if (enclaveQuorumPublic) { - if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { - throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); - } - } - - // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format - const publicSignatureBuf = fromDerSignature(publicSignature); - const publicKeyBuf = uint8arrayFromHexString(publicKey); - const encryptionQuorumPublicBuf = uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY); - const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); - return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); + function additionalAssociatedData(senderPubBuf, receiverPubBuf) { + const s = Array.from(new Uint8Array(senderPubBuf)); + const r = Array.from(new Uint8Array(receiverPubBuf)); + return new Uint8Array([...s, ...r]); } /** * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. */ - const fromDerSignature = (derSignature) => { + function fromDerSignature(derSignature) { const derSignatureBuf = uint8arrayFromHexString(derSignature); - // Skip the sequence tag (0x30) + // Check and skip the sequence tag (0x30) let index = 2; // Parse 'r' and check for integer tag (0x02) @@ -248,23 +259,37 @@ const s = derSignatureBuf.slice(index, index + sLength); // Normalize 'r' and 's' to 32 bytes each - const rPadded = padStartWithZeroes(r, 32); - const sPadded = padStartWithZeroes(s, 32); + const rPadded = normalizePadding(r, 32); + const sPadded = normalizePadding(s, 32); // Concatenate and return the raw signature return new Uint8Array([...rPadded, ...sPadded]); } /** - * Function to pad byte array with 0's + * Function to verify enclave signature on import bundle received from the server. */ - const padStartWithZeroes = (byteArray, targetLength) => { - const paddingLength = targetLength - byteArray.length; - if (paddingLength > 0) { - const padding = new Uint8Array(paddingLength).fill(0); - return new Uint8Array([...padding, ...byteArray]); + async function verifyEnclaveSignature(enclaveQuorumPublic, publicSignature, publicKey) { + /** Turnkey Signer enclave's public key */ + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"; + + // todo(olivia): throw error if enclave quorum public is null once server changes are deployed + if (enclaveQuorumPublic) { + if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { + throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); + } } - return byteArray; + + const encryptionQuorumPublicBuf = new Uint8Array(uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY)); + const quorumKey = await loadQuorumKey(encryptionQuorumPublicBuf); + if (!quorumKey) { + throw new Error("failed to load quorum key"); + } + + // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + const publicSignatureBuf = fromDerSignature(publicSignature); + const publicKeyBuf = uint8arrayFromHexString(publicKey); + return await crypto.subtle.verify({ name: "ECDSA", namedCurve: "P-256", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } /** @@ -272,7 +297,7 @@ * @param type message type. Can be "PUBLIC_KEY_CREATED" or "BUNDLE_INJECTED" * @param value message value */ - const sendMessageUp = (type, value) => { + function sendMessageUp(type, value) { if (window.top !== null) { window.top.postMessage({ "type": type, @@ -291,6 +316,8 @@ uint8arrayToHexString, base58Decode, decodeKey, + normalizePadding, + fromDerSignature, additionalAssociatedData, verifyEnclaveSignature } @@ -348,7 +375,7 @@ * @param {string} bundle * Example bundle: {"targetPublic":"0491ccb68758b822a6549257f87769eeed37c6cb68a6c6255c5f238e2b6e6e61838c8ac857f2e305970a6435715f84e5a2e4b02a4d1e5289ba7ec7910e47d2d50f","targetPublicSignature":"3045022100cefc333c330c9fa300d1aa10a439a76539b4d6967301638ab9edc9fd9468bfdb0220339bba7e2b00b45d52e941d068ecd3bfd16fd1926da69dd7769893268990d62f","enclaveQuorumPublic":"04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"} */ - const onInjectImportBundle = async bundle => { + async function onInjectImportBundle(bundle) { // Parse the import bundle const bundleObj = JSON.parse(bundle); @@ -376,7 +403,7 @@ * an `encrypted_bundle` containing the ciphertext and encapped public key. * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} */ - const onExtractWalletEncryptedBundle = async () => { + async function onExtractWalletEncryptedBundle() { // Get target embedded key from previous step (onInjectImportBundle) const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); if (targetPublicKeyJwk == null) { @@ -413,7 +440,7 @@ * an `encrypted_bundle` containing the ciphertext and encapped public key. * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} */ - const onExtractKeyEncryptedBundle = async keyFormat => { + async function onExtractKeyEncryptedBundle(keyFormat) { // Get target embedded key from previous step (onInjectImportBundle) const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); if (targetPublicKeyJwk == null) { @@ -441,7 +468,7 @@ TKHQ.sendMessageUp("ENCRYPTED_BUNDLE_EXTRACTED", encryptedBundle) } - const HpkeEncrypt = async ({ plaintextBuf, receiverPubJwk }) => { + async function HpkeEncrypt({ plaintextBuf, receiverPubJwk }) { const kemContext = new hpke.DhkemP256HkdfSha256(); const receiverPub = await kemContext.importKey("jwk", {...receiverPubJwk}, true); diff --git a/import/index.test.js b/import/index.test.js index af0cfe0..f424539 100644 --- a/import/index.test.js +++ b/import/index.test.js @@ -101,6 +101,46 @@ describe("TKHQ", () => { expect(true).toBe(true); }) + it("normalizes padding in a byte array", () => { + // Array with no leading 0's and a valid target length + const arr = new Uint8Array(32).fill(1); + expect(TKHQ.normalizePadding(arr, 32).length).toBe(32); + expect(TKHQ.normalizePadding(arr, 32)).toBe(arr); + + // Array with an extra leading 0 and valid target length + const zeroesArr = new Uint8Array(1).fill(0); + const zeroesLeadingArr = new Uint8Array([...zeroesArr, ...arr]); + expect(TKHQ.normalizePadding(zeroesLeadingArr, 32).length).toBe(32); + expect(TKHQ.normalizePadding(zeroesLeadingArr, 32)).toStrictEqual(arr); + + // Array with a missing leading 0 and valid target length + const zeroesMissingArr = new Uint8Array(31).fill(1); + const paddedArr = new Uint8Array(32); + paddedArr.fill(1, 1); + expect(TKHQ.normalizePadding(zeroesMissingArr, 32).length).toBe(32); + expect(Array.from(TKHQ.normalizePadding(zeroesMissingArr, 32))).toStrictEqual(Array.from(paddedArr)); + + // Array with an extra leading 0 and invalid zero count + expect(() => TKHQ.normalizePadding(zeroesLeadingArr, 31)).toThrow("invalid number of starting zeroes. Expected number of zeroes: 2. Found: 1."); + }) + + it("decodes a ASN.1 DER-encoded signature to raw format", () => { + // Valid signature where r and s don't need padding + expect(TKHQ.fromDerSignature("304402202b769b6dd410ff8a1cbcd5dd7fb2733e80f11922443b1eb629e6e538d1054c3b022020b9715d140f079190123411370971cc6daba8e61b6b58d36321c31ae331799b").length).toBe(64); + + // Valid signature where r and s have extra padding + expect(TKHQ.fromDerSignature("3046022100b71f5a377a7ae6d245d1aa22145f52f7c7d87fcaf7c68c60f43fecf3817b22cf022100cdea30eb54c099a8c86b14c3d2c4accd59c21fbeacd878842d5e9bdd39d19d55").length).toBe(64); + + // Valid signature where r has extra padding + expect(TKHQ.fromDerSignature("304502210088f4f3b59e277f30cb16c05541551eca702ce925002dbc3de3a7c0a7f76b23f902202a0f272c3e5724848dc5232c3409918277d65fd7e8c6eb1630bf6eb2eeb472e3").length).toBe(64); + + // Invalid signature. Wrong integer tag for r + expect(() => TKHQ.fromDerSignature("304503210088f4f3b59e277f30cb16c05541551eca702ce925002dbc3de3a7c0a7f76b23f902202a0f272c3e5724848dc5232c3409918277d65fd7e8c6eb1630bf6eb2eeb472e3")).toThrow("failed to convert DER-encoded signature: invalid tag for r"); + + // Invalid signature. Wrong integer tag for s + expect(() => TKHQ.fromDerSignature("304502210088f4f3b59e277f30cb16c05541551eca702ce925002dbc3de3a7c0a7f76b23f903202a0f272c3e5724848dc5232c3409918277d65fd7e8c6eb1630bf6eb2eeb472e3")).toThrow("failed to convert DER-encoded signature: invalid tag for s"); + }) + it("verifies enclave signature", async () => { // No "enclaveQuorumPublic" field in the export bundle. Valid signature let verified = await TKHQ.verifyEnclaveSignature(null, "30440220773382ac39085f58a584fd5ad8c8b91b50993ad480af2c5eaefe0b09447b6dca02205201c8e20a92bce524caac08a956b0c2e7447de9c68f91ab1e09fd58988041b5", "04e479640d6d3487bbf132f6258ee24073411b8325ea68bb28883e45b650d059f82c48db965b8f777b30ab9e7810826bfbe8ad1789f9f10bf76dcd36b2ee399bc5"); diff --git a/import/standalone.html b/import/standalone.html index b704df4..1d1e714 100644 --- a/import/standalone.html +++ b/import/standalone.html @@ -106,13 +106,11 @@

Message log

window.TKHQ = function() { /** constants for LocalStorage */ const TURNKEY_TARGET_EMBEDDED_KEY = "TURNKEY_TARGET_EMBEDDED_KEY" - /** Turnkey Signer enclave's public key */ - const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569" /* * Load a key to encrypt to as a CryptoKey and return it as a JSON Web Key. */ - const loadTargetKey = async (targetPublic) => { + async function loadTargetKey(targetPublic) { const targetKey = await crypto.subtle.importKey("raw", targetPublic, { name: 'ECDH', namedCurve: 'P-256', @@ -124,7 +122,7 @@

Message log

/* * Loads the quorum public key as a CryptoKey. */ - const loadQuorumKey = async quorumPublic => { + async function loadQuorumKey(quorumPublic) { return await crypto.subtle.importKey("raw", quorumPublic, { name: 'ECDSA', namedCurve: 'P-256' @@ -134,7 +132,7 @@

Message log

/** * Gets the current target embedded private key JWK. Returns `null` if not found. */ - const getTargetEmbeddedKey = () => { + function getTargetEmbeddedKey() { const jwtKey = window.localStorage.getItem(TURNKEY_TARGET_EMBEDDED_KEY); return jwtKey ? JSON.parse(jwtKey) : null; } @@ -143,21 +141,23 @@

Message log

* Sets the target embedded public key JWK. * @param {JsonWebKey} targetKey */ - const setTargetEmbeddedKey = targetKey => + function setTargetEmbeddedKey(targetKey) { window.localStorage.setItem(TURNKEY_TARGET_EMBEDDED_KEY, JSON.stringify(targetKey)); + } /** * Resets the current target embedded private key JWK. */ - const resetTargetEmbeddedKey = () => + function resetTargetEmbeddedKey() { window.localStorage.removeItem(TURNKEY_TARGET_EMBEDDED_KEY); + } /** * Takes a hex string (e.g. "e4567ab") and returns an array buffer (Uint8Array) * @param {string} hexString * @returns {Uint8Array} */ - const uint8arrayFromHexString = hexString => { + function uint8arrayFromHexString(hexString) { var hexRegex = /^[0-9A-Fa-f]+$/; if (!hexString || hexString.length % 2 != 0 || !hexRegex.test(hexString)) { throw new Error('cannot create uint8array from invalid hex string: "' + hexString + '"'); @@ -166,11 +166,11 @@

Message log

} /** - * Takes a Uint8Array and returns a hex string - * @param {Uint8Array} buffer - * @return {string} - */ - const uint8arrayToHexString = buffer => { + * Takes a Uint8Array and returns a hex string + * @param {Uint8Array} buffer + * @return {string} + */ + function uint8arrayToHexString(buffer) { return [...buffer] .map(x => x.toString(16).padStart(2, '0')) .join(''); @@ -182,7 +182,7 @@

Message log

* @param {string} s The base58-encoded string. * @return {Uint8Array} The decoded buffer. */ - function base58Decode(s) { + function base58Decode(s) { // See https://en.bitcoin.it/wiki/Base58Check_encoding var alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; var decoded = BigInt(0); @@ -232,7 +232,7 @@

Message log

* @param {string} privateKey * @param {string} keyFormat Can be "HEXADECIMAL" or "SOLANA" */ - const decodeKey = (privateKey, keyFormat) => { + function decodeKey(privateKey, keyFormat) { switch (keyFormat) { case "SOLANA": const decodedKeyBytes = base58Decode(privateKey); @@ -255,40 +255,51 @@

Message log

} /** - * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. + * Function to normalize padding of byte array with 0's to a target length */ - const additionalAssociatedData = (senderPubBuf, receiverPubBuf) => { - const s = Array.from(new Uint8Array(senderPubBuf)); - const r = Array.from(new Uint8Array(receiverPubBuf)); - return new Uint8Array([...s, ...r]); + function normalizePadding(byteArray, targetLength) { + const paddingLength = targetLength - byteArray.length; + + // Add leading 0's to array + if (paddingLength > 0) { + const padding = new Uint8Array(paddingLength).fill(0); + return new Uint8Array([...padding, ...byteArray]); + } + + // Remove leading 0's from array + if (paddingLength < 0) { + const expectedZeroCount = paddingLength * -1; + let zeroCount = 0; + for (let i = 0; i < expectedZeroCount && i < byteArray.length; i++) { + if (byteArray[i] === 0) { + zeroCount++; + } + } + // Check if the number of zeros found equals the number of zeroes expected + if (zeroCount !== expectedZeroCount) { + throw new Error(`invalid number of starting zeroes. Expected number of zeroes: ${expectedZeroCount}. Found: ${zeroCount}.`); + } + return byteArray.slice(expectedZeroCount, expectedZeroCount + targetLength); + } + return byteArray; } /** - * Function to verify enclave signature on import bundle received from the server. + * Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate. */ - const verifyEnclaveSignature = async (enclaveQuorumPublic, publicSignature, publicKey) => { - // todo(olivia): throw error if enclave quorum public is null once server changes are deployed - if (enclaveQuorumPublic) { - if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { - throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); - } - } - - // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format - const publicSignatureBuf = fromDerSignature(publicSignature); - const publicKeyBuf = uint8arrayFromHexString(publicKey); - const encryptionQuorumPublicBuf = uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY); - const quorumKey = await loadQuorumKey(new Uint8Array(encryptionQuorumPublicBuf)); - return await crypto.subtle.verify( { name: "ECDSA", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); + function additionalAssociatedData(senderPubBuf, receiverPubBuf) { + const s = Array.from(new Uint8Array(senderPubBuf)); + const r = Array.from(new Uint8Array(receiverPubBuf)); + return new Uint8Array([...s, ...r]); } /** * Converts an ASN.1 DER-encoded ECDSA signature to the raw format that WebCrypto uses. */ - const fromDerSignature = (derSignature) => { + function fromDerSignature(derSignature) { const derSignatureBuf = uint8arrayFromHexString(derSignature); - // Skip the sequence tag (0x30) + // Check and skip the sequence tag (0x30) let index = 2; // Parse 'r' and check for integer tag (0x02) @@ -311,23 +322,37 @@

Message log

const s = derSignatureBuf.slice(index, index + sLength); // Normalize 'r' and 's' to 32 bytes each - const rPadded = padStartWithZeroes(r, 32); - const sPadded = padStartWithZeroes(s, 32); + const rPadded = normalizePadding(r, 32); + const sPadded = normalizePadding(s, 32); // Concatenate and return the raw signature return new Uint8Array([...rPadded, ...sPadded]); } /** - * Function to pad byte array with 0's + * Function to verify enclave signature on import bundle received from the server. */ - const padStartWithZeroes = (byteArray, targetLength) => { - const paddingLength = targetLength - byteArray.length; - if (paddingLength > 0) { - const padding = new Uint8Array(paddingLength).fill(0); - return new Uint8Array([...padding, ...byteArray]); + async function verifyEnclaveSignature(enclaveQuorumPublic, publicSignature, publicKey) { + /** Turnkey Signer enclave's public key */ + const TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY = "04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"; + + // todo(olivia): throw error if enclave quorum public is null once server changes are deployed + if (enclaveQuorumPublic) { + if (enclaveQuorumPublic !== TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY) { + throw new Error(`enclave quorum public keys from client and bundle do not match. Client: ${TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY}. Bundle: ${enclaveQuorumPublic}.`); + } } - return byteArray; + + const encryptionQuorumPublicBuf = new Uint8Array(uint8arrayFromHexString(TURNKEY_SIGNER_ENCLAVE_QUORUM_PUBLIC_KEY)); + const quorumKey = await loadQuorumKey(encryptionQuorumPublicBuf); + if (!quorumKey) { + throw new Error("failed to load quorum key"); + } + + // The ECDSA signature is ASN.1 DER encoded but WebCrypto uses raw format + const publicSignatureBuf = fromDerSignature(publicSignature); + const publicKeyBuf = uint8arrayFromHexString(publicKey); + return await crypto.subtle.verify({ name: "ECDSA", namedCurve: "P-256", hash: {name: "SHA-256" }}, quorumKey, publicSignatureBuf, publicKeyBuf); } /** @@ -335,7 +360,7 @@

Message log

* @param type message type. Can be "PUBLIC_KEY_CREATED" or "BUNDLE_INJECTED" * @param value message value */ - const sendMessageUp = (type, value) => { + function sendMessageUp(type, value) { if (window.top !== null) { window.top.postMessage({ "type": type, @@ -348,7 +373,7 @@

Message log

/** * Function to log a message and persist it in the page's DOM. */ - const logMessage = content => { + function logMessage(content) { const messageLog = document.getElementById("message-log"); const message = document.createElement("p") message.innerText = content; @@ -366,6 +391,8 @@

Message log

uint8arrayToHexString, base58Decode, decodeKey, + normalizePadding, + fromDerSignature, additionalAssociatedData, verifyEnclaveSignature } @@ -454,7 +481,7 @@

Message log

* @param {string} bundle * Example bundle: {"targetPublic":"0491ccb68758b822a6549257f87769eeed37c6cb68a6c6255c5f238e2b6e6e61838c8ac857f2e305970a6435715f84e5a2e4b02a4d1e5289ba7ec7910e47d2d50f","targetPublicSignature":"3045022100cefc333c330c9fa300d1aa10a439a76539b4d6967301638ab9edc9fd9468bfdb0220339bba7e2b00b45d52e941d068ecd3bfd16fd1926da69dd7769893268990d62f","enclaveQuorumPublic":"04cf288fe433cc4e1aa0ce1632feac4ea26bf2f5a09dcfe5a42c398e06898710330f0572882f4dbdf0f5304b8fc8703acd69adca9a4bbf7f5d00d20a5e364b2569"} */ - const onInjectImportBundle = async bundle => { + async function onInjectImportBundle(bundle) { // Parse the import bundle const bundleObj = JSON.parse(bundle); @@ -482,7 +509,7 @@

Message log

* an `encrypted_bundle` containing the ciphertext and encapped public key. * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} */ - const onExtractWalletEncryptedBundle = async bundle => { + async function onExtractWalletEncryptedBundle(bundle) { // Get target embedded key from previous step (onInjectImportBundle) const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); if (targetPublicKeyJwk == null) { @@ -519,7 +546,7 @@

Message log

* an `encrypted_bundle` containing the ciphertext and encapped public key. * Example bundle: {"encappedPublic":"0497f33f3306f67f4402d4824e15b63b04786b6558d417aac2fef69051e46fa7bfbe776b142e4ded4f02097617a7588e93c53b71f900a4a8831a31be6f95e5f60f","ciphertext":"c17c3085505f3c094f0fa61791395b83ab1d8c90bdf9f12a64fc6e2e9cba266beb528f65c88bd933e36e6203752a9b63e6a92290a0ab6bf0ed591cf7bfa08006001e2cc63870165dc99ec61554ffdc14dea7d567e62cceed29314ae6c71a013843f5c06146dee5bf9c1d"} */ - const onExtractKeyEncryptedBundle = async (bundle, keyFormat) => { + async function onExtractKeyEncryptedBundle(bundle, keyFormat) { // Get target embedded key from previous step (onInjectImportBundle) const targetPublicKeyJwk = TKHQ.getTargetEmbeddedKey(); if (targetPublicKeyJwk == null) { @@ -547,7 +574,7 @@

Message log

TKHQ.sendMessageUp("ENCRYPTED_BUNDLE_EXTRACTED", encryptedBundle) } - const HpkeEncrypt = async ({ plaintextBuf, receiverPubJwk }) => { + async function HpkeEncrypt({ plaintextBuf, receiverPubJwk }) { const kemContext = new hpke.DhkemP256HkdfSha256(); const receiverPub = await kemContext.importKey("jwk", {...receiverPubJwk}, true);