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> — 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/engine"
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"
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 option is "rsa-sha512". The signer's X.509 certificate is embedded in the signature's KeyInfo, so the verifier needs no key out of band — unless you want to pin it, which the last step below covers.
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>. There is no Signatures requirement: verification uses the certificate embedded in KeyInfo. Use requireReferenceUri to require a specific URI among the signed references.
import { verifyXml } from "@signature-kit/xml/verify"
import { xmlRuntimeLayer } from "@signature-kit/xml/engine"
import { Effect } from "effect"
// verifyXml -> Effect<XmlVerificationResult, XmlError, XmlRuntime>
// NO Signatures requirement: runs outside the signer boundary
const result = yield* verifyXml({
xml: signed,
requireReferenceUri: "#nfe-1", // fails if the URI does not exist
}).pipe(Effect.provide(xmlRuntimeLayer))
// XmlVerificationResult
result.valid // boolean
result.signatureCount // number — how many <Signature> elements were verified
result.referenceUris // readonly string[] — e.g. ["#nfe-1"]By default the public key comes from the embedded certificate. To pin the expected key — ignoring KeyInfo — pass publicKeyDer (the SubjectPublicKeyInfo in DER). Useful when you already trust a specific key and won't accept the one in the document.
// Verification with an explicit key (instead of the cert embedded in KeyInfo)
const result = yield* verifyXml({
xml: signed,
publicKeyDer, // Uint8Array — SubjectPublicKeyInfo DER
requireReferenceUri: "#nfe-1",
})
// result.valid: booleanErrors you might see
Every signing failure is a typed SignatureKitError on the error channel; XML parsing or canonicalization failures arrive as XmlError. The most common: