Skip to content

Commit 21bc410

Browse files
committed
Init
1 parent 4397aa2 commit 21bc410

36 files changed

+1101
-0
lines changed

lib/errors.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/** Error subclass for signaling keystore errors */
2+
class KeystoreError extends Error {
3+
constructor (message) {
4+
super(message)
5+
this.name = this.constructor.name
6+
this.message = message
7+
Error.captureStackTrace(this, this.constructor)
8+
}
9+
}
10+
11+
/** Error subclass for signaling keystore error when a given private key is not found */
12+
class PrivateKeyNotFoundError extends KeystoreError {
13+
constructor (keyId) {
14+
super(`private key not found, key id: ${keyId}`)
15+
this.keyId = keyId
16+
}
17+
}
18+
19+
/** Error subclass for signaling keystore error when a given public key is not found */
20+
class CertificateNotFoundError extends KeystoreError {
21+
constructor (keyId) {
22+
super(`certificate not found, key id: ${keyId}`)
23+
this.keyId = keyId
24+
}
25+
}
26+
27+
module.exports = {
28+
KeystoreError, PrivateKeyNotFoundError, CertificateNotFoundError
29+
}

lib/errors.spec.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const chai = require('chai')
2+
const chaiAsPromised = require('chai-as-promised')
3+
chai.use(chaiAsPromised)
4+
const dirtyChai = require('dirty-chai')
5+
chai.use(dirtyChai)
6+
const expect = chai.expect
7+
const { KeystoreError, PrivateKeyNotFoundError, CertificateNotFoundError } = require('./errors')
8+
9+
const message = 'Sample message'
10+
const keyId = 'keyid_1'
11+
12+
function throwKeystoreError () {
13+
throw new KeystoreError(message)
14+
}
15+
16+
function throwPrivateKeyNotFoundError () {
17+
throw new PrivateKeyNotFoundError(keyId)
18+
}
19+
20+
function throwCertificateNotFoundError () {
21+
throw new CertificateNotFoundError(keyId)
22+
}
23+
24+
describe('errors', function () {
25+
describe('KeystoreError', function () {
26+
it('a new instance should have the appropriate properties', function () {
27+
try {
28+
throwKeystoreError()
29+
} catch (err) {
30+
expect(err.name).to.equal('KeystoreError')
31+
expect(err instanceof KeystoreError).to.be.true()
32+
expect(err instanceof Error).to.be.true()
33+
expect(require('util').isError(err)).to.be.true()
34+
expect(err.stack).to.exist()
35+
expect(err.toString()).to.equal(`KeystoreError: ${message}`)
36+
expect(err.stack.split('\n')[0]).to.equal(`KeystoreError: ${message}`)
37+
expect(err.stack.split('\n')[1].indexOf('throwKeystoreError')).to.equal(7)
38+
}
39+
})
40+
})
41+
42+
describe('PrivateKeyNotFoundError', function () {
43+
it('a new instance should have the appropriate properties', function () {
44+
try {
45+
throwPrivateKeyNotFoundError()
46+
} catch (err) {
47+
expect(err.name).to.equal('PrivateKeyNotFoundError')
48+
expect(err instanceof PrivateKeyNotFoundError).to.be.true()
49+
expect(err instanceof KeystoreError).to.be.true()
50+
expect(err instanceof Error).to.be.true()
51+
expect(require('util').isError(err)).to.be.true()
52+
expect(err.stack).to.exist()
53+
expect(err.toString()).to.equal(`PrivateKeyNotFoundError: private key not found, key id: ${keyId}`)
54+
expect(err.keyId).to.equal(keyId)
55+
expect(err.message).to.equal(`private key not found, key id: ${keyId}`)
56+
expect(err.stack.split('\n')[0]).to.equal(`PrivateKeyNotFoundError: private key not found, key id: ${keyId}`)
57+
expect(err.stack.split('\n')[1].indexOf('throwPrivateKeyNotFoundError')).to.equal(7)
58+
}
59+
})
60+
})
61+
62+
describe('CertificateNotFoundError', function () {
63+
it('a new instance should have the appropriate properties', function () {
64+
try {
65+
throwCertificateNotFoundError()
66+
} catch (err) {
67+
expect(err.name).to.equal('CertificateNotFoundError')
68+
expect(err instanceof CertificateNotFoundError).to.be.true()
69+
expect(err instanceof KeystoreError).to.be.true()
70+
expect(err instanceof Error).to.be.true()
71+
expect(require('util').isError(err)).to.be.true()
72+
expect(err.stack).to.exist()
73+
expect(err.toString()).to.equal(`CertificateNotFoundError: certificate not found, key id: ${keyId}`)
74+
expect(err.keyId).to.equal(keyId)
75+
expect(err.message).to.equal(`certificate not found, key id: ${keyId}`)
76+
expect(err.stack.split('\n')[0]).to.equal(`CertificateNotFoundError: certificate not found, key id: ${keyId}`)
77+
expect(err.stack.split('\n')[1].indexOf('throwCertificateNotFoundError')).to.equal(7)
78+
}
79+
})
80+
})
81+
})

