@f-o-t/e-signature 1.2.8 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,56 @@ bun add @f-o-t/e-signature
16
16
  - RFC 3161 timestamp support
17
17
  - QR code generation for signature verification
18
18
  - Configurable DocMDP permissions for document modification control
19
+ - **Browser compatible** — no `Buffer` or Node-only APIs; runs in browsers, Edge Runtime, and Cloudflare Workers
20
+
21
+ ## React Hook
22
+
23
+ ### `useSignPdf(): UseSignPdfReturn`
24
+
25
+ React hook for client-side PDF signing. Import from `@f-o-t/e-signature/plugins/react` (requires `react >= 18` as a peer dependency).
26
+
27
+ ```tsx
28
+ import { useSignPdf } from "@f-o-t/e-signature/plugins/react";
29
+
30
+ function SignForm() {
31
+ const { sign, isSigning, isDone, isError, result, error, download, reset } = useSignPdf();
32
+
33
+ async function handleSubmit(pdfFile: File, p12File: File) {
34
+ try {
35
+ await sign({
36
+ pdf: pdfFile, // File, Blob, or Uint8Array
37
+ p12: p12File, // File, Blob, or Uint8Array
38
+ password: "secret",
39
+ options: { reason: "Approval", location: "São Paulo", policy: "pades-icp-brasil" },
40
+ });
41
+ } catch {
42
+ // error state is also set on the hook
43
+ }
44
+ }
45
+
46
+ if (isSigning) return <p>Signing…</p>;
47
+ if (isError) return <p>Error: {error?.message}</p>;
48
+ if (isDone) return <button onClick={() => download("signed.pdf")}>Download</button>;
49
+ return <button onClick={() => handleSubmit(myPdf, myCert)}>Sign</button>;
50
+ }
51
+ ```
52
+
53
+ **Return value:**
54
+
55
+ | Property | Type | Description |
56
+ |---|---|---|
57
+ | `status` | `"idle" \| "signing" \| "done" \| "error"` | Current lifecycle state |
58
+ | `isIdle` | `boolean` | No operation in progress |
59
+ | `isSigning` | `boolean` | Signing is running |
60
+ | `isDone` | `boolean` | Last sign call succeeded |
61
+ | `isError` | `boolean` | Last sign call failed |
62
+ | `result` | `Uint8Array \| null` | Signed PDF bytes (non-null when `isDone`) |
63
+ | `error` | `Error \| null` | Failure reason (non-null when `isError`) |
64
+ | `sign(input)` | `(SignInput) => Promise<Uint8Array \| undefined>` | Trigger signing; concurrent calls while signing are ignored (returns `undefined`) |
65
+ | `download(filename?)` | `(string?) => void` | Trigger browser download of signed PDF; no-op if not done |
66
+ | `reset()` | `() => void` | Return to idle, clearing result and error |
67
+
68
+ `pdf` and `p12` accept `File`, `Blob`, or `Uint8Array` — the hook converts `File`/`Blob` to `Uint8Array` automatically before calling `signPdf`.
19
69
 
20
70
  ## API
21
71
 
