OpenTTD Source 20241224-master-gf74b0cf984
signature.cpp
Go to the documentation of this file.
1/*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
7
10#include "stdafx.h"
11
12#include "signature.h"
13
14#include "debug.h"
15#include "fileio_func.h"
16#include "string_func.h"
17
18#include "3rdparty/monocypher/monocypher.h"
19#include "3rdparty/monocypher/monocypher-ed25519.h"
20#include "3rdparty/nlohmann/json.hpp"
21
22#include "safeguards.h"
23
25static const std::initializer_list<std::array<uint8_t, 32>> _public_keys_v1 = {
26 /* 2024-01-20 - Public key for Social Integration Plugins. */
27 { 0xed, 0x5d, 0x57, 0x47, 0x21, 0x99, 0x8b, 0x02, 0xdf, 0x6e, 0x3d, 0x69, 0xe1, 0x87, 0xca, 0xd0, 0x0e, 0x88, 0xc3, 0xe2, 0xb2, 0xa6, 0x7b, 0xc0, 0x42, 0xc8, 0xd6, 0x4b, 0x65, 0xe6, 0x48, 0xf7 },
28};
29
36static std::string CalculateHashV1(const std::string &filename)
37{
38 auto f = FioFOpenFile(filename, "rb", NO_DIRECTORY);
39 if (!f.has_value()) return {};
40
41 std::array<uint8_t, 32> digest;
42 crypto_blake2b_ctx ctx;
43 crypto_blake2b_init(&ctx, digest.size());
44
45 while (!feof(*f)) {
46 std::array<uint8_t, 1024> buf;
47 size_t len = fread(buf.data(), 1, buf.size(), *f);
48
49 crypto_blake2b_update(&ctx, buf.data(), len);
50 }
51
52 crypto_blake2b_final(&ctx, digest.data());
53 return FormatArrayAsHex(digest);
54}
55
63static bool ValidateChecksum(const std::string &filename, const std::string &checksum)
64{
65 /* Checksums are "<version>$<hash>". Split out the version. */
66 auto pos = checksum.find('$');
67 assert(pos != std::string::npos); // Already validated by ValidateSchema().
68 const std::string version = checksum.substr(0, pos);
69 const std::string hash = checksum.substr(pos + 1);
70
71 /* Calculate the checksum over the file. */
72 std::string calculated_hash;
73 if (version == "1") {
74 calculated_hash = CalculateHashV1(filename);
75 } else {
76 Debug(misc, 0, "Failed to validate signature: unknown checksum version: {}", filename);
77 return false;
78 }
79
80 /* Validate the checksum is the same. */
81 if (calculated_hash.empty()) {
82 Debug(misc, 0, "Failed to validate signature: couldn't calculate checksum for: {}", filename);
83 return false;
84 }
85 if (calculated_hash != hash) {
86 Debug(misc, 0, "Failed to validate signature: checksum mismatch for: {}", filename);
87 return false;
88 }
89
90 return true;
91}
92
101static bool ValidateSignature(const std::string &signature, const nlohmann::json &files, const std::string &filename)
102{
103 /* Signatures are "<version>$<signature>". Split out the version. */
104 auto pos = signature.find('$');
105 assert(pos != std::string::npos); // Already validated by ValidateSchema().
106 const std::string version = signature.substr(0, pos);
107 const std::string sig_value = signature.substr(pos + 1);
108
109 /* Create the message we are going to validate. */
110 std::string message = files.dump(-1);
111
112 /* Validate the signature. */
113 if (version == "1") {
114 std::array<uint8_t, 64> sig;
115 if (sig_value.size() != 128 || !ConvertHexToBytes(sig_value, sig)) {
116 Debug(misc, 0, "Failed to validate signature: invalid signature: {}", filename);
117 return false;
118 }
119
120 for (auto &pk_value : _public_keys_v1) {
121 /* Check if the message is valid with this public key. */
122 auto res = crypto_ed25519_check(sig.data(), pk_value.data(), reinterpret_cast<uint8_t *>(message.data()), message.size());
123 if (res == 0) {
124 return true;
125 }
126 }
127
128 Debug(misc, 0, "Failed to validate signature: signature validation failed: {}", filename);
129 return false;
130 } else {
131 Debug(misc, 0, "Failed to validate signature: unknown signature version: {}", filename);
132 return false;
133 }
134
135 return true;
136}
137
145static bool ValidateSchema(const nlohmann::json &signatures, const std::string &filename)
146{
147 if (signatures["files"].is_null()) {
148 Debug(misc, 0, "Failed to validate signature: no files found: {}", filename);
149 return false;
150 }
151
152 if (signatures["signature"].is_null()) {
153 Debug(misc, 0, "Failed to validate signature: no signature found: {}", filename);
154 return false;
155 }
156
157 for (auto &signature : signatures["files"]) {
158 if (signature["filename"].is_null() || signature["checksum"].is_null()) {
159 Debug(misc, 0, "Failed to validate signature: invalid entry in files: {}", filename);
160 return false;
161 }
162
163 const std::string sig_filename = signature["filename"];
164 const std::string sig_checksum = signature["checksum"];
165
166 if (sig_filename.empty() || sig_checksum.empty()) {
167 Debug(misc, 0, "Failed to validate signature: invalid entry in files: {}", filename);
168 return false;
169 }
170
171 auto pos = sig_checksum.find('$');
172 if (pos == std::string::npos) {
173 Debug(misc, 0, "Failed to validate signature: invalid checksum format: {}", filename);
174 return false;
175 }
176 }
177
178 const std::string signature = signatures["signature"];
179 auto pos = signature.find('$');
180 if (pos == std::string::npos) {
181 Debug(misc, 0, "Failed to validate signature: invalid signature format: {}", filename);
182 return false;
183 }
184
185 return true;
186}
187
194static bool _ValidateSignatureFile(const std::string &filename)
195{
196 size_t filesize;
197 auto f = FioFOpenFile(filename, "rb", NO_DIRECTORY, &filesize);
198 if (!f.has_value()) {
199 Debug(misc, 0, "Failed to validate signature: file not found: {}", filename);
200 return false;
201 }
202
203 std::string text(filesize, '\0');
204 size_t len = fread(text.data(), filesize, 1, *f);
205 if (len != 1) {
206 Debug(misc, 0, "Failed to validate signature: failed to read file: {}", filename);
207 return false;
208 }
209
210 nlohmann::json signatures;
211 try {
212 signatures = nlohmann::json::parse(text);
213 } catch (nlohmann::json::exception &) {
214 Debug(misc, 0, "Failed to validate signature: not a valid JSON file: {}", filename);
215 return false;
216 }
217
218 /*
219 * The JSON file should look like:
220 *
221 * {
222 * "files": [
223 * {
224 * "checksum": "version$hash"
225 * "filename": "filename",
226 * },
227 * ...
228 * ],
229 * "signature": "version$signature"
230 * }
231 *
232 * The signature is a signed message of the content of "files", dumped as
233 * JSON without spaces / newlines, keys in the order as indicated above.
234 */
235
236 if (!ValidateSchema(signatures, filename)) {
237 return false;
238 }
239
240 if (!ValidateSignature(signatures["signature"], signatures["files"], filename)) {
241 return false;
242 }
243
244 std::string dirname = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).parent_path());
245
246 for (auto &signature : signatures["files"]) {
247 const std::string sig_filename = dirname + PATHSEPCHAR + signature["filename"].get<std::string>();
248 const std::string sig_checksum = signature["checksum"];
249
250 if (!ValidateChecksum(sig_filename, sig_checksum)) {
251 return false;
252 }
253 }
254
255 return true;
256}
257
267bool ValidateSignatureFile(const std::string &filename)
268{
269 auto res = _ValidateSignatureFile(filename);;
270#if defined(ALLOW_INVALID_SIGNATURE)
271 (void)res; // Ignore the result.
272 return true;
273#else
274 return res;
275#endif
276}
Functions related to debugging.
#define Debug(category, level, format_string,...)
Ouptut a line of debugging information.
Definition debug.h:37
std::optional< FileHandle > FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
Definition fileio.cpp:242
Functions for Standard In/Out file operations.
@ NO_DIRECTORY
A path without any base directory.
A number of safeguards to prevent using unsafe methods.
static const std::initializer_list< std::array< uint8_t, 32 > > _public_keys_v1
The public keys used for signature validation.
Definition signature.cpp:25
static std::string CalculateHashV1(const std::string &filename)
Calculate the 32-byte blake2b hash of a file.
Definition signature.cpp:36
static bool ValidateSchema(const nlohmann::json &signatures, const std::string &filename)
Validate the signatures file complies with the JSON schema.
static bool _ValidateSignatureFile(const std::string &filename)
Validate that the signatures mentioned in the signature file are matching the files in question.
bool ValidateSignatureFile(const std::string &filename)
Validate that the signatures mentioned in the signature file are matching the files in question.
static bool ValidateSignature(const std::string &signature, const nlohmann::json &files, const std::string &filename)
Validate whether the signature is valid for this set of files.
static bool ValidateChecksum(const std::string &filename, const std::string &checksum)
Validate whether the checksum of a file is the same.
Definition signature.cpp:63
Routines to validate signature files.
Definition of base types and functions in a cross-platform compatible way.
bool ConvertHexToBytes(std::string_view hex, std::span< uint8_t > bytes)
Convert a hex-string to a byte-array, while validating it was actually hex.
Definition string.cpp:734
std::string FormatArrayAsHex(std::span< const uint8_t > data)
Format a byte array into a continuous hex string.
Definition string.cpp:81
Functions related to low-level strings.
std::wstring OTTD2FS(const std::string &name)
Convert from OpenTTD's encoding to a wide string.
Definition win32.cpp:354
std::string FS2OTTD(const std::wstring &name)
Convert to OpenTTD's encoding from a wide string.
Definition win32.cpp:337