Skip to content

Commit 12a766a

Browse files
authored
perf(signatures): use Web Crypto API for hashing instead of @noble/hashes (#51)
The hashData functions used during PDF signing hash the entire document bytes through pure-JS @noble/hashes, which is ~10x slower than native crypto for large inputs. Switch to crypto.subtle.digest() which delegates to the platform's native implementation (OpenSSL/BoringSSL). Benchmarked on a 23MB PDF: 80ms -> 8ms (9.8x faster).
1 parent cfff103 commit 12a766a

File tree

4 files changed

+31
-32
lines changed

4 files changed

+31
-32
lines changed

src/api/pdf-signature.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export class PDFSignature {
182182

183183
// Extract bytes to sign and hash them
184184
const signedBytes = extractSignedBytes(pdfBytes, byteRange);
185-
const documentHash = hashData(signedBytes, resolved.digestAlgorithm);
185+
const documentHash = await hashData(signedBytes, resolved.digestAlgorithm);
186186

187187
// Build CMS signature
188188
const formatBuilder = this.getFormatBuilder(resolved.subFilter);
@@ -202,7 +202,7 @@ export class PDFSignature {
202202
if (resolved.timestampAuthority) {
203203
// Hash the signature value for timestamping
204204
const signatureValue = signedData.getSignatureValue();
205-
const signatureHash = hashData(signatureValue, resolved.digestAlgorithm);
205+
const signatureHash = await hashData(signatureValue, resolved.digestAlgorithm);
206206

207207
// Request timestamp from TSA
208208
const timestampToken = await resolved.timestampAuthority.timestamp(
@@ -476,7 +476,7 @@ export class PDFSignature {
476476

477477
// Hash and get timestamp
478478
const signedBytes = extractSignedBytes(savedBytes, byteRange);
479-
const documentHash = hashData(signedBytes, digestAlgorithm);
479+
const documentHash = await hashData(signedBytes, digestAlgorithm);
480480
const timestampToken = await timestampAuthority.timestamp(documentHash, digestAlgorithm);
481481

482482
// Patch Contents

src/signatures/formats/cades-detached.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class CAdESDetachedBuilder implements CMSFormatBuilder, CMSSignedData {
6262
const allCerts = [signerCert, ...chainCerts];
6363

6464
// Build signed attributes
65-
const signedAttrs = this.buildSignedAttributes(
65+
const signedAttrs = await this.buildSignedAttributes(
6666
documentHash,
6767
digestAlgorithm,
6868
signer,
@@ -151,13 +151,13 @@ export class CAdESDetachedBuilder implements CMSFormatBuilder, CMSSignedData {
151151
/**
152152
* Build the signed attributes for CAdES signature.
153153
*/
154-
private buildSignedAttributes(
154+
private async buildSignedAttributes(
155155
documentHash: Uint8Array,
156156
digestAlgorithm: DigestAlgorithm,
157157
signer: Signer,
158158
signerCert: pkijs.Certificate,
159159
signingTime?: Date,
160-
): pkijs.Attribute[] {
160+
): Promise<pkijs.Attribute[]> {
161161
const attrs: pkijs.Attribute[] = [];
162162

163163
// Content Type (required)
@@ -190,7 +190,7 @@ export class CAdESDetachedBuilder implements CMSFormatBuilder, CMSSignedData {
190190
);
191191

192192
// ESS signing-certificate-v2 (required for CAdES/PAdES)
193-
attrs.push(this.buildSigningCertificateV2(signerCert, digestAlgorithm));
193+
attrs.push(await this.buildSigningCertificateV2(signerCert, digestAlgorithm));
194194

195195
return attrs;
196196
}
@@ -213,13 +213,13 @@ export class CAdESDetachedBuilder implements CMSFormatBuilder, CMSSignedData {
213213
* issuerSerial IssuerSerial OPTIONAL
214214
* }
215215
*/
216-
private buildSigningCertificateV2(
216+
private async buildSigningCertificateV2(
217217
signerCert: pkijs.Certificate,
218218
digestAlgorithm: DigestAlgorithm,
219-
): pkijs.Attribute {
219+
): Promise<pkijs.Attribute> {
220220
// Hash the certificate
221221
const certDer = signerCert.toSchema().toBER(false);
222-
const certHash = hashData(new Uint8Array(certDer), digestAlgorithm);
222+
const certHash = await hashData(new Uint8Array(certDer), digestAlgorithm);
223223

224224
// Build IssuerSerial using pkijs classes for proper encoding
225225
// IssuerSerial ::= SEQUENCE {

src/signatures/signers/google-kms.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import { toArrayBuffer } from "#src/helpers/buffer.ts";
1010
import { derToPem, isPem, normalizePem, parsePem } from "#src/helpers/pem.ts";
11-
import { sha256, sha384, sha512 } from "@noble/hashes/sha2.js";
1211
import { fromBER } from "asn1js";
1312
import * as pkijs from "pkijs";
1413

@@ -659,7 +658,7 @@ export class GoogleKmsSigner implements Signer {
659658
}
660659

661660
// Hash data locally and build digest object for KMS
662-
const { digest, digestKey } = this.hashData(data, algorithm);
661+
const { digest, digestKey } = await this.hashData(data, algorithm);
663662

664663
try {
665664
const [response] = await this.client.asymmetricSign({
@@ -701,19 +700,21 @@ export class GoogleKmsSigner implements Signer {
701700
/**
702701
* Hash data using the specified algorithm.
703702
*
703+
* Uses the Web Crypto API for native-speed hashing.
704+
*
704705
* @returns The digest bytes and the KMS digest key name
705706
*/
706-
private hashData(
707+
private async hashData(
707708
data: Uint8Array,
708709
algorithm: DigestAlgorithm,
709-
): { digest: Uint8Array; digestKey: "sha256" | "sha384" | "sha512" } {
710-
switch (algorithm) {
711-
case "SHA-256":
712-
return { digest: sha256(data), digestKey: "sha256" };
713-
case "SHA-384":
714-
return { digest: sha384(data), digestKey: "sha384" };
715-
case "SHA-512":
716-
return { digest: sha512(data), digestKey: "sha512" };
717-
}
710+
): Promise<{ digest: Uint8Array; digestKey: "sha256" | "sha384" | "sha512" }> {
711+
const digestKeyMap: Record<DigestAlgorithm, "sha256" | "sha384" | "sha512"> = {
712+
"SHA-256": "sha256",
713+
"SHA-384": "sha384",
714+
"SHA-512": "sha512",
715+
};
716+
717+
const arrayBuffer = await crypto.subtle.digest(algorithm, data as Uint8Array<ArrayBuffer>);
718+
return { digest: new Uint8Array(arrayBuffer), digestKey: digestKeyMap[algorithm] };
718719
}
719720
}

src/signatures/utils.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* Shared utilities for signature operations.
33
*/
44

5-
import { sha256, sha384, sha512 } from "@noble/hashes/sha2.js";
65
import { fromBER } from "asn1js";
76
import * as pkijs from "pkijs";
87

@@ -38,19 +37,18 @@ export function escapePdfString(str: string): string {
3837
/**
3938
* Hash data using the specified algorithm.
4039
*
40+
* Uses the Web Crypto API for native-speed hashing, which is significantly
41+
* faster than pure-JS implementations for large inputs (e.g. hashing an
42+
* entire PDF during signing).
43+
*
4144
* @param data - Data to hash
4245
* @param algorithm - Digest algorithm
4346
* @returns Hash bytes
4447
*/
45-
export function hashData(data: Uint8Array, algorithm: DigestAlgorithm): Uint8Array {
46-
switch (algorithm) {
47-
case "SHA-256":
48-
return sha256(data);
49-
case "SHA-384":
50-
return sha384(data);
51-
case "SHA-512":
52-
return sha512(data);
53-
}
48+
export async function hashData(data: Uint8Array, algorithm: DigestAlgorithm): Promise<Uint8Array> {
49+
const digest = await crypto.subtle.digest(algorithm, data as Uint8Array<ArrayBuffer>);
50+
51+
return new Uint8Array(digest);
5452
}
5553

5654
// ─────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)