1

I am using the browser built in SubtleCrypto library in javascript to generate public and private keys as such:

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

This generates the following (just a test key):

enter image description here

Is there a way to convert the exported keys into a bit more "portable" format? Something which isn't JSON, isn't super long, maybe hex format? Maybe compressed format?

I tried passing "raw" instead of "jwk" for the exportKey function and that returns an "Operation is not supported" error because it's probably not supported for this format.

For example, Eth-Crypto has a publicKey.compress() way to compress the public key into a short enough string: '03a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b'

https://github.com/pubkey/eth-crypto#createidentity

Similarly, Schnorr keys are also pretty short.

  • WebCrypto API provides little regarding conversion of format and encoding. You can export both keys as JWK and extract the parameters d, x and y. With x and y, creating the uncompressed/compressed key is trivial. Furthermore, export of the *public* EC key in raw format is supported (but not of the private key). The exported public key is uncompressed. – Topaco May 24 '23 at 10:07
  • @Topaco can you share more details on how "With x and y, creating the uncompressed/compressed key is trivial."? I have tried researching but can't seem to find it. Is there a library or built in way to do that? – sudoExclaimationExclaimation May 24 '23 at 19:50
  • The uncompressed key is the concatenation of x and y coordinate, the compressed key is the x coordinate. Preceded by a marker byte (4, 3 or 2). With known x and y both can be easily created, see my answer for more details. – Topaco May 25 '23 at 07:30

1 Answers1

2

WebCrypto limitations:
WebCrypto does not support the export of raw keys for private EC keys. As a workaround the key can be exported as JWK and the d parameter extracted.
For public EC keys, export as raw key is supported. The exported key is uncompressed. The conversion to a compressed key has to be implemented by the user (if needed).
In addition, the public raw key (uncompressed or compressed) can of course also be created from the x and y coordinates of the private or public key exported as JWK.

Uncompressed/compressed public key:
The public EC key corresponds to an EC point (x, y).
The uncompressed public key is the concatenation of a 0x04 marker byte, the x coordinate, and the y coordinate (in this order): 0x04|x|y. For P-521, it has a length of 1 + 66 + 66 = 133 bytes.
The compressed key is the concatenation of a marker byte and the x-coordinate (in this order). The marker byte is 0x02 for an even y: 0x02|x, and 0x03 for an odd y: 0x03|x. For P-521, the compressed key has a length of 1 + 66 = 67 bytes.
For completeness, the compressed key contains the complete information to reconstruct the EC point and y, respectively: From the x coordinate of the compressed key, the two possible y values are determined using the curve equation. The marker byte determines which of the two values is the searched y value.

Possible implementation of raw key export with WebCrypto:

(async () => {

var keyPair = await crypto.subtle.generateKey(
    {
        name: "ECDSA",
        namedCurve: "P-521",
    },
    true,
    ['sign', 'verify']
);

rawUncomp.innerHTML = "public/uncompressed (hex):<br>" + ab2hex(await exportRawPublicKey(keyPair.publicKey, true));
rawComp.innerHTML = "public/compressed (hex):<br>" + ab2hex(await exportRawPublicKey(keyPair.publicKey, false));
rawPriv.innerHTML = "private (hex):<br>" + ab2hex(await exportRawPrivateKey(keyPair.privateKey));

// Export raw public key (compressed or uncompressed) as ArrayBuffer
async function exportRawPublicKey(publicKey, uncompressed){
    var key = await crypto.subtle.exportKey("raw", publicKey);
    if (!uncompressed) {
        var keyUncompressed = Array.from(new Uint8Array(key));
        var keySize = (keyUncompressed.length - 1)/2;
        var keyCompressed =[];
        keyCompressed.push(keyUncompressed[2 * keySize] % 2 ? 3 : 2);
        keyCompressed.push(...keyUncompressed.slice(1, keySize + 1));
        key = new Uint8Array(keyCompressed).buffer;
    }
    return key;     
}

// Export raw private key as ArrayBuffer
async function exportRawPrivateKey(privateKey){
    var keyJwk = await crypto.subtle.exportKey("jwk", privateKey);
    var rawKeyB64 = toBase64(keyJwk.d);
    return b642ab(rawKeyB64);
}

//
// Helper
//

// Base64url -> Base64
function toBase64(input) {
    input = input.replace(/-/g, '+').replace(/_/g, '/');
    return input + "=".repeat(3 - (3 + input.length) % 4);
}

// Base64 -> ArrayBuffer
function b642ab(b64){
    return Uint8Array.from(window.atob(b64), c => c.charCodeAt(0));
}

// ArrayBuffer -> hex
function ab2hex(ab) { 
    return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}

})();
<div  style="font-family:'Courier New', monospace;" id="rawUncomp"></div>
<div  style="font-family:'Courier New', monospace;" id="rawComp"></div>
<div  style="font-family:'Courier New', monospace;" id="rawPriv"></div>
Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Your `exportRawPrivateKey` function only includes the `d` from the `JWK`. Shouldn't it also include the `x` and `y`? Or does it mean that the public key cannot be extracted from your private key? How would the private key also include the public key? – sudoExclaimationExclaimation May 26 '23 at 20:56
  • @sudoExclaimationExclaimation - No, the raw private key consists only of `d`. This format is minimalistic, the raw public key cannot be *extracted*, but must be *calculated* if required (`pubKey = privKey * G`, s. [here](https://cryptobook.nakov.com/digital-signatures/ecdsa-sign-verify-messages#key-generation)). Other formats like PKCS#8 or SEC1 allow extraction of the public key, but these are less compact. – Topaco May 26 '23 at 22:01
  • How would I generate `G` then? I read that link previously but didn't really understand what `Generator point G` means. Crypto isn't my expertise. I don't mind the private key being less compact as it doesn't need to be shared. – sudoExclaimationExclaimation May 26 '23 at 22:15
  • G is the base point of the curve, s. [here](https://www.secg.org/sec2-v2.pdf), sec. 2.6 – Topaco May 26 '23 at 22:18
  • Is that the `0200C6 858E06B7 0404E9CD......` value? Is that something which gets hardcoded? – sudoExclaimationExclaimation May 26 '23 at 22:23
  • I don't quite understand the problem. You only need to store both raw keys after key generation (due to the fixed lengths you can also concatenate the keys, e.g. private|public). – Topaco May 26 '23 at 22:26
  • I want the user to be able to 1. send their public key to everyone. Therefore, I wanted it to be compact. 2. Store the private key locally and provide it to my site for things like signature and also for extracting public key (for requirement 1). I don't want the user to have to provide both public and private key to my site as it's unnecessary work. So I want the private key to contain public key too. – sudoExclaimationExclaimation May 26 '23 at 22:31
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253854/discussion-between-topaco-and-sudoexclaimationexclaimation). – Topaco May 26 '23 at 22:31