diafygi / webcrypto-examples

Web Cryptography API Examples Demo: https://diafygi.github.io/webcrypto-examples/
GNU General Public License v2.0
1.64k stars 194 forks source link

How to import ECDSA public key from PEM chain? #30

Closed leplatrem closed 8 years ago

leplatrem commented 8 years ago

Thanks for your repo, it was really helpful (if not vital) to get started :)

I have a ECDSA P-384 public key as PEM chain that I fetch from a public URL:

-----BEGIN CERTIFICATE-----
MIIC0DCCAlUCCQDh7ZXFZjOO+jAKBggqhkjOPQQDAjCB0DELMAkGA1UEBhMCVVMx
EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxHDAa
BgNVBAoME01vemlsbGEgQ29ycG9yYXRpb24xNTAzBgNVBAsMLEF1dG9ncmFwaCBm
b3IgS2ludG8gU2V0dGluZ3MgRGV2IGVudmlyb25tZW50MRYwFAYDVQQDDA1BdXRv
Z3JhcGggWDVVMScwJQYJKoZIhvcNAQkBFhhzdG9yYWdlLXRlYW1AbW96aWxsYS5j
b20wHhcNMTYwNDI5MTQ1ODA4WhcNMTcwNDI5MTQ1ODA4WjCB0DELMAkGA1UEBhMC
VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcx
HDAaBgNVBAoME01vemlsbGEgQ29ycG9yYXRpb24xNTAzBgNVBAsMLEF1dG9ncmFw
aCBmb3IgS2ludG8gU2V0dGluZ3MgRGV2IGVudmlyb25tZW50MRYwFAYDVQQDDA1B
dXRvZ3JhcGggWDVVMScwJQYJKoZIhvcNAQkBFhhzdG9yYWdlLXRlYW1AbW96aWxs
YS5jb20wdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAS12UsecmKNRm4+L+jt1++/dENE
EJIICW7X7q8sbaKKlbZWGkcgHqGWrGrIzK0wgMjtLiRKIwwp7izx+XEgueku7K/o
bAKrURi/twi8DGCWOU7JV4Os+MV+pLLab2ehGWcwCgYIKoZIzj0EAwIDaQAwZgIx
AKtsG9nBvvgc81gHZCiVwYgK6qdw1B9eHloKN6aCRfppth/2vLwn4EaB7cmBiI67
UwIxALKkJz8tMlCloUgFaXuuzcoP16O1EB7l9RNM16hrA7Dz3NWMGCUMxSSYBiFo
WHFmvQ==
-----END CERTIFICATE-----

And would like to import it :

const stripped = pemChain.split("\n").slice(1, -2).join("");
const binaryKey = base64ToBinary(stripped);
const usages = ["verify"]; //"verify" for public key import, "sign" for private key imports
return window.crypto.subtle.importKey("spki", binaryKey, {
      name: "ECDSA",
      namedCurve: "P-384"
    },
    false, //whether the key is extractable (i.e. can be used in exportKey),
    usages
  ).catch((e) => console.error(e));

function base64ToBinary(base64) {
  var binary_string =  window.atob(base64);
  var len = binary_string.length;
  var bytes = new Uint8Array( len );
  for (var i = 0; i < len; i++)        {
      bytes[i] = binary_string.charCodeAt(i);
  }
  return bytes;
}

But it throws with DataError: "Data provided to an operation does not meet requirements" and I can't figure out where to start :)

I will continue to investigate and keep you posted...

leplatrem commented 8 years ago

@ttaubert and @mozmark confirmed that loading ECDSA from spki is not supported.

We should use the jwk instead.

That means we should parse the chain with an ASN.1 library, extract the x and y coordinates from the result and provide them as a an object:

  const stripped = pemChain.split("\n").slice(1, -2).join("");
  const binaryKey = base64ToBinary(stripped);

  const asn1 = org.pkijs.fromBER(binaryKey.buffer);
  console.log(asn1.result);
  const coordX = ?
  const coordY = ?  

  const jwk = {
    kty: "EC",
    crv: "P-384",
    x: coordX,
    y: coordY
  }
  const usages = ["verify"]; //"verify" for public key import, "sign" for private key imports
  return window.crypto.subtle.importKey("jwk", jwk, {
      name: "ECDSA",
      namedCurve: "P-384"
    },
    false, //whether the key is extractable (i.e. can be used in exportKey),
    usages
  );

I didn't figure out how to parse and extract the x and y values yet. Plus I'm afraid to be lost between the different expected types/encoding (base64, buffer, UintArray, hex...) :]

edit: example in Go : https://play.golang.org/p/v9GKNxAuKj

leplatrem commented 8 years ago

Ok I could make it work thanks to @engelke articles 1 and 2.

Using the live parsing tool of asn1js I could figure out how to reach field containing the binary key.

Once I had obtained the bytes of the public key field, I had to split them to obtain the two values for x and y. The first byte of ECDSA binary key contains the compression info (x04 for uncompressed), followed by x and y. The number of bytes obtained was 97, which makes sense: 97 == 1 + 48 + 48, and 48 bytes is 384 bits (ECDSA P-384).

  const content = publicKey.bits.bytes.slice(1);
  const length = content.length;
  if (length * 8 != 384 * 2) {
    throw new Error(`Invalid key size (${length * 8} bits)`)
  }
  publicKey.x = content.slice(0, length/2);
  publicKey.y = content.slice(length/2);

But the jwk expects the x and y to be «base64url» encoded:

function binaryToBase64URL(int8Array) {
  return window.btoa(String.fromCharCode.apply(null, int8Array))
               .replace(/\+/g, '-').replace(/\//g, '_')  // URL friendly
               .replace(/\=+$/, '');  // No padding.
}

As a summary, I could get the PEM to be loaded with:

function loadKey(pemChain) {
  const stripped = pemChain.split("\n").slice(1, -2).join("");
  const der = base64ToBinary(stripped);
  var certificate = parseX509ECDSACertificate(der);  // x509ecdsa.js
  const jwk = {
    kty: "EC",
    crv: "P-384",
    x: binaryToBase64URL(certificate.publicKey.x),
    y: binaryToBase64URL(certificate.publicKey.y),
    ext: true,
  }
  const usages = ["verify"]; //"verify" for public key import, "sign" for private key imports
  return window.crypto.subtle.importKey("jwk", jwk, {
      name: "ECDSA",
      namedCurve: "P-384"
    },
    false, //whether the key is extractable (i.e. can be used in exportKey),
    usages
  );
}

The parseX509ECDSACertificate() function can be found in this pull-request elsewhere.

The key is now loaded properly. I now have some failing verification, but that's another story ;)

(I'm closing this, feel free to re-open of ping me if you think this is worth adding to the documentation somewhere)

Thanks @jvehent also for your support!