CryptoSys Home > PKI > SAT Mexico and CryptoSys PKI

SAT Mexico and the CryptoSys PKI Toolkit


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.

Warning: old code, out of date
Please note all the code on this page was written in 2010 and is for SAT version 2.0 using the old MD5 algorithm and out-of-date certificates (see Revisions Required below). This code is not maintained. Please use our independent FirmaSAT program (available here) to create SAT Mexico digital signatures and check input XML files using the latest specifications from S.A.T. See also:

Contents

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

Code on this page

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#).

Revisions required

Change MD5 to SHA-1
On 2011-01-01 the message digest algorithm used to create the signature (sello) changed from MD5 to SHA-1. To make the change in the code below, just change all occurrences of 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.
Old sample files
The sample files on this page are old and out-of-date. Please see the latest sample files in the FirmaSAT download.
Obsolete UTF-8 functions
The original code includes obsolete UTF-8 functions to handle Latin-1-to-UTF-8 encoding conversions. The code below at Creating the message digest of the piped-string has been updated (but not in the downloadable zip files). See also Obsolete UTF-8 functions in the manual.

Sample files and code

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.

Sample VB6 code

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)

Procedures included in the code

Jorge Perez has kindly prepared a port of an earlier version of this code to PowerBuilder: see Example in Powerbuilder Script.

Sample VB.NET code

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)

Sample C# code

Some examples in C-sharp. You will need to reference the diCrSysPKINet.dll class library in your project and include the line

using CryptoSysPKI;

C# code (zipped 5 kB)

Creating the message digest of the piped-string

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

Creating a signature (sello)

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)

Checking the signature (sello)

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))

Verifying a signature (sello)

To verify a signature, we must do three things:

  1. Extract the message digest from the signature value using the X.509 certificate.
  2. Create a message digest of the relevant fields from the signed XML document. This requires concatenating all the relevant fields in the correct order into a "piped string", converting to UTF-8 encoding, if necessary, and passing this as input to the MD5 message digest (hash) function.
  3. Verifying that the two message digests from steps 1 and 2 are the same.

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

SAT X.509 Certificates and Base64 Format

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

SAT X.509 Certificate Serial Numbers

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!

User Comments

FirmaSat used to be a module included in the toolkit. Is not anymore included? Thanks in advance for your answer.

Manuel Tavernier | - Wed, May 19 2010 22:10 GMT

It was never included in the Toolkit per se. It was available as an add-on and still is. It's just no longer free. See https://www.cryptosys.net/fsa/index.html.

Dave | Moderator - Thu, May 20 2010 08:49 GMT

FirmaSAT is now a stand-alone module. It no longer needs the CryptoSys PKI Toolkit.

Dave | Moderator - Mon, Jan 8 2012 17:32 GMT

The HASH_File function used in Mex_CreateSignature, does it have a declaration which uses "the piped-string" instead of the filename with the piped-string? Thanks in advance.

Manuel Tavernier | Monterrey, MEXICO - Mon, Sep 27 2010 14:22 GMT

Use the HASH_Bytes function instead. See the example in the manual for more details:

https://www.cryptosys.net/pki/manpki/pki_HASH_Bytes.html

Dave | Moderator - Sun, Oct 3 2010 13:02 GMT

Contact

For more information or to comment on this page, please send us a message.

This page last updated 3 January 2019