ECDSA primitives (Ciphers bundle)
This commit is contained in:
parent
7e302caded
commit
7f33ed5b91
@ -6,13 +6,14 @@
|
|||||||
|
|
||||||
- [x] PR 1 — Setup + ASN.1 utilities
|
- [x] PR 1 — Setup + ASN.1 utilities
|
||||||
- [x] PR 2 — SM2 rewrite
|
- [x] PR 2 — SM2 rewrite
|
||||||
- [ ] PR 3 — ECDSA primitives
|
- [x] PR 3 — ECDSA primitives
|
||||||
- [ ] PR 4 — PEM/JWK conversion + key extraction
|
- [ ] PR 4 — PEM/JWK conversion + key extraction
|
||||||
- [ ] PR 5 — X.509 / CSR / CRL parsing
|
- [ ] PR 5 — X.509 / CSR / CRL parsing
|
||||||
- [ ] PR 6 — Removal
|
- [ ] PR 6 — Removal
|
||||||
|
|
||||||
_Notes for next session:_
|
_Notes for next session:_
|
||||||
- **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 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 5 blocker:** `@peculiar/x509` v2 needs a `reflect-metadata` polyfill at every entry point — PR 1 pinned to `^1.14.3` to avoid that. Stay on v1 unless the polyfill cost gets resolved.
|
- **PR 5 blocker:** `@peculiar/x509` v2 needs a `reflect-metadata` polyfill at every entry point — PR 1 pinned to `^1.14.3` to avoid that. Stay on v1 unless the polyfill cost gets resolved.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
@ -238,6 +239,15 @@ After **PR 6:**
|
|||||||
|
|
||||||
Record deviations from the original plan here, newest at the top. One bullet per change: what changed, why, and which PR.
|
Record deviations from the original plan here, newest at the top. One bullet per change: what changed, why, and which PR.
|
||||||
|
|
||||||
|
### 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
|
### 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.
|
- `@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.
|
- 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.
|
||||||
|
|||||||
596
src/core/lib/Ecdsa.mjs
Normal file
596
src/core/lib/Ecdsa.mjs
Normal file
@ -0,0 +1,596 @@
|
|||||||
|
/**
|
||||||
|
* Shared ECDSA helpers built on @noble/curves and @peculiar/asn1-*.
|
||||||
|
*
|
||||||
|
* Used by the ECDSA Sign/Verify/Signature Conversion/Generate Key Pair
|
||||||
|
* operations. Migrated from jsrsasign.
|
||||||
|
*
|
||||||
|
* @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 { AsnParser, AsnSerializer, OctetString } from "@peculiar/asn1-schema";
|
||||||
|
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 matches how jsrsasign / CryptoJS fed strings
|
||||||
|
* into MessageDigest.update — keeping signatures interoperable for inputs
|
||||||
|
* coming out of upstream byte-producing ops, while preserving the
|
||||||
|
* (questionable) UTF-16-truncating behaviour 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. Mirrors jsrsasign's ASN1HEX.isASN1HEX.
|
||||||
|
*
|
||||||
|
* @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) — this matches the legacy
|
||||||
|
* jsrsasign ECDSA.parseSigHexInHexRS behaviour that existing tests assume.
|
||||||
|
*
|
||||||
|
* @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 derToPem(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 derToPem(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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a hex string to a Uint8Array.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function hexToBytes(hex) {
|
||||||
|
if (hex.length % 2 !== 0) throw new OperationError("Hex string has odd length");
|
||||||
|
const out = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Uint8Array to a lowercase hex string.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function bytesToHex(bytes) {
|
||||||
|
let out = "";
|
||||||
|
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap raw DER bytes in a PEM envelope with LF line endings.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @param {string} label
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function derToPem(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.match(/.{1,64}/g) || [""];
|
||||||
|
return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`;
|
||||||
|
}
|
||||||
@ -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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,12 @@
|
|||||||
|
|
||||||
import Operation from "../Operation.mjs";
|
import Operation from "../Operation.mjs";
|
||||||
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 +59,59 @@ 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) => {
|
switch (outputFormat) {
|
||||||
let internalCurveName;
|
case "PEM":
|
||||||
switch (curveName) {
|
return publicKeyToSpkiPem(pair) + privateKeyToPkcs8Pem(pair);
|
||||||
case "P-256":
|
case "DER":
|
||||||
internalCurveName = "secp256r1";
|
return bytesToHex(pair.d);
|
||||||
break;
|
case "JWK": {
|
||||||
case "P-384":
|
const pubJwk = {
|
||||||
internalCurveName = "secp384r1";
|
kty: "EC",
|
||||||
break;
|
crv: curveName,
|
||||||
case "P-521":
|
x: b64url(pair.x),
|
||||||
internalCurveName = "secp521r1";
|
y: b64url(pair.y),
|
||||||
break;
|
"key_ops": ["verify"],
|
||||||
|
kid: "PublicKey",
|
||||||
|
};
|
||||||
|
const privJwk = {
|
||||||
|
kty: "EC",
|
||||||
|
crv: curveName,
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
const keyPair = r.KEYUTIL.generateKeypair("EC", internalCurveName);
|
default:
|
||||||
|
throw new Error(`Unsupported output format: ${outputFormat}`);
|
||||||
let pubKey;
|
}
|
||||||
let privKey;
|
|
||||||
let result;
|
|
||||||
switch (outputFormat) {
|
|
||||||
case "PEM":
|
|
||||||
pubKey = r.KEYUTIL.getPEM(keyPair.pubKeyObj).replace(/\r/g, "");
|
|
||||||
privKey = r.KEYUTIL.getPEM(keyPair.prvKeyObj, "PKCS8PRV").replace(/\r/g, "");
|
|
||||||
result = pubKey + "\n" + privKey;
|
|
||||||
break;
|
|
||||||
case "DER":
|
|
||||||
result = keyPair.prvKeyObj.prvKeyHex;
|
|
||||||
break;
|
|
||||||
case "JWK":
|
|
||||||
pubKey = r.KEYUTIL.getJWKFromKey(keyPair.pubKeyObj);
|
|
||||||
pubKey.key_ops = ["verify"]; // eslint-disable-line camelcase
|
|
||||||
pubKey.kid = "PublicKey";
|
|
||||||
privKey = r.KEYUTIL.getJWKFromKey(keyPair.prvKeyObj);
|
|
||||||
privKey.key_ops = ["sign"]; // eslint-disable-line camelcase
|
|
||||||
privKey.kid = "PrivateKey";
|
|
||||||
result = JSON.stringify({keys: [privKey, pubKey]}, null, 4);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(result);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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-_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a byte array to a lowercase hex string.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function bytesToHex(bytes) {
|
||||||
|
let out = "";
|
||||||
|
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GenerateECDSAKeyPair;
|
export default GenerateECDSAKeyPair;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user