xmlsig

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

xmlsig

Fluent, crypto-agile XML Digital Signature (XML-DSIG) library for Go.

Creates and validates enveloped, enveloping, and detached signatures per W3C REC-xmlsignature-20020212 (DSIG v1.1) with partial DSIG R2 support.

Security Notice

This library has not been officially audited for security vulnerabilities. It should not be used blindly in production: review the code, validate its behavior against your threat model, and test with your own use cases before relying on it for production signing or validation.

The codebase was analyzed with deepsec; all findings from that analysis were reviewed and fixed. This is a tool-assisted analysis, not a security audit — see the notice above.

See Security Model section at the bottom for more details on security assessment.

Quick Start

Signing
package main

import (
    "crypto"
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "crypto/x509/pkix"
    "fmt"
    "math/big"
    "time"

    "github.com/isri-pqc/xmlsig"
    "github.com/isri-pqc/xmlsig/crypto"
    "github.com/beevik/etree"
)

func main() {
    // 1. Generate a key pair (in production, load from HSM or key store)
    sk, _ := rsa.GenerateKey(rand.Reader, 2048)

    template := x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject:      pkix.Name{CommonName: "Signer"},
        NotBefore:    time.Now().Add(-1 * time.Hour),
        NotAfter:     time.Now().AddDate(1, 0, 0),
    }
    certBytes, _ := x509.CreateCertificate(rand.Reader, &template, &template, &sk.PublicKey, sk)

    // 2. Prepare crypto modules
    dm := crypto.NewStdXMLDigestModule()
    signerMod, _ := crypto.NewStdXMLSignatureSignerModule(sk, crypto.SHA256)

    // 3. Load the document to sign
    doc := etree.NewDocument()
    doc.ReadFromString(`<Invoice id="inv-001"><Amount>100.00</Amount></Invoice>`)
    doc.Root().CreateAttr("Id", "inv-001")

    // 4. Build and sign (enveloped signature)
    builder := xmlsig.NewXMLSignatureBuilder(dm).
        WithParent(doc.Root(), []string{xmlsig.SignEntireElement}).
        WithCerts([][]byte{certBytes}).
        WithKeyName("invoice-server")

    sig, err := builder.BuildSignature(signerMod)
    if err != nil {
        panic(err)
    }

    // 5. Serialize
    doc.SetRoot(sig)
    signedXML, _ := doc.WriteToString()
    fmt.Println(signedXML)
}
Validating
// Load the signed document
doc := etree.NewDocument()
doc.ReadFromString(signedXML)

// Prepare validation context
vc := xmlsig.NewDefaultValidationContext(
    [][]byte{certBytes}, // trusted certificates
    crypto.NewStdXMLStandardCryptoVerifierModuleWithNoRevocationCheck(),
    crypto.NewStdXMLDigestModule(),
)

err := vc.Validate(doc.Root())
if err != nil {
    panic(err) // invalid signature
}
fmt.Println("Signature verified successfully")

The element passed to Validate / ValidateSig must carry the signed document's full namespace context. For wrapper-file shapes where the signatures rely on namespace declarations made on the wrapper root (the signatures declare none of their own), pass the wrapper root — a bare <ds:Signature> element cannot reproduce the ancestor-declared context its digests were computed against and cannot be validated on its own.

Signature Shapes

Enveloped

Signature is a child of the signed element. The element signs itself minus the signature node.

builder.WithParent(doc.Root(), []string{xmlsig.SignEntireElement})
Enveloping

Signature wraps the signed data inside <Object> children.

builder.WithObjects([]xmlsig.ObjectEntry{
    {Element: dataElement, IDsToSign: []string{xmlsig.SignEntireElement}},
})
Detached

Signature references external resources by URI or in-memory byte slices.

// External reference with in-memory data
builder.WithExternalRef(xmlsig.ExternalRef{
    URI:  "invoice.xml",
    Data: []byte(`<Invoice>...</Invoice>`),
})

// External reference with URL or file path
builder.WithExternalReferences([]string{
    "https://example.com/document.pdf",
    "/path/to/local/file.xml",
})

ExternalRef supports three optional fields:

  • Type / Id — emitted as @Type / @Id on <ds:Reference> (omitted when empty).
  • Raw: true — digest the raw bytes of Data (identity), without canonicalizing even when the data parses as XML. Raw references emit no <Transforms> child.
Mixed

Combine all three — enveloped parent, enveloped objects, and detached references.

builder.
    WithParent(doc.Root(), []string{xmlsig.SignEntireElement, "child-id"}).
    WithObject(objectElement, []string{xmlsig.SignEntireElement}).
    WithExternalRef(xmlsig.ExternalRef{URI: "ext.xml", Data: payload})
Embedded elements

Embed a caller-built element into the signature and bind it with a reference that carries no <Transforms> child — the shape XAdES uses for xades:QualifyingProperties / xades:SignedProperties.

// QP carries only Target (no Id); the reference targets the nested
// SignedProperties element via RefTargetId.
qp := etree.NewElement("xades:QualifyingProperties")
qp.Space = "xades"
qp.CreateAttr("Target", "#S0")
sp := etree.NewElement("xades:SignedProperties")
sp.Space = "xades"
sp.CreateAttr("Id", "S0-SignedProperties")
// ... fill sp (xades:SigningTime, xades:SigningCertificate, ...) ...
qp.AddChild(sp)

builder.WithEmbeddedElement(xmlsig.EmbeddedElement{
    Element:     qp,
    Type:        "http://uri.etsi.org/01903/v1.3.2#SignedProperties",
    RefId:       "S0-SP",
    InObject:    true, // place inside a bare <ds:Object> (no attributes)
    RefTargetId: "S0-SignedProperties",
})
  • The element is deep-copied into the signature; the caller's tree is not mutated.
  • The reference digest covers the element (or its RefTargetId descendant) at its final in-document position, against the in-scope namespace context — the same code path the validator uses, so the digest is stable across a serialize → re-parse round trip.
  • RefTargetId set: the reference targets the descendant and the embedded element itself needs no Id. Unset: the element must carry the builder's Id attribute.
  • InObject: true places elements in a single shared, attribute-free <ds:Object>; InObject: false places them directly under <ds:Signature>.
