OpenTTD Source: src/signature.cpp Source File

OpenTTD Source 14.0-beta1

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
25 static 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
36 static std::string CalculateHashV1(const std::string &filename)
37 {
38 FILE *f = FioFOpenFile(filename, "rb", NO_DIRECTORY);
39 if (f == nullptr) {
40 return "";
41 }
42
43 std::array<uint8_t, 32> digest;
44 crypto_blake2b_ctx ctx;
45 crypto_blake2b_init(&ctx, digest.size());
46
47 while (!feof(f)) {
48 std::array<uint8_t, 1024> buf;
49 size_t len = fread(buf.data(), 1, buf.size(), f);
50
51 crypto_blake2b_update(&ctx, buf.data(), len);
52 }
53 fclose(f);
54
55 crypto_blake2b_final(&ctx, digest.data());
56 return FormatArrayAsHex(digest);
57 }
58
66 static bool ValidateChecksum(const std::string &filename, const std::string &checksum)
67 {
68 /* Checksums are "<version>$<hash>". Split out the version. */
69 auto pos = checksum.find('$');
70 assert(pos != std::string::npos); // Already validated by ValidateSchema().
71 const std::string version = checksum.substr(0, pos);
72 const std::string hash = checksum.substr(pos + 1);
73
74 /* Calculate the checksum over the file. */
75 std::string calculated_hash;
76 if (version == "1") {
77 calculated_hash = CalculateHashV1(filename);
78 } else {
79 Debug(misc, 0, "Failed to validate signature: unknown checksum version: {}", filename);
80 return false;
81 }
82
83 /* Validate the checksum is the same. */
84 if (calculated_hash.empty()) {
85 Debug(misc, 0, "Failed to validate signature: couldn't calculate checksum for: {}", filename);
86 return false;
87 }
88 if (calculated_hash != hash) {
89 Debug(misc, 0, "Failed to validate signature: checksum mismatch for: {}", filename);
90 return false;
91 }
92
93 return true;
94 }
95
104 static bool ValidateSignature(const std::string &signature, const nlohmann::json &files, const std::string &filename)
105 {
106 /* Signatures are "<version>$<signature>". Split out the version. */
107 auto pos = signature.find('$');
108 assert(pos != std::string::npos); // Already validated by ValidateSchema().
109 const std::string version = signature.substr(0, pos);
110 const std::string sig_value = signature.substr(pos + 1);
111
112 /* Create the message we are going to validate. */
113 std::string message = files.dump(-1);
114
115 /* Validate the signature. */
116 if (version == "1") {
117 std::array<uint8_t, 64> sig;
118 if (sig_value.size() != 128 || !ConvertHexToBytes(sig_value, sig)) {
119 Debug(misc, 0, "Failed to validate signature: invalid signature: {}", filename);
120 return false;
121 }
122
123 for (auto &pk_value : _public_keys_v1) {
124 /* Check if the message is valid with this public key. */
125 auto res = crypto_ed25519_check(sig.data(), pk_value.data(), reinterpret_cast<uint8_t *>(message.data()), message.size());
126 if (res == 0) {
127 return true;
128 }
129 }
130
131 Debug(misc, 0, "Failed to validate signature: signature validation failed: {}", filename);
132 return false;
133 } else {
134 Debug(misc, 0, "Failed to validate signature: unknown signature version: {}", filename);
135 return false;
136 }
137
138 return true;
139 }
140
148 static bool ValidateSchema(const nlohmann::json &signatures, const std::string &filename)
149 {
150 if (signatures["files"].is_null()) {
151 Debug(misc, 0, "Failed to validate signature: no files found: {}", filename);
152 return false;
153 }
154
155 if (signatures["signature"].is_null()) {
156 Debug(misc, 0, "Failed to validate signature: no signature found: {}", filename);
157 return false;
158 }
159
160 for (auto &signature : signatures["files"]) {
161 if (signature["filename"].is_null() || signature["checksum"].is_null()) {
162 Debug(misc, 0, "Failed to validate signature: invalid entry in files: {}", filename);
163 return false;
164 }
165
166 const std::string sig_filename = signature["filename"];
167 const std::string sig_checksum = signature["checksum"];
168
169 if (sig_filename.empty() || sig_checksum.empty()) {
170 Debug(misc, 0, "Failed to validate signature: invalid entry in files: {}", filename);
171 return false;
172 }
173
174 auto pos = sig_checksum.find('$');
175 if (pos == std::string::npos) {
176 Debug(misc, 0, "Failed to validate signature: invalid checksum format: {}", filename);
177 return false;
178 }
179 }
180
181 const std::string signature = signatures["signature"];
182 auto pos = signature.find('$');
183 if (pos == std::string::npos) {
184 Debug(misc, 0, "Failed to validate signature: invalid signature format: {}", filename);
185 return false;
186 }
187
188 return true;
189 }
190
197 static bool _ValidateSignatureFile(const std::string &filename)
198 {
199 size_t filesize;
200 FILE *f = FioFOpenFile(filename, "rb", NO_DIRECTORY, &filesize);
201 if (f == nullptr) {
202 Debug(misc, 0, "Failed to validate signature: file not found: {}", filename);
203 return false;
204 }
205
206 std::string text(filesize, '\0');
207 size_t len = fread(text.data(), filesize, 1, f);
208 if (len != 1) {
209 Debug(misc, 0, "Failed to validate signature: failed to read file: {}", filename);
210 return false;
211 }
212
213 nlohmann::json signatures;
214 try {
215 signatures = nlohmann::json::parse(text);
216 } catch (nlohmann::json::exception &) {
217 Debug(misc, 0, "Failed to validate signature: not a valid JSON file: {}", filename);
218 return false;
219 }
220
221 /*
222 * The JSON file should look like:
223 *
224 * {
225 * "files": [
226 * {
227 * "checksum": "version$hash"
228 * "filename": "filename",
229 * },
230 * ...
231 * ],
232 * "signature": "version$signature"
233 * }
234 *
235 * The signature is a signed message of the content of "files", dumped as
236 * JSON without spaces / newlines, keys in the order as indicated above.
237 */
238
239 if (!ValidateSchema(signatures, filename)) {
240 return false;
241 }
242
243 if (!ValidateSignature(signatures["signature"], signatures["files"], filename)) {
244 return false;
245 }
246
247 std::string dirname = std::filesystem::path(filename).parent_path().string();
248
249 for (auto &signature : signatures["files"]) {
250 const std::string sig_filename = dirname + PATHSEPCHAR + signature["filename"].get<std::string>();
251 const std::string sig_checksum = signature["checksum"];
252
253 if (!ValidateChecksum(sig_filename, sig_checksum)) {
254 return false;
255 }
256 }
257
258 return true;
259 }
260
270 bool ValidateSignatureFile(const std::string &filename)
271 {
272 auto res = _ValidateSignatureFile(filename);;
273 #if defined(ALLOW_INVALID_SIGNATURE)
274 (void)res; // Ignore the result.
275 return true;
276 #else
277 return res;
278 #endif
279 }
FormatArrayAsHex
std::string FormatArrayAsHex(std::span< const byte > data)
Format a byte array into a continuous hex string.
Definition: string.cpp:88
fileio_func.h
ConvertHexToBytes
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:730
_ValidateSignatureFile
static bool _ValidateSignatureFile(const std::string &filename)
Validate that the signatures mentioned in the signature file are matching the files in question.
Definition: signature.cpp:197
Debug
#define Debug(category, level, format_string,...)
Ouptut a line of debugging information.
Definition: debug.h:37
CalculateHashV1
static std::string CalculateHashV1(const std::string &filename)
Calculate the 32-byte blake2b hash of a file.
Definition: signature.cpp:36
ValidateChecksum
static bool ValidateChecksum(const std::string &filename, const std::string &checksum)
Validate whether the checksum of a file is the same.
Definition: signature.cpp:66
FioFOpenFile
FILE * 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:263
signature.h
safeguards.h
ValidateSignatureFile
bool ValidateSignatureFile(const std::string &filename)
Validate that the signatures mentioned in the signature file are matching the files in question.
Definition: signature.cpp:270
_public_keys_v1
static const std::initializer_list< std::array< uint8_t, 32 > > _public_keys_v1
The public keys used for signature validation.
Definition: signature.cpp:25
stdafx.h
string_func.h
ValidateSchema
static bool ValidateSchema(const nlohmann::json &signatures, const std::string &filename)
Validate the signatures file complies with the JSON schema.
Definition: signature.cpp:148
NO_DIRECTORY
@ NO_DIRECTORY
A path without any base directory.
Definition: fileio_type.h:126
debug.h
ValidateSignature
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.
Definition: signature.cpp:104