-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathrelease_verify_tests.cpp
More file actions
294 lines (254 loc) · 13.1 KB
/
Copy pathrelease_verify_tests.cpp
File metadata and controls
294 lines (254 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// Copyright (c) 2014-2026 The Reddcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <node/release_verify.h>
#include <test/util/setup_common.h>
#include <util/strencodings.h>
#include <util/system.h>
#include <boost/test/unit_test.hpp>
#include <fstream>
#include <string>
using node::FindArtifactDigest;
using node::HashFile;
using node::RELEASE_PUBKEY;
using node::StagedRelease;
using node::VerifyReleaseManifest;
namespace {
//! A manifest in the shape the release actually publishes, including the signed
//! and unsigned variants that sit side by side. Those pairs are the reason the
//! filename match has to be exact, so they belong in the fixture rather than in
//! one test that remembers to add them.
const std::string MANIFEST{
"94e9735251606649b8087c5e7af93e26265bd742bbcf688dd87b22401514807f reddcoin-4.22.9.4-aarch64-linux-gnu.tar.gz\n"
"05a818023af2a88af6dde102820df5c5c2655935ca1a8f7b707bc7214dc13293 reddcoin-4.22.9.4-x86_64-linux-gnu.tar.gz\n"
"1111111111111111111111111111111111111111111111111111111111111111 reddcoin-4.22.9.4-win64-setup-signed.exe\n"
"2222222222222222222222222222222222222222222222222222222222222222 reddcoin-4.22.9.4-win64-setup-unsigned.exe\n"
"3333333333333333333333333333333333333333333333333333333333333333 reddcoin-4.22.9.4-osx-signed.dmg\n"
"4444444444444444444444444444444444444444444444444444444444444444 reddcoin-4.22.9.4-osx-unsigned.dmg\n"};
std::string HexOf(const uint256& value)
{
return HexStr(Span<const unsigned char>(value.begin(), value.size()));
}
} // namespace
BOOST_FIXTURE_TEST_SUITE(release_verify_tests, BasicTestingSetup)
BOOST_AUTO_TEST_CASE(the_pinned_key_is_usable)
{
// A well formed 32 bytes need not be a point on the curve, and an anchor
// that is not one would fail at the first verification rather than here.
const std::vector<unsigned char> bytes{ParseHex(RELEASE_PUBKEY)};
BOOST_REQUIRE_EQUAL(bytes.size(), 32U);
BOOST_CHECK_EQUAL(RELEASE_PUBKEY.size(), 64U);
BOOST_CHECK(RELEASE_PUBKEY == ToLower(RELEASE_PUBKEY));
const XOnlyPubKey pubkey{bytes};
BOOST_CHECK(pubkey.IsFullyValid());
}
BOOST_AUTO_TEST_CASE(an_exact_filename_is_required)
{
// The one that matters. A substring match for "signed.exe" also matches
// "win64-setup-unsigned.exe", which would hand a Windows user the unsigned
// installer while reporting a verified download.
uint256 digest;
std::string error;
BOOST_REQUIRE(FindArtifactDigest(MANIFEST, "reddcoin-4.22.9.4-win64-setup-signed.exe", digest, error));
BOOST_CHECK_EQUAL(HexOf(digest),
"1111111111111111111111111111111111111111111111111111111111111111");
BOOST_REQUIRE(FindArtifactDigest(MANIFEST, "reddcoin-4.22.9.4-win64-setup-unsigned.exe", digest, error));
BOOST_CHECK_EQUAL(HexOf(digest),
"2222222222222222222222222222222222222222222222222222222222222222");
// A partial name resolves to nothing rather than to whichever line happens
// to contain it.
BOOST_CHECK(!FindArtifactDigest(MANIFEST, "signed.exe", digest, error));
BOOST_CHECK(!FindArtifactDigest(MANIFEST, "win64-setup-signed.exe", digest, error));
BOOST_CHECK(!FindArtifactDigest(MANIFEST, "reddcoin-4.22.9.4-win64-setup-signed", digest, error));
}
BOOST_AUTO_TEST_CASE(a_name_the_manifest_does_not_list_fails)
{
uint256 digest;
std::string error;
BOOST_CHECK(!FindArtifactDigest(MANIFEST, "reddcoin-4.22.9.4-riscv64-linux-gnu.tar.gz", digest, error));
BOOST_CHECK(!error.empty());
BOOST_CHECK(!FindArtifactDigest(MANIFEST, "", digest, error));
}
BOOST_AUTO_TEST_CASE(a_duplicated_entry_fails_rather_than_picking_one)
{
// Which digest is correct is precisely what a duplicate makes unknowable,
// so taking either would be inventing an answer.
const std::string duplicated{
MANIFEST +
"5555555555555555555555555555555555555555555555555555555555555555 reddcoin-4.22.9.4-osx-signed.dmg\n"};
uint256 digest;
std::string error;
BOOST_CHECK(!FindArtifactDigest(duplicated, "reddcoin-4.22.9.4-osx-signed.dmg", digest, error));
BOOST_CHECK(!error.empty());
}
BOOST_AUTO_TEST_CASE(malformed_lines_are_skipped_not_matched)
{
const std::string messy{
"\n"
"not a manifest line at all\n"
"zzzz reddcoin-4.22.9.4-x86_64-linux-gnu.tar.gz\n"
"05a818023af2a88af6dde102820df5c5c2655935ca1a8f7b707bc7214dc13293 one-space-only.tar.gz\n" +
MANIFEST};
uint256 digest;
std::string error;
// The good line is still found, and the malformed ones matched nothing.
BOOST_REQUIRE(FindArtifactDigest(messy, "reddcoin-4.22.9.4-x86_64-linux-gnu.tar.gz", digest, error));
BOOST_CHECK_EQUAL(HexOf(digest),
"05a818023af2a88af6dde102820df5c5c2655935ca1a8f7b707bc7214dc13293");
BOOST_CHECK(!FindArtifactDigest(messy, "one-space-only.tar.gz", digest, error));
}
BOOST_AUTO_TEST_CASE(a_signature_of_the_wrong_length_is_rejected_before_verifying)
{
// Load bearing rather than defensive. XOnlyPubKey::VerifySchnorr asserts on
// any length other than 64, so reaching it with a malformed signature file
// would abort the process instead of reporting a bad release.
std::string error;
BOOST_CHECK(!VerifyReleaseManifest(MANIFEST, std::string(126, 'a'), error));
BOOST_CHECK(!error.empty());
BOOST_CHECK(!VerifyReleaseManifest(MANIFEST, std::string(130, 'a'), error));
BOOST_CHECK(!VerifyReleaseManifest(MANIFEST, "", error));
BOOST_CHECK(!VerifyReleaseManifest(MANIFEST, "not hexadecimal at all", error));
}
BOOST_AUTO_TEST_CASE(a_signature_from_another_key_is_rejected)
{
// Structurally valid, correct length, and not ours.
std::string error;
BOOST_CHECK(!VerifyReleaseManifest(MANIFEST, std::string(128, '0'), error));
BOOST_CHECK_EQUAL(error, "Release manifest signature is not valid");
}
BOOST_AUTO_TEST_CASE(hashing_a_file_matches_the_manifest_form)
{
// The comparison phase 4b makes: a file on disk against a digest read out
// of a verified manifest.
const fs::path path{gArgs.GetDataDirNet() / "artifact"};
{
fsbridge::ofstream out{path, std::ios::binary};
out << "reddcoin";
}
uint256 digest;
std::string error;
BOOST_REQUIRE(HashFile(path, digest, error));
// sha256("reddcoin")
BOOST_CHECK_EQUAL(HexOf(digest),
"827ea7b9e26989931cbb1f10711d6575ad01aeac043607044d7eb09f322d0d8c");
}
BOOST_AUTO_TEST_CASE(hashing_a_missing_file_fails_cleanly)
{
uint256 digest;
std::string error;
BOOST_CHECK(!HashFile(gArgs.GetDataDirNet() / "no-such-file", digest, error));
BOOST_CHECK(!error.empty());
}
BOOST_AUTO_TEST_CASE(the_real_published_release_verifies)
{
// The published 4.22.9.4 manifest and its signature, byte for byte as the
// server serves them. This is the check that the pinned key, the tag, the
// message construction and the parser all agree with what the release
// process actually produced, rather than merely with each other.
//
// Offline on purpose: a regression test, not a liveness check.
const std::string published{
"94e9735251606649b8087c5e7af93e26265bd742bbcf688dd87b22401514807f reddcoin-4.22.9.4-aarch64-linux-gnu.tar.gz\n"
"a61fb2624553e85bc4b7353a45cb0b633f7e943efe298e741985788b9e6553f9 reddcoin-4.22.9.4-arm-linux-gnueabihf.tar.gz\n"
"e7930ad5fd11f43f5a0c472a502ccee95af9a78f82507f9795b4e64d05daf19b reddcoin-4.22.9.4.tar.gz\n"
"5f57deca7d27419e7774f2058ca7ea15a8179a70d4bc131dcba741486cc8baa3 reddcoin-4.22.9.4-powerpc64-linux-gnu.tar.gz\n"
"c856d963da6332cfae8301742c6ccd9900042445002666204661a79fcd6a5fc4 reddcoin-4.22.9.4-powerpc64le-linux-gnu.tar.gz\n"
"aeaf2ef8605f23e5bc59f26863b25a5bda56f8b11a427e2854f7506231a0f56f reddcoin-4.22.9.4-riscv64-linux-gnu.tar.gz\n"
"e9a9d13701bc6bb010848c346306abf103e5bb67a68fc86a914f2538b96ca4c8 reddcoin-4.22.9.4-osx-signed.dmg\n"
"ffa2120b392df5f4abae316d835d07091b221b11692bff7ff433edaa772c31a5 reddcoin-4.22.9.4-osx-unsigned.dmg\n"
"25d9156bbcfc624b92fc29af4260854de8d35464bd1eeea2859f4ba2ddfd5dfa reddcoin-4.22.9.4-osx-unsigned.tar.gz\n"
"ea169121d668952a26ef8c25b4db2f9831e255452da72d424d0be8d4fca11f51 reddcoin-4.22.9.4-osx64.tar.gz\n"
"05a818023af2a88af6dde102820df5c5c2655935ca1a8f7b707bc7214dc13293 reddcoin-4.22.9.4-x86_64-linux-gnu.tar.gz\n"
"96dbc096e6ff41d0fa34051fc5b1fb9ec09eabd5a79e850c5042c1692a27c85a reddcoin-4.22.9.4-win64-setup-signed.exe\n"
"9a44ebfa6dfcf21027b1132d6482cc96d6806faf4e8d695379e8f6bfb947fdc5 reddcoin-4.22.9.4-win-unsigned.tar.gz\n"
"541e1e48598fdc68bb59ed0f845390c63a18e32f871212d42cb8f53a0abd7f1f reddcoin-4.22.9.4-win64-setup-unsigned.exe\n"
"facc901ddf2ce860df9b0e1d2fe21b723a7283e1dc0487694bcdde938d597dc6 reddcoin-4.22.9.4-win64.zip\n"};
const std::string signature{
"67b1904a0fee2f3e313124b5ddb513356db7e46285f6fe2f044a6453e3364e39"
"b991765a99c423c85a14f612bf29fbd37abfcd9abc7e67220188cae1fbcdc648"};
std::string error;
BOOST_CHECK_MESSAGE(VerifyReleaseManifest(published, signature, error),
"the real release failed to verify: " << error);
// And the digest it gives for this host's artifact is the one downloading
// that artifact actually produced.
uint256 digest;
BOOST_REQUIRE(FindArtifactDigest(published, "reddcoin-4.22.9.4-x86_64-linux-gnu.tar.gz",
digest, error));
BOOST_CHECK_EQUAL(HexOf(digest),
"05a818023af2a88af6dde102820df5c5c2655935ca1a8f7b707bc7214dc13293");
// One byte different is a different release, which shows the signature is
// bound to these bytes rather than verifying for some other reason.
std::string tampered{published};
tampered[0] = tampered[0] == '9' ? '8' : '9';
BOOST_CHECK(!VerifyReleaseManifest(tampered, signature, error));
// As does an appended line: the realistic corruption is a manifest
// regenerated and republished without being re-signed.
BOOST_CHECK(!VerifyReleaseManifest(
published + "0000000000000000000000000000000000000000000000000000000000000000 extra\n",
signature, error));
}
//! Staging, added with phase 4b. The download itself needs a network, but the
//! decisions around it, what is kept, what is thrown away, are the part that
//! can be got wrong quietly and are testable here.
BOOST_AUTO_TEST_CASE(staging_refuses_an_empty_version_or_name)
{
// These are the arguments StageVerifiedRelease rejects before it fetches
// anything, which is what makes them safe to assert here. Anything that
// reaches the fetch performs real DNS and TLS, so it does not belong in a
// unit test however certain the failure is.
const fs::path root{gArgs.GetDataDirNet() / "staging-empty"};
StagedRelease staged;
std::string error;
BOOST_CHECK(!node::StageVerifiedRelease("", "x.tar.gz", root, node::DownloadProgress{},
node::DownloadCancel{}, staged, error));
BOOST_CHECK(!error.empty());
BOOST_CHECK(!node::StageVerifiedRelease("4.22.9.4", "", root, node::DownloadProgress{},
node::DownloadCancel{}, staged, error));
BOOST_CHECK(!error.empty());
BOOST_CHECK(!fs::exists(root));
}
BOOST_AUTO_TEST_CASE(a_staged_file_is_recognised_by_its_hash_not_its_name)
{
// The reuse decision. A file already present and already correct must not
// be downloaded again, and a file present under the right name with the
// wrong contents must not be mistaken for it.
const fs::path dir{gArgs.GetDataDirNet() / "staged"};
fs::create_directories(dir);
const fs::path artifact{dir / "artifact.tar.gz"};
{
fsbridge::ofstream out{artifact, std::ios::binary};
out << "reddcoin";
}
uint256 present;
std::string error;
BOOST_REQUIRE(HashFile(artifact, present, error));
uint256 expected;
BOOST_REQUIRE(FindArtifactDigest(
"827ea7b9e26989931cbb1f10711d6575ad01aeac043607044d7eb09f322d0d8c artifact.tar.gz\n",
"artifact.tar.gz", expected, error));
BOOST_CHECK(present == expected);
// Same name, different contents. The name proves nothing.
{
fsbridge::ofstream out{artifact, std::ios::binary};
out << "reddcoin ";
}
uint256 changed;
BOOST_REQUIRE(HashFile(artifact, changed, error));
BOOST_CHECK(changed != expected);
}
BOOST_AUTO_TEST_CASE(hashing_is_not_confused_by_size_alone)
{
// Two files of identical length and different content, since a size check
// is the tempting shortcut and would pass both.
const fs::path a{gArgs.GetDataDirNet() / "a.bin"};
const fs::path b{gArgs.GetDataDirNet() / "b.bin"};
{ fsbridge::ofstream out{a, std::ios::binary}; out << std::string(100000, 'x'); }
{ fsbridge::ofstream out{b, std::ios::binary}; out << std::string(99999, 'x') << 'y'; }
uint256 ha, hb;
std::string error;
BOOST_REQUIRE(HashFile(a, ha, error));
BOOST_REQUIRE(HashFile(b, hb, error));
BOOST_CHECK_EQUAL(fs::file_size(a), fs::file_size(b));
BOOST_CHECK(ha != hb);
}
BOOST_AUTO_TEST_SUITE_END()