Wrapper root / multi-signature

Place one or more detached signatures under a caller-supplied wrapper root (e.g. a BDOC asic:XAdESSignatures file). The root is a container, not a signing target; no reference is created for it.

rootDoc := etree.NewDocument()
rootDoc.ReadFromString(wrapperXML) // root declares the in-scope namespaces

builder.WithRoot(rootDoc.Root())
sig, err := builder.BuildSignature(signerMod) // signature appended under the root
  • Multiple signatures: one builder per signature, each WithRoot(sameRoot); they appear in the order their builders run (each builder is single-use — BuildSignature may be called at most once per builder).
  • When the root's in-scope context already binds the DS prefix, the <ds:Signature> element omits its own xmlns:ds declaration.
  • WithRoot may be combined with WithParent only when the parent is the root (an enveloped signature inside the wrapper). A different parent is rejected fail-fast in BuildSignature.
  • Validation: pass the wrapper root — vc.Validate(root) selects the first matching signature, vc.ValidateSig(root, sigId) selects by Id.
Timestamped signatures (two-phase)

BuildSignature returns the live <ds:Signature> element; the caller owns serialization. For timestamped profiles, imprint the TST over the canonicalized ds:SignatureValue before appending the timestamp:

sig, _ := builder.BuildSignature(signerMod)
sv := sig.FindElement("./ds:SignatureValue")
imprint, _ := builder.CanonicalizeElement(sv) // C14N 1.1, in document context
// ... obtain a TST over `imprint` from your TSA, append it (plus OCSP) to an
// xades:UnsignedProperties element under the signature ...
signedXML, _ := rootDoc.WriteToString() // signed regions are byte-stable

CanonicalizeElement applies the builder's canonicalizer to an element at its current in-document position — the same code path the validator uses, so the imprint recomputes identically after a serialize → re-parse round trip.

API Reference

Signing
Method Purpose
NewXMLSignatureBuilder(dm) Create builder with digest module
WithIdAttribute(name) Custom ID attribute name (default: "Id")
WithPrefix(prefix) DS namespace prefix (default: "ds")
WithSignatureId(id) Explicit <ds:Signature Id="..."> value
WithCanonicalizer(c) C14N algorithm (default: C14N 1.1)
WithDigestMethod(algo) Hash for reference digests (default: SHA-256)
WithParent(elem, ids) Enveloped reference target
WithObject(elem, ids) Enveloping <Object> entry
WithObjects(entries) Multiple enveloping entries
WithExternalRef(ref) Detached in-memory reference
WithExternalReferences(uris) Detached URL/file references
WithCerts(ders) Add X.509 certificates to KeyInfo
WithKeyName(name) Add <ds:KeyName> to KeyInfo
WithKeyInfo(el) Replace entire KeyInfo subtree
WithUnsignedSignatureProperties(props) Add unsigned qualifying properties
WithEmbeddedElement(e) Embed a caller-built element, bound by a no-Transforms reference (XAdES QP→SP)
WithRoot(root) Place the signature under a caller-supplied wrapper root
WithSignedPropertiesId(id) Deterministic Id for the legacy signed-properties wrapper
WithSignedSignatureProperties(props) Deprecated — legacy ds:SignedProperties wrapper, not an XAdES mechanism; use WithEmbeddedElement
CanonicalizeElement(el) Canonicalize an element at its in-document position (TST imprint)
BuildSignature(signerMod) Build + sign; returns the live <ds:Signature> element — the caller serializes
Validation
Method Purpose
NewDefaultValidationContext(certs, vm, dm) Create validation context
WithIdAttribute(name) Custom ID attribute name
ResolveExternalRef(fn) Resolver for detached references
Validate(el) Verify the signature found in el
ValidateSig(el, sigId) Verify the signature with the given Id within el
Constants
Constant Meaning
SignEntireElement Sentinel: sign the entire element (default: "")

Supported Algorithms

Category Algorithms
Canonicalization C14N 1.0, C14N 1.0 (with comments), C14N 1.1, C14N 1.1 (with comments), exclusive C14N 1.0, exclusive C14N 1.0 (with comments)
Signature RSA-SHA256/384/512, ECDSA-SHA256/384/512, ML-DSA-44/65/87 (URIs registered; implementations may need custom modules)
Digest SHA-256, SHA-384, SHA-512, SHA-224
Transforms Enveloped-signature, exclusive C14N (with optional InclusiveNamespaces)

SHA-1 is rejected by default for signature hash algorithms.

Crypto Agility

Algorithms are decoupled from the library behind three small interfaces in the xmlsig/crypto package:

Interface Role
XMLDigestModule Resolves a DigestMethod URI to a hash function
XMLSignatureSignerModule Supplies the SignatureMethod URI and signs the canonicalized SignedInfo
XMLSignatureVerifierModule Validates certificates and verifies signatures, given the URI declared in the doc
Standard modules

The xmlsig/crypto package ships implementations backed by Go's stdlib crypto:

  • NewStdXMLDigestModule() — SHA-1/224/256/384/512
  • NewStdXMLSignatureSignerModule(privKey, hashAlgo) — RSA and ECDSA keys (*rsa.PrivateKey, *ecdsa.PrivateKey); rejects SHA-1
  • NewStdXMLStandardCryptoVerifierModuleWithNoRevocationCheck() — RSA PKCS#1 v1.5 and ECDSA verification; checks certificate validity period but performs no revocation checks
Custom modules

To use an HSM/PKCS#11 backend, Ed25519, or post-quantum algorithms (ML-DSA URIs are already registered in spec but require a custom module), implement the interfaces and pass your module to BuildSignature / NewDefaultValidationContext:

