SignatureKit
Signers

PDF / PAdES

Assine PDFs com PAdES via signPdf, aplique a política ICP-Brasil (AD-RB) e verifique com verifyPdf.

O que você vai fazer: assinar os bytes de um PDF em PAdES com signPdf, escolher a política "pades-ades" ou "pades-icp-brasil" (AD-RB) e verificar o resultado com verifyPdf. A camada Signatures, fornecida pelo a1SignaturesLayer, faz a assinatura; o módulo de PDF apenas modifica o documento.

Instale o pacote

O módulo de PDF é @signature-kit/pdf. Ele consome a camada fornecida pelo signatário A1.

npm install @signature-kit/pdf

signPdf exige o serviço Signatures no canal de requisitos; verifyPdf não.

Assine um PDF

signPdf(input: PdfSigningRequest) retorna um Effect<Uint8Array, PdfError | CmsError | SignatureKitError, Signatures> — os bytes do PDF assinado. Forneça Signatures com Effect.provide(a1SignaturesLayer(...)).

sign-pdf.ts
import { signPdf } from "@signature-kit/pdf/sign"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { Effect, Redacted } from "effect"

const program = signPdf({
  pdf,                          // Uint8Array — bytes of the original PDF
  reason: "Contract approval",
  name: "Maria Souza",
  location: "New York, US",
  signatureLength: 16384,       // bytes reserved for the CMS /Contents
}).pipe(
  Effect.provide(
    a1SignaturesLayer({
      pfx,                                   // Uint8Array — PKCS#12 (.pfx/.p12)
      password: Redacted.make(process.env.A1_PASSWORD ?? ""),
    }),
  ),
)

const signedPdf = await Effect.runPromise(program)  // signed PDF, pades-ades policy

signatureLength reserva os bytes do campo /Contents que guarda o CMS — um valor curto demais faz a assinatura falhar. reason e name têm como padrão "Digital signature" e "SignatureKit signer". Outros campos opcionais: contactInfo, location, signingTime, timestamp e appearance.

Dois certificados A1, dois PDFs assinados

Signatures é um requisito do Effect, então a escolha do certificado fica no ponto de chamada. Forneça um a1SignaturesLayer por signatário quando duas pessoas assinam saídas de PDF independentes a partir dos mesmos bytes de origem.

two-a1-signers.ts
import { signPdf } from "@signature-kit/pdf/sign"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { Effect, Redacted } from "effect"

const mariaProgram = signPdf({
  pdf: unsignedPdf,
  reason: "Maria aprovou o contrato",
  name: "Maria Souza",
  signatureLength: 16384,
}).pipe(
  Effect.provide(
    a1SignaturesLayer({
      pfx: mariaPfx,
      password: Redacted.make(mariaPassword),
    }),
  ),
)

const joaoProgram = signPdf({
  pdf: unsignedPdf,
  reason: "João aprovou o contrato",
  name: "João Silva",
  signatureLength: 16384,
}).pipe(
  Effect.provide(
    a1SignaturesLayer({
      pfx: joaoPfx,
      password: Redacted.make(joaoPassword),
    }),
  ),
)

const [mariaSignedPdf, joaoSignedPdf] = await Effect.runPromise(
  Effect.all([mariaProgram, joaoProgram]),
)

Isso assina duas cópias de PDF com dois certificados PKCS#12 diferentes sem compartilhar segredos nem estado global de signatário. Hoje signPdf grava uma assinatura PDF por arquivo de saída; não modele um fluxo multipartes no mesmo arquivo reassinando bytes já assinados em silêncio sem cobrir esse fluxo com o verificador.

hashAlgorithm aceita "sha256" (→ rsa-sha256, padrão), "sha512" (→ rsa-sha512) e o legado "sha1" (→ rsa-sha1). Só "sha384" falha, com pdf.SIGN_FAILED.

Posicione uma assinatura visual

appearance.placement.kind: "auto" calcula um retângulo visível na página escolhida a partir da âncora preferida, evitando colisão com widgets/anotações existentes — sem coordenadas manuais quando você só quer uma assinatura no rodapé/canto. Sem appearance, a assinatura permanece invisível por compatibilidade.

