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/pdfsignPdf 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(...)).
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 policysignatureLength 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.
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.
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.
// 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.
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 certificatebyteRange é a quádrupla coberta pela assinatura. Um PDF sem assinatura (sem /ByteRange) não é um resultado de sucesso com signerSerialNumber: null — verifyPdf 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.*.
pdf.SIGN_FAILED—hashAlgorithmnão suportado (apenassha384é rejeitado).pdf.SIGNATURE_TOO_LARGE— o CMS excedeu o espaço reservado; aumentesignatureLength.pdf.SIGNATURE_PLACEMENT_FAILED— o posicionamento automático não encontrou espaço ou recebeu dimensões inválidas.pdf.INVALID_PDF— os bytes não formam um PDF válido para assinar ou verificar.pdf.PLACEHOLDER_NOT_FOUND—verifyPdfnão encontrou/ByteRange, por exemplo em um PDF sem assinatura.signature-kit.WRONG_PASSWORD— senha A1 incorreta ao fornecer o serviço de assinaturas.
Continue a partir daqui
Como está este guia?