Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ENG-1446: Verify enclave sig in export/import #31

Merged
merged 8 commits into from
Apr 3, 2024
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
directory: ["auth", "export"]
directory: ["auth", "export", "import"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woah. Good catch!


steps:
- name: Checkout
Expand Down
164 changes: 141 additions & 23 deletions export/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,20 @@ <h2>Message log</h2>
/** 48 hours in milliseconds */
const TURNKEY_EMBEDDED_KEY_TTL_IN_MILLIS = 1000 * 60 * 60 * 48;

/*
* Loads the quorum public key as a CryptoKey.
*/
async function loadQuorumKey(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
*/
const initEmbeddedKey = async () => {
async function initEmbeddedKey() {
const retrievedKey = await getEmbeddedKey();
if (retrievedKey === null) {
const targetKey = await generateTargetKey();
Expand All @@ -137,7 +147,7 @@ <h2>Message log</h2>
/*
* 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',
Expand All @@ -149,7 +159,7 @@ <h2>Message log</h2>
/**
* 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;
}
Expand All @@ -158,22 +168,24 @@ <h2>Message log</h2>
* 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
* @param {string} key
* @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,
Expand All @@ -188,7 +200,7 @@ <h2>Message log</h2>
* expiry time is missing.
* @param {string} key
*/
const getItemWithExpiry = (key) => {
function getItemWithExpiry(key) {
const itemStr = window.localStorage.getItem(key);
if (!itemStr) {
return null;
Expand All @@ -206,13 +218,12 @@ <h2>Message log</h2>
return item.value;
};


/**
* Takes a hex string (e.g. "e4567ab") and returns an array buffer (Uint8Array)
* @param {string} hexString
* @returns {Uint8Array}
*/
const uint8arrayFromHexString = function(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 + '"');
Expand All @@ -225,27 +236,119 @@ <h2>Message log</h2>
* @param {Uint8Array} buffer
* @return {string}
*/
const uint8arrayToHexString = buffer => {
function uint8arrayToHexString(buffer) {
return [...buffer]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}

/**
* Function to normalize padding of byte array with 0's to a target length
*/
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;
}

/**
* Additional Associated Data (AAD) in the format dictated by the enclave_encrypt crate.
*/
const additionalAssociatedData = (senderPubBuf, receiverPubBuf) => {
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.
*/
function fromDerSignature(derSignature) {
const derSignatureBuf = uint8arrayFromHexString(derSignature);

// Check and 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 = normalizePadding(r, 32);
const sPadded = normalizePadding(s, 32);

// Concatenate and return the raw signature
return new Uint8Array([...rPadded, ...sPadded]);
}

/**
* Function to verify enclave signature on import bundle received from the server.
*/
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}.`);
}
}

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);
}

/**
* 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"
* @param value message value
*/
const sendMessageUp = (type, value) => {
function sendMessageUp(type, value) {
if (window.top !== null) {
window.top.postMessage({
"type": type,
Expand All @@ -258,7 +361,7 @@ <h2>Message log</h2>
/**
* 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;
Expand All @@ -270,7 +373,7 @@ <h2>Message log</h2>
* 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
Expand Down Expand Up @@ -374,7 +477,7 @@ <h2>Message log</h2>
* @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) {
Expand Down Expand Up @@ -403,7 +506,7 @@ <h2>Message log</h2>
* 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;
Expand Down Expand Up @@ -440,7 +543,10 @@ <h2>Message log</h2>
logMessage,
uint8arrayFromHexString,
uint8arrayToHexString,
additionalAssociatedData
normalizePadding,
fromDerSignature,
additionalAssociatedData,
verifyEnclaveSignature
}
}();
</script>
Expand Down Expand Up @@ -526,7 +632,7 @@ <h2>Message log</h2>
* 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';
Expand Down Expand Up @@ -556,11 +662,23 @@ <h2>Message log</h2>
* 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 => {
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: ${bundle}`);
}

// Decrypt the ciphertext
const encappedKeyBuf = TKHQ.uint8arrayFromHexString(bundleObj.encappedPublic);
const ciphertextBuf = TKHQ.uint8arrayFromHexString(bundleObj.ciphertext);
const embeddedKeyJwk = await TKHQ.getEmbeddedKey();
Expand All @@ -576,7 +694,7 @@ <h2>Message log</h2>
* 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);

Expand Down Expand Up @@ -605,7 +723,7 @@ <h2>Message log</h2>
* 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);

Expand All @@ -626,7 +744,7 @@ <h2>Message log</h2>
* 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);

Expand Down
Loading
Loading