Skip to content

fix(crypto): harden ECKey and signature validation#13

Open
Federico2014 wants to merge 1 commit intodevelopfrom
fix/eckey-signature-validation
Open

fix(crypto): harden ECKey and signature validation#13
Federico2014 wants to merge 1 commit intodevelopfrom
fix/eckey-signature-validation

Conversation

@Federico2014
Copy link
Copy Markdown
Owner

@Federico2014 Federico2014 commented Apr 15, 2026

What does this PR do?

Hardens input validation across ECKey, Rsv, SignUtils, and WitnessInitializer to reject invalid key material at API boundaries.

  1. Added strict null and length checks on ECKey.fromPrivate, fromPublicOnly, publicKeyFromPrivate, signatureToKeyBytes, preventing silent truncation of oversized key material
  2. Added EC point validation (isInfinity(), isValid()) in the ECKey constructor to reject invalid public keys; removed redundant duplicate validation from fromPublicOnly()
  3. Removed ECKey.fromPrivateAndPrecalculatedPublic() (unused factory method)
  4. Added class-level documentation on ECKey about signature malleability and BIP-62; cleaned up Javadoc across ECKey.java — removed orphan/duplicate comment blocks, converted /* */ to /** */, removed meaningless @param - / @return - tags, and simplified verbose descriptions
  5. Added isValidPrivateKey() to both ECKey and SM2 for key material validation
  6. Added null and length validation on signature bytes in Rsv.fromSignature() (rejects null or < 65 bytes)
  7. Enforced key material validity in SignUtils before signing
  8. Hardened WitnessInitializer to reject malformed private keys on node startup and zeroes key material in a finally block after use

Why are these changes required?

  • ECKey.fromPrivate(null) silently returned null instead of failing fast, allowing invalid key material to propagate
  • ECKey.fromPublicOnly() accepted infinity points and off-curve points without validation
  • Rsv.fromSignature() lacked null/length checks on signature bytes
  • WitnessInitializer did not zero key material in error paths, risking private key leakage in heap dumps
  • No isValidPrivateKey utility existed for pre-validation of key material

⚠️ Breaking Changes:

  • ECKey.fromPrivate(byte[]) — null/empty input now throws IllegalArgumentException instead of returning null
  • ECKey.fromPrivateAndPrecalculatedPublic() removed
  • ECKey constructor and fromPublicOnly() now validate EC points — invalid public keys throw IllegalArgumentException

This PR has been tested by:

  • Unit Tests — ECKeyTest, WitnessInitializerTest
  • Build passes (./gradlew clean build -x test)

Follow up

Consensus Safety

These changes do not affect consensus. All modifications are defensive input validation at API boundaries — they reject invalid inputs earlier with clear error messages, without changing the behavior for any valid input.

In particular, Rsv.fromSignature() now throws IllegalArgumentException for null or short (< 65 bytes) signature arrays. Since signatures come from protobuf deserialization of network data, a malicious peer could send a short or empty signature. The callers handle this correctly:

  • TransactionCapsule.validateSignature() (transaction verification): already checks sig.size() < 65 before calling, so the new check is redundant here.
  • BlockCapsule.validateSignature() (block witness signature verification): calls getBase64FromByteString() without a length pre-check. Previously a short signature would cause a raw ArrayIndexOutOfBoundsException; now it throws a clear IllegalArgumentException — both are caught by the surrounding try-catch and result in ValidateSignatureException (block rejected), so the outcome is unchanged.
  • PrecompiledContracts.recoverAddrBySign() (TVM ecrecover precompile): checks isEmpty || length < 65 before calling, and wraps in try-catch(Throwable).
  • PbftBaseMessage.analyzeSignature(), RelayService, PbftDataSyncHandler: no length pre-check, but all wrapped in exception handlers. The new validation converts silent ArrayIndexOutOfBoundsException into explicit IllegalArgumentException — same outcome (message rejected).
  • TransactionResult (JSON-RPC layer): read-only API formatting, not in the consensus path.

In all cases, the new validation only changes the exception type for invalid inputs — from an opaque ArrayIndexOutOfBoundsException to a descriptive IllegalArgumentException. No valid signature that previously passed is now rejected, and no consensus outcome changes.

Extra details

Split from #7

Closes #17

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

This PR hardens cryptographic input validation and defensive handling: ECKey/SM2 gained explicit private/public validators, constructors and factories now reject invalid keys, Rsv validates signatures, SignUtils routes validations, WitnessInitializer validates and zeroes keystore bytes, and tests updated accordingly.

Changes

