Certificate Generate Pure NodeJS
This post will show example code of how to generate a Certificate Authority and Host Certificates (signed by the CA we generate) using NodeJS without using OpenSSL. This will be done using the node-forge npm module. This post will assume you are familiar with Certificates Authorities and Host Certificates already. There is not much explanation given as the code is well documented.
Install Package
First step is to install the required npm package:
1
npm i node-forge
Setup
To start with we’ll need some helper functions for creating valid serial numbers, and also for generating the notBefore
and notAfter
dates. We will also define some constants that will be included in all the certificates (Country, State, Locality), as well as a boilerplate class with two methods we will fill in later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const forge = require('node-forge');
const makeNumberPositive = (hexString) => {
let mostSignificativeHexDigitAsInt = parseInt(hexString[0], 16);
if (mostSignificativeHexDigitAsInt < 8) return hexString;
mostSignificativeHexDigitAsInt -= 8
return mostSignificativeHexDigitAsInt.toString() + hexString.substring(1)
}
// Generate a random serial number for the Certificate
const randomSerialNumber = () => {
return makeNumberPositive(forge.util.bytesToHex(forge.random.getBytesSync(20)));
}
// Get the Not Before Date for a Certificate (will be valid from 2 days ago)
const getCertNotBefore = () => {
let twoDaysAgo = new Date(Date.now() - 60*60*24*2*1000);
let year = twoDaysAgo.getFullYear();
let month = (twoDaysAgo.getMonth() + 1).toString().padStart(2, '0');
let day = twoDaysAgo.getDate();
return new Date(`${year}-${month}-${day} 00:00:00Z`);
}
// Get Certificate Expiration Date (Valid for 90 Days)
const getCertNotAfter = (notBefore) => {
let ninetyDaysLater = new Date(notBefore.getTime() + 60*60*24*90*1000);
let year = ninetyDaysLater.getFullYear();
let month = (ninetyDaysLater.getMonth() + 1).toString().padStart(2, '0');
let day = ninetyDaysLater.getDate();
return new Date(`${year}-${month}-${day} 23:59:59Z`);
}
// Get CA Expiration Date (Valid for 100 Years)
const getCANotAfter = (notBefore) => {
let year = notBefore.getFullYear() + 100;
let month = (notBefore.getMonth() + 1).toString().padStart(2, '0');
let day = notBefore.getDate();
return new Date(`${year}-${month}-${day} 23:59:59Z`);
}
const DEFAULT_C = 'Australia';
const DEFAULT_ST = 'Victoria';
const DEFAULT_L = 'Melbourne';
class CertificateGeneration {
static CreateRootCA() {}
static CreateHostCert(hostCertCN, validDomains, rootCAObject) {}
}
Generating a Certificate Authority
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static CreateRootCA() {
// Create a new Keypair for the Root CA
const { privateKey, publicKey } = forge.pki.rsa.generateKeyPair(2048);
// Define the attributes for the new Root CA
const attributes = [{
shortName: 'C',
value: DEFAULT_C
}, {
shortName: 'ST',
value: DEFAULT_ST
}, {
shortName: 'L',
value: DEFAULT_L
}, {
shortName: 'CN',
value: 'My Custom Testing RootCA'
}];
const extensions = [{
name: 'basicConstraints',
cA: true
}, {
name: 'keyUsage',
keyCertSign: true,
cRLSign: true
}];
// Create an empty Certificate
const cert = forge.pki.createCertificate();
// Set the Certificate attributes for the new Root CA
cert.publicKey = publicKey;
cert.privateKey = privateKey;
cert.serialNumber = randomSerialNumber();
cert.validity.notBefore = getCertNotBefore();
cert.validity.notAfter = getCANotAfter(cert.validity.notBefore);
cert.setSubject(attributes);
cert.setIssuer(attributes);
cert.setExtensions(extensions);
// Self-sign the Certificate
cert.sign(privateKey, forge.md.sha512.create());
// Convert to PEM format
const pemCert = forge.pki.certificateToPem(cert);
const pemKey = forge.pki.privateKeyToPem(privateKey);
// Return the PEM encoded cert and private key
return { certificate: pemCert, privateKey: pemKey, notBefore: cert.validity.notBefore, notAfter: cert.validity.notAfter };
}
Generating a Host Certificate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
static CreateHostCert(hostCertCN, validDomains, rootCAObject) {
if (!hostCertCN.toString().trim()) throw new Error('"hostCertCN" must be a String');
if (!Array.isArray(validDomains)) throw new Error('"validDomains" must be an Array of Strings');
if (!rootCAObject || !rootCAObject.hasOwnProperty('certificate') || !rootCAObject.hasOwnProperty('privateKey')) throw new Error('"rootCAObject" must be an Object with the properties "certificate" & "privateKey"');
// Convert the Root CA PEM details, to a forge Object
let caCert = forge.pki.certificateFromPem(rootCAObject.certificate);
let caKey = forge.pki.privateKeyFromPem(rootCAObject.privateKey);
// Create a new Keypair for the Host Certificate
const hostKeys = forge.pki.rsa.generateKeyPair(2048);
// Define the attributes/properties for the Host Certificate
const attributes = [{
shortName: 'C',
value: DEFAULT_C
}, {
shortName: 'ST',
value: DEFAULT_ST
}, {
shortName: 'L',
value: DEFAULT_L
}, {
shortName: 'CN',
value: hostCertCN
}];
const extensions = [{
name: 'basicConstraints',
cA: false
}, {
name: 'nsCertType',
server: true
}, {
name: 'subjectKeyIdentifier'
}, {
name: 'authorityKeyIdentifier',
authorityCertIssuer: true,
serialNumber: caCert.serialNumber
}, {
name: 'keyUsage',
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true
}, {
name: 'extKeyUsage',
serverAuth: true
}, {
name: 'subjectAltName',
altNames: validDomains.map(domain => { return { type: 2, value: domain } })
}];
// Create an empty Certificate
let newHostCert = forge.pki.createCertificate();
// Set the attributes for the new Host Certificate
newHostCert.publicKey = hostKeys.publicKey;
newHostCert.serialNumber = randomSerialNumber();
newHostCert.validity.notBefore = getCertNotBefore();
newHostCert.validity.notAfter = getCertNotAfter(newHostCert.validity.notBefore);
newHostCert.setSubject(attributes);
newHostCert.setIssuer(caCert.subject.attributes);
newHostCert.setExtensions(extensions);
// Sign the new Host Certificate using the CA
newHostCert.sign(caKey, forge.md.sha512.create());
// Convert to PEM format
let pemHostCert = forge.pki.certificateToPem(newHostCert);
let pemHostKey = forge.pki.privateKeyToPem(hostKeys.privateKey);
return { certificate: pemHostCert, privateKey: pemHostKey, notAfter: newHostCert.validity.notBefore, notAfter: newHostCert.validity.notAfter };
}
Full Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
const forge = require('node-forge');
const makeNumberPositive = (hexString) => {
let mostSignificativeHexDigitAsInt = parseInt(hexString[0], 16);
if (mostSignificativeHexDigitAsInt < 8) return hexString;
mostSignificativeHexDigitAsInt -= 8
return mostSignificativeHexDigitAsInt.toString() + hexString.substring(1)
}
// Generate a random serial number for the Certificate
const randomSerialNumber = () => {
return makeNumberPositive(forge.util.bytesToHex(forge.random.getBytesSync(20)));
}
// Get the Not Before Date for a Certificate (will be valid from 2 days ago)
const getCertNotBefore = () => {
let twoDaysAgo = new Date(Date.now() - 60*60*24*2*1000);
let year = twoDaysAgo.getFullYear();
let month = (twoDaysAgo.getMonth() + 1).toString().padStart(2, '0');
let day = twoDaysAgo.getDate();
return new Date(`${year}-${month}-${day} 00:00:00Z`);
}
// Get Certificate Expiration Date (Valid for 90 Days)
const getCertNotAfter = (notBefore) => {
let ninetyDaysLater = new Date(notBefore.getTime() + 60*60*24*90*1000);
let year = ninetyDaysLater.getFullYear();
let month = (ninetyDaysLater.getMonth() + 1).toString().padStart(2, '0');
let day = ninetyDaysLater.getDate();
return new Date(`${year}-${month}-${day} 23:59:59Z`);
}
// Get CA Expiration Date (Valid for 100 Years)
const getCANotAfter = (notBefore) => {
let year = notBefore.getFullYear() + 100;
let month = (notBefore.getMonth() + 1).toString().padStart(2, '0');
let day = notBefore.getDate();
return new Date(`${year}-${month}-${day} 23:59:59Z`);
}
const DEFAULT_C = 'Australia';
const DEFAULT_ST = 'Victoria';
const DEFAULT_L = 'Melbourne';
class CertificateGeneration {
static CreateRootCA() {
// Create a new Keypair for the Root CA
const { privateKey, publicKey } = forge.pki.rsa.generateKeyPair(2048);
// Define the attributes for the new Root CA
const attributes = [{
shortName: 'C',
value: DEFAULT_C
}, {
shortName: 'ST',
value: DEFAULT_ST
}, {
shortName: 'L',
value: DEFAULT_L
}, {
shortName: 'CN',
value: 'My Custom Testing RootCA'
}];
const extensions = [{
name: 'basicConstraints',
cA: true
}, {
name: 'keyUsage',
keyCertSign: true,
cRLSign: true
}];
// Create an empty Certificate
const cert = forge.pki.createCertificate();
// Set the Certificate attributes for the new Root CA
cert.publicKey = publicKey;
cert.privateKey = privateKey;
cert.serialNumber = randomSerialNumber();
cert.validity.notBefore = getCertNotBefore();
cert.validity.notAfter = getCANotAfter(cert.validity.notBefore);
cert.setSubject(attributes);
cert.setIssuer(attributes);
cert.setExtensions(extensions);
// Self-sign the Certificate
cert.sign(privateKey, forge.md.sha512.create());
// Convert to PEM format
const pemCert = forge.pki.certificateToPem(cert);
const pemKey = forge.pki.privateKeyToPem(privateKey);
// Return the PEM encoded cert and private key
return { certificate: pemCert, privateKey: pemKey, notBefore: cert.validity.notBefore, notAfter: cert.validity.notAfter };
}
static CreateHostCert(hostCertCN, validDomains, rootCAObject) {
if (!hostCertCN.toString().trim()) throw new Error('"hostCertCN" must be a String');
if (!Array.isArray(validDomains)) throw new Error('"validDomains" must be an Array of Strings');
if (!rootCAObject || !rootCAObject.hasOwnProperty('certificate') || !rootCAObject.hasOwnProperty('privateKey')) throw new Error('"rootCAObject" must be an Object with the properties "certificate" & "privateKey"');
// Convert the Root CA PEM details, to a forge Object
let caCert = forge.pki.certificateFromPem(rootCAObject.certificate);
let caKey = forge.pki.privateKeyFromPem(rootCAObject.privateKey);
// Create a new Keypair for the Host Certificate
const hostKeys = forge.pki.rsa.generateKeyPair(2048);
// Define the attributes/properties for the Host Certificate
const attributes = [{
shortName: 'C',
value: DEFAULT_C
}, {
shortName: 'ST',
value: DEFAULT_ST
}, {
shortName: 'L',
value: DEFAULT_L
}, {
shortName: 'CN',
value: hostCertCN
}];
const extensions = [{
name: 'basicConstraints',
cA: false
}, {
name: 'nsCertType',
server: true
}, {
name: 'subjectKeyIdentifier'
}, {
name: 'authorityKeyIdentifier',
authorityCertIssuer: true,
serialNumber: caCert.serialNumber
}, {
name: 'keyUsage',
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true
}, {
name: 'extKeyUsage',
serverAuth: true
}, {
name: 'subjectAltName',
altNames: validDomains.map(domain => { return { type: 2, value: domain } })
}];
// Create an empty Certificate
let newHostCert = forge.pki.createCertificate();
// Set the attributes for the new Host Certificate
newHostCert.publicKey = hostKeys.publicKey;
newHostCert.serialNumber = randomSerialNumber();
newHostCert.validity.notBefore = getCertNotBefore();
newHostCert.validity.notAfter = getCertNotAfter(newHostCert.validity.notBefore);
newHostCert.setSubject(attributes);
newHostCert.setIssuer(caCert.subject.attributes);
newHostCert.setExtensions(extensions);
// Sign the new Host Certificate using the CA
newHostCert.sign(caKey, forge.md.sha512.create());
// Convert to PEM format
let pemHostCert = forge.pki.certificateToPem(newHostCert);
let pemHostKey = forge.pki.privateKeyToPem(hostKeys.privateKey);
return { certificate: pemHostCert, privateKey: pemHostKey, notAfter: newHostCert.validity.notBefore, notAfter: newHostCert.validity.notAfter };
}
}
let CA = CertificateGeneration.CreateRootCA();
/* The following certificate:
- Will be called 'testing.com'.
- Will be valid for 'testing.com' and 'test.com'.
- Will be signed by the CA we just created above.
*/
let hostCert = CertificateGeneration.CreateHostCert('testing.com', ['testing.com', 'test.com'], CA);
console.log(CA.certificate);
console.log(hostCert.certificate);