The CryptoSys PKI Toolkit includes support for the RSA private key files published by the Servicio de Administración Tributaria in Mexico. You can use the toolkit to generate the required MD5 digest, convert your X.509 certificate file to a base64 format "certificado" string, create a digital signature "sello" suitable for use in your XML files, and verify such a signature.
Code on this page | SAT Mexico downloads and validation site | Sample files and code | Sample VB6 code | Sample VB.NET code | Sample C# code | Signing and UTF-8 Data | Creating the message digest of the piped-string | Creating a signature | Checking the signature | Verifying a signature | SAT X.509 Certificates and Base64 Format | SAT X.509 Certificate Serial Numbers | Contact Us | Comment
The code snippets on this page can be displayed or hidden if Javascript is turned on: Show all code Hide all code
Please note that VB6 refers to "classic" Visual Basic VB6/VBA whereas VB.NET refers to the newer Visual Basic-dot-NET/VB2005/2008/etc. As far as we are concerned, these are different languages and need to be coded differently for CryptoSys PKI. Sample code in VB6 and VB.NET is provided below. There is also some code in C Sharp (C#).
Sample key files and lots of other information including a sample XML file (sadly, out of date)
Muestra.xml (zipped, 1 kB) can be downloaded
from ftp://ftp2.sat.gob.mx in folder
/asistencia_ftp/publicaciones/solcedi.
SAT provide a page to validate your XML files and digital signatures at Validador de forma y sintaxis de Comprobantes Fiscales Digitales.
There is a problem with this website's security certificate. The security certificate presented by this website was issued for a different website's address. www.consulta.sat.gob.mx uses an invalid security certificate. The certificate is only valid for www.sat.gob.mx. (error code: ssl_error_bad_cert_domain).
This is not a good example to set for a site meant to be promoting security!Here are some test certificates and private keys provided by S.A.T. and some sample XML files updated to SAT version 2: Muestra_v2_test-200908.zip (7 kB). The XML file Muestra_v2_base2.xml is the base file before signing and Muestra_v2_signed2.xml is the file after signing, which validates OK.
This file includes all the VB6/VBA code on this page plus some more.
You will need to include the module basCrPKI in your project.
VB6/VBA code (zipped 6 kB)
Mex_CreateSignature: Creates a signature (sello) value in base64 format given the piped-string input and the private key.
(more...)Mex_ExtractDigestFromSignature: Extracts the message digest from a signature (sello) string using the X.509 certificate (certificado) value.
(more...)Mex_CreateDigestFromString: Creates the MD5 digest of the input string after converting to UTF-8 encoding.
(more...)Mex_CheckKeyAndCertMatch: Checks that the keys in the private key and certificate matchMex_Convert_Latin1_To_UTF8: Checks if a string is valid UTF-8 and converts between Latin-1 and UTF-8 encodings.
(more...)Mex_ValidateCert: Checks that a given X.509 certificate really was issued by the issuer and has not expiredMex_CertToBase64String: Converts an X.509 certificate file into a base64 string suitable for Certificado field in XML,
and shows how this string form can be treated just like the file itself.
(more...)Mex_SAT_SerialNumber: Extracts the serial number from a SAT-issued X.509 certificate and displays in base64 format.
(more...)Mex_QueryCertString: Extracts various details from a certificate stringJorge Perez has kindly prepared a port of an earlier version of this code to PowerBuilder: see Example in Powerbuilder Script.
The VB6 code above ported to VB.NET.
You will need to reference the diCrSysPKINet.dll class library in your project
VB.NET/VB200x code (zipped 6 kB)
Some examples in C-sharp.
You will need to reference the diCrSysPKINet.dll class library in your project
The catch with signing digital documents according to the SAT requirements
is that the text must be in UTF-8 format before creating the MD5 message digest. This only matters where the text contains
accented characters like áéíóúñ.
If your original string is in Latin-1 format (8-bit ASCII) and includes an accented character,
you will not obtain the correct message digest.
The in-built function CNV_CheckUTF8 will check if the string is valid UTF-8 or not.
If not, use the CNV_UTF8FromLatin1 function to convert.
Code to test for valid UTF-8: Show/hide VB6 code (requires Javascript)
'@ $Proc: Mex_Convert_Latin_To_UTF8 '@ $Lang: VB6 $ '@ Checks if a string is valid UTF-8 and converts between Latin-1 and UTF-8 encodings Dim strData As String Dim strDataUTF8 As String Dim strDataLatin1 As String Dim nRet As Long Dim nLen As Long ' Our original string data is in "Latin-1" encoding strData = "Asociación Mexicana de Estándares para el Comercio Electrónico A.C.|México|" Debug.Print "INPUT: " & strData ' Is it valid UTF-8? nRet = CNV_CheckUTF8(strData) Debug.Print "CNV_CheckUTF8 returns " & nRet & " (0 => Not valid UTF-8)" ' So convert to UTF-8 nLen = CNV_UTF8FromLatin1("", 0, strData) If nLen < 0 Then Debug.Print "Failed to convert to UTF-8: " & nLen Exit Function End If strDataUTF8 = String(nLen, " ") nLen = CNV_UTF8FromLatin1(strDataUTF8, nLen, strData) ' Which may not display correctly in VB6...! Debug.Print "UTF-8: " & strDataUTF8 ' Now convert back to Latin-1 nLen = CNV_Latin1FromUTF8("", 0, strDataUTF8) strDataLatin1 = String(nLen, " ") nLen = CNV_Latin1FromUTF8(strDataLatin1, nLen, strDataUTF8) Debug.Print "Latin-1: " & strDataLatin1
The fields from the XML file are concatenated in a certain order and separated with the pipe symbol "|" to create the "piped string". This must be in UTF-8 encoding. An MD5 message digest of this input is then formed. This digest is used both to create the signature and to verify a given signature. Note we convert the input directly to a byte array before digesting, rather than using the string itself. Alternatively, use a UTF-8-compliant text editor to create a file and use that.
Code to create the message digest of the piped-string: Show/hide VB6 code (requires Javascript) Show/hide VB.NET code
'@Proc: Mex_CreateDigestFromString '@Lang: VB6 '@ Creates the MD5 digest of the input string after converting to UTF-8 encoding Dim strData As String Dim strDataUTF8 As String Dim strDigest As String Dim nRet As Long Dim nLen As Long ' INPUT: Our original string data in "Latin-1" encoding 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||" Debug.Print "INPUT=" & strData ' 1. Convert Latin-1 to UTF-8, if necessary If CNV_CheckUTF8(strData) = 0 Then ' Our input string is in Latin-1 and we want UTF-8 nLen = CNV_UTF8FromLatin1("", 0, strData) strDataUTF8 = String(nLen, " ") nLen = CNV_UTF8FromLatin1(strDataUTF8, Len(strDataUTF8), strData) Else strDataUTF8 = strData End If ' 2. Create the message digest hash ' but first dimension the string to receive it strDigest = String(PKI_MD5_CHARS, " ") nRet = HASH_HexFromString(strDigest, Len(strDigest), strDataUTF8, Len(strDataUTF8), PKI_HASH_MD5) Debug.Print "HASH_HexFromString returns " & nRet ' OUTPUT: Display digest in hex format Debug.Print "Digest=" & strDigest Mex_CreateDigestFromString = strDigest
'@Proc: Mex_CreateDigestFromString '@Lang: VB.NET '@ Creates the MD5 digest of the input string after converting to UTF-8 encoding Dim strData As String Dim abDataUTF8 As Byte() Dim strDigest As String ' INPUT: Our original string data in "Latin-1" encoding 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("INPUT=" & strData) ' 1. Convert to UTF-8 ' -- In VB.NET, it is easier to convert directly to a byte array abDataUTF8 = System.Text.Encoding.UTF8.GetBytes(strData) ' 2. Create the message digest hash strDigest = Hash.HexFromBytes(abDataUTF8, HashAlgorithm.Md5) ' OUTPUT: Display digest in hex format Console.WriteLine("Digest=" & strDigest) Mex_CreateDigestFromString = strDigest
To create a signature (sello) we need the "piped-string" input created from the XML file according to the rules specified by S.A.T. We also need the private key file and its password. The piped-string is converted to UTF-8 encoding, digested with MD5, and then the digest is passed to the RSA private key function to create the signature. Finally the signature value is encoded into base64 format suitable for inclusion in a Sello field in the XML file.
Code to create a signature (sello) value in base64 format: Show/hide VB6 code (requires Javascript) Show/hide VB.NET code
'@Proc: Mex_CreateSignature '@Lang: VB6 '@ Creates a signature (sello) value in base64 format given the piped-string input and the private key Dim strDataFile As String Dim strKeyFile As String Dim strPassword As String Dim strPrivateKey As String Dim abDigest() As Byte Dim abBlock() As Byte Dim nBlockLen As Long Dim nLen As Long Dim nRet As Long Dim strBase64 As String ' INPUT: File containing piped-string formed from XML doc in UTF-8 format (NOTE: no Unicode markers in the file), ' private key file and its secret password(!) strDataFile = "Muestra-v2_PipedString-UTF8.txt" strKeyFile = "aaa010101aaa_CSD_01.key" ' Test password - CAUTION: DO NOT hardcode production passwords! strPassword = "a0123456789" ' 1. Form the message digest hash of the piped-string directly from the file ReDim abDigest(PKI_MD5_BYTES - 1) nRet = HASH_File(abDigest(0), PKI_MD5_BYTES, strDataFile, PKI_HASH_MD5) Debug.Print "HASH_File returns " & nRet If nRet <= 0 Then Debug.Print "ERROR: Failed to create hash of file. Error code: " & nRet Exit Sub End If ' Display in hex Debug.Print "Digest=" & cnvHexStrFromBytes(abDigest) ' 2. Sign the message digest using the private key ' 2.1 Read in private key from encrypted .key file strPrivateKey = rsaReadPrivateKey(strKeyFile, strPassword) If Len(strPrivateKey) = 0 Then Debug.Print "ERROR: Failed to read private key" Exit Sub End If ' -- show we got something Debug.Print "Private key is " & RSA_KeyBits(strPrivateKey) & " bits long" ' 2.2 Encode the digest ready for signing with `Encoded Message for Signature' block using PKCS#1 v1.5 method nBlockLen = RSA_KeyBytes(strPrivateKey) ReDim abBlock(nBlockLen - 1) nLen = RSA_EncodeMsg(abBlock(0), nBlockLen, abDigest(0), PKI_MD5_BYTES, PKI_EMSIG_DEFAULT + PKI_EMSIG_DIGESTONLY + PKI_HASH_MD5) If nLen < 0 Then Debug.Print "RSA_EncodeMsg: ERROR: " & nRet Exit Sub End If Debug.Print "INPUT BLOCK= " & cnvHexStrFromBytes(abBlock) ' 2.3 Sign using the RSA private key nRet = RSA_RawPrivate(abBlock(0), nBlockLen, strPrivateKey, 0) ' Display in hex Debug.Print "OUTPUT BLOCK=" & cnvHexStrFromBytes(abBlock) ' 2.4 Clean up strPrivateKey = wipeString(strPrivateKey) strPassword = wipeString(strPassword) ' 3. Convert to base64 and output result strBase64 = cnvB64StrFromBytes(abBlock) Debug.Print "SIGNATURE VALUE=" & strBase64
'@Proc: Mex_CreateSignature '@Lang: VB.NET '@ Creates a signature (sello) value in base64 format given the piped-string input and the private key Dim strDataFile As String Dim strKeyFile As String Dim strPassword As String Dim sbPrivateKey As New StringBuilder Dim abDigest() As Byte Dim abBlock() As Byte Dim nBlockLen As Integer ''Dim nLen As Integer ''Dim nRet As Integer Dim strBase64 As String ' INPUT: File containing piped-string formed from XML doc in UTF-8 format (NOTE: no Unicode markers in the file), ' private key file and its secret password(!) strDataFile = "Muestra-v2_PipedString-UTF8.txt" strKeyFile = "aaa010101aaa_CSD_01.key" ' Test password - CAUTION: DO NOT hardcode production passwords! strPassword = "a0123456789" ' 1. Form the message digest hash of the piped-string directly from the file abDigest = Hash.BytesFromFile(strDataFile, HashAlgorithm.Md5) If abDigest.Length <= 0 Then Console.WriteLine("ERROR: Failed to create hash of file.") Exit Sub End If ' Display in hex Console.WriteLine("Digest=" & Cnv.ToHex(abDigest)) ' 2. Sign the message digest using the private key ' 2.1 Read in private key from encrypted .key file sbPrivateKey = Rsa.ReadEncPrivateKey(strKeyFile, strPassword) If sbPrivateKey.Length = 0 Then Console.WriteLine("ERROR: Failed to read private key") Exit Sub End If ' -- show we got something Console.WriteLine("Private key is " & Rsa.KeyBits(sbPrivateKey.ToString) & " bits long") ' 2.2 Encode the digest ready for signing with `Encoded Message for Signature' block using PKCS#1 v1.5 method nBlockLen = Rsa.KeyBytes(sbPrivateKey.ToString) abBlock = Rsa.EncodeDigestForSignature(nBlockLen, abDigest, HashAlgorithm.Md5) If abBlock.Length = 0 Then Console.WriteLine("ERROR with Rsa.EncodeDigestForSignature") Exit Sub End If Console.WriteLine("INPUT BLOCK= " & Cnv.ToHex(abBlock)) ' 2.3 Sign using the RSA private key abBlock = Rsa.RawPrivate(abBlock, sbPrivateKey.ToString) ' Display in hex Console.WriteLine("OUTPUT BLOCK=" & Cnv.ToHex(abBlock)) ' 2.4 Clean up Wipe.String(sbPrivateKey) ' 3. Convert to base64 and output result strBase64 = System.Convert.ToBase64String(abBlock) Console.WriteLine("SIGNATURE VALUE=" & strBase64)
Given a signature (Sello) field from an XML file and the associated X.509 certificate (Certificado) string, we can extract the message digest from the signature. If this operation fails, it means either we have the wrong certificate or the signature is corrupted. If it succeeds, then we must also check that the message digest matches the digest for the signed XML document. Note that being able to extract a digest does not mean that document signature is valid, just that it was correctly formed by the holder of the certificate.
Code to extract the digest from a signature (sello) string in an XML file: Show/hide VB6 code (requires Javascript) Show/hide VB.NET code
'@Proc: Mex_ExtractDigestFromSignature '@Lang: VB6 '@ Extracts the message digest from a signature (sello) string using the X.509 certificate (certificado) value Dim strPublicKey As String Dim strSello As String Dim strCertificado As String Dim abMsg() As Byte Dim abData() As Byte Dim nRet As Long Dim nSigLen As Long Dim nMsgLen As Long Dim strDigestHex As String ' INPUT: Base64 strings extracted from the XML file (Ref: Muestra_v2_signed2.xml) strSello = "UlUSwGNEicfigV6i4RhTy0eb2RYWFYyFatJFcM/u5Wlkb5XRxXiCizTGw5Yxz9oZNk8msAgO4C5Gevjh+S2TJPZueYhaQeZlo6k0rE3CQexkOGVRpHkvAoAgOM5kGKzYe24DKZbTgjNL+ai+tbhEHmRAFcpv2rDpehbL3w6BnYU=" strCertificado = "MIIDhDCCAmygAwIBAgIUMTAwMDEyMDAwMDAwMDAwMjI1MTcwDQYJKoZIhvcNAQEFBQAwgcMxGTAXBgNVBAcTEENpdWRhZCBkZSBNZXhpY28xFTATBgNVBAgTDE1leGljbywgRC5GLjELMAkGA1UEBhMCTVgxGjAYBgNVBAMTEUFDIGRlIFBydWViYXMgU0FUMTYwNAYDVQQLFC1BZG1pbmlzdHJhY2nzbiBkZSBTZWd1cmlkYWQgZGUgbGEgSW5" & _ "mb3JtYWNp824xLjAsBgNVBAoUJVNlcnZpY2lvIGRlIEFkbWluaXN0cmFjafNuIFRyaWJ1dGFyaWEwHhcNMDgwODIxMTUyMjA4WhcNMTAwODIxMTUyMjA4WjCBmDElMCMGA1UELRMcQUFBMDEwMTAxQUFBIC8gQUFBQTAxMDEwMUFBQTEeMBwGA1UEBRMVIC8gQUFBQTAxMDEwMUhERlJYWDAxMRIwEAYDVQQKEwlNYXRyaXogU0ExEzARBgNVBA" & _ "sTClVuaWRhZCAxMCAxEjAQBgNVBAMTCU1hdHJpeiBTQTESMBAGA1UEKRMJTWF0cml6IFNBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDpmiW1q9gyzCFtMcbaFDJexk2IpLoTdNXg4ToGRZ/f+hIjmj3N6ODWX1ARNFGYocEHf113GpW5Oe/mj6UqhBpiH4JRTNR4Udb8myJTArIlODynVHuIUuyhKo7gbMbDdXjilTAYY2XWQuQ7aDtWw" & _ "ntUmNg4vAC/F3OtRz3+y9wM5QIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQUFAAOCAQEAafyD4gMsOvq7E3raPntmQlJTxpWwNySqskE7fe23HVL9UKFCUlWWx/W8gluxIX9S19y17iWnGbtmbNddHxG5PznPsy/a8PlwNHjDW0FOpia2LsvDrNcdPiJhzL/1OVagkenffFf8bLEetF3ktxZ7ifcH1yxV" & _ "xpZ7PS/pe8YIOpWRuMmTV4ypGdsw9TW3HVP5IJ/canuQGPTb3LQ8ojihW2dHnC6ojaWW4GHFSZAPhQJ/DaH/UgFjaQke/RBtoAketfROdG+1qYeA1q/is04O4AXNmMByGp7ZnvGNrO9LDBvs3eKN4ZYcQyjxFEbr1X/xUqHCRF1VEkkC5jJQ1ktC4g==" ' 1. Read in Public key from X.509 certificate string directly strPublicKey = rsaGetPublicKeyFromCert(strCertificado) If Len(strPublicKey) = 0 Then Debug.Print "ERROR: failed to read Certificado string" Exit Function End If ' --Show we got something useful Debug.Print "Public key is " & RSA_KeyBits(strPublicKey) & " bits long" ' 2. Convert base64 signature value to byte array abData = cnvBytesFromB64Str(strSello) nSigLen = UBound(abData) - LBound(abData) + 1 ' 2a. Check lengths match Debug.Print "Signature bytes=" & nSigLen Debug.Print "Key bytes =" & RSA_KeyBytes(strPublicKey) If nSigLen <> RSA_KeyBytes(strPublicKey) Then Debug.Print "ERROR: key length does not match signature" Exit Function End If ' 3.Decrypt using RSA public key nRet = RSA_RawPublic(abData(0), nSigLen, strPublicKey, 0) Debug.Print "RSA_RawPublic returns " & nRet & " (expected 0)" If nRet <> 0 Then Debug.Print "ERROR: failed to decrypt RSA signature: error code " & nRet Exit Function End If ' Display result in hex Debug.Print "Decrypted signature=" & vbCrLf & cnvHexStrFromBytes(abData) ' 4. Decode to extract the original message digest nMsgLen = RSA_DecodeMsg(0, 0, abData(0), nSigLen, PKI_EMSIG_DEFAULT) ReDim abMsg(nMsgLen - 1) nMsgLen = RSA_DecodeMsg(abMsg(0), nMsgLen, abData(0), nSigLen, PKI_EMSIG_DEFAULT) ' 5. Convert to hex format strDigestHex = cnvHexStrFromBytes(abMsg) ' OUTPUT: Digest in hex format Debug.Print "MD5 digest as hex: " & strDigestHex Mex_ExtractDigestFromSignature = LCase(cnvHexStrFromBytes(abMsg))
'@Proc: Mex_ExtractDigestFromSignature '@Lang: VB6 '@ Extracts the message digest from a signature (sello) string using the X.509 certificate (certificado) value Dim sbPublicKey As StringBuilder Dim strSello As String Dim strCertificado As String Dim abDigest() As Byte Dim abData() As Byte Dim nSigLen As Integer Dim strDigestHex As String Mex_ExtractDigestFromSignature = "" ' INPUT: Base64 strings extracted from the XML file (Ref: Muestra_v2_signed2.xml) strSello = "UlUSwGNEicfigV6i4RhTy0eb2RYWFYyFatJFcM/u5Wlkb5XRxXiCizTGw5Yxz9oZNk8msAgO4C5Gevjh+S2TJPZueYhaQeZlo6k0rE3CQexkOGVRpHkvAoAgOM5kGKzYe24DKZbTgjNL+ai+tbhEHmRAFcpv2rDpehbL3w6BnYU=" strCertificado = "MIIDhDCCAmygAwIBAgIUMTAwMDEyMDAwMDAwMDAwMjI1MTcwDQYJKoZIhvcNAQEFBQAwgcMxGTAXBgNVBAcTEENpdWRhZCBkZSBNZXhpY28xFTATBgNVBAgTDE1leGljbywgRC5GLjELMAkGA1UEBhMCTVgxGjAYBgNVBAMTEUFDIGRlIFBydWViYXMgU0FUMTYwNAYDVQQLFC1BZG1pbmlzdHJhY2nzbiBkZSBTZWd1cmlkYWQgZGUgbGEgSW5" & _ "mb3JtYWNp824xLjAsBgNVBAoUJVNlcnZpY2lvIGRlIEFkbWluaXN0cmFjafNuIFRyaWJ1dGFyaWEwHhcNMDgwODIxMTUyMjA4WhcNMTAwODIxMTUyMjA4WjCBmDElMCMGA1UELRMcQUFBMDEwMTAxQUFBIC8gQUFBQTAxMDEwMUFBQTEeMBwGA1UEBRMVIC8gQUFBQTAxMDEwMUhERlJYWDAxMRIwEAYDVQQKEwlNYXRyaXogU0ExEzARBgNVBA" & _ "sTClVuaWRhZCAxMCAxEjAQBgNVBAMTCU1hdHJpeiBTQTESMBAGA1UEKRMJTWF0cml6IFNBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDpmiW1q9gyzCFtMcbaFDJexk2IpLoTdNXg4ToGRZ/f+hIjmj3N6ODWX1ARNFGYocEHf113GpW5Oe/mj6UqhBpiH4JRTNR4Udb8myJTArIlODynVHuIUuyhKo7gbMbDdXjilTAYY2XWQuQ7aDtWw" & _ "ntUmNg4vAC/F3OtRz3+y9wM5QIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQUFAAOCAQEAafyD4gMsOvq7E3raPntmQlJTxpWwNySqskE7fe23HVL9UKFCUlWWx/W8gluxIX9S19y17iWnGbtmbNddHxG5PznPsy/a8PlwNHjDW0FOpia2LsvDrNcdPiJhzL/1OVagkenffFf8bLEetF3ktxZ7ifcH1yxV" & _ "xpZ7PS/pe8YIOpWRuMmTV4ypGdsw9TW3HVP5IJ/canuQGPTb3LQ8ojihW2dHnC6ojaWW4GHFSZAPhQJ/DaH/UgFjaQke/RBtoAketfROdG+1qYeA1q/is04O4AXNmMByGp7ZnvGNrO9LDBvs3eKN4ZYcQyjxFEbr1X/xUqHCRF1VEkkC5jJQ1ktC4g==" ' 1. Read in Public key from X.509 certificate string directly sbPublicKey = Rsa.GetPublicKeyFromCert(strCertificado) If sbPublicKey.Length = 0 Then Console.WriteLine("ERROR: failed to read Certificado string") Exit Function End If ' --Show we got something useful Console.WriteLine("Public key is " & Rsa.KeyBits(sbPublicKey.ToString) & " bits long") ' 2. Convert base64 signature value to byte array abData = System.Convert.FromBase64String(strSello) nSigLen = abData.Length ' 2a. Check lengths match Console.WriteLine("Signature bytes=" & nSigLen) Console.WriteLine("Key bytes =" & Rsa.KeyBytes(sbPublicKey.ToString)) If nSigLen <> Rsa.KeyBytes(sbPublicKey.ToString) Then Console.WriteLine("ERROR: key length does not match signature") Exit Function End If ' 3.Decrypt using RSA public key abData = Rsa.RawPublic(abData, sbPublicKey.ToString) Console.WriteLine("RSA_RawPublic returns " & abData.Length & " bytes (expected >0)") If abData.Length = 0 Then Console.WriteLine("ERROR: failed to decrypt RSA signature.") Exit Function End If ' Display result in hex Console.WriteLine("Decrypted signature=" & vbCrLf & Cnv.ToHex(abData)) ' 4. Decode to extract the original message digest abDigest = Rsa.DecodeDigestForSignature(abData) ' 5. Convert to hex format strDigestHex = Cnv.ToHex(abDigest) ' OUTPUT: Digest in hex format Console.WriteLine("MD5 digest as hex: " & strDigestHex) Mex_ExtractDigestFromSignature = LCase(Cnv.ToHex(abDigest))
To verify a signature, we must do three things:
If we find an error in any of the above, we should return the error message "Invalid Signature" with no further clues as to the problem. If we are debugging our own code, we should try step 1 as a first check. If we are able to extract a message digest from the signature value, then it means we are creating the RSA signature correctly. If we don't get the same message digest value (or our signature fails to verify with the validator), then we are creating the input to the digest incorrectly. See My signature will not validate.
strDigest1 = Mex_ExtractDigestFromSignature(strSello, strCertificado) strDigest2 = Mex_CreateDigestFromString(strPipedString) If StrComp(strDigest1, strDigest2, 1) = 0 Then Debug.Print "VALID SIGNATURE" Else Debug.Print "ERROR: Invalid signature" End If
strDigest1 = Mex_ExtractDigestFromSignature(strSello, strCertificado)
strDigest2 = Mex_CreateDigestFromString(strPipedString)
If String.Compare(strDigest1, strDigest2, True) = 0 Then
Console.WriteLine("VALID SIGNATURE")
Else
Console.WriteLine("ERROR: Invalid signature")
End If
The functions
X509_ReadStringFromFile
and
X509_SaveFileFromString
in CryptoSys PKI allow simple conversion of an X.509 certificate file to a base64 string and vice versa.
The certificate is stored in a one-line base64 string, which can be cut-and-pasted into an XML file, for example
<?xml version="1.0" encoding="UTF-8"?>
<Comprobante fecha="2005-09-02T16:30:00" folio="1" noAprobacion="1"
noCertificado="00001000000000000114"
certificado="MIIDWjCCAkKgAwIBAgIUMDAwMDExMDAwMDAyMDA..."
serie="A" version="1.0">
The following example reads the SAT Mexico test certificate file into a base64 string
and then uses the
X509_CertThumb
function to check that the two certificate forms are identical.
Code to convert an X.509 certificate file into a base64 string: Show/hide VB6 code (requires Javascript) Show/hide VB.NET code
'@Proc: Mex_CertToBase64String '@Lang: VB6 '@ Converts an X.509 certificate file into a base64 string suitable for Certificado field in XML, ' and shows how this string form can be treated just like the .cer file. Dim nRet As Long Dim strCertString As String Dim strCertFile As String Dim strThumb1 As String Dim strThumb2 As String strCertFile = "aaa010101aaa_CSD_01.cer" ' Read in certificate file's data to a string nRet = X509_ReadStringFromFile("", 0, strCertFile, 0) Debug.Print "X509_ReadStringFromFile returns " & nRet If nRet <= 0 Then Debug.Print "ERROR: Unable to read certificate file. Error: " & nRet Exit Sub End If strCertString = String(nRet, " ") nRet = X509_ReadStringFromFile(strCertString, Len(strCertString), strCertFile, 0) Debug.Print "For certificate '" & strCertFile & "':" Debug.Print strCertString ' Check that the two versions of the certificate are identical by computing their SHA-1 thumbprints strThumb1 = String(PKI_SHA1_CHARS, " ") strThumb2 = String(PKI_SHA1_CHARS, " ") nRet = X509_CertThumb(strCertFile, strThumb1, Len(strThumb1), 0) nRet = X509_CertThumb(strCertString, strThumb2, Len(strThumb2), 0) Debug.Print "SHA-1(file) =" & strThumb1 Debug.Print "SHA-1(string)=" & strThumb2 If strThumb1 = strThumb2 Then Debug.Print "Certificates are identical" Else Debug.Print "ERROR: certificates do not match" End If
'@Proc: Mex_CertToBase64String '@Lang: VB.NET '@ Converts an X.509 certificate file into a base64 string suitable for Certificado field in XML, ' and shows how this string form can be treated just like the .cer file. Dim strCertString As String Dim strCertFile As String Dim strThumb1 As String Dim strThumb2 As String strCertFile = "aaa010101aaa_CSD_01.cer" ' Read in certificate file's data to a string strCertString = X509.ReadStringFromFile(strCertFile) Console.WriteLine("For certificate '" & strCertFile & "':") Console.WriteLine(strCertString) ' Check that the two versions of the certificate are identical by computing their SHA-1 thumbprints strThumb1 = X509.CertThumb(strCertFile, HashAlgorithm.Sha1) strThumb2 = X509.CertThumb(strCertString, HashAlgorithm.Sha1) Console.WriteLine("SHA-1(file) =" & strThumb1) Console.WriteLine("SHA-1(string)=" & strThumb2) If strThumb1 = strThumb2 Then Console.WriteLine("Certificates are identical") Else Console.WriteLine("ERROR: certificates do not match") End If
The output should look like this:
X509_ReadStringFromFile returns 1208 For certificate 'aaa010101aaa_CSD_01.cer': MIIDhDCCAmygAwIBAgIUMTAwMDEyMDAwMDAwMDAwMjI1MTcwDQYJKoZIhvcNAQEFBQAwgcMxGTAXBgNVBAcTEENpdWRhZCBkZSBNZXhpY28xFTATBgNVBAgTDE1leGljbywgRC5GLjELMAkGA1UEBhMCTVgxGjAYBgNVBAMTEUFDIGRlIFBydWViYXMgU0FUMTYwNAYDVQQLFC1BZG1pbmlzdHJhY2nzbiBkZSBTZWd1cmlkYWQgZGUgbGEgSW5mb3JtYWNp824xLjAsBgNVBAoUJVNlcnZpY2lvIGRlIEFkbWluaXN0cmFjafNuIFRyaWJ1dGFyaWEwHhcNMDgwODIxMTUyMjA4WhcNMTAwODIxMTUyMjA4WjCBmDElMCMGA1UELRMcQUFBMDEwMTAxQUFBIC8gQUFBQTAxMDEwMUFBQTEeMBwGA1UEBRMVIC8gQUFBQTAxMDEwMUhERlJYWDAxMRIwEAYDVQQKEwlNYXRyaXogU0ExEzARBgNVBAsTClVuaWRhZCAxMCAxEjAQBgNVBAMTCU1hdHJpeiBTQTESMBAGA1UEKRMJTWF0cml6IFNBMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDpmiW1q9gyzCFtMcbaFDJexk2IpLoTdNXg4ToGRZ/f+hIjmj3N6ODWX1ARNFGYocEHf113GpW5Oe/mj6UqhBpiH4JRTNR4Udb8myJTArIlODynVHuIUuyhKo7gbMbDdXjilTAYY2XWQuQ7aDtWwntUmNg4vAC/F3OtRz3+y9wM5QIDAQABox0wGzAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIGwDANBgkqhkiG9w0BAQUFAAOCAQEAafyD4gMsOvq7E3raPntmQlJTxpWwNySqskE7fe23HVL9UKFCUlWWx/W8gluxIX9S19y17iWnGbtmbNddHxG5PznPsy/a8PlwNHjDW0FOpia2LsvDrNcdPiJhzL/1OVagkenffFf8bLEetF3ktxZ7ifcH1yxVxpZ 7PS/pe8YIOpWRuMmTV4ypGdsw9TW3HVP5IJ/canuQGPTb3LQ8ojihW2dHnC6ojaWW4GHFSZAPhQJ/DaH/UgFjaQke/RBtoAketfROdG+1qYeA1q/is04O4AXNmMByGp7ZnvGNrO9LDBvs3eKN4ZYcQyjxFEbr1X/xUqHCRF1VEkkC5jJQ1ktC4g== SHA-1(file) =12363384059988478ca8a5c2cc1ea2c72170518d SHA-1(string)=12363384059988478ca8a5c2cc1ea2c72170518d Certificates are identical
The X.509 standard is that the serial number is stored as an integer value. SAT in Mexico allocates serial numbers in the form of a hexadecimal-encoded value of a string of 20 ASCII digits, which represents an extremely large integer.
Code to extract the serial number in the required SAT format: Show/hide VB6 code (requires Javascript) Show/hide VB.NET code
'@Proc: Mex_SAT_SerialNumber '@Lang: VB6 '@ Extracts the serial number from a SAT-issued X.509 certificate and displays in base64 format Dim nLen As Long Dim strCertFile As String Dim strSerialNumber As String Dim strSerialSAT As String ' Extract the certificate's serial number strCertFile = "AAA010101AAAsd.cer" nLen = X509_CertSerialNumber(strCertFile, "", 0, 0) If (nLen <= 0) Then Exit Sub strSerialNumber = String(nLen, " ") nLen = X509_CertSerialNumber(strCertFile, strSerialNumber, Len(strSerialNumber), 0) Debug.Print "X.509 Serial Number=0x" & strSerialNumber ' Decode from hex-encoded integer to string of ASCII digits strSerialSAT = StrConv(cnvBytesFromHexStr(strSerialNumber), vbUnicode) Debug.Print "Decoded SAT Format ='" & strSerialSAT & "'"
'@Proc: Mex_SAT_SerialNumber '@Lang: VB.NET '@ Extracts the serial number from a SAT-issued X.509 certificate and displays in base64 format Dim strCertFile As String Dim strSerialNumber As String Dim strSerialSAT As String ' Extract the certificate's serial number strCertFile = "AAA010101AAAsd.cer" strSerialNumber = X509.CertSerialNumber(strCertFile) Console.WriteLine("X.509 Serial Number=0x" & strSerialNumber) ' Decode from hex-encoded integer to string of ASCII digits strSerialSAT = System.Text.Encoding.Default.GetString(Cnv.FromHex(strSerialNumber)) Console.WriteLine("Decoded SAT Format ='" & strSerialSAT & "'")
For the sample X.509 certificate provided by SAT (AAA010101AAAsd.cer), this should produce the following output:
X.509 Serial Number=0x3030303031303030303030303030303030313134 Decoded SAT Format ='00001000000000000114'
Note that the code above is only applicable for certificates with serial numbers in this particular format.
If you examine the test certificate
using Microsoft's CERTMGR.EXE program (just double-click on the .CER file in Windows Explorer),
you will see that the Serial Number value is given as
3030 3030 3130 3030 3030 3030 3030 3030 3031 3134
which is what our CryptoSys PKI program returns. This is the hexadecimal-encoded value of the decimal integer 275106190557734483187066766737145316642573594932 (2.75 x 1047), a number almost big enough to count every atom in the planet!
For more information, please Email Us. To comment on this page, see below.
This page last updated 14 January 2010
Comments
0 comments so far