Disassembler

Artificial intelligence is no match for natural stupidity.
13listopad2013

Podepisování XML SHA256 hashi v .NET


.NET mám docela rád a obecně jej považuji za poměrně kvalitní platformu, na které se dá napsat ledacos a která se mi zdá daleko méně overengineered než třeba Java. I přesto se jí jednou za čas povede vyběhnout na mě s nějakou chuťovkou, nad jejímž řešením strávím celý den a zopakuju si u toho svou zásobu invektiv několikrát dokola.

A tady dole váš podpis


Zákon o povinném značení lihu 307/2013 Sb. zavádí od 1. prosince 2013 dvě nové oznamovací povinnosti. § 38 - Oznamovací povinnost držitele a § 43 - Oznamovací povinnost distributora lihu. Obě oznámení se odevzdávají elektronicky v XML formátu skrze aplikace celní správy a odevzdávané XML musí být podepsané zaručeným elektronickým podpisem. Vyrábím tedy aplikaci, která si z firemní databáze nacucá faktury za daný den, vytáhne si z nich, komu byl jaký alkohol prodán, dle specifikací poskládá XML, podepíše jej a pošle. Brnkačka. Když znáte databázi a specifikace, stačí čtyřicet minut a je hotovo. Když dostanete blackbox, databázi bez dokumentace a specifikace vidíte poprvé, za tři dny máte aplikaci taky. Tedy za předpokladu, že nevychytáte nějaký špek jako já.

Naprosto jednoduchý a naivní úryvek C# kódu použitý pro podepisování by tedy mohl vypadat následovně

public static XmlElement GetSignature(XmlDocument doc)
{
    string certPath = "cert.pfx";
    string certPass = "heslo";

    // Nacteni certifikatu vcetne privatniho klice
    X509Certificate2Collection collection = new X509Certificate2Collection();
    collection.Import(certPath, certPass, X509KeyStorageFlags.PersistKeySet);
    X509Certificate2 cert = collection[0];

    // Podpis bude enveloped - tj. vnoreny do root elementu podepisovaneho XML
    Reference reference = new Reference("");
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());

    // Vytvoreni SignedXml objektu a nastaveni kanonizace a privatniho klice
    SignedXml signedXml = new SignedXml(doc);
    signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
    signedXml.SigningKey = cert.PrivateKey;
    signedXml.AddReference(reference);

    // Pridani verejneho klice do objektu pro moznost kontroly podpisu prijemcem
    signedXml.KeyInfo = new KeyInfo();
    signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert, X509IncludeOption.WholeChain));

    // Jachyme, hod ho do stroje
    signedXml.ComputeSignature();
    return signedXml.GetXml();
}

Na velikosti záleží


XML podepsané kódem zmíněným výše pak vyplivne zhruba takovýhle podpis

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
        <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
        <Reference URI="">
            <Transforms>
                <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
            </Transforms>
            <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
            <DigestValue>sjkNgzHUg5QaCVC9ieGvPYyMvHY=</DigestValue>
        </Reference>
    </SignedInfo>
    <SignatureValue>LFj+aAT6Umm9i...iocGB5VJwQ==</SignatureValue>
    <KeyInfo>
        <X509Data>
            <X509Certificate>MIIDxzCCAq+gAw...kR07m839Pc=</X509Certificate>
        </X509Data>
    </KeyInfo>
</Signature>

Což je samozřejmě nádhera, protože všechno funguje a naše práce je hotová. Nebo ne?

Na stránkách celní správy píšou „V souvislosti s přechodem na hashovací algoritmus SHA-2 v podpisech elektronických certifikátů vydávaných certifikačními autoritami v ČR po 1.1.2010 a s odpovídajícím ročním přechodném obdobím bude přecházet na tento algoritmus i Externí komunikační doména celní správy k 1.12.2010. Od tohoto data budou celní správou přijímány a odesílány pouze zprávy označené zaručeným elektronickým podpisem (respektive zaručenou elektronickou značkou) s využitím hash algoritmu SHA256.“ Aha. No dobře. I přesto, že moje *.pfx má deklarovaný algoritmus podpisu sha256RSA, z řádek

