diff --git a/src/api/addAttachedSignature.test.ts b/src/api/addAttachedSignature.test.ts new file mode 100644 index 0000000..620a696 --- /dev/null +++ b/src/api/addAttachedSignature.test.ts @@ -0,0 +1,88 @@ +import 'cadesplugin'; +import { rawCertificates, parsedCertificates } from '../__mocks__/certificates'; +import { createAttachedSignature } from './createAttachedSignature'; +import { _getCadesCert } from '../helpers/_getCadesCert'; +import { addAttachedSignature } from './addAttachedSignature'; + +const [rawCertificateMock] = rawCertificates; +const [parsedCertificateMock] = parsedCertificates; + +jest.mock('../helpers/_getCadesCert', () => ({ _getCadesCert: jest.fn(() => rawCertificateMock) })); + +beforeEach(() => { + (_getCadesCert as jest.Mock).mockClear(); +}); + +const executionSteps = [ + Symbol('step 0'), + Symbol('step 1'), + Symbol('step 2'), + Symbol('step 3'), + Symbol('step 4'), + Symbol('step 5'), +]; + +const executionFlow = { + [executionSteps[0]]: { + propset_Name: jest.fn(), + propset_Value: jest.fn(), + }, + [executionSteps[1]]: { + propset_ContentEncoding: jest.fn(), + propset_Content: jest.fn(), + SignCades: jest.fn(() => executionSteps[4]), + CoSignCades: jest.fn(() => executionSteps[5]), + }, + [executionSteps[2]]: { + propset_Certificate: jest.fn(), + AuthenticatedAttributes2: executionSteps[3], + propset_Options: jest.fn(), + }, + [executionSteps[3]]: { + Add: jest.fn(), + }, + [executionSteps[4]]: 'signature', + [executionSteps[5]]: 'newSignature', +}; + +window.cadesplugin.__defineExecutionFlow(executionFlow); +window.cadesplugin.CreateObjectAsync.mockImplementation((object) => { + switch (object) { + case 'CADESCOM.CPAttribute': + return executionSteps[0]; + case 'CAdESCOM.CadesSignedData': + return executionSteps[1]; + case 'CAdESCOM.CPSigner': + return executionSteps[2]; + } +}); + +describe('addAttachedSignature', () => { + test('uses Buffer to encrypt the message', async () => { + const originalBufferFrom = global.Buffer.from; + + (global.Buffer.from as jest.Mock) = jest.fn(() => ({ + toString: jest.fn(), + })); + + await createAttachedSignature(parsedCertificateMock.thumbprint, 'message'); + await addAttachedSignature(parsedCertificateMock.thumbprint, 'message'); + + expect(global.Buffer.from).toHaveBeenCalledTimes(2); + + global.Buffer.from = originalBufferFrom; + }); + + test('uses specified certificate', async () => { + await addAttachedSignature(parsedCertificateMock.thumbprint, 'message'); + + expect(_getCadesCert).toHaveBeenCalledWith(parsedCertificateMock.thumbprint); + }); + + test('returns new signature', async () => { + await createAttachedSignature(parsedCertificateMock.thumbprint, 'message'); + const signature = await addAttachedSignature(parsedCertificateMock.thumbprint, 'message'); + + expect(signature).toEqual('newSignature'); + }); +}); diff --git a/src/api/addAttachedSignature.ts b/src/api/addAttachedSignature.ts new file mode 100644 index 0000000..f9d2f0a --- /dev/null +++ b/src/api/addAttachedSignature.ts @@ -0,0 +1,86 @@ +import { CADESCOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME } from '../constants'; +import { _afterPluginsLoaded } from '../helpers/_afterPluginsLoaded'; +import { _extractMeaningfulErrorMessage } from '../helpers/_extractMeaningfulErrorMessage'; +import { __cadesAsyncToken__, __createCadesPluginObject__, _generateCadesFn } from '../helpers/_generateCadesFn'; +import { _getCadesCert } from '../helpers/_getCadesCert'; +import { _getDateObj } from '../helpers/_getDateObj'; + +/** + * Создает присоединенную подпись сообщения по отпечатку сертификата + * + * @param thumbprint - отпечаток сертификата + * @param message - подписываемое сообщение + * @returns подпись в формате PKCS#7 + */ +export const addAttachedSignature = _afterPluginsLoaded( + async (thumbprint: string, unencryptedMessage: string | ArrayBuffer): Promise => { + const { cadesplugin } = window; + const cadesCertificate = await _getCadesCert(thumbprint); + + return eval( + _generateCadesFn(function addAttachedSignature(): string { + let cadesAttrs; + let cadesSignedData; + let cadesSigner; + + try { + cadesAttrs = __cadesAsyncToken__ + __createCadesPluginObject__('CADESCOM.CPAttribute'); + cadesSignedData = __cadesAsyncToken__ + __createCadesPluginObject__('CAdESCOM.CadesSignedData'); + cadesSigner = __cadesAsyncToken__ + __createCadesPluginObject__('CAdESCOM.CPSigner'); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при инициализации подписи'); + } + + const currentTime = _getDateObj(new Date()); + + try { + void (__cadesAsyncToken__ + cadesAttrs.propset_Name(CADESCOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME)); + void (__cadesAsyncToken__ + cadesAttrs.propset_Value(currentTime)); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при установке времени подписи'); + } + + let messageBase64; + + try { + messageBase64 = Buffer.from(unencryptedMessage).toString('base64'); + } catch (error) { + console.error(error); + + throw new Error('Ошибка при преобразовании сообщения в Base64'); + } + + let cadesAuthAttrs; + + try { + void (__cadesAsyncToken__ + cadesSigner.propset_Certificate(cadesCertificate)); + cadesAuthAttrs = __cadesAsyncToken__ + cadesSigner.AuthenticatedAttributes2; + void (__cadesAsyncToken__ + cadesAuthAttrs.Add(cadesAttrs)); + void (__cadesAsyncToken__ + cadesSignedData.propset_ContentEncoding(cadesplugin.CADESCOM_BASE64_TO_BINARY)); + void (__cadesAsyncToken__ + cadesSignedData.propset_Content(messageBase64)); + void (__cadesAsyncToken__ + cadesSigner.propset_Options(cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN)); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при указании данных для подписи'); + } + + let signature: string; + + try { + signature = __cadesAsyncToken__ + cadesSignedData.CoSignCades(cadesSigner, cadesplugin.CADESCOM_PKCS7_TYPE); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при подписании данных'); + } + + return signature; + }), + ); + }, +); diff --git a/src/api/addDetachedSignature.test.ts b/src/api/addDetachedSignature.test.ts new file mode 100644 index 0000000..70d289e --- /dev/null +++ b/src/api/addDetachedSignature.test.ts @@ -0,0 +1,81 @@ +import 'cadesplugin'; +import { rawCertificates, parsedCertificates } from '../__mocks__/certificates'; +import { createDetachedSignature } from './createDetachedSignature'; +import { _getCadesCert } from '../helpers/_getCadesCert'; +import { addDetachedSignature } from "./addDetachedSignature"; + +const [rawCertificateMock] = rawCertificates; +const [parsedCertificateMock] = parsedCertificates; + +jest.mock('../helpers/_getCadesCert', () => ({ _getCadesCert: jest.fn(() => rawCertificateMock) })); + +beforeEach(() => { + (_getCadesCert as jest.Mock).mockClear(); +}); + +const executionSteps = [ + Symbol('step 0'), + Symbol('step 1'), + Symbol('step 2'), + Symbol('step 3'), + Symbol('step 4'), + Symbol('step 5'), + Symbol('step 6'), +]; + +const executionFlow = { + [executionSteps[0]]: { + propset_Name: jest.fn(), + propset_Value: jest.fn(), + }, + [executionSteps[1]]: { + propset_ContentEncoding: jest.fn(), + propset_Content: jest.fn(), + SignHash: jest.fn(() => executionSteps[4]), + CoSignHash: jest.fn(() => executionSteps[6]), + }, + [executionSteps[2]]: { + propset_Certificate: jest.fn(), + AuthenticatedAttributes2: executionSteps[3], + propset_Options: jest.fn(), + }, + [executionSteps[3]]: { + Add: jest.fn(), + }, + [executionSteps[4]]: 'signature', + [executionSteps[5]]: { + propset_Algorithm: jest.fn(), + SetHashValue: jest.fn(), + }, + [executionSteps[6]]: 'newSignature', +}; + +window.cadesplugin.__defineExecutionFlow(executionFlow); +window.cadesplugin.CreateObjectAsync.mockImplementation((object) => { + switch (object) { + case 'CADESCOM.CPAttribute': + return executionSteps[0]; + case 'CAdESCOM.CadesSignedData': + return executionSteps[1]; + case 'CAdESCOM.CPSigner': + return executionSteps[2]; + case 'CAdESCOM.HashedData': + return executionSteps[5]; + } +}); + +describe('addDetachedSignature', () => { + test('uses specified certificate', async () => { + const signature = await createDetachedSignature(parsedCertificateMock.thumbprint, 'message'); + await addDetachedSignature(parsedCertificateMock.thumbprint, signature); + + expect(_getCadesCert).toHaveBeenCalledWith(parsedCertificateMock.thumbprint); + }); + + test('returns new signature', async () => { + let signature = await createDetachedSignature(parsedCertificateMock.thumbprint, 'message'); + signature = await addDetachedSignature(parsedCertificateMock.thumbprint, signature); + + expect(signature).toEqual('newSignature'); + }); +}); diff --git a/src/api/addDetachedSignature.ts b/src/api/addDetachedSignature.ts new file mode 100644 index 0000000..adfa75c --- /dev/null +++ b/src/api/addDetachedSignature.ts @@ -0,0 +1,90 @@ +import { CADESCOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME } from '../constants'; +import { _afterPluginsLoaded } from '../helpers/_afterPluginsLoaded'; +import { _extractMeaningfulErrorMessage } from '../helpers/_extractMeaningfulErrorMessage'; +import { __cadesAsyncToken__, __createCadesPluginObject__, _generateCadesFn } from '../helpers/_generateCadesFn'; +import { _getCadesCert } from '../helpers/_getCadesCert'; +import { _getDateObj } from '../helpers/_getDateObj'; + +/** + * Создает отсоединенную подпись хеша по отпечатку сертификата + * + * @param thumbprint - отпечаток сертификата + * @param messageHash - хеш подписываемого сообщения, сгенерированный по ГОСТ Р 34.11-2012 256 бит + * @returns подпись в формате PKCS#7 + */ +export const addDetachedSignature = _afterPluginsLoaded( + async (thumbprint: string, messageHash: string): Promise => { + const { cadesplugin } = window; + const cadesCertificate = await _getCadesCert(thumbprint); + + return eval( + _generateCadesFn(function addDetachedSignature(): string { + let cadesAttrs; + let cadesHashedData; + let cadesSignedData; + let cadesSigner; + + try { + cadesAttrs = __cadesAsyncToken__ + __createCadesPluginObject__('CADESCOM.CPAttribute'); + cadesHashedData = __cadesAsyncToken__ + __createCadesPluginObject__('CAdESCOM.HashedData'); + cadesSignedData = __cadesAsyncToken__ + __createCadesPluginObject__('CAdESCOM.CadesSignedData'); + cadesSigner = __cadesAsyncToken__ + __createCadesPluginObject__('CAdESCOM.CPSigner'); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при инициализации подписи'); + } + + const currentTime = _getDateObj(new Date()); + + try { + void (__cadesAsyncToken__ + cadesAttrs.propset_Name(CADESCOM_AUTHENTICATED_ATTRIBUTE_SIGNING_TIME)); + void (__cadesAsyncToken__ + cadesAttrs.propset_Value(currentTime)); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при установке времени подписи'); + } + + let cadesAuthAttrs; + + try { + void (__cadesAsyncToken__ + cadesSigner.propset_Certificate(cadesCertificate)); + cadesAuthAttrs = __cadesAsyncToken__ + cadesSigner.AuthenticatedAttributes2; + void (__cadesAsyncToken__ + cadesAuthAttrs.Add(cadesAttrs)); + void (__cadesAsyncToken__ + cadesSigner.propset_Options(cadesplugin.CAPICOM_CERTIFICATE_INCLUDE_WHOLE_CHAIN)); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при установке сертификата'); + } + + try { + void ( + __cadesAsyncToken__ + + cadesHashedData.propset_Algorithm(cadesplugin.CADESCOM_HASH_ALGORITHM_CP_GOST_3411_2012_256) + ); + void (__cadesAsyncToken__ + cadesHashedData.SetHashValue(messageHash)); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при установке хеша'); + } + + let signature: string; + + try { + signature = + __cadesAsyncToken__ + + cadesSignedData.CoSignHash(cadesHashedData, cadesSigner, cadesplugin.CADESCOM_PKCS7_TYPE); + } catch (error) { + console.error(error); + + throw new Error(_extractMeaningfulErrorMessage(error) || 'Ошибка при подписании данных'); + } + + return signature; + }), + ); + }, +);