Merge ac2def74d0a0605790f9303dff132a55ecc8b9d5 into 4290ea753912378913b1f3f54e0fc5720afeda5d

This commit is contained in:
rayane-ara 2026-08-10 00:29:54 -07:00 committed by GitHub
commit 7d2505c1db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 175 additions and 0 deletions

View File

@ -212,6 +212,7 @@
"RSA Verify", "RSA Verify",
"RSA Encrypt", "RSA Encrypt",
"RSA Decrypt", "RSA Decrypt",
"RSA Key Modulus",
"Generate ECDSA Key Pair", "Generate ECDSA Key Pair",
"ECDSA Signature Conversion", "ECDSA Signature Conversion",
"ECDSA Sign", "ECDSA Sign",

View File

@ -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.<br><br>This operation provides the same functionality as the <code>openssl rsa -noout -modulus</code> and <code>openssl x509 -noout -modulus</code> commands.<br><br>Accepts PEM-encoded input. Supported formats:<br><ul><li><code>-----BEGIN CERTIFICATE-----</code> (X.509 certificate)</li><li><code>-----BEGIN PRIVATE KEY-----</code> (PKCS#8 unencrypted)</li><li><code>-----BEGIN RSA PRIVATE KEY-----</code> (PKCS#1)</li><li><code>-----BEGIN PUBLIC KEY-----</code> (PKCS#8 / SubjectPublicKeyInfo)</li><li><code>-----BEGIN RSA PUBLIC KEY-----</code> (PKCS#1)</li></ul>";
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;