Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions include/xrpl/basics/base64.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function silently truncates output when it encounters invalid base64url characters — the underlying decoder breaks on the first unrecognized byte and returns a shorter string with no indication of failure. Because the return type is std::string, callers cannot distinguish a valid empty decode from a malformed input that produced zero bytes. The current WebAuthn call site in verifyPasskeySignature is safe because it compares both size and content against expectedSigningData, but any future caller that only checks for a non-empty result will silently accept malformed input. Return std::optional<std::string> (nullopt on error) or throw on invalid characters to make failure observable at the API boundary.

Suggested fix

Change the signature to std::optional<std::string> base64urlDecode(std::string_view data) and return std::nullopt if, after character substitution and padding, the decoded byte count is not consistent with the padded input length (i.e., if base64::decode consumed fewer characters than expected). Alternatively, validate that every character in the input is a member of the base64url alphabet (A-Z, a-z, 0-9, -, _) before decoding and return an error for any violation. Update the sole call site in verifyPasskeySignature to treat std::nullopt as a verification failure.


} // namespace xrpl
3 changes: 3 additions & 0 deletions include/xrpl/protocol/Indexes.h
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,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:
Expand Down
7 changes: 7 additions & 0 deletions include/xrpl/protocol/KeyType.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace xrpl {
enum class KeyType {
Secp256k1 = 0,
Ed25519 = 1,
P256 = 2,
};

inline std::optional<KeyType>
Expand All @@ -19,6 +20,9 @@ keyTypeFromString(std::string const& s)
if (s == "ed25519")
return KeyType::Ed25519;

if (s == "p256")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyTypeFromString now returns KeyType::P256 for the string "p256", but the entire downstream signing pipeline — publicKeyType(), verifySigObject(), checkSingleSign(), and checkMultiSign() in both STTx.cpp and Transactor.cpp — accepts P256 keys without ever consulting featurePasskey. The Passkey amendment only gates the PasskeyListSet transaction type, so any other transaction (Payment, TrustSet, OfferCreate) can be signed with a P256 key and accepted by nodes running this code regardless of whether the amendment has activated. Nodes that have not adopted this build will reject those transactions as invalid-signature, causing a ledger fork. Every P256 recognition point in publicKeyType(), verifySigObject(), and Transactor::checkSingleSign() must be gated behind rules.enabled(featurePasskey).

Suggested fix

Gate P256 key recognition inside publicKeyType() behind a Rules parameter checked against featurePasskey — returning nullopt when the amendment is not active causes the entire signing pipeline to reject P256 keys as malformed without needing changes throughout verifySigObject or checkSingleSign. Alternatively, add rules.enabled(featurePasskey) guards in verifySigObject and Transactor::checkSingleSign before the P256 routing branches.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding P256 to keyTypeFromString without updating the allowlist in keypairForSignature (RPCHelpers.cpp) creates an unauthenticated remote crash. A USER-role channel_authorize request with "key_type": "p256" now gets a valid KeyType::P256 optional back from keyTypeFromString, which bypasses the !keyType early-exit guard at line 261 of RPCHelpers.cpp, then hits the hard allowlist if (keyType != KeyType::Secp256k1 && keyType != KeyType::Ed25519) logicError(...) at line 338-339 — which calls std::abort() and takes down the node. The fix is to add KeyType::P256 to that allowlist in keypairForSignature, or implement P256 key-pair generation and signing so the function can actually handle it; the same pattern was confirmed exploitable with KeyType::Dilithium on this codebase.

Suggested fix

In RPCHelpers.cpp keypairForSignature, update the allowlist check to include KeyType::P256. The current guard if (keyType != KeyType::Secp256k1 && keyType != KeyType::Ed25519) logicError(...) must be extended: if (keyType != KeyType::Secp256k1 && keyType != KeyType::Ed25519 && keyType != KeyType::P256) logicError(...). Longer term, replace the allowlist with a proper dispatch table so new KeyType additions cannot silently fall through to abort.

return KeyType::P256;

return {};
}

Expand All @@ -31,6 +35,9 @@ to_string(KeyType type)
if (type == KeyType::Ed25519)
return "ed25519";

if (type == KeyType::P256)
return "p256";

return "INVALID";
}

Expand Down
27 changes: 15 additions & 12 deletions include/xrpl/protocol/PublicKey.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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*;
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions include/xrpl/protocol/detail/features.macro
Original file line number Diff line number Diff line change
Expand Up @@ -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(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialTransfer, Supported::Yes, VoteBehavior::DefaultNo)
Expand Down
12 changes: 12 additions & 0 deletions include/xrpl/protocol/detail/ledger_entries.macro
Original file line number Diff line number Diff line change
Expand Up @@ -616,5 +616,17 @@ 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},
}))

#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE
7 changes: 7 additions & 0 deletions include/xrpl/protocol/detail/sfields.macro
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ TYPED_SFIELD(sfLateInterestRate, UINT32, 66) // 1/10 basis points (bi
TYPED_SFIELD(sfCloseInterestRate, UINT32, 67) // 1/10 basis points (bips)
TYPED_SFIELD(sfOverpaymentInterestRate, UINT32, 68) // 1/10 basis points (bips)
TYPED_SFIELD(sfConfidentialBalanceVersion, UINT32, 69)
TYPED_SFIELD(sfSignCount, UINT32, 70)

// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
Expand Down Expand Up @@ -317,6 +318,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)
Expand Down Expand Up @@ -407,6 +411,8 @@ UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34)
UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35)
UNTYPED_SFIELD(sfBook, OBJECT, 36)
UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning)
UNTYPED_SFIELD(sfPasskeySignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning)
UNTYPED_SFIELD(sfPasskey, OBJECT, 39)

// array of objects (common)
// ARRAY/1 is reserved for end of array
Expand Down Expand Up @@ -441,3 +447,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)
12 changes: 12 additions & 0 deletions include/xrpl/protocol/detail/transactions.macro
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,18 @@ TRANSACTION(ttCONFIDENTIAL_MPT_CLAWBACK, 89, ConfidentialMPTClawback,
{sfZKProof, SoeRequired},
}))

/** This transaction type sets the passkey list. */
#if TRANSACTION_INCLUDE
# include <xrpl/tx/transactors/account/SetPasskeyList.h>
#endif
TRANSACTION(ttPASSKEY_LIST_SET, 90, 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
Expand Down
10 changes: 10 additions & 0 deletions include/xrpl/protocol/digest.h
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,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
Loading