SignatureKit
Signers

XML-DSig

Assine XML com signXml e valide com verifyXml: referência enveloped, X.509 no KeyInfo, verificação sem signatário.

O que você vai fazer: assinar uma string XML com signXml na camada do Signatures, depois validar com verifyXml — que não exige signatário e roda em qualquer lugar.

O formato XML vive em @signature-kit/xml e consome o serviço Signatures fornecido por um signatário.

npm install @signature-kit/xml

signXml recebe um XmlSigningRequest e retorna Effect<string, XmlError | SignatureKitError, Signatures | XmlRuntime> — o XML assinado como string. Satisfaça o requisito Signatures com Effect.provide(a1SignaturesLayer(...)): o A1 fornece o poder de assinatura, o módulo de formato só altera o documento.

sign-xml.ts
import { signXml } from "@signature-kit/xml/sign"
import { xmlRuntimeLayer } from "@signature-kit/xml/runtime"
import { a1SignaturesLayer } from "@signature-kit/a1/signer"
import { Effect, Redacted } from "effect"

const layer = a1SignaturesLayer({
  pfx,                                       // Uint8Array — bytes of the .pfx/.p12
  password: Redacted.make(process.env.A1_PASSWORD ?? ""),
})

// signXml -> Effect<string, XmlError | SignatureKitError, Signatures | XmlRuntime>
const signed: string = yield* signXml({
  xml,                                       // string — input document
  referenceId: "nfe-1",
}).pipe(Effect.provide(layer), Effect.provide(xmlRuntimeLayer))
// 'signed' is the signed XML, with the embedded <Signature>

verifyXml não tem requisito Signatures — apenas signXml precisa da camada de assinatura.

referenceId define o alvo da assinatura enveloped: passar referenceId: "nfe-1" produz uma Reference com URI "#nfe-1", apontando para o elemento de mesmo Id no próprio documento. Os demais campos de XmlSigningRequest são opcionais:

request-xml.ts
// XmlSigningRequest — only 'xml' is required
const signed = yield* signXml({
  xml,
  algorithm: "rsa-sha256",                   // default; or "rsa-sha512", or legacy "rsa-sha1"
  referenceId: "nfe-1",                      // -> Reference URI "#nfe-1"
  signatureId: "SignatureKit-NFe",              // id of the <Signature> element
  signingTime: new Date(),
}).pipe(Effect.provide(layer), Effect.provide(xmlRuntimeLayer))

Sem algorithm, vale o padrão "rsa-sha256"; as outras opções são "rsa-sha512" e a legada e desaconselhada "rsa-sha1". O certificado X.509 do signatário é embutido no KeyInfo da assinatura, mas o verificador não confia nele automaticamente — os últimos dois passos abaixo cobrem como fornecer a chave ou o certificado confiável por fora.

A NF-e é um caso comum: o Id de infNFe é a chave de acesso, e a assinatura aponta para ele. Use a chave de acesso como referenceId para que a URI fique "#NFe35...".

sign-nfe.ts
// NF-e: the id of <infNFe> is the target of the enveloped reference
const xml = `<NFe xmlns="http://www.portalfiscal.inf.br/nfe">
  <infNFe Id="NFe35200114200166000187550010000000071234567890" versao="4.00">
    <!-- ide, emit, dest, det, total, transp, ... -->
  </infNFe>
</NFe>`

const signed = yield* signXml({
  xml,
  referenceId: "NFe35200114200166000187550010000000071234567890",
}).pipe(Effect.provide(layer), Effect.provide(xmlRuntimeLayer))
// the <Signature> is inserted as a sibling of <infNFe>, pointing to "#NFe35..."

A string de saída é o XML pronto para enviar. Como signXml retorna string, jogue-a direto no payload da SEFAZ.

verifyXml recebe um XmlVerificationRequest e retorna Effect<XmlVerificationResult, XmlError, XmlRuntime>. Não há requisito Signatures, mas o serviço XmlRuntime é exigido. Forneça exatamente um entre trustedCertificateDer (o certificado X.509 em que você confia) e publicKeyDer (o SubjectPublicKeyInfo em DER) para que verifyXml saiba contra qual chave checar a assinatura.

