ds-01 1.0.8 → 1.0.9

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.
@@ -1 +1 @@
1
- {"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AAwTA;;;;GAIG;AAEH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD"}
1
+ {"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AAmZA;;;;GAIG;AAEH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD"}
@@ -23,24 +23,66 @@ function ensureSupportedOS() {
23
23
  * ---------------------------------------------------------
24
24
  * WINDOWS
25
25
  * ---------------------------------------------------------
26
+ *
27
+ * Windows uses:
28
+ *
29
+ * Microsoft Platform Crypto Provider
30
+ * RSA 2048
31
+ * NonExportable private key
32
+ *
33
+ * The private key never enters Node.js.
34
+ *
35
+ * Node -> PowerShell -> Windows crypto provider
36
+ * ↓
37
+ * private key
38
+ * ↓
39
+ * signature
40
+ * ↓
41
+ * Node
42
+ *
43
+ * IMPORTANT:
44
+ *
45
+ * We identify the certificate using its properties rather
46
+ * than blindly taking the first certificate with the same
47
+ * subject.
26
48
  */
49
+ /**
50
+ * PowerShell expression used to identify a valid DS01
51
+ * certificate.
52
+ *
53
+ * Requirements:
54
+ *
55
+ * - Correct subject
56
+ * - Has private key
57
+ * - RSA certificate
58
+ * - Platform Crypto Provider
59
+ */
60
+ const WINDOWS_CERT_FILTER = `
61
+ $_.Subject -eq "CN=${KEY_NAME}" -and
62
+ $_.HasPrivateKey -and
63
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
64
+ `;
27
65
  async function createWindowsDeviceKey() {
28
66
  const script = `
29
67
  $ErrorActionPreference = "Stop"
30
68
 
31
- $existing = Get-ChildItem Cert:\\CurrentUser\\My |
69
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
32
70
  Where-Object {
33
- $_.Subject -eq "CN=${KEY_NAME}"
34
- } |
35
- Select-Object -First 1
71
+ ${WINDOWS_CERT_FILTER}
72
+ })
73
+
74
+ if ($certificates.Count -gt 1) {
75
+ throw "Multiple valid DS01 device certificates found. Remove old DS01 device certificates before continuing."
76
+ }
36
77
 
37
- if ($existing) {
78
+ if ($certificates.Count -eq 1) {
38
79
  Write-Output "EXISTS"
39
80
  exit 0
40
81
  }
41
82
 
42
83
  $params = @{
43
84
  Type = "Custom"
85
+
44
86
  Subject = "CN=${KEY_NAME}"
45
87
 
46
88
  Provider = "Microsoft Platform Crypto Provider"
@@ -48,6 +90,7 @@ $params = @{
48
90
  KeyAlgorithm = "RSA"
49
91
  KeyLength = 2048
50
92
 
93
+ # Private key cannot be exported.
51
94
  KeyExportPolicy = "NonExportable"
52
95
 
53
96
  KeyUsage = "DigitalSignature"
@@ -58,7 +101,11 @@ $params = @{
58
101
  NotAfter = (Get-Date).AddYears(10)
59
102
  }
60
103
 
61
- New-SelfSignedCertificate @params | Out-Null
104
+ $certificate = New-SelfSignedCertificate @params
105
+
106
+ if (-not $certificate) {
107
+ throw "Windows failed to create the DS01 device certificate."
108
+ }
62
109
 
63
110
  Write-Output "CREATED"
64
111
  `;
@@ -77,20 +124,42 @@ Write-Output "CREATED"
77
124
  throw new Error("Failed to create Windows DS01 device key.");
78
125
  }
79
126
  }
80
- async function getWindowsPublicKey() {
81
- const script = `
82
- $ErrorActionPreference = "Stop"
83
-
84
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
127
+ /**
128
+ * Find the exact DS01 certificate.
129
+ *
130
+ * We deliberately DO NOT use Select-Object -First 1.
131
+ *
132
+ * If multiple matching certificates exist, we fail instead
133
+ * of potentially using the wrong private key.
134
+ */
135
+ function windowsCertificateLookupScript() {
136
+ return `
137
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
85
138
  Where-Object {
86
- $_.Subject -eq "CN=${KEY_NAME}"
87
- } |
88
- Select-Object -First 1
139
+ ${WINDOWS_CERT_FILTER}
140
+ })
89
141
 
