cyberchef/plan-jsrsasign.md
Leon Zandman 6e2cd91af5 Add plan.
2026-05-16 23:58:56 +02:00

16 KiB
Raw Blame History

Replace jsrsasign in CyberChef

See AGENTS.md for cross-PR conventions, workflow, and library decisions agents must follow.

Status

  • PR 1 — Setup + ASN.1 utilities
  • 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: (none yet)

Context

jsrsasign (currently pinned at ^11.1.3 in package.json:148) has announced end-of-support. It is used in 14 operations and one library file in CyberChef and covers X.509/CSR/CRL parsing, ECDSA signing/verification, key format conversion, ASN.1/OID utilities, and SM2 elliptic-curve encryption.

Continuing to ship an unmaintained crypto library is a security risk. This plan replaces every jsrsasign call with proven, actively-maintained alternatives, then removes the dependency entirely.

Library choices

Add these production dependencies, all MIT/BSD-3 (compatible with CyberChef's Apache-2.0 license), all pure-JS, all browser + Node:

Library Used for Why this one
@peculiar/x509 (^1.14) X.509 certs, PKCS#10 CSRs, X.509 CRLs, SPKI/PKCS#8 key parsing Peculiar Ventures specialise in browser PKI. Built on Web Crypto + asn1js. Used by Microsoft, Cloudflare. MIT.
asn1js (^3) Generic ASN.1 parse + tree dump for ParseASN1HexString Same author. BSD-3. Pulled in transitively by @peculiar/x509 anyway.
@noble/curves (^2) ECDSA sign/verify, signature format conversion, key generation, SM2 curve Audited by Cure53, Trail of Bits, Kudelski. Zero dependencies. Used by MetaMask, Coinbase, Ethereum Foundation. Built-in @noble/curves/sm2. Same author as the already-installed @noble/hashes.
node-forge (already a dep) DSA path inside PubKeyFromPrivKey only Avoids adding another lib just for DSA.

Not adopted, with reasons: pkijs (covered by @peculiar/x509); jose/panva (overkill for two JWK round-trip ops); sm-crypto/sm-crypto-v2 (@noble/curves/sm2 handles it); native Web Crypto for ECDSA (refuses MD5/SHA-1 digests that the UI exposes, and JWK/format gymnastics outweigh the dependency saving).

End state: npm uninstall jsrsasign; remove from package.json.

Decisions confirmed with user

  • Phased delivery: 6 PRs, one per phase below.
  • Output fidelity: cosmetic drift accepted for golden text-output tests (ParseX509Certificate, ParseCSR, ParseX509CRL, ParseASN1HexString). Update fixtures, document in CHANGELOG.
  • DSA in PubKeyFromPrivKey: keep via node-forge (no user-visible regression).
  • ECDSA signature fixtures: update to @noble values if RFC 6979 outputs diverge (signatures remain valid).

Files to be modified

14 operations (all under src/core/operations/):

  • ASN.1 / encoding: HexToObjectIdentifier.mjs, ObjectIdentifierToHex.mjs, HexToPEM.mjs, ParseASN1HexString.mjs
  • X.509 / CSR / CRL: ParseX509Certificate.mjs, PubKeyFromCert.mjs, ParseCSR.mjs, ParseX509CRL.mjs
  • Key conversion: PEMToJWK.mjs, JWKToPem.mjs, PubKeyFromPrivKey.mjs
  • ECDSA: ECDSASign.mjs, ECDSAVerify.mjs, ECDSASignatureConversion.mjs, GenerateECDSAKeyPair.mjs

Library files:

Build/manifest:

  • package.json — add 3 deps in PR 1, remove jsrsasign in PR 6
  • CHANGELOG.md — note in PR 6

Phased plan

PR 1 — Setup + ASN.1 utilities (PublicKey bundle)

Goal: Add the three new deps, ship the Asn1.mjs helper module, migrate the four utility operations.

  1. npm install --save @peculiar/x509 asn1js @noble/curves. Confirm webpack builds; smoke-test npm test.
  2. Create src/core/lib/Asn1.mjs:
    • oidHexToInt(hex) — inline BER OID decode (~25 lines, handles arc0/arc1 combined byte and base-128 multibyte arcs).
    • oidIntToHex(oid) — inverse.
    • derToPem(hex, label) — base64 + 64-col wrap with \n line endings.
    • dumpAsn1Hex(hex, { truncate, startIndex }) — walks asn1js.fromBER output and emits an indented tree.
  3. Migrate:
  4. Tests:
    • Add round-trip OID tests (sample OIDs incl. 2.5.4.3, 1.3.6.1.4.1.311.2.1.4, edge case with arc > 127).
    • Update ParseASN1HexString golden output to new tree format (drift allowed).

Risks: OID encoding edge cases — verify the 40·a + b first byte and base-128 arcs.

PR 2 — SM2 (Ciphers bundle)

Goal: Rewrite src/core/lib/SM2.mjs on top of @noble/curves/sm2.

API mapping:

jsrsasign @noble/curves/sm2
r.crypto.ECParameterDB.regist("sm2p256v1", …) + getByName import { sm2 } from "@noble/curves/sm2" (curve built-in)
r.SecureRandom + new r.BigInteger(bits, rng).mod(n-1)+1 sm2.utils.randomPrivateKey()
ecParams.G.multiply(k) sm2.Point.BASE.multiply(k) (k is bigint)
ecParams.curve.decodePointHex("04"+x+y) sm2.Point.fromHex("04"+x+y)
point.getX().toBigInteger() / getY() point.toAffine().x / .y (both bigint)
point.isInfinity() point.is0() (or equals(sm2.Point.ZERO))
new r.BigInteger(hex, 16) BigInt("0x" + hex)

Gotchas:

  • Pad point coords with .padStart(64, "0") after bigint.toString(16) — existing tests depend on fixed-width hex.
  • KDF logic (SM3-based) is already pure-JS; do not change it.
  • Preserve both ciphertext layouts (GMT 0009 BBB vs GMT 0010 C1C2C3/C1C3C2) — keep current format-string handling.

Tests: tests/operations/tests/SM2.mjs has hard-coded ciphertext→plaintext fixtures. They MUST pass unchanged — they pin cryptographic correctness.

PR 3 — ECDSA primitives (Ciphers bundle)

Goal: Migrate the four ECDSA operations to @noble/curves + @noble/hashes.

  1. Create src/core/lib/Ecdsa.mjs with shared helpers:

    • loadEcKey(pem) — parse SEC1 / PKCS#8 / SPKI via @peculiar/asn1-ecc + @peculiar/asn1-pkcs8; return { curve, isPrivate, d?, x, y, jwk }.
    • signEcdsa(curve, dBytes, digest), verifyEcdsa(curve, qBytes, digest, sigDer).
    • asn1SigToConcatHex, concatHexToAsn1Sig, parseAsn1SigToHexRS, hexRSToAsn1Sig — wrap Signature.fromBytes(.., 'der'|'compact') and .toBytes(format).
    • isAsn1Hex(hex) — try asn1js.fromBER.
    • generateEcKeyPair(curveName)curve.utils.randomPrivateKey(), build SPKI + PKCS#8 via @peculiar/asn1-* schemas, derive JWK.
  2. Migrate operations:

    • ECDSASign.mjs: use loadEcKey + hash (md5/sha1/sha256/sha384/sha512 from @noble/hashes; md5/sha1 come from @noble/hashes/legacy) + signEcdsa. Format conversion via the helpers.
    • ECDSAVerify.mjs: isAsn1Hex for input-format auto-detect; verifyEcdsa.
    • ECDSASignatureConversion.mjs: format conversions only — no key handling.
    • GenerateECDSAKeyPair.mjs: generateEcKeyPair; drop the \r stripping (was a jsrsasign artefact).

Gotchas:

  • MD5/SHA-1 digests@noble/hashes/legacy (the v2 split). Web Crypto cannot do this, which is why we chose @noble/curves.
  • P-521 byte length is 66, not 65 — important for P1363 padding and JWK x/y encoding.
  • parseSigHexInHexRS historically returns r/s with a leading 00 when the MSB is set (DER 2's-complement artefact). Replicate this — tests depend on it.
  • RFC 6979 determinism — outputs should match jsrsasign byte-for-byte. If they don't, update fixtures (signatures still valid; document in CHANGELOG).
  • JWK field ordering for the JWK output of GenerateECDSAKeyPair: build the literal as {kty, crv, x, y, d?}JSON.stringify preserves insertion order.

Tests: tests/operations/tests/ECDSA.mjs has 83 tests covering all curves × digests × formats. Run; update fixtures where needed.

PR 4 — PEM/JWK conversion + key extraction (PublicKey bundle)

Goal: Migrate PEMToJWK.mjs, JWKToPem.mjs, PubKeyFromPrivKey.mjs.

API mapping:

jsrsasign Replacement
r.KEYUTIL.getKey(pem) RSA node-forge pki.privateKeyFromPem / publicKeyFromPem
r.KEYUTIL.getKey(pem) EC loadEcKey from Ecdsa.mjs
r.KEYUTIL.getKey(pem) DSA node-forge DSA parsing
r.KEYUTIL.getKey(jwk) Inline: detect kty, dispatch to RSA / EC builders
r.KEYUTIL.getJWKFromKey(key) Inline literal { kty, …, [d] }
r.KEYUTIL.getPEM(key) SPKI @peculiar/x509 PublicKey(spki).toString("pem")
r.KEYUTIL.getPEM(key, "PKCS8PRV") Build PKCS#8 via @peculiar/asn1-pkcs8's PrivateKeyInfo
new r.RSAKey().setPublic(n, e) + getPEM node-forge pki.setRsaPublicKey(n, e) + publicKeyToPem
new r.KJUR.crypto.ECDSA({curve}).setPublicKeyHex/generatePublicKeyHex Derive Q with curve.getPublicKey(d, false); build SPKI
new r.KJUR.crypto.DSA().setPublic(p, q, g, y) node-forge DSA public key + publicKeyToPem

Gotchas:

  • JWK field order and PEM line endings (\n not \r\n) — see PR 3.
  • JWKToPem's PKCS8PRV output: build via @peculiar/asn1-pkcs8 + the algorithm-specific key schema (RSA or EC).
  • DSA PKCS#8 with no y: keep the current "unsupported" throw — test already accepts it.

Tests: tests/operations/tests/JWK.mjs, tests/operations/tests/PubKeyFromPrivKey.mjs — should pass with minor whitespace adjustments.

PR 5 — X.509 / CSR / CRL parsing (PublicKey bundle)

Goal: Migrate the four cert-family operations to @peculiar/x509.

  1. Before any code changes: add a regression test for ParseX509Certificate.mjs (currently uncovered — confirm with ls tests/operations/tests/ | grep -i x509certificate). Use the existing test certs from PubKeyFromCert.mjs.
  2. Extend src/core/lib/PublicKey.mjs: adapt formatDnObj to accept either the legacy {array: [[{type, value}]]} shape OR @peculiar/x509's JsonName array shape.
  3. Migrate:

ParseX509Certificate:

jsrsasign @peculiar/x509
new r.X509(); readCertHex/readCertPEM new X509Certificate(input) (accepts PEM string or BufferSource)
cert.hex bytesToHex(cert.rawData)
cert.getSerialNumberHex() cert.serialNumber
cert.getIssuer() / getSubject() cert.issuerName.toJSON() / cert.subjectName.toJSON()formatDnObj
cert.getPublicKey() cert.publicKey; parse rawData with @peculiar/asn1-rsa RSAPublicKey or @peculiar/asn1-ecc ECPoint to extract n/e or (x, y)
cert.getSignatureValueHex() bytesToHex(cert.signature)
cert.version cert.version
cert.getSignatureAlgorithm* cert.signatureAlgorithm.name
cert.getNotBefore() / getNotAfter() cert.notBefore / cert.notAfter (Date) — reformat to the existing yymmddHHMMSSZ string
cert.getInfo() extensions text Iterate cert.extensions, format known types (BasicConstraints, KeyUsage, EKU, SAN, AKI, SKI, CRLDistributionPoints, AIA) — reuse formatters in current ParseCSR.mjs
r.BigInteger keylen native BigInt("0x"+hex)

PubKeyFromCert: new X509Certificate(pem).publicKey.toString("pem").

ParseCSR:

jsrsasign @peculiar/x509
r.KJUR.asn1.csr.CSRUtil.getParam(input) new Pkcs10CertificateRequest(input)
csrParam.sbjpubkey csr.publicKey.toString("pem")
csrParam.sigalg csr.signatureAlgorithm.name
csrParam.sighex bytesToHex(csr.signature)
csrParam.extreq walk csr.attributes, find OID 1.2.840.113549.1.9.14, decode with @peculiar/asn1-pkcs9

ParseX509CRL:

jsrsasign @peculiar/x509
new r.X509CRL(input) new X509Crl(input) (convert hex input → Uint8Array first)
crl.getVersion / SignatureAlgorithm* / Issuer / ThisUpdate / NextUpdate crl.version / signatureAlgorithm.name / issuer / thisUpdate / nextUpdate
crl.getParam().ext crl.extensions
crl.getRevCertArray() crl.entries (each has serialNumber, revocationDate, extensions)
crl.getSignatureValueHex() bytesToHex(crl.signature)

Gotchas:

  • cert.getInfo() is the trickiest dependency — the current op extracts extensions text by string-splitting on "X509v3 Extensions:\n" and "signature". Replace with a real extension walker (reuse formatGeneralNames, KeyUsage bit mapping, etc. from existing ParseCSR.mjs).
  • CRL hex input: convert hex → Uint8Array before passing to X509Crl.
  • DSA-in-cert: rare; use @peculiar/asn1-x509 DSA params or fall back to a placeholder text block.
  • Date formatting: produce both raw yymmddHHMMSSZ and a human-readable form, as currently displayed.

Tests: Update goldens for ParseCSR.mjs, ParseX509CRL.mjs, PubKeyFromCert.mjs as needed. Add fixtures for the new ParseX509Certificate test from step 1.

PR 6 — Removal

  1. grep -rn jsrsasign src/ — must return zero matches.
  2. npm uninstall jsrsasign; remove from package.json dependencies.
  3. npm run lint && npm test — full suite green.
  4. npm run build — confirm bundle builds; check PublicKey and Ciphers chunk size deltas.
  5. Update CHANGELOG.md: removed jsrsasign; added @peculiar/x509, asn1js, @noble/curves; note any cosmetic output-format changes in the affected ops.

Verification

After each PR:

  • npm run lint
  • npm test (Node + browser test suites)
  • npm run build succeeds
  • grep -rn "from \"jsrsasign\"" src/core/ shrinks monotonically toward zero

After PR 6:

  • grep -rn jsrsasign . returns zero results outside package-lock.json and CHANGELOG.md
  • Manual UI smoke test: launch npm start, try each migrated operation in the browser with representative input
  • Check bundle size: npm run build and compare web/assets/*.js sizes against the pre-migration baseline

Open follow-ups (not in scope)

  • Audit other crypto deps (crypto-api, crypto-js, node-forge) for similar end-of-life risk.
  • Consider whether crypto-api (used for SM3 in SM2.mjs) could also be retired in favour of @noble/hashes/sm3 in a future cleanup.

Changelog

Record deviations from the original plan here, newest at the top. One bullet per change: what changed, why, and which PR.