CryptoSys Home > PKI > SAT Mexico and the CryptoSys PKI Toolkit

SAT Mexico and the CryptoSys PKI Toolkit


22 May 2026. Complete rewrite of this page and associated code samples to include the latest CFDi version 4 specification and SHA-256 message digest algorithm, and latest coding techniques.

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 create a digital signature "Sello" element suitable for use in your CFDi XML files, verify such a signature, generate the required message digest, convert your X.509 certificate file to a base64 format "Certificado" string, and derive the special SAT 20-digit format for your certificate serial number "NoCertificado".

Tip: It is much more convenient to 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.

Outline of Procedure | Functions included in the code | The Code | Notes | Download the full code | Composing the Piped-string | See also | Contact us

The code snippets on this page can be displayed or hidden if Javascript is turned on: Show all code   Hide all code

Outline of Procedure

The procedure to sign a CFDi document is as follows.

1. Create the base XML document

Create the base XML document according to the SAT specifications. Here is an example: cfdv40-ejemplo.xml. This is up to you to do.

Leave the NoCertificado, Sello and Certificado nodes empty.

<cfdi:Comprobante ...
NoCertificado=""  Certificado=""  Sello="">

2. Compute the NoCertificado value

Compute the NoCertificado value of your signing certificate and add this to your XML document. This is the 20-decimal digit decoded SAT format. See Mex_SAT_SerialNumber below.
<cfdi:Comprobante ...
NoCertificado="30001000000300023708"  Certificado=""  Sello="">

3. Compute the base64 Certificado value

Compute the base64-encoded value of your signing certificate and add this to the Certificado node. See Mex_CertToBase64String below. You can do this now or later.

4. Compose the cadena original (piped-string)

Compose the cadena original, we call it the piped-string (after the pipe symbol '|'). This must include the NoCertificado value for the certificate you are going to use to sign, but does not need the Certificado or Sello values (we can add these later). For more details on how to compose this see Composing the Piped-string.
||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|99999|A1234|05|18|2021|09|ED1752FE-E865-4FF2-BFE1-0F552E770DC9|AAA010101AAA|Esta es una demostración|630|0123456789|BASJ600902KL9|Juanito Bananas De la Sierra|99999|MEX|0000000000000|630|S01|01010101|00001|1.5|C81|TONELADA|ACERO|1500000|2250000|01|2250000|002|Tasa|1.600000|360000|2250000|001|Tasa|0.300000|247500|51888|95141904|00002|1.6|WEE|TONELADA|ALUMINIO|1500|2400|02|2400|002|Tasa|1.600000|384|2400|001|Tasa|0.300000|264|AAA010101AAA|NombreACuentaTerceros|630|99999|15 48 4567 6001234|84101604|00003|1.7|G66|TONELADA|ZAMAC|10000|17000|0|03|17000|002|Tasa|1.600000|2720|17000|001|Tasa|0.300000|1870|25201513|055155|1.0|UNIDAD|PARTE EJEMPLO|1.00|1.00|15 48 4567 6001235|001|247000|003|500|247500|1.00|002|Tasa|1.600000|360000|360000||

5. Sign the piped-string to create the Sello value

Use the RSA_Sign function to create a signature over the piped-string using your private key.

Add the base64-encoded signature value to the Sello node.

Sello="Gnm1yXaIig5hr1dJ+88gjLY5usQxXP2s+zdmlLl4iokWENaUUlhpG/crkUFEzcJfdq1FbBxV/d/GN50MGuw2fP5f6MkRYz75UKaKfzubUak+SCkDzYot5jZRkO6hXKe4+KAfaulP7wa8Q7oSW5ccivppLnikme0CS3KtGBrQHU/q3pjNrw+jMvsnpUc1tx91REqMWrzMTZ2D6UkGdqn8i/0mLRU2vT8vPaNg/Hr2jpVVAgQmPtfvnfnORCWSm/5qZg9Tli7nTRRVwupF4o9ajH2/Is7LoCsIgZUg3wCHkZCjaKk2mJoK7FyMTsPXRRSGhoYgwi1kNHs1aY4RHNwe5w=="
Your CFDi document is now signed.

Note we used a dummy user certificate (emisor.cer) and its private key to sign our example. These test files are included in the demonstration download.

6. Get this counter-signed by your PAC

Your PAC will counter-sign your CFDi document and add a tfd:TimbreFiscalDigital node in the cfdi:Complemento element. Here is an example of a counter-signed document using the base document from above: cfdv40-ejemplo-signed-tfd.xml.

(Note we used a dummy PAC certificate (pac.cer) and its private key to create our example here.)

Show/hide TimbreFiscalDigital element
<tfd:TimbreFiscalDigital xmlns:tfd="http://www.sat.gob.mx/TimbreFiscalDigital"
 xsi:schemaLocation="http://www.sat.gob.mx/TimbreFiscalDigital http://www.sat.gob.mx/sitio_internet/cfd/TimbreFiscalDigital/TimbreFiscalDigitalv11.xsd" 
 Version="1.1" UUID="0e52ecfc-69a1-4c7f-88c5-3dea2a7e8863" FechaTimbrado="2026-04-18T15:19:00" RfcProvCertif="PUT211201AX4" 
 SelloCFD="Gnm1yXaIig5hr1dJ+88gjLY5usQxXP2s+zdmlLl4iokWENaUUlhpG/crkUFEzcJfdq1FbBxV/d/GN50MGuw2fP5f6MkRYz75UKaKfzubUak+SCkDzYot5jZRkO6hXKe4+KAfaulP7wa8Q7oSW5ccivppLnikme0CS3KtGBrQHU/q3pjNrw+jMvsnpUc1tx91REqMWrzMTZ2D6UkGdqn8i/0mLRU2vT8vPaNg/Hr2jpVVAgQmPtfvnfnORCWSm/5qZg9Tli7nTRRVwupF4o9ajH2/Is7LoCsIgZUg3wCHkZCjaKk2mJoK7FyMTsPXRRSGhoYgwi1kNHs1aY4RHNwe5w==" 
 NoCertificadoSAT="30001000000300023699" 
 SelloSAT="Uqe2OrznW6pdzpUqncfXr3hcGn6C1f3BumUvhmGnZJcJcsL0COgrZ3G5N1H0lKoDWx5+P8jDpjWEiqACxaxgxGqkSP/LY2bRKZ0prFG+NU6LdLQRAblxqkNx/VQcsGcNwqT3XHLqz0ZXtLXRxAdtncrwjM4VF0Q1et3vMm31rBlKsPzTz07v1VjXdrsBdzfc0wLGoVGNG41e/hObJNb5JkltBu9pj4C2xfufclpZHpu9Tr7ekJPwqerJWJPHXkiGmB+7SITA+LnW0KkMeVS0mn/IZtBMCVb9v9xzS6gEi4PqBZxpNkLBf8eHwoB79vmjN86zPA6xR5qgpto5ZgLAFw==" />

Functions included in the code

The Code

