Skip to content

Latest commit

 

History

History
668 lines (575 loc) · 34.5 KB

File metadata and controls

668 lines (575 loc) · 34.5 KB

xmldsig1

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

Reference processing

Same-document URI forms

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.

Transforms

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.

XSLT transform (opt-in, verify-only)

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.

General XPointer references (opt-in)

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.

External references (opt-in)

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 with ErrUnsupportedTransform.

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.

Manifest inner-reference validation (opt-in)

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.

Security: SHA-1 rejected by default

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 call Signer.AllowSHA1(true). SHA-256 and stronger algorithms are unaffected.

Verification resource limits

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.

Detached signature placement (inclusive C14N)

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.

Legacy and interop KeyInfo (verification)

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 (namespace http://www.w3.org/2001/04/xmldsig-more#): DomainParameters/NamedCurve@URN selects the curve (P-256/P-384/P-521) and PublicKey/X,/Y carry the point as decimal integer Value attributes. It is surfaced through the same KeyInfoData.ECKeyValue as a 1.1 ECKeyValue, so a KeySource builds an *ecdsa.PublicKey from ECKeyValue.Curve/X/Y. Emitting RFC 4050 on the signing side is not supported.
  • X509IssuerSerial and X509SubjectName inside X509Data: the issuer DN + serial number (KeyInfoData.X509IssuerSerials) and subject DN (KeyInfoData.X509SubjectNames) are extracted verbatim — the library does no DName canonicalization or matching — so a KeySource can select the right certificate out of band.
  • DSAKeyValue: P/Q/G/Y are parsed into KeyInfoData.DSAKeyValue; a KeySource builds a *dsa.PublicKey from them.

DSA-SHA1 (verify-only)

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").

W3C interop conformance

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:

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.