mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 16:56:48 +00:00
953 lines
36 KiB
C++
953 lines
36 KiB
C++
#include <test/jtx/Env.h>
|
|
#include <test/unit_test/utils.h>
|
|
|
|
#include <xrpld/app/misc/ValidatorList.h>
|
|
|
|
#include <xrpl/basics/Slice.h>
|
|
#include <xrpl/basics/base64.h>
|
|
#include <xrpl/basics/contract.h>
|
|
#include <xrpl/basics/strHex.h>
|
|
#include <xrpl/beast/unit_test/suite.h>
|
|
#include <xrpl/protocol/HashPrefix.h>
|
|
#include <xrpl/protocol/KeyType.h>
|
|
#include <xrpl/protocol/PublicKey.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STObject.h>
|
|
#include <xrpl/protocol/SecretKey.h>
|
|
#include <xrpl/protocol/Seed.h>
|
|
#include <xrpl/protocol/Serializer.h>
|
|
#include <xrpl/protocol/Sign.h>
|
|
#include <xrpl/protocol/tokens.h>
|
|
#include <xrpl/server/Manifest.h>
|
|
#include <xrpl/server/Wallet.h>
|
|
|
|
#include <boost/filesystem/operations.hpp>
|
|
#include <boost/filesystem/path.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <exception>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl::test {
|
|
|
|
class Manifest_test : public beast::unit_test::Suite
|
|
{
|
|
private:
|
|
static PublicKey
|
|
randomNode()
|
|
{
|
|
return derivePublicKey(KeyType::Secp256k1, randomSecretKey());
|
|
}
|
|
|
|
static PublicKey
|
|
randomMasterKey()
|
|
{
|
|
return derivePublicKey(KeyType::Ed25519, randomSecretKey());
|
|
}
|
|
|
|
static void
|
|
cleanupDatabaseDir(boost::filesystem::path const& dbPath)
|
|
{
|
|
using namespace boost::filesystem;
|
|
if (!exists(dbPath) || !is_directory(dbPath) || !is_empty(dbPath))
|
|
return;
|
|
remove(dbPath);
|
|
}
|
|
|
|
static void
|
|
setupDatabaseDir(boost::filesystem::path const& dbPath)
|
|
{
|
|
using namespace boost::filesystem;
|
|
if (!exists(dbPath))
|
|
{
|
|
create_directory(dbPath);
|
|
return;
|
|
}
|
|
|
|
if (!is_directory(dbPath))
|
|
{
|
|
// someone created a file where we want to put our directory
|
|
Throw<std::runtime_error>("Cannot create directory: " + dbPath.string());
|
|
}
|
|
}
|
|
static boost::filesystem::path
|
|
getDatabasePath()
|
|
{
|
|
return boost::filesystem::current_path() / "manifest_test_databases";
|
|
}
|
|
|
|
public:
|
|
Manifest_test()
|
|
{
|
|
try
|
|
{
|
|
setupDatabaseDir(getDatabasePath());
|
|
}
|
|
catch (std::exception const&) // NOLINT(bugprone-empty-catch)
|
|
{
|
|
}
|
|
}
|
|
~Manifest_test() override
|
|
{
|
|
try
|
|
{
|
|
cleanupDatabaseDir(getDatabasePath());
|
|
}
|
|
catch (std::exception const&) // NOLINT(bugprone-empty-catch)
|
|
{
|
|
}
|
|
}
|
|
|
|
static std::string
|
|
makeManifestString(
|
|
PublicKey const& pk,
|
|
SecretKey const& sk,
|
|
PublicKey const& spk,
|
|
SecretKey const& ssk,
|
|
int seq)
|
|
{
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = seq;
|
|
st[sfPublicKey] = pk;
|
|
st[sfSigningPubKey] = spk;
|
|
|
|
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
|
sign(st, HashPrefix::Manifest, *publicKeyType(spk), ssk);
|
|
|
|
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
|
sign(st, HashPrefix::Manifest, *publicKeyType(pk), sk, sfMasterSignature);
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return base64Encode(std::string(static_cast<char const*>(s.data()), s.size()));
|
|
}
|
|
|
|
std::string
|
|
makeRevocationString(SecretKey const& sk, KeyType type, bool invalidSig = false)
|
|
{
|
|
auto const pk = derivePublicKey(type, sk);
|
|
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
|
st[sfPublicKey] = pk;
|
|
|
|
sign(
|
|
st, HashPrefix::Manifest, type, invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
|
BEAST_EXPECT(invalidSig ^ verify(st, HashPrefix::Manifest, pk, sfMasterSignature));
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return base64Encode(std::string(static_cast<char const*>(s.data()), s.size()));
|
|
}
|
|
|
|
Manifest
|
|
makeRevocation(SecretKey const& sk, KeyType type, bool invalidSig = false)
|
|
{
|
|
auto const pk = derivePublicKey(type, sk);
|
|
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = std::numeric_limits<std::uint32_t>::max();
|
|
st[sfPublicKey] = pk;
|
|
|
|
sign(
|
|
st, HashPrefix::Manifest, type, invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
|
BEAST_EXPECT(invalidSig ^ verify(st, HashPrefix::Manifest, pk, sfMasterSignature));
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
std::string const m(static_cast<char const*>(s.data()), s.size());
|
|
if (auto r = deserializeManifest(m))
|
|
return std::move(*r);
|
|
Throw<std::runtime_error>("Could not create a revocation manifest");
|
|
return *deserializeManifest(std::string{}); // Silence compiler warning.
|
|
}
|
|
|
|
Manifest
|
|
makeManifest(
|
|
SecretKey const& sk,
|
|
KeyType type,
|
|
SecretKey const& ssk,
|
|
KeyType stype,
|
|
int seq,
|
|
bool invalidSig = false)
|
|
{
|
|
auto const pk = derivePublicKey(type, sk);
|
|
auto const spk = derivePublicKey(stype, ssk);
|
|
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = seq;
|
|
st[sfPublicKey] = pk;
|
|
st[sfSigningPubKey] = spk;
|
|
|
|
sign(st, HashPrefix::Manifest, stype, ssk);
|
|
BEAST_EXPECT(verify(st, HashPrefix::Manifest, spk));
|
|
|
|
sign(
|
|
st, HashPrefix::Manifest, type, invalidSig ? randomSecretKey() : sk, sfMasterSignature);
|
|
BEAST_EXPECT(invalidSig ^ verify(st, HashPrefix::Manifest, pk, sfMasterSignature));
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
std::string const m(static_cast<char const*>(s.data()), s.size());
|
|
if (auto r = deserializeManifest(m))
|
|
return std::move(*r);
|
|
Throw<std::runtime_error>("Could not create a manifest");
|
|
return *deserializeManifest(std::string{}); // Silence compiler warning.
|
|
}
|
|
|
|
static Manifest
|
|
clone(Manifest const& m)
|
|
{
|
|
Manifest m2(m.serialized, m.masterKey, m.signingKey, m.sequence, m.domain);
|
|
return m2;
|
|
}
|
|
|
|
void
|
|
testLoadStore(ManifestCache& m)
|
|
{
|
|
testcase("load/store");
|
|
|
|
std::string const dbName("ManifestCacheTestDB");
|
|
{
|
|
jtx::Env env(*this);
|
|
DatabaseCon::Setup setup;
|
|
setup.dataDir = getDatabasePath();
|
|
assert(!setup.useGlobalPragma);
|
|
|
|
auto dbCon = makeTestWalletDB(setup, dbName, env.journal);
|
|
|
|
auto getPopulatedManifests =
|
|
[](ManifestCache const& cache) -> std::vector<Manifest const*> {
|
|
std::vector<Manifest const*> result;
|
|
result.reserve(32);
|
|
cache.forEachManifest([&result](Manifest const& man) { result.push_back(&man); });
|
|
return result;
|
|
};
|
|
auto sort = [](std::vector<Manifest const*> mv) -> std::vector<Manifest const*> {
|
|
std::ranges::sort(mv, [](Manifest const* lhs, Manifest const* rhs) {
|
|
return lhs->serialized < rhs->serialized;
|
|
});
|
|
return mv;
|
|
};
|
|
std::vector<Manifest const*> const inManifests(sort(getPopulatedManifests(m)));
|
|
|
|
auto& app = env.app();
|
|
auto unl = std::make_unique<ValidatorList>(
|
|
m, m, env.timeKeeper(), app.config().legacy("database_path"), env.journal);
|
|
|
|
{
|
|
// save should not store untrusted master keys to db
|
|
// except for revocations
|
|
m.save(*dbCon, "ValidatorManifests", [&unl](PublicKey const& pubKey) {
|
|
return unl->listed(pubKey);
|
|
});
|
|
|
|
ManifestCache loaded;
|
|
|
|
loaded.load(*dbCon, "ValidatorManifests");
|
|
|
|
// check that all loaded manifests are revocations
|
|
std::vector<Manifest const*> const loadedManifests(
|
|
sort(getPopulatedManifests(loaded)));
|
|
|
|
for (auto const& man : loadedManifests)
|
|
BEAST_EXPECT(man->revoked());
|
|
}
|
|
{
|
|
// save should store all trusted master keys to db
|
|
std::vector<std::string> s1;
|
|
std::vector<std::string> const keys;
|
|
s1.reserve(inManifests.size());
|
|
|
|
for (auto const& man : inManifests)
|
|
s1.push_back(toBase58(TokenType::NodePublic, man->masterKey));
|
|
unl->load({}, s1, keys);
|
|
|
|
m.save(*dbCon, "ValidatorManifests", [&unl](PublicKey const& pubKey) {
|
|
return unl->listed(pubKey);
|
|
});
|
|
ManifestCache loaded;
|
|
loaded.load(*dbCon, "ValidatorManifests");
|
|
|
|
// check that the manifest caches are the same
|
|
std::vector<Manifest const*> const loadedManifests(
|
|
sort(getPopulatedManifests(loaded)));
|
|
|
|
if (inManifests.size() == loadedManifests.size())
|
|
{
|
|
BEAST_EXPECT(
|
|
std::equal(
|
|
inManifests.begin(),
|
|
inManifests.end(),
|
|
loadedManifests.begin(),
|
|
[](Manifest const* lhs, Manifest const* rhs) { return *lhs == *rhs; }));
|
|
}
|
|
else
|
|
{
|
|
fail();
|
|
}
|
|
}
|
|
{
|
|
// load config manifest
|
|
ManifestCache loaded;
|
|
std::vector<std::string> const emptyRevocation;
|
|
|
|
std::string const badManifest = "bad manifest";
|
|
BEAST_EXPECT(
|
|
!loaded.load(*dbCon, "ValidatorManifests", badManifest, emptyRevocation));
|
|
|
|
auto const sk = randomSecretKey();
|
|
auto const pk = derivePublicKey(KeyType::Ed25519, sk);
|
|
auto const kp = randomKeyPair(KeyType::Secp256k1);
|
|
|
|
std::string const cfgManifest = makeManifestString(pk, sk, kp.first, kp.second, 0);
|
|
|
|
BEAST_EXPECT(
|
|
loaded.load(*dbCon, "ValidatorManifests", cfgManifest, emptyRevocation));
|
|
}
|
|
{
|
|
// load config revocation
|
|
ManifestCache loaded;
|
|
std::string const emptyManifest;
|
|
|
|
std::vector<std::string> const badRevocation = {"bad revocation"};
|
|
BEAST_EXPECT(
|
|
!loaded.load(*dbCon, "ValidatorManifests", emptyManifest, badRevocation));
|
|
|
|
auto const sk = randomSecretKey();
|
|
auto const keyType = KeyType::Ed25519;
|
|
auto const pk = derivePublicKey(keyType, sk);
|
|
auto const kp = randomKeyPair(KeyType::Secp256k1);
|
|
std::vector<std::string> const nonRevocation = {
|
|
makeManifestString(pk, sk, kp.first, kp.second, 0)};
|
|
|
|
BEAST_EXPECT(
|
|
!loaded.load(*dbCon, "ValidatorManifests", emptyManifest, nonRevocation));
|
|
BEAST_EXPECT(!loaded.revoked(pk));
|
|
|
|
std::vector<std::string> const badSigRevocation = {
|
|
makeRevocationString(sk, keyType, true)};
|
|
BEAST_EXPECT(
|
|
!loaded.load(*dbCon, "ValidatorManifests", emptyManifest, badSigRevocation));
|
|
BEAST_EXPECT(!loaded.revoked(pk));
|
|
|
|
std::vector<std::string> const cfgRevocation = {makeRevocationString(sk, keyType)};
|
|
BEAST_EXPECT(
|
|
loaded.load(*dbCon, "ValidatorManifests", emptyManifest, cfgRevocation));
|
|
|
|
BEAST_EXPECT(loaded.revoked(pk));
|
|
}
|
|
}
|
|
boost::filesystem::remove(getDatabasePath() / boost::filesystem::path(dbName));
|
|
}
|
|
|
|
void
|
|
testGetSignature()
|
|
{
|
|
testcase("getSignature");
|
|
auto const sk = randomSecretKey();
|
|
auto const pk = derivePublicKey(KeyType::Ed25519, sk);
|
|
auto const kp = randomKeyPair(KeyType::Secp256k1);
|
|
auto const m = makeManifest(sk, KeyType::Ed25519, kp.second, KeyType::Secp256k1, 0);
|
|
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = 0;
|
|
st[sfPublicKey] = pk;
|
|
st[sfSigningPubKey] = kp.first;
|
|
Serializer ss;
|
|
ss.add32(HashPrefix::Manifest);
|
|
st.addWithoutSigningFields(ss);
|
|
auto const sig = sign(KeyType::Secp256k1, kp.second, ss.slice());
|
|
BEAST_EXPECT(
|
|
strHex(sig) ==
|
|
strHex(*m.getSignature())); // NOLINT(bugprone-unchecked-optional-access)
|
|
|
|
auto const masterSig = sign(KeyType::Ed25519, sk, ss.slice());
|
|
BEAST_EXPECT(strHex(masterSig) == strHex(m.getMasterSignature()));
|
|
}
|
|
|
|
void
|
|
testGetKeys()
|
|
{
|
|
testcase("getKeys");
|
|
|
|
ManifestCache cache;
|
|
auto const sk = randomSecretKey();
|
|
auto const pk = derivePublicKey(KeyType::Ed25519, sk);
|
|
|
|
// getSigningKey should return same key if there is no manifest
|
|
BEAST_EXPECT(cache.getSigningKey(pk) == pk);
|
|
|
|
// getSigningKey should return the ephemeral public key
|
|
// for the listed validator master public key
|
|
// getMasterKey should return the listed validator master key
|
|
// for that ephemeral public key
|
|
auto const kp0 = randomKeyPair(KeyType::Secp256k1);
|
|
BEAST_EXPECT(
|
|
ManifestDisposition::Accepted ==
|
|
cache.applyManifest(
|
|
makeManifest(sk, KeyType::Ed25519, kp0.second, KeyType::Secp256k1, 0)));
|
|
BEAST_EXPECT(cache.getSigningKey(pk) == kp0.first);
|
|
BEAST_EXPECT(cache.getMasterKey(kp0.first) == pk);
|
|
|
|
// getSigningKey should return the latest ephemeral public key
|
|
// for the listed validator master public key
|
|
// getMasterKey should only return a master key for the latest
|
|
// ephemeral public key
|
|
auto const kp1 = randomKeyPair(KeyType::Secp256k1);
|
|
BEAST_EXPECT(
|
|
ManifestDisposition::Accepted ==
|
|
cache.applyManifest(
|
|
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 1)));
|
|
BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first);
|
|
BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk);
|
|
BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first);
|
|
|
|
// getSigningKey and getMasterKey should fail if a new manifest is
|
|
// applied with the same signing key but a higher sequence
|
|
BEAST_EXPECT(
|
|
ManifestDisposition::BadEphemeralKey ==
|
|
cache.applyManifest(
|
|
makeManifest(sk, KeyType::Ed25519, kp1.second, KeyType::Secp256k1, 2)));
|
|
BEAST_EXPECT(cache.getSigningKey(pk) == kp1.first);
|
|
BEAST_EXPECT(cache.getMasterKey(kp1.first) == pk);
|
|
BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first);
|
|
|
|
// getSigningKey should return std::nullopt for a revoked master public
|
|
// key getMasterKey should return std::nullopt for an ephemeral public
|
|
// key from a revoked master public key
|
|
BEAST_EXPECT(
|
|
ManifestDisposition::Accepted ==
|
|
cache.applyManifest(makeRevocation(sk, KeyType::Ed25519)));
|
|
BEAST_EXPECT(cache.revoked(pk));
|
|
BEAST_EXPECT(cache.getSigningKey(pk) == pk);
|
|
BEAST_EXPECT(cache.getMasterKey(kp0.first) == kp0.first);
|
|
BEAST_EXPECT(cache.getMasterKey(kp1.first) == kp1.first);
|
|
}
|
|
|
|
void
|
|
testValidatorToken()
|
|
{
|
|
testcase("validator token");
|
|
|
|
{
|
|
auto const valSecret = parseBase58<SecretKey>(
|
|
TokenType::NodePrivate, "paQmjZ37pKKPMrgadBLsuf9ab7Y7EUNzh27LQrZqoexpAs31nJi");
|
|
|
|
// Format token string to test trim()
|
|
std::vector<std::string> const tokenBlob = {
|
|
" eyJ2YWxpZGF0aW9uX3NlY3JldF9rZXkiOiI5ZWQ0NWY4NjYyNDFjYzE4YTI3NDdiNT\n",
|
|
" \tQzODdjMDYyNTkwNzk3MmY0ZTcxOTAyMzFmYWE5Mzc0NTdmYTlkYWY2IiwibWFuaWZl \n",
|
|
"\tc3QiOiJKQUFBQUFGeEllMUZ0d21pbXZHdEgyaUNjTUpxQzlnVkZLaWxHZncxL3ZDeE\n",
|
|
"\t hYWExwbGMyR25NaEFrRTFhZ3FYeEJ3RHdEYklENk9NU1l1TTBGREFscEFnTms4U0tG\t \t\n",
|
|
"bjdNTzJmZGtjd1JRSWhBT25ndTlzQUtxWFlvdUorbDJWMFcrc0FPa1ZCK1pSUzZQU2\n",
|
|
"hsSkFmVXNYZkFpQnNWSkdlc2FhZE9KYy9hQVpva1MxdnltR21WcmxIUEtXWDNZeXd1\n",
|
|
"NmluOEhBU1FLUHVnQkQ2N2tNYVJGR3ZtcEFUSGxHS0pkdkRGbFdQWXk1QXFEZWRGdj\n",
|
|
"VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0ifQ==\n",
|
|
};
|
|
|
|
auto const manifest =
|
|
"JAAAAAFxIe1FtwmimvGtH2iCcMJqC9gVFKilGfw1/"
|
|
"vCxHXXLplc2GnMhAkE1agqXxBwD"
|
|
"wDbID6OMSYuM0FDAlpAgNk8SKFn7MO2fdkcwRQIhAOngu9sAKqXYouJ+l2V0W+"
|
|
"sAOkVB"
|
|
"+ZRS6PShlJAfUsXfAiBsVJGesaadOJc/"
|
|
"aAZokS1vymGmVrlHPKWX3Yywu6in8HASQKPu"
|
|
"gBD67kMaRFGvmpATHlGKJdvDFlWPYy5AqDedFv5TJa2w0i21eq3MYywLVJZnFO"
|
|
"r7C0kw"
|
|
"2AiTzSCjIzditQ8=";
|
|
|
|
auto const token = loadValidatorToken(tokenBlob);
|
|
BEAST_EXPECT(token);
|
|
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
|
BEAST_EXPECT(test::equal(token->validationSecret, *valSecret));
|
|
// NOLINTNEXTLINE(bugprone-unchecked-optional-access)
|
|
BEAST_EXPECT(token->manifest == manifest);
|
|
}
|
|
{
|
|
std::vector<std::string> const badToken = {"bad token"};
|
|
BEAST_EXPECT(!loadValidatorToken(badToken));
|
|
}
|
|
}
|
|
|
|
void
|
|
testManifestVersioning()
|
|
{
|
|
testcase("Versioning");
|
|
|
|
auto const sk = generateSecretKey(KeyType::Ed25519, randomSeed());
|
|
auto const pk = derivePublicKey(KeyType::Ed25519, sk);
|
|
|
|
auto const ssk = generateSecretKey(KeyType::Secp256k1, randomSeed());
|
|
auto const spk = derivePublicKey(KeyType::Secp256k1, ssk);
|
|
|
|
auto buildManifestObject = [&](std::uint16_t version) {
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = 3;
|
|
st[sfPublicKey] = pk;
|
|
st[sfSigningPubKey] = spk;
|
|
|
|
if (version != 0)
|
|
st[sfVersion] = version;
|
|
|
|
sign(st, HashPrefix::Manifest, KeyType::Ed25519, sk, sfMasterSignature);
|
|
sign(st, HashPrefix::Manifest, KeyType::Secp256k1, ssk);
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return std::string(static_cast<char const*>(s.data()), s.size());
|
|
};
|
|
|
|
// We understand version 0 manifests:
|
|
BEAST_EXPECT(deserializeManifest(buildManifestObject(0)));
|
|
|
|
// We don't understand any other versions:
|
|
BEAST_EXPECT(!deserializeManifest(buildManifestObject(1)));
|
|
BEAST_EXPECT(!deserializeManifest(buildManifestObject(2001)));
|
|
}
|
|
|
|
void
|
|
testManifestDeserialization()
|
|
{
|
|
std::array<KeyType, 2> const keyTypes{{KeyType::Ed25519, KeyType::Secp256k1}};
|
|
|
|
std::uint32_t sequence = 0;
|
|
|
|
// public key with invalid type
|
|
std::array<std::uint8_t, 33> const badKey{
|
|
0x99, 0x30, 0xE7, 0xFC, 0x9D, 0x56, 0xBB, 0x25, 0xD6, 0x89, 0x3B,
|
|
0xA3, 0xF3, 0x17, 0xAE, 0x5B, 0xCF, 0x33, 0xB3, 0x29, 0x1B, 0xD6,
|
|
0x3D, 0xB3, 0x26, 0x54, 0xA3, 0x13, 0x22, 0x2F, 0x7F, 0xD0, 0x20};
|
|
|
|
// Short public key:
|
|
std::array<std::uint8_t, 16> const shortKey{
|
|
0x03,
|
|
0x30,
|
|
0xE7,
|
|
0xFC,
|
|
0x9D,
|
|
0x56,
|
|
0xBB,
|
|
0x25,
|
|
0xD6,
|
|
0x89,
|
|
0x3B,
|
|
0xA3,
|
|
0xF3,
|
|
0x17,
|
|
0xAE,
|
|
0x5B};
|
|
|
|
auto toString = [](STObject const& st) {
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return std::string(static_cast<char const*>(s.data()), s.size());
|
|
};
|
|
|
|
for (auto const keyType : keyTypes)
|
|
{
|
|
auto const sk = generateSecretKey(keyType, randomSeed());
|
|
auto const pk = derivePublicKey(keyType, sk);
|
|
|
|
for (auto const sKeyType : keyTypes)
|
|
{
|
|
auto const ssk = generateSecretKey(sKeyType, randomSeed());
|
|
auto const spk = derivePublicKey(sKeyType, ssk);
|
|
|
|
auto buildManifestObject = [&](std::uint32_t seq,
|
|
std::optional<std::string> domain,
|
|
bool noSigningPublic = false,
|
|
bool noSignature = false) {
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = seq;
|
|
st[sfPublicKey] = pk;
|
|
|
|
if (domain)
|
|
st[sfDomain] = makeSlice(*domain);
|
|
|
|
if (!noSigningPublic)
|
|
st[sfSigningPubKey] = spk;
|
|
|
|
sign(st, HashPrefix::Manifest, keyType, sk, sfMasterSignature);
|
|
|
|
if (!noSignature)
|
|
sign(st, HashPrefix::Manifest, sKeyType, ssk);
|
|
|
|
return st;
|
|
};
|
|
|
|
{
|
|
testcase << "deserializeManifest: normal manifest (" << to_string(keyType)
|
|
<< " + " << to_string(sKeyType) << ")";
|
|
|
|
{ // valid manifest without domain
|
|
auto const st = buildManifestObject(++sequence, std::nullopt);
|
|
|
|
auto const m = toString(st);
|
|
auto const manifest = deserializeManifest(m);
|
|
|
|
BEAST_EXPECT(manifest);
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
|
BEAST_EXPECT(manifest->masterKey == pk);
|
|
BEAST_EXPECT(manifest->signingKey == spk);
|
|
BEAST_EXPECT(manifest->sequence == sequence);
|
|
BEAST_EXPECT(manifest->serialized == m);
|
|
BEAST_EXPECT(manifest->domain.empty());
|
|
BEAST_EXPECT(manifest->verify());
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
|
|
{ // invalid manifest (empty domain)
|
|
auto const st = buildManifestObject(++sequence, std::string{});
|
|
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
|
|
{ // invalid manifest (domain too short)
|
|
auto const st = buildManifestObject(++sequence, std::string{"a.b"});
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
{ // invalid manifest (domain too long)
|
|
std::string const s(254, 'a');
|
|
auto const st = buildManifestObject(++sequence, s + ".example.com");
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
{ // invalid manifest (domain component too long)
|
|
std::string const s(72, 'a');
|
|
auto const st = buildManifestObject(++sequence, s + ".example.com");
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
|
|
auto const st = buildManifestObject(++sequence, std::string{"example.com"});
|
|
|
|
{
|
|
// valid manifest with domain
|
|
auto const m = toString(st);
|
|
auto const manifest = deserializeManifest(m);
|
|
|
|
BEAST_EXPECT(manifest);
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
|
BEAST_EXPECT(manifest->masterKey == pk);
|
|
BEAST_EXPECT(manifest->signingKey == spk);
|
|
BEAST_EXPECT(manifest->sequence == sequence);
|
|
BEAST_EXPECT(manifest->serialized == m);
|
|
BEAST_EXPECT(manifest->domain == "example.com");
|
|
BEAST_EXPECT(manifest->verify());
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
{
|
|
// valid manifest with invalid signature
|
|
auto badSigSt = st;
|
|
badSigSt[sfSequence] = sequence + 1;
|
|
|
|
auto const m = toString(badSigSt);
|
|
auto const manifest = deserializeManifest(m);
|
|
|
|
BEAST_EXPECT(manifest);
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
|
BEAST_EXPECT(manifest->masterKey == pk);
|
|
BEAST_EXPECT(manifest->signingKey == spk);
|
|
BEAST_EXPECT(manifest->sequence == sequence + 1);
|
|
BEAST_EXPECT(manifest->serialized == m);
|
|
BEAST_EXPECT(manifest->domain == "example.com");
|
|
BEAST_EXPECT(!manifest->verify());
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
{
|
|
// reject missing sequence
|
|
auto badSt = st;
|
|
BEAST_EXPECT(badSt.delField(sfSequence));
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject missing public key
|
|
auto badSt = st;
|
|
BEAST_EXPECT(badSt.delField(sfPublicKey));
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject invalid public key type
|
|
auto badSt = st;
|
|
badSt[sfPublicKey] = makeSlice(badKey);
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject short public key
|
|
auto badSt = st;
|
|
badSt[sfPublicKey] = makeSlice(shortKey);
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject missing signing public key
|
|
auto badSt = st;
|
|
BEAST_EXPECT(badSt.delField(sfSigningPubKey));
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject invalid signing public key type
|
|
auto badSt = st;
|
|
badSt[sfSigningPubKey] = makeSlice(badKey);
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject short signing public key
|
|
auto badSt = st;
|
|
badSt[sfSigningPubKey] = makeSlice(shortKey);
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject missing signature
|
|
auto badSt = st;
|
|
BEAST_EXPECT(badSt.delField(sfMasterSignature));
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject missing signing key signature
|
|
auto badSt = st;
|
|
BEAST_EXPECT(badSt.delField(sfSignature));
|
|
BEAST_EXPECT(!deserializeManifest(toString(badSt)));
|
|
}
|
|
{
|
|
// reject matching master & ephemeral keys
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = 314159;
|
|
st[sfPublicKey] = pk;
|
|
st[sfSigningPubKey] = pk;
|
|
|
|
sign(st, HashPrefix::Manifest, keyType, sk, sfMasterSignature);
|
|
|
|
sign(st, HashPrefix::Manifest, sKeyType, sk);
|
|
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
}
|
|
|
|
{
|
|
testcase << "deserializeManifest: revocation manifest (" << to_string(keyType)
|
|
<< " + " << to_string(sKeyType) << ")";
|
|
|
|
// valid revocation
|
|
{
|
|
auto const st = buildManifestObject(
|
|
std::numeric_limits<std::uint32_t>::max(), std::nullopt, true, true);
|
|
|
|
auto const m = toString(st);
|
|
auto const manifest = deserializeManifest(m);
|
|
|
|
BEAST_EXPECT(manifest);
|
|
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
|
BEAST_EXPECT(manifest->masterKey == pk);
|
|
|
|
// Since this manifest is revoked, it should not have a signingKey
|
|
BEAST_EXPECT(!manifest->signingKey);
|
|
BEAST_EXPECT(manifest->revoked());
|
|
BEAST_EXPECT(manifest->domain.empty());
|
|
BEAST_EXPECT(manifest->serialized == m);
|
|
BEAST_EXPECT(manifest->verify());
|
|
// NOLINTEND(bugprone-unchecked-optional-access)
|
|
}
|
|
|
|
{ // can't specify an ephemeral signing key
|
|
auto const st = buildManifestObject(
|
|
std::numeric_limits<std::uint32_t>::max(), std::nullopt, true, false);
|
|
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
{ // can't specify an ephemeral signature
|
|
auto const st = buildManifestObject(
|
|
std::numeric_limits<std::uint32_t>::max(), std::nullopt, false, true);
|
|
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
{ // can't specify an ephemeral key & signature
|
|
auto const st = buildManifestObject(
|
|
std::numeric_limits<std::uint32_t>::max(), std::nullopt, false, false);
|
|
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testManifestDomainNames()
|
|
{
|
|
testcase("Manifest Domain Names");
|
|
|
|
auto const sk1 = generateSecretKey(KeyType::Secp256k1, randomSeed());
|
|
auto const pk1 = derivePublicKey(KeyType::Secp256k1, sk1);
|
|
|
|
auto const sk2 = generateSecretKey(KeyType::Secp256k1, randomSeed());
|
|
auto const pk2 = derivePublicKey(KeyType::Secp256k1, sk2);
|
|
|
|
auto test = [&](std::string domain) {
|
|
STObject st(sfGeneric);
|
|
st[sfSequence] = 7;
|
|
st[sfPublicKey] = pk1;
|
|
st[sfDomain] = makeSlice(domain);
|
|
st[sfSigningPubKey] = pk2;
|
|
|
|
sign(st, HashPrefix::Manifest, KeyType::Secp256k1, sk1, sfMasterSignature);
|
|
sign(st, HashPrefix::Manifest, KeyType::Secp256k1, sk2);
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return deserializeManifest(std::string(static_cast<char const*>(s.data()), s.size()));
|
|
};
|
|
|
|
BEAST_EXPECT(test("example.com"));
|
|
BEAST_EXPECT(test("test.example.com"));
|
|
BEAST_EXPECT(test("example-domain.com"));
|
|
BEAST_EXPECT(test("xn--mxavchb.gr"));
|
|
BEAST_EXPECT(test("test.xn--mxavchb.gr"));
|
|
BEAST_EXPECT(test("123.gr"));
|
|
BEAST_EXPECT(test("x.yz"));
|
|
BEAST_EXPECT(test(std::string(63, 'a') + ".example.com"));
|
|
BEAST_EXPECT(test(std::string(63, 'a') + "." + std::string(63, 'b')));
|
|
|
|
// No period
|
|
BEAST_EXPECT(!test("example"));
|
|
|
|
// Leading period:
|
|
BEAST_EXPECT(!test(".com"));
|
|
BEAST_EXPECT(!test(".example.com"));
|
|
|
|
// A trailing period is technically valid but we don't allow it
|
|
BEAST_EXPECT(!test("example.com."));
|
|
|
|
// A component can't start or end with a dash
|
|
BEAST_EXPECT(!test("-example.com"));
|
|
BEAST_EXPECT(!test("example-.com"));
|
|
|
|
// Empty component:
|
|
BEAST_EXPECT(!test("double..periods.example.com"));
|
|
|
|
// TLD too short or too long:
|
|
BEAST_EXPECT(!test("example.x"));
|
|
BEAST_EXPECT(!test("example." + std::string(64, 'a')));
|
|
|
|
// Invalid characters:
|
|
BEAST_EXPECT(!test("example.com-org"));
|
|
BEAST_EXPECT(!test("bang!.com"));
|
|
BEAST_EXPECT(!test("bang!.example.com"));
|
|
|
|
// Too short
|
|
BEAST_EXPECT(!test("a.b"));
|
|
|
|
// Single component too long:
|
|
BEAST_EXPECT(!test(std::string(64, 'a') + ".com"));
|
|
BEAST_EXPECT(!test(std::string(64, 'a') + ".example.com"));
|
|
|
|
// Multiple components too long:
|
|
BEAST_EXPECT(!test(std::string(64, 'a') + "." + std::string(64, 'b')));
|
|
BEAST_EXPECT(!test(std::string(64, 'a') + "." + std::string(64, 'b')));
|
|
|
|
// Overall too long:
|
|
BEAST_EXPECT(!test(std::string(63, 'a') + "." + std::string(63, 'b') + ".example.com"));
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
ManifestCache cache;
|
|
{
|
|
testcase("apply");
|
|
|
|
auto const skA = randomSecretKey();
|
|
auto const pkA = derivePublicKey(KeyType::Ed25519, skA);
|
|
auto const kpA0 = randomKeyPair(KeyType::Secp256k1);
|
|
auto const kpA1 = randomKeyPair(KeyType::Secp256k1);
|
|
auto const sA0 =
|
|
makeManifest(skA, KeyType::Ed25519, kpA0.second, KeyType::Secp256k1, 0);
|
|
auto const sA1 =
|
|
makeManifest(skA, KeyType::Ed25519, kpA1.second, KeyType::Secp256k1, 1);
|
|
auto const sA2 =
|
|
makeManifest(skA, KeyType::Ed25519, kpA1.second, KeyType::Secp256k1, 2);
|
|
auto const sAMax = makeRevocation(skA, KeyType::Ed25519);
|
|
|
|
auto const skB = randomSecretKey();
|
|
auto const kpB0 = randomKeyPair(KeyType::Secp256k1);
|
|
auto const kpB1 = randomKeyPair(KeyType::Secp256k1);
|
|
auto const kpB2 = randomKeyPair(KeyType::Secp256k1);
|
|
auto const sB0 =
|
|
makeManifest(skB, KeyType::Ed25519, kpB0.second, KeyType::Secp256k1, 0);
|
|
auto const sB1 = makeManifest(
|
|
skB,
|
|
KeyType::Ed25519,
|
|
kpB1.second,
|
|
KeyType::Secp256k1,
|
|
1,
|
|
true); // invalidSig
|
|
auto const sB2 = makeManifest(skB, KeyType::Ed25519, kpB2.second, KeyType::Ed25519, 2);
|
|
|
|
auto const fake = sB2.serialized + '\0';
|
|
|
|
// applyManifest should accept new manifests with
|
|
// higher sequence numbers
|
|
auto const seq0 = cache.sequence();
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Accepted);
|
|
BEAST_EXPECT(cache.sequence() > seq0);
|
|
|
|
auto const seq1 = cache.sequence();
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale);
|
|
BEAST_EXPECT(cache.sequence() == seq1);
|
|
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Accepted);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale);
|
|
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA2)) == ManifestDisposition::BadEphemeralKey);
|
|
|
|
// applyManifest should accept manifests with max sequence numbers
|
|
// that revoke the master public key
|
|
BEAST_EXPECT(!cache.revoked(pkA));
|
|
BEAST_EXPECT(sAMax.revoked());
|
|
BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Accepted);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sAMax)) == ManifestDisposition::Stale);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA1)) == ManifestDisposition::Stale);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sA0)) == ManifestDisposition::Stale);
|
|
BEAST_EXPECT(cache.revoked(pkA));
|
|
|
|
// applyManifest should reject manifests with invalid signatures
|
|
BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Accepted);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sB0)) == ManifestDisposition::Stale);
|
|
BEAST_EXPECT(!deserializeManifest(fake));
|
|
BEAST_EXPECT(cache.applyManifest(clone(sB1)) == ManifestDisposition::Invalid);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sB2)) == ManifestDisposition::Accepted);
|
|
|
|
auto const sC0 = makeManifest(
|
|
kpB2.second, KeyType::Ed25519, randomSecretKey(), KeyType::Ed25519, 47);
|
|
BEAST_EXPECT(cache.applyManifest(clone(sC0)) == ManifestDisposition::BadMasterKey);
|
|
}
|
|
|
|
testLoadStore(cache);
|
|
testGetSignature();
|
|
testGetKeys();
|
|
testValidatorToken();
|
|
testManifestDeserialization();
|
|
testManifestDomainNames();
|
|
testManifestVersioning();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Manifest, app, xrpl);
|
|
|
|
} // namespace xrpl::test
|