<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />

je vidět, že .NET ve výchozím nastavení použil pouze SHA-1. Není tedy nic jednoduššího než kýžené algoritmy deklarovat, protože .NET je strašně fajn a deklaraci podle URI a W3C standardů umožňuje. Upravený kód tedy bude vypadat takto

public static XmlElement GetSignature(XmlDocument doc)
{
    string certPath = "cert.pfx";
    string certPass = "heslo";

    // Nacteni certifikatu vcetne privatniho klice
    X509Certificate2Collection collection = new X509Certificate2Collection();
    collection.Import(certPath, certPass, X509KeyStorageFlags.PersistKeySet);
    X509Certificate2 cert = collection[0];

    // Podpis bude enveloped - tj. vnoreny do root elementu podepisovaneho XML
    Reference reference = new Reference("");
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
    reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256"; // Chci SHA256

    // Vytvoreni SignedXml objektu a nastaveni kanonizace a privatniho klice
    SignedXml signedXml = new SignedXml(doc);
    signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
    signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; // Chci SHA256RSA
    signedXml.SigningKey = cert.PrivateKey;
    signedXml.AddReference(reference);

    // Pridani verejneho klice do objektu pro moznost kontroly podpisu prijemcem
    signedXml.KeyInfo = new KeyInfo();
    signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert, X509IncludeOption.WholeChain));

    // Pada program, prejte si neco
    signedXml.ComputeSignature();
    return signedXml.GetXml();
}

Spustím jej a Visual Studio mi na řádce s ComputeSignature() vyhodí výjimku hlásající

Pro zadaný podpisový algoritmus nelze vytvořit popis SignatureDescription.

Cože? Jak jako nelze? Systémové kryptografické knihovny přece umí SHA-2 už od Windows XP SP3. Jediný eventuální problém by mohl nastat u Win2003 Server a i tam se dá doinstalovat KB938397, které aktualizuje kryptografické knihovny a přidá podporu pro SHA-2. Já ale vyvíjím na sedmičkách, tak jaképak „nelze vytvořit“?

Security by obscurity


Vtip je úplně v něčem jiném, milé děti. Windows i .NET SHA-2 samozřejmě znají, jen jej out-of-the-box .NET nezná v kombinaci s RSA. Takže je potřeba jej naučit, že http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 je jako fakt RSA a zároveň i SHA256. V .NET 4 je to záležitost relativně jednoduchá. V .NET 2.0 je to pak vyloženě na přes držku.

V obou zmíněných verzích .NET existuje jakási statická třída CryptoConfig, která definuje známé typy podpisů, šifrovacích a hashovacích algoritmů, OID a všelijakých dalších kryptografických blbinek. V .NET 4 si můžete definovat vlastní typ skrze metodu AddAlgorithm(). V nižších verzích tato metoda neexistuje, takže se deklarace musí řešit zásahem do .NET Global Assembly Cache (GAC) a úpravou machine.config souborů.

Oba problémy jsou již velmi dobře popsány a vyřešeny komunitou kolem projektu CLR Security, takže jen jednoduše zmíním postupy vedoucí k cíli.

.NET 4


Nejprve ten příjemnější způsob. V .NET 4 je potřeba si definovat vlastní třídu s popisem podpisu za pomoci podědění z třídy SignatureDescription. CLR Security to řeší následovně

public sealed class RSAPKCS1SHA256SignatureDescription : SignatureDescription
{
    public RSAPKCS1SHA256SignatureDescription()
    {
        KeyAlgorithm = typeof(RSACryptoServiceProvider).FullName;
        DigestAlgorithm = typeof(SHA256Managed).FullName;
        FormatterAlgorithm = typeof(RSAPKCS1SignatureFormatter).FullName;
        DeformatterAlgorithm = typeof(RSAPKCS1SignatureDeformatter).FullName;
    }

