using System; using System.Text; using System.Diagnostics; using CryptoSysPKI; namespace MexicoSATcsharp { class MexicoSAT { [STAThread] static void Main(string[] args) { Console.WriteLine("PKI Version={0}", General.Version()); Test_Mex_Show_Encoding(); Test_Mex_Make_Digest(); Test_Mex_Sign_Data(); Test_Mex_AuthCert(); Test_Mex_Verify(); } static void Test_Mex_Show_Encoding() { string strData; byte[] b; string digest; int n; Console.WriteLine("SHOW LATIN-1 AND UTF-8 ENCODINGS..."); // // For SAT, the message data required for the MD5 digest function must be in // UTF-8 format. // For Mexican, this only matters for accented characters like [óéíáñ]. // The best way in C# is to convert directly to byte format using UTF-8 // encoding and create the message digest directly from these bytes. // As an aside, let's do a simple test first. // Given a string that includes some Latin-1 accented characters: strData = "abcóéíáñ"; Console.WriteLine("Original Latin-1 string={0}", strData); Console.WriteLine("# of characters in Latin-1 string = {0}", strData.Length); // Check and see if this tests as valid UTF-8 n = Cnv.CheckUTF8(strData); Console.WriteLine("Cnv.CheckUTF8(strData) returns {0} (expected 0 => not valid UTF-8)", n); // Convert directly to bytes using default encoding // (NB results may differ depending on your default encoding setup) // We get: // Latin-1 bytes: 61 62 63 f3 e9 ed e1 f1 b = System.Text.Encoding.Default.GetBytes(strData); DisplayBytes("Latin-1 bytes: ", b); // Now convert directly to bytes using UTF-8 encoding // You should get: // UTF-8 bytes: 61 62 63 c3 b3 c3 a9 c3 ad c3 a1 c3 b1 // THIS is the input we need when using a CryptoSys PKI Hash function for SAT purposes b = System.Text.Encoding.UTF8.GetBytes(strData); DisplayBytes("UTF-8 bytes: ", b); Console.WriteLine("# of bytes when converted to UTF-8 = {0}", b.Length); // because this digest (from the string) is wrong for SAT purposes... digest = Hash.HexFromString(strData, HashAlgorithm.Md5); Console.WriteLine("MD5(string)={0} (wrong!)", digest); // Compare this to the hash of the bytes directly (which is correct) digest = Hash.HexFromBytes(b, HashAlgorithm.Md5); Console.WriteLine("MD5(bytes) ={0} (correct)", digest); } static void Test_Mex_Make_Digest() { // INPUT: message data in string format; digest algorithm // OUTPUT: message digest string strData; byte[] b; string digest; Console.WriteLine("\nMAKING MD5 DIGEST..."); // Original SAT input string in Latin-1 format strData = "||2.0|A|1|2009-08-16T16:30:00|1|2009|ingreso|Una sola exhibición|350.00|5.25|397.25|ISP900909Q88|Industrias del Sur Poniente, S.A. de C.V.|Alvaro Obregón|37|3|Col. Roma Norte|México|Cuauhtémoc|Distrito Federal|México|06700|Pino Suarez|23|Centro|Monterrey|Monterrey|Nuevo Léon|México|95460|CAUR390312S87|Rosa María Calderón Uriegas|Topochico|52|Jardines del Valle|Monterrey|Monterrey|Nuevo León|México|95465|10|Caja|Vasos decorados|20.00|200|1|pieza|Charola metálica|150.00|150|IVA|15.00|52.50||"; Console.WriteLine("Original Latin-1 string={0}", strData); // Convert directly to a byte array using the System.Text.Encoding function b = System.Text.Encoding.UTF8.GetBytes(strData); // Generate the MD5 message digest directly from these bytes digest = Hash.HexFromBytes(b, HashAlgorithm.Md5); Console.WriteLine("Digest={0}", digest); } static void Test_Mex_Sign_Data() { // INPUT: message data to be signed; RSA private key file; private key password. // OUTPUT: signature block // NOTE: Error testing is done here just using crude Debug.Assert statements // A proper implementation should wipe any sensitive data on error and exit gracefully. string strData; StringBuilder sbPassword; StringBuilder sbPrivateKey; byte[] b; byte[] block; int keyBytes; Console.WriteLine("\nSIGNING THE DATA..."); // Read in the private key from encrypted .key file // (files need to be in the default working directory) string strKeyFile = "aaa010101aaa_CSD_01.key"; // CAUTION: DO NOT hardcode production passwords! sbPassword = new StringBuilder("a0123456789"); // Note that the key is read into a StringBuilder, not a string. sbPrivateKey = Rsa.ReadEncPrivateKey(strKeyFile, sbPassword.ToString()); Debug.Assert(sbPrivateKey.Length > 0); // How big is this key? keyBytes = Rsa.KeyBytes(sbPrivateKey.ToString()); Debug.Assert(keyBytes > 0); Console.WriteLine("Key length is {0} bits/{1} bytes", Rsa.KeyBits(sbPrivateKey.ToString()), keyBytes); // Original SAT input string in Latin-1 format strData = "||2.0|A|1|2009-08-16T16:30:00|1|2009|ingreso|Una sola exhibición|350.00|5.25|397.25|ISP900909Q88|Industrias del Sur Poniente, S.A. de C.V.|Alvaro Obregón|37|3|Col. Roma Norte|México|Cuauhtémoc|Distrito Federal|México|06700|Pino Suarez|23|Centro|Monterrey|Monterrey|Nuevo Léon|México|95460|CAUR390312S87|Rosa María Calderón Uriegas|Topochico|52|Jardines del Valle|Monterrey|Monterrey|Nuevo León|México|95465|10|Caja|Vasos decorados|20.00|200|1|pieza|Charola metálica|150.00|150|IVA|15.00|52.50||"; //Console.WriteLine("Original Latin-1 string={0}", strData); Console.WriteLine("# of characters in Latin-1 string = {0}", strData.Length); // Convert directly to a byte array using the System.Text.Encoding function b = System.Text.Encoding.UTF8.GetBytes(strData); Console.WriteLine("# of bytes when converted to UTF-8 = {0}", b.Length); // Encode this data ready for signing into an `Encoded Message For Signature' block // using PKCS#1 v1.5 method and the MD5 hash algorithm. block = Rsa.EncodeMsgForSignature(keyBytes, b, HashAlgorithm.Md5); Debug.Assert(block.Length > 0); Console.WriteLine("Encoded Block=\n{0}", Cnv.ToHex(block)); // Now sign using the RSA private key block = Rsa.RawPrivate(block, sbPrivateKey.ToString()); Debug.Assert(block.Length > 0); // Display in hex format Console.WriteLine("Signature in hex=\n{0}", Cnv.ToHex(block)); // Display in base64 format Console.WriteLine("Signature in base64=\n{0}", System.Convert.ToBase64String(block)); // Clean up sensitive data (NB should do on error, too, in a real program) Wipe.String(sbPassword); Wipe.String(sbPrivateKey); Wipe.Data(block); } static void Test_Mex_Verify() { // INPUT: signature in base64 format; signer's X.509 certificate; // message data that has been signed (or its digest). // OUTPUT: "valid signature" or "invalid signature". string strSello; string strCertificado; StringBuilder sbPublicKey; byte[] block; int keyBytes; byte[] digest; Console.WriteLine("\nVERIFYING A SIGNATURE..."); // Given the signature in base64 format strSello = "UlUSwGNEicfigV6i4RhTy0eb2RYWFYyFatJFcM/u5Wlkb5XRxXiCizTGw5Yxz9oZNk8msAgO4C5Gevjh+S2TJPZueYhaQeZlo6k0rE3CQexkOGVRpHkvAoAgOM5kGKzYe24DKZbTgjNL+ai+tbhEHmRAFcpv2rDpehbL3w6BnYU="; // And the X.509 certificate (as a base64 string) strCertificado = "MIIDhDCCAmygAwIBAgIUMTAwMDEyMDAwMDAwMDAwMjI1MTcwDQYJKoZIhvcNAQEFBQAwgcMxGTAXBgNVBAcTEENpdWRhZCBkZSBNZXhpY28xFTATBgNVBAgTDE1leGljbywgRC5GLjELMAkGA1UEBhMCTVgxGjAYBgNVBAMTEUFDIGRlIFBydWViYXMgU0FUMTYwNAYDVQQLFC1BZG1pbmlzdHJhY2nzbiBkZSBTZWd1cmlkYWQgZGUgbGEgSW5" + "mb3JtYWNp824xLjAsBgNVBAoUJVNlcnZpY2lvIGRlIEFkbWluaXN0cmFjafNuIFRyaWJ1dGFyaWEwHhcNMDgwODIxMTUyMjA4WhcNMTAwODIxMTUyMjA4WjCBmDElMCMGA1UELRMcQUFBMDEwMTAxQUFBIC8gQUFBQTAxMDEwMUFBQTEeMBwGA1UEBRMVIC8gQUFBQTAxMDEwMUhERlJYWDAxMRIwEAYDVQQKEwlNYXRyaXogU0ExEzARBgNVBA" + "sTClVuaWRhZCAxMCAxEjAQBgNVBAMTCU1hdHJpeiBTQTESMBAGA1UEKRMJTWF0cml6IFNBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDpmiW1q9gyzCFtMcbaFDJexk2IpLoTdNXg4ToGRZ/f+hIjmj3N6ODWX1ARNFGYocEHf113GpW5Oe/mj6UqhBpiH4JRTNR4Udb8myJTArIlODynVHuIUuyhKo7gbMbDdXjilTAYY2XWQuQ7aDtWw" + "ntUmNg4vAC/F3OtRz3+y9wM5QIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQUFAAOCAQEAafyD4gMsOvq7E3raPntmQlJTxpWwNySqskE7fe23HVL9UKFCUlWWx/W8gluxIX9S19y17iWnGbtmbNddHxG5PznPsy/a8PlwNHjDW0FOpia2LsvDrNcdPiJhzL/1OVagkenffFf8bLEetF3ktxZ7ifcH1yxV" + "xpZ7PS/pe8YIOpWRuMmTV4ypGdsw9TW3HVP5IJ/canuQGPTb3LQ8ojihW2dHnC6ojaWW4GHFSZAPhQJ/DaH/UgFjaQke/RBtoAketfROdG+1qYeA1q/is04O4AXNmMByGp7ZnvGNrO9LDBvs3eKN4ZYcQyjxFEbr1X/xUqHCRF1VEkkC5jJQ1ktC4g=="; // Read in Public key from X.509 certificate sbPublicKey = Rsa.GetPublicKeyFromCert(strCertificado); Debug.Assert(sbPublicKey.Length > 0); keyBytes = Rsa.KeyBytes(sbPublicKey.ToString()); Console.WriteLine("Key length is {0} bits/{1} bytes", Rsa.KeyBits(sbPublicKey.ToString()), keyBytes); // Convert the signature into an array of bytes block = System.Convert.FromBase64String(strSello); Console.WriteLine("Signature block is {0} bytes long", block.Length); // Check the sizes match. If not: "invalid signature" error if (block.Length != keyBytes) { Console.WriteLine("ERROR: Invalid signature"); return; } // Decrypt using RSA public key block = Rsa.RawPublic(block, sbPublicKey.ToString()); if (block.Length == 0) { Console.WriteLine("ERROR: Invalid signature"); return; } Console.WriteLine("Decrypted block, EM=\n{0}", Cnv.ToHex(block)); // METHOD 1. If we have an independently-generated hash value, we extract // the digest from the EMSA-PKCS1-V1_5 encoded block and compare // the hash values directly. string newHash = "4cd8ed248d7a02314c50778a37d1522d"; // Decode this block to extract the message digest digest = Rsa.DecodeDigestForSignature(block); if (digest.Length == 0) { Console.WriteLine("ERROR: Invalid signature"); return; } Console.WriteLine("Digest={0}", Cnv.ToHex(digest)); // Compare the hash value hex strings (careful with the case) if (String.Compare(newHash, Cnv.ToHex(digest), true) == 0) Console.WriteLine("VALID SIGNATURE"); else Console.WriteLine("ERROR: Invalid signature"); // METHOD 2. If we have the original message data, we create the MD5 message // digest of that and then do as in method (1) above. // i.e. newHash = Hash.HexFromBytes(message, HashAlgorithm.Md5); // METHOD 3. Instead of decoding the block, we apply the message encoding // to the original message data (or its digest) and compare the encoded blocks // themselves. // Given a message digest (in bytes) create a new EMSA-PKCS1-V1_5 encoded block byte[] block2 = Rsa.EncodeDigestForSignature(keyBytes, Cnv.FromHex(newHash), HashAlgorithm.Md5); Console.WriteLine("EM'=\n{0}", Cnv.ToHex(block2)); // Amazingly, .NET does not have a memcmp() function to compare byte arrays directly, // so we compare the blocks as hex strings instead if (String.Compare(Cnv.ToHex(block), Cnv.ToHex(block2)) == 0) Console.WriteLine("VALID SIGNATURE"); else Console.WriteLine("ERROR: Invalid signature"); } static void Test_Mex_AuthCert() { // INPUT: X.509 certificate to be authenticated; issuer's certificate [; issuer's issuer's certificate;...]. // OUTPUT: "certificate is authentic", "certificate is not authentic" Console.WriteLine("\nAUTHENTICATING A CERTIFICATE..."); string strCertFile = "aaa010101aaa_CSD_01.cer"; // Authenticate X509 certificate using X509_VerifyCert, if you have the signer's certificate string strIssuerCert = "AC_SAT2048.cer"; if (X509.VerifyCert(strCertFile, strIssuerCert) == 0) Console.WriteLine("OK, certificate {0} was issued by holder of cert {1}", strCertFile, strIssuerCert); else Console.WriteLine("AUTHENTICATION ERROR: Certificate {0} was NOT issued by holder of cert {1}", strCertFile, strIssuerCert); // TODO: authenticate the AC_SAT2048 certificate as well (it was issued by the // Agencia Registradora Central de Pruebas), and so on up the chain } static private void DisplayBytes(string msg, byte[] b) { int i; Console.Write("{0}", msg); for (i = 0; i < b.Length; i++) Console.Write("{0:x2} ", b[i]); Console.Write("\n"); } } }