mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-09 11:46:49 +00:00
Per XLS-0095, we are taking steps to rename ripple(d) to xrpl(d). This change specifically removes all copyright notices referencing Ripple, XRPLF, and certain affiliated contributors upon mutual agreement, so the notice in the LICENSE.md file applies throughout. Copyright notices referencing external contributions remain as-is. Duplicate verbiage is also removed.
1073 lines
38 KiB
C++
1073 lines
38 KiB
C++
#include <test/jtx.h>
|
|
|
|
#include <xrpld/app/main/DBInit.h>
|
|
#include <xrpld/app/misc/Manifest.h>
|
|
#include <xrpld/app/misc/ValidatorList.h>
|
|
#include <xrpld/app/rdb/Wallet.h>
|
|
|
|
#include <xrpl/basics/base64.h>
|
|
#include <xrpl/basics/contract.h>
|
|
#include <xrpl/protocol/STExchange.h>
|
|
#include <xrpl/protocol/SecretKey.h>
|
|
#include <xrpl/protocol/Sign.h>
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/utility/in_place_factory.hpp>
|
|
|
|
namespace ripple {
|
|
namespace 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&)
|
|
{
|
|
}
|
|
}
|
|
~Manifest_test()
|
|
{
|
|
try
|
|
{
|
|
cleanupDatabaseDir(getDatabasePath());
|
|
}
|
|
catch (std::exception const&)
|
|
{
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk);
|
|
sign(
|
|
st,
|
|
HashPrefix::manifest,
|
|
*publicKeyType(pk),
|
|
sk,
|
|
sfMasterSignature);
|
|
|
|
Serializer s;
|
|
st.add(s);
|
|
|
|
return base64_encode(
|
|
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 base64_encode(
|
|
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);
|
|
|
|
// m is non-const so it can be moved from
|
|
std::string m(static_cast<char const*>(s.data()), s.size());
|
|
if (auto r = deserializeManifest(std::move(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 m(
|
|
static_cast<char const*>(s.data()),
|
|
s.size()); // non-const so can be moved
|
|
if (auto r = deserializeManifest(std::move(m)))
|
|
return std::move(*r);
|
|
Throw<std::runtime_error>("Could not create a manifest");
|
|
return *deserializeManifest(
|
|
std::string{}); // Silence compiler warning.
|
|
}
|
|
|
|
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.for_each_manifest(
|
|
[&result](Manifest const& man) { result.push_back(&man); });
|
|
return result;
|
|
};
|
|
auto sort = [](std::vector<Manifest const*> mv)
|
|
-> std::vector<Manifest const*> {
|
|
std::sort(
|
|
mv.begin(),
|
|
mv.end(),
|
|
[](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> keys;
|
|
std::string cfgManifest;
|
|
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()));
|
|
|
|
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 = {
|
|
" "
|
|
"eyJ2YWxpZGF0aW9uX3NlY3JldF9rZXkiOiI5ZWQ0NWY4NjYyNDFjYzE4YTI3ND"
|
|
"diNT\n",
|
|
" \tQzODdjMDYyNTkwNzk3MmY0ZTcxOTAyMzFmYWE5Mzc0NTdmYTlkYWY2Iiwib"
|
|
"WFuaWZl \n",
|
|
"\tc3QiOiJKQUFBQUFGeEllMUZ0d21pbXZHdEgyaUNjTUpxQzlnVkZLaWxHZncx"
|
|
"L3ZDeE\n",
|
|
"\t "
|
|
"hYWExwbGMyR25NaEFrRTFhZ3FYeEJ3RHdEYklENk9NU1l1TTBGREFscEFnTms4"
|
|
"U0tG\t \t\n",
|
|
"bjdNTzJmZGtjd1JRSWhBT25ndTlzQUtxWFlvdUorbDJWMFcrc0FPa1ZCK1pSUz"
|
|
"ZQU2\n",
|
|
"hsSkFmVXNYZkFpQnNWSkdlc2FhZE9KYy9hQVpva1MxdnltR21WcmxIUEtXWDNZ"
|
|
"eXd1\n",
|
|
"NmluOEhBU1FLUHVnQkQ2N2tNYVJGR3ZtcEFUSGxHS0pkdkRGbFdQWXk1QXFEZW"
|
|
"RGdj\n",
|
|
"VUSmEydzBpMjFlcTNNWXl3TFZKWm5GT3I3QzBrdzJBaVR6U0NqSXpkaXRROD0i"
|
|
"fQ==\n"};
|
|
|
|
auto const manifest =
|
|
"JAAAAAFxIe1FtwmimvGtH2iCcMJqC9gVFKilGfw1/"
|
|
"vCxHXXLplc2GnMhAkE1agqXxBwD"
|
|
"wDbID6OMSYuM0FDAlpAgNk8SKFn7MO2fdkcwRQIhAOngu9sAKqXYouJ+l2V0W+"
|
|
"sAOkVB"
|
|
"+ZRS6PShlJAfUsXfAiBsVJGesaadOJc/"
|
|
"aAZokS1vymGmVrlHPKWX3Yywu6in8HASQKPu"
|
|
"gBD67kMaRFGvmpATHlGKJdvDFlWPYy5AqDedFv5TJa2w0i21eq3MYywLVJZnFO"
|
|
"r7C0kw"
|
|
"2AiTzSCjIzditQ8=";
|
|
|
|
auto const token = loadValidatorToken(tokenBlob);
|
|
BEAST_EXPECT(token);
|
|
BEAST_EXPECT(token->validationSecret == *valSecret);
|
|
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);
|
|
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());
|
|
}
|
|
|
|
{ // 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 s(254, 'a');
|
|
auto const st =
|
|
buildManifestObject(++sequence, s + ".example.com");
|
|
BEAST_EXPECT(!deserializeManifest(toString(st)));
|
|
}
|
|
{ // invalid manifest (domain component too long)
|
|
std::string 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);
|
|
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());
|
|
}
|
|
{
|
|
// 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);
|
|
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());
|
|
}
|
|
{
|
|
// 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);
|
|
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());
|
|
}
|
|
|
|
{ // 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 sk_a = randomSecretKey();
|
|
auto const pk_a = derivePublicKey(KeyType::ed25519, sk_a);
|
|
auto const kp_a0 = randomKeyPair(KeyType::secp256k1);
|
|
auto const kp_a1 = randomKeyPair(KeyType::secp256k1);
|
|
auto const s_a0 = makeManifest(
|
|
sk_a, KeyType::ed25519, kp_a0.second, KeyType::secp256k1, 0);
|
|
auto const s_a1 = makeManifest(
|
|
sk_a, KeyType::ed25519, kp_a1.second, KeyType::secp256k1, 1);
|
|
auto const s_a2 = makeManifest(
|
|
sk_a, KeyType::ed25519, kp_a1.second, KeyType::secp256k1, 2);
|
|
auto const s_aMax = makeRevocation(sk_a, KeyType::ed25519);
|
|
|
|
auto const sk_b = randomSecretKey();
|
|
auto const kp_b0 = randomKeyPair(KeyType::secp256k1);
|
|
auto const kp_b1 = randomKeyPair(KeyType::secp256k1);
|
|
auto const kp_b2 = randomKeyPair(KeyType::secp256k1);
|
|
auto const s_b0 = makeManifest(
|
|
sk_b, KeyType::ed25519, kp_b0.second, KeyType::secp256k1, 0);
|
|
auto const s_b1 = makeManifest(
|
|
sk_b,
|
|
KeyType::ed25519,
|
|
kp_b1.second,
|
|
KeyType::secp256k1,
|
|
1,
|
|
true); // invalidSig
|
|
auto const s_b2 = makeManifest(
|
|
sk_b, KeyType::ed25519, kp_b2.second, KeyType::ed25519, 2);
|
|
|
|
auto const fake = s_b2.serialized + '\0';
|
|
|
|
// applyManifest should accept new manifests with
|
|
// higher sequence numbers
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a0)) ==
|
|
ManifestDisposition::accepted);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a0)) == ManifestDisposition::stale);
|
|
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a1)) ==
|
|
ManifestDisposition::accepted);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a1)) == ManifestDisposition::stale);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a0)) == ManifestDisposition::stale);
|
|
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a2)) ==
|
|
ManifestDisposition::badEphemeralKey);
|
|
|
|
// applyManifest should accept manifests with max sequence numbers
|
|
// that revoke the master public key
|
|
BEAST_EXPECT(!cache.revoked(pk_a));
|
|
BEAST_EXPECT(s_aMax.revoked());
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_aMax)) ==
|
|
ManifestDisposition::accepted);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_aMax)) ==
|
|
ManifestDisposition::stale);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a1)) == ManifestDisposition::stale);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_a0)) == ManifestDisposition::stale);
|
|
BEAST_EXPECT(cache.revoked(pk_a));
|
|
|
|
// applyManifest should reject manifests with invalid signatures
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_b0)) ==
|
|
ManifestDisposition::accepted);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_b0)) == ManifestDisposition::stale);
|
|
BEAST_EXPECT(!deserializeManifest(fake));
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_b1)) ==
|
|
ManifestDisposition::invalid);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_b2)) ==
|
|
ManifestDisposition::accepted);
|
|
|
|
auto const s_c0 = makeManifest(
|
|
kp_b2.second,
|
|
KeyType::ed25519,
|
|
randomSecretKey(),
|
|
KeyType::ed25519,
|
|
47);
|
|
BEAST_EXPECT(
|
|
cache.applyManifest(clone(s_c0)) ==
|
|
ManifestDisposition::badMasterKey);
|
|
}
|
|
|
|
testLoadStore(cache);
|
|
testGetSignature();
|
|
testGetKeys();
|
|
testValidatorToken();
|
|
testManifestDeserialization();
|
|
testManifestDomainNames();
|
|
testManifestVersioning();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Manifest, app, ripple);
|
|
|
|
} // namespace test
|
|
} // namespace ripple
|