diff --git a/lib/jwk/eckey.js b/lib/jwk/eckey.js index 0c3f1442..98161bc5 100644 --- a/lib/jwk/eckey.js +++ b/lib/jwk/eckey.js @@ -40,6 +40,28 @@ function oidToCurveName(oid) { } } +function readUncompressedPoint(point) { + if (typeof point !== "string" && !Buffer.isBuffer(point)) { + return null; + } + var pt = Buffer.isBuffer(point) ? point : Buffer.from(point, "binary"); + if (pt.length >= 2 && 0x00 === pt[0] && 0x04 === pt[1]) { + pt = pt.slice(2); + } else if (pt.length >= 1 && 0x04 === pt[0]) { + pt = pt.slice(1); + } else { + return null; + } + if (pt.length < 2 || (pt.length % 2)) { + return null; + } + var len = pt.length / 2; + return { + x: pt.slice(0, len), + y: pt.slice(len) + }; +} + var JWKEcCfg = { publicKey: function(props) { var fields = JWK.helpers.COMMON_PROPS.concat([ @@ -294,7 +316,12 @@ var JWKEcFactory = { if ("private" === input.type) { // coerce capture.value to DER *iff* private if ("string" === typeof input.keyValue) { - input.keyValue = forge.asn1.fromDer(input.keyValue); + // Keep the public BIT STRING as raw bytes. An uncompressed + // point (0x04 || X || Y) can look like an OCTET STRING when + // X[0] is a plausible length, and decoding it drops that byte. + input.keyValue = forge.asn1.fromDer(input.keyValue, { + decodeBitStrings: false + }); } else if (Array.isArray(input.keyValue)) { input.keyValue = input.keyValue[0]; } @@ -306,7 +333,9 @@ var JWKEcFactory = { return null; } } else { - capture.point = input.keyValue; + capture.point = (typeof input.keyValueBits === "string") ? + input.keyValueBits : + input.keyValue; } input = capture; } @@ -319,16 +348,14 @@ var JWKEcFactory = { if (input.d) { output.d = Buffer.from(input.d, "binary"); } - if (input.point) { - var pt = Buffer.from(input.point, "binary"); - // only support uncompressed - if (4 !== pt.readUInt16BE(0)) { - return null; - } - pt = pt.slice(2); - var len = pt.length / 2; - output.x = pt.slice(0, len); - output.y = pt.slice(len); + var coords = readUncompressedPoint(input.point); + if (coords) { + output.x = coords.x; + output.y = coords.y; + } else if (output.d) { + var pub = depsecc.asPrivateKey(crv, output.d).toPublicKey(); + output.x = pub.x; + output.y = pub.y; } return output; } diff --git a/lib/jwk/helpers.js b/lib/jwk/helpers.js index c50b0668..9fe1fd26 100644 --- a/lib/jwk/helpers.js +++ b/lib/jwk/helpers.js @@ -87,7 +87,8 @@ var publicKeyValidator = { tagClass: forge.asn1.Class.UNIVERSAL, type: forge.asn1.Type.BITSTRING, constructed: false, - capture: "keyValue" + capture: "keyValue", + captureBitStringContents: "keyValueBits" } ] }; diff --git a/test/jwk/keystore-test.js b/test/jwk/keystore-test.js index 9bb52c8e..254d8d69 100644 --- a/test/jwk/keystore-test.js +++ b/test/jwk/keystore-test.js @@ -569,7 +569,9 @@ describe("jwk/keystore", function() { rawPrivate: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIJDUxX9BJpsF49W9TKPofckq2npNlYXRhsM+PApuYxdcoAoGCCqGSM49\nAwEHoUQDQgAEWVXns1zCDbbGWVnXQRUvT4342FNEzAiTy1tDz5mfhJqzbqR5D8qy\njjBPkCnLhLn6HNRD3zz3EGCcLFvYhjSpCg==\n-----END EC PRIVATE KEY-----\n", spki: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWVXns1zCDbbGWVnXQRUvT4342FNE\nzAiTy1tDz5mfhJqzbqR5D8qyjjBPkCnLhLn6HNRD3zz3EGCcLFvYhjSpCg==\n-----END PUBLIC KEY-----\n", pkcs8: "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkNTFf0EmmwXj1b1M\no+h9ySraek2VhdGGwz48Cm5jF1yhRANCAARZVeezXMINtsZZWddBFS9PjfjYU0TM\nCJPLW0PPmZ+EmrNupHkPyrKOME+QKcuEufoc1EPfPPcQYJwsW9iGNKkK\n-----END PRIVATE KEY-----\n", - cert: "-----BEGIN CERTIFICATE-----\nMIICMzCCAdmgAwIBAgIJAMwo6FDPY28LMAoGCCqGSM49BAMCMHYxCzAJBgNVBAYT\nAlVTMREwDwYDVQQIDAhDb2xvcmFkbzEPMA0GA1UEBwwGRGVudmVyMRwwGgYDVQQK\nDBNDaXNjbyBTeXN0ZW1zLCBJbmMuMQ0wCwYDVQQLDARDQ1RHMRYwFAYDVQQDDA1r\nbXMuY2lzY28uY29tMB4XDTE1MDkzMDE5MzgxMloXDTE2MDkyOTE5MzgxMlowdjEL\nMAkGA1UEBhMCVVMxETAPBgNVBAgMCENvbG9yYWRvMQ8wDQYDVQQHDAZEZW52ZXIx\nHDAaBgNVBAoME0Npc2NvIFN5c3RlbXMsIEluYy4xDTALBgNVBAsMBENDVEcxFjAU\nBgNVBAMMDWttcy5jaXNjby5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARZ\nVeezXMINtsZZWddBFS9PjfjYU0TMCJPLW0PPmZ+EmrNupHkPyrKOME+QKcuEufoc\n1EPfPPcQYJwsW9iGNKkKo1AwTjAdBgNVHQ4EFgQUNmIbQpQISpglBzdUyLLg5zR3\nu5swHwYDVR0jBBgwFoAUNmIbQpQISpglBzdUyLLg5zR3u5swDAYDVR0TBAUwAwEB\n/zAKBggqhkjOPQQDAgNIADBFAiAvkFO6ok2tadxhXjCCJ99+P1MhQ3FPUav1cs9m\ndCjkUgIhAKZGQ118RwlQpMX8B1nVsI7wP8c6iGfKwTkRwoKrSFr7\n-----END CERTIFICATE-----\n" + cert: "-----BEGIN CERTIFICATE-----\nMIICMzCCAdmgAwIBAgIJAMwo6FDPY28LMAoGCCqGSM49BAMCMHYxCzAJBgNVBAYT\nAlVTMREwDwYDVQQIDAhDb2xvcmFkbzEPMA0GA1UEBwwGRGVudmVyMRwwGgYDVQQK\nDBNDaXNjbyBTeXN0ZW1zLCBJbmMuMQ0wCwYDVQQLDARDQ1RHMRYwFAYDVQQDDA1r\nbXMuY2lzY28uY29tMB4XDTE1MDkzMDE5MzgxMloXDTE2MDkyOTE5MzgxMlowdjEL\nMAkGA1UEBhMCVVMxETAPBgNVBAgMCENvbG9yYWRvMQ8wDQYDVQQHDAZEZW52ZXIx\nHDAaBgNVBAoME0Npc2NvIFN5c3RlbXMsIEluYy4xDTALBgNVBAsMBENDVEcxFjAU\nBgNVBAMMDWttcy5jaXNjby5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARZ\nVeezXMINtsZZWddBFS9PjfjYU0TMCJPLW0PPmZ+EmrNupHkPyrKOME+QKcuEufoc\n1EPfPPcQYJwsW9iGNKkKo1AwTjAdBgNVHQ4EFgQUNmIbQpQISpglBzdUyLLg5zR3\nu5swHwYDVR0jBBgwFoAUNmIbQpQISpglBzdUyLLg5zR3u5swDAYDVR0TBAUwAwEB\n/zAKBggqhkjOPQQDAgNIADBFAiAvkFO6ok2tadxhXjCCJ99+P1MhQ3FPUav1cs9m\ndCjkUgIhAKZGQ118RwlQpMX8B1nVsI7wP8c6iGfKwTkRwoKrSFr7\n-----END CERTIFICATE-----\n", + trickyPkcs8: "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGhxHIBRF78CETf2Y\n4naT6iLT5UJVgy9Pa7TMEMSRCSehRANCAAQ/Idg7BVShuglkq7uV5gN9Bo4tCJIi\nZLAWMdYCKfsfhGk0SRwGKO8WejalYv8YhudHIrwakIcLY0DkkeBAPyOw\n-----END PRIVATE KEY-----\n", + trickySpki: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPyHYOwVUoboJZKu7leYDfQaOLQiS\nImSwFjHWAin7H4RpNEkcBijvFno2pWL/GIbnRyK8GpCHC2NA5JHgQD8jsA==\n-----END PUBLIC KEY-----\n" }; it("coerces Private String to JWK.Key istance", function() { var promise = JWK.store.KeyStore.asKey(rawPrivate, "private"); @@ -640,6 +642,32 @@ describe("jwk/keystore", function() { return promise; }); + it("coerces a PKCS8 EC PEM whose point looks like ASN.1", function() { + var promise = JWK.store.KeyStore.asKey(pem.trickyPkcs8, "pem"); + promise = promise.then(function(jwk) { + assert.ok(JWK.store.KeyStore.isKey(jwk)); + assert.strictEqual(jwk.kty, "EC"); + var json = jwk.toJSON(true); + assert.strictEqual(json.crv, "P-256"); + assert.strictEqual(json.x, "PyHYOwVUoboJZKu7leYDfQaOLQiSImSwFjHWAin7H4Q"); + assert.strictEqual(json.y, "aTRJHAYo7xZ6NqVi_xiG50civBqQhwtjQOSR4EA_I7A"); + assert.strictEqual(json.d, "GhxHIBRF78CETf2Y4naT6iLT5UJVgy9Pa7TMEMSRCSc"); + }); + + return promise; + }); + it("coerces an EC SPKI PEM whose point looks like ASN.1", function() { + var promise = JWK.store.KeyStore.asKey(pem.trickySpki, "pem"); + promise = promise.then(function(jwk) { + assert.ok(JWK.store.KeyStore.isKey(jwk)); + var json = jwk.toJSON(); + assert.strictEqual(json.crv, "P-256"); + assert.strictEqual(json.x, "PyHYOwVUoboJZKu7leYDfQaOLQiSImSwFjHWAin7H4Q"); + assert.strictEqual(json.y, "aTRJHAYo7xZ6NqVi_xiG50civBqQhwtjQOSR4EA_I7A"); + }); + + return promise; + }); it("coerces (SPKI) PUBLIC KEY PEM String to JWK.Key instance", function() { var promise = JWK.store.KeyStore.asKey(pem.spki, "pem"); promise = promise.then(function(jwk) {