📜 ⬆️ ⬇️

Library for embedding electronic signatures in C ++ applications



Our company continues to develop a line of libraries that allow you to embed an electronic signature using Russian cryptoalgorithms in information systems of various types.

Some time ago, we supported Rutoken EDS in openssl, then released a cross-platform browser plugin, and now we created a high-level crypto library for embedding in C ++ applications.
')
Conceptually, these solutions are made identically: the hardware implementation of Russian cryptographic algorithms on the Rutoken EDS chip is used, support for X.509 digital certificates, requests for PKCS # 10 certificates, signed and encrypted CMS is provided.

The new library is useful to those who write "fat clients", desktop applications, their browser plug-ins, etc.

Supported devices:


The main scenarios for using the library with code examples under the cat.

Basic work with devices


The library provides an interface designed as a CryptoCore class:

class CryptoCore { public: CryptoCore(const std::string& pkcs11path); ~CryptoCore(); ... } 


The class constructor requires passing the PKCS # 11 library path.

Any user script begins with a search for Rutoken devices connected to the computer. For this purpose, the method is used:

 std::vector<unsigned long> enumerateDevices(); 

This method returns the vector of identifiers of the slots in which the Rutoken devices are connected.

The method has the following features:


After receiving the list of devices you need to get information about each specific device. For this purpose, a method is provided:

 DeviceInfo getDeviceInfo(unsigned long deviceId); 


 struct DeviceInfo { //    std::string label; //    (    ) std::string serial; //   std::string model; //   unsigned int type; //       bool isLoggedIn; //   PIN-        bool isPinCached; ... }; 

The possible types of Rutoken devices are given as follows:

 class CryptoCore { ... public: enum DeviceType { UNKNOWN, RUTOKEN_ECP, RUTOKEN_WEB, RUTOKEN_PINPAD_IN, KAZTOKEN, RUTOKEN_PINPAD_2 }; ... } 


An example of basic work with devices:

 std::auto_ptr<CryptoCore> cp(new CryptoCore(pkcs11path)); std::vector<unsigned long> devices = cp->enumerateDevices(); std::cout <<"Found " << devices.size() << " devices" << std::endl; for (std::vector<unsigned long>::const_iterator it = devices.begin(); it != devices.end(); it++) { unsigned int id = *it; DeviceInfo info = cp->getDeviceInfo(id); std::cout << "Device ID: " << id << std::endl; std::cout << "\tLabel: " << info.label << std::endl; std::cout << "\tSerial: " << info.serial << std::endl; std::cout << "\tModel: " << info.model << std::endl; std::cout << "\tType: "; switch (info.type) { case 0: std::cout << "Unknown"; break; case 1: std::cout << "Rutoken ECP"; break; case 2: std::cout << "Rutoken WEB"; break; case 3: std::cout << "Rutoken PINPAD IN"; break; case 4: std::cout << "KAZTOKEN"; break; case 5: std::cout << "Rutoken PINPAD2"; break; } std::cout << std::endl; } 

It should be noted that the Rutoken EDS USB token and the Rutoken DS digital card are of type RUTOKEN_ECP. For more accurate identification, you should clarify the device model. In the case of a smart card, the string “Rutoken ECP SC” will be returned.

Type RUTOKEN_PINPAD_2 corresponds to the serial device Rutoken PINPad. The RUTOKEN_PINPAD_IN type is obsolete and is used exclusively for backward compatibility.

Authorization on the device


For authorization on the device requires a PIN.

The method is intended for this:
 void login(unsigned long deviceId, const std::string& pin); 


To unlock the device, respectively:
 void logout(unsigned long deviceId); 


To obtain information on whether the device was logged in, you should use the method:
 DeviceInfo getDeviceInfo(unsigned long deviceId); 


with the isLoggedIn option;

Types of objects, possible operations with objects


The library supports work with key pairs of GOST R 34.10-2001 and digital certificates of the public key GOST R 34.10-2001 in the X.509 format.

For certain operations with these objects authorization is required on the device, for others it is not. Using the library, the following operations with objects are possible:
  1. get a list of certificates stored on the device
  2. write certificate to device
  3. read certificate from device
  4. remove certificate from device
  5. get a list of key pairs stored on the device
  6. create a key pair on the device
  7. remove key pair from device


Operations 1 and 3 do not require authorization on the device, operations 2, 4, 5, 6, 7 require.

Work with key pairs


To get the list of key pairs available on the token, use the method:
 std::vector<std::string> enumerateKeys( unsigned long deviceId, const std::string& marker); 


Returned key pair handles are unique and persistent for this key pair.

An example of obtaining key pairs stored on the device:
 cp->login(id, "12345678"); std::vector<std::string> keys = cp->enumerateKeys(id, "Test marker"); if (keys.empty()) { std::cerr << "No keys were found on token" << std::endl; } else { std::cerr << "Found " << keys.size() << " key(s) on token" << std::endl; for (size_t i = 0; i < keys.size(); i++) { std::string kId = keys[i]; std::cerr << "Key with id: " << kId << " on token with label: " << cp->getKeyLabel(id, kId) << std::endl; } } 


You can create a key pair on the device using the function:
 std::string generateKeyPair( unsigned long deviceId, const std::string& params, const std::string& marker, const std::map<std::string, bool>& options); 


Params sets the parameters in accordance with RFC 4357 to be used for the generated key.

The following options are possible:


An example of generating a key pair on the device:
 cp->login(id, "12345678"); std::map<std::string, bool> keyOptions; keyOptions["needPin"] = false; std::string keyId = cp->generateKeyPair(id, "A", "Test marker", keyOptions); std::string keyLabel; std::cerr << "Please, enter new key label: "; std::cin >> keyLabel;cp->setKeyLabel(id, keyId, keyLabel); 


To delete a key pair, use the method:
 void deleteKeyPair(unsigned long deviceId, const std::string& keyId); 


Work with certificates


The certificate is issued by the CA on PKCS # 10 request and can be recorded on the device.

To write (import) a certificate to a device, use the method:
 std::string importCertificate(unsigned long deviceId, const std::string& certificate, unsigned long category); 

The category parameter sets the attribute with which the certificate will be saved to the device:


The list of certificates stored on the device is obtained using the function:
 std::vector<std::string> enumerateCertificates( unsigned long deviceId, unsigned long category); 


The category parameter allows you to restrict the search for certificates of one of the above groups.

Example of finding user certificates on a token:
 std::vector<std::string> certs = cp->enumerateCertificates(id, PKCS11_CERT_CATEGORY_USER); // get certificates info by ID if (certs.size() > 0) { std::cout << "Certificates with USER category(" << certs.size() << "): " << std::endl; for (size_t i = 0; i < certs.size(); i++) { printCertInfo(cp.get(), id, certs.at(i)); } } 


In the example above, the printCertInfo function renders a certificate using the method:
 CertFields parseCertificate( unsigned long deviceId, const std::string& certId); 

First, information about the issuer of the certificate (issuer) is visualized, then about the owner (subject). In addition, the validity period of the certificate and its serial number are displayed. The method also allows you to parse such certificate extensions as keyUsage, extendedKeyUsage, certificatePolicies.

Certificate visualization example:

 typedef std::vector<std::map<std::string, std::string> > DnList; typedef std::map<std::string, std::vector<std::string> > ExtensionsMap; struct CertFields { DnList issuer; DnList subject; std::string serialNumber; std::string validNotBefore; std::string validNotAfter; ExtensionsMap extensions; std::string certificateText; }; void printCertInfo(CryptoCore* cp, unsigned int tokenId, std::string certId) { CertFields info = cp->parseCertificate(tokenId, certId); DnList& dn = info.issuer; std::cout << "Certificate ID: " << certId << std::endl << "\tIssuer: "; for (DnList::iterator it = dn.begin(); it != dn.end(); it++) { std::map<std::string, std::string>& rdn = *it; if (it != dn.begin()) std::cout << ", ";std::cout << rdn[ "rdn"] << "=" << rdn["value"]; } std::cout << std::endl;dn = info.subject;std::cout << "\tSubject: "; for (DnList::iterator it = dn.begin(); it != dn.end(); it++) { std::map<std::string, std::string>& rdn = *it; if (it != dn.begin()) std::cout << ", ";std::cout << rdn[ "rdn"] << "=" << rdn["value"]; } std::cout << std::endl; std::cout << "\tSerialNumber: " << info.serialNumber << std::endl; std::cout << "\tValid Not Before: " << info.validNotBefore << std::endl; std::cout << "\tValid Not After: " << info.validNotAfter << std::endl; } 

Reading the certificate from the device (export) is carried out in PEM-format using the method:
 std::string getCertificate(unsigned long deviceId, const std::string& certId); 


It turns out like this:

 -----BEGIN CERTIFICATE----- MIIBmjCCAUegAwIBAgIBATAKBgYqhQMCAgMFADBUMQswCQYDVQQGEwJSVTEPMA0G A1UEBxMGTW9zY293MSIwIAYDVQQKFBlPT08gIkdhcmFudC1QYXJrLVRlbGVjb20i MRAwDgYDVQQDEwdUZXN0IENBMB4XDTE0MTIyMjE2NTEyNVoXDTE1MTIyMjE2NTEy NVowEDEOMAwGA1UEAxMFZmZmZmYwYzAcBgYqhQMCAhMwEgYHKoUDAgIjAQYHKoUD AgIeAQNDAARADKA/O1Zw50PzMpcNkWnW39mAJcTehAhkQ2Vg7bHkIwIdf7zPe2Px HyAr6lH+stqdACK6sFYmkZ58cBjzL0WBwaNEMEIwJQYDVR0lBB4wHAYIKwYBBQUH AwIGCCsGAQUFBwMEBgYpAQEBAQIwCwYDVR0PBAQDAgKkMAwGA1UdEwEB/wQCMAAw CgYGKoUDAgIDBQADQQD5TY55KbwADGKJRK+bwCGZw24sdIyayIX5dn9hrKkNrZsW detWY3KJFylSulykS/dfJ871IT+8dXPU5A7WqG4+ -----END CERTIFICATE----- 

To delete a certificate from a device, there is a method:
 void deleteCertificate(unsigned long deviceId, const std::string& certId); 


We now turn to the finished user scripts work in the system.

Registration in the system


The certificate used for user authentication can be issued both directly in the system (if there is a CA) and external CA.

If a digital certificate is issued directly in the system, then registration takes place according to the presented scheme:



An example of generating a key and generating a PKCS # 10 request:
 std::string pkcs11path = "./"; cp = new CryptoCore(pkcs11path); std::vector<unsigned long> devices = cp->enumerateDevices(); std::cerr << "Found " << devices.size() << " devices" << std::endl; if (devices.empty()) { std::cerr << "Can't find any device" << std::endl; return 1; } unsigned long id = devices.front(); DeviceInfo info = cp->getDeviceInfo(id); std::cerr << "Device ID: " << id << std::endl; std::cerr << "\tLabel: " << info.label << std::endl; std::cerr << "\tSerial: " << info.serial << std::endl; std::cerr << std::endl; cp->login(id, "12345678"); std::vector<std::string> keys = cp->enumerateKeys(id, "Test marker"); if (keys.empty()) { std::cerr << "No keys were found on token" << std::endl; } else { std::cerr << "Found " << keys.size() << " key(s) on token" << std::endl; for (size_t i = 0; i < keys.size(); i++) { std::string kId = keys[i]; std::cerr << "Key with id: " << kId << " on token with label: " << cp->getKeyLabel(id, kId) << std::endl; } } std::map<std::string, bool> keyOptions; keyOptions["needPin"] = false; std::string keyId; // key generation keyId = cp->generateKeyPair(id, "A", "Test marker", keyOptions); std::string keyLabel; std::cerr << "Please, enter new key label: "; std::cin >> keyLabel; cp->setKeyLabel(id, keyId, keyLabel); std::cerr << "Creating PKCS#10 request on key with ID: " << keyId << std::endl; std::string str; std::vector<std::map<std::string, std::string> > subject; typedef std::map<std::string, std::string> RdnType; RdnType rdn; // country name for (;; ) { std::cerr << "Please, enter new request country name (2 symbol): "; std::cin >> str; if (str.length() != 2) { std::cerr << "try again" << std::endl; continue; } else { rdn["rdn"] = "countryName"; rdn["value"] = str; subject.push_back(rdn); break; } } // commonName std::cerr << "Please, enter new request commonName: "; std::cin >> str; rdn.clear(); rdn["rdn"] = "commonName"; rdn["value"] = str; subject.push_back(rdn); // stateOrProvince std::cerr << "Please, enter new request stateOrProvinceName: "; std::cin >> str; rdn.clear(); rdn["rdn"] = "stateOrProvinceName"; rdn["value"] = str; subject.push_back(rdn); // locality std::cerr << "Please, enter new request localityName: "; std::cin >> str; rdn.clear(); rdn["rdn"] = "localityName"; rdn["value"] = str; subject.push_back(rdn); // organization std::cerr << "Please, enter new request organizationName: "; std::cin >> str; rdn.clear(); rdn["rdn"] = "organizationName"; rdn["value"] = str; subject.push_back(rdn); std::cerr << "Please, enter new request organizationalUnitName: "; std::cin >> str; rdn.clear(); // organizationalUnit rdn["rdn"] = "organizationalUnitName"; rdn["value"] = str; subject.push_back(rdn); std::map<std::string, std::vector<std::string> > extensions; std::cout << "PKCS10 request: "<< std::endl<< cp->createPkcs10(id, keyId, subject, extensions, true); cp->logout(id); 


Example of importing a user certificate:
 std::string pkcs11path = "./"; std::auto_ptr<CryptoCore> cp(new CryptoCore(pkcs11path)); std::vector<unsigned long> devices = cp->enumerateDevices(); if (devices.empty()) { std::cout << "Can't find any device" << std::endl; return 1; } std::cout << "Found " << devices.size() << " devices" << std::endl; unsigned long id = devices.front(); DeviceInfo info = cp->getDeviceInfo(id); std::cout << "Device ID: " << id << std::endl; std::cout << "\tLabel: " << info.label << std::endl; std::cout << "\tSerial: " << info.serial << std::endl; std::cout << "\tModel: " << info.model << std::endl; cp->login(id, "12345678"); std::ifstream certFile(file, std::ios::in | std::ios::binary); std::string certBody((std::istreambuf_iterator<char>(certFile)), std::istreambuf_iterator<char>()); CertFields certInfo = cp->parseCertificateFromString(certBody); DnList& dn = certInfo.subject; std::cout << "Importing certificate: " << std::endl << "\tSubject: "; for (DnList::iterator it = dn.begin(); it != dn.end(); it++) { std::map<std::string, std::string>& rdn = *it; if (it != dn.begin()) std::cout << ", "; std::cout << rdn["rdn"] << "=" << rdn["value"]; } std::cout << std::endl; std::string certId = cp->importCertificate(id, certBody, PKCS11_CERT_CATEGORY_USER)); std::cout << "Certificate imported with ID: " << certId << std::endl; 


