-
Notifications
You must be signed in to change notification settings - Fork 18
NestedMultiSign #675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
NestedMultiSign #675
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -369,64 +369,124 @@ STTx::checkMultiSign( | |
| bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || | ||
| (requireCanonicalSig == RequireFullyCanonicalSig::yes); | ||
|
|
||
| // Signers must be in sorted order by AccountID. | ||
| AccountID lastAccountID(beast::zero); | ||
|
|
||
| bool const isWildcardNetwork = | ||
| isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535; | ||
|
|
||
| for (auto const& signer : signers) | ||
| { | ||
| auto const accountID = signer.getAccountID(sfAccount); | ||
| // Set max depth based on feature flag | ||
| int const maxDepth = rules.enabled(featureNestedMultiSign) ? 4 : 1; | ||
|
|
||
| // The account owner may not multisign for themselves. | ||
| if (accountID == txnAccountID) | ||
| return Unexpected("Invalid multisigner."); | ||
| // Define recursive lambda for checking signatures at any depth | ||
| std::function<Expected<void, std::string>( | ||
| STArray const&, AccountID const&, int)> | ||
| checkSignersArray; | ||
|
|
||
| // No duplicate signers allowed. | ||
| if (lastAccountID == accountID) | ||
| return Unexpected("Duplicate Signers not allowed."); | ||
| checkSignersArray = [&](STArray const& signersArray, | ||
| AccountID const& parentAccountID, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unused parentAccuontID |
||
| int depth) -> Expected<void, std::string> { | ||
| // Check depth limit | ||
| if (depth > maxDepth) | ||
| return Unexpected("Multi-signing depth limit exceeded."); | ||
|
|
||
| // Accounts must be in order by account ID. No duplicates allowed. | ||
| if (lastAccountID > accountID) | ||
| return Unexpected("Unsorted Signers array."); | ||
| // There are well known bounds that the number of signers must be | ||
| // within. | ||
| if (signersArray.size() < minMultiSigners || | ||
| signersArray.size() > maxMultiSigners(&rules)) | ||
| return Unexpected("Invalid Signers array size."); | ||
|
|
||
| // The next signature must be greater than this one. | ||
| lastAccountID = accountID; | ||
| // Signers must be in sorted order by AccountID. | ||
| AccountID lastAccountID(beast::zero); | ||
|
|
||
| // Verify the signature. | ||
| bool validSig = false; | ||
| try | ||
| for (auto const& signer : signersArray) | ||
| { | ||
| Serializer s = dataStart; | ||
| finishMultiSigningData(accountID, s); | ||
| auto const accountID = signer.getAccountID(sfAccount); | ||
|
|
||
| // The account owner may not multisign for themselves. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nested signer matching parent account is not rejected in STTx::checkMultiSign There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But parentAccountID (the account being signed for at the current nesting level) is never checked. This means if account B's signer list includes B itself, and B is a nested signer for account A, the recursive call would pass parentAccountID = B but never reject B appearing in its own nested signer array. |
||
| if (accountID == txnAccountID) | ||
| return Unexpected("Invalid multisigner."); | ||
|
|
||
| // No duplicate signers allowed. | ||
| if (lastAccountID == accountID) | ||
| return Unexpected("Duplicate Signers not allowed."); | ||
|
|
||
| // Accounts must be in order by account ID. No duplicates allowed. | ||
| if (lastAccountID > accountID) | ||
| return Unexpected("Unsorted Signers array."); | ||
|
|
||
| auto spk = signer.getFieldVL(sfSigningPubKey); | ||
| // The next signature must be greater than this one. | ||
| lastAccountID = accountID; | ||
|
|
||
| if (publicKeyType(makeSlice(spk))) | ||
| // Check if this signer has nested signers | ||
| if (signer.isFieldPresent(sfSigners)) | ||
| { | ||
| Blob const signature = signer.getFieldVL(sfTxnSignature); | ||
|
|
||
| // wildcard network gets a free pass | ||
| validSig = isWildcardNetwork || | ||
| verify(PublicKey(makeSlice(spk)), | ||
| s.slice(), | ||
| makeSlice(signature), | ||
| fullyCanonical); | ||
| // This is a nested multi-signer | ||
| if (maxDepth == 1) | ||
| { | ||
| // amendment is not enabled, this is an error | ||
| return Unexpected("FeatureNestedMultiSign is disabled"); | ||
| } | ||
|
|
||
| // Ensure it doesn't also have signature fields | ||
| if (signer.isFieldPresent(sfSigningPubKey) || | ||
| signer.isFieldPresent(sfTxnSignature)) | ||
| return Unexpected( | ||
| "Signer cannot have both nested signers and signature " | ||
| "fields."); | ||
|
|
||
| // Recursively check nested signers | ||
| STArray const& nestedSigners = signer.getFieldArray(sfSigners); | ||
| auto result = | ||
| checkSignersArray(nestedSigners, accountID, depth + 1); | ||
| if (!result) | ||
| return result; | ||
| } | ||
| else | ||
| { | ||
| // This is a leaf node - must have signature | ||
| if (!signer.isFieldPresent(sfSigningPubKey) || | ||
| !signer.isFieldPresent(sfTxnSignature)) | ||
| return Unexpected( | ||
| "Leaf signer must have SigningPubKey and " | ||
| "TxnSignature."); | ||
|
|
||
| // Verify the signature | ||
| bool validSig = false; | ||
| try | ||
| { | ||
| Serializer s = dataStart; | ||
| finishMultiSigningData(accountID, s); | ||
|
|
||
| auto spk = signer.getFieldVL(sfSigningPubKey); | ||
|
|
||
| if (publicKeyType(makeSlice(spk))) | ||
| { | ||
| Blob const signature = | ||
| signer.getFieldVL(sfTxnSignature); | ||
|
|
||
| // wildcard network gets a free pass | ||
| validSig = isWildcardNetwork || | ||
| verify(PublicKey(makeSlice(spk)), | ||
| s.slice(), | ||
| makeSlice(signature), | ||
| fullyCanonical); | ||
| } | ||
| } | ||
| catch (std::exception const&) | ||
| { | ||
| // We assume any problem lies with the signature. | ||
| validSig = false; | ||
| } | ||
| if (!validSig) | ||
| return Unexpected( | ||
| std::string("Invalid signature on account ") + | ||
| toBase58(accountID) + "."); | ||
| } | ||
| } | ||
| catch (std::exception const&) | ||
| { | ||
| // We assume any problem lies with the signature. | ||
| validSig = false; | ||
| } | ||
| if (!validSig) | ||
| return Unexpected( | ||
| std::string("Invalid signature on account ") + | ||
| toBase58(accountID) + "."); | ||
| } | ||
| // All signatures verified. | ||
| return {}; | ||
|
|
||
| return {}; | ||
| }; | ||
|
|
||
| // Start the recursive check at depth 1 | ||
| return checkSignersArray(signers, txnAccountID, 1); | ||
| } | ||
|
|
||
| //------------------------------------------------------------------------------ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1183,12 +1183,21 @@ transactionSubmitMultiSigned( | |
| // The Signers array may only contain Signer objects. | ||
| if (std::find_if_not( | ||
| signers.begin(), signers.end(), [](STObject const& obj) { | ||
| return ( | ||
| // A Signer object always contains these fields and no | ||
| // others. | ||
| obj.isFieldPresent(sfAccount) && | ||
| obj.isFieldPresent(sfSigningPubKey) && | ||
| obj.isFieldPresent(sfTxnSignature) && obj.getCount() == 3); | ||
| if (obj.getCount() != 4 || !obj.isFieldPresent(sfAccount)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getCount() != 4 check in TransactionSign.cpp is correct but non-obvious |
||
| return false; | ||
|
Comment on lines
-1186
to
+1187
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove 4 here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // leaf signer | ||
| if (obj.isFieldPresent(sfSigningPubKey) && | ||
| obj.isFieldPresent(sfTxnSignature) && | ||
| !obj.isFieldPresent(sfSigners)) | ||
| return true; | ||
|
|
||
| // nested signer | ||
| if (!obj.isFieldPresent(sfSigningPubKey) && | ||
| !obj.isFieldPresent(sfTxnSignature) && | ||
| obj.isFieldPresent(sfSigners)) | ||
| return true; | ||
|
|
||
| return false; | ||
| }) != signers.end()) | ||
| { | ||
| return RPC::make_param_error( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making sfSigningPubKey and sfTxnSignature optional changes validation for ALL signers, not just nested
The change in InnerObjectFormats.cpp:47-49 from soeREQUIRED to soeOPTIONAL for sfSigningPubKey and sfTxnSignature in the sfSigner inner object format is a global change that affects all signer deserialization, not just when featureNestedMultiSign is enabled. This means that even without the amendment enabled, a malformed signer object missing sfSigningPubKey or sfTxnSignature will now successfully deserialize (whereas before it would throw during applyTemplate). The runtime checks in STTx::checkMultiSign (STTx.cpp:445-449) and Transactor::checkMultiSign (Transactor.cpp:1101-1107) do validate that leaf signers have these fields, so this is caught later. However, the test in STTx_test.cpp:1797-1803 (Test case 2) had to be commented out because it tested that deserialization itself would reject a signer without sfSigningPubKey. This is a defense-in-depth regression—invalid data that was previously rejected at the serialization layer now passes through to application-level validation.