    public override AsymmetricSignatureDeformatter CreateDeformatter(AsymmetricAlgorithm key)
    {
        if (key == null)
            throw new ArgumentNullException("key");

        RSAPKCS1SignatureDeformatter deformatter = new RSAPKCS1SignatureDeformatter(key);
        deformatter.SetHashAlgorithm("SHA256");
        return deformatter;
    }

    public override AsymmetricSignatureFormatter CreateFormatter(AsymmetricAlgorithm key)
    {
        if (key == null)
            throw new ArgumentNullException("key");

        RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(key);
        formatter.SetHashAlgorithm("SHA256");
        return formatter;
    }
}

Tuto třídu je pak potřeba zaregistrovat v CryptoConfig pod výše zmíněným aliasem. Registrace samozřejmě musí proběhnout předtím, než se alias použije. Ideálně někde na začátku programu, protože CryptoConfig si interně drží reference v nějakém key-value slovníku, takže vícenásobná registrace totožného aliasu by vyvolala výjimku, stěžující si, že alias již existuje.

CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");

Po této akci bude .NET chápat co se po něm chce, ale trápení ještě nemusí být konec.

.NET 2


Zde už CryptoConfig.AddAlgorithm() neexistuje, takže musíme znásilnit celý kryptosystém, který .NET využívá, a to prosím na všech počítačích, kde náš program poběží. K tomu je potřeba si stáhnout hromádku souborů z clrsecurity.codeplex.com. Ze staženého zipu vykopírovat

a vložit je do %WINDIR%\assembly. Touto prostou akcí budou knihovny zaregistrovány v Global Assembly Cache.

Pak je třeba .NETu vysvětlit, že když se po něm bude chtít http://www.w3.org/2001/04/xmldsig-more#rsa-sha256, má si do nových knihoven sáhnout. To se dá vyřešit editací souborů

Soubory jsou v XML formátu, takže na konec, ale ještě před uzavřením kořenového tagu </configuration> je potřeba přidat následující kus XML

<mscorlib>
    <cryptographySettings>
        <cryptoNameMapping>
            <cryptoClasses>
                <cryptoClass RSASHA256SignatureDescription="Security.Cryptography.RSAPKCS1SHA256SignatureDescription, Security.Cryptography, Version=1.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
            </cryptoClasses>
            <nameEntry name="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" class="RSASHA256SignatureDescription" />
        </cryptoNameMapping>
    </cryptographySettings>
</mscorlib>

Výše uvedená úprava XML je platná pro verzi aktuální v době psaní článku. Pro novější verze budete muset Version=1.6.0.0 upravit na číslo vaší verze. To společně s hodnotou PublicKeyToken zjistíte ze záznamu v GAC - tj. ve složce %WINDIR%\assembly

Stejně jako u .NET 4, kryptografické funkce začnou rozeznávat rsa-sha256, ale program ještě nutně nemusí fungovat.

Komplikace kompilace


V případě že jste úspěšně přemluvili .NET, aby signaturu rozeznával, je možné, že vám program bude vyhazovat výjimku stále na stejném místě, tentokrát však s novým zněním. V případě že váš program podepisuje jak má, máte o startost méně. V případě, že na vás vyskočí

Byl zadán neplatný algoritmus.

čeká vás ještě trocha teorie.

Aby byla legrace zaručena, v systémech Windows existuje možnost použití různých Cryptographic Service Providers (CSP) z nichž každý může umět jiné algoritmy. Od XP SP3 existují v prostředí Windows následující CSP dodávané přímo Microsoftem

A hned vám prozradím, že ME_CP neumí SHA-2. Jaký CSP se použije ovšem závisí na certifikátu samotném, protože při jeho vytvoření existuje možnost definovat CSP skrze kterého má certifikát fungovat. Tedy v případě, že testujete aplikaci na self-signed certifikátu, s velkou pravděpodobností nemáte CSP explicitně definovaný a Windows jako výchozí použijou ME_CP. Pokud nemáte možnost nechat vygenerovat certifikát tak, aby jeho výchozí CSP byl ME_RSA_AES_CP, musíte certifikát trochu přehnout přes koleno, a to tak, že nejprve inicializujete prazdného CSP a až pak skrze něj nahrajete soukromý klíč certifikátu. V takovém případě se při inicializaci použije ME_RSA_AES_CP, který už SHA-2 bez problému umí. Programové řešení tedy vypadá následovně

  1. Načíst certifikát (ME_CP)
  2. Exportovat privátní klíč
  3. Inicializovat prázdného CSP (ME_RSA_AES_CP)
  4. Importovat privátní klíč skrze nového CSP
