Implement paychannel verify/authorize

This commit is contained in:
Nathan Nichols
2021-05-20 08:08:46 -07:00
committed by GitHub
parent 8ee213f6bc
commit 1de29d3a44
7 changed files with 411 additions and 18 deletions

View File

@@ -76,6 +76,8 @@ target_sources(reporting PRIVATE
handlers/AccountCurrencies.cpp
handlers/AccountOffers.cpp
handlers/AccountObjects.cpp)
handlers/ChannelAuthorize.cpp
handlers/ChannelVerify.cpp)
message(${Boost_LIBRARIES})

View File

@@ -0,0 +1,109 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2014 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/PayChan.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/jss.h>
#include <ripple/resource/Fees.h>
#include <handlers/RPCHelpers.h>
#include <optional>
void
serializePayChanAuthorization(
ripple::Serializer& msg,
ripple::uint256 const& key,
ripple::XRPAmount const& amt)
{
msg.add32(ripple::HashPrefix::paymentChannelClaim);
msg.addBitString(key);
msg.add64(amt.drops());
}
boost::json::object
doChannelAuthorize(boost::json::object const& request)
{
boost::json::object response;
if(!request.contains("channel_id"))
{
response["error"] = "missing field channel_id";
return response;
}
if(!request.contains("amount"))
{
response["error"] = "missing field amount";
return response;
}
if (!request.contains("key_type") && !request.contains("secret"))
{
response["error"] = "missing field secret";
return response;
}
boost::json::value error = nullptr;
auto const [pk, sk] = keypairFromRequst(request, error);
if (!error.is_null())
{
response["error"] = error;
return response;
}
ripple::uint256 channelId;
if (!channelId.parseHex(request.at("channel_id").as_string().c_str()))
{
response["error"] = "channel id malformed";
return response;
}
if (!request.at("amount").is_string())
{
response["error"] = "channel amount malformed";
return response;
}
auto optDrops =
ripple::to_uint64(request.at("amount").as_string().c_str());
if (!optDrops)
{
response["error"] = "could not parse channel amount";
return response;
}
std::uint64_t drops = *optDrops;
ripple::Serializer msg;
ripple::serializePayChanAuthorization(msg, channelId, ripple::XRPAmount(drops));
try
{
auto const buf = ripple::sign(pk, sk, msg.slice());
response["signature"] = ripple::strHex(buf);
}
catch (std::exception&)
{
response["error"] = "Exception occurred during signing.";
return response;
}
return response;
}

119
handlers/ChannelVerify.cpp Normal file
View File

@@ -0,0 +1,119 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012-2014 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/ErrorCodes.h>
#include <ripple/protocol/PayChan.h>
#include <ripple/protocol/STAccount.h>
#include <ripple/protocol/jss.h>
#include <ripple/resource/Fees.h>
#include <handlers/RPCHelpers.h>
#include <optional>
boost::json::object
doChannelVerify(boost::json::object const& request)
{
boost::json::object response;
if(!request.contains("channel_id"))
{
response["error"] = "missing field channel_id";
return response;
}
if(!request.contains("amount"))
{
response["error"] = "missing field amount";
return response;
}
if (!request.contains("signature"))
{
response["error"] = "missing field signature";
return response;
}
if (!request.contains("public_key"))
{
response["error"] = "missing field public_key";
return response;
}
boost::optional<ripple::PublicKey> pk;
{
std::string const strPk = request.at("public_key").as_string().c_str();
pk = ripple::parseBase58<ripple::PublicKey>(ripple::TokenType::AccountPublic, strPk);
if (!pk)
{
auto pkHex = ripple::strUnHex(strPk);
if (!pkHex)
{
response["error"] = "malformed public key";
return response;
}
auto const pkType = ripple::publicKeyType(ripple::makeSlice(*pkHex));
if (!pkType)
{
response["error"] = "invalid key type";
}
pk.emplace(ripple::makeSlice(*pkHex));
}
}
ripple::uint256 channelId;
if (!channelId.parseHex(request.at("channel_id").as_string().c_str()))
{
response["error"] = "channel id malformed";
return response;
}
auto optDrops =
ripple::to_uint64(request.at("amount").as_string().c_str());
if (!optDrops)
{
response["error"] = "could not parse channel amount";
return response;
}
std::uint64_t drops = *optDrops;
if (!request.at("signature").is_string())
{
response["error"] = "signature must be type string";
return response;
}
auto sig = ripple::strUnHex(request.at("signature").as_string().c_str());
if (!sig || !sig->size())
{
response["error"] = "Invalid signature";
return response;
}
ripple::Serializer msg;
ripple::serializePayChanAuthorization(msg, channelId, ripple::XRPAmount(drops));
response["signature_verified"] =
ripple::verify(*pk, msg.slice(), ripple::makeSlice(*sig), true);
return response;
}

