RO EN

Semnare XML și documente în .NET — de la zero la producție

Semnare XML și documente în .NET — de la zero la producție
Doru Bulubașa
07 May 2026

Certificatele X.509 nu servesc doar la autentificare — le-am văzut în articolele #9 și #10 în rolul de identitate pentru conexiuni TLS. Același mecanism criptografic stă la baza semnăturilor digitale pe documente: dovedești că un document a fost emis de tine și că nu a fost modificat după semnare.

În contextul românesc, subiectul devine concret și urgent odată cu e-Factura — sistemul național de facturare electronică în care documentele XML trebuie să poată fi semnate digital pentru a fi valide în relațiile B2B. Dar semnarea XML are aplicabilitate mult mai largă: contracte digitale, mesaje între servicii, documente fiscale, răspunsuri API care trebuie să fie non-repudiabile.

În acest articol construim semnarea XML în .NET de la zero, pas cu pas.


1. Ce este o semnătură digitală XML (XMLDSig)?

XML Digital Signature (XMLDSig) este un standard W3C care definește cum se semnează conținut XML — sau orice alt conținut referit dintr-un document XML. Standardul descrie o structură <Signature> care conține:

  • SignedInfo — ce s-a semnat și cum (algoritmul de canonicalizare, algoritmul de semnare, referințele la conținut)
  • SignatureValue — valoarea criptografică a semnăturii (hash semnat cu cheia privată)
  • KeyInfo — informații despre cheia publică / certificatul folosit (opțional, dar recomandat)
  • Object — date adiționale, inclusiv timestamp-uri în XAdES

Trei tipuri de semnătură XMLDSig

Enveloped — semnătura este inclusă în interiorul documentului semnat. Cel mai comun pentru documente XML self-contained (facturi, contracte).

<Invoice>
  <Header>...</Header>
  <Lines>...</Lines>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <!-- semnătura e înglobată în document -->
  </Signature>
</Invoice>

Enveloping — documentul semnat este inclus în interiorul semnăturii, ca element <Object>.

Detached — semnătura este un document XML separat, care referă conținutul extern prin URI (URL, hash, etc.).

Pentru e-Factura și documente fiscale vei folosi aproape întotdeauna enveloped.

XMLDSig vs. XAdES

XMLDSig este standardul de bază — semnătură criptografică simplă. XAdES (XML Advanced Electronic Signatures) extinde XMLDSig cu:

  • Timestamp criptografic (semnătura nu poate fi backdatată)
  • Informații de revocare incluse în semnătură (OCSP response, CRL)
  • Arhivabilitate pe termen lung (semnătura rămâne validă și după expirarea certificatului)

XAdES este cerut în scenarii de conformitate legală ridicată. Pentru acest articol construim pe XMLDSig — înțelegând baza, XAdES devine o extensie naturală.


2. Canonicalizare XML — de ce contează

Înainte de a semna, XML-ul trebuie canonicalizat — transformat într-o formă standard, deterministă. Același document XML poate fi reprezentat în zeci de moduri echivalente semantic:

<!-- Aceste două fragmente sunt semantic identice -->
<Invoice id="1" date="2025-01-01"/>
<Invoice date="2025-01-01" id="1"></Invoice>

Dacă semnezi primul și validezi al doilea, semnătura ar pica — hash-urile ar fi diferite. Canonicalizarea rezolvă asta: înainte de a calcula hash-ul, XML-ul e adus la o formă unică (atribute sortate, spații albe normalizate, namespace-uri explicite etc.).

Algoritmii principali de canonicalizare:

  • http://www.w3.org/TR/2001/REC-xml-c14n-20010315 — Canonical XML 1.0 (cel mai folosit)
  • http://www.w3.org/2001/10/xml-exc-c14n# — Exclusive Canonical XML (mai potrivit când documentul e inclus în alt XML)

În .NET, SignedXml gestionează canonicalizarea automat — dar trebuie să știi că există pentru a înțelege de ce semnătura pică dacă documentul e reformatat.


3. Semnare enveloped cu certificat X.509

Clasa centrală în .NET este System.Security.Cryptography.Xml.SignedXml, disponibilă în System.Security.Cryptography.Xml (NuGet pentru .NET Core).

