Skip to content

Commit 9f816c7

Browse files
author
Frank Schmid
committed
Added PEM key generation
1 parent 1852fdf commit 9f816c7

7 files changed

Lines changed: 273 additions & 16 deletions

File tree

lib/JWKSet.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ class JWKSet {
1515
constructor(data) {
1616

1717
// Filter unsupported key types and invalid keys (@see RFC-7517 par. 5)
18-
this._keys = _.filter(data.keys, key => _.includes(JWA.supportedKeyTypes, key.kty) && JWK.validate(key));
18+
const supportedKeys = _.filter(data.keys, key => _.includes(JWA.supportedKeyTypes, key.kty) && JWK.validate(key));
19+
this._keys = _.map(supportedKeys, key => JWK.fromObject(key));
1920

2021
}
2122

lib/keytypes/ECKey.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,39 @@
55

66
const _ = require('lodash');
77
const base64url = require('base64url');
8+
const asn = require('asn1.js');
9+
10+
const COORD_PREFIX_UNCOMPRESSED = new Buffer([0x04]);
811

912
const SUPPORTED_CURVES = [
1013
'P-256',
1114
'P-384',
1215
'P-521'
1316
];
1417

18+
// @see RFC-6637 par. 11
19+
const CURVE_OIDS = {
20+
'1.2.840.10045.3.1.7': 'P-256',
21+
'1.3.132.0.34': 'P-384',
22+
'1.3.132.0.35': 'P-521'
23+
};
24+
25+
const ECPublicKeyHeader = asn.define('ECPublicKeyHeader', function() {
26+
this.seq().obj(
27+
this.key('keyType').objid({
28+
'1.2.840.10045.2.1': 'EC'
29+
}),
30+
this.key('curve').objid(CURVE_OIDS)
31+
);
32+
});
33+
34+
const ECPublicKey = asn.define('ECPublicKey', function() {
35+
this.seq().obj(
36+
this.key('header').use(ECPublicKeyHeader),
37+
this.key('content').bitstr()
38+
);
39+
});
40+
1541
class ECKey {
1642

1743
constructor(crv, x, y, d) {
@@ -25,6 +51,22 @@ class ECKey {
2551
return !_.isNil(this._d);
2652
}
2753

54+
toPublicKeyPEM() {
55+
// Construct x/y coordinate
56+
const coordinate = Buffer.concat([COORD_PREFIX_UNCOMPRESSED, this._x, this._y]);
57+
58+
const params = {
59+
header: {
60+
keyType: 'EC',
61+
curve: this._crv
62+
},
63+
content: {
64+
data: coordinate
65+
}
66+
};
67+
return ECPublicKey.encode(params, 'pem', { label: 'PUBLIC KEY' });
68+
}
69+
2870
static validate(key) {
2971
// y must only be defined for the three curves defined in RFC-7517 par. 6.2.1
3072
// FIXME: Currently y is treated as mandatory here.

lib/keytypes/RSAKey.js

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,93 @@
55

66
const _ = require('lodash');
77
const base64url = require('base64url');
8+
const asn = require('asn1.js');
9+
const util = require('../util');
810

911
const OPTIONAL_PRIVATE_PROPS = [
1012
'p', 'q', 'dp', 'dq', 'qi'
1113
];
1214

15+
// Define ASN structures
16+
const RSAPrivateKey = asn.define('RSAPrivateKey', function() {
17+
this.seq().obj(
18+
this.key('id').int(),
19+
this.key('n').int(),
20+
this.key('e').int(),
21+
this.key('d').int(),
22+
this.key('p').int(),
23+
this.key('q').int(),
24+
this.key('dp').int(),
25+
this.key('dq').int(),
26+
this.key('qi').int()
27+
);
28+
});
29+
30+
const RSAPublicKeyHeader = asn.define('RSAPublicKeyHeader', function() {
31+
this.seq().obj(
32+
this.key('keyType').objid({
33+
'1.2.840.113549.1.1.1': 'RSA'
34+
}),
35+
this.null_()
36+
);
37+
});
38+
39+
const RSAPublicKeyParams = asn.define('RSAPublicKeyParams', function() {
40+
this.seq().obj(
41+
this.key('n').int(),
42+
this.key('e').int()
43+
);
44+
});
45+
46+
const RSAPublicKey = asn.define('RSAPublicKey', function() {
47+
this.seq().obj(
48+
this.key('header').use(RSAPublicKeyHeader),
49+
this.key('content').bitstr()
50+
);
51+
});
52+
1353
class RSAKey {
1454

15-
constructor(n, e, d, p, q, dp, dq, qi, oth) {
16-
this._n = n;
17-
this._e = e;
18-
this._d = d;
19-
this._p = p;
20-
this._q = q;
21-
this._dp = dp;
22-
this._dq = dq;
23-
this._qi = qi;
24-
this._oth = oth;
55+
constructor(n, e, d, p, q, dp, dq, qi, oth, r) {
56+
this._data = {
57+
id: 0,
58+
n: util.unsigned(n),
59+
e: util.unsigned(e),
60+
d: util.unsigned(d),
61+
p: util.unsigned(p),
62+
q: util.unsigned(q),
63+
dp: util.unsigned(dp),
64+
dq: util.unsigned(dq),
65+
qi: util.unsigned(qi),
66+
oth: util.unsigned(oth),
67+
r: util.unsigned(r)
68+
};
2569
}
2670

2771
get hasPrivateKey() {
28-
return !_.isNil(this._d);
72+
return !_.isNil(this._data.d);
73+
}
74+
75+
toPublicKeyPEM() {
76+
const keyParams = RSAPublicKeyParams.encode(this._data, 'der');
77+
78+
const params = {
79+
header: {
80+
keyType: 'RSA'
81+
},
82+
content: {
83+
data: keyParams
84+
}
85+
};
86+
return RSAPublicKey.encode(params, 'pem', { label: 'PUBLIC KEY' });
87+
}
88+
89+
toPrivateKeyPEM() {
90+
if (!this.hasPrivateKey) {
91+
return null;
92+
}
93+
94+
return RSAPrivateKey.encode(this._data, 'pem', { label: 'RSA PRIVATE KEY' });
2995
}
3096

3197
static validate(key) {
@@ -43,11 +109,18 @@ class RSAKey {
43109
}
44110

45111
static fromKey(key) {
46-
const x = base64url.toBuffer(key.x);
47-
const y = base64url.toBuffer(key.y);
48-
const d = key.d ? base64url.toBuffer(key.d) : undefined;
112+
const n = base64url.toBuffer(key.n);
113+
const e = base64url.toBuffer(key.e);
114+
const d = key.d ? base64url.toBuffer(key.d) : null;
115+
const p = key.p ? base64url.toBuffer(key.p) : null;
116+
const q = key.q ? base64url.toBuffer(key.q) : null;
117+
const dp = key.dp ? base64url.toBuffer(key.dp) : null;
118+
const dq = key.dq ? base64url.toBuffer(key.dq) : null;
119+
const qi = key.qi ? base64url.toBuffer(key.qi) : null;
120+
const oth = key.oth ? base64url.toBuffer(key.oth) : null;
121+
const r = key.r ? base64url.toBuffer(key.r) : null;
49122

50-
return new RSAKey(key.crv, x, y, d);
123+
return new RSAKey(n, e, d, p, q, dp, dq, qi, oth, r);
51124
}
52125

53126
}

lib/util.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict';
2+
/**
3+
* Utilities
4+
*/
5+
6+
const _ = require('lodash');
7+
8+
const zeroBuffer = new Buffer([0]);
9+
10+
function unsigned(bignum) {
11+
if (_.isNil(bignum) || !_.isBuffer(bignum) || _.isEmpty(bignum)) {
12+
return bignum;
13+
}
14+
15+
if (bignum.readInt8(0) < 0) {
16+
return Buffer.concat([ zeroBuffer, bignum ], bignum.length + 1);
17+
}
18+
return bignum;
19+
}
20+
21+
module.exports.unsigned = unsigned;
22+
module.exports.zeroBuffer = zeroBuffer;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
],
2727
"bugs": "https://github.com/HyperBrain/node-jwk/issues",
2828
"dependencies": {
29+
"asn1.js": "^4.9.1",
2930
"base64url": "^2.0.0",
3031
"lodash": "^4.17.4"
3132
},

test/tests/ec.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const publicKS = require('../data/publicKeySet.json');
2+
const privateKS = require('../data/privateKeySet.json');
3+
4+
const JWKSet = require('../../lib/JWKSet');
5+
6+
const expect = require('chai').expect;
7+
8+
describe('EC key', () => {
9+
10+
it('should be initialized from public key objects', () => {
11+
12+
const keySet = JWKSet.fromObject(publicKS);
13+
const jwk = keySet.findKeyById('2011-04-29');
14+
15+
expect(keySet.keys).to.satisfy(k => /(?!.*_invalid)$/.test(k.kid));
16+
17+
});
18+
19+
it('should be initialized from private key objects', () => {
20+
21+
const keySet = JWKSet.fromObject(privateKS);
22+
const jwk = keySet.findKeyById('k5');
23+
24+
expect(jwk.kid).to.be.equal('k5');
25+
expect(jwk.key.hasPrivateKey).to.be.true;
26+
27+
const pubKey = jwk.key.toPublicKeyPEM();
28+
console.log(pubKey);
29+
exit();
30+
expect(pubKey).to.be.equal(`-----BEGIN PUBLIC KEY-----
31+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0vx7agoebGcQSuuPiLJX
32+
ZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tS
33+
oc/BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ/2W+5JsGY4Hc5n9yBXArwl93lqt
34+
7/RN5w6Cf0h4QyQ5v+65YGjQR0/FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0
35+
zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt+bFTWhAI4vMQFh6WeZu0f
36+
M4lFd2NcRwr3XPksINHaQ+G/xBniIqbw0Ls1jF44+csFCur+kEgU8awapJzKnqDK
37+
gwIDAQAB
38+
-----END PUBLIC KEY-----`);
39+
40+
});
41+
42+
it('should be initialized from private key objects', () => {
43+
44+
const keySet = JWKSet.fromObject(privateKS);
45+
const jwk = keySet.findKeyById('2011-04-29');
46+
47+
const pubKey = jwk.key.toPublicKeyPEM();
48+
expect(pubKey).to.be.equal(`-----BEGIN PUBLIC KEY-----
49+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0vx7agoebGcQSuuPiLJX
50+
ZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tS
51+
oc/BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ/2W+5JsGY4Hc5n9yBXArwl93lqt
52+
7/RN5w6Cf0h4QyQ5v+65YGjQR0/FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0
53+
zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt+bFTWhAI4vMQFh6WeZu0f
54+
M4lFd2NcRwr3XPksINHaQ+G/xBniIqbw0Ls1jF44+csFCur+kEgU8awapJzKnqDK
55+
gwIDAQAB
56+
-----END PUBLIC KEY-----`);
57+
58+
});
59+
60+
});

test/tests/rsa.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const publicKS = require('../data/publicKeySet.json');
2+
const privateKS = require('../data/privateKeySet.json');
3+
4+
const JWKSet = require('../../lib/JWKSet');
5+
6+
const expect = require('chai').expect;
7+
8+
describe('RSA key', () => {
9+
10+
it('should be initialized from public key objects', () => {
11+
12+
const keySet = JWKSet.fromObject(publicKS);
13+
const jwk = keySet.findKeyById('2011-04-29');
14+
15+
expect(keySet.keys).to.satisfy(k => /(?!.*_invalid)$/.test(k.kid));
16+
17+
});
18+
19+
it('should be initialized from private key objects', () => {
20+
21+
const keySet = JWKSet.fromObject(privateKS);
22+
const jwk = keySet.findKeyById('2011-04-29');
23+
24+
expect(jwk.kid).to.be.equal('2011-04-29');
25+
expect(jwk.key.hasPrivateKey).to.be.true;
26+
27+
const pubKey = jwk.key.toPublicKeyPEM();
28+
expect(pubKey).to.be.equal(`-----BEGIN PUBLIC KEY-----
29+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0vx7agoebGcQSuuPiLJX
30+
ZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tS
31+
oc/BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ/2W+5JsGY4Hc5n9yBXArwl93lqt
32+
7/RN5w6Cf0h4QyQ5v+65YGjQR0/FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0
33+
zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt+bFTWhAI4vMQFh6WeZu0f
34+
M4lFd2NcRwr3XPksINHaQ+G/xBniIqbw0Ls1jF44+csFCur+kEgU8awapJzKnqDK
35+
gwIDAQAB
36+
-----END PUBLIC KEY-----`);
37+
38+
});
39+
40+
it('should be initialized from private key objects', () => {
41+
42+
const keySet = JWKSet.fromObject(privateKS);
43+
const jwk = keySet.findKeyById('2011-04-29');
44+
45+
const pubKey = jwk.key.toPublicKeyPEM();
46+
expect(pubKey).to.be.equal(`-----BEGIN PUBLIC KEY-----
47+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0vx7agoebGcQSuuPiLJX
48+
ZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tS
49+
oc/BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ/2W+5JsGY4Hc5n9yBXArwl93lqt
50+
7/RN5w6Cf0h4QyQ5v+65YGjQR0/FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0
51+
zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt+bFTWhAI4vMQFh6WeZu0f
52+
M4lFd2NcRwr3XPksINHaQ+G/xBniIqbw0Ls1jF44+csFCur+kEgU8awapJzKnqDK
53+
gwIDAQAB
54+
-----END PUBLIC KEY-----`);
55+
56+
});
57+
58+
});

0 commit comments

Comments
 (0)