If registration takes place according to a certificate already held by the user, then the following scheme applies:



An example of the formation of an authentication signature:
 std::string pkcs11path = "./"; cp = new CryptoCore(pkcs11path); std::vector<unsigned long> devices = cp->enumerateDevices(); std::cerr << "Found " << devices.size() << " devices" << std::endl; if (devices.empty()) { std::cerr << "Can't find any device" << std::endl; return 1; } unsigned long id = devices.front(); DeviceInfo info = cp->getDeviceInfo(id); std::cerr << "Device ID: " << id << std::endl; std::cerr << "\tLabel: " << info.label << std::endl; std::cerr << "\tSerial: " << info.serial << std::endl; std::cerr << std::endl; std::vector<std::string> certs = cp->enumerateCertificates(id, PKCS11_CERT_CATEGORY_USER); // get certificates info by ID if (certs.size() > 0) { std::cout << "Certificates with USER category(" << certs.size() << "): " << std::endl; for (size_t i = 0; i < certs.size(); i++) { printCertInfo(cp.get(), id, certs.at(i)); } } cp->login(id, "12345678"); // serverSalt - random string from server std::string authSignature = cp->authenticate(id, certs.front(), serverSalt); 


The string of random data received from the server inside the method:
 std::string authenticate(unsigned long deviceId, const std::string& certId, const std::string& salt); 

