1
>
/*---------------------------------------------------------------------------------------------
selfSignedCert.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
/**
7
>
* Result of generating a self-signed certificate.
8
>
*/
9
>
export interface ISelfSignedCert {
10
>
/** PEM-encoded private key. */
11
>
key: string;
12
>
/** PEM-encoded X.509 certificate. */
13
>
cert: string;
14
>
/** SHA-256 fingerprint in Electron's format: `sha256/<base64>`. */
15
>
fingerprint: string;
16
>
}
17
>
18
>
/**
19
>
* Generate a self-signed ECDSA (P-256) certificate for `127.0.0.1` using
20
>
* only Node's built-in `crypto` module. The certificate is valid for one
21
>
* year from the current time.
22
>
*
23
>
* The raw ASN.1/DER construction avoids external dependencies. Only a
24
>
* minimal X.509 v3 certificate is produced — just enough for TLS on the
25
>
* loopback interface with certificate pinning.
26
>
*
27
>
* **Security note:** this certificate is a defence-in-depth measure for a
28
>
* proxy that is already bound exclusively to `127.0.0.1`. TLS prevents
29
>
* other local processes from passively sniffing tunnel traffic and the
30
>
* pinned fingerprint stops active MITM on loopback. If certificate
31
>
* generation fails the proxy simply will not start — the failure is
32
>
* non-critical to the overall application.
33
>
*
34
>
* Do not rely on this certificate for security-critical scenarios.
35
>
*/
36
>
export async function generateSelfSignedCert(): Promise<ISelfSignedCert> {
37
>
const crypto = await import('crypto');
38
>
39
>
const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
40
>
namedCurve: 'prime256v1',
41
>
publicKeyEncoding: { type: 'spki', format: 'pem' },
42
>
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
43
>
});
44
>
45
>
const cert = createSelfSignedCertPem(crypto, privateKey, publicKey);
46
>
47
>
// Compute SHA-256 fingerprint in Electron's format: "sha256/<base64>"
48
>
const certDer = pemToDer(cert);
49
>
const hash = crypto.createHash('sha256').update(certDer).digest('base64');
50
>
const fingerprint = `sha256/${hash}`;
51
>
52
>
return { key: privateKey, cert, fingerprint };
53
>
}
54
>
55
>
/**
56
>
* Build a minimal self-signed X.509 v3 certificate in DER, then
57
>
* PEM-encode it. Uses raw ASN.1 construction to avoid external
58
>
* dependencies.
59
>
*/
60
>
function createSelfSignedCertPem(
61
>
crypto: typeof import('crypto'),
62
>
privateKeyPem: string,
63
>
publicKeyPem: string,
64
>
): string {
65
>
// Parse the SPKI public key from PEM
66
>
const spkiDer = pemToDer(publicKeyPem);
67
>
68
>
// Build the TBS (To Be Signed) certificate
69
>
const serial = crypto.randomBytes(8);
70
>
// Ensure serial is positive (clear high bit)
71
>
serial[0] &= 0x7f;
72
>
73
>
const now = new Date();
74
>
const notAfter = new Date(now);
75
>
notAfter.setFullYear(now.getFullYear() + 1);
76
>
77
>
const cnOid = derOid(Buffer.from([0x55, 0x04, 0x03])); // 2.5.4.3
78
>
79
>
const issuerAndSubject = derSequence([
80
>
derSet([
81
>
derSequence([
82
>
cnOid,
83
>
derUtf8String('TunnelProxy'),
84
>
]),
85
>
]),
86
>
]);
87
>
88
>
// Validity
89
>
const validity = derSequence([
90
>
derTime(now),
91
>
derTime(notAfter),
92
>
]);
93
>
94
>
// Version v3 [0] EXPLICIT INTEGER 2
95
>
const version = Buffer.from([0xa0, 0x03, 0x02, 0x01, 0x02]);
96
>
97
>
// Serial number
98
>
const serialNumber = derInteger(serial);
99
>
100
>
// Signature algorithm: ecdsa-with-SHA256 (1.2.840.10045.4.3.2)
101
>
const sigAlgOidBytes = Buffer.from([0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]);
102
>
const sigAlg = derSequence([derOid(sigAlgOidBytes)]);
103
>
104
>
// Extensions [3] EXPLICIT SEQUENCE — SAN with IP 127.0.0.1
105
>
const sanExtension = buildSanExtension();
106
>
const extensions = Buffer.concat([
107
>
Buffer.from([0xa3]),
108
>
derLengthPrefix(derSequence([sanExtension])),
109
>
]);
110
>
111
>
// TBSCertificate
112
>
const tbs = derSequence([
113
>
version,
114
>
serialNumber,
115
>
sigAlg,
116
>
issuerAndSubject,
117
>
validity,
118
>
issuerAndSubject,
119
>
spkiDer,
120
>
extensions,
121
>
]);
122
>
123
>
// Sign the TBS
124
>
const signer = crypto.createSign('SHA256');
125
>
signer.update(tbs);
126
>
const signature = signer.sign(privateKeyPem);
127
>
128
>
// Wrap signature as BIT STRING
129
>
const sigBitString = Buffer.concat([
130
>
Buffer.from([0x03]),
131
>
derLength(signature.length + 1),
132
>
Buffer.from([0x00]), // no unused bits
133
>
signature,
134
>
]);
135
>
136
>
// Full certificate
137
>
const certDer = derSequence([tbs, sigAlg, sigBitString]);
138
>
139
>
// PEM encode
140
>
const b64 = certDer.toString('base64');
141
>
const lines: string[] = [];
142
>
for (let i = 0; i < b64.length; i += 64) {
143
>
lines.push(b64.substring(i, i + 64));
144
>
}
145
>
return `-----BEGIN CERTIFICATE-----\n${lines.join('\n')}\n-----END CERTIFICATE-----\n`;
146
>
}
147
>
148
>
/** Build a SAN extension with IP:127.0.0.1 */
149
>
function buildSanExtension(): Buffer {
150
>
// Extension OID: 2.5.29.17 (subjectAltName)
151
>
const sanOid = derOid(Buffer.from([0x55, 0x1d, 0x11]));
152
>
153
>
// GeneralName: iPAddress [7] 127.0.0.1
154
>
const ipBytes = Buffer.from([0x87, 0x04, 0x7f, 0x00, 0x00, 0x01]);
155
>
156
>
const sanValue = derOctetString(derSequence([ipBytes]));
157
>
158
>
return derSequence([sanOid, sanValue]);
159
>
}
160
>
161
>
// #region ASN.1 DER helpers
162
>
163
>
function pemToDer(pem: string): Buffer {
164
>
const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s/g, '');
165
>
return Buffer.from(b64, 'base64');
166
>
}
167
>
168
>
function derLength(length: number): Buffer {
169
>
if (length < 0x80) {
170
>
return Buffer.from([length]);
171
>
} else if (length < 0x100) {
172
>
return Buffer.from([0x81, length]);
173
>
} else if (length < 0x10000) {
174
>
return Buffer.from([0x82, (length >> 8) & 0xff, length & 0xff]);
175
>
} else if (length < 0x1000000) {
176
return Buffer.from([0x83, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]);
177
} else {