PR 5 — X.509 / CSR / CRL parsing (PublicKey bundle).
This commit is contained in:
parent
65181be825
commit
f61d26f189
@ -8,14 +8,15 @@
|
|||||||
- [x] PR 2 — SM2 rewrite
|
- [x] PR 2 — SM2 rewrite
|
||||||
- [x] PR 3 — ECDSA primitives
|
- [x] PR 3 — ECDSA primitives
|
||||||
- [x] PR 4 — PEM/JWK conversion + key extraction
|
- [x] PR 4 — PEM/JWK conversion + key extraction
|
||||||
- [ ] PR 5 — X.509 / CSR / CRL parsing
|
- [x] 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 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 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.
|
- **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
|
## Context
|
||||||
|
|
||||||
@ -240,6 +241,20 @@ 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 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
|
### 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).
|
- 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.
|
- **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.
|
||||||
|
|||||||
@ -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 jsrsasign-style `{ 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
513
src/core/lib/X509.mjs
Normal file
513
src/core/lib/X509.mjs
Normal file
@ -0,0 +1,513 @@
|
|||||||
|
/**
|
||||||
|
* Shared X.509 / CSR / CRL helpers built on @peculiar/x509 + @peculiar/asn1-*.
|
||||||
|
*
|
||||||
|
* Used by ParseX509Certificate / PubKeyFromCert / ParseCSR / ParseX509CRL.
|
||||||
|
* Replaces the jsrsasign X.509 plumbing.
|
||||||
|
*
|
||||||
|
* @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 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 jsrsasign-style display name
|
||||||
|
* (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 ---------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Uint8Array to a lowercase hex string.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function bytesToHex(bytes) {
|
||||||
|
let out = "";
|
||||||
|
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
||||||
|
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}
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
@ -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. Mirrors the legacy
|
||||||
|
* jsrsasign labelling: "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 the jsrsasign 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -142,6 +142,7 @@ import "./tests/ParityBit.mjs";
|
|||||||
import "./tests/PHPSerialize.mjs";
|
import "./tests/PHPSerialize.mjs";
|
||||||
import "./tests/PowerSet.mjs";
|
import "./tests/PowerSet.mjs";
|
||||||
import "./tests/Protobuf.mjs";
|
import "./tests/Protobuf.mjs";
|
||||||
|
import "./tests/ParseX509Certificate.mjs";
|
||||||
import "./tests/PubKeyFromCert.mjs";
|
import "./tests/PubKeyFromCert.mjs";
|
||||||
import "./tests/PubKeyFromPrivKey.mjs";
|
import "./tests/PubKeyFromPrivKey.mjs";
|
||||||
import "./tests/Rabbit.mjs";
|
import "./tests/Rabbit.mjs";
|
||||||
|
|||||||
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 jsrsasign → @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,9 @@ HRMBAf8EBTADAQH/MAUGAytlcANBAI/+03iVq4yJ+DaLVs61w41cVX2UxKvquSzv
|
|||||||
lllkpkclM9LH5dLrw4ArdTjS9zAjzY/02WkphHhICHXt3KqZTwI=
|
lllkpkclM9LH5dLrw4ArdTjS9zAjzY/02WkphHhICHXt3KqZTwI=
|
||||||
-----END CERTIFICATE-----`;
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
/*
|
|
||||||
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 +115,10 @@ VkLqpoDNMRcM3Eb6h3AJpQM0oxGj8q9arjDXqJkXgaO2e0tVn8KKVfy7S8qO72Kd
|
|||||||
rWzZowcOjnWKhXm7JgA=
|
rWzZowcOjnWKhXm7JgA=
|
||||||
-----END CERTIFICATE-----`;
|
-----END CERTIFICATE-----`;
|
||||||
|
|
||||||
/*
|
|
||||||
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 +137,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 +150,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 +163,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 +176,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 +187,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 +200,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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user