@superhero/eventflow-certificates 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENCE +21 -0
- package/README.md +218 -0
- package/index.js +298 -0
- package/index.test.js +115 -0
- package/package.json +37 -0
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Erik Landvall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# @superhero/eventflow-certificates
|
|
2
|
+
|
|
3
|
+
**Version:** 4.0.0
|
|
4
|
+
|
|
5
|
+
Eventflow Certificates is a TLS certificates management library designed for use within the Eventflow ecosystem. It handles the creation, management, and lifecycle of root, intermediate, and leaf certificates with encryption and secure storage capabilities.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @superhero/eventflow-certificates
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Root, intermediate, and leaf certificate generation
|
|
20
|
+
- Secure encryption and storage of private keys and passwords
|
|
21
|
+
- Automatic certificate renewal upon expiration
|
|
22
|
+
- Configurable encryption algorithms and certificate parameters
|
|
23
|
+
- Lazy-loading with caching to minimize resource usage
|
|
24
|
+
- Easy integration with Eventflow's database layer
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Dependencies
|
|
29
|
+
|
|
30
|
+
- [@superhero/eventflow-db](https://npmjs.com/package/@superhero/eventflow-db)
|
|
31
|
+
- [@superhero/log](https://npmjs.com/package/@superhero/log)
|
|
32
|
+
- [@superhero/openssl](https://npmjs.com/package/@superhero/openssl)
|
|
33
|
+
- [@superhero/deep](https://npmjs.com/package/@superhero/deep)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Example
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import Certificates from '@superhero/eventflow-certificates';
|
|
43
|
+
import Locator from '@superhero/locator';
|
|
44
|
+
import Config from '@superhero/config';
|
|
45
|
+
|
|
46
|
+
const locator = new Locator();
|
|
47
|
+
const config = new Config();
|
|
48
|
+
await config.add('@superhero/eventflow-db');
|
|
49
|
+
locator.set('@superhero/config', config);
|
|
50
|
+
|
|
51
|
+
const db = await locator.lazyload('@superhero/eventflow-db');
|
|
52
|
+
|
|
53
|
+
const configData =
|
|
54
|
+
{
|
|
55
|
+
CERT_PASS_ENCRYPTION_KEY : 'encryptionKey123',
|
|
56
|
+
CERT_ROOT_DAYS : 365,
|
|
57
|
+
CERT_INTERMEDIATE_DAYS : 30,
|
|
58
|
+
CERT_LEAF_DAYS : 7,
|
|
59
|
+
CERT_ALGORITHM : 'EdDSA:Ed25519',
|
|
60
|
+
CERT_HASH : 'sha256',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const intermediateUID = 'INTERMEDIATE-CERT-ID';
|
|
64
|
+
const leafUID = 'LEAF-CERT-ID';
|
|
65
|
+
const certificates = new Certificates(intermediateUID, leafUID, configData, db);
|
|
66
|
+
const rootCert = await certificates.root;
|
|
67
|
+
const intermediateCert = await certificates.intermediate;
|
|
68
|
+
const leafCert = await certificates.leaf;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
The `Certificates` class accepts a configuration object with the following properties:
|
|
76
|
+
|
|
77
|
+
| Property | Type | Description |
|
|
78
|
+
|---------------------------|----------|-----------------------------------------------------------------------------|
|
|
79
|
+
| `CERT_PASS_ENCRYPTION_KEY`| `string` | Encryption key used to secure certificate passwords and private keys. |
|
|
80
|
+
| `CERT_ROOT_DAYS` | `number` | Validity period of the root certificate in days. |
|
|
81
|
+
| `CERT_INTERMEDIATE_DAYS` | `number` | Validity period of the intermediate certificate in days. |
|
|
82
|
+
| `CERT_LEAF_DAYS` | `number` | Validity period of the leaf certificate in days. |
|
|
83
|
+
| `CERT_ALGORITHM` | `string` | Algorithm used for certificate generation (e.g., `rsa`, `ecdsa`). |
|
|
84
|
+
| `CERT_HASH` | `string` | Hash function for certificate signing (e.g., `sha256`, `sha512`). |
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Methods
|
|
89
|
+
|
|
90
|
+
### Constructor
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
constructor(intermediateUID, leafUID, config, db)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **Parameters:**
|
|
97
|
+
- `intermediateUID`: A unique identifier for the intermediate certificate.
|
|
98
|
+
- `leafUID`: A unique identifier for the leaf certificate.
|
|
99
|
+
- `config`: Configuration object.
|
|
100
|
+
- `db`: Database instance from Eventflow's database layer.
|
|
101
|
+
|
|
102
|
+
### clearCache
|
|
103
|
+
|
|
104
|
+
Clears the cached certificates.
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
clearCache();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### persist
|
|
111
|
+
|
|
112
|
+
Stores a certificate in the database.
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
persist(id, validity, cert, key, pass);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **Parameters:**
|
|
119
|
+
- `id`: Certificate identifier.
|
|
120
|
+
- `validity`: Expiration date of the certificate.
|
|
121
|
+
- `cert`: Certificate content.
|
|
122
|
+
- `key`: Private key of the certificate.
|
|
123
|
+
- `pass`: Password for the private key.
|
|
124
|
+
|
|
125
|
+
### revoke
|
|
126
|
+
|
|
127
|
+
Revokes a certificate by its ID.
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
revoke(id);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **Parameters:**
|
|
134
|
+
- `id`: Certificate identifier.
|
|
135
|
+
|
|
136
|
+
### root
|
|
137
|
+
|
|
138
|
+
Retrieves the root certificate.
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
const rootCertificate = await certificates.root;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### intermediate
|
|
145
|
+
|
|
146
|
+
Retrieves the intermediate certificate.
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
const intermediateCertificate = await certificates.intermediate;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### leaf
|
|
153
|
+
|
|
154
|
+
Retrieves the leaf certificate.
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const leafCertificate = await certificates.leaf;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Testing
|
|
163
|
+
|
|
164
|
+
Run the test suite with the following command:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run test-build
|
|
168
|
+
npm test
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The test suite includes comprehensive cases to validate functionality, such as:
|
|
172
|
+
|
|
173
|
+
- Certificate creation and retrieval
|
|
174
|
+
- Cache clearing and lazy loading
|
|
175
|
+
- Certificate expiration handling
|
|
176
|
+
- Error handling for missing configuration
|
|
177
|
+
|
|
178
|
+
### Test Coverage
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
▶ @superhero/eventflow-certificates
|
|
182
|
+
✔ Throw error if CERT_PASS_ENCRYPTION_KEY is missing in config (2.592666ms)
|
|
183
|
+
|
|
184
|
+
▶ Get root certificate
|
|
185
|
+
✔ Get same root certificate each time lazyloading it (0.443311ms)
|
|
186
|
+
|
|
187
|
+
▶ Get intermediate certificate
|
|
188
|
+
▶ Get leaf certificate
|
|
189
|
+
✔ Clear cache and still get the same certificates (5666.373892ms)
|
|
190
|
+
✔ Revoke certificate and regenerate when expired (11.303099ms)
|
|
191
|
+
✔ Get leaf certificate (7800.137773ms)
|
|
192
|
+
✔ Get intermediate certificate (9664.033522ms)
|
|
193
|
+
✔ Get root certificate (11692.320218ms)
|
|
194
|
+
✔ @superhero/eventflow-certificates (11699.129313ms)
|
|
195
|
+
|
|
196
|
+
tests 7
|
|
197
|
+
suites 1
|
|
198
|
+
pass 7
|
|
199
|
+
|
|
200
|
+
----------------------------------------------------------------------------------------
|
|
201
|
+
file | line % | branch % | funcs % | uncovered lines
|
|
202
|
+
----------------------------------------------------------------------------------------
|
|
203
|
+
index.js | 85.91 | 87.50 | 88.24 | 137-140 154-158 198-204 208-217 220-235
|
|
204
|
+
index.test.js | 100.00 | 100.00 | 100.00 |
|
|
205
|
+
----------------------------------------------------------------------------------------
|
|
206
|
+
all files | 89.83 | 91.67 | 92.86 |
|
|
207
|
+
----------------------------------------------------------------------------------------
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
This project is licensed under the MIT License.
|
|
215
|
+
|
|
216
|
+
## Contributing
|
|
217
|
+
|
|
218
|
+
Feel free to submit issues or pull requests for improvements or additional features.
|
package/index.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import Log from '@superhero/log'
|
|
3
|
+
import OpenSSL from '@superhero/openssl'
|
|
4
|
+
import deepassign from '@superhero/deep/assign'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @memberof Eventflow
|
|
8
|
+
*/
|
|
9
|
+
export default class Certificates
|
|
10
|
+
{
|
|
11
|
+
#db
|
|
12
|
+
log = new Log({ label: '[EVENTFLOW:CERTIFICATES]' })
|
|
13
|
+
#map = new Map()
|
|
14
|
+
openSSL = new OpenSSL()
|
|
15
|
+
config =
|
|
16
|
+
{
|
|
17
|
+
CERT_ALGORITHM : OpenSSL.ALGO.EdDSAEd448,
|
|
18
|
+
CERT_HASH : OpenSSL.HASH.SHA512,
|
|
19
|
+
CERT_ROOT_DAYS : 365,
|
|
20
|
+
CERT_INTERMEDIATE_DAYS : 30,
|
|
21
|
+
CERT_LEAF_DAYS : 7,
|
|
22
|
+
CERT_PASS_CIPHER : 'aes-256-gcm',
|
|
23
|
+
CERT_PASS_PBKDF2_HASH : 'sha512',
|
|
24
|
+
CERT_PASS_PBKDF2_BYTES : 32,
|
|
25
|
+
CERT_PASS_PBKDF2_ITERATIONS : 1e6
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
constructor(intermediateUID, leafUID, config, db)
|
|
29
|
+
{
|
|
30
|
+
this.intermediateUID = intermediateUID
|
|
31
|
+
this.leafUID = leafUID
|
|
32
|
+
this.#db = db
|
|
33
|
+
|
|
34
|
+
deepassign(this.config, config)
|
|
35
|
+
|
|
36
|
+
if(false === !!this.config.CERT_PASS_ENCRYPTION_KEY)
|
|
37
|
+
{
|
|
38
|
+
const error = new Error('missing configured certification password encryption key')
|
|
39
|
+
error.code = 'E_EVENTFLOW_CERTIFICATES_MISSING_CONFIGURATION'
|
|
40
|
+
error.cause = 'The encryption key is required to encrypt and decrypt the certificates password and private key'
|
|
41
|
+
throw error
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clearCache()
|
|
46
|
+
{
|
|
47
|
+
this.#map.clear()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Persist a specific certificate by ID in the database.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} id the certificate identifier
|
|
54
|
+
* @param {string} validity the validity expiration date of the certificate
|
|
55
|
+
* @param {string} cert the certificate
|
|
56
|
+
* @param {string} key the certificate private key
|
|
57
|
+
* @param {string} pass the certificate private key password
|
|
58
|
+
*
|
|
59
|
+
* @returns {Promise<boolean>} true if the certificate was persisted, false if it already exists
|
|
60
|
+
*/
|
|
61
|
+
perist(id, validity, cert, key, pass)
|
|
62
|
+
{
|
|
63
|
+
const
|
|
64
|
+
encryptionKey = this.config.CERT_PASS_ENCRYPTION_KEY,
|
|
65
|
+
encryptedKey = this.#encrypt(encryptionKey, key),
|
|
66
|
+
encryptedPass = this.#encrypt(encryptionKey, pass)
|
|
67
|
+
|
|
68
|
+
return this.#db.persistCertificate(
|
|
69
|
+
{
|
|
70
|
+
id,
|
|
71
|
+
validity,
|
|
72
|
+
cert,
|
|
73
|
+
// Encrypted Certificate Private Key
|
|
74
|
+
key : encryptedKey.encrypted,
|
|
75
|
+
key_salt : encryptedKey.salt,
|
|
76
|
+
key_iv : encryptedKey.iv,
|
|
77
|
+
key_tag : encryptedKey.tag,
|
|
78
|
+
// Encrypted Certificate Private Key Password
|
|
79
|
+
pass : encryptedPass.encrypted,
|
|
80
|
+
pass_salt : encryptedPass.salt,
|
|
81
|
+
pass_iv : encryptedPass.iv,
|
|
82
|
+
pass_tag : encryptedPass.tag
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Revoke a specific certificate by ID in the database.
|
|
88
|
+
* @param {string} id the certificate identifier
|
|
89
|
+
* @returns {Promise<boolean>} true if the certificate was revoked, false if it does not exist
|
|
90
|
+
*/
|
|
91
|
+
revoke(id)
|
|
92
|
+
{
|
|
93
|
+
return this.#db.revokeCertificate(id)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The root certificate authority (CA).
|
|
98
|
+
* @returns {Promise<{cert:string, key:string, pass:string}>}
|
|
99
|
+
*/
|
|
100
|
+
get root()
|
|
101
|
+
{
|
|
102
|
+
return this.#lazyload('EVENTFLOW-ROOT-CA', this.#createRoot.bind(this))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The intermediate certificate authority (ICA).
|
|
107
|
+
* @returns {Promise<{cert:string, key:string, pass:string}>}
|
|
108
|
+
*/
|
|
109
|
+
get intermediate()
|
|
110
|
+
{
|
|
111
|
+
return this.#lazyload(this.intermediateUID, this.#createIntermediate.bind(this))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The leaf end-entity certificate.
|
|
116
|
+
* @returns {Promise<{cert:string, key:string, pass:string}>}
|
|
117
|
+
*/
|
|
118
|
+
get leaf()
|
|
119
|
+
{
|
|
120
|
+
return this.#lazyload(this.leafUID, this.#createLeaf.bind(this))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async #lazyload(id, factory)
|
|
124
|
+
{
|
|
125
|
+
if(false === this.#map.has(id))
|
|
126
|
+
{
|
|
127
|
+
try
|
|
128
|
+
{
|
|
129
|
+
this.#map.set(id, await this.#readCertificate(id))
|
|
130
|
+
}
|
|
131
|
+
catch(error)
|
|
132
|
+
{
|
|
133
|
+
if('E_EVENTFLOW_DB_CERTIFICATE_NOT_FOUND' === error.code)
|
|
134
|
+
{
|
|
135
|
+
this.#map.set(id, await this.#eagerload(id, factory))
|
|
136
|
+
}
|
|
137
|
+
else
|
|
138
|
+
{
|
|
139
|
+
throw error
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.log.info`certificate loaded ${id}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const crt = this.#map.get(id)
|
|
147
|
+
|
|
148
|
+
// If the certificate is expired, then revoke it and create a new one.
|
|
149
|
+
// 10 minutes before expiration, the certificate is considered expired
|
|
150
|
+
// to prevent the certificate from being validated during a borderline
|
|
151
|
+
// expiration.
|
|
152
|
+
if(Date.now() + 6e5 > crt.validity)
|
|
153
|
+
{
|
|
154
|
+
this.log.warn`certificate expired ${id}`
|
|
155
|
+
this.#map.delete(id)
|
|
156
|
+
await this.#db.revokeCertificate(id)
|
|
157
|
+
return this.#lazyload(id, factory)
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
{
|
|
161
|
+
return crt
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async #readCertificate(id)
|
|
166
|
+
{
|
|
167
|
+
const { cert, key, key_iv, key_salt, key_tag,
|
|
168
|
+
validity, pass, pass_iv, pass_salt, pass_tag } = await this.#db.readCertificate(id)
|
|
169
|
+
const
|
|
170
|
+
encryptionKey = this.config.CERT_PASS_ENCRYPTION_KEY,
|
|
171
|
+
encryptedPass = { encrypted:pass, salt:pass_salt, iv:pass_iv, tag:pass_tag },
|
|
172
|
+
encryptedKey = { encrypted:key, salt:key_salt, iv:key_iv, tag:key_tag },
|
|
173
|
+
decryptedPass = this.#decrypt(encryptionKey, encryptedPass),
|
|
174
|
+
decryptedKey = this.#decrypt(encryptionKey, encryptedKey)
|
|
175
|
+
|
|
176
|
+
return { validity, cert, key:decryptedKey, pass:decryptedPass }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async #eagerload(id, factory)
|
|
180
|
+
{
|
|
181
|
+
const
|
|
182
|
+
keyPass = this.#generateRandomPassword(),
|
|
183
|
+
certificate = await factory(id, keyPass)
|
|
184
|
+
|
|
185
|
+
const { cert, key } = certificate
|
|
186
|
+
const
|
|
187
|
+
x509 = new crypto.X509Certificate(certificate.cert),
|
|
188
|
+
validity = x509.validToDate,
|
|
189
|
+
isPersisted = await this.perist(id, validity, cert, key, keyPass)
|
|
190
|
+
|
|
191
|
+
// If multiple replicas tries at the same time to creater the certificate,
|
|
192
|
+
// then only the first one to persist is valid, the rest should throw away
|
|
193
|
+
// the work they done and instead read what is persisted in the database.
|
|
194
|
+
if(isPersisted)
|
|
195
|
+
{
|
|
196
|
+
return { validity, cert, key, pass:keyPass }
|
|
197
|
+
}
|
|
198
|
+
else
|
|
199
|
+
{
|
|
200
|
+
// If the certificate was not persisted, then it was already created
|
|
201
|
+
// by another replica. We should then read the persisted certificate
|
|
202
|
+
// from the database, and return it instead of what has been generated.
|
|
203
|
+
return await this.#readCertificate(id)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#createRoot(UID, password)
|
|
208
|
+
{
|
|
209
|
+
return this.openSSL.root(
|
|
210
|
+
{
|
|
211
|
+
days : this.config.CERT_ROOT_DAYS,
|
|
212
|
+
algorithm : this.config.CERT_ALGORITHM,
|
|
213
|
+
hash : this.config.CERT_HASH,
|
|
214
|
+
subject : { UID },
|
|
215
|
+
password
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async #createIntermediate(UID, password)
|
|
220
|
+
{
|
|
221
|
+
const root = await this.root
|
|
222
|
+
return await this.openSSL.intermediate(root,
|
|
223
|
+
{
|
|
224
|
+
days : this.config.CERT_INTERMEDIATE_DAYS,
|
|
225
|
+
algorithm : this.config.CERT_ALGORITHM,
|
|
226
|
+
hash : this.config.CERT_HASH,
|
|
227
|
+
dns : [ '.' + UID ],
|
|
228
|
+
subject : { UID },
|
|
229
|
+
password :
|
|
230
|
+
{
|
|
231
|
+
input : root.pass,
|
|
232
|
+
output : password
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async #createLeaf(UID, password)
|
|
238
|
+
{
|
|
239
|
+
const ica = await this.intermediate
|
|
240
|
+
return await this.openSSL.leaf(ica,
|
|
241
|
+
{
|
|
242
|
+
days : this.config.CERT_LEAF_DAYS,
|
|
243
|
+
algorithm : this.config.CERT_ALGORITHM,
|
|
244
|
+
hash : this.config.CERT_HASH,
|
|
245
|
+
dns : [ this.leafUID ],
|
|
246
|
+
subject : { UID },
|
|
247
|
+
password :
|
|
248
|
+
{
|
|
249
|
+
input : ica.pass,
|
|
250
|
+
output : password
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#encrypt(password, decrypted)
|
|
256
|
+
{
|
|
257
|
+
const
|
|
258
|
+
cipher = this.config.CERT_PASS_CIPHER,
|
|
259
|
+
hash = this.config.CERT_PASS_PBKDF2_HASH,
|
|
260
|
+
bytes = this.config.CERT_PASS_PBKDF2_BYTES,
|
|
261
|
+
iterations = this.config.CERT_PASS_PBKDF2_ITERATIONS,
|
|
262
|
+
salt = crypto.randomBytes(16),
|
|
263
|
+
iv = crypto.randomBytes(16),
|
|
264
|
+
key = crypto.pbkdf2Sync(password, salt, iterations, bytes, hash), // derive key
|
|
265
|
+
cipheriv = crypto.createCipheriv(cipher, key, iv),
|
|
266
|
+
encrypted = Buffer.concat([ cipheriv.update(decrypted, 'utf8'), cipheriv.final() ]),
|
|
267
|
+
tag = cipheriv.getAuthTag() // helps identify corruption
|
|
268
|
+
|
|
269
|
+
return { encrypted, salt, iv, tag }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#decrypt(password, { encrypted, salt, iv, tag })
|
|
273
|
+
{
|
|
274
|
+
const
|
|
275
|
+
cipher = this.config.CERT_PASS_CIPHER,
|
|
276
|
+
hash = this.config.CERT_PASS_PBKDF2_HASH,
|
|
277
|
+
bytes = this.config.CERT_PASS_PBKDF2_BYTES,
|
|
278
|
+
iterations = this.config.CERT_PASS_PBKDF2_ITERATIONS,
|
|
279
|
+
key = crypto.pbkdf2Sync(password, salt, iterations, bytes, hash),
|
|
280
|
+
decipher = crypto.createDecipheriv(cipher, key, iv)
|
|
281
|
+
|
|
282
|
+
decipher.setAuthTag(tag)
|
|
283
|
+
|
|
284
|
+
const decrypted = Buffer.concat([ decipher.update(encrypted), decipher.final() ])
|
|
285
|
+
return decrypted.toString('utf8')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#generateRandomPassword()
|
|
289
|
+
{
|
|
290
|
+
const
|
|
291
|
+
length = crypto.randomInt(64, 128),
|
|
292
|
+
random = crypto.randomBytes(length),
|
|
293
|
+
ascii = random.toString('latin1'),
|
|
294
|
+
nonNull = ascii.replaceAll('\x00', '').replaceAll('$', '')
|
|
295
|
+
|
|
296
|
+
return nonNull
|
|
297
|
+
}
|
|
298
|
+
}
|
package/index.test.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import Config from '@superhero/config'
|
|
2
|
+
import Locator from '@superhero/locator'
|
|
3
|
+
import Certificates from '@superhero/eventflow-certificates'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
import { X509Certificate } from 'node:crypto'
|
|
6
|
+
import { suite, test, before, after } from 'node:test'
|
|
7
|
+
|
|
8
|
+
suite('@superhero/eventflow-certificates', async () =>
|
|
9
|
+
{
|
|
10
|
+
const
|
|
11
|
+
config = new Config(),
|
|
12
|
+
locator = new Locator()
|
|
13
|
+
|
|
14
|
+
await config.add('@superhero/eventflow-db')
|
|
15
|
+
locator.set('@superhero/config', config)
|
|
16
|
+
const db = await locator.lazyload('@superhero/eventflow-db')
|
|
17
|
+
|
|
18
|
+
let conf, manager, icaUID = 'INTERMEDIATE-CERT-ID', leafUID = 'LEAF-CERT-ID'
|
|
19
|
+
|
|
20
|
+
before(() =>
|
|
21
|
+
{
|
|
22
|
+
conf =
|
|
23
|
+
{
|
|
24
|
+
CERT_PASS_ENCRYPTION_KEY : 'encryptionKey123',
|
|
25
|
+
CERT_ROOT_DAYS : 365,
|
|
26
|
+
CERT_INTERMEDIATE_DAYS : 30,
|
|
27
|
+
CERT_LEAF_DAYS : 7,
|
|
28
|
+
CERT_ALGORITHM : 'rsa',
|
|
29
|
+
CERT_HASH : 'sha256'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
manager = new Certificates(icaUID, leafUID, conf, db)
|
|
33
|
+
manager.log.config.mute = true
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
after(() => db.close())
|
|
37
|
+
|
|
38
|
+
test('Throw error if CERT_PASS_ENCRYPTION_KEY is missing in config', () =>
|
|
39
|
+
{
|
|
40
|
+
delete conf.CERT_PASS_ENCRYPTION_KEY
|
|
41
|
+
assert.throws(
|
|
42
|
+
() => new Certificates(icaUID, leafUID, conf, db),
|
|
43
|
+
{ code: 'E_EVENTFLOW_CERTIFICATES_MISSING_CONFIGURATION' })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('Get root certificate', async (sub) =>
|
|
47
|
+
{
|
|
48
|
+
const root = await manager.root
|
|
49
|
+
|
|
50
|
+
assert.ok(root.validity > Date.now())
|
|
51
|
+
assert.ok(root.cert)
|
|
52
|
+
assert.ok(root.key)
|
|
53
|
+
assert.ok(root.pass)
|
|
54
|
+
|
|
55
|
+
const rootX509 = new X509Certificate(root.cert)
|
|
56
|
+
assert.ok(rootX509.checkIssued(rootX509))
|
|
57
|
+
|
|
58
|
+
await sub.test('Get same root certificate each time lazyloading it', async () =>
|
|
59
|
+
{
|
|
60
|
+
const root2 = await manager.root
|
|
61
|
+
assert.deepEqual(root, root2)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
await sub.test('Get intermediate certificate', async (sub) =>
|
|
65
|
+
{
|
|
66
|
+
const ica = await manager.intermediate
|
|
67
|
+
|
|
68
|
+
assert.ok(ica.validity > Date.now())
|
|
69
|
+
assert.ok(ica.cert)
|
|
70
|
+
assert.ok(ica.key)
|
|
71
|
+
assert.ok(ica.pass)
|
|
72
|
+
|
|
73
|
+
const icaX509 = new X509Certificate(ica.cert)
|
|
74
|
+
assert.ok(icaX509.checkIssued(rootX509))
|
|
75
|
+
|
|
76
|
+
await sub.test('Get leaf certificate', async (sub) =>
|
|
77
|
+
{
|
|
78
|
+
const leaf = await manager.leaf
|
|
79
|
+
|
|
80
|
+
assert.ok(leaf.validity > Date.now())
|
|
81
|
+
assert.ok(leaf.cert)
|
|
82
|
+
assert.ok(leaf.key)
|
|
83
|
+
assert.ok(leaf.pass)
|
|
84
|
+
|
|
85
|
+
const leafX509 = new X509Certificate(leaf.cert)
|
|
86
|
+
assert.ok(leafX509.checkIssued(icaX509))
|
|
87
|
+
|
|
88
|
+
await sub.test('Clear cache and still get the same certificates', async (sub) =>
|
|
89
|
+
{
|
|
90
|
+
manager.clearCache()
|
|
91
|
+
|
|
92
|
+
const
|
|
93
|
+
root2 = await manager.root,
|
|
94
|
+
ica2 = await manager.intermediate,
|
|
95
|
+
leaf2 = await manager.leaf
|
|
96
|
+
|
|
97
|
+
assert.deepEqual(root, root2)
|
|
98
|
+
assert.deepEqual(ica, ica2)
|
|
99
|
+
assert.deepEqual(leaf, leaf2)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
await sub.test('Revoke certificate and regenerate when expired', async () =>
|
|
103
|
+
{
|
|
104
|
+
await assert.doesNotReject(manager.revoke(leafUID))
|
|
105
|
+
|
|
106
|
+
const
|
|
107
|
+
leaf = await manager.leaf,
|
|
108
|
+
leafX509 = new X509Certificate(leaf.cert)
|
|
109
|
+
|
|
110
|
+
assert.ok(leafX509.checkIssued(icaX509))
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@superhero/eventflow-certificates",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Eventflow certificates is common tls certificates logic used in the eventflow ecosystem.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eventflow"
|
|
7
|
+
],
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@superhero/eventflow-db": "^4.1.0",
|
|
16
|
+
"@superhero/log": "^4.0.0",
|
|
17
|
+
"@superhero/openssl": "^4.0.2",
|
|
18
|
+
"@superhero/deep": "^4.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@superhero/config": "^4.1.2",
|
|
22
|
+
"@superhero/locator": "^4.2.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test-build": "npm explore @superhero/eventflow-db -- npm run test-build",
|
|
26
|
+
"test-only": "node --test-only --trace-warnings --test --experimental-test-coverage",
|
|
27
|
+
"test": "node --test --experimental-test-coverage"
|
|
28
|
+
},
|
|
29
|
+
"author": {
|
|
30
|
+
"name": "Erik Landvall",
|
|
31
|
+
"email": "erik@landvall.se"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/superhero/eventflow-certificates.git"
|
|
36
|
+
}
|
|
37
|
+
}
|