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: Uint8Array = yield* 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] = 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.
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.
// 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.
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 certificatebyteRange é 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.*.
pdf.SIGN_FAILED—hashAlgorithmnão suportado (por exemplo,sha1/sha384).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.signature-kit.WRONG_PASSWORD— senha A1 incorreta ao fornecer o serviço de assinaturas.