How to import ECDSA public key from PEM chain?
leplatrem opened this issue · 2 comments
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...
@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
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!