OpenTTD Source  20241108-master-g80f628063a
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  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 
63 static 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 
101 static 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 
145 static 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 
194 static 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 
267 bool 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.
Definition: fileio_type.h:133
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.
Definition: signature.cpp:145
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:194
bool ValidateSignatureFile(const std::string &filename)
Validate that the signatures mentioned in the signature file are matching the files in question.
Definition: signature.cpp:267
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:101
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