Skip to content

refactor(crypto): enhance SM2 security and simplify signing model#12

Open
Federico2014 wants to merge 1 commit intodevelopfrom
refactor/sm2-signing-model
Open

refactor(crypto): enhance SM2 security and simplify signing model#12
Federico2014 wants to merge 1 commit intodevelopfrom
refactor/sm2-signing-model

Conversation

@Federico2014
Copy link
Copy Markdown
Owner

@Federico2014 Federico2014 commented Apr 15, 2026

What does this PR do?

Hardens and simplifies the SM2 cryptographic implementation to align with how ECDSA is used on TRON.

  1. Deterministic nonces: Replaced RandomDSAKCalculator with HMacDSAKCalculator (RFC 6979 equivalent), eliminating nonce-reuse risk
  2. Private key range validation: Added isValidPrivateKey(byte[]) and isValidPrivateKey(BigInteger), enforcing [1, n) range check on all key construction paths (SM2(byte[], boolean), fromPrivate(BigInteger), fromPrivate(byte[]), privateKeyFromBigInteger(), publicKeyFromPrivate())
  3. Signature component range checks: Tightened recoverPubBytesFromSignature() to enforce r ∈ (0, n), s ∈ (0, n), recId ∈ [0, 3], and messageHash.length == 32
  4. Null/empty guards: Added input validation on all public entry points: sign(), verify(), signatureToKeyBytes(), fromNodeId(), fromPublicOnly()
  5. Hash-signing model: Simplified SM2 to sign 32-byte transaction hashes directly, intentionally omitting the standard Z_A pre-hash step to keep SM2 and ECDSA interchangeable at the SignInterface level
  6. Dead code removal: Removed userID chain (userID field, ParametersWithID handling, addUserID(), forSigning parameter), message-level signing path (generateSignature, generateSM3Hash, getZ, addFieldElement, verifySignature, verifyMessage), and unused methods (hash(), signMessage(), signMsg(), fromPrivateAndPrecalculatedPublic())
  7. Renamed getSM2SignerForHash()getVerifier()
  8. Documentation: Added class-level Javadoc on SM2 and SM2Signer stating the non-standard design and non-interoperability with GB/T 32918

Why are these changes required?

  • The random K path creates nonce-reuse risk — two signatures with the same nonce leak the private key
  • No private key range validation: out-of-range keys (zero, >= curve order) lead to undefined behavior in scalar multiplication
  • No signature component range validation: r >= 0 and s >= 0 is insufficient — zero or out-of-range values cause incorrect public key recovery
  • The userID code path accepted ParametersWithID but silently ignored them (addUserID was commented out), creating a false sense of GB/T 32918 compliance
  • Message-level signing APIs were inconsistent with the blockchain hash-signing model and unused in production
  • Multiple dead methods and unused factory methods increased maintenance burden

⚠️ Breaking Changes:

  • SM2Signer.init() signature changed — forSigning parameter removed
  • SM2 signing output is now deterministic (same key+hash = same signature)
  • Invalid private keys now throw IllegalArgumentException instead of silently proceeding
  • SM2 message-level signing APIs removed (hash(), signMessage(), signMsg(), generateSM3Hash(), generateSignature(), verifySignature())
  • SM2.fromPrivateAndPrecalculatedPublic() removed
  • getSM2SignerForHash() renamed to getVerifier()

This PR has been tested by:

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

Follow up

SM2 has never been enabled on the TRON network — all production nodes use ECKey (SECP256K1) exclusively. The isECKeyCryptoEngine flag is always true at runtime. Therefore, these changes have zero impact on the consensus mechanism and do not affect any on-chain transaction processing or block validation.

Extra details

Split from #7

Closes #18

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

The PR refactors the SM2 elliptic curve cryptography implementation with stricter input validation, removal of message-level signing APIs (moving hash responsibility to callers), deterministic signature generation via HMAC-based k derivation, and improved public key recovery validation. Field naming is standardized, defensive copying added for sensitive data, and DER signature decoding is rewritten using ASN.1 primitives.

Changes

Cohort / File(s) Summary
SM2 Core Cryptography
crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
Renamed static fields to camelCase, made curve constants static final. Added explicit input validation in constructors and factory methods; removed message-hashing/signing methods; changed sign() to require 32-byte hash input; rewrote DER decoding using ASN.1 primitives; enhanced public key recovery validation; added isValidPrivateKey() helpers; improved encapsulation with defensive copies for getAddress() and getNodeId(). Parameter names changed from data to hash to clarify semantics.
SM2 Signer Logic
crypto/src/main/java/org/tron/common/crypto/sm2/SM2Signer.java
Switched nonce generator from RandomDSAKCalculator to deterministic HMacDSAKCalculator(SM3Digest); replaced dual-mode init(boolean, ...) with single init(CipherParameters); removed message-level APIs (generateSignature, generateSM3Hash, verifySignature); tightened hash validation to enforce exactly 32 bytes and throw on null/invalid inputs; moved k-calculation initialization inside generateHashSignature.
Test Updates
framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java, framework/src/test/java/org/tron/common/utils/PublicMethod.java
Updated SM2 tests to expect IllegalArgumentException on invalid inputs; removed SM3 hashing test; added deterministic signature, DER compatibility, and malformed DER handling tests. Removed getSM2HashByPubKey() utility method due to removal of message-level hashing API.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • Federico2014/bn128-ipc#1: Both target the SM2 cryptography implementation; this PR refactors and tightens SM2 APIs while the related issue proposes removal, suggesting potential overlap or conflict in roadmap planning.

