ECDH for P-521 (Web Crypto Api) / secp521r1 (NodeJS Crypto) generate a slightly different shared secret

1.2k Views Asked by At

I have generated a public and private key pair with ECDH from NodeJS

function _genPrivateKey(curveName = "secp384r1", encoding = "hex") {
    const private_0 = crypto.createECDH(curveName);
    private_0.generateKeys();
    return private_0.getPrivateKey().toString(encoding);
}

BOB PRIVATE KEY

9d0c809d692c83c7d8d1355205dd78e679066fd9c15d12cdea3e1103b041873765264351e8939083b876d89d423301d8486a956da455ddbdc4a91ef60af7dd2325

BOB PUBLIC KEY

[hex] 
0400d7a342e89f501e1cd8224e2463ef1ad057c9b64bf45c1d3627cc1f06c055c80f75c2013c27b63dc984b467ecfc5202cf9a126ef1f1487e92b9acfb52abaeb7022e01f4259918ca34f442214a31d1bad329f9be1c67ce98af6621f622e44887264e856ee8c664a51e56f24008c932ee1cb5514c02d03ba27f6b6a1cd0aa0f8eac261250

[jwk] {
  key_ops: [ 'deriveKey' ],
  ext: true,
  kty: 'EC',
  x:'ANejQuifUB4c2CJOJGPvGtBXybZL9FwdNifMHwbAVcgPdcIBPCe2PcmEtGfs_FICz5oSbvHxSH6Suaz7UquutwIu',
  y:'AfQlmRjKNPRCIUox0brTKfm-HGfOmK9mIfYi5EiHJk6FbujGZKUeVvJACMky7hy1UUwC0Duif2tqHNCqD46sJhJQ',
  crv: 'P-521'
}

and Alice's keys from a web page with Web Crypto API

const generateAlicesKeyPair = window.crypto.subtle.generateKey({
        name: "ECDH",
        namedCurve: "P-521"
    },
    false,
    ["deriveBits"]
);

ALICE PUBLIC KEY

04000eefa90c3de22e79e6742f807806a603059d16afaa9f1bc69f420050aae100d0006e6510fe17a8f6767fe1e69bada039175ef5a375e30af4085e4315cf7527655f00ed9a39552a5f9170cc7626c1f4584d0e6de17870e336bcc5b6e251e3ea2c7cd9633e1afe2f9aee5f9a7445d38218c20695cc7ba2a462b67ce39a060e6464133609

When I try to derive the shared key a strange thing happens, the key has different bits at the end.

NodeJS:

function _getSharedSecret(privateKey, publicKey, curveName = "secp521r1", encoding = "hex") {
    const private_0 = crypto.createECDH(curveName);
    private_0.setPrivateKey(privateKey, encoding);
    const _sharedSecret = private_0.computeSecret(publicKey, encoding);
    return _sharedSecret
};

Web Crypto API

const sharedSecret = await window.crypto.subtle.deriveBits({
        name: "ECDH",
        namedCurve: "P-521",
        public: publicKey
    },
    privateKey, 
    521
);

Results:


//NodeJS: 
0089b99212c9348a6fd3aa78225a773a90ef45f57bbd10dc86d8e52fa26662c550b56d2368ee358ab240ceec10191b6cdd7d09bb0a8763ea48a487a5676ebdf7af[eb]

//WebCryptoAPI:

0089b99212c9348a6fd3aa78225a773a90ef45f57bbd10dc86d8e52fa26662c550b56d2368ee358ab240ceec10191b6cdd7d09bb0a8763ea48a487a5676ebdf7af[80]

This happens only with the curve P-521/secp521r1 but not with the curves P-256/secp256r1 and P-384/secp384r1

1

There are 1 best solutions below

0
On BEST ANSWER

Checking with a third library (Python, Cryptography library) shows that the NodeJS result is the correct one (i.e. the one ending with 0xEB).

X and Y coordinates of P-521 are represented by 66 bytes (521 bit = 65 bytes + 1 bit) (s. here). The shared secret is the X coordinate of a curve point (s. here) and therefore also has 66 bytes = 528 bits. This value is to be specified in the WebCrypto API implementation in deriveBits() as the length of the shared secret.

If you specify 521 bits only the highest bit (which is set for 0xEB) is taken into account, the remaining bits are set to 0, resulting in the value 0x80.

The following code illustrates this (please note that the script doesn't run under Firefox, which is presumably a bug):

(async () => {
    await getSharedSecret(521);
    await getSharedSecret(528);
})();

async function getSharedSecret(bits) {
    var bobPrivateKeyJwk = {   
        kty: "EC",
        crv: "P-521",
        x:'ANejQuifUB4c2CJOJGPvGtBXybZL9FwdNifMHwbAVcgPdcIBPCe2PcmEtGfs_FICz5oSbvHxSH6Suaz7UquutwIu',
        y:'AfQlmRjKNPRCIUox0brTKfm-HGfOmK9mIfYi5EiHJk6FbujGZKUeVvJACMky7hy1UUwC0Duif2tqHNCqD46sJhJQ',
        d: "AJ0MgJ1pLIPH2NE1UgXdeOZ5Bm_ZwV0Szeo-EQOwQYc3ZSZDUeiTkIO4dtidQjMB2EhqlW2kVd29xKke9gr33SMl",
        ext: true,
    }
    var alicePublicKeyBuffer = typedArray('04000eefa90c3de22e79e6742f807806a603059d16afaa9f1bc69f420050aae100d0006e6510fe17a8f6767fe1e69bada039175ef5a375e30af4085e4315cf7527655f00ed9a39552a5f9170cc7626c1f4584d0e6de17870e336bcc5b6e251e3ea2c7cd9633e1afe2f9aee5f9a7445d38218c20695cc7ba2a462b67ce39a060e6464133609');    
    var privateKey = await window.crypto.subtle.importKey(
        "jwk", 
        bobPrivateKeyJwk,
        { name: "ECDH", namedCurve: "P-521" },
        true, 
        ["deriveKey", "deriveBits"] 
    );
    var publicKey = await window.crypto.subtle.importKey(
        "raw", 
        alicePublicKeyBuffer.buffer,
        { name: "ECDH", namedCurve: "P-521"},
        true, 
        [] 
    );
    var sharedSecret = await window.crypto.subtle.deriveBits(
        { name: "ECDH", namedCurve: "P-521", public: publicKey },
        privateKey, 
        bits 
    );
    console.log("Bob's shared secret:\n", buf2hex(sharedSecret).replace(/(.{48})/g,'$1\n'));
}; 

function typedArray(hex) {
    return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) { // from: https://stackoverflow.com/a/43131635
        return parseInt(h, 16)
    }))
}

function buf2hex(buffer) {
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); // from: https://stackoverflow.com/a/40031979/9014097 
}

With the curves P-256 and P-384 the problem does not occur, because the prime field is already a multiple of 8 (32 bytes and 48 bytes, respectively).