.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
- Security.dll
- Security.Cryptography.dll
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ů
- %WINDIR%\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.config
- %WINDIR%\Microsoft.NET\Framework64\v2.0.50727\CONFIG\machine.config
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
- Microsoft Enhanced Cryptographic Provider v1.0 (ME_CP 1.0)
- Microsoft Enhanced RSA and AES Cryptographic Provider (ME_RSA_AES_CP)
- Microsoft Base Smart Card Crypto Provider (MBSC_CP)
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ě
- Načíst certifikát (ME_CP)
- Exportovat privátní klíč
- Inicializovat prázdného CSP (ME_RSA_AES_CP)
- 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.