From 14ad720a7ff9b1532f825dcb7dc4ff93f60216c4 Mon Sep 17 00:00:00 2001 From: ashendes Date: Fri, 22 Mar 2024 15:27:05 +0530 Subject: [PATCH 1/3] [Automated] Update the native jar versions --- ballerina/Dependencies.toml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 6212e698..a57030f0 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -13,6 +13,7 @@ name = "crypto" version = "2.6.2" dependencies = [ {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.array"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"} ] @@ -28,6 +29,29 @@ modules = [ {org = "ballerina", packageName = "jballerina.java", moduleName = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "lang.__internal" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.object"} +] + +[[package]] +org = "ballerina" +name = "lang.array" +version = "0.0.0" +scope = "testOnly" +dependencies = [ + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.__internal"} +] +modules = [ + {org = "ballerina", packageName = "lang.array", moduleName = "lang.array"} +] + [[package]] org = "ballerina" name = "lang.error" @@ -37,6 +61,12 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "lang.object" +version = "0.0.0" +scope = "testOnly" + [[package]] org = "ballerina" name = "test" From a9714a42c3a2bc14d59e04bd044ba3ce99f62ce7 Mon Sep 17 00:00:00 2001 From: ashendes Date: Mon, 25 Mar 2024 15:17:27 +0530 Subject: [PATCH 2/3] PQC crypto unit tests added. --- ballerina/kdf.bal | 2 +- ballerina/kem.bal | 6 +- ballerina/tests/hpke_test.bal | 56 +++++++ ballerina/tests/kdf_test.bal | 67 ++++++++ ballerina/tests/kem_test.bal | 145 ++++++++++++++++++ .../ballerina/stdlib/crypto/CryptoUtils.java | 12 +- 6 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 ballerina/tests/hpke_test.bal create mode 100644 ballerina/tests/kdf_test.bal create mode 100644 ballerina/tests/kem_test.bal diff --git a/ballerina/kdf.bal b/ballerina/kdf.bal index 41fc5b23..648096bd 100644 --- a/ballerina/kdf.bal +++ b/ballerina/kdf.bal @@ -23,7 +23,7 @@ import ballerina/jballerina.java; # + salt - Optional salt value, a non-secret random value # + info - Optional context and application-specific information # + return - The derived keying material (OKM) of the specified length -public isolated function hkdfSha256(byte[] input, int length, byte[] salt = [0], byte[] info = []) returns byte[]|Error = @java:Method { +public isolated function hkdfSha256(byte[] input, int length, byte[] salt = [], byte[] info = []) returns byte[]|Error = @java:Method { name: "hkdfSha256", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Kdf" } external; diff --git a/ballerina/kem.bal b/ballerina/kem.bal index dbbabe8c..e62ba18b 100644 --- a/ballerina/kem.bal +++ b/ballerina/kem.bal @@ -31,7 +31,7 @@ public type EncapsulationResult record {| # path: "/path/to/keyStore.p12", # password: "keyStorePassword" # }; -# crypto:PublicKey publicKey = check crypto:decodeMlKem768PublicKeyFromTrustStore(keyStore, "keyAlias", "keyStorePassword"); +# crypto:PublicKey publicKey = check crypto:decodeMlKem768PublicKeyFromTrustStore(keyStore, "keyAlias"); # crypto:EncapsulationResult encapsulationResult = check crypto:encapsulateMlKem768(publicKey); # ``` # + publicKey - Public key @@ -105,8 +105,8 @@ public isolated function encapsulateRsaKemMlKem768(PublicKey rsaPublicKey, Publi # crypto:PublicKey rsaPublicKey = check crypto:decodeRsaPublicKeyFromTrustStore(rsaKeyStore, "keyAlias"); # crypto:EncapsulationResult encapsulationResult = check crypto:encapsulateRsaKemMlKem768(rsaPublicKey, mlkemPublicKey); # byte[] encapsulatedSecret = encapsulationResult.encapsulatedSecret; -# crypto:PrivateKey mlkemPrivateKey = check crypto:decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "keyAlias"); -# crypto:PrivateKey rsaPrivateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(rsaKeyStore, "keyAlias"); +# crypto:PrivateKey mlkemPrivateKey = check crypto:decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "keyAlias", "keyStorePassword"); +# crypto:PrivateKey rsaPrivateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(rsaKeyStore, "keyAlias", "keyStorePassword"); # byte[] sharedSecret = check crypto:decapsulateRsaKemMlKem768(encapsulatedSecret, rsaPrivateKey, mlkemPrivateKey); # ``` # + encapsulatedSecret - Encapsulated secret diff --git a/ballerina/tests/hpke_test.bal b/ballerina/tests/hpke_test.bal new file mode 100644 index 00000000..6be1fc9a --- /dev/null +++ b/ballerina/tests/hpke_test.bal @@ -0,0 +1,56 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you 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 ballerina/test; + +@test:Config {} +isolated function testEncryptAndDecryptMlKem768Hpke() returns Error? { + byte[] message = "Ballerina crypto test ".toBytes(); + KeyStore mlkemKeyStore = { + path: MLKEM_KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey publicKey = check decodeMlKem768PublicKeyFromTrustStore(mlkemKeyStore, "mlkem-keypair"); + HybridEncryptionResult hybridEncryptionResult = check encryptMlKem768Hpke(message, publicKey); + + PrivateKey privateKey = check decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "mlkem-keypair", "ballerina"); + byte[] decryptedMessage = check decryptMlKem768Hpke(hybridEncryptionResult.cipherText, hybridEncryptionResult.encapsulatedSecret, privateKey); + + test:assertEquals(decryptedMessage, message); +} + +@test:Config {} +isolated function testEncryptAndDecryptRsaMlKem768Hpke() returns Error? { + byte[] message = "Ballerina crypto test ".toBytes(); + KeyStore mlkemKeyStore = { + path: MLKEM_KEYSTORE_PATH, + password: "ballerina" + }; + KeyStore rsaKeyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey mlKemPublicKey = check decodeMlKem768PublicKeyFromTrustStore(mlkemKeyStore, "mlkem-keypair"); + PublicKey rsaPublicKey = check decodeRsaPublicKeyFromTrustStore(rsaKeyStore, "ballerina"); + HybridEncryptionResult hybridEncryptionResult = check encryptRsaMlKem768Hpke(message, rsaPublicKey, mlKemPublicKey); + + PrivateKey mlKemPrivateKey = check decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "mlkem-keypair", "ballerina"); + PrivateKey rsaPrivateKey = check decodeRsaPrivateKeyFromKeyStore(rsaKeyStore, "ballerina", "ballerina"); + byte[] decryptedMessage = check decryptRsaMlKem768Hpke(hybridEncryptionResult.cipherText, hybridEncryptionResult.encapsulatedSecret, + rsaPrivateKey, mlKemPrivateKey); + + test:assertEquals(decryptedMessage, message); +} diff --git a/ballerina/tests/kdf_test.bal b/ballerina/tests/kdf_test.bal new file mode 100644 index 00000000..5965732d --- /dev/null +++ b/ballerina/tests/kdf_test.bal @@ -0,0 +1,67 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you 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 ballerina/test; +import ballerina/lang.array; + +@test:Config {} +isolated function testHkdfSha256() returns error? { + string sharedSecret = "Hu7q5+8SI61d7kKsD3qMxkdPYOnp+6tMp5YkR6NuN28="; + string expectedDerivedKey = "Oze/CwrXylVzqWXirsT15/qGWe/iXwe+xeCRB9PrRZE="; + + byte[] decodedSharedSecret = check array:fromBase64(sharedSecret); + byte[] derivedKey = check hkdfSha256(decodedSharedSecret, 32); + string encodedDerivedKey = array:toBase64(derivedKey); + test:assertEquals(derivedKey.length(), 32); + test:assertEquals(encodedDerivedKey, expectedDerivedKey); +} + +@test:Config {} +isolated function testHkdfSha256WithSalt() returns error? { + string sharedSecret = "Hu7q5+8SI61d7kKsD3qMxkdPYOnp+6tMp5YkR6NuN28="; + string salt = "NaCl"; + string expectedDerivedKey = "JsmMnsD8uGUePvUAAuweZoAZteedTbK2Xs2ezbf/LWc="; + + byte[] decodedSharedSecret = check array:fromBase64(sharedSecret); + byte[] derivedKey = check hkdfSha256(decodedSharedSecret, 32, salt.toBytes()); + string encodedDerivedKey = array:toBase64(derivedKey); + test:assertEquals(encodedDerivedKey, expectedDerivedKey); +} + +@test:Config {} +isolated function testHkdfSha256WithInfo() returns error? { + string sharedSecret = "Hu7q5+8SI61d7kKsD3qMxkdPYOnp+6tMp5YkR6NuN28="; + string info = "info"; + string expectedDerivedKey = "oTPUhp3AWCESr1dIZyYJySgixUPMb+8hn3gm/t2sQ6M="; + + byte[] decodedSharedSecret = check array:fromBase64(sharedSecret); + byte[] derivedKey = check hkdfSha256(decodedSharedSecret, 32, info = info.toBytes()); + string encodedDerivedKey = array:toBase64(derivedKey); + test:assertEquals(encodedDerivedKey, expectedDerivedKey); +} + +@test:Config {} +isolated function testHkdfSha256WithSaltAndInfo() returns error? { + string sharedSecret = "Hu7q5+8SI61d7kKsD3qMxkdPYOnp+6tMp5YkR6NuN28="; + string salt = "NaCl"; + string info = "info"; + string expectedDerivedKey = "7ciQw6QKifikWk4NccsJ2q5CYPyJVfVrlblSBqGaB3o="; + + byte[] decodedSharedSecret = check array:fromBase64(sharedSecret); + byte[] derivedKey = check hkdfSha256(decodedSharedSecret, 32, salt.toBytes(), info.toBytes()); + string encodedDerivedKey = array:toBase64(derivedKey); + test:assertEquals(encodedDerivedKey, expectedDerivedKey); +} diff --git a/ballerina/tests/kem_test.bal b/ballerina/tests/kem_test.bal new file mode 100644 index 00000000..b94bd1f4 --- /dev/null +++ b/ballerina/tests/kem_test.bal @@ -0,0 +1,145 @@ +// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you 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 ballerina/test; + +@test:Config {} +isolated function testEncapsulateandDecapsulateMlKem768() returns Error? { + KeyStore mlkemKeyStore = { + path: MLKEM_KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey publicKey = check decodeMlKem768PublicKeyFromTrustStore(mlkemKeyStore, "mlkem-keypair"); + EncapsulationResult encapsulationResult = check encapsulateMlKem768(publicKey); + + PrivateKey privateKey = check decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "mlkem-keypair", "ballerina"); + byte[] sharedSecret = check decapsulateMlKem768(encapsulationResult.encapsulatedSecret, privateKey); + + test:assertEquals(sharedSecret, encapsulationResult.sharedSecret); +} + +@test:Config {} +isolated function testEncapsulateMlKem768WithInvalidPublicKey() returns Error? { + KeyStore rsaKeyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey publicKey = check decodeRsaPublicKeyFromTrustStore(rsaKeyStore, "ballerina"); + EncapsulationResult|Error encapsulationResult = encapsulateMlKem768(publicKey); + if encapsulationResult is Error { + test:assertEquals(encapsulationResult.message(), "Error occurred while generating encapsulated key: key generator locked to KYBER768"); + } else { + test:assertFail("Expected error not found."); + } +} + +@test:Config {} +isolated function testDecapsulateMlKem768WithInvalidPrivateKey() returns Error? { + KeyStore mlkemKeyStore = { + path: MLKEM_KEYSTORE_PATH, + password: "ballerina" + }; + KeyStore rsaKeyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey publicKey = check decodeMlKem768PublicKeyFromTrustStore(mlkemKeyStore, "mlkem-keypair"); + EncapsulationResult encapsulationResult = check encapsulateMlKem768(publicKey); + + PrivateKey privateKey = check decodeRsaPrivateKeyFromKeyStore(rsaKeyStore, "ballerina", "ballerina"); + byte[]|Error sharedSecret = decapsulateMlKem768(encapsulationResult.encapsulatedSecret, privateKey); + + if sharedSecret is Error { + test:assertEquals(sharedSecret.message(), "Error occurred while extracting secret: key generator locked to KYBER768"); + } else { + test:assertFail("Expected error not found."); + } +} + +@test:Config {} +isolated function testEncapsulateAndDecapsulateRsaKem() returns Error? { + KeyStore rsaKeyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey publicKey = check decodeRsaPublicKeyFromTrustStore(rsaKeyStore, "ballerina"); + EncapsulationResult encapsulationResult = check encapsulateRsaKem(publicKey); + + PrivateKey privateKey = check decodeRsaPrivateKeyFromKeyStore(rsaKeyStore, "ballerina", "ballerina"); + byte[] sharedSecret = check decapsulateRsaKem(encapsulationResult.encapsulatedSecret, privateKey); + + test:assertEquals(sharedSecret, encapsulationResult.sharedSecret); +} + +@test:Config {} +isolated function testEncapsulateRsaKemWithInvalidPublicKey() returns Error? { + KeyStore mlkemKeyStore = { + path: MLKEM_KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey publicKey = check decodeMlKem768PublicKeyFromTrustStore(mlkemKeyStore, "mlkem-keypair"); + EncapsulationResult|Error encapsulationResult = encapsulateRsaKem(publicKey); + if encapsulationResult is Error { + test:assertEquals(encapsulationResult.message(), "Error occurred while generating encapsulated key: " + + "valid RSA public key expected"); + } else { + test:assertFail("Expected error not found."); + } +} + +@test:Config {} +isolated function testDecapsulateRsaKemWithInvalidPrivateKey() returns Error? { + KeyStore rsaKeyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + KeyStore mlkemKeyStore = { + path: MLKEM_KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey publicKey = check decodeRsaPublicKeyFromTrustStore(rsaKeyStore, "ballerina"); + EncapsulationResult encapsulationResult = check encapsulateRsaKem(publicKey); + + PrivateKey privateKey = check decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "mlkem-keypair", "ballerina"); + byte[]|Error sharedSecret = decapsulateRsaKem(encapsulationResult.encapsulatedSecret, privateKey); + + if sharedSecret is Error { + test:assertEquals(sharedSecret.message(), "Error occurred while extracting secret: valid RSA privatekey expected"); + } else { + test:assertFail("Expected error not found."); + } +} + +@test:Config {} +isolated function testEncapsulateAndDecapsulateRsaKemMlKem768() returns Error? { + KeyStore mlkemKeyStore = { + path: MLKEM_KEYSTORE_PATH, + password: "ballerina" + }; + KeyStore rsaKeyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PublicKey mlkemPublicKey = check decodeMlKem768PublicKeyFromTrustStore(mlkemKeyStore, "mlkem-keypair"); + PublicKey rsaPublicKey = check decodeRsaPublicKeyFromTrustStore(rsaKeyStore, "ballerina"); + EncapsulationResult encapsulationResult = check encapsulateRsaKemMlKem768(rsaPublicKey, mlkemPublicKey); + + PrivateKey mlkemPrivateKey = check decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "mlkem-keypair", "ballerina"); + PrivateKey rsaPrivateKey = check decodeRsaPrivateKeyFromKeyStore(rsaKeyStore, "ballerina", "ballerina"); + byte[] sharedSecret = check decapsulateRsaKemMlKem768(encapsulationResult.encapsulatedSecret, rsaPrivateKey, mlkemPrivateKey); + + test:assertEquals(sharedSecret, encapsulationResult.sharedSecret); +} diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java b/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java index 28a39f8b..d28d3e4e 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/CryptoUtils.java @@ -196,13 +196,17 @@ public static Object generateEncapsulated(String algorithm, PublicKey publicKey, keyGenerator.init(kemGenerateSpec); return keyGenerator.generateKey(); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { - throw CryptoUtils.createError("Error occurred while generating encapsulated key: " + e.getMessage()); + return CryptoUtils.createError("Error occurred while generating encapsulated key: " + e.getMessage()); } catch (NoSuchProviderException e) { throw CryptoUtils.createError("Provider not found: " + provider); } } public static Object generateRsaEncapsulated(PublicKey publicKey) { + if (!(publicKey instanceof RSAPublicKey)) { + return CryptoUtils.createError("Error occurred while generating encapsulated key: valid RSA " + + "public key expected"); + } RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey; RSAKEMGenerator keyGenerator = new RSAKEMGenerator( 32, new KDF2BytesGenerator(new SHA256Digest()), new SecureRandom()); @@ -222,13 +226,17 @@ public static Object extractSecret(byte[] encapsulation, String algorithm, Priva (SecretKeyWithEncapsulation) keyGenerator.generateKey(); return ValueCreator.createArrayValue(secretKeyWithEncapsulation.getEncoded()); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { - throw CryptoUtils.createError("Error occurred while extracting secret: " + e.getMessage()); + return CryptoUtils.createError("Error occurred while extracting secret: " + e.getMessage()); } catch (NoSuchProviderException e) { throw CryptoUtils.createError("Provider not found: " + e.getMessage()); } } public static Object extractRsaSecret(byte[] encapsulation, PrivateKey privateKey) { + if (!(privateKey instanceof RSAPrivateKey)) { + return CryptoUtils.createError("Error occurred while extracting secret: valid RSA private" + + "key expected"); + } RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) privateKey; RSAKeyParameters rsaKeyParameters = new RSAKeyParameters( true, rsaPrivateKey.getModulus(), rsaPrivateKey.getPrivateExponent()); From c443312786632ae85f30afc1f7edd8dfe631c182 Mon Sep 17 00:00:00 2001 From: ashendes Date: Mon, 25 Mar 2024 15:47:16 +0530 Subject: [PATCH 3/3] Tests updated. --- ballerina/tests/hpke_test.bal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ballerina/tests/hpke_test.bal b/ballerina/tests/hpke_test.bal index 6be1fc9a..8a1abac7 100644 --- a/ballerina/tests/hpke_test.bal +++ b/ballerina/tests/hpke_test.bal @@ -45,11 +45,11 @@ isolated function testEncryptAndDecryptRsaMlKem768Hpke() returns Error? { }; PublicKey mlKemPublicKey = check decodeMlKem768PublicKeyFromTrustStore(mlkemKeyStore, "mlkem-keypair"); PublicKey rsaPublicKey = check decodeRsaPublicKeyFromTrustStore(rsaKeyStore, "ballerina"); - HybridEncryptionResult hybridEncryptionResult = check encryptRsaMlKem768Hpke(message, rsaPublicKey, mlKemPublicKey); + HybridEncryptionResult hybridEncryptionResult = check encryptRsaKemMlKem768Hpke(message, rsaPublicKey, mlKemPublicKey); PrivateKey mlKemPrivateKey = check decodeMlKem768PrivateKeyFromKeyStore(mlkemKeyStore, "mlkem-keypair", "ballerina"); PrivateKey rsaPrivateKey = check decodeRsaPrivateKeyFromKeyStore(rsaKeyStore, "ballerina", "ballerina"); - byte[] decryptedMessage = check decryptRsaMlKem768Hpke(hybridEncryptionResult.cipherText, hybridEncryptionResult.encapsulatedSecret, + byte[] decryptedMessage = check decryptRsaKemMlKem768Hpke(hybridEncryptionResult.cipherText, hybridEncryptionResult.encapsulatedSecret, rsaPrivateKey, mlKemPrivateKey); test:assertEquals(decryptedMessage, message);