@@ -0,0 +1,541 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/icp-brasil.ts
5
+ import {
6
+ contextTag,
7
+ decodeDer,
8
+ encodeDer,
9
+ ia5String,
10
+ nullValue,
11
+ octetString,
12
+ oid,
13
+ sequence
14
+ } from "@f-o-t/asn1";
15
+ import { hash } from "@f-o-t/crypto";
16
+ var SHA256_OID = "2.16.840.1.101.3.4.2.1";
17
+ var SIGNING_CERTIFICATE_V2_OID = "1.2.840.113549.1.9.16.2.47";
18
+ var SIGNATURE_POLICY_OID = "1.2.840.113549.1.9.16.2.15";
19
+ var POLICY_CONFIG = {
20
+ OID: "2.16.76.1.7.1.11.1.1",
21
+ URL: "http://politicas.icpbrasil.gov.br/PA_PAdES_AD_RB_v1_1.der"
22
+ };
23
+ var SPQ_ETS_URI_OID = "1.2.840.113549.1.9.16.5.1";
24
+ var cachedPolicyData = null;
25
+ function clearPolicyCache() {
26
+ cachedPolicyData = null;
27
+ }
28
+ function buildSigningCertificateV2(certDer) {
29
+ const certHash = hash("sha256", certDer);
30
+ const hashAlgId = sequence(oid(SHA256_OID), nullValue());
31
+ const cert = decodeDer(certDer);
32
+ const tbsCert = cert.value[0];
33
+ const tbs = tbsCert.value;
34
+ let idx = 0;
35
+ if (tbs[0].class === "context" && tbs[0].tag === 0) {
36
+ idx = 1;
37
+ }
38
+ const serialNumber = tbs[idx];
39
+ const issuerName = tbs[idx + 2];
40
+ const generalName = contextTag(4, [issuerName]);
41
+ const generalNames = sequence(generalName);
42
+ const issuerSerial = sequence(generalNames, serialNumber);
43
+ const essCertIdV2 = sequence(hashAlgId, octetString(certHash), issuerSerial);
44
+ const signingCertV2 = sequence(sequence(essCertIdV2));
45
+ return encodeDer(signingCertV2);
46
+ }
47
+ async function downloadAndParsePolicyDocument() {
48
+ if (cachedPolicyData) {
49
+ return cachedPolicyData;
50
+ }
51
+ const response = await fetch(POLICY_CONFIG.URL);
52
+ if (!response.ok) {
53
+ throw new SignaturePolicyError(`Failed to download signature policy: HTTP ${response.status}`);
54
+ }
55
+ const arrayBuffer = await response.arrayBuffer();
56
+ const data = new Uint8Array(arrayBuffer);
57
+ if (data.length === 0 || data[0] !== 48) {
58
+ throw new SignaturePolicyError("Invalid DER format in policy document");
59
+ }
60
+ const asn1 = decodeDer(data);
61
+ const children = asn1.value;
62
+ if (!Array.isArray(children) || children.length < 3) {
63
+ throw new SignaturePolicyError(`Unexpected policy structure: expected 3+ children, got ${children?.length}`);
64
+ }
65
+ const algIdChildren = children[0].value;
66
+ if (!Array.isArray(algIdChildren) || algIdChildren.length === 0) {
67
+ throw new SignaturePolicyError("Invalid AlgorithmIdentifier in policy");
68
+ }
69
+ const { bytesToOid } = await import("@f-o-t/asn1");
70
+ const hashAlgOid = bytesToOid(algIdChildren[0].value);
71
+ const hashNode = children[2];
72
+ if (hashNode.tag !== 4) {
73
+ throw new SignaturePolicyError(`Expected OCTET STRING at child[2], got tag 0x${hashNode.tag.toString(16)}`);
74
+ }
75
+ cachedPolicyData = {
76
+ hashAlgOid,
77
+ policyHash: hashNode.value
78
+ };
79
+ return cachedPolicyData;
80
+ }
81
+ async function buildSignaturePolicy() {
82
+ const { hashAlgOid, policyHash } = await downloadAndParsePolicyDocument();
83
+ const hashAlgId = sequence(oid(hashAlgOid));
84
+ const sigPolicyHash = sequence(hashAlgId, octetString(policyHash));
85
+ const sigPolicyQualifiers = sequence(sequence(oid(SPQ_ETS_URI_OID), ia5String(POLICY_CONFIG.URL)));
86
+ const signaturePolicyId = sequence(oid(POLICY_CONFIG.OID), sigPolicyHash, sigPolicyQualifiers);
87
+ return encodeDer(signaturePolicyId);
88
+ }
89
+ var ICP_BRASIL_OIDS = {
90
+ signingCertificateV2: SIGNING_CERTIFICATE_V2_OID,
91
+ signaturePolicy: SIGNATURE_POLICY_OID
92
+ };
93
+
94
+ class SignaturePolicyError extends Error {
95
+ constructor(message) {
96
+ super(message);
97
+ this.name = "SignaturePolicyError";
98
+ }
99
+ }
100
+
101
+ // src/schemas.ts
102
+ import { z } from "zod";
103
+ var signatureAppearanceSchema = z.object({
104
+ x: z.number(),
105
+ y: z.number(),
106
+ width: z.number().positive(),
107
+ height: z.number().positive(),
108
+ page: z.number().int().min(0).optional(),
109
+ showQrCode: z.boolean().optional(),
110
+ showCertInfo: z.boolean().optional()
111
+ });
112
+ var qrCodeConfigSchema = z.object({
113
+ data: z.string().optional(),
114
+ size: z.number().int().positive().optional()
115
+ });
116
+ var pdfSignOptionsSchema = z.object({
117
+ certificate: z.object({
118
+ p12: z.instanceof(Uint8Array).refine((v) => v.length > 0, {
119
+ message: "P12 data must not be empty"
120
+ }),
121
+ password: z.string(),
122
+ name: z.string().optional()
123
+ }),
124
+ reason: z.string().optional(),
125
+ location: z.string().optional(),
126
+ contactInfo: z.string().optional(),
127
+ policy: z.enum(["pades-ades", "pades-icp-brasil"]).optional(),
128
+ timestamp: z.boolean().optional(),
129
+ tsaUrl: z.string().url().optional(),
130
+ tsaTimeout: z.number().positive().optional(),
131
+ tsaRetries: z.number().int().min(0).optional(),
132
+ tsaFallbackUrls: z.array(z.string().url()).optional(),
133
+ onTimestampError: z.function({ input: z.tuple([z.unknown()]), output: z.void() }).optional(),
134
+ appearance: z.union([signatureAppearanceSchema, z.literal(false)]).optional(),
135
+ appearances: z.array(signatureAppearanceSchema).optional(),
136
+ qrCode: qrCodeConfigSchema.optional(),
137
+ docMdpPermission: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional()
138
+ });
139
+
140
+ // src/timestamp.ts
141
+ import {
142
+ boolean as asn1Boolean,
143
+ decodeDer as decodeDer2,
144
+ encodeDer as encodeDer2,
145
+ integer,
146
+ octetString as octetString2,
147
+ oid as oid2,
148
+ sequence as sequence2
149
+ } from "@f-o-t/asn1";
150
+ import { hash as hash2 } from "@f-o-t/crypto";
151
+ var HASH_OIDS = {
152
+ sha256: "2.16.840.1.101.3.4.2.1",
153
+ sha384: "2.16.840.1.101.3.4.2.2",
154
+ sha512: "2.16.840.1.101.3.4.2.3"
155
+ };
156
+ var TIMESTAMP_SERVERS = {
157
+ VALID: "http://timestamp.valid.com.br/tsa",
158
+ SAFEWEB: "http://tsa.safeweb.com.br/tsa/tsa",
159
+ CERTISIGN: "http://timestamp.certisign.com.br"
160
+ };
161
+ var TIMESTAMP_TOKEN_OID = "1.2.840.113549.1.9.16.2.14";
162
+ async function requestTimestamp(dataToTimestamp, tsaUrl, hashAlgorithm = "sha256", options) {
163
+ const messageHash = hash2(hashAlgorithm, dataToTimestamp);
164
+ const timestampReq = buildTimestampRequest(messageHash, hashAlgorithm);
165
+ const tsaTimeout = options?.tsaTimeout ?? 1e4;
166
+ const tsaRetries = options?.tsaRetries ?? 0;
167
+ const tsaFallbackUrls = options?.tsaFallbackUrls ?? [];
168
+ let lastError;
169
+ for (let attempt = 1;attempt <= 1 + tsaRetries; attempt++) {
170
+ if (attempt > 1) {
171
+ await sleep(2 ** (attempt - 2) * 1000);
172
+ }
173
+ try {
174
+ return await fetchTimestamp(tsaUrl, timestampReq, tsaTimeout);
175
+ } catch (err) {
176
+ lastError = err instanceof Error ? err : new Error(String(err));
177
+ }
178
+ }
179
+ for (const fallbackUrl of tsaFallbackUrls) {
180
+ try {
181
+ return await fetchTimestamp(fallbackUrl, timestampReq, tsaTimeout);
182
+ } catch (err) {
183
+ lastError = err instanceof Error ? err : new Error(String(err));
184
+ }
185
+ }
186
+ const fallbackList = tsaFallbackUrls.length > 0 ? `, fallbacks: [${tsaFallbackUrls.join(", ")}]` : "";
187
+ throw new TimestampError(`TSA request failed: all servers unreachable (primary: ${tsaUrl}${fallbackList}). Last error: ${lastError?.message ?? "unknown"}`);
188
+ }
189
+ function sleep(ms) {
190
+ return new Promise((resolve) => setTimeout(resolve, ms));
191
+ }
192
+ async function fetchTimestamp(url, timestampReq, timeoutMs) {
193
+ let response;
194
+ try {
195
+ response = await fetch(url, {
196
+ method: "POST",
197
+ headers: {
198
+ "Content-Type": "application/timestamp-query"
199
+ },
200
+ body: timestampReq,
201
+ signal: AbortSignal.timeout(timeoutMs)
202
+ });
203
+ } catch (err) {
204
+ const msg = err instanceof Error ? err.message : String(err);
205
+ throw new Error(`TSA server unreachable: ${url} \u2014 ${msg}`);
206
+ }
207
+ if (!response.ok) {
208
+ throw new TimestampError(`TSA returned HTTP ${response.status}`);
209
+ }
210
+ const respBuffer = new Uint8Array(await response.arrayBuffer());
211
+ return extractTimestampToken(respBuffer);
212
+ }
213
+ function buildTimestampRequest(messageHash, hashAlgorithm) {
214
+ const hashOid = HASH_OIDS[hashAlgorithm];
215
+ if (!hashOid) {
216
+ throw new TimestampError(`Unsupported hash algorithm: ${hashAlgorithm}`);
217
+ }
218
+ const timestampReq = sequence2(integer(1), sequence2(sequence2(oid2(hashOid)), octetString2(messageHash)), asn1Boolean(true));
219
+ return encodeDer2(timestampReq);
220
+ }
221
+ function extractTimestampToken(respDer) {
222
+ let resp;
223
+ try {
224
+ resp = decodeDer2(respDer);
225
+ } catch {
226
+ throw new TimestampError("Invalid timestamp response: not valid DER");
227
+ }
228
+ const children = resp.value;
229
+ if (!Array.isArray(children) || children.length < 1) {
230
+ throw new TimestampError("Invalid timestamp response: unexpected structure");
231
+ }
232
+ const statusInfo = children[0].value;
233
+ const statusBytes = statusInfo[0].value;
234
+ const statusValue = statusBytes[statusBytes.length - 1];
235
+ if (statusValue !== 0 && statusValue !== 1) {
236
+ throw new TimestampError(`Timestamp request rejected with status: ${statusValue}`);
237
+ }
238
+ if (!children[1]) {
239
+ throw new TimestampError("Timestamp response does not contain a token");
240
+ }
241
+ return encodeDer2(children[1]);
242
+ }
243
+
244
+ class TimestampError extends Error {
245
+ constructor(message) {
246
+ super(message);
247
+ this.name = "TimestampError";
248
+ }
249
+ }
250
+
251
+ // src/sign-pdf.ts
252
+ import { decodeDer as decodeDer3 } from "@f-o-t/asn1";
253
+ import {
254
+ appendUnauthAttributes,
255
+ createSignedData,
256
+ parsePkcs12
257
+ } from "@f-o-t/crypto";
258
+ import { parseCertificate } from "@f-o-t/digital-certificate";
259
+ import {
260
+ embedSignature,
261
+ extractBytesToSign,
262
+ findByteRange,
263
+ loadPdf
264
+ } from "@f-o-t/pdf/plugins/editing";
265
+
266
+ // src/appearance.ts
267
+ import { hash as hash3 } from "@f-o-t/crypto";
268
+ import { generateQrCode } from "@f-o-t/qrcode";
269
+ function drawSignatureAppearance(doc, page, appearance, certInfo, options) {
270
+ const { x, width, height } = appearance;
271
+ const showQrCode = appearance.showQrCode !== false;
272
+ const showCertInfo = appearance.showCertInfo !== false;
273
+ const y = page.height - appearance.y - height;
274
+ let qrSize = 0;
275
+ if (showQrCode) {
276
+ const qrImage = options.preEmbeddedQr ?? (() => {
277
+ const qrData = options.qrCode?.data ?? createVerificationData(certInfo, options.pdfData);
278
+ const qrPng = generateQrCode(qrData, {
279
+ size: options.qrCode?.size ?? 100
280
+ });
281
+ return doc.embedPng(qrPng);
282
+ })();
283
+ qrSize = Math.min(100, height - 20);
284
+ page.drawImage(qrImage, {
285
+ x: x + 10,
286
+ y: y + 10,
287
+ width: qrSize,
288
+ height: qrSize
289
+ });
290
+ }
291
+ if (showCertInfo) {
292
+ drawCertInfo(page, certInfo, {
293
+ x,
294
+ y,
295
+ width,
296
+ height,
297
+ qrOffset: qrSize > 0 ? qrSize + 20 : 10,
298
+ reason: options.reason,
299
+ location: options.location
300
+ });
301
+ }
302
+ }
303
+ function drawCertInfo(page, certInfo, opts) {
304
+ const textX = opts.x + opts.qrOffset;
305
+ let textY = opts.y + opts.height - 20;
306
+ const fontSize = 10;
307
+ const lineHeight = 14;
308
+ page.drawText("ASSINADO DIGITALMENTE", {
309
+ x: textX,
310
+ y: textY,
311
+ size: 12
312
+ });
313
+ textY -= lineHeight * 1.5;
314
+ if (certInfo) {
315
+ const signerName = certInfo.subject.commonName || "N/A";
316
+ page.drawText(`Assinado por: ${signerName}`, {
317
+ x: textX,
318
+ y: textY,
319
+ size: fontSize
320
+ });
321
+ textY -= lineHeight;
322
+ if (certInfo.brazilian.cnpj) {
323
+ const cnpj = formatCnpj(certInfo.brazilian.cnpj);
324
+ page.drawText(`CNPJ: ${cnpj}`, {
325
+ x: textX,
326
+ y: textY,
327
+ size: fontSize
328
+ });
329
+ textY -= lineHeight;
330
+ } else if (certInfo.brazilian.cpf) {
331
+ const cpf = formatCpf(certInfo.brazilian.cpf);
332
+ page.drawText(`CPF: ${cpf}`, {
333
+ x: textX,
334
+ y: textY,
335
+ size: fontSize
336
+ });
337
+ textY -= lineHeight;
338
+ }
339
+ const now = new Date;
340
+ const dateStr = now.toLocaleDateString("pt-BR");
341
+ const timeStr = now.toLocaleTimeString("pt-BR");
342
+ page.drawText(`Data: ${dateStr} ${timeStr}`, {
343
+ x: textX,
344
+ y: textY,
345
+ size: fontSize
346
+ });
347
+ textY -= lineHeight;
348
+ if (opts.location) {
349
+ page.drawText(`Local: ${opts.location}`, {
350
+ x: textX,
351
+ y: textY,
352
+ size: fontSize - 1
353
+ });
354
+ }
355
+ } else {
356
+ page.drawText(`Signed: ${opts.reason || "Digital Signature"}`, {
357
+ x: textX,
358
+ y: textY,
359
+ size: fontSize
360
+ });
361
+ textY -= lineHeight;
362
+ if (opts.location) {
363
+ page.drawText(`Location: ${opts.location}`, {
364
+ x: textX,
365
+ y: textY,
366
+ size: fontSize
367
+ });
368
+ }
369
+ }
370
+ }
371
+ function precomputeSharedQrImage(doc, certInfo, pdfData, qrConfig) {
372
+ const qrData = qrConfig?.data ?? createVerificationData(certInfo, pdfData);
373
+ const qrPng = generateQrCode(qrData, { size: qrConfig?.size ?? 100 });
374
+ return doc.embedPng(qrPng);
375
+ }
376
+ function createVerificationData(certInfo, pdfData) {
377
+ const documentHash = toHex(hash3("sha256", pdfData));
378
+ const timestamp = new Date().toISOString();
379
+ if (certInfo) {
380
+ const certFingerprint = certInfo.fingerprint;
381
+ return `https://validar.iti.gov.br/?` + `doc=${documentHash.substring(0, 16)}&` + `cert=${certFingerprint.substring(0, 16)}&` + `time=${encodeURIComponent(timestamp)}`;
382
+ }
383
+ return `https://validar.iti.gov.br/?doc=${documentHash.substring(0, 16)}&time=${encodeURIComponent(timestamp)}`;
384
+ }
385
+ function formatCnpj(cnpj) {
386
+ return cnpj.replace(/(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})/, "$1.$2.$3/$4-$5");
387
+ }
388
+ function formatCpf(cpf) {
389
+ return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
390
+ }
391
+ function toHex(data) {
392
+ const chars = [];
393
+ for (let i = 0;i < data.length; i++) {
394
+ chars.push(data[i].toString(16).padStart(2, "0"));
395
+ }
396
+ return chars.join("");
397
+ }
398
+
399
+ // src/sign-pdf.ts
400
+ async function signPdf(pdf, options) {
401
+ let pdfBytes;
402
+ if (pdf instanceof ReadableStream) {
403
+ const chunks = [];
404
+ const reader = pdf.getReader();
405
+ try {
406
+ while (true) {
407
+ const { done, value } = await reader.read();
408
+ if (done)
409
+ break;
410
+ if (value)
411
+ chunks.push(value);
412
+ }
413
+ } finally {
414
+ reader.releaseLock();
415
+ }
416
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
417
+ pdfBytes = new Uint8Array(totalLength);
418
+ let offset = 0;
419
+ for (const chunk of chunks) {
420
+ pdfBytes.set(chunk, offset);
421
+ offset += chunk.length;
422
+ }
423
+ } else {
424
+ pdfBytes = pdf;
425
+ }
426
+ const opts = pdfSignOptionsSchema.parse(options);
427
+ const { certificate, privateKey, chain } = parsePkcs12(opts.certificate.p12, opts.certificate.password);
428
+ let certInfo = null;
429
+ try {
430
+ certInfo = parseCertificate(opts.certificate.p12, opts.certificate.password);
431
+ } catch {}
432
+ const doc = loadPdf(pdfBytes);
433
+ if (opts.appearance !== false && opts.appearance) {
434
+ const pageIndex = opts.appearance.page ?? 0;
435
+ if (pageIndex < 0 || pageIndex >= doc.pageCount) {
436
+ throw new PdfSignError(`Invalid page index: ${pageIndex}. PDF has ${doc.pageCount} pages.`);
437
+ }
438
+ const page = doc.getPage(pageIndex);
439
+ drawSignatureAppearance(doc, page, opts.appearance, certInfo, {
440
+ reason: opts.reason,
441
+ location: opts.location,
442
+ qrCode: opts.qrCode,
443
+ pdfData: pdfBytes
444
+ });
445
+ }
446
+ if (opts.appearances && opts.appearances.length > 0) {
447
+ const needsQr = opts.appearances.some((a) => a.showQrCode !== false);
448
+ const sharedQrImage = needsQr ? precomputeSharedQrImage(doc, certInfo, pdfBytes, opts.qrCode) : undefined;
449
+ for (const app of opts.appearances) {
450
+ const pageIndex = app.page ?? 0;
451
+ if (pageIndex < 0 || pageIndex >= doc.pageCount) {
452
+ throw new PdfSignError(`Invalid page index ${pageIndex} in appearances. PDF has ${doc.pageCount} pages.`);
453
+ }
454
+ const page = doc.getPage(pageIndex);
455
+ drawSignatureAppearance(doc, page, app, certInfo, {
456
+ reason: opts.reason,
457
+ location: opts.location,
458
+ qrCode: opts.qrCode,
459
+ pdfData: pdfBytes,
460
+ preEmbeddedQr: sharedQrImage
461
+ });
462
+ }
463
+ }
464
+ const signerName = certInfo?.subject.commonName || opts.certificate.name || "Digital Signature";
465
+ const certChainBytes = certificate.length + chain.reduce((sum, c) => sum + c.length, 0);
466
+ const signatureLength = Math.max(16384, certChainBytes * 2 + (opts.tsaUrl ? 4096 : 0) + 8192);
467
+ const appearancePage = opts.appearance ? opts.appearance.page ?? 0 : opts.appearances?.[0]?.page ?? 0;
468
+ const { pdf: pdfWithPlaceholder } = doc.saveWithPlaceholder({
469
+ reason: opts.reason || "Digitally signed",
470
+ name: signerName,
471
+ location: opts.location,
472
+ contactInfo: opts.contactInfo,
473
+ signatureLength,
474
+ docMdpPermission: opts.docMdpPermission ?? 2,
475
+ appearancePage
476
+ });
477
+ const { byteRange } = findByteRange(pdfWithPlaceholder);
478
+ const bytesToSign = extractBytesToSign(pdfWithPlaceholder, byteRange);
479
+ const authenticatedAttributes = [];
480
+ if (opts.policy === "pades-icp-brasil") {
481
+ const sigCertV2 = buildSigningCertificateV2(certificate);
482
+ authenticatedAttributes.push({
483
+ oid: ICP_BRASIL_OIDS.signingCertificateV2,
484
+ values: [sigCertV2]
485
+ });
486
+ try {
487
+ const sigPolicy = await buildSignaturePolicy();
488
+ authenticatedAttributes.push({
489
+ oid: ICP_BRASIL_OIDS.signaturePolicy,
490
+ values: [sigPolicy]
491
+ });
492
+ } catch {}
493
+ }
494
+ const unauthenticatedAttributes = [];
495
+ const signedData = createSignedData({
496
+ content: bytesToSign,
497
+ certificate,
498
+ privateKey,
499
+ chain,
500
+ hashAlgorithm: "sha256",
501
+ authenticatedAttributes: authenticatedAttributes.length > 0 ? authenticatedAttributes : undefined,
502
+ unauthenticatedAttributes: unauthenticatedAttributes.length > 0 ? unauthenticatedAttributes : undefined,
503
+ detached: true
504
+ });
505
+ if (opts.timestamp && opts.tsaUrl) {
506
+ try {
507
+ const tsToken = await requestTimestamp(extractSignatureValue(signedData), opts.tsaUrl, "sha256", {
508
+ tsaTimeout: opts.tsaTimeout,
509
+ tsaRetries: opts.tsaRetries,
510
+ tsaFallbackUrls: opts.tsaFallbackUrls
511
+ });
512
+ unauthenticatedAttributes.push({
513
+ oid: TIMESTAMP_TOKEN_OID,
514
+ values: [tsToken]
515
+ });
516
+ } catch (err) {
517
+ opts.onTimestampError?.(err);
518
+ }
519
+ }
520
+ const finalSignedData = appendUnauthAttributes(signedData, unauthenticatedAttributes);
521
+ return embedSignature(pdfWithPlaceholder, finalSignedData);
522
+ }
523
+ function extractSignatureValue(contentInfoDer) {
524
+ const contentInfo = decodeDer3(contentInfoDer);
525
+ const signedDataNode = contentInfo.value[1].value[0];
526
+ const signerInfosSet = signedDataNode.value.at(-1);
527
+ const signerInfo = signerInfosSet.value[0];
528
+ const signatureNode = signerInfo.value[5];
529
+ return signatureNode.value;
530
+ }
531
+
532
+ class PdfSignError extends Error {
533
+ constructor(message) {
534
+ super(message);
535
+ this.name = "PdfSignError";
536
+ }
537
+ }
538
+
539
+ export { clearPolicyCache, buildSigningCertificateV2, buildSignaturePolicy, ICP_BRASIL_OIDS, SignaturePolicyError, pdfSignOptionsSchema, TIMESTAMP_SERVERS, TIMESTAMP_TOKEN_OID, requestTimestamp, TimestampError, signPdf, PdfSignError };
540
+
541
+ //# debugId=7AE89559679974C564756E2164756E21