Scoped production support — The same-document XMLDSig 1.1 verification profile is supported when the application supplies an explicit trusted key or certificate source and checks which element the verified signature covers. External references and XSLT are opt-in advanced features with caller-owned transport, resource, and execution policy.
The xmldsig1 package implements W3C XML Digital Signatures 1.1 for helium documents.
Import path: github.com/lestrrat-go/helium/xmldsig1
package examples_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"strings"
"github.com/lestrrat-go/helium"
"github.com/lestrrat-go/helium/xmldsig1"
)
func Example_xmldsig1_sign_verify() {
// Parse an XML document to sign. In SAML, this is typically an
// Assertion or Response element.
const src = `<root Id="doc1"><data>Hello, World!</data></root>`
doc, err := helium.NewParser().Parse(context.Background(), []byte(src))
if err != nil {
fmt.Printf("parse error: %s\n", err)
return
}
// Generate an RSA key pair. In production, load your private key
// from a PEM file or key store.
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
fmt.Printf("keygen error: %s\n", err)
return
}
// Create a Signer configured for the most common SAML pattern:
// RSA-SHA256 signature, enveloped signature transform + Exclusive
// C14N, SHA-256 digest. NewEnvelopedReference() bundles these defaults.
signer := xmldsig1.NewSigner().
SignatureAlgorithm(xmldsig1.AlgRSASHA256).
Reference(xmldsig1.NewEnvelopedReference())
// SignEnveloped inserts a <ds:Signature> element as a child of the
// given parent element. The signature covers the entire document
// (URI=""), excluding the Signature element itself.
err = signer.SignEnveloped(context.Background(), doc, doc.DocumentElement(), key)
if err != nil {
fmt.Printf("sign error: %s\n", err)
return
}
out, _ := helium.WriteString(doc)
fmt.Println(strings.Contains(out, "ds:Signature"))
// To verify, create a Verifier with a KeySource that provides the
// public key. StaticKey always returns the same key; for SAML you
// would typically use X509CertKeySource with the IdP's certificate.
//
// Verify requires the document to contain exactly one ds:Signature
// element; it returns ErrAmbiguousSignature when more than one is
// present (use VerifyElement to disambiguate in that case). It
// validates both the SignatureValue (cryptographic signature over the
// canonical SignedInfo) and each Reference digest.
_, err = xmldsig1.NewVerifier(xmldsig1.StaticKey(&key.PublicKey)).
Verify(context.Background(), doc)
if err != nil {
fmt.Printf("verification failed: %s\n", err)
return
}
fmt.Println("signature valid")
// Output:
// true
// signature valid
}source: examples/xmldsig1_sign_verify_example_test.go
A Reference URI is dereferenced to a node-set fail-closed: only same-document
forms are supported, and every other URI (an external reference, or an
unrecognized XPointer scheme) is rejected with ErrReferenceNotFound. The
supported forms and their comment-node semantics (XMLDSig core §4.3.3.2-3) are:
| URI | Node-set | Comment nodes |
|---|---|---|
"" |
whole document | excluded |
"#id" |
element with that id | excluded |
"#xpointer(/)" |
whole document | included |
"#xpointer(id('id'))" |
element with that id | included |
Comment membership is a property of the reference form, not of the
canonicalization method. A C14N #WithComments method only emits comment nodes
that are part of the node-set, so a bare "#id" or "" reference never emits
comments even under a #WithComments canonicalization — the two #xpointer
forms are the only ones that carry comments through. An "#id" that matches more
than one element (across the document and any enveloping Object content) is
rejected with ErrAmbiguousReference, defending against XML Signature Wrapping.
ds:RetrievalMethod requires its URI attribute. An absent attribute is
ErrInvalidKeyInfo, including when LenientKeyInfo(true) is enabled; a present
empty value remains the null same-document URI. External RetrievalMethod URIs
are joined against the effective base of the RetrievalMethod element, including
any inherited xml:base, before the configured resolver receives them.
The supported transforms are the enveloped-signature transform, the
canonicalization transforms (Canonical XML 1.0 / 1.1 and Exclusive C14N 1.0,
each with an optional #WithComments variant), the XPath filter transform
(http://www.w3.org/TR/1999/REC-xpath-19991116), and the base64 decode transform
(http://www.w3.org/2000/09/xmldsig#base64). The XPath filter evaluates its
ds:Transform/XPath expression once per input node — with that node as the
context node, under the XPath element's in-scope namespace bindings — and keeps
each node whose result converts to boolean true (XPath 1.0 semantics: no default
element namespace). During Verify, every XPath filter across all top-level
Reference chains is compiled and statically validated before any Reference
resolver or transformer runs, and the prepared state is reused during digest
execution. An invalid expression therefore fails even when an earlier filter
produces an empty node-set or an earlier Reference carries XSLT. The XMLDSig
here() function (core §6.6.3.1) is available
inside an XPath filter expression: it returns the ds:XPath element that bears
the expression, which is what the standard "enveloped signature via here()"
filter uses to omit the enclosing ds:Signature. Evaluation runs on a bounded
XPath 1.0 evaluator (an operation-count cap on top of the recursion and node-set
caps), so an attacker-supplied expression cannot stall verification. The Base64
transform decodes octet input directly. For node-set input it concatenates the
remaining text-node string-values, with element markup, comments, and processing
instructions stripped, before decoding. Signing supports Base64 through the
Transform interface, although no typed constructor is provided.
Transforms run in declared order over either a node-set or octets. The executor
parses octets when the next transform requires a node-set and applies inclusive
Canonical XML 1.0 when the next transform requires octets. A final node-set gets
the same default canonicalization. This permits repeated transitions such as
XPath → C14N 1.1 → XSLT → XPath → C14N 1.1, multiple XSLT steps, and a second
canonicalization. Base64's node-set text conversion is its one algorithm-specific
exception to the generic C14N conversion. Unknown algorithms and unusable
parameters fail with ErrUnsupportedTransform before any injected transformer
runs. A runtime XPath evaluation failure wraps both ErrUnsupportedTransform
and the evaluator error, so cancellation and deadline errors remain matchable
through the reference wrapper. ErrHereUnavailable remains direct. An
enveloped transform is limited to the original same-document node-set;
it fails after an octet boundary because the containing Signature's node identity
cannot be reconstructed from serialized markup. XPath and XSLT remain
verify-only because the signing API cannot emit their required child content.
RetrievalMethod pipelines are all statically validated, including reachable
transform-free same-document chains, before any resolver or transformer
callback runs. They also have a pre-authentication step cap; see
Verification resource limits.
The XSLT transform is off by default and verify-only. XSLT is a powerful
language (document(), unbounded recursion and compute), and both the stylesheet
and its input are attacker-controlled on verification, so helium never runs XSLT
on its own: an XSLT transform fails closed with ErrUnsupportedTransform unless a
transformer is injected, mirroring the "no HTTP resolver shipped" stance for
external references.
To verify a signature whose Reference carries an XSLT transform, supply an
XSLTTransformer:
type XSLTTransformer interface {
TransformXSLT(ctx context.Context, stylesheet []byte, input []byte) ([]byte, error)
}Verifier.XSLTTransformer(t) opts in. The single ds:Transform/xsl:stylesheet
(or xsl:transform) child is captured and serialized, and passed to t together
with the current pipeline octets. Its output feeds the next transform or the
digest, and one Reference may invoke t multiple times. The implementer owns
all resource and XXE policy — compute/time/memory limits and disabling
document()/external access — because both inputs are attacker-controlled.
The core package runs no XSLT automatically; the separate
xmldsig1/transform.XSLT adapter is an explicit opt-in. The xslt3 direct XML
serializer disables helium.Writer's per-document-child terminators. The adapter
retains explicit top-level text content as result content, including
non-indented newline cases and non-UTF-8 XML output, except when serializer
indentation intentionally discards whitespace-only text because indentation is
enabled and the result has an element child. It does not promise byte-for-byte
preservation of serializer-added document-child terminators.
The shipped adapter returns []byte and does not expose an output-size cap or
transform-step budget. It uses ctx during parsing, compilation, and
invocation, but final serialization into its in-memory buffer remains unbounded
and does not consult ctx. Use it only for interoperability testing or a
controlled profile. A production boundary that accepts attacker-controlled
stylesheets must inject a caller-supplied transformer with cancellation-aware,
bounded serialization and explicit CPU, memory, output, URI, and step limits.
By default a Reference URI is resolved fail-closed to the four same-document
forms above. Verifier.AllowXPointer(true) additionally resolves a general
XPointer framework URI — zero or more xmlns(prefix=uri) scheme parts followed
by one xpointer(<expr>) part, for example
#xmlns(a=urn:x)xpointer(//a:Target). Each xmlns() prefix must be an XML
NCName; a malformed binding cannot select a same-document target even when the
XPath does not use it. It stays fail-closed by default: with
AllowXPointer off, a general XPointer URI is treated as an external reference
and, without a ReferenceResolver, rejected with ErrReferenceNotFound, so
default verification is unchanged.
When enabled, every top-level xpointer() expression joins the verification-wide
static preflight before any Reference resolver or transformer runs, and its
prepared evaluator is reused during digest execution. It uses the same bounded
XPath 1.0 evaluator (the document element's in-scope namespaces overlaid with the
xmlns() bindings). An unresolved variable, function, or prefix fails with
ErrReferenceNotFound before evaluation. The
result must identify a single element — the XML Signature Wrapping defense.
An empty node-set is ErrReferenceNotFound; a node-set selecting more than one
element, or a non-element node, is ErrAmbiguousReference. A literal
xpointer(id('X')) keeps the same duplicate-detecting id resolution the #id
form uses (never a last-one-wins id table). The here() function is not available
inside a URI-borne XPointer.
By default a Reference URI that is not one of the four same-document forms — an
absolute URL, or a relative path pointing outside the document — is rejected with
ErrReferenceNotFound. This fail-closed default is unchanged: helium never
dereferences external content on its own.
To verify a detached signature whose References point outside the document,
supply a ReferenceResolver:
type ReferenceResolver interface {
ResolveReference(ctx context.Context, uri string) ([]byte, error)
}Verifier.ReferenceResolver(r) opts in. An external Reference URI is joined
against the document's base URI (the BaseURI the document was parsed with, via
the same libxml2 URI-resolution helium uses elsewhere) and passed to the resolver;
the resolved octets are then run through the Reference's transform pipeline before
digesting:
- an empty transform chain digests the resolved octets directly;
- any transform that requires a node-set parses the current octets through
Verifier.ReferenceParser, including octets produced by an earlier Base64, canonicalization, or XSLT step. The parser is locked down by default (helium.NewParser(): XXE blocked, no filesystem, no network); - an enveloped-signature transform on an external reference is rejected
fail-closed (
ErrUnsupportedTransform): removing the Signature's own subtree is meaningless on a resource that does not contain the Signature; - an XSLT transform on an external reference applies the same off-by-default,
verify-only rule as a same-document reference: the current octets are handed to
the injected
XSLTTransformer, and with no (or a typed-nil) transformer it fails closed withErrUnsupportedTransform.
A Reference satisfied through the resolver is marked External in the result. An
external reference covers bytes outside the document, not an element, so
VerifyResult.Covers and VerifyResult.SignedElement never attribute
in-document coverage to it — confirming a specific *Element was signed still
requires a same-document reference.
helium ships one resolver, FSReferenceResolver(fsys fs.FS), which serves the
(base-joined) URI as a slash path inside fsys with no network access. It is
fail-closed on anything that is not a plain in-tree path: a URI carrying a scheme
(http:, https:, file:, urn:, any scheme: per RFC 3986, or a Windows
drive letter) is refused; a path escaping the root (absolute, or .. past the
root) is refused; a leftover fragment is refused. Reads are bounded — a resource
larger than 64 MiB fails with ErrReferenceTooLarge before it can be buffered in
full.
No HTTP resolver is provided. The interface is public so callers can dereference over any transport, but anyone implementing network dereferencing owns the resulting SSRF and availability risk (an attacker who controls a Reference URI could otherwise steer requests at internal hosts or stall verification), so that decision is left explicitly to the caller.
For detached-signature services, compose a smaller per-resource cap around the
filesystem resolver, set a low Verifier.MaxReferences value, and pass a
deadline-bearing context:
resolver := xmldsig1.LimitReferenceResolver(
xmldsig1.FSReferenceResolver(fsys),
1<<20, // 1 MiB per resource
)The built-in filesystem resolver applies this cap while reading. A custom resolver receives a post-return size check, so it must enforce its own aggregate byte, connection, and transport budget.
Signer.ReferenceResolver / Signer.ReferenceParser are the symmetric signing
side, letting a detached signature cover external content. The sign and verify
paths funnel through the same octet-to-digest logic, so the signed digest is
byte-identical to what verification recomputes for the same input.
A ds:Reference whose Type is
http://www.w3.org/2000/09/xmldsig#Manifest points at a ds:Manifest, which
holds its own list of ds:Reference elements (XMLDSig core §5.1). The signature
commits to the Manifest's own bytes — the top-level Manifest reference's digest
over the ds:Manifest subtree is checked exactly like any other reference — but
by design it says nothing about whether the Manifest's inner references still
match their targets. Per §5.1 that is left to the application.
Verifier.ValidateManifests(true) opts in to walking those inner references.
When enabled, after a top-level Manifest-typed reference has itself verified,
every direct inner ds:Reference is parsed and statically prepared before any
is executed. If preparation succeeds for all of them, each is resolved, run
through its transform pipeline, and digested through the same fail-closed
path as a top-level reference. If one fails preparation, resolver and
transformer callbacks do not run for that Manifest; the failing result reports
the error, and each otherwise prepared peer reports an advisory error wrapping
the same cause. Per-reference outcomes are reported in
VerifyResult.Manifests:
type ManifestResult struct {
Reference *VerifiedReference // the top-level Manifest reference
Element *helium.Element // the ds:Manifest element
References []ManifestReference
}
type ManifestReference struct {
URI, DigestAlgorithm string
Element *helium.Element
Valid bool
Err error
}Inner-reference results are advisory. A failed inner digest, an unsupported
inner transform, or an unresolved external inner reference is recorded as that
ManifestReference's Valid:false / Err — it does not fail Verify, and
it never contributes to VerifyResult.Covers or SignedElement. Coverage is
never attributed through a Manifest, preserving the XML Signature Wrapping
guarantee: confirming a specific *Element was signed still requires a
top-level same-document reference. Only one level is walked — a Manifest nested
inside a Manifest is digested but not recursively expanded, which bounds the
work.
The toggle defaults to false: VerifyResult.Manifests is nil and no inner
references are walked, byte-identical to a Verifier without it. It is opt-in
because inner references may pull in transforms or external URIs the top-level
policy did not intend. The top-level VerifiedReference.Type is reported in the
result regardless of the toggle.
SHA-1-based algorithms (rsa-sha1, ecdsa-sha1, hmac-sha1, and the sha1
digest) are rejected by default for both signing and verification. SHA-1 is
cryptographically weak; accepting it silently exposes callers to algorithm
downgrade and collision attacks. When a SHA-1 algorithm is encountered without
an explicit opt-in, the operation fails with ErrWeakAlgorithm.
If you must interoperate with a legacy system that cannot be upgraded, opt in
explicitly by calling AllowSHA1(true) on both the Signer and the
Verifier, as shown in the example below:
package examples_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"fmt"
"github.com/lestrrat-go/helium"
"github.com/lestrrat-go/helium/xmldsig1"
)
// Example_xmldsig1_sha1_optin demonstrates the explicit opt-in required to
// produce and verify legacy SHA-1 signatures. SHA-1 (rsa-sha1, hmac-sha1, and
// the sha1 digest) is rejected by default with ErrWeakAlgorithm; call
// AllowSHA1(true) on both the Signer and the Verifier only when you must
// interoperate with a legacy system that cannot be upgraded.
func Example_xmldsig1_sha1_optin() {
const src = `<root Id="doc1"><data>Hello, World!</data></root>`
doc, err := helium.NewParser().Parse(context.Background(), []byte(src))
if err != nil {
fmt.Printf("parse error: %s\n", err)
return
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
fmt.Printf("keygen error: %s\n", err)
return
}
// Produce a legacy SHA-1 signature (discouraged). AllowSHA1(true) is
// required; without it SignEnveloped returns ErrWeakAlgorithm.
signer := xmldsig1.NewSigner().
AllowSHA1(true).
SignatureAlgorithm(xmldsig1.AlgRSASHA1).
Reference(xmldsig1.ReferenceConfig{
URI: "",
DigestAlgorithm: xmldsig1.DigestSHA1,
Transforms: []xmldsig1.Transform{xmldsig1.Enveloped(), xmldsig1.ExcC14NTransform()},
})
if err := signer.SignEnveloped(context.Background(), doc, doc.DocumentElement(), key); err != nil {
fmt.Printf("sign error: %s\n", err)
return
}
// Verify the legacy SHA-1 signature. The default verifier rejects SHA-1,
// so AllowSHA1(true) is required here as well.
_, err = xmldsig1.NewVerifier(xmldsig1.StaticKey(&key.PublicKey)).
AllowSHA1(true).
Verify(context.Background(), doc)
if err != nil {
fmt.Printf("verification failed: %s\n", err)
return
}
fmt.Println("legacy SHA-1 signature valid")
// Output:
// legacy SHA-1 signature valid
}source: examples/xmldsig1_sha1_optin_example_test.go
Note (breaking default change): earlier versions accepted SHA-1 signatures and digests without any opt-in. Code that relied on verifying SHA-1 signatures must now call
Verifier.AllowSHA1(true); code that produced SHA-1 signatures must callSigner.AllowSHA1(true). SHA-256 and stronger algorithms are unaffected.
An attacker-controlled, unsigned document can force verification to do
substantial decode/parse work before the SignatureValue is ever checked:
many or large DigestValue/SignatureValue/X509Certificate values to
base64-decode, one x509.ParseCertificate per embedded certificate, and a
namespace-heavy XPath filter input to collect and evaluate. To bound that work
the Verifier enforces four resource caps. Their defaults preserve the package's
interop vectors while bounding hostile work:
| Builder | Bounds | Default |
|---|---|---|
Verifier.MaxReferences(n) |
number of ds:Reference elements |
1024 |
Verifier.MaxKeyInfoEntries(n) |
KeyInfo children + X509Data children |
256 |
Verifier.MaxDecodedBytes(n) |
running total of certificate and signature octets | 10 MiB |
Verifier.MaxXPathFilterNodes(n) |
members in one XPath filter input node-set | 65,536 |
Exceeding a cap fails with ErrResourceLimitExceeded. For each builder,
n == 0 selects the default and a negative n disables that cap.
MaxDecodedBytes charges five sites. Four are base64 values decoded straight
off the document — the Signature's own DigestValue, SignatureValue, and
X509Certificate content, plus the rawX509Certificate a same-document
ds:RetrievalMethod points at. That last one is not confined to the Signature:
a RetrievalMethod URI names any element in the document by ID, of any local name
and any namespace, inside ds:Signature or outside it.
Each of those four is charged before the value is materialized, so for them
the cap bounds what verification builds, and not merely what it keeps.
xs:base64Binary permits XML whitespace between characters and a value may be
spread over any number of text and CDATA children, so the lexical text wrapped
around a value is unbounded and unrelated to the bytes it decodes to; counting
it first is what keeps the memory under the cap both for a value the cap refuses
and for every value it accepts. Text, CDATA, and entity-reference children carry
a value's characters — a comment or processing instruction contributes none, and
an element child, which xs:base64Binary does not admit at all, is rejected
unread. An entity reference is read as the declared replacement text
of the entity it names, taken in one step and not expanded further, so a
document that writes a value as an entity reference verifies while the read
stays bounded; a replacement that is not base64 fails the decode just as the
same characters written inline would. A reference to an entity nothing
declares contributes no characters. The parser keeps such a reference rather
than refusing the document — with an external subset it did not read, or a
parameter-entity reference, an undeclared general entity is a validity error and
not a well-formedness one — and canonicalization renders it as nothing, so
reading it as nothing is what keeps the two agreeing about the same document.
The fifth site is the exception to both halves of that. An external
ds:RetrievalMethod is dereferenced through the configured
ReferenceResolver, which materializes the whole resource under its own size
cap (FSReferenceResolver bounds one resource at 64 MiB) and runs it through
the RetrievalMethod's transforms; only the result is charged. Those octets are
therefore charged after they are materialized, and they are raw certificate
bytes that were never base64-decoded.
Verification also polls the context inside the KeyInfo and Reference parse loops, and inside every stage that grows or narrows a node set: the subtree and whole-document collections, the comment-excluding and enveloped-signature filters, and the base64 node-set-to-text conversion. A cancelled context or a passed deadline stops that work where it stands, without waiting for a stage boundary. Growing a node set is a single operation that charges what it added, so a stage added later inherits the poll instead of having to remember it.
What a deadline does NOT bound is the canonicalization those stages feed.
Canonical XML is written by the c14n package, and
c14n.Canonicalizer.CanonicalizeTo takes no context; neither does the
helium.CopyDoc an enveloped canonicalization clones the document with. Once a
node set is handed over, that stage runs to completion however large it is, and
giving it a deadline would be a public API change in two other packages. So a
deadline bounds the node-set stages and the gaps between pipeline steps, NOT the
whole verify path.
What bounds the canonicalization of a subtree is its SIZE. The node set built
for it carries one namespace node per declaration actually written, plus at most
one per element, so it is linear in the document. It is deliberately not the
complete XPath in-scope namespace axis, which would be one namespace node per
(element × ancestor declaration) — quadratic work an attacker gets from a small
well-formed document, before any signature is checked, since SignedInfo is
canonicalized before the SignatureValue and a ds:RetrievalMethod can name a
subtree anywhere in the document. The reduced set renders byte-identical
canonical octets in every supported method, Exclusive C14N and its
InclusiveNamespaces PrefixList included.
The one node set that does carry the complete axis is the input to an XPath
filter transform. That transform is evaluated once per node — namespace nodes
included — and may keep an element whose parent it drops, so every element there
needs its own axis. The node set and its evaluations can therefore be quadratic
in the document. MaxXPathFilterNodes bounds the members collected and
evaluated by one filter while preserving the complete standards-required axis
for every accepted input. The limit is checked before the over-limit namespace
wrapper is allocated and before evaluation begins. Namespace declaration and
attribute iteration stop at the boundary without copying the complete
over-limit collection. The limit applies to same-document References, external
or intermediate octets parsed for a later XPath transform, Manifest inner
references, and ds:RetrievalMethod transforms.
Both collection and per-node evaluation also poll ctx. A context error wins
when cancellation coincides with the member boundary. Keep the default finite
for untrusted documents: a Reference's transforms run only after the
SignatureValue has verified, but a ds:RetrievalMethod's transforms run
before it, so a RetrievalMethod can reach the XPath node set without a key or a
valid signature.
RetrievalMethod transforms have a separate fixed maxRetrievalTransformSteps
cap because they execute before the SignatureValue check. It is not affected by
the builder limits above.
The two KeyInfo values written as decimal text, where the rest carry base64, have their
own fixed digit ceilings, also unaffected by the builder limits above: 1024
digits for a ds:X509SerialNumber, and 1024 digits for each of the RFC
4050 ECDSAKeyValue PublicKey X and Y Value attributes. Both are read
before the SignatureValue is checked, and both are converted to a big.Int —
a conversion that is quadratic in the number of digits, so a megabyte of digits
costs about a second and gigabytes of scratch, hundreds of times what parsing
the document carrying them cost. Each value is refused past its ceiling
before it is converted, so that cost is never paid.
Both are fixed internal constants with no builder knob, and deliberately not
folded into MaxDecodedBytes: a byte budget generous enough for real
certificates and keys still admits a conversion that runs for minutes, so a byte
budget is the wrong shape for a quadratic cost. Nothing conforming comes close
to either ceiling — RFC 5280 §4.1.2.2 caps a certificate serial at 20 octets,
which is at most 49 decimal digits, and a P-521 field element needs at most 157
— so there is no legitimate value for a knob to admit.
An XPath filter expression has a fixed 8 KiB length ceiling for the same reason:
every ds:Transform/XPath expression is compiled during Reference preflight,
before the SignatureValue is checked, and compiling one costs far more than its
own length. The expression is refused with ErrResourceLimitExceeded where it is
read off the document, so no over-length expression is compiled. Real filter
expressions are tens to hundreds of bytes — the W3C defCan-1 interop vector is
75 characters — so the ceiling sits orders of magnitude above anything
interoperable. It is a policy limit, and no conformance boundary requires it; like
the RetrievalMethod cap it is not affected by the builder limits above.
SignDetached and SignEnveloping return a detached ds:Signature for the
caller to place. SignedInfo (and any in-Object Reference for SignEnveloping)
is canonicalized under a proxy carrying the signing document element's
inherited canonicalization context. If SignedInfo's CanonicalizationMethod
— or an in-Object Reference — uses inclusive Canonical XML (C14N10 /
C14N11), the caller MUST place the returned Signature directly under the
document element, or under an element with the same in-scope namespaces and
inherited xml:* attributes. Placing it under an element that contributes extra
in-scope namespace declarations or xml:* attributes changes the bytes
inclusive C14N canonicalizes, so verification recomputes a different canonical
form and fails. Exclusive Canonical XML (the NewSigner default,
ExcC14NTransform) inherits no namespaces or xml:* and is unaffected by
placement.
For interoperating with older producers, verification-side KeyInfo parsing
recognizes several legacy constructs and surfaces them through KeyInfoData so
a KeySource can build the verification key. Parsing is namespace-strict and
fails closed (ErrInvalidKeyInfo) on unknown or partial key material.
Security: KeyInfoData is untrusted. A KeySource receives the parsed
KeyInfoData before the signature is verified, so every value in it —
embedded X509Certificates, RSAKeyValue/ECKeyValue/DSAKeyValue,
issuer/serial and subject-name selectors — is attacker-controlled and NOT
authenticated by the signature. A KeySource.ResolveKey implementation MUST
decide trust itself: match the KeyInfoData against a trust store, a pinned
key, or a validated certificate chain, and return a key the caller already
trusts. It MUST NOT blindly return an embedded certificate's public key or a
KeyValue as the verification key — that lets an attacker sign with their own
key and have it verify. KeyInfoData is a selector into trusted key material,
never the key material itself. StaticKey and X509CertKeySource ignore
KeyInfoData entirely and return a pre-trusted key, which is the safe default; a
custom KeySource that consults KeyInfoData owns the trust decision.
- RFC 4050
ECDSAKeyValue(namespacehttp://www.w3.org/2001/04/xmldsig-more#):DomainParameters/NamedCurve@URNselects the curve (P-256/P-384/P-521) andPublicKey/X,/Ycarry the point as decimal integerValueattributes. It is surfaced through the sameKeyInfoData.ECKeyValueas a 1.1ECKeyValue, so aKeySourcebuilds an*ecdsa.PublicKeyfromECKeyValue.Curve/X/Y. Emitting RFC 4050 on the signing side is not supported. X509IssuerSerialandX509SubjectNameinsideX509Data: the issuer DN + serial number (KeyInfoData.X509IssuerSerials) and subject DN (KeyInfoData.X509SubjectNames) are extracted verbatim — the library does no DName canonicalization or matching — so aKeySourcecan select the right certificate out of band.DSAKeyValue:P/Q/G/Yare parsed intoKeyInfoData.DSAKeyValue; aKeySourcebuilds a*dsa.PublicKeyfrom them.
DSA-SHA1 (xmldsig#dsa-sha1) is supported for verification only, as legacy
interop. It sits behind the same SHA-1 weak gate as rsa-sha1: verification
requires Verifier.AllowSHA1(true), otherwise it fails with ErrWeakAlgorithm.
The SignatureValue is the XML-DSig fixed-width r||s concatenation. A DSA key
may come from a parsed DSAKeyValue or from an X.509 certificate (which
crypto/x509 parses into a *dsa.PublicKey). Signing with DSA is not
supported — a signing attempt with the DSA URI fails with a clear
ErrUnsupportedAlgorithm ("DSA signing is not supported").
The package is measured against three W3C XML Signature interop suites through
the helium-w3c-tests
harness (merlinxmldsig, xmldsig2ed, and xmldsig11 suites). Committed
point-in-time evidence:
- Test Cases for C14N 1.1 and XMLDSig Interoperability (W3C Note, 2008) — canonicalization node-set cases plus signature verification (C14N 1.1, XPointer references, X.509 Distinguished Name KeyInfo).
- XML Signature 1.1 interop vectors — enveloping signatures covering ECDSA P-256/P-384/P-521, RSA and HMAC with the SHA-2 family, and RFC 4050 ECDSAKeyValue KeyInfo.
- merlin-xmldsig-twenty-three baseline (2002 IETF/W3C interop collection) — DSA/RSA/HMAC-SHA1 signatures, the base64 transform, the truncated-HMAC must-reject case, and the X.509 KeyInfo variants.
The merlin, xmldsig2ed, and xmldsig11 suites pass in full. The xmldsig2ed
defCan-2/3 cases exercise a multi-phase
XPath → c14n → XSLT → XPath → c14n chain. The ordered transform pipeline
executes its repeated node-set/octet transitions when the Verifier has an
XSLTTransformer configured, such as the ready xslt3-backed
xmldsig1/transform.XSLT adapter.