mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-26 23:19:07 +00:00
Merge remote-tracking branch 'Transia-RnD-rippled/feature-p256' into develop
# Conflicts: # include/xrpl/protocol/Indexes.h # include/xrpl/protocol/KeyType.h # include/xrpl/protocol/PublicKey.h # src/libxrpl/protocol/Indexes.cpp # src/libxrpl/protocol/PublicKey.cpp # src/libxrpl/protocol/SecretKey.cpp
This commit is contained in:
@@ -82,4 +82,10 @@ base64Encode(std::string_view s)
|
||||
std::string
|
||||
base64Decode(std::string_view data);
|
||||
|
||||
/** Decode a base64url-encoded string (RFC 4648 S5).
|
||||
Converts '-' to '+' and '_' to '/', adds padding, then decodes.
|
||||
*/
|
||||
std::string
|
||||
base64urlDecode(std::string_view data);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -408,6 +408,8 @@ contract(uint256 const& contractID)
|
||||
Keylet
|
||||
contractData(AccountID const& owner, AccountID const& contractAccount) noexcept;
|
||||
|
||||
Keylet
|
||||
passkeyList(AccountID const& account) noexcept;
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -9,6 +9,7 @@ enum class KeyType {
|
||||
Secp256k1 = 0,
|
||||
Ed25519 = 1,
|
||||
Dilithium = 2,
|
||||
P256 = 3,
|
||||
};
|
||||
|
||||
inline std::optional<KeyType>
|
||||
@@ -23,6 +24,9 @@ keyTypeFromString(std::string const& s)
|
||||
if (s == "dilithium")
|
||||
return KeyType::Dilithium;
|
||||
|
||||
if (s == "p256")
|
||||
return KeyType::P256;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -38,6 +42,9 @@ to_string(KeyType type)
|
||||
if (type == KeyType::Dilithium)
|
||||
return "dilithium";
|
||||
|
||||
if (type == KeyType::P256)
|
||||
return "p256";
|
||||
|
||||
return "INVALID";
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace xrpl {
|
||||
* secp256k1
|
||||
* ed25519
|
||||
* dilithium
|
||||
* p256
|
||||
*
|
||||
* secp256k1 public keys consist of a 33 byte
|
||||
* compressed public key, with the lead byte equal
|
||||
@@ -57,9 +58,10 @@ class PublicKey
|
||||
protected:
|
||||
// Minimum / standard public key size (secp256k1, ed25519).
|
||||
static constexpr std::size_t kSize = 33;
|
||||
// Buffer sized for the largest supported key (dilithium = 1312 bytes).
|
||||
// Actual length is tracked in size_.
|
||||
std::uint8_t buf_[1312]{};
|
||||
// Buffer sized for the largest supported key (dilithium = 1312 bytes;
|
||||
// uncompressed p256 = 65 bytes). Actual length is tracked in size_.
|
||||
static constexpr std::size_t kMaxSize = 1312;
|
||||
std::uint8_t buf_[kMaxSize]{};
|
||||
std::size_t size_ = 0;
|
||||
|
||||
public:
|
||||
@@ -137,7 +139,7 @@ operator<<(std::ostream& os, PublicKey const& pk);
|
||||
inline bool
|
||||
operator==(PublicKey const& lhs, PublicKey const& rhs)
|
||||
{
|
||||
return std::memcmp(lhs.data(), rhs.data(), rhs.size()) == 0;
|
||||
return lhs.size() == rhs.size() && std::memcmp(lhs.data(), rhs.data(), rhs.size()) == 0;
|
||||
}
|
||||
|
||||
inline bool
|
||||
|
||||
@@ -151,3 +151,4 @@ XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultN
|
||||
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(TokenPaychan, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Quantum, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Passkey, Supported::Yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -693,6 +693,13 @@ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({
|
||||
{sfOwnerNode, SoeRequired},
|
||||
{sfSponseeNode, SoeRequired},
|
||||
}))
|
||||
LEDGER_ENTRY(ltPASSKEY_LIST, 0x0091, PasskeyList, passkey_list, ({
|
||||
{sfPreviousTxnID, SoeRequired},
|
||||
{sfPreviousTxnLgrSeq, SoeRequired},
|
||||
{sfOwnerNode, SoeRequired},
|
||||
{sfOwner, SoeRequired},
|
||||
{sfPasskeys, SoeRequired},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
@@ -471,3 +471,10 @@ UNTYPED_SFIELD(sfParameters, ARRAY, 35)
|
||||
TYPED_SFIELD(sfParameterValue, DATA, 1, SField::kSmdDefault)
|
||||
TYPED_SFIELD(sfParameterType, DATATYPE, 1)
|
||||
TYPED_SFIELD(sfContractJson, JSON, 1)
|
||||
TYPED_SFIELD(sfSignCount, UINT32, 87)
|
||||
TYPED_SFIELD(sfPasskeyID, VL, 50)
|
||||
TYPED_SFIELD(sfAuthenticatorData, VL, 51)
|
||||
TYPED_SFIELD(sfClientDataJSON, VL, 52)
|
||||
UNTYPED_SFIELD(sfPasskeySignature, OBJECT, 43, SField::kSmdDefault, SField::kNotSigning)
|
||||
UNTYPED_SFIELD(sfPasskey, OBJECT, 44)
|
||||
UNTYPED_SFIELD(sfPasskeys, ARRAY, 36)
|
||||
|
||||
@@ -1310,3 +1310,11 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify,
|
||||
{sfLedgerSequence, SoeRequired},
|
||||
{sfUNLModifyValidator, SoeRequired},
|
||||
}))
|
||||
TRANSACTION(ttPASSKEY_LIST_SET, 103, PasskeyListSet,
|
||||
({
|
||||
.delegable = Delegation::Delegable,
|
||||
.amendment = featurePasskey,
|
||||
}),
|
||||
({
|
||||
{sfPasskeys, SoeRequired},
|
||||
}))
|
||||
|
||||
@@ -237,4 +237,14 @@ sha512HalfS(Args const&... args)
|
||||
return static_cast<sha512_half_hasher_s::result_type>(h);
|
||||
}
|
||||
|
||||
template <class... Args>
|
||||
sha256_hasher::result_type
|
||||
sha256(Args const&... args)
|
||||
{
|
||||
xrpl::sha256_hasher h;
|
||||
using beast::hash_append;
|
||||
hash_append(h, args...);
|
||||
return static_cast<typename sha256_hasher::result_type>(h);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
218
include/xrpl/protocol_autogen/ledger_entries/PasskeyList.h
Normal file
218
include/xrpl/protocol_autogen/ledger_entries/PasskeyList.h
Normal file
@@ -0,0 +1,218 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBase.h>
|
||||
#include <xrpl/protocol_autogen/LedgerEntryBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
class PasskeyListBuilder;
|
||||
|
||||
/**
|
||||
* @brief Ledger Entry: PasskeyList
|
||||
*
|
||||
* Type: ltPASSKEY_LIST (0x0091)
|
||||
* RPC Name: passkey_list
|
||||
*
|
||||
* Immutable wrapper around SLE providing type-safe field access.
|
||||
* Use PasskeyListBuilder to construct new ledger entries.
|
||||
*/
|
||||
class PasskeyList : public LedgerEntryBase
|
||||
{
|
||||
public:
|
||||
static constexpr LedgerEntryType entryType = ltPASSKEY_LIST;
|
||||
|
||||
/**
|
||||
* @brief Construct a PasskeyList ledger entry wrapper from an existing SLE object.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
explicit PasskeyList(SLE::const_pointer sle)
|
||||
: LedgerEntryBase(std::move(sle))
|
||||
{
|
||||
// Verify ledger entry type
|
||||
if (sle_->getType() != entryType)
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for PasskeyList");
|
||||
}
|
||||
}
|
||||
|
||||
// Ledger entry-specific field getters
|
||||
|
||||
/**
|
||||
* @brief Get sfPreviousTxnID (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT256::type::value_type
|
||||
getPreviousTxnID() const
|
||||
{
|
||||
return this->sle_->at(sfPreviousTxnID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfPreviousTxnLgrSeq (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT32::type::value_type
|
||||
getPreviousTxnLgrSeq() const
|
||||
{
|
||||
return this->sle_->at(sfPreviousTxnLgrSeq);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfOwnerNode (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_UINT64::type::value_type
|
||||
getOwnerNode() const
|
||||
{
|
||||
return this->sle_->at(sfOwnerNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfOwner (SoeRequired)
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
SF_ACCOUNT::type::value_type
|
||||
getOwner() const
|
||||
{
|
||||
return this->sle_->at(sfOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Get sfPasskeys (SoeRequired)
|
||||
* @note This is an untyped field (unknown).
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
STArray const&
|
||||
getPasskeys() const
|
||||
{
|
||||
return this->sle_->getFieldArray(sfPasskeys);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for PasskeyList ledger entries.
|
||||
*
|
||||
* Provides a fluent interface for constructing ledger entries with method chaining.
|
||||
* Uses STObject internally for flexible ledger entry construction.
|
||||
* Inherits common field setters from LedgerEntryBuilderBase.
|
||||
*/
|
||||
class PasskeyListBuilder : public LedgerEntryBuilderBase<PasskeyListBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new PasskeyListBuilder with required fields.
|
||||
* @param previousTxnID The sfPreviousTxnID field value.
|
||||
* @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value.
|
||||
* @param ownerNode The sfOwnerNode field value.
|
||||
* @param owner The sfOwner field value.
|
||||
* @param passkeys The sfPasskeys field value.
|
||||
*/
|
||||
PasskeyListBuilder(std::decay_t<typename SF_UINT256::type::value_type> const& previousTxnID,std::decay_t<typename SF_UINT32::type::value_type> const& previousTxnLgrSeq,std::decay_t<typename SF_UINT64::type::value_type> const& ownerNode,std::decay_t<typename SF_ACCOUNT::type::value_type> const& owner,STArray const& passkeys)
|
||||
: LedgerEntryBuilderBase<PasskeyListBuilder>(ltPASSKEY_LIST)
|
||||
{
|
||||
setPreviousTxnID(previousTxnID);
|
||||
setPreviousTxnLgrSeq(previousTxnLgrSeq);
|
||||
setOwnerNode(ownerNode);
|
||||
setOwner(owner);
|
||||
setPasskeys(passkeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a PasskeyListBuilder from an existing SLE object.
|
||||
* @param sle The existing ledger entry to copy from.
|
||||
* @throws std::runtime_error if the ledger entry type doesn't match.
|
||||
*/
|
||||
PasskeyListBuilder(SLE::const_pointer sle)
|
||||
{
|
||||
if (sle->at(sfLedgerEntryType) != ltPASSKEY_LIST)
|
||||
{
|
||||
throw std::runtime_error("Invalid ledger entry type for PasskeyList");
|
||||
}
|
||||
object_ = *sle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Ledger entry-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfPreviousTxnID (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PasskeyListBuilder&
|
||||
setPreviousTxnID(std::decay_t<typename SF_UINT256::type::value_type> const& value)
|
||||
{
|
||||
object_[sfPreviousTxnID] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfPreviousTxnLgrSeq (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PasskeyListBuilder&
|
||||
setPreviousTxnLgrSeq(std::decay_t<typename SF_UINT32::type::value_type> const& value)
|
||||
{
|
||||
object_[sfPreviousTxnLgrSeq] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfOwnerNode (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PasskeyListBuilder&
|
||||
setOwnerNode(std::decay_t<typename SF_UINT64::type::value_type> const& value)
|
||||
{
|
||||
object_[sfOwnerNode] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfOwner (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PasskeyListBuilder&
|
||||
setOwner(std::decay_t<typename SF_ACCOUNT::type::value_type> const& value)
|
||||
{
|
||||
object_[sfOwner] = value;
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set sfPasskeys (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PasskeyListBuilder&
|
||||
setPasskeys(STArray const& value)
|
||||
{
|
||||
object_.setFieldArray(sfPasskeys, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the completed PasskeyList wrapper.
|
||||
* @param index The ledger entry index.
|
||||
* @return The constructed ledger entry wrapper.
|
||||
*/
|
||||
PasskeyList
|
||||
build(uint256 const& index)
|
||||
{
|
||||
return PasskeyList{std::make_shared<SLE>(std::move(object_), index)};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::ledger_entries
|
||||
131
include/xrpl/protocol_autogen/transactions/PasskeyListSet.h
Normal file
131
include/xrpl/protocol_autogen/transactions/PasskeyListSet.h
Normal file
@@ -0,0 +1,131 @@
|
||||
// This file is auto-generated. Do not edit.
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBase.h>
|
||||
#include <xrpl/protocol_autogen/TransactionBuilderBase.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
class PasskeyListSetBuilder;
|
||||
|
||||
/**
|
||||
* @brief Transaction: PasskeyListSet
|
||||
*
|
||||
* Type: ttPASSKEY_LIST_SET (92)
|
||||
* Delegable: Delegation::Delegable
|
||||
* Amendment: featurePasskey
|
||||
* Privileges: Privilege::NoPriv
|
||||
*
|
||||
* Immutable wrapper around STTx providing type-safe field access.
|
||||
* Use PasskeyListSetBuilder to construct new transactions.
|
||||
*/
|
||||
class PasskeyListSet : public TransactionBase
|
||||
{
|
||||
public:
|
||||
static constexpr xrpl::TxType txType = ttPASSKEY_LIST_SET;
|
||||
|
||||
/**
|
||||
* @brief Construct a PasskeyListSet transaction wrapper from an existing STTx object.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
explicit PasskeyListSet(std::shared_ptr<STTx const> tx)
|
||||
: TransactionBase(std::move(tx))
|
||||
{
|
||||
// Verify transaction type
|
||||
if (tx_->getTxnType() != txType)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for PasskeyListSet");
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction-specific field getters
|
||||
/**
|
||||
* @brief Get sfPasskeys (SoeRequired)
|
||||
* @note This is an untyped field.
|
||||
* @return The field value.
|
||||
*/
|
||||
[[nodiscard]]
|
||||
STArray const&
|
||||
getPasskeys() const
|
||||
{
|
||||
return this->tx_->getFieldArray(sfPasskeys);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder for PasskeyListSet transactions.
|
||||
*
|
||||
* Provides a fluent interface for constructing transactions with method chaining.
|
||||
* Uses STObject internally for flexible transaction construction.
|
||||
* Inherits common field setters from TransactionBuilderBase.
|
||||
*/
|
||||
class PasskeyListSetBuilder : public TransactionBuilderBase<PasskeyListSetBuilder>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new PasskeyListSetBuilder with required fields.
|
||||
* @param account The account initiating the transaction.
|
||||
* @param passkeys The sfPasskeys field value.
|
||||
* @param sequence Optional sequence number for the transaction.
|
||||
* @param fee Optional fee for the transaction.
|
||||
*/
|
||||
PasskeyListSetBuilder(SF_ACCOUNT::type::value_type account,
|
||||
STArray const& passkeys, std::optional<SF_UINT32::type::value_type> sequence = std::nullopt,
|
||||
std::optional<SF_AMOUNT::type::value_type> fee = std::nullopt
|
||||
)
|
||||
: TransactionBuilderBase<PasskeyListSetBuilder>(ttPASSKEY_LIST_SET, account, sequence, fee)
|
||||
{
|
||||
setPasskeys(passkeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct a PasskeyListSetBuilder from an existing STTx object.
|
||||
* @param tx The existing transaction to copy from.
|
||||
* @throws std::runtime_error if the transaction type doesn't match.
|
||||
*/
|
||||
PasskeyListSetBuilder(std::shared_ptr<STTx const> tx)
|
||||
{
|
||||
if (tx->getTxnType() != ttPASSKEY_LIST_SET)
|
||||
{
|
||||
throw std::runtime_error("Invalid transaction type for PasskeyListSetBuilder");
|
||||
}
|
||||
object_ = *tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Transaction-specific field setters
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Set sfPasskeys (SoeRequired)
|
||||
* @return Reference to this builder for method chaining.
|
||||
*/
|
||||
PasskeyListSetBuilder&
|
||||
setPasskeys(STArray const& value)
|
||||
{
|
||||
object_.setFieldArray(sfPasskeys, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build and return the PasskeyListSet wrapper.
|
||||
* @param publicKey The public key for signing.
|
||||
* @param secretKey The secret key for signing.
|
||||
* @return The constructed transaction wrapper.
|
||||
*/
|
||||
PasskeyListSet
|
||||
build(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
sign(publicKey, secretKey);
|
||||
return PasskeyListSet{std::make_shared<STTx>(std::move(object_))};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace xrpl::transactions
|
||||
42
include/xrpl/tx/transactors/account/SetPasskeyList.h
Normal file
42
include/xrpl/tx/transactors/account/SetPasskeyList.h
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
#include <xrpl/tx/ApplyContext.h>
|
||||
#include <xrpl/tx/Transactor.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class SetPasskeyList : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Blocker;
|
||||
|
||||
explicit SetPasskeyList(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
|
||||
void
|
||||
visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override;
|
||||
|
||||
[[nodiscard]] bool
|
||||
finalizeInvariants(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j) override;
|
||||
};
|
||||
|
||||
using PasskeyListSet = SetPasskeyList;
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -205,4 +205,20 @@ base64Decode(std::string_view data)
|
||||
return dest;
|
||||
}
|
||||
|
||||
std::string
|
||||
base64urlDecode(std::string_view data)
|
||||
{
|
||||
std::string b64(data);
|
||||
for (auto& c : b64)
|
||||
{
|
||||
if (c == '-')
|
||||
c = '+';
|
||||
else if (c == '_')
|
||||
c = '/';
|
||||
}
|
||||
while (b64.size() % 4 != 0)
|
||||
b64 += '=';
|
||||
return base64Decode(b64);
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -103,6 +103,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
Vault = 'V',
|
||||
LoanBroker = 'l', // lower-case L
|
||||
Loan = 'L',
|
||||
PasskeyList = 'k',
|
||||
Sponsorship = '>',
|
||||
|
||||
ContractSource = 'Z',
|
||||
@@ -631,6 +632,18 @@ contractData(AccountID const& owner, AccountID const& contractAccount) noexcept
|
||||
return {ltCONTRACT_DATA, indexHash(LedgerNameSpace::ContractData, owner, contractAccount)};
|
||||
}
|
||||
|
||||
static Keylet
|
||||
passkeyList(AccountID const& account, std::uint32_t page) noexcept
|
||||
{
|
||||
return {ltPASSKEY_LIST, indexHash(LedgerNameSpace::PasskeyList, account, page)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
passkeyList(AccountID const& account) noexcept
|
||||
{
|
||||
return passkeyList(account, 0);
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -153,6 +153,22 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfBookNode, SoeRequired},
|
||||
});
|
||||
|
||||
add(sfPasskey.jsonName,
|
||||
sfPasskey.getCode(),
|
||||
{
|
||||
{sfPasskeyID, SoeRequired},
|
||||
{sfPublicKey, SoeRequired},
|
||||
});
|
||||
|
||||
add(sfPasskeySignature.jsonName,
|
||||
sfPasskeySignature.getCode(),
|
||||
{
|
||||
{sfPasskeyID, SoeRequired},
|
||||
{sfAuthenticatorData, SoeRequired},
|
||||
{sfClientDataJSON, SoeRequired},
|
||||
{sfSignature, SoeRequired},
|
||||
});
|
||||
|
||||
add(sfCounterpartySignature.jsonName,
|
||||
sfCounterpartySignature.getCode(),
|
||||
{
|
||||
|
||||
@@ -13,10 +13,16 @@
|
||||
|
||||
#include <boost/multiprecision/number.hpp>
|
||||
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/ecdsa.h>
|
||||
#include <openssl/obj_mac.h>
|
||||
|
||||
#include <ed25519.h>
|
||||
#include <secp256k1.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
@@ -187,10 +193,10 @@ ed25519Canonical(Slice const& sig)
|
||||
|
||||
PublicKey::PublicKey(Slice const& slice)
|
||||
{
|
||||
if (slice.size() < kSize)
|
||||
if (slice.size() > kMaxSize)
|
||||
{
|
||||
logicError(
|
||||
"PublicKey::PublicKey - Input slice cannot be an undersized "
|
||||
"PublicKey::PublicKey - Input slice cannot be an oversized "
|
||||
"buffer");
|
||||
}
|
||||
|
||||
@@ -237,6 +243,9 @@ publicKeyType(Slice const& slice)
|
||||
return KeyType::Dilithium;
|
||||
}
|
||||
|
||||
if (slice.size() == 65 && slice[0] == 0xF6)
|
||||
return KeyType::P256;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -312,6 +321,128 @@ verifyDigest(
|
||||
}
|
||||
}
|
||||
|
||||
struct ECDSASignature
|
||||
{
|
||||
std::array<uint8_t, 32> r;
|
||||
std::array<uint8_t, 32> s;
|
||||
};
|
||||
|
||||
static std::optional<ECDSASignature>
|
||||
parseDERSignature(Slice const& derSig) noexcept
|
||||
{
|
||||
if (derSig.size() < 8)
|
||||
return std::nullopt;
|
||||
|
||||
uint8_t const* data = derSig.data();
|
||||
size_t offset = 0;
|
||||
|
||||
// Check sequence tag
|
||||
if (data[offset++] != 0x30)
|
||||
return std::nullopt;
|
||||
|
||||
// Skip total length
|
||||
offset++;
|
||||
|
||||
// Parse R
|
||||
if (data[offset++] != 0x02)
|
||||
return std::nullopt;
|
||||
uint8_t rLen = data[offset++];
|
||||
if (offset + rLen >= derSig.size())
|
||||
return std::nullopt;
|
||||
|
||||
ECDSASignature result{};
|
||||
|
||||
// Copy R, handling leading zeros
|
||||
int rStart = (rLen > 32 && data[offset] == 0x00) ? 1 : 0;
|
||||
int rCopyLen = std::min(32, static_cast<int>(rLen - rStart));
|
||||
std::memcpy(result.r.data() + (32 - rCopyLen), data + offset + rStart, rCopyLen);
|
||||
offset += rLen;
|
||||
|
||||
// Parse S
|
||||
if (data[offset++] != 0x02)
|
||||
return std::nullopt;
|
||||
uint8_t sLen = data[offset++];
|
||||
if (offset + sLen > derSig.size())
|
||||
return std::nullopt;
|
||||
|
||||
// Copy S, handling leading zeros
|
||||
int sStart = (sLen > 32 && data[offset] == 0x00) ? 1 : 0;
|
||||
int sCopyLen = std::min(32, static_cast<int>(sLen - sStart));
|
||||
std::memcpy(result.s.data() + (32 - sCopyLen), data + offset + sStart, sCopyLen);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
verifyP256ECDSA(
|
||||
uint8_t const* hash,
|
||||
size_t hashLen,
|
||||
uint8_t const* r,
|
||||
size_t rLen,
|
||||
uint8_t const* s,
|
||||
size_t sLen,
|
||||
uint8_t const* x,
|
||||
size_t xLen,
|
||||
uint8_t const* y,
|
||||
size_t yLen) noexcept
|
||||
{
|
||||
if (hashLen != 32 || rLen > 32 || sLen > 32 || xLen > 32 || yLen > 32)
|
||||
return false;
|
||||
|
||||
// Create curve object
|
||||
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1);
|
||||
if (!group)
|
||||
return false;
|
||||
|
||||
// Set group to EC_KEY
|
||||
EC_KEY* key = EC_KEY_new();
|
||||
if (!key)
|
||||
{
|
||||
EC_GROUP_free(group);
|
||||
return false;
|
||||
}
|
||||
EC_KEY_set_group(key, group);
|
||||
|
||||
// Restore public key point from coordinates
|
||||
EC_POINT* point = EC_POINT_new(group);
|
||||
BIGNUM* bnX = BN_bin2bn(x, xLen, nullptr);
|
||||
BIGNUM* bnY = BN_bin2bn(y, yLen, nullptr);
|
||||
|
||||
bool success = false;
|
||||
if (point && bnX && bnY &&
|
||||
EC_POINT_set_affine_coordinates_GFp(group, point, bnX, bnY, nullptr) == 1 &&
|
||||
EC_KEY_set_public_key(key, point) == 1)
|
||||
{
|
||||
// Pack r/s into ECDSA_SIG structure
|
||||
ECDSA_SIG* sig = ECDSA_SIG_new();
|
||||
BIGNUM* bnR = BN_bin2bn(r, rLen, nullptr);
|
||||
BIGNUM* bnS = BN_bin2bn(s, sLen, nullptr);
|
||||
|
||||
if (sig && bnR && bnS && ECDSA_SIG_set0(sig, bnR, bnS) == 1)
|
||||
{
|
||||
// Verify (ECDSA_SIG_set0 takes ownership of bnR, bnS)
|
||||
int verified = ECDSA_do_verify(hash, hashLen, sig, key);
|
||||
success = (verified == 1);
|
||||
bnR = nullptr; // ownership transferred
|
||||
bnS = nullptr; // ownership transferred
|
||||
}
|
||||
|
||||
ECDSA_SIG_free(sig);
|
||||
if (bnR)
|
||||
BN_free(bnR);
|
||||
if (bnS)
|
||||
BN_free(bnS);
|
||||
}
|
||||
|
||||
EC_POINT_free(point);
|
||||
BN_free(bnX);
|
||||
BN_free(bnY);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool
|
||||
verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept
|
||||
{
|
||||
@@ -338,6 +469,37 @@ verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept
|
||||
size_t ctxlen = 0;
|
||||
return crypto_sign_verify(sig.data(), sig.size(), m.data(), m.size(), ctx, ctxlen, publicKey.data()) == 0;
|
||||
}
|
||||
if (*type == KeyType::P256)
|
||||
{
|
||||
// Parse DER signature to extract r and s values
|
||||
auto parsedSig = parseDERSignature(sig);
|
||||
if (!parsedSig)
|
||||
return false;
|
||||
|
||||
// Hash the message with SHA-256 (P-256 uses ECDSA-SHA256)
|
||||
auto hash = sha256(m);
|
||||
|
||||
// We internally prefix P-256 keys with a prefix byte
|
||||
// so strip it to get the raw public key coordinates
|
||||
if (publicKey.size() != 65) // 1 prefix + 32-byte x + 32-byte y
|
||||
return false;
|
||||
|
||||
// Extract x and y coordinates (skip prefix byte)
|
||||
uint8_t const* xCoord = publicKey.data() + 1;
|
||||
uint8_t const* yCoord = publicKey.data() + 33;
|
||||
|
||||
return verifyP256ECDSA(
|
||||
hash.data(),
|
||||
hash.size(),
|
||||
parsedSig->r.data(),
|
||||
32, // r component
|
||||
parsedSig->s.data(),
|
||||
32, // s component
|
||||
xCoord,
|
||||
32, // x coordinate
|
||||
yCoord,
|
||||
32); // y coordinate
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/safe_cast.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/utility/Zero.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Batch.h>
|
||||
@@ -30,6 +32,7 @@
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/TxFormats.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <boost/container/flat_set.hpp>
|
||||
@@ -187,6 +190,14 @@ STTx::getSignature(STObject const& sigObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
|
||||
if (publicKeyType(makeSlice(spk)) == KeyType::P256 &&
|
||||
sigObject.isFieldPresent(sfPasskeySignature))
|
||||
{
|
||||
auto const& passkeySig =
|
||||
static_cast<STObject const&>(sigObject.peekAtField(sfPasskeySignature));
|
||||
return passkeySig.getFieldVL(sfSignature);
|
||||
}
|
||||
return sigObject.getFieldVL(sfTxnSignature);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
@@ -430,6 +441,93 @@ STTx::getMetaSQL(
|
||||
escapedMetaData);
|
||||
}
|
||||
|
||||
/** Verify a P-256 passkey signature with WebAuthn challenge validation.
|
||||
Validates that the clientDataJSON challenge matches the expected signing
|
||||
data, then verifies the ECDSA signature against the WebAuthn authenticator
|
||||
data. Returns true if the signature is valid, false otherwise.
|
||||
*/
|
||||
static bool
|
||||
verifyPasskeySignature(
|
||||
Slice const& publicKey,
|
||||
STObject const& passkeySig,
|
||||
Slice const& expectedSigningData) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const authenticatorData = passkeySig.getFieldVL(sfAuthenticatorData);
|
||||
auto const clientDataJSON = passkeySig.getFieldVL(sfClientDataJSON);
|
||||
|
||||
// Validate that the WebAuthn challenge in clientDataJSON matches
|
||||
// the transaction signing data. Without this, a passkey signature
|
||||
// from any website could be replayed to authorize transactions.
|
||||
std::string const cdj(clientDataJSON.begin(), clientDataJSON.end());
|
||||
json::Value parsed;
|
||||
json::Reader reader;
|
||||
if (!reader.parse(cdj, parsed) || !parsed.isObject())
|
||||
return false;
|
||||
|
||||
// Verify type is "webauthn.get"
|
||||
if (!parsed.isMember("type") || parsed["type"].asString() != "webauthn.get")
|
||||
return false;
|
||||
|
||||
// Verify challenge field exists
|
||||
if (!parsed.isMember("challenge") || !parsed["challenge"].isString())
|
||||
return false;
|
||||
|
||||
// Decode the base64url-encoded challenge and compare
|
||||
auto const challengeBytes = base64urlDecode(parsed["challenge"].asString());
|
||||
if (challengeBytes.size() != expectedSigningData.size() ||
|
||||
!std::equal(challengeBytes.begin(), challengeBytes.end(), expectedSigningData.data()))
|
||||
return false;
|
||||
|
||||
// Build WebAuthn signing data: authenticatorData || SHA-256(clientDataJSON)
|
||||
auto const clientDataHash = sha256(makeSlice(clientDataJSON));
|
||||
|
||||
Blob signingData(authenticatorData.begin(), authenticatorData.end());
|
||||
signingData.insert(
|
||||
signingData.end(),
|
||||
clientDataHash.data(),
|
||||
clientDataHash.data() + clientDataHash.size());
|
||||
|
||||
Blob const signature = passkeySig.getFieldVL(sfSignature);
|
||||
return verify(PublicKey(publicKey), makeSlice(signingData), makeSlice(signature));
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Verify a signature on a signing object.
|
||||
Handles both standard signatures (sfTxnSignature) and P-256 passkey
|
||||
signatures (sfPasskeySignature with WebAuthn challenge validation).
|
||||
*/
|
||||
static bool
|
||||
verifySigObject(STObject const& sigObject, Slice const& data) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
|
||||
auto const keyType = publicKeyType(makeSlice(spk));
|
||||
if (!keyType)
|
||||
return false;
|
||||
|
||||
if (*keyType == KeyType::P256 && sigObject.isFieldPresent(sfPasskeySignature))
|
||||
{
|
||||
auto const& passkeySig =
|
||||
static_cast<STObject const&>(sigObject.peekAtField(sfPasskeySignature));
|
||||
return verifyPasskeySignature(makeSlice(spk), passkeySig, data);
|
||||
}
|
||||
|
||||
Blob const signature = sigObject.getFieldVL(sfTxnSignature);
|
||||
return verify(PublicKey(makeSlice(spk)), data, makeSlice(signature));
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static std::expected<void, std::string>
|
||||
singleSignHelper(STObject const& sigObject, Slice const& data)
|
||||
{
|
||||
@@ -439,22 +537,7 @@ singleSignHelper(STObject const& sigObject, Slice const& data)
|
||||
if (sigObject.isFieldPresent(sfSigners))
|
||||
return std::unexpected("Cannot both single- and multi-sign.");
|
||||
|
||||
bool validSig = false;
|
||||
try
|
||||
{
|
||||
auto const spk = sigObject.getFieldVL(sfSigningPubKey);
|
||||
if (publicKeyType(makeSlice(spk)))
|
||||
{
|
||||
Blob const signature = sigObject.getFieldVL(sfTxnSignature);
|
||||
validSig = verify(PublicKey(makeSlice(spk)), data, makeSlice(signature));
|
||||
}
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
validSig = false;
|
||||
}
|
||||
|
||||
if (!validSig)
|
||||
if (!verifySigObject(sigObject, data))
|
||||
return std::unexpected("Invalid signature.");
|
||||
|
||||
return {};
|
||||
@@ -528,13 +611,8 @@ multiSignHelper(
|
||||
std::optional<std::string> errorWhat;
|
||||
try
|
||||
{
|
||||
auto spk = signer.getFieldVL(sfSigningPubKey);
|
||||
if (publicKeyType(makeSlice(spk)))
|
||||
{
|
||||
Blob const signature = signer.getFieldVL(sfTxnSignature);
|
||||
validSig = verify(
|
||||
PublicKey(makeSlice(spk)), makeMsg(accountID).slice(), makeSlice(signature));
|
||||
}
|
||||
auto const msgSerializer = makeMsg(accountID);
|
||||
validSig = verifySigObject(signer, msgSerializer.slice());
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
|
||||
#include <boost/utility/string_view.hpp>
|
||||
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/ec.h>
|
||||
#include <openssl/ecdsa.h>
|
||||
#include <openssl/obj_mac.h>
|
||||
|
||||
#include <ed25519.h>
|
||||
#include <secp256k1.h>
|
||||
|
||||
@@ -376,6 +381,84 @@ sign(PublicKey const& pk, SecretKey const& sk, Slice const& m)
|
||||
crypto_sign_signature(sig, &len, m.data(), m.size(), ctx, ctxlen, sk.data());
|
||||
return Buffer{sig, len};
|
||||
}
|
||||
case KeyType::P256: {
|
||||
// Hash the message with SHA-256 (P-256 uses ECDSA-SHA256)
|
||||
auto digest = sha256(m);
|
||||
|
||||
// Create curve object
|
||||
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1);
|
||||
if (!group)
|
||||
logicError("sign: EC_GROUP_new_by_curve_name failed");
|
||||
|
||||
// Create EC_KEY and set the group
|
||||
EC_KEY* key = EC_KEY_new();
|
||||
if (!key)
|
||||
{
|
||||
EC_GROUP_free(group);
|
||||
logicError("sign: EC_KEY_new failed");
|
||||
}
|
||||
|
||||
if (EC_KEY_set_group(key, group) != 1)
|
||||
{
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("sign: EC_KEY_set_group failed");
|
||||
}
|
||||
|
||||
// Convert secret key to BIGNUM and set as private key
|
||||
BIGNUM* privKey =
|
||||
BN_bin2bn(reinterpret_cast<unsigned char const*>(sk.data()), sk.size(), nullptr);
|
||||
|
||||
if (!privKey || EC_KEY_set_private_key(key, privKey) != 1)
|
||||
{
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("sign: failed to set private key");
|
||||
}
|
||||
|
||||
// Sign the digest
|
||||
ECDSA_SIG* sigObj = ECDSA_do_sign(
|
||||
reinterpret_cast<unsigned char const*>(digest.data()), digest.size(), key);
|
||||
|
||||
if (!sigObj)
|
||||
{
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("sign: ECDSA_do_sign failed");
|
||||
}
|
||||
|
||||
// Convert signature to DER format
|
||||
unsigned char sig[72];
|
||||
int len = i2d_ECDSA_SIG(sigObj, nullptr);
|
||||
if (len <= 0 || len > 72)
|
||||
{
|
||||
ECDSA_SIG_free(sigObj);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("sign: i2d_ECDSA_SIG length check failed");
|
||||
}
|
||||
|
||||
unsigned char* sigPtr = sig;
|
||||
if (i2d_ECDSA_SIG(sigObj, &sigPtr) != len)
|
||||
{
|
||||
ECDSA_SIG_free(sigObj);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("sign: i2d_ECDSA_SIG serialization failed");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
ECDSA_SIG_free(sigObj);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
|
||||
return Buffer{sig, static_cast<size_t>(len)};
|
||||
}
|
||||
default:
|
||||
logicError("sign: invalid type");
|
||||
}
|
||||
@@ -555,6 +638,14 @@ generateSecretKey(KeyType type, Seed const& seed)
|
||||
return sk;
|
||||
}
|
||||
|
||||
if (type == KeyType::P256)
|
||||
{
|
||||
auto key = detail::deriveDeterministicRootKey(seed);
|
||||
SecretKey const sk{Slice{key.data(), key.size()}};
|
||||
secureErase(key.data(), key.size());
|
||||
return sk;
|
||||
}
|
||||
|
||||
logicError("generateSecretKey: unknown key type");
|
||||
}
|
||||
|
||||
@@ -593,6 +684,119 @@ derivePublicKey(KeyType type, SecretKey const& sk)
|
||||
|
||||
return PublicKey{Slice{pk_data, CRYPTO_PUBLICKEYBYTES}};
|
||||
}
|
||||
case KeyType::P256: {
|
||||
// Create curve object
|
||||
EC_GROUP* group = EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1);
|
||||
if (!group)
|
||||
logicError("derivePublicKey: EC_GROUP_new_by_curve_name failed");
|
||||
|
||||
// Create EC_KEY and set the group
|
||||
EC_KEY* key = EC_KEY_new();
|
||||
if (!key)
|
||||
{
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: EC_KEY_new failed");
|
||||
}
|
||||
|
||||
if (EC_KEY_set_group(key, group) != 1)
|
||||
{
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: EC_KEY_set_group failed");
|
||||
}
|
||||
|
||||
// Convert secret key to BIGNUM
|
||||
BIGNUM* privKey =
|
||||
BN_bin2bn(reinterpret_cast<unsigned char const*>(sk.data()), sk.size(), nullptr);
|
||||
|
||||
if (!privKey)
|
||||
{
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: BN_bin2bn failed");
|
||||
}
|
||||
|
||||
// Set the private key
|
||||
if (EC_KEY_set_private_key(key, privKey) != 1)
|
||||
{
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: EC_KEY_set_private_key failed");
|
||||
}
|
||||
|
||||
// Generate the public key from the private key
|
||||
EC_POINT* pubKeyPoint = EC_POINT_new(group);
|
||||
if (!pubKeyPoint)
|
||||
{
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: EC_POINT_new failed");
|
||||
}
|
||||
|
||||
if (EC_POINT_mul(group, pubKeyPoint, privKey, nullptr, nullptr, nullptr) != 1)
|
||||
{
|
||||
EC_POINT_free(pubKeyPoint);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: EC_POINT_mul failed");
|
||||
}
|
||||
|
||||
// Extract x and y coordinates
|
||||
BIGNUM* x = BN_new();
|
||||
BIGNUM* y = BN_new();
|
||||
if (!x || !y ||
|
||||
EC_POINT_get_affine_coordinates_GFp(group, pubKeyPoint, x, y, nullptr) != 1)
|
||||
{
|
||||
BN_free(x);
|
||||
BN_free(y);
|
||||
EC_POINT_free(pubKeyPoint);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: EC_POINT_get_affine_coordinates_GFp failed");
|
||||
}
|
||||
|
||||
// Convert coordinates to bytes
|
||||
unsigned char buf[65]; // 1 prefix + 32-byte x + 32-byte y
|
||||
buf[0] = 0xF6; // P-256 prefix byte
|
||||
|
||||
// Convert x coordinate to 32 bytes
|
||||
if (BN_bn2binpad(x, &buf[1], 32) != 32)
|
||||
{
|
||||
BN_free(x);
|
||||
BN_free(y);
|
||||
EC_POINT_free(pubKeyPoint);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: BN_bn2binpad failed for x coordinate");
|
||||
}
|
||||
|
||||
// Convert y coordinate to 32 bytes
|
||||
if (BN_bn2binpad(y, &buf[33], 32) != 32)
|
||||
{
|
||||
BN_free(x);
|
||||
BN_free(y);
|
||||
EC_POINT_free(pubKeyPoint);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
logicError("derivePublicKey: BN_bn2binpad failed for y coordinate");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
BN_free(x);
|
||||
BN_free(y);
|
||||
EC_POINT_free(pubKeyPoint);
|
||||
BN_free(privKey);
|
||||
EC_KEY_free(key);
|
||||
EC_GROUP_free(group);
|
||||
|
||||
return PublicKey{Slice{buf, sizeof(buf)}};
|
||||
}
|
||||
default:
|
||||
logicError("derivePublicKey: bad key type");
|
||||
};
|
||||
@@ -607,6 +811,10 @@ generateKeyPair(KeyType type, Seed const& seed)
|
||||
detail::Generator const g(seed);
|
||||
return g(0);
|
||||
}
|
||||
case KeyType::P256: {
|
||||
auto const sk = generateSecretKey(type, seed);
|
||||
return {derivePublicKey(type, sk), sk};
|
||||
}
|
||||
case KeyType::Ed25519: {
|
||||
auto const sk = generateSecretKey(type, seed);
|
||||
return {derivePublicKey(type, sk), sk};
|
||||
|
||||
@@ -30,6 +30,7 @@ TxFormats::getCommonFields()
|
||||
{sfSigners, SoeOptional}, // submit_multisigned
|
||||
{sfNetworkID, SoeOptional},
|
||||
{sfDelegate, SoeOptional},
|
||||
{sfPasskeySignature, SoeOptional},
|
||||
{sfSponsor, SoeOptional},
|
||||
{sfSponsorFlags, SoeOptional},
|
||||
{sfSponsorSignature, SoeOptional},
|
||||
|
||||
@@ -1051,6 +1051,28 @@ Transactor::checkSingleSign(
|
||||
return tefMASTER_DISABLED;
|
||||
}
|
||||
|
||||
// Check passkey list.
|
||||
{
|
||||
std::shared_ptr<STLedgerEntry const> slePasskeyList =
|
||||
view.read(keylet::passkeyList(idAccount));
|
||||
if (slePasskeyList)
|
||||
{
|
||||
auto const passkeys =
|
||||
slePasskeyList->getFieldArray(sfPasskeys);
|
||||
auto hasMatchingPasskey = std::any_of(
|
||||
passkeys.begin(),
|
||||
passkeys.end(),
|
||||
[&idSigner](STObject const& passkey) {
|
||||
return passkey.isFieldPresent(sfPublicKey) &&
|
||||
calcAccountID(PublicKey(makeSlice(
|
||||
passkey.getFieldVL(sfPublicKey)))) ==
|
||||
idSigner;
|
||||
});
|
||||
if (hasMatchingPasskey)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
// Signed with any other key.
|
||||
return tefBAD_AUTH;
|
||||
}
|
||||
|
||||
118
src/libxrpl/tx/transactors/account/SetPasskeyList.cpp
Normal file
118
src/libxrpl/tx/transactors/account/SetPasskeyList.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include <xrpl/tx/transactors/account/SetPasskeyList.h>
|
||||
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
|
||||
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
NotTEC
|
||||
SetPasskeyList::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
auto const& passkeys = ctx.tx.getFieldArray(sfPasskeys);
|
||||
|
||||
if (passkeys.empty())
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "SetPasskeyList: empty passkeys array.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// Validate each passkey entry and check for duplicates
|
||||
std::set<Blob> seenPasskeyIDs;
|
||||
std::set<Blob> seenPublicKeys;
|
||||
for (auto const& passkey : passkeys)
|
||||
{
|
||||
if (!passkey.isFieldPresent(sfPasskeyID) || !passkey.isFieldPresent(sfPublicKey))
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "SetPasskeyList: missing required fields.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// Check for duplicate PasskeyIDs
|
||||
auto const passkeyID = passkey.getFieldVL(sfPasskeyID);
|
||||
if (!seenPasskeyIDs.insert(passkeyID).second)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "SetPasskeyList: duplicate PasskeyID.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// Check for duplicate PublicKeys
|
||||
auto const pk = passkey.getFieldVL(sfPublicKey);
|
||||
if (!seenPublicKeys.insert(pk).second)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "SetPasskeyList: duplicate PublicKey.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// Validate public key is a valid P256 key
|
||||
auto const keyType = publicKeyType(makeSlice(pk));
|
||||
if (!keyType || *keyType != KeyType::P256)
|
||||
{
|
||||
JLOG(ctx.j.debug()) << "SetPasskeyList: invalid P256 public key.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
SetPasskeyList::doApply()
|
||||
{
|
||||
auto viewJ = ctx_.registry.get().getJournal("View");
|
||||
auto const sleAccount = ctx_.view().peek(keylet::account(accountID_));
|
||||
if (!sleAccount)
|
||||
return tecINTERNAL;
|
||||
|
||||
auto const passkeyKeylet = keylet::passkeyList(accountID_);
|
||||
auto sle = std::make_shared<SLE>(passkeyKeylet);
|
||||
sle->setAccountID(sfOwner, ctx_.tx.getAccountID(sfAccount));
|
||||
auto const& passkeys = ctx_.tx.getFieldArray(sfPasskeys);
|
||||
sle->setFieldArray(sfPasskeys, passkeys);
|
||||
|
||||
auto page = ctx_.view().dirInsert(
|
||||
keylet::ownerDir(accountID_), sle->key(), describeOwnerDir(accountID_));
|
||||
if (!page)
|
||||
return tecDIR_FULL; // LCOV_EXCL_LINE
|
||||
|
||||
(*sle)[sfOwnerNode] = *page;
|
||||
|
||||
increaseOwnerCount(ctx_.view(), sleAccount, {}, 1, viewJ);
|
||||
|
||||
ctx_.view().insert(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
SetPasskeyList::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
}
|
||||
|
||||
bool
|
||||
SetPasskeyList::finalizeInvariants(
|
||||
STTx const&,
|
||||
TER,
|
||||
XRPAmount,
|
||||
ReadView const&,
|
||||
beast::Journal const&)
|
||||
{
|
||||
// No transaction-specific invariants yet (future work).
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
338
src/test/app/PasskeyListSet_test.cpp
Normal file
338
src/test/app/PasskeyListSet_test.cpp
Normal file
@@ -0,0 +1,338 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 XRPL-Labs.
|
||||
|
||||
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 <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/multisign.h>
|
||||
#include <test/jtx/noop.h>
|
||||
#include <test/jtx/pay.h>
|
||||
#include <test/jtx/sig.h>
|
||||
#include <test/jtx/ter.h>
|
||||
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class PasskeyListSet_test : public beast::unit_test::Suite
|
||||
{
|
||||
json::Value
|
||||
passkeyListSet(test::jtx::Account const& account)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfAccount.jsonName] = account.human();
|
||||
jv[sfTransactionType.jsonName] = jss::PasskeyListSet;
|
||||
jv[sfPasskeys.jsonName] = json::ValueType::Array;
|
||||
jv[sfPasskeys.jsonName][0u][sfPasskey.jsonName][sfPasskeyID.jsonName] = "DEADBEEF";
|
||||
jv[sfPasskeys.jsonName][0u][sfPasskey.jsonName][sfPublicKey.jsonName] =
|
||||
strHex(account.pk());
|
||||
return jv;
|
||||
}
|
||||
|
||||
json::Value
|
||||
passkeyListSetMulti(
|
||||
test::jtx::Account const& account,
|
||||
std::vector<std::pair<std::string, std::string>> const& entries)
|
||||
{
|
||||
json::Value jv;
|
||||
jv[sfAccount.jsonName] = account.human();
|
||||
jv[sfTransactionType.jsonName] = jss::PasskeyListSet;
|
||||
jv[sfPasskeys.jsonName] = json::ValueType::Array;
|
||||
for (json::UInt i = 0; i < entries.size(); ++i)
|
||||
{
|
||||
jv[sfPasskeys.jsonName][i][sfPasskey.jsonName][sfPasskeyID.jsonName] =
|
||||
entries[i].first;
|
||||
jv[sfPasskeys.jsonName][i][sfPasskey.jsonName][sfPublicKey.jsonName] =
|
||||
entries[i].second;
|
||||
}
|
||||
return jv;
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
testBasicPasskeyListSet(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("basic passkey list set");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
env(passkeyListSet(alice));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testValidMultiplePasskeys(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("valid multiple passkeys");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
Account const bob{"bob", KeyType::P256};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Two valid entries with different IDs and different PublicKeys
|
||||
auto jv = passkeyListSetMulti(
|
||||
alice, {{"DEADBEEF01", strHex(alice.pk())}, {"DEADBEEF02", strHex(bob.pk())}});
|
||||
env(jv);
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testEmptyPasskeyList(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("empty passkey list rejected");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
json::Value jv;
|
||||
jv[sfAccount.jsonName] = alice.human();
|
||||
jv[sfTransactionType.jsonName] = jss::PasskeyListSet;
|
||||
jv[sfPasskeys.jsonName] = json::ValueType::Array;
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testDuplicatePasskeyID(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("duplicate passkey ID rejected");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
Account const bob{"bob", KeyType::P256};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Two entries with the same PasskeyID but different PublicKeys
|
||||
auto jv = passkeyListSetMulti(
|
||||
alice, {{"DEADBEEF", strHex(alice.pk())}, {"DEADBEEF", strHex(bob.pk())}});
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testDuplicatePublicKey(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("duplicate public key rejected");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// Two entries with different PasskeyIDs but the same PublicKey
|
||||
auto jv = passkeyListSetMulti(
|
||||
alice, {{"DEADBEEF01", strHex(alice.pk())}, {"DEADBEEF02", strHex(alice.pk())}});
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidKeyType(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("non-P256 key rejected");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
Account const bob{"bob"}; // secp256k1
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// A secp256k1 key should be rejected
|
||||
auto jv = passkeyListSetMulti(alice, {{"DEADBEEF", strHex(bob.pk())}});
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testEd25519KeyRejected(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("ed25519 key rejected");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
Account const carol{"carol", KeyType::Ed25519};
|
||||
env.fund(XRP(1000), alice, carol);
|
||||
env.close();
|
||||
|
||||
// An ed25519 key should be rejected
|
||||
auto jv = passkeyListSetMulti(alice, {{"DEADBEEF", strHex(carol.pk())}});
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testInvalidKeyPrefix(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("invalid P256 prefix rejected");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
|
||||
// Create a 65-byte key with wrong prefix (0x04 instead of 0xF6)
|
||||
auto pkHex = strHex(alice.pk());
|
||||
pkHex[0] = '0';
|
||||
pkHex[1] = '4';
|
||||
|
||||
auto jv = passkeyListSetMulti(alice, {{"DEADBEEF", pkHex}});
|
||||
env(jv, Ter(temMALFORMED));
|
||||
}
|
||||
|
||||
void
|
||||
testPasskeyPayment(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("payment with passkey signer");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const dave{"dave", KeyType::P256};
|
||||
env.fund(XRP(1000), alice, bob, dave);
|
||||
env.close();
|
||||
|
||||
// Register dave's P-256 key as a passkey for alice's account.
|
||||
env(passkeyListSetMulti(alice, {{"DEADBEEF", strHex(dave.pk())}}));
|
||||
env(pay(alice, bob, XRP(100)), Sig(dave));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.balance(bob) == XRP(1100));
|
||||
}
|
||||
|
||||
void
|
||||
testMultisignWithP256(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("multisign with P256 signers");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob", KeyType::P256};
|
||||
Account const carol{"carol", KeyType::P256};
|
||||
env.fund(XRP(1000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// Set up a signer list with P-256 accounts
|
||||
env(signers(alice, 1, {{bob, 1}, {carol, 1}}));
|
||||
env.close();
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
// Multi-sign with one P-256 signer
|
||||
env(noop(alice), Msig(bob), Fee(2 * baseFee));
|
||||
env.close();
|
||||
|
||||
// Multi-sign with both P-256 signers
|
||||
env(noop(alice), Msig(bob, carol), Fee(3 * baseFee));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testMultisignMixedKeyTypes(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("multisign with mixed key types including P256");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"}; // secp256k1
|
||||
Account const carol{"carol", KeyType::Ed25519};
|
||||
Account const dave{"dave", KeyType::P256};
|
||||
env.fund(XRP(1000), alice, bob, carol, dave);
|
||||
env.close();
|
||||
|
||||
// Set up a signer list with mixed key types
|
||||
env(signers(alice, 2, {{bob, 1}, {carol, 1}, {dave, 1}}));
|
||||
env.close();
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
|
||||
// Multi-sign with secp256k1 + P-256
|
||||
env(noop(alice), Msig(bob, dave), Fee(3 * baseFee));
|
||||
env.close();
|
||||
|
||||
// Multi-sign with ed25519 + P-256
|
||||
env(noop(alice), Msig(carol, dave), Fee(3 * baseFee));
|
||||
env.close();
|
||||
|
||||
// Multi-sign with all three key types
|
||||
env(noop(alice), Msig(bob, carol, dave), Fee(4 * baseFee));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = testableAmendments();
|
||||
|
||||
testBasicPasskeyListSet(sa);
|
||||
testValidMultiplePasskeys(sa);
|
||||
testEmptyPasskeyList(sa);
|
||||
testDuplicatePasskeyID(sa);
|
||||
testDuplicatePublicKey(sa);
|
||||
testInvalidKeyType(sa);
|
||||
testEd25519KeyRejected(sa);
|
||||
testInvalidKeyPrefix(sa);
|
||||
testPasskeyPayment(sa);
|
||||
testMultisignWithP256(sa);
|
||||
testMultisignMixedKeyTypes(sa);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(PasskeyListSet, app, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
143
src/test/protocol/PassKey_test.cpp
Normal file
143
src/test/protocol/PassKey_test.cpp
Normal file
@@ -0,0 +1,143 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 XRPL-Labs.
|
||||
|
||||
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 <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/amount.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <test/jtx/pay.h>
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class PassKey_test : public beast::unit_test::Suite
|
||||
{
|
||||
void
|
||||
testP256KeyTypeDetection()
|
||||
{
|
||||
testcase("P256 key type requires 0xF6 prefix");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
// Valid P-256 key from the test framework
|
||||
Account const p256acct{"p256acct", KeyType::P256};
|
||||
auto const keyType = publicKeyType(p256acct.pk());
|
||||
BEAST_EXPECT(keyType.has_value());
|
||||
BEAST_EXPECT(*keyType == KeyType::P256);
|
||||
|
||||
// A 65-byte buffer with 0x04 prefix (standard uncompressed EC)
|
||||
// must NOT be accepted as P-256 on XRPL
|
||||
std::array<uint8_t, 65> badKey{};
|
||||
badKey[0] = 0x04;
|
||||
auto const badType = publicKeyType(makeSlice(badKey));
|
||||
BEAST_EXPECT(!badType.has_value());
|
||||
|
||||
// A 65-byte buffer with 0xF6 prefix should be accepted
|
||||
std::array<uint8_t, 65> goodKey{};
|
||||
goodKey[0] = 0xF6;
|
||||
auto const goodType = publicKeyType(makeSlice(goodKey));
|
||||
BEAST_EXPECT(goodType.has_value());
|
||||
BEAST_EXPECT(*goodType == KeyType::P256);
|
||||
|
||||
// Wrong size keys should not be detected as P-256
|
||||
std::array<uint8_t, 33> shortKey{};
|
||||
shortKey[0] = 0xF6;
|
||||
auto const shortType = publicKeyType(makeSlice(shortKey));
|
||||
BEAST_EXPECT(!shortType.has_value() || *shortType != KeyType::P256);
|
||||
|
||||
std::array<uint8_t, 66> longKey{};
|
||||
longKey[0] = 0xF6;
|
||||
auto const longType = publicKeyType(makeSlice(longKey));
|
||||
BEAST_EXPECT(!longType.has_value());
|
||||
}
|
||||
|
||||
void
|
||||
testP256SingleSign(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("P256 single sign");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
env(pay(alice, bob, XRP(100)));
|
||||
env.close();
|
||||
|
||||
// Verify the payment went through
|
||||
BEAST_EXPECT(env.balance(bob) == XRP(1100));
|
||||
}
|
||||
|
||||
void
|
||||
testP256WithOtherKeyTypes(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase("P256 alongside other key types");
|
||||
|
||||
Env env{*this, envconfig(), features};
|
||||
Account const alice{"alice", KeyType::P256};
|
||||
Account const bob{"bob"}; // secp256k1
|
||||
Account const carol{"carol", KeyType::Ed25519};
|
||||
env.fund(XRP(1000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// All key types should work for payments
|
||||
env(pay(alice, bob, XRP(10)));
|
||||
env(pay(bob, carol, XRP(10)));
|
||||
env(pay(carol, alice, XRP(10)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
testP256SingleSign(features);
|
||||
testP256WithOtherKeyTypes(features);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
auto const sa = testableAmendments();
|
||||
|
||||
// Protocol-level tests (no env needed)
|
||||
testP256KeyTypeDetection();
|
||||
|
||||
// Integration tests with env
|
||||
testWithFeats(sa);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(PassKey, protocol, xrpl);
|
||||
|
||||
} // namespace xrpl
|
||||
@@ -0,0 +1,203 @@
|
||||
// Auto-generated unit tests for ledger entry PasskeyList
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/PasskeyList.h>
|
||||
#include <xrpl/protocol_autogen/ledger_entries/Ticket.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::ledger_entries {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed for both the
|
||||
// builder's STObject and the wrapper's SLE.
|
||||
TEST(PasskeyListTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
uint256 const index{1u};
|
||||
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const ownerValue = canonical_ACCOUNT();
|
||||
auto const passkeysValue = canonical_ARRAY();
|
||||
|
||||
PasskeyListBuilder builder{
|
||||
previousTxnIDValue,
|
||||
previousTxnLgrSeqValue,
|
||||
ownerNodeValue,
|
||||
ownerValue,
|
||||
passkeysValue
|
||||
};
|
||||
|
||||
|
||||
builder.setLedgerIndex(index);
|
||||
builder.setFlags(0x1u);
|
||||
|
||||
EXPECT_TRUE(builder.validate());
|
||||
|
||||
auto const entry = builder.build(index);
|
||||
|
||||
EXPECT_TRUE(entry.validate());
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
auto const actual = entry.getPreviousTxnID();
|
||||
expectEqualField(expected, actual, "sfPreviousTxnID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnLgrSeqValue;
|
||||
auto const actual = entry.getPreviousTxnLgrSeq();
|
||||
expectEqualField(expected, actual, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
auto const actual = entry.getOwnerNode();
|
||||
expectEqualField(expected, actual, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerValue;
|
||||
auto const actual = entry.getOwner();
|
||||
expectEqualField(expected, actual, "sfOwner");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = passkeysValue;
|
||||
auto const actual = entry.getPasskeys();
|
||||
expectEqualField(expected, actual, "sfPasskeys");
|
||||
}
|
||||
|
||||
EXPECT_TRUE(entry.hasLedgerIndex());
|
||||
auto const ledgerIndex = entry.getLedgerIndex();
|
||||
ASSERT_TRUE(ledgerIndex.has_value());
|
||||
EXPECT_EQ(*ledgerIndex, index);
|
||||
EXPECT_EQ(entry.getKey(), index);
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an SLE, set fields directly on it, construct a builder
|
||||
// from that SLE, build a new wrapper, and verify all fields (and validate()).
|
||||
TEST(PasskeyListTests, BuilderFromSleRoundTrip)
|
||||
{
|
||||
uint256 const index{2u};
|
||||
|
||||
auto const previousTxnIDValue = canonical_UINT256();
|
||||
auto const previousTxnLgrSeqValue = canonical_UINT32();
|
||||
auto const ownerNodeValue = canonical_UINT64();
|
||||
auto const ownerValue = canonical_ACCOUNT();
|
||||
auto const passkeysValue = canonical_ARRAY();
|
||||
|
||||
auto sle = std::make_shared<SLE>(PasskeyList::entryType, index);
|
||||
|
||||
sle->at(sfPreviousTxnID) = previousTxnIDValue;
|
||||
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
|
||||
sle->at(sfOwnerNode) = ownerNodeValue;
|
||||
sle->at(sfOwner) = ownerValue;
|
||||
sle->setFieldArray(sfPasskeys, passkeysValue);
|
||||
|
||||
PasskeyListBuilder builderFromSle{sle};
|
||||
EXPECT_TRUE(builderFromSle.validate());
|
||||
|
||||
auto const entryFromBuilder = builderFromSle.build(index);
|
||||
|
||||
PasskeyList entryFromSle{sle};
|
||||
EXPECT_TRUE(entryFromBuilder.validate());
|
||||
EXPECT_TRUE(entryFromSle.validate());
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnIDValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getPreviousTxnID();
|
||||
auto const fromBuilder = entryFromBuilder.getPreviousTxnID();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfPreviousTxnID");
|
||||
expectEqualField(expected, fromBuilder, "sfPreviousTxnID");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = previousTxnLgrSeqValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getPreviousTxnLgrSeq();
|
||||
auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq");
|
||||
expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerNodeValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getOwnerNode();
|
||||
auto const fromBuilder = entryFromBuilder.getOwnerNode();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfOwnerNode");
|
||||
expectEqualField(expected, fromBuilder, "sfOwnerNode");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = ownerValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getOwner();
|
||||
auto const fromBuilder = entryFromBuilder.getOwner();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfOwner");
|
||||
expectEqualField(expected, fromBuilder, "sfOwner");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& expected = passkeysValue;
|
||||
|
||||
auto const fromSle = entryFromSle.getPasskeys();
|
||||
auto const fromBuilder = entryFromBuilder.getPasskeys();
|
||||
|
||||
expectEqualField(expected, fromSle, "sfPasskeys");
|
||||
expectEqualField(expected, fromBuilder, "sfPasskeys");
|
||||
}
|
||||
|
||||
EXPECT_EQ(entryFromSle.getKey(), index);
|
||||
EXPECT_EQ(entryFromBuilder.getKey(), index);
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong ledger entry type.
|
||||
TEST(PasskeyListTests, WrapperThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{3u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
// Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq
|
||||
// Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq
|
||||
TicketBuilder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(PasskeyList{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong ledger entry type.
|
||||
TEST(PasskeyListTests, BuilderThrowsOnWrongEntryType)
|
||||
{
|
||||
uint256 const index{4u};
|
||||
|
||||
// Build a valid ledger entry of a different type
|
||||
TicketBuilder wrongBuilder{
|
||||
canonical_ACCOUNT(),
|
||||
canonical_UINT64(),
|
||||
canonical_UINT32(),
|
||||
canonical_UINT256(),
|
||||
canonical_UINT32()};
|
||||
auto wrongEntry = wrongBuilder.build(index);
|
||||
|
||||
EXPECT_THROW(PasskeyListBuilder{wrongEntry.getSle()}, std::runtime_error);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Auto-generated unit tests for transaction PasskeyListSet
|
||||
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <protocol_autogen/TestHelpers.h>
|
||||
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Seed.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol_autogen/transactions/PasskeyListSet.h>
|
||||
#include <xrpl/protocol_autogen/transactions/AccountSet.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xrpl::transactions {
|
||||
|
||||
// 1 & 4) Set fields via builder setters, build, then read them back via
|
||||
// wrapper getters. After build(), validate() should succeed.
|
||||
TEST(TransactionsPasskeyListSetTests, BuilderSettersRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testPasskeyListSet"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 1;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const passkeysValue = canonical_ARRAY();
|
||||
|
||||
PasskeyListSetBuilder builder{
|
||||
accountValue,
|
||||
passkeysValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
// Set optional fields
|
||||
|
||||
auto tx = builder.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(tx.validate(reason)) << reason;
|
||||
|
||||
// Verify signing was applied
|
||||
EXPECT_FALSE(tx.getSigningPubKey().empty());
|
||||
EXPECT_TRUE(tx.hasTxnSignature());
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(tx.getAccount(), accountValue);
|
||||
EXPECT_EQ(tx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(tx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = passkeysValue;
|
||||
auto const actual = tx.getPasskeys();
|
||||
expectEqualField(expected, actual, "sfPasskeys");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
|
||||
// and verify all fields match.
|
||||
TEST(TransactionsPasskeyListSetTests, BuilderFromStTxRoundTrip)
|
||||
{
|
||||
// Generate a deterministic keypair for signing
|
||||
auto const [publicKey, secretKey] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testPasskeyListSetFromTx"));
|
||||
|
||||
// Common transaction fields
|
||||
auto const accountValue = calcAccountID(publicKey);
|
||||
std::uint32_t const sequenceValue = 2;
|
||||
auto const feeValue = canonical_AMOUNT();
|
||||
|
||||
// Transaction-specific field values
|
||||
auto const passkeysValue = canonical_ARRAY();
|
||||
|
||||
// Build an initial transaction
|
||||
PasskeyListSetBuilder initialBuilder{
|
||||
accountValue,
|
||||
passkeysValue,
|
||||
sequenceValue,
|
||||
feeValue
|
||||
};
|
||||
|
||||
|
||||
auto initialTx = initialBuilder.build(publicKey, secretKey);
|
||||
|
||||
// Create builder from existing STTx
|
||||
PasskeyListSetBuilder builderFromTx{initialTx.getSTTx()};
|
||||
|
||||
auto rebuiltTx = builderFromTx.build(publicKey, secretKey);
|
||||
|
||||
std::string reason;
|
||||
EXPECT_TRUE(rebuiltTx.validate(reason)) << reason;
|
||||
|
||||
// Verify common fields
|
||||
EXPECT_EQ(rebuiltTx.getAccount(), accountValue);
|
||||
EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue);
|
||||
EXPECT_EQ(rebuiltTx.getFee(), feeValue);
|
||||
|
||||
// Verify required fields
|
||||
{
|
||||
auto const& expected = passkeysValue;
|
||||
auto const actual = rebuiltTx.getPasskeys();
|
||||
expectEqualField(expected, actual, "sfPasskeys");
|
||||
}
|
||||
|
||||
// Verify optional fields
|
||||
}
|
||||
|
||||
// 3) Verify wrapper throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsPasskeyListSetTests, WrapperThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(PasskeyListSet{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
// 4) Verify builder throws when constructed from wrong transaction type.
|
||||
TEST(TransactionsPasskeyListSetTests, BuilderThrowsOnWrongTxType)
|
||||
{
|
||||
// Build a valid transaction of a different type
|
||||
auto const [pk, sk] =
|
||||
generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder"));
|
||||
auto const account = calcAccountID(pk);
|
||||
|
||||
AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()};
|
||||
auto wrongTx = wrongBuilder.build(pk, sk);
|
||||
|
||||
EXPECT_THROW(PasskeyListSetBuilder{wrongTx.getSTTx()}, std::runtime_error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -720,6 +720,15 @@ parseRippleState(
|
||||
return keylet::trustLine(*id1, *id2, uCurrency).key;
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parsePasskeyList(
|
||||
json::Value const& params,
|
||||
json::StaticString const fieldName,
|
||||
[[maybe_unused]] unsigned const apiVersion)
|
||||
{
|
||||
return parseObjectID(params, fieldName, "hex string");
|
||||
}
|
||||
|
||||
static std::expected<uint256, json::Value>
|
||||
parseSignerList(
|
||||
json::Value const& params,
|
||||
|
||||
Reference in New Issue
Block a user