Skip to content

Commit 85d8698

Browse files
committed
Reimplement signature validation using gpgme
1 parent 3cdbe3e commit 85d8698

File tree

9 files changed

+477
-235
lines changed

9 files changed

+477
-235
lines changed

include/appimage/update.h

-3
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,10 @@ namespace appimage::update {
3030
// warning states -- check like >= WARNING && < ERROR
3131
VALIDATION_WARNING = 1000,
3232
VALIDATION_NOT_SIGNED,
33-
VALIDATION_GPG_MISSING,
3433

3534
// error states -- check like >= ERROR
3635
VALIDATION_FAILED = 2000,
3736
VALIDATION_KEY_CHANGED,
38-
VALIDATION_GPG_CALL_FAILED,
39-
VALIDATION_TEMPDIR_CREATION_FAILED,
4037
VALIDATION_NO_LONGER_SIGNED,
4138
VALIDATION_BAD_SIGNATURE,
4239
};

src/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ endif()
2626

2727
add_subdirectory(util)
2828
add_subdirectory(updateinformation)
29+
add_subdirectory(signing)
2930
add_subdirectory(updater)
3031

3132
if(NOT BUILD_LIBAPPIMAGEUPDATE_ONLY)

src/signing/CMakeLists.txt

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
find_package(PkgConfig)
2+
pkg_check_modules(gpgme gpgme>=1.10 IMPORTED_TARGET)
3+
4+
add_library(signing STATIC signaturevalidator.cpp)
5+
target_link_libraries(signing
6+
PUBLIC PkgConfig::gpgme
7+
PRIVATE util
8+
)
9+
# include the complete source to force the use of project-relative include paths
10+
target_include_directories(signing
11+
PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>/src
12+
)
13+
14+
# "demonstration" application
15+
# used to be located within AppImageKit, but there is no sense in maintaining two implementations
16+
add_executable(validate validate_main.cpp)
17+
target_link_libraries(validate signing)

src/signing/signaturevalidator.cpp

