Skip to content

Commit f7a6dca

Browse files
committed
Support importing EC and RSA "raw" PEM keys (Close #33)
1 parent 23d0b83 commit f7a6dca

File tree

4 files changed

+240
-86
lines changed

4 files changed

+240
-86
lines changed

lib/jwk/eckey.js

+101-35
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,63 @@ var JWKEcCfg = {
118118

119119
// Inspired by digitalbaazar/node-forge/js/rsa.js
120120
var validators = {
121+
oid: "1.2.840.10045.2.1",
121122
privateKey: {
123+
// ECPrivateKey
124+
name: "ECPrivateKey",
125+
tagClass: forge.asn1.Class.UNIVERSAL,
126+
type: forge.asn1.Type.SEQUENCE,
127+
constructed: true,
128+
value: [
129+
{
130+
// EC version
131+
name: "ECPrivateKey.version",
132+
tagClass: forge.asn1.Class.UNIVERSAL,
133+
type: forge.asn1.Type.INTEGER,
134+
constructed: false
135+
},
136+
{
137+
// private value (d)
138+
name: "ECPrivateKey.private",
139+
tagClass: forge.asn1.Class.UNIVERSAL,
140+
type: forge.asn1.Type.OCTETSTRING,
141+
constructed: false,
142+
capture: "d"
143+
},
144+
{
145+
// EC parameters
146+
tagClass: forge.asn1.Class.CONTEXT_SPECIFIC,
147+
name: "ECPrivateKey.parameters",
148+
constructed: true,
149+
value: [
150+
{
151+
// namedCurve (crv)
152+
name: "ECPrivateKey.namedCurve",
153+
tagClass: forge.asn1.Class.UNIVERSAL,
154+
type: forge.asn1.Type.OID,
155+
constructed: false,
156+
capture: "crv"
157+
}
158+
]
159+
},
160+
{
161+
// publicKey
162+
name: "ECPrivateKey.publicKey",
163+
tagClass: forge.asn1.Class.CONTEXT_SPECIFIC,
164+
constructed: true,
165+
value: [
166+
{
167+
name: "ECPrivateKey.point",
168+
tagClass: forge.asn1.Class.UNIVERSAL,
169+
type: forge.asn1.Type.BITSTRING,
170+
constructed: false,
171+
capture: "point"
172+
}
173+
]
174+
}
175+
]
176+
},
177+
embeddedPrivateKey: {
122178
// ECPrivateKey
123179
name: "ECPrivateKey",
124180
tagClass: forge.asn1.Class.UNIVERSAL,
@@ -159,8 +215,22 @@ var validators = {
159215
}
160216
};
161217

218+
function oidToCurveName(oid) {
219+
switch (oid) {
220+
case "1.2.840.10045.3.1.7":
221+
return "P-256";
222+
case "1.3.132.0.34":
223+
return "P-384";
224+
case "1.3.132.0.35":
225+
return "P-521";
226+
default:
227+
return null;
228+
}
229+
}
230+
162231
var JWKEcFactory = {
163232
kty: "EC",
233+
validators: validators,
164234
prepare: function() {
165235
return Promise.resolve(JWKEcCfg);
166236
},
@@ -175,60 +245,56 @@ var JWKEcFactory = {
175245
return Promise.resolve(result);
176246
},
177247
import: function(input) {
178-
if ("1.2.840.10045.2.1" !== input.keyOid) {
248+
if (validators.oid !== input.keyOid) {
179249
return null;
180250
}
181251

182252
// coerce key params to OID
183253
var crv;
184254
if (input.keyParams && forge.asn1.Type.OID === input.keyParams.type) {
185255
crv = forge.asn1.derToOid(input.keyParams.value);
186-
// convert OID to common name
187-
switch (crv) {
188-
case "1.2.840.10045.3.1.7":
189-
crv = "P-256";
190-
break;
191-
case "1.3.132.0.34":
192-
crv = "P-384";
193-
break;
194-
case "1.3.132.0.35":
195-
crv = "P-521";
196-
break;
197-
default:
198-
return null;
199-
}
256+
crv = oidToCurveName(crv);
257+
} else if (input.crv) {
258+
crv = forge.asn1.derToOid(input.crv);
259+
crv = oidToCurveName(crv);
260+
}
261+
if (!crv) {
262+
return null;
200263
}
201264

202-
var capture = {},
203-
errors = [];
204-
if ("private" === input.type) {
205-
// coerce capture.value to DER *iff* private
206-
if ("string" === typeof input.keyValue) {
207-
input.keyValue = forge.asn1.fromDer(input.keyValue);
208-
} else if (Array.isArray(input.keyValue)) {
209-
input.keyValue = input.keyValue[0];
210-
}
265+
if (!input.parsed) {
266+
var capture = {},
267+
errors = [];
268+
if ("private" === input.type) {
269+
// coerce capture.value to DER *iff* private
270+
if ("string" === typeof input.keyValue) {
271+
input.keyValue = forge.asn1.fromDer(input.keyValue);
272+
} else if (Array.isArray(input.keyValue)) {
273+
input.keyValue = input.keyValue[0];
274+
}
211275

212-
if (!forge.asn1.validate(input.keyValue,
213-
validators.privateKey,
214-
capture,
215-
errors)) {
216-
return null;
276+
if (!forge.asn1.validate(input.keyValue,
277+
validators.embeddedPrivateKey,
278+
capture,
279+
errors)) {
280+
return null;
281+
}
282+
} else {
283+
capture.point = input.keyValue;
217284
}
218-
} else {
219-
capture.point = input.keyValue;
285+
input = capture;
220286
}
221287

222288
// convert factors to Buffers
223289
var output = {
224290
kty: "EC",
225291
crv: crv
226292
};
227-
if (capture.d) {
228-
output.d = new Buffer(capture.d, "binary");
293+
if (input.d) {
294+
output.d = new Buffer(input.d, "binary");
229295
}
230-
if (capture.point) {
231-
var pt = new Buffer(capture.point, "binary");
296+
if (input.point) {
297+
var pt = new Buffer(input.point, "binary");
232298
// only support uncompressed
233299
if (4 !== pt.readUInt16BE(0)) {
234300
return null;

lib/jwk/keystore.js

+72-22
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,76 @@ function processCert(input) {
8282
return input;
8383
}
8484

85-
function importFrom(input, form) {
86-
// form can be one of: 'pkcs8' | 'spki' | 'pkix' | 'x509'
85+
function fromPEM(input) {
86+
var result = {};
87+
var pems = forge.pem.decode(input);
88+
var found = pems.some(function(p) {
89+
switch (p.type) {
90+
case "CERTIFICATE":
91+
result.form = "pkix";
92+
break;
93+
case "PUBLIC KEY":
94+
result.form = "spki";
95+
break;
96+
case "PRIVATE KEY":
97+
result.form = "pkcs8";
98+
break;
99+
case "EC PRIVATE KEY":
100+
result.form = "raw-private";
101+
break;
102+
case "RSA PRIVATE KEY":
103+
result.form = "raw-private";
104+
break;
105+
default:
106+
return false;
107+
}
108+
109+
result.body = p.body;
110+
return true;
111+
});
112+
if (!found) {
113+
throw new Error("supported PEM type not found");
114+
}
115+
return result;
116+
}
117+
function importFrom(registry, input) {
118+
// form can be one of:
119+
// 'raw-private' | 'raw-public' | 'pkcs8' | 'spki' | 'pkix' | 'x509'
87120
var capture = {},
88121
errors = [],
89122
result;
90123

91124
// conver from DER to ASN1
92-
var der = input,
125+
var form = input.form,
126+
der = input.body,
93127
thumbprint = null;
94-
input = forge.asn1.fromDer(input);
128+
input = forge.asn1.fromDer(der);
95129
switch(form) {
130+
case "raw-private":
131+
registry.all().some(function(factory) {
132+
if (result) {
133+
return false;
134+
}
135+
if (!factory.validators) {
136+
return false;
137+
}
138+
139+
var oid = factory.validators.oid,
140+
validator = factory.validators.privateKey;
141+
if (!validator) {
142+
return false;
143+
}
144+
capture = {};
145+
errors = [];
146+
result = forge.asn1.validate(input, validator, capture, errors);
147+
if (result) {
148+
capture.keyOid = forge.asn1.oidToDer(oid);
149+
capture.parsed = true;
150+
}
151+
return result;
152+
});
153+
capture.type = "private";
154+
break;
96155
case "pkcs8":
97156
result = forge.asn1.validate(input, JWK.helpers.validators.privateKey, capture, errors);
98157
capture.type = "private";
@@ -222,6 +281,8 @@ var JWKStore = function(registry, parent) {
222281
Object.defineProperty(this, "add", {
223282
value: function(jwk, form, extras) {
224283
extras = extras || {};
284+
285+
var factors;
225286
if (Buffer.isBuffer(jwk) || typeof jwk === "string") {
226287
// form can be 'json', 'pkcs8', 'spki', 'pkix', 'x509', 'pem'
227288
form = (form || "json").toLowerCase();
@@ -231,28 +292,17 @@ var JWKStore = function(registry, parent) {
231292
try {
232293
if ("pem" === form) {
233294
// convert *first* PEM -> DER
234-
jwk = forge.pem.decode(jwk);
235-
jwk = jwk[0];
236-
switch (jwk.type) {
237-
case "CERTIFICATE":
238-
form = "pkix";
239-
break;
240-
case "PUBLIC KEY":
241-
form = "spki";
242-
break;
243-
case "PRIVATE KEY":
244-
form = "pkcs8";
245-
break;
246-
default:
247-
throw new Error("unsupported PEM type '" + jwk.type + "'");
248-
}
249-
jwk = jwk.body;
295+
factors = fromPEM(jwk);
296+
} else {
297+
factors = {
298+
body: jwk.toString("binary"),
299+
form: form
300+
};
250301
}
251-
jwk = importFrom(jwk.toString("binary"), form);
302+
jwk = importFrom(registry, factors);
252303
if (!jwk) {
253304
throw new Error("no importer for key");
254305
}
255-
256306
Object.keys(extras).forEach(function(field){
257307
jwk[field] = extras[field];
258308
});

lib/jwk/rsakey.js

+22-17
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ function convertBNtoBuffer(bn) {
117117

118118
// Adapted from digitalbaazar/node-forge/js/rsa.js
119119
var validators = {
120+
oid: "1.2.840.113549.1.1.1",
120121
privateKey: {
121122
name: "RSAPrivateKey",
122123
tagClass: forge.asn1.Class.UNIVERSAL,
@@ -227,6 +228,7 @@ var validators = {
227228
// Factory
228229
var JWKRsaFactory = {
229230
kty: "RSA",
231+
validators: validators,
230232
prepare: function() {
231233
// TODO: validate key properties
232234
return Promise.resolve(JWKRsaCfg);
@@ -269,35 +271,38 @@ var JWKRsaFactory = {
269271
return Promise.resolve(result);
270272
},
271273
import: function(input) {
272-
if ("1.2.840.113549.1.1.1" !== input.keyOid) {
274+
if (validators.oid !== input.keyOid) {
273275
return null;
274276
}
275277

276-
// coerce capture.value to DER
277-
if ("string" === typeof input.keyValue) {
278-
input.keyValue = forge.asn1.fromDer(input.keyValue);
279-
} else if (Array.isArray(input.keyValue)) {
280-
input.keyValue = input.keyValue[0];
281-
}
282-
// capture key factors
283-
var validator = ("private" === input.type) ?
284-
validators.privateKey :
285-
validators.publicKey;
286-
var capture = {},
287-
errors = [];
288-
if (!forge.asn1.validate(input.keyValue, validator, capture, errors)) {
289-
return null;
278+
if (!input.parsed) {
279+
// coerce capture.keyValue to DER
280+
if ("string" === typeof input.keyValue) {
281+
input.keyValue = forge.asn1.fromDer(input.keyValue);
282+
} else if (Array.isArray(input.keyValue)) {
283+
input.keyValue = input.keyValue[0];
284+
}
285+
// capture key factors
286+
var validator = ("private" === input.type) ?
287+
validators.privateKey :
288+
validators.publicKey;
289+
var capture = {},
290+
errors = [];
291+
if (!forge.asn1.validate(input.keyValue, validator, capture, errors)) {
292+
return null;
293+
}
294+
input = capture;
290295
}
291296

292297
// convert factors to Buffers
293298
var output = {
294299
kty: "RSA"
295300
};
296301
["n", "e", "d", "p", "q", "dp", "dq", "qi"].forEach(function(f) {
297-
if (!(f in capture)) {
302+
if (!(f in input)) {
298303
return;
299304
}
300-
var b = new Buffer(capture[f], "binary");
305+
var b = new Buffer(input[f], "binary");
301306
// remove leading zero padding if any
302307
if (0 === b[0]) {
303308
b = b.slice(1);

0 commit comments

Comments
 (0)