Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of

## Details

## [Unreleased]
- Removed the unmaintained `jsrsasign` dependency. ASN.1/OID/PEM utilities, SM2, ECDSA, PEM/JWK key conversion, and X.509/CSR/CRL parsing now use `@noble/curves`, `@peculiar/x509`, and `asn1js`; related golden text fixtures were updated during the migration for accepted cosmetic output-format drift.

## [11.0.0] - 2026-04-28
- Revert sitemap to v8.0.X to fix build/deploy on master [@GCHQDeveloper581] | [#2348]
- Node version update from 22 to 24 [@lzandman] [@GCHQDeveloper581] | [#2347]
Expand Down Expand Up @@ -1220,4 +1223,3 @@ Breaking changes:
[#2273]: https://github.com/gchq/CyberChef/pull/2273
[#2342]: https://github.com/gchq/CyberChef/pull/2342
[#1922]: https://github.com/gchq/CyberChef/pull/1922

140 changes: 140 additions & 0 deletions CRYPTO_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Cryptography Implementation Guide

This document captures the architectural decisions and coding conventions established during the migration away from `jsrsasign`. It serves as a reference for understanding library choices, implementation patterns, and maintenance of cryptographic operations.

## Library Choices

The following libraries were selected for specific cryptographic operations. These decisions prioritize compatibility with the existing CyberChef API surface and cryptographic correctness.

### ECDSA: `@noble/curves`

Uses `@noble/curves` with `@noble/hashes/legacy` for MD5/SHA-1 digests.

**Why not Web Crypto?** The Web Crypto API refuses MD5 and SHA-1 digests, which the CyberChef UI exposes to users. `@noble/curves` with legacy hash support maintains this functionality.

### X.509 / CSR / CRL: `@peculiar/x509`

Uses `@peculiar/x509` for certificate, CSR, and CRL parsing and generation, with `@peculiar/asn1-*` schema packages as needed.

**Version pin:** Keep `@peculiar/x509` on the `1.14.x` line.

**Why pinned?** `@peculiar/x509` v2 requires a `reflect-metadata` polyfill/import at each runtime entry point. CyberChef currently avoids that cross-bundle polyfill cost and complexity, so v1 remains the project baseline unless that tradeoff is revisited.

**Why not pkijs?** `@peculiar/x509` offers a cleaner API better suited to CyberChef's operation model.

### SM2: `@noble/curves/sm2`

Uses the SM2 implementation from `@noble/curves`.

**Why not sm-crypto?** `@noble/curves` provides a reliable, actively maintained SM2 implementation.

### DSA: `node-forge`

The `PubKeyFromPrivKey` operation continues to use `node-forge` for DSA key generation.

**Why?** `node-forge` is already a project dependency, and DSA is a single, localized operation not worth introducing a new library for.

### Generic ASN.1 Dump: `asn1js`

Uses `asn1js` for generic ASN.1 hex structure dumping.

**Note:** This is a transitive dependency of `@peculiar/x509` and requires no additional imports for most use cases.

## Coding Conventions

These conventions ensure consistency across cryptographic operations and maintain compatibility with existing test fixtures.

### PEM Line Endings

Use `\n` only for PEM-encoded output. Do not use `\r\n`.

The legacy `jsrsasign` output used `\r\n` in some cases. All test fixtures have been updated to expect `\n` exclusively.

### Hex Coordinate Padding

When converting elliptic curve point coordinates from `bigint` to hex:

- **P-256 and SM2:** Use `.padStart(64, "0")` (32 bytes = 64 hex characters)
- **P-521:** Use `.padStart(132, "0")` (66 bytes = 132 hex characters)

Apply this after calling `bigint.toString(16)`.

### JWK Field Ordering

Build JWK objects using literal syntax in this exact order so `JSON.stringify` emits fields in the correct sequence:

**Elliptic Curve:**
```javascript
{ kty, crv, x, y, d? }
```

**RSA:**
```javascript
{ kty, n, e, d?, p?, q?, dp?, dq?, qi? }
```

JavaScript preserves insertion order for object properties, and downstream consumers may depend on this ordering.

### ECDSA r/s Leading-Zero Quirk

The `parseSigHexInHexRS` function historically prepends `00` to the r or s component when its most significant bit is set. This is a quirk from DER 2's-complement encoding.

**Replicate this behavior.** Existing test fixtures depend on it, and changing it breaks compatibility.

### RFC 6979 Determinism

ECDSA signature outputs from `@noble/curves` *should* match `jsrsasign` byte-for-byte when using the same curve and digest algorithm.

**If they diverge:** The signature is still cryptographically valid. Update the test fixture and document the divergence in the changelog. Do not attempt to massage the library output to force a match.

### Golden Output Cosmetic Drift

For text-based operations that dump parsed structures (`ParseX509Certificate`, `ParseCSR`, `ParseX509CRL`, `ParseASN1HexString`), minor formatting differences between `jsrsasign` and the new implementation are acceptable.

**Action:** Update the test fixture and note the change in the changelog.

### Cryptographic Correctness is Non-Negotiable

Fixture data that exercises actual cryptographic operations (e.g., SM2 ciphertext→plaintext transformations in [tests/operations/tests/SM2.mjs](tests/operations/tests/SM2.mjs)) must pass unchanged. These fixtures pin real cryptographic behavior and cannot be updated cosmetically.

## Shared Helper Modules

To avoid duplicating logic across operations, use these utility modules:

### [src/core/lib/Asn1.mjs](src/core/lib/Asn1.mjs)

ASN.1 utilities for OID and DER manipulation:
- `oidHexToInt()` — convert ASN.1 OID hex encoding to integer dotted notation
- `oidIntToHex()` — convert integer dotted notation to ASN.1 OID hex
- `derToPem()` — wrap DER bytes in PEM format
- `dumpAsn1Hex()` — generic ASN.1 hex structure dump (uses `asn1js`)

### [src/core/lib/Ecdsa.mjs](src/core/lib/Ecdsa.mjs)

ECDSA operations with `@noble/curves`:
- `loadEcKey()` — parse EC private/public keys from PEM or JWK
- `signEcdsa()` — sign data with an EC private key
- `verifyEcdsa()` — verify an ECDSA signature
- Signature format converters (ASN.1 DER ↔ r/s hex format)
- `isAsn1Hex()` — detect ASN.1 DER-encoded data
- `generateEcKeyPair()` — generate a new EC key pair

### [src/core/lib/PublicKey.mjs](src/core/lib/PublicKey.mjs)

Public key utilities and DN formatting. The `formatDnObj()` function handles both legacy jsrsasign DN objects and `@peculiar/x509` `JsonName` shapes for backward compatibility.

### [src/core/lib/SM2.mjs](src/core/lib/SM2.mjs)

SM2 encryption and decryption with support for multiple ciphertext layouts:
- GMT 0009 BBB format
- GMT 0010 C1C2C3 format
- GMT 0010 C1C3C2 format

Preserves compatibility with existing test fixtures for all three layouts.

## Key File Locations

- **Cryptographic operations:** [src/core/operations/](src/core/operations/)
- **Test fixtures:** [tests/operations/tests/](tests/operations/tests/) — golden output fixtures live here
- **Dependencies:** [package.json](package.json)
- **Library modules:** [src/core/lib/](src/core/lib/)
23 changes: 21 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ const path = require("path");

const nodeFlags = "--no-warnings --no-deprecation";

/**
* Generates the bundle analyzer report from Webpack stats only.
*/
class StatsOnlyBundleAnalyzerPlugin extends BundleAnalyzerPlugin {
/**
* Prevent webpack-bundle-analyzer from reading emitted bundle files.
*
* Inline worker-loader assets are present in Webpack stats but intentionally
* not written to disk, so stat sizes are the most complete source for this
* production report.
*
* @returns {null}
*/
getBundleDirFromCompiler() {
return null;
}
}

/**
* Grunt configuration for building the app in various formats.
*
Expand Down Expand Up @@ -141,10 +159,11 @@ module.exports = function (grunt) {
minifyCSS: true
}
}),
new BundleAnalyzerPlugin({
new StatsOnlyBundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "BundleAnalyzerReport.html",
openAnalyzer: false
openAnalyzer: false,
defaultSizes: "stat"
}),
]
};
Expand Down
Loading