How Do I Implement the ISO 20022 Digital Signature?

I have a C# library and I have many message containers implemented consistent with ISO 20022 specifications. I need to implement SignatureEnvelope and SignatureEnvelopeReference, but I don’t understand how the value should be created when I originate a message, or how it should be used to verify contents when I receive one incoming. Can you explain and give an example or two?

Good question — and one that trips up most implementers, because SignatureEnvelope sounds like it’s ISO 20022’s own invention, when it’s actually a thin wrapper around a well-established W3C standard.

[Querying the ISO 20022 MCP Server: universal_lookupSignatureEnvelope definition…]

Here’s what the spec says: SignatureEnvelope is a MessageComponent whose sole purpose is to carry a W3C XML Digital Signature (ds:Signature) element. The ISO 20022 standard doesn’t define how to sign — it delegates entirely to the W3C XML-DSig specification. Your message containers hold a Sgntr element (short for Signature) of this type in the Business Application Header (AppHdr), and it contains the full <ds:Signature> XML subtree.

SignatureEnvelopeReference is a companion type used when a signature defined in one structural location needs to be referenced from another — for example, in multi-document message bundles where the signature appears once and multiple message parts point to it by URI. For single-message flows (the most common case), you’ll only deal with SignatureEnvelope directly.

Before I show you code: what kind of keys will you be using? X.509 certificates issued by your bank or a CA, or raw RSA/ECDSA keys for development?


Developer: X.509 certificates from the bank.

Claude: Perfect — that’s the right answer for production and it simplifies verification considerably, because the certificate travels with the signature and the verifier just needs to trust the issuing CA.

Here’s the full picture.


What’s Inside a SignatureEnvelope

When your Sgntr element is populated, it contains a structure like this:

<AppHdr xmlns="urn:iso:std:iso:20022:tech:xsd:head.001.001.02">
  <!-- ... Fr, To, BizMsgIdr, MsgDefIdr, CreDt ... -->
  <Sgntr>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
      <ds:SignedInfo>
        <ds:CanonicalizationMethod
          Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <ds:SignatureMethod
          Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
        <ds:Reference URI="">
          <ds:Transforms>
            <ds:Transform
              Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
            <ds:Transform
              Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          </ds:Transforms>
          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
          <ds:DigestValue>7Kdwx3...base64...</ds:DigestValue>
        </ds:Reference>
      </ds:SignedInfo>
      <ds:SignatureValue>rQ8f2p...base64...</ds:SignatureValue>
      <ds:KeyInfo>
        <ds:X509Data>
          <ds:X509Certificate>MIIBxz...base64...</ds:X509Certificate>
        </ds:X509Data>
      </ds:KeyInfo>
    </ds:Signature>
  </Sgntr>
</AppHdr>

The Reference URI="" means “sign the document I’m embedded in.” The enveloped-signature transform tells the verifier to exclude the <ds:Signature> element itself when computing the digest (otherwise it would be a circular reference). The exclusive C14N transform normalises the XML before hashing so that insignificant whitespace differences don’t break the signature.


Originating: Signing When You Send

The .NET System.Security.Cryptography.Xml namespace gives you SignedXml, which handles the C14N, digest, and signature mechanics. Your job is to wire it up to your message XML and your certificate.

using System.Security.Cryptography.Xml;
using System.Security.Cryptography.X509Certificates;
using System.Xml;

/// <summary>
/// Signs an ISO 20022 AppHdr+Document XML pair.
/// The signature is placed in the Sgntr element of the AppHdr.
/// </summary>
public static XmlElement CreateSignatureEnvelope(
    XmlDocument documentToSign,
    X509Certificate2 signingCertificate)
{
    var signedXml = new SignedXml(documentToSign)
    {
        SigningKey = signingCertificate.GetRSAPrivateKey()
    };

    // Use RSA-SHA256 (required by most banks; SHA-1 is obsolete)
    signedXml.SignedInfo.SignatureMethod =
        "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";

    // Exclusive C14N for canonicalization (more robust than inclusive)
    signedXml.SignedInfo.CanonicalizationMethod =
        "http://www.w3.org/2001/10/xml-exc-c14n#";

    // Reference URI="" = sign the whole document
    var reference = new Reference { Uri = "" };
    reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";

    // Exclude the Signature element itself from the digest
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
    // Normalise before hashing
    reference.AddTransform(new XmlDsigExcC14NTransform());
    signedXml.AddReference(reference);

    // Embed the X.509 certificate so the verifier can find the public key
    var keyInfo = new KeyInfo();
    keyInfo.AddClause(new KeyInfoX509Data(signingCertificate));
    signedXml.KeyInfo = keyInfo;

    signedXml.ComputeSignature();

    // Return the <ds:Signature> element — place this inside your <Sgntr> element
    return signedXml.GetXml();
}

Usage with your message containers:

