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?
How to derive public key from private key using WebCryptoApi?
Asked Answered
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
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.
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-key –
Serotine
@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
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(''));
}
© 2022 - 2024 — McMap. All rights reserved.