Quando a aplicação consome um elemento assinado específico, use requiredReference. Seu path vai do elemento do documento até o alvo; cada segmento deve corresponder a exatamente um filho direto, com namespace. Use namespaceUri: null para elementos sem namespace.

Para limitar o trabalho de verificação, o documento pode conter no máximo 4 elementos <Signature>, 4 elementos <Reference> diretos de SignedInfo por assinatura, 8 dessas referências no total, 3 elementos <Transform> diretos por referência e 8 transformações no total. Uma referência pode conter somente uma transformação de canonicalização. Exceder um limite retorna result.valid === false antes da verificação criptográfica.

Somente XML 1.0 é aceito. As proteções do parser rejeitam entrada acima de 10 MiB, 16.384 nós, 2.048 atributos, 64 declarações de namespace ou profundidade 1.024. Valores de PrefixList em InclusiveNamespaces são limitados a 4.096 caracteres e 64 tokens.

Documentos devem conter exatamente um elemento raiz. Fora a declaração XML inicial opcional e espaços em branco XML, conteúdo no prólogo e epílogo é rejeitado.

signXml revalida seu resultado serializado com essas proteções e falha com xml.SIGN_FAILED em vez de retornar XML que verifyXml rejeitaria.

verifyXml nunca confia automaticamente no certificado embutido no KeyInfo da assinatura — isso é uma medida de hardening deliberada contra ataques de signature-wrapping, em que um atacante troca o certificado por outro seu junto de uma assinatura forjada. Omitir uma fonte de confiança ou fornecer as duas falha com xml.INVALID_INPUT.

verify-xml.ts
import { verifyXml } from "@signature-kit/xml/verify"
import { xmlRuntimeLayer } from "@signature-kit/xml/runtime"
import { Effect } from "effect"

// verifyXml -> Effect<XmlVerificationResult, XmlError, XmlRuntime>
// NO Signatures requirement, but XmlRuntime IS required
const result = yield* verifyXml({
  xml: signed,
  trustedCertificateDer,                     // Uint8Array — X.509 DER you trust out-of-band
  requiredReference: {
    uri: "#nfe-1",
    path: [
      { localName: "NFe", namespaceUri: "http://www.portalfiscal.inf.br/nfe" },
      { localName: "infNFe", namespaceUri: "http://www.portalfiscal.inf.br/nfe" },
    ],
  },
}).pipe(Effect.provide(xmlRuntimeLayer))

// XmlVerificationResult
result.valid           // boolean
result.signatureCount  // number — quantos elementos <Signature> foram encontrados
result.referenceUris   // readonly string[] — e.g. ["#nfe-1"]

trustedCertificateDer e publicKeyDer são as duas únicas formas de fornecer uma chave de verificação — passe publicKeyDer quando você já tem o SubjectPublicKeyInfo bruto em vez de um certificado completo.

verify-key.ts
import { verifyXml } from "@signature-kit/xml/verify"
import { xmlRuntimeLayer } from "@signature-kit/xml/runtime"
import { Effect } from "effect"

// Verification with an explicit key (instead of a trusted certificate)
const result = yield* verifyXml({
  xml: signed,
  publicKeyDer,                              // Uint8Array — SubjectPublicKeyInfo DER
  requiredReference: {
    uri: "#nfe-1",
    path: [
      { localName: "NFe", namespaceUri: "http://www.portalfiscal.inf.br/nfe" },
      { localName: "infNFe", namespaceUri: "http://www.portalfiscal.inf.br/nfe" },
    ],
  },
}).pipe(Effect.provide(xmlRuntimeLayer))
// result.valid: boolean

Erros que você pode encontrar

Toda falha de assinatura é um SignatureKitError tipado no canal de erro; falhas de parsing, canonicalização e verificação de XML chegam como XmlError. As mais comuns:

Uma assinatura inválida, um requiredReference cuja URI assinada e caminho semântico não correspondem ou um limite de trabalho de verificação não é um erro do Effect — verifyXml ainda é bem-sucedido e retorna result.valid === false. verifyXml só falha no canal de erro nos casos abaixo.

Como está este guia?

Nesta página