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");
}
}
}