supplemented with random data length of 32 characters and signed in the format CMS attached.

The output is the following message:



After checking the signature on the server under the message using the certificate in it, you should extract the data, disconnect serverSalt and verify it.

After that, you need to register the user in the system with a certificate.

Electronic signature


The library supports two types of signatures:


To sign in the CMS format, use the method:
 std::string sign(unsigned long deviceId, const std::string& certId, const std::string& data, const std::map<std::string, bool>& options); 


Signing options in CMS format:


For the "raw" signature method is used:
 std::string rawSign(unsigned long deviceId, const std::string& keyId, const std::string& data, const std::map<std::string, bool>& options); 

The data parameter can be either the data itself in the hex representation or the precomputed hash GOST R 34.11-94 from the data. In the second case, you need to set the calculateHash option to false.

The useHardwareHash option is used in both functions and specifies the need for hardware hash calculation in accordance with GOST R 34.11-94. If this option is set to false, then a quick software implementation of the calculation of the hash function according to GOST R 34.11-94 is applied.

To verify the "raw" signature on the server uses the public key. You can get the open part of a key pair using the method:

 std::string getPublicKeyValue(unsigned long deviceId, const std::string& keyId, const std::map<std::string, bool>& options); 

Example of signing data in CMS format:

 std::auto_ptr<CryptoCore> cp( new CryptoCore(pkcs11path)); std::vector< unsigned long> devices = cp->enumerateDevices(); std::cerr << "Found " << devices.size() << " devices" << std::endl; unsigned long id = devices.front(); DeviceInfo info = cp->getDeviceInfo(id); std::cerr << "Device ID: " << id << std::endl; std::cerr << "\tLabel: " << info.label << std::endl; std::cerr << "\tSerial: " << info.serial << std::endl; std::cerr << "\tModel: " << info.model << std::endl; std::vector<std::string> certs = cp->enumerateCertificates(id, PKCS11_CERT_CATEGORY_USER); if(certs.size() > 0) { cp->login(id, "12345678"); std::map<std::string, bool> options; options[ "addUserCertificate"] = true; options[ "addSignTime"] = true; options[ "useHardwareHash"] = false; std::string cms = cp->sign(id, certs.front(), data, options); std::cout << "-----BEGIN CMS-----" << std::endl; std::cout << cms; std::cout << "-----END CMS-----" << std::endl; } 

