Skip to content
File

Blob: src/node/internal/crypto_sign.ts

typescript441 lines
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 
29import {
30 default as cryptoImpl,
31 type SignHandle,
32 type VerifyHandle,
33 type CreateAsymmetricKeyOptions,
34} from 'node-internal:crypto';
35 
36import { validateString } from 'node-internal:validators';
37 
38import { Buffer } from 'node-internal:internal_buffer';
39 
40import {
41 KeyObject,
42 isKeyObject,
43 getKeyObjectHandle,
44 createPrivateKey,
45 createPublicKey,
46} from 'node-internal:crypto_keys';
47 
48import {
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 
55import { Writable } from 'node-internal:streams_writable';
56import { isArrayBufferView } from 'node-internal:internal_types';
57 
58export interface SignOptions {}
59 
60export interface Sign extends Writable {
61 [kHandle]: SignHandle;
62 update(data: any): void;
63 sign(privateKey: any): any;
64}
65export interface Verify extends Writable {
66 [kHandle]: VerifyHandle;
67 update(data: any): void;
68 verify(publicKey: any, signature: any): any;
69}
70 
71const kHandle = Symbol('kHandle');
72 
73// Uses old-style class syntax for Node.js compatibility
74export 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};
83Object.setPrototypeOf(Sign.prototype, Writable.prototype);
84Object.setPrototypeOf(Sign, Writable);
85 
86Sign.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 
95Sign.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 
114function 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 
125function 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 
137function 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 
160Sign.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
190export 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};
199Object.setPrototypeOf(Verify.prototype, Writable.prototype);
200Object.setPrototypeOf(Verify, Writable);
201 
202Verify.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 
211Verify.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 
230Verify.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 
278export function createSign(algorithm: string, options: any) {
279 return new Sign(algorithm, options);
280}
281 
282export function createVerify(algorithm: string, options: any) {
283 return new Verify(algorithm, options);
284}
285 
286type SignCallback = (err: any, signature?: Buffer) => void;
287type VerifyCallback = (err: any, valid?: boolean) => void;
288 
289export 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 
351export 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}