Skip to content

Commit 7f0ed06

Browse files
committed
Move OpenSSL-based verification from qtutilities to c++utilities
This doesn't depend on Qt (with the use of QByteArray removed) and is thus better added here.
1 parent d268efc commit 7f0ed06

File tree

4 files changed

+210
-3
lines changed

4 files changed

+210
-3
lines changed

CMakeLists.txt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ set(HEADER_FILES
3333
misc/parseerror.h
3434
misc/traits.h
3535
misc/levenshtein.h
36+
misc/verification.h
3637
tests/testutils.h
3738
tests/cppunit.h
3839
tests/outputcheck.h)
@@ -117,8 +118,8 @@ set(META_APP_AUTHOR "Martchus")
117118
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
118119
set(META_APP_DESCRIPTION "Useful C++ classes and routines such as argument parser, IO and conversion utilities")
119120
set(META_VERSION_MAJOR 5)
120-
set(META_VERSION_MINOR 28)
121-
set(META_VERSION_PATCH 2)
121+
set(META_VERSION_MINOR 29)
122+
set(META_VERSION_PATCH 0)
122123

123124
# find required 3rd party libraries
124125
include(3rdParty)
@@ -257,6 +258,17 @@ if (USE_PLATFORM_SPECIFIC_API_FOR_OPTIMIZING_COPY_HELPER)
257258
list(APPEND META_PUBLIC_COMPILE_DEFINITIONS ${META_PROJECT_VARNAME}_USE_PLATFORM_SPECIFIC_API_FOR_OPTIMIZING_COPY_HELPER)
258259
endif ()
259260

261+
# configure tests for verification helpers which require OpenSSL
262+
use_crypto(LIBRARIES_VARIABLE "TEST_LIBRARIES" PACKAGES_VARIABLE "TEST_PACKAGES" OPTIONAL)
263+
if ("OpenSSL::Crypto" IN_LIST "TEST_LIBRARIES")
264+
set_property(
265+
SOURCE tests/misctests.cpp
266+
APPEND
267+
PROPERTY COMPILE_DEFINITIONS ${META_PROJECT_VARNAME}_HAS_OPENSSL_CRYPTO)
268+
else ()
269+
message(WARNING "Unable to test verification helper of setup tools because OpenSSL crypto library is not available.")
270+
endif ()
271+
260272
# apply basic configuration
261273
include(BasicConfig)
262274

conversion/stringconversion.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ string encodeBase64(const std::uint8_t *data, std::uint32_t dataSize)
421421
* \throw Throws a ConversionException if the specified string is no valid Base64.
422422
* \sa [RFC 4648](http://www.ietf.org/rfc/rfc4648.txt)
423423
*/
424-
std::pair<unique_ptr<std::uint8_t[]>, std::uint32_t> decodeBase64(const char *encodedStr, const std::uint32_t strSize)
424+
std::pair<std::unique_ptr<std::uint8_t[]>, std::uint32_t> decodeBase64(const char *encodedStr, const std::uint32_t strSize)
425425
{
426426
if (!strSize) {
427427
return std::make_pair(std::make_unique<std::uint8_t[]>(0), 0); // early return to prevent clazy warning

misc/verification.h

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

tests/misctests.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
#include "../misc/levenshtein.h"
33
#include "../misc/multiarray.h"
44

5+
#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
6+
#include "../misc/verification.h"
7+
#endif
8+
59
#include "../conversion/stringbuilder.h"
610
#include "../conversion/stringconversion.h"
711

@@ -43,6 +47,9 @@ class MiscTests : public TestFixture {
4347
CPPUNIT_TEST(testMultiArray);
4448
CPPUNIT_TEST(testLevenshtein);
4549
CPPUNIT_TEST(testTestUtilities);
50+
#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
51+
CPPUNIT_TEST(testVerification);
52+
#endif
4653
CPPUNIT_TEST_SUITE_END();
4754

4855
public:
@@ -56,6 +63,9 @@ class MiscTests : public TestFixture {
5663
void testMultiArray();
5764
void testLevenshtein();
5865
void testTestUtilities();
66+
#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
67+
void testVerification();
68+
#endif
5969
};
6070

6171
CPPUNIT_TEST_SUITE_REGISTRATION(MiscTests);
@@ -173,6 +183,31 @@ void MiscTests::testTestUtilities()
173183
TESTUTILS_ASSERT_LIKE("assert like works", ".*foo.*", " foo ");
174184
}
175185

186+
#ifdef CPP_UTILITIES_HAS_OPENSSL_CRYPTO
187+
void MiscTests::testVerification()
188+
{
189+
const auto key = std::string_view(
190+
R"(-----BEGIN PUBLIC KEY-----
191+
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAWJAn1E7ZE5Q6H69oaV5sqCIppJdg
192+
4bXDan9dJv6GOg70/t7q2CvwcwUXhV4FvCZxCHo25+rWYINfqKU2Utul8koAx8tK
193+
59ohfOzI63I+CC76GfX41uRGU0P5i6hS7o/hgBLiVXqT0FgS2BMfmnLMUvUjqnI2
194+
YQM7C55/5BM5Vrblkow=
195+
-----END PUBLIC KEY-----)");
196+
const auto signature = std::string_view(
197+
R"(-----BEGIN SIGNATURE-----
198+
MIGIAkIB+LB01DduBFMVs7Ea2McD7/kXpP0XktDNR7WpVgkOn4+/ilR8b8lpO9dd
199+
FGmxKj5UVr2GpcWX6I216PjaVL9tr5oCQgFMpvNjSgFQ/KFaE+0d+QCegr3V7Uz6
200+
sWB0iGdPa+oXbRish7HoNCU/k0lD3ffXaf8ueC78Zme9NFO18Ol+NWXJDA==
201+
-----END SIGNATURE-----)");
202+
203+
auto message = std::string("test message");
204+
CPPUNIT_ASSERT_EQUAL_MESSAGE("valid message", std::string(), verifySignature(key, signature, message));
205+
206+
message[5] = '?';
207+
CPPUNIT_ASSERT_EQUAL_MESSAGE("manipulate message", "incorrect signature"s, verifySignature(key, signature, message));
208+
}
209+
#endif
210+
176211
// test flagenumclass.h
177212

178213
namespace FlagEnumTests {

0 commit comments

Comments
 (0)