Codefix: Documentation comment in IndustryDirectoryWindow (#13059)
[openttd-github.git] / src / signature.cpp
blob60090240591429b2d940b4cb3d6822d0e0c385e5
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 */
8 /** @file signature.cpp Implementation of signature validation routines. */
10 #include "stdafx.h"
12 #include "signature.h"
14 #include "debug.h"
15 #include "fileio_func.h"
16 #include "string_func.h"
18 #include "3rdparty/monocypher/monocypher.h"
19 #include "3rdparty/monocypher/monocypher-ed25519.h"
20 #include "3rdparty/nlohmann/json.hpp"
22 #include "safeguards.h"
24 /** The public keys used for signature validation. */
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 },
30 /**
31 * Calculate the 32-byte blake2b hash of a file.
33 * @param filename The filename to calculate the hash of.
34 * @return The 32-byte blake2b hash of the file, hex-encoded.
36 static std::string CalculateHashV1(const std::string &filename)
38 auto f = FioFOpenFile(filename, "rb", NO_DIRECTORY);
39 if (!f.has_value()) return {};
41 std::array<uint8_t, 32> digest;
42 crypto_blake2b_ctx ctx;
43 crypto_blake2b_init(&ctx, digest.size());
45 while (!feof(*f)) {
46 std::array<uint8_t, 1024> buf;
47 size_t len = fread(buf.data(), 1, buf.size(), *f);
49 crypto_blake2b_update(&ctx, buf.data(), len);
52 crypto_blake2b_final(&ctx, digest.data());
53 return FormatArrayAsHex(digest);
56 /**
57 * Validate whether the checksum of a file is the same.
59 * @param filename The filename to validate the checksum of.
60 * @param checksum The expected checksum.
61 * @return True iff the checksum of the file is the same as the expected checksum.
63 static bool ValidateChecksum(const std::string &filename, const std::string &checksum)
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);
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;
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;
85 if (calculated_hash != hash) {
86 Debug(misc, 0, "Failed to validate signature: checksum mismatch for: {}", filename);
87 return false;
90 return true;
93 /**
94 * Validate whether the signature is valid for this set of files.
96 * @param signature The signature to validate.
97 * @param files The files to validate the signature against.
98 * @param filename The filename of the signatures file (for error-reporting).
99 * @return True iff the signature is valid for this set of files.
101 static bool ValidateSignature(const std::string &signature, const nlohmann::json &files, const std::string &filename)
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);
109 /* Create the message we are going to validate. */
110 std::string message = files.dump(-1);
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;
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;
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;
135 return true;
139 * Validate the signatures file complies with the JSON schema.
141 * @param signatures The signatures JSON to validate.
142 * @param filename The filename of the signatures file (for error-reporting).
143 * @return True iff the signatures file complies with the JSON schema.
145 static bool ValidateSchema(const nlohmann::json &signatures, const std::string &filename)
147 if (signatures["files"].is_null()) {
148 Debug(misc, 0, "Failed to validate signature: no files found: {}", filename);
149 return false;
152 if (signatures["signature"].is_null()) {
153 Debug(misc, 0, "Failed to validate signature: no signature found: {}", filename);
154 return false;
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;
163 const std::string sig_filename = signature["filename"];
164 const std::string sig_checksum = signature["checksum"];
166 if (sig_filename.empty() || sig_checksum.empty()) {
167 Debug(misc, 0, "Failed to validate signature: invalid entry in files: {}", filename);
168 return false;
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;
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;
185 return true;
189 * Validate that the signatures mentioned in the signature file are matching
190 * the files in question.
192 * @return True iff the files in the signature file passed validation.
194 static bool _ValidateSignatureFile(const std::string &filename)
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;
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;
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;
219 * The JSON file should look like:
222 * "files": [
224 * "checksum": "version$hash"
225 * "filename": "filename",
226 * },
227 * ...
228 * ],
229 * "signature": "version$signature"
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.
236 if (!ValidateSchema(signatures, filename)) {
237 return false;
240 if (!ValidateSignature(signatures["signature"], signatures["files"], filename)) {
241 return false;
244 std::string dirname = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).parent_path());
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"];
250 if (!ValidateChecksum(sig_filename, sig_checksum)) {
251 return false;
255 return true;
259 * Validate that the signatures mentioned in the signature file are matching
260 * the files in question.
262 * @note if ALLOW_INVALID_SIGNATURE is defined, this function will always
263 * return true (but will still report any errors in the console).
265 * @return True iff the files in the signature file passed validation.
267 bool ValidateSignatureFile(const std::string &filename)
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