Mex_CreateSignature
C# code to create a signature (Sello) value given the piped-string input and private key: Show/hide C# code
/// <summary>
/// Creates a signature (sello) value in base64 format given the piped-string input and the private key
/// </summary>
public static void Mex_CreateSignature()
{
    // Ref: cfdv40-ejemplo.xml
    string strData = "||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|99999|A1234|05|18|2021|09|ED1752FE-E865-4FF2-BFE1-0F552E770DC9|AAA010101AAA|Esta es una demostración|630|0123456789|BASJ600902KL9|Juanito Bananas De la Sierra|99999|MEX|0000000000000|630|S01|01010101|00001|1.5|C81|TONELADA|ACERO|1500000|2250000|01|2250000|002|Tasa|1.600000|360000|2250000|001|Tasa|0.300000|247500|51888|95141904|00002|1.6|WEE|TONELADA|ALUMINIO|1500|2400|02|2400|002|Tasa|1.600000|384|2400|001|Tasa|0.300000|264|AAA010101AAA|NombreACuentaTerceros|630|99999|15 48 4567 6001234|84101604|00003|1.7|G66|TONELADA|ZAMAC|10000|17000|0|03|17000|002|Tasa|1.600000|2720|17000|001|Tasa|0.300000|1870|25201513|055155|1.0|UNIDAD|PARTE EJEMPLO|1.00|1.00|15 48 4567 6001235|001|247000|003|500|247500|1.00|002|Tasa|1.600000|360000|360000||";
    string strKeyFile = "emisor.key";
    // Use a StringBuilder to store the password so we can wipe it
    StringBuilder sbPassword = new StringBuilder("12345678a"); // CAUTION: DO NOT hardcode production passwords!

    // 1. Convert string to UTF-8-encoded byte array
    byte[] abData = Encoding.UTF8.GetBytes(strData);

    // 2. Sign the input data using the private key
    string strSig64 = Sig.SignData(abData, strKeyFile, sbPassword.ToString(), SigAlgorithm.Rsa_Sha256);
    Console.WriteLine("SIGNATURE VALUE=" + strSig64);
    // Expecting Gnm1yX...Y4RHNwe5w==

    // 3. Clean up
    Wipe.String(sbPassword);
}
VBA/VB6 code to create a signature (Sello) value given the piped-string input and private key: Show/hide VBA code
'/**
' Create signature (sello) value in base64 format given the piped-string input and the private key.
'**/
Public Sub Mex_CreateSignature()
    Dim strData As String
    Dim abData() As Byte
    Dim strKeyFile As String
    Dim strPassword As String
    Dim strSig64 As String
    
    ' INPUT: Our original piped-string data (NB includes non-ASCII character ó (LATIN SMALL LETTER O WITH ACUTE U+00F3)
    ' derived from file cfdv40-ejemplo.xml
    strData = "||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|99999|A1234|05|18|2021|09|ED1752FE-E865-4FF2-BFE1-0F552E770DC9|AAA010101AAA|Esta es una demostración|630|0123456789|BASJ600902KL9|Juanito Bananas De la Sierra|99999|MEX|0000000000000|630|S01|01010101|00001|1.5|C81|TONELADA|ACERO|1500000|2250000|01|2250000|002|Tasa|1.600000|360000|2250000|001|Tasa|0.300000|247500|51888|95141904|00002|1.6|WEE|TONELADA|ALUMINIO|1500|2400|02|2400|002|Tasa|1.600000|384|2400|001|Tasa|0.300000|264|AAA010101AAA|NombreACuentaTerceros|630|99999|15 48 4567 6001234|84101604|00003|1.7|G66|TONELADA|ZAMAC|10000|17000|0|03|17000|002|Tasa|1.600000|2720|17000|001|Tasa|0.300000|1870|25201513|055155|1.0|UNIDAD|PARTE EJEMPLO|1.00|1.00|15 48 4567 6001235|001|247000|003|500|247500|1.00|002|Tasa|1.600000|360000|360000||"
    strKeyFile = "emisor.key"
    ' Test password - CAUTION: DO NOT hardcode production passwords!
    strPassword = "12345678a"
       
    ' 1. Convert VBA piped-string to UTF-8-encoded byte array
    abData = cnvUTF8BytesFromLatin1(strData)
         
    ' 2. Sign the input data using the private key, output direct to a base64 string,
    '  using sha256WithRSAEncryption algorithm (RSA-SHA-256)
    strSig64 = sigSignData(abData, strKeyFile, strPassword, "sha256WithRSAEncryption")
    Debug.Print "SIGNATURE VALUE=" & strSig64
    ' Expecting Gnm1yX...Y4RHNwe5w==

    ' 3. Clean up
    strPassword = wipeString(strPassword)
    
End Sub
Mex_VerifySignature
C# code to verify a signature (Sello) using the public key in the X.509 certificate: Show/hide C# code
/// <summary>
/// Verify a signature (sello) using the public key in the X.509 certificate.
/// </summary>
public static void Mex_VerifySignature()
{
    string strData = "||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|99999|A1234|05|18|2021|09|ED1752FE-E865-4FF2-BFE1-0F552E770DC9|AAA010101AAA|Esta es una demostración|630|0123456789|BASJ600902KL9|Juanito Bananas De la Sierra|99999|MEX|0000000000000|630|S01|01010101|00001|1.5|C81|TONELADA|ACERO|1500000|2250000|01|2250000|002|Tasa|1.600000|360000|2250000|001|Tasa|0.300000|247500|51888|95141904|00002|1.6|WEE|TONELADA|ALUMINIO|1500|2400|02|2400|002|Tasa|1.600000|384|2400|001|Tasa|0.300000|264|AAA010101AAA|NombreACuentaTerceros|630|99999|15 48 4567 6001234|84101604|00003|1.7|G66|TONELADA|ZAMAC|10000|17000|0|03|17000|002|Tasa|1.600000|2720|17000|001|Tasa|0.300000|1870|25201513|055155|1.0|UNIDAD|PARTE EJEMPLO|1.00|1.00|15 48 4567 6001235|001|247000|003|500|247500|1.00|002|Tasa|1.600000|360000|360000||";
    string strSig64 = "Gnm1yXaIig5hr1dJ+88gjLY5usQxXP2s+zdmlLl4iokWENaUUlhpG/crkUFEzcJfdq1FbBxV/d/GN50MGuw2fP5f6MkRYz75UKaKfzubUak+SCkDzYot5jZRkO6hXKe4+KAfaulP7wa8Q7oSW5ccivppLnikme0CS3KtGBrQHU/q3pjNrw+jMvsnpUc1tx91REqMWrzMTZ2D6UkGdqn8i/0mLRU2vT8vPaNg/Hr2jpVVAgQmPtfvnfnORCWSm/5qZg9Tli7nTRRVwupF4o9ajH2/Is7LoCsIgZUg3wCHkZCjaKk2mJoK7FyMTsPXRRSGhoYgwi1kNHs1aY4RHNwe5w==";
    string strCertFile = "emisor.cer";

    // Convert input string to UTF-8-encoded byte array
    byte[] abData = Encoding.UTF8.GetBytes(strData);

    // Verify signature over data
    int r = Sig.VerifyData(strSig64, abData, strCertFile, SigAlgorithm.Rsa_Sha256);
    Console.WriteLine("VerifyData returns " + r + " (expecting 0)");
}
VBA/VB6 code to verify a signature (Sello) using the public key in the X.509 certificate: Show/hide VBA code
'/**
' Verify a signature (sello) using the public key in the X.509 certificate.
'**/
Public Sub Mex_VerifySignature()
    Dim strData As String
    Dim abData() As Byte
    Dim strCertFile As String
    Dim strSig64 As String
    Dim r As Long
    
    ' INPUT: Our original piped-string data (NB includes non-ASCII character ó (LATIN SMALL LETTER O WITH ACUTE U+00F3)
    ' derived from file cfdv40-ejemplo.xml
    strData = "||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|99999|A1234|05|18|2021|09|ED1752FE-E865-4FF2-BFE1-0F552E770DC9|AAA010101AAA|Esta es una demostración|630|0123456789|BASJ600902KL9|Juanito Bananas De la Sierra|99999|MEX|0000000000000|630|S01|01010101|00001|1.5|C81|TONELADA|ACERO|1500000|2250000|01|2250000|002|Tasa|1.600000|360000|2250000|001|Tasa|0.300000|247500|51888|95141904|00002|1.6|WEE|TONELADA|ALUMINIO|1500|2400|02|2400|002|Tasa|1.600000|384|2400|001|Tasa|0.300000|264|AAA010101AAA|NombreACuentaTerceros|630|99999|15 48 4567 6001234|84101604|00003|1.7|G66|TONELADA|ZAMAC|10000|17000|0|03|17000|002|Tasa|1.600000|2720|17000|001|Tasa|0.300000|1870|25201513|055155|1.0|UNIDAD|PARTE EJEMPLO|1.00|1.00|15 48 4567 6001235|001|247000|003|500|247500|1.00|002|Tasa|1.600000|360000|360000||"
    ' The signature (sello) value in base64
    strSig64 = "Gnm1yXaIig5hr1dJ+88gjLY5usQxXP2s+zdmlLl4iokWENaUUlhpG/crkUFEzcJfdq1FbBxV/d/GN50MGuw2fP5f6MkRYz75UKaKfzubUak+SCkDzYot5jZRkO6hXKe4+KAfaulP7wa8Q7oSW5ccivppLnikme0CS3KtGBrQHU/q3pjNrw+jMvsnpUc1tx91REqMWrzMTZ2D6UkGdqn8i/0mLRU2vT8vPaNg/Hr2jpVVAgQmPtfvnfnORCWSm/5qZg9Tli7nTRRVwupF4o9ajH2/Is7LoCsIgZUg3wCHkZCjaKk2mJoK7FyMTsPXRRSGhoYgwi1kNHs1aY4RHNwe5w=="
    ' The certificate used to sign
    strCertFile = "emisor.cer"

    ' Convert input string to UTF-8-encoded byte array
    abData = cnvUTF8BytesFromLatin1(strData)
    ' Verify signature over data
    r = sigVerifyData(strSig64, abData, strCertFile, "sha256WithRSAEncryption")
    Debug.Print "sigVerifyData returns " & r & " (expecting 0)"
    
