From ac2def74d0a0605790f9303dff132a55ecc8b9d5 Mon Sep 17 00:00:00 2001 From: rayane-ara Date: Sun, 15 Mar 2026 00:16:11 +0100 Subject: [PATCH] Add RSA Key Modulus operation (#1443) --- src/core/config/Categories.json | 1 + src/core/operations/RSAKeyModulus.mjs | 174 ++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/core/operations/RSAKeyModulus.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 8b62046e3..b4d014215 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -196,6 +196,7 @@ "RSA Verify", "RSA Encrypt", "RSA Decrypt", + "RSA Key Modulus", "Generate ECDSA Key Pair", "ECDSA Signature Conversion", "ECDSA Sign", diff --git a/src/core/operations/RSAKeyModulus.mjs b/src/core/operations/RSAKeyModulus.mjs new file mode 100644 index 000000000..d25353d60 --- /dev/null +++ b/src/core/operations/RSAKeyModulus.mjs @@ -0,0 +1,174 @@ +/** + * @author rayane-ara [] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import forge from "node-forge"; + +/** + * RSA Key Modulus operation + */ +class RSAKeyModulus extends Operation { + + /** + * RSAKeyModulus constructor + */ + constructor() { + super(); + + this.name = "RSA Key Modulus"; + this.module = "Crypto"; + this.description = "Extracts the modulus from an RSA private key, public key, or X.509 certificate. This is commonly used to verify if a private key corresponds to a specific X.509 certificate.

This operation provides the same functionality as the openssl rsa -noout -modulus and openssl x509 -noout -modulus commands.

Accepts PEM-encoded input. Supported formats:
"; + this.infoURL = "https://wikipedia.org/wiki/RSA_(cryptosystem)"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Output format", + type: "option", + value: ["Hex", "Decimal", "Base64"] + } + ]; + } + + /** + * Extracts the RSA modulus from a PEM-encoded RSA private key, public key, + * or X.509 certificate. + * + * Supports the following PEM headers: + * - -----BEGIN CERTIFICATE----- (X.509 certificate) + * - -----BEGIN RSA PRIVATE KEY----- (PKCS#1 private key) + * - -----BEGIN PRIVATE KEY----- (PKCS#8 unencrypted private key) + * - -----BEGIN RSA PUBLIC KEY----- (PKCS#1 public key) + * - -----BEGIN PUBLIC KEY----- (PKCS#8 / SubjectPublicKeyInfo public key) + * + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [outputFormat] = args; + + if (!input || input.trim() === "") { + throw new OperationError("No input provided. Please provide a PEM-encoded RSA key."); + } + + const pem = input.trim(); + let key = null; + + // Detect key type from PEM header + if (pem.includes("-----BEGIN CERTIFICATE-----")) { + // X.509 certificate - extract the embedded RSA public key + try { + const cert = forge.pki.certificateFromPem(pem); + key = cert.publicKey; + if (!key || !key.n) { + throw new OperationError("The certificate does not contain an RSA public key."); + } + } catch (err) { + if (err instanceof OperationError) throw err; + throw new OperationError(`Failed to parse X.509 certificate: ${err.message}`); + } + } else if (pem.includes("-----BEGIN RSA PRIVATE KEY-----")) { + // PKCS#1 RSA private key + try { + key = forge.pki.privateKeyFromPem(pem); + } catch (err) { + throw new OperationError(`Failed to parse PKCS#1 RSA private key: ${err.message}`); + } + } else if (pem.includes("-----BEGIN PRIVATE KEY-----")) { + // PKCS#8 unencrypted private key + try { + key = forge.pki.privateKeyFromPem(pem); + } catch (err) { + throw new OperationError(`Failed to parse PKCS#8 private key: ${err.message}. Note: encrypted private keys (-----BEGIN ENCRYPTED PRIVATE KEY-----) are not supported.`); + } + } else if (pem.includes("-----BEGIN RSA PUBLIC KEY-----")) { + // PKCS#1 RSA public key - forge does not support this header natively. + // We parse the ASN.1 DER directly to extract n and e, then build a + // forge public key object from those two integers. + // PKCS#1 RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER } + try { + const der = forge.util.decode64( + pem + .replace("-----BEGIN RSA PUBLIC KEY-----", "") + .replace("-----END RSA PUBLIC KEY-----", "") + .replace(/\s+/g, "") + ); + const asn1 = forge.asn1.fromDer(der); + if ( + asn1.type !== forge.asn1.Type.SEQUENCE || + !asn1.value || + asn1.value.length < 2 + ) { + throw new Error("Unexpected ASN.1 structure in PKCS#1 public key."); + } + // Each INTEGER value in the ASN.1 tree is a raw DER byte string + const nDer = asn1.value[0].value; + const eDer = asn1.value[1].value; + + // forge.jsbn.BigInteger can be constructed directly from a hex string + const n = new forge.jsbn.BigInteger(forge.util.bytesToHex(nDer), 16); + const e = new forge.jsbn.BigInteger(forge.util.bytesToHex(eDer), 16); + + key = forge.pki.rsa.setPublicKey(n, e); + } catch (err) { + throw new OperationError(`Failed to parse PKCS#1 RSA public key: ${err.message}`); + } + } else if (pem.includes("-----BEGIN PUBLIC KEY-----")) { + // PKCS#8 / SubjectPublicKeyInfo public key + try { + key = forge.pki.publicKeyFromPem(pem); + } catch (err) { + throw new OperationError(`Failed to parse PKCS#8 public key: ${err.message}`); + } + } else if (pem.includes("-----BEGIN ENCRYPTED PRIVATE KEY-----")) { + throw new OperationError("Encrypted private keys are not supported. Please decrypt your key first (e.g. with openssl pkcs8 -in key.pem -out decrypted.pem)."); + } else { + throw new OperationError("Unrecognised PEM header. Supported types:\n -----BEGIN CERTIFICATE-----\n -----BEGIN RSA PRIVATE KEY-----\n -----BEGIN PRIVATE KEY-----\n -----BEGIN RSA PUBLIC KEY-----\n -----BEGIN PUBLIC KEY-----"); + } + + // Extract the modulus BigInteger from the parsed key + // For both public and private forge keys the modulus is exposed as key.n + const n = key.n; + if (!n) { + throw new OperationError("Could not extract modulus from key. The key may not be an RSA key."); + } + + // Convert the forge BigInteger to a hex string (unsigned, big-endian) + // n.toString(16) returns lowercase hex without leading zeros + let hexModulus = n.toString(16); + + // Ensure even number of hex characters (full bytes) + if (hexModulus.length % 2 !== 0) { + hexModulus = "0" + hexModulus; + } + + // Format output according to user preference + switch (outputFormat) { + case "Hex": + // Match openssl output style: uppercase hex + return "Modulus=" + hexModulus.toUpperCase(); + + case "Decimal": + return n.toString(10); + + case "Base64": { + // Convert hex string to binary then base64 + const bytes = hexModulus.match(/.{2}/g).map(b => parseInt(b, 16)); + const binary = bytes.map(b => String.fromCharCode(b)).join(""); + return forge.util.encode64(binary); + } + + default: + return hexModulus.toUpperCase(); + } + } + +} + +export default RSAKeyModulus; +