type hsmSignerModule struct {
    handle []byte // HSM key handle
}

func (m *hsmSignerModule) GetXMLSignatureAlgorithmID() (spec.XMLSignatureAlgorithmID, error) {
    return spec.RSASHA256SignatureMethod, nil
}

func (m *hsmSignerModule) SignSignedInfo(signedInfo []byte) ([]byte, error) {
    return hsm.Sign(m.handle, signedInfo) // your HSM call
}

builder := xmlsig.NewXMLSignatureBuilder(dm)
// ... configure references ...
sig, err := builder.BuildSignature(&hsmSignerModule{handle: handle})
Security note

During validation, algorithm URIs come from the signed document — i.e. from untrusted input. The library passes them through to your module unchanged; the module is the only enforcement point for which algorithms are accepted. A correct implementation pins itself to the algorithm(s) it supports and returns an error for every other URI it receives.

Architecture

xmlsig/               ← fluent builder + validation (public API)
├── canonicalizers/   ← C14N algorithms
├── crypto/           ← digest, signer, verifier modules
├── etreeutils/       ← namespace helpers (prefix from russellhaering/goxmldsig)
└── spec/             ← XML-DSIG constants, structs, URI registry

Import sub-packages only when building custom modules. Most code uses only the top-level xmlsig package.

Thread Safety

  • XMLSignatureBuildernot thread-safe. Construct on one goroutine, sign on one.
  • ValidationContext — safe for concurrent reuse.

Dependencies

Dependency Purpose
github.com/beevik/etree XML DOM manipulation
github.com/lafriks/go-xmldsig/v2 Test cross-validation (indirect)
github.com/russellhaering/goxmldsig Test cross-validation (indirect)

Security model

The behavior documented here was verified by executable PoC tests (security_docs_audit_test.go) against the two wrapping-attack references cited at the end of this section.

What the library enforces
  • SHA-1 is rejected for both signing and validation; the standard crypto module pins RSA/ECDSA to SHA-224/256/384/512.
  • The validator accepts only canonicalization transforms plus enveloped-signature; any other transform (XSLT, XPath, ...) is a hard error — fail closed.
  • Unresolvable external references fail validation unless AllowUnresolvedExternalRefs is explicitly set.
  • The certificate used for validation must be byte-identical to one in the trust store; KeyInfo is not covered by the signature value, so swapping the KeyInfo certificate cannot pass validation.
  • The parser (etree / Go stdlib decoder) performs no entity resolution, no external-DTD fetching, and no network I/O while parsing.
  • The enveloped-signature transform removes only the ds-namespace Signature element being validated (matched by Id when present, always namespace-checked).
Position and coverage caveats