3.1 Instalare pachet

dotnet add package System.Security.Cryptography.Xml

3.2 Semnare document XML

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

public static class XmlSigner
{
    /// <summary>
    /// Adaugă o semnătură enveloped în documentul XML.
    /// Returnează documentul XML semnat.
    /// </summary>
    public static XmlDocument Sign(
        XmlDocument document,
        X509Certificate2 certificate)
    {
        if (!certificate.HasPrivateKey)
            throw new InvalidOperationException(
                "Certificatul nu conține cheia privată.");

        // Extragem cheia privată RSA din certificat
        var rsaKey = certificate.GetRSAPrivateKey()
            ?? throw new InvalidOperationException(
                "Nu s-a putut extrage cheia RSA din certificat.");

        var signedXml = new SignedXml(document)
        {
            SigningKey = rsaKey
        };

        // Algoritmul de semnare: RSA cu SHA-256
        signedXml.SignedInfo.SignatureMethod =
            SignedXml.XmlDsigRSASHA256Url;

        // Algoritmul de canonicalizare
        signedXml.SignedInfo.CanonicalizationMethod =
            SignedXml.XmlDsigExcC14NTransformUrl;

        // Referință la documentul întreg ("") cu transform enveloped
        var reference = new Reference
        {
            Uri            = "",         // semnăm tot documentul
            DigestMethod   = SignedXml.XmlDsigSHA256Url
        };

        // Transform enveloped — exclude elementul <Signature> din hash
        // (altfel am semna inclusiv semnătura, cerc vicios)
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        reference.AddTransform(new XmlDsigExcC14NTransform());

        signedXml.AddReference(reference);

        // Includem certificatul în KeyInfo
        // — validatorul poate extrage cheia publică din document
        var keyInfo = new KeyInfo();
        keyInfo.AddClause(new KeyInfoX509Data(certificate));
        signedXml.KeyInfo = keyInfo;

        // Calculăm și adăugăm semnătura în document
        signedXml.ComputeSignature();

        var signatureElement = signedXml.GetXml();
        document.DocumentElement!.AppendChild(
            document.ImportNode(signatureElement, deep: true));

        return document;
    }
}

3.3 Utilizare

// Încarcă documentul XML
var doc = new XmlDocument { PreserveWhitespace = true };
doc.Load("invoice.xml");

// Încarcă certificatul cu cheie privată
var cert = new X509Certificate2(
    "my-cert.pfx",
    "password",
    X509KeyStorageFlags.EphemeralKeySet);

// Semnează
var signedDoc = XmlSigner.Sign(doc, cert);

// Salvează
signedDoc.Save("invoice-signed.xml");

PreserveWhitespace = true este critic. Dacă e false, parser-ul XML poate modifica spațiile albe la încărcare, schimbând hash-ul documentului — semnătura va pica la validare chiar dacă documentul nu a fost alterat.


4. Validare semnătură

Validarea verifică două lucruri: integritatea documentului (hash-ul e corect) și autenticitatea semnăturii (a fost produsă de deținătorul cheii private).

public static class XmlSignatureValidator
{
    public static ValidationResult Validate(XmlDocument document)
    {
        var signedXml = new SignedXml(document);

        // Găsim elementul <Signature> în document
        var signatureNode = document.GetElementsByTagName(
            "Signature", SignedXml.XmlDsigNamespaceUrl);

        if (signatureNode.Count == 0)
            return ValidationResult.Fail("Documentul nu conține semnătură.");

        signedXml.LoadXml((XmlElement)signatureNode[0]!);

        // Extragem certificatul din KeyInfo
        X509Certificate2? signingCert = null;

        foreach (KeyInfoClause clause in signedXml.KeyInfo)
        {
            if (clause is KeyInfoX509Data x509Data
                && x509Data.Certificates.Count > 0)
            {
                signingCert = (X509Certificate2)x509Data.Certificates[0]!;
                break;
            }
        }

        if (signingCert is null)
            return ValidationResult.Fail(
                "Nu s-a găsit certificatul în KeyInfo.");

        // Verificăm că certificatul nu a expirat
        if (DateTime.UtcNow < signingCert.NotBefore ||
            DateTime.UtcNow > signingCert.NotAfter)
        {
            return ValidationResult.Fail(
                $"Certificatul a expirat sau nu e încă valid. " +
                $"Valid: {signingCert.NotBefore:u} — {signingCert.NotAfter:u}");
        }

        // Verificăm semnătura cu cheia publică din certificat
        var rsaKey = signingCert.GetRSAPublicKey()!;
        var isValid = signedXml.CheckSignature(rsaKey);

        if (!isValid)
            return ValidationResult.Fail("Semnătura digitală nu este validă.");

        return ValidationResult.Ok(
            signingCert.Subject,
            signingCert.Thumbprint);
    }
}

