javascriptcryptographyjwksubtlecrypto

How does JWK EC public key co-ordinate encoding work?


I'm attempting to re-create a P-521 JWK from just the private key.

According to the spec (section 4.2.1):

"The x member contains the x coordinate for the elliptic curve point. It is represented as the base64url encoding of the coordinate's big endian representation."

I've attempted to base64url-decode the private key, and scalar-multiply it with the P-521 base point to get the public key point. I've then attempted to convert the x and y co-ordinates into a big-endian octet representation, and encode that back to base64url format. The results do not match the original x and y base64url-encoded fields in the JWK.

I've also tried this with the P-256 curve, and that does not work either.

const elliptic = require('elliptic');
const EC = elliptic.ec;
const {base16, base64url} = require('rfc4648');
const BN = require("bn.js");
const padBase16ToWholeOctets = s => s.length%2===0 ? s : '0'+s;
const bnToB64 = n => base64url.stringify(base16.parse(padBase16ToWholeOctets(n.toString(16))))

console.log('begin'); // forces console output to show from async IIFE

(async () => {
  let keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-521" }, true, ['sign'])
  let jwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey)
  console.log(jwk)

  const dHex = base16.stringify(base64url.parse(jwk.d, { loose: true }))

  const ec = new EC('p521')
  const [x,y] = ec.curve.g.mul(new BN(dHex, 16, 'be')).toJSON()

  console.log(`expected x: ` + jwk.x)
  console.log(`actual   x: ` + bnToB64(x))

  console.log(`expected y: ` + jwk.y)
  console.log(`actual   y: ` + bnToB64(y))

})();

Solution

  • The data generated with the WebCrypto API is correct. Your different data is caused by two problems during calculation:

    The first problem can be fixed with:

    const point = ec.curve.g.mul(new BN(dHex, 16, 'be'))
    console.log(`actual   x: ` + bnToB64(point.getX()))
    console.log(`actual   y: ` + bnToB64(point.getY()))
    

    the second with:

    const bnToB64 = n => base64url.stringify(base16.parse(padBase16ToWholeOctets(n.toString(16)).padStart(132, '0')))