diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 087a6e549d9..4681b420f2b 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -94,6 +94,7 @@ JSS(Fee); // in/out: TransactionSign; field. JSS(FeeSettings); // ledger type. JSS(Flags); // in/out: TransactionSign; field. JSS(Invalid); // +JSS(Issuer); // in: Credential transactions JSS(LastLedgerSequence); // in: TransactionSign; field JSS(LastUpdateTime); // field. JSS(LedgerHashes); // ledger type. @@ -145,6 +146,7 @@ JSS(Signature); // in: Credential transactions JSS(SignerList); // ledger type. JSS(SignerListSet); // transaction type. JSS(SigningPubKey); // field. +JSS(Subject); // in: Credential transactions JSS(TakerGets); // field. JSS(TakerPays); // field. JSS(Ticket); // ledger type. diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 5fbb0ba38b1..442eec3f9ca 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -912,6 +912,116 @@ class AccountDelete_test : public beast::unit_test::suite env.close(); } + void + testDestCreds() + { + { + testcase("Destination Constraints with Credentials"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const carol{"carol"}; + + const char credType[] = "abcd"; + + Env env{*this}; + env.fund(XRP(100000), alice, becky, carol); + env.close(); + + // carol issue credentials for becky + env(credentials::create(becky, carol, credType)); + env.close(); + // becky accept the credentials + env(credentials::accept(becky, carol, credType)); + env.close(); + // get credentials index + auto const jCred = + credentials::ledgerEntryCredential(env, becky, carol, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + // alice sets the lsfDepositAuth flag on her account. This should + // prevent becky from deleting her account while using alice as the + // destination. + env(fset(alice, asfDepositAuth), fee(drops(10))); + env.close(); + // and create DepositPreauth Object + env(deposit::auth( + alice, + std::vector{{carol, credType}})); + env.close(); + + // Close enough ledgers to be able to delete becky's account. + incLgrSeqForAccDel(env, becky); + + auto const acctDelFee{drops(env.current()->fees().increment)}; + + // becky attempts to delete her account, but alice won't take her + // XRP, so the delete is blocked. + env(acctdelete(becky, alice), + fee(acctDelFee), + ter(tecNO_PERMISSION)); + env.close(); + + // becky use bad credentials and can't delete account + env(acctdelete( + becky, + alice, + {"48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6E" + "A288BE4"}), + fee(acctDelFee), + ter(temBAD_CREDENTIALS)); + env.close(); + + // becky use credentials and can delete account + env(acctdelete(becky, alice, {credIdx}), fee(acctDelFee)); + env.close(); + + // check that credential object deleted too + auto const jNoCred = + credentials::ledgerEntryCredential(env, becky, carol, credType); + BEAST_EXPECT( + jNoCred.isObject() && jNoCred.isMember(jss::result) && + jNoCred[jss::result].isMember(jss::error)); + } + + { + testcase("Credentials feature disabled"); + using namespace test::jtx; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const carol{"carol"}; + + Env env{*this, supported_amendments() - featureCredentials}; + env.fund(XRP(100000), alice, becky, carol); + env.close(); + + // alice sets the lsfDepositAuth flag on her account. This should + // prevent becky from deleting her account while using alice as the + // destination. + env(fset(alice, asfDepositAuth), fee(drops(10))); + env.close(); + + // Close enough ledgers to be able to delete becky's account. + incLgrSeqForAccDel(env, becky); + + auto const acctDelFee{drops(env.current()->fees().increment)}; + + // becky can't delete account as feature disabled + env(acctdelete( + becky, + alice, + {"098B7F1B146470A1C5084DC7832C04A72939E3EBC58E68AB8B579BA07" + "2B0CECB"}), + fee(acctDelFee), + ter(temDISABLED)); + env.close(); + } + } + void run() override { @@ -925,6 +1035,7 @@ class AccountDelete_test : public beast::unit_test::suite testBalanceTooSmallForFee(); testWithTickets(); testDest(); + testDestCreds(); } }; diff --git a/src/test/app/Credentials_test.cpp b/src/test/app/Credentials_test.cpp new file mode 100644 index 00000000000..612b02044af --- /dev/null +++ b/src/test/app/Credentials_test.cpp @@ -0,0 +1,651 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 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 + +#include +#include + +namespace ripple { +namespace test { + +// Helper function that returns the owner count of an account root. +static inline std::uint32_t +ownerCnt(test::jtx::Env const& env, test::jtx::Account const& acct) +{ + std::uint32_t ret{0}; + if (auto const sleAcct = env.le(acct)) + ret = sleAcct->at(sfOwnerCount); + return ret; +} + +static inline bool +checkVL( + std::shared_ptr const& sle, + SField const& field, + std::string const& expected) +{ + auto const b = sle->getFieldVL(field); + return strHex(expected) == strHex(b); +} + +static inline Keylet +credKL( + test::jtx::Account const& subj, + test::jtx::Account const& iss, + std::string_view credType) +{ + return keylet::credential( + subj.id(), iss.id(), Slice(credType.data(), credType.size())); +} + +struct Credentials_test : public beast::unit_test::suite +{ + void + testSuccessful(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + const char uri[] = "uri"; + + { + testcase("Credentials from issuing side."); + + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + auto const kCred = credKL(subj, iss, credType); + + env.fund(XRP(5000), subj, iss); + env.close(); + + // Test Create credentials + env(credentials::create(subj, iss, credType)); + env.close(); + { + BEAST_EXPECT(ownerCnt(env, iss) == 1); + auto const sleCred = env.le(kCred); + BEAST_EXPECT(static_cast(sleCred)); + if (!sleCred) + return; + BEAST_EXPECT(!(sleCred->getFieldU32(sfFlags) & lsfAccepted)); + BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + auto const jle = credentials::ledgerEntryCredential( + env, subj, iss, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential); + } + + env(credentials::accept(subj, iss, credType)); + env.close(); + { + // check switching owner of the credentials from isser to + // subject + BEAST_EXPECT(ownerCnt(env, subj) == 1); + auto const sleCred = env.le(kCred); + BEAST_EXPECT(static_cast(sleCred)); + if (!sleCred) + return; + BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + BEAST_EXPECT(sleCred->getFieldU32(sfFlags) == lsfAccepted); + } + + env(credentials::del(subj, subj, iss, credType)); + env.close(); + { + BEAST_EXPECT(!env.le(kCred)); + BEAST_EXPECT(!ownerCnt(env, iss)); + BEAST_EXPECT(!ownerCnt(env, subj)); + + // check no credential exists anymore + auto const jle = credentials::ledgerEntryCredential( + env, subj, iss, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + jle[jss::result].isMember(jss::error)); + } + } + + { + testcase("Credentials from subject side."); + + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + auto const kCred = credKL(subj, iss, credType); + + env.fund(XRP(5000), subj, iss); + env.close(); + + // Test Create credentials + env(credentials::create(subj, iss, credType, false)); + env.close(); + { + BEAST_EXPECT(ownerCnt(env, subj) == 1); + auto const sleCred = env.le(kCred); + BEAST_EXPECT(static_cast(sleCred)); + if (!sleCred) + return; + BEAST_EXPECT(sleCred->getFieldU32(sfFlags) == lsfAccepted); + BEAST_EXPECT(checkVL(sleCred, sfCredentialType, credType)); + BEAST_EXPECT(checkVL(sleCred, sfURI, uri)); + auto const jle = credentials::ledgerEntryCredential( + env, subj, iss, credType); + BEAST_EXPECT( + jle.isObject() && jle.isMember(jss::result) && + !jle[jss::result].isMember(jss::error) && + jle[jss::result].isMember(jss::node) && + jle[jss::result][jss::node].isMember("LedgerEntryType") && + jle[jss::result][jss::node]["LedgerEntryType"] == + jss::Credential); + } + + env(credentials::del(iss, subj, iss, credType)); + env.close(); + { + BEAST_EXPECT(!env.le(kCred)); + BEAST_EXPECT(!ownerCnt(env, iss)); + BEAST_EXPECT(!ownerCnt(env, subj)); + } + } + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + Account const other{"other"}; + + env.fund(XRP(5000), subj, iss, other); + env.close(); + + testcase("CredentialsDelete other"); + + auto jv = credentials::create(subj, iss, credType); + jv.removeMember(sfExpiration.jsonName); + env(jv); + env.close(); + + // Other account delete credentials without expiration day + jv = credentials::del(other, subj, iss, credType); + env(jv); + env.close(); + + jv = credentials::create(subj, iss, credType); + uint32_t const t = env.now().time_since_epoch().count(); + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // Other account delete credentials when expired + env(credentials::del(other, subj, iss, credType)); + env.close(); + } + } + + void + testCreateFailed(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + env.fund(XRP(5000), subj, iss); + env.close(); + + { + testcase("Credentials fail, no subject param."); + auto jv = credentials::create(subj, iss, credType); + jv.removeMember(jss::Subject); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail no issuer param."); + auto jv = credentials::create(subj, iss, credType, false); + jv.removeMember(sfPublicKey.jsonName); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail, no signature param."); + auto jv = credentials::create(subj, iss, credType, false); + jv.removeMember(jss::Signature); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail, no credentialType param."); + auto jv = credentials::create(subj, iss, credType); + jv.removeMember(sfCredentialType.jsonName); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail, URI length > 256."); + auto jv = credentials::create(subj, iss, credType); + constexpr std::string_view longURI = + "abcdefghijklmnopqrstuvwxyz01234567890qwertyuiop[]" + "asdfghjkl;'zxcvbnm8237tr28weufwldebvfv8734t07p " + "9hfup;wDJFBVSD8f72 " + "pfhiusdovnbs;djvbldafghwpEFHdjfaidfgio84763tfysgdvhjasbd " + "vujhgWQIE7F6WEUYFGWUKEYFVQW87FGWOEFWEFUYWVEF8723GFWEFBWULE" + "fv28o37gfwEFB3872TFO8GSDSDVD"; + static_assert(longURI.size() > 256); + jv[sfURI.jsonName] = strHex(longURI); + env(jv, ter(temMALFORMED)); + } + + { + testcase("Credentials fail, expiration in the past."); + auto jv = credentials::create(subj, iss, credType); + // current time in ripple epoch - 1s + uint32_t const t = env.now().time_since_epoch().count() - 1; + jv[sfExpiration.jsonName] = t; + env(jv, ter(tecEXPIRED)); + } + + { + testcase("Credentials fail, invalid fee."); + + auto jv = credentials::create(subj, iss, credType); + jv[jss::Fee] = -1; + env(jv, ter(temBAD_FEE)); + } + + { + testcase("Credentials fail, duplicate."); + auto const jv = credentials::create(subj, iss, credType); + env(jv); + env.close(); + env(jv, ter(tecDUPLICATE)); + env.close(); + } + + { + testcase("Credentials fail, duplicate from issuer side."); + auto const jv = credentials::create(subj, iss, credType, false); + env(jv, ter(tecDUPLICATE)); + env.close(); + } + } + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + env.fund(XRP(5000), iss); + env.close(); + + { + testcase("Credentials fail, subject doesn't exist."); + auto const jv = credentials::create(subj, iss, credType); + env(jv, ter(tecNO_TARGET)); + } + + { + auto jv = credentials::create(subj, iss, credType); + jv[jss::Subject] = to_string(xrpAccount()); + env(jv, ter(temINVALID_ACCOUNT_ID)); + } + + { + env.fund(XRP(5000), subj); + env.close(); + + Account const notIss{"not_issuer"}; + + auto jv = credentials::create(subj, iss, credType, false); + jv[sfPublicKey.jsonName] = strHex(notIss.pk()); + env(jv, ter(temBAD_SIGNATURE)); + } + + { + Account const notIss{"not_issuer"}; + + auto jv = credentials::create(subj, iss, credType, false); + jv[jss::Signature] = strHex(signCredential( + notIss.pk(), notIss.sk(), subj.id(), credType)); + env(jv, ter(temBAD_SIGNATURE)); + } + + env.close(); + } + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + env.fund(XRP(5000), subj); + env.close(); + + testcase("Credentials created, issuer doesn't exist."); + auto const jv = credentials::create(subj, iss, credType, false); + env(jv); + env.close(); + } + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + auto const reserve = drops(env.current()->fees().accountReserve(0)); + env.fund(reserve, subj, iss); + env.close(); + + testcase("Credentials fail, not enough reserve."); + { + auto const jv = credentials::create(subj, iss, credType); + env(jv, ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + { + auto const jv = credentials::create(subj, iss, credType, false); + env(jv, ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + } + } + + void + testAcceptFailed(FeatureBitset features) + { + const char credType[] = "abcde"; + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + env.fund(XRP(5000), subj, iss); + + { + testcase("CredentialsAccept fail, Credential doesn't exist."); + env(credentials::accept(subj, iss, credType), ter(tecNO_ENTRY)); + env.close(); + } + + { + testcase("CredentialsAccept fail, invalid Issuer account."); + auto jv = credentials::accept(subj, iss, credType); + jv[jss::Issuer] = to_string(xrpAccount()); + env(jv, ter(temINVALID_ACCOUNT_ID)); + env.close(); + } + } + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + env.fund(drops(env.current()->fees().accountReserve(1)), iss); + env.fund(drops(env.current()->fees().accountReserve(0)), subj); + env.close(); + + { + testcase("CredentialsAccept fail, not enough reserve."); + env(credentials::create(subj, iss, credType)); + env.close(); + + env(credentials::accept(subj, iss, credType), + ter(tecINSUFFICIENT_RESERVE)); + env.close(); + } + } + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + env.fund(XRP(5000), subj, iss); + env.close(); + + { + env(credentials::create(subj, iss, credType)); + env.close(); + + testcase("CredentialsAccept fail, invalid fee."); + auto jv = credentials::accept(subj, iss, credType); + jv[jss::Fee] = -1; + env(jv, ter(temBAD_FEE)); + + testcase("CredentialsAccept fail, lsfAccepted already set."); + env(credentials::accept(subj, iss, credType)); + env.close(); + env(credentials::accept(subj, iss, credType), + ter(tecDUPLICATE)); + env.close(); + } + + { + const char credType2[] = "efghi"; + + testcase("CredentialsAccept fail, expired credentials."); + auto jv = credentials::create(subj, iss, credType2); + uint32_t const t = env.now().time_since_epoch().count(); + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // credentials are expired now + env(credentials::accept(subj, iss, credType2), ter(tecEXPIRED)); + env.close(); + + // check that expired credentials were deleted + auto const jDelCred = credentials::ledgerEntryCredential( + env, subj, iss, credType2); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error)); + } + } + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + Account const other{"other"}; + + env.fund(XRP(5000), iss, subj, other); + env.close(); + + { + testcase("CredentialsAccept fail, issuer doesn't exist."); + auto jv = credentials::create(subj, iss, credType); + env(jv); + env.close(); + + // delete issuer + int const delta = env.seq(iss) + 255; + for (int i = 0; i < delta; ++i) + env.close(); + auto const acctDelFee{drops(env.current()->fees().increment)}; + env(acctdelete(iss, other), fee(acctDelFee)); + + // can't accept - no issuer account + jv = credentials::accept(subj, iss, credType); + env(jv, ter(tecNO_TARGET)); + env.close(); + } + } + } + + void + testDeleteFailed(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + Account const other{"other"}; + + env.fund(XRP(5000), subj, iss, other); + env.close(); + + { + testcase("CredentialsDelete fail, no Credentials."); + + env(credentials::del(subj, subj, iss, credType), + ter(tecNO_ENTRY)); + env.close(); + } + + { + testcase("CredentialsDelete fail, invalid Subject account."); + auto jv = credentials::del(subj, subj, iss, credType); + jv[jss::Subject] = to_string(xrpAccount()); + env(jv, ter(temINVALID_ACCOUNT_ID)); + env.close(); + } + + { + testcase("CredentialsDelete fail, invalid Issuer account."); + auto jv = credentials::del(subj, subj, iss, credType); + jv[jss::Issuer] = to_string(xrpAccount()); + env(jv, ter(temINVALID_ACCOUNT_ID)); + env.close(); + } + + { + testcase("CredentialsDelete fail, time not expired yet."); + + auto jv = credentials::create(subj, iss, credType); + // current time in ripple epoch + 1000s + uint32_t const t = env.now().time_since_epoch().count() + 1000; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + env(credentials::del(other, subj, iss, credType), + ter(tecNO_PERMISSION)); + env.close(); + } + + { + testcase("CredentialsDelete fail, no Issuer and Subject."); + + auto jv = credentials::del(subj, subj, iss, credType); + jv.removeMember(jss::Subject); + jv.removeMember(jss::Issuer); + env(jv, ter(temMALFORMED)); + env.close(); + } + + { + testcase("CredentialsDelete fail, invalid fee."); + + auto jv = credentials::del(subj, subj, iss, credType); + jv[jss::Fee] = -1; + env(jv, ter(temBAD_FEE)); + env.close(); + } + + { + testcase("deleteSLE fail, bad SLE."); + auto view = std::make_shared( + env.current().get(), ApplyFlags::tapNONE); + auto ter = CredentialDelete::deleteSLE(*view, {}, env.journal); + BEAST_EXPECT(ter == tecNO_ENTRY); + } + } + } + + void + testFeatureFailed(FeatureBitset features) + { + using namespace test::jtx; + + const char credType[] = "abcde"; + + { + using namespace jtx; + Env env{*this, features}; + Account const iss{"issuer"}; + Account const subj{"subject"}; + + env.fund(XRP(5000), subj, iss); + env.close(); + + { + testcase("Credentials fail, Feature is not enabled."); + env(credentials::create(subj, iss, credType), ter(temDISABLED)); + env(credentials::accept(subj, iss, credType), ter(temDISABLED)); + env(credentials::del(subj, subj, iss, credType), + ter(temDISABLED)); + } + } + } + + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + testSuccessful(all); + testCreateFailed(all); + testAcceptFailed(all); + testDeleteFailed(all); + testFeatureFailed(all - featureCredentials); + } +}; + +BEAST_DEFINE_TESTSUITE(Credentials, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 9a11785b38c..72d1ad9d71c 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -381,6 +381,25 @@ struct DepositAuth_test : public beast::unit_test::suite } }; +static Json::Value +ledgerEntryDepositPreauth( + jtx::Env& env, + jtx::Account const& acc, + std::vector const& auth) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = acc.human(); + auto& arr( + jvParams[jss::deposit_preauth][jss::authorize_credentials] = + Json::arrayValue); + for (auto const& o : auth) + { + arr.append(o.toLEJson()); + } + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + struct DepositPreauth_test : public beast::unit_test::suite { void @@ -634,6 +653,59 @@ struct DepositPreauth_test : public beast::unit_test::suite sendmax(XRP(10)), ter(expect)); env.close(); + + { + // becky setup depositpreauth with credentials + + const char credType[] = "abcde"; + Account const carol{"carol"}; + + env.fund(XRP(5000), carol); + + if (supportsPreauth) + { + env(deposit::auth( + becky, + std::vector{ + {carol, credType}})); + env.close(); + } + + // gw accept credentials + auto jv = credentials::create(gw, carol, credType); + env(jv); + env.close(); + env(credentials::accept(gw, carol, credType)); + env.close(); + + auto const jCred = credentials::ledgerEntryCredential( + env, gw, carol, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + TER const expect{ + supportsPreauth ? TER{tesSUCCESS} : TER{temDISABLED}}; + env(pay(gw, becky, USD(100), {credIdx}), ter(expect)); + env.close(); + } + + { + using namespace std::chrono; + + if (!supportsPreauth) + { + auto const seq1 = env.seq(alice); + env(escrow(alice, becky, XRP(100)), + finish_time(env.now() + 1s)); + env.close(); + + // Failed as rule is disabled + env(finish(gw, alice, seq1), + fee(1500), + ter(tecNO_PERMISSION)); + env.close(); + } + } } if (supportsPreauth) @@ -724,6 +796,578 @@ struct DepositPreauth_test : public beast::unit_test::suite } } + void + testCredentials() + { + using namespace jtx; + + const char credType[] = "abcde"; + + { + testcase("Payment with credentials."); + + Env env(*this); + Account const iss{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(5000), iss, alice, bob); + env.close(); + + // Issuer create credentials, but Alice didn't accept them yet + env(credentials::create(alice, iss, credType)); + env.close(); + + // Get the index of the credentials + auto const jCred = + credentials::ledgerEntryCredential(env, alice, iss, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth), fee(drops(10))); + env.close(); + + // Bob will accept payements from accounts with credentials signed + // by 'iss' + env(deposit::auth( + bob, + std::vector{{iss, credType}})); + env.close(); + + auto const jDP = + ledgerEntryDepositPreauth(env, bob, {{iss, credType}}); + BEAST_EXPECT( + jDP.isObject() && jDP.isMember(jss::result) && + !jDP[jss::result].isMember(jss::error) && + jDP[jss::result].isMember(jss::node) && + jDP[jss::result][jss::node].isMember("LedgerEntryType") && + jDP[jss::result][jss::node]["LedgerEntryType"] == + jss::DepositPreauth); + + // Alice can't pay - empty credentials array + { + auto jv = pay(alice, bob, XRP(100)); + jv[sfCredentialIDs.jsonName] = Json::arrayValue; + env(jv, ter(temMALFORMED)); + env.close(); + } + + // Alice can't pay - not accepeted credentials + env(pay(alice, bob, XRP(100), {credIdx}), ter(temBAD_CREDENTIALS)); + env.close(); + + // Alice accept the credentials + env(credentials::accept(alice, iss, credType)); + env.close(); + + // Now Alice can pay + env(pay(alice, bob, XRP(100), {credIdx})); + env.close(); + + { + testcase("Escrow with credentials."); + using namespace std::chrono; + auto const seq1 = env.seq(alice); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env.close(); + + Account const john("john"); + + env.fund(XRP(5000), john); + env.close(); + + { + // Don't use credentials for yourself + env(finish(bob, alice, seq1, {credIdx}), + fee(1500), + ter(tecNO_PERMISSION)); + env.close(); + } + { + env(credentials::create(john, iss, credType)); + env.close(); + env(credentials::accept(john, iss, credType)); + env.close(); + auto const jCred = credentials::ledgerEntryCredential( + env, john, iss, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + // john is pre-authorized and can finish escrow for bob + env(finish(john, alice, seq1, {credIdx}), fee(1500)); + env.close(); + } + } + } + + { + testcase("Creating / deleting with credentials."); + + Env env(*this); + Account const iss{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + + env.fund(XRP(5000), iss, alice, bob); + env.close(); + + { + // both included [AuthorizeCredentials UnauthorizeCredentials] + auto jv = deposit::auth( + bob, + std::vector{ + {iss, credType}}); + jv[sfUnauthorizeCredentials.jsonName] = Json::arrayValue; + env(jv, ter(temMALFORMED)); + } + + { + // both included [Unauthorize, AuthorizeCredentials] + auto jv = deposit::auth( + bob, + std::vector{ + {iss, credType}}); + jv[sfUnauthorize.jsonName] = iss.human(); + env(jv, ter(temMALFORMED)); + } + + { + // both included [Authorize, AuthorizeCredentials] + auto jv = deposit::auth( + bob, + std::vector{ + {iss, credType}}); + jv[sfAuthorize.jsonName] = iss.human(); + env(jv, ter(temMALFORMED)); + } + + { + // AuthorizeCredentials is empty + auto jv = deposit::auth( + bob, std::vector{}); + env(jv, ter(temMALFORMED)); + } + + { + // invalid account + auto jv = deposit::auth( + bob, std::vector{}); + auto& arr(jv[sfAuthorizeCredentials.jsonName]); + Json::Value jcred = Json::objectValue; + jcred[jss::Issuer] = to_string(xrpAccount()); + jcred[sfCredentialType.jsonName] = + strHex(std::string_view(credType)); + Json::Value j2; + j2[jss::Credential] = jcred; + arr.append(std::move(j2)); + + env(jv, ter(temINVALID_ACCOUNT_ID)); + } + + { + // try preauth itself + auto jv = deposit::auth( + bob, + std::vector{ + {bob, credType}}); + env(jv, ter(temCANNOT_PREAUTH_SELF)); + } + + { + // empty credential type + auto jv = deposit::auth( + bob, std::vector{{iss, {}}}); + env(jv, ter(temMALFORMED)); + } + + { + // AuthorizeCredentials is larger than 8 elements + Account const a("a"), b("b"), c("c"), d("d"), e("e"), f("f"), + g("g"), h("h"), i("i"); + auto const& z = credType; + auto jv = deposit::auth( + bob, + std::vector{ + {a, z}, + {b, z}, + {c, z}, + {d, z}, + {e, z}, + {f, z}, + {g, z}, + {h, z}, + {i, z}}); + env(jv, ter(temMALFORMED)); + } + + { + // Can create with non-existing issuer + Account const rick{"rick"}; + auto jv = deposit::auth( + bob, + std::vector{ + {rick, credType}}); + env(jv); + env.close(); + } + + { + // not enough resevre + Account const john{"john"}; + env.fund(env.current()->fees().accountReserve(0), john); + auto jv = deposit::auth( + john, + std::vector{ + {iss, credType}}); + env(jv, ter(tecINSUFFICIENT_RESERVE)); + } + + { + // NO deposit object exists + env(deposit::unauth( + bob, + std::vector{ + {iss, credType}}), + ter(tecNO_ENTRY)); + } + + // Create DepositPreauth object + { + env(deposit::auth( + bob, + std::vector{ + {iss, credType}})); + env.close(); + + auto const jDP = + ledgerEntryDepositPreauth(env, bob, {{iss, credType}}); + BEAST_EXPECT( + jDP.isObject() && jDP.isMember(jss::result) && + !jDP[jss::result].isMember(jss::error) && + jDP[jss::result].isMember(jss::node) && + jDP[jss::result][jss::node].isMember("LedgerEntryType") && + jDP[jss::result][jss::node]["LedgerEntryType"] == + jss::DepositPreauth); + + // can't create duplicate + env(deposit::auth( + bob, + std::vector{ + {iss, credType}}), + ter(tecDUPLICATE)); + } + + // Delete DepositPreauth object + { + env(deposit::unauth( + bob, + std::vector{ + {iss, credType}})); + env.close(); + auto const jDP = + ledgerEntryDepositPreauth(env, bob, {{iss, credType}}); + BEAST_EXPECT( + jDP.isObject() && jDP.isMember(jss::result) && + jDP[jss::result].isMember(jss::error)); + } + } + + { + testcase("Payment failed with invalid credentials."); + + Env env(*this); + Account const iss{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const maria{"maria"}; + + Account const gw{"gw"}; + IOU const USD = gw["USD"]; + + env.fund(XRP(10000), iss, alice, bob, maria, gw); + env.close(); + + // Issuer create credentials, but Alice didn't accept them yet + env(credentials::create(alice, iss, credType)); + env.close(); + // Alice accept the credentials + env(credentials::accept(alice, iss, credType)); + env.close(); + // Get the index of the credentials + auto const jCred = + credentials::ledgerEntryCredential(env, alice, iss, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + { + // Fail as destination didn't enable preauthorization + env(pay(alice, bob, XRP(100), {credIdx}), + ter(tecNO_PERMISSION)); + } + + // Bob require preauthorization + env(fset(bob, asfDepositAuth), fee(drops(10))); + env.close(); + + { + // Fail as destination didn't setup DepositPreauth object + env(pay(alice, bob, XRP(100), {credIdx}), + ter(tecNO_PERMISSION)); + } + + // Bob setup DepositPreauth object, duplicates will be eliminated + env(deposit::auth( + bob, + std::vector{ + {iss, credType}, {iss, credType}})); + env.close(); + + { + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" + "01E034"; + // Alice can't pay with non-existing credentials + env(pay(alice, bob, XRP(100), {invalidIdx}), + ter(temBAD_CREDENTIALS)); + } + + { // maria can't pay using valid credentials but issued for + // different account + env(pay(maria, bob, XRP(100), {credIdx}), + ter(temBAD_CREDENTIALS)); + } + + { + // create another valid credential + const char credType2[] = "fghij"; + env(credentials::create(alice, iss, credType2)); + env.close(); + env(credentials::accept(alice, iss, credType2)); + env.close(); + auto const jCred2 = credentials::ledgerEntryCredential( + env, alice, iss, credType2); + std::string const credIdx2 = + jCred2[jss::result][jss::index].asString(); + + // Alice can't pay with invalid set of valid credentials + env(pay(alice, bob, XRP(100), {credIdx, credIdx2}), + ter(tecNO_PERMISSION)); + } + + // Alice can pay, duplicate credentials will be eliminated + env(pay(alice, bob, XRP(100), {credIdx, credIdx})); + env.close(); + env(pay(alice, bob, XRP(100), {credIdx})); + env.close(); + } + + { + testcase("Payment failed with disabled rules."); + + Env env(*this, supported_amendments() - featureCredentials); + Account const iss{"issuer"}; + Account const bob{"bob"}; + + env.fund(XRP(5000), iss, bob); + env.close(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth), fee(drops(10))); + env.close(); + + // Setup DepositPreauth object failed - amendent is not supported + env(deposit::auth( + bob, + std::vector{ + {iss, credType}}), + ter(temDISABLED)); + env.close(); + + { + // Payment with CredentialsIDs failed - amendent is not + // supported + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" + "01E034"; + env(pay(iss, bob, XRP(10), {invalidIdx}), ter(temDISABLED)); + } + } + } + + void + testExpCreds() + { + using namespace jtx; + const char credType[] = "abcde"; + + { + testcase("Payment failed with expired credentials."); + + Env env(*this); + Account const iss{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const gw{"gw"}; + IOU const USD = gw["USD"]; + + env.fund(XRP(10000), iss, alice, bob, gw); + env.close(); + + // Create credentials + auto jv = credentials::create(alice, iss, credType); + // Current time in ripple epoch. + // Every time ledger close, unittest timer increase by 10s + uint32_t const t = env.now().time_since_epoch().count() + 40; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // Alice accept the credentials + env(credentials::accept(alice, iss, credType)); + env.close(); + + // Get the index of the credentials + auto const jCred = + credentials::ledgerEntryCredential(env, alice, iss, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth), fee(drops(10))); + env.close(); + // Bob setup DepositPreauth object + env(deposit::auth( + bob, + std::vector{{iss, credType}})); + env.close(); + + { + // Alice can pay + env(pay(alice, bob, XRP(100), {credIdx})); + env.close(); + + // Ledger closed, time increased, alice can't pay anymore + env(pay(alice, bob, XRP(100), {credIdx}), ter(tecEXPIRED)); + env.close(); + + // check that expired credentials were deleted + auto const jDelCred = credentials::ledgerEntryCredential( + env, alice, iss, credType); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error)); + } + + { + auto jv = credentials::create(gw, iss, credType); + uint32_t const t = env.now().time_since_epoch().count() + 40; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + env(credentials::accept(gw, iss, credType)); + env.close(); + + auto const jCred = + credentials::ledgerEntryCredential(env, gw, iss, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + env.close(); + env.close(); + env.close(); + + // credentails are expired + env(pay(gw, bob, USD(150), {credIdx}), ter(tecEXPIRED)); + env.close(); + + // check that expired credentials were deleted + auto const jDelCred = + credentials::ledgerEntryCredential(env, gw, iss, credType); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error)); + } + } + + { + using namespace std::chrono; + + testcase("Escrow failed with expired credentials."); + + Env env(*this); + Account const iss{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const zelda{"zelda"}; + + env.fund(XRP(5000), iss, alice, bob, zelda); + env.close(); + + // Create credentials + auto jv = credentials::create(zelda, iss, credType); + uint32_t const t = env.now().time_since_epoch().count() + 50; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + + // Zelda accept the credentials + env(credentials::accept(zelda, iss, credType)); + env.close(); + + // Get the index of the credentials + auto const jCred = + credentials::ledgerEntryCredential(env, zelda, iss, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + // Bob require preauthorization + env(fset(bob, asfDepositAuth), fee(drops(10))); + env.close(); + // Bob setup DepositPreauth object + env(deposit::auth( + bob, + std::vector{{iss, credType}})); + env.close(); + + auto const seq1 = env.seq(alice); + env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s)); + env.close(); + + // zelda can't finish escrow with invalid credentials + { + auto jv = finish(zelda, alice, seq1, {}); + jv[sfCredentialIDs.jsonName] = Json::arrayValue; + env(jv, fee(1500), ter(temMALFORMED)); + env.close(); + } + + { + // zelda can't finish escrow with invalid credentials + std::string const invalidIdx = + "0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E" + "01E034"; + auto jv = finish(zelda, alice, seq1, {invalidIdx}); + env(jv, fee(1500), ter(temBAD_CREDENTIALS)); + env.close(); + } + + { // Ledger closed, time increased, zelda can't finish escrow + env(finish(zelda, alice, seq1, {credIdx}), + fee(1500), + ter(tecEXPIRED)); + env.close(); + } + + // check that expired credentials were deleted + auto const jDelCred = + credentials::ledgerEntryCredential(env, zelda, iss, credType); + BEAST_EXPECT( + jDelCred.isObject() && jDelCred.isMember(jss::result) && + jDelCred[jss::result].isMember(jss::error)); + } + } + void run() override { @@ -732,6 +1376,8 @@ struct DepositPreauth_test : public beast::unit_test::suite auto const supported{jtx::supported_amendments()}; testPayment(supported - featureDepositPreauth); testPayment(supported); + testCredentials(); + testExpCreds(); } }; diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index e49e5cbd6dc..5f26c8dcc6d 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -832,6 +832,90 @@ struct PayChan_test : public beast::unit_test::suite } } + void + testDepositAuthCreds() + { + testcase("Deposit Authorization with Credentials"); + using namespace jtx; + using namespace std::literals::chrono_literals; + + const char credType[] = "abcde"; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + { + Env env{*this}; + env.fund(XRP(10000), alice, bob, carol); + + env(fset(bob, asfDepositAuth)); + env.close(); + env(deposit::auth( + bob, + std::vector{{carol, credType}})); + env.close(); + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, XRP(1000), settleDelay, pk)); + env.close(); + + // alice can add more funds to the channel even though bob has + // asfDepositAuth set. + env(fund(alice, chan, XRP(1000))); + env.close(); + + // alice claims. Fails because bob's lsfDepositAuth flag is set. + env(claim(alice, chan, XRP(500).value(), XRP(500).value()), + ter(tecNO_PERMISSION)); + + { + auto jv = credentials::create(alice, carol, credType); + uint32_t const t = env.now().time_since_epoch().count() + 100; + jv[sfExpiration.jsonName] = t; + env(jv); + env.close(); + env(credentials::accept(alice, carol, credType)); + env.close(); + } + + auto const jCred = + credentials::ledgerEntryCredential(env, alice, carol, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + + { + // claim fails cause of empty credentials + auto jv = + claim(alice, chan, XRP(500).value(), XRP(500).value()); + jv[sfCredentialIDs.jsonName] = Json::arrayValue; + env(jv, ter(temMALFORMED)); + env.close(); + } + + { + // claim fails cause of expired credentials + + // Every cycle +10sec. + for (int i = 0; i < 10; ++i) + env.close(); + + auto jv = claim( + alice, + chan, + XRP(500).value(), + XRP(500).value(), + std::nullopt, + std::nullopt, + {credIdx}); + env(jv, ter(tecEXPIRED)); + env.close(); + } + } + } + void testMultiple(FeatureBitset features) { @@ -2116,6 +2200,7 @@ struct PayChan_test : public beast::unit_test::suite FeatureBitset const all{supported_amendments()}; testWithFeats(all - disallowIncoming); testWithFeats(all); + testDepositAuthCreds(); } }; diff --git a/src/test/jtx.h b/src/test/jtx.h index 6de7cd480fa..12ed02bcff9 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 800662aa05b..4b73ab2a343 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -231,12 +231,20 @@ escrow(Account const& account, Account const& to, STAmount const& amount) } Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq); +finish( + AccountID const& account, + AccountID const& from, + std::uint32_t seq, + std::vector const& credentialIDs); inline Json::Value -finish(Account const& account, Account const& from, std::uint32_t seq) +finish( + Account const& account, + Account const& from, + std::uint32_t seq, + std::vector const& credentialIDs = {}) { - return finish(account.id(), from.id(), seq); + return finish(account.id(), from.id(), seq, credentialIDs); } Json::Value @@ -380,7 +388,8 @@ claim( std::optional const& balance = std::nullopt, std::optional const& amount = std::nullopt, std::optional const& signature = std::nullopt, - std::optional const& pk = std::nullopt); + std::optional const& pk = std::nullopt, + std::vector const& credentialIDs = {}); uint256 channel( diff --git a/src/test/jtx/credentials.h b/src/test/jtx/credentials.h new file mode 100644 index 00000000000..c71948c611b --- /dev/null +++ b/src/test/jtx/credentials.h @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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. +*/ +//============================================================================== + +#pragma once + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace credentials { + +Json::Value +create( + jtx::Account const& iss, + jtx::Account const& subj, + std::string const& credType, + bool iss_own = true); + +Json::Value +accept( + jtx::Account const& iss, + jtx::Account const& subj, + std::string const& credType); + +Json::Value +del(jtx::Account const& acc, + jtx::Account const& iss, + jtx::Account const& subj, + std::string const& credType); + +Json::Value +ledgerEntryCredential( + jtx::Env& env, + jtx::Account const& subj, + jtx::Account const& iss, + std::string_view credType); + +} // namespace credentials + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/deposit.h b/src/test/jtx/deposit.h index 720254e7eae..bf7ab40cc10 100644 --- a/src/test/jtx/deposit.h +++ b/src/test/jtx/deposit.h @@ -38,6 +38,41 @@ auth(Account const& account, Account const& auth); Json::Value unauth(Account const& account, Account const& unauth); +struct AuthorizeCredentials +{ + jtx::Account issuer; + std::string credType; + + Json::Value + toJson() const + { + Json::Value jv; + jv[jss::Issuer] = issuer.human(); + jv[sfCredentialType.jsonName] = strHex(credType); + return jv; + } + + // "ledger_entry" uses a different naming convention + Json::Value + toLEJson() const + { + Json::Value jv; + jv[jss::issuer] = issuer.human(); + jv[jss::credential_type] = strHex(credType); + return jv; + } +}; + +Json::Value +auth( + jtx::Account const& account, + std::vector const& auth); + +Json::Value +unauth( + jtx::Account const& account, + std::vector const& auth); + } // namespace deposit } // namespace jtx diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index b8105b1a631..a9f2afe7037 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -221,7 +221,11 @@ escrow(AccountID const& account, AccountID const& to, STAmount const& amount) } Json::Value -finish(AccountID const& account, AccountID const& from, std::uint32_t seq) +finish( + AccountID const& account, + AccountID const& from, + std::uint32_t seq, + std::vector const& credentialIDs) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowFinish; @@ -229,6 +233,12 @@ finish(AccountID const& account, AccountID const& from, std::uint32_t seq) jv[jss::Account] = to_string(account); jv[sfOwner.jsonName] = to_string(from); jv[sfOfferSequence.jsonName] = seq; + if (!credentialIDs.empty()) + { + auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue); + for (auto const& o : credentialIDs) + arr.append(o); + } return jv; } @@ -296,7 +306,8 @@ claim( std::optional const& balance, std::optional const& amount, std::optional const& signature, - std::optional const& pk) + std::optional const& pk, + std::vector const& credentialIDs) { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; @@ -311,6 +322,12 @@ claim( jv["Signature"] = strHex(*signature); if (pk) jv["PublicKey"] = strHex(pk->slice()); + if (!credentialIDs.empty()) + { + auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue); + for (auto const& o : credentialIDs) + arr.append(o); + } return jv; } @@ -385,4 +402,4 @@ allpe(AccountID const& a, Issue const& iss) } // namespace jtx } // namespace test -} // namespace ripple \ No newline at end of file +} // namespace ripple diff --git a/src/test/jtx/impl/credentials.cpp b/src/test/jtx/impl/credentials.cpp new file mode 100644 index 00000000000..d799a18a1c2 --- /dev/null +++ b/src/test/jtx/impl/credentials.cpp @@ -0,0 +1,111 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2019 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 + +namespace ripple { +namespace test { +namespace jtx { + +namespace credentials { + +Json::Value +create( + jtx::Account const& subj, + jtx::Account const& iss, + std::string const& credType, + bool iss_own) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::CredentialCreate; + if (iss_own) + { + jv[jss::Account] = to_string(iss.id()); + jv[jss::Subject] = to_string(subj.id()); + } + else + { + jv[jss::Account] = to_string(subj.id()); + jv[sfPublicKey.jsonName] = strHex(iss.pk()); + jv[jss::Signature] = + strHex(signCredential(iss.pk(), iss.sk(), subj.id(), credType)); + } + jv[jss::Flags] = tfUniversal; + jv[sfURI.jsonName] = strHex(std::string{"uri"}); + jv[sfCredentialType.jsonName] = strHex(credType); + + return jv; +} + +Json::Value +accept( + jtx::Account const& subj, + jtx::Account const& iss, + std::string const& credType) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::CredentialAccept; + jv[jss::Account] = to_string(subj.id()); + jv[jss::Issuer] = to_string(iss.id()); + jv[sfCredentialType.jsonName] = strHex(credType); + jv[jss::Flags] = tfUniversal; + + return jv; +} + +Json::Value +del(jtx::Account const& acc, + jtx::Account const& subj, + jtx::Account const& iss, + std::string const& credType) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::CredentialDelete; + jv[jss::Account] = acc.human(); + jv[jss::Subject] = subj.human(); + jv[jss::Issuer] = iss.human(); + jv[sfCredentialType.jsonName] = strHex(credType); + jv[jss::Flags] = tfUniversal; + return jv; +} + +Json::Value +ledgerEntryCredential( + jtx::Env& env, + jtx::Account const& subj, + jtx::Account const& iss, + std::string_view credType) +{ + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::credential][jss::subject] = subj.human(); + jvParams[jss::credential][jss::issuer] = iss.human(); + jvParams[jss::credential][jss::credential_type] = strHex(credType); + return env.rpc("json", "ledger_entry", to_string(jvParams)); +} + +} // namespace credentials + +} // namespace jtx + +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/deposit.cpp b/src/test/jtx/impl/deposit.cpp index 09f0cb704b0..7f8fcdd7127 100644 --- a/src/test/jtx/impl/deposit.cpp +++ b/src/test/jtx/impl/deposit.cpp @@ -48,6 +48,42 @@ unauth(jtx::Account const& account, jtx::Account const& unauth) return jv; } +// Add DepositPreauth. +Json::Value +auth(jtx::Account const& account, std::vector const& auth) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + auto& arr(jv[sfAuthorizeCredentials.jsonName] = Json::arrayValue); + for (auto const& o : auth) + { + Json::Value j2; + j2[jss::Credential] = o.toJson(); + arr.append(std::move(j2)); + } + jv[sfTransactionType.jsonName] = jss::DepositPreauth; + return jv; +} + +// Remove DepositPreauth. +Json::Value +unauth( + jtx::Account const& account, + std::vector const& auth) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + auto& arr(jv[sfUnauthorizeCredentials.jsonName] = Json::arrayValue); + for (auto const& o : auth) + { + Json::Value j2; + j2[jss::Credential] = o.toJson(); + arr.append(std::move(j2)); + } + jv[sfTransactionType.jsonName] = jss::DepositPreauth; + return jv; +} + } // namespace deposit } // namespace jtx diff --git a/src/test/jtx/impl/pay.cpp b/src/test/jtx/impl/pay.cpp index 2a627223fdd..4f2bd3e34ed 100644 --- a/src/test/jtx/impl/pay.cpp +++ b/src/test/jtx/impl/pay.cpp @@ -26,7 +26,10 @@ namespace test { namespace jtx { Json::Value -pay(AccountID const& account, AccountID const& to, AnyAmount amount) +pay(AccountID const& account, + AccountID const& to, + AnyAmount amount, + std::vector const& credentialIDs) { amount.to(to); Json::Value jv; @@ -35,12 +38,22 @@ pay(AccountID const& account, AccountID const& to, AnyAmount amount) jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; jv[jss::Flags] = tfUniversal; + if (!credentialIDs.empty()) + { + auto& arr(jv[sfCredentialIDs.jsonName] = Json::arrayValue); + for (auto const& o : credentialIDs) + arr.append(o); + } return jv; } + Json::Value -pay(Account const& account, Account const& to, AnyAmount amount) +pay(Account const& account, + Account const& to, + AnyAmount amount, + std::vector const& credentialIDs) { - return pay(account.id(), to.id(), amount); + return pay(account.id(), to.id(), amount, credentialIDs); } } // namespace jtx diff --git a/src/test/jtx/pay.h b/src/test/jtx/pay.h index 6294b5b3082..253b66f78bf 100644 --- a/src/test/jtx/pay.h +++ b/src/test/jtx/pay.h @@ -30,9 +30,15 @@ namespace jtx { /** Create a payment. */ Json::Value -pay(AccountID const& account, AccountID const& to, AnyAmount amount); +pay(AccountID const& account, + AccountID const& to, + AnyAmount amount, + std::vector const& credentialIDs = {}); Json::Value -pay(Account const& account, Account const& to, AnyAmount amount); +pay(Account const& account, + Account const& to, + AnyAmount amount, + std::vector const& credentialIDs = {}); } // namespace jtx } // namespace test diff --git a/src/test/rpc/DepositAuthorized_test.cpp b/src/test/rpc/DepositAuthorized_test.cpp index ebabe1fbe3f..28f4ec1e68f 100644 --- a/src/test/rpc/DepositAuthorized_test.cpp +++ b/src/test/rpc/DepositAuthorized_test.cpp @@ -31,13 +31,22 @@ class DepositAuthorized_test : public beast::unit_test::suite depositAuthArgs( jtx::Account const& source, jtx::Account const& dest, - std::string const& ledger = "") + std::string const& ledger = "", + std::vector const& credentials = {}) { Json::Value args{Json::objectValue}; args[jss::source_account] = source.human(); args[jss::destination_account] = dest.human(); if (!ledger.empty()) args[jss::ledger_index] = ledger; + + if (!credentials.empty()) + { + auto& arr(args[jss::credentials] = Json::arrayValue); + for (auto const& s : credentials) + arr.append(s); + } + return args; } @@ -276,11 +285,208 @@ class DepositAuthorized_test : public beast::unit_test::suite } } + void + testCredentials() + { + using namespace jtx; + + const char credType[] = "abcde"; + + Account const alice{"alice"}; + Account const becky{"becky"}; + Account const carol{"carol"}; + + Env env(*this); + env.fund(XRP(1000), alice, becky, carol); + env.close(); + + // carol recognize becky + env(credentials::create(alice, carol, credType)); + env.close(); + // retrieve the index of the credentials + auto const jCred = + credentials::ledgerEntryCredential(env, alice, carol, credType); + std::string const credIdx = jCred[jss::result][jss::index].asString(); + + // becky sets the DepositAuth flag in the current ledger. + env(fset(becky, asfDepositAuth)); + env.close(); + + // becky authorize any account recognized by carol to make a payment + env(deposit::auth( + becky, + std::vector{{carol, credType}})); + env.close(); + + { + testcase( + "deposit_authorized with credentials failed: empty array."); + + auto args = depositAuthArgs(alice, becky, "validated"); + args[jss::credentials] = Json::arrayValue; + + auto jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result.isMember(jss::error)); + } + + { + testcase( + "deposit_authorized with credentials failed: not a string " + "credentials"); + + auto args = depositAuthArgs(alice, becky, "validated"); + args[jss::credentials] = Json::arrayValue; + args[jss::credentials].append(1); + args[jss::credentials].append(3); + + auto jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result.isMember(jss::error)); + } + + { + testcase( + "deposit_authorized with credentials failed: not a hex string " + "credentials"); + + auto args = depositAuthArgs(alice, becky, "validated"); + args[jss::credentials] = Json::arrayValue; + args[jss::credentials].append("hello world"); + + auto jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result.isMember(jss::error)); + } + + { + testcase( + "deposit_authorized with credentials failed: not a credential " + "index"); + + auto args = depositAuthArgs( + alice, + becky, + "validated", + {"0127AB8B4B29CCDBB61AA51C0799A8A6BB80B86A9899807C11ED576AF8516" + "473"}); + + auto jv = + env.rpc("json", "deposit_authorized", args.toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result[jss::status] == jss::success); + BEAST_EXPECT(result[jss::deposit_authorized] == false); + } + + { + testcase( + "deposit_authorized with credentials not authorized: " + "credential not accepted"); + auto jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx}) + .toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result[jss::status] == jss::success); + BEAST_EXPECT(result[jss::deposit_authorized] == false); + } + + // alice accept credentials + env(credentials::accept(alice, carol, credType)); + env.close(); + + { + testcase("deposit_authorized with credentials"); + auto jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx}) + .toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result[jss::status] == jss::success); + BEAST_EXPECT(result[jss::deposit_authorized] == true); + } + + { + testcase("deposit_authorized account without preauth"); + auto jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(becky, alice, "validated", {credIdx}) + .toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result[jss::status] == jss::success); + BEAST_EXPECT(result[jss::deposit_authorized] == false); + } + + { + testcase("deposit_authorized with expired credentials"); + + // check expired credentials + const char credType2[] = "fghijk"; + std::uint32_t const x = env.now().time_since_epoch().count() + 40; + + // create credentials with expire time 1s + auto jv = credentials::create(alice, carol, credType2); + jv[sfExpiration.jsonName] = x; + env(jv); + env.close(); + env(credentials::accept(alice, carol, credType2)); + env.close(); + auto const jCred2 = credentials::ledgerEntryCredential( + env, alice, carol, credType2); + std::string const credIdx2 = + jCred2[jss::result][jss::index].asString(); + + // becky sets the DepositAuth flag in the current ledger. + env(fset(becky, asfDepositAuth)); + env.close(); + + // becky authorize any account recognized by carol to make a payment + env(deposit::auth( + becky, + std::vector{ + {carol, credType2}})); + env.close(); + + { + // this should be fine + jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx2}) + .toStyledString()); + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result[jss::status] == jss::success); + BEAST_EXPECT(result[jss::deposit_authorized] == true); + } + + env.close(); // increase timer by 10s + { + // now credentials expired + jv = env.rpc( + "json", + "deposit_authorized", + depositAuthArgs(alice, becky, "validated", {credIdx2}) + .toStyledString()); + + auto const& result{jv[jss::result]}; + BEAST_EXPECT(result[jss::status] == jss::success); + BEAST_EXPECT(result[jss::deposit_authorized] == false); + } + } + } + void run() override { testValid(); testErrors(); + testCredentials(); } }; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 70e4ffbe8dc..48f96bdab39 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -737,6 +737,7 @@ class LedgerRPC_test : public beast::unit_test::suite Env env{*this}; Account const alice{"alice"}; Account const becky{"becky"}; + Account const iss{"issuer"}; env.fund(XRP(10000), alice, becky); env.close(); @@ -858,6 +859,123 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryDepositPreauthCred() + { + testcase("ledger_entry Deposit Preauth with credentials"); + + using namespace test::jtx; + + Env env(*this); + Account const iss{"issuer"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + const char credType[] = "abcde"; + + env.fund(XRP(5000), iss, alice, bob); + env.close(); + + { + // Setup credentials with DepositAuth object for Alice and Bob + env(credentials::create(alice, iss, credType)); + env.close(); + auto const jCred = + credentials::ledgerEntryCredential(env, alice, iss, credType); + std::string const credIdx = + jCred[jss::result][jss::index].asString(); + env(fset(bob, asfDepositAuth), fee(drops(10))); + env.close(); + env(deposit::auth( + bob, + std::vector{{iss, credType}})); + env.close(); + } + + { + // Failed with invalid subject + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = 42; + jv[jss::credential][jss::issuer] = iss.human(); + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Failed with no subject + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = ""; + jv[jss::credential][jss::issuer] = iss.human(); + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Failed with no issuer + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = ""; + jv[jss::credential][jss::credential_type] = + strHex(std::string_view(credType)); + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Failed with no credentials + Json::Value jv; + jv[jss::ledger_index] = jss::validated; + jv[jss::credential][jss::subject] = alice.human(); + jv[jss::credential][jss::issuer] = iss.human(); + jv[jss::credential][jss::credential_type] = ""; + auto const jrr = env.rpc("json", "ledger_entry", to_string(jv)); + checkErrorValue(jrr[jss::result], "malformedRequest", ""); + } + + { + // Failed with invalid account + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + auto& arr( + jvParams[jss::deposit_preauth][jss::authorize_credentials] = + Json::arrayValue); + Json::Value jo; + jo[jss::issuer] = to_string(xrpAccount()); + jo[jss::credential_type] = strHex(std::string_view(credType)); + arr.append(std::move(jo)); + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizeCredentials", ""); + } + + { + // Failed with invalid credential_type + Json::Value jvParams; + jvParams[jss::ledger_index] = jss::validated; + jvParams[jss::deposit_preauth][jss::owner] = bob.human(); + auto& arr( + jvParams[jss::deposit_preauth][jss::authorize_credentials] = + Json::arrayValue); + Json::Value jo; + jo[jss::issuer] = to_string(xrpAccount()); + jo[jss::credential_type] = ""; + arr.append(std::move(jo)); + + auto const jrr = + env.rpc("json", "ledger_entry", to_string(jvParams)); + checkErrorValue( + jrr[jss::result], "malformedAuthorizeCredentials", ""); + } + } + void testLedgerEntryDirectory() { @@ -2375,6 +2493,7 @@ class LedgerRPC_test : public beast::unit_test::suite testLedgerEntryAccountRoot(); testLedgerEntryCheck(); testLedgerEntryDepositPreauth(); + testLedgerEntryDepositPreauthCred(); testLedgerEntryDirectory(); testLedgerEntryEscrow(); testLedgerEntryOffer(); diff --git a/src/test/rpc/RPCCall_test.cpp b/src/test/rpc/RPCCall_test.cpp index 5f13c9799a1..b812740fb3f 100644 --- a/src/test/rpc/RPCCall_test.cpp +++ b/src/test/rpc/RPCCall_test.cpp @@ -2458,7 +2458,15 @@ static RPCCallTestData const rpcCallTestArray[] = { {"deposit_authorized", "source_account_NotValidated", "destination_account_NotValidated", - "4294967295"}, + "4294967295", + "cred1", + "cred2", + "cred3", + "cred4", + "cred5", + "cred6", + "cred7", + "cred8"}, RPCCallTestData::no_exception, R"({ "method" : "deposit_authorized", @@ -2467,7 +2475,8 @@ static RPCCallTestData const rpcCallTestArray[] = { "api_version" : %API_VER%, "destination_account" : "destination_account_NotValidated", "ledger_index" : 4294967295, - "source_account" : "source_account_NotValidated" + "source_account" : "source_account_NotValidated", + "credentials": ["cred1", "cred2", "cred3", "cred4", "cred5", "cred6", "cred7", "cred8"] } ] })"}, @@ -2512,7 +2521,15 @@ static RPCCallTestData const rpcCallTestArray[] = { "source_account_NotValidated", "destination_account_NotValidated", "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789", - "spare"}, + "cred1", + "cred2", + "cred3", + "cred4", + "cred5", + "cred6", + "cred7", + "cred8", + "too_much"}, RPCCallTestData::no_exception, R"({ "method" : "deposit_authorized",