public record ValidationResult(
    bool IsValid,
    string? SignedBy,
    string? Thumbprint,
    string? Error)
{
    public static ValidationResult Ok(string signedBy, string thumbprint) =>
        new(true, signedBy, thumbprint, null);

    public static ValidationResult Fail(string error) =>
        new(false, null, null, error);
}

Validare cu CA de încredere

Validarea de mai sus verifică semnătura matematică, dar nu verifică că certificatul provine dintr-un CA de încredere. Într-un scenariu de producție, adaugi chain validation:

// Adaugă după validarea semnăturii matematice
var chain = new X509Chain();
chain.ChainPolicy.RevocationMode    = X509RevocationMode.Online;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;

if (!chain.Build(signingCert))
{
    var errors = string.Join(", ",
        chain.ChainStatus.Select(s => s.StatusInformation));
    return ValidationResult.Fail(
        $"Chain validation eșuată: {errors}");
}

5. Semnare detached

Semnătura detached e utilă când vrei să semnezi un fișier extern (PDF, CSV, orice), sau când documentul semnat nu poate fi modificat pentru a include semnătura.

public static XmlDocument SignDetached(
    byte[] contentToSign,
    X509Certificate2 certificate,
    string contentUri = "document.xml")
{
    var rsaKey = certificate.GetRSAPrivateKey()!;

    // Document XML care va conține DOAR semnătura
    var signatureDoc = new XmlDocument();
    var signedXml    = new SignedXml(signatureDoc)
    {
        SigningKey = rsaKey
    };

    signedXml.SignedInfo.SignatureMethod =
        SignedXml.XmlDsigRSASHA256Url;
    signedXml.SignedInfo.CanonicalizationMethod =
        SignedXml.XmlDsigExcC14NTransformUrl;

    // Referință externă — URI spre fișierul semnat
    var reference = new Reference(contentUri)
    {
        DigestMethod = SignedXml.XmlDsigSHA256Url
    };

    // Furnizăm conținutul direct (pentru calcul hash)
    // în loc să îl citi din URI la runtime
    reference.AddTransform(new XmlDsigExcC14NTransform());

    // Precomputed hash al conținutului
    using var sha256    = SHA256.Create();
    var contentHash     = sha256.ComputeHash(contentToSign);
    // Notă: pentru referințe externe cu hash precomputat
    // folosim DataObject cu hash manual

    signedXml.AddReference(reference);

    var keyInfo = new KeyInfo();
    keyInfo.AddClause(new KeyInfoX509Data(certificate));
    signedXml.KeyInfo = keyInfo;

    signedXml.ComputeSignature();

    // Documentul de semnătură conține doar <Signature>
    signatureDoc.AppendChild(
        signatureDoc.ImportNode(signedXml.GetXml(), true));

    return signatureDoc;
}

6. Semnare cu cheie RSA fără certificat

Uneori vrei să semnezi cu o pereche de chei RSA generate programatic, fără un certificat complet — util pentru teste sau scenarii interne unde nu ai nevoie de un lanț de certificare.

// Generare pereche de chei RSA
using var rsa = RSA.Create(2048);

var doc = new XmlDocument { PreserveWhitespace = true };
doc.LoadXml("<Root><Data>test</Data></Root>");

var signedXml = new SignedXml(doc) { SigningKey = rsa };
signedXml.SignedInfo.SignatureMethod =
    SignedXml.XmlDsigRSASHA256Url;