XML signature protects element content, not document position. References identify elements by Id, not by where they sit:

  • Whole-wrapper signing — WithParent(root, []string{xmlsig.SignEntireElement}) producing a Reference with URI="" — is the safe default: the digest covers the entire document minus the Signature, which defeats wrapping (moving the signed element into a wrapper element), added-signature, and swap attacks. (Verified by test.)
  • Sub-element (#Id) references alone are position-independent: relocating the signed element under the same namespace bindings leaves the digest unchanged (a re-binding to a different namespace DOES break it). Applications that rely on where an element sits must enforce that the element they process is the one they validated.
  • Validate(el) only selects signatures whose references target el — pass the wrapper root. If no reference targets el and el carries no Id, validation falls back to the last signature found (dormant for whole-wrapper/BDOC documents whose references use URI=""). Use ValidateSig to select a signature explicitly by Id.
  • A signature protects only what its references cover: an enveloping signature over a ds:Object does not cover sibling elements; sibling ordering is not protected; KeyInfo and other ds:Signature children are not bound by the signature value (use WithSignedSignatureProperties if they must be).

The wrapping-attack classes above are the core of the two source documents used as the analysis references for this section: McIntosh & Austel, "XML Signature Element Wrapping Attacks and Countermeasures" (IBM Research RC23691, 2005), https://web.archive.org/web/20160303193057/http://domino.research.ibm.com/library/cyberdig.nsf/papers/73053F26BFE5D1D385257067004CFD80/$File/rc23691.pdf, and Peter Gutmann, "Why XML Security is Broken" (2004), https://www.cs.auckland.ac.nz/~pgut001/pubs/xmlsec.txt.

Acknowledgements

This project is based in large part on goxmldsig by Russell Haering — a pure Go implementation of XML Digital Signatures, licensed under Apache-2.0. Much of the code in this repo derives from that project, and etreeutils is a fork of its namespace/tree helpers.

Security and correctness findings were identified with deepsec.

License

Apache-2.0 — see LICENSE and NOTICE.

Documentation

Overview

Package xmlsig provides a fluent builder API for creating and validating XML Digital Signatures (XML-DSIG) per the W3C specification.

Overview

XML-DSIG attaches cryptographic signatures to XML documents. This library supports three signature shapes:

  • Enveloped: the Signature is a child of the signed element. (The element signs itself minus the Signature node.)
  • Enveloping: the Signature wraps the signed data inside <Object> children.
  • Detached: the Signature references external resources by URI.

Getting Started — Signing

// Prepare crypto materials
dm := crypto.NewStdXMLDigestModule()
signerMod, err := crypto.NewStdXMLSignatureSignerModule(privKey, crypto.SHA256)
if err != nil {
	log.Fatal(err)
}

// Build the signature structure
b := xmlsig.NewXMLSignatureBuilder(dm).
	WithParent(doc.Root(), []string{xmlsig.SignEntireElement}).
	WithCerts([][]byte{certDER}).
	WithKeyName("invoice-server")

// Produce the signed XML
signedEl, err := b.BuildSignature(signerMod)
if err != nil {
	log.Fatal(err)
}

Getting Started — Validation

// Prepare a trust store and validation modules
certPool := []*x509.Certificate{trustedCert}
vcm := crypto.NewStdXMLStandardCryptoVerifierModuleWithNoRevocationCheck()
dm := crypto.NewStdXMLDigestModule()
vc := xmlsig.NewDefaultValidationContext(certPool, vcm, dm)

// Load and validate
doc := etree.NewDocument()
doc.ReadFromFile("signed.xml")
err = vc.Validate(doc.Root())
if err != nil {
	log.Fatalf("validation failed: %v", err)
}
fmt.Println("signature verified successfully")

Key Concepts

  • Element: any etree.Node representing an XML element (<Person>, <Invoice>, etc.). Elements are identified by their "Id" attribute (W3C ID AS). By default the library looks for the "Id" attribute; configure with WithIdAttribute.

  • SignedInfo: the canonicalized component containing the digest algorithms, reference URIs, transform chain, and the signature algorithm. This is what the cryptographic signature protects.

  • Object: an arbitrary container inside <Signature> that holds embedded data. Useful for enveloping signatures where the signed content travels WITH the signature. Each Object can reference zero or more child elements to sign.

  • KeyInfo: metadata describing the public key or certificate used for verification. Typically carries X509 certificates or a key name.

  • External Reference: a reference to data outside the signed document (by URI or in-memory byte slice). Used for detached signatures.

Architecture

The package splits concerns into four sub-packages:

  • canonicalizers — C14N algorithms (1.0, 1.1, exclusive, with/without comments)
  • crypto — digest functions, RSA/ECDSA signers, X509 verifiers
  • spec — XML-DSIG constants, URI registry, XML schema structs
  • etreeutils — namespace helpers forked from russellhaering/goxmldsig

Import these sub-packages only when you need fine-grained control (custom canonicalizers, alternate hash functions, etc.). Most applications interact exclusively with the top-level xmlsig types.

Thread Safety

XMLSignatureBuilder is NOT thread-safe. Construct on one goroutine, sign on another — but never share a builder instance across goroutines concurrently.

ValidationContext is designed for reuse across goroutines after construction.

Specification Support

Primary: W3C REC-xmlsignature-20020212 (DSIG v1.1) Partial: W3C REC-xmlsignature-v2.0-20130401 (DSIG R2)

Index

Constants

View Source
const SignEntireElement = "#_XMLODSIG_SIGN_ENTIRE_"

SignEntireElement is a sentinel value for IDsToSign passed to WithParent and WithObject, indicating that the entire parent/Object element should be signed as an enveloped signature.

Unlike the deprecated empty-string literal (""), this named constant cannot accidentally collide with a real element's Id attribute. The leading '#' is illegal in XML NCNames, making collisions practically impossible. Both values are accepted for backward compatibility.

Variables

View Source
var (
	// ErrMissingSignature indicates that no enveloped signature was found referencing
	// the top level element passed for signature verification.
	ErrMissingSignature = errors.New("missing signature referencing the top-level element")
	// ErrInvalidSignature indicates the Signature element does not conform to the
	// expected XML-DSIG shape.
	ErrInvalidSignature = errors.New("invalid Signature")
	// ErrParentElementNotFound indicates that a parent element identified by ID
	// could not be located during signature construction.
	ErrParentElementNotFound = errors.New("xmldsig: parent element not found")
	// ErrObjectElementNotFound indicates that an element referenced by an Object
	// could not be located during signature construction.
	ErrObjectElementNotFound = errors.New("xmldsig: object element not found")
	// ErrUnknownTransform indicates a transform algorithm URI is not recognised.
	ErrUnknownTransform = errors.New("xmldsig: unknown transform algorithm")
	// ErrReferencedElementNotFound indicates that a URI-based reference could not
	// be resolved to an element during validation.
	ErrReferencedElementNotFound = errors.New("xmldsig: referenced element not found")
	// ErrUnresolvedExternalRef indicates that an external-reference URI could not
	// be resolved and its content was never verified.
	ErrUnresolvedExternalRef = errors.New("xmldsig: external reference could not be resolved and was not verified")
)
View Source
var ErrMalformedSignatureElement = errors.New("malformed Signature element")

In most places, we use etree Elements, but while deserializing the Signature, we use encoding/xml unmarshal directly to convert to a convenient go struct. This presents a problem in some cases because when an xml element repeats under the parent, the last element will win and/or be appended. We need to assert that the Signature object matches the expected shape of a Signature object. ErrMalformedSignatureElement signals that a candidate Signature element violates the XML-DSIG structural invariant: its required children either appear with the wrong multiplicity or belong to the wrong namespace.

Functions

This section is empty.

Types

type EmbeddedElement added in v0.3.0

type EmbeddedElement struct {
	Element     *etree.Element
	Type        string // optional @Type on the emitted ds:Reference
	RefId       string // optional @Id on the emitted ds:Reference
	InObject    bool   // false => under ds:Signature; true => inside ds:Object
	RefTargetId string // optional: when non-empty, the emitted

}

EmbeddedElement couples a caller-built element with the ds:Reference that binds it into SignedInfo. This is the XML-DSig pattern for signed embedded data: the element is placed in the document and referenced by URI="#<element Id>" with a no-Transforms reference — its digest is the element's canonical form in its final namespace context (C14N 1.1 document subsets), per the document's CanonicalizationMethod.

Element must carry an Id attribute (the builder's IdAttribute); the Id is the reference target and is caller-controlled (deterministic). The element must carry or inherit all namespace declarations it uses at its final position (e.g. a wrapper-root document declaring them on its root).

InObject=false places the element directly under ds:Signature (last child); InObject=true places it inside a shared, attribute-free ds:Object (BDOC shape: the Object is a pure container — no Id, no attributes — and is not itself referenced).

type ExternalRef

type ExternalRef struct {
	URI  string
	Data []byte
	Type string
	Id   string

	// Raw digests the payload as raw octets: the ds:Reference carries NO
	// Transforms element and the payload is never parsed or canonicalized,
	// even when it is well-formed XML (XML-DSig §4.3.3.2 / §2.1.1: with no
	// Transforms the resource's content is digested directly).
	Raw bool
}

ExternalRef represents a detached (standalone) reference whose payload is provided in-memory rather than fetched from a URI.

When Data is non-empty, the digest is computed directly from Data and no I/O (file read / HTTP fetch) is attempted. The URI attribute is set to the given URI (empty string is valid per XML-DSIG).

type ObjectEntry

type ObjectEntry struct {
	Element   *etree.Element
	IDsToSign []string
}

ObjectEntry groups an XML element with the subset of its children to sign.

When appended to a signature via WithObject, the Element becomes the content of a <ds:Object> child of <ds:Signature>. The IDsToSign slice controls which descendants (or the object itself) appear as <Reference> elements inside SignedInfo:

  • Use SignEntireElement to sign the entire <ds:Object> as-is.
  • Provide concrete element IDs (e.g., []string{"item1", "item2"}) to sign specific children.
  • Avoid passing an empty string (""); it is kept for backward compatibility but treated identically to SignEntireElement.

type ResolveFn

type ResolveFn func(uri string) ([]byte, error)

ResolveFn resolves an external reference URI to its raw byte payload. Return nil when the URI is unavailable; the caller decides whether that constitutes a validation error.

type ValidationContext

type ValidationContext struct {
	TrustedCertificates [][]byte
	IdAttribute         string
	// AllowUnresolvedExternalRefs controls handling of external-reference URIs that
	// cannot be resolved (no resolver configured, or the resolver returns nil).
	// When false (default) validation fails with ErrUnresolvedExternalRef: the
	// referenced content was never verified. When true such references are skipped.
	AllowUnresolvedExternalRefs bool
	ResolveExternalRef          ResolveFn // optional; unresolved externals fail unless AllowUnresolvedExternalRefs is set
	// contains filtered or unexported fields
}

func NewDefaultValidationContext

func NewDefaultValidationContext(trustedCerts [][]byte, vm c.XMLSignatureVerifierModule, dm c.XMLDigestModule) *ValidationContext

func (*ValidationContext) Validate

func (ctx *ValidationContext) Validate(el *etree.Element) error

Validate validates the signature found in el.

el must provide the signed document's full namespace context. For documents whose signatures rely on namespace declarations made on ancestor elements (BDOC shape: the wrapper root declares the namespaces and the ds:Signature elements carry none of their own), pass the wrapper root — a bare ds:Signature element cannot reproduce the ancestor-declared context that the signed digests were computed against.

In a multi-signature wrapper, Validate selects the first signature whose references target the passed element. If no reference matches, the last signature found is used — but only when the passed element has no Id (it cannot be targeted by a reference URI) or is itself that signature; use ValidateSig to select a specific signature by its Id.

func (*ValidationContext) ValidateSig added in v0.3.0

func (ctx *ValidationContext) ValidateSig(el *etree.Element, sigId string) error

ValidateSig validates the ds:Signature element whose Id attribute equals sigId, found within el. el must be the namespace-complete context of the signed document (e.g. the BDOC wrapper root) — see Validate.

type XMLSignatureBuilder

type XMLSignatureBuilder struct {
	IdAttribute string
	Prefix      string
	// contains filtered or unexported fields
}

XMLSignatureBuilder constructs a complete XML Digital Signature (<ds:Signature>) ready for serialization and cryptographic signing.

Fluent API

Configure the builder by chaining method calls, then invoke Sign to produce the signed XML. Every setter returns *XMLSignatureBuilder for chaining.

Typical Construction Flow

  1. Create a builder: NewXMLSignatureBuilder(dm)
  2. (Optional) Customize settings: WithIdAttribute, WithPrefix, WithCanonicalizer, WithDigestMethod
  3. Attach the element to sign: WithParent(parentElem, idsToSign)
  4. (Optional) Embed extra data: WithObject(childElem, idsToSign)
  5. (Optional) Attach identity info: WithCerts(certDERs), WithKeyName(name)
  6. (Optional) Add detached refs: WithExternalReferences(urls), WithExternalRef(extRef)
  7. (Optional) Append UNSIGNED qualifying sig properties: WithUnsignedSignatureProperties(...) 7b. (Optional) Append SIGNED qualifying sig properties (QSCD/EIDAS): WithSignedSignatureProperties(...)
  8. Serialize + sign: BuildSignature()

Method Interaction Rules

  • WithParent must be called before Build. Its argument determines what element serves as the root of the signature graph.
  • WithCerts and WithKeyName both initialize b.keyInfo lazily. Calling either one first ensures the KeyInfo subtree is created before the next call appends to it. Order between these two does not matter.
  • Objects accumulate: repeated WithObject/WithObjects calls append; they do not replace prior entries.
  • External references are processed in the order they are registered.

Default Configuration

Unless overridden, the builder uses:

  • ID attribute: "Id" (W3C-spec compliant; overrides legacy "ID")
  • Prefix: "ds"
  • Canonicalizer: Exclusive C14N 1.1
  • Digest algorithm: SHA-256

Errors

Building a signature never returns an error directly. Errors surface during the subsequent Sign() call as structured errors wrapping package-level sentinel values (e.g., ErrParentElementNotFound). Inspect with errors.Is().

A builder is single-use: BuildSignature mutates its state in place and may be called at most once — create a new builder for each signature.

func NewXMLSignatureBuilder

func NewXMLSignatureBuilder(dm crypto.XMLDigestModule) *XMLSignatureBuilder

NewXMLSignatureBuilder initializes a builder with sensible defaults.

Parameters:

  • dm: the digest module providing hash-function lookup and computation. Obtain via crypto.StandardDigestModule{}.

Defaults:

IdAttribute:   "Id"         (W3C XML-DSIG spec; camelCase)
Prefix:        "ds"         (standard XML-DSIG namespace prefix)
Canonicalizer: C14N 1.1    (exclusive, most efficient for modern parsers)
DigestMethod:  SHA-256     (secure default; upgrade to SHA-384/512 for HSM-grade)

The caller retains responsibility for supplying a private key and certificate chain to the Sign() method. This constructor only sets up the XML structure.

func (*XMLSignatureBuilder) BuildSignature

func (b *XMLSignatureBuilder) BuildSignature(signerModule crypto.XMLSignatureSignerModule) (*etree.Element, error)

Returns the signature element. If parent was set, it will have the signature appended to it in memory (when WithRoot is also set, the parent must be the root itself).

The returned element is the signature node of the live tree (attached to the parent/root when configured); the caller owns serialization (XML declaration, indentation, base64 wrapping) — the library bakes no formatting into the document.

func (*XMLSignatureBuilder) CanonicalizeElement added in v0.3.0

func (b *XMLSignatureBuilder) CanonicalizeElement(el *etree.Element) ([]byte, error)

CanonicalizeElement canonicalizes an element of the current in-memory tree (as built by BuildSignature — e.g. a ds:SignatureValue element) at its in-tree location, using the builder's canonicalizer. This is the primitive for two-phase signature timestamping: after signing, canonicalize the target element, use the returned bytes as the TST imprint (hash them for the timestamp request), append the (unsigned) timestamp element to the tree, and re-serialize. Appending unsigned elements does not change the canonical form of the signed regions, so the signature remains valid after re-serialization.

el must be attached to a tree; a detached element is canonicalized with the default namespace context (its own declarations only).

func (*XMLSignatureBuilder) WithCanonicalizer

func (b *XMLSignatureBuilder) WithCanonicalizer(canonicalizer canonicalizers.Canonicalizer) *XMLSignatureBuilder

WithCanonicalizer selects the canonicalization algorithm applied to SignedInfo and to each reference before hashing.

Available choices (from the canonicalizers package):

MakeC14N11Canonicalizer()           - Exclusive C14N 1.1 (recommended, fastest)
MakeC14N11CommentCanonicalizer()    - C14N 1.1 retaining comments
MakeExcC14NCanonicalizer()          - Exclusive C14N 1.0 (legacy)
MakeExcC14NCommentCanonicalizer()   - C14N 1.0 retaining comments
MakeCanonCanonicalizer()            - Literal C14N 1.0 (broader compat, slower)
MakeCanonCommentCanonicalizer()     - C14N 1.0 retaining comments

The canonicalizer chosen here also applies to external references parsed as XML. For non-XML external resources, raw bytes are hashed directly.

func (*XMLSignatureBuilder) WithCerts

func (b *XMLSignatureBuilder) WithCerts(certsDer [][]byte)

WithCerts populates the KeyInfo with X509 certificates.

Each DER-encoded certificate is base64-encoded and placed inside a <ds:X509Certificate> element within <ds:X509Data>.

If no KeyInfo exists yet, this method creates one. Subsequent calls to WithCerts append to the existing X509Data (do not replace it). Combine with WithKeyName for a complete KeyInfo.

func (*XMLSignatureBuilder) WithDigestMethod

func (b *XMLSignatureBuilder) WithDigestMethod(digestMethod spec.XMLDigestAlgorithmID) *XMLSignatureBuilder

WithDigestMethod selects the hash algorithm for computing reference digests.

Accepted values (from spec package constants):

SHA256DigestAlgorithmId  - SHA-256 (recommended minimum)
SHA384DigestAlgorithmId  - SHA-384 (stronger)
SHA512DigestAlgorithmId  - SHA-512 (strongest widely supported)
SHA1DigestAlgorithmId    - SHA-1 (deprecated; always rejected — GetDigestFunc fails with ErrUnsupportedDigestAlgorithm)

The digest algorithm must match what the crypto.SignerModule expects.

func (*XMLSignatureBuilder) WithEmbeddedElement added in v0.3.0

func (b *XMLSignatureBuilder) WithEmbeddedElement(e EmbeddedElement) *XMLSignatureBuilder

WithEmbeddedElement registers a caller-built element to embed and cryptographically bind into SignedInfo.

The builder deep-COPIES the element into the signature (the caller's tree is not mutated — the same copy semantics as WithObject and WithSignedSignatureProperties). Multiple embedded elements are placed and bound in registration order. The builder is single-use with respect to embedded elements: BuildSignature consumes them (even on a late failure), so a reused builder will not re-sign them. See EmbeddedElement for the placement rules (InObject) and the Id / namespace requirements on Element.

When RefTargetId is set, the emitted reference instead targets a DESCENDANT of the element carrying that Id and the digest is computed over that descendant at its final in-document position. This is the standard XAdES 1.3.2 QP → SP pattern: xades:QualifyingProperties (Target only, no Id) inside ds:Object, referenced via the nested xades:SignedProperties' Id.

func (*XMLSignatureBuilder) WithExternalRef

func (b *XMLSignatureBuilder) WithExternalRef(r ExternalRef) *XMLSignatureBuilder

WithExternalRef registers a single detached reference backed by in-memory data — no network I/O occurs.

Parameters

  • r.URI: the reference URI (used in <Reference URI="...">). May be empty to refer to the root element, or a relative/local path for clarity.
  • r.Data: the raw payload bytes. If parseable as XML, the builder's canonicalizer processes the data before hashing. Otherwise, raw bytes are hashed directly (identity canonicalization).

Difference from WithExternalReferences

  • WithExternalRef: eager, in-memory, zero I/O. Ideal for testing or when the payload is already available locally.
  • WithExternalReferences: lazy, network-fetched. Ideal for remote resources.

Example

receiptJSON := []byte(`{"amount": 42.00}`)
b.WithExternalRef(xmlsig.ExternalRef{
    URI:  "/receipt",
    Data: receiptJSON,
})

func (*XMLSignatureBuilder) WithExternalReferences

func (b *XMLSignatureBuilder) WithExternalReferences(uris []string) *XMLSignatureBuilder

WithExternalReferences registers detached-reference URIs whose payloads are fetched at sign-time.

How It Works

For each URI:

  1. At sign time, the library performs an HTTP GET to retrieve the resource.
  2. If the response parses as well-formed XML, the canonicalizer (chosen via WithCanonicalizer) processes it before hashing.
  3. If the response is NOT valid XML (binary, JSON, etc.), the raw bytes are hashed directly.

Limitations

  • HTTP requests use a 10-second timeout with no configurability.
  • Redirects are followed (etree's transport uses net/http defaults).
  • Authentication headers, TLS client certs, and proxy configuration are not supported. For advanced scenarios, use WithExternalRef with manually fetched data.

Example

b.WithExternalReferences([]string{
    "https://example.com/invoice.pdf",
    "https://example.com/receipt.json",
})

func (*XMLSignatureBuilder) WithIdAttribute

func (b *XMLSignatureBuilder) WithIdAttribute(idAttribute string) *XMLSignatureBuilder

WithIdAttribute sets the name of the attribute used to identify XML elements for signing and referencing.

Per W3C XML-DSIG, the recommended attribute is "Id" (camelCase). Override only when interacting with legacy systems that use "ID" (uppercase) or a custom name.

Default: "Id"

func (*XMLSignatureBuilder) WithKeyInfo

func (b *XMLSignatureBuilder) WithKeyInfo(keyInfo *etree.Element) *XMLSignatureBuilder

WithKeyInfo replaces the entire KeyInfo subtree with a caller-supplied element.

Use this when you need full control over KeyInfo content (e.g., embedding RSAKeyValue, RetrievalMethod, or proprietary extensions). If called after WithCerts or WithKeyName, those previously-added elements are discarded.

For simple cases, prefer WithCerts and/or WithKeyName which build a standard X509Data/KeyName structure automatically.

func (*XMLSignatureBuilder) WithKeyName

func (b *XMLSignatureBuilder) WithKeyName(name string) *XMLSignatureBuilder

WithKeyName adds a <KeyName> element to the KeyInfo for identifying the key.

The key name is an opaque string chosen by the signer. Validators compare it against local policy (e.g., "match the key named 'invoice-server'").

If no KeyInfo exists yet, this method creates one. Combining with WithCerts produces a standard KeyInfo carrying both identity (KeyName) and material (X509Data).

func (*XMLSignatureBuilder) WithObject

func (b *XMLSignatureBuilder) WithObject(element *etree.Element, IDsToSign []string) *XMLSignatureBuilder

WithObject embeds an XML element inside the signature as a <ds:Object> child and optionally references its descendants for signing.

Purpose

An Object lets you carry additional data alongside the signature — for example, embedding the signed invoice body within an enveloping signature so the recipient receives everything in one blob.

How It Works

  1. The supplied element is deep-copied into a newly-created <ds:Object>.
  2. If IDsToSign contains SignEntireElement, the Object itself gets an Id attribute and a corresponding <Reference> pointing to it.
  3. If IDsToSign lists concrete element IDs, each is turned into a <Reference URI="#<id>"> inside SignedInfo.
  4. The Object receives an auto-generated Id ("objN") for internal tracking.

Parameters

  • element: the XML element to embed. Must belong to a known namespace.
  • IDsToSign: which descendants (or self) to include in SignedInfo.<References>. Use SignEntireElement to sign the whole Object, or a list of concrete element IDs for granular references.

Example

// Embed an <Item> element and sign both the Item and its <Price> child
body := doc.SelectElements("//ns:Body", nsMap)[0]
item := body.FindElement("ns:Item")
item.CreateAttr("Id", "item-ref")
price := item.FindElement("ns:Price")
price.CreateAttr("Id", "price-val")
b.WithObject(item, []string{
    xmlsig.SignEntireElement,
    "price-val",
})

Notes

  • Calling WithObject multiple times appends multiple <ds:Object> elements.
  • Objects are independent — references inside one Object do not reach into another Object or into the parent element.
  • Deprecated: passing an empty string ("") in IDsToSign is equivalent to SignEntireElement but will trigger a lint warning in strict mode.

func (*XMLSignatureBuilder) WithObjects

func (b *XMLSignatureBuilder) WithObjects(objects []ObjectEntry) *XMLSignatureBuilder

WithObjects appends multiple ObjectEntries in a single call. Equivalent to calling WithObject repeatedly, but avoids per-call overhead.

See WithObject for the semantics of ObjectEntry, IDsToSign, and SignEntireElement.

func (*XMLSignatureBuilder) WithParent

func (b *XMLSignatureBuilder) WithParent(parent *etree.Element, IDsToSign []string) *XMLSignatureBuilder

WithParent declares the root element to sign and which of its descendants to reference.

Purpose

The parent element is the anchor of the signature graph. For enveloped signatures, the Signature becomes a child of this element. For detached signatures, this element serves as the implicit reference target when IDsToSign is empty.

How IDsToSign Works

  • SignEntireElement: sign the entire parent element as an enveloped signature (adds enveloped-signature transform automatically).
  • Concrete IDs like []string{"child1", "child2"}: create individual <Reference URI="#child1"> entries targeting descendant elements.
  • Avoid empty string (""): deprecated, behaves like SignEntireElement.

Example

// Sign the <Invoice> element and its <LineItem> children
invoice := doc.FindElement("Invoice")
invoice.CreateAttr("Id", "inv-root")
for i, item := range doc.FindElements("//LineItem") {
    item.CreateAttr("Id", fmt.Sprintf("line-%d", i))
}
b.WithParent(invoice, []string{
    xmlsig.SignEntireElement,
    "line-0", "line-1", "line-2",
})

func (*XMLSignatureBuilder) WithPrefix

func (b *XMLSignatureBuilder) WithPrefix(prefix string) *XMLSignatureBuilder

WithPrefix sets the namespace prefix used for XML-DSIG elements.

The conventional prefix is "ds" mapping to http://www.w3.org/2000/09/xmldsig#. Changing this alters the serialized output and may break interoperability with off-the-shelf validators.

Default: "ds"

func (*XMLSignatureBuilder) WithRoot added in v0.3.0

WithRoot places the constructed ds:Signature under the caller-supplied wrapper root element (e.g. a BDOC asic:XAdESSignatures root). The root is a container, NOT a signing target — no reference is created for it. The signature is appended to the root at sign time, so reference digests (embedded elements, in-document contexts) reflect the final document context.

The ds:Signature element omits its own xmlns:<prefix> declaration when the prefix is already bound to the XML-DSig namespace in the in-scope namespace context at the root's position; otherwise the declaration is emitted.

Multiple detached signatures under one root: create one builder per signature, each WithRoot(sameRoot); signatures appear in the order their builders are run. WithRoot may be combined with WithParent only when the parent IS the root itself (an enveloped signature inside the wrapper root). When both are set and the parent differs from the root, BuildSignature fails fast — the enveloped digest targets the parent while the signature lives under the root, which would yield a document no validator can verify.

Validation: pass the wrapper root to ValidationContext.Validate (selects the first matching signature) or ValidationContext.ValidateSig(root, sigId) for a specific signature — the root provides the namespace context the signed digests were computed against.

func (*XMLSignatureBuilder) WithSignatureId

func (b *XMLSignatureBuilder) WithSignatureId(signatureId string) *XMLSignatureBuilder

WithSignatureId sets an explicit Id attribute on the outermost <ds:Signature> element.

Leave blank (default) to let the library assign a generated UUID-like value.

Setting a custom ID enables other signatures or policies to reference this specific signature by URI fragment (e.g., #mysig).

func (*XMLSignatureBuilder) WithSignedPropertiesId added in v0.3.0

func (b *XMLSignatureBuilder) WithSignedPropertiesId(id string) *XMLSignatureBuilder

WithSignedPropertiesId sets a deterministic Id for the <ds:SignedProperties> wrapper created by WithSignedSignatureProperties (default: wall-clock "sp-<UnixNano>"). The wrapper Id and its SignedInfo reference URI are set from this value, so combined with WithSignatureId the legacy mechanism's output carries no wall-clock Ids.

func (*XMLSignatureBuilder) WithSignedSignatureProperties deprecated

func (b *XMLSignatureBuilder) WithSignedSignatureProperties(
	props ...*etree.Element,
) *XMLSignatureBuilder

WithSignedSignatureProperties attaches qualifying-signature metadata that IS cryptographically bound to the signature via a <Reference> in <SignedInfo>.

This method:

  1. Wraps the supplied <ds:SignatureProperty> elements in a <ds:SignedProperties> wrapper element in the ds: (XML-DSig) namespace, placed directly under <ds:Signature>, with a wall-clock auto-generated Id ("sp-<UnixNano>").
  2. Sets the Target attribute on each <SignatureProperty> to point to the <ds:Signature> element (using its Id).
  3. Injects a <Reference URI="#<signedPropsId">> into <SignedInfo> that DECLARES the builder's canonicalization transform.

The resulting structure:

<ds:Signature Id="sig-1">
  ...
  <ds:SignedProperties Id="sp-1">
    <ds:SignatureProperty Target="#sig-1">
      <etsi:SigningTime>2025-01-15T10:30:00Z</etsi:SigningTime>
    </ds:SignatureProperty>
  </ds:SignedProperties>
</ds:Signature>

This emitted wrapper is NOT an XAdES element — XAdES SignedProperties belongs to the ETSI xades: namespace and is built by the caller — and it is not part of the base XML-DSig schema. Do not treat this method as an XAdES/BDOC conformance mechanism.

Typical Usage

// Create a SigningTime claim
cpDoc := etree.NewDocument()
cpDoc.ReadFromString(`<ds:SignatureProperty xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#">
  <etsi:SigningTime>2025-01-15T10:30:00Z</etsi:SigningTime>

</ds:SignatureProperty>`)

b.WithSignedSignatureProperties(cpDoc.Root())

Warning

Each supplied element must be a <ds:SignatureProperty>. The builder wraps them in <ds:SignedProperties>; do NOT pass a <ds:SignedProperties> element itself. Namespace prefixes on the <SignatureProperty> children are preserved as-is.

Deprecated: use WithEmbeddedElement with a caller-built element in the appropriate profile namespace (e.g. xades:SignedProperties) for new code. Together with WithSignatureId and WithSignedPropertiesId the output is fully deterministic (no wall-clock Ids).

func (*XMLSignatureBuilder) WithUnsignedSignatureProperties

func (b *XMLSignatureBuilder) WithUnsignedSignatureProperties(
	props ...*etree.Element,
) *XMLSignatureBuilder

WithUnsignedSignatureProperties attaches unqualified metadata elements directly beneath <ds:Signature>.

Per XML-DSIG §5.5, <SignatureProperties> conveys supplemental information such as timestamps, policy identifiers, or countersignatures. This method inserts whatever elements you pass directly — the caller is responsible for wrapping them in <ds:SignatureProperties> if desired.

These elements are NOT included in <SignedInfo>/<References> and therefore are NOT cryptographically protected by the signature.

Typical Usage

// Unqualified timestamp — visible but not signed
tp := etree.NewElement("ds:SignatureProperties")
tp.Space = "ds"
tp.CreateAttr("Id", "ts-unqual")
tp.AddChild(etree.NewElement("ds:SignatureProperty").SetText("2025-01-15T10:30:00Z"))
b.WithUnsignedSignatureProperties(tp)

Warning

The caller is responsible for ensuring correct namespace prefixes and element names. Incorrectly formed elements will produce signatures that fail schema validation.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL