import * as asn1js from "asn1js"; import * as pvtsutils from "pvtsutils"; import * as pvutils from "pvutils"; import * as common from "./common"; import { AlgorithmIdentifier, AlgorithmIdentifierJson } from "./AlgorithmIdentifier"; import { EncapsulatedContentInfo, EncapsulatedContentInfoJson, EncapsulatedContentInfoSchema } from "./EncapsulatedContentInfo"; import { Certificate, checkCA } from "./Certificate"; import { CertificateRevocationList, CertificateRevocationListJson } from "./CertificateRevocationList"; import { OtherRevocationInfoFormat, OtherRevocationInfoFormatJson } from "./OtherRevocationInfoFormat"; import { SignerInfo, SignerInfoJson } from "./SignerInfo"; import { CertificateSet, CertificateSetItem, CertificateSetItemJson } from "./CertificateSet"; import { RevocationInfoChoices, RevocationInfoChoicesSchema } from "./RevocationInfoChoices"; import { IssuerAndSerialNumber } from "./IssuerAndSerialNumber"; import { TSTInfo } from "./TSTInfo"; import { CertificateChainValidationEngine, CertificateChainValidationEngineParameters, FindIssuerCallback, FindOriginCallback } from "./CertificateChainValidationEngine"; import { BasicOCSPResponse, BasicOCSPResponseJson } from "./BasicOCSPResponse"; import { OtherCertificateFormat } from "./OtherCertificateFormat"; import { AttributeCertificateV1 } from "./AttributeCertificateV1"; import { AttributeCertificateV2 } from "./AttributeCertificateV2"; import * as Schema from "./Schema"; import { id_ContentType_Data, id_eContentType_TSTInfo, id_PKIX_OCSP_Basic } from "./ObjectIdentifiers"; import { AsnError } from "./errors"; import { PkiObject, PkiObjectParameters } from "./PkiObject"; import { EMPTY_BUFFER, EMPTY_STRING } from "./constants"; import { ICryptoEngine } from "./CryptoEngine/CryptoEngineInterface"; export type SignedDataCRL = CertificateRevocationList | OtherRevocationInfoFormat; export type SignedDataCRLJson = CertificateRevocationListJson | OtherRevocationInfoFormatJson; const VERSION = "version"; const DIGEST_ALGORITHMS = "digestAlgorithms"; const ENCAP_CONTENT_INFO = "encapContentInfo"; const CERTIFICATES = "certificates"; const CRLS = "crls"; const SIGNER_INFOS = "signerInfos"; const OCSPS = "ocsps"; const SIGNED_DATA = "SignedData"; const SIGNED_DATA_VERSION = `${SIGNED_DATA}.${VERSION}`; const SIGNED_DATA_DIGEST_ALGORITHMS = `${SIGNED_DATA}.${DIGEST_ALGORITHMS}`; const SIGNED_DATA_ENCAP_CONTENT_INFO = `${SIGNED_DATA}.${ENCAP_CONTENT_INFO}`; const SIGNED_DATA_CERTIFICATES = `${SIGNED_DATA}.${CERTIFICATES}`; const SIGNED_DATA_CRLS = `${SIGNED_DATA}.${CRLS}`; const SIGNED_DATA_SIGNER_INFOS = `${SIGNED_DATA}.${SIGNER_INFOS}`; const CLEAR_PROPS = [ SIGNED_DATA_VERSION, SIGNED_DATA_DIGEST_ALGORITHMS, SIGNED_DATA_ENCAP_CONTENT_INFO, SIGNED_DATA_CERTIFICATES, SIGNED_DATA_CRLS, SIGNED_DATA_SIGNER_INFOS ]; export interface ISignedData { version: number; digestAlgorithms: AlgorithmIdentifier[]; encapContentInfo: EncapsulatedContentInfo; certificates?: CertificateSetItem[]; crls?: SignedDataCRL[]; ocsps?: BasicOCSPResponse[]; signerInfos: SignerInfo[]; } export interface SignedDataJson { version: number; digestAlgorithms: AlgorithmIdentifierJson[]; encapContentInfo: EncapsulatedContentInfoJson; certificates?: CertificateSetItemJson[]; crls?: SignedDataCRLJson[]; ocsps?: BasicOCSPResponseJson[]; signerInfos: SignerInfoJson[]; } export type SignedDataParameters = PkiObjectParameters & Partial; export interface SignedDataVerifyParams { signer?: number; data?: ArrayBuffer; trustedCerts?: Certificate[]; checkDate?: Date; checkChain?: boolean; passedWhenNotRevValues?: boolean; extendedMode?: boolean; findOrigin?: FindOriginCallback | null; findIssuer?: FindIssuerCallback | null; } export interface SignedDataVerifyErrorParams { message: string; date?: Date; code?: number; timestampSerial?: ArrayBuffer | null; signatureVerified?: boolean | null; signerCertificate?: Certificate | null; signerCertificateVerified?: boolean | null; certificatePath?: Certificate[]; } export interface SignedDataVerifyResult { message: string; date?: Date; code?: number; timestampSerial?: ArrayBuffer | null; signatureVerified?: boolean | null; signerCertificate?: Certificate | null; signerCertificateVerified?: boolean | null; certificatePath: Certificate[]; } export class SignedDataVerifyError extends Error implements SignedDataVerifyResult { public date: Date; public code: number; public signatureVerified: boolean | null; public signerCertificate: Certificate | null; public signerCertificateVerified: boolean | null; public timestampSerial: ArrayBuffer | null; public certificatePath: Certificate[]; constructor({ message, code = 0, date = new Date(), signatureVerified = null, signerCertificate = null, signerCertificateVerified = null, timestampSerial = null, certificatePath = [], }: SignedDataVerifyErrorParams) { super(message); this.name = "SignedDataVerifyError"; this.date = date; this.code = code; this.timestampSerial = timestampSerial; this.signatureVerified = signatureVerified; this.signerCertificate = signerCertificate; this.signerCertificateVerified = signerCertificateVerified; this.certificatePath = certificatePath; } } /** * Represents the SignedData structure described in [RFC5652](https://datatracker.ietf.org/doc/html/rfc5652) * * @example The following example demonstrates how to create and sign CMS Signed Data * ```js * // Create a new CMS Signed Data * const cmsSigned = new pkijs.SignedData({ * encapContentInfo: new pkijs.EncapsulatedContentInfo({ * eContentType: pkijs.ContentInfo.DATA,, // "data" content type * eContent: new asn1js.OctetString({ valueHex: buffer }) * }), * signerInfos: [ * new pkijs.SignerInfo({ * sid: new pkijs.IssuerAndSerialNumber({ * issuer: cert.issuer, * serialNumber: cert.serialNumber * }) * }) * ], * // Signer certificate for chain validation * certificates: [cert] * }); * * await cmsSigned.sign(keys.privateKey, 0, "SHA-256"); * * // Add Signed Data to Content Info * const cms = new pkijs.ContentInfo({ * contentType: pkijs.ContentInfo.SIGNED_DATA,, * content: cmsSigned.toSchema(true), * }); * * // Encode CMS to ASN.1 * const cmsRaw = cms.toSchema().toBER(); * ``` * * @example The following example demonstrates how to verify CMS Signed Data * ```js * // Parse CMS and detect it's Signed Data * const cms = pkijs.ContentInfo.fromBER(cmsRaw); * if (cms.contentType !== pkijs.ContentInfo.SIGNED_DATA) { * throw new Error("CMS is not Signed Data"); * } * * // Read Signed Data * const signedData = new pkijs.SignedData({ schema: cms.content }); * * // Verify Signed Data signature * const ok = await signedData.verify({ * signer: 0, * checkChain: true, * trustedCerts: [trustedCert], * }); * * if (!ok) { * throw new Error("CMS signature is invalid") * } * ``` */ export class SignedData extends PkiObject implements ISignedData { public static override CLASS_NAME = "SignedData"; public static ID_DATA: typeof id_ContentType_Data = id_ContentType_Data; public version!: number; public digestAlgorithms!: AlgorithmIdentifier[]; public encapContentInfo!: EncapsulatedContentInfo; public certificates?: CertificateSetItem[]; public crls?: SignedDataCRL[]; public ocsps?: BasicOCSPResponse[]; public signerInfos!: SignerInfo[]; /** * Initializes a new instance of the {@link SignedData} class * @param parameters Initialization parameters */ constructor(parameters: SignedDataParameters = {}) { super(); this.version = pvutils.getParametersValue(parameters, VERSION, SignedData.defaultValues(VERSION)); this.digestAlgorithms = pvutils.getParametersValue(parameters, DIGEST_ALGORITHMS, SignedData.defaultValues(DIGEST_ALGORITHMS)); this.encapContentInfo = pvutils.getParametersValue(parameters, ENCAP_CONTENT_INFO, SignedData.defaultValues(ENCAP_CONTENT_INFO)); if (CERTIFICATES in parameters) { this.certificates = pvutils.getParametersValue(parameters, CERTIFICATES, SignedData.defaultValues(CERTIFICATES)); } if (CRLS in parameters) { this.crls = pvutils.getParametersValue(parameters, CRLS, SignedData.defaultValues(CRLS)); } if (OCSPS in parameters) { this.ocsps = pvutils.getParametersValue(parameters, OCSPS, SignedData.defaultValues(OCSPS)); } this.signerInfos = pvutils.getParametersValue(parameters, SIGNER_INFOS, SignedData.defaultValues(SIGNER_INFOS)); if (parameters.schema) { this.fromSchema(parameters.schema); } } /** * Returns default values for all class members * @param memberName String name for a class member * @returns Default value */ public static override defaultValues(memberName: typeof VERSION): number; public static override defaultValues(memberName: typeof DIGEST_ALGORITHMS): AlgorithmIdentifier[]; public static override defaultValues(memberName: typeof ENCAP_CONTENT_INFO): EncapsulatedContentInfo; public static override defaultValues(memberName: typeof CERTIFICATES): CertificateSetItem[]; public static override defaultValues(memberName: typeof CRLS): SignedDataCRL[]; public static override defaultValues(memberName: typeof OCSPS): BasicOCSPResponse[]; public static override defaultValues(memberName: typeof SIGNER_INFOS): SignerInfo[]; public static override defaultValues(memberName: string): any { switch (memberName) { case VERSION: return 0; case DIGEST_ALGORITHMS: return []; case ENCAP_CONTENT_INFO: return new EncapsulatedContentInfo(); case CERTIFICATES: return []; case CRLS: return []; case OCSPS: return []; case SIGNER_INFOS: return []; default: return super.defaultValues(memberName); } } /** * Compare values with default values for all class members * @param memberName String name for a class member * @param memberValue Value to compare with default value */ public static compareWithDefault(memberName: string, memberValue: any): boolean { switch (memberName) { case VERSION: return (memberValue === SignedData.defaultValues(VERSION)); case ENCAP_CONTENT_INFO: return EncapsulatedContentInfo.compareWithDefault("eContentType", memberValue.eContentType) && EncapsulatedContentInfo.compareWithDefault("eContent", memberValue.eContent); case DIGEST_ALGORITHMS: case CERTIFICATES: case CRLS: case OCSPS: case SIGNER_INFOS: return (memberValue.length === 0); default: return super.defaultValues(memberName); } } /** * @inheritdoc * @asn ASN.1 schema * ```asn * SignedData ::= SEQUENCE { * version CMSVersion, * digestAlgorithms DigestAlgorithmIdentifiers, * encapContentInfo EncapsulatedContentInfo, * certificates [0] IMPLICIT CertificateSet OPTIONAL, * crls [1] IMPLICIT RevocationInfoChoices OPTIONAL, * signerInfos SignerInfos } *``` */ public static override schema(parameters: Schema.SchemaParameters<{ version?: string; digestAlgorithms?: string; encapContentInfo?: EncapsulatedContentInfoSchema; certificates?: string; crls?: RevocationInfoChoicesSchema; signerInfos?: string; }> = {}): Schema.SchemaType { const names = pvutils.getParametersValue>(parameters, "names", {}); if (names.optional === undefined) { names.optional = false; } return (new asn1js.Sequence({ name: (names.blockName || SIGNED_DATA), optional: names.optional, value: [ new asn1js.Integer({ name: (names.version || SIGNED_DATA_VERSION) }), new asn1js.Set({ value: [ new asn1js.Repeated({ name: (names.digestAlgorithms || SIGNED_DATA_DIGEST_ALGORITHMS), value: AlgorithmIdentifier.schema() }) ] }), EncapsulatedContentInfo.schema(names.encapContentInfo || { names: { blockName: SIGNED_DATA_ENCAP_CONTENT_INFO } }), new asn1js.Constructed({ name: (names.certificates || SIGNED_DATA_CERTIFICATES), optional: true, idBlock: { tagClass: 3, // CONTEXT-SPECIFIC tagNumber: 0 // [0] }, value: CertificateSet.schema().valueBlock.value }), // IMPLICIT CertificateSet new asn1js.Constructed({ optional: true, idBlock: { tagClass: 3, // CONTEXT-SPECIFIC tagNumber: 1 // [1] }, value: RevocationInfoChoices.schema(names.crls || { names: { crls: SIGNED_DATA_CRLS } }).valueBlock.value }), // IMPLICIT RevocationInfoChoices new asn1js.Set({ value: [ new asn1js.Repeated({ name: (names.signerInfos || SIGNED_DATA_SIGNER_INFOS), value: SignerInfo.schema() }) ] }) ] })); } public fromSchema(schema: Schema.SchemaType): void { // Clear input data first pvutils.clearProps(schema, CLEAR_PROPS); //#region Check the schema is valid const asn1 = asn1js.compareSchema(schema, schema, SignedData.schema() ); AsnError.assertSchema(asn1, this.className); //#endregion //#region Get internal properties from parsed schema this.version = asn1.result[SIGNED_DATA_VERSION].valueBlock.valueDec; if (SIGNED_DATA_DIGEST_ALGORITHMS in asn1.result) // Could be empty SET of digest algorithms this.digestAlgorithms = Array.from(asn1.result[SIGNED_DATA_DIGEST_ALGORITHMS], algorithm => new AlgorithmIdentifier({ schema: algorithm })); this.encapContentInfo = new EncapsulatedContentInfo({ schema: asn1.result[SIGNED_DATA_ENCAP_CONTENT_INFO] }); if (SIGNED_DATA_CERTIFICATES in asn1.result) { const certificateSet = new CertificateSet({ schema: new asn1js.Set({ value: asn1.result[SIGNED_DATA_CERTIFICATES].valueBlock.value }) }); this.certificates = certificateSet.certificates.slice(0); // Copy all just for making comfortable access } if (SIGNED_DATA_CRLS in asn1.result) { this.crls = Array.from(asn1.result[SIGNED_DATA_CRLS], (crl: Schema.SchemaType) => { if (crl.idBlock.tagClass === 1) return new CertificateRevocationList({ schema: crl }); //#region Create SEQUENCE from [1] crl.idBlock.tagClass = 1; // UNIVERSAL crl.idBlock.tagNumber = 16; // SEQUENCE //#endregion return new OtherRevocationInfoFormat({ schema: crl }); }); } if (SIGNED_DATA_SIGNER_INFOS in asn1.result) // Could be empty SET SignerInfos this.signerInfos = Array.from(asn1.result[SIGNED_DATA_SIGNER_INFOS], signerInfoSchema => new SignerInfo({ schema: signerInfoSchema })); //#endregion } public toSchema(encodeFlag = false): Schema.SchemaType { //#region Create array for output sequence const outputArray = []; // IF ((certificates is present) AND // (any certificates with a type of other are present)) OR // ((crls is present) AND // (any crls with a type of other are present)) // THEN version MUST be 5 // ELSE // IF (certificates is present) AND // (any version 2 attribute certificates are present) // THEN version MUST be 4 // ELSE // IF ((certificates is present) AND // (any version 1 attribute certificates are present)) OR // (any SignerInfo structures are version 3) OR // (encapContentInfo eContentType is other than id-data) // THEN version MUST be 3 // ELSE version MUST be 1 if ((this.certificates && this.certificates.length && this.certificates.some(o => o instanceof OtherCertificateFormat)) || (this.crls && this.crls.length && this.crls.some(o => o instanceof OtherRevocationInfoFormat))) { this.version = 5; } else if (this.certificates && this.certificates.length && this.certificates.some(o => o instanceof AttributeCertificateV2)) { this.version = 4; } else if ((this.certificates && this.certificates.length && this.certificates.some(o => o instanceof AttributeCertificateV1)) || this.signerInfos.some(o => o.version === 3) || this.encapContentInfo.eContentType !== SignedData.ID_DATA) { this.version = 3; } else { this.version = 1; } outputArray.push(new asn1js.Integer({ value: this.version })); //#region Create array of digest algorithms outputArray.push(new asn1js.Set({ value: Array.from(this.digestAlgorithms, algorithm => algorithm.toSchema()) })); //#endregion outputArray.push(this.encapContentInfo.toSchema()); if (this.certificates) { const certificateSet = new CertificateSet({ certificates: this.certificates }); const certificateSetSchema = certificateSet.toSchema(); outputArray.push(new asn1js.Constructed({ idBlock: { tagClass: 3, tagNumber: 0 }, value: certificateSetSchema.valueBlock.value })); } if (this.crls) { outputArray.push(new asn1js.Constructed({ idBlock: { tagClass: 3, // CONTEXT-SPECIFIC tagNumber: 1 // [1] }, value: Array.from(this.crls, crl => { if (crl instanceof OtherRevocationInfoFormat) { const crlSchema = crl.toSchema(); crlSchema.idBlock.tagClass = 3; crlSchema.idBlock.tagNumber = 1; return crlSchema; } return crl.toSchema(encodeFlag); }) })); } //#region Create array of signer infos outputArray.push(new asn1js.Set({ value: Array.from(this.signerInfos, signerInfo => signerInfo.toSchema()) })); //#endregion //#endregion //#region Construct and return new ASN.1 schema for this object return (new asn1js.Sequence({ value: outputArray })); //#endregion } public toJSON(): SignedDataJson { const res: SignedDataJson = { version: this.version, digestAlgorithms: Array.from(this.digestAlgorithms, algorithm => algorithm.toJSON()), encapContentInfo: this.encapContentInfo.toJSON(), signerInfos: Array.from(this.signerInfos, signerInfo => signerInfo.toJSON()), }; if (this.certificates) { res.certificates = Array.from(this.certificates, certificate => certificate.toJSON()); } if (this.crls) { res.crls = Array.from(this.crls, crl => crl.toJSON()); } return res; } public verify(params?: SignedDataVerifyParams & { extendedMode?: false; }, crypto?: ICryptoEngine): Promise; public verify(params: SignedDataVerifyParams & { extendedMode: true; }, crypto?: ICryptoEngine): Promise; public async verify({ signer = (-1), data = (EMPTY_BUFFER), trustedCerts = [], checkDate = (new Date()), checkChain = false, passedWhenNotRevValues = false, extendedMode = false, findOrigin = null, findIssuer = null }: SignedDataVerifyParams = {}, crypto = common.getCrypto(true)): Promise { let signerCert: Certificate | null = null; let timestampSerial: ArrayBuffer | null = null; try { //#region Global variables let messageDigestValue = EMPTY_BUFFER; let shaAlgorithm = EMPTY_STRING; let certificatePath: Certificate[] = []; //#endregion //#region Get a signer number const signerInfo = this.signerInfos[signer]; if (!signerInfo) { throw new SignedDataVerifyError({ date: checkDate, code: 1, message: "Unable to get signer by supplied index", }); } //#endregion //#region Check that certificates field was included in signed data if (!this.certificates) { throw new SignedDataVerifyError({ date: checkDate, code: 2, message: "No certificates attached to this signed data", }); } //#endregion //#region Find a certificate for specified signer if (signerInfo.sid instanceof IssuerAndSerialNumber) { for (const certificate of this.certificates) { if (!(certificate instanceof Certificate)) continue; if ((certificate.issuer.isEqual(signerInfo.sid.issuer)) && (certificate.serialNumber.isEqual(signerInfo.sid.serialNumber))) { signerCert = certificate; break; } } } else { // Find by SubjectKeyIdentifier const sid = signerInfo.sid; const keyId = sid.idBlock.isConstructed ? sid.valueBlock.value[0].valueBlock.valueHex // EXPLICIT OCTET STRING : sid.valueBlock.valueHex; // IMPLICIT OCTET STRING for (const certificate of this.certificates) { if (!(certificate instanceof Certificate)) { continue; } const digest = await crypto.digest({ name: "sha-1" }, certificate.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHexView); if (pvutils.isEqualBuffer(digest, keyId)) { signerCert = certificate; break; } } } if (!signerCert) { throw new SignedDataVerifyError({ date: checkDate, code: 3, message: "Unable to find signer certificate", }); } //#endregion //#region Verify internal digest in case of "tSTInfo" content type if (this.encapContentInfo.eContentType === id_eContentType_TSTInfo) { //#region Check "eContent" presence if (!this.encapContentInfo.eContent) { throw new SignedDataVerifyError({ date: checkDate, code: 15, message: "Error during verification: TSTInfo eContent is empty", signatureVerified: null, signerCertificate: signerCert, timestampSerial, signerCertificateVerified: true }); } //#endregion //#region Initialize TST_INFO value let tstInfo: TSTInfo; try { tstInfo = TSTInfo.fromBER(this.encapContentInfo.eContent.valueBlock.valueHexView); } catch (ex) { throw new SignedDataVerifyError({ date: checkDate, code: 15, message: "Error during verification: TSTInfo wrong ASN.1 schema ", signatureVerified: null, signerCertificate: signerCert, timestampSerial, signerCertificateVerified: true }); } //#endregion //#region Change "checkDate" and append "timestampSerial" checkDate = tstInfo.genTime; timestampSerial = tstInfo.serialNumber.valueBlock.valueHexView.slice(); //#endregion //#region Check that we do have detached data content if (data.byteLength === 0) { throw new SignedDataVerifyError({ date: checkDate, code: 4, message: "Missed detached data input array", }); } //#endregion if (!(await tstInfo.verify({ data }, crypto))) { throw new SignedDataVerifyError({ date: checkDate, code: 15, message: "Error during verification: TSTInfo verification is failed", signatureVerified: false, signerCertificate: signerCert, timestampSerial, signerCertificateVerified: true }); } } //#endregion if (checkChain) { const certs = this.certificates.filter(certificate => (certificate instanceof Certificate && !!checkCA(certificate, signerCert))) as Certificate[]; const chainParams: CertificateChainValidationEngineParameters = { checkDate, certs, trustedCerts, }; if (findIssuer) { chainParams.findIssuer = findIssuer; } if (findOrigin) { chainParams.findOrigin = findOrigin; } const chainEngine = new CertificateChainValidationEngine(chainParams); chainEngine.certs.push(signerCert); if (this.crls) { for (const crl of this.crls) { if ("thisUpdate" in crl) chainEngine.crls.push(crl); else // Assumed "revocation value" has "OtherRevocationInfoFormat" { if (crl.otherRevInfoFormat === id_PKIX_OCSP_Basic) // Basic OCSP response chainEngine.ocsps.push(new BasicOCSPResponse({ schema: crl.otherRevInfo })); } } } if (this.ocsps) { chainEngine.ocsps.push(...(this.ocsps)); } const verificationResult = await chainEngine.verify({ passedWhenNotRevValues }, crypto) .catch(e => { throw new SignedDataVerifyError({ date: checkDate, code: 5, message: `Validation of signer's certificate failed with error: ${((e instanceof Object) ? e.resultMessage : e)}`, signerCertificate: signerCert, signerCertificateVerified: false }); }); if (verificationResult.certificatePath) { certificatePath = verificationResult.certificatePath; } if (!verificationResult.result) throw new SignedDataVerifyError({ date: checkDate, code: 5, message: `Validation of signer's certificate failed: ${verificationResult.resultMessage}`, signerCertificate: signerCert, signerCertificateVerified: false }); } //#endregion //#region Find signer's hashing algorithm const signerInfoHashAlgorithm = crypto.getAlgorithmByOID(signerInfo.digestAlgorithm.algorithmId); if (!("name" in signerInfoHashAlgorithm)) { throw new SignedDataVerifyError({ date: checkDate, code: 7, message: `Unsupported signature algorithm: ${signerInfo.digestAlgorithm.algorithmId}`, signerCertificate: signerCert, signerCertificateVerified: true }); } shaAlgorithm = signerInfoHashAlgorithm.name; //#endregion //#region Create correct data block for verification const eContent = this.encapContentInfo.eContent; if (eContent) // Attached data { if ((eContent.idBlock.tagClass === 1) && (eContent.idBlock.tagNumber === 4)) { data = eContent.getValue(); } else data = eContent.valueBlock.valueBeforeDecodeView; } else // Detached data { if (data.byteLength === 0) // Check that "data" already provided by function parameter { throw new SignedDataVerifyError({ date: checkDate, code: 8, message: "Missed detached data input array", signerCertificate: signerCert, signerCertificateVerified: true }); } } if (signerInfo.signedAttrs) { //#region Check mandatory attributes let foundContentType = false; let foundMessageDigest = false; for (const attribute of signerInfo.signedAttrs.attributes) { //#region Check that "content-type" attribute exists if (attribute.type === "1.2.840.113549.1.9.3") foundContentType = true; //#endregion //#region Check that "message-digest" attribute exists if (attribute.type === "1.2.840.113549.1.9.4") { foundMessageDigest = true; messageDigestValue = attribute.values[0].valueBlock.valueHex; } //#endregion //#region Speed-up searching if (foundContentType && foundMessageDigest) break; //#endregion } if (foundContentType === false) { throw new SignedDataVerifyError({ date: checkDate, code: 9, message: "Attribute \"content-type\" is a mandatory attribute for \"signed attributes\"", signerCertificate: signerCert, signerCertificateVerified: true }); } if (foundMessageDigest === false) { throw new SignedDataVerifyError({ date: checkDate, code: 10, message: "Attribute \"message-digest\" is a mandatory attribute for \"signed attributes\"", signatureVerified: null, signerCertificate: signerCert, signerCertificateVerified: true }); } //#endregion } //#endregion //#region Verify "message-digest" attribute in case of "signedAttrs" if (signerInfo.signedAttrs) { const messageDigest = await crypto.digest(shaAlgorithm, new Uint8Array(data)); if (!pvutils.isEqualBuffer(messageDigest, messageDigestValue)) { throw new SignedDataVerifyError({ date: checkDate, code: 15, message: "Error during verification: Message digest doesn't match", signatureVerified: null, signerCertificate: signerCert, timestampSerial, signerCertificateVerified: true }); } data = signerInfo.signedAttrs.encodedValue; } //#endregion const verifyResult = await crypto.verifyWithPublicKey(data, signerInfo.signature, signerCert.subjectPublicKeyInfo, signerCert.signatureAlgorithm, shaAlgorithm); //#region Make a final result if (extendedMode) { return { date: checkDate, code: 14, message: EMPTY_STRING, signatureVerified: verifyResult, signerCertificate: signerCert, timestampSerial, signerCertificateVerified: true, certificatePath }; } else { return verifyResult; } } catch (e) { if (e instanceof SignedDataVerifyError) { throw e; } throw new SignedDataVerifyError({ date: checkDate, code: 15, message: `Error during verification: ${e instanceof Error ? e.message : e}`, signatureVerified: null, signerCertificate: signerCert, timestampSerial, signerCertificateVerified: true }); } } /** * Signing current SignedData * @param privateKey Private key for "subjectPublicKeyInfo" structure * @param signerIndex Index number (starting from 0) of signer index to make signature for * @param hashAlgorithm Hashing algorithm. Default SHA-1 * @param data Detached data * @param crypto Crypto engine */ public async sign(privateKey: CryptoKey, signerIndex: number, hashAlgorithm = "SHA-1", data: BufferSource = (EMPTY_BUFFER), crypto = common.getCrypto(true)): Promise { //#region Initial checking if (!privateKey) throw new Error("Need to provide a private key for signing"); //#endregion //#region Simple check for supported algorithm const hashAlgorithmOID = crypto.getOIDByAlgorithm({ name: hashAlgorithm }, true, "hashAlgorithm"); //#endregion //#region Append information about hash algorithm if ((this.digestAlgorithms.filter(algorithm => algorithm.algorithmId === hashAlgorithmOID)).length === 0) { this.digestAlgorithms.push(new AlgorithmIdentifier({ algorithmId: hashAlgorithmOID, algorithmParams: new asn1js.Null() })); } const signerInfo = this.signerInfos[signerIndex]; if (!signerInfo) { throw new RangeError("SignerInfo index is out of range"); } signerInfo.digestAlgorithm = new AlgorithmIdentifier({ algorithmId: hashAlgorithmOID, algorithmParams: new asn1js.Null() }); //#endregion //#region Get a "default parameters" for current algorithm and set correct signature algorithm const signatureParams = await crypto.getSignatureParameters(privateKey, hashAlgorithm); const parameters = signatureParams.parameters; signerInfo.signatureAlgorithm = signatureParams.signatureAlgorithm; //#endregion //#region Create TBS data for signing if (signerInfo.signedAttrs) { if (signerInfo.signedAttrs.encodedValue.byteLength !== 0) data = signerInfo.signedAttrs.encodedValue; else { data = signerInfo.signedAttrs.toSchema().toBER(); //#region Change type from "[0]" to "SET" accordingly to standard const view = pvtsutils.BufferSourceConverter.toUint8Array(data); view[0] = 0x31; //#endregion } } else { const eContent = this.encapContentInfo.eContent; if (eContent) // Attached data { if ((eContent.idBlock.tagClass === 1) && (eContent.idBlock.tagNumber === 4)) { data = eContent.getValue(); } else data = eContent.valueBlock.valueBeforeDecodeView; } else // Detached data { if (data.byteLength === 0) // Check that "data" already provided by function parameter throw new Error("Missed detached data input array"); } } //#endregion //#region Signing TBS data on provided private key const signature = await crypto.signWithPrivateKey(data, privateKey, parameters as any); signerInfo.signature = new asn1js.OctetString({ valueHex: signature }); //#endregion } }