From 7a2c9ddbc498f9b7188a52fa41be3707c4804654 Mon Sep 17 00:00:00 2001 From: CPlusSharp Date: Sun, 7 Nov 2021 15:46:25 +0100 Subject: [PATCH 1/6] Operation: Generate ECDSA Key Pair --- src/core/config/Categories.json | 1 + src/core/operations/GenerateECDSAKeyPair.mjs | 98 ++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/core/operations/GenerateECDSAKeyPair.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index a519cf09e..53b8a2f05 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -179,6 +179,7 @@ "RSA Verify", "RSA Encrypt", "RSA Decrypt", + "Generate ECDSA Key Pair", "Parse SSH Host Key", "Parse CSR" ] diff --git a/src/core/operations/GenerateECDSAKeyPair.mjs b/src/core/operations/GenerateECDSAKeyPair.mjs new file mode 100644 index 000000000..c098fe019 --- /dev/null +++ b/src/core/operations/GenerateECDSAKeyPair.mjs @@ -0,0 +1,98 @@ +/** + * @author cplussharp + * @copyright Crown Copyright 2021 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { cryptNotice } from "../lib/Crypt.mjs"; +import r from "jsrsasign"; + +/** + * Generate ECDSA Key Pair operation + */ +class GenerateECDSAKeyPair extends Operation { + + /** + * GenerateECDSAKeyPair constructor + */ + constructor() { + super(); + + this.name = "Generate ECDSA Key Pair"; + this.module = "Ciphers"; + this.description = `Generate an ECDSA key pair with a given Curve.

${cryptNotice}`; + this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Elliptic Curve", + type: "option", + value: [ + "P-256", + "P-384", + "P-521" + ] + }, + { + name: "Output Format", + type: "option", + value: [ + "PEM", + "DER", + "JWK" + ] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + async run(input, args) { + const [curveName, outputFormat] = args; + + return new Promise((resolve, reject) => { + let internalCurveName; + switch (curveName) { + case "P-256": + internalCurveName = "secp256r1"; + break; + case "P-384": + internalCurveName = "secp384r1"; + break; + case "P-521": + internalCurveName = "secp521r1"; + break; + } + const keyPair = r.KEYUTIL.generateKeypair("EC", internalCurveName); + + let pubKey; + let privKey; + let result; + switch (outputFormat) { + case "PEM": + pubKey = r.KEYUTIL.getPEM(keyPair.pubKeyObj); + privKey = r.KEYUTIL.getPEM(keyPair.prvKeyObj, "PKCS8PRV"); + result = pubKey + "\n" + privKey; + break; + case "DER": + result = keyPair.prvKeyObj.prvKeyHex; + break; + case "JWK": + pubKey = JSON.stringify(r.KEYUTIL.getJWKFromKey(keyPair.pubKeyObj)); + privKey = JSON.stringify(r.KEYUTIL.getJWKFromKey(keyPair.prvKeyObj)); + result = pubKey + "\n\n" + privKey; + break; + } + + resolve(result); + }); + } + +} + +export default GenerateECDSAKeyPair; From 8f182e4a9b149f13f703f26a85778ee8e5921db8 Mon Sep 17 00:00:00 2001 From: CPlusSharp Date: Sat, 13 Nov 2021 01:46:31 +0100 Subject: [PATCH 2/6] Sign/Verify Operations for ECDSA also an Operation for ECDSA signature conversion, as there could be multiple formats of the signature --- src/core/config/Categories.json | 3 + src/core/operations/ECDSASign.mjs | 100 +++++ .../operations/ECDSASignatureConversion.mjs | 106 +++++ src/core/operations/ECDSAVerify.mjs | 120 ++++++ tests/operations/index.mjs | 1 + tests/operations/tests/ECDSA.mjs | 397 ++++++++++++++++++ 6 files changed, 727 insertions(+) create mode 100644 src/core/operations/ECDSASign.mjs create mode 100644 src/core/operations/ECDSASignatureConversion.mjs create mode 100644 src/core/operations/ECDSAVerify.mjs create mode 100644 tests/operations/tests/ECDSA.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 53b8a2f05..98d35bbd6 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -180,6 +180,9 @@ "RSA Encrypt", "RSA Decrypt", "Generate ECDSA Key Pair", + "ECDSA Signature Conversion", + "ECDSA Sign", + "ECDSA Verify", "Parse SSH Host Key", "Parse CSR" ] diff --git a/src/core/operations/ECDSASign.mjs b/src/core/operations/ECDSASign.mjs new file mode 100644 index 000000000..d7bd32e00 --- /dev/null +++ b/src/core/operations/ECDSASign.mjs @@ -0,0 +1,100 @@ +/** + * @author cplussharp + * @copyright Crown Copyright 2021 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import r from "jsrsasign"; + +/** + * ECDSA Sign operation + */ +class ECDSASign extends Operation { + + /** + * ECDSASign constructor + */ + constructor() { + super(); + + this.name = "ECDSA Sign"; + this.module = "Ciphers"; + this.description = "Sign a plaintext message with a PEM encoded EC key."; + this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "ECDSA Private Key (PEM)", + type: "text", + value: "-----BEGIN EC PRIVATE KEY-----" + }, + { + name: "Message Digest Algorithm", + type: "option", + value: [ + "SHA-256", + "SHA-384", + "SHA-512", + "SHA-1", + "MD5" + ] + }, + { + name: "Output Format", + type: "option", + value: [ + "ASN.1 HEX", + "Concat HEX", + "JSON" + ] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [keyPem, mdAlgo, outputFormat] = args; + + if (keyPem.replace("-----BEGIN EC PRIVATE KEY-----", "").length === 0) { + throw new OperationError("Please enter a private key."); + } + + const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA"; + const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName }); + const key = r.KEYUTIL.getKey(keyPem); + if (key.type !== "EC") { + throw new OperationError("Provided key is not an EC key."); + } + if (!key.isPrivate) { + throw new OperationError("Provided key is not a private key."); + } + sig.init(key); + const signatureASN1Hex = sig.signString(input); + + let result; + switch (outputFormat) { + case "ASN.1 HEX": + result = signatureASN1Hex; + break; + case "Concat HEX": + result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); + break; + case "JSON": { + const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex); + result = JSON.stringify(signatureRS); + break; + } + } + + return result; + } +} + +export default ECDSASign; diff --git a/src/core/operations/ECDSASignatureConversion.mjs b/src/core/operations/ECDSASignatureConversion.mjs new file mode 100644 index 000000000..fe5dc3c22 --- /dev/null +++ b/src/core/operations/ECDSASignatureConversion.mjs @@ -0,0 +1,106 @@ +/** + * @author cplussharp + * @copyright Crown Copyright 2021 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import r from "jsrsasign"; + +/** + * ECDSA Sign operation + */ +class ECDSASignatureConversion extends Operation { + + /** + * ECDSASignatureConversion constructor + */ + constructor() { + super(); + + this.name = "ECDSA Signature Conversion"; + this.module = "Ciphers"; + this.description = "Convert an ECDSA signature between hex, asn1 and json."; + this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Input Format", + type: "option", + value: [ + "Auto", + "ASN.1 HEX", + "Concat HEX", + "JSON" + ] + }, + { + name: "Output Format", + type: "option", + value: [ + "ASN.1 HEX", + "Concat HEX", + "JSON" + ] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let inputFormat = args[0]; + const outputFormat = args[1]; + + // detect input format + if (inputFormat === "Auto") { + if (input.substr(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { + inputFormat = "ASN.1 HEX"; + } else if (input.indexOf("{") !== -1) { + inputFormat = "JSON"; + } else { + inputFormat = "Concat HEX"; + } + } + + // convert input to ASN.1 hex + let signatureASN1Hex; + switch (inputFormat) { + case "ASN.1 HEX": + signatureASN1Hex = input; + break; + case "Concat HEX": + signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); + break; + case "JSON": { + const inputJson = JSON.parse(input); + signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s); + break; + } + } + + // convert ASN.1 hex to output format + let result; + switch (outputFormat) { + case "ASN.1 HEX": + result = signatureASN1Hex; + break; + case "Concat HEX": + result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); + break; + case "JSON": { + const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex); + result = JSON.stringify(signatureRS); + break; + } + } + + return result; + } +} + +export default ECDSASignatureConversion; diff --git a/src/core/operations/ECDSAVerify.mjs b/src/core/operations/ECDSAVerify.mjs new file mode 100644 index 000000000..68e3a1aa2 --- /dev/null +++ b/src/core/operations/ECDSAVerify.mjs @@ -0,0 +1,120 @@ +/** + * @author cplussharp + * @copyright Crown Copyright 2021 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import r from "jsrsasign"; + +/** + * ECDSA Verify operation + */ +class ECDSAVerify extends Operation { + + /** + * ECDSAVerify constructor + */ + constructor() { + super(); + + this.name = "ECDSA Verify"; + this.module = "Ciphers"; + this.description = "Verify a message against a signature and a public PEM encoded EC key."; + this.infoURL = "https://wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Input Format", + type: "option", + value: [ + "Auto", + "ASN.1 HEX", + "Concat HEX", + "JSON" + ] + }, + { + name: "Message Digest Algorithm", + type: "option", + value: [ + "SHA-256", + "SHA-384", + "SHA-512", + "SHA-1", + "MD5" + ] + }, + { + name: "ECDSA Public Key (PEM)", + type: "text", + value: "-----BEGIN PUBLIC KEY-----" + }, + { + name: "Message", + type: "text", + value: "" + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let inputFormat = args[0]; + const [, mdAlgo, keyPem, msg] = args; + + if (keyPem.replace("-----BEGIN PUBLIC KEY-----", "").length === 0) { + throw new OperationError("Please enter a public key."); + } + + // detect input format + if (inputFormat === "Auto") { + if (input.substr(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { + inputFormat = "ASN.1 HEX"; + } else if (input.indexOf("{") !== -1) { + inputFormat = "JSON"; + } else { + inputFormat = "Concat HEX"; + } + } + + // convert to ASN.1 signature + let signatureASN1Hex; + switch (inputFormat) { + case "ASN.1 HEX": + signatureASN1Hex = input; + break; + case "Concat HEX": + signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); + break; + case "JSON": { + const inputJson = JSON.parse(input); + signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s); + break; + } + } + + // verify signature + const internalAlgorithmName = mdAlgo.replace("-", "") + "withECDSA"; + const sig = new r.KJUR.crypto.Signature({ alg: internalAlgorithmName }); + const key = r.KEYUTIL.getKey(keyPem); + if (key.type !== "EC") { + throw new OperationError("Provided key is not an EC key."); + } + if (!key.isPublic) { + throw new OperationError("Provided key is not a public key."); + } + sig.init(key); + sig.updateString(msg); + const result = sig.verify(signatureASN1Hex); + return result ? "Verified OK" : "Verification Failure"; + } +} + +export default ECDSAVerify; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index ab6dac688..96ede7a3b 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -59,6 +59,7 @@ import "./tests/Crypt.mjs"; import "./tests/CSV.mjs"; import "./tests/DateTime.mjs"; import "./tests/DefangIP.mjs"; +import "./tests/ECDSA.mjs"; import "./tests/ELFInfo.mjs"; import "./tests/Enigma.mjs"; import "./tests/ExtractEmailAddresses.mjs"; diff --git a/tests/operations/tests/ECDSA.mjs b/tests/operations/tests/ECDSA.mjs new file mode 100644 index 000000000..dd3013d35 --- /dev/null +++ b/tests/operations/tests/ECDSA.mjs @@ -0,0 +1,397 @@ +/** + * ECDSA tests. + * + * @author cplussharp + * @copyright Crown Copyright 2021 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; +import { ASCII_TEXT } from "../../samples/Ciphers.mjs"; + +const P256 = { + // openssl ecparam -name prime256v1 -genkey -noout -out p256.priv.key + privateKeyPkcs1: `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINtTjwUkgfAiSwqgcGAXWyE0ueIW6n2k395dmQZ3vGr4oAoGCCqGSM49 +AwEHoUQDQgAEDUc8A0EDNKoCYIPWMHz1yUzqE5mJgusgcAE8H6810fkJ8ZmTNiCC +a6sLgR2vD1VNh2diirWgKPH4PVMKav5e6Q== +-----END EC PRIVATE KEY-----`, + privateKeyPkcs8: `-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg21OPBSSB8CJLCqBw +YBdbITS54hbqfaTf3l2ZBne8avihRANCAAQNRzwDQQM0qgJgg9YwfPXJTOoTmYmC +6yBwATwfrzXR+QnxmZM2IIJrqwuBHa8PVU2HZ2KKtaAo8fg9Uwpq/l7p +-----END PRIVATE KEY-----`, + + // openssl ec -in p256.priv.key -pubout -out p256.pub.key + publicKey: `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDUc8A0EDNKoCYIPWMHz1yUzqE5mJ +gusgcAE8H6810fkJ8ZmTNiCCa6sLgR2vD1VNh2diirWgKPH4PVMKav5e6Q== +-----END PUBLIC KEY-----`, + + signature: { + sha256: { + asn1: "3046022100e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127022100b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d", + concat: "e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d", + json: `{"r":"00e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127","s":"00b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d"}` + } + } +}; + +// openssl pkcs8 -topk8 -in p256.priv.key -out p256.enc-priv.key -v2 des3 -v2prf hmacWithSHA1 -passout pass:Test1234 +/* const PEM_PRIV_P256_ENCRYPTED_PASS = "Test1234"; +const PEM_PRIV_P256_ENCRYPTED = `-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAg+4ckqI9Q9ZAICCAAw +DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEOnMUW15Hn/ub0OcCCj9lksEgZCk +kxaK4d430lZHovcA4ZeKTt94QcfjnIHRk65aZt93l17l52pv6n/srs3aRo/n5RV+ +wZ5sTLF0925ZQWJB5cIhzc8KQIvguGCX1znLQJJaRHyYOUXIN77AKEfALKAinBit +25paDnbXAqGn1CR3UwFWUZZW+c3UEhWhmpghQpS1tIl0KI6IAvnrGIdw2kKIouo= +-----END ENCRYPTED PRIVATE KEY-----`;*/ + +const P384 = { + privateKeyPkcs8: `-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAYo22xn2kZjN8MInom +NDsgD/zhpUwnCYch634jUgO59fN9m2lR5ekaI1XABHz39rihZANiAAQwXoCsPOLv +Nn2STUs/hpL41CQveSL3WUmJ4QdtD7UFCl1mBO6ME0xSUgIQTUNkHt5k9CpOq3x9 +r+LG5+GcisoLn7R54R+bRoGp/p1ZBeuBXoCgthvs+RFoT3OewUmA8oQ= +-----END PRIVATE KEY-----`, + publicKey: `-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMF6ArDzi7zZ9kk1LP4aS+NQkL3ki91lJ +ieEHbQ+1BQpdZgTujBNMUlICEE1DZB7eZPQqTqt8fa/ixufhnIrKC5+0eeEfm0aB +qf6dWQXrgV6AoLYb7PkRaE9znsFJgPKE +-----END PUBLIC KEY-----` +}; + +const P521 = { + privateKeyPkcs8: `-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAifBaJDqNwOtKgThc +FU34GzPQ73ubOQg9dnighpVGwA3b/KwCifimCNKDmKnXJaE04mEcxg8yzcFKausF +5I8o206hgYkDgYYABAGwpkwrBBlZOdx4u9mxqYxJvtzAHaFFAzl21WQVbAjyrqXe +nFPMkhbFpEEWr1ualPYKQkHe14AX33iU3fQ9MlBkgAAripsPbiKggAaog74cUERo +qbrUFZwMbptGgovpE6pU93h7A1wb3Vtw9DZQCgiNbwzMbdsft+p2RJ8iSxWEC6Gd +mw== +-----END PRIVATE KEY-----`, + publicKey: `-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBsKZMKwQZWTnceLvZsamMSb7cwB2h +RQM5dtVkFWwI8q6l3pxTzJIWxaRBFq9bmpT2CkJB3teAF994lN30PTJQZIAAK4qb +D24ioIAGqIO+HFBEaKm61BWcDG6bRoKL6ROqVPd4ewNcG91bcPQ2UAoIjW8MzG3b +H7fqdkSfIksVhAuhnZs= +-----END PUBLIC KEY-----` +}; + +const PEM_PPRIV_RSA512 = `-----BEGIN RSA PRIVATE KEY----- +MIIBOQIBAAJBAPKr0Dp6YdItzOfk6a7ma7L4BF4LnelMYKtboGLrk6ihtqFPZFRL +NcJi68Hvnt8stMrP50t6jqwWQ2EjMdkj6fsCAwEAAQJAOJUpM0lv36MAQR3WAwsF +F7DOy+LnigteCvaNWiNVxZ6jByB5Qb7sall/Qlu9sFI0ZwrlVcKS0kldee7JTYlL +WQIhAP3UKEfOtpTgT1tYmdhaqjxqMfxBom0Ri+rt9ajlzs6vAiEA9L85B8/Gnb7p +6Af7/wpmafL277OV4X4xBfzMR+TUzHUCIBq+VLQkInaTH6lXL3ZtLwyIf9W9MJjf +RWeuRLjT5bM/AiBF7Kw6kx5Hy1fAtydEApCoDIaIjWJw/kC7WTJ0B+jUUQIgV6dw +NSyj0feakeD890gmId+lvl/w/3oUXiczqvl/N9o= +-----END RSA PRIVATE KEY-----`; +const PEM_PUB_RSA512 = `-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAPKr0Dp6YdItzOfk6a7ma7L4BF4LnelM +YKtboGLrk6ihtqFPZFRLNcJi68Hvnt8stMrP50t6jqwWQ2EjMdkj6fsCAwEAAQ== +-----END PUBLIC KEY-----`; + +TestRegister.addTests([ + { + name: "ECDSA Sign/Verify: P-256 with MD5", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "MD5", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "MD5", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA1", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-1", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-1", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA256", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-256", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA384", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-384", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-384", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-256 with SHA512", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs1, "SHA-512", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-512", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Sign/Verify:: Using a private key in PKCS#8 format works", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.privateKeyPkcs8, "SHA-256", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-384 with SHA384", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P384.privateKeyPkcs8, "SHA-384", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-384", P384.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Sign/Verify: P-521 with SHA512", + input: ASCII_TEXT, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P521.privateKeyPkcs8, "SHA-512", "ASN.1 HEX"] + }, + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-512", P521.publicKey, ASCII_TEXT] + } + ] + }, + + // ECDSA Sign + { + name: "ECDSA Sign: Using public key fails", + input: ASCII_TEXT, + expectedOutput: "Provided key is not a private key.", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [P256.publicKey, "SHA-256", "ASN.1 HEX"] + } + ] + }, + { + name: "ECDSA Sign: Using an RSA key fails", + input: ASCII_TEXT, + expectedOutput: "Provided key is not an EC key.", + recipeConfig: [ + { + "op": "ECDSA Sign", + "args": [PEM_PPRIV_RSA512, "SHA-256", "ASN.1 HEX"] + } + ] + }, + + // ECDSA Verify + { + name: "ECDSA Verify: P-256 with SHA256 (ASN.1 signature)", + input: P256.signature.sha256.asn1, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Verify: P-256 with SHA256 (Concat signature)", + input: P256.signature.sha256.concat, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Verify: P-256 with SHA256 (JSON signature)", + input: P256.signature.sha256.json, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Verify: Using private key fails", + input: P256.signature.sha256.asn1, + expectedOutput: "Provided key is not a public key.", + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", P256.privateKeyPkcs1, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Verify: Using an RSA key fails", + input: P256.signature.sha256.asn1, + expectedOutput: "Provided key is not an EC key.", + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["ASN.1 HEX", "SHA-256", PEM_PUB_RSA512, ASCII_TEXT] + } + ] + }, + + // ECDSA Signatur Conversion + { + name: "ECDSA Signature Conversion: ASN.1 To ASN.1", + input: P256.signature.sha256.asn1, + expectedOutput: P256.signature.sha256.asn1, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "ASN.1 HEX"] + } + ] + }, + { + name: "ECDSA Signature Conversion: ASN.1 To Concat", + input: P256.signature.sha256.asn1, + expectedOutput: P256.signature.sha256.concat, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "Concat HEX"] + } + ] + }, + { + name: "ECDSA Signature Conversion: ASN.1 To JSON", + input: P256.signature.sha256.asn1, + expectedOutput: P256.signature.sha256.json, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "JSON"] + } + ] + }, + { + name: "ECDSA Signature Conversion: Concat To ASN.1", + input: P256.signature.sha256.concat, + expectedOutput: P256.signature.sha256.asn1, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "ASN.1 HEX"] + } + ] + }, + { + name: "ECDSA Signature Conversion: Concat To Concat", + input: P256.signature.sha256.concat, + expectedOutput: P256.signature.sha256.concat, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "Concat HEX"] + } + ] + }, + { + name: "ECDSA Signature Conversion: Concat To JSON", + input: P256.signature.sha256.concat, + expectedOutput: P256.signature.sha256.json, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "JSON"] + } + ] + }, + { + name: "ECDSA Signature Conversion: JSON To ASN.1", + input: P256.signature.sha256.json, + expectedOutput: P256.signature.sha256.asn1, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "ASN.1 HEX"] + } + ] + }, + { + name: "ECDSA Signature Conversion: JSON To Concat", + input: P256.signature.sha256.json, + expectedOutput: P256.signature.sha256.concat, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "Concat HEX"] + } + ] + }, + { + name: "ECDSA Signature Conversion: JSON To JSON", + input: P256.signature.sha256.json, + expectedOutput: P256.signature.sha256.json, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "JSON"] + } + ] + } +]); From 7b54d9e8736dd17334a13c2ad3d0234c58dd3936 Mon Sep 17 00:00:00 2001 From: CPlusSharp Date: Sun, 14 Apr 2024 11:34:59 +0200 Subject: [PATCH 3/6] ECDSA rename signature format "Concat HEX" to "P1363 HEX" this format name is more specific and easier to search for on the internet --- src/core/operations/ECDSASign.mjs | 4 +-- .../operations/ECDSASignatureConversion.mjs | 10 +++--- src/core/operations/ECDSAVerify.mjs | 6 ++-- tests/operations/tests/ECDSA.mjs | 34 +++++++++---------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/core/operations/ECDSASign.mjs b/src/core/operations/ECDSASign.mjs index d7bd32e00..5cce8cc22 100644 --- a/src/core/operations/ECDSASign.mjs +++ b/src/core/operations/ECDSASign.mjs @@ -47,7 +47,7 @@ class ECDSASign extends Operation { type: "option", value: [ "ASN.1 HEX", - "Concat HEX", + "P1363 HEX", "JSON" ] } @@ -83,7 +83,7 @@ class ECDSASign extends Operation { case "ASN.1 HEX": result = signatureASN1Hex; break; - case "Concat HEX": + case "P1363 HEX": result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); break; case "JSON": { diff --git a/src/core/operations/ECDSASignatureConversion.mjs b/src/core/operations/ECDSASignatureConversion.mjs index fe5dc3c22..1dfcb4d15 100644 --- a/src/core/operations/ECDSASignatureConversion.mjs +++ b/src/core/operations/ECDSASignatureConversion.mjs @@ -31,7 +31,7 @@ class ECDSASignatureConversion extends Operation { value: [ "Auto", "ASN.1 HEX", - "Concat HEX", + "P1363 HEX", "JSON" ] }, @@ -40,7 +40,7 @@ class ECDSASignatureConversion extends Operation { type: "option", value: [ "ASN.1 HEX", - "Concat HEX", + "P1363 HEX", "JSON" ] } @@ -63,7 +63,7 @@ class ECDSASignatureConversion extends Operation { } else if (input.indexOf("{") !== -1) { inputFormat = "JSON"; } else { - inputFormat = "Concat HEX"; + inputFormat = "P1363 HEX"; } } @@ -73,7 +73,7 @@ class ECDSASignatureConversion extends Operation { case "ASN.1 HEX": signatureASN1Hex = input; break; - case "Concat HEX": + case "P1363 HEX": signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); break; case "JSON": { @@ -89,7 +89,7 @@ class ECDSASignatureConversion extends Operation { case "ASN.1 HEX": result = signatureASN1Hex; break; - case "Concat HEX": + case "P1363 HEX": result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); break; case "JSON": { diff --git a/src/core/operations/ECDSAVerify.mjs b/src/core/operations/ECDSAVerify.mjs index 68e3a1aa2..8f3174a29 100644 --- a/src/core/operations/ECDSAVerify.mjs +++ b/src/core/operations/ECDSAVerify.mjs @@ -32,7 +32,7 @@ class ECDSAVerify extends Operation { value: [ "Auto", "ASN.1 HEX", - "Concat HEX", + "P1363 HEX", "JSON" ] }, @@ -80,7 +80,7 @@ class ECDSAVerify extends Operation { } else if (input.indexOf("{") !== -1) { inputFormat = "JSON"; } else { - inputFormat = "Concat HEX"; + inputFormat = "P1363 HEX"; } } @@ -90,7 +90,7 @@ class ECDSAVerify extends Operation { case "ASN.1 HEX": signatureASN1Hex = input; break; - case "Concat HEX": + case "P1363 HEX": signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); break; case "JSON": { diff --git a/tests/operations/tests/ECDSA.mjs b/tests/operations/tests/ECDSA.mjs index dd3013d35..8fe93ef36 100644 --- a/tests/operations/tests/ECDSA.mjs +++ b/tests/operations/tests/ECDSA.mjs @@ -30,7 +30,7 @@ gusgcAE8H6810fkJ8ZmTNiCCa6sLgR2vD1VNh2diirWgKPH4PVMKav5e6Q== signature: { sha256: { asn1: "3046022100e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127022100b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d", - concat: "e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d", + p1363: "e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d", json: `{"r":"00e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127","s":"00b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d"}` } } @@ -250,8 +250,8 @@ TestRegister.addTests([ ] }, { - name: "ECDSA Verify: P-256 with SHA256 (Concat signature)", - input: P256.signature.sha256.concat, + name: "ECDSA Verify: P-256 with SHA256 (P1363 signature)", + input: P256.signature.sha256.p1363, expectedOutput: "Verified OK", recipeConfig: [ { @@ -307,13 +307,13 @@ TestRegister.addTests([ ] }, { - name: "ECDSA Signature Conversion: ASN.1 To Concat", + name: "ECDSA Signature Conversion: ASN.1 To P1363", input: P256.signature.sha256.asn1, - expectedOutput: P256.signature.sha256.concat, + expectedOutput: P256.signature.sha256.p1363, recipeConfig: [ { "op": "ECDSA Signature Conversion", - "args": ["Auto", "Concat HEX"] + "args": ["Auto", "P1363 HEX"] } ] }, @@ -329,8 +329,8 @@ TestRegister.addTests([ ] }, { - name: "ECDSA Signature Conversion: Concat To ASN.1", - input: P256.signature.sha256.concat, + name: "ECDSA Signature Conversion: P1363 To ASN.1", + input: P256.signature.sha256.p1363, expectedOutput: P256.signature.sha256.asn1, recipeConfig: [ { @@ -340,19 +340,19 @@ TestRegister.addTests([ ] }, { - name: "ECDSA Signature Conversion: Concat To Concat", - input: P256.signature.sha256.concat, - expectedOutput: P256.signature.sha256.concat, + name: "ECDSA Signature Conversion: P1363 To P1363", + input: P256.signature.sha256.p1363, + expectedOutput: P256.signature.sha256.p1363, recipeConfig: [ { "op": "ECDSA Signature Conversion", - "args": ["Auto", "Concat HEX"] + "args": ["Auto", "P1363 HEX"] } ] }, { - name: "ECDSA Signature Conversion: Concat To JSON", - input: P256.signature.sha256.concat, + name: "ECDSA Signature Conversion: P1363 To JSON", + input: P256.signature.sha256.p1363, expectedOutput: P256.signature.sha256.json, recipeConfig: [ { @@ -373,13 +373,13 @@ TestRegister.addTests([ ] }, { - name: "ECDSA Signature Conversion: JSON To Concat", + name: "ECDSA Signature Conversion: JSON To P1363", input: P256.signature.sha256.json, - expectedOutput: P256.signature.sha256.concat, + expectedOutput: P256.signature.sha256.p1363, recipeConfig: [ { "op": "ECDSA Signature Conversion", - "args": ["Auto", "Concat HEX"] + "args": ["Auto", "P1363 HEX"] } ] }, From 1fbc7e03f010b5c7c097021da32b37a742342e87 Mon Sep 17 00:00:00 2001 From: CPlusSharp Date: Sun, 14 Apr 2024 12:42:44 +0200 Subject: [PATCH 4/6] make the ECDSA JSON signature parsing more robust also rename the format to "Raw JSON" as I will later introduce "JSON Web Signature" --- src/core/operations/ECDSASign.mjs | 4 +-- .../operations/ECDSASignatureConversion.mjs | 31 ++++++++++++++----- src/core/operations/ECDSAVerify.mjs | 26 ++++++++++++---- tests/operations/tests/ECDSA.mjs | 28 +++++++++++++++-- 4 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/core/operations/ECDSASign.mjs b/src/core/operations/ECDSASign.mjs index 5cce8cc22..0278eeebe 100644 --- a/src/core/operations/ECDSASign.mjs +++ b/src/core/operations/ECDSASign.mjs @@ -48,7 +48,7 @@ class ECDSASign extends Operation { value: [ "ASN.1 HEX", "P1363 HEX", - "JSON" + "Raw JSON" ] } ]; @@ -86,7 +86,7 @@ class ECDSASign extends Operation { case "P1363 HEX": result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); break; - case "JSON": { + case "Raw JSON": { const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex); result = JSON.stringify(signatureRS); break; diff --git a/src/core/operations/ECDSASignatureConversion.mjs b/src/core/operations/ECDSASignatureConversion.mjs index 1dfcb4d15..49c5ef538 100644 --- a/src/core/operations/ECDSASignatureConversion.mjs +++ b/src/core/operations/ECDSASignatureConversion.mjs @@ -5,6 +5,7 @@ */ import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; import r from "jsrsasign"; /** @@ -32,7 +33,7 @@ class ECDSASignatureConversion extends Operation { "Auto", "ASN.1 HEX", "P1363 HEX", - "JSON" + "Raw JSON" ] }, { @@ -41,7 +42,7 @@ class ECDSASignatureConversion extends Operation { value: [ "ASN.1 HEX", "P1363 HEX", - "JSON" + "Raw JSON" ] } ]; @@ -57,11 +58,19 @@ class ECDSASignatureConversion extends Operation { const outputFormat = args[1]; // detect input format + let inputJson; if (inputFormat === "Auto") { - if (input.substr(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { + try { + inputJson = JSON.parse(input); + if (typeof(inputJson) === "object") { + inputFormat = "Raw JSON"; + } + } catch {} + } + + if (inputFormat === "Auto") { + if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { inputFormat = "ASN.1 HEX"; - } else if (input.indexOf("{") !== -1) { - inputFormat = "JSON"; } else { inputFormat = "P1363 HEX"; } @@ -76,8 +85,14 @@ class ECDSASignatureConversion extends Operation { case "P1363 HEX": signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); break; - case "JSON": { - const inputJson = JSON.parse(input); + case "Raw JSON": { + if (!inputJson) inputJson = JSON.parse(input); + if (!inputJson.r) { + throw new OperationError('No "r" value in the signature JSON'); + } + if (!inputJson.s) { + throw new OperationError('No "s" value in the signature JSON'); + } signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s); break; } @@ -92,7 +107,7 @@ class ECDSASignatureConversion extends Operation { case "P1363 HEX": result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); break; - case "JSON": { + case "Raw JSON": { const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex); result = JSON.stringify(signatureRS); break; diff --git a/src/core/operations/ECDSAVerify.mjs b/src/core/operations/ECDSAVerify.mjs index 8f3174a29..2cd38398a 100644 --- a/src/core/operations/ECDSAVerify.mjs +++ b/src/core/operations/ECDSAVerify.mjs @@ -33,7 +33,7 @@ class ECDSAVerify extends Operation { "Auto", "ASN.1 HEX", "P1363 HEX", - "JSON" + "Raw JSON" ] }, { @@ -74,11 +74,19 @@ class ECDSAVerify extends Operation { } // detect input format + let inputJson; if (inputFormat === "Auto") { - if (input.substr(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { + try { + inputJson = JSON.parse(input); + if (typeof(inputJson) === "object") { + inputFormat = "Raw JSON"; + } + } catch {} + } + + if (inputFormat === "Auto") { + if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { inputFormat = "ASN.1 HEX"; - } else if (input.indexOf("{") !== -1) { - inputFormat = "JSON"; } else { inputFormat = "P1363 HEX"; } @@ -93,8 +101,14 @@ class ECDSAVerify extends Operation { case "P1363 HEX": signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); break; - case "JSON": { - const inputJson = JSON.parse(input); + case "Raw JSON": { + if (!inputJson) inputJson = JSON.parse(input); + if (!inputJson.r) { + throw new OperationError('No "r" value in the signature JSON'); + } + if (!inputJson.s) { + throw new OperationError('No "s" value in the signature JSON'); + } signatureASN1Hex = r.KJUR.crypto.ECDSA.hexRSSigToASN1Sig(inputJson.r, inputJson.s); break; } diff --git a/tests/operations/tests/ECDSA.mjs b/tests/operations/tests/ECDSA.mjs index 8fe93ef36..f0d4305a9 100644 --- a/tests/operations/tests/ECDSA.mjs +++ b/tests/operations/tests/ECDSA.mjs @@ -271,6 +271,28 @@ TestRegister.addTests([ } ] }, + { + name: "ECDSA Verify: JSON signature missing r", + input: JSON.stringify({s: JSON.parse(P256.signature.sha256.json).s}), + expectedOutput: 'No "r" value in the signature JSON', + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, + { + name: "ECDSA Verify: JSON signature missing s", + input: JSON.stringify({r: JSON.parse(P256.signature.sha256.json).r}), + expectedOutput: 'No "s" value in the signature JSON', + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, { name: "ECDSA Verify: Using private key fails", input: P256.signature.sha256.asn1, @@ -324,7 +346,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Signature Conversion", - "args": ["Auto", "JSON"] + "args": ["Auto", "Raw JSON"] } ] }, @@ -357,7 +379,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Signature Conversion", - "args": ["Auto", "JSON"] + "args": ["Auto", "Raw JSON"] } ] }, @@ -390,7 +412,7 @@ TestRegister.addTests([ recipeConfig: [ { "op": "ECDSA Signature Conversion", - "args": ["Auto", "JSON"] + "args": ["Auto", "Raw JSON"] } ] } From 7e7195c29157e7f5ecbce47cfa4cf7377d2a9ed1 Mon Sep 17 00:00:00 2001 From: CPlusSharp Date: Sun, 14 Apr 2024 15:19:57 +0200 Subject: [PATCH 5/6] ECDSA: Output keys as JSONWebKeySet instead of two JWK --- src/core/operations/GenerateECDSAKeyPair.mjs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/core/operations/GenerateECDSAKeyPair.mjs b/src/core/operations/GenerateECDSAKeyPair.mjs index c098fe019..14714a02e 100644 --- a/src/core/operations/GenerateECDSAKeyPair.mjs +++ b/src/core/operations/GenerateECDSAKeyPair.mjs @@ -75,17 +75,21 @@ class GenerateECDSAKeyPair extends Operation { let result; switch (outputFormat) { case "PEM": - pubKey = r.KEYUTIL.getPEM(keyPair.pubKeyObj); - privKey = r.KEYUTIL.getPEM(keyPair.prvKeyObj, "PKCS8PRV"); + pubKey = r.KEYUTIL.getPEM(keyPair.pubKeyObj).replace(/\r/g, ""); + privKey = r.KEYUTIL.getPEM(keyPair.prvKeyObj, "PKCS8PRV").replace(/\r/g, ""); result = pubKey + "\n" + privKey; break; case "DER": result = keyPair.prvKeyObj.prvKeyHex; break; case "JWK": - pubKey = JSON.stringify(r.KEYUTIL.getJWKFromKey(keyPair.pubKeyObj)); - privKey = JSON.stringify(r.KEYUTIL.getJWKFromKey(keyPair.prvKeyObj)); - result = pubKey + "\n\n" + privKey; + pubKey = r.KEYUTIL.getJWKFromKey(keyPair.pubKeyObj); + pubKey.key_ops = ["verify"]; // eslint-disable-line camelcase + pubKey.kid = "PublicKey"; + privKey = r.KEYUTIL.getJWKFromKey(keyPair.prvKeyObj); + privKey.key_ops = ["sign"]; // eslint-disable-line camelcase + privKey.kid = "PrivateKey"; + result = JSON.stringify({keys: [privKey, pubKey]}, null, 4); break; } From 21ac516248246d17eb074534d3e795c1436d584d Mon Sep 17 00:00:00 2001 From: CPlusSharp Date: Sun, 14 Apr 2024 16:11:11 +0200 Subject: [PATCH 6/6] ECDSA JSON Web Signature format used e.g. by JWT --- src/core/operations/ECDSASign.mjs | 7 +++ .../operations/ECDSASignatureConversion.mjs | 33 ++++++++++++-- src/core/operations/ECDSAVerify.mjs | 28 ++++++++++-- tests/operations/tests/ECDSA.mjs | 45 +++++++++++++++++++ 4 files changed, 105 insertions(+), 8 deletions(-) diff --git a/src/core/operations/ECDSASign.mjs b/src/core/operations/ECDSASign.mjs index 0278eeebe..7b8f57f18 100644 --- a/src/core/operations/ECDSASign.mjs +++ b/src/core/operations/ECDSASign.mjs @@ -6,6 +6,8 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; +import { fromHex } from "../lib/Hex.mjs"; +import { toBase64 } from "../lib/Base64.mjs"; import r from "jsrsasign"; /** @@ -48,6 +50,7 @@ class ECDSASign extends Operation { value: [ "ASN.1 HEX", "P1363 HEX", + "JSON Web Signature", "Raw JSON" ] } @@ -86,6 +89,10 @@ class ECDSASign extends Operation { case "P1363 HEX": result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); break; + case "JSON Web Signature": + result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); + result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url + break; case "Raw JSON": { const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex); result = JSON.stringify(signatureRS); diff --git a/src/core/operations/ECDSASignatureConversion.mjs b/src/core/operations/ECDSASignatureConversion.mjs index 49c5ef538..3f6c6bfb0 100644 --- a/src/core/operations/ECDSASignatureConversion.mjs +++ b/src/core/operations/ECDSASignatureConversion.mjs @@ -6,6 +6,8 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; +import { fromBase64, toBase64 } from "../lib/Base64.mjs"; +import { fromHex, toHexFast } from "../lib/Hex.mjs"; import r from "jsrsasign"; /** @@ -33,6 +35,7 @@ class ECDSASignatureConversion extends Operation { "Auto", "ASN.1 HEX", "P1363 HEX", + "JSON Web Signature", "Raw JSON" ] }, @@ -42,6 +45,7 @@ class ECDSASignatureConversion extends Operation { value: [ "ASN.1 HEX", "P1363 HEX", + "JSON Web Signature", "Raw JSON" ] } @@ -69,22 +73,39 @@ class ECDSASignatureConversion extends Operation { } if (inputFormat === "Auto") { - if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { - inputFormat = "ASN.1 HEX"; - } else { - inputFormat = "P1363 HEX"; + const hexRegex = /^[a-f\d]{2,}$/gi; + if (hexRegex.test(input)) { + if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { + inputFormat = "ASN.1 HEX"; + } else { + inputFormat = "P1363 HEX"; + } } } + let inputBase64; + if (inputFormat === "Auto") { + try { + inputBase64 = fromBase64(input, "A-Za-z0-9-_", false); + inputFormat = "JSON Web Signature"; + } catch {} + } + // convert input to ASN.1 hex let signatureASN1Hex; switch (inputFormat) { + case "Auto": + throw new OperationError("Signature format could not be detected"); case "ASN.1 HEX": signatureASN1Hex = input; break; case "P1363 HEX": signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); break; + case "JSON Web Signature": + if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_"); + signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64)); + break; case "Raw JSON": { if (!inputJson) inputJson = JSON.parse(input); if (!inputJson.r) { @@ -107,6 +128,10 @@ class ECDSASignatureConversion extends Operation { case "P1363 HEX": result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); break; + case "JSON Web Signature": + result = r.KJUR.crypto.ECDSA.asn1SigToConcatSig(signatureASN1Hex); + result = toBase64(fromHex(result), "A-Za-z0-9-_"); // base64url + break; case "Raw JSON": { const signatureRS = r.KJUR.crypto.ECDSA.parseSigHexInHexRS(signatureASN1Hex); result = JSON.stringify(signatureRS); diff --git a/src/core/operations/ECDSAVerify.mjs b/src/core/operations/ECDSAVerify.mjs index 2cd38398a..7e46e867d 100644 --- a/src/core/operations/ECDSAVerify.mjs +++ b/src/core/operations/ECDSAVerify.mjs @@ -6,6 +6,8 @@ import Operation from "../Operation.mjs"; import OperationError from "../errors/OperationError.mjs"; +import { fromBase64 } from "../lib/Base64.mjs"; +import { toHexFast } from "../lib/Hex.mjs"; import r from "jsrsasign"; /** @@ -33,6 +35,7 @@ class ECDSAVerify extends Operation { "Auto", "ASN.1 HEX", "P1363 HEX", + "JSON Web Signature", "Raw JSON" ] }, @@ -85,22 +88,39 @@ class ECDSAVerify extends Operation { } if (inputFormat === "Auto") { - if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { - inputFormat = "ASN.1 HEX"; - } else { - inputFormat = "P1363 HEX"; + const hexRegex = /^[a-f\d]{2,}$/gi; + if (hexRegex.test(input)) { + if (input.substring(0, 2) === "30" && r.ASN1HEX.isASN1HEX(input)) { + inputFormat = "ASN.1 HEX"; + } else { + inputFormat = "P1363 HEX"; + } } } + let inputBase64; + if (inputFormat === "Auto") { + try { + inputBase64 = fromBase64(input, "A-Za-z0-9-_", false); + inputFormat = "JSON Web Signature"; + } catch {} + } + // convert to ASN.1 signature let signatureASN1Hex; switch (inputFormat) { + case "Auto": + throw new OperationError("Signature format could not be detected"); case "ASN.1 HEX": signatureASN1Hex = input; break; case "P1363 HEX": signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(input); break; + case "JSON Web Signature": + if (!inputBase64) inputBase64 = fromBase64(input, "A-Za-z0-9-_"); + signatureASN1Hex = r.KJUR.crypto.ECDSA.concatSigToASN1Sig(toHexFast(inputBase64)); + break; case "Raw JSON": { if (!inputJson) inputJson = JSON.parse(input); if (!inputJson.r) { diff --git a/tests/operations/tests/ECDSA.mjs b/tests/operations/tests/ECDSA.mjs index f0d4305a9..560afc5c5 100644 --- a/tests/operations/tests/ECDSA.mjs +++ b/tests/operations/tests/ECDSA.mjs @@ -31,6 +31,7 @@ gusgcAE8H6810fkJ8ZmTNiCCa6sLgR2vD1VNh2diirWgKPH4PVMKav5e6Q== sha256: { asn1: "3046022100e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127022100b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d", p1363: "e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d", + jws: "4GkFYIovp9vanihMKnlZ37aPtSel8AOy15df8TUUUSe2uqJTeTM0-Lk-od1iK8YAEk2AkLq9gH7-P3e4syQ4jQ", json: `{"r":"00e06905608a2fa7dbda9e284c2a7959dfb68fb527a5f003b2d7975ff135145127","s":"00b6baa253793334f8b93ea1dd622bc600124d8090babd807efe3f77b8b324388d"}` } } @@ -260,6 +261,17 @@ TestRegister.addTests([ } ] }, + { + name: "ECDSA Verify: P-256 with SHA256 (JWS signature)", + input: P256.signature.sha256.jws, + expectedOutput: "Verified OK", + recipeConfig: [ + { + "op": "ECDSA Verify", + "args": ["Auto", "SHA-256", P256.publicKey, ASCII_TEXT] + } + ] + }, { name: "ECDSA Verify: P-256 with SHA256 (JSON signature)", input: P256.signature.sha256.json, @@ -339,6 +351,17 @@ TestRegister.addTests([ } ] }, + { + name: "ECDSA Signature Conversion: ASN.1 To JWS", + input: P256.signature.sha256.asn1, + expectedOutput: P256.signature.sha256.jws, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "JSON Web Signature"] + } + ] + }, { name: "ECDSA Signature Conversion: ASN.1 To JSON", input: P256.signature.sha256.asn1, @@ -372,6 +395,17 @@ TestRegister.addTests([ } ] }, + { + name: "ECDSA Signature Conversion: P1363 To JWS", + input: P256.signature.sha256.p1363, + expectedOutput: P256.signature.sha256.jws, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "JSON Web Signature"] + } + ] + }, { name: "ECDSA Signature Conversion: P1363 To JSON", input: P256.signature.sha256.p1363, @@ -405,6 +439,17 @@ TestRegister.addTests([ } ] }, + { + name: "ECDSA Signature Conversion: JSON To JWS", + input: P256.signature.sha256.json, + expectedOutput: P256.signature.sha256.jws, + recipeConfig: [ + { + "op": "ECDSA Signature Conversion", + "args": ["Auto", "JSON Web Signature"] + } + ] + }, { name: "ECDSA Signature Conversion: JSON To JSON", input: P256.signature.sha256.json,