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

Summary by CodeRabbit

Release Notes

  • Security Improvements

    • Enhanced validation of cryptographic keys (private and public) to prevent invalid key material from being accepted
    • Added stricter error handling with explicit exceptions for invalid keys instead of silent failures
    • Improved key initialization security with validation checks before use
  • Breaking Changes

    • Methods now throw exceptions for invalid inputs instead of returning null
    • Removed deprecated factory methods for key creation

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

This PR adds defensive validation for private/public key material and signatures across crypto and framework modules, rejects invalid inputs at API boundaries, ensures keystore-derived private bytes are validated and zeroed, and updates tests to expect exceptions for malformed keys.

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 and factories now throw IllegalArgumentException on invalid keys; removed unused factories; derive SECP256K1N from curve params; make cached arrays transient volatile and return defensive copies.
Signature/Rsv input checks
crypto/src/main/java/org/tron/common/crypto/Rsv.java
fromSignature(byte[]) now checks for null and minimum length (>=65) and throws IllegalArgumentException for invalid input.
Validation routing helper
crypto/src/main/java/org/tron/common/crypto/SignUtils.java
Added isValidPrivateKey(byte[], boolean) delegating to ECKey or SM2 depending on the crypto engine flag.
SM2 private-key validation
crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
Added isValidPrivateKey(byte[]) enforcing canonical length/sign-padding rules and numeric range [1, SM2_N).
Keystore handling & zeroing
framework/src/main/java/org/tron/core/config/args/WitnessInitializer.java
initFromKeystore(...) keeps raw privKeyBytes, validates via SignUtils.isValidPrivateKey(...), throws TronError on invalid input, and zeroes privKeyBytes in a finally block.
Tests updated
framework/src/test/java/org/tron/common/crypto/ECKeyTest.java, framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java, framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java
Tests now assert IllegalArgumentException for null/empty/invalid private keys, add SM2 private-key validation tests, and adjust a Mockito stub to decode hex at invocation.

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

Poem

🐰
I nibble faulty keys with care,
I hush the bytes and hide them there,
I check each point and toss the wrong,
I zero traces when done strong,
A rabbit guards the cryptographic lair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: hardening ECKey and signature validation through defensive input validation.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #17: input validation for ECKey/Rsv/SignUtils/SM2, EC point validation, removal of unused methods, private key zeroing in WitnessInitializer, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are within scope: crypto validation hardening, key material security, WitnessInitializer integration, and supporting test updates; no unrelated modifications detected.

✏️ 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
Copy link
Copy Markdown
Owner Author

@Federico2014 Federico2014 left a comment

Choose a reason for hiding this comment

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

Changes requested (1 [SHOULD] blocker on double-decode; 1 [SHOULD] advisory on zeroization).

Copy link
Copy Markdown
Owner Author

@Federico2014 Federico2014 left a comment

Choose a reason for hiding this comment

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

Changes requested (1 [SHOULD] blocker on double-decode; 1 [SHOULD] advisory on zeroization).

*/
//The parameters of the secp256k1 curve.
public static final ECDomainParameters CURVE;
public static final ECParameterSpec CURVE_SPEC;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[NIT] CURVE and CURVE_SPEC had their /** */ Javadoc converted to a // inline comment. Public static final fields should keep /** */ so IDEs and javadoc tooling pick them up. HALF_CURVE_ORDER now has no comment at all after this PR, which worsens the inconsistency.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

已修复:将 CURVE 和 CURVE_SPEC 的 // 注释恢复为 /** */ Javadoc。

}
this.privKey = null;
this.pub = CURVE.getCurve().decodePoint(key);
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[SHOULD] Double decodePoint in the isPrivateKey=false path. isValidPublicKey(key) already calls CURVE.getCurve().decodePoint(keyBytes) internally, and this line calls it again to assign this.pub. Decode once, validate, then assign:

} else {
  ECPoint point;
  try {
    point = CURVE.getCurve().decodePoint(key);
  } catch (RuntimeException e) {
    throw new IllegalArgumentException("Invalid public key.", e);
  }
  if (point.isInfinity() || !point.isValid()) {
    throw new IllegalArgumentException("Invalid public key.");
  }
  this.privKey = null;
  this.pub = point;
}

This also distinguishes malformed encoding errors from off-curve point errors.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

已修复:重构 isPrivateKey=false 路径,改为一次 decodePoint + 手动 isInfinity/isValid 检查,消除重复解码。同时区分了编码格式错误和曲线外点两种错误类型。

