Home Certificate Generate Pure NodeJS
Post
Cancel

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.

  1. Install Package
  2. Setup
  3. Generating a Certificate Authority
  4. Generating a Host Certificate
  5. Full Code

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);
This post is licensed under CC BY 4.0 by the author.