CryptoSys PKI Data Exchange in the German Health Service with CryptoSys PKI

Our CryptoSys PKI Toolkit provides all the functions you should need to carry out the basic cryptography operations for the security interface for data exchange in the German health service, version 1.5. This page shows how to do it in VB6/VBA code.

Disclaimer

We must point out that we are completely independent of the ITSG Trustcenter and anything on this site has not been approved in any way by that organisation. The contents of this page are based on discussions we've had with users of our CryptoSys PKI Toolkit who have successfuly used it to create applications they claimed worked.

Our examples use test certificates and private keys that we made up ourselves. In no way do we intend to represent ourselves as trying to pass off as ITSG or any associated firm. We hope the names we use in our examples appear obviously false, as they were intended to be. The individual's names used in the test X.509 certificates were generated from random combinations of typical German first names and surnames. And just so there is no doubt,

The characters and organisations depicted in these examples are fictitious. Any similarity to actual persons, living or dead, or to actual organisations is purely coincidental.

Apologies, too, that this is in English. We are English speakers and can't write German to an acceptable standard. Well, actually, not to any standard. We got stuck at "Zwei Biere, bitte". We can, though, make a pretty good stab at understanding the technical documents involved here.

Overview

The basic procedures to be carried out break down into the following tasks.

  1. Creating your own public/private key pair and getting a certificate from your Certification Authority (CA).
  2. Extracting your own X.509 certificate from the .p7c file sent by the CA.
  3. Extracting the X.509 certificates of other users from the text file sent by the CA.
  4. Creating a signed and encrypted file in the required CMS (PKCS#7) format to send to a recipient.
  5. Decrypting and verifying such a file when someone sends one to you.

Of these, the first three only need be done once.

The source code

The full source code in VB6 is in GermanHealthCode.zip (8 kB) and the test files we used are in GermanHealthTestFiles.zip (28 kB). You will need to have installed the CryptoSys PKI Toolkit on your system and include the basCrPKI VB6 module in your project (you should find it in the folder C:\Program Files\CryptoSysPKI\VB6). A fully-functional trial version of CryptoSys PKI can be downloaded from here.

The source code provided uses dummy test files that we made up ourselves. The files should be in the correct format as the real ones. You will need to tailor the code to your own circumstances and identity and substitute the file names and folders to match the ones on your system. If you find a problem, please let us know. The current code is in VB6/VBA. We will get around to producing a version in C# at some stage in the future. Drop us a line to remind us if you are interested.

File list

GermanHealthCode.zip
basGermanHealthSetup.bas - VB6 procedures to get set up.
basGermanHealthExamples.bas - VB6 procedures to create signed and encrypted messages, etc.
GermanHealthCode.zip
999009991_pri.p8 - our encrypted private key file
999009991_pub.p1 - our public key file
999009991.p10 - the CRS file we sent to our CA
999009991.p7c - the .p7c certificate chain file we received back
annahme-test-certs.txt - the dummy list of other certificates we received
TheCert1.cer - a certificate as extracted from the .p7c file
TheCert2.cer - ditto
TheCert3.cer - ditto
999009991.cer - our own certificate extracted from the .p7c file (=TheCert1.cer)
Int_Cert.cer - Our dummy CA's self-signed root certificate (=TheCert2.cer)
CA_Cert.cer  - Our dummy intermediate CA's certificate (=TheCert3.cer)
999009051.cer - the certificate for user with dummy id IK999009051
To_999009051.p7m - a message we sent to user IK999009051
999009051_pri.p8 - user IK999009051's private key file (!)

Creating your own public/private key pair and getting a certificate from your Certification Authority (CA)

Step 1.
On your machine, create a new RSA public/private key pair using RSA_MakeKeys.

You currently need a key of length 2048 bits and should use an exponent e=65537. You will need to supply a password at this stage. This is used to protect your private key, so make it something decently long and secure, and keep it secret.

' Set filenames to be created
strPublicKeyFile = TESTPATH & YOUR_PKID & "_pub.p1"
strPrivateKeyFile = TESTPATH & YOUR_PKID & "_pri.p8"
' Set your password - use something decent!
strPassword = "password"

' Create a new pair of 2048-bit RSA keys saved as binary BER-encoded files
nRet = RSA_MakeKeys(strPublicKeyFile, strPrivateKeyFile, 2048, _
    PKI_RSAEXP_EQ_65537, 50, 1000, strPassword, "", 0, PKI_KEYGEN_INDICATE)

This function creates two files: your public key in what we call here a .p1 file and an encrypted private key in a .p8 file. These file extensions (.p1 and .p8) are our own convention. As far as we know, there is no accepted conventions to name these files.

This process takes a minute or so, depending on the speed of your machine. Every time you do this you will create a new RSA public/private key pair. Each one will be different. You will never be able to reproduce the same values again.

Step 2.
Generate a Certificate Request file (CRS, PKCS#10, .p10) file using X509_CertRequest.
strDN = "C=DE;O=ISTG Beispiel Vertrauen Mitte;OU=Unsere Firma;OU=IK999009991;CN=Erika Mustermann"
strPrivateKeyFile = TESTPATH & YOUR_PKID & "_pri.p8"
strPassword = "password"
strCrsFile = TESTPATH & YOUR_PKID & "1.p10"

nRet = X509_CertRequest(strCrsFile, strPrivateKeyFile, strDN, "", _
   strPassword, PKI_X509_FORMAT_BIN + PKI_SIG_SHA1RSA)

To submit to ITSG, you need to generate a binary file. You need to specify a valid Distinguished Name. Here is what we have used in our dummy test examples and what we have been told that ITSG actually require. Please make your own checks before submitting your data to them. You may be charged a fee if you are wrong. Don't blame us!

Field Name What we have used in our dummy test example  What ITSG probably expects
countryNameC=DEC=DE
organizationNameO=ISTG Beispiel Vertrauen MitteO=ITSG TrustCenter fuer sonstige Leistungserbringer
organizationalUnitOU=Unsere FirmaOU=(your company)
organizationalUnitOU=IK999009991OU=IK(ID number)
commonNameCN=Erika MustermannCN=(name of the person responsible)

These keys need to be put into a string in the correct order, separated by a semi-colon character (;), with no extra spaces and no umlaute or essetz characters (use the "ae", "oe", "ue" and "ss" forms). The order of field names is important and, yes, there are two "OU" fields, one for your company name and one for your IK id number, in that order.

The distinguished name string for our dummy example above would be

"C=DE;O=ISTG Beispiel Vertrauen Mitte;OU=Unsere Firma;OU=IK999009991;CN=Erika Mustermann"

The misspelling of "ISTG" in our example is deliberate on our part. Fill in your own correct details as required.

Step 3.
Create an MD5 message digest hash of your public key file using the HASH_HexFromFile function with the PKI_HASH_MD5 option.
strPublicKeyFile = TESTPATH & YOUR_PKID & "_pub.p1"
strDigest = String(PKI_MD5_CHARS, " ")
nRet = HASH_HexFromFile(strDigest, Len(strDigest), strPublicKeyFile, PKI_HASH_MD5)
Debug.Print "MD5(PublicKey)=" & strDigest

In our example, we got

MD5(PublicKey)=be854ca565a0951633f6d101d964d8e5
Step 4.
Send this .p10 file and message digest hash plus any fees and any other identification information required to your CA; in your case, ITSG.
Step 5.
Receive back a .p7c file containing your X.509 certificate and the CA's X.509 certificates used to sign your certificate, together with a text file containing the certificates of valid recipients in base64 format. The latter file is currently sent with the filename annahme-pkcs.key. It is a text file and can be viewed with any text file editor, including NotePad.

Our test files

Here are the test files we have created: 999009991.p7c.zip and annahme-test-certs.txt. These should be similar to what you get back in practice. These files are included in the source code above.

This is the output from using the Windows CERTMGR.EXE program on the test certificates we created.

Extracting your own X.509 certificate from the .p7c file sent by the CA

To use with CryptoSys PKI you need to extract your X.509 certificate from the .p7c file you receieved. Use the X509_GetCertFromP7Chain function.

Public Sub Split_p7c_file()
' Extracts all X.509 certificates from a PKCS#7 certificate list file
' INPUT:  Name of pkcs7 certificate list file containing sender's X.509 certificate and certificates of all issuers.
' OUTPUT: Set of X.509 certificate files: TheCert1, TheCert2, TheCert3, etc.
    Dim nRet As Long
    Dim strListFile As String
    Dim strCertFile As String
    Dim iCert As Long
    Dim nCerts As Long
    
    strListFile = TESTPATH & YOUR_PKID & ".p7c"
    ' How many certificates?
    nCerts = X509_GetCertFromP7Chain("", strListFile, 0, 0)
    Debug.Print "X509_GetCertFromP7Chain(0) returns " & nCerts & " for " & strListFile
    ' Enumerate through them all
    If nCerts > 0 Then
        For iCert = 1 To nCerts
            strCertFile = TESTPATH & "TheCert" & iCert & ".cer"
            nRet = X509_GetCertFromP7Chain(strCertFile, strListFile, iCert, 0)
            Debug.Print "X509_GetCertFromP7Chain(" & iCert & ") returns " _
                & nRet & "->" & strCertFile
        Next
    End If
    
' But we don't know which one is ours...so see the next procedure
' (it's most likely that the first one is yours, but we'll check anyway)
End Sub

There is a more detailed procedure Check_CertList_With_PrivateKey() in the full source code that will actually find and extract your own X.509 certificate if you provide your private key. The procedure TestTheCerts(), also in the full source code, that will verify that your certificate is valid and was indeed issued by the owners of the certificates claimed.

The Genuine Root Certificate

If you use the real root certificate, you will need to change the hard-coded SHA-1 thumbprint in the code. The correct value we have for the genuine root certificate issued by "Datenaustausch im Gesundheits- und Sozialwesen" with serial number 01 issued 2005-11-14, expiring 2013-01-04, is

SHA-1 thumbprint=8ce6e391d1dba9afd0f809dc7697fa768ea6b709

Extracting the X.509 certificates of other users from the text file sent by the CA

The text file contains the currently valid X.509 certificates of all the registered participants in the German Health system. There is no secret information here. It is all deliberately public. You can find the public key details of your recipient from this file.

The code reads the base64 string from the text file using standard file reading techniques, and saves to a new X.509 certificate file using the X509_SaveFileFromString function. We use the new X.509 certificate file to send an encrypted message to the owner.

Private Function ExtractCertsFromB64File(strFileIn As String, ByVal strOutPath As String) As Integer
' Given a text file consisting of base64-encoded X.509 certificates separated by a blank line
' extract all the certificates [cert01.cer, cert02.cer, ..., certN.cer] and return N on success
' or 0 if no certs found, or -1 if file error.

    Dim hFile As Integer
    Dim strLine As String
    Dim strData As String
    Dim iCert As Integer
    Dim strCertName As String
    Dim nRet As Long
    
    ' Make sure strOutpath has a trailing backslash, unless blank
    strOutPath = Trim(strOutPath)
    If Len(strOutPath) > 0 And Right$(strOutPath, 1) <> "\" Then
        strOutPath = strOutPath & "\"
    End If
    
    ' Make sure file exists
    If Len(Dir(strFileIn)) = 0 Then
        MsgBox "Cannot find file '" & strFileIn & "'", vbCritical
        ExtractCertsFromB64File = -1
        Exit Function
    End If
    
    hFile = FreeFile
    Open strFileIn For Input As #hFile
    If LOF(hFile) = 0 Then
        MsgBox "File is empty", vbExclamation
        Close #hFile
        Exit Function
    End If
    
    ' Read in the data file line-by-line until find a blank line or EOF
    iCert = 0
    strData = ""
    Do Until EOF(hFile)
        Line Input #hFile, strLine
        If Len(Trim(strLine)) = 0 Then
            ' We have blank line, so save what we have so far as an X.509 cert
            iCert = iCert + 1
            strCertName = strOutPath & "cert" & Format(iCert, "00") & ".cer"
            ''Debug.Print strData
            
            ' Save from base64 string to a DER-encoded X.509 certificate file
            nRet = X509_SaveFileFromString(strCertName, strData, 0)
            Debug.Print "X509_SaveFileFromString returns " & nRet & " (expecting 0)"
            If nRet = 0 Then
                Debug.Print "Saved X.509 certificate file '" & strCertName & "'"
            Else
                Debug.Print "ERROR (" & nRet & ") saving cert file: " & pkiErrorLookup(nRet)
            End If
            
            ' NOTE: we can actually use the X509_CertSerialNumber, X509_QueryCert, etc. functions
            ' to query directly the base64 string itself instead of the file.
            Call ShowCertDetailsFromString(strData)
            
            strData = ""
        Else
            ' Just append the line to existing base64 data
            strData = strData & strLine
        End If
    Loop
    ' Catch final cert, if any
    If Len(strData) > 0 Then
        iCert = iCert + 1
        strCertName = strOutPath & "cert" & Format(iCert, "00")
        Debug.Print strData
        strData = ""
    End If
    Close #hFile
    
    ' Return number of X.509 cert files created
    ExtractCertsFromB64File = iCert

End Function

Public Function ShowCertDetailsFromString(strData As String)
' NOTE: We can replace the strCertFile parameter with a string containing the cert as a base64 string
    Dim strOutput As String
    Dim strQuery As String
    Dim nRet As Long
    
    ' Make a large buffer to receive output
    strOutput = String(1024, " ")
    
    strQuery = "serialNumber"
    nRet = X509_QueryCert(strOutput, Len(strOutput), strData, strQuery, 0)
    If nRet <= 0 Then Exit Function ' catch error
    Debug.Print strQuery & "=" & Left(strOutput, nRet)

    strQuery = "subjectName"
    nRet = X509_QueryCert(strOutput, Len(strOutput), strData, strQuery, 0)
    If nRet <= 0 Then Exit Function ' catch error
    Debug.Print strQuery & ": " & Left(strOutput, nRet)
End Function

Creating a signed and encrypted file in the required CMS (PKCS#7) format to send to a recipient.

Finally, after all the one-off preparation above, we can actually send some information to another user. The required format is to sign then encrypt the information in CMS/PKCS#7 format. We read our private key with the pre-written rsaReadPrivateKey function (which calls RSA_ReadEncPrivateKey), and use the CMS_MakeSigDataFromString function and then the CMS_MakeEnvData function.

The general algorithm is

1. (sigfile) <-- MakeSigData(message, certlist, privatekey, password)
2. (envfile) <-- MakeEnvData(sigfile, recipcert)

where message is the text data to be sent; certlist is a list of filenames of X.509 certificates, the first of which must be yours; privatekey is your own RSA private key; password is the password that protects your private key; sigdata is the binary signed-data value output by the signing procedure; recipcert is the filename of the recipient's X.509 certificate (a .cer file); and envfile is the binary enveloped-data value to be sent to the recipient.

Remarks

Public Function Make_Signed_And_EnvelopedData( _
    strOutputFile As String, _
    strMsg As String, _
    strPriKeyFile As String, _
    strPassword As String, _
    strSignersCertList As String, _
    strRecipCertFile As String _
) As Long

    Dim nRet As Long
    Dim strPrivateKey As String
    Dim strSigFile As String
    
    ' Intermediate signed-data file we will create
    strSigFile = strOutputFile & ".int.tmp"
    
    ' Read in the private key string
    Debug.Print "Reading private key from PRI file..."
    strPrivateKey = rsaReadPrivateKey(strPriKeyFile, strPassword)
    ' Check for success
    If Len(strPrivateKey) = 0 Then
        MsgBox "Cannot read private key", vbCritical
        Exit Function
    Else
        Debug.Print "...OK, read private key: key length=" & RSA_KeyBits(strPrivateKey) & " bits"
    End If
    
    ' Create a signed-data object with signed attributes and signing-time and all certificates.
    nRet = CMS_MakeSigDataFromString(strSigFile, strMsg, strSignersCertList, _
        strPrivateKey, PKI_CMS_INCLUDE_ATTRS + PKI_CMS_ADD_SIGNTIME)
    Debug.Print "CMS_MakeSigDataFromString returns " & nRet & " (expecting 0)"
    ' Clean up as we go
    Call WIPE_String(strPrivateKey, Len(strPrivateKey))
    ' Check for success
    If nRet <> 0 Then
        Debug.Print "ERROR (" & nRet & "): " & pkiErrorLookup(nRet) & ": " & pkiGetLastError()
        MsgBox "Cannot create signed-data file", vbCritical
        Exit Function
    Else
        Debug.Print "OK, created signed-data file '" & strSigFile & "'"
    End If
    
    ' Now we encrypt the signed-data object directly using the recipient's certificate
    ' this produces a binary enveloped-data file
    nRet = CMS_MakeEnvData(strOutputFile, strSigFile, strRecipCertFile, "", 0, 0)
    Debug.Print "CMS_MakeEnvData returns " & nRet & " (expecting 1)"
    If nRet <= 0 Then
        Debug.Print "ERROR (" & nRet & "): " & pkiErrorLookup(nRet) & ": " & pkiGetLastError()
        MsgBox "Cannot create enveloped-data file", vbCritical
        Exit Function
    Else
        Debug.Print "OK, created enveloped-data file '" & strOutputFile & "'"
    End If
    
    ' Clean up intermediate file
    Call WIPE_File(strSigFile, 0)
    
    ' Now send the output file to the recipient...
    
End Function

Decrypting and verifying such a file when someone sends one to you.

To decrypt and verify a file sent to you by another user, do the following. You will need your own private key and its password.

But how do I create such a file to test it? Answer: send yourself a test message signed by yourself.

Public Function Read_Signed_and_Enveloped_Data( _
    strInputFile As String, _
    strPriKeyFile As String, _
    strPassword As String, _
    Optional strCertFile As String _
) As String
' Returns string containing output message or an empty string on error
    Dim nRet As Long
    Dim strPrivateKey As String
    Dim strSigFile As String
    Dim strOutput As String
    Dim strQuery As String
       
    ' Read in the recipient's private key
    strPrivateKey = rsaReadPrivateKey(strPriKeyFile, strPassword)
    If Len(strPrivateKey) = 0 Then
        Debug.Print "ERROR:  " & pkiGetLastError()
        MsgBox "Cannot read private key", vbCritical
        Exit Function
    End If
    
    ' Intermediate file we will create
    strSigFile = strInputFile & ".i2.tmp"
    
    ' Read the encrypted data from the enveloped-data file
    nRet = CMS_ReadEnvData(strSigFile, strInputFile, "", strPrivateKey, 0)
    Debug.Print "CMS_ReadEnvData returns " & nRet & " (expected 0)"
    If nRet <> 0 Then
        Debug.Print "ERROR (" & nRet & "): " & pkiErrorLookup(nRet) & ": " & pkiGetLastError()
    Else
        Debug.Print "Extracted signed-data file '" & strSigFile & "'"
    End If
    
    ' Pre-dimension output string for query result
    strOutput = String(64, " ")
    
    Debug.Print "For SigData file '" & strSigFile & "'..."
    nRet = CMS_QuerySigData(strOutput, Len(strOutput), strSigFile, "version", 0)
    Debug.Print "Version=" & nRet
    nRet = CMS_QuerySigData(strOutput, Len(strOutput), strSigFile, "signingTime", 0)
    If nRet > 0 Then
        Debug.Print "signingTime=" & Left$(strOutput, nRet)
    Else
        Debug.Print "ERROR=" & nRet
    End If
    strQuery = "messageDigest"
    nRet = CMS_QuerySigData(strOutput, Len(strOutput), strSigFile, strQuery, 0)
    If nRet > 0 Then
        Debug.Print strQuery & "=" & Left$(strOutput, nRet)
    Else
        Debug.Print "ERROR=" & nRet
    End If
    
    strQuery = "CountOfSignerInfos"
    nRet = CMS_QuerySigData(strOutput, Len(strOutput), strSigFile, strQuery, 0)
    If nRet > 0 Then
        Debug.Print strQuery & "=" & nRet
    Else
        Debug.Print "ERROR=" & nRet
    End If
    
    strQuery = "CountOfCertificates"
    nRet = CMS_QuerySigData(strOutput, Len(strOutput), strSigFile, strQuery, 0)
    If nRet > 0 Then
        Debug.Print strQuery & "=" & nRet
    Else
        Debug.Print "ERROR=" & nRet
    End If
    
    nRet = CMS_VerifySigData(strSigFile, "", "", 0)
    Debug.Print "CMS_VerifySigData('') returns " & nRet & " (expecting 0)"

    nRet = CMS_VerifySigData(strSigFile, strCertFile, "", 0)
    Debug.Print "CMS_VerifySigData('" & strCertFile & "') returns " & nRet & " (expecting 0)"

    Dim nDataLen As Long
    Dim strData As String
    ' How long is the content to be read?
    nDataLen = CMS_ReadSigDataToString("", 0, strSigFile, 0)
    If nDataLen <= 0 Then
        Debug.Print "ERROR (" & nRet & "): " & pkiErrorLookup(nRet) & ": " & pkiGetLastError()
        MsgBox "Cannot read signed-data file", vbCritical
        Exit Function
    End If
    ' Pre-dimension string to receive data
    strData = String(nDataLen, " ")
    nRet = CMS_ReadSigDataToString(strData, nDataLen, strSigFile, 0)
    Debug.Print "CMS_ReadSigDataToString returns " & nRet
    Debug.Print "Data is [" & strData & "]"
    
    ' Return message string
    Read_Signed_and_Enveloped_Data = strData
    
    ' Clean up intermediate file
    Call WIPE_File(strSigFile, 0)
    Call WIPE_String(strData, Len(strData))

End Function

Test user keys and certificates

In the example code, we send a message to a dummy end user called Ingrid Kruger from the company Weiss GmbH with id IK999009051. To read the message we use the private key for that user, which we have because we made it, but in practice, you won't be able to read the messages you create because they can only be decrypted by the owner of the private key, which you won't have.

If you want the actual private and public keys and binary X.509 test certificates for the dummy test users, download GermanHealthTestUserKeys.zip (52 kB) and the private keys for the root CA and intermediate CA in GermanHealthCAIntKeys.zip (6 kB). The password for all of the private key files is "password". Needless to say, in practice you will not have this information.

References

For more information, please contact us.

This page last updated: 30 July 2008