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:
Denis Angell
2026-09-13 19:25:42 -04:00
26 changed files with 1943 additions and 29 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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";
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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},
}))

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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(),
{

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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};

View File

@@ -30,6 +30,7 @@ TxFormats::getCommonFields()
{sfSigners, SoeOptional}, // submit_multisigned
{sfNetworkID, SoeOptional},
{sfDelegate, SoeOptional},
{sfPasskeySignature, SoeOptional},
{sfSponsor, SoeOptional},
{sfSponsorFlags, SoeOptional},
{sfSponsorSignature, SoeOptional},

View File

@@ -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;
}

View 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

View 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

View 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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,