1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
|
#ifndef CPP_UTILITIES_MISC_VERIFICATION_H
#define CPP_UTILITIES_MISC_VERIFICATION_H
#include "../conversion/stringconversion.h"
#include <openssl/ec.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <algorithm>
#include <array>
#include <cctype>
#include <string>
#include <string_view>
namespace CppUtilities {
namespace Detail {
/// \brief Returns the current OpenSSL error.
/// \remarks This function is an implementation detail and must not be called by users this library.
inline std::string getOpenSslError()
{
const auto errCode = ERR_get_error();
if (errCode == 0) {
return "unknown OpenSSL error";
}
auto buffer = std::array<char, 256>();
ERR_error_string_n(errCode, buffer.data(), buffer.size());
return std::string(buffer.data());
}
/// \brief Extracts the base64-encoded body from a PEM block.
/// \remarks This function is an implementation detail and must not be called by users of this library.
inline std::string extractPemBody(std::string_view pem, std::string_view header)
{
auto body = std::string();
auto begin = pem.find(header);
if (begin == std::string_view::npos) {
return body;
}
begin += header.size();
auto end = pem.find("-----END", begin);
if (end == std::string_view::npos) {
return body;
}
body = std::string(pem.data() + begin, end - begin);
body.erase(std::remove_if(body.begin(), body.end(), ::isspace), body.end());
return body;
}
/// \brief Converts a PEM-encoded signature into a DER-encoded signature.
/// \remarks This function is an implementation detail and must not be called by users of this library.
inline std::string parsePemSignature(std::string_view pemSignature, std::pair<std::unique_ptr<std::uint8_t[]>, std::uint32_t> &decodedSignature)
{
const auto pemSignatureBody = extractPemBody(pemSignature, "-----BEGIN SIGNATURE-----");
if (pemSignatureBody.empty()) {
return "invalid or missing PEM signature block";
}
try {
decodedSignature = decodeBase64(pemSignatureBody.data(), static_cast<std::uint32_t>(pemSignatureBody.size()));
return std::string();
} catch (const ConversionException &e) {
return "unable to decode PEM signature block";
}
}
} // namespace Detail
/// \brief The signature of the main verifySignature() function.
using MainVerifyFunctionType = std::string (*)(std::string_view, std::string_view, std::string_view);
/*!
* \brief Verifies \a data with the specified public key \a publicKeyPem and signature \a signaturePem.
* \returns Returns an empty string if \a data and \a signature are correct and an error message otherwise.
* \remarks
* - The digest algorithm is assumed to be SHA256.
* - The key and signature must both be provided in PEM format.
* - This function requires linking with the OpenSSL crypto library. It will *not* initialize the OpenSSL crypto library
* explicitly assuming OpenSSL version 1.1.0 or higher is used (which no longer requires explicit initialization). If
* you are using an older version of OpenSSL you may need to call ERR_load_crypto_strings() and OpenSSL_add_all_algorithms()
* before invoking this function.
* - This function is experimental and might be changed in incompatible ways (API and ABI wise) or be completely removed
* in further minor/patch releases.
*
* A key pair for signing can be created with the following commands:
* ```
* openssl ecparam -name secp521r1 -genkey -noout -out release-signing-private-openssl-secp521r1.pem
* openssl ec -in release-signing-private-openssl-secp521r1.pem -pubout > release-signing-public-openssl-secp521r1.pem
* ```
*
* A signature can be created and verified using the following commands:
* ```
* openssl dgst -sha256 -sign release-signing-private-openssl-secp521r1.pem test_msg.txt > test_msg-secp521r1.txt.sig
* openssl dgst -sha256 -verify release-signing-public-openssl-secp521r1.pem -signature test_msg-secp521r1.txt.sig test_msg.txt
* ```
*
* The signature can be converted to the PEM format using the following commands:
* ```
* echo "-----BEGIN SIGNATURE-----" > test_msg-secp521r1.txt.sig.pem
* cat test_msg-secp521r1.txt.sig | base64 -w 64 >> test_msg-secp521r1.txt.sig.pem
* echo "-----END SIGNATURE-----" >> test_msg-secp521r1.txt.sig.pem
* ```
*/
inline std::string verifySignature(std::string_view publicKeyPem, std::string_view signaturePem, std::string_view data)
{
auto error = std::string();
auto derSignature = std::pair<std::unique_ptr<std::uint8_t[]>, std::uint32_t>();
if (error = Detail::parsePemSignature(signaturePem, derSignature); !error.empty()) {
return error;
}
BIO *const keyBio = BIO_new_mem_buf(publicKeyPem.data(), static_cast<int>(publicKeyPem.size()));
if (!keyBio) {
return error = "BIO_new_mem_buf failed: " + Detail::getOpenSslError();
}
EVP_PKEY *const publicKey = PEM_read_bio_PUBKEY(keyBio, nullptr, nullptr, nullptr);
BIO_free(keyBio);
if (!publicKey) {
return error = "PEM_read_bio_PUBKEY failed: " + Detail::getOpenSslError();
}
EVP_MD_CTX *const mdCtx = EVP_MD_CTX_new();
if (!mdCtx) {
EVP_PKEY_free(publicKey);
return error = "EVP_MD_CTX_new failed: " + Detail::getOpenSslError();
}
if (EVP_DigestVerifyInit(mdCtx, nullptr, EVP_sha256(), nullptr, publicKey) != 1) {
error = "EVP_DigestVerifyInit failed: " + Detail::getOpenSslError();
} else if (EVP_DigestVerifyUpdate(mdCtx, data.data(), data.size()) != 1) {
error = "EVP_DigestVerifyUpdate failed: " + Detail::getOpenSslError();
} else {
switch (EVP_DigestVerifyFinal(mdCtx, derSignature.first.get(), derSignature.second)) {
case 0:
error = "incorrect signature";
break;
case 1:
break; // signature is correct
default:
error = "EVP_DigestVerifyFinal failed: " + Detail::getOpenSslError();
break;
}
}
EVP_MD_CTX_free(mdCtx);
EVP_PKEY_free(publicKey);
return error;
}
/*!
* \brief Verifies \a data with the specified public keys \a publicKeysPem and signature \a signaturePem.
* \returns Returns an empty string if \a data and \a signature are correct and an error message otherwise.
* \remarks
* - This is a version of verifySignature() that takes more than one public key trying out different keys.
* This allows rotating keys once in a while without breaking verification by temporarily allowing the
* old and new key at the same time.
*/
template <class Keys, class VerifyFunction = MainVerifyFunctionType>
inline std::string verifySignature(Keys &&publicKeysPem, std::string_view signaturePem, std::string_view data,
VerifyFunction &&verifyFunction = static_cast<MainVerifyFunctionType>(&verifySignature))
{
auto error = std::string("no keys provided");
for (const auto publicKeyPem : publicKeysPem) {
if ((error = verifyFunction(publicKeyPem, signaturePem, data)).empty()) {
return error;
}
}
return error;
}
} // namespace CppUtilities
#endif // CPP_UTILITIES_MISC_VERIFICATION_H
|