File: verification.h

package info (click to toggle)
martchus-cpp-utilities 5.33.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,396 kB
  • sloc: cpp: 12,679; awk: 18; ansic: 12; makefile: 10
file content (177 lines) | stat: -rw-r--r-- 7,183 bytes parent folder | download
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