Skip to content

An enveloped XML signature with explicit verification boundaries

An enveloped XML signature with explicit verification boundaries

Illustrative code, not executed.

Original English publication date: 2022-08-25T15:19:00.003+03:00. Original English article

XML icon from the original article

An enveloped XML signature lives inside the document it signs. The example below signs one whole document and only accepts the same restricted profile when verifying it. It is not a general SAML, SOAP or arbitrary XML-signature verifier. The signer receives an explicitly selected RSA key; it does not open a certificate store or select the first thumbprint match. [1]

GetRSAPrivateKey is the algorithm-specific way to request a certificate’s RSA private key and may return null. It returns an RSA abstraction, not a promise that one particular CSP or CNG implementation will be used on every platform. Avoid the obsolete PrivateKey property; do not claim that GetRSAPrivateKey first appeared only in .NET Framework 4.6.2. Dispose key handles acquired from certificates, but do not dispose a caller-owned key inside a helper that merely borrows it. [4][5][6]

A deliberately narrow profile

This illustrative code targets modern .NET with System.Security.Cryptography.Xml. It prohibits DTD processing, disables XML resolution and caps input size. It explicitly uses RSA-SHA256, SHA-256 digests and exclusive canonicalization. The one empty-URI reference covers the document, with exactly the enveloped-signature and exclusive-canonicalization transforms. All code is unexecuted; no key or certificate operation has been performed. [1][7]

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Xml;

public static class NarrowXmlSignature
{
    private const string PayloadNs = "urn:example:payload";
    private const long Limit = 1_000_000; // Example application limit.

    private static XmlDocument Parse(string xml)
    {
        if (xml == null || xml.Length > Limit)
            throw new ArgumentException("XML exceeds the input policy.");
        var settings = new XmlReaderSettings {
            DtdProcessing = DtdProcessing.Prohibit,
            XmlResolver = null,
            MaxCharactersInDocument = Limit
        };
        using var input = new StringReader(xml);
        using var reader = XmlReader.Create(input, settings);
        var doc = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
        doc.Load(reader);
        if (doc.DocumentElement == null ||
            doc.DocumentElement.LocalName != "payload" ||
            doc.DocumentElement.NamespaceURI != PayloadNs)
            throw new CryptographicException("Unexpected payload root.");
        return doc;
    }

    public static string Sign(string xml, RSA callerOwnedKey)
    {
        var doc = Parse(xml);
        if (doc.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl).Count != 0)
            throw new CryptographicException("Input must not already contain a signature.");
        var sx = new SignedXml(doc) { SigningKey = callerOwnedKey, Resolver = null };
        sx.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
        sx.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA256Url;
        var reference = new Reference("") { DigestMethod = SignedXml.XmlDsigSHA256Url };
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        reference.AddTransform(new XmlDsigExcC14NTransform());
        sx.AddReference(reference);
        sx.ComputeSignature();
        doc.DocumentElement!.AppendChild(doc.ImportNode(sx.GetXml(), true));
        return doc.OuterXml;
    }

    public static XmlElement VerifyAndGetPayload(string xml, RSA trustedPublicKey)
    {
        var doc = Parse(xml);
        var signatures = doc.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
        if (signatures.Count != 1 || signatures[0]!.ParentNode != doc.DocumentElement)
            throw new CryptographicException("Exactly one root-child signature is required.");
        var sx = new SignedXml(doc) { Resolver = null };
        sx.LoadXml((XmlElement)signatures[0]!);
        if (sx.SignedInfo.SignatureMethod != SignedXml.XmlDsigRSASHA256Url ||
            sx.SignedInfo.CanonicalizationMethod != SignedXml.XmlDsigExcC14NTransformUrl ||
            sx.SignedInfo.References.Count != 1 || sx.KeyInfo.Count != 0 ||
            sx.Signature.ObjectList.Count != 0)
            throw new CryptographicException("Signature profile mismatch.");
        var r = (Reference)sx.SignedInfo.References[0]!;
        if (r.Uri != "" || r.DigestMethod != SignedXml.XmlDsigSHA256Url ||
            r.TransformChain.Count != 2 ||
            r.TransformChain[0].Algorithm != SignedXml.XmlDsigEnvelopedSignatureTransformUrl ||
            r.TransformChain[1].Algorithm != SignedXml.XmlDsigExcC14NTransformUrl)
            throw new CryptographicException("Reference profile mismatch.");
        if (!sx.CheckSignature(trustedPublicKey))
            throw new CryptographicException("Signature verification failed.");
        return doc.DocumentElement!; // Consume this verified root; do not reparse another copy.
    }
}

The trustedPublicKey parameter must come from an independently authorized identity or validated certificate, never from untrusted KeyInfo in the message. The verifier checks both the signature and its references through CheckSignature, and rejects extra signatures, external references and unexpected transforms before that call. A successful cryptographic check is not enough if the application later consumes a different node. Apply the expected payload schema, authorization and replay policy to the returned root; root-name checking alone is not a complete business schema. [2][3]

The fixed namespace is an illustrative application identifier, not a network location. Do not change whitespace or reserialize the document through a different transformation between signing and verification without testing interoperability. Production review must include wrapping/substitution cases, duplicate signatures, forbidden references, malformed XML and changes to application data. No such tests are claimed here. [2]

Primary references

  1. Microsoft SignedXml
  2. W3C XML Signature Best Practices
  3. Microsoft XML signature security considerations
  4. Microsoft GetRSAPrivateKey
  5. Microsoft X509Certificate2.PrivateKey
  6. Microsoft GetRSAPrivateKey applicability
  7. Official .NET SignedXml source

Post a Comment

Previous Post Next Post