XML-DSig
Sign XML with signXml and validate with verifyXml: enveloped reference, X.509 in KeyInfo, verification needs no signer.
What you'll do: sign an XML string with signXml over the Signatures boundary, then validate with verifyXml — which needs no signer and runs anywhere.
The XML format lives in @signature-kit/xml and consumes the Signatures service provided by a signer.
npm install @signature-kit/xmlsignXml takes an XmlSigningRequest and returns Effect<string, XmlError | SignatureKitError, Signatures | XmlRuntime> — the signed XML as a string. Satisfy the Signatures requirement with Effect.provide(a1SignaturesLayer(...)): A1 provides the signing power, the format module only mutates the document.
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 has no Signatures requirement — only signXml needs the signer boundary.
referenceId sets the target of the enveloped signature: passing referenceId: "nfe-1" produces a Reference with URI "#nfe-1", pointing to the element with the same Id in the document. The remaining XmlSigningRequest fields are optional:
// 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))Without algorithm, the default "rsa-sha256" applies; the other options are "rsa-sha512" and the legacy, discouraged "rsa-sha1". The signer's X.509 certificate is embedded in the signature's KeyInfo, but the verifier does not trust it automatically — the last two steps below cover supplying the trusted key or certificate out of band.
The NF-e is a common case: the Id of infNFe is the access key, and the signature points to it. Use the access key as referenceId so the URI becomes "#NFe35...".
// 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..."The output string is the XML ready to send. Since signXml returns string, drop it straight
into the SEFAZ payload.
verifyXml takes an XmlVerificationRequest and returns Effect<XmlVerificationResult, XmlError, XmlRuntime>. There is no Signatures requirement, but the XmlRuntime service is required. Supply exactly one of trustedCertificateDer (the X.509 certificate DER you trust) or publicKeyDer (the SubjectPublicKeyInfo DER) so verifyXml knows which key to check the signature against.
When an application consumes a specific signed element, use requiredReference. Its path runs from the document element through the target; each segment must match exactly one namespace-aware direct child. Use namespaceUri: null for elements without a namespace.
To bound verification work, documents may contain at most 4 <Signature> elements, 4 direct SignedInfo <Reference> elements per signature, 8 of those references in total, 3 direct <Transform> elements per reference, and 8 transforms in total. A reference may contain only one canonicalization transform. Exceeding a limit returns result.valid === false before cryptographic verification.
Only XML 1.0 is accepted. Parser guardrails reject input above 10 MiB, 16,384 nodes, 2,048 attributes, 64 namespace declarations, or depth 1,024. InclusiveNamespaces PrefixList values are capped at 4,096 characters and 64 tokens.
Documents must contain exactly one root element. Other than the optional leading XML declaration and XML whitespace, prolog and epilog content is rejected.
signXml revalidates its serialized result with these guards and fails with xml.SIGN_FAILED rather than returning XML that verifyXml would reject.
verifyXml never trusts the certificate embedded in the signature's KeyInfo automatically — that's a deliberate hardening measure against signature-wrapping attacks, where an attacker swaps in their own certificate alongside a forged signature. Omitting a trust source or supplying both sources fails with xml.INVALID_INPUT.
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 — how many <Signature> elements were found
result.referenceUris // readonly string[] — e.g. ["#nfe-1"]trustedCertificateDer and publicKeyDer are the only two ways to supply a verification key — pass publicKeyDer instead when you already hold the raw SubjectPublicKeyInfo rather than a full certificate.
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: booleanErrors you might see
Every signing failure is a typed SignatureKitError on the error channel; XML parsing, canonicalization, and verification failures arrive as XmlError. The most common:
xml.INVALID_INPUT
xml.SIGN_FAILED
An invalid signature, a requiredReference whose signed URI and semantic path do not match, or a verification work limit is not an Effect error — verifyXml still succeeds and returns result.valid === false. verifyXml only fails on the error channel for the cases below.
How is this guide?