+347
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
// system headers
2+
#include <cassert>
3+
#include <cstring>
4+
#include <filesystem>
5+
#include <iostream>
6+
#include <memory>
7+
#include <string>
8+
#include <vector>
9+
10+
// library headers
11+
#include <gpgme.h>
12+
13+
// local headers
14+
#include "signaturevalidator.h"
15+
#include "util/util.h"
16+
17+
namespace appimage::update::signing {
18+
using namespace util;
19+
20+
class GpgmeInMemoryData {
21+
private:
22+
gpgme_data_t _dh = nullptr;
23+
24+
public:
25+
explicit GpgmeInMemoryData(const std::string& buffer) {
26+
const auto error = gpgme_data_new_from_mem(&_dh, buffer.c_str(), buffer.size(), true);
27+
if (error != GPG_ERR_NO_ERROR) {
28+
throw GpgError(error, "failed to initialize in-memory data for gpgme");
29+
}
30+
}
31+
32+
~GpgmeInMemoryData() noexcept {
33+
gpgme_data_release(_dh);
34+
}
35+
36+
[[nodiscard]] auto get() const {
37+
return _dh;
38+
}
39+
};
40+
41+
class GpgmeContext {
42+
private:
43+
gpgme_ctx_t _ctx = nullptr;
44+
45+
static void gpgmeThrowIfNecessary(gpg_err_code_t error, const std::string& message) {
46+
if (error != GPG_ERR_NO_ERROR) {
47+
throw GpgError(error, message);
48+
}
49+
}
50+
51+
static void gpgmeThrowIfNecessary(gpgme_error_t error, const std::string& message) {
52+
return gpgmeThrowIfNecessary(gpgme_err_code(error), message);
53+
}
54+
55+
public:
56+
explicit GpgmeContext(const std::string& gnupgHome) {
57+
static const char gpgme_minimum_required_version[] = "1.10.0";
58+
const char* gpgme_version = gpgme_check_version(gpgme_minimum_required_version);
59+
60+
if (gpgme_version == nullptr) {
61+
std::stringstream error;
62+
error << "could not initialize gpgme (>= " << gpgme_minimum_required_version << ")";
63+
throw GpgError(GPG_ERR_NO_ERROR, error.str());
64+
}
65+
66+
gpgmeThrowIfNecessary(gpgme_new(&_ctx), "failed to initialize gpgme context");
67+
assert(_ctx != nullptr);
68+
69+
gpgmeThrowIfNecessary(gpgme_set_ctx_flag(_ctx, "full-status", "1"), "failed to initialize gpgme context");
70+
gpgmeThrowIfNecessary(gpgme_set_protocol(_ctx, GPGME_PROTOCOL_OpenPGP), "failed to set OpenPGP protocol");
71+
72+
auto engine_info = gpgme_ctx_get_engine_info(_ctx);
73+
74+
while (engine_info && engine_info->protocol != gpgme_get_protocol(_ctx)) {
75+
engine_info = engine_info->next;
76+
}
77+
78+
// experience within AppImageKit shows that gnupg versions <= 2.2 are likely to cause issues
79+
// therefore, we warn users if an incompatible version was found
80+
{
81+
const std::string format = "%lu.%lu";
82+
unsigned long majorVersion, minorVersion;
83+
84+
if (sscanf(engine_info->version, format.c_str(), &majorVersion, &minorVersion) < 2) {
85+
throw GpgError(GPG_ERR_NO_ERROR, "failed to parse engine version number");
86+
}
87+
88+
if (majorVersion != 2 || minorVersion < 2) {
89+
// TODO: use regular logging system
90+
std::cerr << "gpg engine version " << engine_info->version << " is likely incompatible, "
91+
<< "consider using version >= 2.2" << std::endl;
92+
}
93+
}
94+
95+
if (!gnupgHome.empty()) {
96+
// we reuse the existing engine configuration, but use a custom home dir
97+
gpgmeThrowIfNecessary(
98+
gpgme_ctx_set_engine_info(_ctx, engine_info->protocol, engine_info->file_name, gnupgHome.c_str()),
99+
"failed to set engine info"
100+
);
101+
}
102+
}
103+
104+
void importKey(const std::string& key) {
105+
GpgmeInMemoryData data(key);
106+
107+
gpgmeThrowIfNecessary(gpgme_op_import(_ctx, data.get()), "failed to import key");
108+
109+
auto result = gpgme_op_import_result(_ctx);
110+
111+
// some "assertions" to make sure importing worked
112+
if (result->not_imported > 0) {
113+
std::stringstream errorMessage;
114+
errorMessage << result->not_imported << " keys could not be imported";
115+
throw GpgError(GPG_ERR_NO_ERROR, errorMessage.str());
116+
}
117+
if (result->imported < 0) {
118+
throw GpgError(GPG_ERR_NO_ERROR, "result implies no keys were imported");
119+
}
120+
}
121+
122+
~GpgmeContext() {
123+
gpgme_release(_ctx);
124+
}
125+
126+
SignatureValidationResult validateSignature(const std::string& signedDataString, const std::string& signatureString) {
127+
GpgmeInMemoryData signedDataData(signedDataString);
128+
GpgmeInMemoryData signatureData(signatureString);
129+
130+
const auto error = gpgme_err_code(gpgme_op_verify(_ctx, signatureData.get(), signedDataData.get(), nullptr));
131+
132+
std::stringstream errorMessage;
133+
134+
switch (error) {
135+
case GPG_ERR_NO_ERROR: {
136+
break;
137+
}
138+
case GPG_ERR_INV_VALUE: {
139+
// this should never ever happen, and implies an issue within our code
140+
throw GpgError(error, "unexpected error while validating signature");
141+
}
142+
default: {
143+
errorMessage << "unexpected error";
144+
return {SignatureValidationResult::ResultType::ERROR, errorMessage.str(), {}};
145+
}
146+
}
147+
148+
auto verificationResult = gpgme_op_verify_result(_ctx);
149+
150+
auto signature = verificationResult->signatures;
151+
152+
if (signature == nullptr) {
153+
return {SignatureValidationResult::ResultType::ERROR, "no signatures found", {}};
154+
}
155+
156+
std::stringstream message;
157+
std::vector<std::string> fingerprints;
158+
// we're optimistic: we assume the result is good unless we find clues it's not
159+
SignatureValidationResult::ResultType resultType = SignatureValidationResult::ResultType::SUCCESS;
160+
161+
// there should not be more than one signature, but we don't know for sure
162+
do {
163+
fingerprints.emplace_back(signature->fpr);
164+
165+
message << "Signature checked for key with fingerprint " << signature->fpr << ": ";
166+
if (
167+
(signature->summary & GPGME_SIGSUM_VALID | signature->summary & GPGME_SIGSUM_GREEN) != 0 ||
168+
169+
// according to rpm, signature is valid but the key is not certified with a trusted signature
170+
(signature->summary == 0 && signature->status == GPG_ERR_NO_ERROR)
171+
) {
172+
// valid signature. no change to status required
173+
} else if (
174+
// an expired signature or key may happen any time with AppImages
175+
// as long as the signature itself is valid, we report a warning state
176+
(signature->summary & GPGME_SIGSUM_KEY_EXPIRED | signature->summary & GPGME_SIGSUM_KEY_MISSING) > 0
177+
) {
178+
message << "warning";
179+
if (resultType < SignatureValidationResult::ResultType::WARNING) {
180+
resultType = SignatureValidationResult::ResultType::WARNING;
181+
}
182+
} else {
183+
message << "error";
184+
// invalid signature
185+
resultType = SignatureValidationResult::ResultType::ERROR;
186+
}
187+
188+
std::vector<std::string> summaryInfos;
189+
190+
// inform user about other information we can gather from the summary
191+
if ((signature->summary & GPGME_SIGSUM_KEY_REVOKED) > 0) {
192+
summaryInfos.emplace_back("key revoked");
193+
}
194+
if ((signature->summary & GPGME_SIGSUM_KEY_EXPIRED) > 0) {
195+
summaryInfos.emplace_back("key expired");
196+
}
197+
if ((signature->summary & GPGME_SIGSUM_SIG_EXPIRED) > 0) {
198+
summaryInfos.emplace_back("signature expired");
199+
}
200+
if ((signature->summary & GPGME_SIGSUM_KEY_MISSING) > 0) {
201+
summaryInfos.emplace_back("key missing");
202+
}
203+
if ((signature->summary & GPGME_SIGSUM_CRL_MISSING) > 0) {
204+
summaryInfos.emplace_back("CRL missing");
205+
}
206+
if ((signature->summary & GPGME_SIGSUM_CRL_TOO_OLD) > 0) {
207+
summaryInfos.emplace_back("CRL too old");
208+
}
209+
if ((signature->summary & GPGME_SIGSUM_BAD_POLICY) > 0) {
210+
summaryInfos.emplace_back("bad polcy");
211+
}
212+
if ((signature->summary & GPGME_SIGSUM_SYS_ERROR) > 0) {
213+
summaryInfos.emplace_back("system error");
214+
}
215+
if ((signature->summary & GPGME_SIGSUM_TOFU_CONFLICT) > 0) {
216+
summaryInfos.emplace_back("TOFU conflict");
217+
}
218+
219+
message << join(summaryInfos, ", ") << std::endl;
220+
} while (signature->next != nullptr);
221+
222+
switch (resultType) {
223+
case SignatureValidationResult::ResultType::SUCCESS: {
224+
message << "Validation successful";
225+
break;
226+
}
227+
case SignatureValidationResult::ResultType::WARNING: {
228+
message << "Validation resulted in warning state";
229+
break;
230+
}
231+
case SignatureValidationResult::ResultType::ERROR: {
232+
message << "Validation failed";
233+
break;
234+
}
235+
}
236+
237+
return {resultType, message.str(), fingerprints};
238+
}
239+
};
240+
241+
class GpgError::Private {
242+
public:
243+
std::string what;
244+
245+
Private(gpg_error_t error, const std::string& message)
246+
{
247+
std::ostringstream oss;
248+
oss << message;
249+
250+
if (error != GPG_ERR_NO_ERROR) {
251+
oss << " (gpg error: " << gpgme_strerror(error) << ")";
252+
}
253+
254+
what = oss.str();
255+
}
256+
};
257+
258+
GpgError::GpgError(gpg_error_t error, const std::string& message) :
259+
d(new Private(error, message))
260+
{}
261+
262+
GpgError::~GpgError() noexcept = default;
263+
264+
const char* GpgError::what() const noexcept {
265+
return d->what.c_str();
266+
}
267+
268+
269+
class SignatureValidationResult::Private {
270+
public:
271+
Private(ResultType type, const std::string& description, const std::vector<std::string>& keyFingerprints) :
272+
type(type),
273+
description(description),
274+
keyFingerprints(keyFingerprints)
275+
{}
276+
277+
ResultType type;
278+
std::string description;
279+
std::vector<std::string> keyFingerprints;
280+
};
281+
282+
SignatureValidationResult::SignatureValidationResult(ResultType type, const std::string& description, const std::vector<std::string>& keyFingerprints) :
283+
d(new Private(type, description, keyFingerprints))
284+
{}
285+
286+
SignatureValidationResult::ResultType SignatureValidationResult::type() const {
287+
return d->type;
288+
}
289+
290+
std::string SignatureValidationResult::message() const {
291+
return d->description;
292+
}
293+
294+
std::vector<std::string> SignatureValidationResult::keyFingerprints() const {
295+
return d->keyFingerprints;
296+
}
297+
298+
SignatureValidationResult::~SignatureValidationResult() = default;
299+
300+
301+
class SignatureValidator::Private {
302+
public:
303+
// we want to initialize this only once, since the constructor may have side effects on the system
304+
std::unique_ptr<GpgmeContext> context = nullptr;
305+
306+
// we need a temporary keyring to work with
307+
std::filesystem::path tempGpgHomeDir;
308+
309+
explicit Private() {
310+
std::string tempGpgHomeDirTemplate = std::filesystem::temp_directory_path() / "appimageupdate-XXXXXX";
311+
std::vector<char> tempGpgHomeDirCStr(tempGpgHomeDirTemplate.begin(), tempGpgHomeDirTemplate.end());
312+
313+
if (mkdtemp(tempGpgHomeDirCStr.data()) == nullptr) {
314+
const auto error = errno;
315+
throw std::runtime_error(
316+
std::string("failed to create temporary directory: ") + strerror(error)
317+
);
318+
}
319+
320+
tempGpgHomeDir = std::string(tempGpgHomeDirCStr.data());
321+
322+
{
323+
// create keyring file, otherwise GPG will likely complain
324+
std::ofstream ofs(tempGpgHomeDir / "keyring");
325+
}
326+
327+
context = std::make_unique<GpgmeContext>(tempGpgHomeDir);
328+
}
329+
330+
~Private() noexcept {
331+
// clean up temporary home
332+
std::filesystem::remove_all(tempGpgHomeDir);
333+
}
334+
};
335+
336+
SignatureValidator::SignatureValidator() : d(new Private) {}
337+
338+
SignatureValidationResult SignatureValidator::validate(const UpdatableAppImage& appImage) {
339+
d->context->importKey(appImage.readSigningKey());
340+
341+
auto hashData = appImage.calculateHash();
342+
auto signatureData = appImage.readSignature();
343+
return d->context->validateSignature(hashData, signatureData);
344+
}
345+
346+
SignatureValidator::~SignatureValidator() = default;
347+
}

0 commit comments

Comments
 (0)