End Sub
Mex_ValidateCert
C# code to check that a given X.509 certificate was issued by the issuer and has not expired: Show/hide C# code
/// <summary>
/// Check that a given X.509 certificate was issued by the issuer and has not expired.
/// </summary>
public static void Mex_ValidateCert()
{
    string strCert = "emisor.cer";
    string strIssuerCert = "satac.cer";

    // 1. Was this cert signed by the purported issuer?
    int n = X509.VerifyCert(strCert, strIssuerCert);
    Console.WriteLine("X509_VerifyCert returns " + n);
    if (n < 0) {
        Console.WriteLine("ERROR: Validation failed");
    } else if (n > 0) {
        Console.WriteLine("ERROR: " + General.ErrorLookup(n));
    } else {
        Console.WriteLine("OK, cert was signed by issuer.");
    }

    // 2. Is this cert still valid now?
    bool isValidNow = X509.CertIsValidNow(strCert);
    Console.WriteLine("X509_CertIsValidNow returns " + isValidNow);
    if (!isValidNow) {
        Console.WriteLine("ERROR: cert has expired");
    } else {
        Console.WriteLine("OK, cert is still valid now.");
    }
}
VBA/VB6 code to check that a given X.509 certificate was issued by the issuer and has not expired: Show/hide VBA code
'/**
' Check that a given X.509 certificate really was issued by the issuer and has not expired.
'**/
Public Sub Mex_ValidateCert()
    Dim strCert As String
    Dim strIssuerCert As String
    Dim nRet As Long
    
    ' INPUT: Filenames of certificate to be checked and issuer's certificate
    strCert = "emisor.cer"
    strIssuerCert = "satac.cer"
    
    ' 1. Was this cert signed by the purported issuer?
    nRet = X509_VerifyCert(strCert, strIssuerCert, 0)
    Debug.Print "X509_VerifyCert returns " & nRet
    If nRet < 0 Then
        Debug.Print "ERROR: Validation failed"
    ElseIf nRet > 0 Then
        Debug.Print "ERROR: " & pkiErrorLookup(nRet)
    Else
        Debug.Print "OK, cert was signed by issuer."
    End If
    
    ' 2. Is this cert still valid now?
    nRet = X509_CertIsValidNow(strCert, 0)
    Debug.Print "X509_CertIsValidNow returns " & nRet
    If nRet < 0 Then
        Debug.Print "ERROR: cert has expired"
    ElseIf nRet > 0 Then
        Debug.Print "ERROR: " & pkiErrorLookup(nRet)
    Else
        Debug.Print "OK, cert is still valid now."
    End If
    
End Sub
Mex_CertToBase64String
C# code to convert X.509 certificate to base64 string for Certificado node: Show/hide C# code
/// <summary>
/// Convert an X.509 certificate file into a base64 string suitable for XML Certificado element
/// </summary>
public static void Mex_CertToBase64String()
{ 
    string strCertFile = "emisor.cer";

    // Read in certificate file's data to a string
    string strCertString = X509.ReadStringFromFile(strCertFile);
    Console.WriteLine("For certificate '" + strCertFile + "':");
    Console.WriteLine(strCertString);

    // Check that the two versions are identical by computing SHA-1 thumbprints
    string strThumb1 = X509.CertThumb(strCertFile, HashAlgorithm.Sha1);
    string strThumb2 = X509.CertThumb(strCertString, HashAlgorithm.Sha1);
    Console.WriteLine("SHA-1(file)  =" + strThumb1);
    Console.WriteLine("SHA-1(string)=" + strThumb2);

    if (strThumb1 == strThumb2) {
        Console.WriteLine("Certificates are identical");
    } else {
        Console.WriteLine("ERROR: certificates do not match");
    }
}
VBA/VB6 code to convert X.509 certificate to base64 string for Certificado node: Show/hide VBA code
'/**
' Convert an X.509 certificate file into a base64 string suitable for Certificado field in XML,
' and show how this string form can be treated just like the .cer file.
'**/
Public Sub Mex_CertToBase64String()
    Dim strCertString As String
    Dim strCertFile As String
    Dim strThumb1 As String
    Dim strThumb2 As String
    
    strCertFile = "emisor.cer"
    
    ' Read in certificate file's data to a string
    strCertString = x509ReadStringFromFile(strCertFile)
    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 = x509CertThumb(strCertFile)
    strThumb2 = x509CertThumb(strCertString)
    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

End Sub
Mex_SAT_SerialNumber
C# code to extract the serial number from a SAT-issued X.509 certificate: Show/hide C# code
/// <summary>
/// Extract the serial number from a SAT-issued X.509 certificate
/// </summary>
public static void Mex_SATSerialNumber()
{ 
    string strCertFile = "emisor.cer";

    // Extract the certificate's serial number
    string strSerialNumber = X509.QueryCert(strCertFile, "serialNumber");
    Console.WriteLine("X.509 Serial Number=0x" + strSerialNumber);

    // Decode from hex-encoded integer to string of ASCII digits
    byte[] serialBytes = Cnv.FromHex(strSerialNumber);
    string strSerialSAT = Encoding.ASCII.GetString(serialBytes);
    Console.WriteLine("Decoded SAT Format ='" + strSerialSAT + "'");
}
VBA/VB6 code to extract the serial number from a SAT-issued X.509 certificate: Show/hide VBA code
'/**
' Extract the serial number from a SAT-issued X.509 certificate and display in SAT integer format
'**/
Public Sub Mex_SAT_SerialNumber()
    Dim nLen As Long
    Dim strCertFile As String
    Dim strSerialNumber As String
    Dim strSerialSAT As String
    
    ' Extract the certificate's serial number
    strCertFile = "emisor.cer"
    strSerialNumber = x509QueryCert(strCertFile, "serialNumber")
    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 &"'"
End Sub
Mex_CheckKeyAndCertMatch
C# code to check that the keys in the private key and certificate match: Show/hide C# code
/// <summary>
/// Check that the keys in the private key and certificate match.
/// </summary>
public static void Mex_CheckKeyAndCertMatch()
{
    string strCertFile = "emisor.cer";
    string strKeyFile = "emisor.key";
    // Use StringBuilder types for password and private key so we can wipe them
    StringBuilder sbPassword = new StringBuilder("12345678a"); // CAUTION: DO NOT hardcode production passwords!

    // 1. Read in private key from encrypted .key file
    StringBuilder sbPrivateKey = Rsa.ReadPrivateKey(strKeyFile, sbPassword.ToString());
    if (sbPrivateKey.Length > 0) {
        Console.WriteLine("Private key is " + Rsa.KeyBits(sbPrivateKey.ToString()) + " bits");
    } else {
        Console.WriteLine("ERROR: Cannot read private key file.");
        return;
    }

    // 2. Clean up password
    Wipe.String(sbPassword);


    // 3. Read in public key from certificate
    string strPublicKey = Rsa.ReadPublicKey(strCertFile).ToString();
    if (strPublicKey.Length > 0) {
        Console.WriteLine("Public key is " + Rsa.KeyBits(strPublicKey) + " bits");
    } else {
        Console.WriteLine("ERROR: Cannot read certificate file.");
        return;
    }

    // 4. See if the two key strings match
    int nRet = Rsa.KeyMatch(sbPrivateKey.ToString(), strPublicKey);
    if (nRet == 0) {
        Console.WriteLine("OK, key strings match.");
    } else {
        Console.WriteLine("FAILED: key strings do not match.");
    }

    // 5. Clean up private key string
    Wipe.String(sbPrivateKey);
}
VBA/VB6 code to check that the keys in the private key and certificate match: Show/hide VBA code
'/**
' Check that the keys in the private key and certificate match.
'**/
Public Sub Mex_CheckKeyAndCertMatch()
    Dim strCertFile As String
    Dim strKeyFile As String
    Dim strPassword As String
    Dim strPublicKey As String
    Dim strPrivateKey As String
    Dim nRet As Long
    
    ' INPUT: filenames for certificate and private key files
    strCertFile = "emisor.cer"
    strKeyFile = "emisor.key"
    ' Test password - CAUTION: DO NOT hardcode production passwords!
    strPassword = "12345678a"

    ' 1. Read in private key from encrypted .key file
    strPrivateKey = rsaReadPrivateKey(strKeyFile, strPassword, 0)
    If Len(strPrivateKey) > 0 Then
        Debug.Print "Private key is " &rsaKeyBits(strPrivateKey) & " bits"
    Else
        Debug.Print "ERROR: Cannot read private key file."
        Exit Sub
    End If
    
    ' 2. Clean up password as we are done with it
    strPassword = wipeString(strPassword)
    
    ' 3. Read in public key from certificate
    strPublicKey = rsaReadPublicKey(strCertFile)
    If Len(strPublicKey) > 0 Then
        Debug.Print "Public key is " &rsaKeyBits(strPublicKey) & " bits"
    Else
        Debug.Print "ERROR: Cannot read certificate file."
        Exit Sub
    End I
    
    ' 4. See if the two key strings match
    nRet = RSA_KeyMatch(strPrivateKey, strPublicKey)
    If nRet = 0 Then
        Debug.Print "OK, key strings match."
    Else
        Debug.Print "FAILED: key strings do not match."
    End If
    
    ' 5. Clean up private key string
    strPrivateKey = wipeString(strPrivateKey)

