File
Blob: src/node/internal/crypto_sign.ts
| 1 | // Copyright (c) 2017-2023 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 { |
| 30 | default as cryptoImpl, |
| 31 | type SignHandle, |
| 32 | type VerifyHandle, |
| 33 | type CreateAsymmetricKeyOptions, |
| 34 | } from 'node-internal:crypto'; |
| 35 | |
| 36 | import { validateString } from 'node-internal:validators'; |
| 37 | |
| 38 | import { Buffer } from 'node-internal:internal_buffer'; |
| 39 | |
| 40 | import { |
| 41 | KeyObject, |
| 42 | isKeyObject, |
| 43 | getKeyObjectHandle, |
| 44 | createPrivateKey, |
| 45 | createPublicKey, |
| 46 | } from 'node-internal:crypto_keys'; |
| 47 | |
| 48 | import { |
| 49 | ERR_CRYPTO_SIGN_KEY_REQUIRED, |
| 50 | ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, |
| 51 | ERR_INVALID_ARG_TYPE, |
| 52 | ERR_INVALID_ARG_VALUE, |
| 53 | } from 'node-internal:internal_errors'; |
| 54 | |
| 55 | import { Writable } from 'node-internal:streams_writable'; |
| 56 | import { isArrayBufferView } from 'node-internal:internal_types'; |
| 57 | |
| 58 | export interface SignOptions {} |
| 59 | |
| 60 | export interface Sign extends Writable { |
| 61 | [kHandle]: SignHandle; |
| 62 | update(data: any): void; |
| 63 | sign(privateKey: any): any; |
| 64 | } |
| 65 | export interface Verify extends Writable { |
| 66 | [kHandle]: VerifyHandle; |
| 67 | update(data: any): void; |
| 68 | verify(publicKey: any, signature: any): any; |
| 69 | } |
| 70 | |
| 71 | const kHandle = Symbol('kHandle'); |
| 72 | |
| 73 | // Uses old-style class syntax for Node.js compatibility |
| 74 | export const Sign = function (this: Sign, algorithm: string, options: any) { |
| 75 | if (!(this instanceof Sign)) return new Sign(algorithm, options); |
| 76 | validateString(algorithm, 'algorithm'); |
| 77 | this[kHandle] = new cryptoImpl.SignHandle(algorithm); |
| 78 | Writable.call(this, options); |
| 79 | return this; |
| 80 | } as any as { |
| 81 | new (algorithm: string, options?: SignOptions): Sign; |
| 82 | }; |
| 83 | Object.setPrototypeOf(Sign.prototype, Writable.prototype); |
| 84 | Object.setPrototypeOf(Sign, Writable); |
| 85 | |
| 86 | Sign.prototype._write = function _write( |
| 87 | chunk: string | ArrayBufferView, |
| 88 | encoding: string | undefined | null, |
| 89 | callback: (err?: any) => void |
| 90 | ) { |
| 91 | this.update(chunk, encoding); |
| 92 | callback(); |
| 93 | }; |
| 94 | |
| 95 | Sign.prototype.update = function ( |
| 96 | this: Sign, |
| 97 | data: string | ArrayBufferView, |
| 98 | encoding?: string |
| 99 | ) { |
| 100 | if (typeof data === 'string') { |
| 101 | data = Buffer.from(data, encoding); |
| 102 | } |
| 103 | if (!isArrayBufferView(data)) { |
| 104 | throw new ERR_INVALID_ARG_TYPE( |
| 105 | 'data', |
| 106 | ['string', 'Buffer', 'TypedArray', 'DataView'], |
| 107 | data |
| 108 | ); |
| 109 | } |
| 110 | this[kHandle].update(data); |
| 111 | return this; |
| 112 | }; |
| 113 | |
| 114 | function getIntOption(name: string, options: any): number | undefined { |
| 115 | const value = options[name]; |
| 116 | if (value !== undefined) { |
| 117 | if (value === value >> 0) { |
| 118 | return value; |
| 119 | } |
| 120 | throw new ERR_INVALID_ARG_VALUE(`options.${name}`, value); |
| 121 | } |
| 122 | return undefined; |
| 123 | } |
| 124 | |
| 125 | function getDSASignatureEncoding(options: any): number { |
| 126 | if (typeof options === 'object') { |
| 127 | const { dsaEncoding = 'der' } = options; |
| 128 | if (dsaEncoding === 'der') |
| 129 | return 0; // kSigEncDER; |
| 130 | else if (dsaEncoding === 'ieee-p1363') return 1; // kSigEncP1363 |
| 131 | throw new ERR_INVALID_ARG_VALUE('options.dsaEncoding', dsaEncoding); |
| 132 | } |
| 133 | |
| 134 | return 0; |
| 135 | } |
| 136 | |
| 137 | function getPrivateKey(options: any): CryptoKey { |
| 138 | if (options instanceof CryptoKey) { |
| 139 | return options as CryptoKey; |
| 140 | } else if (isKeyObject(options)) { |
| 141 | const keyObject = options as KeyObject; |
| 142 | if (keyObject.type === 'secret') { |
| 143 | throw new ERR_INVALID_ARG_TYPE( |
| 144 | 'options', |
| 145 | ['PublicKeyObject', 'PrivateKeyObject', 'CryptoKey', 'object'], |
| 146 | keyObject.type |
| 147 | ); |
| 148 | } |
| 149 | if (keyObject.type === 'public') { |
| 150 | throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE('public', 'private'); |
| 151 | } |
| 152 | return getKeyObjectHandle(keyObject); |
| 153 | } else { |
| 154 | return getKeyObjectHandle( |
| 155 | createPrivateKey(options as CreateAsymmetricKeyOptions) |
| 156 | ); |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | Sign.prototype.sign = function ( |
| 161 | this: Sign, |
| 162 | options: any, |
| 163 | encoding?: string |
| 164 | ): Buffer | string { |
| 165 | if (!options) { |
| 166 | throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); |
| 167 | } |
| 168 | |
| 169 | const key = getPrivateKey(options); |
| 170 | |
| 171 | // Options specific to RSA |
| 172 | const rsaPadding = getIntOption('padding', options); |
| 173 | const pssSaltLength = getIntOption('saltLength', options); |
| 174 | |
| 175 | // Options specific to (EC)DSA |
| 176 | const dsaSigEnc = getDSASignatureEncoding(options); |
| 177 | |
| 178 | const u8 = this[kHandle].sign(key, rsaPadding, pssSaltLength, dsaSigEnc); |
| 179 | |
| 180 | const res = Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength); |
| 181 | |
| 182 | if (encoding && encoding !== 'buffer') { |
| 183 | return res.toString(encoding); |
| 184 | } |
| 185 | |
| 186 | return res; |
| 187 | }; |
| 188 | |
| 189 | // Uses old-style class syntax for Node.js compatibility |
| 190 | export const Verify = function (this: Verify, algorithm: string, options: any) { |
| 191 | if (!(this instanceof Verify)) return new Verify(algorithm, options); |
| 192 | validateString(algorithm, 'algorithm'); |
| 193 | this[kHandle] = new cryptoImpl.VerifyHandle(algorithm); |
| 194 | Writable.call(this, options); |
| 195 | return this; |
| 196 | } as any as { |
| 197 | new (algorithm: string, options?: SignOptions): Verify; |
| 198 | }; |
| 199 | Object.setPrototypeOf(Verify.prototype, Writable.prototype); |
| 200 | Object.setPrototypeOf(Verify, Writable); |
| 201 | |
| 202 | Verify.prototype._write = function _write( |
| 203 | chunk: string | ArrayBufferView, |
| 204 | encoding: string | undefined | null, |
| 205 | callback: (err?: unknown) => void |
| 206 | ) { |
| 207 | this.update(chunk, encoding); |
| 208 | callback(); |
| 209 | }; |
| 210 | |
| 211 | Verify.prototype.update = function ( |
| 212 | this: Verify, |
| 213 | data: string | ArrayBufferView, |
| 214 | encoding?: string |
| 215 | ) { |
| 216 | if (typeof data === 'string') { |
| 217 | data = Buffer.from(data, encoding); |
| 218 | } |
| 219 | if (!isArrayBufferView(data)) { |
| 220 | throw new ERR_INVALID_ARG_TYPE( |
| 221 | 'data', |
| 222 | ['string', 'Buffer', 'TypedArray', 'DataView'], |
| 223 | data |
| 224 | ); |
| 225 | } |
| 226 | this[kHandle].update(data); |
| 227 | return this; |
| 228 | }; |
| 229 | |
| 230 | Verify.prototype.verify = function ( |
| 231 | this: Verify, |
| 232 | options: any, |
| 233 | signature: ArrayBufferView | string, |
| 234 | encoding: string = 'utf8' |
| 235 | ) { |
| 236 | if (!options) { |
| 237 | throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); |
| 238 | } |
| 239 | |
| 240 | let key: CryptoKey; |
| 241 | if (options instanceof CryptoKey) { |
| 242 | key = options as CryptoKey; |
| 243 | } else if (isKeyObject(options)) { |
| 244 | key = getKeyObjectHandle(options as KeyObject); |
| 245 | } else { |
| 246 | key = getKeyObjectHandle( |
| 247 | createPublicKey(options as CreateAsymmetricKeyOptions) |
| 248 | ); |
| 249 | } |
| 250 | if (!(key instanceof CryptoKey)) { |
| 251 | throw new ERR_INVALID_ARG_TYPE( |
| 252 | 'options', |
| 253 | ['KeyObject', 'CryptoKey', 'object'], |
| 254 | key |
| 255 | ); |
| 256 | } |
| 257 | |
| 258 | // Options specific to RSA |
| 259 | const rsaPadding = getIntOption('padding', options); |
| 260 | const pssSaltLength = getIntOption('saltLength', options); |
| 261 | |
| 262 | // Options specific to (EC)DSA |
| 263 | const dsaSigEnc = getDSASignatureEncoding(options); |
| 264 | |
| 265 | if (typeof signature === 'string') { |
| 266 | signature = Buffer.from(signature, encoding); |
| 267 | } |
| 268 | |
| 269 | return this[kHandle].verify( |
| 270 | key, |
| 271 | signature, |
| 272 | rsaPadding, |
| 273 | pssSaltLength, |
| 274 | dsaSigEnc |
| 275 | ); |
| 276 | }; |
| 277 | |
| 278 | export function createSign(algorithm: string, options: any) { |
| 279 | return new Sign(algorithm, options); |
| 280 | } |
| 281 | |
| 282 | export function createVerify(algorithm: string, options: any) { |
| 283 | return new Verify(algorithm, options); |
| 284 | } |
| 285 | |
| 286 | type SignCallback = (err: any, signature?: Buffer) => void; |
| 287 | type VerifyCallback = (err: any, valid?: boolean) => void; |
| 288 | |
| 289 | export function sign( |
| 290 | algorithm: string | null | undefined, |
| 291 | data: BufferSource, |
| 292 | options: any, |
| 293 | callback?: SignCallback |
| 294 | ): Buffer | void { |
| 295 | if (algorithm != null) { |
| 296 | validateString(algorithm, 'algorithm'); |
| 297 | } else { |
| 298 | algorithm = undefined; |
| 299 | } |
| 300 | if (!isArrayBufferView(data)) { |
| 301 | throw new ERR_INVALID_ARG_TYPE( |
| 302 | 'data', |
| 303 | ['Buffer', 'TypedArray', 'DataView'], |
| 304 | data |
| 305 | ); |
| 306 | } |
| 307 | |
| 308 | if (!options) { |
| 309 | throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); |
| 310 | } |
| 311 | |
| 312 | const key = getPrivateKey(options); |
| 313 | |
| 314 | // Options specific to RSA |
| 315 | const rsaPadding = getIntOption('padding', options); |
| 316 | const pssSaltLength = getIntOption('saltLength', options); |
| 317 | |
| 318 | // Options specific to (EC)DSA |
| 319 | const dsaSigEnc = getDSASignatureEncoding(options); |
| 320 | |
| 321 | if (callback === undefined) { |
| 322 | return Buffer.from( |
| 323 | cryptoImpl.signOneShot( |
| 324 | key, |
| 325 | algorithm, |
| 326 | data, |
| 327 | rsaPadding, |
| 328 | pssSaltLength, |
| 329 | dsaSigEnc |
| 330 | ) |
| 331 | ); |
| 332 | } |
| 333 | |
| 334 | try { |
| 335 | const signature = Buffer.from( |
| 336 | cryptoImpl.signOneShot( |
| 337 | key, |
| 338 | algorithm, |
| 339 | data, |
| 340 | rsaPadding, |
| 341 | pssSaltLength, |
| 342 | dsaSigEnc |
| 343 | ) |
| 344 | ); |
| 345 | queueMicrotask(() => callback(null, signature)); |
| 346 | } catch (err) { |
| 347 | queueMicrotask(() => callback(err)); |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | export function verify( |
| 352 | algorithm: string | null | undefined, |
| 353 | data: BufferSource, |
| 354 | options: any, |
| 355 | signature: BufferSource, |
| 356 | callback?: VerifyCallback |
| 357 | ): boolean | undefined { |
| 358 | // Node.js allows the algorithm to be either undefined or null, in which |
| 359 | // case we just normalize it to undefined. |
| 360 | if (algorithm != null) { |
| 361 | validateString(algorithm, 'algorithm'); |
| 362 | } else { |
| 363 | algorithm = undefined; |
| 364 | } |
| 365 | |
| 366 | if (!isArrayBufferView(data)) { |
| 367 | throw new ERR_INVALID_ARG_TYPE( |
| 368 | 'data', |
| 369 | ['Buffer', 'TypedArray', 'DataView'], |
| 370 | data |
| 371 | ); |
| 372 | } |
| 373 | |
| 374 | if (!isArrayBufferView(signature)) { |
| 375 | throw new ERR_INVALID_ARG_TYPE( |
| 376 | 'signature', |
| 377 | ['Buffer', 'TypedArray', 'DataView'], |
| 378 | signature |
| 379 | ); |
| 380 | } |
| 381 | |
| 382 | if (!options) { |
| 383 | throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); |
| 384 | } |
| 385 | |
| 386 | let key: CryptoKey; |
| 387 | if (options instanceof CryptoKey) { |
| 388 | key = options; |
| 389 | } else if (isKeyObject(options)) { |
| 390 | key = getKeyObjectHandle(options as KeyObject); |
| 391 | } else { |
| 392 | key = getKeyObjectHandle( |
| 393 | createPublicKey(options as CreateAsymmetricKeyOptions) |
| 394 | ); |
| 395 | } |
| 396 | if (!(key instanceof CryptoKey)) { |
| 397 | throw new ERR_INVALID_ARG_TYPE( |
| 398 | 'options', |
| 399 | ['KeyObject', 'CryptoKey', 'object'], |
| 400 | key |
| 401 | ); |
| 402 | } |
| 403 | |
| 404 | // Options specific to RSA |
| 405 | const rsaPadding = getIntOption('padding', options); |
| 406 | const pssSaltLength = getIntOption('saltLength', options); |
| 407 | |
| 408 | // Options specific to (EC)DSA |
| 409 | const dsaSigEnc = getDSASignatureEncoding(options); |
| 410 | |
| 411 | if (callback === undefined) { |
| 412 | return cryptoImpl.verifyOneShot( |
| 413 | key, |
| 414 | algorithm, |
| 415 | data, |
| 416 | signature, |
| 417 | rsaPadding, |
| 418 | pssSaltLength, |
| 419 | dsaSigEnc |
| 420 | ); |
| 421 | } |
| 422 | |
| 423 | try { |
| 424 | callback( |
| 425 | null, |
| 426 | cryptoImpl.verifyOneShot( |
| 427 | key, |
| 428 | algorithm, |
| 429 | data, |
| 430 | signature, |
| 431 | rsaPadding, |
| 432 | pssSaltLength, |
| 433 | dsaSigEnc |
| 434 | ) |
| 435 | ); |
| 436 | } catch (err) { |
| 437 | queueMicrotask(() => callback(err)); |
| 438 | } |
| 439 | return; // explicit return is necessary to squelch typescript warning. |
| 440 | } |