Cohort / File(s) Summary
Core ECKey validation & immutability
crypto/src/main/java/org/tron/common/crypto/ECKey.java
Added isValidPrivateKey(byte[]), isValidPrivateKey(BigInteger), isValidPublicKey(byte[]); constructors/factories now throw IllegalArgumentException for invalid keys; removed fromPrivateAndPrecalculatedPublic(...) and fromPublicOnly(ECPoint); SECP256K1N initialized from curve params; cached byte arrays marked transient volatile and returned defensively.
Signature/Rsv input checks
crypto/src/main/java/org/tron/common/crypto/Rsv.java
fromSignature(byte[]) now validates null and minimum length (>=65) and throws on invalid input.
Validation routing helper
crypto/src/main/java/org/tron/common/crypto/SignUtils.java
Added isValidPrivateKey(byte[], boolean) that delegates to ECKey or SM2 based on engine flag.
SM2 private-key validation
crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
Added isValidPrivateKey(byte[]) enforcing canonical 33-byte rule and numeric range [1, SM2_N).
Keystore handling & zeroing
framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java
initFromKeystore(...) now validates raw privKeyBytes via SignUtils.isValidPrivateKey(...), throws TronError on invalid input, and zeroes privKeyBytes with Arrays.fill() in finally.
Tests updated
framework/src/test/java/org/tron/common/crypto/ECKeyTest.java, framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java
Tests updated to expect IllegalArgumentException for invalid/empty private keys; added tests for zero and curve-order private keys and invalid public key encoding; Mockito stub adjusted to decode hex at invocation time.

Sequence Diagram(s)

sequenceDiagram
    participant WI as WitnessInitializer
    participant SU as SignUtils
    participant ECK as ECKey
    participant SM2 as SM2

    WI->>SU: isValidPrivateKey(privKeyBytes, isECKeyCryptoEngine)
    alt isECKeyCryptoEngine == true
        SU->>ECK: isValidPrivateKey(privKeyBytes)
        ECK-->>SU: true / false
    else isECKeyCryptoEngine == false
        SU->>SM2: isValidPrivateKey(privKeyBytes)
        SM2-->>SU: true / false
    end
    SU-->>WI: validation result
    alt valid
        WI->>WI: convert to hex & add to privateKeys
    else invalid
        WI-->>WI: throw TronError(WITNESS_KEYSTORE_LOAD)
    end
    WI->>WI: Arrays.fill(privKeyBytes, 0) (finally)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Poem

🐰
I hop through bytes both strong and meek,
Rejecting keys that are too weak,
I zero traces when the job is done,
Safe seeds beneath the morning sun,
A rabbit's nod — cryptography snug and sleek.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main objective: hardening ECKey and signature validation through stricter input checks and error handling.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/eckey-signature-validation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java">

<violation number="1" location="crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java:261">
P2: `isValidPrivateKey(byte[])` should enforce private-key byte length/padding rules; currently it accepts malformed overlong encodings if the numeric value is in range.

