From ef8532e89d0caf8156b822805b38c1f3953c1090 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Sep 2024 02:45:09 +0200 Subject: [PATCH 1/3] Fix P-521 bit length in Web Crypto ECDH over P-521 returns 66 bytes, i.e. 528 bits. Requesting 521 bits entails a truncation of the derived value, which seems unintended. --- packages/crypto/src/keys/ecdh/index.browser.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/crypto/src/keys/ecdh/index.browser.ts b/packages/crypto/src/keys/ecdh/index.browser.ts index 63d3470673..4b880cf871 100644 --- a/packages/crypto/src/keys/ecdh/index.browser.ts +++ b/packages/crypto/src/keys/ecdh/index.browser.ts @@ -7,13 +7,13 @@ import webcrypto from '../../webcrypto/index.js' import type { Curve } from './index.js' import type { ECDHKey, ECDHKeyPair, JWKEncodedPrivateKey, JWKEncodedPublicKey } from '../interface.js' -const bits = { - 'P-256': 256, - 'P-384': 384, - 'P-521': 521 +const curveLengths = { + 'P-256': 32, + 'P-384': 48, + 'P-521': 66 } -const curveTypes = Object.keys(bits) +const curveTypes = Object.keys(curveLengths) const names = curveTypes.join(' / ') export async function generateEphemeralKeyPair (curve: Curve): Promise { @@ -68,7 +68,7 @@ export async function generateEphemeralKeyPair (curve: Curve): Promise public: key }, privateKey, - bits[curve] + curveLengths[curve] * 8 ) return new Uint8Array(buffer, 0, buffer.byteLength) @@ -84,12 +84,6 @@ export async function generateEphemeralKeyPair (curve: Curve): Promise return ecdhKey } -const curveLengths = { - 'P-256': 32, - 'P-384': 48, - 'P-521': 66 -} - // Marshal converts a jwk encoded ECDH public key into the // form specified in section 4.3.6 of ANSI X9.62. (This is the format // go-ipfs uses) From 7353bf6ab4d9567f6fe477b4c476777847336632 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Sep 2024 02:47:43 +0200 Subject: [PATCH 2/3] Remove superfluous `namedCurve` property in deriveBits algorithm --- packages/crypto/src/keys/ecdh/index.browser.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/crypto/src/keys/ecdh/index.browser.ts b/packages/crypto/src/keys/ecdh/index.browser.ts index 4b880cf871..742bd19a7b 100644 --- a/packages/crypto/src/keys/ecdh/index.browser.ts +++ b/packages/crypto/src/keys/ecdh/index.browser.ts @@ -63,8 +63,6 @@ export async function generateEphemeralKeyPair (curve: Curve): Promise const buffer = await webcrypto.get().subtle.deriveBits( { name: 'ECDH', - // @ts-expect-error namedCurve is missing from the types - namedCurve: curve, public: key }, privateKey, From 6270bf2facf6ca8e60f2d0c58adc76e74610b30d Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Fri, 20 Sep 2024 15:37:45 +0200 Subject: [PATCH 3/3] Test P-521 --- .../crypto/test/fixtures/go-elliptic-key.ts | 62 ++++++++++++++----- .../crypto/test/keys/ephemeral-keys.spec.ts | 36 ++++++----- 2 files changed, 66 insertions(+), 32 deletions(-) diff --git a/packages/crypto/test/fixtures/go-elliptic-key.ts b/packages/crypto/test/fixtures/go-elliptic-key.ts index e5257a2d46..476abc47f7 100644 --- a/packages/crypto/test/fixtures/go-elliptic-key.ts +++ b/packages/crypto/test/fixtures/go-elliptic-key.ts @@ -1,21 +1,53 @@ import type { Curve } from '../../src/keys/ecdh/index.js' -export interface GoEllipticKey { - curve: Curve - bob: { - private: Uint8Array - public: Uint8Array +type GoEllipticKeys = { + [key in Curve]: { + alice: { + private: Uint8Array + public: Uint8Array + } + bob: { + private: Uint8Array + public: Uint8Array + } + shared: Uint8Array } } -export default { - curve: 'P-256', - bob: { - private: Uint8Array.from([ - 181, 217, 162, 151, 225, 36, 53, 253, 107, 66, 27, 27, 232, 72, 0, 0, 103, 167, 84, 62, 203, 91, 97, 137, 131, 193, 230, 126, 98, 242, 216, 170 - ]), - public: Uint8Array.from([ - 4, 53, 59, 128, 56, 162, 250, 72, 141, 206, 117, 232, 57, 96, 39, 39, 247, 7, 27, 57, 251, 232, 120, 186, 21, 239, 176, 139, 195, 129, 125, 85, 11, 188, 191, 32, 227, 0, 6, 163, 101, 68, 208, 1, 43, 131, 124, 112, 102, 91, 104, 79, 16, 119, 152, 208, 4, 147, 155, 83, 20, 146, 104, 55, 90 - ]) +const fixtures: GoEllipticKeys = { + 'P-256': { + alice: { + private: Uint8Array.from([37, 140, 166, 161, 212, 132, 254, 140, 126, 65, 100, 6, 148, 131, 215, 92, 208, 103, 221, 6, 128, 23, 135, 79, 255, 73, 11, 225, 82, 212, 107, 80]), + public: Uint8Array.from([4, 122, 24, 120, 242, 177, 155, 244, 37, 171, 235, 22, 163, 121, 156, 182, 143, 217, 16, 233, 66, 127, 128, 60, 26, 57, 152, 210, 225, 251, 222, 8, 23, 85, 128, 21, 98, 28, 210, 66, 226, 90, 84, 107, 116, 233, 254, 6, 104, 58, 145, 7, 111, 60, 176, 86, 42, 191, 157, 27, 102, 77, 35, 66, 57]) + }, + bob: { + private: Uint8Array.from([250, 49, 115, 112, 140, 2, 12, 1, 189, 35, 30, 96, 177, 1, 181, 249, 110, 31, 240, 17, 27, 30, 19, 58, 209, 1, 63, 147, 184, 139, 208, 171]), + public: Uint8Array.from([4, 177, 148, 249, 217, 112, 201, 61, 113, 88, 206, 195, 25, 181, 241, 5, 244, 233, 252, 46, 233, 204, 48, 252, 250, 247, 66, 217, 130, 209, 62, 155, 242, 232, 26, 29, 150, 122, 131, 101, 138, 74, 137, 33, 136, 156, 46, 63, 213, 158, 51, 163, 91, 203, 21, 150, 227, 190, 218, 117, 235, 254, 148, 117, 199]) + }, + shared: Uint8Array.from([157, 145, 9, 131, 239, 13, 39, 207, 4, 151, 238, 38, 98, 169, 181, 194, 83, 36, 88, 238, 172, 133, 32, 251, 118, 90, 233, 47, 197, 40, 231, 182]) + }, + 'P-384': { + alice: { + private: Uint8Array.from([220, 34, 222, 224, 82, 178, 23, 121, 165, 184, 228, 178, 77, 76, 66, 55, 35, 122, 195, 222, 229, 66, 41, 75, 221, 48, 32, 99, 117, 118, 26, 96, 156, 131, 97, 121, 17, 251, 157, 202, 50, 241, 238, 195, 126, 211, 223, 250]), + public: Uint8Array.from([4, 91, 188, 238, 169, 244, 3, 234, 174, 151, 59, 224, 217, 125, 111, 92, 118, 37, 26, 14, 102, 223, 122, 181, 164, 142, 48, 97, 59, 33, 33, 99, 122, 51, 111, 125, 199, 218, 171, 248, 117, 45, 39, 9, 243, 31, 62, 156, 221, 95, 110, 148, 158, 232, 50, 83, 98, 91, 68, 162, 173, 125, 87, 56, 101, 118, 243, 47, 203, 135, 246, 194, 249, 78, 42, 70, 205, 229, 100, 59, 17, 153, 198, 185, 202, 146, 227, 242, 136, 36, 18, 169, 213, 227, 90, 77, 163]) + }, + bob: { + private: Uint8Array.from([234, 194, 98, 206, 63, 134, 230, 192, 54, 44, 167, 95, 131, 97, 64, 109, 241, 143, 69, 216, 170, 102, 70, 53, 209, 24, 145, 207, 102, 240, 123, 165, 12, 4, 28, 47, 159, 143, 131, 96, 114, 43, 144, 227, 114, 166, 174, 232]), + public: Uint8Array.from([4, 118, 187, 146, 219, 61, 124, 142, 240, 183, 239, 180, 86, 241, 181, 238, 4, 135, 205, 109, 253, 130, 91, 183, 151, 161, 208, 10, 92, 167, 37, 71, 1, 68, 174, 30, 49, 30, 161, 22, 23, 249, 82, 148, 58, 22, 238, 95, 213, 13, 63, 15, 219, 167, 210, 74, 101, 19, 70, 53, 199, 197, 103, 73, 110, 90, 195, 208, 28, 195, 96, 196, 11, 187, 26, 2, 233, 54, 75, 177, 213, 55, 196, 171, 164, 233, 218, 161, 221, 61, 92, 19, 153, 74, 149, 131, 50]) + }, + shared: Uint8Array.from([23, 182, 212, 185, 218, 192, 252, 17, 210, 198, 144, 134, 45, 210, 69, 232, 121, 210, 97, 36, 188, 115, 104, 137, 69, 79, 249, 125, 158, 151, 247, 142, 87, 137, 229, 225, 222, 244, 203, 22, 211, 85, 57, 130, 178, 12, 81, 43]) + }, + 'P-521': { + alice: { + private: Uint8Array.from([1, 159, 121, 134, 240, 103, 228, 193, 28, 197, 193, 159, 230, 12, 193, 254, 160, 159, 230, 96, 254, 230, 241, 164, 158, 239, 164, 238, 165, 55, 119, 33, 89, 229, 193, 83, 97, 58, 245, 11, 5, 143, 253, 142, 176, 36, 225, 253, 133, 46, 148, 176, 144, 24, 157, 205, 50, 126, 110, 85, 238, 52, 207, 23, 64, 203]), + public: Uint8Array.from([4, 1, 75, 171, 78, 143, 67, 235, 246, 10, 144, 92, 51, 186, 8, 32, 61, 204, 148, 130, 91, 86, 108, 157, 163, 134, 193, 160, 187, 177, 143, 112, 132, 227, 207, 178, 141, 199, 51, 249, 3, 245, 51, 37, 192, 0, 188, 239, 216, 244, 243, 168, 232, 49, 158, 164, 140, 87, 90, 220, 18, 26, 101, 235, 71, 206, 142, 1, 124, 224, 170, 139, 25, 6, 0, 17, 153, 45, 0, 41, 102, 106, 211, 255, 116, 191, 219, 66, 16, 72, 161, 235, 106, 41, 19, 132, 38, 64, 5, 90, 154, 22, 71, 178, 85, 52, 225, 195, 39, 112, 124, 136, 133, 42, 220, 110, 51, 237, 85, 55, 90, 105, 212, 252, 50, 113, 8, 205, 92, 246, 154, 49, 124]) + }, + bob: { + private: Uint8Array.from([0, 48, 128, 144, 205, 13, 19, 62, 68, 13, 80, 138, 138, 166, 70, 7, 26, 180, 229, 237, 141, 10, 70, 72, 58, 100, 140, 3, 62, 14, 182, 241, 79, 229, 170, 205, 15, 50, 24, 238, 46, 194, 18, 179, 225, 238, 221, 25, 42, 49, 110, 133, 201, 21, 139, 155, 184, 170, 129, 214, 189, 202, 36, 19, 224, 86]), + public: Uint8Array.from([4, 1, 222, 110, 155, 186, 20, 231, 105, 66, 36, 75, 141, 111, 195, 120, 250, 27, 80, 251, 156, 245, 226, 181, 242, 40, 218, 227, 78, 217, 162, 221, 54, 29, 67, 216, 82, 160, 196, 57, 241, 33, 54, 52, 23, 109, 156, 247, 253, 89, 199, 141, 27, 105, 232, 230, 97, 159, 156, 215, 224, 72, 147, 124, 16, 43, 69, 0, 174, 197, 20, 217, 121, 68, 66, 130, 205, 40, 209, 16, 46, 78, 244, 51, 162, 27, 141, 30, 146, 245, 211, 149, 81, 195, 174, 233, 42, 177, 16, 141, 217, 63, 193, 149, 68, 204, 216, 153, 167, 139, 194, 182, 96, 209, 249, 151, 172, 153, 24, 243, 4, 143, 120, 178, 228, 155, 14, 16, 222, 196, 96, 10, 154]) + }, + shared: Uint8Array.from([1, 135, 156, 15, 251, 68, 247, 200, 153, 189, 60, 183, 63, 215, 139, 185, 242, 96, 23, 86, 39, 175, 173, 170, 7, 189, 168, 209, 241, 1, 212, 136, 241, 212, 136, 90, 145, 48, 133, 46, 41, 142, 218, 133, 198, 127, 56, 9, 70, 35, 16, 156, 217, 58, 74, 72, 69, 188, 53, 11, 130, 164, 57, 160, 113, 76]) } -} satisfies GoEllipticKey +} + +export default fixtures diff --git a/packages/crypto/test/keys/ephemeral-keys.spec.ts b/packages/crypto/test/keys/ephemeral-keys.spec.ts index ecf96d4197..a564ea5e06 100644 --- a/packages/crypto/test/keys/ephemeral-keys.spec.ts +++ b/packages/crypto/test/keys/ephemeral-keys.spec.ts @@ -5,7 +5,7 @@ import { generateEphemeralKeyPair } from '../../src/keys/index.js' import fixtures from '../fixtures/go-elliptic-key.js' import type { Curve } from '../../src/keys/ecdh/index.js' -const curves: Curve[] = ['P-256', 'P-384'] // 'P-521' fails in tests :( no clue why +const curves: Curve[] = ['P-256', 'P-384', 'P-521'] const lengths: Record = { 'P-256': 65, 'P-384': 97, @@ -35,25 +35,27 @@ describe('generateEphemeralKeyPair', () => { }) describe('go interop', () => { - it('generates a shared secret', async () => { - const curve = fixtures.curve + curves.forEach((curve) => { + it(`generates a shared secret ${curve}`, async () => { + const keys = await Promise.all([ + generateEphemeralKeyPair(curve), + generateEphemeralKeyPair(curve) + ]) - const keys = await Promise.all([ - generateEphemeralKeyPair(curve), - generateEphemeralKeyPair(curve) - ]) - - const alice = keys[0] - const bob = keys[1] - bob.key = fixtures.bob.public + const alice = keys[0] + const bob = keys[1] + alice.key = fixtures[curve].alice.public + bob.key = fixtures[curve].bob.public - const secrets = await Promise.all([ - alice.genSharedKey(bob.key), - bob.genSharedKey(alice.key, fixtures.bob) - ]) + const secrets = await Promise.all([ + alice.genSharedKey(bob.key, fixtures[curve].alice), + bob.genSharedKey(alice.key, fixtures[curve].bob) + ]) - expect(secrets[0]).to.eql(secrets[1]) - expect(secrets[0]).to.have.length(32) + expect(secrets[0]).to.eql(secrets[1]) + expect(secrets[0]).to.eql(fixtures[curve].shared) + expect(secrets[0]).to.have.length(secretLengths[curve]) + }) }) })