diff --git a/include/xrpl/basics/base64.h b/include/xrpl/basics/base64.h index 24fd660e654..a82e589b37b 100644 --- a/include/xrpl/basics/base64.h +++ b/include/xrpl/basics/base64.h @@ -53,4 +53,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 diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 79ecd05ad51..f5a5f97a9dc 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -337,6 +337,9 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept; Keylet permissionedDomain(uint256 const& domainID) noexcept; + +Keylet +passkeyList(AccountID const& account) noexcept; } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/include/xrpl/protocol/KeyType.h b/include/xrpl/protocol/KeyType.h index c709eb897ab..007c760be7b 100644 --- a/include/xrpl/protocol/KeyType.h +++ b/include/xrpl/protocol/KeyType.h @@ -8,6 +8,7 @@ namespace xrpl { enum class KeyType { Secp256k1 = 0, Ed25519 = 1, + P256 = 2, }; inline std::optional @@ -19,6 +20,9 @@ keyTypeFromString(std::string const& s) if (s == "ed25519") return KeyType::Ed25519; + if (s == "p256") + return KeyType::P256; + return {}; } @@ -31,6 +35,9 @@ to_string(KeyType type) if (type == KeyType::Ed25519) return "ed25519"; + if (type == KeyType::P256) + return "p256"; + return "INVALID"; } diff --git a/include/xrpl/protocol/PublicKey.h b/include/xrpl/protocol/PublicKey.h index 13db17fc6e1..e7ee78debdb 100644 --- a/include/xrpl/protocol/PublicKey.h +++ b/include/xrpl/protocol/PublicKey.h @@ -35,10 +35,11 @@ namespace xrpl { information needed to determine the cryptosystem parameters used is stored inside the key. - As of this writing two systems are supported: + As of this writing three systems are supported: secp256k1 ed25519 + p256 secp256k1 public keys consist of a 33 byte compressed public key, with the lead byte equal @@ -51,10 +52,12 @@ namespace xrpl { class PublicKey { protected: - // All the constructed public keys are valid, non-empty and contain 33 - // bytes of data. - static constexpr std::size_t kSize = 33; - std::uint8_t buf_[kSize]{}; // should be large enough + // All the constructed public keys are valid and non-empty. secp256k1 + // and ed25519 keys hold 33 bytes of data; uncompressed p256 keys hold + // 65 bytes. + static constexpr std::size_t kMaxSize = 65; + std::uint8_t buf_[kMaxSize]{}; // should be large enough + std::size_t size_ = 0; public: using const_iterator = std::uint8_t const*; @@ -79,10 +82,10 @@ class PublicKey return buf_; } - static std::size_t - size() noexcept + [[nodiscard]] std::size_t + size() const noexcept { - return kSize; + return size_; } [[nodiscard]] const_iterator @@ -100,19 +103,19 @@ class PublicKey [[nodiscard]] const_iterator end() const noexcept { - return buf_ + kSize; + return buf_ + size_; } [[nodiscard]] const_iterator cend() const noexcept { - return buf_ + kSize; + return buf_ + size_; } [[nodiscard]] Slice slice() const noexcept { - return {buf_, kSize}; + return {buf_, size_}; } operator Slice() const noexcept @@ -129,7 +132,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 diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index bfe03a63037..2c3bdb99c6f 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,7 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(Passkey, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 90810e06d2d..d80b8bf7c50 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -621,6 +621,18 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanScale, SoeDefault}, })) +/** A ledger object representing a passkey list. + + \sa keylet::passkeyList + */ +LEDGER_ENTRY(ltPASSKEY_LIST, 0x008A, PasskeyList, passkey_list, ({ + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, + {sfOwnerNode, SoeRequired}, + {sfOwner, SoeRequired}, + {sfPasskeys, SoeRequired}, +})) + /** A ledger object representing a sponsorship. \sa keylet::sponsorship */ diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 4ef76c8b759..8bf36eab3e6 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -119,6 +119,7 @@ TYPED_SFIELD(sfSponsoringOwnerCount, UINT32, 71) TYPED_SFIELD(sfSponsoringAccountCount, UINT32, 72) TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73) TYPED_SFIELD(sfSponsorFlags, UINT32, 74) +TYPED_SFIELD(sfSignCount, UINT32, 75) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -326,6 +327,9 @@ TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43) TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44) TYPED_SFIELD(sfAmountCommitment, VL, 45) TYPED_SFIELD(sfBalanceCommitment, VL, 46) +TYPED_SFIELD(sfPasskeyID, VL, 47) +TYPED_SFIELD(sfAuthenticatorData, VL, 48) +TYPED_SFIELD(sfClientDataJSON, VL, 49) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) @@ -422,6 +426,8 @@ UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning) UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfPasskeySignature, OBJECT, 39, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfPasskey, OBJECT, 40) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -456,3 +462,4 @@ UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30) UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfPasskeys, ARRAY, 32) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index e805596c008..95d674dcb96 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1194,6 +1194,18 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, {sfRemainingOwnerCount, SoeOptional}, })) +/** This transaction type sets the passkey list. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttPASSKEY_LIST_SET, 92, PasskeyListSet, + Delegation::Delegable, + featurePasskey, + NoPriv, + ({ + {sfPasskeys, SoeRequired}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/digest.h b/include/xrpl/protocol/digest.h index c1e70cada2f..d807c301e51 100644 --- a/include/xrpl/protocol/digest.h +++ b/include/xrpl/protocol/digest.h @@ -228,4 +228,14 @@ sha512HalfS(Args const&... args) return static_cast(h); } +template +sha256_hasher::result_type +sha256(Args const&... args) +{ + xrpl::sha256_hasher h; + using beast::hash_append; + hash_append(h, args...); + return static_cast(h); +} + } // namespace xrpl diff --git a/include/xrpl/protocol_autogen/ledger_entries/PasskeyList.h b/include/xrpl/protocol_autogen/ledger_entries/PasskeyList.h new file mode 100644 index 00000000000..196e628ddba --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/PasskeyList.h @@ -0,0 +1,216 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class PasskeyListBuilder; + +/** + * @brief Ledger Entry: PasskeyList + * + * Type: ltPASSKEY_LIST (0x008A) + * 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 +{ +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 const& previousTxnID,std::decay_t const& previousTxnLgrSeq,std::decay_t const& ownerNode,std::decay_t const& owner,STArray const& passkeys) + : LedgerEntryBuilderBase(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 const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + PasskeyListBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + PasskeyListBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfOwner (SoeRequired) + * @return Reference to this builder for method chaining. + */ + PasskeyListBuilder& + setOwner(std::decay_t 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(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/transactions/PasskeyListSet.h b/include/xrpl/protocol_autogen/transactions/PasskeyListSet.h new file mode 100644 index 00000000000..64ee54b5986 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PasskeyListSet.h @@ -0,0 +1,129 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class PasskeyListSetBuilder; + +/** + * @brief Transaction: PasskeyListSet + * + * Type: ttPASSKEY_LIST_SET (90) + * Delegable: Delegation::Delegable + * Amendment: featurePasskey + * Privileges: 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 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 +{ +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 sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(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 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(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/tx/transactors/account/SetPasskeyList.h b/include/xrpl/tx/transactors/account/SetPasskeyList.h new file mode 100644 index 00000000000..3b3abefa5ef --- /dev/null +++ b/include/xrpl/tx/transactors/account/SetPasskeyList.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 diff --git a/src/libxrpl/basics/base64.cpp b/src/libxrpl/basics/base64.cpp index 541ddd0839d..9c0167e83dd 100644 --- a/src/libxrpl/basics/base64.cpp +++ b/src/libxrpl/basics/base64.cpp @@ -217,4 +217,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 diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index ced18d4db66..117af9d050c 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -84,6 +84,7 @@ enum class LedgerNameSpace : std::uint16_t { Vault = 'V', LoanBroker = 'l', // lower-case L Loan = 'L', + PasskeyList = 'k', Sponsorship = '>', // No longer used or supported. Left here to reserve the space to avoid accidental reuse. @@ -592,6 +593,18 @@ permissionedDomain(uint256 const& domainID) noexcept return {ltPERMISSIONED_DOMAIN, domainID}; } +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 diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 0bdb217771c..8ca4609a962 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -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(), { diff --git a/src/libxrpl/protocol/PublicKey.cpp b/src/libxrpl/protocol/PublicKey.cpp index 97948fcae39..8e605e981d3 100644 --- a/src/libxrpl/protocol/PublicKey.cpp +++ b/src/libxrpl/protocol/PublicKey.cpp @@ -13,10 +13,16 @@ #include +#include +#include +#include +#include + #include #include #include +#include #include #include #include @@ -174,21 +180,23 @@ 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"); } if (!publicKeyType(slice)) logicError("PublicKey::PublicKey invalid type"); - std::memcpy(buf_, slice.data(), kSize); + size_ = slice.size(); + std::memcpy(buf_, slice.data(), size_); } -PublicKey::PublicKey(PublicKey const& other) +PublicKey::PublicKey(PublicKey const& other) : size_(other.size_) { - std::memcpy(buf_, other.buf_, kSize); + if (size_) + std::memcpy(buf_, other.buf_, size_); } PublicKey& @@ -196,7 +204,9 @@ PublicKey::operator=(PublicKey const& other) { if (this != &other) { - std::memcpy(buf_, other.buf_, kSize); + size_ = other.size_; + if (size_) + std::memcpy(buf_, other.buf_, size_); } return *this; @@ -216,6 +226,9 @@ publicKeyType(Slice const& slice) return KeyType::Secp256k1; } + if (slice.size() == 65 && slice[0] == 0xF6) + return KeyType::P256; + return std::nullopt; } @@ -267,6 +280,128 @@ verifyDigest( &pubkeyImp) == 1; } +struct ECDSASignature +{ + std::array r; + std::array s; +}; + +static std::optional +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(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(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 { @@ -287,6 +422,37 @@ verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept // first strip that prefix. return ed25519_sign_open(m.data(), m.size(), publicKey.data() + 1, sig.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; } diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index be4ba2e8e54..3c1b2dd242b 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -3,12 +3,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -30,6 +32,7 @@ #include #include #include +#include #include #include @@ -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(sigObject.peekAtField(sfPasskeySignature)); + return passkeySig.getFieldVL(sfSignature); + } return sigObject.getFieldVL(sfTxnSignature); } catch (std::exception const&) @@ -403,31 +414,103 @@ STTx::getMetaSQL( safeCast(status) % rTxn % escapedMetaData); } -static std::expected -singleSignHelper(STObject const& sigObject, Slice const& data) +/** 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 { - // We don't allow both a non-empty sfSigningPubKey and an sfSigners. - // That would allow the transaction to be signed two ways. So if both - // fields are present the signature is invalid. - if (sigObject.isFieldPresent(sfSigners)) - return std::unexpected("Cannot both single- and multi-sign."); + 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; - bool validSig = 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); - if (publicKeyType(makeSlice(spk))) + auto const keyType = publicKeyType(makeSlice(spk)); + if (!keyType) + return false; + + if (*keyType == KeyType::P256 && sigObject.isFieldPresent(sfPasskeySignature)) { - Blob const signature = sigObject.getFieldVL(sfTxnSignature); - validSig = verify(PublicKey(makeSlice(spk)), data, makeSlice(signature)); + auto const& passkeySig = + static_cast(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&) { - validSig = false; + return false; } +} - if (!validSig) +static std::expected +singleSignHelper(STObject const& sigObject, Slice const& data) +{ + // We don't allow both a non-empty sfSigningPubKey and an sfSigners. + // That would allow the transaction to be signed two ways. So if both + // fields are present the signature is invalid. + if (sigObject.isFieldPresent(sfSigners)) + return std::unexpected("Cannot both single- and multi-sign."); + + if (!verifySigObject(sigObject, data)) return std::unexpected("Invalid signature."); return {}; @@ -503,13 +586,8 @@ multiSignHelper( std::optional 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) { diff --git a/src/libxrpl/protocol/SecretKey.cpp b/src/libxrpl/protocol/SecretKey.cpp index f33b1871e1a..45628877201 100644 --- a/src/libxrpl/protocol/SecretKey.cpp +++ b/src/libxrpl/protocol/SecretKey.cpp @@ -17,6 +17,11 @@ #include +#include +#include +#include +#include + #include #include @@ -262,6 +267,84 @@ sign(PublicKey const& pk, SecretKey const& sk, Slice const& m) 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(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(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(len)}; + } default: logicError("sign: invalid type"); } @@ -296,6 +379,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"); } @@ -326,6 +417,119 @@ derivePublicKey(KeyType type, SecretKey const& sk) ed25519_publickey(sk.data(), &buf[1]); return PublicKey(Slice{buf, sizeof(buf)}); } + 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(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"); }; @@ -340,6 +544,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}; + } default: case KeyType::Ed25519: { auto const sk = generateSecretKey(type, seed); diff --git a/src/libxrpl/protocol/TxFormats.cpp b/src/libxrpl/protocol/TxFormats.cpp index e4d4c4b03c6..8fa1401721f 100644 --- a/src/libxrpl/protocol/TxFormats.cpp +++ b/src/libxrpl/protocol/TxFormats.cpp @@ -30,6 +30,7 @@ TxFormats::getCommonFields() {sfSigners, SoeOptional}, // submit_multisigned {sfNetworkID, SoeOptional}, {sfDelegate, SoeOptional}, + {sfPasskeySignature, SoeOptional}, {sfSponsor, SoeOptional}, {sfSponsorFlags, SoeOptional}, {sfSponsorSignature, SoeOptional}, diff --git a/src/libxrpl/tx/Transactor.cpp b/src/libxrpl/tx/Transactor.cpp index 065dead1fd7..218628459c8 100644 --- a/src/libxrpl/tx/Transactor.cpp +++ b/src/libxrpl/tx/Transactor.cpp @@ -1034,6 +1034,28 @@ Transactor::checkSingleSign( return tefMASTER_DISABLED; } + // Check passkey list. + { + std::shared_ptr 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; } diff --git a/src/libxrpl/tx/transactors/account/SetPasskeyList.cpp b/src/libxrpl/tx/transactors/account/SetPasskeyList.cpp new file mode 100644 index 00000000000..4ea162c5301 --- /dev/null +++ b/src/libxrpl/tx/transactors/account/SetPasskeyList.cpp @@ -0,0 +1,118 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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 seenPasskeyIDs; + std::set 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(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; + + adjustOwnerCount(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 diff --git a/src/test/app/PasskeyListSet_test.cpp b/src/test/app/PasskeyListSet_test.cpp new file mode 100644 index 00000000000..50978bfab0d --- /dev/null +++ b/src/test/app/PasskeyListSet_test.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +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::arrayValue; + 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> const& entries) + { + json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfTransactionType.jsonName] = jss::PasskeyListSet; + jv[sfPasskeys.jsonName] = json::arrayValue; + 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::arrayValue; + 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 diff --git a/src/test/protocol/PassKey_test.cpp b/src/test/protocol/PassKey_test.cpp new file mode 100644 index 00000000000..fceaf2900c2 --- /dev/null +++ b/src/test/protocol/PassKey_test.cpp @@ -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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +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 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 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 shortKey{}; + shortKey[0] = 0xF6; + auto const shortType = publicKeyType(makeSlice(shortKey)); + BEAST_EXPECT(!shortType.has_value() || *shortType != KeyType::P256); + + std::array 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 diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/PasskeyListTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/PasskeyListTests.cpp new file mode 100644 index 00000000000..a9139fa4b9c --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/PasskeyListTests.cpp @@ -0,0 +1,203 @@ +// Auto-generated unit tests for ledger entry PasskeyList + + +#include + +#include + +#include +#include +#include + +#include + +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(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); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/PasskeyListSetTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/PasskeyListSetTests.cpp new file mode 100644 index 00000000000..a7095645c62 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/PasskeyListSetTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction PasskeyListSet + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +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); +} + + +} diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 784be779bbe..ede5ffdeee2 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -710,6 +710,15 @@ parseRippleState( return keylet::trustLine(*id1, *id2, uCurrency).key; } +static std::expected +parsePasskeyList( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + return parseObjectID(params, fieldName, "hex string"); +} + static std::expected parseSignerList( json::Value const& params,