Poem

🐰 A hop through crypto fields so tight,
Where hashes flow and signatures bite,
Deterministic bounces, no random ways,
Security strengthened through validation days,
The SM2 spring-cleans with newfound might! 🔐

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.17% 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 accurately describes the main changes: refactoring SM2 to enhance security through deterministic nonce generation and simplify the signing model to hash-based signing.
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 refactor/sm2-signing-model

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

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java`:
- Around line 679-686: The verify(byte[] hash, byte[] signature, byte[] pub)
method currently returns false when any argument is null; change this to treat
nulls as programmer errors by replacing the initial null-check and false-return
with the codebase's precondition check (call check(...) or the project's
null-assert helper) for hash, signature, and pub so a thrown
assertion/IllegalArgumentException surfaces instead of a silent false; keep the
try block that calls SM2Signature.decodeFromDER(signature) and the existing
catch for decoding/signature errors so only decoding/signature failures still
return false while null-argument bugs throw.
- Around line 269-280: The isValidPrivateKey method currently allows
non-canonical leading-zero padding; update SM2.isValidPrivateKey to reject any
multi-byte array that begins with 0x00 (i.e., if keyBytes.length > 1 &&
keyBytes[0] == 0x00 return false) so redundant leading-zero prefixes are not
accepted for any length; keep the empty check and the numeric-range check
against SM2_N using new BigInteger(1, keyBytes) to validate 1 <= key < SM2_N.

In `@crypto/src/main/java/org/tron/common/crypto/sm2/SM2Signer.java`:
- Around line 34-39: The init(CipherParameters param) method must validate param
and its runtime type before casting: check param != null and that it is an
instance of ECKeyParameters (use the project's check() pattern) and initialize
ecKey and ecParams only after those checks; in generateHashSignature() validate
that ecKey is non-null and an ECPrivateKeyParameters instance and ecParams is
non-null before casting/using them (throw a clear IllegalArgumentException via
check() if not); likewise in verifyHashSignature() validate ecKey is non-null
and an ECPublicKeyParameters instance and ecParams is initialized before
casting/using; ensure all error messages identify the expected type
(ECPrivateKeyParameters/ECPublicKeyParameters) and the offending state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f29d7f74-8d7d-4aa7-bb1a-8e0be8b2d7e6

📥 Commits

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

📒 Files selected for processing (4)
  • crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
  • crypto/src/main/java/org/tron/common/crypto/sm2/SM2Signer.java
  • framework/src/test/java/org/tron/common/crypto/SM2KeyTest.java
  • framework/src/test/java/org/tron/common/utils/PublicMethod.java
💤 Files with no reviewable changes (1)
  • framework/src/test/java/org/tron/common/utils/PublicMethod.java

Comment thread crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
Comment thread crypto/src/main/java/org/tron/common/crypto/sm2/SM2.java
Comment thread crypto/src/main/java/org/tron/common/crypto/sm2/SM2Signer.java
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.

No issues found across 4 files

@Federico2014 Federico2014 changed the title refactor(crypto): simplify SM2 to blockchain-adapted hash-signing model refactor(crypto): simplify SM2 and remove unused code Apr 15, 2026
@Federico2014 Federico2014 changed the title refactor(crypto): simplify SM2 and remove unused code refactor(crypto): enhance SM2 and simplify code Apr 15, 2026
@Federico2014 Federico2014 changed the title refactor(crypto): enhance SM2 and simplify code refactor(crypto): enhance SM2 security and simplify signing model Apr 15, 2026
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.

Thanks for the review. Responses to each comment:

  1. isValidPrivateKey accepts shorter zero-padded encodings: By design. {0x00, 0x01} represents the value 1, which is a valid private key. ECKey.isValidPrivateKey uses the exact same logic — only rejecting 33-byte arrays with non-zero leading byte (BigInteger sign-byte padding). Keeping SM2 and ECKey consistent. Won't fix.

  2. verify() returns false on null instead of throwing: This is a verification utility — returning false for null input is semantically consistent with "verification failed". The method already catches IllegalArgumentException | SignatureException and returns false, so null handling follows the same pattern. Changing to throw would be an API behavior change. Won't fix.

  3. SM2Signer.init() lacks instanceof type checks: init() already validates null. A wrong type will throw ClassCastException with a clear stack trace. SM2Signer is an internal class only called by SM2.sign() and SM2.getVerifier(), which always pass the correct type. Adding instanceof guards on internal classes adds code bloat without practical benefit. Won't fix.

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] Enhance SM2 security and simplify signing model

1 participant