knaccc
knaccc

Reputation: 262

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

})();

Upvotes: 1

Views: 312

Answers (1)

Topaco
Topaco

Reputation: 49341

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

  • x and y are not normalized, see getX() and getY().
  • x and y are not padded to 66 bytes (with leading 0x00 bytes).

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')))

Upvotes: 1

Related Questions