// 1. Serialise your AppHdr + Document to XmlDocument
var xmlDoc = SerialiseMessage(appHdr, document);

// 2. Load your bank-issued certificate from the Windows cert store
//    (or from a .pfx file in non-production environments)
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var cert = store.Certificates
    .Find(X509FindType.FindByThumbprint, "YOUR_THUMBPRINT", validOnly: true)
    .OfType<X509Certificate2>()
    .Single();

// 3. Create the ds:Signature element
var signatureElement = CreateSignatureEnvelope(xmlDoc, cert);

// 4. Set it on your Sgntr field — the exact property depends on your
//    generated container classes; it typically accepts an XmlElement
appHdr.Sgntr = new SignatureEnvelope { Any = signatureElement };

// 5. Re-serialise and transmit
TransmitMessage(appHdr, document);

Receiving: Verifying an Incoming Message

When a signed message arrives, you need to confirm two things:

  1. Integrity — the message content hasn’t changed since it was signed (the digest holds)
  2. Authenticity — the signature was produced by the private key matching the certificate in KeyInfo, and that certificate is trusted
/// <summary>
/// Verifies the SignatureEnvelope on an incoming ISO 20022 message.
/// Returns true if the signature is cryptographically valid and the
/// certificate is issued by a trusted CA.
/// </summary>
public static bool VerifySignatureEnvelope(
    XmlDocument signedDocument,
    X509Certificate2Collection trustedRoots)
{
    // Locate the ds:Signature element inside the Sgntr element
    var signatureNodes = signedDocument.GetElementsByTagName(
        "Signature", "http://www.w3.org/2000/09/xmldsig#");

    if (signatureNodes.Count == 0)
        throw new InvalidOperationException("No ds:Signature found in message.");

    var signedXml = new SignedXml(signedDocument);
    signedXml.LoadXml((XmlElement)signatureNodes[0]);

    // Extract the X.509 certificate from KeyInfo
    X509Certificate2? signerCert = null;
    foreach (KeyInfoClause clause in signedXml.KeyInfo)
    {
        if (clause is KeyInfoX509Data x509Data
            && x509Data.Certificates.Count > 0)
        {
            signerCert = (X509Certificate2)x509Data.Certificates[0]!;
            break;
        }
    }

    if (signerCert is null)
        throw new InvalidOperationException("No certificate found in ds:KeyInfo.");

    // Step 1: Verify the cryptographic signature (digest + signature value)
    bool signatureValid = signedXml.CheckSignature(signerCert, verifySignatureOnly: true);
    if (!signatureValid)
        return false;

    // Step 2: Verify the certificate chain against your trusted roots
    //         (the bank's root CA certificate, loaded from your trust store)
    var chain = new X509Chain();
    chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
    chain.ChainPolicy.CustomTrustStore.AddRange(trustedRoots);
    chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
    chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;

    return chain.Build(signerCert);
}

Usage:

// Load your bank's root CA from a trusted location (not from the message itself)
var bankRootCa = new X509Certificate2("bank-root-ca.cer");
var trustedRoots = new X509Certificate2Collection(bankRootCa);

var incomingXml = new XmlDocument { PreserveWhitespace = true };
incomingXml.LoadXml(receivedMessageText);

bool isValid = VerifySignatureEnvelope(incomingXml, trustedRoots);

if (!isValid)
    throw new SecurityException("Incoming message signature is invalid — reject and log.");

Critical: PreserveWhitespace = true is not optional. If the XmlDocument normalises whitespace during load, the digest will not match and every valid message will appear invalid.


SignatureEnvelopeReference in Practice

SignatureEnvelopeReference carries a URI that points to a SignatureEnvelope defined elsewhere in the same payload — typically a #fragment reference to an Id-attributed element. You’ll encounter it in bundled file formats or in multi-part message containers where one signing ceremony covers several documents.

<!-- SignatureEnvelope defined once, with an Id -->
<Sgntr Id="sig-001">
  <ds:Signature ...> ... </ds:Signature>
</Sgntr>

<!-- SignatureEnvelopeReference pointing to it from a second element -->
<SgntrRef>#sig-001</SgntrRef>

In C#, resolving it means locating the element with that Id, extracting its ds:Signature child, and running the same SignedXml.CheckSignature verification as above. For single-message flows — which covers the vast majority of pain.001 origination and pain.002 acknowledgment scenarios — you won’t need this.


The MCP Server looked up SignatureEnvelope and confirmed it’s a pure XML-DSig wrapper — no proprietary ISO 20022 signing algorithm to reverse-engineer, no spec section to misread. It identified the mandatory C14N and transform requirements, which drove the exact algorithm URIs in the code above. What would have been a two-day spec archaeology exercise turned into a working implementation in one session.

If you’d like to see how the Sgntr element fits into a specific message version — say, pain.001.001.09’s AppHdr — ask the MCP Server to show you the AppHdr message definition and it will trace the field directly.