How to derive public key from private key using WebCryptoApi?
Asked Answered
C

2

5

The private key is generated using Elliptic Curve. None of the methods from the SubtleCrypto interface of the Web Crypto API seem to be able to derive a public key from a private key, correct me if I'm wrong. Do I have to use a 3rd party library for that?

Crispin answered 7/5, 2022 at 9:14 Comment(4)
AFAIK, there is no support. However, this can easily be done by exporting and importing as JWK, removing the private part before importing.Larrisa
I'll clarify: When I generate a key pair with subtle.generateKeys, I want to be able to recover the public key having only the private key saved.Crispin
In my answer you will find an implementation that illustrates the suggested approach. Without a third party library this is the easiest way.Larrisa
Maxim, we had the same question, and it appears it's not possible natively in the Web Crypto API. If this changes, please send me a message (I'm Zamicol everywhere) and let me know. We'll update our libraries with native support.Shamekashameless
L
6

WebCrypto is a low level API with only a relatively small feature set. To my knowledge there is no dedicated method for deriving a public key from a private key.

However, you can export the private CryptoKey as JWK (JSON Web Key), remove the private portions, and re-import the remaining portion, which thereby becomes the public CryptoKey. The following code shows this for an ECDSA key:

async function getPublic(privateKey){
    const jwkPrivate = await crypto.subtle.exportKey("jwk", privateKey);    
    delete jwkPrivate.d;
    jwkPrivate.key_ops = ["verify"];
    return crypto.subtle.importKey("jwk", jwkPrivate, {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]);
}

async function test(){

    // Generate test key pair
    const keyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]);

    // Derive public from private key
    const publicKeyFromPrivate = await getPublic(keyPair.privateKey)

    // Compare CryptoKeys
    console.log(keyPair.publicKey);
    console.log(publicKeyFromPrivate)

    // Compare keys (in JWK format)
    const jwkPublic = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
    const jwkPublicFromPrivate = await crypto.subtle.exportKey("jwk", publicKeyFromPrivate);
    console.log(jwkPublic);
    console.log(jwkPublicFromPrivate);
}

(async () => {
    await test()
})();

As you can see, the original and the reconstructed public key are identical.

However, it should be mentioned that this solutin has one drawback: the private key must be exportable.

This post shows the same approach for RSA.

Larrisa answered 7/5, 2022 at 15:30 Comment(2)
not sure if this changed recently, but afaik public keys need to have an empty array for usages https://mcmap.net/q/1633419/-javascript-web-crypto-unable-to-import-ecdh-p-256-public-keySerotine
@Serotine - This post is about ECDSA. With ECDSA, the public key is used for verification and thus verify is to be applied for the key usage. In contrast, the linked post is about ECDH. With ECDH, the public key is used to derive the shared secret and in this context an empty key usage is to be applied.Larrisa
S
0

the noble curves library also has functions for that :

// export and extract private key 
const { d } = await crypto.subtle.exportKey("jwk", privateKey);

// transforms url encoded base64 string from the jwk into big number
const validPrivateKey = b64ToBn(urlBase64ToBase64(d)) 

// get x,y from the noble curves lib ProjectivePoint
const pointFromPK = p256.ProjectivePoint.fromPrivateKey(validPrivateKey)

// import using constructed public key jwk x,y
crypto.subtle.importKey(
   "jwk", 
   {  
      x: base64ToUrlBase64(bnToB64(pointFromPK.x)),
      y: base64ToUrlBase64(bnToB64(pointFromPK.y)),
   },
   {name: "ECDSA", namedCurve: "P-256"}, true, [/*must be empty for public keys*/]
)

support functions from coolaj

function b64ToBn(b64) {
  var bin = atob(b64);
  var hex = [];

  bin.split('').forEach(function (ch) {
    var h = ch.charCodeAt(0).toString(16);
    if (h.length % 2) { h = '0' + h; }
    hex.push(h);
  });

  return BigInt('0x' + hex.join(''));
}

function urlBase64ToBase64(str) {
  var r = str % 4;
  if (2 === r) {
    str += '==';
  } else if (3 === r) {
    str += '=';
  }
  return str.replace(/-/g, '+').replace(/_/g, '/');
}

function base64ToUrlBase64(str) {
  return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

function bnToB64(bn) {
  var hex = BigInt(bn).toString(16);
  if (hex.length % 2) { hex = '0' + hex; }

  var bin = [];
  var i = 0;
  var d;
  var b;
  while (i < hex.length) {
    d = parseInt(hex.slice(i, i + 2), 16);
    b = String.fromCharCode(d);
    bin.push(b);
    i += 2;
  }

  return btoa(bin.join(''));
}
Serotine answered 28/6, 2023 at 19:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.