public static XmlElement GetSignature(XmlDocument doc)
{
    string certPath = "cert.pfx";
    string certPass = "heslo";

    // Nacteni certifikatu vcetne privatniho klice
    X509Certificate2Collection collection = new X509Certificate2Collection();
    collection.Import(certPath, certPass, X509KeyStorageFlags.Exportable); // Klic musi byt exportovatelny, jinak ExportCspBlob() selze
    X509Certificate2 cert = collection[0];
    // Export privatniho klice skrze stareho CSP
    byte[] privateKeyBlob = ((RSACryptoServiceProvider)cert.PrivateKey).ExportCspBlob(true);
    // Inicializace noveho CSP. Typ 24 je ME_RSA_AES_CP, ktery umi SHA-2
    RSACryptoServiceProvider privateKey = new RSACryptoServiceProvider(new CspParameters(24));
    // Import privatniho klice skrze noveho CSP
    privateKey.ImportCspBlob(privateKeyBlob);

    // Podpis bude enveloped - tj. vnoreny do root elementu podepisovaneho XML
    Reference reference = new Reference("");
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
    reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256"; // Chci SHA256

    // Vytvoreni SignedXml objektu a nastaveni kanonizace a privatniho klice
    SignedXml signedXml = new SignedXml(doc);
    signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
    signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; // Chci SHA256RSA
    signedXml.SigningKey = privateKey; // Pouziti klice skrze SHA-2 kompatibliniho CSP
    signedXml.AddReference(reference);

    // Pridani verejneho klice do objektu pro moznost kontroly podpisu prijemcem
    signedXml.KeyInfo = new KeyInfo();
    signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert, X509IncludeOption.WholeChain));

    // Ted uz to snad pojede
    signedXml.ComputeSignature();
    return signedXml.GetXml();
}

Ufff


Vygenerovaný podpis konečně vypadá tak jak si celní správa a I.CA přeje

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
        <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
        <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
        <Reference URI="">
            <Transforms>
                <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
            </Transforms>
            <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
            <DigestValue>PF5CNc941eaXO37UCWM2fLj8aEFnoWKbjabjgkjtlI8=</DigestValue>
        </Reference>
    </SignedInfo>
    <SignatureValue>q6FUoiLO9b+8S...bkJ2loqIQ==</SignatureValue>
    <KeyInfo>
        <X509Data>
            <X509Certificate>MIIDxzCCAq+g...N8/1Tavr+9Jnj9kR07m839Pc=</X509Certificate>
        </X509Data>
    </KeyInfo>
</Signature>

Děkují tímto Microsoftu, že programátory nenechává zakrnět a jednou za čas po nich vyžaduje tolik přemýšlení, že se jim až kouří z uší. Malou útěchou pak je, že .NET 4.5 už RSAPKCS1SHA256SignatureDescription umí, byť v docela divokém namespace System.Deployment.Internal.CodeSigning.

Celní správa na svých stránkách jakési vágní informace na téma podepisování SHA-2 hashi v prostředí Windows má, ale část je nepřesná (URI v SignatureMethod a DigestMethod nejsou stejné) a veselé historky zmiňující výběr CSP úplně chybí. Je ovšem možné, že v některých případech je CSP vybráno správně hned napoprvé. V mém případě s načítáním *.pfx tomu tak ovšem nebylo.

Update


Výše zmíněná funkce pro podpis nebere v potaz whitespace znaky v XML dokumentu. Pro další studium pokračujte článkem ORO WebService - Testovací tragikomedie.