View File

@@ -174,3 +174,163 @@ traverseOwnedNodes(
return nextCursor;
}
boost::optional<ripple::Seed>
parseRippleLibSeed(boost::json::value const& value)
{
// ripple-lib encodes seed used to generate an Ed25519 wallet in a
// non-standard way. While rippled never encode seeds that way, we
// try to detect such keys to avoid user confusion.
if (!value.is_string())
return {};
auto const result =
ripple::decodeBase58Token(value.as_string().c_str(), ripple::TokenType::None);
if (result.size() == 18 &&
static_cast<std::uint8_t>(result[0]) == std::uint8_t(0xE1) &&
static_cast<std::uint8_t>(result[1]) == std::uint8_t(0x4B))
return ripple::Seed(ripple::makeSlice(result.substr(2)));
return {};
}
std::pair<ripple::PublicKey, ripple::SecretKey>
keypairFromRequst(boost::json::object const& request, boost::json::value& error)
{
bool const has_key_type = request.contains("key_type");
// All of the secret types we allow, but only one at a time.
// The array should be constexpr, but that makes Visual Studio unhappy.
static std::string const secretTypes[]{
"passphrase",
"secret",
"seed",
"seed_hex"};
// Identify which secret type is in use.
std::string secretType = "";
int count = 0;
for (auto t : secretTypes)
{
if (request.contains(t))
{
++count;
secretType = t;
}
}
if (count == 0)
{
error = "missing field secret";
return {};
}
if (count > 1)
{
error = "Exactly one of the following must be specified: "
" passphrase, secret, seed, or seed_hex";
return {};
}
boost::optional<ripple::KeyType> keyType;
boost::optional<ripple::Seed> seed;
if (has_key_type)
{
if (!request.at("key_type").is_string())
{
error = "key_type must be string";
return {};
}
std::string key_type = request.at("key_type").as_string().c_str();
keyType = ripple::keyTypeFromString(key_type);
if (!keyType)
{
error = "Invalid field key_type";
return {};
}
if (secretType == "secret")
{
error = "The secret field is not allowed if key_type is used.";
return {};
}
}
// ripple-lib encodes seed used to generate an Ed25519 wallet in a
// non-standard way. While we never encode seeds that way, we try
// to detect such keys to avoid user confusion.
if (secretType != "seed_hex")
{
seed = parseRippleLibSeed(request.at(secretType));
if (seed)
{
// If the user passed in an Ed25519 seed but *explicitly*
// requested another key type, return an error.
if (keyType.value_or(ripple::KeyType::ed25519) != ripple::KeyType::ed25519)
{
error = "Specified seed is for an Ed25519 wallet.";
return {};
}
keyType = ripple::KeyType::ed25519;
}
}
if (!keyType)
keyType = ripple::KeyType::secp256k1;
if (!seed)
{
if (has_key_type)
{
if (!request.at(secretType).is_string())
{
error = "secret value must be string";
return {};
}
std::string key = request.at(secretType).as_string().c_str();
if (secretType == "seed")
seed = ripple::parseBase58<ripple::Seed>(key);
else if (secretType == "passphrase")
seed = ripple::parseGenericSeed(key);
else if (secretType == "seed_hex")
{
ripple::uint128 s;
if (s.parseHex(key))
seed.emplace(ripple::Slice(s.data(), s.size()));
}
}
else
{
if (!request.at("secret").is_string())
{
error = "field secret should be a string";
return {};
}
std::string secret = request.at("secret").as_string().c_str();
seed = ripple::parseGenericSeed(secret);
}
}
if (!seed)
{
error = "Bad Seed: invalid field message secretType";
return {};
}
if (keyType != ripple::KeyType::secp256k1
&& keyType != ripple::KeyType::ed25519)
{
error = "keypairForSignature: invalid key type";
return {};
}
return generateKeyPair(*keyType, *seed);
}