To verify the signature method is used:

 bool verify(unsigned long deviceId, const std::string& cms, const std::string& data, const std::vector<std::string> userCerts, const std::vector<std::string> ca, const std::vector<std::string> crl, const std::map<std::string, bool>& options) 


The method accepts a "detached" or "attached" signature in the CMS format. In the case of a “disconnected” signature, data should be transmitted in the data parameter. The signature verification is performed in two stages with the verifyCertificate option set to true: the signature is first verified using the public key from the certificate (which is located in the CMS, or passed through the userCerts parameter), then the signature under the certificate is verified using the public key from the root certificate. In the case of the verificationCertificate set to false, only the signature on the data is verified, the signature on the certificate is not verified.

To verify the signature under the certificate, the root certificates stored on the device (imported to the device as root) are used. In addition, the list of root certificates can be expanded by passing an array of additional root certificates (in the PEM format) in the ca parameter.

To check whether the certificate on which the signature is verified is revoked, the crl parameter is provided in which the CRL (revocation list) array is transmitted, each in the PEM format.

When the useHardwareHash option is set to true, the signature verification will use hardware computation of the hash function GOST R 34.11-94.

Encryption


In order to ensure the confidentiality of data exchange, the library provides for data encryption / decryption using GOST 28147-89.

Data is encrypted in CMS format. To encrypt data in the CMS format, a certificate of the “recipient” public key is required. In this case, only the owner of the private key corresponding to the certificate will be able to decrypt such a message.

To store the certificate of the “addressee” on the device, it should be written to the device by calling the importCertificate method, while passing the PKCS11_CERT_CATEGORY_OTHER as the category parameter.

Data encryption is performed by:
 std::string cmsEncrypt(unsigned long deviceId, const std::string& certId, const std::string& recipientCert, const std::string& data, const std::map<std::string, bool>& options); 


To be used in the method, you need to get the body of the “addressee” certificate, either by reading it from the token using the getCertificate method, or by receiving it in PEM format directly from the information system. To use hardware encryption according to GOST 28147-89, you need to set the useHardwareEncryption option to true. Otherwise, fast software implementation of GOST 28147-89 will be used.

To decrypt data, use the method:
 std::string cmsDecrypt(unsigned long deviceId, const std::string& keyId, const std::string& cmsData, const std::map<std::string, bool>& options); 


The keyId parameter specifies the identifier of the key pair corresponding to the certificate with which the respondent encrypted the message.

Where to get the library


The library will be distributed as part of the Rutoken SDK. Now you can get it by writing a letter to info@rutoken.ru.

Options for Windows:


This library requires the current version of the PKCS # 11 library, which can be obtained at www.rutoken.ru/support/download/pkcs

Source: https://habr.com/ru/post/249723/


All Articles