diff --git a/plan-jsrsasign.md b/plan-jsrsasign.md index 000ab149..d643abf3 100644 --- a/plan-jsrsasign.md +++ b/plan-jsrsasign.md @@ -5,14 +5,14 @@ ## Status - [x] PR 1 — Setup + ASN.1 utilities -- [ ] PR 2 — SM2 rewrite +- [x] PR 2 — SM2 rewrite - [ ] PR 3 — ECDSA primitives - [ ] PR 4 — PEM/JWK conversion + key extraction - [ ] PR 5 — X.509 / CSR / CRL parsing - [ ] PR 6 — Removal _Notes for next session:_ -- **PR 2 blocker:** `@noble/curves` v2 dropped the `/sm2` subpath. v2 exposes only `nist`, `secp256k1`, `bls12-381`, `bn254`, `ed25519`, `ed448` and the `abstract/*` primitives. Before starting PR 2, either pin `@noble/curves` to v1 (which still ships sm2 — but check what other v1→v2 API gaps that introduces) or build SM2 on top of the abstract Weierstrass primitive in `@noble/curves/abstract/weierstrass.js` (curve parameters published in GM/T 0003-2012). +- **PR 2 resolved:** SM2 is now built on `weierstrass(...)` + `ecdh(...)` from `@noble/curves/abstract/weierstrass.js` (curve params per GM/T 0003-2012). No `/sm2` subpath needed. - **PR 5 blocker:** `@peculiar/x509` v2 needs a `reflect-metadata` polyfill at every entry point — PR 1 pinned to `^1.14.3` to avoid that. Stay on v1 unless the polyfill cost gets resolved. ## Context @@ -238,6 +238,13 @@ After **PR 6:** Record deviations from the original plan here, newest at the top. One bullet per change: what changed, why, and which PR. +### PR 2 — 2026-05-17 +- `@noble/curves` v2 has no `/sm2` subpath, so [src/core/lib/SM2.mjs](src/core/lib/SM2.mjs) builds the curve itself with `weierstrass(...)` from `@noble/curves/abstract/weierstrass.js` using the GM/T 0003-2012 parameter set. Curve constructor is memoised per name so repeated `new SM2(...)` calls don't rebuild it. +- Random `k` generation: `ecdh(Point).utils.randomSecretKey()` → `bytesToNumberBE(...) % (n-1n) + 1n`, which mirrors the `[1, n-1]` distribution of the old `r.SecureRandom`-backed `getBigRandom`. The plan suggested `sm2.utils.randomPrivateKey()` directly; the abstract-Weierstrass path needs the explicit `bytes → bigint mod` step because the curve isn't pre-wrapped. +- `Point.fromHex("04" + x + y)` validates that the coords lie on the curve and raises before `is0()` can ever run; the explicit infinity check is kept for symmetry with the previous code. Invalid-point errors are caught and rethrown as `OperationError` so user-facing error messages stay unchanged. +- Coord padding switched from `("0000000000" + ...).slice(-charlen)` to `.padStart(charlen, "0")` per the cross-PR convention in [AGENTS.md](AGENTS.md). Output hex layout is byte-identical to before (still 64-char fields for `sm2p256v1`). +- No fixture changes: [tests/operations/tests/SM2.mjs](tests/operations/tests/SM2.mjs) passes unchanged (the 4 hard-coded ciphertext→plaintext vectors plus the 4 encrypt→decrypt round-trips). + ### PR 1 — 2026-05-17 - Pinned `@peculiar/x509` to `^1.14.3` instead of the latest (`2.x`). v2 hard-requires a `reflect-metadata` import at every entry point and the plan didn't budget for polyfilling every webpack chunk. Sticking with v1 keeps the bundle changes scoped to this PR. - `@noble/curves` installed at `^2.2.0`. v2 no longer exports an `/sm2` subpath — see PR 2 note in "Notes for next session" above. diff --git a/src/core/lib/SM2.mjs b/src/core/lib/SM2.mjs index e8156410..6533c7a5 100644 --- a/src/core/lib/SM2.mjs +++ b/src/core/lib/SM2.mjs @@ -9,39 +9,77 @@ import OperationError from "../errors/OperationError.mjs"; import { fromHex } from "../lib/Hex.mjs"; import Utils from "../Utils.mjs"; import Sm3 from "crypto-api/src/hasher/sm3.mjs"; -import {toHex} from "crypto-api/src/encoder/hex.mjs"; -import r from "jsrsasign"; +import { toHex } from "crypto-api/src/encoder/hex.mjs"; +import { weierstrass, ecdh } from "@noble/curves/abstract/weierstrass.js"; +import { bytesToNumberBE } from "@noble/curves/utils.js"; + +// SM2 curve parameter sets. The Weierstrass `Point` ctor is built lazily on +// first use and memoised across SM2 instances. +const SM2_CURVES = { + // GM/T 0003-2012 / sm2p256v1 — p = 2^256 - 2^224 - 2^96 + 2^64 - 1 + sm2p256v1: { + p: BigInt("0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"), + n: BigInt("0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"), + h: BigInt(1), + a: BigInt("0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC"), + b: BigInt("0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"), + Gx: BigInt("0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"), + Gy: BigInt("0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"), + coordCharLen: 64, + }, +}; + +const curveCache = {}; + +/** + * Resolve a named SM2 curve to its Point constructor, order, hex width and + * random-scalar helper. Builds the underlying Weierstrass curve lazily. + * + * @param {string} name + * @returns {{Point: Function, n: bigint, coordCharLen: number, randomScalar: () => bigint}} + */ +function getCurve(name) { + if (!Object.prototype.hasOwnProperty.call(SM2_CURVES, name)) { + throw new OperationError(`Unsupported SM2 curve: ${name}`); + } + if (curveCache[name]) return curveCache[name]; + const params = SM2_CURVES[name]; + const Point = weierstrass({ + p: params.p, + n: params.n, + h: params.h, + a: params.a, + b: params.b, + Gx: params.Gx, + Gy: params.Gy, + }); + const dh = ecdh(Point); + const cached = { + Point, + n: params.n, + coordCharLen: params.coordCharLen, + // Uniform-ish random scalar in [1, n-1] — matches the bias profile of + // the previous jsrsasign-based getBigRandom. + randomScalar: () => bytesToNumberBE(dh.utils.randomSecretKey()) % (params.n - 1n) + 1n, + }; + curveCache[name] = cached; + return cached; +} /** * SM2 Class for encryption and decryption operations */ export class SM2 { /** - * Constructor for SM2 class; sets up with the curve and the output format as specified in user args - * - * @param {*} curve - * @param {*} format + * @param {string} curve - named SM2 curve (e.g. "sm2p256v1") + * @param {string} format - "C1C3C2" or "C1C2C3" */ constructor(curve, format) { - this.ecParams = null; - this.rng = new r.SecureRandom(); - /* - For any additional curve definitions utilized by SM2, add another block like the below for that curve, then add the curve name to the Curve selection dropdown - */ - r.crypto.ECParameterDB.regist( - "sm2p256v1", // name / p = 2**256 - 2**224 - 2**96 + 2**64 - 1 - 256, - "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", // p - "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", // a - "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", // b - "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", // n - "1", // h - "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", // gx - "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", // gy - [] - ); // alias - this.ecParams = r.crypto.ECParameterDB.getByName(curve); - + const c = getCurve(curve); + this.Point = c.Point; + this.n = c.n; + this.coordCharLen = c.coordCharLen; + this.randomScalar = c.randomScalar; this.format = format; } @@ -52,13 +90,12 @@ export class SM2 { * @param {string} publicKeyY */ setPublicKey(publicKeyX, publicKeyY) { - /* - * TODO: This needs some additional length validation; and checking for errors in the decoding process - * TODO: Can probably support other public key encoding methods here as well in the future - */ - this.publicKey = this.ecParams.curve.decodePointHex("04" + publicKeyX + publicKeyY); - - if (this.publicKey.isInfinity()) { + try { + this.publicKey = this.Point.fromHex("04" + publicKeyX + publicKeyY); + } catch (e) { + throw new OperationError("Invalid Public Key"); + } + if (this.publicKey.is0()) { throw new OperationError("Invalid Public Key"); } } @@ -66,28 +103,26 @@ export class SM2 { /** * Set the private key value for the SM2 class * - * @param {string} privateKey + * @param {string} privateKeyHex */ setPrivateKey(privateKeyHex) { - this.privateKey = new r.BigInteger(privateKeyHex, 16); + this.privateKey = BigInt("0x" + privateKeyHex); } /** * Main encryption function; takes user input, processes encryption and returns the result in hex (with the components arranged as configured by the user args) * - * @param {*} input + * @param {Uint8Array} input * @returns {string} */ encrypt(input) { - const G = this.ecParams.G; - /* * Compute a new, random public key along the same elliptic curve to form the starting point for our encryption process (record the resulting X and Y as hex to provide as part of the operation output) - * k: Randomly generated BigInteger - * c1: Result of dotting our curve generator point `G` with the value of `k` + * k: Randomly generated bigint in [1, n-1] + * c1: Result of dotting our curve generator point with the value of `k` */ - const k = this.generatePublicKey(); - const c1 = G.multiply(k); + const k = this.randomScalar(); + const c1 = this.Point.BASE.multiply(k); const [hexC1X, hexC1Y] = this.getPointAsHex(c1); /* @@ -101,7 +136,7 @@ export class SM2 { const c3 = this.c3(p2, input); /* - * Genreate a proper length encryption key, XOR iteratively, and convert newly encrypted data to hex + * Generate a proper length encryption key, XOR iteratively, and convert newly encrypted data to hex */ const key = this.kdf(p2, input.byteLength); for (let i = 0; i < input.byteLength; i++) { @@ -118,10 +153,12 @@ export class SM2 { return hexC1X + hexC1Y + c2 + c3; } } + /** * Function to decrypt an SM2 encrypted message * - * @param {*} input + * @param {string} input + * @returns {ArrayBuffer} */ decrypt(input) { const c1X = input.slice(0, 64); @@ -138,7 +175,13 @@ export class SM2 { c3 = input.slice(-64); } c2 = Uint8Array.from(fromHex(c2)); - const c1 = this.ecParams.curve.decodePointHex("04" + c1X + c1Y); + + let c1; + try { + c1 = this.Point.fromHex("04" + c1X + c1Y); + } catch (e) { + throw new OperationError("Decryption Error -- Invalid Ciphertext Point"); + } /* * Compute the p2 (secret) value by taking the C1 point provided in the encrypted package, and multiplying by the private k value @@ -162,36 +205,11 @@ export class SM2 { } } - - /** - * Generates a large random number - * - * @param {*} limit - * @returns - */ - getBigRandom(limit) { - return new r.BigInteger(limit.bitLength(), this.rng) - .mod(limit.subtract(r.BigInteger.ONE)) - .add(r.BigInteger.ONE); - } - - /** - * Helper function for generating a large random K number; utilized for generating our initial C1 point - * TODO: Do we need to do any sort of validation on the resulting k values? - * - * @returns {BigInteger} - */ - generatePublicKey() { - const n = this.ecParams.n; - const k = this.getBigRandom(n); - return k; - } - /** * SM2 Key Derivation Function (KDF); Takes P2 point, and generates a key material stream large enough to encrypt all of the input data * - * @param {*} p2 - * @param {*} len + * @param {WeierstrassPoint} p2 + * @param {number} len * @returns {string} */ kdf(p2, len) { @@ -214,8 +232,8 @@ export class SM2 { /** * Calculates the C3 component of our final encrypted payload; which is the SM3 hash of the P2 point and the original, unencrypted input data * - * @param {*} p2 - * @param {*} input + * @param {WeierstrassPoint} p2 + * @param {Uint8Array} input * @returns {string} */ c3(p2, input) { @@ -224,13 +242,12 @@ export class SM2 { const overall = fromHex(hX).concat(Array.from(input)).concat(fromHex(hY)); return toHex(this.sm3(overall)); - } /** * SM3 setup helper function; takes input data as an array, processes the hash and returns the result * - * @param {*} data + * @param {number[]} data * @returns {string} */ sm3(data) { @@ -241,18 +258,16 @@ export class SM2 { } /** - * Utility function, returns an elliptic curve points X and Y values as hex; - * - * @param {EcPointFp} point - * @returns {[]} - */ + * Utility function, returns an elliptic curve point's X and Y values as fixed-width hex + * + * @param {WeierstrassPoint} point + * @returns {[string, string]} + */ getPointAsHex(point) { - const biX = point.getX().toBigInteger(); - const biY = point.getY().toBigInteger(); - - const charlen = this.ecParams.keycharlen; - const hX = ("0000000000" + biX.toString(16)).slice(- charlen); - const hY = ("0000000000" + biY.toString(16)).slice(- charlen); + const { x, y } = point.toAffine(); + const charlen = this.coordCharLen; + const hX = x.toString(16).padStart(charlen, "0"); + const hY = y.toString(16).padStart(charlen, "0"); return [hX, hY]; } }