var reference = new Reference("");
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
reference.AddTransform(new XmlDsigExcC14NTransform());
reference.DigestMethod = SignedXml.XmlDsigSHA256Url;

signedXml.AddReference(reference);

// KeyInfo cu cheia publică RSA (fără certificat)
var keyInfo = new KeyInfo();
keyInfo.AddClause(new RSAKeyValue(rsa));
signedXml.KeyInfo = keyInfo;

signedXml.ComputeSignature();
doc.DocumentElement!.AppendChild(
    doc.ImportNode(signedXml.GetXml(), true));

// Validare cu cheia publică
var validator = new SignedXml(doc);
var sigNode   = doc.GetElementsByTagName(
    "Signature", SignedXml.XmlDsigNamespaceUrl)[0]!;
validator.LoadXml((XmlElement)sigNode);
var isValid = validator.CheckSignature(rsa); // true

7. Semnare XML în contextul e-Factura

e-Factura este sistemul național de facturare electronică B2B din România, obligatoriu din 2024 pentru tranzacțiile cu statul și extins treptat pentru B2B. Facturile sunt documente XML în format UBL 2.1, conforme cu profilul CIUS-RO.

Din perspectiva semnării, lucrurile relevante sunt:

  • Documentele XML trimise prin sistemul e-Factura (ANAF) sunt validate structural și semantic de ANAF — semnătura digitală nu este obligatorie tehnic la upload prin API, dar este obligatorie legal pentru valoarea documentului ca factură electronică validă
  • Semnătura cerută legal este calificată (cu certificat calificat pe token hardware, emis de un prestator acreditat) — ceea ce depășește scope-ul tehnic al acestui articol
  • Din perspectiva codului .NET, mecanismul este identic cu ce am construit mai sus — diferența e sursa certificatului (token hardware via PKCS#11 în loc de fișier PFX)
  • Pentru sisteme automate (ERP, SaaS de facturare), semnarea se face cu certificat calificat pentru semnătură automată — emis de autorități ca DigiSign, certSIGN, Trans Sped — care poate fi stocat în HSM sau Key Vault

Structura unei facturi UBL semnate

<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
         xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
         xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">

  <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
  <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1</cbc:CustomizationID>
  <cbc:ID>FACT-2025-0001</cbc:ID>
  <cbc:IssueDate>2025-05-08</cbc:IssueDate>

  <!-- ... linii de factură ... -->

  <!-- Semnătura XMLDSig adăugată la final -->
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>...</SignedInfo>
    <SignatureValue>...</SignatureValue>
    <KeyInfo>...</KeyInfo>
  </Signature>

</Invoice>

Semnătura se adaugă exact cum am implementat în secțiunea 3 — ca element enveloped la finalul documentului.

Un cuvânt despre XAdES și e-Factura

Standardul european EN 16931 (pe care se bazează CIUS-RO) recomandă XAdES-BES sau XAdES-T pentru semnături cu valoare legală. XAdES adaugă față de XMLDSig simplu timestamp-ul criptografic — esențial ca semnătura să rămână validă și după expirarea certificatului semnatarului. Implementarea completă XAdES în .NET necesită librării specializate (PAdES, DSS) și depășește scope-ul acestui articol.


8. Semnare în ASP.NET Core — integrare în pipeline

Într-o aplicație web care generează și semnează documente XML la cerere, structura recomandată:

// Serviciu dedicat, injectat prin DI
public interface IXmlSigningService
{
    XmlDocument Sign(XmlDocument document);
    ValidationResult Validate(XmlDocument document);
}

public class XmlSigningService : IXmlSigningService
{
    private readonly X509Certificate2 _signingCert;

    public XmlSigningService(IConfiguration config)
    {
        // Certificatul se încarcă o singură dată la startup
        // Din Azure Key Vault în producție
        var certPath     = config["Signing:CertificatePath"]!;
        var certPassword = config["Signing:CertificatePassword"]!;

        _signingCert = new X509Certificate2(
            certPath, certPassword,
            X509KeyStorageFlags.EphemeralKeySet |
            X509KeyStorageFlags.PersistKeySet);
    }

    public XmlDocument Sign(XmlDocument document)
        => XmlSigner.Sign(document, _signingCert);

    public ValidationResult Validate(XmlDocument document)
        => XmlSignatureValidator.Validate(document);
}

// Înregistrare în Program.cs
builder.Services.AddSingleton<IXmlSigningService, XmlSigningService>();
// Controller
[ApiController]
[Route("api/[controller]")]
public class InvoiceController(IXmlSigningService signer) : ControllerBase
{
    [HttpPost("sign")]
    public IActionResult SignInvoice([FromBody] string xmlContent)
    {
        var doc = new XmlDocument { PreserveWhitespace = true };
        doc.LoadXml(xmlContent);

        var signed = signer.Sign(doc);

        return Content(signed.OuterXml, "application/xml");
    }

    [HttpPost("validate")]
    public IActionResult ValidateInvoice([FromBody] string xmlContent)
    {
        var doc = new XmlDocument { PreserveWhitespace = true };
        doc.LoadXml(xmlContent);

        var result = signer.Validate(doc);

        return result.IsValid
            ? Ok(new { result.SignedBy, result.Thumbprint })
            : BadRequest(new { result.Error });
    }
}

9. Probleme comune și soluțiile lor

Problemă Cauza probabilă Soluție
Semnătura pică la validare deși documentul e intact PreserveWhitespace = false la încărcare Setează întotdeauna doc.PreserveWhitespace = true înainte de Load()
CryptographicException la semnare pe Linux Flag-uri de stocare incompatibile Adaugă X509KeyStorageFlags.EphemeralKeySet la încărcarea certificatului
Semnătura validă matematic, dar respinsă de sistem extern Algoritm deprecated (SHA-1) sau canonicalizare greșită Folosește XmlDsigRSASHA256Url și XmlDsigExcC14NTransformUrl
Namespace-uri duplicate după semnare ImportNode adaugă namespace-uri deja prezente Folosește XmlDsigExcC14NTransform (Exclusive C14N) în loc de C14N standard
Semnătura nu include certificatul KeyInfo nu e populat Adaugă KeyInfoX509Data cu certificatul complet, nu doar cheia publică
Eroare la semnare cu certificat din Azure Key Vault Cheia privată nu poate fi exportată din Key Vault Folosește CryptographyClient.SignAsync() din Azure SDK pentru operații cu cheie non-exportabilă

10. Checklist de producție

  • PreserveWhitespace = true la orice operație de încărcare XML care implică semnare sau validare
  • ✅ Algoritm SHA-256 (nu SHA-1) pentru DigestMethod și SignatureMethod
  • XmlDsigExcC14NTransformUrl ca algoritm de canonicalizare
  • ✅ Certificat de semnare stocat în Azure Key Vault, nu pe disc
  • EphemeralKeySet la încărcarea certificatelor în containere Linux
  • ✅ Chain validation activată în producție (nu doar validare matematică)
  • ✅ Logging pentru orice eșec de semnare sau validare cu detalii despre certificat
  • ✅ Testat cu document modificat după semnare → validarea trebuie să returneze eșec
  • ✅ Testat cu certificat expirat → validarea trebuie să returneze eșec cu mesaj clar
  • ✅ Alertă la expirarea certificatului de semnare (minim 30 zile înainte)

Concluzie

Semnarea XML în .NET este surprinzător de accesibilă odată ce înțelegi cele două concepte esențiale: canonicalizarea (de ce același XML poate produce hash-uri diferite) și tipul de semnătură (enveloped, enveloping, detached). Clasa SignedXml din BCL acoperă scenariile comune fără nicio dependință externă.

Pentru scenarii cu valoare legală — e-Factura, contracte electronice, documente fiscale — diferența față de ce am implementat este sursa certificatului (calificat, emis de un prestator acreditat) și eventual adăugarea unui timestamp criptografic (XAdES). Mecanismul de semnare în sine rămâne același.

Seria de securitate continuă cu protecția împotriva CSRF, XSS și Injection — o schimbare de registru față de articolele despre criptografie, spre amenințările cele mai frecvente în aplicații web.