* Creates a verify-only ECKey from the given encoded public key bytes.
*/
public static ECKey fromPublicOnly(byte[] pub) {
return new ECKey(null, CURVE.getCurve().decodePoint(pub));
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[QUESTION] fromPublicOnly(ECPoint pub) and toStringWithPrivate() were both removed. No remaining callers appear in this repo. Were either of these part of a public SDK contract that downstream integrators depend on? If so, a deprecation cycle would be safer than hard removal.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

两处删除均属有意为之,此 repo 内确认无残留调用者。两个方法均不属于外部 SDK 契约:fromPublicOnly(ECPoint) 是内部工厂方法,已被构造函数层的 EC 点校验取代;toStringWithPrivate() 会将私钥明文写入 String(反模式),主动删除是安全改进。无需走 deprecation 流程。

@@ -672,7 +625,7 @@ public byte[] getAddress() {
if (pubKeyHash == null) {
pubKeyHash = Hash.computeAddress(this.pub);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[NIT] volatile ensures write visibility but doesn't prevent two threads from both observing null, both computing, and one overwriting. Since computeAddress is deterministic the race is benign (same result either way), but it contradicts the thread-safety intent implied by adding volatile. Either use synchronized or add a comment documenting that the benign double-computation is intentional.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

已在字段声明处补充注释,明确说明 volatile 保证可见性、竞态属于 benign race(两个线程计算结果相同,最后一次写入生效,正确性不受影响)。

} finally {
if (privKeyBytes != null) {
Arrays.fill(privKeyBytes, (byte) 0);
}
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[SHOULD] Arrays.fill(privKeyBytes, (byte) 0) clears the source byte array, but ByteArray.toHexString(privKeyBytes) already created an immutable String at line 94 before the finally block runs. The hex representation of the private key remains in the JVM heap until GC and cannot be zeroed. The zeroization intent is correct but doesn't cover the sensitive string. This is a known JVM limitation; consider documenting it explicitly, or avoid materialising the hex string until after the key is loaded into LocalWitnesses.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed: added a comment above the Arrays.fill call noting that the hex String already pushed to privateKeys is immutable and cannot be zeroed — a known JVM limitation. This documents the residual risk without overstating the protection.


@Test
public void testInvalidPublicKeyEncoding() {
byte[] invalidPublicKey = new byte[33];
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[NIT] testInvalidPublicKeyEncoding exercises the constructor path but uses a zero-filled compressed-point encoding — this hits decodePoint failure, not isInfinity/isValid. Consider adding a test that passes a validly-encoded but off-curve or infinity point directly to isValidPublicKey(). Also, SM2.isValidPrivateKey() has no test coverage at all in this PR.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed: added testIsValidPublicKey to ECKeyTest (null, empty, invalid curve point, valid uncompressed, valid compressed) and testSM2IsValidPrivateKey to SM2KeyTest (null, empty, zero key, curve-order key, oversized key, minimum valid key).

@Federico2014 Federico2014 force-pushed the fix/eckey-signature-validation branch from 65537c7 to ba307e9 Compare April 20, 2026 08:25
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 (1)
framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java (1)

295-296: "Oversized" case is actually a zero-value case.

new byte[34] is all-zero, so BigInteger decodes to 0 and is rejected for being below the minimum — identical in effect to the all-zero 32-byte assertion on Line 290. To actually exercise length-based rejection (vs. value-based), seed a non-zero 34-byte array (e.g. set the last byte to 1). Per retrieved learning, short arrays with a leading 0x00 byte are intentionally accepted as scalar data, so the exact semantics for an oversized non-zero input are worth pinning down in the test.

🧪 Proposed fix
-    // oversized input (34 bytes)
-    assertFalse(SM2.isValidPrivateKey(new byte[34]));
+    // oversized input (34 bytes, non-zero value to distinguish from the all-zero case)
+    byte[] oversized = new byte[34];
+    oversized[33] = 1;
+    assertFalse(SM2.isValidPrivateKey(oversized));
🤖 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/SM2KeyTest.java` around lines
295 - 296, The test currently passes an all-zero 34-byte array which checks the
zero-value rejection rather than length-based rejection; change the
oversized-case assertion to use a non-zero 34-byte array (e.g., create a
byte[34] and set at least one byte to 1) so SM2.isValidPrivateKey(...) is
exercised for an actually oversized non-zero input; keep the assertion as
assertFalse and reference SM2.isValidPrivateKey to ensure the test distinguishes
length-based rejection from zero-value rejection (note that shorter arrays with
a leading 0x00 are intentionally accepted, so ensure the 34-byte array is
non-zero).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java`:
- Around line 295-296: The test currently passes an all-zero 34-byte array which
checks the zero-value rejection rather than length-based rejection; change the
oversized-case assertion to use a non-zero 34-byte array (e.g., create a
byte[34] and set at least one byte to 1) so SM2.isValidPrivateKey(...) is
exercised for an actually oversized non-zero input; keep the assertion as
assertFalse and reference SM2.isValidPrivateKey to ensure the test distinguishes
length-based rejection from zero-value rejection (note that shorter arrays with
a leading 0x00 are intentionally accepted, so ensure the 34-byte array is
non-zero).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dca9306d-4e83-4f1a-b379-eb0025d01c76

📥 Commits

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

📒 Files selected for processing (8)
  • 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/common/crypto/SM2KeyTest.java
  • framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java
✅ Files skipped from review due to trivial changes (1)
  • crypto/src/main/java/org/tron/common/crypto/Rsv.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • crypto/src/main/java/org/tron/common/crypto/SignUtils.java
  • framework/src/test/java/org/tron/common/crypto/ECKeyTest.java
  • crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
  • framework/src/test/java/org/tron/core/config/args/WitnessInitializerTest.java
  • crypto/src/main/java/org/tron/common/crypto/ECKey.java

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