From 8f6df72e11972579c566158e743c2e9aa484b593 Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 17 Oct 2023 13:27:59 -0500 Subject: [PATCH 1/2] Uncompress public key before running ECDH --- index.html | 124 ++++++++++++++++++++++++++++++++++++++++++++++---- index.test.js | 18 +++++++- 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 0555989..b31cc8c 100644 --- a/index.html +++ b/index.html @@ -393,6 +393,18 @@

Message log

return base64urlEncode(buffer) } + /** + * Converts a `BigInt` into a hex encoded string + * @param {BigInt} num + * @return {string} + */ + var bigIntToHex = function(num) { + var hexString = num.toString(16); + // Add an extra 0 to the start of the string to get a valid hex string (even length) + // (e.g. 0x0123 instead of 0x123) + return hexString.padStart(Math.ceil(hexString.length/2)*2, 0) + } + /** * Accepts a public key array buffer, and returns a buffer with the compressed version of the public key * @param {Uint8Array} rawPublicKey @@ -412,6 +424,94 @@

Message log

return compressedBytes } + /** + * Accepts a public key array buffer, and returns a buffer with the uncomrpessed version of the public key + * @param {Uint8Array} rawPublicKey + * @return {Uint8Array} the uncompressed bytes + */ + var uncompressRawPublicKey = function(rawPublicKey) { + const len = rawPublicKey.byteLength + + // point[0] must be 2 (false) or 3 (true). + // this maps to the initial "02" or "03" prefix + const lsb = rawPublicKey[0] === 3; + const x = BigInt("0x" + uint8arrayToHexString(rawPublicKey.subarray(1))); + + // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf (Appendix D). + const p = BigInt("115792089210356248762697446949407573530086143415290314195533631308867097853951"); + const b = BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b"); + const a = p - BigInt(3); + + // Now compute y based on x + const rhs = ((x * x + a) * x + b) % p; + let y = modSqrt(rhs, p); + if (lsb !== testBit(y, 0)) { + y = (p - y) % p; + } + + if (x < BigInt(0) || x >= p) { + throw new Error("x is out of range"); + } + + if (y < BigInt(0) || y >= p) { + throw new Error("y is out of range"); + } + + var uncompressedHexString = "04" + bigIntToHex(x) + bigIntToHex(y); + return uint8arrayFromHexString(uncompressedHexString) + } + + /** + * Private helper to compute square root modulo p + */ + function modSqrt(x, p) { + if (p <= BigInt(0)) { + throw new Error("p must be positive"); + } + const base = x % p; + // The currently supported NIST curves P-256, P-384, and P-521 all satisfy + // p % 4 == 3. However, although currently a no-op, the following check + // should be left in place in case other curves are supported in the future. + if (testBit(p, 0) && /* istanbul ignore next */ testBit(p, 1)) { + // Case p % 4 == 3 (applies to NIST curves P-256, P-384, and P-521) + // q = (p + 1) / 4 + const q = (p + BigInt(1)) >> BigInt(2); + const squareRoot = modPow(base, q, p); + if ((squareRoot * squareRoot) % p !== base) { + throw new Error("could not find a modular square root"); + } + return squareRoot; + } + // Skipping other elliptic curve types that require Cipolla's algorithm. + throw new Error("unsupported modulus value"); + } + + /** + * Private helper function used by `modSqrt` + */ + function modPow(b, exp, p) { + if (exp === BigInt(0)) { + return BigInt(1); + } + let result = b; + const exponentBitString = exp.toString(2); + for (let i = 1; i < exponentBitString.length; ++i) { + result = (result * result) % p; + if (exponentBitString[i] === "1") { + result = (result * b) % p; + } + } + return result; + } + + /** + * Another private helper function used as part of `modSqrt` + */ + function testBit(n, i) { + const m = BigInt(1) << BigInt(i); + return (n & m) !== BigInt(0); + } + /********************************************************************************************** * Start of private crypto implementation for P256 public key derivation from a private key. * ---- @@ -609,12 +709,14 @@

Message log

clearEmbeddedKey, importRecoveryCredential, compressRawPublicKey, + uncompressRawPublicKey, p256JWKPrivateToPublic, convertEcdsaIeee1363ToDer, sendMessageUp, logMessage, base64urlEncode, base64urlDecode, + bigIntToHex, stringToBase64urlString, uint8arrayToHexString, uint8arrayFromHexString, @@ -685,19 +787,22 @@

Message log

/** * Function triggered when INJECT_RECOVERY_BUNDLE event is received. * The `bundle` param is the concatenation of a public key and an encrypted payload, and then base64 encoded - * Example: BNcKOotRE5od6mmnbLicwH29uUz_EbScMCU0EfmSZguTQIWC40kwdpmpFDwzYgr0jDgbSndeqPsX2kUpYQ_SCg-d1l7GSfiNZQ_P7MIgWtq2ZQx7jlbhNIyNW90fyHwcV_q3AwYl0qRlX7rHyoFRi98 + * Example: A6ZPGAlxBRZhjKWky4RpXnHVceGzJjTuBrzKvMGnIgZ3r6JD4D1iiSg_m-y_u0BgJKI397Xjn0wgu17w9wuRooEp-F38m4ql57FgQ7sX9nQA * @param {string} bundle */ var onInjectBundle = async function(bundle) { - if (bundle.length <= 130) { - throw new Error("bundle size is too low. Expecting an uncompressed public key (130 chars) and an encrypted bundle!") - } var bundleBytes = TKHQ.base64urlDecode(bundle); + if (bundleBytes.byteLength <= 33) { + throw new Error("bundle size " + bundleBytes.byteLength + " is too low. Expecting a compressed public key (33 bytes) and an encrypted credential") + } - var encappedKeyBuf = bundleBytes.subarray(0,65); - var ciphertextBuf = bundleBytes.subarray(65); - + var compressedEncappedKeyBuf = bundleBytes.subarray(0,33); + var ciphertextBuf = bundleBytes.subarray(33); var embeddedKeyJwk = await TKHQ.getEmbeddedKey(); + + // Decompress the compressed key + var encappedKeyBuf = TKHQ.uncompressRawPublicKey(compressedEncappedKeyBuf); + var recoveryCredentialBytes = await HpkeDecrypt( { ciphertextBuf, @@ -710,13 +815,12 @@

Message log

} /** * Function triggered when STAMP_REQUEST event is received. - * @param {string} payload hex-encoded string containing the bytes to sign. + * @param {string} payload to sign */ var onStampRequest = async function(payload) { if (RECOVERY_CREDENTIAL_BYTES === null) { throw new Error("cannot sign payload without credential. Credential bytes are null"); } - var challengeBytes = TKHQ.uint8arrayFromHexString(payload); var recoveryKey = await TKHQ.importRecoveryCredential(RECOVERY_CREDENTIAL_BYTES) var signatureIeee1363 = await window.crypto.subtle.sign( { @@ -724,7 +828,7 @@

Message log

hash: {name: "SHA-256"}, }, recoveryKey, - challengeBytes.buffer + new TextEncoder().encode(payload) ); var derSignature = TKHQ.convertEcdsaIeee1363ToDer(new Uint8Array(signatureIeee1363)); diff --git a/index.test.js b/index.test.js index 6b17f2f..9af473b 100644 --- a/index.test.js +++ b/index.test.js @@ -73,8 +73,17 @@ describe("TKHQ", () => { }) it("compresses raw P-256 public keys", async () => { - let compressed = await TKHQ.compressRawPublicKey(TKHQ.uint8arrayFromHexString("04c6de3e1d08270d39076651a2b14fd38031dae89892dc124d2f9557816e7e5da4f510c344715f84cf0ba0cc71bd04136c0fb2633a3f459e68ffb8620be16900f0")); - expect(compressed).toEqual(TKHQ.uint8arrayFromHexString("02c6de3e1d08270d39076651a2b14fd38031dae89892dc124d2f9557816e7e5da4", "hex")); + let compressed02 = TKHQ.compressRawPublicKey(TKHQ.uint8arrayFromHexString("04c6de3e1d08270d39076651a2b14fd38031dae89892dc124d2f9557816e7e5da4f510c344715f84cf0ba0cc71bd04136c0fb2633a3f459e68ffb8620be16900f0")); + expect(compressed02).toEqual(TKHQ.uint8arrayFromHexString("02c6de3e1d08270d39076651a2b14fd38031dae89892dc124d2f9557816e7e5da4", "hex")); + let compressed03 = TKHQ.compressRawPublicKey(TKHQ.uint8arrayFromHexString("04be3c8147b75405c94e24280a1759374688bf689549cc1c0afd8e8af20621d734dab002b3cced5db9d9cd343b7d2197c757f42dea13f6689b3553ab1c667a8c67")); + expect(compressed03).toEqual(TKHQ.uint8arrayFromHexString("03be3c8147b75405c94e24280a1759374688bf689549cc1c0afd8e8af20621d734", "hex")); + }) + + it("uncompresses raw P-256 public keys", async () => { + let uncompressedFrom02 = TKHQ.uncompressRawPublicKey(TKHQ.uint8arrayFromHexString("02c6de3e1d08270d39076651a2b14fd38031dae89892dc124d2f9557816e7e5da4")); + expect(uncompressedFrom02).toEqual(TKHQ.uint8arrayFromHexString("04c6de3e1d08270d39076651a2b14fd38031dae89892dc124d2f9557816e7e5da4f510c344715f84cf0ba0cc71bd04136c0fb2633a3f459e68ffb8620be16900f0", "hex")); + let uncompressedFrom03 = TKHQ.uncompressRawPublicKey(TKHQ.uint8arrayFromHexString("03be3c8147b75405c94e24280a1759374688bf689549cc1c0afd8e8af20621d734")); + expect(uncompressedFrom03).toEqual(TKHQ.uint8arrayFromHexString("04be3c8147b75405c94e24280a1759374688bf689549cc1c0afd8e8af20621d734dab002b3cced5db9d9cd343b7d2197c757f42dea13f6689b3553ab1c667a8c67", "hex")); }) it("contains p256JWKPrivateToPublic", async () => { @@ -112,6 +121,11 @@ describe("TKHQ", () => { expect(TKHQ.uint8arrayFromHexString("627566666572").toString()).toEqual("98,117,102,102,101,114"); }) + it("contains bigIntToHex", () => { + expect(TKHQ.bigIntToHex(BigInt(1))).toEqual("01"); + expect(TKHQ.bigIntToHex(BigInt(23))).toEqual("17"); + expect(TKHQ.bigIntToHex(BigInt(255))).toEqual("ff"); + }) it("logs messages and sends messages up", async () => { // TODO: test logMessage / sendMessageUp From ee45879f29ccfc4c6f9f17d8bbf2f5689481979d Mon Sep 17 00:00:00 2001 From: Arnaud Brousseau Date: Tue, 17 Oct 2023 16:37:07 -0500 Subject: [PATCH 2/2] Update bigIntToHex to take an expected length --- index.html | 13 ++++++++----- index.test.js | 9 ++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index b31cc8c..50c36a1 100644 --- a/index.html +++ b/index.html @@ -396,13 +396,16 @@

Message log

/** * Converts a `BigInt` into a hex encoded string * @param {BigInt} num + * @param {number} length expected length of the resulting hex string * @return {string} */ - var bigIntToHex = function(num) { + var bigIntToHex = function(num, length) { var hexString = num.toString(16); - // Add an extra 0 to the start of the string to get a valid hex string (even length) - // (e.g. 0x0123 instead of 0x123) - return hexString.padStart(Math.ceil(hexString.length/2)*2, 0) + if (hexString.length > length) { + throw new Error("number cannot fit in a hex string of " + length + " characters"); + } + // Add an extra 0 to the start of the string to get to `length` + return hexString.padStart(length, 0) } /** @@ -457,7 +460,7 @@

Message log

throw new Error("y is out of range"); } - var uncompressedHexString = "04" + bigIntToHex(x) + bigIntToHex(y); + var uncompressedHexString = "04" + bigIntToHex(x, 64) + bigIntToHex(y, 64); return uint8arrayFromHexString(uncompressedHexString) } diff --git a/index.test.js b/index.test.js index 9af473b..bf58b86 100644 --- a/index.test.js +++ b/index.test.js @@ -122,9 +122,12 @@ describe("TKHQ", () => { }) it("contains bigIntToHex", () => { - expect(TKHQ.bigIntToHex(BigInt(1))).toEqual("01"); - expect(TKHQ.bigIntToHex(BigInt(23))).toEqual("17"); - expect(TKHQ.bigIntToHex(BigInt(255))).toEqual("ff"); + expect(TKHQ.bigIntToHex(BigInt(1, 1))).toEqual("1"); + expect(TKHQ.bigIntToHex(BigInt(1), 2)).toEqual("01"); + expect(TKHQ.bigIntToHex(BigInt(1), 4)).toEqual("0001"); + expect(TKHQ.bigIntToHex(BigInt(23), 2)).toEqual("17"); + expect(TKHQ.bigIntToHex(BigInt(255), 2)).toEqual("ff"); + expect(() => { TKHQ.bigIntToHex(BigInt(256), 2) }).toThrow("number cannot fit in a hex string of 2 characters"); }) it("logs messages and sends messages up", async () => {