diff --git a/ecllibrary/std/CMakeLists.txt b/ecllibrary/std/CMakeLists.txt index 2293005ee76..7c5959d5962 100644 --- a/ecllibrary/std/CMakeLists.txt +++ b/ecllibrary/std/CMakeLists.txt @@ -28,6 +28,7 @@ set( Math.ecl Metaphone3.ecl Metaphone.ecl + OpenSSL.ecl Str.ecl Uni.ecl ) diff --git a/ecllibrary/std/OpenSSL.ecl b/ecllibrary/std/OpenSSL.ecl new file mode 100644 index 00000000000..8c351dab67e --- /dev/null +++ b/ecllibrary/std/OpenSSL.ecl @@ -0,0 +1,329 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2025 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the License); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an AS IS BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +EXPORT OpenSSL := MODULE + +IMPORT lib_openssl; + +EXPORT Digest := MODULE + + /** + * Returns a list of the names of the available hash digest algorithms. + * + * @return A dataset containing the hash algorithm names. + * + * @see Hash() + */ + EXPORT DATASET({STRING name}) AvailableAlgorithms() := lib_openssl.OpenSSL.digestAvailableAlgorithms(); + + /** + * Compute the hash of given data according to the named + * hash algorithm. + * + * @param indata The data to hash; REQUIRED + * @param hash_name The name of the hash algorithm to use; + * must be one of the values returned from + * the AvailableAlgorithms() function in + * this module; cannot be empty; REQUIRED + * + * @return A DATA value representing the hash value of indata. + * + * @see AvailableAlgorithms() + */ + EXPORT DATA Hash(DATA _indata, VARSTRING _hash_name) := lib_openssl.OpenSSL.digesthash(_indata, _hash_name); + +END; // Digest + +EXPORT Ciphers := MODULE + + /** + * Returns a list of the names of the available symmetric + * cipher algorithms. + * + * @return A dataset containing the symmetric cipher algorithm names. + * + * @see IVSize() + * SaltSize() + * Encrypt() + * Decrypt() + */ + EXPORT DATASET({STRING name}) AvailableAlgorithms() := lib_openssl.OpenSSL.cipherAvailableAlgorithms(); + + /** + * Return the size of the IV used for the given symmetric + * cipher algorithm. + * + * This is primarily an introspection/discovery function. Once + * you determine the proper value for the algorithm you want to + * use, you should hardcode it. + * + * @param algorithm The name of the symmetric cipher to examine; + * must be one of the values returned from + * the AvailableAlgorithms() function in + * this module; cannot be empty; REQUIRED + * + * @return The size of the IV used by the given algorithm, in bytes. + * + * @see AvailableAlgorithms() + */ + EXPORT UNSIGNED2 IVSize(VARSTRING algorithm) := lib_openssl.OpenSSL.cipherIVSize(algorithm); + + /** + * Return the size of the salt used for the given symmetric + * cipher algorithm. + * + * This is primarily an introspection/discovery function. Once + * you determine the proper value for the algorithm you want to + * use, you should hardcode it. + * + * @param algorithm The name of the symmetric cipher to examine; + * must be one of the values returned from + * the AvailableAlgorithms() function in + * this module; cannot be empty; REQUIRED + * + * @return The size of the salt used by the given algorithm, in bytes. + * + * @see AvailableAlgorithms() + */ + EXPORT UNSIGNED2 SaltSize(VARSTRING algorithm) := 8; + + /** + * Encrypt some plaintext with the given symmetric cipher and a + * passphrase. Optionally, you can specify static IV and salt values. + * The encrypted ciphertext is returned as a DATA value. + * + * If IV or salt values are explicitly provided during encryption then + * those same values must be provided during decryption. + * + * @param plaintext The data to encrypt; REQUIRED + * @param algorithm The name of the symmetric cipher to use; + * must be one of the values returned from + * the AvailableAlgorithms() function in + * this module; cannot be empty; REQUIRED + * @param iv The IV to use during encryption; if not set + * then a random value will be generated; if set, + * it must be of the expected size for the given + * algorithm; OPTIONAL, defaults to creating a + * random value + * @param salt TCURRENT_OPENSSL_VERSIONencryption; if not set + * then a random value will be generated; if set, + * it must be of the expected size for the given + * algorithm; OPTIONAL, defaults to creating a + * random value + * + * @return The ciphertext as a DATA type. + * + * @see AvailableAlgorithms() + * IVSize() + * SaltSize() + * Decrypt() + */ + EXPORT DATA Encrypt(DATA plaintext, VARSTRING algorithm, DATA passphrase, DATA iv = (DATA)'', DATA salt = (DATA)'') := lib_openssl.OpenSSL.cipherEncrypt(plaintext, algorithm, passphrase, iv, salt); + + + /** + * Decrypt some ciphertext with the given symmetric cipher and a + * passphrase. Optionally, you can specify static IV and salt values. + * The decrypted plaintext is returned as a DATA value. + * + * @param ciphertext The data to decrypt; REQUIRED + * @param algorithm The name of the symmetric cipher to use; + * must be one of the values returned from + * the AvailableAlgorithms() function in + * this module; cannot be empty; REQUIRED + * @param iv The IV to use during decryption; if not set + * then a random value will be used; if set, + * it must be of the expected size for the given + * algorithm; OPTIONAL, defaults to creating a + * random value + * @param salt The salt to use during decryption; if not set + * then a random value will be used; if set, + * it must be of the expected size for the given + * algorithm; OPTIONAL, defaults to creating a + * random value + * + * @return The plaintext as a DATA type. + * + * @see AvailableAlgorithms() + * IVSize() + * SaltSize() + * Encrypt() + */ + EXPORT DATA Decrypt(DATA ciphertext, VARSTRING algorithm, DATA passphrase, DATA iv = (DATA)'', DATA salt = (DATA)'') := lib_openssl.OpenSSL.cipherDecrypt(ciphertext, algorithm, passphrase, iv, salt); +END; // Ciphers + +EXPORT RSA := MODULE + + /** + * Perform a hybrid encryption using one or more RSA public keys. + * + * Because asymmetric encryption is computationally expensive, large + * payloads are actually encrypted with a symmetric cipher and a + * randomly-generated passphrase. The passphrase, which is much shorter, + * is then encrypted with the public key. The whole package is then bundled + * together into an "envelope" and "sealed". + * + * The function uses RSA public keys and they must be in PEM format. To + * generate such keys on the command line: + * + * ssh-keygen -b 4096 -t rsa -m pem -f sample2 + * ssh-keygen -f sample2 -e -m pem > sample2.pub + * + * The resulting files, sample2 and sample2.pub, are the private and public + * keys, respectively. Their contents may be passed to this function. + * + * @param plaintext The data to encrypt; REQUIRED + * @param pem_public_keys One or more RSA public keys, in PEM format; + * note that this is a SET -- you can pass + * more than one public key here, and the resulting + * ciphertext can be decrypted by any one of the + * corresponding private keys; REQUIRED + * @param symmetric_algorithm The name of the symmetric algorithm to use + * to encrypt the payload; must be one of those + * returned by Ciphers.AvailableAlgorithms(); + * OPTIONAL, defaults to aes-256-cbc + * + * @return The encrypted ciphertext. + * + * @see Unseal() + * Ciphers.AvailableAlgorithms() + */ + EXPORT DATA Seal(DATA plaintext, SET OF STRING pem_public_keys, VARSTRING symmetric_algorithm = 'aes-256-cbc') := lib_openssl.OpenSSL.rsaSeal(plaintext, pem_public_keys, symmetric_algorithm); + + /** + * Decrypts ciphertext previously generated by the Seal() function. + * + * Because asymmetric encryption is computationally expensive, large + * payloads are actually encrypted with a symmetric cipher and a + * randomly-generated passphrase. The passphrase, which is much shorter, + * is then encrypted with the public key. The whole package is then bundled + * together into an "envelope" and "sealed". Given the private key that + * corresponds to one of the public keys used to create the ciphertext, + * this function unpacks everything and decrypts the payload. + * + * The function uses RSA public keys and they must be in PEM format. To + * generate such keys on the command line: + * + * ssh-keygen -b 4096 -t rsa -m pem -f sample2 + * ssh-keygen -f sample2 -e -m pem > sample2.pub + * + * The resulting files, sample2 and sample2.pub, are the private and public + * keys, respectively. Their contents may be passed to this function. + * + * @param ciphertext The data to decrypt; REQUIRED + * @param pem_private_key An RSA public key in PEM format; REQUIRED + * @param symmetric_algorithm The name of the symmetric algorithm to use + * to decrypt the payload; must be one of those + * returned by Ciphers.AvailableAlgorithms() and + * it must match the algorithm used to create the + * ciphertext; OPTIONAL, defaults to aes-256-cbc + * + * @return The decrypted plaintext. + * + * @see Seal() + * Ciphers.AvailableAlgorithms() + */ + EXPORT DATA Unseal(DATA ciphertext, STRING pem_private_key, VARSTRING symmetric_algorithm = 'aes-256-cbc') := lib_openssl.OpenSSL.rsaUnseal(ciphertext, pem_private_key, symmetric_algorithm); + + /** + * This function performs asymmetric encryption. It should be used to + * encrypt only small plaintext (e.g. less than 100 bytes) because it is + * computationally expensive. + * + * @param plaintext The data to encrypt; REQUIRED + * @param pem_public_key The public key to use for encryption, in + * PEM format; REQUIRED + * + * @return The encrypted ciphertext. + * + * @see Decrypt() + */ + EXPORT DATA Encrypt(DATA plaintext, STRING pem_public_key) := lib_openssl.OpenSSL.rsaEncrypt(plaintext, pem_public_key); + + /** + * This function performs asymmetric decryption. It should be used to + * decrypt only small plaintext (e.g. less than 100 bytes) because it is + * computationally expensive. + * + * @param ciphertext The data to decrypt; REUIRED + * @param pem_private_key The private key to use for decryption, in + * PEM format; REQUIRED + * + * @return The decrypted plaintext. + * + * @see Encrypt() + */ + EXPORT DATA Decrypt(DATA ciphertext, STRING pem_private_key) := lib_openssl.OpenSSL.rsaDecrypt(ciphertext, pem_private_key); + + /** + * Create a digital signature of the given data, using the + * specified private key, passphrase and algorithm. + * + * The function uses an RSA private key and it must be in PEM format. To + * generate such keys on the command line: + * + * ssh-keygen -b 4096 -t rsa -m pem -f sample2 + * ssh-keygen -f sample2 -e -m pem > sample2.pub + * + * The resulting files, sample2 and sample2.pub, are the private and public + * keys, respectively. Their contents may be passed to this function + * + * @param plaintext Contents to sign; REQUIRED + * @param passphrase Passphrase to use for private key; REQUIRED + * @param pem_private_key Private key to use for signing; REQUIRED + * @param hash_name The name of the hash algorithm to use; + * must be one of the values returned from + * the AvailableAlgorithms() function in + * the Digest module; cannot be empty; REQUIRED + * @return Computed Digital signature + * + * @see Digest.AvailableAlgorithms() + * VerifySignature() + */ + EXPORT DATA Sign(DATA plaintext, DATA passphrase, STRING pem_private_key, VARSTRING hash_name) := lib_openssl.OpenSSL.rsaSign(plaintext, passphrase, pem_private_key, hash_name); + + /** + * Verify the given digital signature of the given data, using + * the specified public key, passphrase and algorithm. + * + * The function uses an RSA public key and it must be in PEM format. To + * generate such keys on the command line: + * + * ssh-keygen -b 4096 -t rsa -m pem -f sample2 + * ssh-keygen -f sample2 -e -m pem > sample2.pub + * + * The resulting files, sample2 and sample2.pub, are the private and public + * keys, respectively. Their contents may be passed to this function + * + * @param signature Signature to verify; REQUIRED + * @param signedData Data used to create signature; REQUIRED + * @param passphrase Passphrase to use for private key; REQUIRED + * @param pem_public_key Public key to use for verification; REQUIRED + * @param hash_name The name of the hash algorithm to use; + * must be one of the values returned from + * the AvailableAlgorithms() function in + * the Digest module; cannot be empty; REQUIRED + * @return Boolean TRUE/FALSE + * + * @see Digest.AvailableAlgorithms() + * Sign() + */ + EXPORT BOOLEAN VerifySignature(DATA signature, DATA signedData, DATA passphrase, STRING pem_public_key, VARSTRING hash_name) := lib_openssl.OpenSSL.rsaVerifySignature(signature, signedData, passphrase, pem_public_key, hash_name); + +END; // RSA + +END; diff --git a/ecllibrary/teststd/OpenSSL/TestOpenSSL.ecl b/ecllibrary/teststd/OpenSSL/TestOpenSSL.ecl new file mode 100644 index 00000000000..82d32147318 --- /dev/null +++ b/ecllibrary/teststd/OpenSSL/TestOpenSSL.ecl @@ -0,0 +1,223 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2025 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the License); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an AS IS BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +Import Std; + +EXPORT TestOpenSSL := MODULE + + EXPORT STRING PLAINTEXT := 'Colorless green ideas sleep furiously.'; + EXPORT STRING CIPHERS_CIPHER := 'aes-256-cbc'; + EXPORT STRING PASSPHRASE := 'mypassphrase'; + + EXPORT STRING RSA_PUBLIC_1 := + '-----BEGIN RSA PUBLIC KEY-----' + '\n' + + 'MIICCgKCAgEAuLYYTmPMzp13999s7bUAaJZVON+9k/2PDBF5AfHtQ40FmxwRdG3d' + '\n' + + 'CeUcbhb8Nuw3YizS7WgdZBtjyi7hZ146aHBDXVXhJlInR3y44hAnYB/sOccxQYWQ' + '\n' + + '6xOZ6VpZ7MnJl4j2OFmoc62QAwQuqizXhfFOtd2KfTubKsMPR1tWrWRC6zlDBkYV' + '\n' + + 'D677MqQyhAuZvCjYO5E5dPmoVVxZaBxjsTjElEqtjkRdROG1D0PJY+8XkWP/76ns' + '\n' + + 'ieNsTb9R0umN4x4+lzsHSB4tcGICx+S7cW+FmJzyGlf6J4YWEq4VqoIQ7MI8O8Hj' + '\n' + + 'fuR3jdZwubZQrFzKtvWv3i3Dm16+JxmtK6qjhJhGzlXchBk+j8eOUaWOV0sBMLPo' + '\n' + + 'sVEapY+SmISxj0sn8yLSfbUs5ivHaEMNaZq6BM1Db5y7MEOANv2FplvT9oW7UECg' + '\n' + + '7D7PUZnrBZf16FiNxwXnxK/Sn049y/2nFnHPxACWZVVtXN9N64GZPEP1/vYyYFCN' + '\n' + + 'yt0mdYjJLtf6z87YYLM4kHMNqnbPJKOQahvRC4fW4/QfQYFjLwHFNppUo9D/bx2t' + '\n' + + 'sCmDEWc7cGQE081tQep34rpTel0wkePhxajmSlRNyVtv3ZXph1Hl1XoOj17QLdyw' + '\n' + + 'WjCAqcMKrVHnOkyKo2lTNhY3bsu6PfEe4dTTia3/WlPJe0nGa0SUuvkCAwEAAQ==' + '\n' + + '-----END RSA PUBLIC KEY-----'; + EXPORT STRING RSA_PRIVATE_1 := + '-----BEGIN RSA PRIVATE KEY-----' + '\n' + + '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' + '\n' + + '-----END RSA PRIVATE KEY-----'; + + EXPORT STRING RSA_PUBLIC_2 := + '-----BEGIN RSA PUBLIC KEY-----' + '\n' + + 'MIICCgKCAgEAtHYNEH8DswZUtx3Xk90cPCLtf3vl7awjZvI4V/yJkOb5U/sQRgsw' + '\n' + + '60F84i8Okg3yBxGIEDiZ/cWKBYfQGsycbm+zljIHh0tt8S7hH9iwFiZB41urPZ4Q' + '\n' + + 'zSs84h05SZA9nCVPQOpFYi0I8n6QrGf9eokJf/jO/dZcvsIDYqtiLfTYWMLKibrY' + '\n' + + 'HgP9lIKxJieSmbrX4spYBEf+B9cSQqHBwThAP1mvkIzD2eGuYlQV6hXZb2dSXn8l' + '\n' + + 'Lakgq06mvkCxYglv6GEHKPrEd6KCZKgBLKZsEabvd5+eWXL4VcaAdXjQgRIa5aV2' + '\n' + + 'AJCk5V1ClMraJuw1aw62+2GTi7bTum4t+GJQdTfDSYreTBIZLUzHwlRnlbMrsthd' + '\n' + + 'aVWct3KId2BT/ntqmalBTcu+nK7H3XOEQGrFdQo2tQ459guchYqxk8QFZCVTkNzw' + '\n' + + 'Ky7SyJV9dqDDUhQQSTvwv42CbwpwbqSynE682h1IQThgk1LuDX/IIQRupIgbyJKo' + '\n' + + 'WrFYoNalhyoeXCgQT7gH2y5YdeoGf3oN7dn4HUDFdW2FOYPzpsSMLVr1MUhxelJc' + '\n' + + 'Ru1RUsJDwfkXBUOZM7ChgwGfQYWGjFcEOPK6AbUlSTER2vP3t5R/QPzN6uzih10y' + '\n' + + 'LFFeUxMuvLnmgpKSU9bVsXbN2D4hmInZW8vDRKugNeTrtaV6KkUus5sCAwEAAQ==' + '\n' + + '-----END RSA PUBLIC KEY-----'; + EXPORT STRING RSA_PRIVATE_2 := + '-----BEGIN RSA PRIVATE KEY-----' + '\n' + + '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' + '\n' + + '-----END RSA PRIVATE KEY-----'; + + EXPORT UNSIGNED8 staticSalt := 123456789; + EXPORT DATA staticIV := (DATA)'0123456789012345'; + + EXPORT hash_sha256 := Std.OpenSSL.Digest.Hash((DATA)PLAINTEXT, 'sha256'); + EXPORT hash_sha512 := Std.OpenSSL.Digest.Hash((DATA)PLAINTEXT, 'sha512'); + EXPORT encrypt_my_iv_my_salt := Std.OpenSSL.Ciphers.Encrypt((DATA)PLAINTEXT, CIPHERS_CIPHER, (DATA)PASSPHRASE, salt := (>DATA<)staticSalt, iv := staticIV); + EXPORT encrypt_default_iv_my_salt := Std.OpenSSL.Ciphers.Encrypt((DATA)PLAINTEXT, CIPHERS_CIPHER, (DATA)PASSPHRASE, salt := (>DATA<)staticSalt); + EXPORT encrypt_my_iv_default_salt := Std.OpenSSL.Ciphers.Encrypt((DATA)PLAINTEXT, CIPHERS_CIPHER, (DATA)PASSPHRASE, iv := staticIV); + EXPORT encrypt_default_iv_default_salt := Std.OpenSSL.Ciphers.Encrypt((DATA)PLAINTEXT, CIPHERS_CIPHER, (DATA)PASSPHRASE); + EXPORT encrypt_rsa := Std.OpenSSL.RSA.Encrypt((DATA)PLAINTEXT, RSA_PUBLIC_1); + EXPORT seal_rsa := Std.OpenSSL.RSA.Seal((DATA)PLAINTEXT, [RSA_PUBLIC_1, RSA_PUBLIC_2]); + EXPORT signed_rsa_sha256 := Std.OpenSSL.RSA.Sign((DATA)PLAINTEXT, (DATA)PASSPHRASE, RSA_PRIVATE_1, 'sha256'); + + EXPORT TestDigests := [ + ASSERT(COUNT(Std.OpenSSL.Digest.AvailableAlgorithms()) > 0, 'No digest algorithms available'); + + ASSERT(Std.Str.ToHexPairs(hash_sha256) = '0F6BE6CC79C301CEA386173C0919FEE806E78075618656D24F733AA5052EBF6D'); + ASSERT(LENGTH(hash_sha256) = 32); + ASSERT(Std.Str.ToHexPairs(hash_sha512) = '9E309515371114774EE9E3C216888AFA89CC6616AF2E583601BA6E84750943B246E2CA5A518B44096A3A5929A1A50BF842117676DAFA5435CDF981DCA1344F8F'); + ASSERT(LENGTH(hash_sha512) = 64); + + ASSERT(TRUE) + ]; + + EXPORT TestCiphers := [ + ASSERT(COUNT(Std.OpenSSL.Ciphers.AvailableAlgorithms()) > 0, 'No cipher algorithms available'); + + ASSERT(Std.OpenSSL.Ciphers.IVSize(CIPHERS_CIPHER) = 16); + ASSERT(Std.OpenSSL.Ciphers.SaltSize(CIPHERS_CIPHER) = 8); + + ASSERT(Std.Str.ToHexPairs(encrypt_my_iv_my_salt) = '7D99A971120AF361404F93F27CABB44BEDC9333BF84C3DFDBC803F3CEE7A4BA1A5BE0C2FD98A507E718988A140F2B29D'); + ASSERT(LENGTH(encrypt_my_iv_my_salt) = 48); + ASSERT((STRING)Std.OpenSSL.Ciphers.Decrypt(encrypt_my_iv_my_salt, CIPHERS_CIPHER, (DATA)PASSPHRASE, salt := (>DATA<)staticSalt, iv := staticIV) = PLAINTEXT); + ASSERT(LENGTH(encrypt_default_iv_my_salt) = 48); + ASSERT((STRING)Std.OpenSSL.Ciphers.Decrypt(encrypt_default_iv_my_salt, CIPHERS_CIPHER, (DATA)PASSPHRASE, salt := (>DATA<)staticSalt) = PLAINTEXT); + ASSERT(LENGTH(encrypt_my_iv_default_salt) = 48); + ASSERT((STRING)Std.OpenSSL.Ciphers.Decrypt(encrypt_my_iv_default_salt, CIPHERS_CIPHER, (DATA)PASSPHRASE, iv := staticIV) = PLAINTEXT); + ASSERT(LENGTH(encrypt_default_iv_default_salt) = 48); + ASSERT((STRING)Std.OpenSSL.Ciphers.Decrypt(encrypt_default_iv_default_salt, CIPHERS_CIPHER, (DATA)PASSPHRASE) = PLAINTEXT); + + ASSERT(TRUE) + ]; + + EXPORT TestRSA := [ + ASSERT(LENGTH(encrypt_rsa) = 512); + ASSERT((STRING)Std.OpenSSL.RSA.Decrypt((DATA)encrypt_rsa, RSA_PRIVATE_1) = PLAINTEXT); + ASSERT(LENGTH(seal_rsa) = 1112); + ASSERT((STRING)Std.OpenSSL.RSA.Unseal((DATA)seal_rsa, RSA_PRIVATE_1) = PLAINTEXT); + ASSERT((STRING)Std.OpenSSL.RSA.Unseal((DATA)seal_rsa, RSA_PRIVATE_2) = PLAINTEXT); + + ASSERT(Std.Str.ToHexPairs(signed_rsa_sha256) = '399F5FCB9B1A3C02D734BF3F1C9CB480681126B0F7505697E045ECF22E11A47FADCF7E2BA73761AD6345F6702AAD230957AFA1B0B3C8C29FC537AACA68' + + '13B79DE0CDEF82B4BB6183D0637583D0E2AA78892EE190D7AB9CB20F9AC36D91DB2994A07F0C17A0CFB5AF0385EA4ABA9723FC72FB08081AE4C6C83E659AEBA1C103FB6F4E831EFDAA4CE037A3874D59664B1DE90' + + '313B474897E2BF7D48CF09E19EC706A83B3E86B9F8A5F15BE01BA6A7E0BDC78877D8C60870E4713CCB87EC93A6C47E8EBF522E35BAEC85A1FC0209397CE0C62564B79104514D042F435FF0DF4B4500C8CE0F3AD76' + + '4C26FDAFD14C493D303860C6A9AE7D539F42855C077EE682F6BD234CE035AAFADA5B5E90021D6D82D9030E687234D71E95CA701FDD6E097764443FEC237744FC3530E0AB3715F28B510766F73B6F56DDE7219AA2A' + + 'A77A41AF1D34200CC63F3D35E89398536F4BAA3A5D66E3C9BDF0C3AFFE9AE618413C3EAC2A980DF6B363D6F6F93BCB2D02D5BB4CCFF912CEDEE66FCF916F289C34CA2D45DFD14E545AA8D6CE591F455D993A7E6E4' + + 'C573C9EBE9FE472131A39D60ED53624745A79A29B31D07DE38D1FA64D3DB32EF447F62B64F8A07C012E55C06551F5C60509797A4521DC4E7CB33F9C0759E6B46DA6B758C86E26507CA9D79933532BE923FC449842' + + '17A203F97B97606E18E9F4949DC5E6C21705A89844B316093EFD8C0CC'); + ASSERT(LENGTH(signed_rsa_sha256) = 512); + ASSERT(Std.OpenSSL.RSA.VerifySignature((DATA)signed_rsa_sha256, (DATA)PLAINTEXT, (DATA)PASSPHRASE, RSA_PUBLIC_1, 'sha256') = TRUE); + ASSERT(Std.OpenSSL.RSA.VerifySignature((DATA)((UNSIGNED)signed_rsa_sha256 + 1), (DATA)PLAINTEXT, (DATA)PASSPHRASE, RSA_PUBLIC_1, 'sha256') = FALSE); + + ASSERT(TRUE) + ]; +END; diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 63de3d1cc68..42be5b1e56b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -44,6 +44,7 @@ add_subdirectory (couchbase) add_subdirectory (sqs) add_subdirectory (mongodb) add_subdirectory (parquet) +add_subdirectory (openssl) IF ( INCLUDE_EE_PLUGINS ) add_subdirectory (eeproxies) ENDIF() diff --git a/plugins/openssl/CMakeLists.txt b/plugins/openssl/CMakeLists.txt new file mode 100644 index 00000000000..6d56cfa6447 --- /dev/null +++ b/plugins/openssl/CMakeLists.txt @@ -0,0 +1,62 @@ +############################################################################## +# HPCC SYSTEMS software Copyright (C) 2025 HPCC Systems®. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################## + +# Component: openssl + +############################################################# +# Description: +# ----------- +# Cmake Input File for openssl +############################################################# + +project(openssl) + +find_package(OpenSSL REQUIRED) + +set( + SRCS + openssl.hpp + openssl.cpp +) + +include_directories( + ${HPCC_SOURCE_DIR}/system/include + ${HPCC_SOURCE_DIR}/rtl/eclrtl + ${HPCC_SOURCE_DIR}/rtl/include + ${HPCC_SOURCE_DIR}/common/deftype + ${HPCC_SOURCE_DIR}/system/jlib +) + +add_definitions(-D_USRDLL -DECL_OPENSSL_EXPORTS) +HPCC_ADD_LIBRARY(openssl SHARED ${SRCS}) + +install( + TARGETS openssl + DESTINATION plugins CALC_DEPS +) +install( + FILES ${OPENSSL_LIBRARIES} + DESTINATION ${LIB_DIR} + COMPONENT Runtime +) + +target_link_libraries( + openssl + eclrtl + jlib + OpenSSL::SSL + OpenSSL::Crypto +) diff --git a/plugins/openssl/openssl.cpp b/plugins/openssl/openssl.cpp new file mode 100644 index 00000000000..a39c66fd867 --- /dev/null +++ b/plugins/openssl/openssl.cpp @@ -0,0 +1,915 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2025 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +#include "openssl.hpp" + +#include "openssl/err.h" +#include "openssl/evp.h" +#include "openssl/pem.h" + +#include "jlog.hpp" + +#include +#include + +#define CURRENT_OPENSSL_VERSION "openssl plugin 1.0.0" + +static const char* opensslCompatibleVersions[] = { + CURRENT_OPENSSL_VERSION, + NULL }; + +OPENSSL_API bool OPENSSL_CALL getECLPluginDefinition(ECLPluginDefinitionBlock* pb) +{ + if (pb->size == sizeof(ECLPluginDefinitionBlockEx)) + { + ECLPluginDefinitionBlockEx* pbx = static_cast(pb); + pbx->compatibleVersions = opensslCompatibleVersions; + } + else if (pb->size != sizeof(ECLPluginDefinitionBlock)) + { + return false; + } + + pb->magicVersion = PLUGIN_VERSION; + pb->version = CURRENT_OPENSSL_VERSION; + pb->moduleName = "openssl"; + pb->ECL = NULL; + pb->flags = PLUGIN_IMPLICIT_MODULE; + pb->description = "ECL plugin library for the C++ API in OpenSSL"; + + return true; +} + +namespace nsOpenSSL +{ + +void failOpenSSLError(const std::string& context) +{ + unsigned long errCode = 0; + char buffer[120]; + + ERR_error_string_n(ERR_get_error(), buffer, sizeof(buffer)); + + std::string desc = "Error within "; + desc += context; + desc += ": "; + desc += buffer; + + rtlFail(-1, desc.c_str()); +} + +static constexpr int OPENSSL_MAX_CACHE_SIZE = 10; +static constexpr bool PRINT_STATS = false; +template +class OpenSSLCache +{ +public: + const T * checkCache(const char * algorithm) + { + for (auto& c : cache) + { + if (std::get<0>(c) == algorithm) + { + hits++; + return std::get<1>(c); + } + } + misses++; + const T * newObj = getObjectByName(algorithm); + if (newObj) + { + cache.emplace_front(algorithm, newObj); + if (cache.size() > OPENSSL_MAX_CACHE_SIZE) + cache.pop_back(); + } + else + failOpenSSLError("adding new object to cache"); + + return newObj; + }; + + void printStatistics() {DBGLOG("OPENSSL %s CACHE STATS: HITS = %d, MISSES = %d", cacheName.c_str(), hits, misses);}; + + void init() + { + setCacheName(); + hits = 0; + misses = 0; + }; + +private: + int hits; + int misses; + std::string cacheName; + std::list> cache; + + void setCacheName(); + const T * getObjectByName(const char * name); +}; + +template <> +void OpenSSLCache::setCacheName() { cacheName = "CIPHER"; } + +template <> +void OpenSSLCache::setCacheName() { cacheName = "DIGEST"; } + +template <> +const EVP_CIPHER * OpenSSLCache::getObjectByName(const char * name) { return EVP_get_cipherbyname(name); } + +template <> +const EVP_MD * OpenSSLCache::getObjectByName(const char * name) { return EVP_get_digestbyname(name); } + +// PEM Public/Private keys require parsing from a string +// Store the hash of the original string and parsed key +class PKeyCache +{ +public: + void init() + { + hits = 0; + misses = 0; + }; + + EVP_PKEY * checkCache(size32_t keyLen, const char * key) + { + for (auto& c : cache) + { + if (strncmp(std::get<0>(c).c_str(), key, keyLen) == 0) + { + hits++; + return std::get<1>(c); + } + } + + misses++; + + BIO * bio = BIO_new_mem_buf(key, keyLen); + if (!bio) + failOpenSSLError("creating buffer for EVP_PKEY"); + + EVP_PKEY * pkey; + if (startsWith(key, "-----BEGIN RSA PUBLIC KEY-----")) + pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + else + pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + + BIO_free(bio); + if (pkey) + { + cache.emplace_front(std::string(key, keyLen), pkey); + if (cache.size() > OPENSSL_MAX_CACHE_SIZE) + { + EVP_PKEY_free(std::get<1>(cache.back())); + cache.pop_back(); + } + } + else + failOpenSSLError("loading a pkey"); + + return pkey; + }; + + void clear() + { + for (auto& c : cache) + EVP_PKEY_free(std::get<1>(c)); + cache.clear(); + }; + + void printStatistics() {DBGLOG("OPENSSL PKEY CACHE STATS: HITS = %d, MISSES = %d", hits, misses);}; + +private: + int hits; + int misses; + std::list> cache; +}; + + +static thread_local PKeyCache pkeyCache; + +static thread_local OpenSSLCache cipherCache; +static thread_local OpenSSLCache digestCache; +} // nsOpenSSL + +using namespace nsOpenSSL; + +//-------------------------------------------------------------------------- +// Advertised Entry Point Functions +//-------------------------------------------------------------------------- + +OPENSSL_API void OPENSSL_CALL digestAvailableAlgorithms(ICodeContext *ctx, size32_t & __lenResult, void * & __result) +{ + // Get all the hash (digest) names + OpenSSL_add_all_digests(); + std::vector digestNames; + EVP_MD_do_all([](const EVP_MD * md, const char * name, const char * description, void * arg) { + std::vector * digestNames = static_cast*>(arg); + digestNames->push_back(name); + }, &digestNames); + EVP_cleanup(); + + __lenResult = 0; + __result = nullptr; + + // Determine final size of returned dataset + for (auto& name : digestNames) + __lenResult += (sizeof(size32_t) + name.size()); + + // Allocate and populate result block + if (__lenResult > 0) + { + MemoryBuffer resultBuffer(__lenResult); + size32_t stringSize = 0; + for (auto& name : digestNames) + { + stringSize = name.size(); + resultBuffer.append(stringSize); + resultBuffer.append(stringSize, name.data()); + } + + __result = resultBuffer.detachOwn(); + } +} + +OPENSSL_API void OPENSSL_CALL digestHash(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_indata, const void * _indata, const char * _hash_name) +{ + if (strlen(_hash_name) == 0) + rtlFail(-1, "No hash digest name provided"); + + const EVP_MD * md = digestCache.checkCache(_hash_name); + + EVP_MD_CTX * mdContext = EVP_MD_CTX_new(); + if (!mdContext) + failOpenSSLError("creating a hash digest context"); + + try + { + if (EVP_DigestInit_ex(mdContext, md, nullptr) != 1) + failOpenSSLError("EVP_DigestInit_ex"); + + if (EVP_DigestUpdate(mdContext, _indata, len_indata) != 1) + failOpenSSLError("EVP_DigestUpdate"); + + __lenResult = EVP_MD_CTX_get_size(mdContext); + MemoryBuffer resultBuffer(__lenResult); + + if (EVP_DigestFinal_ex(mdContext, static_cast(resultBuffer.bufferBase()), nullptr) != 1) + failOpenSSLError("EVP_DigestFinal_ex"); + + __result = resultBuffer.detachOwn(); + } + catch (...) + { + EVP_MD_CTX_free(mdContext); + throw; + } + + EVP_MD_CTX_free(mdContext); +} + +// Symmetric ciphers + +OPENSSL_API void OPENSSL_CALL cipherAvailableAlgorithms(ICodeContext *ctx, size32_t & __lenResult, void * & __result) +{ + // Get all the cipher names + OpenSSL_add_all_ciphers(); + std::vector cipherNames; + EVP_CIPHER_do_all([](const EVP_CIPHER * cipher, const char * from, const char * to, void * x) { + auto cipherNames = static_cast *>(x); + cipherNames->push_back(from); + }, &cipherNames); + EVP_cleanup(); + + __lenResult = 0; + __result = nullptr; + + // Determine final size of returned dataset + for (auto& name : cipherNames) + __lenResult += (sizeof(uint32_t) + name.size()); + + // Allocate and populate result block + if (__lenResult > 0) + { + MemoryBuffer resultBuffer(__lenResult); + size32_t stringSize = 0; + for (auto& name : cipherNames) + { + stringSize = name.size(); + resultBuffer.append(stringSize); + resultBuffer.append(stringSize, name.data()); + } + + __result = resultBuffer.detachOwn(); + } +} + +OPENSSL_API uint16_t OPENSSL_CALL cipherIVSize(ICodeContext *ctx, const char * algorithm) +{ + if (strlen(algorithm) == 0) + rtlFail(-1, "No algorithm name provided"); + + // Load the cipher + const EVP_CIPHER * cipher = cipherCache.checkCache(algorithm); + + return static_cast(EVP_CIPHER_iv_length(cipher)); +} + +OPENSSL_API void OPENSSL_CALL cipherEncrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, const char * _algorithm, size32_t len_passphrase, const void * _passphrase, size32_t len_iv, const void * _iv, size32_t len_salt, const void * _salt) +{ + __result = nullptr; + __lenResult = 0; + + bool hasIV = (len_iv > 0); + bool hasSalt = (len_salt > 0); + + // Initial sanity check of our arguments + if (strlen(_algorithm) == 0) + rtlFail(-1, "No algorithm name provided"); + if (len_passphrase == 0) + rtlFail(-1, "No passphrase provided"); + if (hasSalt && len_salt != 8) + rtlFail(-1, "Salt value must be exactly 8 bytes in size"); + + if (len_plaintext > 0) + { + // Load the cipher + const EVP_CIPHER * cipher = cipherCache.checkCache(_algorithm); + + int cipherIVSize = EVP_CIPHER_iv_length(cipher); + if (hasIV && len_iv != static_cast(cipherIVSize)) + rtlFail(-1, "Supplied IV is an incorrect size"); + + // Generate a key and an IV (if one was not provided) + MemoryBuffer key(EVP_MAX_KEY_LENGTH); + MemoryBuffer iv(cipherIVSize); + if (!EVP_BytesToKey(cipher, EVP_sha256(), (hasSalt ? static_cast(_salt) : nullptr), static_cast(_passphrase), len_passphrase, 1, static_cast(key.bufferBase()), static_cast(iv.bufferBase()))) + failOpenSSLError("generating an encryption key from the passphrase"); + + // If an IV was supplied, copy it over the one that was generated + if (hasIV) + iv.append(cipherIVSize, _iv); + + // Initialize the context + EVP_CIPHER_CTX * encryptCtx = EVP_CIPHER_CTX_new(); + if (!encryptCtx) + failOpenSSLError("EVP_CIPHER_CTX_new"); + + // Reserve buffers + __lenResult = len_plaintext + EVP_CIPHER_block_size(cipher); // max size, may be changed later + MemoryBuffer resultBuffer(__lenResult); + + try + { + int len = 0; + int ciphertextLen = 0; + + if (EVP_EncryptInit_ex(encryptCtx, cipher, nullptr, static_cast(key.bufferBase()),static_cast(iv.bufferBase())) != 1) + failOpenSSLError("EVP_EncryptInit_ex"); + + if (EVP_EncryptUpdate(encryptCtx, static_cast(resultBuffer.bufferBase()), &len, static_cast(_plaintext), len_plaintext) != 1) + failOpenSSLError("EVP_EncryptUpdate"); + ciphertextLen = len; + + if (EVP_EncryptFinal_ex(encryptCtx, static_cast(resultBuffer.bufferBase()) + len, &len) != 1) + failOpenSSLError("EVP_EncryptFinal_ex"); + ciphertextLen += len; + __lenResult = ciphertextLen; + __result = resultBuffer.detachOwn(); + } + catch (...) + { + EVP_CIPHER_CTX_free(encryptCtx); + __lenResult = 0; + rtlFree(__result); + __result = nullptr; + throw; + } + + EVP_CIPHER_CTX_free(encryptCtx); + } +} + +OPENSSL_API void OPENSSL_CALL cipherDecrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_ciphertext, const void * _ciphertext, const char * _algorithm, size32_t len_passphrase, const void * _passphrase, size32_t len_iv, const void * _iv, size32_t len_salt, const void * _salt) +{ + __result = nullptr; + __lenResult = 0; + + bool hasIV = (len_iv > 0); + bool hasSalt = (len_salt > 0); + + // Initial sanity check of our arguments + if (strlen(_algorithm) == 0) + rtlFail(-1, "No algorithm name provided"); + if (len_passphrase == 0) + rtlFail(-1, "No passphrase provided"); + if (hasSalt && len_salt != 8) + rtlFail(-1, "Salt value must be exactly 8 bytes in size"); + + if (len_ciphertext > 0) + { + // Load the cipher + const EVP_CIPHER * cipher = cipherCache.checkCache(_algorithm); + + int cipherIVSize = EVP_CIPHER_iv_length(cipher); + if (hasIV && len_iv != static_cast(cipherIVSize)) + rtlFail(-1, "Supplied IV is an incorrect size"); + + // Generate a key and an IV (if one was not provided) + MemoryBuffer key(EVP_MAX_KEY_LENGTH); + MemoryBuffer iv(cipherIVSize); + if (!EVP_BytesToKey(cipher, EVP_sha256(), (hasSalt ? static_cast(_salt) : nullptr), static_cast(_passphrase), len_passphrase, 1, static_cast(key.bufferBase()), static_cast(iv.bufferBase()))) + failOpenSSLError("generating an encryption key from the passphrase"); + + // If an IV was supplied, copy it over the one that was generated + if (hasIV) + iv.append(cipherIVSize, _iv); + + // Initialize the context + EVP_CIPHER_CTX * decryptCtx = EVP_CIPHER_CTX_new(); + if (!decryptCtx) + failOpenSSLError("EVP_CIPHER_CTX_new"); + + // Reserve buffers + __lenResult = len_ciphertext; // max size, may be changed later + MemoryBuffer resultBuffer(__lenResult); + + try + { + int len = 0; + int plaintextLen = 0; + + if (EVP_DecryptInit_ex(decryptCtx, cipher, nullptr, static_cast(key.bufferBase()), static_cast(iv.bufferBase())) != 1) + failOpenSSLError("EVP_DecryptInit_ex"); + + if (EVP_DecryptUpdate(decryptCtx, static_cast(resultBuffer.bufferBase()), &len, static_cast(_ciphertext), len_ciphertext) != 1) + failOpenSSLError("EVP_DecryptUpdate"); + plaintextLen = len; + + if (EVP_DecryptFinal_ex(decryptCtx, static_cast(resultBuffer.bufferBase()) + len, &len) != 1) + failOpenSSLError("EVP_DecryptFinal_ex"); + plaintextLen += len; + __lenResult = plaintextLen; + __result = resultBuffer.detachOwn(); + } + catch (...) + { + EVP_CIPHER_CTX_free(decryptCtx); + __lenResult = 0; + rtlFree(__result); + __result = nullptr; + throw; + } + + EVP_CIPHER_CTX_free(decryptCtx); + } +} + +// RSA functions + +OPENSSL_API void OPENSSL_CALL rsaSeal(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, bool isAll_pem_public_keys, size32_t len_pem_public_keys, const void * _pem_public_keys, const char * _symmetric_algorithm) +{ + // Initial sanity check of our arguments + if (len_pem_public_keys == 0) + rtlFail(-1, "No public keys provided"); + + if (!isAll_pem_public_keys && len_plaintext > 0) + { + std::vector publicKeys; + EVP_CIPHER_CTX * encryptCtx = nullptr; + byte ** encryptedKeys = nullptr; + MemoryBuffer iv; + MemoryBuffer ciphertext; + + try + { + // Parse public keys and stuff them into a vector + const char * pubKeyPtr = static_cast(_pem_public_keys); + const char * endPtr = pubKeyPtr + len_pem_public_keys; + while (pubKeyPtr < endPtr) + { + const size32_t keySize = *(reinterpret_cast(pubKeyPtr)); + pubKeyPtr += sizeof(keySize); + publicKeys.push_back(pkeyCache.checkCache(keySize, pubKeyPtr)); + pubKeyPtr += keySize; + } + + // Load the cipher + const EVP_CIPHER * cipher = cipherCache.checkCache(_symmetric_algorithm); + + // Allocate memory for encrypted keys + size_t keyCount = publicKeys.size(); + encryptedKeys = static_cast(rtlMalloc(sizeof(byte *) * keyCount)); + for (size_t x = 0; x < keyCount; x++) + encryptedKeys[x] = static_cast(rtlMalloc(EVP_PKEY_size(publicKeys[x]))); + + // Allocate memory for the IV + int ivLen = EVP_CIPHER_iv_length(cipher); + iv.ensureCapacity(ivLen); + + // Allocate buffer for ciphertext + int ciphertextLen = len_plaintext + EVP_CIPHER_block_size(cipher); + ciphertext.ensureCapacity(ciphertextLen); + + // Create and initialize the context + encryptCtx = EVP_CIPHER_CTX_new(); + if (!encryptCtx) + failOpenSSLError("creating cipher context"); + + // Initialize the envelope + std::vector keyLens(keyCount); + if (EVP_SealInit(encryptCtx, cipher, encryptedKeys, keyLens.data(), static_cast(iv.bufferBase()), publicKeys.data(), keyCount) != static_cast(keyCount)) + failOpenSSLError("EVP_SealInit"); + + // Update the envelope (encrypt the plaintext) + int len = 0; + if (EVP_SealUpdate(encryptCtx, static_cast(ciphertext.bufferBase()), &len, reinterpret_cast(_plaintext), len_plaintext) != 1) + failOpenSSLError("EVP_SealUpdate"); + ciphertextLen = len; + + // Finalize the envelope's ciphertext + if (EVP_SealFinal(encryptCtx, static_cast(ciphertext.bufferBase()) + len, &len) != 1) + failOpenSSLError("EVP_SealFinal"); + ciphertextLen += len; + + int totalKeyLen = 0; + for (int i = 0; i < keyCount; i++) + totalKeyLen += keyLens[i]; + + // We need to prepend the ciphertext with metadata so the blob can be decrypted; + // this is potentially nonstandard + MemoryBuffer outBuffer; + outBuffer.ensureCapacity(ivLen + (sizeof(size_t)*(keyCount+1)) + totalKeyLen + ciphertextLen); + // IV comes first; its length can be derived from the cipher + outBuffer.append(ivLen, static_cast(iv.bufferBase())); + // Number of keys (size_t) + outBuffer.append(sizeof(keyCount), reinterpret_cast(&keyCount)); + // Keys; each is (size_t) + + for (size_t x = 0; x < keyCount; x++) + { + size_t keyLen = keyLens[x]; + outBuffer.append(sizeof(keyLen), reinterpret_cast(&keyLen)); + outBuffer.append(keyLen, encryptedKeys[x]); + } + // And finally the ciphertext + outBuffer.append(ciphertextLen, static_cast(ciphertext.bufferBase())); + + // Copy to the ECL result buffer + __lenResult = outBuffer.length(); + __result = outBuffer.detachOwn(); + + // Cleanup + EVP_CIPHER_CTX_free(encryptCtx); + for (size_t x = 0; x < publicKeys.size(); x++) + rtlFree(encryptedKeys[x]); + rtlFree(encryptedKeys); + } + catch (...) + { + if (encryptCtx) + EVP_CIPHER_CTX_free(encryptCtx); + if (encryptedKeys) + { + for (size_t x = 0; x < publicKeys.size(); x++) + { + if (encryptedKeys[x]) + rtlFree(encryptedKeys[x]); + } + rtlFree(encryptedKeys); + } + __lenResult = 0; + rtlFree(__result); + __result = nullptr; + throw; + } + } +} + +OPENSSL_API void OPENSSL_CALL rsaUnseal(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_ciphertext, const void * _ciphertext, size32_t len_pem_private_key, const char * _pem_private_key, const char * _symmetric_algorithm) +{ + // Initial sanity check of our arguments + if (len_pem_private_key == 0) + rtlFail(-1, "No private key provided"); + + if (len_ciphertext > 0) + { + EVP_CIPHER_CTX * decryptCtx = nullptr; + MemoryBuffer symmetricKey; + MemoryBuffer iv; + MemoryBuffer plaintext; + + try + { + // Load the private key + EVP_PKEY * privateKey = pkeyCache.checkCache(len_pem_private_key, _pem_private_key); + + // Load the cipher + const EVP_CIPHER * cipher = cipherCache.checkCache(_symmetric_algorithm); + + // Allocate memory for the symmetric key and IV + int keyLen = EVP_PKEY_size(privateKey); + symmetricKey.ensureCapacity(keyLen); + int ivLen = EVP_CIPHER_iv_length(cipher); + iv.ensureCapacity(ivLen); + + // Unpack the structured ciphertext to extract the metadata + const byte * inPtr = static_cast(_ciphertext); + // IV comes first, length determined by the cipher + iv.append(ivLen, inPtr); + inPtr += ivLen; + // Number of keys embedded in the metadata (size_t) + size_t keyCount = 0; + memcpy(&keyCount, inPtr, sizeof(keyCount)); + inPtr += sizeof(keyCount); + // The keys; each has a length (size_t) then contents + std::vector encryptedKeys; + for (size_t x = 0; x < keyCount; x++) + { + size_t keySize = 0; + memcpy(&keySize, inPtr, sizeof(keySize)); + inPtr += sizeof(keySize); + encryptedKeys.emplace_back(reinterpret_cast(inPtr), keySize); + inPtr += keySize; + } + + const byte * newCipherText = inPtr; + size_t newCipherTextLen = (len_ciphertext - (reinterpret_cast(inPtr) - static_cast(_ciphertext))); + + // Initialize the context for decryption + decryptCtx = EVP_CIPHER_CTX_new(); + if (!decryptCtx) + failOpenSSLError("creating cipher context"); + + // Find an encrypted key that we can decrypt + bool found = false; + for (auto& encryptedKey : encryptedKeys) + { + if (EVP_OpenInit(decryptCtx, cipher, reinterpret_cast(encryptedKey.data()), encryptedKey.size(), static_cast(iv.bufferBase()), privateKey) == 1) + { + found = true; + break; + } + } + if (!found) + failOpenSSLError("EVP_OpenInit"); + + // Allocate memory for the plaintext + int plaintextLen = newCipherTextLen; + plaintext.ensureCapacity(plaintextLen); + + int len = 0; + if (EVP_OpenUpdate(decryptCtx, static_cast(plaintext.bufferBase()), &len, newCipherText, newCipherTextLen) != 1) + failOpenSSLError("EVP_OpenUpdate"); + plaintextLen = len; + + if (EVP_OpenFinal(decryptCtx, static_cast(plaintext.bufferBase()) + len, &len) != 1) + failOpenSSLError("EVP_OpenFinal"); + plaintextLen += len; + + // Copy to the ECL result buffer + __lenResult = plaintextLen; + MemoryBuffer resultBuffer(__lenResult); + resultBuffer.append(__lenResult, plaintext.bufferBase()); + __result = resultBuffer.detachOwn(); + + // Cleanup + EVP_CIPHER_CTX_free(decryptCtx); + } + catch (...) + { + if (decryptCtx) + EVP_CIPHER_CTX_free(decryptCtx); + __lenResult = 0; + rtlFree(__result); + __result = nullptr; + throw; + } + } +} + +OPENSSL_API void OPENSSL_CALL rsaEncrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, size32_t len_pem_public_key, const char * _pem_public_key) +{ + __result = nullptr; + __lenResult = 0; + + // Initial sanity check of our arguments + if (len_pem_public_key == 0) + rtlFail(-1, "No public key provided"); + + EVP_PKEY_CTX * encryptCtx = nullptr; + + if (len_plaintext > 0) + { + try + { + // Load key from buffer + EVP_PKEY * publicKey = pkeyCache.checkCache(len_pem_public_key, _pem_public_key); + + // Create encryption context + encryptCtx = EVP_PKEY_CTX_new(publicKey, nullptr); + if (!encryptCtx) + failOpenSSLError("publicKey"); + if (EVP_PKEY_encrypt_init(encryptCtx) <= 0) + failOpenSSLError("EVP_PKEY_encrypt_init"); + + // Determine max size of output + size_t outLen = 0; + if (EVP_PKEY_encrypt(encryptCtx, nullptr, &outLen, reinterpret_cast(_plaintext), len_plaintext) <= 0) + failOpenSSLError("EVP_PKEY_encrypt"); + + // Set actual size of output + MemoryBuffer resultBuffer(outLen); + + if (EVP_PKEY_encrypt(encryptCtx, static_cast(resultBuffer.bufferBase()), &outLen, reinterpret_cast(_plaintext), len_plaintext) <= 0) + failOpenSSLError("EVP_PKEY_encrypt"); + + __lenResult = outLen; + __result = resultBuffer.detachOwn(); + + EVP_PKEY_CTX_free(encryptCtx); + } + catch (...) + { + if (encryptCtx) + EVP_PKEY_CTX_free(encryptCtx); + rtlFree(__result); + __lenResult = 0; + throw; + } + } +} + +OPENSSL_API void OPENSSL_CALL rsaDecrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_ciphertext, const void * _ciphertext, size32_t len_pem_private_key, const char * _pem_private_key) +{ + __result = nullptr; + __lenResult = 0; + + // Initial sanity check of our arguments + if (len_pem_private_key == 0) + rtlFail(-1, "No private key provided"); + + EVP_PKEY_CTX * decryptCtx = nullptr; + + if (len_ciphertext > 0) + { + try + { + // Load key from buffer + EVP_PKEY * privateKey = pkeyCache.checkCache(len_pem_private_key, _pem_private_key); + + // Create decryption context + decryptCtx = EVP_PKEY_CTX_new(privateKey, nullptr); + if (!decryptCtx) + failOpenSSLError("EVP_PKEY_CTX_new"); + if (EVP_PKEY_decrypt_init(decryptCtx) <= 0) + failOpenSSLError("EVP_PKEY_decrypt_init"); + + // Determine max size of output + size_t outLen = 0; + if (EVP_PKEY_decrypt(decryptCtx, nullptr, &outLen, reinterpret_cast(_ciphertext), len_ciphertext) <= 0) + failOpenSSLError("EVP_PKEY_decrypt"); + + // Set actual size of output + MemoryBuffer resultBuffer(outLen); + + if (EVP_PKEY_decrypt(decryptCtx, static_cast(resultBuffer.bufferBase()), &outLen, reinterpret_cast(_ciphertext), len_ciphertext) <= 0) + failOpenSSLError("EVP_PKEY_decrypt"); + + __lenResult = outLen; + __result = resultBuffer.detachOwn(); + + EVP_PKEY_CTX_free(decryptCtx); + } + catch (...) + { + if (decryptCtx) + EVP_PKEY_CTX_free(decryptCtx); + rtlFree(__result); + __lenResult = 0; + throw; + } + } +} + +OPENSSL_API void OPENSSL_CALL rsaSign(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, size32_t len_passphrase, const void * passphrase, size32_t len_pem_private_key, const char * _pem_private_key, const char * _hash_name) +{ + EVP_MD_CTX *mdCtx = nullptr; + + try + { + // Load the private key from the PEM string + EVP_PKEY * privateKey = pkeyCache.checkCache(len_pem_private_key, _pem_private_key); + + // Create and initialize the message digest context + mdCtx = EVP_MD_CTX_new(); + if (!mdCtx) + failOpenSSLError("EVP_MD_CTX_new (rsaSign)"); + + const EVP_MD *md = digestCache.checkCache(_hash_name); + + if (EVP_DigestSignInit(mdCtx, nullptr, md, nullptr, privateKey) <= 0) + failOpenSSLError("EVP_DigestSignInit (rsaSign)"); + + // Add plaintext to context + if (EVP_DigestSignUpdate(mdCtx, _plaintext, len_plaintext) <= 0) + failOpenSSLError("EVP_DigestSignUpdate (rsaSign)"); + + // Determine the buffer length for the signature + size_t signatureLen = 0; + if (EVP_DigestSignFinal(mdCtx, nullptr, &signatureLen) <= 0) + failOpenSSLError("determining result length (rsaSign)"); + + // Allocate memory for the signature + MemoryBuffer signatureBuffer(signatureLen); + + // Perform the actual signing + if (EVP_DigestSignFinal(mdCtx, static_cast(signatureBuffer.bufferBase()), &signatureLen) <= 0) + failOpenSSLError("signing (rsaSign)"); + + // Set the result + __lenResult = signatureLen; + __result = signatureBuffer.detachOwn(); + + // Clean up + EVP_MD_CTX_free(mdCtx); + } + catch (...) + { + if (mdCtx) + EVP_MD_CTX_free(mdCtx); + rtlFree(__result); + __lenResult = 0; + throw; + } +} + +OPENSSL_API bool OPENSSL_CALL rsaVerifySignature(ICodeContext *ctx, size32_t len_signature, const void * _signature, size32_t len_signedData, const void * _signedData, size32_t len_passphrase, const void * passphrase, size32_t len_pem_public_key, const char * _pem_public_key, const char * _hash_name) +{ + EVP_MD_CTX *mdCtx = nullptr; + + try + { + // Load the public key from the PEM string + EVP_PKEY * publicKey = pkeyCache.checkCache(len_pem_public_key, _pem_public_key); + + // Create and initialize the message digest context + mdCtx = EVP_MD_CTX_new(); + if (!mdCtx) + failOpenSSLError("EVP_MD_CTX_new"); + + const EVP_MD *md = digestCache.checkCache(_hash_name); + + if (EVP_DigestVerifyInit(mdCtx, nullptr, md, nullptr, publicKey) <= 0) + failOpenSSLError("EVP_DigestVerifyInit (rsaVerifySignature)"); + + if (EVP_DigestVerifyUpdate(mdCtx, _signedData, len_signedData) <= 0) + failOpenSSLError("EVP_DigestVerifyUpdate (rsaVerifySignature)"); + + // Perform the actual verification + int res = EVP_DigestVerifyFinal(mdCtx, reinterpret_cast(_signature), len_signature); + + // Clean up + EVP_MD_CTX_free(mdCtx); + return res == 1; + } + catch (...) + { + if (mdCtx) + EVP_MD_CTX_free(mdCtx); + throw; + } +} + +MODULE_INIT(INIT_PRIORITY_STANDARD) +{ + pkeyCache.init(); + digestCache.init(); + cipherCache.init(); + + return true; +} + +MODULE_EXIT() +{ + if(PRINT_STATS) + { + pkeyCache.printStatistics(); + digestCache.printStatistics(); + cipherCache.printStatistics(); + } + + pkeyCache.clear(); +} diff --git a/plugins/openssl/openssl.hpp b/plugins/openssl/openssl.hpp new file mode 100644 index 00000000000..0156da45eb7 --- /dev/null +++ b/plugins/openssl/openssl.hpp @@ -0,0 +1,62 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2025 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +#ifndef _OPENSSL_INCL +#define _OPENSSL_INCL + +#ifdef _WIN32 +#define OPENSSL_CALL _cdecl +#else +#define OPENSSL_CALL +#endif + +#ifdef OPENSSL_EXPORTS +#define OPENSSL_API DECL_EXPORT +#else +#define OPENSSL_API DECL_IMPORT +#endif + +#include "platform.h" +#include "jthread.hpp" +#include "hqlplugins.hpp" +#include "eclrtl_imp.hpp" +#include "eclhelper.hpp" + +extern "C++" +{ +OPENSSL_API bool OPENSSL_CALL getECLPluginDefinition(ECLPluginDefinitionBlock *pb); + +// Digest functions +OPENSSL_API void OPENSSL_CALL digestAvailableAlgorithms(ICodeContext *ctx, size32_t & __lenResult, void * & __result); +OPENSSL_API void OPENSSL_CALL digestHash(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_indata, const void * _indata, const char * _hash_name); + +// Cipher functions +OPENSSL_API void OPENSSL_CALL cipherAvailableAlgorithms(ICodeContext *ctx, size32_t & __lenResult, void * & __result); +OPENSSL_API uint16_t OPENSSL_CALL cipherIVSize(ICodeContext *ctx, const char * algorithm); +OPENSSL_API void OPENSSL_CALL cipherEncrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, const char * _algorithm, size32_t len_passphrase, const void * _passphrase, size32_t len_iv, const void * _iv, size32_t len_salt, const void * _salt); +OPENSSL_API void OPENSSL_CALL cipherDecrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_ciphertext, const void * _ciphertext, const char * _algorithm, size32_t len_passphrase, const void * _passphrase, size32_t len_iv, const void * _iv, size32_t len_salt, const void * _salt); + +// RSA functions +OPENSSL_API void OPENSSL_CALL rsaSeal(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, bool isAll_pem_public_keys, size32_t len_pem_public_keys, const void * _pem_public_keys, const char * _symmetric_algorithm); +OPENSSL_API void OPENSSL_CALL rsaUnseal(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_ciphertext, const void * _ciphertext, size32_t len_pem_private_key, const char * _pem_private_key, const char * _symmetric_algorithm); +OPENSSL_API void OPENSSL_CALL rsaEncrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, size32_t len_pem_public_key, const char * _pem_public_key); +OPENSSL_API void OPENSSL_CALL rsaDecrypt(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_ciphertext, const void * _ciphertext, size32_t len_pem_private_key, const char * _pem_private_key); +OPENSSL_API void OPENSSL_CALL rsaSign(ICodeContext *ctx, size32_t & __lenResult, void * & __result, size32_t len_plaintext, const void * _plaintext, size32_t len_passphrase, const void * passphrase, size32_t len_pem_private_key, const char * _pem_private_key, const char * _algorithm); +OPENSSL_API bool OPENSSL_CALL rsaVerifySignature(ICodeContext *ctx, size32_t len_signature, const void * _signature, size32_t len_signedData, const void * _signedData, size32_t len_passphrase, const void * passphrase, size32_t len_pem_public_key, const char * _pem_public_key, const char * _algorithm); +} + +#endif // ECL_OPENSSL_INCL diff --git a/plugins/proxies/CMakeLists.txt b/plugins/proxies/CMakeLists.txt index 459c161123c..1a309332896 100644 --- a/plugins/proxies/CMakeLists.txt +++ b/plugins/proxies/CMakeLists.txt @@ -21,6 +21,7 @@ install ( FILES ${CMAKE_CURRENT_SOURCE_DIR}/lib_metaphone3.ecllib DESTINATION ${ install ( FILES ${CMAKE_CURRENT_SOURCE_DIR}/lib_thorlib.ecllib DESTINATION ${proxies_out_dir} COMPONENT Runtime) install ( FILES ${CMAKE_CURRENT_SOURCE_DIR}/lib_word.ecllib DESTINATION ${proxies_out_dir} COMPONENT Runtime) install ( FILES ${CMAKE_CURRENT_SOURCE_DIR}/lib_fileservices.ecllib DESTINATION ${proxies_out_dir} COMPONENT Runtime) +install ( FILES ${CMAKE_CURRENT_SOURCE_DIR}/lib_openssl.ecllib DESTINATION ${proxies_out_dir} COMPONENT Runtime) IF (USE_OPENSSL) install ( FILES ${CMAKE_CURRENT_SOURCE_DIR}/lib_cryptolib.ecllib DESTINATION plugins COMPONENT Runtime) diff --git a/plugins/proxies/lib_openssl.ecllib b/plugins/proxies/lib_openssl.ecllib new file mode 100644 index 00000000000..93e678832e9 --- /dev/null +++ b/plugins/proxies/lib_openssl.ecllib @@ -0,0 +1,39 @@ +/*############################################################################## + + HPCC SYSTEMS software Copyright (C) 2025 HPCC Systems®. + + Licensed under the Apache License, Version 2.0 (the License); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an AS IS BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +############################################################################## */ + +// Service definition +export OpenSSL := SERVICE : plugin('openssl') + + // Hash functions + DATASET({STRING name}) digestAvailableAlgorithms() : cpp,action,context,entrypoint='digestAvailableAlgorithms'; + DATA digestHash(DATA _indata, VARSTRING _hash_name) : cpp,action,context,entrypoint='digestHash'; + + // Symmetric ciphers + DATASET({STRING name}) cipherAvailableAlgorithms() : cpp,action,context,entrypoint='cipherAvailableAlgorithms'; + UNSIGNED2 cipherIVSize(VARSTRING algorithm) : cpp,action,context,entrypoint='cipherIVSize'; + DATA cipherEncrypt(DATA _plaintext, VARSTRING _algorithm, DATA _passphrase, DATA _iv = (DATA)'', DATA _salt = (DATA)'') : cpp,action,context,entrypoint='cipherEncrypt'; + DATA cipherDecrypt(DATA _ciphertext, VARSTRING _algorithm, DATA _passphrase, DATA _iv = (DATA)'', DATA _salt = (DATA)'') : cpp,action,context,entrypoint='cipherDecrypt'; + + // RSA + DATA rsaSeal(DATA _plaintext, SET OF STRING _pem_public_keys, VARSTRING _symmetric_algorithm) : cpp,action,context,entrypoint='rsaSeal'; + DATA rsaUnseal(DATA _ciphertext, STRING _pem_private_key, VARSTRING _symmetric_algorithm) : cpp,action,context,entrypoint='rsaUnseal'; + DATA rsaEncrypt(DATA _plaintext, STRING _pem_public_key) : cpp,action,context,entrypoint='rsaEncrypt'; + DATA rsaDecrypt(DATA _ciphertext, STRING _pem_private_key) : cpp,action,context,entrypoint='rsaDecrypt'; + DATA rsaSign(DATA _plaintext, DATA _passphrase, STRING _pem_private_key, VARSTRING _hash_name) : cpp,action,context,entrypoint='rsaSign'; + BOOLEAN rsaVerifySignature(DATA _signature, DATA _signedData, DATA _passphrase, STRING _pem_public_key, VARSTRING _hash_name) : cpp,action,context,entrypoint='rsaVerifySignature'; + +END; diff --git a/testing/regress/ecl/teststdlibrary.ecl b/testing/regress/ecl/teststdlibrary.ecl index 40cd8e2a305..b1f706ca6ba 100644 --- a/testing/regress/ecl/teststdlibrary.ecl +++ b/testing/regress/ecl/teststdlibrary.ecl @@ -28,6 +28,7 @@ //version tmod='Crypto' //version tmod='str' //version tmod='Math' +//version tmod='OpenSSL' //version tmod='DataPatterns' // /version tmod='DataPatterns.TestDataPatterns'