End Sub
Mex_QueryCertString
C# code to extract various details from a certificate string: Show/hide C# code
/// <summary>
/// Extract various details from a certificate string.
/// </summary>
public static void Mex_QueryCertString()
{
    string strCertificado = "MIIFXTCCA0WgAwIBAgIUMzAwMDEwMDAwMDAzMDAwMjM3MDgwDQYJKoZIhvcNAQELBQAwgbMxFjAUBgNVBAMTDUFDIGRlIHBydWViYXMxLjAsBgNVBAoTJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGTAXBgNVBAsTEFNBVC1JRVMgRmljdGljaWExCzAJBgNVBAYTAk1YMUEwPwYJKoZIhvcNAQkCEzJSZXNwb25zYWJsZTogRXN0byBubyBlcyB1biBnZW51aW5vIFNBVCBjZXJ0aWZpY2FkbzAeFw0yMTEyMDcwMDAwMDFaFw0yODEyMDcwMDAwMDFaMIH6MSkwJwYDVQQDEyBBQ0NFTSBTRVJWSUNJT1MgRU1QUkVTQVJJQUxFUyBTQzEpMCcGA1UEKRMgQUNDRU0gU0VSVklDSU9TIEVNUFJFU0FSSUFMRVMgU0MxKTAnBgNVBAoTIEFDQ0VNIFNFUlZJQ0lPUyBFTVBSRVNBUklBTEVTIFNDMQswCQYDVQQGEwJNWDEjMCEGCSqGSIb3DQEJARYUcHJ1ZWJhc0BhY2NlbS5jb20ubXgxJTAjBgNVBC0THEFBQTAxMDEwMUFBQSAvIEhFR1Q3NjEwMDM0UzIxHjAcBgNVBAUTFSAvIEhFR1Q3NjEwMDNNREZSTk4wOTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJdUcsHIEIgwivvAantGnYVIO3+7yTdD1tkKopbL+tKSjRFo1ErPdGJxP3gxT5O+ACIDQXN+HS9uMWDYnaURalSIF9COFCdh/OH2Pn+UmkN4culr2DanKztVIO8idXM6c9aHn5hOo7hDxXMC3uOuGV3FS4ObkxTV+9NsvOAV2lMe27SHrSB0DhuLurUbZwXm+/r4dtz3b2uLgBc+Diy95PG+MIu7oNKM89aBNGcjTJw+9k+WzJiPd3ZpQgIedYBD+8QWxlYCgxhnta3k9ylgXKYXCYk0k0qauvBJ1jSRVf5BjjIUbOstaQp59nkgHh45c9gnwJRV618NW0fMeDzuKR0CAwEAAaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwDQYJKoZIhvcNAQELBQADggIBAA1Pck4R2/W/YiY2pmVtv7mJ+dxCHJYal9NKv8gy89DTyW66g/DrpD4c2omv9LO8iRrzJbfcLupg7WKIri8aAdRU9/CwusNW3SrdJc9jjFMNPbvcYtjaRuPdQxVPWDQ+VUMD/iDYKt+4Db4npvht6UjJtUEzaqSIj9jHKSROG/KbMBZVBzyp8/A45q+krpuGEj9UOhBi3InTxv03REbb7jnBWAjSQFOhqloYHg6PtXY2nslnOhRAnvFNY1bZKvkEOVbcpxyNQdhBXvuESHRYoiOCUdlpRsPkkJRbg0BGHpCLS7d8mebdM1A6L1emuqCa8j55vVblDqAk5Re6wOvtRodZbx3QGltsDKYATd3HvR+PfdT5cwlI3sGp1frvVNvYhO29BX7UXqarlQA5HyEfaK48s9vd71/yFDa1XsUXytcvGZoYP8L68tbz1V8rLB0CAcRCOYYbMo4YPkdbP4BlYd8sAsljwsbpDl0DymcUEj/07KN/5diXAB9oqUxzEy5pjPLItF+WAB5XkrLZFtMEwYkpOLGIozwA1sp4EHA+ww/EZU3NQsmLiiQd00e4yIuHA4wkQ8HCcSkE+x4tpJ60j3gGxwHpcY2fb9uruxKaMrtluUIFJJ0AW04ypn0fGhTg8GV1rfvqY7aGUdoDyHZJQHaJrakHipIUYTJ8fk3Jf2UN";

    // 1. Get the Issuer's distinguished name
    string strOutput = X509.QueryCert(strCertificado, "issuerName");
    Console.WriteLine("ISSUER= [" + strOutput + "]");

    // 2. Get the Subject's distinguished name
    strOutput = X509.QueryCert(strCertificado, "subjectName");
    Console.WriteLine("SUBJECT=[" + strOutput + "]");

    // 3. Get the Serial Number
    strOutput = X509.QueryCert(strCertificado, "serialNumber");
    Console.WriteLine("X.509 Serial Number=0x" + strOutput);

    // 4. Get the expiry date
    strOutput = X509.QueryCert(strCertificado, "notAfter");
    Console.WriteLine("Expires on: " + strOutput);

    // 5. Get the signature algorithm
    string strQuery = "signatureAlgorithm";
    strOutput = X509.QueryCert(strCertificado, strQuery);
    Console.WriteLine(strQuery + "=" + strOutput);
}
VBA/VB6 code to extract various details from a certificate string: Show/hide VBA code
'/**
' Extract various details from a certificate string.
'**/
Public Sub Mex_QueryCertString()
    Dim strCertificado As String
    Dim strOutput As String
    Dim strQuery As String
    
    ' INPUT: Certificado string from XML file. This is the same as in the file emisor.cer expiring 2028-12-07.
    strCertificado = "MIIFXTCCA0WgAwIBAgIUMzAwMDEwMDAwMDAzMDAwMjM3MDgwDQYJKoZIhvcNAQELBQAwgbMxFjAUBgNVBAMTDUFDIGRlIHBydWViYXMxLjAsBgNVBAoTJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGTAXBgNVBAsTEFNBVC1JRVMgRmljdGljaWExCzAJBgNVBAYTAk1YMUEwPwYJKoZIhvcNAQkCEzJSZXNwb25zYWJ" & _
        "sZTogRXN0byBubyBlcyB1biBnZW51aW5vIFNBVCBjZXJ0aWZpY2FkbzAeFw0yMTEyMDcwMDAwMDFaFw0yODEyMDcwMDAwMDFaMIH6MSkwJwYDVQQDEyBBQ0NFTSBTRVJWSUNJT1MgRU1QUkVTQVJJQUxFUyBTQzEpMCcGA1UEKRMgQUNDRU0gU0VSVklDSU9TIEVNUFJFU0FSSUFMRVMgU0MxKTAnBgNVBAoTIEFDQ0VNIFNFUlZJQ0lPUyBFTV" & _
        "BSRVNBUklBTEVTIFNDMQswCQYDVQQGEwJNWDEjMCEGCSqGSIb3DQEJARYUcHJ1ZWJhc0BhY2NlbS5jb20ubXgxJTAjBgNVBC0THEFBQTAxMDEwMUFBQSAvIEhFR1Q3NjEwMDM0UzIxHjAcBgNVBAUTFSAvIEhFR1Q3NjEwMDNNREZSTk4wOTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJdUcsHIEIgwivvAantGnYVIO3+7yTdD1" & _
        "tkKopbL+tKSjRFo1ErPdGJxP3gxT5O+ACIDQXN+HS9uMWDYnaURalSIF9COFCdh/OH2Pn+UmkN4culr2DanKztVIO8idXM6c9aHn5hOo7hDxXMC3uOuGV3FS4ObkxTV+9NsvOAV2lMe27SHrSB0DhuLurUbZwXm+/r4dtz3b2uLgBc+Diy95PG+MIu7oNKM89aBNGcjTJw+9k+WzJiPd3ZpQgIedYBD+8QWxlYCgxhnta3k9ylgXKYXCYk0k0qa" & _
        "uvBJ1jSRVf5BjjIUbOstaQp59nkgHh45c9gnwJRV618NW0fMeDzuKR0CAwEAAaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwDQYJKoZIhvcNAQELBQADggIBAA1Pck4R2/W/YiY2pmVtv7mJ+dxCHJYal9NKv8gy89DTyW66g/DrpD4c2omv9LO8iRrzJbfcLupg7WKIri8aAdRU9/CwusNW3SrdJc9jjFMNPbvcYtjaRuPdQxV" & _
        "PWDQ+VUMD/iDYKt+4Db4npvht6UjJtUEzaqSIj9jHKSROG/KbMBZVBzyp8/A45q+krpuGEj9UOhBi3InTxv03REbb7jnBWAjSQFOhqloYHg6PtXY2nslnOhRAnvFNY1bZKvkEOVbcpxyNQdhBXvuESHRYoiOCUdlpRsPkkJRbg0BGHpCLS7d8mebdM1A6L1emuqCa8j55vVblDqAk5Re6wOvtRodZbx3QGltsDKYATd3HvR+PfdT5cwlI3sGp1f" & _
        "rvVNvYhO29BX7UXqarlQA5HyEfaK48s9vd71/yFDa1XsUXytcvGZoYP8L68tbz1V8rLB0CAcRCOYYbMo4YPkdbP4BlYd8sAsljwsbpDl0DymcUEj/07KN/5diXAB9oqUxzEy5pjPLItF+WAB5XkrLZFtMEwYkpOLGIozwA1sp4EHA+ww/EZU3NQsmLiiQd00e4yIuHA4wkQ8HCcSkE+x4tpJ60j3gGxwHpcY2fb9uruxKaMrtluUIFJJ0AW04yp" & _
        "n0fGhTg8GV1rfvqY7aGUdoDyHZJQHaJrakHipIUYTJ8fk3Jf2UN"
    ' 1. Get the Issuer's distinguished name (converting any UTF-8 characters to Latin-1)
    strOutput = x509QueryCert(strCertificado, "issuerName", PKI_X509_LATIN1)
    Debug.Print "ISSUER= [" & strOutput & "]"
    
    ' 2. Get the Subject's distinguished name
    strOutput = x509QueryCert(strCertificado, "subjectName", PKI_X509_LATIN1)
    Debug.Print "SUBJECT=[" & strOutput & "]"
    
    ' 3. Get the Serial Number
    strOutput = x509QueryCert(strCertificado, "serialNumber", PKI_X509_LATIN1)
    Debug.Print "X.509 Serial Number=0x" & strOutput
    
    ' 4. Get the expiry date
    strOutput = x509QueryCert(strCertificado, "notAfter", PKI_X509_LATIN1)
    Debug.Print "Expires on: " & strOutput

    ' 5. Get the signature algorithm
    strQuery = "signatureAlgorithm"
    strOutput = x509QueryCert(strCertificado, strQuery, PKI_X509_LATIN1)
    Debug.Print strQuery & "=" & strOutput