View File

@@ -6,6 +6,7 @@
#include <ripple/protocol/STTx.h>
#include <boost/json.hpp>
#include <reporting/BackendInterface.h>
std::optional<ripple::AccountID>
accountFromStringStrict(std::string const& account);
@@ -32,5 +33,9 @@ traverseOwnedNodes(
std::uint32_t sequence,
ripple::uint256 const& cursor,
std::function<bool(ripple::SLE)> atOwnedNode);
std::pair<ripple::PublicKey, ripple::SecretKey>
keypairFromRequst(
boost::json::object const& request,
boost::json::value& error);
#endif

View File

@@ -1346,9 +1346,9 @@ CassandraBackend::open(bool readOnly)
continue;
query.str("");
query << "CREATE TABLE IF NOT EXISTS " << tablePrefix << "transactions"
<< " ( hash blob PRIMARY KEY, ledger_sequence bigint, "
"transaction "
query
<< "CREATE TABLE IF NOT EXISTS " << tablePrefix << "transactions"
<< " ( hash blob PRIMARY KEY, ledger_sequence bigint, transaction "
"blob, metadata blob)";
if (!executeSimpleStatement(query.str()))
continue;
@@ -1383,7 +1383,6 @@ CassandraBackend::open(bool readOnly)
<< " LIMIT 1";
if (!executeSimpleStatement(query.str()))
continue;
query.str("");
query << "CREATE TABLE IF NOT EXISTS " << tablePrefix << "books"
<< " ( book blob, sequence bigint, quality_key tuple<blob, blob>, PRIMARY KEY "

View File

@@ -49,6 +49,8 @@ enum RPCCommand {
account_currencies,
account_offers,
account_objects
channel_authorize,
channel_verify
};
std::unordered_map<std::string, RPCCommand> commandMap{
{"tx", tx},
@@ -63,7 +65,9 @@ std::unordered_map<std::string, RPCCommand> commandMap{
{"account_lines", account_lines},
{"account_currencies", account_currencies},
{"account_offers", account_offers},
{"account_objects", account_objects}};
{"account_objects", account_objects},
{"channel_authorize", channel_authorize},
{"channel_verify", channel_verify}};
boost::json::object
doAccountInfo(
@@ -113,6 +117,9 @@ boost::json::object
doAccountObjects(
boost::json::object const& request,
BackendInterface const& backend);
doChannelAuthorize(boost::json::object const& request);
boost::json::object
doChannelVerify(boost::json::object const& request);
boost::json::object
buildResponse(
@@ -126,42 +133,34 @@ buildResponse(
{
case tx:
return doTx(request, backend);
break;
case account_tx:
return doAccountTx(request, backend);
break;
case ledger:
return doLedger(request, backend);
break;
case ledger_entry:
return doLedgerEntry(request, backend);
break;
case ledger_range:
return doLedgerRange(request, backend);
break;
case ledger_data:
return doLedgerData(request, backend);
break;
case account_info:
return doAccountInfo(request, backend);
break;
case book_offers:
return doBookOffers(request, backend);
break;
case account_channels:
return doAccountChannels(request, backend);
break;
case account_lines:
return doAccountLines(request, backend);
break;
case account_currencies:
return doAccountCurrencies(request, backend);
break;
case account_offers:
return doAccountOffers(request, backend);
break;
case account_objects:
return doAccountObjects(request, backend);
case channel_authorize:
return doChannelAuthorize(request);
case channel_verify:
return doChannelVerify(request);
break;
default:
BOOST_LOG_TRIVIAL(error) << "Unknown command: " << command;