90
- if (-not $cert) {
142
+ if ($certificates.Count -eq 0) {
91
143
  throw "DS01 device key not found."
92
144
  }
93
145
 
146
+ if ($certificates.Count -gt 1) {
147
+ throw "Multiple valid DS01 device certificates found."
148
+ }
149
+
150
+ $certificates[0]
151
+ `;
152
+ }
153
+ async function getWindowsPublicKey() {
154
+ const script = `
155
+ $ErrorActionPreference = "Stop"
156
+
157
+ $cert = ${windowsCertificateLookupScript()}
158
+
159
+ # Return the complete public certificate.
160
+ #
161
+ # The certificate contains the public key and certificate
162
+ # metadata, but never the private key.
94
163
  [Convert]::ToBase64String($cert.RawData)
95
164
  `;
96
165
  const { stdout } = await execFileAsync("powershell.exe", [
@@ -114,21 +183,14 @@ async function signWindowsDeviceKey(data) {
114
183
  const script = `
115
184
  $ErrorActionPreference = "Stop"
116
185
 
117
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
118
- Where-Object {
119
- $_.Subject -eq "CN=${KEY_NAME}"
120
- } |
121
- Select-Object -First 1
122
-
123
- if (-not $cert) {
124
- throw "DS01 device key not found."
125
- }
186
+ $cert = ${windowsCertificateLookupScript()}
126
187
 
127
188
  if (-not $cert.HasPrivateKey) {
128
189
  throw "DS01 device certificate has no private key."
129
190
  }
130
191
 
131
- $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
192
+ $rsa =
193
+ [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
132
194
 
133
195
  if (-not $rsa) {
134
196
  throw "Unable to access DS01 private signing key."
@@ -169,15 +231,20 @@ async function deleteWindowsDeviceKey() {
169
231
  const script = `
170
232
  $ErrorActionPreference = "Stop"
171
233
 
172
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
234
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
173
235
  Where-Object {
174
- $_.Subject -eq "CN=${KEY_NAME}"
175
- } |
176
- Select-Object -First 1
236
+ ${WINDOWS_CERT_FILTER}
237
+ })
177
238
 
178
- if ($cert) {
179
- Remove-Item $cert.PSPath
239
+ if ($certificates.Count -gt 1) {
240
+ throw "Multiple valid DS01 device certificates found."
180
241
  }
242
+
243
+ if ($certificates.Count -eq 1) {
244
+ Remove-Item $certificates[0].PSPath
245
+ }
246
+
247
+ Write-Output "DELETED"
181
248
  `;
182
249
  await execFileAsync("powershell.exe", [
183
250
  "-NoProfile",
@@ -208,8 +275,10 @@ async function runMacHelper(operation, data) {
208
275
  }
209
276
  async function createMacDeviceKey() {
210
277
  const result = await runMacHelper("create");
211
- if (result !== "CREATED" && result !== "EXISTS") {
212
- throw new Error(result || "Failed to create macOS Secure Enclave device key.");
278
+ if (result !== "CREATED" &&
279
+ result !== "EXISTS") {
280
+ throw new Error(result ||
281
+ "Failed to create macOS Secure Enclave device key.");
213
282
  }
214
283
  }
215
284
  async function getMacPublicKey() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ds-01",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Make your site best with DS01",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@ function getMacHelperPath(): string {
22
22
  );
23
23
  }
24
24
 
25
- function ensureSupportedOS() {
25
+ function ensureSupportedOS(): void {
26
26
  const platform = os.platform();
27
27
 
28
28
  if (platform !== "win32" && platform !== "darwin") {
@@ -36,25 +36,68 @@ function ensureSupportedOS() {
36
36
  * ---------------------------------------------------------
37
37
  * WINDOWS
38
38
  * ---------------------------------------------------------
39
+ *
40
+ * Windows uses:
41
+ *
42
+ * Microsoft Platform Crypto Provider
43
+ * RSA 2048
44
+ * NonExportable private key
45
+ *
46
+ * The private key never enters Node.js.
47
+ *
48
+ * Node -> PowerShell -> Windows crypto provider
49
+ * ↓
50
+ * private key
51
+ * ↓
52
+ * signature
53
+ * ↓
54
+ * Node
55
+ *
56
+ * IMPORTANT:
57
+ *
58
+ * We identify the certificate using its properties rather
59
+ * than blindly taking the first certificate with the same
60
+ * subject.
39
61
  */
40
62
 
63
+ /**
64
+ * PowerShell expression used to identify a valid DS01
65
+ * certificate.
66
+ *
67
+ * Requirements:
68
+ *
69
+ * - Correct subject
70
+ * - Has private key
71
+ * - RSA certificate
72
+ * - Platform Crypto Provider
73
+ */
74
+ const WINDOWS_CERT_FILTER = `
75
+ $_.Subject -eq "CN=${KEY_NAME}" -and
76
+ $_.HasPrivateKey -and
77
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
78
+ `;
79
+
41
80
  async function createWindowsDeviceKey(): Promise<void> {
42
81
  const script = `
43
82
  $ErrorActionPreference = "Stop"
44
83
 
45
- $existing = Get-ChildItem Cert:\\CurrentUser\\My |
84
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
46
85
  Where-Object {
47
- $_.Subject -eq "CN=${KEY_NAME}"
48
- } |
49
- Select-Object -First 1
86
+ ${WINDOWS_CERT_FILTER}
87
+ })
88
+
89
+ if ($certificates.Count -gt 1) {
90
+ throw "Multiple valid DS01 device certificates found. Remove old DS01 device certificates before continuing."
91
+ }
50
92
 
51
- if ($existing) {
93
+ if ($certificates.Count -eq 1) {
52
94
  Write-Output "EXISTS"
53
95
  exit 0
54
96
  }
55
97
 
56
98
  $params = @{
57
99
  Type = "Custom"
100
+
58
101
  Subject = "CN=${KEY_NAME}"
59
102
 
60
103
  Provider = "Microsoft Platform Crypto Provider"
@@ -62,6 +105,7 @@ $params = @{
62
105
  KeyAlgorithm = "RSA"
63
106
  KeyLength = 2048
64
107
 
108
+ # Private key cannot be exported.
65
109
  KeyExportPolicy = "NonExportable"
66
110
 
67
111
  KeyUsage = "DigitalSignature"
@@ -72,7 +116,11 @@ $params = @{
72
116
  NotAfter = (Get-Date).AddYears(10)
73
117
  }
74
118
 
75
- New-SelfSignedCertificate @params | Out-Null
119
+ $certificate = New-SelfSignedCertificate @params
120
+
121
+ if (-not $certificate) {
122
+ throw "Windows failed to create the DS01 device certificate."
123
+ }
76
124
 
77
125
  Write-Output "CREATED"
78
126
  `;
@@ -95,24 +143,49 @@ Write-Output "CREATED"
95
143
  const result = stdout.trim();
96
144
 
97
145
  if (result !== "CREATED" && result !== "EXISTS") {
98
- throw new Error("Failed to create Windows DS01 device key.");
146
+ throw new Error(
147
+ "Failed to create Windows DS01 device key.",
148
+ );
99
149
  }
100
150
  }
101
151
 
102
- async function getWindowsPublicKey(): Promise<string> {
103
- const script = `
104
- $ErrorActionPreference = "Stop"
105
-
106
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
152
+ /**
153
+ * Find the exact DS01 certificate.
154
+ *
155
+ * We deliberately DO NOT use Select-Object -First 1.
156
+ *
157
+ * If multiple matching certificates exist, we fail instead
158
+ * of potentially using the wrong private key.
159
+ */
160
+ function windowsCertificateLookupScript(): string {
161
+ return `
162
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
107
163
  Where-Object {
108
- $_.Subject -eq "CN=${KEY_NAME}"
109
- } |
110
- Select-Object -First 1
164
+ ${WINDOWS_CERT_FILTER}
165
+ })
111
166
 
112
- if (-not $cert) {
167
+ if ($certificates.Count -eq 0) {
113
168
  throw "DS01 device key not found."
114
169
  }
115
170
 
171
+ if ($certificates.Count -gt 1) {
172
+ throw "Multiple valid DS01 device certificates found."
173
+ }
174
+
175
+ $certificates[0]
176
+ `;
177
+ }
178
+
179
+ async function getWindowsPublicKey(): Promise<string> {
180
+ const script = `
181
+ $ErrorActionPreference = "Stop"
182
+
183
+ $cert = ${windowsCertificateLookupScript()}
184
+
185
+ # Return the complete public certificate.
186
+ #
187
+ # The certificate contains the public key and certificate
188
+ # metadata, but never the private key.
116
189
  [Convert]::ToBase64String($cert.RawData)
117
190
  `;
118
191
 
@@ -134,7 +207,9 @@ if (-not $cert) {
134
207
  const certificate = stdout.trim();
135
208
 
136
209
  if (!certificate) {
137
- throw new Error("Unable to read Windows DS01 public certificate.");
210
+ throw new Error(
211
+ "Unable to read Windows DS01 public certificate.",
212
+ );
138
213
  }
139
214
 
140
215
  return certificate;
@@ -143,26 +218,20 @@ if (-not $cert) {
143
218
  async function signWindowsDeviceKey(
144
219
  data: Uint8Array,
145
220
  ): Promise<Uint8Array> {
146
- const dataBase64 = Buffer.from(data).toString("base64");
221
+ const dataBase64 =
222
+ Buffer.from(data).toString("base64");
147
223
 
148
224
  const script = `
149
225
  $ErrorActionPreference = "Stop"
150
226
 
151
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
152
- Where-Object {
153
- $_.Subject -eq "CN=${KEY_NAME}"
154
- } |
155
- Select-Object -First 1
156
-
157
- if (-not $cert) {
158
- throw "DS01 device key not found."
159
- }
227
+ $cert = ${windowsCertificateLookupScript()}
160
228
 
161
229
  if (-not $cert.HasPrivateKey) {
162
230
  throw "DS01 device certificate has no private key."
163
231
  }
164
232
 
165
- $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
233
+ $rsa =
234
+ [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
166
235
 
167
236
  if (-not $rsa) {
168
237
  throw "Unable to access DS01 private signing key."
@@ -202,25 +271,34 @@ finally {
202
271
  const signature = stdout.trim();
203
272
 
204
273
  if (!signature) {
205
- throw new Error("Failed to sign data with Windows device key.");
274
+ throw new Error(
275
+ "Failed to sign data with Windows device key.",
276
+ );
206
277
  }
207
278
 
208
- return new Uint8Array(Buffer.from(signature, "base64"));
279
+ return new Uint8Array(
280
+ Buffer.from(signature, "base64"),
281
+ );
209
282
  }
210
283
 
211
284
  async function deleteWindowsDeviceKey(): Promise<void> {
212
285
  const script = `
213
286
  $ErrorActionPreference = "Stop"
214
287
 
215
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
288
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
216
289
  Where-Object {
217
- $_.Subject -eq "CN=${KEY_NAME}"
218
- } |
219
- Select-Object -First 1
290
+ ${WINDOWS_CERT_FILTER}
291
+ })
220
292
 
221
- if ($cert) {
222
- Remove-Item $cert.PSPath
293
+ if ($certificates.Count -gt 1) {
294
+ throw "Multiple valid DS01 device certificates found."
223
295
  }
296
+
297
+ if ($certificates.Count -eq 1) {
298
+ Remove-Item $certificates[0].PSPath
299
+ }
300
+
301
+ Write-Output "DELETED"
224
302
  `;
225
303
 
226
304
  await execFileAsync(
@@ -246,7 +324,11 @@ if ($cert) {
246
324
  */
247
325
 
248
326
  async function runMacHelper(
249
- operation: "create" | "public" | "sign" | "delete",
327
+ operation:
328
+ | "create"
329
+ | "public"
330
+ | "sign"
331
+ | "delete",
250
332
  data?: Uint8Array,
251
333
  ): Promise<string> {
252
334
  const helper = getMacHelperPath();
@@ -254,7 +336,9 @@ async function runMacHelper(
254
336
  const args = [helper, operation];
255
337
 
256
338
  if (data) {
257
- args.push(Buffer.from(data).toString("base64"));
339
+ args.push(
340
+ Buffer.from(data).toString("base64"),
341
+ );
258
342
  }
259
343
 
260
344
  const { stdout } = await execFileAsync(
@@ -269,17 +353,23 @@ async function runMacHelper(
269
353
  }
270
354
 
271
355
  async function createMacDeviceKey(): Promise<void> {
272
- const result = await runMacHelper("create");
356
+ const result =
357
+ await runMacHelper("create");
273
358
 
274
- if (result !== "CREATED" && result !== "EXISTS") {
359
+ if (
360
+ result !== "CREATED" &&
361
+ result !== "EXISTS"
362
+ ) {
275
363
  throw new Error(
276
- result || "Failed to create macOS Secure Enclave device key.",
364
+ result ||
365
+ "Failed to create macOS Secure Enclave device key.",
277
366
  );
278
367
  }
279
368
  }
280
369
 
281
370
  async function getMacPublicKey(): Promise<string> {
282
- const result = await runMacHelper("public");
371
+ const result =
372
+ await runMacHelper("public");
283
373
 
284
374
  if (!result) {
285
375
  throw new Error(
@@ -293,7 +383,8 @@ async function getMacPublicKey(): Promise<string> {
293
383
  async function signMacDeviceKey(
294
384
  data: Uint8Array,
295
385
  ): Promise<Uint8Array> {
296
- const result = await runMacHelper("sign", data);
386
+ const result =
387
+ await runMacHelper("sign", data);
297
388
 
298
389
  if (!result) {
299
390
  throw new Error(