diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index a2bd2d08c8..9498ae937d 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -204,6 +204,7 @@ "Parse CSR", "Public Key from Certificate", "Public Key from Private Key", + "Public Key from CSR", "SM2 Encrypt", "SM2 Decrypt" ] diff --git a/src/core/operations/PubKeyFromCSR.mjs b/src/core/operations/PubKeyFromCSR.mjs new file mode 100644 index 0000000000..2018006bc4 --- /dev/null +++ b/src/core/operations/PubKeyFromCSR.mjs @@ -0,0 +1,79 @@ +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import forge from "node-forge"; + +const { asn1, pki, util } = forge; + +/** + * Public Key from CSR operation + */ +class PubKeyFromCSR extends Operation { + + /** + * PubKeyFromCSR constructor + */ + constructor() { + super(); + this.name = "Public Key from CSR"; + this.module = "PublicKey"; + this.description = "Extracts the Public Key from a Certificate Signing Request. Currently only supports ECDSA and RSA."; + this.infoURL = "https://en.wikipedia.org/wiki/Certificate_signing_request"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + this.checks = []; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let output = ""; + let match; + const regex = /-----BEGIN (CERTIFICATE REQUEST)-----/g; + while ((match = regex.exec(input)) !== null) { + const indexBase64 = match.index + match[0].length; + const footer = `-----END ${match[1]}-----`; + const indexFooter = input.indexOf(footer, indexBase64); + if (indexFooter === -1) { + throw new OperationError(`CSR footer '${footer}' not found`); + } + const csrString = input.substring(match.index, indexFooter + footer.length); + + let pubKeyPem; + try { + // RSA + const csr = pki.certificationRequestFromPem(csrString); + pubKeyPem = pki.publicKeyToPem(csr.publicKey); + } catch (e) { + if (!e.message.includes("OID is not RSA")) { + throw new OperationError(`Failed to parse CSR or extract public key: ${e}`); + } + // EC + try { + const csrDer = util.decode64( + csrString + .replace("-----BEGIN CERTIFICATE REQUEST-----", "") + .replace("-----END CERTIFICATE REQUEST-----", "") + .replace(/\s+/g, "") + ); + const csrAsn1 = asn1.fromDer(csrDer); + const certReqInfo = csrAsn1.value[0]; + const spki = certReqInfo.value[2]; + const spkiDer = asn1.toDer(spki).getBytes(); + const spkiB64 = util.encode64(spkiDer); + pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${spkiB64.match(/.{1,64}/g).join("\n")}\n-----END PUBLIC KEY-----\n`; + } catch (err) { + throw new OperationError(`Failed to parse CSR or extract public key: ${err}`); + } + } + + output += pubKeyPem; + } + return output; + } +} + +export default PubKeyFromCSR; diff --git a/tests/operations/tests/PubKeyFromCSR.mjs b/tests/operations/tests/PubKeyFromCSR.mjs new file mode 100644 index 0000000000..ff9f0a0747 --- /dev/null +++ b/tests/operations/tests/PubKeyFromCSR.mjs @@ -0,0 +1,167 @@ +import TestRegister from "../../lib/TestRegister.mjs"; + +const RSA_2048_CSR = `-----BEGIN CERTIFICATE REQUEST----- +MIICyzCCAbMCAQAwTzELMAkGA1UEBhMCTkExCzAJBgNVBAgMAm5hMQswCQYDVQQH +DAJuYTESMBAGA1UECgwJY3liZXJjaGVmMRIwEAYDVQQDDAlnaXRodWIuaW8wggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCSFAA8f3IrypTeFiLyGIk/JyYG +VxSfyouNIppgV3QaUr3SuUyCIa8U5BVgzrrLc9NeeYaKXI5TFOq3IrhjLZqrKvcZ +jo6uxLwjwSC8qI5V3f147qF6E8/P18IaZtJn0XJHEWY8zZ1u9wMHURB4iC6juszg +4UhYwAgqKWzRl9ON8aqpXSxp01eUtVX+Ve4+GixKWCZfxMjLWZ8T1rzYUfC/W0wl +1PFJPVGBQKBeBTQaKERgLIjNX5Qk/GvFwt3bUBd34GgH/CybhagP3GBQF/+ZJ7Fc +Wu4N4tF7Gxn6IavjPEIJ86DexgrR9WugzcqBwmdNMA6bK9A4XagekY9ao4RFAgMB +AAGgNzA1BgkqhkiG9w0BCQ4xKDAmMCQGA1UdEQQdMBuCCWdpdGh1Yi5pb4IOZ2No +cS5naXRodWIuaW8wDQYJKoZIhvcNAQELBQADggEBADe+eaTZBg+JOcMYecO+Mf5e +4DbJd1r4bg39UMfRBa3hEq2EZZk2IfLfmU2YDGvzt/ZUQF4QFnW0ih4bBLkXuSxw +alA3BzMeB9Br/j5fAAo+4xm6F4qquzozznFWNMqsnuv4j9NdAU5WqqEkWnVTfQqh +myh7wbLev8yPjAL+WSKoN45MuOlOrLBJp+lr3LlEWconRAfHpHPPhYcLbieaUZgx +/YJEHm4iiaJpPxBOxXAenVovncTCH6XRZsdsria8wuyTZH4hlg3so7gc8C6nmHvl +ia/tkQICn+jPRTFtN1Bkn+SdUKVx1W5HTKy4yTE3Km1yh0nPUS5MGBBFgWnKANo= +-----END CERTIFICATE REQUEST-----`; + +const RSA_2048_CSR_PUB_KEY = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkhQAPH9yK8qU3hYi8hiJ +PycmBlcUn8qLjSKaYFd0GlK90rlMgiGvFOQVYM66y3PTXnmGilyOUxTqtyK4Yy2a +qyr3GY6OrsS8I8EgvKiOVd39eO6hehPPz9fCGmbSZ9FyRxFmPM2dbvcDB1EQeIgu +o7rM4OFIWMAIKils0ZfTjfGqqV0sadNXlLVV/lXuPhosSlgmX8TIy1mfE9a82FHw +v1tMJdTxST1RgUCgXgU0GihEYCyIzV+UJPxrxcLd21AXd+BoB/wsm4WoD9xgUBf/ +mSexXFruDeLRexsZ+iGr4zxCCfOg3sYK0fVroM3KgcJnTTAOmyvQOF2oHpGPWqOE +RQIDAQAB +-----END PUBLIC KEY-----`; + +/* RSA 2048 Private Key +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCSFAA8f3IrypTe +FiLyGIk/JyYGVxSfyouNIppgV3QaUr3SuUyCIa8U5BVgzrrLc9NeeYaKXI5TFOq3 +IrhjLZqrKvcZjo6uxLwjwSC8qI5V3f147qF6E8/P18IaZtJn0XJHEWY8zZ1u9wMH +URB4iC6juszg4UhYwAgqKWzRl9ON8aqpXSxp01eUtVX+Ve4+GixKWCZfxMjLWZ8T +1rzYUfC/W0wl1PFJPVGBQKBeBTQaKERgLIjNX5Qk/GvFwt3bUBd34GgH/CybhagP +3GBQF/+ZJ7FcWu4N4tF7Gxn6IavjPEIJ86DexgrR9WugzcqBwmdNMA6bK9A4Xage +kY9ao4RFAgMBAAECggEAA96TwwZ9N7u+BcQAWPldaVbYIwLbgQAUgkCQZkzqvmfC +r3pJFIlf4eXIyy+uswT2bGI7th6NhpXfQcqhp77lgfM5aGvmS6racPgErfqpCo0+ +0Z1AmcM8lfzZH2np2OYraMaFNscbjHzuj5sOHKM+2QdxteNBz1gG31cJkuO6rt/V +qr3Fy4twJ+htTTbmX4ArVpUaY5F+eZu2qw4kOVDrNnPpz6LKx5UVBFszvW8/8trr +B03ppkloJUUda5aKkKKiyYpC/xOoCBSc9mbQtptiWo/8tMm7E/AfNy1oUeNp3+M/ ++vO7sj5dA2Xr1g/76j/tp43cxvzv89dYdmq/sf6MYQKBgQDMXD9mfQf8FJqL09HH +le6N2xD0K8V2RCbdp5YOPZY6jkw3howddvSzcpQI3qsjGLLc7CrdhfsuheSebPhe +D5SKYejtPNNIcSA8xitjZPtzsi3ELhOyVkUeHaIa7itq35jnSOnc8haZMhhuzkWS +68iHk9ijBwOKvK1fTfXK232LMwKBgQC2/ZFOlCIce0y/5XkBJEGdcGSLw1ncGSlG +6cYY3AoV14dfmeMD1wXf6OQGTC7Nc3gTXXP2KXLI6dHNPkC+cU6DCD7GGThb8kOk +plUZnKVAs+copHspL3nYCjXTgDQ5cvTGX8DSYSyap7BFYLcdTXI4R5HEhb9uqJ8i +NsVWFTtypwKBgQCJus47+55LBXPXM04aDnF1h6QYe/ucJnhvQMhAFr/N/SNe9L4w +CYEIA/vDMpbyk23QuRZ2sBrGkxSutVB6zFNXJH/AjBL1qtCIRSLu3RsfMYHoywkZ +U01H677aGZSHdeTuU9TRxRL38qxG2ZxIVcKTpVAHJ+36LglGxxsVufIVwwKBgBsP +zNluFs1XfrYyXX7JudpqsLPqo/Nk1THjiKRMhkFMqnx86ZG7zuaaLn6v7Yv8s5lJ +jMiuwIbt7VUJC9IeN5oxMfdh62/NmCtVXeh3vgifkmP0TzJ8DuzgNa2dnBuS4Jgl +uQJj1JDak7ru3qW6ulWQYAJMNU9MKJyKtQxR/4SpAoGAOHsyTmptmLO85l39dTBV +RJsYfp24fp/qM9QiB9y4VOauuokqrxHijEi5ZhPIIgsyuk45b31lGARj+SXBdWOf +oKSfwh7rHd7vFqKumRiZydfszbXxtHvLDFi0oukYDI16afOb3y5tmuSdyv8Y6ZsT +H2zqBm8koIaSdtBUYfsJM08= +-----END PRIVATE KEY----- +*/ + +// ------------------------------------------------------------- + +const EC_PUB_KEY = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtV2tWhuWHVvaZGpAfdbz6XK7YS8b +qUx6D7VWiQkYidpzXWCTlnprARCO/2OK5t3EhUwZyPZueFr7lgIAFRFDkw== +-----END PUBLIC KEY-----`; + +const EC_CSR = `-----BEGIN CERTIFICATE REQUEST----- +MIIBADCBpwIBADBFMQswCQYDVQQGEwJOQTETMBEGA1UECAwKU29tZS1TdGF0ZTEh +MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEtV2tWhuWHVvaZGpAfdbz6XK7YS8bqUx6D7VWiQkYidpzXWCT +lnprARCO/2OK5t3EhUwZyPZueFr7lgIAFRFDk6AAMAoGCCqGSM49BAMCA0gAMEUC +IQCEswjMY+jmBXtUogwoZ9GjOb9g72ZAiiL3fbUIKKOuigIgK10fc/M2QSgfv9x6 +r18dKfOku2+q1Sa9M08iee6hGUI= +-----END CERTIFICATE REQUEST-----`; + +/* EC Private Key +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIA42NtMH8T9dPpbKlptxNcUmAIcv112fwJkn9XRYxbdMoAoGCCqGSM49 +AwEHoUQDQgAEtV2tWhuWHVvaZGpAfdbz6XK7YS8bqUx6D7VWiQkYidpzXWCTlnpr +ARCO/2OK5t3EhUwZyPZueFr7lgIAFRFDkw== +-----END EC PRIVATE KEY----- +*/ + +TestRegister.addTests([ + + { + name: "Public Key from CSR: RSA 2048 returns correct public key", + input: RSA_2048_CSR, + expectedOutput: RSA_2048_CSR_PUB_KEY, + recipeConfig: [ + { + op: "Public Key from CSR", + args: [], + } + ], + }, + { + name: "Public Key from CSR: two CSRs returns public key twice", + input: `${RSA_2048_CSR}\n${RSA_2048_CSR}`, + expectedOutput: RSA_2048_CSR_PUB_KEY + RSA_2048_CSR_PUB_KEY, + recipeConfig: [ + { + op: "Public Key from CSR", + args: [], + } + ], + }, + { + name: "Public Key from CSR: missing footer throws", + input: RSA_2048_CSR.substring(0, RSA_2048_CSR.lastIndexOf("-----END")), + expectedOutput: "CSR footer '-----END CERTIFICATE REQUEST-----' not found", + recipeConfig: [ + { + op: "Public Key from CSR", + args: [], + } + ], + }, + { + name: "Public Key from CSR: empty input returns empty output", + input: "", + expectedOutput: "", + recipeConfig: [ + { + op: "Public Key from CSR", + args: [], + } + ], + }, + { + name: "Public Key from CSR: no PEM block returns empty output", + input: "this is not a CSR", + expectedOutput: "", + recipeConfig: [ + { + op: "Public Key from CSR", + args: [], + } + ], + }, + { + name: "Public Key from CSR: wrong PEM type is ignored", + input: `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEA +-----END PRIVATE KEY-----`, + expectedOutput: "", + recipeConfig: [ + { + op: "Public Key from CSR", + args: [], + } + ], + }, + { + name: "Public Key from CSR: EC P-256 returns correct public key", + input: EC_CSR, + expectedOutput: EC_PUB_KEY, + recipeConfig: [ + { + op: "Public Key from CSR", + args: [], + } + ], + }, +]);