position-pdf-signature.ts
const signed = await Effect.runPromise(
  signPdf({
    pdf,
    appearance: {
      placement: {
        kind: "auto",          // visible placement, computed by the PDF
        page: "last",          // auto default; use pageIndex for an exact page
        anchor: "bottom-right",
        width: 180,
        height: 54,
        margin: 36,
        gap: 8,                // clearance against existing widgets/annotations
      },
    },
  }).pipe(Effect.provide(layer)),
)

// Manual: PDF coordinates [left, bottom, right, top]
const manual = { appearance: { placement: { kind: "manual", pageIndex: 0, widgetRect: [72, 72, 216, 108] } } }

// Invisible: /Widget field without a visual area
const invisible = { appearance: { placement: { kind: "invisible" } } }

Use kind: "manual" quando sua aplicação já tiver coordenadas confiáveis, e kind: "invisible" quando o PDF deve carregar apenas a assinatura criptográfica. Se o modo auto não encontrar espaço dentro das margens, o effect falha com pdf.SIGNATURE_PLACEMENT_FAILED no canal tipado.

Política PAdES ICP-Brasil

PdfSignaturePolicy tem exatamente dois valores: "pades-ades" (padrão) e "pades-icp-brasil". Quando você escolhe "pades-icp-brasil" sem icpBrasil, o pacote PDF usa os metadados PA_PAdES_AD_RB_v1_1 fixados em @signature-kit/cms; não há montagem de política na app nem fetch de política em runtime. Assinaturas ICP-Brasil reservam 32768 bytes em /Contents por padrão, a menos que você defina signatureLength.

icp-brasil-policy.ts
// PdfSignaturePolicy = "pades-ades" | "pades-icp-brasil"
// Padrão quando policy é omitido: "pades-ades"

const signed = signPdf({
  pdf,
  policy: "pades-icp-brasil",
}).pipe(Effect.provide(layer))

// O input explícito ainda vence quando você precisa de outra política AD-RB/AD-RT.
const explicit = signPdf({
  pdf,
  policy: "pades-icp-brasil",
  hashAlgorithm: "sha256",
  signatureLength: 49152,
  icpBrasil: customPolicy,
}).pipe(Effect.provide(layer))

O objeto AD-RB embutido carrega policyOid, policyHash, policyHashAlgorithm e policyUri da PA_PAdES_AD_RB_v1_1.

Mantenha policyHashAlgorithm consistente com o hashAlgorithm da assinatura quando fornecer um icpBrasil customizado — use "sha256" ou "sha512" em ambos.

Mesclar PDFs

mergePdfs([first, second]) copia as páginas para um novo PDF. Ele não preserva campos AcroForm, metadados do documento ou estado de formulário no catálogo dos arquivos de origem; use para fluxos de conteúdo de página já achatado, não para preservar formulários interativos.

Verifique com verifyPdf

verifyPdf(input: PdfVerificationRequest) retorna um Effect<PdfVerificationResult, PdfError | CmsError>sem o requisito Signatures, então não há Effect.provide(...) aqui. Opcionalmente, passe trustedRoots (uma lista de Uint8Array) para validar a cadeia contra suas próprias raízes.

verify-pdf.ts
import { verifyPdf } from "@signature-kit/pdf/verify"
import { Effect } from "effect"

// verifyPdf does NOT require the Signatures service — there is no .pipe(Effect.provide(...))
const result = await Effect.runPromise(verifyPdf({ pdf: signedPdf }))

result.valid             // boolean — CMS integrity over the byteRange
result.chainValid        // boolean — signer chain verified
result.revocationStatus  // "checked" | "not_checked" — evidência de revogação foi avaliada
result.signatureCount    // number  — signatures found in the PDF
result.byteRange         // [number, number, number, number]
result.signerSerialNumber // string | null — serial of the signer certificate

byteRange é a quádrupla coberta pela assinatura. Um PDF sem assinatura (sem /ByteRange) não é um resultado de sucesso com signerSerialNumber: nullverifyPdf falha no canal de erro com pdf.PLACEHOLDER_NOT_FOUND.

Erros que você pode encontrar

A assinatura e a verificação de PDF falham com códigos da família pdf.* (PdfError); as falhas do signatário A1 que fornece Signatures chegam como signature-kit.*.

Continue a partir daqui

Como está este guia?

Nesta página