Weryfikacja podpisu elektronicznego XAdES w .NET

10.07.2013

XAdES to najbardziej popularny format podpisu elektronicznego w Polsce. Występuje on w kilku odmianach: zwykły, ze stemplem czasowym, dodatkowymi informacjami itd.

Czasami zachodzi potrzeba zweryfikowania podpisu elektronicznego w .NET. Jakiś czas temu miałem podobny problem. Pokopałem trochę w Google i w MSDN i  znalazłem w .NET Framework ciekawą klasę SignedXml. Klasa ta udostępnia podstawowy zestaw narzędzi do podpisywania i weryfikacji plików XML przy użyciu certyfikatu X.509. Biorąc pod uwagę, że podpis kwalifikowany to nic innego jak certyfikat dostarczony na bezpiecznym nośniku wygenerowany przez zaufanego dostawcę, zaś XAdES to nic innego jak XML kierunek wydawał się być słuszny. Po drodze zawadziłem jeszcze o projekt XAdES.NET, jednak perspektywa prostego rozwiązania opartego na pojedynczej “klasie z frejmłorka” wydała mi się bardziej atrakcyjna.

Po kilku godzinach na ekranie miałem coś takiego:

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

public class XAdES
{
    private static readonly Regex pattern = new Regex("_(.+?)_", RegexOptions.Compiled);

    private X509Certificate2 _cert;
    private SignedXml _xml;

    public byte[] Content
    {
        get
        {
            if (_xml == null)
            {
                throw new NullReferenceException("Xml not loaded");
            }

            var id = _xml.Signature.Id;
            var match = pattern.Match(id);
            if (!match.Success)
            {
                throw new FormatException("Signature id");
            }

            var objId = match.Groups[1].Value;

            foreach (DataObject dataObject in _xml.Signature.ObjectList)
            {
                if (dataObject.Id == null ||
                    !dataObject.Id.Contains(objId))
                {
                    continue;
                }

                return Convert.FromBase64String(dataObject.Data.Item(0).InnerText);
            }

            throw new FormatException("No objects embedded");
        }
    }

    public X509Certificate2 Certificate
    {
        get { return _cert; }
    }

    public void Load(string filePath)
    {
        var doc = new XmlDocument { PreserveWhitespace = true };
        doc.Load(filePath);

        Load(doc);
    }

    public void LoadXml(string xml)
    {
        var doc = new XmlDocument { PreserveWhitespace = true };
        doc.LoadXml(xml);

        Load(doc);
    }

    private void Load(XmlDocument doc)
    {
        if (doc.DocumentElement == null)
        {
            throw new NullReferenceException("Document root");
        }

        _cert = ExtractCertificate(doc);

        _xml = new SignedXml(doc);
        _xml.SigningKey = new RSACryptoServiceProvider(1024);
        _xml.LoadXml(doc.DocumentElement);
    }

    private X509Certificate2 ExtractCertificate(XmlDocument doc)
    {
        var namespaceManager = new XmlNamespaceManager(doc.NameTable);
        namespaceManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#");
        var node = doc.SelectSingleNode("/ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate", namespaceManager);
        if (node == null)
        {
            throw new FormatException("No certificate");
        }

        return new X509Certificate2(Convert.FromBase64String(node.InnerText));
    }

    public bool Verify(bool verifySIgnatureOnly = false)
    {
        return _xml.CheckSignature(_cert, verifySIgnatureOnly);
    }

    public void CheckRoot(string thumbprint)
    {
        var chain = new X509Chain();
        var result = chain.Build(Certificate);
        if (!result)
        {
            throw new Exception("Unable to build certificate chain");
        }

        if (chain.ChainElements.Count == 0)
        {
            throw new Exception("Certificate chain length is 0");
        }

        if (
            StringComparer.OrdinalIgnoreCase.Compare(chain.ChainElements[chain.ChainElements.Count - 1].Certificate.Thumbprint,
                                                     thumbprint) != 0)
        {
            throw new Exception("Root certificate thumbprint mismatch");
        }
    }
}

Zastosowanie:

var xades = new XAdES();
xades.LoadXml("plik.txt.xades");
xades.Verify();

Klasa weryfikuje podpis typu wewnętrznego tj. kiedy plik podpisu elektronicznego zawiera jednocześnie treść podpisywanego pliku. Innym wariantem jest podpis zewnętrzny gdzie plik XAdES nie zawiera pliku, a jedynie sam podpis w formacie nazwa_pliku.rozszerzeni.xades. Dostosowanie tej klasy do drugiej opcji podpisu nie powinno być problemem.

Kod działa zarówno z podpisem kwalifikowanym jak i niekwalifikowanym. W przypadku podpisu kwalifikowanego należy zainstalować wszystkie nadrzędne certyfikaty oraz aktualne listy CRL. Certyfikat do składnia podpisu nieprawiekwalifikowanego można sobie wygenerować samemu, przy pomocy OpenSSL:

openssl genrsa -des3 -out Private-CA.key 2048 
openssl req -new -x509 -days 3650 -extensions v3_ca -key Private-CA.key -out Public-CA.CRT
openssl genrsa -des3 -out Certificate-Request.key 2048 
openssl req -new -extensions v3_req -key Certificate-Request.key -out SigningRequest.csr
openssl x509 -req -days 3650 -extensions v3_req -in SigningRequest.csr -CA Public-CA.CRT -CAkey Private-CA.key -CAcreateserial -out SSL-Cert-signed-by-Public-CA.CRT
openssl pkcs12 -export -out export.pfx -in SSL-Cert-signed-by-Public-CA.CRT -inkey Certificate-Request.key

Z odpowiednimi wpisami w openssl.cnf:

[ v3_ca ]
basicConstraints           = CA:TRUE
subjectKeyIdentifier       = hash
authorityKeyIdentifier     = keyid:always,issuer:always

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

Tak wygenerowane certyfikaty instalujemy w naszym komputerze. Teraz możemy podpisać takim certyfikatem jakiś plik np. przy użyciu programu proCertum SmartSign, a następnie sprawdzić poprawność podpisu we wcześniej pokazany sposób.