End Sub

Expected output:

ISSUER= [CN=AC de pruebas;O=SERVICIO DE ADMINISTRACION TRIBUTARIA;OU=SAT-IES Ficticia;C=MX;1.2.840.113549.1.9.2=Responsable: Esto no es un genuino SAT certificado]
SUBJECT=[CN=ACCEM SERVICIOS EMPRESARIALES SC;2.5.4.41=ACCEM SERVICIOS EMPRESARIALES SC;O=ACCEM SERVICIOS EMPRESARIALES SC;C=MX;E=pruebas@accem.com.mx;2.5.4.45=AAA010101AAA / HEGT7610034S2;SERIALNUMBER= / HEGT761003MDFRNN09]
X.509 Serial Number=0x3330303031303030303030333030303233373038
Expires on: 2028-12-07T00:00:01Z
signatureAlgorithm=sha256WithRSAEncryption
Mex_CreateDigestFromString
C# code to create digest of input string: Show/hide C# code
/// <summary>
/// Create the SHA-256 digest of the input string after converting to UTF-8 encoding
/// </summary>
/// <returns>Digest value in hex</returns>
public static string Mex_CreateDigestFromString()
{
    /// 
    string strData = "||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|99999|A1234|05|18|2021|09|ED1752FE-E865-4FF2-BFE1-0F552E770DC9|AAA010101AAA|Esta es una demostración|630|0123456789|BASJ600902KL9|Juanito Bananas De la Sierra|99999|MEX|0000000000000|630|S01|01010101|00001|1.5|C81|TONELADA|ACERO|1500000|2250000|01|2250000|002|Tasa|1.600000|360000|2250000|001|Tasa|0.300000|247500|51888|95141904|00002|1.6|WEE|TONELADA|ALUMINIO|1500|2400|02|2400|002|Tasa|1.600000|384|2400|001|Tasa|0.300000|264|AAA010101AAA|NombreACuentaTerceros|630|99999|15 48 4567 6001234|84101604|00003|1.7|G66|TONELADA|ZAMAC|10000|17000|0|03|17000|002|Tasa|1.600000|2720|17000|001|Tasa|0.300000|1870|25201513|055155|1.0|UNIDAD|PARTE EJEMPLO|1.00|1.00|15 48 4567 6001235|001|247000|003|500|247500|1.00|002|Tasa|1.600000|360000|360000||";
    Console.WriteLine("INPUT=" + strData);

    // 1. Convert string to UTF-8-encoded byte array
    byte[] abData = Encoding.UTF8.GetBytes(strData);

    // 2. Create the message digest hash
    string strDigest = Hash.HexFromBytes(abData, HashAlgorithm.Sha256);

    // OUTPUT: Display digest in hex format
    Console.WriteLine("Digest=" + strDigest);
    Debug.Assert(strDigest.Equals("c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e", StringComparison.OrdinalIgnoreCase));

    return strDigest.ToLower();
}
VBA/VB6 code to create digest of input string: Show/hide VBA code
'/**
' Create the SHA-256 digest of the input string after converting to UTF-8 encoding.
' @return Digest value in hex
'**/
Public Function Mex_CreateDigestFromString() As String
    Dim strData As String
    Dim abData() As Byte
    Dim strDigest As String
    
    ' INPUT: Our original piped-string data (NB includes non-ASCII character ó (LATIN SMALL LETTER O WITH ACUTE U+00F3)
    ' from file cfdv40-ejemplo.xml
    strData = "||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|99999|A1234|05|18|2021|09|ED1752FE-E865-4FF2-BFE1-0F552E770DC9|AAA010101AAA|Esta es una demostración|630|0123456789|BASJ600902KL9|Juanito Bananas De la Sierra|99999|MEX|0000000000000|630|S01|01010101|00001|1.5|C81|TONELADA|ACERO|1500000|2250000|01|2250000|002|Tasa|1.600000|360000|2250000|001|Tasa|0.300000|247500|51888|95141904|00002|1.6|WEE|TONELADA|ALUMINIO|1500|2400|02|2400|002|Tasa|1.600000|384|2400|001|Tasa|0.300000|264|AAA010101AAA|NombreACuentaTerceros|630|99999|15 48 4567 6001234|84101604|00003|1.7|G66|TONELADA|ZAMAC|10000|17000|0|03|17000|002|Tasa|1.600000|2720|17000|001|Tasa|0.300000|1870|25201513|055155|1.0|UNIDAD|PARTE EJEMPLO|1.00|1.00|15 48 4567 6001235|001|247000|003|500|247500|1.00|002|Tasa|1.600000|360000|360000||"
    Debug.Print "INPUT=" & strData
    
    ' 1. Convert VBA string to UTF-8-encoded byte array
    abData = cnvUTF8BytesFromLatin1(strData)
    
    ' 2. Create the message digest hash
    strDigest = hashHexFromBytes(abData, PKI_HASH_SHA256)
    
    ' OUTPUT: Display digest in hex format
    Debug.Print "Digest=" & strDigest
    Mex_CreateDigestFromString = strDigest
    ' Correct c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e
    Debug.Assert strDigest = "c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e"
    
    Mex_CreateDigestFromString = strDigest
      
