From 1de29d3a447fa88e02a3374d061993730aee8a92 Mon Sep 17 00:00:00 2001 From: Nathan Nichols Date: Thu, 20 May 2021 08:08:46 -0700 Subject: [PATCH] Implement paychannel verify/authorize --- CMakeLists.txt | 2 + handlers/ChannelAuthorize.cpp | 109 ++++++++++++++++++++++ handlers/ChannelVerify.cpp | 119 ++++++++++++++++++++++++ handlers/RPCHelpers.cpp | 160 +++++++++++++++++++++++++++++++++ handlers/RPCHelpers.h | 5 ++ reporting/CassandraBackend.cpp | 9 +- websocket_server_async.cpp | 25 +++--- 7 files changed, 411 insertions(+), 18 deletions(-) create mode 100644 handlers/ChannelAuthorize.cpp create mode 100644 handlers/ChannelVerify.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e37774b..a29e0ee2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/handlers/ChannelAuthorize.cpp b/handlers/ChannelAuthorize.cpp new file mode 100644 index 00000000..ca57f7eb --- /dev/null +++ b/handlers/ChannelAuthorize.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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; +} \ No newline at end of file diff --git a/handlers/ChannelVerify.cpp b/handlers/ChannelVerify.cpp new file mode 100644 index 00000000..c3cad63e --- /dev/null +++ b/handlers/ChannelVerify.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 pk; + { + std::string const strPk = request.at("public_key").as_string().c_str(); + pk = ripple::parseBase58(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; +} \ No newline at end of file diff --git a/handlers/RPCHelpers.cpp b/handlers/RPCHelpers.cpp index 0d891d85..abddbd7e 100644 --- a/handlers/RPCHelpers.cpp +++ b/handlers/RPCHelpers.cpp @@ -174,3 +174,163 @@ traverseOwnedNodes( return nextCursor; } +boost::optional +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(result[0]) == std::uint8_t(0xE1) && + static_cast(result[1]) == std::uint8_t(0x4B)) + return ripple::Seed(ripple::makeSlice(result.substr(2))); + + return {}; +} + +std::pair +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 keyType; + boost::optional 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(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); +} diff --git a/handlers/RPCHelpers.h b/handlers/RPCHelpers.h index 319faf6c..22c0a19a 100644 --- a/handlers/RPCHelpers.h +++ b/handlers/RPCHelpers.h @@ -6,6 +6,7 @@ #include #include #include + std::optional accountFromStringStrict(std::string const& account); @@ -32,5 +33,9 @@ traverseOwnedNodes( std::uint32_t sequence, ripple::uint256 const& cursor, std::function atOwnedNode); +std::pair +keypairFromRequst( + boost::json::object const& request, + boost::json::value& error); #endif diff --git a/reporting/CassandraBackend.cpp b/reporting/CassandraBackend.cpp index e4f79e37..3a48f253 100644 --- a/reporting/CassandraBackend.cpp +++ b/reporting/CassandraBackend.cpp @@ -1346,10 +1346,10 @@ CassandraBackend::open(bool readOnly) continue; query.str(""); - query << "CREATE TABLE IF NOT EXISTS " << tablePrefix << "transactions" - << " ( hash blob PRIMARY KEY, ledger_sequence bigint, " - "transaction " - "blob, metadata blob)"; + 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, PRIMARY KEY " diff --git a/websocket_server_async.cpp b/websocket_server_async.cpp index 1d6e69b7..f4a5cccf 100644 --- a/websocket_server_async.cpp +++ b/websocket_server_async.cpp @@ -49,6 +49,8 @@ enum RPCCommand { account_currencies, account_offers, account_objects + channel_authorize, + channel_verify }; std::unordered_map commandMap{ {"tx", tx}, @@ -63,7 +65,9 @@ std::unordered_map 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;