Skip to content
File

Blob: src/node/internal/crypto_x509.ts

typescript384 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26/* TODO: the following is adopted code, enabling linting one day */
27/* eslint-disable */
28 
29import { default as cryptoImpl, type CheckOptions } from 'node-internal:crypto';
30 
31import {
32 validateString,
33 validateObject,
34 validateBoolean,
35} from 'node-internal:validators';
36 
37import { isArrayBufferView } from 'node-internal:internal_types';
38 
39import { Buffer } from 'node-internal:internal_buffer';
40 
41import {
42 ERR_INVALID_ARG_TYPE,
43 ERR_INVALID_ARG_VALUE,
44} from 'node-internal:internal_errors';
45 
46import { kHandle } from 'node-internal:crypto_util';
47 
48import { PublicKeyObject, PrivateKeyObject } from 'node-internal:crypto_keys';
49 
50function translatePeerCertificate(c: any) {
51 if (!c) return null;
52 
53 if (c.issuerCertificate != null && c.issuerCertificate !== c) {
54 c.issuerCertificate = translatePeerCertificate(c.issuerCertificate);
55 }
56 if (c.infoAccess != null) {
57 const info = c.infoAccess;
58 c.infoAccess = {};
59 
60 // XXX: More key validation?
61 const regex = /([^\n:]*):([^\n]*)(?:\n|$)/g;
62 regex[Symbol.replace](info, (_: any, key: any, val: any): any => {
63 if (val.charCodeAt(0) === 0x22) {
64 // The translatePeerCertificate function is only
65 // used on internally created legacy certificate
66 // objects, and any value that contains a quote
67 // will always be a valid JSON string literal,
68 // so this should never throw.
69 val = JSON.parse(val);
70 }
71 if (key in c.infoAccess) c.infoAccess[key].push(val);
72 else c.infoAccess[key] = [val];
73 });
74 }
75 return c;
76}
77 
78function checkOptions(options?: CheckOptions) {
79 if (options == null) return;
80 validateObject(options, 'options');
81 if (options.multiLabelWildcards !== undefined)
82 validateBoolean(options.multiLabelWildcards, 'options.multiLabelWildcards');
83 if (options.partialWildcards !== undefined)
84 validateBoolean(options.partialWildcards, 'options.partialWildcards');
85 if (options.singleLabelSubdomains !== undefined)
86 validateBoolean(
87 options.singleLabelSubdomains,
88 'options.singleLabelSubdomains'
89 );
90 if (options.wildcards !== undefined)
91 validateBoolean(options.wildcards, 'options.wildcards');
92 if (options.subject !== undefined)
93 validateString(options.subject, 'options.subject');
94}
95 
96export class X509Certificate {
97 #handle?: cryptoImpl.X509Certificate = undefined;
98 #state = new Map();
99 
100 constructor(
101 buffer: ArrayBufferView | ArrayBuffer | cryptoImpl.X509Certificate | string
102 ) {
103 if (buffer instanceof cryptoImpl.X509Certificate) {
104 this.#handle = buffer;
105 return;
106 }
107 if (typeof buffer === 'string') {
108 buffer = Buffer.from(buffer);
109 }
110 if (!isArrayBufferView(buffer)) {
111 throw new ERR_INVALID_ARG_TYPE(
112 'buffer',
113 ['string', 'Buffer', 'TypedArray', 'DataView'],
114 buffer
115 );
116 }
117 const handle = cryptoImpl.X509Certificate.parse(buffer);
118 if (handle == null) {
119 throw new ERR_INVALID_ARG_VALUE(
120 'buffer',
121 buffer,
122 'is not a valid certificate'
123 );
124 }
125 this.#handle = handle;
126 }
127 
128 get subject() {
129 let value = this.#state.get('subject');
130 if (value === undefined) {
131 value = this.#handle!.subject;
132 this.#state.set('subject', value);
133 }
134 return value ?? undefined;
135 }
136 
137 get subjectAltName() {
138 let value = this.#state.get('subjectAltName');
139 if (value === undefined) {
140 value = this.#handle!.subjectAltName;
141 this.#state.set('subjectAltName', value);
142 }
143 return value ?? undefined;
144 }
145 
146 get issuer() {
147 let value = this.#state.get('issuer');
148 if (value === undefined) {
149 value = this.#handle!.issuer;
150 this.#state.set('issuer', value);
151 }
152 return value ?? undefined;
153 }
154 
155 get issuerCertificate() {
156 let value = this.#state.get('issuerCertificate');
157 if (value === undefined) {
158 const cert = this.#handle!.issuerCert;
159 if (cert) value = new X509Certificate(cert);
160 this.#state.set('issuerCertificate', value);
161 }
162 return value ?? undefined;
163 }
164 
165 get infoAccess() {
166 let value = this.#state.get('infoAccess');
167 if (value === undefined) {
168 value = this.#handle!.infoAccess;
169 this.#state.set('infoAccess', value);
170 }
171 return value ?? undefined;
172 }
173 
174 get validFrom() {
175 let value = this.#state.get('validFrom');
176 if (value === undefined) {
177 value = this.#handle!.validFrom;
178 this.#state.set('validFrom', value);
179 }
180 return value ?? undefined;
181 }
182 
183 get validTo() {
184 let value = this.#state.get('validTo');
185 if (value === undefined) {
186 value = this.#handle!.validTo;
187 this.#state.set('validTo', value);
188 }
189 return value ?? undefined;
190 }
191 
192 get fingerprint() {
193 let value = this.#state.get('fingerprint');
194 if (value === undefined) {
195 value = this.#handle!.fingerprint;
196 this.#state.set('fingerprint', value);
197 }
198 return value ?? undefined;
199 }
200 
201 get fingerprint256() {
202 let value = this.#state.get('fingerprint256');
203 if (value === undefined) {
204 value = this.#handle!.fingerprint256;
205 this.#state.set('fingerprint256', value);
206 }
207 return value ?? undefined;
208 }
209 
210 get fingerprint512() {
211 let value = this.#state.get('fingerprint512');
212 if (value === undefined) {
213 value = this.#handle!.fingerprint512;
214 this.#state.set('fingerprint512', value);
215 }
216 return value ?? undefined;
217 }
218 
219 get keyUsage() {
220 let value = this.#state.get('keyUsage');
221 if (value === undefined) {
222 value = this.#handle!.keyUsage;
223 this.#state.set('keyUsage', value);
224 }
225 return value ?? undefined;
226 }
227 
228 get serialNumber() {
229 let value = this.#state.get('serialNumber');
230 if (value === undefined) {
231 value = this.#handle!.serialNumber;
232 if (value != null) value = value.toUpperCase();
233 this.#state.set('serialNumber', value);
234 }
235 return value ?? undefined;
236 }
237 
238 get raw() {
239 let value = this.#state.get('raw');
240 if (value === undefined) {
241 value = this.#handle!.raw;
242 if (value != null) value = Buffer.from(value);
243 this.#state.set('raw', value);
244 }
245 return value ?? undefined;
246 }
247 
248 get publicKey() {
249 let value = this.#state.get('publicKey');
250 if (value === undefined) {
251 const inner = this.#handle!.publicKey;
252 if (inner !== undefined) {
253 value = PublicKeyObject.from(inner);
254 this.#state.set('publicKey', value);
255 }
256 }
257 return value ?? undefined;
258 }
259 
260 toString() {
261 let value = this.#state.get('pem');
262 if (value === undefined) {
263 value = this.#handle!.pem;
264 this.#state.set('pem', value);
265 }
266 return value ?? undefined;
267 }
268 
269 // There's no standardized JSON encoding for X509 certs so we
270 // fallback to providing the PEM encoding as a string.
271 toJSON() {
272 return this.toString();
273 }
274 
275 get ca() {
276 let value = this.#state.get('ca');
277 if (value === undefined) {
278 value = this.#handle!.isCA;
279 this.#state.set('ca', value);
280 }
281 return value ?? false;
282 }
283 
284 checkHost(name: string, options?: CheckOptions) {
285 validateString(name, 'name');
286 checkOptions(options);
287 return this.#handle!.checkHost(name, options) ?? undefined;
288 }
289 
290 checkEmail(email: string, options?: CheckOptions) {
291 validateString(email, 'email');
292 checkOptions(options);
293 return this.#handle!.checkEmail(email, options) ?? undefined;
294 }
295 
296 checkIP(ip: string, options?: CheckOptions) {
297 validateString(ip, 'ip');
298 checkOptions(options);
299 // The options argument is currently undocumented since none of the options
300 // have any effect on the behavior of this function. However, we still parse
301 // the options argument in case OpenSSL adds flags in the future that do
302 // affect the behavior of X509_check_ip. This ensures that no invalid values
303 // are passed as the second argument in the meantime.
304 return this.#handle!.checkIp(ip, options) ?? undefined;
305 }
306 
307 checkIssued(otherCert: X509Certificate) {
308 if (!(otherCert instanceof X509Certificate))
309 throw new ERR_INVALID_ARG_TYPE('otherCert', 'X509Certificate', otherCert);
310 return this.#handle!.checkIssued(otherCert.#handle!) ?? undefined;
311 }
312 
313 checkPrivateKey(pkey: PrivateKeyObject) {
314 if (!(pkey instanceof PrivateKeyObject))
315 throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey);
316 if (pkey.type !== 'private') throw new ERR_INVALID_ARG_VALUE('pkey', pkey);
317 return this.#handle!.checkPrivateKey(pkey[kHandle]) ?? undefined;
318 }
319 
320 verify(pkey: PublicKeyObject) {
321 if (!(pkey instanceof PublicKeyObject))
322 throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey);
323 if (pkey.type !== 'public') throw new ERR_INVALID_ARG_VALUE('pkey', pkey);
324 return this.#handle!.verify(pkey[kHandle]);
325 }
326 
327 toLegacyObject() {
328 let value = this.#state.get('legacy');
329 if (value === undefined) {
330 let {
331 subject,
332 subjectAltName,
333 infoAccess,
334 issuer,
335 ca,
336 modulus,
337 bits,
338 exponent,
339 pubkey,
340 asn1Curve,
341 nistCurve,
342 valid_from,
343 valid_to,
344 fingerprint,
345 fingerprint256,
346 fingerprint512,
347 serialNumber,
348 ext_key_usage,
349 raw,
350 } = this.#handle!.toLegacyObject() as any;
351 if (raw != null) raw = Buffer.from(raw);
352 if (pubkey != null) pubkey = Buffer.from(pubkey);
353 if (modulus != null) modulus = modulus.toUpperCase();
354 if (fingerprint != null) fingerprint = fingerprint.toUpperCase();
355 if (fingerprint256 != null) fingerprint256 = fingerprint256.toUpperCase();
356 if (fingerprint512 != null) fingerprint512 = fingerprint512.toUpperCase();
357 if (serialNumber != null) serialNumber = serialNumber.toUpperCase();
358 value = translatePeerCertificate({
359 subject,
360 subjectAltName,
361 infoAccess,
362 issuer,
363 ca,
364 modulus,
365 bits,
366 exponent,
367 pubkey,
368 asn1Curve,
369 nistCurve,
370 valid_from,
371 valid_to,
372 fingerprint,
373 fingerprint256,
374 fingerprint512,
375 serialNumber,
376 ext_key_usage,
377 raw,
378 });
379 this.#state.set('legacy', value);
380 }
381 return value;
382 }
383}