lib/index.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { KeystoreError, PrivateKeyNotFoundError, CertificateNotFoundError } = require('./errors')
2+
3+
module.exports = (debugName, baseDir, signingKeyPassphrases) => {
4+
const keystoreReaderFs = require('./keystore.reader.fs')(baseDir)
5+
const keystore = require('./keystore')(
6+
debugName, signingKeyPassphrases, keystoreReaderFs, 5 * 60 * 1000
7+
)
8+
return {
9+
getCurrentSigningKeyId: keystore.getCurrentSigningKeyId,
10+
getPrivateKey: keystore.getPrivateKey,
11+
getCertificate: keystore.getCertificate,
12+
getAllCertificatesAsJWK: keystore.getAllCertificatesAsJWK,
13+
KeystoreError,
14+
PrivateKeyNotFoundError,
15+
CertificateNotFoundError
16+
}
17+
}

lib/keystore.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/* eslint-disable padded-blocks */
2+
const Promise = require('bluebird')
3+
4+
const { PrivateKeyNotFoundError, CertificateNotFoundError } = require('./errors')
5+
6+
/**
7+
* Holds a cached private key and certificate data
8+
* @typedef KeyAndCert
9+
* @type {object}
10+
* @property {number} timestamp Timestamp of loading
11+
* @property {string} privKey Private key data in PEM format
12+
* @property {string} cert Certificate data in PEM format
13+
* @property {string} keyAlg JWT key algorithm (RS256, ES256, etc.)
14+
* @property {string} privKeyPassphrase Passphrase for the private key (if any)
15+
*/
16+
17+
/**
18+
* Holds a private key and the corresponding passphrase
19+
* @typedef PrivateKeyAndPassphrase
20+
* @type {object}
21+
* @property {string} key Private key data in PEM format
22+
* @property {string} passphrase Passphrase for the private key (if any)
23+
*/
24+
25+
/**
26+
* Synchronous or asnyhronous callback for loading private keys and certificates
27+
* @callback KeystoreService~keystoreReaderCallback
28+
* @return {Map.<KeyAndCert>} The loaded key and certificate data
29+
*/
30+
31+
/**
32+
* Creates a new keystore service
33+
* @param {Object} signingKeyPassphrases Stores passphrases for each signing keys. Key: key id, value: passphrase
34+
* @param {KeystoreService~keystoreReaderCallback} keystoreReader Keystore reader callback (sync or async)
35+
* @param {integer} refreshInterval Interval of the keystore refresh [millisec]
36+
*/
37+
module.exports = (debugName, signingKeyPassphrases, keystoreReader, refreshInterval) => {
38+
39+
const debug = require('debug')(debugName)
40+
41+
/** The current signing key ID */
42+
let currentSigningKeyId
43+
44+
/** Cache for the private key and certificate data */
45+
let keys = new Map()
46+
47+
// Reading keystore asynchronously
48+
const keystoreReaderAsync = Promise.method(keystoreReader)
49+
const keystoreReaderTask = () => {
50+
keystoreReaderAsync(keys)
51+
.then(newKeys => {
52+
debug('Keystore reloaded, keys: ', newKeys.keys())
53+
keys = newKeys
54+
currentSigningKeyId = selectCurrentSigningKeyId(keys)
55+
debug('Current signing key id: ' + currentSigningKeyId)
56+
})
57+
.catch(err => {
58+
debug('Reading keystore failed', err) // FIXME: should signal this error somehow. an EventEmitter maybe?
59+
})
60+
}
61+
keystoreReaderTask() // first call before starting timer
62+
setInterval(keystoreReaderTask, refreshInterval)
63+
64+
/**
65+
* Selects the current signing key id
66+
*
67+
* @param {Array} currentKeys The current keys
68+
* @return {string} The selected key id
69+
*/
70+
function selectCurrentSigningKeyId (currentKeys) {
71+
/* eslint-disable no-useless-return */
72+
if (currentKeys.size === 0) {
73+
return
74+
} else if (currentKeys.size === 1) {
75+
return currentKeys.keys().next().value
76+
} else {
77+
const now = Math.floor(Date.now() / 1000)
78+
const maxTimestamp = now - (20 * 60) // the youngest key to use
79+
const allKeys = Array.from(currentKeys)
80+
let allowedKeys = allKeys.filter(keyEntry => keyEntry[1].timestamp < maxTimestamp)
81+
debug('allowedKeys: ', allowedKeys)
82+
if (allowedKeys.length === 0) {
83+
allowedKeys = allKeys
84+
}
85+
allowedKeys = allowedKeys.sort((a, b) => a[0] > b[0] ? 1 : a[0] < b[0] ? -1 : 0)
86+
const entryToUse = allowedKeys[allowedKeys.length - 1]
87+
return entryToUse[0]
88+
}
89+
}
90+
91+
/**
92+
* Returns the ID of the signing key that has to be used for signing
93+
* @return {string} The ID of the key that has to be used for signing
94+
*/
95+
function getCurrentSigningKeyId () {
96+
return currentSigningKeyId
97+
}
98+
99+
/**
100+
* PRIVATE function! Returns the passphrase for the given private key
101+
* @param {string} id The id of the private key
102+
* @return {string} The passphrase for the private key
103+
*/
104+
function getPrivateKeyPassphrase (id) {
105+
return signingKeyPassphrases[id]
106+
}
107+
108+
/**
109+
* Returns the private key with the given id
110+
* Throws PrivateKeyNotFoundError if the certificate is not found in the store
111+
*
112+
* @param {string} id The private key id
113+
* @return {Promise.<PrivateKeyAndPassphrase, PrivateKeyNotFoundError>} Promise to the private key {key(PEM), passphrase}
114+
*/
115+
function getPrivateKey (id) {
116+
if (keys.has(id)) {
117+
const key = keys.get(id)
118+
return Promise.resolve({
119+
alg: key.keyAlg,
120+
key: key.privKey,
121+
passphrase: getPrivateKeyPassphrase(id)
122+
})
123+
} else {
124+
return Promise.reject(new PrivateKeyNotFoundError(`Loading private key ${id} failed`))
125+
}
126+
}
127+
128+
/**
129+
* Returns the certificate with the given id
130+
* Throws CertificateNotFoundError if the certificate is not found in the store
131+
*
132+
* @param {string} id The certificate id
133+
* @return {Promise.<string, CertificateNotFoundError>} Promise to the certificate (PEM)
134+
*/
135+
function getCertificate (id) {
136+
if (keys.has(id)) {
137+
const key = keys.get(id)
138+
return Promise.resolve({
139+
alg: key.keyAlg,
140+
cert: key.cert
141+
})
142+
} else {
143+
return Promise.reject(new CertificateNotFoundError(`Loading certificate ${id} failed`))
144+
}
145+
}
146+
147+
/**
148+
* Represents a public key with a certificate chain attached
149+
* @typedef JWK
150+
* @type {object}
151+
* @property {string} kid Key id
152+
* @property {string} x5c X.509 certificate chain
153+
*/
154+
155+
/**
156+
* Returns all certificates in JWKS format
157+
*
158+
* @return {Array.JWK} An array of JWK objects
159+
*/
160+
function getAllCertificatesAsJWKS () {
161+
const keySet = []
162+
keys.forEach((key) => {
163+
keySet.push(key.pubkeyJwk)
164+
})
165+
return keySet
166+
}
167+
168+
return {
169+
getCurrentSigningKeyId,
170+
getPrivateKey,
171+
getCertificate,
172+
getAllCertificatesAsJWKS,
173+
getPrivateKeyPassphrase,
174+
selectCurrentSigningKeyId
175+
}
176+
}

0 commit comments

Comments
 (0)