Skip to content

Commit

Permalink
Support importing EC and RSA "raw" PEM keys (Close #33)
Browse files Browse the repository at this point in the history
  • Loading branch information
linuxwolf committed Dec 11, 2015
1 parent 23d0b83 commit f7a6dca
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 86 deletions.
136 changes: 101 additions & 35 deletions lib/jwk/eckey.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,63 @@ var JWKEcCfg = {

// Inspired by digitalbaazar/node-forge/js/rsa.js
var validators = {
oid: "1.2.840.10045.2.1",
privateKey: {
// ECPrivateKey
name: "ECPrivateKey",
tagClass: forge.asn1.Class.UNIVERSAL,
type: forge.asn1.Type.SEQUENCE,
constructed: true,
value: [
{
// EC version
name: "ECPrivateKey.version",
tagClass: forge.asn1.Class.UNIVERSAL,
type: forge.asn1.Type.INTEGER,
constructed: false
},
{
// private value (d)
name: "ECPrivateKey.private",
tagClass: forge.asn1.Class.UNIVERSAL,
type: forge.asn1.Type.OCTETSTRING,
constructed: false,
capture: "d"
},
{
// EC parameters
tagClass: forge.asn1.Class.CONTEXT_SPECIFIC,
name: "ECPrivateKey.parameters",
constructed: true,
value: [
{
// namedCurve (crv)
name: "ECPrivateKey.namedCurve",
tagClass: forge.asn1.Class.UNIVERSAL,
type: forge.asn1.Type.OID,
constructed: false,
capture: "crv"
}
]
},
{
// publicKey
name: "ECPrivateKey.publicKey",
tagClass: forge.asn1.Class.CONTEXT_SPECIFIC,
constructed: true,
value: [
{
name: "ECPrivateKey.point",
tagClass: forge.asn1.Class.UNIVERSAL,
type: forge.asn1.Type.BITSTRING,
constructed: false,
capture: "point"
}
]
}
]
},
embeddedPrivateKey: {
// ECPrivateKey
name: "ECPrivateKey",
tagClass: forge.asn1.Class.UNIVERSAL,
Expand Down Expand Up @@ -159,8 +215,22 @@ var validators = {
}
};

function oidToCurveName(oid) {
switch (oid) {
case "1.2.840.10045.3.1.7":
return "P-256";
case "1.3.132.0.34":
return "P-384";
case "1.3.132.0.35":
return "P-521";
default:
return null;
}
}

var JWKEcFactory = {
kty: "EC",
validators: validators,
prepare: function() {
return Promise.resolve(JWKEcCfg);
},
Expand All @@ -175,60 +245,56 @@ var JWKEcFactory = {
return Promise.resolve(result);
},
import: function(input) {
if ("1.2.840.10045.2.1" !== input.keyOid) {
if (validators.oid !== input.keyOid) {
return null;
}

// coerce key params to OID
var crv;
if (input.keyParams && forge.asn1.Type.OID === input.keyParams.type) {
crv = forge.asn1.derToOid(input.keyParams.value);
// convert OID to common name
switch (crv) {
case "1.2.840.10045.3.1.7":
crv = "P-256";
break;
case "1.3.132.0.34":
crv = "P-384";
break;
case "1.3.132.0.35":
crv = "P-521";
break;
default:
return null;
}
crv = oidToCurveName(crv);
} else if (input.crv) {
crv = forge.asn1.derToOid(input.crv);
crv = oidToCurveName(crv);
}
if (!crv) {
return null;
}

var capture = {},
errors = [];
if ("private" === input.type) {
// coerce capture.value to DER *iff* private
if ("string" === typeof input.keyValue) {
input.keyValue = forge.asn1.fromDer(input.keyValue);
} else if (Array.isArray(input.keyValue)) {
input.keyValue = input.keyValue[0];
}
if (!input.parsed) {
var capture = {},
errors = [];
if ("private" === input.type) {
// coerce capture.value to DER *iff* private
if ("string" === typeof input.keyValue) {
input.keyValue = forge.asn1.fromDer(input.keyValue);
} else if (Array.isArray(input.keyValue)) {
input.keyValue = input.keyValue[0];
}

if (!forge.asn1.validate(input.keyValue,
validators.privateKey,
capture,
errors)) {
return null;
if (!forge.asn1.validate(input.keyValue,
validators.embeddedPrivateKey,
capture,
errors)) {
return null;
}
} else {
capture.point = input.keyValue;
}
} else {
capture.point = input.keyValue;
input = capture;
}

// convert factors to Buffers
var output = {
kty: "EC",
crv: crv
};
if (capture.d) {
output.d = new Buffer(capture.d, "binary");
if (input.d) {
output.d = new Buffer(input.d, "binary");
}
if (capture.point) {
var pt = new Buffer(capture.point, "binary");
if (input.point) {
var pt = new Buffer(input.point, "binary");
// only support uncompressed
if (4 !== pt.readUInt16BE(0)) {
return null;
Expand Down
94 changes: 72 additions & 22 deletions lib/jwk/keystore.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,76 @@ function processCert(input) {
return input;
}

function importFrom(input, form) {
// form can be one of: 'pkcs8' | 'spki' | 'pkix' | 'x509'
function fromPEM(input) {
var result = {};
var pems = forge.pem.decode(input);
var found = pems.some(function(p) {
switch (p.type) {
case "CERTIFICATE":
result.form = "pkix";
break;
case "PUBLIC KEY":
result.form = "spki";
break;
case "PRIVATE KEY":
result.form = "pkcs8";
break;
case "EC PRIVATE KEY":
result.form = "raw-private";
break;
case "RSA PRIVATE KEY":
result.form = "raw-private";
break;
default:
return false;
}

result.body = p.body;
return true;
});
if (!found) {
throw new Error("supported PEM type not found");
}
return result;
}
function importFrom(registry, input) {
// form can be one of:
// 'raw-private' | 'raw-public' | 'pkcs8' | 'spki' | 'pkix' | 'x509'
var capture = {},
errors = [],
result;

// conver from DER to ASN1
var der = input,
var form = input.form,
der = input.body,
thumbprint = null;
input = forge.asn1.fromDer(input);
input = forge.asn1.fromDer(der);
switch(form) {
case "raw-private":
registry.all().some(function(factory) {
if (result) {
return false;
}
if (!factory.validators) {
return false;
}

var oid = factory.validators.oid,
validator = factory.validators.privateKey;
if (!validator) {
return false;
}
capture = {};
errors = [];
result = forge.asn1.validate(input, validator, capture, errors);
if (result) {
capture.keyOid = forge.asn1.oidToDer(oid);
capture.parsed = true;
}
return result;
});
capture.type = "private";
break;
case "pkcs8":
result = forge.asn1.validate(input, JWK.helpers.validators.privateKey, capture, errors);
capture.type = "private";
Expand Down Expand Up @@ -222,6 +281,8 @@ var JWKStore = function(registry, parent) {
Object.defineProperty(this, "add", {
value: function(jwk, form, extras) {
extras = extras || {};

var factors;
if (Buffer.isBuffer(jwk) || typeof jwk === "string") {
// form can be 'json', 'pkcs8', 'spki', 'pkix', 'x509', 'pem'
form = (form || "json").toLowerCase();
Expand All @@ -231,28 +292,17 @@ var JWKStore = function(registry, parent) {
try {
if ("pem" === form) {
// convert *first* PEM -> DER
jwk = forge.pem.decode(jwk);
jwk = jwk[0];
switch (jwk.type) {
case "CERTIFICATE":
form = "pkix";
break;
case "PUBLIC KEY":
form = "spki";
break;
case "PRIVATE KEY":
form = "pkcs8";
break;
default:
throw new Error("unsupported PEM type '" + jwk.type + "'");
}
jwk = jwk.body;
factors = fromPEM(jwk);
} else {
factors = {
body: jwk.toString("binary"),
form: form
};
}
jwk = importFrom(jwk.toString("binary"), form);
jwk = importFrom(registry, factors);
if (!jwk) {
throw new Error("no importer for key");
}

Object.keys(extras).forEach(function(field){
jwk[field] = extras[field];
});
Expand Down
39 changes: 22 additions & 17 deletions lib/jwk/rsakey.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function convertBNtoBuffer(bn) {

// Adapted from digitalbaazar/node-forge/js/rsa.js
var validators = {
oid: "1.2.840.113549.1.1.1",
privateKey: {
name: "RSAPrivateKey",
tagClass: forge.asn1.Class.UNIVERSAL,
Expand Down Expand Up @@ -227,6 +228,7 @@ var validators = {
// Factory
var JWKRsaFactory = {
kty: "RSA",
validators: validators,
prepare: function() {
// TODO: validate key properties
return Promise.resolve(JWKRsaCfg);
Expand Down Expand Up @@ -269,35 +271,38 @@ var JWKRsaFactory = {
return Promise.resolve(result);
},
import: function(input) {
if ("1.2.840.113549.1.1.1" !== input.keyOid) {
if (validators.oid !== input.keyOid) {
return null;
}

// coerce capture.value to DER
if ("string" === typeof input.keyValue) {
input.keyValue = forge.asn1.fromDer(input.keyValue);
} else if (Array.isArray(input.keyValue)) {
input.keyValue = input.keyValue[0];
}
// capture key factors
var validator = ("private" === input.type) ?
validators.privateKey :
validators.publicKey;
var capture = {},
errors = [];
if (!forge.asn1.validate(input.keyValue, validator, capture, errors)) {
return null;
if (!input.parsed) {
// coerce capture.keyValue to DER
if ("string" === typeof input.keyValue) {
input.keyValue = forge.asn1.fromDer(input.keyValue);
} else if (Array.isArray(input.keyValue)) {
input.keyValue = input.keyValue[0];
}
// capture key factors
var validator = ("private" === input.type) ?
validators.privateKey :
validators.publicKey;
var capture = {},
errors = [];
if (!forge.asn1.validate(input.keyValue, validator, capture, errors)) {
return null;
}
input = capture;
}

// convert factors to Buffers
var output = {
kty: "RSA"
};
["n", "e", "d", "p", "q", "dp", "dq", "qi"].forEach(function(f) {
if (!(f in capture)) {
if (!(f in input)) {
return;
}
var b = new Buffer(capture[f], "binary");
var b = new Buffer(input[f], "binary");
// remove leading zero padding if any
if (0 === b[0]) {
b = b.slice(1);
Expand Down
Loading

0 comments on commit f7a6dca

Please sign in to comment.