PEM/JWK conversion + key extraction (PublicKey bundle).
This commit is contained in:
parent
7f33ed5b91
commit
65181be825
@ -7,13 +7,14 @@
|
||||
- [x] PR 1 — Setup + ASN.1 utilities
|
||||
- [x] PR 2 — SM2 rewrite
|
||||
- [x] PR 3 — ECDSA primitives
|
||||
- [ ] PR 4 — PEM/JWK conversion + key extraction
|
||||
- [x] PR 4 — PEM/JWK conversion + key extraction
|
||||
- [ ] PR 5 — X.509 / CSR / CRL parsing
|
||||
- [ ] PR 6 — Removal
|
||||
|
||||
_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 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 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
|
||||
@ -239,6 +240,16 @@ After **PR 6:**
|
||||
|
||||
Record deviations from the original plan here, newest at the top. One bullet per change: what changed, why, and which PR.
|
||||
|
||||
### PR 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.
|
||||
|
||||
735
src/core/lib/KeyConvert.mjs
Normal file
735
src/core/lib/KeyConvert.mjs
Normal file
@ -0,0 +1,735 @@
|
||||
/**
|
||||
* 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. Replaces the jsrsasign key-format-conversion plumbing.
|
||||
*
|
||||
* @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 {
|
||||
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;
|
||||
|
||||
const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
|
||||
|
||||
// ----- 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).
|
||||
*
|
||||
* @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 x (with p/q/g in the algorithm
|
||||
// parameters). jsrsasign rejected this layout because no y was
|
||||
// present and the existing tests assert that we do the same — we
|
||||
// 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(derToPem(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(derToPem(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 derToPem(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 derToPem(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 derToPem(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),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap raw DER bytes in a PEM envelope with LF line endings.
|
||||
*
|
||||
* @param {Uint8Array} bytes
|
||||
* @param {string} label
|
||||
* @returns {string}
|
||||
*/
|
||||
export 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`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
let out = "";
|
||||
let i = 0;
|
||||
while (i < bytes.length) {
|
||||
const b1 = bytes[i++];
|
||||
const b2 = i < bytes.length ? bytes[i++] : -1;
|
||||
const b3 = i < bytes.length ? bytes[i++] : -1;
|
||||
out += BASE64URL_ALPHABET[b1 >> 2];
|
||||
out += BASE64URL_ALPHABET[((b1 & 0x03) << 4) | (b2 < 0 ? 0 : (b2 >> 4))];
|
||||
if (b2 < 0) break;
|
||||
out += BASE64URL_ALPHABET[((b2 & 0x0f) << 2) | (b3 < 0 ? 0 : (b3 >> 6))];
|
||||
if (b3 < 0) break;
|
||||
out += BASE64URL_ALPHABET[b3 & 0x3f];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base64url-encoded string to bytes. Strips standard base64
|
||||
* padding if present.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function b64urlToBytes(str) {
|
||||
const cleaned = str.replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
|
||||
const pad = cleaned.length % 4 === 0 ? "" : "=".repeat(4 - (cleaned.length % 4));
|
||||
const padded = cleaned + pad;
|
||||
if (typeof Buffer !== "undefined") {
|
||||
return new Uint8Array(Buffer.from(padded, "base64"));
|
||||
}
|
||||
const bin = atob(padded);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
@ -4,17 +4,17 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import r from "jsrsasign";
|
||||
import Operation from "../Operation.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() {
|
||||
super();
|
||||
@ -45,36 +45,27 @@ class PEMToJWK extends Operation {
|
||||
|
||||
let keys = [];
|
||||
if (Array.isArray(inputJson)) {
|
||||
// list of keys => transform all keys
|
||||
keys = inputJson;
|
||||
} else if (Array.isArray(inputJson.keys)) {
|
||||
// JSON Web Key Set => transform all keys
|
||||
keys = inputJson.keys;
|
||||
} else if (typeof inputJson === "object") {
|
||||
// single key
|
||||
} else if (typeof inputJson === "object" && inputJson !== null) {
|
||||
keys.push(inputJson);
|
||||
} else {
|
||||
throw new OperationError("Input is not a JSON Web Key");
|
||||
}
|
||||
|
||||
let output = "";
|
||||
for (let i=0; i<keys.length; i++) {
|
||||
const jwk = keys[i];
|
||||
if (typeof jwk.kty !== "string") {
|
||||
for (const jwk of keys) {
|
||||
if (!jwk || typeof jwk.kty !== "string") {
|
||||
throw new OperationError("Invalid JWK format");
|
||||
} else if ("|RSA|EC|".indexOf(jwk.kty) === -1) {
|
||||
throw new OperationError(`Unsupported JWK key type '${inputJson.kty}'`);
|
||||
}
|
||||
|
||||
const key = r.KEYUTIL.getKey(jwk);
|
||||
const pem = key.isPrivate ? r.KEYUTIL.getPEM(key, "PKCS8PRV") : r.KEYUTIL.getPEM(key);
|
||||
|
||||
// PEM ends with '\n', so a new key always starts on a new line
|
||||
output += pem;
|
||||
if (jwk.kty !== "RSA" && jwk.kty !== "EC") {
|
||||
throw new OperationError(`Unsupported JWK key type '${jwk.kty}'`);
|
||||
}
|
||||
output += keyInfoToPem(keyFromJwk(jwk));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
export default PEMToJWK;
|
||||
export default JWKToPem;
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import r from "jsrsasign";
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import { parseKeyPem, parseCertPublicKey, keyToJwk } from "../lib/KeyConvert.mjs";
|
||||
|
||||
/**
|
||||
* PEM to JWK operation
|
||||
@ -54,32 +54,25 @@ class PEMToJWK extends Operation {
|
||||
}
|
||||
|
||||
const pem = input.substring(match.index, indexFooter + footer.length);
|
||||
|
||||
let info;
|
||||
if (match[1].indexOf("KEY") !== -1) {
|
||||
if (header === "-----BEGIN RSA PUBLIC KEY-----") {
|
||||
throw new OperationError("Unsupported RSA public key format. Only PKCS#8 is supported.");
|
||||
}
|
||||
|
||||
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);
|
||||
info = parseKeyPem(pem);
|
||||
} else if (match[1] === "CERTIFICATE") {
|
||||
const cert = new r.X509();
|
||||
cert.readCertPEM(pem);
|
||||
const key = cert.getPublicKey();
|
||||
const jwk = r.KEYUTIL.getJWKFromKey(key);
|
||||
if (output.length > 0) {
|
||||
output += "\n";
|
||||
}
|
||||
output += JSON.stringify(jwk);
|
||||
info = parseCertPublicKey(pem);
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -4,9 +4,13 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import r from "jsrsasign";
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import {
|
||||
parseKeyPem,
|
||||
derivePublicKeyInfo,
|
||||
keyInfoToPem,
|
||||
} from "../lib/KeyConvert.mjs";
|
||||
|
||||
/**
|
||||
* Public Key from Private Key operation
|
||||
@ -39,7 +43,6 @@ class PubKeyFromPrivKey extends Operation {
|
||||
let match;
|
||||
const regex = /-----BEGIN ((RSA |EC |DSA )?PRIVATE KEY)-----/g;
|
||||
while ((match = regex.exec(input)) !== null) {
|
||||
// find corresponding end tag
|
||||
const indexBase64 = match.index + match[0].length;
|
||||
const footer = `-----END ${match[1]}-----`;
|
||||
const indexFooter = input.indexOf(footer, indexBase64);
|
||||
@ -48,32 +51,15 @@ class PubKeyFromPrivKey extends Operation {
|
||||
}
|
||||
|
||||
const privKeyPem = input.substring(match.index, indexFooter + footer.length);
|
||||
let privKey;
|
||||
let privInfo;
|
||||
try {
|
||||
privKey = r.KEYUTIL.getKey(privKeyPem);
|
||||
privInfo = parseKeyPem(privKeyPem);
|
||||
} catch (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
|
||||
output += pubKeyPem;
|
||||
const pubInfo = derivePublicKeyInfo(privInfo);
|
||||
output += keyInfoToPem(pubInfo);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
@ -290,7 +290,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "JWK to PEM: RSA Private Key",
|
||||
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: [
|
||||
{
|
||||
op: "JWK to PEM",
|
||||
@ -301,7 +301,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "JWK to PEM: RSA Public Key",
|
||||
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: [
|
||||
{
|
||||
op: "JWK to PEM",
|
||||
@ -314,7 +314,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "JWK to PEM: EC Private Key",
|
||||
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: [
|
||||
{
|
||||
op: "JWK to PEM",
|
||||
@ -325,7 +325,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "JWK to PEM: EC Public Key",
|
||||
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: [
|
||||
{
|
||||
op: "JWK to PEM",
|
||||
@ -337,7 +337,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "JWK to PEM: Array of keys",
|
||||
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: [
|
||||
{
|
||||
op: "JWK to PEM",
|
||||
@ -348,7 +348,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "JWK to PEM: JSON Web Key Set",
|
||||
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: [
|
||||
{
|
||||
op: "JWK to PEM",
|
||||
|
||||
@ -147,7 +147,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: RSA PKCS#1",
|
||||
input: RSA_PRIVKEY_PKCS1,
|
||||
expectedOutput: (RSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
||||
expectedOutput: (RSA_PUBKEY + "\n"),
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
@ -158,7 +158,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: RSA PKCS#8",
|
||||
input: RSA_PRIVKEY_PKCS8,
|
||||
expectedOutput: (RSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
||||
expectedOutput: (RSA_PUBKEY + "\n"),
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
@ -171,7 +171,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: EC 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: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
@ -182,7 +182,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: EC PKCS#8",
|
||||
input: EC_P256_PRIVKEY_PKCS8,
|
||||
expectedOutput: (EC_P256_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
||||
expectedOutput: (EC_P256_PUBKEY + "\n"),
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
@ -195,7 +195,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: DSA Traditional",
|
||||
input: DSA_PRIVKEY_TRAD,
|
||||
expectedOutput: (DSA_PUBKEY + "\n").replace(/\r/g, "").replace(/\n/g, "\r\n"),
|
||||
expectedOutput: (DSA_PUBKEY + "\n"),
|
||||
recipeConfig: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
@ -219,7 +219,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: Ed25519",
|
||||
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: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
@ -230,7 +230,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: Ed448",
|
||||
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: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
@ -243,7 +243,7 @@ TestRegister.addTests([
|
||||
{
|
||||
name: "Public Key from Private Key: Multiple keys",
|
||||
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: [
|
||||
{
|
||||
op: "Public Key from Private Key",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user