File
Blob: src/node/internal/crypto_x509.ts
| 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 | |
| 29 | import { default as cryptoImpl, type CheckOptions } from 'node-internal:crypto'; |
| 30 | |
| 31 | import { |
| 32 | validateString, |
| 33 | validateObject, |
| 34 | validateBoolean, |
| 35 | } from 'node-internal:validators'; |
| 36 | |
| 37 | import { isArrayBufferView } from 'node-internal:internal_types'; |
| 38 | |
| 39 | import { Buffer } from 'node-internal:internal_buffer'; |
| 40 | |
| 41 | import { |
| 42 | ERR_INVALID_ARG_TYPE, |
| 43 | ERR_INVALID_ARG_VALUE, |
| 44 | } from 'node-internal:internal_errors'; |
| 45 | |
| 46 | import { kHandle } from 'node-internal:crypto_util'; |
| 47 | |
| 48 | import { PublicKeyObject, PrivateKeyObject } from 'node-internal:crypto_keys'; |
| 49 | |
| 50 | function 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 | |
| 78 | function 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 | |
| 96 | export 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 | } |