End Function
Reference: Hash.HexFromBytes
Mex_ExtractDigestFromSignature
C# code to extract the message digest from a signature (sello) string using the X.509 certificate: Show/hide C# code
/// <summary>
/// Extract the message digest from a signature (sello) string using the X.509 certificate.
/// </summary>
/// <returns></returns>
public static string Mex_ExtractDigestFromSignature()
{
    string strPublicKey;
    string strSello = "Gnm1yXaIig5hr1dJ+88gjLY5usQxXP2s+zdmlLl4iokWENaUUlhpG/crkUFEzcJfdq1FbBxV/d/GN50MGuw2fP5f6MkRYz75UKaKfzubUak+SCkDzYot5jZRkO6hXKe4+KAfaulP7wa8Q7oSW5ccivppLnikme0CS3KtGBrQHU/q3pjNrw+jMvsnpUc1tx91REqMWrzMTZ2D6UkGdqn8i/0mLRU2vT8vPaNg/Hr2jpVVAgQmPtfvnfnORCWSm/5qZg9Tli7nTRRVwupF4o9ajH2/Is7LoCsIgZUg3wCHkZCjaKk2mJoK7FyMTsPXRRSGhoYgwi1kNHs1aY4RHNwe5w==";
    string strCertificado = "MIIFXTCCA0WgAwIBAgIUMzAwMDEwMDAwMDAzMDAwMjM3MDgwDQYJKoZIhvcNAQELBQAwgbMxFjAUBgNVBAMTDUFDIGRlIHBydWViYXMxLjAsBgNVBAoTJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGTAXBgNVBAsTEFNBVC1JRVMgRmljdGljaWExCzAJBgNVBAYTAk1YMUEwPwYJKoZIhvcNAQkCEzJSZXNwb25zYWJsZTogRXN0byBubyBlcyB1biBnZW51aW5vIFNBVCBjZXJ0aWZpY2FkbzAeFw0yMTEyMDcwMDAwMDFaFw0yODEyMDcwMDAwMDFaMIH6MSkwJwYDVQQDEyBBQ0NFTSBTRVJWSUNJT1MgRU1QUkVTQVJJQUxFUyBTQzEpMCcGA1UEKRMgQUNDRU0gU0VSVklDSU9TIEVNUFJFU0FSSUFMRVMgU0MxKTAnBgNVBAoTIEFDQ0VNIFNFUlZJQ0lPUyBFTVBSRVNBUklBTEVTIFNDMQswCQYDVQQGEwJNWDEjMCEGCSqGSIb3DQEJARYUcHJ1ZWJhc0BhY2NlbS5jb20ubXgxJTAjBgNVBC0THEFBQTAxMDEwMUFBQSAvIEhFR1Q3NjEwMDM0UzIxHjAcBgNVBAUTFSAvIEhFR1Q3NjEwMDNNREZSTk4wOTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJdUcsHIEIgwivvAantGnYVIO3+7yTdD1tkKopbL+tKSjRFo1ErPdGJxP3gxT5O+ACIDQXN+HS9uMWDYnaURalSIF9COFCdh/OH2Pn+UmkN4culr2DanKztVIO8idXM6c9aHn5hOo7hDxXMC3uOuGV3FS4ObkxTV+9NsvOAV2lMe27SHrSB0DhuLurUbZwXm+/r4dtz3b2uLgBc+Diy95PG+MIu7oNKM89aBNGcjTJw+9k+WzJiPd3ZpQgIedYBD+8QWxlYCgxhnta3k9ylgXKYXCYk0k0qauvBJ1jSRVf5BjjIUbOstaQp59nkgHh45c9gnwJRV618NW0fMeDzuKR0CAwEAAaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwDQYJKoZIhvcNAQELBQADggIBAA1Pck4R2/W/YiY2pmVtv7mJ+dxCHJYal9NKv8gy89DTyW66g/DrpD4c2omv9LO8iRrzJbfcLupg7WKIri8aAdRU9/CwusNW3SrdJc9jjFMNPbvcYtjaRuPdQxVPWDQ+VUMD/iDYKt+4Db4npvht6UjJtUEzaqSIj9jHKSROG/KbMBZVBzyp8/A45q+krpuGEj9UOhBi3InTxv03REbb7jnBWAjSQFOhqloYHg6PtXY2nslnOhRAnvFNY1bZKvkEOVbcpxyNQdhBXvuESHRYoiOCUdlpRsPkkJRbg0BGHpCLS7d8mebdM1A6L1emuqCa8j55vVblDqAk5Re6wOvtRodZbx3QGltsDKYATd3HvR+PfdT5cwlI3sGp1frvVNvYhO29BX7UXqarlQA5HyEfaK48s9vd71/yFDa1XsUXytcvGZoYP8L68tbz1V8rLB0CAcRCOYYbMo4YPkdbP4BlYd8sAsljwsbpDl0DymcUEj/07KN/5diXAB9oqUxzEy5pjPLItF+WAB5XkrLZFtMEwYkpOLGIozwA1sp4EHA+ww/EZU3NQsmLiiQd00e4yIuHA4wkQ8HCcSkE+x4tpJ60j3gGxwHpcY2fb9uruxKaMrtluUIFJJ0AW04ypn0fGhTg8GV1rfvqY7aGUdoDyHZJQHaJrakHipIUYTJ8fk3Jf2UN";

    // 1. Read in Public key from X.509 certificate string
    strPublicKey = Rsa.ReadPublicKey(strCertificado).ToString();
    if (strPublicKey.Length == 0) {
        Console.WriteLine("ERROR: failed to read Certificado string");
        return "";
    }

    // Show we got something useful
    int keyBits = Rsa.KeyBits(strPublicKey);
    Console.WriteLine("Public key is " + keyBits + " bits long");

    // 2. Convert base64 signature value to byte array
    byte[] abData = Cnv.FromBase64(strSello);
    int nSigLen = abData.Length;
    int keyBytes = Rsa.KeyBytes(strPublicKey);
    Console.WriteLine("Signature bytes=" + nSigLen);
    Console.WriteLine("Key bytes=" + keyBytes);

    if (nSigLen != keyBytes) {
        Console.WriteLine("ERROR: key length does not match signature");
        return "";
    }

    // 3. Decrypt using RSA public key
    abData = Rsa.RawPublic(abData, strPublicKey);
    if (abData.Length == 0) {
        Console.WriteLine("ERROR: failed to decrypt RSA signature");
        return "";
    }

    Console.WriteLine("Decrypted signature=" + Environment.NewLine + Cnv.ToHex(abData));

    // 4. Decode to extract the original message digest
    byte[] abMsg = Rsa.DecodeDigestForSignature(abData);
    if (abMsg.Length == 0) {
        Console.WriteLine("ERROR: failed to extract RSA message");
        return "";
    }

    // 5. Convert to hex format
    string strDigest = Cnv.ToHex(abMsg);

    // OUTPUT: Digest in hex format
    Console.WriteLine("Message digest: " + strDigest);
    Console.WriteLine("Expected:       c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e");
    Debug.Assert(strDigest.Equals("c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e", StringComparison.OrdinalIgnoreCase), "Digest not expected");

    return strDigest.ToLower();
}
VBA/VB6 code to extract the message digest from a signature (sello) string using the X.509 certificate: Show/hide VBA code
'/**
' Extract the message digest from a signature (sello) string using the X.509 certificate (certificado) value.
' @return Digest value in hex
'**/
Public Function Mex_ExtractDigestFromSignature() As String
    Dim strPublicKey As String
    Dim strSello As String
    Dim strCertificado As String
    Dim abMsg() As Byte
    Dim abData() As Byte
    Dim nSigLen As Long
    Dim strDigestHex As String
    
    ' INPUT: Base64 strings extracted from the XML file (Ref: cfdv40-ejemplo-signed.xml)
    strSello = "Gnm1yXaIig5hr1dJ+88gjLY5usQxXP2s+zdmlLl4iokWENaUUlhpG/crkUFEzcJfdq1FbBxV/d/GN50MGuw2fP5f6MkRYz75UKaKfzubUak+SCkDzYot5jZRkO6hXKe4+KAfaulP7wa8Q7oSW5ccivppLnikme0CS3KtGBrQHU/q3pjNrw+jMvsnpUc1tx91REqMWrzMTZ2D6UkGdqn8i/0mLRU2vT8vPaNg/Hr2jpVVAgQmPtfvnfnORCWSm/5qZg9Tli7nTRRVwupF4o9ajH2/Is7LoCsIgZUg3wCHkZCjaKk2mJoK7FyMTsPXRRSGhoYgwi1kNHs1aY4RHNwe5w=="
    strCertificado = "MIIFXTCCA0WgAwIBAgIUMzAwMDEwMDAwMDAzMDAwMjM3MDgwDQYJKoZIhvcNAQELBQAwgbMxFjAUBgNVBAMTDUFDIGRlIHBydWViYXMxLjAsBgNVBAoTJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGTAXBgNVBAsTEFNBVC1JRVMgRmljdGljaWExCzAJBgNVBAYTAk1YMUEwPwYJKoZIhvcNAQkCEzJSZXNwb25zYWJsZTogRXN0byBubyBlcyB1biBnZW51aW5vIFNBVCBjZXJ0aWZpY2FkbzAeFw0yMTEyMDcwMDAwMDFaFw0yODEyMDcwMDAwMDFaMIH6MSkwJwYDVQQDEyBBQ0NFTSBTRVJWSUNJT1MgRU1QUkVTQVJJQUxFUyBTQzEpMCcGA1UEKRMgQUNDRU0gU0VSVklDSU9TIEVNUFJFU0FSSUFMRVMgU0MxKTAnBgNVBAoTIEFDQ0VNIFNFUlZJQ0lPUyBFTVBSRVNBUklBTEVTIFNDMQswCQYDVQQGEwJNWDEjMCEGCSqGSIb3DQEJARYUcHJ1ZWJhc0BhY2NlbS5jb20ubXgxJTAj" & _
        "BgNVBC0THEFBQTAxMDEwMUFBQSAvIEhFR1Q3NjEwMDM0UzIxHjAcBgNVBAUTFSAvIEhFR1Q3NjEwMDNNREZSTk4wOTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJdUcsHIEIgwivvAantGnYVIO3+7yTdD1tkKopbL+tKSjRFo1ErPdGJxP3gxT5O+ACIDQXN+HS9uMWDYnaURalSIF9COFCdh/OH2Pn+UmkN4culr2DanKztVIO8idXM6c9aHn5hOo7hDxXMC3uOuGV3FS4ObkxTV+9NsvOAV2lMe27SHrSB0DhuLurUbZwXm+/r4dtz3b2uLgBc+Diy95PG+MIu7oNKM89aBNGcjTJw+9k+WzJiPd3ZpQgIedYBD+8QWxlYCgxhnta3k9ylgXKYXCYk0k0qauvBJ1jSRVf5BjjIUbOstaQp59nkgHh45c9gnwJRV618NW0fMeDzuKR0CAwEAAaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwDQYJKoZIhvcNAQELBQADggIBAA1Pck4R2/W/YiY2pmVtv7mJ+dxCHJYal9NKv8gy89DTyW66" & _
        "g/DrpD4c2omv9LO8iRrzJbfcLupg7WKIri8aAdRU9/CwusNW3SrdJc9jjFMNPbvcYtjaRuPdQxVPWDQ+VUMD/iDYKt+4Db4npvht6UjJtUEzaqSIj9jHKSROG/KbMBZVBzyp8/A45q+krpuGEj9UOhBi3InTxv03REbb7jnBWAjSQFOhqloYHg6PtXY2nslnOhRAnvFNY1bZKvkEOVbcpxyNQdhBXvuESHRYoiOCUdlpRsPkkJRbg0BGHpCLS7d8mebdM1A6L1emuqCa8j55vVblDqAk5Re6wOvtRodZbx3QGltsDKYATd3HvR+PfdT5cwlI3sGp1frvVNvYhO29BX7UXqarlQA5HyEfaK48s9vd71/yFDa1XsUXytcvGZoYP8L68tbz1V8rLB0CAcRCOYYbMo4YPkdbP4BlYd8sAsljwsbpDl0DymcUEj/07KN/5diXAB9oqUxzEy5pjPLItF+WAB5XkrLZFtMEwYkpOLGIozwA1sp4EHA+ww/EZU3NQsmLiiQd00e4yIuHA4wkQ8HCcSkE+x4tpJ60j3gGxwHpcY2fb9uruxKaMrtluUIFJJ0AW04ypn0fGhTg8GV1rfvq" & _
        "Y7aGUdoDyHZJQHaJrakHipIUYTJ8fk3Jf2UN"

    ' 1. Read in Public key from X.509 certificate string directly
    strPublicKey = rsaReadPublicKey(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 " & rsaKeyBits(strPublicKey) & " bits long"
    
    ' 2. Convert base64 signature value to byte array
    abData = cnvFromBase64(strSello)
    ' 2a. Check lengths match
    nSigLen = cnvBytesLen(abData)
    Debug.Print "Signature bytes=" & nSigLen
    Debug.Print "Key bytes =" & rsaKeyBytes(strPublicKey)
    If nSigLen <> rsaKeyBytes(strPublicKey) Then
        Debug.Print "ERROR: key length does not match signature"
        Exit Function
    End If
       
    ' 3.Decrypt using RSA public key
    abData = rsaRawPublic(abData, strPublicKey)
    If cnvBytesLen(abData) = 0 Then
        Debug.Print "ERROR: failed to decrypt RSA signature"
        Exit Function
    End If
    ' Display result in hex
    Debug.Print "Decrypted signature=" & vbCrLf & cnvHexStrFromBytes(abData)
    
    ' 4. Decode to extract the original message digest
    abMsg = rsaDecodeMsg(abData, PKI_EMSIG_PKCSV1_5)
    If cnvBytesLen(abMsg) = 0 Then
         Debug.Print "ERROR: failed to extract RSA message"
        Exit Function
    End If
    
    ' 5. Convert to hex format
    strDigestHex = cnvToHex(abMsg)
    
    ' OUTPUT: Digest in hex format
    Debug.Print "Message digest: " & strDigestHex
    Debug.Print "Expected:       " & "c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e"
    Debug.Assert strDigestHex = "c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e"
    
    Mex_ExtractDigestFromSignature = LCase(cnvToHex(abMsg))
    
End Function
Mex_SignUsingDigest
C# code to create and verify a signature (Sello) using the digest of the piped-string: Show/hide C# code
/// <summary>
/// Create and verify a signature (Sello) using the digest of the piped-string.
/// </summary>
public static void Mex_SignUsingDigest()
{
    // Digest value in hex of piped-string
    string strDigest = "c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e";
    string strKeyFile = "emisor.key";
    // Use a StringBuilder to store the password so we can wipe it
    StringBuilder sbPassword = new StringBuilder("12345678a"); // CAUTION: DO NOT hardcode production passwords!

    // 1. Convert digest value to bytes
    byte[] abDigest = Cnv.FromHex(strDigest);

    // 2. Sign over the digest value using the private key
    string strSig64 = Sig.SignDigest(abDigest, strKeyFile, sbPassword.ToString(), SigAlgorithm.Rsa_Sha256);
    Console.WriteLine("SIGNATURE VALUE=" + strSig64);
    // Expecting Gnm1yX...Y4RHNwe5w==

    // 3. Clean up
    Wipe.String(sbPassword);

    // 4. Now verify using the digest and the Certificado value
    string strCertificado = "MIIFXTCCA0WgAwIBAgIUMzAwMDEwMDAwMDAzMDAwMjM3MDgwDQYJKoZIhvcNAQELBQAwgbMxFjAUBgNVBAMTDUFDIGRlIHBydWViYXMxLjAsBgNVBAoTJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGTAXBgNVBAsTEFNBVC1JRVMgRmljdGljaWExCzAJBgNVBAYTAk1YMUEwPwYJKoZIhvcNAQkCEzJSZXNwb25zYWJsZTogRXN0byBubyBlcyB1biBnZW51aW5vIFNBVCBjZXJ0aWZpY2FkbzAeFw0yMTEyMDcwMDAwMDFaFw0yODEyMDcwMDAwMDFaMIH6MSkwJwYDVQQDEyBBQ0NFTSBTRVJWSUNJT1MgRU1QUkVTQVJJQUxFUyBTQzEpMCcGA1UEKRMgQUNDRU0gU0VSVklDSU9TIEVNUFJFU0FSSUFMRVMgU0MxKTAnBgNVBAoTIEFDQ0VNIFNFUlZJQ0lPUyBFTVBSRVNBUklBTEVTIFNDMQswCQYDVQQGEwJNWDEjMCEGCSqGSIb3DQEJARYUcHJ1ZWJhc0BhY2NlbS5jb20ubXgxJTAjBgNVBC0THEFBQTAxMDEwMUFBQSAvIEhFR1Q3NjEwMDM0UzIxHjAcBgNVBAUTFSAvIEhFR1Q3NjEwMDNNREZSTk4wOTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJdUcsHIEIgwivvAantGnYVIO3+7yTdD1tkKopbL+tKSjRFo1ErPdGJxP3gxT5O+ACIDQXN+HS9uMWDYnaURalSIF9COFCdh/OH2Pn+UmkN4culr2DanKztVIO8idXM6c9aHn5hOo7hDxXMC3uOuGV3FS4ObkxTV+9NsvOAV2lMe27SHrSB0DhuLurUbZwXm+/r4dtz3b2uLgBc+Diy95PG+MIu7oNKM89aBNGcjTJw+9k+WzJiPd3ZpQgIedYBD+8QWxlYCgxhnta3k9ylgXKYXCYk0k0qauvBJ1jSRVf5BjjIUbOstaQp59nkgHh45c9gnwJRV618NW0fMeDzuKR0CAwEAAaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwDQYJKoZIhvcNAQELBQADggIBAA1Pck4R2/W/YiY2pmVtv7mJ+dxCHJYal9NKv8gy89DTyW66g/DrpD4c2omv9LO8iRrzJbfcLupg7WKIri8aAdRU9/CwusNW3SrdJc9jjFMNPbvcYtjaRuPdQxVPWDQ+VUMD/iDYKt+4Db4npvht6UjJtUEzaqSIj9jHKSROG/KbMBZVBzyp8/A45q+krpuGEj9UOhBi3InTxv03REbb7jnBWAjSQFOhqloYHg6PtXY2nslnOhRAnvFNY1bZKvkEOVbcpxyNQdhBXvuESHRYoiOCUdlpRsPkkJRbg0BGHpCLS7d8mebdM1A6L1emuqCa8j55vVblDqAk5Re6wOvtRodZbx3QGltsDKYATd3HvR+PfdT5cwlI3sGp1frvVNvYhO29BX7UXqarlQA5HyEfaK48s9vd71/yFDa1XsUXytcvGZoYP8L68tbz1V8rLB0CAcRCOYYbMo4YPkdbP4BlYd8sAsljwsbpDl0DymcUEj/07KN/5diXAB9oqUxzEy5pjPLItF+WAB5XkrLZFtMEwYkpOLGIozwA1sp4EHA+ww/EZU3NQsmLiiQd00e4yIuHA4wkQ8HCcSkE+x4tpJ60j3gGxwHpcY2fb9uruxKaMrtluUIFJJ0AW04ypn0fGhTg8GV1rfvqY7aGUdoDyHZJQHaJrakHipIUYTJ8fk3Jf2UN";
    int r = Sig.VerifyDigest(strSig64, abDigest, strCertificado, SigAlgorithm.Rsa_Sha256);
    Console.WriteLine("Sig.VerifyDigest returns " + r + " (0 => signature verified)");
}
VBA/VB6 code to create and verify a signature (Sello) using the digest of the piped-string: Show/hide VBA code
'/**
' Create and verify a signature (Sello) using the digest of the piped-string.
'**/
Public Sub Mex_SignUsingDigest()
    Dim strDigest As String
    Dim abDigest() As Byte
    Dim strKeyFile As String
    Dim strPassword As String
    Dim strSig64 As String
    Dim strCertificado As String
    Dim r As Long
    
    ' INPUT: Digest value in hex of piped-string
    strDigest = "c1492662dbd98ddbb7892d027c10808236e296eded13deec87179c2a8fdc742e"
    strKeyFile = "emisor.key"
    ' Test password - CAUTION: DO NOT hardcode production passwords!
    strPassword = "12345678a"
       
    ' 1. Convert digest value to bytes
    abDigest = cnvFromHex(strDigest)
         
    ' 2. Sign the digest using the private key, output direct to a base64 string,
    '  using sha256WithRSAEncryption algorithm (RSA-SHA-256). Note the USEDIGEST flag
    strSig64 = sigSignData(abDigest, strKeyFile, strPassword, "sha256WithRSAEncryption", PKI_SIG_USEDIGEST)
    Debug.Print "SIGNATURE VALUE=" & strSig64
    ' Expecting Gnm1yX...Y4RHNwe5w==

    ' 3. Clean up
    strPassword = wipeString(strPassword)
    
    ' 4. Now verify using the digest and the Certificado value
    strCertificado = "MIIFXTCCA0WgAwIBAgIUMzAwMDEwMDAwMDAzMDAwMjM3MDgwDQYJKoZIhvcNAQELBQAwgbMxFjAUBgNVBAMTDUFDIGRlIHBydWViYXMxLjAsBgNVBAoTJVNFUlZJQ0lPIERFIEFETUlOSVNUUkFDSU9OIFRSSUJVVEFSSUExGTAXBgNVBAsTEFNBVC1JRVMgRmljdGljaWExCzAJBgNVBAYTAk1YMUEwPwYJKoZIhvcNAQkCEzJSZXNwb25zYWJ" & _
        "sZTogRXN0byBubyBlcyB1biBnZW51aW5vIFNBVCBjZXJ0aWZpY2FkbzAeFw0yMTEyMDcwMDAwMDFaFw0yODEyMDcwMDAwMDFaMIH6MSkwJwYDVQQDEyBBQ0NFTSBTRVJWSUNJT1MgRU1QUkVTQVJJQUxFUyBTQzEpMCcGA1UEKRMgQUNDRU0gU0VSVklDSU9TIEVNUFJFU0FSSUFMRVMgU0MxKTAnBgNVBAoTIEFDQ0VNIFNFUlZJQ0lPUyBFTV" & _
        "BSRVNBUklBTEVTIFNDMQswCQYDVQQGEwJNWDEjMCEGCSqGSIb3DQEJARYUcHJ1ZWJhc0BhY2NlbS5jb20ubXgxJTAjBgNVBC0THEFBQTAxMDEwMUFBQSAvIEhFR1Q3NjEwMDM0UzIxHjAcBgNVBAUTFSAvIEhFR1Q3NjEwMDNNREZSTk4wOTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJdUcsHIEIgwivvAantGnYVIO3+7yTdD1" & _
        "tkKopbL+tKSjRFo1ErPdGJxP3gxT5O+ACIDQXN+HS9uMWDYnaURalSIF9COFCdh/OH2Pn+UmkN4culr2DanKztVIO8idXM6c9aHn5hOo7hDxXMC3uOuGV3FS4ObkxTV+9NsvOAV2lMe27SHrSB0DhuLurUbZwXm+/r4dtz3b2uLgBc+Diy95PG+MIu7oNKM89aBNGcjTJw+9k+WzJiPd3ZpQgIedYBD+8QWxlYCgxhnta3k9ylgXKYXCYk0k0qa" & _
        "uvBJ1jSRVf5BjjIUbOstaQp59nkgHh45c9gnwJRV618NW0fMeDzuKR0CAwEAAaMgMB4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBsAwDQYJKoZIhvcNAQELBQADggIBAA1Pck4R2/W/YiY2pmVtv7mJ+dxCHJYal9NKv8gy89DTyW66g/DrpD4c2omv9LO8iRrzJbfcLupg7WKIri8aAdRU9/CwusNW3SrdJc9jjFMNPbvcYtjaRuPdQxV" & _
        "PWDQ+VUMD/iDYKt+4Db4npvht6UjJtUEzaqSIj9jHKSROG/KbMBZVBzyp8/A45q+krpuGEj9UOhBi3InTxv03REbb7jnBWAjSQFOhqloYHg6PtXY2nslnOhRAnvFNY1bZKvkEOVbcpxyNQdhBXvuESHRYoiOCUdlpRsPkkJRbg0BGHpCLS7d8mebdM1A6L1emuqCa8j55vVblDqAk5Re6wOvtRodZbx3QGltsDKYATd3HvR+PfdT5cwlI3sGp1f" & _
        "rvVNvYhO29BX7UXqarlQA5HyEfaK48s9vd71/yFDa1XsUXytcvGZoYP8L68tbz1V8rLB0CAcRCOYYbMo4YPkdbP4BlYd8sAsljwsbpDl0DymcUEj/07KN/5diXAB9oqUxzEy5pjPLItF+WAB5XkrLZFtMEwYkpOLGIozwA1sp4EHA+ww/EZU3NQsmLiiQd00e4yIuHA4wkQ8HCcSkE+x4tpJ60j3gGxwHpcY2fb9uruxKaMrtluUIFJJ0AW04yp" & _
        "n0fGhTg8GV1rfvqY7aGUdoDyHZJQHaJrakHipIUYTJ8fk3Jf2UN"
    r = sigVerifyData(strSig64, abDigest, strCertificado, "sha256WithRSAEncryption", PKI_SIG_USEDIGEST)
    Debug.Print "sigVerifyData returns " & r & " (0 => signature verified)"

End Sub

Notes on the code

  1. You can use the certificate filename (emisor.cer) or the Certificado base64 string "MIIFXTCCA0WgAwIBAgIUMzAwMDEwMDAwMDAz..." interchangably when calling CryptoSys PKI functions.
  2. You can create the signature and verify it by passing either the original piped-string or its SHA-256 message digest value. See Mex_CreateSignature and Mex_VerifySignature vs Mex_SignUsingDigest.

Download the full code

C# code: MexicoSAT-csharp-3.0.0.zip

VBA code: MexicoSAT-vba-3.0.0.zip

The test files are in the work subdirectory in the zip files. The password for emisor.key is 12345678a

Composing the Piped-string

The critical part of creating a signature over a CFDi document is to compose the cadena original, what we call the piped-string. This is a string of values separated by the pipe symbol "|". For example
||4.0|A|123ABC|2021-12-07T23:59:59|99|30001000000300023708|CONDICIONES|1000|0.00|MXN|1.0|1500|P|03|PPD|...|Tasa|1.600000|360000|360000||

You can do this in several ways.

  1. Following the specification Secuencia de Formación in Anexo20_2022.pdf and all the associated complemento specificiations.

    cadena-original

  2. Performing an XSLT transform on your CDFi XML document using the Secuencia de cadena original cadenaoriginal_4_0.xslt.
  3. (Simplest) Using our FirmaSAT On-line Validator. You can pass a base XML document and it will tell you if it is valid SAT XML and gives you the cadena original string and its message digest value.

    Validator

The order of the fields in the string is important. The string must be accurate byte-for-byte or your signature will be wrong. In particular:

See also

Contact us

To contact us or comment on this page, please send us a message.

[Go to top]

This page first published 16 December 2005. Last updated 23 May 2026