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 message 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 | Revisions required | Sample files and code | Sample VB6 code | Sample VB.NET code | Sample C# code | 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 | User comments | Contact
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 and 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#).
PKI_HASH_MD5
to PKI_HASH_SHA1
and PKI_MD5_BYTES
to PKI_SHA1_BYTES
.
(Be careful with output lengths: SHA-1 digests are 20 bytes (40 hex chars) instead of 16/32.)
In .NET programs, change HashAlgorithm.Md5
to HashAlgorithm.Sha1
.
Sample key files and lots of other information including a sample XML file (updated 2010-05-03) Muestra.xml (zipped, 1 kB)
2010-11-20: Please note also that the code examples below may use public key data and signatures which refer to older, out-of-date files rather than the current samples.
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 message 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.
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 and include the line
Imports CryptoSysPKI
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 and include the line
using CryptoSysPKI;
2016-01-14: This code updated to remove obsolete UTF-8 functions and change digest algorithm from MD5 to SHA-1. See also Obsolete UTF-8 functions in the manual.
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. A 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 (but see UTF-8 and Byte Order Marks).
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 SHA-1 digest of the input string after converting to UTF-8 encoding Dim strData As String Dim abData() As Byte Dim abTemp() As Byte 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. Deal with array of bytes, not a String type abData = StrConv(strData, vbFromUnicode) nLen = UBound(abData) + 1 ' 2. Convert Latin-1 to UTF-8, if necessary If CNV_CheckUTF8Bytes(abData(0), nLen) = 0 Then ' Our input string is in Latin-1 and we want UTF-8 nLen = CNV_UTF8BytesFromLatin1(0, 0, strData) ReDim abTemp(nLen - 1) nLen = CNV_UTF8BytesFromLatin1(abTemp(0), nLen, strData) abData = abTemp End If ' 3. Create the message digest hash ' but first dimension the string to receive it strDigest = String(PKI_SHA1_CHARS, " ") nRet = HASH_HexFromBytes(strDigest, Len(strDigest), abData(0), nLen, PKI_HASH_SHA1) ' OUTPUT: Display digest in hex format Debug.Print "Digest=" & strDigest Mex_CreateDigestFromString = strDigest ' Correct 1b6f53fef6bc63ea817d6ecf6690f24949f570c8
'@Proc: Mex_CreateDigestFromString '@Lang: VB.NET '@ Creates the SHA-1 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 a UTF-8-encoded byte array abDataUTF8 = System.Text.Encoding.UTF8.GetBytes(strData) ' 2. Create the message digest hash strDigest = Hash.HexFromBytes(abDataUTF8, HashAlgorithm.Sha1) ' 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.
CAUTION: Note that the "text" file containing the input in this example must contain exactly the correct bytes of the piped-string with no BOM headers or trailing CR-LF characters. UTF-8 files created by a certain .N*T software may have these additional characters added to them whether you want them or not. If there are additional characters - just one - the signature will be wrong! More on this at UTF-8 and Byte Order Marks.
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 or to comment on this page, please send us a message.
This page last updated 3 January 2019
User Comments