Importing a key for ECDH key derivation in subtle crypto

154 views Asked by At
      let randKey = window.crypto.getRandomValues(new Uint8Array(64));
      let importedKey = await window.crypto.subtle.importKey("raw", randKey,
        {
          name: "ECDH",
          namedCurve: "P-256",
        },
        false,
        ["deriveKey"]
      );

Yet this fails with the error: Uncaught DOMException: Cannot create a key using the specified key usages.

How do I import a key to be used in key derivation?

I am basing my code off this snippet from the subtle crypto docs, which works perfectly:

      let bobsKeyPair = await window.crypto.subtle.generateKey(
        {
          name: "ECDH",
          namedCurve: "P-256",
        },
        false,
        ["deriveKey"],
      );

But I want to do this with a key that has already been generated.

I found another stackoverflow question that said to use [] in the usages section, but all that did was change the error to just be Uncaught Error.

1

There are 1 answers

4
Topaco On BEST ANSWER

Since your raw P-256 key has 64 bytes, I assume that you want to import a public ECDH key (although you don't mention this explicitly).
For public ECDH keys, an empty list must be used for the key usages. The key usage deriveKey is only permitted for private keys.
In addition, the raw public key must be passed in compressed or uncompressed format and it must of course be a valid key for the curve in question (not an arbitrary byte sequence).

In the following code, first a valid raw public key in uncompressed format is generated. To do this, a P-256 key pair is generated, whose public key is then exported in raw, uncompressed format. This is then used to demonstrate how such a key can be imported:

(async () => {

// create key pair
let keyPair = await window.crypto.subtle.generateKey(
    { name: "ECDH", namedCurve: "P-256" },
    true,
    ["deriveKey"],
);
console.log("keyPair:", keyPair)     

// export raw public key (uncompressed format)
let publicKey = await window.crypto.subtle.exportKey("raw", keyPair.publicKey)
console.log("publicKey:", ab2hex(publicKey))

// import raw public key (uncompressed format)
let importedPublicKey = await window.crypto.subtle.importKey(
    "raw", 
    publicKey,                               // valid raw public key in uncompressed format
    { name: "ECDH", namedCurve: "P-256" },
    false,                          
    []                                       // empty list
); 
console.log("importedPublicKey:", importedPublicKey)
  
function ab2hex(ab) { 
    return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}

})();


For completeness: In addition to the raw (uncompressed or compressed) format, public ECDH keys can also be imported in X.509/SPKI format (DER encoded), or in JWK format.
Private ECDH keys cannot be imported in raw format, but only in PKCS#8 format (DER encoded), or in JWK format. The PKCS#8 format for private keys has already been pointed out in the other answer. Permitted key usages are deriveKey or deriveBits.
Note: Keys in PKCS#8 format or X.509/SPKI format can be loaded and inspected in an ASN.1 parser.

Code sample for the import of a private key:

(async () => {

// create key pair
let keyPair = await window.crypto.subtle.generateKey(
    { name: "ECDH", namedCurve: "P-256" },
    true,
    ["deriveKey"],
);
console.log("keyPair:", keyPair)     

// export private key (PKCS#8 format, DER encoded)
let privateKey = await window.crypto.subtle.exportKey("pkcs8", keyPair.privateKey)
console.log("privateKey:", ab2hex(privateKey))

// import private key (PKCS#8 format, DER encoded)
let importedPrivateKey = await window.crypto.subtle.importKey(
    "pkcs8", 
    privateKey,                               // valid private key in PKCS#8 format, DER encoded
    { name: "ECDH", namedCurve: "P-256" },
    false,                          
    ["deriveKey"]                             // deriveKey, deriveBits allowed
); 
console.log("importedPrivateKey:", importedPrivateKey)
  
function ab2hex(ab) { 
    return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
}

})();


Edit - regarding your question from the comment:
The direct import of a raw private ECDH key is not supported by WebCrypto as described above. In the case of a raw private key, the key must be converted into a format that is supported by WebCrypto, i.e. PKCS#8 or JWK. Regarding PKCS#8, it is best to use a suitable library. Conversion to a JWK, on the other hand, is quite simple.
The following code provides an example of converting a raw ECDH key into a JWK and then importing it:

(async () => {

// sample key pair: raw private key and raw public uncompressed key
let rawPrivateKey = hex2ab("609bbc01a89db655a4941be426d3ce6b2bc820f1e82b11145722c0d2052ef5e7");
let rawPublicKeyUncompressed = hex2ab("04926ce2b12acc5b653a153a4d5ba25dfe44330061badcb702a3d24631a81055e38a2b0c7363985f24aeb6279ce0cc022b947192a692300de5e91ee07bb5acf3e9");
let x = rawPublicKeyUncompressed.slice(1, 32 + 1); // ignore 0x04 start byte for uncompressed format
let y = rawPublicKeyUncompressed.slice(32 + 1);
let d = rawPrivateKey

// convert to JWK
let privateKey = {
  crv: "P-256",
  d: ab2b64url(d),
  kty: "EC",
  x: ab2b64url(x),
  y: ab2b64url(y)
}
console.log("jwk", privateKey)

// import private key in JWK format
let importedPrivateKey = await window.crypto.subtle.importKey(
    "jwk", 
    privateKey,                                         // valid private key in JWK format 
    { name: "ECDH", namedCurve: "P-256" },
    true,                          
    ["deriveKey"]                                       // deriveKey, deriveBits allowed
); 
console.log("importedPrivateKey:", importedPrivateKey)
  
// helper ------------

function hex2ab(hex){
    return new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function (h) {return parseInt(h, 16)}));
}

function ab2b64(arrayBuffer) {
        return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}

function ab2b64url(arrayBuffer) {
        return ab2b64(arrayBuffer).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}

})();