(Based on your team's feedback about handling BigInteger sign-padding for private key byte arrays.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
@Federico2014 Federico2014 linked an issue Apr 15, 2026 that may be closed by this pull request
@Federico2014 Federico2014 force-pushed the fix/eckey-signature-validation branch from f1f5543 to b4ab742 Compare April 15, 2026 12:58
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java (1)

250-255: Consider aligning SM2.fromPrivate(byte[]) with ECKey.fromPrivate(byte[]).

ECKey.fromPrivate(byte[]) now throws IllegalArgumentException for null/empty input, but SM2.fromPrivate(byte[]) still returns null. This inconsistency could be confusing for callers who expect uniform behavior across crypto engines when using SignUtils.fromPrivate().

♻️ Suggested alignment
 public static SM2 fromPrivate(byte[] privKeyBytes) {
-  if (ByteArray.isEmpty(privKeyBytes)) {
-    return null;
-  }
+  if (!isValidPrivateKey(privKeyBytes)) {
+    throw new IllegalArgumentException("Invalid private key.");
+  }
   return fromPrivate(new BigInteger(1, privKeyBytes));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java` around lines 250 -
255, SM2.fromPrivate(byte[]) currently returns null for null/empty input which
is inconsistent with ECKey.fromPrivate(byte[]) that throws
IllegalArgumentException; change SM2.fromPrivate(byte[]) to validate
privKeyBytes and throw IllegalArgumentException for null/empty inputs (use the
same error message style as ECKey.fromPrivate), then delegate to
fromPrivate(BigInteger) as before. Ensure callers like SignUtils.fromPrivate()
will now receive the exception instead of a null return.
crypto/src/main/java/org/tron/common/crypto/ECKey.java (1)

359-369: Minor inefficiency: EC point validated twice.

fromPublicOnly(byte[]) decodes and validates the point, then the ECKey(BigInteger, ECPoint) constructor (via the provider constructor at line 207) validates the point again. Consider passing the already-validated point directly to avoid redundant isValid() computation.

♻️ Suggested optimization
 public static ECKey fromPublicOnly(byte[] pub) {
   if (ByteArray.isEmpty(pub)) {
     throw new IllegalArgumentException("Public key bytes cannot be null or empty");
   }
   ECPoint point = CURVE.getCurve().decodePoint(pub);
   if (point.isInfinity() || !point.isValid()) {
     throw new IllegalArgumentException(
         "Public key is not a valid point on secp256k1 curve.");
   }
-  return new ECKey(null, point);
+  // Use provider constructor directly to avoid re-validation
+  return new ECKey(TronCastleProvider.getInstance(), null, point);
 }

Note: This would require adjusting the provider constructor to skip re-validation when called internally, or introducing a private constructor. Given the complexity, the current approach is acceptable for correctness over micro-optimization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crypto/src/main/java/org/tron/common/crypto/ECKey.java` around lines 359 -
369, fromPublicOnly(byte[]) decodes and validates the ECPoint then calls the
public ECKey(BigInteger, ECPoint) which re-validates the point; to avoid the
redundant isValid() call add a private/internal constructor (e.g.,
ECKey(ECPoint, boolean skipValidation) or a private ECKeyFromPoint(ECPoint))
that accepts an already-validated ECPoint and bypasses the extra validation,
then have fromPublicOnly(byte[]) call that private constructor while leaving the
public ECKey(BigInteger, ECPoint) unchanged for external callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crypto/src/main/java/org/tron/common/crypto/ECKey.java`:
- Around line 359-369: fromPublicOnly(byte[]) decodes and validates the ECPoint
then calls the public ECKey(BigInteger, ECPoint) which re-validates the point;
to avoid the redundant isValid() call add a private/internal constructor (e.g.,
ECKey(ECPoint, boolean skipValidation) or a private ECKeyFromPoint(ECPoint))
that accepts an already-validated ECPoint and bypasses the extra validation,
then have fromPublicOnly(byte[]) call that private constructor while leaving the
public ECKey(BigInteger, ECPoint) unchanged for external callers.

In `@crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java`:
- Around line 250-255: SM2.fromPrivate(byte[]) currently returns null for
null/empty input which is inconsistent with ECKey.fromPrivate(byte[]) that
throws IllegalArgumentException; change SM2.fromPrivate(byte[]) to validate
privKeyBytes and throw IllegalArgumentException for null/empty inputs (use the
same error message style as ECKey.fromPrivate), then delegate to
fromPrivate(BigInteger) as before. Ensure callers like SignUtils.fromPrivate()
will now receive the exception instead of a null return.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b835b3ae-5885-4654-81e4-a9dba921e594

📥 Commits

Reviewing files that changed from the base of the PR and between 208807d and b4ab742.

📒 Files selected for processing (7)
  • crypto/src/main/java/org/tron/common/crypto/ECKey.java
  • crypto/src/main/java/org/tron/common/crypto/Rsv.java
  • crypto/src/main/java/org/tron/common/crypto/SignUtils.java
  • crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
  • framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java
  • framework/src/test/java/org/tron/common/crypto/ECKeyTest.java
  • framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java

@Federico2014 Federico2014 force-pushed the fix/eckey-signature-validation branch from b4ab742 to 399e6cf Compare April 16, 2026 02:57
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crypto/src/main/java/org/tron/common/crypto/ECKey.java (1)

378-381: ⚠️ Potential issue | 🟠 Major

Reject oversized signatures instead of silently truncating.

Line 378 only rejects signatures shorter than 65 bytes. Inputs longer than 65 bytes are currently accepted and tail bytes are ignored (Lines 386-388), which weakens boundary validation.

🔧 Proposed fix
-    if (signatureEncoded.length < 65) {
-      throw new SignatureException("Signature truncated, expected 65 " +
-          "bytes and got " + signatureEncoded.length);
-    }
+    if (signatureEncoded.length != 65) {
+      throw new SignatureException("Invalid signature length, expected 65 bytes and got "
+          + signatureEncoded.length);
+    }

Also applies to: 386-388

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crypto/src/main/java/org/tron/common/crypto/ECKey.java` around lines 378 -
381, The code in ECKey that currently only checks if signatureEncoded.length <
65 must also reject inputs > 65 instead of silently truncating: update the
validation around signatureEncoded (and the logic that extracts r, s, v from
signatureEncoded) to throw a SignatureException when signatureEncoded.length !=
65, so oversized signatures are refused; reference the signatureEncoded variable
and the existing SignatureException class in ECKey and ensure downstream
extraction code (the block that reads the first 65 bytes into r, s, v) no longer
ignores extra bytes.
🧹 Nitpick comments (1)
framework/src/test/java/org/tron/common/crypto/ECKeyTest.java (1)

72-74: Add one regression test for oversized (>=66-byte) signatures.

You already cover invalid Base64 and truncated signatures; adding an oversized-signature case will guard the strict-length boundary too.

🧪 Suggested test addition
+import java.util.Base64;
...
+  `@Test`(expected = SignatureException.class)
+  public void testOversizedSignatureLength() throws SignatureException {
+    byte[] messageHash = new byte[32];
+    byte[] oversized = new byte[66];
+    oversized[0] = 27; // valid-ish header domain
+    String sigBase64 = Base64.getEncoder().encodeToString(oversized);
+    ECKey.signatureToKey(messageHash, sigBase64);
+    fail("Expecting a SignatureException for oversized signature length");
+  }

Also applies to: 134-146

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/src/test/java/org/tron/common/crypto/ECKeyTest.java` around lines
72 - 74, Add a regression test in ECKeyTest that asserts parsing an oversized
signature (>=66 bytes) fails: create a byte[] of length 66 (or longer),
Base64-encode it, and use the same parsing method exercised by the existing
invalid Base64/truncated-signature tests to
assertThrows(IllegalArgumentException.class). Name the test clearly (e.g.,
testParseSignature_oversized) and add the same assertion in both relevant
locations referenced (around the existing signature-parsing tests and the block
at lines ~134-146) so the strict-length boundary is enforced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@crypto/src/main/java/org/tron/common/crypto/ECKey.java`:
- Around line 378-381: The code in ECKey that currently only checks if
signatureEncoded.length < 65 must also reject inputs > 65 instead of silently
truncating: update the validation around signatureEncoded (and the logic that
extracts r, s, v from signatureEncoded) to throw a SignatureException when
signatureEncoded.length != 65, so oversized signatures are refused; reference
the signatureEncoded variable and the existing SignatureException class in ECKey
and ensure downstream extraction code (the block that reads the first 65 bytes
into r, s, v) no longer ignores extra bytes.

---

Nitpick comments:
In `@framework/src/test/java/org/tron/common/crypto/ECKeyTest.java`:
- Around line 72-74: Add a regression test in ECKeyTest that asserts parsing an
oversized signature (>=66 bytes) fails: create a byte[] of length 66 (or
longer), Base64-encode it, and use the same parsing method exercised by the
existing invalid Base64/truncated-signature tests to
assertThrows(IllegalArgumentException.class). Name the test clearly (e.g.,
testParseSignature_oversized) and add the same assertion in both relevant
locations referenced (around the existing signature-parsing tests and the block
at lines ~134-146) so the strict-length boundary is enforced.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b5cbf8f-6f47-47ba-ad18-ad4d5fff36e7

📥 Commits

Reviewing files that changed from the base of the PR and between b4ab742 and 399e6cf.

📒 Files selected for processing (7)
  • crypto/src/main/java/org/tron/common/crypto/ECKey.java
  • crypto/src/main/java/org/tron/common/crypto/Rsv.java
  • crypto/src/main/java/org/tron/common/crypto/SignUtils.java
  • crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
  • framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java
  • framework/src/test/java/org/tron/common/crypto/ECKeyTest.java
  • framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java
  • crypto/src/main/java/org/tron/common/crypto/Rsv.java
  • crypto/src/main/java/org/tron/common/crypto/SignUtils.java
  • framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java

@Federico2014
Copy link
Copy Markdown
Owner Author

Thanks for the review.

1. Reject oversized signatures (signatureEncoded.length != 65)

This code (line 374, < 65 check) exists on develop and is not modified by this PR — the review itself notes it is "outside diff range." Changing < 65 to != 65 would alter existing behavior and could introduce compatibility risks if any caller passes padded Base64 signatures. I'd prefer to handle this as a separate follow-up if needed.

2. Add oversized-signature test

Since the code behavior is unchanged in this PR, adding a test for the current < 65 semantics would be testing pre-existing logic rather than the changes introduced here. If the strict != 65 check is adopted in a follow-up, the corresponding test should be added at that time.

@Federico2014 Federico2014 force-pushed the fix/eckey-signature-validation branch from 399e6cf to 65537c7 Compare April 16, 2026 03:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Harden ECKey and signature validation

1 participant