Merge 203101c7155ff17fb9a6e8b77e8eb712a4133e42 into 5105aad91d9091be64182f1d79678fdbecfcfc15
This commit is contained in:
commit
1d0ca8cb3b
@ -13,6 +13,9 @@ All major and minor version changes will be documented in this file. Details of
|
|||||||
|
|
||||||
## Details
|
## 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
|
## [11.0.0] - 2026-04-28
|
||||||
- Revert sitemap to v8.0.X to fix build/deploy on master [@GCHQDeveloper581] | [#2348]
|
- Revert sitemap to v8.0.X to fix build/deploy on master [@GCHQDeveloper581] | [#2348]
|
||||||
- Node version update from 22 to 24 [@lzandman] [@GCHQDeveloper581] | [#2347]
|
- Node version update from 22 to 24 [@lzandman] [@GCHQDeveloper581] | [#2347]
|
||||||
@ -1220,4 +1223,3 @@ Breaking changes:
|
|||||||
[#2273]: https://github.com/gchq/CyberChef/pull/2273
|
[#2273]: https://github.com/gchq/CyberChef/pull/2273
|
||||||
[#2342]: https://github.com/gchq/CyberChef/pull/2342
|
[#2342]: https://github.com/gchq/CyberChef/pull/2342
|
||||||
[#1922]: https://github.com/gchq/CyberChef/pull/1922
|
[#1922]: https://github.com/gchq/CyberChef/pull/1922
|
||||||
|
|
||||||
|
|||||||
140
CRYPTO_IMPLEMENTATION.md
Normal file
140
CRYPTO_IMPLEMENTATION.md
Normal file
@ -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
Gruntfile.js
23
Gruntfile.js
@ -8,6 +8,24 @@ const path = require("path");
|
|||||||
|
|
||||||
const nodeFlags = "--no-warnings --no-deprecation";
|
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.
|
* Grunt configuration for building the app in various formats.
|
||||||
*
|
*
|
||||||
@ -141,10 +159,11 @@ module.exports = function (grunt) {
|
|||||||
minifyCSS: true
|
minifyCSS: true
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new BundleAnalyzerPlugin({
|
new StatsOnlyBundleAnalyzerPlugin({
|
||||||
analyzerMode: "static",
|
analyzerMode: "static",
|
||||||
reportFilename: "BundleAnalyzerReport.html",
|
reportFilename: "BundleAnalyzerReport.html",
|
||||||
openAnalyzer: false
|
openAnalyzer: false,
|
||||||
|
defaultSizes: "stat"
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
306
PLAN_JSRSASIGN-REMOVAL.md
Normal file
306
PLAN_JSRSASIGN-REMOVAL.md
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
# Replace jsrsasign in CyberChef
|
||||||
|
|
||||||
|
> See [AGENTS.md](AGENTS.md) for cross-PR conventions, workflow, and library decisions agents must follow.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- [x] PR 1 — Setup + ASN.1 utilities
|
||||||
|
- [x] PR 2 — SM2 rewrite
|
||||||
|
- [x] PR 3 — ECDSA primitives
|
||||||
|
- [x] PR 4 — PEM/JWK conversion + key extraction
|
||||||
|
- [x] PR 5 — X.509 / CSR / CRL parsing
|
||||||
|
- [x] PR 6 — Removal
|
||||||
|
|
||||||
|
_Notes for next session:_
|
||||||
|
- **PR 6 complete:** dependency removed, source tree is free of runtime references, and the remaining migration references are documentation/changelog only.
|
||||||
|
- **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 3 resolved:** ECDSA primitives migrated; new [src/core/lib/Ecdsa.mjs](src/core/lib/Ecdsa.mjs) is the shared helper module. Existing ECDSA fixture set passes unchanged (sign↔verify round-trips and the canned P-256 signature fixtures both verify against `lowS: false`).
|
||||||
|
- **PR 4 resolved:** Key-conversion ops migrated; new [src/core/lib/KeyConvert.mjs](src/core/lib/KeyConvert.mjs) wraps RSA/EC/DSA PEM⇄JWK⇄SPKI/PKCS#8. DSA private-key parsing/building goes through `asn1js` directly (no peculiar `asn1-dsa` package and `node-forge` doesn't expose DSA), reusing the existing EC helpers from `Ecdsa.mjs`.
|
||||||
|
- **PR 5 resolved:** X.509 / CSR / CRL ops migrated to `@peculiar/x509` v1, plus the matching `@peculiar/asn1-*` schemas. New [src/core/lib/X509.mjs](src/core/lib/X509.mjs) holds the shared helpers (SPKI describer, signature-OID → display-name table, SAN/GeneralName formatter, EC-signature splitter, hex wrapping). Regression coverage added for `ParseX509Certificate` ([tests/operations/tests/ParseX509Certificate.mjs](tests/operations/tests/ParseX509Certificate.mjs)).
|
||||||
|
- **PR 5 blocker (still relevant for PR 6+):** `@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
|
||||||
|
|
||||||
|
[jsrsasign](https://github.com/kjur/jsrsasign) (currently pinned at `^11.1.3` in [package.json:148](package.json#L148)) has [announced end-of-support](https://github.com/kjur/jsrsasign/blob/master/README.md#end-of-support-announcement-for-jsrsasign). 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](https://github.com/PeculiarVentures/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](https://github.com/PeculiarVentures/ASN1.js)** (^3) | Generic ASN.1 parse + tree dump for `ParseASN1HexString` | Same author. BSD-3. Pulled in transitively by @peculiar/x509 anyway. |
|
||||||
|
| **[@noble/curves](https://github.com/paulmillr/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](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/](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:**
|
||||||
|
- [src/core/lib/SM2.mjs](src/core/lib/SM2.mjs) — full rewrite, drop jsrsasign
|
||||||
|
- [src/core/lib/PublicKey.mjs](src/core/lib/PublicKey.mjs) — extend `formatDnObj` to accept @peculiar/x509's `Name.toJSON()` shape
|
||||||
|
- **NEW** [src/core/lib/Asn1.mjs](src/core/lib/Asn1.mjs) — `oidHexToInt`, `oidIntToHex`, `derToPem`, `dumpAsn1Hex`
|
||||||
|
- **NEW** [src/core/lib/Ecdsa.mjs](src/core/lib/Ecdsa.mjs) — shared ECDSA helpers (key load, sign/verify, sig format conversions)
|
||||||
|
|
||||||
|
**Build/manifest:**
|
||||||
|
- [package.json](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](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:
|
||||||
|
- [HexToObjectIdentifier.mjs](src/core/operations/HexToObjectIdentifier.mjs): `r.KJUR.asn1.ASN1Util.oidHexToInt` → `oidHexToInt`.
|
||||||
|
- [ObjectIdentifierToHex.mjs](src/core/operations/ObjectIdentifierToHex.mjs): `r.KJUR.asn1.ASN1Util.oidIntToHex` → `oidIntToHex`.
|
||||||
|
- [HexToPEM.mjs](src/core/operations/HexToPEM.mjs): `r.KJUR.asn1.ASN1Util.getPEMStringFromHex` → `derToPem`.
|
||||||
|
- [ParseASN1HexString.mjs](src/core/operations/ParseASN1HexString.mjs): `r.ASN1HEX.dump` → `dumpAsn1Hex`.
|
||||||
|
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](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](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](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](src/core/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](src/core/operations/ECDSAVerify.mjs): `isAsn1Hex` for input-format auto-detect; `verifyEcdsa`.
|
||||||
|
- [ECDSASignatureConversion.mjs](src/core/operations/ECDSASignatureConversion.mjs): format conversions only — no key handling.
|
||||||
|
- [GenerateECDSAKeyPair.mjs](src/core/operations/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](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](src/core/operations/PEMToJWK.mjs), [JWKToPem.mjs](src/core/operations/JWKToPem.mjs), [PubKeyFromPrivKey.mjs](src/core/operations/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/JWK.mjs), [tests/operations/tests/PubKeyFromPrivKey.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](src/core/operations/ParseX509Certificate.mjs) (currently uncovered — confirm with `ls tests/operations/tests/ | grep -i x509certificate`). Use the existing test certs from [PubKeyFromCert.mjs](tests/operations/tests/PubKeyFromCert.mjs).
|
||||||
|
2. Extend [src/core/lib/PublicKey.mjs](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](src/core/operations/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](tests/operations/tests/ParseCSR.mjs), [ParseX509CRL.mjs](tests/operations/tests/ParseX509CRL.mjs), [PubKeyFromCert.mjs](tests/operations/tests/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](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](src/core/lib/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.
|
||||||
|
|
||||||
|
### Post-migration test cleanup — 2026-05-18
|
||||||
|
- **Ed25519 / Ed448 expected pubkeys in [tests/operations/tests/PubKeyFromCert.mjs](tests/operations/tests/PubKeyFromCert.mjs) cross-verified externally.** Ed25519 matches `openssl x509 -pubkey -noout` byte-for-byte; Ed448 was verified by extracting the SPKI directly from the cert's DER bytes (openssl 3.6.2 in this environment doesn't support Ed448). A short comment above each `ED*_PUBKEY` constant records the verification so the fixtures aren't read as circular snapshots of `@peculiar/x509`'s own output.
|
||||||
|
- **[tests/node/tests/nodeApi.mjs](tests/node/tests/nodeApi.mjs) recipe-format tests no longer feed `Hex to PEM` garbage.** The three `chef.bake` tests previously piped Morse-coded output (dashes/dots) through `Hex to PEM`, which is the reason PR 1 made `derToPem` lenient about non-hex input. They now use `To Hex` → `Hex to PEM` → `To Snake case` and produce well-formed PEM of `"some input"`. Same coverage of compact JSON / clean JSON / optional-args parsing; `derToPem`'s leniency is no longer test-load-bearing (but kept for backwards compatibility).
|
||||||
|
|
||||||
|
### PR 6 — 2026-05-17
|
||||||
|
- Removed `jsrsasign` from [package.json](package.json) and [package-lock.json](package-lock.json). No runtime code changes were needed because PRs 1-5 had already moved operations to `@noble/curves`, `@peculiar/x509`, and `asn1js`.
|
||||||
|
- Removed stale dependency-name mentions from `src/` comments so `rg -n "jsrsasign" src/` is clean. Remaining mentions are intentionally limited to migration documentation and [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
- Added a top-level [CHANGELOG.md](CHANGELOG.md) entry noting the removal and the accepted cosmetic output drift across the migrated ASN.1, ECDSA, key conversion, and X.509-family operations.
|
||||||
|
|
||||||
|
### PR 5 — 2026-05-17
|
||||||
|
- New [src/core/lib/X509.mjs](src/core/lib/X509.mjs) holds the shared X.509 helpers (`decodeX509Input`, `sigAlgOidToName`, `describeSpki`, `parseDerEcdsaSignature`, `isDerEcdsaSignature`, `formatJsonName`, `formatGeneralName`, `formatHexByteLines`/`formatHexColonWrapped`, `asnNameToJson`). [src/core/lib/PublicKey.mjs](src/core/lib/PublicKey.mjs)'s `formatDnObj` now accepts both the legacy jsrsasign shape and `@peculiar/x509`'s `JsonName` (array-of-records) shape. Migrated ops: [ParseX509Certificate.mjs](src/core/operations/ParseX509Certificate.mjs), [PubKeyFromCert.mjs](src/core/operations/PubKeyFromCert.mjs), [ParseCSR.mjs](src/core/operations/ParseCSR.mjs), [ParseX509CRL.mjs](src/core/operations/ParseX509CRL.mjs).
|
||||||
|
- **`X509Certificate` v1.14.3 doesn't expose `.version`.** Plan called for `cert.version`, but on the pinned v1 the property is missing. Worked around by parsing `cert.rawData` with the asn1-x509 `Certificate` schema and reading `tbsCertificate.version` + `signatureAlgorithm.algorithm` directly. The same approach is used in `ParseCSR` (with `CertificationRequest` from `@peculiar/asn1-csr`) and `ParseX509CRL` (with `CertificateList` from `@peculiar/asn1-x509`) — bypassing the WebCrypto algorithm mapping is necessary for DSA anyway (peculiar's algorithm provider doesn't know it).
|
||||||
|
- **DSA bit-length now matches OpenSSL/jsrsasign output.** `describeSpki` strips the leading `00` byte from the DSS-Parms `p` INTEGER before computing the bit length, so `Length: 2048 bits` is reported rather than the 2056 the raw byte count would give.
|
||||||
|
- **P-521 reports `Length: 521 bits`**, not 528. `EC_CURVE_OID_TO_NAMES` carries an explicit `bits` field per curve so the prime field size (rather than `byteLen * 8`) is reported.
|
||||||
|
- **KeyUsage rendering order.** `KeyUsage.toJSON()` on the asn1-x509 wrapper returns the flag names in alphabetical order (`crlSign`, `dataEncipherment`, …); the existing CSR golden fixtures expect bit-position order (`digitalSignature`, `nonRepudiation`, `keyEncipherment`, …). The op now parses the BIT STRING and iterates the bits in order so the output is stable.
|
||||||
|
- **`Public Key from Certificate` now works for Ed25519 / Ed448.** jsrsasign threw "Unsupported public key type"; `@peculiar/x509`'s `cert.publicKey.toString("pem")` handles both. Updated [tests/operations/tests/PubKeyFromCert.mjs](tests/operations/tests/PubKeyFromCert.mjs) — the previously-commented `ED25519_PUBKEY` / `ED448_PUBKEY` constants are live and the two negative test expectations were replaced with the actual PEMs.
|
||||||
|
- **PEM line endings switched from `\r\n` to `\n` everywhere** (per the cross-PR convention in [AGENTS.md](AGENTS.md)). [tests/operations/tests/PubKeyFromCert.mjs](tests/operations/tests/PubKeyFromCert.mjs) loses its `.replace(/\r/g, "").replace(/\n/g, "\r\n")` post-processing.
|
||||||
|
- **OtherName payload is parsed loosely.** The legacy `ParseCSR` fixture for `Other: 1.2.3.4::some value` requires extracting a UTF8String wrapped in the ANY-typed `value` field. `formatGeneralName` first tries to parse it as a primitive `UTF8String`/`BmpString`/`PrintableString`/`IA5String`, then falls back to `DirectoryString`, and finally to a hex dump. Same code path serves the CRL OtherName output (with `OtherName:` label, no space — per the CRL flavor) and the CSR/Cert SAN output (`Other: ` label, with space — per the CSR flavor).
|
||||||
|
- **`ParseX509Certificate` no longer relies on `cert.getInfo()` string-splitting.** The legacy code grabbed the extensions block by splitting on hard-coded delimiters. Replaced with a real walker over `cert.extensions` that recognises BasicConstraints, KeyUsage, EKU, SAN, AKI, SKI, CRLDistributionPoints, IssuerAltName; everything else falls back to `<oid>:` + raw hex.
|
||||||
|
- **Cosmetic drift in the `Certificate Signature` block for ECDSA certs.** The old code split the DER signature with hard-coded offsets (`r.ASN1HEX.getV(sig, 4)` and `getV(sig, 48)`), which only worked for 32-byte components and produced visibly-overlapping output otherwise. The new code uses the proper DER parser. The output difference is a *correction*, not a regression — but it's still drift, and means the `ParseX509Certificate` golden output now reflects the true r/s values.
|
||||||
|
- **New regression coverage for `ParseX509Certificate`.** [tests/operations/tests/ParseX509Certificate.mjs](tests/operations/tests/ParseX509Certificate.mjs) carries golden output for an RSA cert and a P-256 cert (reusing the certs from [PubKeyFromCert.mjs](tests/operations/tests/PubKeyFromCert.mjs)), plus an empty-input test. Wired into [tests/operations/index.mjs](tests/operations/index.mjs).
|
||||||
|
- No new dependencies — everything reuses what PR 1 already pinned.
|
||||||
|
|
||||||
|
### PR 4 — 2026-05-17
|
||||||
|
- New [src/core/lib/KeyConvert.mjs](src/core/lib/KeyConvert.mjs) holds the shared RSA/EC/DSA conversion helpers (`parseKeyPem`, `parseCertPublicKey`, `keyToJwk`, `keyFromJwk`, `keyInfoToPem`, `derivePublicKeyInfo`). Plan called for inlining helpers per-op; factoring them out avoided duplicating the RSA SPKI/PKCS#8 plumbing between [PEMToJWK.mjs](src/core/operations/PEMToJWK.mjs), [JWKToPem.mjs](src/core/operations/JWKToPem.mjs) and [PubKeyFromPrivKey.mjs](src/core/operations/PubKeyFromPrivKey.mjs).
|
||||||
|
- **node-forge dropped from the migration.** The plan reserved node-forge for the DSA path in `PubKeyFromPrivKey`, but `node-forge` only ships RSA support in its `pki` module — no DSA parser/serialiser. DSA traditional `-----BEGIN DSA PRIVATE KEY-----` is parsed via `asn1js.fromBER` directly, and the SPKI is built via `new asn1js.Sequence({value: [new Integer(...), ...]}).toBER(false)`. No new dependency.
|
||||||
|
- **RSA PEMs go through `@peculiar/asn1-rsa` schemas** (`RSAPrivateKey`, `RSAPublicKey`) plus the existing `PrivateKeyInfo` / `SubjectPublicKeyInfo` / `AlgorithmIdentifier` schemas. `id_rsaEncryption` is namespace-imported (`import * as rsaSchemas from "@peculiar/asn1-rsa"`) and aliased to `ID_RSA_ENCRYPTION` to dodge the ESLint `camelcase` rule, matching the convention PR 3 established for the EC OID constants.
|
||||||
|
- **DER `00`/INTEGER plumbing.** Parsed INTEGER fields come out of asn1js with the DER 2's-complement leading `00` still present; `stripLeadingZero` removes it for JWK output. On the way back the helper `prefixForDerInt` re-adds the `00` byte when the magnitude's MSB is set so the serialised INTEGER stays positive. JWK base64url is hand-rolled (no padding) — `toBase64(..., "A-Za-z0-9-_")` in `Base64.mjs` requires going through `Utils.strToArrayBuffer` for string inputs, which would double-encode the bytes for non-ASCII content.
|
||||||
|
- **PEM line endings switched from `\r\n` to `\n`** in the test fixtures for [tests/operations/tests/JWK.mjs](tests/operations/tests/JWK.mjs) and [tests/operations/tests/PubKeyFromPrivKey.mjs](tests/operations/tests/PubKeyFromPrivKey.mjs), per the cross-PR convention in [AGENTS.md](AGENTS.md). The fixtures previously did `.replace(/\n/g, "\r\n")` around every expected PEM; those are gone now.
|
||||||
|
- **Ed25519/Ed448 error-message drift.** PubKeyFromPrivKey's "Unsupported key type" wrapper used to surface jsrsasign's `Error: malformed PKCS8 private key(code:004)`; with the EC parser now coming from `@peculiar/asn1-ecc` + `loadEcKey`, the inner error is the `OperationError("Provided key is not an EC key.")` raised inside [src/core/lib/Ecdsa.mjs](src/core/lib/Ecdsa.mjs). Updated the two `Ed25519` / `Ed448` test fixtures to expect the new message (`Unsupported key type: Error: Provided key is not an EC key.` — `OperationError`'s name field is `Error`, hence the `Error:` prefix).
|
||||||
|
- **`Unsupported JWK key type` error now references `jwk.kty`** rather than the top-level `inputJson.kty`. The legacy op compared against `jwk.kty` but reported `inputJson.kty`, which was undefined for JWK Set / array inputs. Existing test passes either way because it uses a single-key OKP input.
|
||||||
|
- No new dependencies; relies on the deps PR 1 already pinned (`@peculiar/asn1-rsa` etc. ship with `@peculiar/x509`).
|
||||||
|
|
||||||
|
### PR 3 — 2026-05-17
|
||||||
|
- New [src/core/lib/Ecdsa.mjs](src/core/lib/Ecdsa.mjs) replaces the jsrsasign calls in [ECDSASign.mjs](src/core/operations/ECDSASign.mjs), [ECDSAVerify.mjs](src/core/operations/ECDSAVerify.mjs), [ECDSASignatureConversion.mjs](src/core/operations/ECDSASignatureConversion.mjs) and [GenerateECDSAKeyPair.mjs](src/core/operations/GenerateECDSAKeyPair.mjs). `loadEcKey` handles SEC1, PKCS#8 and SPKI PEMs by parsing them with `@peculiar/asn1-ecc` / `@peculiar/asn1-pkcs8` / `@peculiar/asn1-x509`. ECDSA itself is `@noble/curves`'s `p256` / `p384` / `p521` with explicit `{ prehash: false, lowS: false, format: "der" }` — see notes below.
|
||||||
|
- **`lowS: false` on both sign and verify.** Noble defaults to `lowS: true` (BTC/ETH-style malleability rejection), which (a) would normalise produced signatures and could diverge from jsrsasign byte-for-byte, and (b) would reject existing jsrsasign-produced signatures whose `s` happened to be in the upper half. We disable lowS to preserve interoperability with the existing fixtures.
|
||||||
|
- **`prehash: false`.** The operation hashes the message itself (so MD5/SHA-1 work via `@noble/hashes/legacy`); noble would otherwise re-hash with the curve's default digest. The Sign/Verify operations call `digestBytes(...)` and pass the digest in directly.
|
||||||
|
- **Latin-1 truncation for string→bytes.** Both Sign (input string) and Verify (`Utils.convertToByteString(msg, "Raw")` → string) follow what jsrsasign did under the hood: each JS code unit is masked to its low byte. That's wrong for free-text UTF-8 but matches the existing behaviour and is what the round-trip tests assume. Encoded as `strToBytesLatin1` in `Ecdsa.mjs`.
|
||||||
|
- **`parseAsn1SigToHexRS` keeps the DER `00` prefix.** The jsrsasign helper returned r/s as the raw INTEGER bytes (so a 32-byte r whose MSB was set came back as 33 hex bytes starting with `00`). The Raw JSON test fixture in [tests/operations/tests/ECDSA.mjs](tests/operations/tests/ECDSA.mjs) depends on that. Replicated with an inline DER reader rather than going through `bigint` (which would strip the leading zero).
|
||||||
|
- **No fixture changes.** All 83 ECDSA tests pass unchanged: deterministic-k sign↔verify cycles, the canned P-256 SHA-256 ASN.1/P1363/JWS/JSON inputs, and the negative tests (RSA key rejected, private-where-public expected, JSON missing r/s, etc.). The Generate ECDSA Key Pair op has no fixtures; manually exercised PEM/DER/JWK output paths.
|
||||||
|
- `id_*` OID constants from `@peculiar/asn1-ecc` are namespace-imported and re-aliased to SCREAMING_SNAKE locals because ESLint's `camelcase` rule trips on the snake_case export names.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
- `derToPem` was deliberately made lenient (whitespace stripped, odd length left-padded with `0`, non-hex chars treated as nibble `0`) to preserve the recipe-API tests that piped non-hex output from `To Morse Code` through `Hex to PEM`. The actual base64 emitted now follows standard byte-pair semantics, not jsrsasign's quirky `hex2b64` (3-hex-chars-→-2-base64-chars) layout — so the expected outputs in `tests/node/tests/nodeApi.mjs` for those recipe-format tests were regenerated.
|
||||||
|
- `dumpAsn1Hex` returns a plain `ASN.1 parse error: …` string when asn1js can't make sense of the input, rather than throwing. jsrsasign produced a best-effort `UNKNOWN(<tag>) <bytes>` dump in this case; replicating that on asn1js would be a meaningful chunk of code and the operation has no automated-fixture exposure of the difference, so we accepted the drift.
|
||||||
|
- PEM line endings switched from `\r\n` to `\n` (per the cross-PR convention in [AGENTS.md](AGENTS.md)). Updated the `Hex to PEM` and `Parse ASN.1 hex string` golden tests in `tests/node/tests/operations.mjs`.
|
||||||
|
- New per-op fixture file [tests/operations/tests/ASN1.mjs](tests/operations/tests/ASN1.mjs) covers OID round-trips, the multi-byte OID arc edge case, PEM line wrapping, and basic ASN.1 dumps. Wired in via `tests/operations/index.mjs`.
|
||||||
|
|
||||||
|
<!-- e.g.
|
||||||
|
### PR 2 — 2026-05-20
|
||||||
|
- Added `@noble/hashes` peer-dep bump (was on ^1.x, needed ^2 for the `legacy` subpath). No code impact.
|
||||||
|
- SM2 ciphertext fixture #3 was already wrong (decoded to the wrong plaintext under jsrsasign too) — confirmed via independent SM2 implementation, regenerated fixture.
|
||||||
|
-->
|
||||||
50
package-lock.json
generated
50
package-lock.json
generated
@ -13,11 +13,14 @@
|
|||||||
"@alexaltea/capstone-js": "^3.0.5",
|
"@alexaltea/capstone-js": "^3.0.5",
|
||||||
"@astronautlabs/amf": "^0.0.6",
|
"@astronautlabs/amf": "^0.0.6",
|
||||||
"@blu3r4y/lzma": "^2.3.3",
|
"@blu3r4y/lzma": "^2.3.3",
|
||||||
|
"@noble/curves": "^2.2.0",
|
||||||
"@noble/hashes": "2.2.0",
|
"@noble/hashes": "2.2.0",
|
||||||
|
"@peculiar/x509": "^1.14.3",
|
||||||
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
|
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
|
||||||
"@xmldom/xmldom": "^0.8.13",
|
"@xmldom/xmldom": "^0.8.13",
|
||||||
"argon2-browser": "^1.18.0",
|
"argon2-browser": "^1.18.0",
|
||||||
"arrive": "^2.5.3",
|
"arrive": "^2.5.3",
|
||||||
|
"asn1js": "^3.0.10",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"avsc": "^5.7.9",
|
"avsc": "^5.7.9",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@ -62,7 +65,6 @@
|
|||||||
"jsonpath-plus": "^10.4.0",
|
"jsonpath-plus": "^10.4.0",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"jsrsasign": "^11.1.3",
|
|
||||||
"kbpgp": "^2.1.17",
|
"kbpgp": "^2.1.17",
|
||||||
"libbzip2-wasm": "0.0.4",
|
"libbzip2-wasm": "0.0.4",
|
||||||
"libyara-wasm": "^1.2.1",
|
"libyara-wasm": "^1.2.1",
|
||||||
@ -4238,6 +4240,21 @@
|
|||||||
"archiver": "^5.3.1"
|
"archiver": "^5.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/curves": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
||||||
@ -4254,7 +4271,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz",
|
||||||
"integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==",
|
"integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.6.0",
|
"@peculiar/asn1-schema": "^2.6.0",
|
||||||
@ -4268,7 +4284,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz",
|
||||||
"integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==",
|
"integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.6.0",
|
"@peculiar/asn1-schema": "^2.6.0",
|
||||||
@ -4281,7 +4296,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz",
|
||||||
"integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==",
|
"integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.6.0",
|
"@peculiar/asn1-schema": "^2.6.0",
|
||||||
@ -4294,7 +4308,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz",
|
||||||
"integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==",
|
"integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-cms": "^2.6.1",
|
"@peculiar/asn1-cms": "^2.6.1",
|
||||||
@ -4309,7 +4322,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz",
|
||||||
"integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==",
|
"integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.6.0",
|
"@peculiar/asn1-schema": "^2.6.0",
|
||||||
@ -4322,7 +4334,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz",
|
||||||
"integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==",
|
"integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-cms": "^2.6.1",
|
"@peculiar/asn1-cms": "^2.6.1",
|
||||||
@ -4339,7 +4350,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz",
|
||||||
"integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==",
|
"integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.6.0",
|
"@peculiar/asn1-schema": "^2.6.0",
|
||||||
@ -4352,7 +4362,6 @@
|
|||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz",
|
||||||
"integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==",
|
"integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asn1js": "^3.0.6",
|
"asn1js": "^3.0.6",
|
||||||
@ -4364,7 +4373,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz",
|
||||||
"integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==",
|
"integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.6.0",
|
"@peculiar/asn1-schema": "^2.6.0",
|
||||||
@ -4377,7 +4385,6 @@
|
|||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz",
|
||||||
"integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==",
|
"integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.6.0",
|
"@peculiar/asn1-schema": "^2.6.0",
|
||||||
@ -4390,7 +4397,6 @@
|
|||||||
"version": "1.14.3",
|
"version": "1.14.3",
|
||||||
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
|
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
|
||||||
"integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
|
"integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-cms": "^2.6.0",
|
"@peculiar/asn1-cms": "^2.6.0",
|
||||||
@ -5466,14 +5472,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/asn1js": {
|
"node_modules/asn1js": {
|
||||||
"version": "3.0.7",
|
"version": "3.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
|
||||||
"integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==",
|
"integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pvtsutils": "^1.3.6",
|
"pvtsutils": "^1.3.6",
|
||||||
"pvutils": "^1.1.3",
|
"pvutils": "^1.1.5",
|
||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -12460,12 +12465,6 @@
|
|||||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/jsrsasign": {
|
|
||||||
"version": "11.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.3.tgz",
|
|
||||||
"integrity": "sha512-nPnK5D/4lv0Dwr7TlzrKtAd8JlLZwFTqTUUB3NQCbtdobcRcohGFxjbPySDVh74iWUudcCsapYT6OxoyhJLhhA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/jszip": {
|
"node_modules/jszip": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||||
@ -15287,7 +15286,6 @@
|
|||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
||||||
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
@ -15297,7 +15295,6 @@
|
|||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
|
||||||
"integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
|
"integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
@ -17449,14 +17446,12 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsyringe": {
|
"node_modules/tsyringe": {
|
||||||
"version": "4.10.0",
|
"version": "4.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
|
||||||
"integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
|
"integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^1.9.3"
|
"tslib": "^1.9.3"
|
||||||
@ -17469,7 +17464,6 @@
|
|||||||
"version": "1.14.1",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tweetnacl": {
|
"node_modules/tweetnacl": {
|
||||||
|
|||||||
@ -97,11 +97,14 @@
|
|||||||
"@alexaltea/capstone-js": "^3.0.5",
|
"@alexaltea/capstone-js": "^3.0.5",
|
||||||
"@astronautlabs/amf": "^0.0.6",
|
"@astronautlabs/amf": "^0.0.6",
|
||||||
"@blu3r4y/lzma": "^2.3.3",
|
"@blu3r4y/lzma": "^2.3.3",
|
||||||
|
"@noble/curves": "^2.2.0",
|
||||||
"@noble/hashes": "2.2.0",
|
"@noble/hashes": "2.2.0",
|
||||||
|
"@peculiar/x509": "^1.14.3",
|
||||||
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
|
"@wavesenterprise/crypto-gost-js": "^2.1.0-RC1",
|
||||||
"@xmldom/xmldom": "^0.8.13",
|
"@xmldom/xmldom": "^0.8.13",
|
||||||
"argon2-browser": "^1.18.0",
|
"argon2-browser": "^1.18.0",
|
||||||
"arrive": "^2.5.3",
|
"arrive": "^2.5.3",
|
||||||
|
"asn1js": "^3.0.10",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
"avsc": "^5.7.9",
|
"avsc": "^5.7.9",
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@ -146,7 +149,6 @@
|
|||||||
"jsonpath-plus": "^10.4.0",
|
"jsonpath-plus": "^10.4.0",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"jsrsasign": "^11.1.3",
|
|
||||||
"kbpgp": "^2.1.17",
|
"kbpgp": "^2.1.17",
|
||||||
"libbzip2-wasm": "0.0.4",
|
"libbzip2-wasm": "0.0.4",
|
||||||
"libyara-wasm": "^1.2.1",
|
"libyara-wasm": "^1.2.1",
|
||||||
|
|||||||
349
src/core/lib/Asn1.mjs
Normal file
349
src/core/lib/Asn1.mjs
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* ASN.1 / OID / PEM helpers.
|
||||||
|
*
|
||||||
|
* Helpers used by the HexToObjectIdentifier, ObjectIdentifierToHex,
|
||||||
|
* HexToPEM and ParseASN1HexString operations.
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2016
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fromBER } from "asn1js";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a BER-encoded OID (as a hex string) to its dotted-decimal form.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function oidHexToInt(hex) {
|
||||||
|
const cleaned = hex.replace(/\s/g, "").toLowerCase();
|
||||||
|
if (cleaned.length === 0 || cleaned.length % 2 !== 0 || !/^[0-9a-f]+$/.test(cleaned)) {
|
||||||
|
throw new OperationError("Invalid hex string");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(cleaned.length / 2);
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
bytes[i] = parseInt(cleaned.substr(i * 2, 2), 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
const varints = [];
|
||||||
|
let value = 0n;
|
||||||
|
for (const b of bytes) {
|
||||||
|
value = (value << 7n) | BigInt(b & 0x7f);
|
||||||
|
if ((b & 0x80) === 0) {
|
||||||
|
varints.push(value);
|
||||||
|
value = 0n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value !== 0n) {
|
||||||
|
throw new OperationError("Malformed OID: ends with continuation byte");
|
||||||
|
}
|
||||||
|
if (varints.length === 0) {
|
||||||
|
throw new OperationError("Empty OID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const arcs = [];
|
||||||
|
const first = varints[0];
|
||||||
|
if (first < 40n) {
|
||||||
|
arcs.push("0", first.toString());
|
||||||
|
} else if (first < 80n) {
|
||||||
|
arcs.push("1", (first - 40n).toString());
|
||||||
|
} else {
|
||||||
|
arcs.push("2", (first - 80n).toString());
|
||||||
|
}
|
||||||
|
for (let i = 1; i < varints.length; i++) {
|
||||||
|
arcs.push(varints[i].toString());
|
||||||
|
}
|
||||||
|
return arcs.join(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a dotted-decimal OID to its BER hex encoding.
|
||||||
|
*
|
||||||
|
* @param {string} oid
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function oidIntToHex(oid) {
|
||||||
|
if (typeof oid !== "string" || oid.length === 0) {
|
||||||
|
throw new OperationError("Empty OID");
|
||||||
|
}
|
||||||
|
const arcs = oid.split(".").map(a => {
|
||||||
|
if (!/^\d+$/.test(a)) {
|
||||||
|
throw new OperationError(`Invalid OID arc: ${a}`);
|
||||||
|
}
|
||||||
|
return BigInt(a);
|
||||||
|
});
|
||||||
|
if (arcs.length < 2) {
|
||||||
|
throw new OperationError("OID must have at least two arcs");
|
||||||
|
}
|
||||||
|
if (arcs[0] > 2n) {
|
||||||
|
throw new OperationError("First arc must be 0, 1 or 2");
|
||||||
|
}
|
||||||
|
if (arcs[0] < 2n && arcs[1] > 39n) {
|
||||||
|
throw new OperationError("Second arc must be ≤ 39 when first arc is 0 or 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = [arcs[0] * 40n + arcs[1], ...arcs.slice(2)];
|
||||||
|
const out = [];
|
||||||
|
for (const v of values) {
|
||||||
|
if (v === 0n) {
|
||||||
|
out.push(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const parts = [];
|
||||||
|
let n = v;
|
||||||
|
while (n > 0n) {
|
||||||
|
parts.unshift(Number(n & 0x7fn));
|
||||||
|
n >>= 7n;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) parts[i] |= 0x80;
|
||||||
|
out.push(...parts);
|
||||||
|
}
|
||||||
|
return out.map(b => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a hex-encoded DER blob in a PEM envelope.
|
||||||
|
*
|
||||||
|
* Uses LF line endings only.
|
||||||
|
*
|
||||||
|
* Input parsing is intentionally lenient: whitespace is stripped, an
|
||||||
|
* odd-length string is left-padded with a zero, and characters that are not
|
||||||
|
* hex digits are treated as the nibble `0`. This keeps the operation usable
|
||||||
|
* as a generic byte-emitter inside larger recipes where the upstream stage
|
||||||
|
* may not produce strict hex.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @param {string} label
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function derToPem(hex, label) {
|
||||||
|
let cleaned = hex.replace(/\s/g, "");
|
||||||
|
if (cleaned.length % 2 !== 0) cleaned = "0" + cleaned;
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(cleaned.length / 2);
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
const v = parseInt(cleaned.substr(i * 2, 2), 16);
|
||||||
|
bytes[i] = Number.isNaN(v) ? 0 : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
return derBytesToPem(bytes, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap raw DER bytes in a PEM envelope with LF line endings.
|
||||||
|
*
|
||||||
|
* Canonical helper shared by the key/ECDSA conversion modules; the hex-input
|
||||||
|
* {@link derToPem} above delegates here after parsing.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {string} label
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function derBytesToPem(bytes, label) {
|
||||||
|
let b64;
|
||||||
|
if (typeof Buffer !== "undefined") {
|
||||||
|
b64 = Buffer.from(bytes).toString("base64");
|
||||||
|
} else {
|
||||||
|
let bin = "";
|
||||||
|
for (const b of bytes) bin += String.fromCharCode(b);
|
||||||
|
b64 = btoa(bin);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = b64.length === 0 ? [""] : b64.match(/.{1,64}/g);
|
||||||
|
return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk an asn1js parse tree and produce CyberChef's indented ASN.1 dump.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {number} [options.truncate=32] - max bytes of an OCTET/BIT/printable value shown before truncating
|
||||||
|
* @param {number} [options.startIndex=0] - hex-character offset to start parsing at
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function dumpAsn1Hex(hex, options = {}) {
|
||||||
|
const truncate = options.truncate ?? 32;
|
||||||
|
const startIndex = options.startIndex ?? 0;
|
||||||
|
|
||||||
|
let cleaned = hex.replace(/\s/g, "").toLowerCase();
|
||||||
|
if (cleaned.length % 2 !== 0) cleaned = "0" + cleaned;
|
||||||
|
const slice = cleaned.slice(startIndex);
|
||||||
|
const bytes = new Uint8Array(slice.length / 2);
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
const v = parseInt(slice.substr(i * 2, 2), 16);
|
||||||
|
bytes[i] = Number.isNaN(v) ? 0 : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = fromBER(bytes.buffer);
|
||||||
|
const lines = [];
|
||||||
|
if (result.offset === -1 || !result.result || (result.result.error && !result.result.idBlock)) {
|
||||||
|
return `ASN.1 parse error: ${(result.result && result.result.error) || "unknown"}`;
|
||||||
|
}
|
||||||
|
formatAsn1Node(result.result, 0, truncate, lines);
|
||||||
|
if (result.offset === -1 && result.result && result.result.error) {
|
||||||
|
lines.push(`(parse warning: ${result.result.error})`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively format one asn1js node.
|
||||||
|
*
|
||||||
|
* @param {Object} node
|
||||||
|
* @param {number} depth
|
||||||
|
* @param {number} truncate
|
||||||
|
* @param {string[]} out
|
||||||
|
*/
|
||||||
|
function formatAsn1Node(node, depth, truncate, out) {
|
||||||
|
const pad = " ".repeat(depth);
|
||||||
|
const idBlock = node.idBlock || {};
|
||||||
|
const valueBlock = node.valueBlock || {};
|
||||||
|
const tagClass = idBlock.tagClass;
|
||||||
|
const tagNumber = idBlock.tagNumber;
|
||||||
|
const isConstructed = !!idBlock.isConstructed;
|
||||||
|
|
||||||
|
if (tagClass !== 1) {
|
||||||
|
const className = ["", "UNIVERSAL", "APPLICATION", "CONTEXT", "PRIVATE"][tagClass] || "UNKNOWN";
|
||||||
|
const label = `[${className} ${tagNumber}]${isConstructed ? " (constructed)" : ""}`;
|
||||||
|
if (isConstructed && Array.isArray(valueBlock.value)) {
|
||||||
|
out.push(`${pad}${label}`);
|
||||||
|
for (const child of valueBlock.value) formatAsn1Node(child, depth + 1, truncate, out);
|
||||||
|
} else {
|
||||||
|
out.push(`${pad}${label} ${truncateHex(extractHex(valueBlock), truncate)}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctorName = node.constructor && node.constructor.name;
|
||||||
|
switch (ctorName) {
|
||||||
|
case "Sequence":
|
||||||
|
out.push(`${pad}SEQUENCE`);
|
||||||
|
for (const child of valueBlock.value || []) formatAsn1Node(child, depth + 1, truncate, out);
|
||||||
|
return;
|
||||||
|
case "Set":
|
||||||
|
out.push(`${pad}SET`);
|
||||||
|
for (const child of valueBlock.value || []) formatAsn1Node(child, depth + 1, truncate, out);
|
||||||
|
return;
|
||||||
|
case "Null":
|
||||||
|
out.push(`${pad}NULL`);
|
||||||
|
return;
|
||||||
|
case "Boolean":
|
||||||
|
out.push(`${pad}BOOLEAN ${valueBlock.value ? "TRUE" : "FALSE"}`);
|
||||||
|
return;
|
||||||
|
case "Integer":
|
||||||
|
out.push(`${pad}INTEGER ${formatIntegerValue(valueBlock)}`);
|
||||||
|
return;
|
||||||
|
case "ObjectIdentifier":
|
||||||
|
out.push(`${pad}ObjectIdentifier ${valueBlock.toJSON().value}`);
|
||||||
|
return;
|
||||||
|
case "OctetString":
|
||||||
|
out.push(`${pad}OCTET STRING ${truncateHex(extractHex(valueBlock), truncate)}`);
|
||||||
|
return;
|
||||||
|
case "BitString":
|
||||||
|
out.push(`${pad}BIT STRING ${truncateHex(extractHex(valueBlock), truncate)}`);
|
||||||
|
return;
|
||||||
|
case "Utf8String":
|
||||||
|
case "PrintableString":
|
||||||
|
case "Ia5String":
|
||||||
|
case "IA5String":
|
||||||
|
case "VisibleString":
|
||||||
|
case "TeletexString":
|
||||||
|
case "UniversalString":
|
||||||
|
case "BmpString":
|
||||||
|
case "BMPString":
|
||||||
|
case "NumericString":
|
||||||
|
case "GeneralString":
|
||||||
|
case "CharacterString":
|
||||||
|
case "GraphicString":
|
||||||
|
case "VideotexString":
|
||||||
|
out.push(`${pad}${ctorName} "${truncateText(valueBlock.value || "", truncate)}"`);
|
||||||
|
return;
|
||||||
|
case "UTCTime":
|
||||||
|
case "GeneralizedTime":
|
||||||
|
out.push(`${pad}${ctorName} ${valueBlock.toString ? node.toString() : truncateHex(extractHex(valueBlock), truncate)}`);
|
||||||
|
return;
|
||||||
|
default: {
|
||||||
|
const label = ctorName || `UNIVERSAL ${tagNumber}`;
|
||||||
|
if (isConstructed && Array.isArray(valueBlock.value)) {
|
||||||
|
out.push(`${pad}${label}`);
|
||||||
|
for (const child of valueBlock.value) formatAsn1Node(child, depth + 1, truncate, out);
|
||||||
|
} else {
|
||||||
|
out.push(`${pad}${label} ${truncateHex(extractHex(valueBlock), truncate)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the hex representation of an asn1js value block.
|
||||||
|
*
|
||||||
|
* @param {Object} valueBlock
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function extractHex(valueBlock) {
|
||||||
|
if (!valueBlock) return "";
|
||||||
|
const view = valueBlock.valueHexView;
|
||||||
|
if (view && view.length) return bufToHex(view);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Uint8Array view to a lowercase hex string.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} buf
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function bufToHex(buf) {
|
||||||
|
let out = "";
|
||||||
|
for (const b of buf) out += b.toString(16).padStart(2, "0");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate hex string to at most `truncate` bytes, appending an ellipsis marker.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @param {number} truncate
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function truncateHex(hex, truncate) {
|
||||||
|
const maxChars = truncate * 2;
|
||||||
|
if (maxChars > 0 && hex.length > maxChars) {
|
||||||
|
return `${hex.slice(0, maxChars)}... (${hex.length / 2} bytes)`;
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate a text string to at most `truncate` characters.
|
||||||
|
*
|
||||||
|
* @param {string} text
|
||||||
|
* @param {number} truncate
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function truncateText(text, truncate) {
|
||||||
|
if (truncate > 0 && text.length > truncate) {
|
||||||
|
return `${text.slice(0, truncate)}... (${text.length} chars)`;
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an Integer's value. asn1js exposes a small int as `valueDec`; for
|
||||||
|
* arbitrary-length ints we fall back to the raw hex view.
|
||||||
|
*
|
||||||
|
* @param {Object} valueBlock
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatIntegerValue(valueBlock) {
|
||||||
|
if (valueBlock.isHexOnly) return extractHex(valueBlock);
|
||||||
|
if (typeof valueBlock.valueDec === "number" && Number.isFinite(valueBlock.valueDec)) {
|
||||||
|
return valueBlock.valueDec.toString();
|
||||||
|
}
|
||||||
|
return extractHex(valueBlock);
|
||||||
|
}
|
||||||
553
src/core/lib/Ecdsa.mjs
Normal file
553
src/core/lib/Ecdsa.mjs
Normal file
@ -0,0 +1,553 @@
|
|||||||
|
/**
|
||||||
|
* Shared ECDSA helpers built on @noble/curves and @peculiar/asn1-*.
|
||||||
|
*
|
||||||
|
* Used by the ECDSA Sign/Verify/Signature Conversion/Generate Key Pair
|
||||||
|
* operations.
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2016
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { p256, p384, p521 } from "@noble/curves/nist.js";
|
||||||
|
import { DER } from "@noble/curves/abstract/weierstrass.js";
|
||||||
|
import { md5, sha1 } from "@noble/hashes/legacy.js";
|
||||||
|
import { sha256, sha384, sha512 } from "@noble/hashes/sha2.js";
|
||||||
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
||||||
|
import { AsnParser, AsnSerializer, OctetString } from "@peculiar/asn1-schema";
|
||||||
|
import { derBytesToPem } from "./Asn1.mjs";
|
||||||
|
import * as ecc from "@peculiar/asn1-ecc";
|
||||||
|
const { ECPrivateKey, ECParameters } = ecc;
|
||||||
|
const ID_EC_PUBLIC_KEY = ecc.id_ecPublicKey;
|
||||||
|
const ID_SECP256R1 = ecc.id_secp256r1;
|
||||||
|
const ID_SECP384R1 = ecc.id_secp384r1;
|
||||||
|
const ID_SECP521R1 = ecc.id_secp521r1;
|
||||||
|
import { PrivateKeyInfo } from "@peculiar/asn1-pkcs8";
|
||||||
|
import { AlgorithmIdentifier, SubjectPublicKeyInfo } from "@peculiar/asn1-x509";
|
||||||
|
import { fromBER } from "asn1js";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
|
const CURVES = {
|
||||||
|
"P-256": { oid: ID_SECP256R1, curve: p256, byteLen: 32 },
|
||||||
|
"P-384": { oid: ID_SECP384R1, curve: p384, byteLen: 48 },
|
||||||
|
"P-521": { oid: ID_SECP521R1, curve: p521, byteLen: 66 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const OID_TO_CURVE = {
|
||||||
|
[ID_SECP256R1]: "P-256",
|
||||||
|
[ID_SECP384R1]: "P-384",
|
||||||
|
[ID_SECP521R1]: "P-521",
|
||||||
|
};
|
||||||
|
|
||||||
|
const HASHES = {
|
||||||
|
"MD5": md5,
|
||||||
|
"SHA-1": sha1,
|
||||||
|
"SHA-256": sha256,
|
||||||
|
"SHA-384": sha384,
|
||||||
|
"SHA-512": sha512,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the @noble/curves ECDSA instance for a curve name.
|
||||||
|
*
|
||||||
|
* @param {string} name - One of "P-256", "P-384", "P-521".
|
||||||
|
* @returns {{oid: string, curve: object, byteLen: number, name: string}}
|
||||||
|
*/
|
||||||
|
export function getCurveByName(name) {
|
||||||
|
const entry = CURVES[name];
|
||||||
|
if (!entry) throw new OperationError(`Unsupported curve: ${name}`);
|
||||||
|
return { ...entry, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a byte string with one of the supported digests.
|
||||||
|
*
|
||||||
|
* @param {string} algo - "MD5", "SHA-1", "SHA-256", "SHA-384" or "SHA-512".
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function digestBytes(algo, bytes) {
|
||||||
|
const fn = HASHES[algo];
|
||||||
|
if (!fn) throw new OperationError(`Unsupported digest: ${algo}`);
|
||||||
|
return fn(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a JS string to bytes by Latin-1 truncation of each code unit (i.e.
|
||||||
|
* `charCodeAt(i) & 0xff`). This preserves the operation's byte-string
|
||||||
|
* semantics for inputs coming out of upstream byte-producing ops, including
|
||||||
|
* the questionable UTF-16 truncation for free-text inputs.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function strToBytesLatin1(str) {
|
||||||
|
const out = new Uint8Array(str.length);
|
||||||
|
for (let i = 0; i < str.length; i++) out[i] = str.charCodeAt(i) & 0xff;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a PEM-encoded EC key (SEC1, PKCS#8 or SPKI).
|
||||||
|
*
|
||||||
|
* @param {string} pem
|
||||||
|
* @returns {{
|
||||||
|
* curveName: string,
|
||||||
|
* curve: object,
|
||||||
|
* byteLen: number,
|
||||||
|
* isPrivate: boolean,
|
||||||
|
* isPublic: boolean,
|
||||||
|
* d: Uint8Array|null,
|
||||||
|
* publicKey: Uint8Array
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function loadEcKey(pem) {
|
||||||
|
const { label, bytes } = pemToDer(pem);
|
||||||
|
|
||||||
|
if (label === "EC PRIVATE KEY") {
|
||||||
|
return parseSec1PrivateKey(bytes);
|
||||||
|
}
|
||||||
|
if (label === "PRIVATE KEY") {
|
||||||
|
return parsePkcs8PrivateKey(bytes);
|
||||||
|
}
|
||||||
|
if (label === "PUBLIC KEY") {
|
||||||
|
return parseSpkiPublicKey(bytes);
|
||||||
|
}
|
||||||
|
if (label === "RSA PRIVATE KEY" || label === "RSA PUBLIC KEY") {
|
||||||
|
throw new OperationError("Provided key is not an EC key.");
|
||||||
|
}
|
||||||
|
throw new OperationError("Provided key is not an EC key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a digest with an EC private key.
|
||||||
|
*
|
||||||
|
* @param {object} keyInfo - Output of {@link loadEcKey}.
|
||||||
|
* @param {Uint8Array} digest
|
||||||
|
* @returns {string} ASN.1 DER signature, hex-encoded.
|
||||||
|
*/
|
||||||
|
export function signEcdsa(keyInfo, digest) {
|
||||||
|
if (!keyInfo.isPrivate || !keyInfo.d) {
|
||||||
|
throw new OperationError("Provided key is not a private key.");
|
||||||
|
}
|
||||||
|
const sig = keyInfo.curve.sign(digest, keyInfo.d, {
|
||||||
|
prehash: false,
|
||||||
|
lowS: false,
|
||||||
|
format: "der",
|
||||||
|
});
|
||||||
|
return bytesToHex(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a DER-encoded ECDSA signature against a digest and public key.
|
||||||
|
*
|
||||||
|
* @param {object} keyInfo - Output of {@link loadEcKey}.
|
||||||
|
* @param {Uint8Array} digest
|
||||||
|
* @param {string} asn1Hex - DER signature, hex-encoded.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function verifyEcdsa(keyInfo, digest, asn1Hex) {
|
||||||
|
if (!keyInfo.isPublic) {
|
||||||
|
throw new OperationError("Provided key is not a public key.");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return keyInfo.curve.verify(hexToBytes(asn1Hex), digest, keyInfo.publicKey, {
|
||||||
|
prehash: false,
|
||||||
|
lowS: false,
|
||||||
|
format: "der",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quick test for whether a hex string parses as a single DER-encoded ASN.1
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isAsn1Hex(hex) {
|
||||||
|
if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) return false;
|
||||||
|
if (!/^[0-9a-f]+$/i.test(hex)) return false;
|
||||||
|
const bytes = hexToBytes(hex);
|
||||||
|
try {
|
||||||
|
const result = fromBER(bytes.buffer);
|
||||||
|
return result.offset !== -1;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an ASN.1 DER-encoded ECDSA signature and return the raw r/s INTEGER
|
||||||
|
* bytes as hex. Preserves the DER 2's-complement leading 0x00 when present
|
||||||
|
* (i.e. r/s may have a leading "00" pair) because the raw-signature fixtures
|
||||||
|
* assert the INTEGER byte strings, not canonicalized scalar values.
|
||||||
|
*
|
||||||
|
* @param {string} asn1Hex
|
||||||
|
* @returns {{r: string, s: string}}
|
||||||
|
*/
|
||||||
|
export function parseAsn1SigToHexRS(asn1Hex) {
|
||||||
|
const bytes = hexToBytes(asn1Hex);
|
||||||
|
let i = 0;
|
||||||
|
if (bytes[i++] !== 0x30) throw new OperationError("Signature is not an ASN.1 SEQUENCE");
|
||||||
|
const seq = readLength(bytes, i);
|
||||||
|
i = seq.next;
|
||||||
|
if (i + seq.value !== bytes.length) throw new OperationError("Trailing bytes after SEQUENCE");
|
||||||
|
|
||||||
|
if (bytes[i++] !== 0x02) throw new OperationError("First element is not an INTEGER");
|
||||||
|
const rLen = readLength(bytes, i);
|
||||||
|
i = rLen.next;
|
||||||
|
const r = bytes.slice(i, i + rLen.value);
|
||||||
|
i += rLen.value;
|
||||||
|
|
||||||
|
if (bytes[i++] !== 0x02) throw new OperationError("Second element is not an INTEGER");
|
||||||
|
const sLen = readLength(bytes, i);
|
||||||
|
i = sLen.next;
|
||||||
|
const s = bytes.slice(i, i + sLen.value);
|
||||||
|
|
||||||
|
return { r: bytesToHex(r), s: bytesToHex(s) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hex r/s pair to a DER-encoded ECDSA signature hex string.
|
||||||
|
*
|
||||||
|
* @param {string} rHex
|
||||||
|
* @param {string} sHex
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function hexRSToAsn1Sig(rHex, sHex) {
|
||||||
|
const r = BigInt("0x" + rHex);
|
||||||
|
const s = BigInt("0x" + sHex);
|
||||||
|
return DER.hexFromSig({ r, s });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a DER-encoded ECDSA signature (hex) to the P1363 / IEEE concat
|
||||||
|
* form (r || s, each fixed-width). The signature's curve is inferred from
|
||||||
|
* the integer sizes — supports P-256/P-384/P-521.
|
||||||
|
*
|
||||||
|
* @param {string} asn1Hex
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function asn1SigToConcatHex(asn1Hex) {
|
||||||
|
const { r, s } = parseAsn1SigToHexRS(asn1Hex);
|
||||||
|
const rStripped = stripDerLeadingZero(r);
|
||||||
|
const sStripped = stripDerLeadingZero(s);
|
||||||
|
const maxBytes = Math.max(rStripped.length, sStripped.length) / 2;
|
||||||
|
|
||||||
|
let coordBytes;
|
||||||
|
if (maxBytes <= 32) coordBytes = 32;
|
||||||
|
else if (maxBytes <= 48) coordBytes = 48;
|
||||||
|
else if (maxBytes <= 66) coordBytes = 66;
|
||||||
|
else throw new OperationError(`Unsupported ECDSA signature size (${maxBytes} bytes per component)`);
|
||||||
|
|
||||||
|
const width = coordBytes * 2;
|
||||||
|
return rStripped.padStart(width, "0") + sStripped.padStart(width, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a concat (P1363) ECDSA signature hex back to ASN.1 DER hex.
|
||||||
|
*
|
||||||
|
* @param {string} concatHex
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function concatHexToAsn1Sig(concatHex) {
|
||||||
|
if (concatHex.length % 4 !== 0) {
|
||||||
|
throw new OperationError("Concat signature length must be a multiple of 4 hex chars");
|
||||||
|
}
|
||||||
|
const half = concatHex.length / 2;
|
||||||
|
return hexRSToAsn1Sig(concatHex.slice(0, half), concatHex.slice(half));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an EC key pair on the named curve.
|
||||||
|
*
|
||||||
|
* @param {string} curveName - "P-256", "P-384" or "P-521".
|
||||||
|
* @returns {{
|
||||||
|
* curveName: string,
|
||||||
|
* byteLen: number,
|
||||||
|
* d: Uint8Array,
|
||||||
|
* publicKey: Uint8Array,
|
||||||
|
* x: Uint8Array,
|
||||||
|
* y: Uint8Array
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function generateEcKeyPair(curveName) {
|
||||||
|
const info = getCurveByName(curveName);
|
||||||
|
const { secretKey, publicKey } = info.curve.keygen();
|
||||||
|
const { x, y } = splitUncompressedPoint(publicKey, info.byteLen);
|
||||||
|
return {
|
||||||
|
curveName,
|
||||||
|
byteLen: info.byteLen,
|
||||||
|
d: secretKey,
|
||||||
|
publicKey,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the public key half of a generated key pair as SPKI PEM.
|
||||||
|
*
|
||||||
|
* @param {object} pair - Output of {@link generateEcKeyPair}.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function publicKeyToSpkiPem(pair) {
|
||||||
|
const info = getCurveByName(pair.curveName);
|
||||||
|
const params = new ECParameters({ namedCurve: info.oid });
|
||||||
|
const spki = new SubjectPublicKeyInfo({
|
||||||
|
algorithm: new AlgorithmIdentifier({
|
||||||
|
algorithm: ID_EC_PUBLIC_KEY,
|
||||||
|
parameters: AsnSerializer.serialize(params),
|
||||||
|
}),
|
||||||
|
subjectPublicKey: pair.publicKey.slice().buffer,
|
||||||
|
});
|
||||||
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the private key half of a generated key pair as PKCS#8 PEM.
|
||||||
|
*
|
||||||
|
* @param {object} pair - Output of {@link generateEcKeyPair}.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function privateKeyToPkcs8Pem(pair) {
|
||||||
|
const info = getCurveByName(pair.curveName);
|
||||||
|
const params = new ECParameters({ namedCurve: info.oid });
|
||||||
|
const ecKey = new ECPrivateKey({
|
||||||
|
version: 1,
|
||||||
|
privateKey: new OctetString(pair.d),
|
||||||
|
publicKey: pair.publicKey.slice().buffer,
|
||||||
|
});
|
||||||
|
const pkcs8 = new PrivateKeyInfo({
|
||||||
|
version: 0,
|
||||||
|
privateKeyAlgorithm: new AlgorithmIdentifier({
|
||||||
|
algorithm: ID_EC_PUBLIC_KEY,
|
||||||
|
parameters: AsnSerializer.serialize(params),
|
||||||
|
}),
|
||||||
|
privateKey: new OctetString(AsnSerializer.serialize(ecKey)),
|
||||||
|
});
|
||||||
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(pkcs8)), "PRIVATE KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---- internals --------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a PEM blob and return the decoded body bytes plus the label
|
||||||
|
* (e.g. "EC PRIVATE KEY").
|
||||||
|
*
|
||||||
|
* @param {string} pem
|
||||||
|
* @returns {{label: string, bytes: Uint8Array}}
|
||||||
|
*/
|
||||||
|
function pemToDer(pem) {
|
||||||
|
const match = pem.match(/-----BEGIN ([A-Z0-9 ]+)-----([\s\S]+?)-----END \1-----/);
|
||||||
|
if (!match) throw new OperationError("Not a valid PEM");
|
||||||
|
const body = match[2].replace(/\s+/g, "");
|
||||||
|
let bin;
|
||||||
|
if (typeof Buffer !== "undefined") {
|
||||||
|
bin = Buffer.from(body, "base64");
|
||||||
|
} else {
|
||||||
|
const decoded = atob(body);
|
||||||
|
bin = new Uint8Array(decoded.length);
|
||||||
|
for (let i = 0; i < decoded.length; i++) bin[i] = decoded.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return { label: match[1], bytes: new Uint8Array(bin.buffer || bin, bin.byteOffset || 0, bin.byteLength || bin.length) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a SEC1 ECPrivateKey blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function parseSec1PrivateKey(bytes) {
|
||||||
|
let ec;
|
||||||
|
try {
|
||||||
|
ec = AsnParser.parse(bytes, ECPrivateKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse EC private key: ${e.message}`);
|
||||||
|
}
|
||||||
|
const curveName = curveFromParameters(ec.parameters);
|
||||||
|
if (!curveName) throw new OperationError("EC private key missing curve parameters");
|
||||||
|
return buildPrivateKeyInfo(curveName, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a PKCS#8 PrivateKeyInfo blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function parsePkcs8PrivateKey(bytes) {
|
||||||
|
let info;
|
||||||
|
try {
|
||||||
|
info = AsnParser.parse(bytes, PrivateKeyInfo);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse PKCS#8 key: ${e.message}`);
|
||||||
|
}
|
||||||
|
if (info.privateKeyAlgorithm.algorithm !== ID_EC_PUBLIC_KEY) {
|
||||||
|
throw new OperationError("Provided key is not an EC key.");
|
||||||
|
}
|
||||||
|
const params = info.privateKeyAlgorithm.parameters;
|
||||||
|
if (!params) throw new OperationError("EC private key missing curve parameters");
|
||||||
|
const ecParams = AsnParser.parse(params, ECParameters);
|
||||||
|
const curveName = curveFromParameters(ecParams);
|
||||||
|
if (!curveName) throw new OperationError("Unsupported EC curve");
|
||||||
|
|
||||||
|
const innerBytes = new Uint8Array(info.privateKey.buffer, info.privateKey.byteOffset, info.privateKey.byteLength);
|
||||||
|
let ec;
|
||||||
|
try {
|
||||||
|
ec = AsnParser.parse(innerBytes, ECPrivateKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse EC private key: ${e.message}`);
|
||||||
|
}
|
||||||
|
return buildPrivateKeyInfo(curveName, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a SubjectPublicKeyInfo blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function parseSpkiPublicKey(bytes) {
|
||||||
|
let spki;
|
||||||
|
try {
|
||||||
|
spki = AsnParser.parse(bytes, SubjectPublicKeyInfo);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse SPKI: ${e.message}`);
|
||||||
|
}
|
||||||
|
if (spki.algorithm.algorithm !== ID_EC_PUBLIC_KEY) {
|
||||||
|
throw new OperationError("Provided key is not an EC key.");
|
||||||
|
}
|
||||||
|
const params = spki.algorithm.parameters;
|
||||||
|
if (!params) throw new OperationError("EC public key missing curve parameters");
|
||||||
|
const ecParams = AsnParser.parse(params, ECParameters);
|
||||||
|
const curveName = curveFromParameters(ecParams);
|
||||||
|
if (!curveName) throw new OperationError("Unsupported EC curve");
|
||||||
|
const info = getCurveByName(curveName);
|
||||||
|
|
||||||
|
const pub = new Uint8Array(spki.subjectPublicKey);
|
||||||
|
if (pub[0] !== 0x04) {
|
||||||
|
throw new OperationError("Only uncompressed EC public keys are supported");
|
||||||
|
}
|
||||||
|
if (pub.length !== 1 + info.byteLen * 2) {
|
||||||
|
throw new OperationError("EC public key has the wrong length for the named curve");
|
||||||
|
}
|
||||||
|
const { x, y } = splitUncompressedPoint(pub, info.byteLen);
|
||||||
|
return {
|
||||||
|
curveName,
|
||||||
|
curve: info.curve,
|
||||||
|
byteLen: info.byteLen,
|
||||||
|
isPrivate: false,
|
||||||
|
isPublic: true,
|
||||||
|
d: null,
|
||||||
|
publicKey: pub,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the loadEcKey return value for a parsed ECPrivateKey.
|
||||||
|
*
|
||||||
|
* @param {string} curveName
|
||||||
|
* @param {object} ec
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function buildPrivateKeyInfo(curveName, ec) {
|
||||||
|
const info = getCurveByName(curveName);
|
||||||
|
const d = leftPadTo(
|
||||||
|
new Uint8Array(ec.privateKey.buffer, ec.privateKey.byteOffset, ec.privateKey.byteLength),
|
||||||
|
info.byteLen,
|
||||||
|
);
|
||||||
|
const publicKey = info.curve.getPublicKey(d, false);
|
||||||
|
const { x, y } = splitUncompressedPoint(publicKey, info.byteLen);
|
||||||
|
return {
|
||||||
|
curveName,
|
||||||
|
curve: info.curve,
|
||||||
|
byteLen: info.byteLen,
|
||||||
|
isPrivate: true,
|
||||||
|
isPublic: false,
|
||||||
|
d,
|
||||||
|
publicKey,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pad a byte array on the left with zeros to reach `length` bytes.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {number} length
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function leftPadTo(bytes, length) {
|
||||||
|
if (bytes.length === length) return bytes;
|
||||||
|
if (bytes.length > length) throw new OperationError("EC scalar is longer than the curve allows");
|
||||||
|
const out = new Uint8Array(length);
|
||||||
|
out.set(bytes, length - bytes.length);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map an ECParameters object's namedCurve OID to our short curve name.
|
||||||
|
*
|
||||||
|
* @param {object|null|undefined} ecParams
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function curveFromParameters(ecParams) {
|
||||||
|
if (!ecParams || !ecParams.namedCurve) return null;
|
||||||
|
return OID_TO_CURVE[ecParams.namedCurve] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split an uncompressed SEC1 point (04 || X || Y) into its X and Y bytes.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} pub
|
||||||
|
* @param {number} byteLen
|
||||||
|
* @returns {{x: Uint8Array, y: Uint8Array}}
|
||||||
|
*/
|
||||||
|
function splitUncompressedPoint(pub, byteLen) {
|
||||||
|
return {
|
||||||
|
x: pub.slice(1, 1 + byteLen),
|
||||||
|
y: pub.slice(1 + byteLen, 1 + 2 * byteLen),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip a single leading "00" byte from a DER INTEGER hex if it's only there
|
||||||
|
* to keep the value positive — i.e. when the next byte's MSB is set.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function stripDerLeadingZero(hex) {
|
||||||
|
if (hex.length >= 4 && hex.slice(0, 2) === "00") {
|
||||||
|
const second = parseInt(hex.slice(2, 4), 16);
|
||||||
|
if (second & 0x80) return hex.slice(2);
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a BER/DER length octet sequence.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {number} offset
|
||||||
|
* @returns {{value: number, next: number}}
|
||||||
|
*/
|
||||||
|
function readLength(bytes, offset) {
|
||||||
|
const first = bytes[offset];
|
||||||
|
if (first < 0x80) return { value: first, next: offset + 1 };
|
||||||
|
const n = first & 0x7f;
|
||||||
|
let value = 0;
|
||||||
|
for (let i = 0; i < n; i++) value = (value << 8) | bytes[offset + 1 + i];
|
||||||
|
return { value, next: offset + 1 + n };
|
||||||
|
}
|
||||||
|
|
||||||
701
src/core/lib/KeyConvert.mjs
Normal file
701
src/core/lib/KeyConvert.mjs
Normal file
@ -0,0 +1,701 @@
|
|||||||
|
/**
|
||||||
|
* Asymmetric key conversion helpers built on @peculiar/asn1-* and asn1js.
|
||||||
|
*
|
||||||
|
* Shared by the PEM to JWK / JWK to PEM / Public Key from Private Key
|
||||||
|
* operations.
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2016
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AsnParser, AsnSerializer, OctetString } from "@peculiar/asn1-schema";
|
||||||
|
import * as rsaSchemas from "@peculiar/asn1-rsa";
|
||||||
|
import { PrivateKeyInfo } from "@peculiar/asn1-pkcs8";
|
||||||
|
import { AlgorithmIdentifier, SubjectPublicKeyInfo } from "@peculiar/asn1-x509";
|
||||||
|
import { Sequence, Integer, fromBER } from "asn1js";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { toBase64, fromBase64 } from "./Base64.mjs";
|
||||||
|
import { derBytesToPem } from "./Asn1.mjs";
|
||||||
|
import {
|
||||||
|
getCurveByName,
|
||||||
|
loadEcKey,
|
||||||
|
publicKeyToSpkiPem,
|
||||||
|
privateKeyToPkcs8Pem,
|
||||||
|
} from "./Ecdsa.mjs";
|
||||||
|
|
||||||
|
const { RSAPrivateKey, RSAPublicKey } = rsaSchemas;
|
||||||
|
const ID_RSA_ENCRYPTION = rsaSchemas.id_rsaEncryption;
|
||||||
|
const ID_DSA = "1.2.840.10040.4.1";
|
||||||
|
|
||||||
|
// DER NULL — used as the `parameters` field of the rsaEncryption algorithm
|
||||||
|
// identifier in RSA SPKI/PKCS#8 envelopes.
|
||||||
|
const DER_NULL = new Uint8Array([0x05, 0x00]).buffer;
|
||||||
|
|
||||||
|
|
||||||
|
// ----- public API -----------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a PEM-encoded asymmetric key blob. Recognised labels are
|
||||||
|
* `RSA PRIVATE KEY`, `RSA PUBLIC KEY`, `DSA PRIVATE KEY`, `EC PRIVATE KEY`,
|
||||||
|
* `PRIVATE KEY` (PKCS#8) and `PUBLIC KEY` (SPKI).
|
||||||
|
*
|
||||||
|
* @param {string} pem
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function parseKeyPem(pem) {
|
||||||
|
const { label, bytes } = pemToDer(pem);
|
||||||
|
switch (label) {
|
||||||
|
case "RSA PRIVATE KEY":
|
||||||
|
return rsaPrivateFromBytes(parseRsaPrivateKey(bytes));
|
||||||
|
case "RSA PUBLIC KEY":
|
||||||
|
return rsaPublicFromBytes(parseRsaPublicKey(bytes));
|
||||||
|
case "EC PRIVATE KEY":
|
||||||
|
return ecInfoFromLoad(loadEcKey(pem));
|
||||||
|
case "DSA PRIVATE KEY":
|
||||||
|
return parseDsaTraditional(bytes);
|
||||||
|
case "PRIVATE KEY":
|
||||||
|
return parsePkcs8(bytes);
|
||||||
|
case "PUBLIC KEY":
|
||||||
|
return parseSpki(bytes);
|
||||||
|
default:
|
||||||
|
throw new OperationError(`Unsupported PEM label: '${label}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single X.509 certificate PEM and return the normalised public key
|
||||||
|
* info of its subject.
|
||||||
|
*
|
||||||
|
* @param {string} certPem
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function parseCertPublicKey(certPem) {
|
||||||
|
const { bytes } = pemToDer(certPem);
|
||||||
|
// Certificate ::= SEQUENCE { tbsCertificate ::= SEQUENCE { version?, serial,
|
||||||
|
// sigAlg, issuer, validity, subject, spki, ... }, ... }
|
||||||
|
const parsed = fromBER(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
||||||
|
if (parsed.offset === -1) throw new OperationError("Invalid certificate DER");
|
||||||
|
const cert = parsed.result;
|
||||||
|
const tbs = cert.valueBlock.value[0];
|
||||||
|
if (!tbs) throw new OperationError("Certificate is missing TBSCertificate");
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
const first = tbs.valueBlock.value[0];
|
||||||
|
// Skip explicit [0] version tag if present
|
||||||
|
if (first && first.idBlock && first.idBlock.tagClass === 3 && first.idBlock.tagNumber === 0) {
|
||||||
|
idx = 1;
|
||||||
|
}
|
||||||
|
// tbs layout: [version?] serial sigAlg issuer validity subject spki ...
|
||||||
|
const spkiNode = tbs.valueBlock.value[idx + 5];
|
||||||
|
if (!spkiNode) throw new OperationError("Could not locate SubjectPublicKeyInfo in certificate");
|
||||||
|
|
||||||
|
const spkiBytes = derOf(spkiNode);
|
||||||
|
return parseSpki(spkiBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert normalised key info to a JWK object (built in the canonical
|
||||||
|
* field order so `JSON.stringify` emits a deterministic string).
|
||||||
|
*
|
||||||
|
* The JWK fields are marshalled by hand rather than via a library: the
|
||||||
|
* conversion spans both RSA (including the CRT private params p/q/dp/dq/qi)
|
||||||
|
* and EC keys, and no current dependency covers both. `@noble/curves` is
|
||||||
|
* EC-only, and Web Crypto's JWK export is async and unavailable for the
|
||||||
|
* MD5/SHA-1 code paths this module supports. Adding a JOSE library purely
|
||||||
|
* for this base64url field mapping would enlarge the dependency surface for
|
||||||
|
* no real benefit. The per-field encoding is delegated to {@link b64url}.
|
||||||
|
*
|
||||||
|
* @param {object} info
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function keyToJwk(info) {
|
||||||
|
switch (info.kty) {
|
||||||
|
case "RSA": {
|
||||||
|
const jwk = { kty: "RSA", n: b64url(info.n), e: b64url(info.e) };
|
||||||
|
if (info.isPrivate) {
|
||||||
|
jwk.d = b64url(info.d);
|
||||||
|
jwk.p = b64url(info.p);
|
||||||
|
jwk.q = b64url(info.q);
|
||||||
|
jwk.dp = b64url(info.dp);
|
||||||
|
jwk.dq = b64url(info.dq);
|
||||||
|
jwk.qi = b64url(info.qi);
|
||||||
|
}
|
||||||
|
return jwk;
|
||||||
|
}
|
||||||
|
case "EC": {
|
||||||
|
const jwk = {
|
||||||
|
kty: "EC",
|
||||||
|
crv: info.curveName,
|
||||||
|
x: b64url(info.x),
|
||||||
|
y: b64url(info.y),
|
||||||
|
};
|
||||||
|
if (info.isPrivate) jwk.d = b64url(info.d);
|
||||||
|
return jwk;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new OperationError(`Cannot build JWK for key type '${info.kty}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a normalised key info object from a JWK.
|
||||||
|
*
|
||||||
|
* @param {object} jwk
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function keyFromJwk(jwk) {
|
||||||
|
if (!jwk || typeof jwk !== "object" || typeof jwk.kty !== "string") {
|
||||||
|
throw new OperationError("Invalid JWK format");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwk.kty === "RSA") {
|
||||||
|
if (typeof jwk.n !== "string" || typeof jwk.e !== "string") {
|
||||||
|
throw new OperationError("RSA JWK is missing required fields");
|
||||||
|
}
|
||||||
|
const info = {
|
||||||
|
kty: "RSA",
|
||||||
|
isPrivate: typeof jwk.d === "string",
|
||||||
|
n: b64urlToBytes(jwk.n),
|
||||||
|
e: b64urlToBytes(jwk.e),
|
||||||
|
};
|
||||||
|
if (info.isPrivate) {
|
||||||
|
if (
|
||||||
|
typeof jwk.d !== "string" || typeof jwk.p !== "string" ||
|
||||||
|
typeof jwk.q !== "string" || typeof jwk.dp !== "string" ||
|
||||||
|
typeof jwk.dq !== "string" || typeof jwk.qi !== "string"
|
||||||
|
) {
|
||||||
|
throw new OperationError("RSA private JWK is missing CRT components");
|
||||||
|
}
|
||||||
|
info.d = b64urlToBytes(jwk.d);
|
||||||
|
info.p = b64urlToBytes(jwk.p);
|
||||||
|
info.q = b64urlToBytes(jwk.q);
|
||||||
|
info.dp = b64urlToBytes(jwk.dp);
|
||||||
|
info.dq = b64urlToBytes(jwk.dq);
|
||||||
|
info.qi = b64urlToBytes(jwk.qi);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwk.kty === "EC") {
|
||||||
|
if (typeof jwk.crv !== "string" || typeof jwk.x !== "string" || typeof jwk.y !== "string") {
|
||||||
|
throw new OperationError("EC JWK is missing required fields");
|
||||||
|
}
|
||||||
|
const curve = getCurveByName(jwk.crv);
|
||||||
|
const x = b64urlToBytes(jwk.x);
|
||||||
|
const y = b64urlToBytes(jwk.y);
|
||||||
|
if (x.length !== curve.byteLen || y.length !== curve.byteLen) {
|
||||||
|
throw new OperationError(`EC JWK coords have wrong length for curve ${jwk.crv}`);
|
||||||
|
}
|
||||||
|
const publicKey = new Uint8Array(1 + 2 * curve.byteLen);
|
||||||
|
publicKey[0] = 0x04;
|
||||||
|
publicKey.set(x, 1);
|
||||||
|
publicKey.set(y, 1 + curve.byteLen);
|
||||||
|
const info = {
|
||||||
|
kty: "EC",
|
||||||
|
curveName: jwk.crv,
|
||||||
|
byteLen: curve.byteLen,
|
||||||
|
isPrivate: typeof jwk.d === "string",
|
||||||
|
isPublic: typeof jwk.d !== "string",
|
||||||
|
publicKey,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
if (info.isPrivate) {
|
||||||
|
const d = b64urlToBytes(jwk.d);
|
||||||
|
if (d.length !== curve.byteLen) {
|
||||||
|
throw new OperationError(`EC JWK 'd' has wrong length for curve ${jwk.crv}`);
|
||||||
|
}
|
||||||
|
info.d = d;
|
||||||
|
} else {
|
||||||
|
info.d = null;
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new OperationError(`Unsupported JWK key type '${jwk.kty}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode normalised key info as a PEM blob. Private keys come out as
|
||||||
|
* PKCS#8; public keys come out as SPKI. Both use LF line endings.
|
||||||
|
*
|
||||||
|
* @param {object} info
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function keyInfoToPem(info) {
|
||||||
|
if (info.kty === "RSA") {
|
||||||
|
return info.isPrivate ? rsaPkcs8Pem(info) : rsaSpkiPem(info);
|
||||||
|
}
|
||||||
|
if (info.kty === "EC") {
|
||||||
|
return info.isPrivate ? privateKeyToPkcs8Pem(info) : publicKeyToSpkiPem(info);
|
||||||
|
}
|
||||||
|
if (info.kty === "DSA") {
|
||||||
|
if (!info.isPrivate) return dsaSpkiPem(info);
|
||||||
|
throw new OperationError("Building DSA private-key PEMs is not supported");
|
||||||
|
}
|
||||||
|
throw new OperationError(`Cannot serialise key of type '${info.kty}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive the public-key info from a private-key info. RSA keeps `n`/`e`,
|
||||||
|
* EC re-derives `(x,y)` via the curve, DSA keeps `(p,q,g,y)`.
|
||||||
|
*
|
||||||
|
* @param {object} info
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function derivePublicKeyInfo(info) {
|
||||||
|
if (!info.isPrivate) return info;
|
||||||
|
if (info.kty === "RSA") {
|
||||||
|
return { kty: "RSA", isPrivate: false, n: info.n, e: info.e };
|
||||||
|
}
|
||||||
|
if (info.kty === "EC") {
|
||||||
|
const curve = getCurveByName(info.curveName);
|
||||||
|
return {
|
||||||
|
kty: "EC",
|
||||||
|
curveName: info.curveName,
|
||||||
|
byteLen: curve.byteLen,
|
||||||
|
isPrivate: false,
|
||||||
|
isPublic: true,
|
||||||
|
d: null,
|
||||||
|
publicKey: info.publicKey,
|
||||||
|
x: info.x,
|
||||||
|
y: info.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (info.kty === "DSA") {
|
||||||
|
if (!info.y) {
|
||||||
|
throw new OperationError(`DSA Private Key in PKCS#8 is not supported`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kty: "DSA",
|
||||||
|
isPrivate: false,
|
||||||
|
p: info.p,
|
||||||
|
q: info.q,
|
||||||
|
g: info.g,
|
||||||
|
y: info.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new OperationError(`Unsupported key type: ${info.kty}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- format-specific decoding ---------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a PKCS#1 RSAPrivateKey blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {RSAPrivateKey}
|
||||||
|
*/
|
||||||
|
function parseRsaPrivateKey(bytes) {
|
||||||
|
try {
|
||||||
|
return AsnParser.parse(bytes, RSAPrivateKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse RSA private key: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a PKCS#1 RSAPublicKey blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {RSAPublicKey}
|
||||||
|
*/
|
||||||
|
function parseRsaPublicKey(bytes) {
|
||||||
|
try {
|
||||||
|
return AsnParser.parse(bytes, RSAPublicKey);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse RSA public key: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the normalised info object for an RSA private key.
|
||||||
|
*
|
||||||
|
* @param {RSAPrivateKey} rsa
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function rsaPrivateFromBytes(rsa) {
|
||||||
|
return {
|
||||||
|
kty: "RSA",
|
||||||
|
isPrivate: true,
|
||||||
|
n: stripLeadingZero(abufToBytes(rsa.modulus)),
|
||||||
|
e: stripLeadingZero(abufToBytes(rsa.publicExponent)),
|
||||||
|
d: stripLeadingZero(abufToBytes(rsa.privateExponent)),
|
||||||
|
p: stripLeadingZero(abufToBytes(rsa.prime1)),
|
||||||
|
q: stripLeadingZero(abufToBytes(rsa.prime2)),
|
||||||
|
dp: stripLeadingZero(abufToBytes(rsa.exponent1)),
|
||||||
|
dq: stripLeadingZero(abufToBytes(rsa.exponent2)),
|
||||||
|
qi: stripLeadingZero(abufToBytes(rsa.coefficient)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the normalised info object for an RSA public key.
|
||||||
|
*
|
||||||
|
* @param {RSAPublicKey} rsa
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function rsaPublicFromBytes(rsa) {
|
||||||
|
return {
|
||||||
|
kty: "RSA",
|
||||||
|
isPrivate: false,
|
||||||
|
n: stripLeadingZero(abufToBytes(rsa.modulus)),
|
||||||
|
e: stripLeadingZero(abufToBytes(rsa.publicExponent)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reshape an `loadEcKey` return value to the normalised format used here
|
||||||
|
* (adds the `kty` field).
|
||||||
|
*
|
||||||
|
* @param {object} ecInfo
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function ecInfoFromLoad(ecInfo) {
|
||||||
|
return { kty: "EC", ...ecInfo };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a PKCS#8 blob. Dispatches on the algorithm OID to the
|
||||||
|
* RSA/EC parsers.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function parsePkcs8(bytes) {
|
||||||
|
let info;
|
||||||
|
try {
|
||||||
|
info = AsnParser.parse(bytes, PrivateKeyInfo);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse PKCS#8 key: ${e.message}`);
|
||||||
|
}
|
||||||
|
const alg = info.privateKeyAlgorithm.algorithm;
|
||||||
|
if (alg === ID_RSA_ENCRYPTION) {
|
||||||
|
const inner = abufToBytes(info.privateKey.buffer);
|
||||||
|
return rsaPrivateFromBytes(parseRsaPrivateKey(inner));
|
||||||
|
}
|
||||||
|
if (alg === ID_DSA) {
|
||||||
|
// DSA PKCS#8 carries only the private x value, with p/q/g in the
|
||||||
|
// algorithm parameters. It does not carry the public y value needed
|
||||||
|
// here, so mark the info accordingly and let derivePublicKeyInfo throw.
|
||||||
|
return { kty: "DSA", isPrivate: true, y: null };
|
||||||
|
}
|
||||||
|
// EC and everything else delegate to the existing EC loader by
|
||||||
|
// re-wrapping the bytes as a PKCS#8 PEM. loadEcKey will surface its
|
||||||
|
// own "not an EC key" error for unsupported algorithms.
|
||||||
|
return ecInfoFromLoad(loadEcKey(derBytesToPem(bytes, "PRIVATE KEY")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a SubjectPublicKeyInfo blob. Dispatches on the algorithm OID.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function parseSpki(bytes) {
|
||||||
|
let spki;
|
||||||
|
try {
|
||||||
|
spki = AsnParser.parse(bytes, SubjectPublicKeyInfo);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Could not parse SubjectPublicKeyInfo: ${e.message}`);
|
||||||
|
}
|
||||||
|
const alg = spki.algorithm.algorithm;
|
||||||
|
if (alg === ID_RSA_ENCRYPTION) {
|
||||||
|
return rsaPublicFromBytes(parseRsaPublicKey(abufToBytes(spki.subjectPublicKey)));
|
||||||
|
}
|
||||||
|
if (alg === ID_DSA) {
|
||||||
|
if (!spki.algorithm.parameters) {
|
||||||
|
throw new OperationError("DSA SubjectPublicKeyInfo is missing DSS-Parms");
|
||||||
|
}
|
||||||
|
const { p, q, g } = parseDssParms(abufToBytes(spki.algorithm.parameters));
|
||||||
|
const y = parseIntegerBitString(abufToBytes(spki.subjectPublicKey));
|
||||||
|
return { kty: "DSA", isPrivate: false, p, q, g, y };
|
||||||
|
}
|
||||||
|
return ecInfoFromLoad(loadEcKey(derBytesToPem(bytes, "PUBLIC KEY")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a traditional OpenSSL DSAPrivateKey blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
function parseDsaTraditional(bytes) {
|
||||||
|
const parsed = fromBER(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
||||||
|
if (parsed.offset === -1) throw new OperationError("Invalid DSA private key DER");
|
||||||
|
const seq = parsed.result;
|
||||||
|
const items = seq.valueBlock && seq.valueBlock.value;
|
||||||
|
if (!items || items.length < 6) throw new OperationError("Malformed DSA private key");
|
||||||
|
return {
|
||||||
|
kty: "DSA",
|
||||||
|
isPrivate: true,
|
||||||
|
p: extractIntegerBytes(items[1]),
|
||||||
|
q: extractIntegerBytes(items[2]),
|
||||||
|
g: extractIntegerBytes(items[3]),
|
||||||
|
y: extractIntegerBytes(items[4]),
|
||||||
|
x: extractIntegerBytes(items[5]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the DSS-Parms (p, q, g) SEQUENCE from a DSA algorithm parameters
|
||||||
|
* blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {{p: Uint8Array, q: Uint8Array, g: Uint8Array}}
|
||||||
|
*/
|
||||||
|
function parseDssParms(bytes) {
|
||||||
|
const parsed = fromBER(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
||||||
|
if (parsed.offset === -1) throw new OperationError("Invalid DSS-Parms");
|
||||||
|
const items = parsed.result.valueBlock && parsed.result.valueBlock.value;
|
||||||
|
if (!items || items.length < 3) throw new OperationError("Malformed DSS-Parms");
|
||||||
|
return {
|
||||||
|
p: extractIntegerBytes(items[0]),
|
||||||
|
q: extractIntegerBytes(items[1]),
|
||||||
|
g: extractIntegerBytes(items[2]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode an INTEGER wrapped in a BIT STRING (used for DSA subjectPublicKey).
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function parseIntegerBitString(bytes) {
|
||||||
|
const parsed = fromBER(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
||||||
|
if (parsed.offset === -1) throw new OperationError("Invalid INTEGER in SPKI");
|
||||||
|
return extractIntegerBytes(parsed.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull the raw INTEGER bytes out of an asn1js Integer node (without the
|
||||||
|
* DER 2's-complement leading 00 when present).
|
||||||
|
*
|
||||||
|
* @param {Object} node
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function extractIntegerBytes(node) {
|
||||||
|
const view = node.valueBlock && node.valueBlock.valueHexView;
|
||||||
|
if (!view) throw new OperationError("Missing INTEGER value");
|
||||||
|
return stripLeadingZero(new Uint8Array(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- format-specific encoding ---------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode an RSA private-key info object as a PKCS#8 PEM.
|
||||||
|
*
|
||||||
|
* @param {object} info
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function rsaPkcs8Pem(info) {
|
||||||
|
const rsa = new RSAPrivateKey({
|
||||||
|
version: 0,
|
||||||
|
modulus: bytesToAbuf(prefixForDerInt(info.n)),
|
||||||
|
publicExponent: bytesToAbuf(prefixForDerInt(info.e)),
|
||||||
|
privateExponent: bytesToAbuf(prefixForDerInt(info.d)),
|
||||||
|
prime1: bytesToAbuf(prefixForDerInt(info.p)),
|
||||||
|
prime2: bytesToAbuf(prefixForDerInt(info.q)),
|
||||||
|
exponent1: bytesToAbuf(prefixForDerInt(info.dp)),
|
||||||
|
exponent2: bytesToAbuf(prefixForDerInt(info.dq)),
|
||||||
|
coefficient: bytesToAbuf(prefixForDerInt(info.qi)),
|
||||||
|
});
|
||||||
|
const pkcs8 = new PrivateKeyInfo({
|
||||||
|
version: 0,
|
||||||
|
privateKeyAlgorithm: new AlgorithmIdentifier({
|
||||||
|
algorithm: ID_RSA_ENCRYPTION,
|
||||||
|
parameters: DER_NULL,
|
||||||
|
}),
|
||||||
|
privateKey: new OctetString(AsnSerializer.serialize(rsa)),
|
||||||
|
});
|
||||||
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(pkcs8)), "PRIVATE KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode an RSA public-key info object as an SPKI PEM.
|
||||||
|
*
|
||||||
|
* @param {object} info
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function rsaSpkiPem(info) {
|
||||||
|
const rsa = new RSAPublicKey({
|
||||||
|
modulus: bytesToAbuf(prefixForDerInt(info.n)),
|
||||||
|
publicExponent: bytesToAbuf(prefixForDerInt(info.e)),
|
||||||
|
});
|
||||||
|
const spki = new SubjectPublicKeyInfo({
|
||||||
|
algorithm: new AlgorithmIdentifier({
|
||||||
|
algorithm: ID_RSA_ENCRYPTION,
|
||||||
|
parameters: DER_NULL,
|
||||||
|
}),
|
||||||
|
subjectPublicKey: AsnSerializer.serialize(rsa),
|
||||||
|
});
|
||||||
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a DSA public-key info object as an SPKI PEM.
|
||||||
|
*
|
||||||
|
* @param {object} info
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function dsaSpkiPem(info) {
|
||||||
|
const params = buildDssParms(info.p, info.q, info.g);
|
||||||
|
const innerY = buildDerInteger(info.y);
|
||||||
|
const spki = new SubjectPublicKeyInfo({
|
||||||
|
algorithm: new AlgorithmIdentifier({
|
||||||
|
algorithm: ID_DSA,
|
||||||
|
parameters: params,
|
||||||
|
}),
|
||||||
|
subjectPublicKey: innerY,
|
||||||
|
});
|
||||||
|
return derBytesToPem(new Uint8Array(AsnSerializer.serialize(spki)), "PUBLIC KEY");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the DER encoding of an INTEGER from its raw magnitude bytes.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
function buildDerInteger(bytes) {
|
||||||
|
const node = new Integer({ valueHex: prefixForDerInt(bytes) });
|
||||||
|
return node.toBER(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the DER encoding of DSS-Parms (SEQUENCE { p, q, g }).
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} p
|
||||||
|
* @param {Uint8Array} q
|
||||||
|
* @param {Uint8Array} g
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
function buildDssParms(p, q, g) {
|
||||||
|
const seq = new Sequence({
|
||||||
|
value: [
|
||||||
|
new Integer({ valueHex: prefixForDerInt(p) }),
|
||||||
|
new Integer({ valueHex: prefixForDerInt(q) }),
|
||||||
|
new Integer({ valueHex: prefixForDerInt(g) }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return seq.toBER(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- byte/PEM utilities ---------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a PEM blob to its raw DER bytes and label.
|
||||||
|
*
|
||||||
|
* @param {string} pem
|
||||||
|
* @returns {{label: string, bytes: Uint8Array}}
|
||||||
|
*/
|
||||||
|
export function pemToDer(pem) {
|
||||||
|
const match = pem.match(/-----BEGIN ([A-Z0-9 ]+)-----([\s\S]+?)-----END \1-----/);
|
||||||
|
if (!match) throw new OperationError("Not a valid PEM blob");
|
||||||
|
const body = match[2].replace(/\s+/g, "");
|
||||||
|
let bin;
|
||||||
|
if (typeof Buffer !== "undefined") {
|
||||||
|
bin = Buffer.from(body, "base64");
|
||||||
|
} else {
|
||||||
|
const decoded = atob(body);
|
||||||
|
bin = new Uint8Array(decoded.length);
|
||||||
|
for (let i = 0; i < decoded.length; i++) bin[i] = decoded.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: match[1],
|
||||||
|
bytes: new Uint8Array(bin.buffer || bin, bin.byteOffset || 0, bin.byteLength || bin.length),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a byte array as base64url (no padding).
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function b64url(bytes) {
|
||||||
|
if (!(bytes instanceof Uint8Array)) bytes = new Uint8Array(bytes);
|
||||||
|
return toBase64(bytes, "A-Za-z0-9-_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a base64url-encoded string to bytes. Tolerates standard base64
|
||||||
|
* padding if present.
|
||||||
|
*
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function b64urlToBytes(str) {
|
||||||
|
return new Uint8Array(fromBase64(str, "A-Za-z0-9-_", "byteArray"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip a single leading 0x00 byte from a buffer when it's only present
|
||||||
|
* to keep a DER INTEGER positive (i.e. the next byte's MSB is set).
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function stripLeadingZero(bytes) {
|
||||||
|
if (bytes.length > 1 && bytes[0] === 0 && (bytes[1] & 0x80)) {
|
||||||
|
return bytes.slice(1);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a leading 0x00 byte to a buffer when its MSB is set, so it survives
|
||||||
|
* round-tripping through a DER INTEGER without being interpreted as a
|
||||||
|
* negative value.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function prefixForDerInt(bytes) {
|
||||||
|
if (bytes.length === 0) return new Uint8Array([0]);
|
||||||
|
if (bytes[0] & 0x80) {
|
||||||
|
const out = new Uint8Array(bytes.length + 1);
|
||||||
|
out.set(bytes, 1);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an ArrayBuffer / Uint8Array / typed view to a Uint8Array.
|
||||||
|
*
|
||||||
|
* @param {ArrayBuffer|ArrayBufferView} abuf
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function abufToBytes(abuf) {
|
||||||
|
if (abuf instanceof Uint8Array) return abuf;
|
||||||
|
if (ArrayBuffer.isView(abuf)) return new Uint8Array(abuf.buffer, abuf.byteOffset, abuf.byteLength);
|
||||||
|
return new Uint8Array(abuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a Uint8Array in a freshly allocated ArrayBuffer.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
|
*/
|
||||||
|
function bytesToAbuf(bytes) {
|
||||||
|
const copy = new Uint8Array(bytes);
|
||||||
|
return copy.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-serialise a parsed asn1js node to its DER bytes.
|
||||||
|
*
|
||||||
|
* @param {Object} node
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function derOf(node) {
|
||||||
|
return new Uint8Array(node.toBER(false));
|
||||||
|
}
|
||||||
@ -11,28 +11,38 @@ import { toHex, fromHex } from "./Hex.mjs";
|
|||||||
/**
|
/**
|
||||||
* Formats Distinguished Name (DN) objects to strings.
|
* Formats Distinguished Name (DN) objects to strings.
|
||||||
*
|
*
|
||||||
* @param {Object} dnObj
|
* Accepts either the legacy `{ array: [[{type, value}, ...], ...] }` shape
|
||||||
|
* OR `@peculiar/x509`'s `JsonName` shape — an array of records keyed by
|
||||||
|
* RDN short-name (`[{ CN: ["foo"], OU: ["bar"] }, ...]`).
|
||||||
|
*
|
||||||
|
* @param {Object|Array} dnObj
|
||||||
* @param {number} indent
|
* @param {number} indent
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function formatDnObj(dnObj, indent) {
|
export function formatDnObj(dnObj, indent) {
|
||||||
let output = "";
|
const rows = [];
|
||||||
|
|
||||||
const maxKeyLen = dnObj.array.reduce((max, item) => {
|
if (Array.isArray(dnObj)) {
|
||||||
return item[0].type.length > max ? item[0].type.length : max;
|
for (const rdn of dnObj) {
|
||||||
}, 0);
|
for (const key of Object.keys(rdn)) {
|
||||||
|
for (const value of rdn[key]) rows.push({ key, value });
|
||||||
for (let i = 0; i < dnObj.array.length; i++) {
|
}
|
||||||
if (!dnObj.array[i].length) continue;
|
}
|
||||||
|
} else if (dnObj && Array.isArray(dnObj.array)) {
|
||||||
const key = dnObj.array[i][0].type;
|
for (const rdn of dnObj.array) {
|
||||||
const value = dnObj.array[i][0].value;
|
if (!rdn || !rdn.length) continue;
|
||||||
const str = `${key.padEnd(maxKeyLen, " ")} = ${value}\n`;
|
rows.push({ key: rdn[0].type, value: rdn[0].value });
|
||||||
|
}
|
||||||
output += str.padStart(indent + str.length, " ");
|
} else {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.slice(0, -1);
|
if (rows.length === 0) return "";
|
||||||
|
|
||||||
|
const maxKeyLen = rows.reduce((max, r) => Math.max(max, r.key.length), 0);
|
||||||
|
const pad = " ".repeat(indent);
|
||||||
|
|
||||||
|
return rows.map(({ key, value }) => `${pad}${key.padEnd(maxKeyLen, " ")} = ${value}`).join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -10,38 +10,76 @@ import { fromHex } from "../lib/Hex.mjs";
|
|||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
import Sm3 from "crypto-api/src/hasher/sm3.mjs";
|
import Sm3 from "crypto-api/src/hasher/sm3.mjs";
|
||||||
import { toHex } from "crypto-api/src/encoder/hex.mjs";
|
import { toHex } from "crypto-api/src/encoder/hex.mjs";
|
||||||
import r from "jsrsasign";
|
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]. The modulo reduction keeps
|
||||||
|
// the existing scalar-generation distribution for SM2 ciphertexts.
|
||||||
|
randomScalar: () => bytesToNumberBE(dh.utils.randomSecretKey()) % (params.n - 1n) + 1n,
|
||||||
|
};
|
||||||
|
curveCache[name] = cached;
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SM2 Class for encryption and decryption operations
|
* SM2 Class for encryption and decryption operations
|
||||||
*/
|
*/
|
||||||
export class SM2 {
|
export class SM2 {
|
||||||
/**
|
/**
|
||||||
* Constructor for SM2 class; sets up with the curve and the output format as specified in user args
|
* @param {string} curve - named SM2 curve (e.g. "sm2p256v1")
|
||||||
*
|
* @param {string} format - "C1C3C2" or "C1C2C3"
|
||||||
* @param {*} curve
|
|
||||||
* @param {*} format
|
|
||||||
*/
|
*/
|
||||||
constructor(curve, format) {
|
constructor(curve, format) {
|
||||||
this.ecParams = null;
|
const c = getCurve(curve);
|
||||||
this.rng = new r.SecureRandom();
|
this.Point = c.Point;
|
||||||
/*
|
this.n = c.n;
|
||||||
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
|
this.coordCharLen = c.coordCharLen;
|
||||||
*/
|
this.randomScalar = c.randomScalar;
|
||||||
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);
|
|
||||||
|
|
||||||
this.format = format;
|
this.format = format;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,13 +90,12 @@ export class SM2 {
|
|||||||
* @param {string} publicKeyY
|
* @param {string} publicKeyY
|
||||||
*/
|
*/
|
||||||
setPublicKey(publicKeyX, publicKeyY) {
|
setPublicKey(publicKeyX, publicKeyY) {
|
||||||
/*
|
try {
|
||||||
* TODO: This needs some additional length validation; and checking for errors in the decoding process
|
this.publicKey = this.Point.fromHex("04" + publicKeyX + publicKeyY);
|
||||||
* TODO: Can probably support other public key encoding methods here as well in the future
|
} catch (e) {
|
||||||
*/
|
throw new OperationError("Invalid Public Key");
|
||||||
this.publicKey = this.ecParams.curve.decodePointHex("04" + publicKeyX + publicKeyY);
|
}
|
||||||
|
if (this.publicKey.is0()) {
|
||||||
if (this.publicKey.isInfinity()) {
|
|
||||||
throw new OperationError("Invalid Public Key");
|
throw new OperationError("Invalid Public Key");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,28 +103,26 @@ export class SM2 {
|
|||||||
/**
|
/**
|
||||||
* Set the private key value for the SM2 class
|
* Set the private key value for the SM2 class
|
||||||
*
|
*
|
||||||
* @param {string} privateKey
|
* @param {string} privateKeyHex
|
||||||
*/
|
*/
|
||||||
setPrivateKey(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)
|
* 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}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
encrypt(input) {
|
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)
|
* 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
|
* k: Randomly generated bigint in [1, n-1]
|
||||||
* c1: Result of dotting our curve generator point `G` with the value of `k`
|
* c1: Result of dotting our curve generator point with the value of `k`
|
||||||
*/
|
*/
|
||||||
const k = this.generatePublicKey();
|
const k = this.randomScalar();
|
||||||
const c1 = G.multiply(k);
|
const c1 = this.Point.BASE.multiply(k);
|
||||||
const [hexC1X, hexC1Y] = this.getPointAsHex(c1);
|
const [hexC1X, hexC1Y] = this.getPointAsHex(c1);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -101,7 +136,7 @@ export class SM2 {
|
|||||||
const c3 = this.c3(p2, input);
|
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);
|
const key = this.kdf(p2, input.byteLength);
|
||||||
for (let i = 0; i < input.byteLength; i++) {
|
for (let i = 0; i < input.byteLength; i++) {
|
||||||
@ -118,10 +153,12 @@ export class SM2 {
|
|||||||
return hexC1X + hexC1Y + c2 + c3;
|
return hexC1X + hexC1Y + c2 + c3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to decrypt an SM2 encrypted message
|
* Function to decrypt an SM2 encrypted message
|
||||||
*
|
*
|
||||||
* @param {*} input
|
* @param {string} input
|
||||||
|
* @returns {ArrayBuffer}
|
||||||
*/
|
*/
|
||||||
decrypt(input) {
|
decrypt(input) {
|
||||||
const c1X = input.slice(0, 64);
|
const c1X = input.slice(0, 64);
|
||||||
@ -138,7 +175,13 @@ export class SM2 {
|
|||||||
c3 = input.slice(-64);
|
c3 = input.slice(-64);
|
||||||
}
|
}
|
||||||
c2 = Uint8Array.from(fromHex(c2));
|
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
|
* 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
|
* 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 {WeierstrassPoint} p2
|
||||||
* @param {*} len
|
* @param {number} len
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
kdf(p2, len) {
|
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
|
* 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 {WeierstrassPoint} p2
|
||||||
* @param {*} input
|
* @param {Uint8Array} input
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
c3(p2, input) {
|
c3(p2, input) {
|
||||||
@ -224,13 +242,12 @@ export class SM2 {
|
|||||||
const overall = fromHex(hX).concat(Array.from(input)).concat(fromHex(hY));
|
const overall = fromHex(hX).concat(Array.from(input)).concat(fromHex(hY));
|
||||||
|
|
||||||
return toHex(this.sm3(overall));
|
return toHex(this.sm3(overall));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SM3 setup helper function; takes input data as an array, processes the hash and returns the result
|
* SM3 setup helper function; takes input data as an array, processes the hash and returns the result
|
||||||
*
|
*
|
||||||
* @param {*} data
|
* @param {number[]} data
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
sm3(data) {
|
sm3(data) {
|
||||||
@ -241,18 +258,16 @@ export class SM2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function, returns an elliptic curve points X and Y values as hex;
|
* Utility function, returns an elliptic curve point's X and Y values as fixed-width hex
|
||||||
*
|
*
|
||||||
* @param {EcPointFp} point
|
* @param {WeierstrassPoint} point
|
||||||
* @returns {[]}
|
* @returns {[string, string]}
|
||||||
*/
|
*/
|
||||||
getPointAsHex(point) {
|
getPointAsHex(point) {
|
||||||
const biX = point.getX().toBigInteger();
|
const { x, y } = point.toAffine();
|
||||||
const biY = point.getY().toBigInteger();
|
const charlen = this.coordCharLen;
|
||||||
|
const hX = x.toString(16).padStart(charlen, "0");
|
||||||
const charlen = this.ecParams.keycharlen;
|
const hY = y.toString(16).padStart(charlen, "0");
|
||||||
const hX = ("0000000000" + biX.toString(16)).slice(- charlen);
|
|
||||||
const hY = ("0000000000" + biY.toString(16)).slice(- charlen);
|
|
||||||
return [hX, hY];
|
return [hX, hY];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
505
src/core/lib/X509.mjs
Normal file
505
src/core/lib/X509.mjs
Normal file
@ -0,0 +1,505 @@
|
|||||||
|
/**
|
||||||
|
* Shared X.509 / CSR / CRL helpers built on @peculiar/x509 + @peculiar/asn1-*.
|
||||||
|
*
|
||||||
|
* Used by ParseX509Certificate / PubKeyFromCert / ParseCSR / ParseX509CRL.
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2016
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AsnParser } from "@peculiar/asn1-schema";
|
||||||
|
import * as asn1X509 from "@peculiar/asn1-x509";
|
||||||
|
import * as ecc from "@peculiar/asn1-ecc";
|
||||||
|
import * as rsaSchemas from "@peculiar/asn1-rsa";
|
||||||
|
import { fromBER, Utf8String, BmpString, PrintableString, IA5String } from "asn1js";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { fromBase64 } from "./Base64.mjs";
|
||||||
|
import { fromHex } from "./Hex.mjs";
|
||||||
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
const { SubjectPublicKeyInfo, DirectoryString } = asn1X509;
|
||||||
|
const { ECParameters } = ecc;
|
||||||
|
const { RSAPublicKey } = rsaSchemas;
|
||||||
|
|
||||||
|
const ID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
|
||||||
|
const ID_EC_PUBLIC_KEY = ecc.id_ecPublicKey;
|
||||||
|
const ID_DSA = "1.2.840.10040.4.1";
|
||||||
|
const ID_ED25519 = "1.3.101.112";
|
||||||
|
const ID_ED448 = "1.3.101.113";
|
||||||
|
|
||||||
|
const SIG_ALG_OID_TO_NAME = {
|
||||||
|
"1.2.840.113549.1.1.4": "MD5withRSA",
|
||||||
|
"1.2.840.113549.1.1.5": "SHA1withRSA",
|
||||||
|
"1.2.840.113549.1.1.11": "SHA256withRSA",
|
||||||
|
"1.2.840.113549.1.1.12": "SHA384withRSA",
|
||||||
|
"1.2.840.113549.1.1.13": "SHA512withRSA",
|
||||||
|
"1.2.840.113549.1.1.14": "SHA224withRSA",
|
||||||
|
"1.2.840.113549.1.1.10": "SHA256withRSAandMGF1",
|
||||||
|
"1.2.840.10045.4.1": "SHA1withECDSA",
|
||||||
|
"1.2.840.10045.4.3.1": "SHA224withECDSA",
|
||||||
|
"1.2.840.10045.4.3.2": "SHA256withECDSA",
|
||||||
|
"1.2.840.10045.4.3.3": "SHA384withECDSA",
|
||||||
|
"1.2.840.10045.4.3.4": "SHA512withECDSA",
|
||||||
|
"1.2.840.10040.4.3": "SHA1withDSA",
|
||||||
|
"2.16.840.1.101.3.4.3.1": "SHA224withDSA",
|
||||||
|
"2.16.840.1.101.3.4.3.2": "SHA256withDSA",
|
||||||
|
"2.16.840.1.101.3.4.3.3": "SHA384withDSA",
|
||||||
|
"2.16.840.1.101.3.4.3.4": "SHA512withDSA",
|
||||||
|
[ID_ED25519]: "Ed25519",
|
||||||
|
[ID_ED448]: "Ed448",
|
||||||
|
};
|
||||||
|
|
||||||
|
const EC_CURVE_OID_TO_NAMES = {
|
||||||
|
"1.2.840.10045.3.1.1": { asn1: "secp192r1", nist: "P-192", byteLen: 24, bits: 192 },
|
||||||
|
"1.3.132.0.33": { asn1: "secp224r1", nist: "P-224", byteLen: 28, bits: 224 },
|
||||||
|
"1.2.840.10045.3.1.7": { asn1: "secp256r1", nist: "P-256", byteLen: 32, bits: 256 },
|
||||||
|
"1.3.132.0.34": { asn1: "secp384r1", nist: "P-384", byteLen: 48, bits: 384 },
|
||||||
|
"1.3.132.0.35": { asn1: "secp521r1", nist: "P-521", byteLen: 66, bits: 521 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const OID_TO_SHORT_NAME = {
|
||||||
|
"2.5.4.3": "CN",
|
||||||
|
"2.5.4.4": "SN",
|
||||||
|
"2.5.4.5": "serialNumber",
|
||||||
|
"2.5.4.6": "C",
|
||||||
|
"2.5.4.7": "L",
|
||||||
|
"2.5.4.8": "ST",
|
||||||
|
"2.5.4.9": "street",
|
||||||
|
"2.5.4.10": "O",
|
||||||
|
"2.5.4.11": "OU",
|
||||||
|
"2.5.4.12": "T",
|
||||||
|
"2.5.4.42": "G",
|
||||||
|
"2.5.4.43": "I",
|
||||||
|
"2.5.4.44": "generationQualifier",
|
||||||
|
"2.5.4.45": "x500UniqueIdentifier",
|
||||||
|
"2.5.4.46": "dnQualifier",
|
||||||
|
"2.5.4.65": "pseudonym",
|
||||||
|
"1.2.840.113549.1.9.1": "E",
|
||||||
|
"0.9.2342.19200300.100.1.25": "DC",
|
||||||
|
"0.9.2342.19200300.100.1.1": "UID",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ----- input decoding -------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the X.509 input string in the requested wire format into a Uint8Array
|
||||||
|
* of DER bytes. Accepts "PEM", "DER Hex", "Base64", "Raw".
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
* @param {string} format
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function decodeX509Input(input, format) {
|
||||||
|
switch (format) {
|
||||||
|
case "PEM": {
|
||||||
|
const stripped = input
|
||||||
|
.replace(/-----BEGIN [^-]+-----/g, "")
|
||||||
|
.replace(/-----END [^-]+-----/g, "")
|
||||||
|
.replace(/\s+/g, "");
|
||||||
|
return new Uint8Array(fromBase64(stripped, null, "byteArray"));
|
||||||
|
}
|
||||||
|
case "DER Hex":
|
||||||
|
return new Uint8Array(fromHex(input.replace(/\s/g, "")));
|
||||||
|
case "Base64":
|
||||||
|
return new Uint8Array(fromBase64(input, null, "byteArray"));
|
||||||
|
case "Raw":
|
||||||
|
return new Uint8Array(Utils.strToArrayBuffer(input));
|
||||||
|
default:
|
||||||
|
throw new OperationError(`Undefined input format: ${format}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- signature algorithm --------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a signature-algorithm OID to the compact display name used by these
|
||||||
|
* operations (e.g. "1.2.840.113549.1.1.11" -> "SHA256withRSA"). Falls back
|
||||||
|
* to the raw OID if unknown.
|
||||||
|
*
|
||||||
|
* @param {string} oid
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function sigAlgOidToName(oid) {
|
||||||
|
return SIG_ALG_OID_TO_NAME[oid] || oid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- public key info extraction -------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a SubjectPublicKeyInfo DER blob and return a normalised description.
|
||||||
|
*
|
||||||
|
* The return shape varies by algorithm:
|
||||||
|
* RSA: { type: "RSA", nHex, eValue, bitLength }
|
||||||
|
* EC: { type: "EC", curveOid, asn1Curve, nistCurve, pubKeyHex, bitLength, x, y }
|
||||||
|
* DSA: { type: "DSA", yHex, pHex, qHex, gHex, bitLength }
|
||||||
|
* Ed25519/Ed448: { type: "EdDSA", curveName, pubKeyHex }
|
||||||
|
* Other: { type: "Unknown", algorithm }
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} spkiBytes
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function describeSpki(spkiBytes) {
|
||||||
|
const spki = AsnParser.parse(spkiBytes, SubjectPublicKeyInfo);
|
||||||
|
const alg = spki.algorithm.algorithm;
|
||||||
|
const keyBytes = new Uint8Array(spki.subjectPublicKey);
|
||||||
|
|
||||||
|
if (alg === ID_RSA_ENCRYPTION) {
|
||||||
|
const rsa = AsnParser.parse(keyBytes, RSAPublicKey);
|
||||||
|
const n = stripDerLeadingZero(new Uint8Array(rsa.modulus));
|
||||||
|
const e = stripDerLeadingZero(new Uint8Array(rsa.publicExponent));
|
||||||
|
return {
|
||||||
|
type: "RSA",
|
||||||
|
nHex: bytesToHex(n),
|
||||||
|
eValue: Number(BigInt("0x" + (bytesToHex(e) || "0"))),
|
||||||
|
bitLength: n.length === 0 ? 0 : ((n.length - 1) * 8) + (32 - Math.clz32(n[0])),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alg === ID_EC_PUBLIC_KEY) {
|
||||||
|
if (!spki.algorithm.parameters) throw new OperationError("EC SubjectPublicKeyInfo missing parameters");
|
||||||
|
const params = AsnParser.parse(new Uint8Array(spki.algorithm.parameters), ECParameters);
|
||||||
|
const curveOid = params.namedCurve;
|
||||||
|
const info = EC_CURVE_OID_TO_NAMES[curveOid] || { asn1: curveOid, nist: curveOid, byteLen: null, bits: null };
|
||||||
|
if (keyBytes[0] !== 0x04) {
|
||||||
|
throw new OperationError("Only uncompressed EC public keys are supported");
|
||||||
|
}
|
||||||
|
const byteLen = info.byteLen || ((keyBytes.length - 1) / 2);
|
||||||
|
return {
|
||||||
|
type: "EC",
|
||||||
|
curveOid,
|
||||||
|
asn1Curve: info.asn1,
|
||||||
|
nistCurve: info.nist,
|
||||||
|
pubKeyHex: bytesToHex(keyBytes),
|
||||||
|
bitLength: info.bits || byteLen * 8,
|
||||||
|
x: keyBytes.slice(1, 1 + byteLen),
|
||||||
|
y: keyBytes.slice(1 + byteLen, 1 + 2 * byteLen),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alg === ID_DSA) {
|
||||||
|
if (!spki.algorithm.parameters) throw new OperationError("DSA SubjectPublicKeyInfo missing DSS-Parms");
|
||||||
|
const { p, q, g } = parseDssParms(new Uint8Array(spki.algorithm.parameters));
|
||||||
|
const y = parseIntegerBitStringBytes(keyBytes);
|
||||||
|
const pStripped = stripDerLeadingZero(p);
|
||||||
|
return {
|
||||||
|
type: "DSA",
|
||||||
|
yHex: bytesToHex(y),
|
||||||
|
pHex: bytesToHex(p),
|
||||||
|
qHex: bytesToHex(q),
|
||||||
|
gHex: bytesToHex(g),
|
||||||
|
bitLength: pStripped.length * 8,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alg === ID_ED25519 || alg === ID_ED448) {
|
||||||
|
return {
|
||||||
|
type: "EdDSA",
|
||||||
|
curveName: alg === ID_ED25519 ? "Ed25519" : "Ed448",
|
||||||
|
pubKeyHex: bytesToHex(keyBytes),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "Unknown", algorithm: alg };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- signature value parsing ----------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a DER-encoded ECDSA/DSA signature (SEQUENCE { r INTEGER, s INTEGER })
|
||||||
|
* and return r and s as hex strings (without the DER 2's-complement leading
|
||||||
|
* 0x00, when present).
|
||||||
|
*
|
||||||
|
* @param {string} sigHex
|
||||||
|
* @returns {{r: string, s: string}}
|
||||||
|
*/
|
||||||
|
export function parseDerEcdsaSignature(sigHex) {
|
||||||
|
const bytes = fromHex(sigHex);
|
||||||
|
let i = 0;
|
||||||
|
if (bytes[i++] !== 0x30) throw new OperationError("Signature is not an ASN.1 SEQUENCE");
|
||||||
|
const seqLen = readDerLength(bytes, i); i = seqLen.next;
|
||||||
|
if (i + seqLen.value !== bytes.length) throw new OperationError("Trailing bytes after SEQUENCE");
|
||||||
|
|
||||||
|
if (bytes[i++] !== 0x02) throw new OperationError("First element is not an INTEGER");
|
||||||
|
const rLen = readDerLength(bytes, i); i = rLen.next;
|
||||||
|
const r = bytes.slice(i, i + rLen.value); i += rLen.value;
|
||||||
|
|
||||||
|
if (bytes[i++] !== 0x02) throw new OperationError("Second element is not an INTEGER");
|
||||||
|
const sLen = readDerLength(bytes, i); i = sLen.next;
|
||||||
|
const s = bytes.slice(i, i + sLen.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
r: bytesToHex(stripDerLeadingZero(Uint8Array.from(r))),
|
||||||
|
s: bytesToHex(stripDerLeadingZero(Uint8Array.from(s))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the supplied hex bytes parse as a SEQUENCE of two INTEGERs
|
||||||
|
* (the wire format used for ECDSA/DSA signatures).
|
||||||
|
*
|
||||||
|
* @param {string} sigHex
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isDerEcdsaSignature(sigHex) {
|
||||||
|
try {
|
||||||
|
parseDerEcdsaSignature(sigHex);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- formatters -----------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a peculiar/x509 `JsonName` (the array-of-records form returned by
|
||||||
|
* `name.toJSON()`) as a multi-line string of `KEY = value` pairs, indented.
|
||||||
|
*
|
||||||
|
* @param {Array<Record<string, string[]>>} jsonName
|
||||||
|
* @param {number} indent
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatJsonName(jsonName, indent) {
|
||||||
|
if (!Array.isArray(jsonName) || jsonName.length === 0) return "";
|
||||||
|
const rows = [];
|
||||||
|
for (const rdn of jsonName) {
|
||||||
|
for (const key of Object.keys(rdn)) {
|
||||||
|
const values = rdn[key];
|
||||||
|
for (const value of values) rows.push({ key, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rows.length === 0) return "";
|
||||||
|
const maxKey = rows.reduce((m, row) => Math.max(m, row.key.length), 0);
|
||||||
|
const pad = " ".repeat(indent);
|
||||||
|
return rows.map(({ key, value }) => `${pad}${key.padEnd(maxKey, " ")} = ${value}`).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert peculiar/x509's `JsonName` into the OpenSSL-style
|
||||||
|
* "/C=…/ST=…/O=…/CN=…" single-line representation.
|
||||||
|
*
|
||||||
|
* @param {Array<Record<string, string[]>>} jsonName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function jsonNameToSlashString(jsonName) {
|
||||||
|
if (!Array.isArray(jsonName) || jsonName.length === 0) return "";
|
||||||
|
let out = "";
|
||||||
|
for (const rdn of jsonName) {
|
||||||
|
for (const key of Object.keys(rdn)) {
|
||||||
|
for (const value of rdn[key]) out += "/" + key + "=" + value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a hex string as `aa:bb:cc:...` groups of `bytesPerLine` bytes per
|
||||||
|
* line, with each line after the first prefixed by `indent` spaces.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @param {number} bytesPerLine
|
||||||
|
* @param {number} indent
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatHexByteLines(hex, bytesPerLine, indent) {
|
||||||
|
if (hex.length % 2 !== 0) hex = "0" + hex;
|
||||||
|
const colonHex = hex.replace(/(..)/g, "$1:");
|
||||||
|
const trimmed = colonHex.slice(0, -1);
|
||||||
|
const lineLen = bytesPerLine * 3;
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < trimmed.length; i += lineLen) {
|
||||||
|
const chunk = trimmed.slice(i, i + lineLen) + "\n";
|
||||||
|
out += i === 0 ? chunk : " ".repeat(indent) + chunk;
|
||||||
|
}
|
||||||
|
return out.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a hex string as colon-delimited bytes wrapped to `maxLineChars`
|
||||||
|
* characters per line, with continuation lines indented by `indent` spaces.
|
||||||
|
*
|
||||||
|
* Used by ParseCSR / ParseX509CRL where the wrap width is measured in
|
||||||
|
* characters rather than bytes per line.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @param {number} maxLineChars
|
||||||
|
* @param {number} indent
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatHexColonWrapped(hex, maxLineChars, indent) {
|
||||||
|
if (hex.length % 2 !== 0) hex = "0" + hex;
|
||||||
|
const colonHex = hex.replace(/(..)/g, "$1:");
|
||||||
|
const trimmed = colonHex.slice(0, -1);
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < trimmed.length; i += maxLineChars) {
|
||||||
|
lines.push(trimmed.substring(i, i + maxLineChars));
|
||||||
|
}
|
||||||
|
const pad = " ".repeat(indent);
|
||||||
|
return lines.join("\n" + pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- SAN / GeneralName formatting -----------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a single ASN.1 GeneralName (from `@peculiar/asn1-x509`). The
|
||||||
|
* `flavor` arg controls the punctuation: "csr" produces "KEY: value" (with
|
||||||
|
* a space), "crl" produces "KEY:value" (no space and slightly different
|
||||||
|
* label set).
|
||||||
|
*
|
||||||
|
* @param {object} gn - An asn1-x509 GeneralName instance.
|
||||||
|
* @param {"csr"|"crl"} flavor
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatGeneralName(gn, flavor) {
|
||||||
|
const sep = flavor === "crl" ? ":" : ": ";
|
||||||
|
if (gn.dNSName !== undefined) return `DNS${sep}${gn.dNSName}`;
|
||||||
|
if (gn.iPAddress !== undefined) return `IP${sep}${gn.iPAddress}`;
|
||||||
|
if (gn.rfc822Name !== undefined) return `EMAIL${sep}${gn.rfc822Name}`;
|
||||||
|
if (gn.uniformResourceIdentifier !== undefined) return `URI${sep}${gn.uniformResourceIdentifier}`;
|
||||||
|
if (gn.directoryName !== undefined) {
|
||||||
|
return `DIR${sep}${jsonNameToSlashString(asnNameToJson(gn.directoryName))}`;
|
||||||
|
}
|
||||||
|
if (gn.registeredID !== undefined) return `ID${sep}${gn.registeredID}`;
|
||||||
|
if (gn.otherName !== undefined) {
|
||||||
|
const value = otherNameValueToString(gn.otherName);
|
||||||
|
const label = flavor === "crl" ? "OtherName" : "Other";
|
||||||
|
return `${label}${sep}${gn.otherName.typeId}::${value}`;
|
||||||
|
}
|
||||||
|
return `(unsupported general name)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to extract a printable string from an OtherName's ANY-typed value.
|
||||||
|
*
|
||||||
|
* @param {{typeId: string, value: ArrayBuffer}} otherName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function otherNameValueToString(otherName) {
|
||||||
|
const bytes = new Uint8Array(otherName.value);
|
||||||
|
const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||||
|
const parsed = fromBER(ab);
|
||||||
|
if (parsed.offset === -1) return bytesToHex(bytes);
|
||||||
|
const node = parsed.result;
|
||||||
|
if (node instanceof Utf8String || node instanceof BmpString ||
|
||||||
|
node instanceof PrintableString || node instanceof IA5String) {
|
||||||
|
return node.valueBlock.value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const ds = AsnParser.parse(bytes, DirectoryString);
|
||||||
|
return ds.toString();
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
return bytesToHex(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an asn1-x509 Name (CHOICE { RDNSequence }) into the JsonName
|
||||||
|
* representation used by `formatJsonName` / `jsonNameToSlashString`. Uses
|
||||||
|
* the same field-name vocabulary as peculiar/x509's `Name.toJSON()`.
|
||||||
|
*
|
||||||
|
* @param {object} asnName
|
||||||
|
* @returns {Array<Record<string, string[]>>}
|
||||||
|
*/
|
||||||
|
export function asnNameToJson(asnName) {
|
||||||
|
const out = [];
|
||||||
|
if (!asnName || typeof asnName[Symbol.iterator] !== "function") return out;
|
||||||
|
for (const rdn of asnName) {
|
||||||
|
const obj = {};
|
||||||
|
for (const atv of rdn) {
|
||||||
|
const key = OID_TO_SHORT_NAME[atv.type] || atv.type;
|
||||||
|
const val = atv.value.toString();
|
||||||
|
if (!obj[key]) obj[key] = [];
|
||||||
|
obj[key].push(val);
|
||||||
|
}
|
||||||
|
out.push(obj);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----- byte helpers ---------------------------------------------------------
|
||||||
|
|
||||||
|
// bytesToHex is re-exported from @noble/hashes/utils (imported above) so
|
||||||
|
// operations can continue importing it from this module.
|
||||||
|
export { bytesToHex };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip a single leading 0x00 byte from a buffer when it's only present to
|
||||||
|
* keep a DER INTEGER positive (i.e. the next byte's MSB is set).
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
export function stripDerLeadingZero(bytes) {
|
||||||
|
if (bytes.length > 1 && bytes[0] === 0 && (bytes[1] & 0x80)) {
|
||||||
|
return bytes.slice(1);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a BER/DER length octet sequence.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {number} offset
|
||||||
|
* @returns {{value: number, next: number}}
|
||||||
|
*/
|
||||||
|
function readDerLength(bytes, offset) {
|
||||||
|
const first = bytes[offset];
|
||||||
|
if (first < 0x80) return { value: first, next: offset + 1 };
|
||||||
|
const n = first & 0x7f;
|
||||||
|
let value = 0;
|
||||||
|
for (let i = 0; i < n; i++) value = (value << 8) | bytes[offset + 1 + i];
|
||||||
|
return { value, next: offset + 1 + n };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the DSS-Parms (p, q, g) SEQUENCE from a DSA algorithm parameters
|
||||||
|
* blob.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {{p: Uint8Array, q: Uint8Array, g: Uint8Array}}
|
||||||
|
*/
|
||||||
|
function parseDssParms(bytes) {
|
||||||
|
const parsed = fromBER(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
||||||
|
if (parsed.offset === -1) throw new OperationError("Invalid DSS-Parms");
|
||||||
|
const items = parsed.result.valueBlock && parsed.result.valueBlock.value;
|
||||||
|
if (!items || items.length < 3) throw new OperationError("Malformed DSS-Parms");
|
||||||
|
return {
|
||||||
|
p: extractIntegerBytes(items[0]),
|
||||||
|
q: extractIntegerBytes(items[1]),
|
||||||
|
g: extractIntegerBytes(items[2]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode an INTEGER wrapped in a BIT STRING (used for DSA subjectPublicKey).
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function parseIntegerBitStringBytes(bytes) {
|
||||||
|
const parsed = fromBER(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
||||||
|
if (parsed.offset === -1) throw new OperationError("Invalid INTEGER in SPKI");
|
||||||
|
return extractIntegerBytes(parsed.result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull the raw INTEGER bytes out of an asn1js Integer node (preserving the
|
||||||
|
* DER 2's-complement leading 00 — callers strip it if they need a magnitude).
|
||||||
|
*
|
||||||
|
* @param {object} node
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function extractIntegerBytes(node) {
|
||||||
|
const view = node.valueBlock && node.valueBlock.valueHexView;
|
||||||
|
if (!view) throw new OperationError("Missing INTEGER value");
|
||||||
|
return new Uint8Array(view);
|
||||||
|
}
|
||||||
@ -8,7 +8,14 @@ import Operation from "../Operation.mjs";
|
|||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import { fromHex } from "../lib/Hex.mjs";
|
import { fromHex } from "../lib/Hex.mjs";
|
||||||
import { toBase64 } from "../lib/Base64.mjs";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
import r from "jsrsasign";
|
import {
|
||||||
|
loadEcKey,
|
||||||
|
digestBytes,
|
||||||
|
signEcdsa,
|
||||||
|
strToBytesLatin1,
|
||||||
|
asn1SigToConcatHex,
|
||||||
|
parseAsn1SigToHexRS,
|
||||||
|
} from "../lib/Ecdsa.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ECDSA Sign operation
|
* ECDSA Sign operation
|
||||||
@ -69,38 +76,28 @@ class ECDSASign extends Operation {
|
|||||||
throw new OperationError("Please enter a private key.");
|
throw new OperationError("Please enter a private key.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA";
|
const key = loadEcKey(keyPem);
|
||||||
const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName });
|
|
||||||
const key = r.KEYUTIL.getKey(keyPem);
|
|
||||||
if (key.type !== "EC") {
|
|
||||||
throw new OperationError("Provided key is not an EC key.");
|
|
||||||
}
|
|
||||||
if (!key.isPrivate) {
|
if (!key.isPrivate) {
|
||||||
throw new OperationError("Provided key is not a private key.");
|
throw new OperationError("Provided key is not a private key.");
|
||||||
}
|
}
|
||||||
sig.init(key);
|
|
||||||
const signatureASN1Hex = sig.signString(input);
|
|
||||||
|
|
||||||
let result;
|
const digest = digestBytes(mdAlgo, strToBytesLatin1(input));
|
||||||
|
const signatureASN1Hex = signEcdsa(key, digest);
|
||||||
|
|
||||||
switch (outputFormat) {
|
switch (outputFormat) {
|
||||||
case "ASN.1 HEX":
|
case "ASN.1 HEX":
|
||||||
result = signatureASN1Hex;
|
return signatureASN1Hex;
|
||||||
break;
|
|
||||||
case "P1363 HEX":
|
case "P1363 HEX":
|
||||||
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
|
return asn1SigToConcatHex(signatureASN1Hex);
|
||||||
break;
|
case "JSON Web Signature": {
|
||||||
case "JSON Web Signature":
|
const concat = asn1SigToConcatHex(signatureASN1Hex);
|
||||||
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
|
return toBase64(fromHex(concat), "A-Za-z0-9-_"); // base64url
|
||||||
result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url
|
|
||||||
break;
|
|
||||||
case "Raw JSON": {
|
|
||||||
const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex);
|
|
||||||
result = JSON.stringify(signatureRS);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
case "Raw JSON":
|
||||||
|
return JSON.stringify(parseAsn1SigToHexRS(signatureASN1Hex));
|
||||||
|
default:
|
||||||
|
throw new OperationError(`Unsupported output format: ${outputFormat}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,10 +8,16 @@ import Operation from "../Operation.mjs";
|
|||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import { fromBase64, toBase64 } from "../lib/Base64.mjs";
|
import { fromBase64, toBase64 } from "../lib/Base64.mjs";
|
||||||
import { fromHex, toHexFast } from "../lib/Hex.mjs";
|
import { fromHex, toHexFast } from "../lib/Hex.mjs";
|
||||||
import r from "jsrsasign";
|
import {
|
||||||
|
asn1SigToConcatHex,
|
||||||
|
concatHexToAsn1Sig,
|
||||||
|
hexRSToAsn1Sig,
|
||||||
|
parseAsn1SigToHexRS,
|
||||||
|
isAsn1Hex,
|
||||||
|
} from "../lib/Ecdsa.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ECDSA Sign operation
|
* ECDSA Signature Conversion operation
|
||||||
*/
|
*/
|
||||||
class ECDSASignatureConversion extends Operation {
|
class ECDSASignatureConversion extends Operation {
|
||||||
|
|
||||||
@ -75,7 +81,7 @@ class ECDSASignatureConversion extends Operation {
|
|||||||
if (inputFormat === "Auto") {
|
if (inputFormat === "Auto") {
|
||||||
const hexRegex = /^[a-f\d]{2,}$/gi;
|
const hexRegex = /^[a-f\d]{2,}$/gi;
|
||||||
if (hexRegex.test(input)) {
|
if (hexRegex.test(input)) {
|
||||||
if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) {
|
if (input.substring(0, 2) === "30" && isAsn1Hex(input)) {
|
||||||
inputFormat = "ASN.1 HEX";
|
inputFormat = "ASN.1 HEX";
|
||||||
} else {
|
} else {
|
||||||
inputFormat = "P1363 HEX";
|
inputFormat = "P1363 HEX";
|
||||||
@ -100,11 +106,11 @@ class ECDSASignatureConversion extends Operation {
|
|||||||
signatureASN1Hex = input;
|
signatureASN1Hex = input;
|
||||||
break;
|
break;
|
||||||
case "P1363 HEX":
|
case "P1363 HEX":
|
||||||
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input);
|
signatureASN1Hex = concatHexToAsn1Sig(input);
|
||||||
break;
|
break;
|
||||||
case "JSON Web Signature":
|
case "JSON Web Signature":
|
||||||
if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
|
if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
|
||||||
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64));
|
signatureASN1Hex = concatHexToAsn1Sig(toHexFast(inputBase64));
|
||||||
break;
|
break;
|
||||||
case "Raw JSON": {
|
case "Raw JSON": {
|
||||||
if (!inputJson) inputJson = JSON.parse(input);
|
if (!inputJson) inputJson = JSON.parse(input);
|
||||||
@ -114,32 +120,26 @@ class ECDSASignatureConversion extends Operation {
|
|||||||
if (!inputJson.s) {
|
if (!inputJson.s) {
|
||||||
throw new OperationError('No "s" value in the signature JSON');
|
throw new OperationError('No "s" value in the signature JSON');
|
||||||
}
|
}
|
||||||
signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s);
|
signatureASN1Hex = hexRSToAsn1Sig(inputJson.r, inputJson.s);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert ASN.1 hex to output format
|
// convert ASN.1 hex to output format
|
||||||
let result;
|
|
||||||
switch (outputFormat) {
|
switch (outputFormat) {
|
||||||
case "ASN.1 HEX":
|
case "ASN.1 HEX":
|
||||||
result = signatureASN1Hex;
|
return signatureASN1Hex;
|
||||||
break;
|
|
||||||
case "P1363 HEX":
|
case "P1363 HEX":
|
||||||
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
|
return asn1SigToConcatHex(signatureASN1Hex);
|
||||||
break;
|
case "JSON Web Signature": {
|
||||||
case "JSON Web Signature":
|
const concat = asn1SigToConcatHex(signatureASN1Hex);
|
||||||
result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex);
|
return toBase64(fromHex(concat), "A-Za-z0-9-_"); // base64url
|
||||||
result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url
|
|
||||||
break;
|
|
||||||
case "Raw JSON": {
|
|
||||||
const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex);
|
|
||||||
result = JSON.stringify(signatureRS);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
case "Raw JSON":
|
||||||
|
return JSON.stringify(parseAsn1SigToHexRS(signatureASN1Hex));
|
||||||
|
default:
|
||||||
|
throw new OperationError(`Unsupported output format: ${outputFormat}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,16 @@ import Operation from "../Operation.mjs";
|
|||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import { fromBase64 } from "../lib/Base64.mjs";
|
import { fromBase64 } from "../lib/Base64.mjs";
|
||||||
import { toHexFast } from "../lib/Hex.mjs";
|
import { toHexFast } from "../lib/Hex.mjs";
|
||||||
import r from "jsrsasign";
|
|
||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
|
import {
|
||||||
|
loadEcKey,
|
||||||
|
digestBytes,
|
||||||
|
verifyEcdsa,
|
||||||
|
strToBytesLatin1,
|
||||||
|
concatHexToAsn1Sig,
|
||||||
|
hexRSToAsn1Sig,
|
||||||
|
isAsn1Hex,
|
||||||
|
} from "../lib/Ecdsa.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ECDSA Verify operation
|
* ECDSA Verify operation
|
||||||
@ -96,7 +104,7 @@ class ECDSAVerify extends Operation {
|
|||||||
if (inputFormat === "Auto") {
|
if (inputFormat === "Auto") {
|
||||||
const hexRegex = /^[a-f\d]{2,}$/gi;
|
const hexRegex = /^[a-f\d]{2,}$/gi;
|
||||||
if (hexRegex.test(input)) {
|
if (hexRegex.test(input)) {
|
||||||
if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) {
|
if (input.substring(0, 2) === "30" && isAsn1Hex(input)) {
|
||||||
inputFormat = "ASN.1 HEX";
|
inputFormat = "ASN.1 HEX";
|
||||||
} else {
|
} else {
|
||||||
inputFormat = "P1363 HEX";
|
inputFormat = "P1363 HEX";
|
||||||
@ -121,11 +129,11 @@ class ECDSAVerify extends Operation {
|
|||||||
signatureASN1Hex = input;
|
signatureASN1Hex = input;
|
||||||
break;
|
break;
|
||||||
case "P1363 HEX":
|
case "P1363 HEX":
|
||||||
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input);
|
signatureASN1Hex = concatHexToAsn1Sig(input);
|
||||||
break;
|
break;
|
||||||
case "JSON Web Signature":
|
case "JSON Web Signature":
|
||||||
if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
|
if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_");
|
||||||
signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64));
|
signatureASN1Hex = concatHexToAsn1Sig(toHexFast(inputBase64));
|
||||||
break;
|
break;
|
||||||
case "Raw JSON": {
|
case "Raw JSON": {
|
||||||
if (!inputJson) inputJson = JSON.parse(input);
|
if (!inputJson) inputJson = JSON.parse(input);
|
||||||
@ -135,26 +143,20 @@ class ECDSAVerify extends Operation {
|
|||||||
if (!inputJson.s) {
|
if (!inputJson.s) {
|
||||||
throw new OperationError('No "s" value in the signature JSON');
|
throw new OperationError('No "s" value in the signature JSON');
|
||||||
}
|
}
|
||||||
signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s);
|
signatureASN1Hex = hexRSToAsn1Sig(inputJson.r, inputJson.s);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify signature
|
const key = loadEcKey(keyPem);
|
||||||
const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA";
|
|
||||||
const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName });
|
|
||||||
const key = r.KEYUTIL.getKey(keyPem);
|
|
||||||
if (key.type !== "EC") {
|
|
||||||
throw new OperationError("Provided key is not an EC key.");
|
|
||||||
}
|
|
||||||
if (!key.isPublic) {
|
if (!key.isPublic) {
|
||||||
throw new OperationError("Provided key is not a public key.");
|
throw new OperationError("Provided key is not a public key.");
|
||||||
}
|
}
|
||||||
sig.init(key);
|
|
||||||
const messageStr = Utils.convertToByteString(msg, msgFormat);
|
const messageStr = Utils.convertToByteString(msg, msgFormat);
|
||||||
sig.updateString(messageStr);
|
const digest = digestBytes(mdAlgo, strToBytesLatin1(messageStr));
|
||||||
const result = sig.verify(signatureASN1Hex);
|
const ok = verifyEcdsa(key, digest, signatureASN1Hex);
|
||||||
return result ? "Verified OK" : "Verification Failure";
|
return ok ? "Verified OK" : "Verification Failure";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
||||||
import { cryptNotice } from "../lib/Crypt.mjs";
|
import { cryptNotice } from "../lib/Crypt.mjs";
|
||||||
import r from "jsrsasign";
|
import { toBase64 } from "../lib/Base64.mjs";
|
||||||
|
import {
|
||||||
|
generateEcKeyPair,
|
||||||
|
publicKeyToSpkiPem,
|
||||||
|
privateKeyToPkcs8Pem,
|
||||||
|
} from "../lib/Ecdsa.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate ECDSA Key Pair operation
|
* Generate ECDSA Key Pair operation
|
||||||
@ -54,49 +60,47 @@ class GenerateECDSAKeyPair extends Operation {
|
|||||||
*/
|
*/
|
||||||
async run(input, args) {
|
async run(input, args) {
|
||||||
const [curveName, outputFormat] = args;
|
const [curveName, outputFormat] = args;
|
||||||
|
const pair = generateEcKeyPair(curveName);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let internalCurveName;
|
|
||||||
switch (curveName) {
|
|
||||||
case "P-256":
|
|
||||||
internalCurveName = "secp256r1";
|
|
||||||
break;
|
|
||||||
case "P-384":
|
|
||||||
internalCurveName = "secp384r1";
|
|
||||||
break;
|
|
||||||
case "P-521":
|
|
||||||
internalCurveName = "secp521r1";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const keyPair = r.KEYUTIL.generateKeypair("EC", internalCurveName);
|
|
||||||
|
|
||||||
let pubKey;
|
|
||||||
let privKey;
|
|
||||||
let result;
|
|
||||||
switch (outputFormat) {
|
switch (outputFormat) {
|
||||||
case "PEM":
|
case "PEM":
|
||||||
pubKey = r.KEYUTIL.getPEM(keyPair.pubKeyObj).replace(/\r/g, "");
|
return publicKeyToSpkiPem(pair) + privateKeyToPkcs8Pem(pair);
|
||||||
privKey = r.KEYUTIL.getPEM(keyPair.prvKeyObj, "PKCS8PRV").replace(/\r/g, "");
|
|
||||||
result = pubKey + "\n" + privKey;
|
|
||||||
break;
|
|
||||||
case "DER":
|
case "DER":
|
||||||
result = keyPair.prvKeyObj.prvKeyHex;
|
return bytesToHex(pair.d);
|
||||||
break;
|
case "JWK": {
|
||||||
case "JWK":
|
const pubJwk = {
|
||||||
pubKey = r.KEYUTIL.getJWKFromKey(keyPair.pubKeyObj);
|
kty: "EC",
|
||||||
pubKey.key_ops = ["verify"]; // eslint-disable-line camelcase
|
crv: curveName,
|
||||||
pubKey.kid = "PublicKey";
|
x: b64url(pair.x),
|
||||||
privKey = r.KEYUTIL.getJWKFromKey(keyPair.prvKeyObj);
|
y: b64url(pair.y),
|
||||||
privKey.key_ops = ["sign"]; // eslint-disable-line camelcase
|
"key_ops": ["verify"],
|
||||||
privKey.kid = "PrivateKey";
|
kid: "PublicKey",
|
||||||
result = JSON.stringify({keys: [privKey, pubKey]}, null, 4);
|
};
|
||||||
break;
|
const privJwk = {
|
||||||
}
|
kty: "EC",
|
||||||
|
crv: curveName,
|
||||||
resolve(result);
|
x: b64url(pair.x),
|
||||||
});
|
y: b64url(pair.y),
|
||||||
|
d: b64url(pair.d),
|
||||||
|
"key_ops": ["sign"],
|
||||||
|
kid: "PrivateKey",
|
||||||
|
};
|
||||||
|
return JSON.stringify({ keys: [privJwk, pubJwk] }, null, 4);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported output format: ${outputFormat}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base64url-encode a byte array (no padding, URL-safe alphabet).
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function b64url(bytes) {
|
||||||
|
return toBase64(bytes, "A-Za-z0-9-_");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GenerateECDSAKeyPair;
|
export default GenerateECDSAKeyPair;
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import { oidHexToInt } from "../lib/Asn1.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hex to Object Identifier operation
|
* Hex to Object Identifier operation
|
||||||
@ -33,7 +33,7 @@ class HexToObjectIdentifier extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
return r.KJUR.asn1.ASN1Util.oidHexToInt(input.replace(/\s/g, ""));
|
return oidHexToInt(input.replace(/\s/g, ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import { derToPem } from "../lib/Asn1.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hex to PEM operation
|
* Hex to PEM operation
|
||||||
@ -39,7 +39,7 @@ class HexToPEM extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
return r.KJUR.asn1.ASN1Util.getPEMStringFromHex(input.replace(/\s/g, ""), args[0]);
|
return derToPem(input.replace(/\s/g, ""), args[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,17 +4,17 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { keyFromJwk, keyInfoToPem } from "../lib/KeyConvert.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PEM to JWK operation
|
* JWK to PEM operation
|
||||||
*/
|
*/
|
||||||
class PEMToJWK extends Operation {
|
class JWKToPem extends Operation {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PEMToJWK constructor
|
* JWKToPem constructor
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -45,36 +45,27 @@ class PEMToJWK extends Operation {
|
|||||||
|
|
||||||
let keys = [];
|
let keys = [];
|
||||||
if (Array.isArray(inputJson)) {
|
if (Array.isArray(inputJson)) {
|
||||||
// list of keys => transform all keys
|
|
||||||
keys = inputJson;
|
keys = inputJson;
|
||||||
} else if (Array.isArray(inputJson.keys)) {
|
} else if (Array.isArray(inputJson.keys)) {
|
||||||
// JSON Web Key Set => transform all keys
|
|
||||||
keys = inputJson.keys;
|
keys = inputJson.keys;
|
||||||
} else if (typeof inputJson === "object") {
|
} else if (typeof inputJson === "object" && inputJson !== null) {
|
||||||
// single key
|
|
||||||
keys.push(inputJson);
|
keys.push(inputJson);
|
||||||
} else {
|
} else {
|
||||||
throw new OperationError("Input is not a JSON Web Key");
|
throw new OperationError("Input is not a JSON Web Key");
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = "";
|
let output = "";
|
||||||
for (let i=0; i<keys.length; i++) {
|
for (const jwk of keys) {
|
||||||
const jwk = keys[i];
|
if (!jwk || typeof jwk.kty !== "string") {
|
||||||
if (typeof jwk.kty !== "string") {
|
|
||||||
throw new OperationError("Invalid JWK format");
|
throw new OperationError("Invalid JWK format");
|
||||||
} else if ("|RSA|EC|".indexOf(jwk.kty) === -1) {
|
|
||||||
throw new OperationError(`Unsupported JWK key type '${inputJson.kty}'`);
|
|
||||||
}
|
}
|
||||||
|
if (jwk.kty !== "RSA" && jwk.kty !== "EC") {
|
||||||
const key = r.KEYUTIL.getKey(jwk);
|
throw new OperationError(`Unsupported JWK key type '${jwk.kty}'`);
|
||||||
const pem = key.isPrivate ? r.KEYUTIL.getPEM(key, "PKCS8PRV") : r.KEYUTIL.getPEM(key);
|
}
|
||||||
|
output += keyInfoToPem(keyFromJwk(jwk));
|
||||||
// PEM ends with '\n', so a new key always starts on a new line
|
|
||||||
output += pem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PEMToJWK;
|
export default JWKToPem;
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import { oidIntToHex } from "../lib/Asn1.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object Identifier to Hex operation
|
* Object Identifier to Hex operation
|
||||||
@ -33,7 +33,7 @@ class ObjectIdentifierToHex extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
return r.KJUR.asn1.ASN1Util.oidIntToHex(input);
|
return oidIntToHex(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import { parseKeyPem, parseCertPublicKey, keyToJwk } from "../lib/KeyConvert.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PEM to JWK operation
|
* PEM to JWK operation
|
||||||
@ -54,32 +54,25 @@ class PEMToJWK extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pem = input.substring(match.index, indexFooter + footer.length);
|
const pem = input.substring(match.index, indexFooter + footer.length);
|
||||||
|
|
||||||
|
let info;
|
||||||
if (match[1].indexOf("KEY") !== -1) {
|
if (match[1].indexOf("KEY") !== -1) {
|
||||||
if (header === "-----BEGIN RSA PUBLIC KEY-----") {
|
if (header === "-----BEGIN RSA PUBLIC KEY-----") {
|
||||||
throw new OperationError("Unsupported RSA public key format. Only PKCS#8 is supported.");
|
throw new OperationError("Unsupported RSA public key format. Only PKCS#8 is supported.");
|
||||||
}
|
}
|
||||||
|
info = parseKeyPem(pem);
|
||||||
const key = r.KEYUTIL.getKey(pem);
|
|
||||||
if (key.type === "DSA") {
|
|
||||||
throw new OperationError("DSA keys are not supported for JWK");
|
|
||||||
}
|
|
||||||
const jwk = r.KEYUTIL.getJWKFromKey(key);
|
|
||||||
if (output.length > 0) {
|
|
||||||
output += "\n";
|
|
||||||
}
|
|
||||||
output += JSON.stringify(jwk);
|
|
||||||
} else if (match[1] === "CERTIFICATE") {
|
} else if (match[1] === "CERTIFICATE") {
|
||||||
const cert = new r.X509();
|
info = parseCertPublicKey(pem);
|
||||||
cert.readCertPEM(pem);
|
|
||||||
const key = cert.getPublicKey();
|
|
||||||
const jwk = r.KEYUTIL.getJWKFromKey(key);
|
|
||||||
if (output.length > 0) {
|
|
||||||
output += "\n";
|
|
||||||
}
|
|
||||||
output += JSON.stringify(jwk);
|
|
||||||
} else {
|
} else {
|
||||||
throw new OperationError(`Unsupported PEM type '${match[1]}'`);
|
throw new OperationError(`Unsupported PEM type '${match[1]}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info.kty === "DSA") {
|
||||||
|
throw new OperationError("DSA keys are not supported for JWK");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.length > 0) output += "\n";
|
||||||
|
output += JSON.stringify(keyToJwk(info));
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import { dumpAsn1Hex } from "../lib/Asn1.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse ASN.1 hex string operation
|
* Parse ASN.1 hex string operation
|
||||||
@ -45,9 +45,10 @@ class ParseASN1HexString extends Operation {
|
|||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
const [index, truncateLen] = args;
|
const [index, truncateLen] = args;
|
||||||
return r.ASN1HEX.dump(input.replace(/\s/g, "").toLowerCase(), {
|
return dumpAsn1Hex(input.replace(/\s/g, "").toLowerCase(), {
|
||||||
"ommit_long_octet": truncateLen
|
truncate: truncateLen,
|
||||||
}, index);
|
startIndex: index,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,34 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
import { Pkcs10CertificateRequest } from "@peculiar/x509";
|
||||||
|
import { AsnParser } from "@peculiar/asn1-schema";
|
||||||
|
import {
|
||||||
|
BasicConstraints, ExtendedKeyUsage, Extensions, KeyUsage,
|
||||||
|
SubjectAlternativeName,
|
||||||
|
} from "@peculiar/asn1-x509";
|
||||||
|
import * as asn1X509 from "@peculiar/asn1-x509";
|
||||||
|
import { CertificationRequest } from "@peculiar/asn1-csr";
|
||||||
|
import * as asnPkcs9 from "@peculiar/asn1-pkcs9";
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import { formatDnObj } from "../lib/PublicKey.mjs";
|
import { formatDnObj } from "../lib/PublicKey.mjs";
|
||||||
|
import {
|
||||||
|
bytesToHex,
|
||||||
|
describeSpki,
|
||||||
|
formatGeneralName,
|
||||||
|
formatHexColonWrapped,
|
||||||
|
parseDerEcdsaSignature,
|
||||||
|
sigAlgOidToName,
|
||||||
|
} from "../lib/X509.mjs";
|
||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
|
const ID_CE_BASIC_CONSTRAINTS = asn1X509.id_ce_basicConstraints;
|
||||||
|
const ID_CE_EXT_KEY_USAGE = asn1X509.id_ce_extKeyUsage;
|
||||||
|
const ID_CE_KEY_USAGE = asn1X509.id_ce_keyUsage;
|
||||||
|
const ID_CE_SUBJECT_ALT_NAME = asn1X509.id_ce_subjectAltName;
|
||||||
|
const ID_PKCS9_AT_EXTENSION_REQUEST = asnPkcs9.id_pkcs9_at_extensionRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse CSR operation
|
* Parse CSR operation
|
||||||
*/
|
*/
|
||||||
@ -45,333 +68,232 @@ class ParseCSR extends Operation {
|
|||||||
/**
|
/**
|
||||||
* @param {string} input
|
* @param {string} input
|
||||||
* @param {Object[]} args
|
* @param {Object[]} args
|
||||||
* @returns {string} Human-readable description of a Certificate Signing Request (CSR).
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
if (!input.length) {
|
if (!input.length) return "No input";
|
||||||
return "No input";
|
|
||||||
|
let csr;
|
||||||
|
try {
|
||||||
|
csr = new Pkcs10CertificateRequest(input);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Failed to parse CSR: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the CSR into JSON parameters
|
const subjectStr = formatDnObj(csr.subjectName.toJSON(), 2);
|
||||||
const csrParam = new r.KJUR.asn1.csr.CSRUtil.getParam(input);
|
const spki = describeSpki(new Uint8Array(csr.publicKey.rawData));
|
||||||
|
const asnCsr = AsnParser.parse(new Uint8Array(csr.rawData), CertificationRequest);
|
||||||
|
const sigAlgName = sigAlgOidToName(asnCsr.signatureAlgorithm.algorithm);
|
||||||
|
const sigHex = bytesToHex(new Uint8Array(csr.signature));
|
||||||
|
|
||||||
return `Subject\n${formatDnObj(csrParam.subject, 2)}
|
return `Subject\n${subjectStr}
|
||||||
Public Key${formatSubjectPublicKey(csrParam.sbjpubkey)}
|
Public Key${formatPublicKey(spki)}
|
||||||
Signature${formatSignature(csrParam.sigalg, csrParam.sighex)}
|
Signature${formatSignature(sigAlgName, sigHex)}
|
||||||
Requested Extensions${formatRequestedExtensions(csrParam)}`;
|
Requested Extensions${formatRequestedExtensions(csr)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format signature of a CSR
|
* Format the public-key section.
|
||||||
* @param {*} sigAlg string
|
*
|
||||||
* @param {*} sigHex string
|
* @param {object} spki
|
||||||
* @returns Multi-line string describing CSR Signature
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatSignature(sigAlg, sigHex) {
|
function formatPublicKey(spki) {
|
||||||
let out = `\n`;
|
|
||||||
|
|
||||||
out += ` Algorithm: ${sigAlg}\n`;
|
|
||||||
|
|
||||||
if (new RegExp("withdsa", "i").test(sigAlg)) {
|
|
||||||
const d = new r.KJUR.crypto.DSA();
|
|
||||||
const sigParam = d.parseASN1Signature(sigHex);
|
|
||||||
out += ` Signature:
|
|
||||||
R: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[0]))}
|
|
||||||
S: ${formatHexOntoMultiLine(absBigIntToHex(sigParam[1]))}\n`;
|
|
||||||
} else if (new RegExp("withrsa", "i").test(sigAlg)) {
|
|
||||||
out += ` Signature: ${formatHexOntoMultiLine(sigHex)}\n`;
|
|
||||||
} else {
|
|
||||||
out += ` Signature: ${formatHexOntoMultiLine(ensureHexIsPositiveInTwosComplement(sigHex))}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return chop(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format Subject Public Key from PEM encoded public key string
|
|
||||||
* @param {*} publicKeyPEM string
|
|
||||||
* @returns Multi-line string describing Subject Public Key Info
|
|
||||||
*/
|
|
||||||
function formatSubjectPublicKey(publicKeyPEM) {
|
|
||||||
let out = "\n";
|
let out = "\n";
|
||||||
|
if (spki.type === "RSA") {
|
||||||
const publicKey = r.KEYUTIL.getKey(publicKeyPEM);
|
|
||||||
if (publicKey instanceof r.RSAKey) {
|
|
||||||
out += ` Algorithm: RSA
|
out += ` Algorithm: RSA
|
||||||
Length: ${publicKey.n.bitLength()} bits
|
Length: ${spki.bitLength} bits
|
||||||
Modulus: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.n))}
|
Modulus: ${formatHexColonWrapped(prefix00IfMsbSet(spki.nHex), 48, 18)}
|
||||||
Exponent: ${publicKey.e} (0x${Utils.hex(publicKey.e)})\n`;
|
Exponent: ${spki.eValue} (0x${Utils.hex(spki.eValue)})\n`;
|
||||||
} else if (publicKey instanceof r.KJUR.crypto.ECDSA) {
|
} else if (spki.type === "EC") {
|
||||||
out += ` Algorithm: ECDSA
|
out += ` Algorithm: ECDSA
|
||||||
Length: ${publicKey.ecparams.keylen} bits
|
Length: ${spki.bitLength} bits
|
||||||
Pub: ${formatHexOntoMultiLine(publicKey.pubKeyHex)}
|
Pub: ${formatHexColonWrapped(spki.pubKeyHex, 48, 18)}
|
||||||
ASN1 OID: ${r.KJUR.crypto.ECDSA.getName(publicKey.getShortNISTPCurveName())}
|
ASN1 OID: ${spki.asn1Curve}
|
||||||
NIST CURVE: ${publicKey.getShortNISTPCurveName()}\n`;
|
NIST CURVE: ${spki.nistCurve}\n`;
|
||||||
} else if (publicKey instanceof r.KJUR.crypto.DSA) {
|
} else if (spki.type === "DSA") {
|
||||||
out += ` Algorithm: DSA
|
out += ` Algorithm: DSA
|
||||||
Length: ${publicKey.p.toString(16).length * 4} bits
|
Length: ${spki.bitLength} bits
|
||||||
Pub: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.y))}
|
Pub: ${formatHexColonWrapped(prefix00IfMsbSet(spki.yHex), 48, 18)}
|
||||||
P: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.p))}
|
P: ${formatHexColonWrapped(prefix00IfMsbSet(spki.pHex), 48, 18)}
|
||||||
Q: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.q))}
|
Q: ${formatHexColonWrapped(prefix00IfMsbSet(spki.qHex), 48, 18)}
|
||||||
G: ${formatHexOntoMultiLine(absBigIntToHex(publicKey.g))}\n`;
|
G: ${formatHexColonWrapped(prefix00IfMsbSet(spki.gHex), 48, 18)}\n`;
|
||||||
} else {
|
} else {
|
||||||
out += `unsupported public key algorithm\n`;
|
out += `unsupported public key algorithm\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return chop(out);
|
return chop(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format known extensions of a CSR
|
* Prefix the hex string with "00" when its most significant bit is set.
|
||||||
* @param {*} csrParam object
|
* Mirrors the legacy "ensureHexIsPositiveInTwosComplement" behaviour the
|
||||||
* @returns Multi-line string describing CSR Requested Extensions
|
* golden CSR fixtures depend on.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatRequestedExtensions(csrParam) {
|
function prefix00IfMsbSet(hex) {
|
||||||
const formattedExtensions = new Array(4).fill("");
|
if (hex.length % 2 !== 0) hex = "0" + hex;
|
||||||
|
if (hex.length >= 2 && (parseInt(hex.substring(0, 2), 16) & 0x80)) {
|
||||||
if (Object.hasOwn(csrParam, "extreq")) {
|
|
||||||
for (const extension of csrParam.extreq) {
|
|
||||||
let parts = [];
|
|
||||||
switch (extension.extname) {
|
|
||||||
case "basicConstraints" :
|
|
||||||
parts = describeBasicConstraints(extension);
|
|
||||||
formattedExtensions[0] = ` Basic Constraints:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
|
|
||||||
break;
|
|
||||||
case "keyUsage" :
|
|
||||||
parts = describeKeyUsage(extension);
|
|
||||||
formattedExtensions[1] = ` Key Usage:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
|
|
||||||
break;
|
|
||||||
case "extKeyUsage" :
|
|
||||||
parts = describeExtendedKeyUsage(extension);
|
|
||||||
formattedExtensions[2] = ` Extended Key Usage:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
|
|
||||||
break;
|
|
||||||
case "subjectAltName" :
|
|
||||||
parts = describeSubjectAlternativeName(extension);
|
|
||||||
formattedExtensions[3] = ` Subject Alternative Name:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`;
|
|
||||||
break;
|
|
||||||
default :
|
|
||||||
parts = ["(unsuported extension)"];
|
|
||||||
formattedExtensions.push(` ${extension.extname}:${formatExtensionCriticalTag(extension)}\n${indent(4, parts)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let out = "\n";
|
|
||||||
|
|
||||||
formattedExtensions.forEach((formattedExtension) => {
|
|
||||||
if (formattedExtension !== undefined && formattedExtension !== null && formattedExtension.length !== 0) {
|
|
||||||
out += formattedExtension;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return chop(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format extension critical tag
|
|
||||||
* @param {*} extension Object
|
|
||||||
* @returns String describing whether the extension is critical or not
|
|
||||||
*/
|
|
||||||
function formatExtensionCriticalTag(extension) {
|
|
||||||
return Object.hasOwn(extension, "critical") && extension.critical ? " critical" : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format string input as a comma separated hex string on multiple lines
|
|
||||||
* @param {*} hex String
|
|
||||||
* @returns Multi-line string describing the Hex input
|
|
||||||
*/
|
|
||||||
function formatHexOntoMultiLine(hex) {
|
|
||||||
if (hex.length % 2 !== 0) {
|
|
||||||
hex = "0" + hex;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatMultiLine(chop(hex.replace(/(..)/g, "$&:")));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert BigInt to abs value in Hex
|
|
||||||
* @param {*} int BigInt
|
|
||||||
* @returns String representing absolute value in Hex
|
|
||||||
*/
|
|
||||||
function absBigIntToHex(int) {
|
|
||||||
int = int < 0n ? -int : int;
|
|
||||||
|
|
||||||
return ensureHexIsPositiveInTwosComplement(int.toString(16));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure Hex String remains positive in 2's complement
|
|
||||||
* @param {*} hex String
|
|
||||||
* @returns Hex String ensuring value remains positive in 2's complement
|
|
||||||
*/
|
|
||||||
function ensureHexIsPositiveInTwosComplement(hex) {
|
|
||||||
if (hex.length % 2 !== 0) {
|
|
||||||
return "0" + hex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepend 00 if most significant bit is 1 (sign bit)
|
|
||||||
if (hex.length >=2 && (parseInt(hex.substring(0, 2), 16) & 128)) {
|
|
||||||
hex = "00" + hex;
|
hex = "00" + hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return hex;
|
return hex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format string onto multiple lines
|
* Format the signature section.
|
||||||
* @param {*} longStr
|
*
|
||||||
* @returns String as a multi-line string
|
* @param {string} sigAlgName
|
||||||
|
* @param {string} sigHex
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatMultiLine(longStr) {
|
function formatSignature(sigAlgName, sigHex) {
|
||||||
const lines = [];
|
let out = `\n Algorithm: ${sigAlgName}\n`;
|
||||||
|
|
||||||
for (let remain = longStr ; remain !== "" ; remain = remain.substring(48)) {
|
if (/withdsa/i.test(sigAlgName)) {
|
||||||
lines.push(remain.substring(0, 48));
|
const { r, s } = parseDerEcdsaSignature(sigHex);
|
||||||
}
|
out += ` Signature:
|
||||||
|
R: ${formatHexColonWrapped(prefix00IfMsbSet(r), 48, 18)}
|
||||||
return lines.join("\n ");
|
S: ${formatHexColonWrapped(prefix00IfMsbSet(s), 48, 18)}\n`;
|
||||||
}
|
} else if (/withrsa/i.test(sigAlgName)) {
|
||||||
|
out += ` Signature: ${formatHexColonWrapped(sigHex, 48, 18)}\n`;
|
||||||
/**
|
|
||||||
* Describe Basic Constraints
|
|
||||||
* @see RFC 5280 4.2.1.9. Basic Constraints https://www.ietf.org/rfc/rfc5280.txt
|
|
||||||
* @param {*} extension CSR extension with the name `basicConstraints`
|
|
||||||
* @returns Array of strings describing Basic Constraints
|
|
||||||
*/
|
|
||||||
function describeBasicConstraints(extension) {
|
|
||||||
const constraints = [];
|
|
||||||
|
|
||||||
constraints.push(`CA = ${Object.hasOwn(extension, "cA") && extension.cA ? "true" : "false"}`);
|
|
||||||
if (Object.hasOwn(extension, "pathLen")) constraints.push(`PathLenConstraint = ${extension.pathLen}`);
|
|
||||||
|
|
||||||
return constraints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describe Key Usage extension permitted use cases
|
|
||||||
* @see RFC 5280 4.2.1.3. Key Usage https://www.ietf.org/rfc/rfc5280.txt
|
|
||||||
* @param {*} extension CSR extension with the name `keyUsage`
|
|
||||||
* @returns Array of strings describing Key Usage extension permitted use cases
|
|
||||||
*/
|
|
||||||
function describeKeyUsage(extension) {
|
|
||||||
const usage = [];
|
|
||||||
|
|
||||||
const kuIdentifierToName = {
|
|
||||||
digitalSignature: "Digital Signature",
|
|
||||||
nonRepudiation: "Non-repudiation",
|
|
||||||
keyEncipherment: "Key encipherment",
|
|
||||||
dataEncipherment: "Data encipherment",
|
|
||||||
keyAgreement: "Key agreement",
|
|
||||||
keyCertSign: "Key certificate signing",
|
|
||||||
cRLSign: "CRL signing",
|
|
||||||
encipherOnly: "Encipher Only",
|
|
||||||
decipherOnly: "Decipher Only",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.hasOwn(extension, "names")) {
|
|
||||||
extension.names.forEach((ku) => {
|
|
||||||
if (Object.hasOwn(kuIdentifierToName, ku)) {
|
|
||||||
usage.push(kuIdentifierToName[ku]);
|
|
||||||
} else {
|
} else {
|
||||||
usage.push(`unknown key usage (${ku})`);
|
out += ` Signature: ${formatHexColonWrapped(prefix00IfMsbSet(sigHex), 48, 18)}\n`;
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usage.length === 0) usage.push("(none)");
|
return chop(out);
|
||||||
|
|
||||||
return usage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describe Extended Key Usage extension permitted use cases
|
* Format the Requested Extensions section. Reads the extensionRequest
|
||||||
* @see RFC 5280 4.2.1.12. Extended Key Usage https://www.ietf.org/rfc/rfc5280.txt
|
* attribute (OID 1.2.840.113549.1.9.14) and dispatches the well-known
|
||||||
* @param {*} extension CSR extension with the name `extendedKeyUsage`
|
* extension types to per-type formatters. Unknown extensions render as
|
||||||
* @returns Array of strings describing Extended Key Usage extension permitted use cases
|
* `(unsuported extension)` to match the existing golden output.
|
||||||
|
*
|
||||||
|
* @param {object} csr
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function describeExtendedKeyUsage(extension) {
|
function formatRequestedExtensions(csr) {
|
||||||
const usage = [];
|
const extReqAttr = csr.attributes.find(a => a.type === ID_PKCS9_AT_EXTENSION_REQUEST);
|
||||||
|
if (!extReqAttr || !extReqAttr.values || extReqAttr.values.length === 0) {
|
||||||
|
return "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
let extensions;
|
||||||
|
try {
|
||||||
|
extensions = AsnParser.parse(new Uint8Array(extReqAttr.values[0]), Extensions);
|
||||||
|
} catch {
|
||||||
|
return "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = new Array(4).fill("");
|
||||||
|
const tail = [];
|
||||||
|
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const criticalTag = ext.critical ? " critical" : "";
|
||||||
|
const value = new Uint8Array(ext.extnValue.buffer);
|
||||||
|
switch (ext.extnID) {
|
||||||
|
case ID_CE_BASIC_CONSTRAINTS: {
|
||||||
|
const bc = AsnParser.parse(value, BasicConstraints);
|
||||||
|
const parts = [`CA = ${bc.cA ? "true" : "false"}`];
|
||||||
|
if (bc.pathLenConstraint !== undefined) parts.push(`PathLenConstraint = ${bc.pathLenConstraint}`);
|
||||||
|
formatted[0] = ` Basic Constraints:${criticalTag}\n${indent(4, parts)}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ID_CE_KEY_USAGE: {
|
||||||
|
const ku = AsnParser.parse(value, KeyUsage);
|
||||||
|
formatted[1] = ` Key Usage:${criticalTag}\n${indent(4, describeKeyUsage(ku.toNumber()))}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ID_CE_EXT_KEY_USAGE: {
|
||||||
|
const eku = AsnParser.parse(value, ExtendedKeyUsage);
|
||||||
|
formatted[2] = ` Extended Key Usage:${criticalTag}\n${indent(4, describeExtendedKeyUsage(Array.from(eku)))}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ID_CE_SUBJECT_ALT_NAME: {
|
||||||
|
const san = AsnParser.parse(value, SubjectAlternativeName);
|
||||||
|
const items = san.map(gn => formatGeneralName(gn, "csr"));
|
||||||
|
formatted[3] = ` Subject Alternative Name:${criticalTag}\n${indent(4, items)}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
tail.push(` ${ext.extnID}:${criticalTag}\n${indent(4, ["(unsuported extension)"])}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = "\n";
|
||||||
|
for (const block of [...formatted, ...tail]) {
|
||||||
|
if (block && block.length !== 0) out += block;
|
||||||
|
}
|
||||||
|
return chop(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the KeyUsage bit-string flags into the bit-order list of
|
||||||
|
* human-readable names the existing golden fixtures expect.
|
||||||
|
*
|
||||||
|
* @param {number} flags
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function describeKeyUsage(flags) {
|
||||||
|
const bitOrder = [
|
||||||
|
[0x001, "Digital Signature"],
|
||||||
|
[0x002, "Non-repudiation"],
|
||||||
|
[0x004, "Key encipherment"],
|
||||||
|
[0x008, "Data encipherment"],
|
||||||
|
[0x010, "Key agreement"],
|
||||||
|
[0x020, "Key certificate signing"],
|
||||||
|
[0x040, "CRL signing"],
|
||||||
|
[0x080, "Encipher Only"],
|
||||||
|
[0x100, "Decipher Only"],
|
||||||
|
];
|
||||||
|
const out = [];
|
||||||
|
for (const [bit, label] of bitOrder) {
|
||||||
|
if (flags & bit) out.push(label);
|
||||||
|
}
|
||||||
|
if (out.length === 0) out.push("(none)");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate the EKU OIDs (and aliases) into the human-readable names the
|
||||||
|
* existing golden fixtures expect.
|
||||||
|
*
|
||||||
|
* @param {string[]} usages - List of OIDs/short names.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function describeExtendedKeyUsage(usages) {
|
||||||
const ekuIdentifierToName = {
|
const ekuIdentifierToName = {
|
||||||
|
"1.3.6.1.5.5.7.3.1": "TLS Web Server Authentication",
|
||||||
|
"1.3.6.1.5.5.7.3.2": "TLS Web Client Authentication",
|
||||||
|
"1.3.6.1.5.5.7.3.3": "Code signing",
|
||||||
|
"1.3.6.1.5.5.7.3.4": "E-mail Protection (S/MIME)",
|
||||||
|
"1.3.6.1.5.5.7.3.8": "Trusted Timestamping",
|
||||||
"serverAuth": "TLS Web Server Authentication",
|
"serverAuth": "TLS Web Server Authentication",
|
||||||
"clientAuth": "TLS Web Client Authentication",
|
"clientAuth": "TLS Web Client Authentication",
|
||||||
"codeSigning": "Code signing",
|
"codeSigning": "Code signing",
|
||||||
"emailProtection": "E-mail Protection (S/MIME)",
|
"emailProtection": "E-mail Protection (S/MIME)",
|
||||||
"timeStamping": "Trusted Timestamping",
|
"timeStamping": "Trusted Timestamping",
|
||||||
"1.3.6.1.4.1.311.2.1.21": "Microsoft Individual Code Signing", // msCodeInd
|
"1.3.6.1.4.1.311.2.1.21": "Microsoft Individual Code Signing",
|
||||||
"1.3.6.1.4.1.311.2.1.22": "Microsoft Commercial Code Signing", // msCodeCom
|
"1.3.6.1.4.1.311.2.1.22": "Microsoft Commercial Code Signing",
|
||||||
"1.3.6.1.4.1.311.10.3.1": "Microsoft Trust List Signing", // msCTLSign
|
"1.3.6.1.4.1.311.10.3.1": "Microsoft Trust List Signing",
|
||||||
"1.3.6.1.4.1.311.10.3.3": "Microsoft Server Gated Crypto", // msSGC
|
"1.3.6.1.4.1.311.10.3.3": "Microsoft Server Gated Crypto",
|
||||||
"1.3.6.1.4.1.311.10.3.4": "Microsoft Encrypted File System", // msEFS
|
"1.3.6.1.4.1.311.10.3.4": "Microsoft Encrypted File System",
|
||||||
"1.3.6.1.4.1.311.20.2.2": "Microsoft Smartcard Login", // msSmartcardLogin
|
"1.3.6.1.4.1.311.20.2.2": "Microsoft Smartcard Login",
|
||||||
"2.16.840.1.113730.4.1": "Netscape Server Gated Crypto", // nsSGC
|
"2.16.840.1.113730.4.1": "Netscape Server Gated Crypto",
|
||||||
};
|
};
|
||||||
|
const out = usages.map(eku => ekuIdentifierToName[eku] || eku);
|
||||||
if (Object.hasOwn(extension, "array")) {
|
if (out.length === 0) out.push("(none)");
|
||||||
extension.array.forEach((eku) => {
|
return out;
|
||||||
if (Object.hasOwn(ekuIdentifierToName, eku)) {
|
|
||||||
usage.push(ekuIdentifierToName[eku]);
|
|
||||||
} else {
|
|
||||||
usage.push(eku);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (usage.length === 0) usage.push("(none)");
|
|
||||||
|
|
||||||
return usage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format Subject Alternative Names from the name `subjectAltName` extension
|
|
||||||
* @see RFC 5280 4.2.1.6. Subject Alternative Name https://www.ietf.org/rfc/rfc5280.txt
|
|
||||||
* @param {*} extension object
|
|
||||||
* @returns Array of strings describing Subject Alternative Name extension
|
|
||||||
*/
|
|
||||||
function describeSubjectAlternativeName(extension) {
|
|
||||||
const names = [];
|
|
||||||
|
|
||||||
if (Object.hasOwn(extension, "extname") && extension.extname === "subjectAltName") {
|
|
||||||
if (Object.hasOwn(extension, "array")) {
|
|
||||||
for (const altName of extension.array) {
|
|
||||||
Object.keys(altName).forEach((key) => {
|
|
||||||
switch (key) {
|
|
||||||
case "rfc822":
|
|
||||||
names.push(`EMAIL: ${altName[key]}`);
|
|
||||||
break;
|
|
||||||
case "dns":
|
|
||||||
names.push(`DNS: ${altName[key]}`);
|
|
||||||
break;
|
|
||||||
case "uri":
|
|
||||||
names.push(`URI: ${altName[key]}`);
|
|
||||||
break;
|
|
||||||
case "ip":
|
|
||||||
names.push(`IP: ${altName[key]}`);
|
|
||||||
break;
|
|
||||||
case "dn":
|
|
||||||
names.push(`DIR: ${altName[key].str}`);
|
|
||||||
break;
|
|
||||||
case "other" :
|
|
||||||
names.push(`Other: ${altName[key].oid}::${altName[key].value.utf8str.str}`);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
names.push(`(unable to format SAN '${key}':${altName[key]})\n`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return names;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Join an array of strings and add leading spaces to each line.
|
* Join an array of strings and add leading spaces to each line.
|
||||||
* @param {*} n How many leading spaces
|
*
|
||||||
* @param {*} parts Array of strings
|
* @param {number} n
|
||||||
* @returns Joined and indented string.
|
* @param {string[]} parts
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function indent(n, parts) {
|
function indent(n, parts) {
|
||||||
const fluff = " ".repeat(n);
|
const fluff = " ".repeat(n);
|
||||||
@ -379,12 +301,13 @@ function indent(n, parts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove last character from a string.
|
* Remove the last character from a string.
|
||||||
* @param {*} s String
|
*
|
||||||
* @returns Chopped string.
|
* @param {string} s
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function chop(s) {
|
function chop(s) {
|
||||||
return s.substring(0, s.length - 1);
|
return s.length === 0 ? s : s.substring(0, s.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ParseCSR;
|
export default ParseCSR;
|
||||||
|
|||||||
@ -4,13 +4,50 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
import { X509Crl } from "@peculiar/x509";
|
||||||
|
import { AsnParser } from "@peculiar/asn1-schema";
|
||||||
|
import {
|
||||||
|
AuthorityKeyIdentifier, CertificateList, CRLDistributionPoints, CRLNumber, CRLReason,
|
||||||
|
InvalidityDate, IssueAlternativeName,
|
||||||
|
} from "@peculiar/asn1-x509";
|
||||||
|
import * as asn1X509 from "@peculiar/asn1-x509";
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import { fromBase64 } from "../lib/Base64.mjs";
|
|
||||||
import { toHex } from "../lib/Hex.mjs";
|
|
||||||
import { formatDnObj } from "../lib/PublicKey.mjs";
|
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import Utils from "../Utils.mjs";
|
import { formatDnObj } from "../lib/PublicKey.mjs";
|
||||||
|
import {
|
||||||
|
bytesToHex,
|
||||||
|
decodeX509Input,
|
||||||
|
formatGeneralName,
|
||||||
|
sigAlgOidToName,
|
||||||
|
} from "../lib/X509.mjs";
|
||||||
|
|
||||||
|
const ID_CE_AUTHORITY_KEY_IDENTIFIER = asn1X509.id_ce_authorityKeyIdentifier;
|
||||||
|
const ID_CE_CRL_DISTRIBUTION_POINTS = asn1X509.id_ce_cRLDistributionPoints;
|
||||||
|
const ID_CE_CRL_NUMBER = asn1X509.id_ce_cRLNumber;
|
||||||
|
const ID_CE_CRL_REASONS = asn1X509.id_ce_cRLReasons;
|
||||||
|
const ID_CE_INVALIDITY_DATE = asn1X509.id_ce_invalidityDate;
|
||||||
|
const ID_CE_ISSUER_ALT_NAME = asn1X509.id_ce_issuerAltName;
|
||||||
|
|
||||||
|
const HOLD_INSTRUCTION_EXT_OID = "2.5.29.23";
|
||||||
|
|
||||||
|
const CRL_REASON_TO_NAME = {
|
||||||
|
0: "Unspecified",
|
||||||
|
1: "Key Compromise",
|
||||||
|
2: "CA Compromise",
|
||||||
|
3: "Affiliation Changed",
|
||||||
|
4: "Superseded",
|
||||||
|
5: "Cessation Of Operation",
|
||||||
|
6: "Certificate Hold",
|
||||||
|
8: "Remove From CRL",
|
||||||
|
9: "Privilege Withdrawn",
|
||||||
|
10: "AA Compromise",
|
||||||
|
};
|
||||||
|
|
||||||
|
const HOLD_INSTRUCTION_OID_TO_NAME = {
|
||||||
|
"1.2.840.10040.2.1": "Hold Instruction None",
|
||||||
|
"1.2.840.10040.2.2": "Hold Instruction Call Issuer",
|
||||||
|
"1.2.840.10040.2.3": "Hold Instruction Reject",
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse X.509 CRL operation
|
* Parse X.509 CRL operation
|
||||||
@ -48,344 +85,311 @@ class ParseX509CRL extends Operation {
|
|||||||
/**
|
/**
|
||||||
* @param {string} input
|
* @param {string} input
|
||||||
* @param {Object[]} args
|
* @param {Object[]} args
|
||||||
* @returns {string} Human-readable description of a Certificate Revocation List (CRL).
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
if (!input.length) {
|
if (!input.length) return "No input";
|
||||||
return "No input";
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputFormat = args[0];
|
const inputFormat = args[0];
|
||||||
|
let derBytes;
|
||||||
let undefinedInputFormat = false;
|
|
||||||
try {
|
try {
|
||||||
switch (inputFormat) {
|
derBytes = decodeX509Input(input, inputFormat);
|
||||||
case "DER Hex":
|
|
||||||
input = input.replace(/\s/g, "").toLowerCase();
|
|
||||||
break;
|
|
||||||
case "PEM":
|
|
||||||
break;
|
|
||||||
case "Base64":
|
|
||||||
input = toHex(fromBase64(input, null, "byteArray"), "");
|
|
||||||
break;
|
|
||||||
case "Raw":
|
|
||||||
input = toHex(Utils.strToArrayBuffer(input), "");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
undefinedInputFormat = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw "Certificate load error (non-certificate input?)";
|
throw new OperationError(`Certificate load error (non-certificate input?): ${e.message}`);
|
||||||
}
|
}
|
||||||
if (undefinedInputFormat) throw "Undefined input format";
|
|
||||||
|
|
||||||
const crl = new r.X509CRL(input);
|
let crl;
|
||||||
|
try {
|
||||||
|
crl = new X509Crl(derBytes);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Certificate load error (non-certificate input?): ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const asnCrl = AsnParser.parse(new Uint8Array(crl.rawData), CertificateList);
|
||||||
|
const sigAlgName = sigAlgOidToName(asnCrl.signatureAlgorithm.algorithm);
|
||||||
|
|
||||||
let out = `Certificate Revocation List (CRL):
|
let out = `Certificate Revocation List (CRL):
|
||||||
Version: ${crl.getVersion() === null ? "1 (0x0)" : "2 (0x1)"}
|
Version: ${crl.version === undefined || crl.version === 0 ? "1 (0x0)" : "2 (0x1)"}
|
||||||
Signature Algorithm: ${crl.getSignatureAlgorithmField()}
|
Signature Algorithm: ${sigAlgName}
|
||||||
Issuer:\n${formatDnObj(crl.getIssuer(), 8)}
|
Issuer:\n${formatDnObj(crl.issuerName.toJSON(), 8)}
|
||||||
Last Update: ${generalizedDateTimeToUTC(crl.getThisUpdate())}
|
Last Update: ${crl.thisUpdate.toUTCString()}
|
||||||
Next Update: ${generalizedDateTimeToUTC(crl.getNextUpdate())}\n`;
|
Next Update: ${crl.nextUpdate ? crl.nextUpdate.toUTCString() : "undefined"}\n`;
|
||||||
|
|
||||||
if (crl.getParam().ext !== undefined) {
|
if (crl.extensions && crl.extensions.length > 0) {
|
||||||
out += `\tCRL extensions:\n${formatCRLExtensions(crl.getParam().ext, 8)}\n`;
|
out += `\tCRL extensions:\n${formatCRLExtensions(crl.extensions, 8)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
out += `Revoked Certificates:\n${formatRevokedCertificates(crl.getRevCertArray(), 4)}
|
out += `Revoked Certificates:\n${formatRevokedCertificates(crl.entries, 4)}
|
||||||
Signature Value:\n${formatCRLSignature(crl.getSignatureValueHex(), 8)}`;
|
Signature Value:\n${formatCRLSignature(bytesToHex(new Uint8Array(crl.signature)), 8)}`;
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generalized date time string to UTC.
|
* Format the CRL extensions block.
|
||||||
* @param {string} datetime
|
*
|
||||||
* @returns UTC datetime string.
|
* Extensions are emitted in OID-ascending order to match the legacy output:
|
||||||
|
* the old code sorted by `extname` string, but the asn1-x509 OID constants
|
||||||
|
* we get back from peculiar/x509 sort identically for the supported types.
|
||||||
|
*
|
||||||
|
* Unsupported extensions are listed as `<oid>:` followed by an
|
||||||
|
* "Unsupported CRL extension. Try openssl CLI." line — same as before.
|
||||||
|
*
|
||||||
|
* @param {object[]} extensions - peculiar/x509 Extension objects.
|
||||||
|
* @param {number} indentSpaces
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function generalizedDateTimeToUTC(datetime) {
|
function formatCRLExtensions(extensions, indentSpaces) {
|
||||||
// Ensure the string is in the correct format
|
if (!Array.isArray(extensions) || extensions.length === 0) {
|
||||||
if (!/^\d{12,14}Z$/.test(datetime)) {
|
return indentString("No CRL extensions.", indentSpaces);
|
||||||
throw new OperationError(`failed to format datetime string ${datetime}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract components
|
// Sort to match the legacy alphabetical-by-extname ordering as closely
|
||||||
let centuary = "20";
|
// as possible. Use a synthetic name per OID.
|
||||||
if (datetime.length === 15) {
|
const sorted = [...extensions].sort((a, b) => {
|
||||||
centuary = datetime.substring(0, 2);
|
const an = extDisplayName(a.type);
|
||||||
datetime = datetime.slice(2);
|
const bn = extDisplayName(b.type);
|
||||||
|
if (an < bn) return -1;
|
||||||
|
if (an > bn) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
let out = "";
|
||||||
|
for (const ext of sorted) {
|
||||||
|
out += formatCRLExtension(ext) + "\n";
|
||||||
}
|
}
|
||||||
const year = centuary + datetime.substring(0, 2);
|
|
||||||
const month = datetime.substring(2, 4);
|
|
||||||
const day = datetime.substring(4, 6);
|
|
||||||
const hour = datetime.substring(6, 8);
|
|
||||||
const minute = datetime.substring(8, 10);
|
|
||||||
const second = datetime.substring(10, 12);
|
|
||||||
|
|
||||||
// Construct ISO 8601 format string
|
return indentString(chop(out), indentSpaces);
|
||||||
const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}Z`;
|
|
||||||
|
|
||||||
// Parse using standard Date object
|
|
||||||
const isoDateTime = new Date(isoString);
|
|
||||||
|
|
||||||
return isoDateTime.toUTCString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format CRL extensions.
|
* Pick a display name for an extension OID (used only for sort stability).
|
||||||
* @param {r.ExtParam[] | undefined} extensions
|
*
|
||||||
* @param {Number} indent
|
* @param {string} oid
|
||||||
* @returns Formatted string detailing CRL extensions.
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatCRLExtensions(extensions, indent) {
|
function extDisplayName(oid) {
|
||||||
if (Array.isArray(extensions) === false || extensions.length === 0) {
|
switch (oid) {
|
||||||
return indentString(`No CRL extensions.`, indent);
|
case ID_CE_AUTHORITY_KEY_IDENTIFIER: return "authorityKeyIdentifier";
|
||||||
|
case ID_CE_CRL_DISTRIBUTION_POINTS: return "cRLDistributionPoints";
|
||||||
|
case ID_CE_CRL_NUMBER: return "cRLNumber";
|
||||||
|
case ID_CE_ISSUER_ALT_NAME: return "issuerAltName";
|
||||||
|
default: return oid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let out = ``;
|
/**
|
||||||
|
* Format a single CRL extension.
|
||||||
|
*
|
||||||
|
* @param {object} ext - peculiar/x509 Extension
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatCRLExtension(ext) {
|
||||||
|
const value = new Uint8Array(ext.value);
|
||||||
|
|
||||||
extensions.sort((a, b) => {
|
if (ext.type === ID_CE_AUTHORITY_KEY_IDENTIFIER) {
|
||||||
if (!Object.hasOwn(a, "extname") || !Object.hasOwn(b, "extname")) {
|
let out = `X509v3 Authority Key Identifier:\n`;
|
||||||
return 0;
|
const aki = AsnParser.parse(value, AuthorityKeyIdentifier);
|
||||||
|
if (aki.keyIdentifier) {
|
||||||
|
out += `\tkeyid:${colonHex(bytesToHex(new Uint8Array(aki.keyIdentifier.buffer))).toUpperCase()}\n`;
|
||||||
}
|
}
|
||||||
if (a.extname < b.extname) {
|
if (aki.authorityCertIssuer && aki.authorityCertIssuer.length > 0) {
|
||||||
return -1;
|
for (const gn of aki.authorityCertIssuer) {
|
||||||
} else if (a.extname === b.extname) {
|
if (gn.directoryName) {
|
||||||
return 0;
|
out += `\tDirName:${slashName(gn.directoryName)}\n`;
|
||||||
} else {
|
} else {
|
||||||
return 1;
|
out += `\t${formatGeneralName(gn, "crl")}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aki.authorityCertSerialNumber) {
|
||||||
|
const serial = bytesToHex(new Uint8Array(aki.authorityCertSerialNumber)).toUpperCase();
|
||||||
|
out += `\tserial:${colonHex(serial)}\n`;
|
||||||
|
}
|
||||||
|
return chop(out);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
extensions.forEach((ext) => {
|
if (ext.type === ID_CE_CRL_DISTRIBUTION_POINTS) {
|
||||||
if (!Object.hasOwn(ext, "extname")) {
|
const dps = AsnParser.parse(value, CRLDistributionPoints);
|
||||||
throw new OperationError(`CRL entry extension object missing 'extname' key: ${ext}`);
|
let out = `X509v3 CRL Distribution Points:\n`;
|
||||||
}
|
for (const dp of dps) {
|
||||||
switch (ext.extname) {
|
if (dp.distributionPoint && dp.distributionPoint.fullName) {
|
||||||
case "authorityKeyIdentifier":
|
const fullName = `Full Name:\n${dp.distributionPoint.fullName.map(gn => ` ${formatGeneralName(gn, "crl")}`).join("\n")}`;
|
||||||
out += `X509v3 Authority Key Identifier:\n`;
|
|
||||||
if (Object.hasOwn(ext, "kid")) {
|
|
||||||
out += `\tkeyid:${colonDelimitedHexFormatString(ext.kid.hex.toUpperCase())}\n`;
|
|
||||||
}
|
|
||||||
if (Object.hasOwn(ext, "issuer")) {
|
|
||||||
out += `\tDirName:${ext.issuer.str}\n`;
|
|
||||||
}
|
|
||||||
if (Object.hasOwn(ext, "sn")) {
|
|
||||||
out += `\tserial:${colonDelimitedHexFormatString(ext.sn.hex.toUpperCase())}\n`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "cRLDistributionPoints":
|
|
||||||
out += `X509v3 CRL Distribution Points:\n`;
|
|
||||||
ext.array.forEach((distPoint) => {
|
|
||||||
const fullName = `Full Name:\n${formatGeneralNames(distPoint.dpname.full, 4)}`;
|
|
||||||
out += indentString(fullName, 4) + "\n";
|
out += indentString(fullName, 4) + "\n";
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "cRLNumber":
|
|
||||||
if (!Object.hasOwn(ext, "num")) {
|
|
||||||
throw new OperationError(`'cRLNumber' CRL entry extension missing 'num' key: ${ext}`);
|
|
||||||
}
|
}
|
||||||
out += `X509v3 CRL Number:\n\t${ext.num.hex.toUpperCase()}\n`;
|
|
||||||
break;
|
|
||||||
case "issuerAltName":
|
|
||||||
out += `X509v3 Issuer Alternative Name:\n${formatGeneralNames(ext.array, 4)}\n`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
out += `${ext.extname}:\n`;
|
|
||||||
out += `\tUnsupported CRL extension. Try openssl CLI.\n`;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
return chop(out);
|
||||||
|
}
|
||||||
|
|
||||||
return indentString(chop(out), indent);
|
if (ext.type === ID_CE_CRL_NUMBER) {
|
||||||
|
const num = AsnParser.parse(value, CRLNumber);
|
||||||
|
const hex = BigInt(num.value).toString(16).toUpperCase();
|
||||||
|
return `X509v3 CRL Number:\n\t${hex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext.type === ID_CE_ISSUER_ALT_NAME) {
|
||||||
|
const ian = AsnParser.parse(value, IssueAlternativeName);
|
||||||
|
const lines = ian.map(gn => ` ${formatGeneralName(gn, "crl")}`).join("\n");
|
||||||
|
return `X509v3 Issuer Alternative Name:\n${lines}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${ext.type}:\n\tUnsupported CRL extension. Try openssl CLI.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format general names array.
|
* Format an asn1-x509 Name as the OpenSSL slash representation
|
||||||
* @param {Object[]} names
|
* `/C=…/ST=…/…`.
|
||||||
* @returns Multi-line formatted string describing all supported general name types.
|
*
|
||||||
|
* @param {object} asnName
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatGeneralNames(names, indent) {
|
function slashName(asnName) {
|
||||||
let out = ``;
|
const OID_SHORT = {
|
||||||
|
"2.5.4.3": "CN", "2.5.4.4": "SN", "2.5.4.5": "serialNumber",
|
||||||
names.forEach((name) => {
|
"2.5.4.6": "C", "2.5.4.7": "L", "2.5.4.8": "ST",
|
||||||
const key = Object.keys(name)[0];
|
"2.5.4.9": "street", "2.5.4.10": "O", "2.5.4.11": "OU",
|
||||||
|
"2.5.4.12": "T", "2.5.4.42": "G", "2.5.4.43": "I",
|
||||||
switch (key) {
|
"1.2.840.113549.1.9.1": "E",
|
||||||
case "ip":
|
|
||||||
out += `IP:${name.ip}\n`;
|
|
||||||
break;
|
|
||||||
case "dns":
|
|
||||||
out += `DNS:${name.dns}\n`;
|
|
||||||
break;
|
|
||||||
case "uri":
|
|
||||||
out += `URI:${name.uri}\n`;
|
|
||||||
break;
|
|
||||||
case "rfc822":
|
|
||||||
out += `EMAIL:${name.rfc822}\n`;
|
|
||||||
break;
|
|
||||||
case "dn":
|
|
||||||
out += `DIR:${name.dn.str}\n`;
|
|
||||||
break;
|
|
||||||
case "other":
|
|
||||||
out += `OtherName:${name.other.oid}::${Object.values(name.other.value)[0].str}\n`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
out += `${key}: unsupported general name type`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return indentString(chop(out), indent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Colon-delimited hex formatted output.
|
|
||||||
* @param {string} hexString Hex String
|
|
||||||
* @returns String representing input hex string with colon delimiter.
|
|
||||||
*/
|
|
||||||
function colonDelimitedHexFormatString(hexString) {
|
|
||||||
if (hexString.length % 2 !== 0) {
|
|
||||||
hexString = "0" + hexString;
|
|
||||||
}
|
|
||||||
|
|
||||||
return chop(hexString.replace(/(..)/g, "$&:"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format revoked certificates array
|
|
||||||
* @param {r.RevokedCertificate[] | null} revokedCertificates
|
|
||||||
* @param {Number} indent
|
|
||||||
* @returns Multi-line formatted string output of revoked certificates array
|
|
||||||
*/
|
|
||||||
function formatRevokedCertificates(revokedCertificates, indent) {
|
|
||||||
if (Array.isArray(revokedCertificates) === false || revokedCertificates.length === 0) {
|
|
||||||
return indentString("No Revoked Certificates.", indent);
|
|
||||||
}
|
|
||||||
|
|
||||||
let out=``;
|
|
||||||
|
|
||||||
revokedCertificates.forEach((revCert) => {
|
|
||||||
if (!Object.hasOwn(revCert, "sn") || !Object.hasOwn(revCert, "date")) {
|
|
||||||
throw new OperationError("invalid revoked certificate object, missing either serial number or date");
|
|
||||||
}
|
|
||||||
|
|
||||||
out += `Serial Number: ${revCert.sn.hex.toUpperCase()}
|
|
||||||
Revocation Date: ${generalizedDateTimeToUTC(revCert.date)}\n`;
|
|
||||||
if (Object.hasOwn(revCert, "ext") && Array.isArray(revCert.ext) && revCert.ext.length !== 0) {
|
|
||||||
out += `\tCRL entry extensions:\n${indentString(formatCRLEntryExtensions(revCert.ext), 2*indent)}\n`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return indentString(chop(out), indent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format CRL entry extensions.
|
|
||||||
* @param {Object[]} exts
|
|
||||||
* @returns Formatted multi-line string describing CRL entry extensions.
|
|
||||||
*/
|
|
||||||
function formatCRLEntryExtensions(exts) {
|
|
||||||
let out = ``;
|
|
||||||
|
|
||||||
const crlReasonCodeToReasonMessage = {
|
|
||||||
0: "Unspecified",
|
|
||||||
1: "Key Compromise",
|
|
||||||
2: "CA Compromise",
|
|
||||||
3: "Affiliation Changed",
|
|
||||||
4: "Superseded",
|
|
||||||
5: "Cessation Of Operation",
|
|
||||||
6: "Certificate Hold",
|
|
||||||
8: "Remove From CRL",
|
|
||||||
9: "Privilege Withdrawn",
|
|
||||||
10: "AA Compromise",
|
|
||||||
};
|
};
|
||||||
|
let out = "";
|
||||||
const holdInstructionOIDToName = {
|
for (const rdn of asnName) {
|
||||||
"1.2.840.10040.2.1": "Hold Instruction None",
|
for (const atv of rdn) {
|
||||||
"1.2.840.10040.2.2": "Hold Instruction Call Issuer",
|
const key = OID_SHORT[atv.type] || atv.type;
|
||||||
"1.2.840.10040.2.3": "Hold Instruction Reject",
|
out += `/${key}=${atv.value.toString()}`;
|
||||||
};
|
|
||||||
|
|
||||||
exts.forEach((ext) => {
|
|
||||||
if (!Object.hasOwn(ext, "extname")) {
|
|
||||||
throw new OperationError(`CRL entry extension object missing 'extname' key: ${ext}`);
|
|
||||||
}
|
}
|
||||||
switch (ext.extname) {
|
|
||||||
case "cRLReason":
|
|
||||||
if (!Object.hasOwn(ext, "code")) {
|
|
||||||
throw new OperationError(`'cRLReason' CRL entry extension missing 'code' key: ${ext}`);
|
|
||||||
}
|
}
|
||||||
out += `X509v3 CRL Reason Code:
|
return out;
|
||||||
${Object.hasOwn(crlReasonCodeToReasonMessage, ext.code) ? crlReasonCodeToReasonMessage[ext.code] : `invalid reason code: ${ext.code}`}\n`;
|
|
||||||
break;
|
|
||||||
case "2.5.29.23": // Hold instruction
|
|
||||||
out += `Hold Instruction Code:\n\t${Object.hasOwn(holdInstructionOIDToName, ext.extn.oid) ? holdInstructionOIDToName[ext.extn.oid] : `${ext.extn.oid}: unknown hold instruction OID`}\n`;
|
|
||||||
break;
|
|
||||||
case "2.5.29.24": // Invalidity Date
|
|
||||||
out += `Invalidity Date:\n\t${generalizedDateTimeToUTC(ext.extn.gentime.str)}\n`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
out += `${ext.extname}:\n`;
|
|
||||||
out += `\tUnsupported CRL entry extension. Try openssl CLI.\n`;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an array of bytes as `AA:BB:CC:...`.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function colonHex(hex) {
|
||||||
|
if (hex.length % 2 !== 0) hex = "0" + hex;
|
||||||
|
return chop(hex.replace(/(..)/g, "$&:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the revoked certificates list.
|
||||||
|
*
|
||||||
|
* @param {readonly object[]} entries - peculiar/x509 X509CrlEntry objects.
|
||||||
|
* @param {number} indentSpaces
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatRevokedCertificates(entries, indentSpaces) {
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
return indentString("No Revoked Certificates.", indentSpaces);
|
||||||
|
}
|
||||||
|
let out = "";
|
||||||
|
for (const entry of entries) {
|
||||||
|
out += `Serial Number: ${entry.serialNumber.toUpperCase()}
|
||||||
|
Revocation Date: ${entry.revocationDate.toUTCString()}\n`;
|
||||||
|
if (entry.extensions && entry.extensions.length > 0) {
|
||||||
|
out += `\tCRL entry extensions:\n${indentString(formatCRLEntryExtensions(entry.extensions), 2 * indentSpaces)}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indentString(chop(out), indentSpaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the CRL entry extensions for a single revoked-certificate row.
|
||||||
|
*
|
||||||
|
* @param {object[]} extensions
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatCRLEntryExtensions(extensions) {
|
||||||
|
let out = "";
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const value = new Uint8Array(ext.value);
|
||||||
|
if (ext.type === ID_CE_CRL_REASONS) {
|
||||||
|
const reason = AsnParser.parse(value, CRLReason);
|
||||||
|
const code = reason.reason;
|
||||||
|
const name = Object.prototype.hasOwnProperty.call(CRL_REASON_TO_NAME, code) ?
|
||||||
|
CRL_REASON_TO_NAME[code] :
|
||||||
|
`invalid reason code: ${code}`;
|
||||||
|
out += `X509v3 CRL Reason Code:\n ${name}\n`;
|
||||||
|
} else if (ext.type === HOLD_INSTRUCTION_EXT_OID) {
|
||||||
|
// Hold Instruction; payload is an OID
|
||||||
|
const oid = decodeOidValue(value);
|
||||||
|
const name = HOLD_INSTRUCTION_OID_TO_NAME[oid] || `${oid}: unknown hold instruction OID`;
|
||||||
|
out += `Hold Instruction Code:\n\t${name}\n`;
|
||||||
|
} else if (ext.type === ID_CE_INVALIDITY_DATE) {
|
||||||
|
const inv = AsnParser.parse(value, InvalidityDate);
|
||||||
|
out += `Invalidity Date:\n\t${inv.value.toUTCString()}\n`;
|
||||||
|
} else {
|
||||||
|
out += `${ext.type}:\n\tUnsupported CRL entry extension. Try openssl CLI.\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
return chop(out);
|
return chop(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format CRL signature.
|
* Decode a DER OBJECT IDENTIFIER from raw bytes (including the 0x06 tag).
|
||||||
* @param {String} sigHex
|
*
|
||||||
* @param {Number} indent
|
* @param {Uint8Array} bytes
|
||||||
* @returns String representing hex signature value formatted on multiple lines.
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatCRLSignature(sigHex, indent) {
|
function decodeOidValue(bytes) {
|
||||||
if (sigHex.length % 2 !== 0) {
|
if (bytes[0] !== 0x06) throw new OperationError("Expected OBJECT IDENTIFIER");
|
||||||
sigHex = "0" + sigHex;
|
// Length octet(s) follow tag; for any sensible OID this is one byte.
|
||||||
|
let i = 1;
|
||||||
|
if (bytes[i] >= 0x80) i += (bytes[i] & 0x7f);
|
||||||
|
i += 1;
|
||||||
|
const first = bytes[i++];
|
||||||
|
const arcs = [Math.floor(first / 40).toString(), (first % 40).toString()];
|
||||||
|
let acc = 0n;
|
||||||
|
for (; i < bytes.length; i++) {
|
||||||
|
acc = (acc << 7n) | BigInt(bytes[i] & 0x7f);
|
||||||
|
if ((bytes[i] & 0x80) === 0) {
|
||||||
|
arcs.push(acc.toString());
|
||||||
|
acc = 0n;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return indentString(formatMultiLine(chop(sigHex.replace(/(..)/g, "$&:"))), indent);
|
return arcs.join(".");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format string onto multiple lines.
|
* Format the CRL signature as colon-delimited byte pairs wrapped to 54
|
||||||
* @param {string} longStr
|
* characters per line, indented.
|
||||||
* @returns String as a multi-line string.
|
*
|
||||||
|
* @param {string} sigHex
|
||||||
|
* @param {number} indentSpaces
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatMultiLine(longStr) {
|
function formatCRLSignature(sigHex, indentSpaces) {
|
||||||
|
if (sigHex.length % 2 !== 0) sigHex = "0" + sigHex;
|
||||||
|
const colonHexStr = chop(sigHex.replace(/(..)/g, "$&:"));
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
for (let remain = colonHexStr; remain !== ""; remain = remain.substring(54)) {
|
||||||
for (let remain = longStr ; remain !== "" ; remain = remain.substring(54)) {
|
|
||||||
lines.push(remain.substring(0, 54));
|
lines.push(remain.substring(0, 54));
|
||||||
}
|
}
|
||||||
|
return indentString(lines.join("\n"), indentSpaces);
|
||||||
return lines.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indent a multi-line string by n spaces.
|
* Indent a multi-line string by n spaces.
|
||||||
* @param {string} input String
|
*
|
||||||
* @param {number} spaces How many leading spaces
|
* @param {string} input
|
||||||
* @returns Indented string.
|
* @param {number} spaces
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function indentString(input, spaces) {
|
function indentString(input, spaces) {
|
||||||
const indent = " ".repeat(spaces);
|
const pad = " ".repeat(spaces);
|
||||||
return input.replace(/^/gm, indent);
|
return input.replace(/^/gm, pad);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove last character from a string.
|
* Remove the last character from a string.
|
||||||
* @param {string} s String
|
*
|
||||||
* @returns Chopped string.
|
* @param {string} s
|
||||||
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function chop(s) {
|
function chop(s) {
|
||||||
if (s.length < 1) {
|
return s.length === 0 ? s : s.substring(0, s.length - 1);
|
||||||
return s;
|
|
||||||
}
|
|
||||||
return s.substring(0, s.length - 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ParseX509CRL;
|
export default ParseX509CRL;
|
||||||
|
|||||||
@ -4,12 +4,22 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
import { X509Certificate, KeyUsagesExtension, BasicConstraintsExtension, ExtendedKeyUsageExtension, SubjectAlternativeNameExtension, SubjectKeyIdentifierExtension, AuthorityKeyIdentifierExtension, CRLDistributionPointsExtension } from "@peculiar/x509";
|
||||||
import { fromBase64 } from "../lib/Base64.mjs";
|
import { AsnParser } from "@peculiar/asn1-schema";
|
||||||
|
import { Certificate, SubjectAlternativeName, IssueAlternativeName } from "@peculiar/asn1-x509";
|
||||||
import { runHash } from "../lib/Hash.mjs";
|
import { runHash } from "../lib/Hash.mjs";
|
||||||
import { fromHex, toHex } from "../lib/Hex.mjs";
|
|
||||||
import { formatByteStr, formatDnObj } from "../lib/PublicKey.mjs";
|
import { formatByteStr, formatDnObj } from "../lib/PublicKey.mjs";
|
||||||
|
import {
|
||||||
|
bytesToHex,
|
||||||
|
decodeX509Input,
|
||||||
|
describeSpki,
|
||||||
|
formatGeneralName,
|
||||||
|
isDerEcdsaSignature,
|
||||||
|
parseDerEcdsaSignature,
|
||||||
|
sigAlgOidToName,
|
||||||
|
} from "../lib/X509.mjs";
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
import Utils from "../Utils.mjs";
|
import Utils from "../Utils.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -51,162 +61,195 @@ class ParseX509Certificate extends Operation {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
run(input, args) {
|
run(input, args) {
|
||||||
if (!input.length) {
|
if (!input.length) return "No input";
|
||||||
return "No input";
|
|
||||||
|
const inputFormat = args[0];
|
||||||
|
let derBytes;
|
||||||
|
try {
|
||||||
|
derBytes = decodeX509Input(input, inputFormat);
|
||||||
|
} catch (e) {
|
||||||
|
throw new OperationError(`Certificate load error (non-certificate input?): ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cert = new r.X509(),
|
let cert;
|
||||||
inputFormat = args[0];
|
|
||||||
|
|
||||||
let undefinedInputFormat = false;
|
|
||||||
try {
|
try {
|
||||||
switch (inputFormat) {
|
cert = new X509Certificate(derBytes);
|
||||||
case "DER Hex":
|
} catch (e) {
|
||||||
input = input.replace(/\s/g, "").toLowerCase();
|
throw new OperationError(`Certificate load error (non-certificate input?): ${e.message}`);
|
||||||
cert.readCertHex(input);
|
}
|
||||||
|
|
||||||
|
const fingerprintInput = Utils.strToArrayBuffer(Utils.byteArrayToChars(Array.from(new Uint8Array(cert.rawData))));
|
||||||
|
|
||||||
|
const asnCert = AsnParser.parse(new Uint8Array(cert.rawData), Certificate);
|
||||||
|
const versionInt = (asnCert.tbsCertificate.version || 0) + 1;
|
||||||
|
const serialHex = bytesToHex(new Uint8Array(asnCert.tbsCertificate.serialNumber));
|
||||||
|
const serialDecimal = serialHex.length ? BigInt("0x" + serialHex).toString() : "0";
|
||||||
|
const sigAlgOid = asnCert.signatureAlgorithm.algorithm;
|
||||||
|
const sigAlgName = sigAlgOidToName(sigAlgOid);
|
||||||
|
|
||||||
|
const spki = describeSpki(new Uint8Array(cert.publicKey.rawData));
|
||||||
|
const pkFields = [{ key: "Algorithm", value: spkiAlgorithmLabel(spki) }];
|
||||||
|
|
||||||
|
switch (spki.type) {
|
||||||
|
case "EC":
|
||||||
|
pkFields.push({ key: "Curve Name", value: spki.asn1Curve });
|
||||||
|
pkFields.push({ key: "Length", value: spki.bitLength + " bits" });
|
||||||
|
pkFields.push({ key: "pub", value: formatByteStr(spki.pubKeyHex, 16, 18) });
|
||||||
break;
|
break;
|
||||||
case "PEM":
|
case "DSA":
|
||||||
cert.readCertPEM(input);
|
pkFields.push({ key: "pub", value: formatByteStr(spki.yHex, 16, 18) });
|
||||||
|
pkFields.push({ key: "P", value: formatByteStr(spki.pHex, 16, 18) });
|
||||||
|
pkFields.push({ key: "Q", value: formatByteStr(spki.qHex, 16, 18) });
|
||||||
|
pkFields.push({ key: "G", value: formatByteStr(spki.gHex, 16, 18) });
|
||||||
break;
|
break;
|
||||||
case "Base64":
|
case "RSA":
|
||||||
cert.readCertHex(toHex(fromBase64(input, null, "byteArray"), ""));
|
pkFields.push({ key: "Length", value: spki.bitLength + " bits" });
|
||||||
|
pkFields.push({ key: "Modulus", value: formatByteStr(spki.nHex, 16, 18) });
|
||||||
|
pkFields.push({ key: "Exponent", value: spki.eValue + " (0x" + spki.eValue.toString(16) + ")" });
|
||||||
break;
|
break;
|
||||||
case "Raw":
|
case "EdDSA":
|
||||||
cert.readCertHex(toHex(Utils.strToArrayBuffer(input), ""));
|
pkFields.push({ key: "Curve Name", value: spki.curveName });
|
||||||
|
pkFields.push({ key: "pub", value: formatByteStr(spki.pubKeyHex, 16, 18) });
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
undefinedInputFormat = true;
|
pkFields.push({ key: "Error", value: "Unknown Public Key type" });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
throw "Certificate load error (non-certificate input?)";
|
let pkStr = "";
|
||||||
|
for (const field of pkFields) {
|
||||||
|
pkStr += ` ${field.key}:${(field.value + "\n").padStart(
|
||||||
|
18 - (field.key.length + 3) + field.value.length + 1, " ")}`;
|
||||||
}
|
}
|
||||||
if (undefinedInputFormat) throw "Undefined input format";
|
|
||||||
|
|
||||||
const hex = Utils.strToArrayBuffer(Utils.byteArrayToChars(fromHex(cert.hex))),
|
const sigHex = bytesToHex(new Uint8Array(cert.signature));
|
||||||
sn = cert.getSerialNumberHex(),
|
let sigStr;
|
||||||
issuer = cert.getIssuer(),
|
if (isDerEcdsaSignature(sigHex)) {
|
||||||
subject = cert.getSubject(),
|
const { r, s } = parseDerEcdsaSignature(sigHex);
|
||||||
pk = cert.getPublicKey(),
|
sigStr = ` r: ${formatByteStr(r, 16, 18)}\n s: ${formatByteStr(s, 16, 18)}`;
|
||||||
pkFields = [],
|
|
||||||
sig = cert.getSignatureValueHex();
|
|
||||||
|
|
||||||
let pkStr = "",
|
|
||||||
sigStr = "",
|
|
||||||
extensions = "";
|
|
||||||
|
|
||||||
// Public Key fields
|
|
||||||
pkFields.push({
|
|
||||||
key: "Algorithm",
|
|
||||||
value: pk.type
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pk.type === "EC") { // ECDSA
|
|
||||||
pkFields.push({
|
|
||||||
key: "Curve Name",
|
|
||||||
value: pk.curveName
|
|
||||||
});
|
|
||||||
pkFields.push({
|
|
||||||
key: "Length",
|
|
||||||
value: (((new r.BigInteger(pk.pubKeyHex, 16)).bitLength()-3) /2) + " bits"
|
|
||||||
});
|
|
||||||
pkFields.push({
|
|
||||||
key: "pub",
|
|
||||||
value: formatByteStr(pk.pubKeyHex, 16, 18)
|
|
||||||
});
|
|
||||||
} else if (pk.type === "DSA") { // DSA
|
|
||||||
pkFields.push({
|
|
||||||
key: "pub",
|
|
||||||
value: formatByteStr(pk.y.toString(16), 16, 18)
|
|
||||||
});
|
|
||||||
pkFields.push({
|
|
||||||
key: "P",
|
|
||||||
value: formatByteStr(pk.p.toString(16), 16, 18)
|
|
||||||
});
|
|
||||||
pkFields.push({
|
|
||||||
key: "Q",
|
|
||||||
value: formatByteStr(pk.q.toString(16), 16, 18)
|
|
||||||
});
|
|
||||||
pkFields.push({
|
|
||||||
key: "G",
|
|
||||||
value: formatByteStr(pk.g.toString(16), 16, 18)
|
|
||||||
});
|
|
||||||
} else if (pk.e) { // RSA
|
|
||||||
pkFields.push({
|
|
||||||
key: "Length",
|
|
||||||
value: pk.n.bitLength() + " bits"
|
|
||||||
});
|
|
||||||
pkFields.push({
|
|
||||||
key: "Modulus",
|
|
||||||
value: formatByteStr(pk.n.toString(16), 16, 18)
|
|
||||||
});
|
|
||||||
pkFields.push({
|
|
||||||
key: "Exponent",
|
|
||||||
value: pk.e + " (0x" + pk.e.toString(16) + ")"
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
pkFields.push({
|
sigStr = ` Signature: ${formatByteStr(sigHex, 16, 18)}`;
|
||||||
key: "Error",
|
|
||||||
value: "Unknown Public Key type"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format Public Key fields
|
const nbDate = formatDate(formatAsn1Date(cert.notBefore));
|
||||||
for (let i = 0; i < pkFields.length; i++) {
|
const naDate = formatDate(formatAsn1Date(cert.notAfter));
|
||||||
pkStr += ` ${pkFields[i].key}:${(pkFields[i].value + "\n").padStart(
|
const issuerStr = formatDnObj(cert.issuerName.toJSON(), 2);
|
||||||
18 - (pkFields[i].key.length + 3) + pkFields[i].value.length + 1,
|
const subjectStr = formatDnObj(cert.subjectName.toJSON(), 2);
|
||||||
" "
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature fields
|
const versionDisplay = `${versionInt} (0x${Utils.hex(versionInt - 1)})`;
|
||||||
let breakoutSig = false;
|
|
||||||
try {
|
|
||||||
breakoutSig = r.ASN1HEX.dump(sig).indexOf("SEQUENCE") === 0;
|
|
||||||
} catch (err) {
|
|
||||||
// Error processing signature, output without further breakout
|
|
||||||
}
|
|
||||||
|
|
||||||
if (breakoutSig) { // DSA or ECDSA
|
const extensionsText = formatExtensions(cert);
|
||||||
sigStr = ` r: ${formatByteStr(r.ASN1HEX.getV(sig, 4), 16, 18)}
|
|
||||||
s: ${formatByteStr(r.ASN1HEX.getV(sig, 48), 16, 18)}`;
|
|
||||||
} else { // RSA or unknown
|
|
||||||
sigStr = ` Signature: ${formatByteStr(sig, 16, 18)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extensions
|
return `Version: ${versionDisplay}
|
||||||
try {
|
Serial number: ${serialDecimal} (0x${serialHex})
|
||||||
extensions = cert.getInfo().split("X509v3 Extensions:\n")[1].split("signature")[0];
|
Algorithm ID: ${sigAlgName}
|
||||||
} catch (err) {}
|
|
||||||
|
|
||||||
const issuerStr = formatDnObj(issuer, 2),
|
|
||||||
nbDate = formatDate(cert.getNotBefore()),
|
|
||||||
naDate = formatDate(cert.getNotAfter()),
|
|
||||||
subjectStr = formatDnObj(subject, 2);
|
|
||||||
|
|
||||||
return `Version: ${cert.version} (0x${Utils.hex(cert.version - 1)})
|
|
||||||
Serial number: ${new r.BigInteger(sn, 16).toString()} (0x${sn})
|
|
||||||
Algorithm ID: ${cert.getSignatureAlgorithmField()}
|
|
||||||
Validity
|
Validity
|
||||||
Not Before: ${nbDate} (dd-mm-yyyy hh:mm:ss) (${cert.getNotBefore()})
|
Not Before: ${nbDate} (dd-mm-yyyy hh:mm:ss) (${formatAsn1Date(cert.notBefore)})
|
||||||
Not After: ${naDate} (dd-mm-yyyy hh:mm:ss) (${cert.getNotAfter()})
|
Not After: ${naDate} (dd-mm-yyyy hh:mm:ss) (${formatAsn1Date(cert.notAfter)})
|
||||||
Issuer
|
Issuer
|
||||||
${issuerStr}
|
${issuerStr}
|
||||||
Subject
|
Subject
|
||||||
${subjectStr}
|
${subjectStr}
|
||||||
Fingerprints
|
Fingerprints
|
||||||
MD5: ${runHash("md5", hex)}
|
MD5: ${runHash("md5", fingerprintInput)}
|
||||||
SHA1: ${runHash("sha1", hex)}
|
SHA1: ${runHash("sha1", fingerprintInput)}
|
||||||
SHA256: ${runHash("sha256", hex)}
|
SHA256: ${runHash("sha256", fingerprintInput)}
|
||||||
Public Key
|
Public Key
|
||||||
${pkStr.slice(0, -1)}
|
${pkStr.slice(0, -1)}
|
||||||
Certificate Signature
|
Certificate Signature
|
||||||
Algorithm: ${cert.getSignatureAlgorithmName()}
|
Algorithm: ${sigAlgName}
|
||||||
${sigStr}
|
${sigStr}
|
||||||
|
|
||||||
Extensions
|
Extensions
|
||||||
${extensions}`;
|
${extensionsText}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the algorithm label for the Public Key block. The golden fixtures
|
||||||
|
* use short labels for the classic key families: "EC", "DSA", "RSA".
|
||||||
|
*
|
||||||
|
* @param {object} spki
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function spkiAlgorithmLabel(spki) {
|
||||||
|
if (spki.type === "EC") return "EC";
|
||||||
|
if (spki.type === "DSA") return "DSA";
|
||||||
|
if (spki.type === "RSA") return "RSA";
|
||||||
|
if (spki.type === "EdDSA") return spki.curveName;
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the certificate extensions block. Uses the rich peculiar/x509
|
||||||
|
* extension types where available, falls back to the OID for unknown ones.
|
||||||
|
*
|
||||||
|
* @param {object} cert
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatExtensions(cert) {
|
||||||
|
const lines = [];
|
||||||
|
for (const ext of cert.extensions) {
|
||||||
|
if (ext instanceof SubjectKeyIdentifierExtension) {
|
||||||
|
lines.push(` subjectKeyIdentifier${ext.critical ? " CRITICAL" : ""} :`);
|
||||||
|
lines.push(` ${ext.keyId}`);
|
||||||
|
} else if (ext instanceof AuthorityKeyIdentifierExtension) {
|
||||||
|
lines.push(` authorityKeyIdentifier${ext.critical ? " CRITICAL" : ""} :`);
|
||||||
|
if (ext.keyId) lines.push(` kid=${ext.keyId}`);
|
||||||
|
if (ext.certId && ext.certId.serialNumber) {
|
||||||
|
lines.push(` serial=${ext.certId.serialNumber.toUpperCase()}`);
|
||||||
|
}
|
||||||
|
} else if (ext instanceof BasicConstraintsExtension) {
|
||||||
|
lines.push(` basicConstraints${ext.critical ? " CRITICAL" : ""}:`);
|
||||||
|
lines.push(` cA=${ext.ca}` + (ext.pathLength !== undefined ? `,pathLen=${ext.pathLength}` : ""));
|
||||||
|
} else if (ext instanceof KeyUsagesExtension) {
|
||||||
|
lines.push(` keyUsage${ext.critical ? " CRITICAL" : ""}:`);
|
||||||
|
lines.push(` ${ext.usages.toString()}`);
|
||||||
|
} else if (ext instanceof ExtendedKeyUsageExtension) {
|
||||||
|
lines.push(` extKeyUsage${ext.critical ? " CRITICAL" : ""}:`);
|
||||||
|
for (const usage of ext.usages) lines.push(` ${usage}`);
|
||||||
|
} else if (ext instanceof SubjectAlternativeNameExtension) {
|
||||||
|
lines.push(` subjectAltName${ext.critical ? " CRITICAL" : ""}:`);
|
||||||
|
const asn = AsnParser.parse(new Uint8Array(ext.value), SubjectAlternativeName);
|
||||||
|
for (const gn of asn) lines.push(` ${formatGeneralName(gn, "csr")}`);
|
||||||
|
} else if (ext instanceof CRLDistributionPointsExtension) {
|
||||||
|
lines.push(` cRLDistributionPoints${ext.critical ? " CRITICAL" : ""}:`);
|
||||||
|
for (const dp of ext.distributionPoints) {
|
||||||
|
if (dp.distributionPoint && dp.distributionPoint.fullName) {
|
||||||
|
for (const gn of dp.distributionPoint.fullName) {
|
||||||
|
lines.push(` ${formatGeneralName(gn, "csr")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (ext.type === "2.5.29.18") { // issuerAltName
|
||||||
|
lines.push(` issuerAltName${ext.critical ? " CRITICAL" : ""}:`);
|
||||||
|
const asn = AsnParser.parse(new Uint8Array(ext.value), IssueAlternativeName);
|
||||||
|
for (const gn of asn) lines.push(` ${formatGeneralName(gn, "csr")}`);
|
||||||
|
} else {
|
||||||
|
lines.push(` ${ext.type}${ext.critical ? " CRITICAL" : ""} :`);
|
||||||
|
lines.push(` ${bytesToHex(new Uint8Array(ext.value))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a JS Date as an ASN.1 UTCTime/GeneralizedTime string: `yymmddHHMMSSZ`
|
||||||
|
* or `yyyymmddHHMMSSZ` for dates past 2049.
|
||||||
|
*
|
||||||
|
* @param {Date} date
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatAsn1Date(date) {
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getUTCDate()).padStart(2, "0");
|
||||||
|
const hour = String(date.getUTCHours()).padStart(2, "0");
|
||||||
|
const min = String(date.getUTCMinutes()).padStart(2, "0");
|
||||||
|
const sec = String(date.getUTCSeconds()).padStart(2, "0");
|
||||||
|
if (year < 1950 || year > 2049) {
|
||||||
|
return `${year}${month}${day}${hour}${min}${sec}Z`;
|
||||||
|
}
|
||||||
|
return `${String(year).slice(2)}${month}${day}${hour}${min}${sec}Z`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -216,7 +259,7 @@ ${extensions}`;
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
if (dateStr.length === 13) { // UTC Time
|
if (dateStr.length === 13) {
|
||||||
dateStr = (dateStr[0] < "5" ? "20" : "19") + dateStr;
|
dateStr = (dateStr[0] < "5" ? "20" : "19") + dateStr;
|
||||||
}
|
}
|
||||||
return dateStr[6] + dateStr[7] + "/" +
|
return dateStr[6] + dateStr[7] + "/" +
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
import { X509Certificate } from "@peculiar/x509";
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
|
||||||
@ -39,27 +39,31 @@ class PubKeyFromCert extends Operation {
|
|||||||
let match;
|
let match;
|
||||||
const regex = /-----BEGIN CERTIFICATE-----/g;
|
const regex = /-----BEGIN CERTIFICATE-----/g;
|
||||||
while ((match = regex.exec(input)) !== null) {
|
while ((match = regex.exec(input)) !== null) {
|
||||||
// find corresponding end tag
|
|
||||||
const indexBase64 = match.index + match[0].length;
|
const indexBase64 = match.index + match[0].length;
|
||||||
const footer = "-----END CERTIFICATE-----";
|
const footer = "-----END CERTIFICATE-----";
|
||||||
const indexFooter = input.indexOf(footer, indexBase64);
|
const indexFooter = input.indexOf(footer, indexBase64);
|
||||||
if (indexFooter === -1) {
|
if (indexFooter === -1) {
|
||||||
throw new OperationError(`PEM footer '${footer}' not found`);
|
throw new OperationError(`PEM footer '${footer}' not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const certPem = input.substring(match.index, indexFooter + footer.length);
|
const certPem = input.substring(match.index, indexFooter + footer.length);
|
||||||
const cert = new r.X509();
|
|
||||||
cert.readCertPEM(certPem);
|
let cert;
|
||||||
let pubKey;
|
|
||||||
try {
|
try {
|
||||||
pubKey = cert.getPublicKey();
|
cert = new X509Certificate(certPem);
|
||||||
} catch {
|
} catch {
|
||||||
throw new OperationError("Unsupported public key type");
|
throw new OperationError("Unsupported public key type");
|
||||||
}
|
}
|
||||||
const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
|
|
||||||
|
|
||||||
// PEM ends with '\n', so a new key always starts on a new line
|
let pubKeyPem;
|
||||||
output += pubKeyPem;
|
try {
|
||||||
|
pubKeyPem = cert.publicKey.toString("pem");
|
||||||
|
} catch {
|
||||||
|
throw new OperationError("Unsupported public key type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalise to LF endings + trailing newline so multi-cert input
|
||||||
|
// produces a clean separator between successive keys.
|
||||||
|
output += pubKeyPem.replace(/\r\n/g, "\n").replace(/\n?$/, "\n");
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,13 @@
|
|||||||
* @license Apache-2.0
|
* @license Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import r from "jsrsasign";
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
import OperationError from "../errors/OperationError.mjs";
|
import OperationError from "../errors/OperationError.mjs";
|
||||||
|
import {
|
||||||
|
parseKeyPem,
|
||||||
|
derivePublicKeyInfo,
|
||||||
|
keyInfoToPem,
|
||||||
|
} from "../lib/KeyConvert.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public Key from Private Key operation
|
* Public Key from Private Key operation
|
||||||
@ -39,7 +43,6 @@ class PubKeyFromPrivKey extends Operation {
|
|||||||
let match;
|
let match;
|
||||||
const regex = /-----BEGIN ((RSA |EC |DSA )?PRIVATE KEY)-----/g;
|
const regex = /-----BEGIN ((RSA |EC |DSA )?PRIVATE KEY)-----/g;
|
||||||
while ((match = regex.exec(input)) !== null) {
|
while ((match = regex.exec(input)) !== null) {
|
||||||
// find corresponding end tag
|
|
||||||
const indexBase64 = match.index + match[0].length;
|
const indexBase64 = match.index + match[0].length;
|
||||||
const footer = `-----END ${match[1]}-----`;
|
const footer = `-----END ${match[1]}-----`;
|
||||||
const indexFooter = input.indexOf(footer, indexBase64);
|
const indexFooter = input.indexOf(footer, indexBase64);
|
||||||
@ -48,32 +51,15 @@ class PubKeyFromPrivKey extends Operation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const privKeyPem = input.substring(match.index, indexFooter + footer.length);
|
const privKeyPem = input.substring(match.index, indexFooter + footer.length);
|
||||||
let privKey;
|
let privInfo;
|
||||||
try {
|
try {
|
||||||
privKey = r.KEYUTIL.getKey(privKeyPem);
|
privInfo = parseKeyPem(privKeyPem);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new OperationError(`Unsupported key type: ${err}`);
|
throw new OperationError(`Unsupported key type: ${err}`);
|
||||||
}
|
}
|
||||||
let pubKey;
|
|
||||||
if (privKey.type && privKey.type === "EC") {
|
|
||||||
pubKey = new r.KJUR.crypto.ECDSA({ curve: privKey.curve });
|
|
||||||
pubKey.setPublicKeyHex(privKey.generatePublicKeyHex());
|
|
||||||
} else if (privKey.type && privKey.type === "DSA") {
|
|
||||||
if (!privKey.y) {
|
|
||||||
throw new OperationError(`DSA Private Key in PKCS#8 is not supported`);
|
|
||||||
}
|
|
||||||
pubKey = new r.KJUR.crypto.DSA();
|
|
||||||
pubKey.setPublic(privKey.p, privKey.q, privKey.g, privKey.y);
|
|
||||||
} else if (privKey.n && privKey.e) {
|
|
||||||
pubKey = new r.RSAKey();
|
|
||||||
pubKey.setPublic(privKey.n, privKey.e);
|
|
||||||
} else {
|
|
||||||
throw new OperationError(`Unsupported key type`);
|
|
||||||
}
|
|
||||||
const pubKeyPem = r.KEYUTIL.getPEM(pubKey);
|
|
||||||
|
|
||||||
// PEM ends with '\n', so a new key always starts on a new line
|
const pubInfo = derivePublicKeyInfo(privInfo);
|
||||||
output += pubKeyPem;
|
output += keyInfoToPem(pubInfo);
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -318,31 +318,31 @@ TestRegister.addApiTests([
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
it("chef.bake: should take compact JSON format from Chef Website as recipe", async () => {
|
it("chef.bake: should take compact JSON format from Chef Website as recipe", async () => {
|
||||||
const result = await chef.bake("some input", [{"op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]);
|
const result = await chef.bake("some input", [{"op": "To Hex", "args": ["Space", 0]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]);
|
||||||
assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something");
|
assert.strictEqual(result.toString(), "begin_something_c_29_t_zs_bpbn_b_1_d_a_end_something");
|
||||||
}),
|
}),
|
||||||
|
|
||||||
it("chef.bake: should accept Clean JSON format from Chef website as recipe", async () => {
|
it("chef.bake: should accept Clean JSON format from Chef website as recipe", async () => {
|
||||||
const result = await chef.bake("some input", [
|
const result = await chef.bake("some input", [
|
||||||
{ "op": "To Morse Code",
|
{ "op": "To Hex",
|
||||||
"args": ["Dash/Dot", "Backslash", "Comma"] },
|
"args": ["Space", 0] },
|
||||||
{ "op": "Hex to PEM",
|
{ "op": "Hex to PEM",
|
||||||
"args": ["SOMETHING"] },
|
"args": ["SOMETHING"] },
|
||||||
{ "op": "To Snake case",
|
{ "op": "To Snake case",
|
||||||
"args": [false] }
|
"args": [false] }
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something");
|
assert.strictEqual(result.toString(), "begin_something_c_29_t_zs_bpbn_b_1_d_a_end_something");
|
||||||
}),
|
}),
|
||||||
|
|
||||||
it("chef.bake: should accept Clean JSON format from Chef website - args optional", async () => {
|
it("chef.bake: should accept Clean JSON format from Chef website - args optional", async () => {
|
||||||
const result = await chef.bake("some input", [
|
const result = await chef.bake("some input", [
|
||||||
{ "op": "To Morse Code" },
|
{ "op": "To Hex" },
|
||||||
{ "op": "Hex to PEM",
|
{ "op": "Hex to PEM",
|
||||||
"args": ["SOMETHING"] },
|
"args": ["SOMETHING"] },
|
||||||
{ "op": "To Snake case",
|
{ "op": "To Snake case",
|
||||||
"args": [false] }
|
"args": [false] }
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(result.toString(), "begin_something_aaaaaaaaaaaaaa_end_something");
|
assert.strictEqual(result.toString(), "begin_something_c_29_t_zs_bpbn_b_1_d_a_end_something");
|
||||||
}),
|
}),
|
||||||
|
|
||||||
it("chef.bake: should accept operation names from Chef Website which contain forward slash", async () => {
|
it("chef.bake: should accept operation names from Chef Website which contain forward slash", async () => {
|
||||||
@ -366,7 +366,7 @@ TestRegister.addApiTests([
|
|||||||
{ "op": "Parse ASN.1 hex string",
|
{ "op": "Parse ASN.1 hex string",
|
||||||
"args": [0, 32] }
|
"args": [0, 32] }
|
||||||
]);
|
]);
|
||||||
assert.strictEqual(result.toString(), `SEQUENCE\n INTEGER 05\n IA5String 'Anybody there?'\n`);
|
assert.strictEqual(result.toString(), `SEQUENCE\n INTEGER 5\n IA5String "Anybody there?"`);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
it("Excluded operations: throw a sensible error when you try and call one", () => {
|
it("Excluded operations: throw a sensible error when you try and call one", () => {
|
||||||
|
|||||||
@ -651,9 +651,10 @@ Password: 282760`;
|
|||||||
|
|
||||||
it("Hex to PEM", () => {
|
it("Hex to PEM", () => {
|
||||||
const result = chef.hexToPEM(chef.toHex("Yada Yada"));
|
const result = chef.hexToPEM(chef.toHex("Yada Yada"));
|
||||||
const expected = `-----BEGIN CERTIFICATE-----\r
|
const expected = `-----BEGIN CERTIFICATE-----
|
||||||
WWFkYSBZYWRh\r
|
WWFkYSBZYWRh
|
||||||
-----END CERTIFICATE-----\r\n`;
|
-----END CERTIFICATE-----
|
||||||
|
`;
|
||||||
assert.strictEqual(result.toString(), expected);
|
assert.strictEqual(result.toString(), expected);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@ -686,7 +687,13 @@ WWFkYSBZYWRh\r
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
it("Parse ASN.1 Hex string", () => {
|
it("Parse ASN.1 Hex string", () => {
|
||||||
assert.strictEqual(chef.parseASN1HexString(chef.toHex("Mouth-watering")).toString(), "UNKNOWN(77) 7574682d7761746572696e67\n");
|
// The bytes for "Mouth-watering" don't form a well-formed ASN.1 structure
|
||||||
|
// (tag 0x4d declares length 0x6f but only 12 bytes follow), so we report
|
||||||
|
// a parse error rather than a partial best-effort dump.
|
||||||
|
assert.strictEqual(
|
||||||
|
chef.parseASN1HexString(chef.toHex("Mouth-watering")).toString(),
|
||||||
|
"ASN.1 parse error: End of input reached before message was fully decoded (inconsistent offset and length values)"
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
it("Parse DateTime", () => {
|
it("Parse DateTime", () => {
|
||||||
@ -1178,4 +1185,3 @@ ExifImageHeight: 57`);
|
|||||||
|
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
218
tests/operations/index.mjs
Normal file
218
tests/operations/index.mjs
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* THIS FILE IS AUTOMATICALLY GENERATED BY src/core/config/scripts/generateOpsIndex.mjs
|
||||||
|
*
|
||||||
|
* @author john [john19696@protonmail.com]
|
||||||
|
* @author tlwr [toby@toby.codes]
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
setLongTestFailure,
|
||||||
|
logTestReport,
|
||||||
|
} from "../lib/utils.mjs";
|
||||||
|
|
||||||
|
import "../lib/wasmFetchPolyfill.mjs";
|
||||||
|
|
||||||
|
import TestRegister from "../lib/TestRegister.mjs";
|
||||||
|
import "./tests/A1Z26CipherDecode.mjs";
|
||||||
|
import "./tests/AESKeyWrap.mjs";
|
||||||
|
import "./tests/ASN1.mjs";
|
||||||
|
import "./tests/AlternatingCaps.mjs";
|
||||||
|
import "./tests/AnalyseUUID.mjs";
|
||||||
|
import "./tests/AvroToJSON.mjs";
|
||||||
|
import "./tests/BCD.mjs";
|
||||||
|
import "./tests/BLAKE2b.mjs";
|
||||||
|
import "./tests/BLAKE2s.mjs";
|
||||||
|
import "./tests/BLAKE3.mjs";
|
||||||
|
import "./tests/BSON.mjs";
|
||||||
|
import "./tests/BaconCipher.mjs";
|
||||||
|
import "./tests/Base32.mjs";
|
||||||
|
import "./tests/Base45.mjs";
|
||||||
|
import "./tests/Base58.mjs";
|
||||||
|
import "./tests/Base62.mjs";
|
||||||
|
import "./tests/Base64.mjs";
|
||||||
|
import "./tests/Base85.mjs";
|
||||||
|
import "./tests/Base92.mjs";
|
||||||
|
import "./tests/Bech32.mjs";
|
||||||
|
import "./tests/BitwiseOp.mjs";
|
||||||
|
import "./tests/Bombe.mjs";
|
||||||
|
import "./tests/ByteRepr.mjs";
|
||||||
|
import "./tests/CBORDecode.mjs";
|
||||||
|
import "./tests/CBOREncode.mjs";
|
||||||
|
import "./tests/CMAC.mjs";
|
||||||
|
import "./tests/CRCChecksum.mjs";
|
||||||
|
import "./tests/CSV.mjs";
|
||||||
|
import "./tests/CaesarBoxCipher.mjs";
|
||||||
|
import "./tests/CaretMdecode.mjs";
|
||||||
|
import "./tests/CartesianProduct.mjs";
|
||||||
|
import "./tests/CetaceanCipherDecode.mjs";
|
||||||
|
import "./tests/CetaceanCipherEncode.mjs";
|
||||||
|
import "./tests/ChaCha.mjs";
|
||||||
|
import "./tests/ChangeIPFormat.mjs";
|
||||||
|
import "./tests/CharEnc.mjs";
|
||||||
|
import "./tests/Charts.mjs";
|
||||||
|
import "./tests/CipherSaber2.mjs";
|
||||||
|
import "./tests/Ciphers.mjs";
|
||||||
|
import "./tests/Code.mjs";
|
||||||
|
import "./tests/Colossus.mjs";
|
||||||
|
import "./tests/Comment.mjs";
|
||||||
|
import "./tests/Compress.mjs";
|
||||||
|
import "./tests/ConditionalJump.mjs";
|
||||||
|
import "./tests/ConvertCoordinateFormat.mjs";
|
||||||
|
import "./tests/ConvertLeetSpeak.mjs";
|
||||||
|
import "./tests/ConvertToNATOAlphabet.mjs";
|
||||||
|
import "./tests/Crypt.mjs";
|
||||||
|
import "./tests/DateTime.mjs";
|
||||||
|
import "./tests/DefangIP.mjs";
|
||||||
|
import "./tests/DisassembleARM.mjs";
|
||||||
|
import "./tests/DropNthBytes.mjs";
|
||||||
|
import "./tests/ECDSA.mjs";
|
||||||
|
import "./tests/ELFInfo.mjs";
|
||||||
|
import "./tests/Enigma.mjs";
|
||||||
|
import "./tests/EscapeSmartCharacters.mjs";
|
||||||
|
import "./tests/ExtractAudioMetadata.mjs";
|
||||||
|
import "./tests/ExtractEmailAddresses.mjs";
|
||||||
|
import "./tests/ExtractHashes.mjs";
|
||||||
|
import "./tests/ExtractIPAddresses.mjs";
|
||||||
|
import "./tests/Fernet.mjs";
|
||||||
|
import "./tests/FileTree.mjs";
|
||||||
|
import "./tests/FlaskSession.mjs";
|
||||||
|
import "./tests/FletcherChecksum.mjs";
|
||||||
|
import "./tests/Float.mjs";
|
||||||
|
import "./tests/Fork.mjs";
|
||||||
|
import "./tests/FromDecimal.mjs";
|
||||||
|
import "./tests/GOST.mjs";
|
||||||
|
import "./tests/GenerateAllChecksums.mjs";
|
||||||
|
import "./tests/GenerateAllHashes.mjs";
|
||||||
|
import "./tests/GenerateDeBruijnSequence.mjs";
|
||||||
|
import "./tests/GenerateQRCode.mjs";
|
||||||
|
import "./tests/GetAllCasings.mjs";
|
||||||
|
import "./tests/Gunzip.mjs";
|
||||||
|
import "./tests/Gzip.mjs";
|
||||||
|
import "./tests/HASSH.mjs";
|
||||||
|
import "./tests/HKDF.mjs";
|
||||||
|
import "./tests/Hash.mjs";
|
||||||
|
import "./tests/HaversineDistance.mjs";
|
||||||
|
import "./tests/Hex.mjs";
|
||||||
|
import "./tests/Hexdump.mjs";
|
||||||
|
import "./tests/IPv6Transition.mjs";
|
||||||
|
import "./tests/Image.mjs";
|
||||||
|
import "./tests/IndexOfCoincidence.mjs";
|
||||||
|
import "./tests/JA3Fingerprint.mjs";
|
||||||
|
import "./tests/JA3SFingerprint.mjs";
|
||||||
|
import "./tests/JA4.mjs";
|
||||||
|
import "./tests/JSONBeautify.mjs";
|
||||||
|
import "./tests/JSONMinify.mjs";
|
||||||
|
import "./tests/JSONtoCSV.mjs";
|
||||||
|
import "./tests/JSONtoYAML.mjs";
|
||||||
|
import "./tests/JWK.mjs";
|
||||||
|
import "./tests/JWTDecode.mjs";
|
||||||
|
import "./tests/JWTSign.mjs";
|
||||||
|
import "./tests/JWTVerify.mjs";
|
||||||
|
import "./tests/Jq.mjs";
|
||||||
|
import "./tests/Jsonata.mjs";
|
||||||
|
import "./tests/Jump.mjs";
|
||||||
|
import "./tests/LS47.mjs";
|
||||||
|
import "./tests/LZNT1Decompress.mjs";
|
||||||
|
import "./tests/LZString.mjs";
|
||||||
|
import "./tests/LevenshteinDistance.mjs";
|
||||||
|
import "./tests/Lorenz.mjs";
|
||||||
|
import "./tests/LuhnChecksum.mjs";
|
||||||
|
import "./tests/MIMEDecoding.mjs";
|
||||||
|
import "./tests/MS.mjs";
|
||||||
|
import "./tests/Magic.mjs";
|
||||||
|
import "./tests/Media.mjs";
|
||||||
|
import "./tests/Modhex.mjs";
|
||||||
|
import "./tests/MorseCode.mjs";
|
||||||
|
import "./tests/MultipleBombe.mjs";
|
||||||
|
import "./tests/MurmurHash3.mjs";
|
||||||
|
import "./tests/NTLM.mjs";
|
||||||
|
import "./tests/NetBIOS.mjs";
|
||||||
|
import "./tests/NormaliseUnicode.mjs";
|
||||||
|
import "./tests/OTP.mjs";
|
||||||
|
import "./tests/PEMtoHex.mjs";
|
||||||
|
import "./tests/PGP.mjs";
|
||||||
|
import "./tests/PHP.mjs";
|
||||||
|
import "./tests/PHPSerialize.mjs";
|
||||||
|
import "./tests/ParityBit.mjs";
|
||||||
|
import "./tests/ParseCSR.mjs";
|
||||||
|
import "./tests/ParseEthernetFrame.mjs";
|
||||||
|
import "./tests/ParseIPRange.mjs";
|
||||||
|
import "./tests/ParseIPv4Header.mjs";
|
||||||
|
import "./tests/ParseObjectIDTimestamp.mjs";
|
||||||
|
import "./tests/ParseQRCode.mjs";
|
||||||
|
import "./tests/ParseSSHHostKey.mjs";
|
||||||
|
import "./tests/ParseTCP.mjs";
|
||||||
|
import "./tests/ParseTLSRecord.mjs";
|
||||||
|
import "./tests/ParseTLV.mjs";
|
||||||
|
import "./tests/ParseUDP.mjs";
|
||||||
|
import "./tests/ParseX509CRL.mjs";
|
||||||
|
import "./tests/ParseX509Certificate.mjs";
|
||||||
|
import "./tests/PowerSet.mjs";
|
||||||
|
import "./tests/Protobuf.mjs";
|
||||||
|
import "./tests/PubKeyFromCert.mjs";
|
||||||
|
import "./tests/PubKeyFromPrivKey.mjs";
|
||||||
|
import "./tests/RAKE.mjs";
|
||||||
|
import "./tests/RC6.mjs";
|
||||||
|
import "./tests/RSA.mjs";
|
||||||
|
import "./tests/Rabbit.mjs";
|
||||||
|
import "./tests/Regex.mjs";
|
||||||
|
import "./tests/Register.mjs";
|
||||||
|
import "./tests/RegularExpression.mjs";
|
||||||
|
import "./tests/RemoveANSIEscapeCodes.mjs";
|
||||||
|
import "./tests/RenderMarkdown.mjs";
|
||||||
|
import "./tests/RisonEncodeDecode.mjs";
|
||||||
|
import "./tests/Rotate.mjs";
|
||||||
|
import "./tests/SIGABA.mjs";
|
||||||
|
import "./tests/SM2.mjs";
|
||||||
|
import "./tests/SM4.mjs";
|
||||||
|
import "./tests/SQLBeautify.mjs";
|
||||||
|
import "./tests/Salsa20.mjs";
|
||||||
|
import "./tests/SeqUtils.mjs";
|
||||||
|
import "./tests/SetDifference.mjs";
|
||||||
|
import "./tests/SetIntersection.mjs";
|
||||||
|
import "./tests/SetUnion.mjs";
|
||||||
|
import "./tests/Shuffle.mjs";
|
||||||
|
// Cannot test operations that use the File type yet
|
||||||
|
// import "./tests/SplitColourChannels.mjs";
|
||||||
|
import "./tests/StrUtils.mjs";
|
||||||
|
import "./tests/StripIPv4Header.mjs";
|
||||||
|
import "./tests/StripTCPHeader.mjs";
|
||||||
|
import "./tests/StripUDPHeader.mjs";
|
||||||
|
import "./tests/Subsection.mjs";
|
||||||
|
import "./tests/SwapCase.mjs";
|
||||||
|
import "./tests/SymmetricDifference.mjs";
|
||||||
|
import "./tests/TakeNthBytes.mjs";
|
||||||
|
import "./tests/Template.mjs";
|
||||||
|
import "./tests/TextEncodingBruteForce.mjs";
|
||||||
|
import "./tests/TextIntegerConverter.mjs";
|
||||||
|
import "./tests/ToFromInsensitiveRegex.mjs";
|
||||||
|
import "./tests/TranslateDateTimeFormat.mjs";
|
||||||
|
import "./tests/Typex.mjs";
|
||||||
|
import "./tests/URLEncodeDecode.mjs";
|
||||||
|
import "./tests/UnescapeString.mjs";
|
||||||
|
import "./tests/Unicode.mjs";
|
||||||
|
import "./tests/Wrap.mjs";
|
||||||
|
import "./tests/XORChecksum.mjs";
|
||||||
|
import "./tests/XSalsa20.mjs";
|
||||||
|
import "./tests/XXTEA.mjs";
|
||||||
|
import "./tests/YARA.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
const testStatus = {
|
||||||
|
allTestsPassing: true,
|
||||||
|
counts: {
|
||||||
|
total: 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setLongTestFailure();
|
||||||
|
|
||||||
|
const logOpsTestReport = logTestReport.bind(null, testStatus);
|
||||||
|
|
||||||
|
(async function() {
|
||||||
|
const results = await TestRegister.runTests();
|
||||||
|
logOpsTestReport(results);
|
||||||
|
})();
|
||||||
94
tests/operations/tests/ASN1.mjs
Normal file
94
tests/operations/tests/ASN1.mjs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
/**
|
||||||
|
* ASN.1 / OID / PEM tests.
|
||||||
|
*
|
||||||
|
* Covers the four operations backed by the in-house Asn1.mjs helper:
|
||||||
|
* - Hex to Object Identifier
|
||||||
|
* - Object Identifier to Hex
|
||||||
|
* - Hex to PEM
|
||||||
|
* - Parse ASN.1 hex string
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
|
|
||||||
|
TestRegister.addTests([
|
||||||
|
{
|
||||||
|
name: "Hex to Object Identifier: rsaEncryption (1.2.840.113549.1.1.1)",
|
||||||
|
input: "2a864886f70d010101",
|
||||||
|
expectedOutput: "1.2.840.113549.1.1.1",
|
||||||
|
recipeConfig: [{ op: "Hex to Object Identifier", args: [] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hex to Object Identifier: commonName (2.5.4.3)",
|
||||||
|
input: "550403",
|
||||||
|
expectedOutput: "2.5.4.3",
|
||||||
|
recipeConfig: [{ op: "Hex to Object Identifier", args: [] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hex to Object Identifier: Microsoft CTL signing (1.3.6.1.4.1.311.2.1.4) — multi-byte arc",
|
||||||
|
input: "2b060104018237020104",
|
||||||
|
expectedOutput: "1.3.6.1.4.1.311.2.1.4",
|
||||||
|
recipeConfig: [{ op: "Hex to Object Identifier", args: [] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hex to Object Identifier: handles whitespace in input",
|
||||||
|
input: "55 04 03",
|
||||||
|
expectedOutput: "2.5.4.3",
|
||||||
|
recipeConfig: [{ op: "Hex to Object Identifier", args: [] }],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Object Identifier to Hex: rsaEncryption",
|
||||||
|
input: "1.2.840.113549.1.1.1",
|
||||||
|
expectedOutput: "2a864886f70d010101",
|
||||||
|
recipeConfig: [{ op: "Object Identifier to Hex", args: [] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Object Identifier to Hex: commonName",
|
||||||
|
input: "2.5.4.3",
|
||||||
|
expectedOutput: "550403",
|
||||||
|
recipeConfig: [{ op: "Object Identifier to Hex", args: [] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Object Identifier to Hex: Microsoft CTL signing (multi-byte arc)",
|
||||||
|
input: "1.3.6.1.4.1.311.2.1.4",
|
||||||
|
expectedOutput: "2b060104018237020104",
|
||||||
|
recipeConfig: [{ op: "Object Identifier to Hex", args: [] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Object Identifier to Hex: 2.999 (multi-byte first combined arc)",
|
||||||
|
input: "2.999",
|
||||||
|
expectedOutput: "8837",
|
||||||
|
recipeConfig: [{ op: "Object Identifier to Hex", args: [] }],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Hex to PEM: short payload",
|
||||||
|
input: "48656c6c6f",
|
||||||
|
expectedOutput: "-----BEGIN CERTIFICATE-----\nSGVsbG8=\n-----END CERTIFICATE-----\n",
|
||||||
|
recipeConfig: [{ op: "Hex to PEM", args: ["CERTIFICATE"] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hex to PEM: wraps at 64 base64 characters",
|
||||||
|
// 60 bytes -> 80 base64 chars (no padding) -> wraps after 64
|
||||||
|
input: "00".repeat(60),
|
||||||
|
expectedOutput: "-----BEGIN PUBLIC KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAA\n-----END PUBLIC KEY-----\n",
|
||||||
|
recipeConfig: [{ op: "Hex to PEM", args: ["PUBLIC KEY"] }],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Parse ASN.1 hex string: simple SEQUENCE of two INTEGERs",
|
||||||
|
input: "3006020101020102",
|
||||||
|
expectedOutput: "SEQUENCE\n INTEGER 1\n INTEGER 2",
|
||||||
|
recipeConfig: [{ op: "Parse ASN.1 hex string", args: [0, 32] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parse ASN.1 hex string: SEQUENCE { OID, NULL }",
|
||||||
|
input: "300d06092a864886f70d0107010500",
|
||||||
|
expectedOutput: "SEQUENCE\n ObjectIdentifier 1.2.840.113549.1.7.1\n NULL",
|
||||||
|
recipeConfig: [{ op: "Parse ASN.1 hex string", args: [0, 32] }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
@ -286,6 +286,22 @@ TestRegister.addTests([
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Published known-answer test pinning correctness against the spec
|
||||||
|
// (not against the previous jsrsasign output). RFC 6979 Appendix
|
||||||
|
// A.2.5: curve P-256 (prime256v1), message "sample", SHA-256. The
|
||||||
|
// public key below is the SPKI form of the (Ux, Uy) point from the
|
||||||
|
// RFC; the r||s signature is the deterministic signature it lists.
|
||||||
|
name: "ECDSA Verify: RFC 6979 A.2.5 P-256/SHA-256 known-answer vector",
|
||||||
|
input: "efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8",
|
||||||
|
expectedOutput: "Verified OK",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
"op": "ECDSA Verify",
|
||||||
|
"args": ["Auto", "SHA-256", "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYP7UuiVanTHJYet0xjVtaMBJuJI7\nYfps5mliLmDyn7Z5A/4QCLi8maQa6elWKLxk8vGyDC1+n1F3o8KU1EYimQ==\n-----END PUBLIC KEY-----", "sample", "Raw"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "ECDSA Verify: JSON signature missing r",
|
name: "ECDSA Verify: JSON signature missing r",
|
||||||
input: JSON.stringify({s: JSON.parse(P256.signature.sha256.json).s}),
|
input: JSON.stringify({s: JSON.parse(P256.signature.sha256.json).s}),
|
||||||
|
|||||||
@ -290,7 +290,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "JWK to PEM: RSA Private Key",
|
name: "JWK to PEM: RSA Private Key",
|
||||||
input: JSON.stringify(RSA_512.private.jwk),
|
input: JSON.stringify(RSA_512.private.jwk),
|
||||||
expectedOutput: RSA_512.private.pem8.replace(/\r/g, "").replace(/\n/g, "\r\n")+"\r\n",
|
expectedOutput: RSA_512.private.pem8+"\n",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JWK to PEM",
|
op: "JWK to PEM",
|
||||||
@ -301,7 +301,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "JWK to PEM: RSA Public Key",
|
name: "JWK to PEM: RSA Public Key",
|
||||||
input: JSON.stringify(RSA_512.public.jwk),
|
input: JSON.stringify(RSA_512.public.jwk),
|
||||||
expectedOutput: RSA_512.public.pem8.replace(/\r/g, "").replace(/\n/g, "\r\n")+"\r\n",
|
expectedOutput: RSA_512.public.pem8+"\n",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JWK to PEM",
|
op: "JWK to PEM",
|
||||||
@ -314,7 +314,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "JWK to PEM: EC Private Key",
|
name: "JWK to PEM: EC Private Key",
|
||||||
input: JSON.stringify(EC_P256.private.jwk),
|
input: JSON.stringify(EC_P256.private.jwk),
|
||||||
expectedOutput: EC_P256.private.pem8.replace(/\r/g, "").replace(/\n/g, "\r\n")+"\r\n",
|
expectedOutput: EC_P256.private.pem8+"\n",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JWK to PEM",
|
op: "JWK to PEM",
|
||||||
@ -325,7 +325,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "JWK to PEM: EC Public Key",
|
name: "JWK to PEM: EC Public Key",
|
||||||
input: JSON.stringify(EC_P256.public.jwk),
|
input: JSON.stringify(EC_P256.public.jwk),
|
||||||
expectedOutput: EC_P256.public.pem8.replace(/\r/g, "").replace(/\n/g, "\r\n")+"\r\n",
|
expectedOutput: EC_P256.public.pem8+"\n",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JWK to PEM",
|
op: "JWK to PEM",
|
||||||
@ -337,7 +337,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "JWK to PEM: Array of keys",
|
name: "JWK to PEM: Array of keys",
|
||||||
input: JSON.stringify([RSA_512.public.jwk, EC_P256.public.jwk]),
|
input: JSON.stringify([RSA_512.public.jwk, EC_P256.public.jwk]),
|
||||||
expectedOutput: (RSA_512.public.pem8 + "\n" + EC_P256.public.pem8 + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (RSA_512.public.pem8 + "\n" + EC_P256.public.pem8 + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JWK to PEM",
|
op: "JWK to PEM",
|
||||||
@ -348,7 +348,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "JWK to PEM: JSON Web Key Set",
|
name: "JWK to PEM: JSON Web Key Set",
|
||||||
input: JSON.stringify({"keys": [RSA_512.public.jwk, EC_P256.public.jwk]}),
|
input: JSON.stringify({"keys": [RSA_512.public.jwk, EC_P256.public.jwk]}),
|
||||||
expectedOutput: (RSA_512.public.pem8 + "\n" + EC_P256.public.pem8 + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (RSA_512.public.pem8 + "\n" + EC_P256.public.pem8 + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "JWK to PEM",
|
op: "JWK to PEM",
|
||||||
|
|||||||
138
tests/operations/tests/ParseX509Certificate.mjs
Normal file
138
tests/operations/tests/ParseX509Certificate.mjs
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Parse X.509 Certificate tests.
|
||||||
|
*
|
||||||
|
* Added as part of the @peculiar/x509 migration (PR 5) to give the
|
||||||
|
* operation regression coverage it previously lacked. The certificate
|
||||||
|
* fixtures are reused from PubKeyFromCert.mjs.
|
||||||
|
*
|
||||||
|
* @author n1474335 [n1474335@gmail.com]
|
||||||
|
* @copyright Crown Copyright 2016
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
import TestRegister from "../../lib/TestRegister.mjs";
|
||||||
|
|
||||||
|
const RSA_CERT = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBfTCCASegAwIBAgIUeisK5Nwss2DGg5PCs4uSxxXyyNkwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwEzERMA8GA1UEAwwIUlNBIHRlc3QwHhcNMjExMTE5MTcyMDI2WhcNMzExMTE3
|
||||||
|
MTcyMDI2WjATMREwDwYDVQQDDAhSU0EgdGVzdDBcMA0GCSqGSIb3DQEBAQUAA0sA
|
||||||
|
MEgCQQDyq9A6emHSLczn5Omu5muy+AReC53pTGCrW6Bi65OoobahT2RUSzXCYuvB
|
||||||
|
757fLLTKz+dLeo6sFkNhIzHZI+n7AgMBAAGjUzBRMB0GA1UdDgQWBBRO+jvkqq5p
|
||||||
|
pnQgwMMnRoun6e7eiTAfBgNVHSMEGDAWgBRO+jvkqq5ppnQgwMMnRoun6e7eiTAP
|
||||||
|
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EAR/5HAZM5qBhU/ezDUIFx
|
||||||
|
gmUGoFbIb5kJD41YCnaSdrgWglh4He4melSs42G/oxBBjuCJ0bUpqWnLl+lJkv1z
|
||||||
|
IA==
|
||||||
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
|
const RSA_EXPECTED = `Version: 3 (0x02)
|
||||||
|
Serial number: 697456755083946472681082503344832984412880816345 (0x7a2b0ae4dc2cb360c68393c2b38b92c715f2c8d9)
|
||||||
|
Algorithm ID: SHA256withRSA
|
||||||
|
Validity
|
||||||
|
Not Before: 19/11/2021 17:20:26 (dd-mm-yyyy hh:mm:ss) (211119172026Z)
|
||||||
|
Not After: 17/11/2031 17:20:26 (dd-mm-yyyy hh:mm:ss) (311117172026Z)
|
||||||
|
Issuer
|
||||||
|
CN = RSA test
|
||||||
|
Subject
|
||||||
|
CN = RSA test
|
||||||
|
Fingerprints
|
||||||
|
MD5: 0807638eee1403fbcacc1b6d25e00d95
|
||||||
|
SHA1: cae48f6fac74e143e10e0d8db1597385d62f24c4
|
||||||
|
SHA256: cd9125e6e7caa729c766e4d9ed84ef44e6bc57d00614c5f051af661e9ce3e436
|
||||||
|
Public Key
|
||||||
|
Algorithm: RSA
|
||||||
|
Length: 512 bits
|
||||||
|
Modulus: f2:ab:d0:3a:7a:61:d2:2d:cc:e7:e4:e9:ae:e6:6b:b2:
|
||||||
|
f8:04:5e:0b:9d:e9:4c:60:ab:5b:a0:62:eb:93:a8:a1:
|
||||||
|
b6:a1:4f:64:54:4b:35:c2:62:eb:c1:ef:9e:df:2c:b4:
|
||||||
|
ca:cf:e7:4b:7a:8e:ac:16:43:61:23:31:d9:23:e9:fb
|
||||||
|
Exponent: 65537 (0x10001)
|
||||||
|
Certificate Signature
|
||||||
|
Algorithm: SHA256withRSA
|
||||||
|
Signature: 47:fe:47:01:93:39:a8:18:54:fd:ec:c3:50:81:71:82:
|
||||||
|
65:06:a0:56:c8:6f:99:09:0f:8d:58:0a:76:92:76:b8:
|
||||||
|
16:82:58:78:1d:ee:26:7a:54:ac:e3:61:bf:a3:10:41:
|
||||||
|
8e:e0:89:d1:b5:29:a9:69:cb:97:e9:49:92:fd:73:20
|
||||||
|
|
||||||
|
Extensions
|
||||||
|
subjectKeyIdentifier :
|
||||||
|
4efa3be4aaae69a67420c0c327468ba7e9eede89
|
||||||
|
authorityKeyIdentifier :
|
||||||
|
kid=4efa3be4aaae69a67420c0c327468ba7e9eede89
|
||||||
|
basicConstraints CRITICAL:
|
||||||
|
cA=true`;
|
||||||
|
|
||||||
|
const EC_P256_CERT = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBfzCCASWgAwIBAgIUK4H8J3Hr7NpRLPrACj8Pje4JJJ0wCgYIKoZIzj0EAwIw
|
||||||
|
FTETMBEGA1UEAwwKUC0yNTYgdGVzdDAeFw0yMTExMTkxNzE5NDVaFw0zMTExMTcx
|
||||||
|
NzE5NDVaMBUxEzARBgNVBAMMClAtMjU2IHRlc3QwWTATBgcqhkjOPQIBBggqhkjO
|
||||||
|
PQMBBwNCAAQNRzwDQQM0qgJgg9YwfPXJTOoTmYmC6yBwATwfrzXR+QnxmZM2IIJr
|
||||||
|
qwuBHa8PVU2HZ2KKtaAo8fg9Uwpq/l7po1MwUTAdBgNVHQ4EFgQU/SxodXrpkybM
|
||||||
|
gcIgkxnRKd7HMzowHwYDVR0jBBgwFoAU/SxodXrpkybMgcIgkxnRKd7HMzowDwYD
|
||||||
|
VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiBU9PrOa/kXCpTTBInRf/sN
|
||||||
|
ac2iDHmbdpWzcXI+xLKNYAIhAIRR1LRSHVwOTLQ/iBXd+8LCkm5aTB27RW46LN80
|
||||||
|
ylxt
|
||||||
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
|
const EC_P256_EXPECTED = `Version: 3 (0x02)
|
||||||
|
Serial number: 248385364994530420657018049397175541334954026141 (0x2b81fc2771ebecda512cfac00a3f0f8dee09249d)
|
||||||
|
Algorithm ID: SHA256withECDSA
|
||||||
|
Validity
|
||||||
|
Not Before: 19/11/2021 17:19:45 (dd-mm-yyyy hh:mm:ss) (211119171945Z)
|
||||||
|
Not After: 17/11/2031 17:19:45 (dd-mm-yyyy hh:mm:ss) (311117171945Z)
|
||||||
|
Issuer
|
||||||
|
CN = P-256 test
|
||||||
|
Subject
|
||||||
|
CN = P-256 test
|
||||||
|
Fingerprints
|
||||||
|
MD5: d58a07c73ac5353acd1799174472478f
|
||||||
|
SHA1: 562feb8b1a2c9808a98b350557ab80eab619ed48
|
||||||
|
SHA256: 584662f4632e221a1d58f91f772fb6f617af7aa3d8542281af2efcc93c1b79eb
|
||||||
|
Public Key
|
||||||
|
Algorithm: EC
|
||||||
|
Curve Name: secp256r1
|
||||||
|
Length: 256 bits
|
||||||
|
pub: 04:0d:47:3c:03:41:03:34:aa:02:60:83:d6:30:7c:f5:
|
||||||
|
c9:4c:ea:13:99:89:82:eb:20:70:01:3c:1f:af:35:d1:
|
||||||
|
f9:09:f1:99:93:36:20:82:6b:ab:0b:81:1d:af:0f:55:
|
||||||
|
4d:87:67:62:8a:b5:a0:28:f1:f8:3d:53:0a:6a:fe:5e:
|
||||||
|
e9
|
||||||
|
Certificate Signature
|
||||||
|
Algorithm: SHA256withECDSA
|
||||||
|
r: 54:f4:fa:ce:6b:f9:17:0a:94:d3:04:89:d1:7f:fb:0d:
|
||||||
|
69:cd:a2:0c:79:9b:76:95:b3:71:72:3e:c4:b2:8d:60
|
||||||
|
s: 84:51:d4:b4:52:1d:5c:0e:4c:b4:3f:88:15:dd:fb:c2:
|
||||||
|
c2:92:6e:5a:4c:1d:bb:45:6e:3a:2c:df:34:ca:5c:6d
|
||||||
|
|
||||||
|
Extensions
|
||||||
|
subjectKeyIdentifier :
|
||||||
|
fd2c68757ae99326cc81c2209319d129dec7333a
|
||||||
|
authorityKeyIdentifier :
|
||||||
|
kid=fd2c68757ae99326cc81c2209319d129dec7333a
|
||||||
|
basicConstraints CRITICAL:
|
||||||
|
cA=true`;
|
||||||
|
|
||||||
|
TestRegister.addTests([
|
||||||
|
{
|
||||||
|
name: "Parse X.509 certificate: No input",
|
||||||
|
input: "",
|
||||||
|
expectedOutput: "No input",
|
||||||
|
recipeConfig: [
|
||||||
|
{ op: "Parse X.509 certificate", args: ["PEM"] }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parse X.509 certificate: RSA / PEM",
|
||||||
|
input: RSA_CERT,
|
||||||
|
expectedOutput: RSA_EXPECTED,
|
||||||
|
recipeConfig: [
|
||||||
|
{ op: "Parse X.509 certificate", args: ["PEM"] }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Parse X.509 certificate: EC P-256 / PEM",
|
||||||
|
input: EC_P256_CERT,
|
||||||
|
expectedOutput: EC_P256_EXPECTED,
|
||||||
|
recipeConfig: [
|
||||||
|
{ op: "Parse X.509 certificate", args: ["PEM"] }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
@ -99,11 +99,10 @@ HRMBAf8EBTADAQH/MAUGAytlcANBAI/+03iVq4yJ+DaLVs61w41cVX2UxKvquSzv
|
|||||||
lllkpkclM9LH5dLrw4ArdTjS9zAjzY/02WkphHhICHXt3KqZTwI=
|
lllkpkclM9LH5dLrw4ArdTjS9zAjzY/02WkphHhICHXt3KqZTwI=
|
||||||
-----END CERTIFICATE-----`;
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
/*
|
// Cross-checked against `openssl x509 -pubkey -noout` on ED25519_CERT.
|
||||||
const ED25519_PUBKEY = `-----BEGIN PUBLIC KEY-----
|
const ED25519_PUBKEY = `-----BEGIN PUBLIC KEY-----
|
||||||
MCowBQYDK2VwAyEAELP6AflXwsuZ5q4NDIO0LP2iCdKRvds4nwsUmRhOw3g=
|
MCowBQYDK2VwAyEAELP6AflXwsuZ5q4NDIO0LP2iCdKRvds4nwsUmRhOw3g=
|
||||||
-----END PUBLIC KEY-----`;
|
-----END PUBLIC KEY-----`;
|
||||||
*/
|
|
||||||
|
|
||||||
const ED448_CERT = `-----BEGIN CERTIFICATE-----
|
const ED448_CERT = `-----BEGIN CERTIFICATE-----
|
||||||
MIIBijCCAQqgAwIBAgIUZaCS7zEjOnQ7O4KUFym6fJF5vl8wBQYDK2VxMBUxEzAR
|
MIIBijCCAQqgAwIBAgIUZaCS7zEjOnQ7O4KUFym6fJF5vl8wBQYDK2VxMBUxEzAR
|
||||||
@ -117,12 +116,12 @@ VkLqpoDNMRcM3Eb6h3AJpQM0oxGj8q9arjDXqJkXgaO2e0tVn8KKVfy7S8qO72Kd
|
|||||||
rWzZowcOjnWKhXm7JgA=
|
rWzZowcOjnWKhXm7JgA=
|
||||||
-----END CERTIFICATE-----`;
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
/*
|
// Cross-checked against the SPKI extracted directly from ED448_CERT's DER
|
||||||
|
// bytes (openssl 3.6.2 in this environment does not support Ed448).
|
||||||
const ED448_PUBKEY = `-----BEGIN PUBLIC KEY-----
|
const ED448_PUBKEY = `-----BEGIN PUBLIC KEY-----
|
||||||
MEMwBQYDK2VxAzoAVN8kG0TMVyGOu/OvBTe8H0Wi4HJrQAlSv4XLwJbkuoi4EeRl
|
MEMwBQYDK2VxAzoAVN8kG0TMVyGOu/OvBTe8H0Wi4HJrQAlSv4XLwJbkuoi4EeRl
|
||||||
EHQwXsNYLZTtY2Jra6AWhbVYYaEA
|
EHQwXsNYLZTtY2Jra6AWhbVYYaEA
|
||||||
-----END PUBLIC KEY-----`
|
-----END PUBLIC KEY-----`;
|
||||||
*/
|
|
||||||
|
|
||||||
TestRegister.addTests([
|
TestRegister.addTests([
|
||||||
{
|
{
|
||||||
@ -141,7 +140,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Certificate: RSA",
|
name: "Public Key from Certificate: RSA",
|
||||||
input: RSA_CERT,
|
input: RSA_CERT,
|
||||||
expectedOutput: (RSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (RSA_PUBKEY + "\n").replace(/\r/g, ""),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Certificate",
|
op: "Public Key from Certificate",
|
||||||
@ -154,7 +153,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Certificate: EC",
|
name: "Public Key from Certificate: EC",
|
||||||
input: EC_P256_CERT,
|
input: EC_P256_CERT,
|
||||||
expectedOutput: (EC_P256_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (EC_P256_PUBKEY + "\n").replace(/\r/g, ""),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Certificate",
|
op: "Public Key from Certificate",
|
||||||
@ -167,7 +166,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Certificate: DSA",
|
name: "Public Key from Certificate: DSA",
|
||||||
input: DSA_CERT,
|
input: DSA_CERT,
|
||||||
expectedOutput: (DSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (DSA_PUBKEY + "\n").replace(/\r/g, ""),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Certificate",
|
op: "Public Key from Certificate",
|
||||||
@ -180,7 +179,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Certificate: Ed25519",
|
name: "Public Key from Certificate: Ed25519",
|
||||||
input: ED25519_CERT,
|
input: ED25519_CERT,
|
||||||
expectedOutput: "Unsupported public key type",
|
expectedOutput: ED25519_PUBKEY + "\n",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Certificate",
|
op: "Public Key from Certificate",
|
||||||
@ -191,7 +190,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Certificate: Ed448",
|
name: "Public Key from Certificate: Ed448",
|
||||||
input: ED448_CERT,
|
input: ED448_CERT,
|
||||||
expectedOutput: "Unsupported public key type",
|
expectedOutput: ED448_PUBKEY + "\n",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Certificate",
|
op: "Public Key from Certificate",
|
||||||
@ -204,7 +203,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Certificate: Multiple certificates",
|
name: "Public Key from Certificate: Multiple certificates",
|
||||||
input: RSA_CERT + "\n" + EC_P256_CERT,
|
input: RSA_CERT + "\n" + EC_P256_CERT,
|
||||||
expectedOutput: (RSA_PUBKEY + "\n" + EC_P256_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (RSA_PUBKEY + "\n" + EC_P256_PUBKEY + "\n").replace(/\r/g, ""),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Certificate",
|
op: "Public Key from Certificate",
|
||||||
|
|||||||
@ -147,7 +147,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: RSA PKCS#1",
|
name: "Public Key from Private Key: RSA PKCS#1",
|
||||||
input: RSA_PRIVKEY_PKCS1,
|
input: RSA_PRIVKEY_PKCS1,
|
||||||
expectedOutput: (RSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (RSA_PUBKEY + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
@ -158,7 +158,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: RSA PKCS#8",
|
name: "Public Key from Private Key: RSA PKCS#8",
|
||||||
input: RSA_PRIVKEY_PKCS8,
|
input: RSA_PRIVKEY_PKCS8,
|
||||||
expectedOutput: (RSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (RSA_PUBKEY + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
@ -171,7 +171,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: EC SEC1",
|
name: "Public Key from Private Key: EC SEC1",
|
||||||
input: EC_P256_PRIVKEY_SEC1,
|
input: EC_P256_PRIVKEY_SEC1,
|
||||||
expectedOutput: (EC_P256_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (EC_P256_PUBKEY + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
@ -182,7 +182,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: EC PKCS#8",
|
name: "Public Key from Private Key: EC PKCS#8",
|
||||||
input: EC_P256_PRIVKEY_PKCS8,
|
input: EC_P256_PRIVKEY_PKCS8,
|
||||||
expectedOutput: (EC_P256_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (EC_P256_PUBKEY + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
@ -195,7 +195,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: DSA Traditional",
|
name: "Public Key from Private Key: DSA Traditional",
|
||||||
input: DSA_PRIVKEY_TRAD,
|
input: DSA_PRIVKEY_TRAD,
|
||||||
expectedOutput: (DSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (DSA_PUBKEY + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
@ -219,7 +219,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: Ed25519",
|
name: "Public Key from Private Key: Ed25519",
|
||||||
input: ED25519_PRIVKEY,
|
input: ED25519_PRIVKEY,
|
||||||
expectedOutput: "Unsupported key type: Error: malformed PKCS8 private key(code:004)",
|
expectedOutput: "Unsupported key type: Error: Provided key is not an EC key.",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
@ -230,7 +230,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: Ed448",
|
name: "Public Key from Private Key: Ed448",
|
||||||
input: ED448_PRIVKEY,
|
input: ED448_PRIVKEY,
|
||||||
expectedOutput: "Unsupported key type: Error: malformed PKCS8 private key(code:004)",
|
expectedOutput: "Unsupported key type: Error: Provided key is not an EC key.",
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
@ -243,7 +243,7 @@ TestRegister.addTests([
|
|||||||
{
|
{
|
||||||
name: "Public Key from Private Key: Multiple keys",
|
name: "Public Key from Private Key: Multiple keys",
|
||||||
input: RSA_PRIVKEY_PKCS8 + "\n" + EC_P256_PRIVKEY_PKCS8,
|
input: RSA_PRIVKEY_PKCS8 + "\n" + EC_P256_PRIVKEY_PKCS8,
|
||||||
expectedOutput: (RSA_PUBKEY + "\n" + EC_P256_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
expectedOutput: (RSA_PUBKEY + "\n" + EC_P256_PUBKEY + "\n"),
|
||||||
recipeConfig: [
|
recipeConfig: [
|
||||||
{
|
{
|
||||||
op: "Public Key from Private Key",
|
op: "Public Key from Private Key",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user