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: Uint8Array = yield* 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] = yield* 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 apenas "sha256" (→ rsa-sha256) ou "sha512" (→ rsa-sha512). Valores como sha1 ou sha384 falham 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 = yield* 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 o campo icpBrasil, o módulo busca automaticamente a Signature Policy AD-RB — controle essa busca remota com policyTimeoutMillis. Para evitar a rede, passe o objeto icpBrasil explícito.

icp-brasil-policy.ts
// PdfSignaturePolicy = "pades-ades" | "pades-icp-brasil"
// Signature default when policy is omitted: "pades-ades"

// Automatic fetch of the AD-RB policy (without explicit icpBrasil)
const auto = signPdf({
  pdf,
  policy: "pades-icp-brasil",
  policyTimeoutMillis: 10_000,   // limit for the remote SP fetch
}).pipe(Effect.provide(layer))

// Explicit ICP-Brasil policy — you provide the identifiers
const explicit = signPdf({
  pdf,
  policy: "pades-icp-brasil",
  hashAlgorithm: "sha256",       // only "sha256" or "sha512"
  icpBrasil: {
    policyOid: "2.16.76.1.7.1.11.1.1",
    policyHash: new Uint8Array(32),
    policyHashAlgorithm: "sha256",
    policyUri: "http://politicas.icpbrasil.gov.br/PA_PAdES_AD_RB_v1_1.der",
  },
}).pipe(Effect.provide(layer))

O objeto explícito carrega policyOid, policyHash, policyHashAlgorithm e policyUri — os mesmos identificadores da SP AD-RB que a busca automática traria, agora fixos no seu código.

Busca remota da SP

Sem o objeto icpBrasil explícito, "pades-icp-brasil" faz uma busca remota da Signature Policy AD-RB — sujeita à rede e a policyTimeoutMillis. Em ambientes sem acesso de saída, forneça o objeto icpBrasil para evitar a chamada.

mantenha policyHashAlgorithm consistente com o hashAlgorithm da assinatura — use "sha256" ou "sha512" em ambos.

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 = yield* verifyPdf({ pdf: signedPdf })

result.valid             // boolean — CMS integrity over the byteRange
result.chainValid        // boolean — signer chain verified
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; signerSerialNumber é null quando o PDF não tem assinatura.

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

Nesta página