From f04386b83937c89aa901c91ec4ff5a30da7bc7f8 Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:08:28 +0530 Subject: [PATCH 01/15] Add RSASSA-PSS with SHA-256 signing and verification functions --- ballerina/sign_verify.bal | 58 ++++++++++++++++--- ballerina/tests/sign_verify_test.bal | 47 +++++++++++++-- .../stdlib/crypto/nativeimpl/Sign.java | 14 +++++ 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/ballerina/sign_verify.bal b/ballerina/sign_verify.bal index 8ded90e2..65171ac9 100644 --- a/ballerina/sign_verify.bal +++ b/ballerina/sign_verify.bal @@ -136,6 +136,26 @@ public isolated function signRsaSha512(byte[] input, PrivateKey privateKey) retu 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; +# Returns the RSASSA-PSS with SHA-256 based signature value for the given data. +# ```ballerina +# string input = "Hello Ballerina"; +# byte[] data = input.toBytes(); +# crypto:KeyStore keyStore = { +# path: "/path/to/keyStore.p12", +# password: "keyStorePassword" +# }; +# crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +# byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +# ``` +# +# + input - The content to be signed +# + privateKey - Private key used for signing +# + return - The generated signature or else a `crypto:Error` if the private key is invalid +public isolated function signRsaSsaPss256(byte[] input, PrivateKey privateKey) returns byte[]|Error = @java:Method { + name: "signRsaSsaPss256", + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" +} external; + # Returns the SHA384withECDSA based signature value for the given data. # ```ballerina # string input = "Hello Ballerina"; @@ -195,7 +215,7 @@ public isolated function signSha256withEcdsa(byte[] input, PrivateKey privateKey # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaMd5Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaMd5Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -219,7 +239,7 @@ public isolated function verifyRsaMd5Signature(byte[] data, byte[] signature, Pu # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyMlDsa65Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyMlDsa65Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -267,7 +287,7 @@ public isolated function verifyRsaSha1Signature(byte[] data, byte[] signature, P # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSha256Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaSha256Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -291,7 +311,7 @@ public isolated function verifyRsaSha256Signature(byte[] data, byte[] signature, # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSha384Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaSha384Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -315,11 +335,35 @@ public isolated function verifyRsaSha384Signature(byte[] data, byte[] signature, # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSha512Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaSha512Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; +# Verifies the RSASSA-PSS with SHA-256 based signature. +# ```ballerina +# string input = "Hello Ballerina"; +# byte[] data = input.toBytes(); +# crypto:KeyStore keyStore = { +# path: "/path/to/keyStore.p12", +# password: "keyStorePassword" +# }; +# crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +# byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +# crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keyStore, "keyAlias"); +# boolean validity = check crypto:verifyRsaSsaPss256Signature(data, signature, publicKey); +# ``` +# +# + data - The content to be verified +# + signature - Signature value +# + publicKey - Public key used for verification +# + return - Validity of the signature or else a `crypto:Error` if the public key is invalid +public isolated function verifyRsaSsaPss256Signature(byte[] data, byte[] signature, PublicKey publicKey) + returns boolean|Error = @java:Method { + name: "verifyRsaSsaPss256Signature", + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" +} external; + # Verifies the SHA384withECDSA based signature. # ```ballerina # string input = "Hello Ballerina"; @@ -339,7 +383,7 @@ public isolated function verifyRsaSha512Signature(byte[] data, byte[] signature, # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifySha384withEcdsaSignature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifySha384withEcdsaSignature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -363,7 +407,7 @@ public isolated function verifySha384withEcdsaSignature(byte[] data, byte[] sign # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifySha256withEcdsaSignature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifySha256withEcdsaSignature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; diff --git a/ballerina/tests/sign_verify_test.bal b/ballerina/tests/sign_verify_test.bal index 46ea7c4a..21b3baf0 100644 --- a/ballerina/tests/sign_verify_test.bal +++ b/ballerina/tests/sign_verify_test.bal @@ -106,6 +106,18 @@ isolated function testSignRsaSha512() returns Error? { test:assertEquals(sha512Signature.toBase16(), expectedSha512Signature); } +@test:Config {} +isolated function testSignRsaSsaPss256() returns Error? { + byte[] payload = "Ballerina test".toBytes(); + KeyStore keyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PrivateKey privateKey = check decodeRsaPrivateKeyFromKeyStore(keyStore, "ballerina", "ballerina"); + byte[] pssSignature = check signRsaSsaPss256(payload, privateKey); + test:assertTrue(pssSignature.length() > 0); +} + @test:Config {} isolated function testSignMlDsa65() returns Error? { byte[] payload = "Ballerina test".toBytes(); @@ -204,7 +216,7 @@ isolated function testSignMlDsa65() returns Error? { @test:Config {} isolated function testSignRsaMd5WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaMd5(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -216,7 +228,7 @@ isolated function testSignRsaMd5WithInvalidKey() { @test:Config {} isolated function testSignRsaSha1WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha1(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -228,7 +240,7 @@ isolated function testSignRsaSha1WithInvalidKey() { @test:Config {} isolated function testSignRsaSha256WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha256(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -240,7 +252,7 @@ isolated function testSignRsaSha256WithInvalidKey() { @test:Config {} isolated function testSignRsaSha384WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha384(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -252,7 +264,7 @@ isolated function testSignRsaSha384WithInvalidKey() { @test:Config {} isolated function testSignRsaSha512WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha512(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -261,6 +273,18 @@ isolated function testSignRsaSha512WithInvalidKey() { } } +@test:Config {} +isolated function testSignRsaSsaPss256WithInvalidKey() { + byte[] payload = "Ballerina test".toBytes(); + PrivateKey privateKey = {algorithm: "RSA"}; + byte[]|Error result = signRsaSsaPss256(payload, privateKey); + if result is Error { + test:assertTrue(result.message().includes("Uninitialized private key:")); + } else { + test:assertFail("Expected error not found."); + } +} + @test:Config {} isolated function testSignMlDsa65WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); @@ -338,6 +362,19 @@ isolated function testVerifyRsaSha512() returns Error? { test:assertTrue(check verifyRsaSha512Signature(payload, sha512Signature, publicKey)); } +@test:Config {} +isolated function testVerifyRsaSsaPss256() returns Error? { + byte[] payload = "Ballerina test".toBytes(); + KeyStore keyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PrivateKey privateKey = check decodeRsaPrivateKeyFromKeyStore(keyStore, "ballerina", "ballerina"); + PublicKey publicKey = check decodeRsaPublicKeyFromTrustStore(keyStore, "ballerina"); + byte[] pssSignature = check signRsaSsaPss256(payload, privateKey); + test:assertTrue(check verifyRsaSsaPss256Signature(payload, pssSignature, publicKey)); +} + @test:Config {} isolated function testVerifySha384withEcdsa() returns Error? { byte[] payload = "Ballerina test".toBytes(); diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java index 35a9ed7d..815a5152 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java @@ -83,6 +83,12 @@ public static Object signRsaSha512(BArray inputValue, BMap privateKey) { return CryptoUtils.sign("SHA512withRSA", key, input); } + public static Object signRsaSsaPss256(BArray inputValue, BMap privateKey) { + byte[] input = inputValue.getBytes(); + PrivateKey key = (PrivateKey) privateKey.getNativeData(Constants.NATIVE_DATA_PRIVATE_KEY); + return CryptoUtils.sign("SHA256withRSAandMGF1", key, input); + } + public static Object verifyMlDsa65Signature(BArray dataValue, BArray signatureValue, BMap publicKey) { byte[] data = dataValue.getBytes(); @@ -131,6 +137,14 @@ public static Object verifyRsaSha512Signature(BArray dataValue, BArray signature return CryptoUtils.verify("SHA512withRSA", key, data, signature); } + public static Object verifyRsaSsaPss256Signature(BArray dataValue, BArray signatureValue, + BMap publicKey) { + byte[] data = dataValue.getBytes(); + byte[] signature = signatureValue.getBytes(); + PublicKey key = (PublicKey) publicKey.getNativeData(Constants.NATIVE_DATA_PUBLIC_KEY); + return CryptoUtils.verify("SHA256withRSAandMGF1", key, data, signature); + } + public static Object verifySha384withEcdsaSignature(BArray dataValue, BArray signatureValue, BMap publicKey) { byte[] data = dataValue.getBytes(); byte[] signature = signatureValue.getBytes(); From 3f32b9ce05be9ae9ea6b48900dd3ad22a3d4132f Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:18:33 +0530 Subject: [PATCH 02/15] Add RSASSA-PSS (PS256) signature support proposal --- .../proposals/rsassa-pss-signature-support.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/proposals/rsassa-pss-signature-support.md diff --git a/docs/proposals/rsassa-pss-signature-support.md b/docs/proposals/rsassa-pss-signature-support.md new file mode 100644 index 00000000..ad3030f1 --- /dev/null +++ b/docs/proposals/rsassa-pss-signature-support.md @@ -0,0 +1,94 @@ +# Proposal: Introduce RSASSA-PSS (PS256) Signature Support to Ballerina Crypto Module + +_Authors_: @randilt +_Reviewers_: +_Created_: 2025/10/01 +_Updated_: 2025/10/01 +_Issue_: [#8292](https://github.com/ballerina-platform/ballerina-library/issues/8292) + +## Summary + +This proposal introduces support for the RSASSA-PSS signature scheme with SHA-256 (PS256) to the Ballerina Crypto Module. Currently, only classic RSA signatures (PKCS#1 v1.5) are available, but RSASSA-PSS provides enhanced security properties and is increasingly required by modern cryptographic standards and protocols. + +## Goals + +- Provide support for RSASSA-PSS signature generation with SHA-256 +- Provide support for RSASSA-PSS signature verification with SHA-256 +- Enable higher-level modules like JWT to utilize PS256 signatures + +## Motivation + +The Ballerina Crypto Module currently supports several RSA signature algorithms using PKCS#1 v1.5 padding (RSA-MD5, RSA-SHA1, RSA-SHA256, RSA-SHA384, RSA-SHA512). However, RSASSA-PSS, which is defined in RFC 8017 and provides enhanced security properties, is not yet supported. + +RSASSA-PSS offers several advantages over PKCS#1 v1.5: + +1. **Provable Security**: RSASSA-PSS has a security proof in the random oracle model +2. **Probabilistic Signatures**: Uses random salt generation, making signatures non-deterministic +3. **Modern Standard**: Required by many modern protocols and standards, including JWT RS256 alternative (PS256) +4. **Recommended by Standards**: NIST and other standards bodies recommend RSASSA-PSS over PKCS#1 v1.5 + +The lack of native support for RSASSA-PSS limits the cryptographic capabilities of Ballerina applications, particularly those implementing modern security protocols that require or prefer PS256 signatures. + +## Description + +This proposal adds RSASSA-PSS signature generation and verification with SHA-256 to the Ballerina Crypto Module, following the same architectural patterns as existing RSA signature functions. + +The key functionalities expected from this change are: + +- API to generate RSASSA-PSS signatures with `crypto:signRsaSsaPss256` +- API to verify RSASSA-PSS signatures with `crypto:verifyRsaSsaPss256Signature` + +### API additions + +Two new APIs will be added following the existing RSA signature function patterns: + +```ballerina +# Returns the RSASSA-PSS based signature value for the given data. +# +# + input - The content to be signed +# + privateKey - Private key used for signing +# + return - The generated signature or else a `crypto:Error` if the private key is invalid +public isolated function signRsaSsaPss256(byte[] input, PrivateKey privateKey) returns byte[]|Error; +``` + +```ballerina +# Verifies the RSASSA-PSS based signature. +# +# + data - The content to be verified +# + signature - Signature value +# + publicKey - Public key used for verification +# + return - Validity of the signature or else a `crypto:Error` if the key is invalid +public isolated function verifyRsaSsaPss256Signature(byte[] data, byte[] signature, PublicKey publicKey) returns boolean|Error; +``` + +### Implementation Details + +The implementation follows the existing architecture: + +1. **Java Native Implementation**: Uses Java's `Signature.getInstance("SHA256withRSAandMGF1")` with default PSS parameters +2. **Algorithm String**: Uses `"SHA256withRSAandMGF1"` directly as string literal, consistent with other algorithms +3. **Method Naming**: Follows the pattern `signRsaSsaPss256` and `verifyRsaSsaPss256Signature` +4. **Documentation**: Matches the style and format of existing signature functions + +### PSS Parameters + +The implementation uses Java's default RSASSA-PSS parameters: + +- **Hash Algorithm**: SHA-256 +- **MGF**: MGF1 with SHA-256 +- **Salt Length**: Same as hash length (32 bytes for SHA-256) +- **Trailer Field**: 1 (standard value) + +These parameters align with common RSASSA-PSS usage and provide strong security guarantees. + +## Testing + +The implementation includes comprehensive test coverage following the existing test patterns: + +1. **Basic Signing Test**: Verifies signature generation returns valid length (probabilistic nature prevents deterministic comparison) +2. **Invalid Key Test**: Verifies proper error handling with invalid private keys +3. **Sign-Verify Test**: Tests complete workflow of signing with private key and verifying with corresponding public key + +## Backward Compatibility + +This addition is fully backward compatible as it only adds new functions without modifying existing APIs or behavior. From 0bbf396c0cc726eb0eee86ee715a15b257466688 Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:22:48 +0530 Subject: [PATCH 03/15] Add RSASSA-PSS-SHA256 signing and verification APIs to the specification --- docs/spec/spec.md | 58 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 28830500..05508c3f 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -72,18 +72,20 @@ The conforming implementation of the specification is released and included in t * 6.1.3. [RSA-SHA256](#613-rsa-sha256) * 6.1.4. [RSA-SHA384](#614-rsa-sha384) * 6.1.5. [RSA-SHA512](#615-rsa-sha512) - * 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) - * 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) - * 6.1.8. [ML-DSA-65](#618-mldsa65) + * 6.1.6. [RSASSA-PSS-SHA256](#616-rsassa-pss-sha256) + * 6.1.7. [SHA384withECDSA](#617-sha384withecdsa) + * 6.1.8. [SHA256withECDSA](#618-sha256withecdsa) + * 6.1.9. [ML-DSA-65](#619-mldsa65) * 6.2. [Verify signature](#62-verify-signature) * 6.2.1. [RSA-MD5](#621-rsa-md5) * 6.2.2. [RSA-SHA1](#622-rsa-sha1) * 6.2.3. [RSA-SHA256](#623-rsa-sha256) * 6.2.4. [RSA-SHA384](#624-rsa-sha384) * 6.2.5. [RSA-SHA512](#625-rsa-sha512) - * 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) - * 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) - * 6.2.8. [ML-DSA-65](#618-mldsa65) + * 6.2.6. [RSASSA-PSS-SHA256](#626-rsassa-pss-sha256) + * 6.2.7. [SHA384withECDSA](#627-sha384withecdsa) + * 6.2.8. [SHA256withECDSA](#628-sha256withecdsa) + * 6.2.9. [ML-DSA-65](#629-mldsa65) 7. [Key Derivation Function (KDF)](#7-key-derivation-function-kdf) * 7.1. [HKDF-SHA256](#71-hkdf-sha256) 8. [Key Exchange Mechanism (KEM)](#8-key-exchange-mechanism-kem) @@ -730,7 +732,22 @@ crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyS byte[] signature = check crypto:signRsaSha512(data, privateKey); ``` -#### 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) +#### 6.1.6. [RSASSA-PSS-SHA256](#616-rsassa-pss-sha256) + +This API can be used to create the RSASSA-PSS based signature value for the given data. + +```ballerina +string input = "Hello Ballerina"; +byte[] data = input.toBytes(); +crypto:KeyStore keyStore = { + path: "/path/to/keyStore.p12", + password: "keyStorePassword" +}; +crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +``` + +#### 6.1.7. [SHA384withECDSA](#617-sha384withecdsa) This API can be used to create the SHA384withECDSA based signature value for the given data. @@ -745,7 +762,7 @@ crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keySt byte[] signature = check crypto:signSha384withEcdsa(data, privateKey); ``` -#### 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) +#### 6.1.8. [SHA256withECDSA](#618-sha256withecdsa) This API can be used to create the SHA256withECDSA based signature value for the given data. @@ -760,7 +777,7 @@ crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keySt byte[] signature = check crypto:signSha256withEcdsa(data, privateKey); ``` -#### 6.1.8. [ML-DSA-65](#618-mldsa65) +#### 6.1.9. [ML-DSA-65](#619-mldsa65) This API can be used to create the ML-DSA-65 based signature value for the given data. @@ -862,7 +879,24 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt boolean validity = check crypto:verifyRsaSha512Signature(data, signature, publicKey); ``` -#### 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) +#### 6.2.6. [RSASSA-PSS-SHA256](#626-rsassa-pss-sha256) + +This API can be used to verify the RSASSA-PSS based signature. + +```ballerina +string input = "Hello Ballerina"; +byte[] data = input.toBytes(); +crypto:KeyStore keyStore = { + path: "/path/to/keyStore.p12", + password: "keyStorePassword" +}; +crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keyStore, "keyAlias"); +boolean validity = check crypto:verifyRsaSsaPss256Signature(data, signature, publicKey); +``` + +#### 6.2.7. [SHA384withECDSA](#627-sha384withecdsa) This API can be used to verify the SHA384withECDSA based signature. @@ -879,7 +913,7 @@ crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(keySto boolean validity = check crypto:verifySha384withEcdsaSignature(data, signature, publicKey); ``` -#### 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) +#### 6.2.8. [SHA256withECDSA](#628-sha256withecdsa) This API can be used to verify the SHA256withECDSA based signature. @@ -896,7 +930,7 @@ crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(keySto boolean validity = check crypto:verifySha256withEcdsaSignature(data, signature, publicKey); ``` -#### 6.2.8. [ML-DSA-65](#628-mldsa65) +#### 6.2.9. [ML-DSA-65](#629-mldsa65) This API can be used to verify the ML-DSA-65 based signature. From cfb6f79832eccbb39baf52533cbfd05d4dbe57b5 Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:30:45 +0530 Subject: [PATCH 04/15] Update changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index c2787a4e..be795b31 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added +- [Introduce support for RSASSA-PSS (PS256) signature algorithm](https://github.com/ballerina-platform/ballerina-library/issues/8292) - [Introduce support for PBKDF2 password hashing and verification](https://github.com/ballerina-platform/ballerina-lang/issues/43926) ### Changed From cf1437ff609133c8d21b67714c89170c8c9168ae Mon Sep 17 00:00:00 2001 From: Krishnananthalingam Tharmigan <63336800+TharmiganK@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:26:23 +0530 Subject: [PATCH 05/15] Refactor scan rule implementation (#613) * Refactor static code rule implementation * Update spec with scan rules * Update changelog * Address sonar cloud reported issues * Add enable code coverage report for compiler plugin * Bump to the next minor version * [Automated] Update the native jar versions * Improve code coverage * Update AES encryption example from CCM to CBC * Fix reference to SHA256 in password hashing function --- ballerina/Ballerina.toml | 6 +- ballerina/CompilerPlugin.toml | 2 +- ballerina/Dependencies.toml | 2 +- changelog.md | 13 +- compiler-plugin-tests/build.gradle | 13 + .../StaticCodeAnalyzerTest.java | 129 +-- .../ballerina_packages/rule1/aes_cbc.bal | 2 +- .../rule1/aes_cbc_as_import.bal | 2 +- .../ballerina_packages/rule1/aes_ecb.bal | 2 +- .../rule1/aes_ecb_as_import.bal | 2 +- .../rule1/weak_cipher_algo.bal | 46 ++ .../ballerina_packages/rule2/argon_func.bal | 42 + .../rule2/argon_func_var_named_arg.bal | 2 +- .../rule2/argon_func_var_pos_arg.bal | 2 +- .../rule2/argon_inline_named_arg.bal | 2 +- .../rule2/argon_inline_pos_arg.bal | 2 +- .../rule2/argon_mod_var_named_arg.bal | 2 +- .../rule2/argon_mod_var_pos_arg.bal | 2 +- .../rule2/bcrypt_func_var_named_arg.bal | 2 +- .../rule2/bcrypt_func_var_pos_arg.bal | 2 +- .../rule2/bcrypt_inline_named_arg.bal | 2 +- .../rule2/bcrypt_inline_pos_arg.bal | 2 +- .../rule2/bcrypt_mod_var_named_arg.bal | 2 +- .../rule2/bcrypt_mod_var_pos_arg.bal | 2 +- .../modules/module/module.bal} | 17 +- .../ballerina_packages/rule2/pbkdf2_func.bal | 42 + .../rule2/pbkdf2_func_var_named_arg.bal | 2 +- .../rule2/pbkdf2_func_var_pos_arg.bal | 2 +- .../rule2/pbkdf2_inline_named_arg.bal | 2 +- .../rule2/pbkdf2_inline_pos_arg.bal | 2 +- .../rule2/pbkdf2_mod_var_named_arg.bal | 2 +- .../rule2/pbkdf2_mod_var_pos_arg.bal | 2 +- .../rule3/func_hardcoded_iv_param.bal | 61 ++ .../rule3/func_var_named_arg.bal | 2 +- .../rule3/func_var_pos_arg.bal | 2 +- .../rule3/inline_named_arg.bal | 2 +- .../rule3/inline_pos_arg.bal | 2 +- .../rule3/mod_var_named_arg.bal | 2 +- .../rule3/mod_var_pos_arg.bal | 2 +- .../rule3/modules/module/module.bal | 19 + .../ballerina_packages/rule4/Ballerina.toml | 8 - .../expected_output/rule1.json | 160 +++- .../expected_output/rule2.json | 260 ++++++ .../expected_output/rule3.json | 160 ++++ .../expected_output/rule4.json | 22 - .../CryptoAnalyzerUtils.java | 575 ++++++-------- .../CryptoCipherAlgorithmAnalyzer.java | 288 +------ .../CryptoFunctionRulesEngine.java | 62 ++ .../staticcodeanalyzer/CryptoRule.java | 4 +- .../staticcodeanalyzer/FunctionContext.java | 230 ++++++ .../AvoidFastHashAlgorithmsRule.java | 166 ++++ .../AvoidReusingCounterModeVectorsRule.java | 118 +++ .../AvoidWeakCipherAlgorithmsRule.java | 69 ++ .../functionrules/CryptoFunctionRule.java | 50 ++ compiler-plugin/src/main/resources/rules.json | 5 - docs/spec/spec.md | 739 +++++++++++++----- gradle.properties | 2 +- 57 files changed, 2385 insertions(+), 981 deletions(-) create mode 100644 compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal create mode 100644 compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal rename compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/{rule4/main.bal => rule2/modules/module/module.bal} (53%) create mode 100644 compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal create mode 100644 compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal create mode 100644 compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/modules/module/module.bal delete mode 100644 compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/Ballerina.toml delete mode 100644 compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule4.json create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoFunctionRulesEngine.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/FunctionContext.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidFastHashAlgorithmsRule.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidReusingCounterModeVectorsRule.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidWeakCipherAlgorithmsRule.java create mode 100644 compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/CryptoFunctionRule.java diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 6ca89f60..00c6116e 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "crypto" -version = "2.9.1" +version = "2.10.0" authors = ["Ballerina"] keywords = ["security", "hash", "hmac", "sign", "encrypt", "decrypt", "private key", "public key"] repository = "https://github.com/ballerina-platform/module-ballerina-crypto" @@ -15,8 +15,8 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "crypto-native" -version = "2.9.1" -path = "../native/build/libs/crypto-native-2.9.1-SNAPSHOT.jar" +version = "2.10.0" +path = "../native/build/libs/crypto-native-2.10.0-SNAPSHOT.jar" [[platform.java21.dependency]] groupId = "org.bouncycastle" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 2e2112d3..738a1475 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "crypto-compiler-plugin" class = "io.ballerina.stdlib.crypto.compiler.CryptoCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/crypto-compiler-plugin-2.9.1-SNAPSHOT.jar" +path = "../compiler-plugin/build/libs/crypto-compiler-plugin-2.10.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 6488f734..46534761 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -10,7 +10,7 @@ distribution-version = "2201.12.0" [[package]] org = "ballerina" name = "crypto" -version = "2.9.1" +version = "2.10.0" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, diff --git a/changelog.md b/changelog.md index c2787a4e..9ddba451 100644 --- a/changelog.md +++ b/changelog.md @@ -7,14 +7,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - [Introduce support for PBKDF2 password hashing and verification](https://github.com/ballerina-platform/ballerina-lang/issues/43926) +- [Add static analysis rule - Encryption algorithms should be used with secure mode and padding scheme](https://github.com/ballerina-platform/ballerina-library/issues/7940) +- [Add static analysis rule - Passwords should not be stored in plaintext or with a fast hashing algorithm](https://github.com/ballerina-platform/ballerina-library/issues/7950) +- [Add static analysis rule - Counter Mode initialization vectors should not be reused](https://github.com/ballerina-platform/ballerina-library/issues/8010) -### Changed -- [Update OIDS of NIST approved post quantum algorithms](https://github.com/ballerina-platform/ballerina-library/issues/7678) -- [Optimize hardcoded IV detection using semantic model reference counting](https://github.com/ballerina-platform/ballerina-library/issues/8257) +## [2.9.1] - 2025-09-29 ### Fixed - [Implement optional close method check for BStream](https://github.com/ballerina-platform/ballerina-library/issues/8288) +## [2.9.0] - 2025-03-12 + +### Changed +- [Update OIDS of NIST approved post quantum algorithms](https://github.com/ballerina-platform/ballerina-library/issues/7678) + + ## [2.8.0] - 2025-02-11 ### Added diff --git a/compiler-plugin-tests/build.gradle b/compiler-plugin-tests/build.gradle index 24d488c6..454347e0 100644 --- a/compiler-plugin-tests/build.gradle +++ b/compiler-plugin-tests/build.gradle @@ -20,6 +20,7 @@ plugins { id 'java' id 'checkstyle' id 'com.github.spotbugs' + id 'jacoco' } description = 'Ballerina - Crypto Compiler Plugin Tests' @@ -61,6 +62,18 @@ test { } } } + jacoco { + destinationFile = file("$buildDir/jacoco/test.exec") + } + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + } + sourceSets project(':crypto-compiler-plugin').sourceSets.main } spotbugsTest { diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java index 81c3ff62..a7374a5a 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/StaticCodeAnalyzerTest.java @@ -49,7 +49,6 @@ import static io.ballerina.scan.RuleKind.VULNERABILITY; import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_FAST_HASH_ALGORITHMS; import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_REUSING_COUNTER_MODE_VECTORS; -import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_USING_UNSECURE_RANDOM_NUMBER_GENERATORS; import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_WEAK_CIPHER_ALGORITHMS; import static java.nio.charset.StandardCharsets.UTF_8; @@ -118,89 +117,127 @@ private void validateRules(List rules) { "ballerina/crypto:3", AVOID_REUSING_COUNTER_MODE_VECTORS.getDescription(), VULNERABILITY); - Assertions.assertRule( - rules, - "ballerina/crypto:4", - AVOID_USING_UNSECURE_RANDOM_NUMBER_GENERATORS.getDescription(), - VULNERABILITY); } private void validateIssues(CryptoRule rule, List issues) { + int index; switch (rule) { case AVOID_WEAK_CIPHER_ALGORITHMS: - Assert.assertEquals(issues.size(), 6); - Assertions.assertIssue(issues, 0, "ballerina/crypto:1", "aes_cbc.bal", - 30, 30, Source.BUILT_IN); - Assertions.assertIssue(issues, 1, "ballerina/crypto:4", "aes_cbc.bal", + index = 0; + Assert.assertEquals(issues.size(), 10); + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "aes_cbc.bal", 30, 30, Source.BUILT_IN); - Assertions.assertIssue(issues, 2, "ballerina/crypto:1", "aes_cbc_as_import.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "aes_cbc_as_import.bal", 30, 30, Source.BUILT_IN); - Assertions.assertIssue(issues, 3, "ballerina/crypto:4", "aes_cbc_as_import.bal", - 30, 30, Source.BUILT_IN); - Assertions.assertIssue(issues, 4, "ballerina/crypto:1", "aes_ecb.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "aes_ecb.bal", 26, 26, Source.BUILT_IN); - Assertions.assertIssue(issues, 5, "ballerina/crypto:1", "aes_ecb_as_import.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "aes_ecb_as_import.bal", 26, 26, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "weak_cipher_algo.bal", + 23, 23, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "weak_cipher_algo.bal", + 27, 27, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "weak_cipher_algo.bal", + 27, 27, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "weak_cipher_algo.bal", + 35, 35, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:1", "weak_cipher_algo.bal", + 36, 36, Source.BUILT_IN); + Assertions.assertIssue(issues, index, "ballerina/crypto:1", "weak_cipher_algo.bal", + 39, 39, Source.BUILT_IN); break; case AVOID_FAST_HASH_ALGORITHMS: - Assert.assertEquals(issues.size(), 18); - Assertions.assertIssue(issues, 0, "ballerina/crypto:2", "argon_func_var_named_arg.bal", + Assert.assertEquals(issues.size(), 29); + index = 0; + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_func.bal", + 31, 31, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_func.bal", + 33, 33, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_func.bal", + 35, 35, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_func.bal", + 37, 37, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_func_var_named_arg.bal", 23, 23, Source.BUILT_IN); - Assertions.assertIssue(issues, 1, "ballerina/crypto:2", "argon_func_var_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_func_var_pos_arg.bal", 23, 23, Source.BUILT_IN); - Assertions.assertIssue(issues, 2, "ballerina/crypto:2", "argon_inline_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_inline_named_arg.bal", 20, 20, Source.BUILT_IN); - Assertions.assertIssue(issues, 3, "ballerina/crypto:2", "argon_inline_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_inline_pos_arg.bal", 20, 20, Source.BUILT_IN); - Assertions.assertIssue(issues, 4, "ballerina/crypto:2", "argon_mod_var_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_mod_var_named_arg.bal", 24, 24, Source.BUILT_IN); - Assertions.assertIssue(issues, 5, "ballerina/crypto:2", "argon_mod_var_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "argon_mod_var_pos_arg.bal", 24, 24, Source.BUILT_IN); - Assertions.assertIssue(issues, 6, "ballerina/crypto:2", "bcrypt_func_var_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "bcrypt_func_var_named_arg.bal", 21, 21, Source.BUILT_IN); - Assertions.assertIssue(issues, 7, "ballerina/crypto:2", "bcrypt_func_var_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "bcrypt_func_var_pos_arg.bal", 21, 21, Source.BUILT_IN); - Assertions.assertIssue(issues, 8, "ballerina/crypto:2", "bcrypt_inline_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "bcrypt_inline_named_arg.bal", 20, 20, Source.BUILT_IN); - Assertions.assertIssue(issues, 9, "ballerina/crypto:2", "bcrypt_inline_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "bcrypt_inline_pos_arg.bal", 20, 20, Source.BUILT_IN); - Assertions.assertIssue(issues, 10, "ballerina/crypto:2", "bcrypt_mod_var_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "bcrypt_mod_var_named_arg.bal", 22, 22, Source.BUILT_IN); - Assertions.assertIssue(issues, 11, "ballerina/crypto:2", "bcrypt_mod_var_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "bcrypt_mod_var_pos_arg.bal", 22, 22, Source.BUILT_IN); - Assertions.assertIssue(issues, 12, "ballerina/crypto:2", "pbkdf2_func_var_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func.bal", 21, 21, Source.BUILT_IN); - Assertions.assertIssue(issues, 13, "ballerina/crypto:2", "pbkdf2_func_var_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func.bal", + 22, 22, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func.bal", + 23, 23, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func.bal", + 24, 24, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func.bal", + 38, 38, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func.bal", + 39, 39, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func.bal", + 40, 40, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func_var_named_arg.bal", + 21, 21, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_func_var_pos_arg.bal", 21, 21, Source.BUILT_IN); - Assertions.assertIssue(issues, 14, "ballerina/crypto:2", "pbkdf2_inline_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_inline_named_arg.bal", 20, 20, Source.BUILT_IN); - Assertions.assertIssue(issues, 15, "ballerina/crypto:2", "pbkdf2_inline_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_inline_pos_arg.bal", 20, 20, Source.BUILT_IN); - Assertions.assertIssue(issues, 16, "ballerina/crypto:2", "pbkdf2_mod_var_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:2", "pbkdf2_mod_var_named_arg.bal", 22, 22, Source.BUILT_IN); - Assertions.assertIssue(issues, 17, "ballerina/crypto:2", "pbkdf2_mod_var_pos_arg.bal", + Assertions.assertIssue(issues, index, "ballerina/crypto:2", "pbkdf2_mod_var_pos_arg.bal", 22, 22, Source.BUILT_IN); break; case AVOID_REUSING_COUNTER_MODE_VECTORS: - Assert.assertEquals(issues.size(), 6); - Assertions.assertIssue(issues, 0, "ballerina/crypto:3", "func_var_named_arg.bal", + Assert.assertEquals(issues.size(), 13); + index = 0; + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_hardcoded_iv_param.bal", + 24, 24, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_hardcoded_iv_param.bal", + 26, 26, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_hardcoded_iv_param.bal", + 29, 29, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_hardcoded_iv_param.bal", + 32, 32, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_hardcoded_iv_param.bal", + 36, 36, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_hardcoded_iv_param.bal", + 39, 39, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_hardcoded_iv_param.bal", + 42, 42, Source.BUILT_IN); + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_var_named_arg.bal", 22, 22, Source.BUILT_IN); - Assertions.assertIssue(issues, 1, "ballerina/crypto:3", "func_var_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "func_var_pos_arg.bal", 22, 22, Source.BUILT_IN); - Assertions.assertIssue(issues, 2, "ballerina/crypto:3", "inline_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "inline_named_arg.bal", 21, 21, Source.BUILT_IN); - Assertions.assertIssue(issues, 3, "ballerina/crypto:3", "inline_pos_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "inline_pos_arg.bal", 21, 21, Source.BUILT_IN); - Assertions.assertIssue(issues, 4, "ballerina/crypto:3", "mod_var_named_arg.bal", + Assertions.assertIssue(issues, index++, "ballerina/crypto:3", "mod_var_named_arg.bal", 23, 23, Source.BUILT_IN); - Assertions.assertIssue(issues, 5, "ballerina/crypto:3", "mod_var_pos_arg.bal", + Assertions.assertIssue(issues, index, "ballerina/crypto:3", "mod_var_pos_arg.bal", 23, 23, Source.BUILT_IN); break; - case AVOID_USING_UNSECURE_RANDOM_NUMBER_GENERATORS: - Assert.assertEquals(issues.size(), 1); - Assertions.assertIssue(issues, 0, "ballerina/crypto:4", "main.bal", - 27, 27, Source.BUILT_IN); - break; default: Assert.fail("Unhandled rule in validateIssues: " + rule); break; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc.bal index e8391166..cd9e17ef 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc_as_import.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc_as_import.bal index 278b1f8a..60c1b325 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc_as_import.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc_as_import.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb.bal index b344c0b8..632eca1d 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb_as_import.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb_as_import.bal index d4934ab9..d70eb0d0 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb_as_import.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb_as_import.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal new file mode 100644 index 00000000..c9c9e3e5 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal @@ -0,0 +1,46 @@ +// Copyright (c) 2025 WSO2 LLC. (http://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/crypto; + +public function main() returns error? { + byte[] inputBytes = "input".toBytes(); + byte[] keyBytes = "key".toBytes(); + + // Encrypt using AES ECB mode is not secure + _ = check crypto:encryptAesEcb(inputBytes, keyBytes); + + byte[] iv = "1234567890123456".toBytes(); + // Encrypt using AES CBC mode is secure + _ = check crypto:encryptAesCbc(inputBytes, keyBytes, iv); + + crypto:KeyStore keyStore = { + path: "/path/to/keyStore.p12", + password: "keyStorePassword" + }; + crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keyStore, "keyAlias"); + // Encrypt using RSA ECB with PKCS1 padding is not secure + _ = check crypto:encryptRsaEcb(inputBytes, publicKey, crypto:PKCS1); + _ = check crypto:encryptRsaEcb(inputBytes, publicKey, "PKCS1"); + + // Default RSA ECB encryption uses PKCS1 padding. Hence, it is not secure + _ = check crypto:encryptRsaEcb(inputBytes, publicKey); + + // Encrypt using RSA ECB with OAEP padding is secure + _ = check crypto:encryptRsaEcb(inputBytes, publicKey, crypto:OAEPWithSHA1AndMGF1); + _ = check crypto:encryptRsaEcb(inputBytes, publicKey, "OAEPWithSHA1AndMGF1"); + +} diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal new file mode 100644 index 00000000..61fb385a --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal @@ -0,0 +1,42 @@ +// Copyright (c) 2025 WSO2 LLC. (http://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/crypto; +import wso2/rule2.'module as mod; + +const MEMORY = 16384; + +public function ArgonFunc() returns error? { + // Default parameters - iterations: 3, memory: 65536, parallelism: 4 + _ = check crypto:hashArgon2("password"); + // Custom least secure parameters: iterations: 2, memory: 19456, parallelism: 1 + _ = check crypto:hashArgon2("password", 2, 19456, 1); + // Secure parameters - memory constant from different module + _ = check crypto:hashArgon2("password", memory = mod:MINIMUM_ALLOWED_MEMORY); + + // Unsecure parameters + // iterations + _ = check crypto:hashArgon2("password", 1); + // memory + _ = check crypto:hashArgon2("password", memory = 8192); + // memory with constant + _ = check crypto:hashArgon2("password", memory = MEMORY); + // memory with module constant + _ = check crypto:hashArgon2("password", memory = mod:MEMORY); + // memory with module variable - This is a negative test case + // Cannot be determined at compile time + _ = check crypto:hashArgon2("password", memory = mod:memory); +} diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_named_arg.bal index 94c76b34..60c58d92 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_pos_arg.bal index ad3ba9fc..5d8ff4d4 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_named_arg.bal index 5a4747f9..00772606 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_pos_arg.bal index 72f14384..0c48ea4b 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_inline_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_named_arg.bal index 47e21059..81559feb 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_pos_arg.bal index c5b817c3..46ce9fbd 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_mod_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_named_arg.bal index aab4d280..5f45e77f 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_pos_arg.bal index e1200b9f..7801dd4a 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_func_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_named_arg.bal index 09d09604..cd08eb98 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_pos_arg.bal index 10bfcb9a..48bcfcca 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_inline_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_named_arg.bal index f272b4ab..65b39420 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_pos_arg.bal index 5f194182..b7658b4c 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/main.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/modules/module/module.bal similarity index 53% rename from compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/main.bal rename to compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/modules/module/module.bal index 8d0d5d01..aaf60623 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/main.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/modules/module/module.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 @@ -14,16 +14,7 @@ // specific language governing permissions and limitations // under the License. -import ballerina/crypto; -import ballerina/random; +public const MEMORY = 16384; +public const MINIMUM_ALLOWED_MEMORY = 19456; -public isolated function main() returns error? { - string data = "Hello, World!"; - byte[16] initialVector = []; - byte[16] key = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - byte[] dataBytes = data.toBytes(); - foreach int i in 0 ... 15 { - initialVector[i] = (check random:createIntInRange(0, 255)); - } - byte[] _ = check crypto:encryptAesGcm(dataBytes, key, initialVector); -} +public int memory = 16384; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal new file mode 100644 index 00000000..cf8aa9ac --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal @@ -0,0 +1,42 @@ +// Copyright (c) 2025 WSO2 LLC. (http://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/crypto; + +public function pbkdf2Func() returns error? { + // Default parameters - iterations: 10000 which is lower than recommended for all algorithms + // Hence vulnerable + _ = check crypto:hashPbkdf2("password"); // Default algorithm is SHA-256. + _ = check crypto:hashPbkdf2("password", algorithm = crypto:SHA1); + _ = check crypto:hashPbkdf2("password", algorithm = crypto:SHA256); + _ = check crypto:hashPbkdf2("password", algorithm = crypto:SHA512); + + + // Default algorithm is SHA-256. Iterations set to minimum recommended value for SHA-256 (600000) + _ = check crypto:hashPbkdf2("password", 600000); + _ = check crypto:hashPbkdf2("password", 600000, "SHA256"); + + // Iterations set to minimum recommended value for SHA-1 (1000000) + _ = check crypto:hashPbkdf2("password", 1300000, crypto:SHA1); + + // Iterations set to minimum recommended value for SHA-512 (500000) + _ = check crypto:hashPbkdf2("password", 210000, "SHA512"); + + // Vulnerable examples + _ = check crypto:hashPbkdf2("password", 200000, crypto:SHA256); + _ = check crypto:hashPbkdf2("password", 900000, "SHA1"); + _ = check crypto:hashPbkdf2("password", 200000, crypto:SHA512); +} diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_named_arg.bal index 2fe4c74b..fff7c8d8 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_pos_arg.bal index 82a75dba..641f973f 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_named_arg.bal index ea8186a8..c6fc40e9 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_pos_arg.bal index 904c9d91..d3729d07 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_inline_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_named_arg.bal index 16d9937a..c5c64f4a 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_pos_arg.bal index add43235..ab01d983 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal new file mode 100644 index 00000000..c8493093 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal @@ -0,0 +1,61 @@ +// Copyright (c) 2025 WSO2 LLC. (http://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/crypto; +import wso2/rule3.'module as mod; + +const HARDCODED_IV = "constHardcodedIV!"; + +public function funcHardcodedIV(string data) returns error? { + byte[16] key = [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; + byte[] dataBytes = data.toBytes(); + _ = check crypto:encryptAesGcm(dataBytes, key, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + + _ = check crypto:encryptAesGcm(dataBytes, key, "hardcodedIV1234".toBytes()); + byte[] iv; + iv = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + _ = check crypto:encryptAesGcm(dataBytes, key, iv); + + iv = "anotherHardcodedIV".toBytes(); + _ = check crypto:encryptAesGcm(dataBytes, key, iv); + + string ivStr = "dynamicIVValue"; + iv = ivStr.toBytes(); + _ = check crypto:encryptAesGcm(dataBytes, key, iv); + + iv = HARDCODED_IV.toBytes(); + _ = check crypto:encryptAesGcm(dataBytes, key, iv); + do { + iv = mod:IV_STRING_VALUE.toBytes(); + _ = check crypto:encryptAesGcm(dataBytes, key, iv); + } +} + +public function funcHardcodedIVNegative(string data) returns error? { + byte[16] key = [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; + byte[] dataBytes = data.toBytes(); + // Negative test where we cannot determine at compile time + byte[] iv = mod:ivStringValue.toBytes(); + _ = check crypto:encryptAesGcm(dataBytes, key, iv); + + // Negative test with conditional assignment which is not supported yet + if data.length() > 5 { + iv = "shortIV".toBytes(); + } else { + iv = "longerHardcodedIV".toBytes(); + } + _ = check crypto:encryptAesGcm(dataBytes, key, iv); +} diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_named_arg.bal index 28546ed2..5085746e 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_pos_arg.bal index 07f640c7..67897b63 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_named_arg.bal index f24affd0..26ae881b 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_pos_arg.bal index d534b33e..ea7c66cc 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/inline_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_named_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_named_arg.bal index b5851f4e..fc770977 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_named_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_named_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_pos_arg.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_pos_arg.bal index 435a57a7..cdc7db2f 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_pos_arg.bal +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_pos_arg.bal @@ -1,4 +1,4 @@ -// Copyright (c) 2025 WSO2 LLC. (http://www.wso2.org) +// Copyright (c) 2025 WSO2 LLC. (http://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 diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/modules/module/module.bal b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/modules/module/module.bal new file mode 100644 index 00000000..354f599c --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/modules/module/module.bal @@ -0,0 +1,19 @@ +// Copyright (c) 2025 WSO2 LLC. (http://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. + +public const IV_STRING_VALUE = "IV_CONST_VALUE"; + +public string ivStringValue = "IV_VALUE"; diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/Ballerina.toml b/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/Ballerina.toml deleted file mode 100644 index 49663051..00000000 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/Ballerina.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -org = "wso2" -name = "rule4" -version = "0.1.0" -distribution = "2201.12.3" - -[build-options] -observabilityIncluded = true diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json index ef8ede4a..b50cc84c 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule1.json @@ -99,26 +99,6 @@ "fileName": "rule1/aes_cbc.bal", "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc.bal" }, - { - "location": { - "filePath": "aes_cbc.bal", - "startLine": 30, - "endLine": 30, - "startColumn": 21, - "endColumn": 67, - "startOffset": 1203, - "length": 46 - }, - "rule": { - "id": "ballerina/crypto:4", - "numericId": 4, - "description": "Secure random number generators should not output predictable values", - "ruleKind": "VULNERABILITY" - }, - "source": "BUILT_IN", - "fileName": "rule1/aes_cbc.bal", - "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc.bal" - }, { "location": { "filePath": "aes_cbc_as_import.bal", @@ -139,26 +119,6 @@ "fileName": "rule1/aes_cbc_as_import.bal", "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc_as_import.bal" }, - { - "location": { - "filePath": "aes_cbc_as_import.bal", - "startLine": 30, - "endLine": 30, - "startColumn": 21, - "endColumn": 62, - "startOffset": 1216, - "length": 41 - }, - "rule": { - "id": "ballerina/crypto:4", - "numericId": 4, - "description": "Secure random number generators should not output predictable values", - "ruleKind": "VULNERABILITY" - }, - "source": "BUILT_IN", - "fileName": "rule1/aes_cbc_as_import.bal", - "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_cbc_as_import.bal" - }, { "location": { "filePath": "aes_ecb.bal", @@ -198,5 +158,125 @@ "source": "BUILT_IN", "fileName": "rule1/aes_ecb_as_import.bal", "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/aes_ecb_as_import.bal" + }, + { + "location": { + "filePath": "weak_cipher_algo.bal", + "startLine": 23, + "endLine": 23, + "startColumn": 14, + "endColumn": 56, + "startOffset": 854, + "length": 42 + }, + "rule": { + "id": "ballerina/crypto:1", + "numericId": 1, + "description": "Avoid using insecure cipher modes or padding schemes", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/weak_cipher_algo.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal" + }, + { + "location": { + "filePath": "weak_cipher_algo.bal", + "startLine": 27, + "endLine": 27, + "startColumn": 14, + "endColumn": 60, + "startOffset": 1003, + "length": 46 + }, + "rule": { + "id": "ballerina/crypto:1", + "numericId": 1, + "description": "Avoid using insecure cipher modes or padding schemes", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/weak_cipher_algo.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal" + }, + { + "location": { + "filePath": "weak_cipher_algo.bal", + "startLine": 27, + "endLine": 27, + "startColumn": 14, + "endColumn": 60, + "startOffset": 1003, + "length": 46 + }, + "rule": { + "id": "ballerina/crypto:3", + "numericId": 3, + "description": "Avoid reusing counter mode initialization vectors", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/weak_cipher_algo.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal" + }, + { + "location": { + "filePath": "weak_cipher_algo.bal", + "startLine": 35, + "endLine": 35, + "startColumn": 14, + "endColumn": 71, + "startOffset": 1346, + "length": 57 + }, + "rule": { + "id": "ballerina/crypto:1", + "numericId": 1, + "description": "Avoid using insecure cipher modes or padding schemes", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/weak_cipher_algo.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal" + }, + { + "location": { + "filePath": "weak_cipher_algo.bal", + "startLine": 36, + "endLine": 36, + "startColumn": 14, + "endColumn": 66, + "startOffset": 1419, + "length": 52 + }, + "rule": { + "id": "ballerina/crypto:1", + "numericId": 1, + "description": "Avoid using insecure cipher modes or padding schemes", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/weak_cipher_algo.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal" + }, + { + "location": { + "filePath": "weak_cipher_algo.bal", + "startLine": 39, + "endLine": 39, + "startColumn": 14, + "endColumn": 57, + "startOffset": 1566, + "length": 43 + }, + "rule": { + "id": "ballerina/crypto:1", + "numericId": 1, + "description": "Avoid using insecure cipher modes or padding schemes", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule1/weak_cipher_algo.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule1/weak_cipher_algo.bal" } ] diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule2.json b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule2.json index 8c37a13a..cb9c9eb4 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule2.json +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule2.json @@ -1,4 +1,24 @@ [ + { + "location": { + "filePath": "argon_func.bal", + "startLine": 21, + "endLine": 21, + "startColumn": 0, + "endColumn": 6, + "startOffset": 726, + "length": 6 + }, + "rule": { + "id": "ballerina:3", + "numericId": 3, + "description": "Non isolated public function", + "ruleKind": "CODE_SMELL" + }, + "source": "BUILT_IN", + "fileName": "rule2/argon_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal" + }, { "location": { "filePath": "argon_func_var_named_arg.bal", @@ -239,6 +259,26 @@ "fileName": "rule2/bcrypt_mod_var_pos_arg.bal", "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_pos_arg.bal" }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 18, + "endLine": 18, + "startColumn": 0, + "endColumn": 6, + "startOffset": 669, + "length": 6 + }, + "rule": { + "id": "ballerina:3", + "numericId": 3, + "description": "Non isolated public function", + "ruleKind": "CODE_SMELL" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, { "location": { "filePath": "pbkdf2_func_var_named_arg.bal", @@ -359,6 +399,86 @@ "fileName": "rule2/pbkdf2_mod_var_pos_arg.bal", "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_mod_var_pos_arg.bal" }, + { + "location": { + "filePath": "argon_func.bal", + "startLine": 31, + "endLine": 31, + "startColumn": 14, + "endColumn": 46, + "startOffset": 1238, + "length": 32 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/argon_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal" + }, + { + "location": { + "filePath": "argon_func.bal", + "startLine": 33, + "endLine": 33, + "startColumn": 14, + "endColumn": 58, + "startOffset": 1300, + "length": 44 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/argon_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal" + }, + { + "location": { + "filePath": "argon_func.bal", + "startLine": 35, + "endLine": 35, + "startColumn": 14, + "endColumn": 60, + "startOffset": 1388, + "length": 46 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/argon_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal" + }, + { + "location": { + "filePath": "argon_func.bal", + "startLine": 37, + "endLine": 37, + "startColumn": 14, + "endColumn": 64, + "startOffset": 1485, + "length": 50 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/argon_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/argon_func.bal" + }, { "location": { "filePath": "argon_func_var_named_arg.bal", @@ -599,6 +719,146 @@ "fileName": "rule2/bcrypt_mod_var_pos_arg.bal", "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/bcrypt_mod_var_pos_arg.bal" }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 21, + "endLine": 21, + "startColumn": 14, + "endColumn": 43, + "startOffset": 850, + "length": 29 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 22, + "endLine": 22, + "startColumn": 14, + "endColumn": 68, + "startOffset": 928, + "length": 54 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 23, + "endLine": 23, + "startColumn": 14, + "endColumn": 70, + "startOffset": 998, + "length": 56 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 24, + "endLine": 24, + "startColumn": 14, + "endColumn": 70, + "startOffset": 1070, + "length": 56 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 38, + "endLine": 38, + "startColumn": 14, + "endColumn": 66, + "startOffset": 1665, + "length": 52 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 39, + "endLine": 39, + "startColumn": 14, + "endColumn": 59, + "startOffset": 1733, + "length": 45 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, + { + "location": { + "filePath": "pbkdf2_func.bal", + "startLine": 40, + "endLine": 40, + "startColumn": 14, + "endColumn": 66, + "startOffset": 1794, + "length": 52 + }, + "rule": { + "id": "ballerina/crypto:2", + "numericId": 2, + "description": "Avoid using fast hashing algorithms", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule2/pbkdf2_func.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule2/pbkdf2_func.bal" + }, { "location": { "filePath": "pbkdf2_func_var_named_arg.bal", diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule3.json b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule3.json index cfde788c..fb101468 100644 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule3.json +++ b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule3.json @@ -1,4 +1,44 @@ [ + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 21, + "endLine": 21, + "startColumn": 0, + "endColumn": 6, + "startOffset": 746, + "length": 6 + }, + "rule": { + "id": "ballerina:3", + "numericId": 3, + "description": "Non isolated public function", + "ruleKind": "CODE_SMELL" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 46, + "endLine": 46, + "startColumn": 0, + "endColumn": 6, + "startOffset": 1681, + "length": 6 + }, + "rule": { + "id": "ballerina:3", + "numericId": 3, + "description": "Non isolated public function", + "ruleKind": "CODE_SMELL" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, { "location": { "filePath": "func_var_named_arg.bal", @@ -119,6 +159,126 @@ "fileName": "rule3/mod_var_pos_arg.bal", "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/mod_var_pos_arg.bal" }, + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 24, + "endLine": 24, + "startColumn": 14, + "endColumn": 107, + "startOffset": 937, + "length": 93 + }, + "rule": { + "id": "ballerina/crypto:3", + "numericId": 3, + "description": "Avoid reusing counter mode initialization vectors", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 26, + "endLine": 26, + "startColumn": 14, + "endColumn": 79, + "startOffset": 1047, + "length": 65 + }, + "rule": { + "id": "ballerina/crypto:3", + "numericId": 3, + "description": "Avoid reusing counter mode initialization vectors", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 29, + "endLine": 29, + "startColumn": 14, + "endColumn": 54, + "startOffset": 1209, + "length": 40 + }, + "rule": { + "id": "ballerina/crypto:3", + "numericId": 3, + "description": "Avoid reusing counter mode initialization vectors", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 32, + "endLine": 32, + "startColumn": 14, + "endColumn": 54, + "startOffset": 1307, + "length": 40 + }, + "rule": { + "id": "ballerina/crypto:3", + "numericId": 3, + "description": "Avoid reusing counter mode initialization vectors", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 39, + "endLine": 39, + "startColumn": 14, + "endColumn": 54, + "startOffset": 1517, + "length": 40 + }, + "rule": { + "id": "ballerina/crypto:3", + "numericId": 3, + "description": "Avoid reusing counter mode initialization vectors", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, + { + "location": { + "filePath": "func_hardcoded_iv_param.bal", + "startLine": 42, + "endLine": 42, + "startColumn": 18, + "endColumn": 58, + "startOffset": 1630, + "length": 40 + }, + "rule": { + "id": "ballerina/crypto:3", + "numericId": 3, + "description": "Avoid reusing counter mode initialization vectors", + "ruleKind": "VULNERABILITY" + }, + "source": "BUILT_IN", + "fileName": "rule3/func_hardcoded_iv_param.bal", + "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule3/func_hardcoded_iv_param.bal" + }, { "location": { "filePath": "func_var_named_arg.bal", diff --git a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule4.json b/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule4.json deleted file mode 100644 index 51c655e8..00000000 --- a/compiler-plugin-tests/src/test/resources/static_code_analyzer/expected_output/rule4.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "location": { - "filePath": "main.bal", - "startLine": 27, - "endLine": 27, - "startColumn": 21, - "endColumn": 72, - "startOffset": 1058, - "length": 51 - }, - "rule": { - "id": "ballerina/crypto:4", - "numericId": 4, - "description": "Secure random number generators should not output predictable values", - "ruleKind": "VULNERABILITY" - }, - "source": "BUILT_IN", - "fileName": "rule4/main.bal", - "filePath": "module-ballerina-crypto/compiler-plugin-tests/src/test/resources/static_code_analyzer/ballerina_packages/rule4/main.bal" - } -] diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoAnalyzerUtils.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoAnalyzerUtils.java index e79049b7..f197e7b3 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoAnalyzerUtils.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoAnalyzerUtils.java @@ -18,30 +18,43 @@ package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer; +import io.ballerina.compiler.api.ModuleID; import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.ConstantSymbol; +import io.ballerina.compiler.api.symbols.FunctionSymbol; +import io.ballerina.compiler.api.symbols.ParameterSymbol; import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.values.ConstantValue; +import io.ballerina.compiler.syntax.tree.AssignmentStatementNode; import io.ballerina.compiler.syntax.tree.BasicLiteralNode; +import io.ballerina.compiler.syntax.tree.BindingPatternNode; +import io.ballerina.compiler.syntax.tree.BlockStatementNode; import io.ballerina.compiler.syntax.tree.CaptureBindingPatternNode; import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionArgumentNode; import io.ballerina.compiler.syntax.tree.FunctionBodyBlockNode; -import io.ballerina.compiler.syntax.tree.ListConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionCallExpressionNode; import io.ballerina.compiler.syntax.tree.ModuleMemberDeclarationNode; import io.ballerina.compiler.syntax.tree.ModulePartNode; import io.ballerina.compiler.syntax.tree.ModuleVariableDeclarationNode; +import io.ballerina.compiler.syntax.tree.NameReferenceNode; +import io.ballerina.compiler.syntax.tree.NamedArgumentNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.PositionalArgumentNode; +import io.ballerina.compiler.syntax.tree.SeparatedNodeList; import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode; import io.ballerina.compiler.syntax.tree.StatementNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.VariableDeclarationNode; import io.ballerina.projects.Document; import io.ballerina.projects.DocumentId; import io.ballerina.projects.Module; -import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.function.BiPredicate; -import java.util.function.Predicate; /** * Utility class containing helper methods for crypto cipher algorithm analysis. @@ -49,438 +62,308 @@ * variable analysis. */ public final class CryptoAnalyzerUtils { - public static final String ENCRYPT_AES_ECB = "encryptAesEcb"; - public static final String ENCRYPT_AES_CBC = "encryptAesCbc"; - public static final String ENCRYPT_AES_GCM = "encryptAesGcm"; - public static final String HASH_BCRYPT = "hashBcrypt"; - public static final String HASH_ARGON2 = "hashArgon2"; - public static final String HASH_PBKDF2 = "hashPbkdf2"; - public static final String ITERATIONS = "iterations"; - public static final String MEMORY = "memory"; - public static final String PARALLELISM = "parallelism"; - public static final String CREATE_INT_IN_RANGE = "createIntInRange"; - public static final int BCRYPT_RECOMMENDED_WORK_FACTOR = 10; - public static final int ARGON2_RECOMMENDED_ITERATIONS = 2; - public static final int ARGON2_RECOMMENDED_MEMORY = 19456; - public static final int ARGON2_RECOMMENDED_PARALLELISM = 1; - public static final int PBKDF2_RECOMMENDED_ITERATIONS = 100000; - - /** - * Enum representing the different parameters of Argon2. - */ - public enum ArgonParameter { - ITERATIONS, - MEMORY, - PARALLELISM - } + private static final String BALLERINA_ORG = "ballerina"; + private static final String CRYPTO = "crypto"; + // Private constructor to prevent instantiation private CryptoAnalyzerUtils() { - // Prevent instantiation - } - /** - * Checks if the given function name corresponds to a weak cipher function. - * - * @param functionName the name of the function - * @return true if the function is a weak cipher function, false otherwise - */ - public static boolean isWeakCipherFunction(String functionName) { - return ENCRYPT_AES_ECB.equals(functionName) || ENCRYPT_AES_CBC.equals(functionName); } /** - * Checks if the given function requires secure initialization vectors. + * Retrieves the FunctionSymbol for a given FunctionCallExpressionNode if it belongs to the Ballerina + * crypto module. * - * @param functionName the name of the function - * @return true if the function requires secure IVs, false otherwise + * @param functionCall the function call expression node + * @param semanticModel the semantic model + * @return an Optional containing the FunctionSymbol if it belongs to the crypto module, otherwise empty */ - public static boolean requiresSecureIV(String functionName) { - return ENCRYPT_AES_GCM.equals(functionName) || ENCRYPT_AES_CBC.equals(functionName); - } - - /** - * Checks if the given expression represents a weak bcrypt work factor. - * - * @param expression the expression to check - * @return true if the work factor is weak, false otherwise - */ - public static boolean isWeakBcryptParameter(ExpressionNode expression) { - if (expression instanceof BasicLiteralNode basicLiteral) { - try { - return Integer.parseInt(basicLiteral.literalToken().text()) < BCRYPT_RECOMMENDED_WORK_FACTOR; - } catch (NumberFormatException e) { - return false; - } - } else if (expression instanceof SimpleNameReferenceNode varRef) { - return hasWeakParameterSettings(varRef, CryptoAnalyzerUtils::isWeakBcryptVariable); + public static Optional getCryptoFunctionSymbol(FunctionCallExpressionNode functionCall, + SemanticModel semanticModel) { + Optional functionCallSymbolOptional = semanticModel.symbol(functionCall); + if (functionCallSymbolOptional.isEmpty() + || !(functionCallSymbolOptional.get() instanceof FunctionSymbol functionSymbol) + || functionSymbol.getModule().isEmpty()) { + return Optional.empty(); + } + ModuleID moduleId = (functionCallSymbolOptional.get()).getModule().get().id(); + if (BALLERINA_ORG.equals(moduleId.orgName()) && CRYPTO.equals(moduleId.packageName())) { + return Optional.of(functionSymbol); } - return false; + return Optional.empty(); } /** - * Checks if the given expression represents a weak Argon2 parameter value. + * Retrieves the Document corresponding to the given module and document ID. * - * @param expression the expression to check - * @param paramType the parameter type (iterations, memory, or parallelism) - * @return true if the parameter is weak, false otherwise + * @param module the module + * @param documentId the document ID + * @return the Document for the given module and document ID */ - public static boolean isWeakArgon2Parameter(ExpressionNode expression, ArgonParameter paramType) { - if (expression instanceof BasicLiteralNode basicLiteral) { - try { - int value = Integer.parseInt(basicLiteral.literalToken().text()); - return switch (paramType) { - case ITERATIONS -> value < ARGON2_RECOMMENDED_ITERATIONS; - case MEMORY -> value < ARGON2_RECOMMENDED_MEMORY; - case PARALLELISM -> value < ARGON2_RECOMMENDED_PARALLELISM; - }; - } catch (NumberFormatException e) { - return false; - } - } else if (expression instanceof SimpleNameReferenceNode varRef) { - return hasWeakParameterSettings(varRef, (stmt, varName) -> isWeakArgon2Variable(stmt, varName, paramType)); - } - return false; + public static Document getDocument(Module module, DocumentId documentId) { + return module.document(documentId); } /** - * Checks if the given expression represents a weak PBKDF2 iterations count. + * Unescape the given identifier name by removing leading escape quote and backslashes. * - * @param expression the expression to check - * @return true if the iterations count is weak, false otherwise + * @param identifierName The identifier name to unescape + * @return The unescaped identifier name */ - public static boolean isWeakPbkdf2Parameter(ExpressionNode expression) { - if (expression instanceof BasicLiteralNode basicLiteral) { - try { - int value = Integer.parseInt(basicLiteral.literalToken().text()); - return value < PBKDF2_RECOMMENDED_ITERATIONS; - } catch (NumberFormatException e) { - return false; - } - } else if (expression instanceof SimpleNameReferenceNode varRef) { - return hasWeakParameterSettings(varRef, CryptoAnalyzerUtils::isWeakPbkdf2Variable); + public static String unescapeIdentifier(String identifierName) { + String result = identifierName; + if (result.startsWith("'")) { + result = result.substring(1); } - return false; + return result.replace("\\\\", ""); } - /** - * Checks if the given statement declares a variable with a weak bcrypt work - * factor. - * - * @param stmt the statement to check - * @param varName the name of the variable - * @return true if the statement declares a variable with a weak bcrypt work - * factor, false otherwise - */ - public static boolean isWeakBcryptVariable(Node stmt, String varName) { - return isWeakVariableWithInitializer(stmt, varName, initText -> { - try { - return Integer.parseInt(initText) < BCRYPT_RECOMMENDED_WORK_FACTOR; - } catch (NumberFormatException e) { - return false; - } - }); - } /** - * Checks if the given statement declares a variable with a weak Argon2 - * parameter value. + * Maps parameter names to their corresponding argument expressions in a function call. * - * @param stmt the statement to check - * @param varName the name of the variable - * @param paramType the parameter type (iterations, memory, or parallelism) - * @return true if the statement declares a variable with a weak parameter - * value, false otherwise + * @param params List of ParameterSymbol representing the function parameters + * @param arguments SeparatedNodeList of FunctionArgumentNode representing the function arguments + * @return A map where keys are parameter names and values are the corresponding argument expressions */ - public static boolean isWeakArgon2Variable(Node stmt, String varName, ArgonParameter paramType) { - return isWeakVariableWithInitializer(stmt, varName, initText -> { - try { - int value = Integer.parseInt(initText); - return switch (paramType) { - case ITERATIONS -> value < ARGON2_RECOMMENDED_ITERATIONS; - case MEMORY -> value < ARGON2_RECOMMENDED_MEMORY; - case PARALLELISM -> value < ARGON2_RECOMMENDED_PARALLELISM; - }; - } catch (NumberFormatException e) { - return false; + public static Map getParamExpressions(List params, + SeparatedNodeList arguments) { + Map paramExpressions = new HashMap<>(); + // Argument types: Positional, Named and Rest + // Parameter types: Required, Defaultable, Included and Rest + List paramNames = params.stream() + .map(ParameterSymbol::getName) + .filter(Optional::isPresent) + .map(Optional::get) + .map(CryptoAnalyzerUtils::unescapeIdentifier) + .toList(); + // For each argument we need to find the corresponding parameter name and added it to the map + for (int i = 0; i < arguments.size(); i++) { + FunctionArgumentNode argument = arguments.get(i); + if (argument instanceof PositionalArgumentNode positionalArg) { + if (i < paramNames.size()) { + String paramName = paramNames.get(i); + paramName = unescapeIdentifier(paramName); + ExpressionNode expression = positionalArg.expression(); + paramExpressions.put(paramName, expression); + } + } else if (argument instanceof NamedArgumentNode namedArg) { + String paramName = namedArg.argumentName().name().text(); + paramName = unescapeIdentifier(paramName); + ExpressionNode expression = namedArg.expression(); + paramExpressions.put(paramName, expression); } - }); - } - - /** - * Checks if the given statement declares a variable with a weak PBKDF2 - * iterations count. - * - * @param stmt the statement to check - * @param varName the name of the variable - * @return true if the statement declares a variable with a weak iterations - * count, false otherwise - */ - public static boolean isWeakPbkdf2Variable(Node stmt, String varName) { - return isWeakVariableWithInitializer(stmt, varName, CryptoAnalyzerUtils::checkPbkdf2InitializerValue); - } - - /** - * Checks if the given initializer value is a weak PBKDF2 iterations count. - * - * @param initText the initializer text - * @return true if the initializer value is weak, false otherwise - */ - public static boolean checkPbkdf2InitializerValue(String initText) { - try { - int value = Integer.parseInt(initText); - return value < PBKDF2_RECOMMENDED_ITERATIONS; - } catch (NumberFormatException e) { - return false; + // Not handling RestArgumentNode at the moment as crypto functions do not have rest parameters } + return paramExpressions; } /** - * Checks if the given expression represents a hardcoded initialization vector. - * A hardcoded IV is considered one that contains literal values or array - * literals. + * Retrieves the StatementNode that contains the given FunctionCallExpressionNode. * - * @param expression the expression to check - * @return true if the expression is a hardcoded IV, false otherwise + * @param functionCall the function call expression node + * @return an Optional containing the StatementNode if found, otherwise empty */ - public static boolean isHardcodedIV(ExpressionNode expression, SyntaxNodeAnalysisContext context) { - if (expression instanceof BasicLiteralNode || expression instanceof ListConstructorExpressionNode) { - return true; - } else if (expression instanceof SimpleNameReferenceNode varRef) { - SemanticModel semanticModel = context.semanticModel(); - Optional symbolOpt = semanticModel.symbol(varRef); - return symbolOpt.isPresent() && semanticModel.references(symbolOpt.get()).size() == 2; + public static Optional getStatementNode(FunctionCallExpressionNode functionCall) { + Node parent = functionCall.parent(); + if (parent.kind().equals(SyntaxKind.CHECK_EXPRESSION)) { + parent = parent.parent(); } - return false; + if (parent instanceof StatementNode statementNode) { + return Optional.of(statementNode); + } + return Optional.empty(); } /** - * Generic method to check if a variable declaration has a weak initializer - * value. + * Recursively retrieves the nearest parent block node (FunctionBodyBlockNode or BlockStatementNode) + * of the given node. * - * @param stmt the statement to check - * @param varName the name of the variable - * @param initializerChecker predicate to check if the initializer value is weak - * @return true if the statement declares a variable with a weak value, false - * otherwise + * @param node the starting node + * @return an Optional containing the parent block node if found, otherwise empty */ - public static boolean isWeakVariableWithInitializer(Node stmt, String varName, - Predicate initializerChecker) { - if (stmt instanceof VariableDeclarationNode varDecl && - varDecl.typedBindingPattern().bindingPattern() instanceof CaptureBindingPatternNode capture && - capture.variableName().text().equals(varName) && - varDecl.initializer().isPresent()) { - return initializerChecker.test(varDecl.initializer().get().toSourceCode()); - } - - if (stmt instanceof ModuleVariableDeclarationNode varDecl && - varDecl.typedBindingPattern().bindingPattern() instanceof CaptureBindingPatternNode capture && - capture.variableName().text().equals(varName) && - varDecl.initializer().isPresent()) { - return initializerChecker.test(varDecl.initializer().get().toSourceCode()); - } - - return false; + public static Optional getParentBlockNode(Node node) { + Node parent = node.parent(); + return switch (parent) { + case null -> Optional.empty(); + case FunctionBodyBlockNode functionBody -> Optional.of(functionBody); + case BlockStatementNode blockStatementNode -> Optional.of(blockStatementNode); + default -> getParentBlockNode(parent); + }; } /** - * Checks if the given variable reference refers to a variable with a weak - * parameter value. + * Recursively retrieves the ModulePartNode that contains the given node. * - * @param varRef the variable reference - * @param checker predicate to check if a statement declares a variable with a - * weak value - * @return true if the variable has a weak parameter value, false otherwise + * @param node the starting node + * @return an Optional containing the ModulePartNode if found, otherwise empty */ - public static boolean hasWeakParameterSettings(SimpleNameReferenceNode varRef, BiPredicate checker) { - String varName = varRef.name().text(); - return hasWeakParameterInScope(varRef.parent(), varName, checker); + public static Optional getModulePartNode(Node node) { + Node parent = node.parent(); + return switch (parent) { + case null -> Optional.empty(); + case ModulePartNode modulePartNode -> Optional.of(modulePartNode); + default -> getModulePartNode(parent); + }; } /** - * Checks if a variable with the given name has weak parameter values within a - * specific scope. + * Retrieves a map of module-level variable names to their initializer expressions + * from the given ModulePartNode. * - * @param startNode the node to start searching from - * @param varName the name of the variable - * @param checker predicate to check if a statement declares a variable with a - * weak value - * @return true if a weak parameter value is found, false otherwise + * @param modulePartNode the module part node + * @return a map where keys are variable names and values are their initializer expressions */ - public static boolean hasWeakParameterInScope(Node startNode, String varName, BiPredicate checker) { - Node current = startNode; - while (current != null) { - if (current instanceof FunctionBodyBlockNode functionBodyBlock) { - if (checkStatementsForWeakParameter(functionBodyBlock.statements(), varName, checker)) { - return true; + public static Map getModuleLevelVarExpressions(ModulePartNode modulePartNode) { + Map varExpressions = new HashMap<>(); + for (ModuleMemberDeclarationNode member : modulePartNode.members()) { + if (member instanceof ModuleVariableDeclarationNode variableDeclarationNode) { + BindingPatternNode bindingPatternNode = variableDeclarationNode.typedBindingPattern().bindingPattern(); + if (variableDeclarationNode.initializer().isEmpty() || + !(bindingPatternNode instanceof CaptureBindingPatternNode captureBindingPattern)) { + continue; } - } else if (current instanceof ModulePartNode modulePart - && checkModuleMembersForWeakParameter(modulePart.members(), varName, checker)) { - return true; + String varName = captureBindingPattern.variableName().text(); + varName = unescapeIdentifier(varName); + ExpressionNode initializer = variableDeclarationNode.initializer().get(); + varExpressions.put(varName, initializer); } - - current = current.parent(); } - return false; + return varExpressions; } /** - * Checks a list of statements for a weak parameter value. + * Collects variable declarations and assignments from the given block node + * up to the specified statement node. * - * @param statements the statements to check - * @param varName the name of the variable - * @param checker predicate to check if a statement declares a variable with - * a weak value - * @return true if a weak parameter value is found, false otherwise + * @param blockNode the block node (FunctionBodyBlockNode or BlockStatementNode) + * @param statementNode the statement node to stop at + * @param varExpressions the map to store variable names and their expressions */ - public static boolean checkStatementsForWeakParameter(NodeList statements, String varName, - BiPredicate checker) { - for (StatementNode stmt : statements) { - if (checker.test(stmt, varName)) { - return true; - } - } - return false; + public static void collectVariableExpressionsUntilStatement(Node blockNode, StatementNode statementNode, + Map varExpressions) { + NodeList statements = switch (blockNode) { + case FunctionBodyBlockNode functionBody -> functionBody.statements(); + case BlockStatementNode blockStatementNode -> blockStatementNode.statements(); + default -> throw new IllegalArgumentException("Unsupported block node type: " + blockNode.kind()); + }; + processStatementsForVariableExpressions(statements, statementNode, varExpressions); } /** - * Checks a list of module members for a weak parameter value. + * Checks if the given statement node is a block statement or not. * - * @param members the module members to check - * @param varName the name of the variable - * @param checker predicate to check if a statement declares a variable with a - * weak value - * @return true if a weak parameter value is found, false otherwise + * @param statement the statement node to check + * @return true if the statement is a block statement, false otherwise */ - public static boolean checkModuleMembersForWeakParameter(NodeList members, - String varName, - BiPredicate checker) { - for (ModuleMemberDeclarationNode member : members) { - if (checker.test(member, varName)) { - return true; - } - } - return false; + public static boolean isBlockStatementNode(StatementNode statement) { + SyntaxKind kind = statement.kind(); + return kind.equals(SyntaxKind.BLOCK_STATEMENT) || kind.equals(SyntaxKind.DO_STATEMENT) + || kind.equals(SyntaxKind.FORK_STATEMENT) || kind.equals(SyntaxKind.IF_ELSE_STATEMENT) + || kind.equals(SyntaxKind.LOCK_STATEMENT) || kind.equals(SyntaxKind.MATCH_STATEMENT) + || kind.equals(SyntaxKind.FOREACH_STATEMENT) || kind.equals(SyntaxKind.WHILE_STATEMENT) + || kind.equals(SyntaxKind.TRANSACTION_STATEMENT) || kind.equals(SyntaxKind.RETRY_STATEMENT); } /** - * Checks if the given expression uses unsecure random number generation. - * This includes checking for random:createIntInRange() calls with various - * prefixes. + * Retrieves the string value of a parameter from the function context. * - * @param expression the expression to check - * @param randomPrefixes set of prefixes used for the random module - * @return true if the expression uses unsecure random, false otherwise + * @param key the parameter name + * @param context the function context + * @return an Optional containing the string value if found, otherwise empty */ - public static boolean usesUnsecureRandom(ExpressionNode expression, Set randomPrefixes) { - if (expression instanceof SimpleNameReferenceNode varRef) { - return hasUnsecureRandomInVariableDeclaration(varRef, randomPrefixes); + public static Optional getStringValue(String key, FunctionContext context) { + Optional valueExprOpt = context.getParamExpression(key); + if (valueExprOpt.isEmpty()) { + return Optional.empty(); } - return false; - } - /** - * Checks if the given variable reference refers to a variable that uses - * unsecure random. - * - * @param varRef the variable reference - * @param randomPrefixes set of prefixes used for the random module - * @return true if the variable uses unsecure random, false otherwise - */ - private static boolean hasUnsecureRandomInVariableDeclaration(SimpleNameReferenceNode varRef, - Set randomPrefixes) { - String varName = varRef.name().text(); - return hasUnsecureRandomInScope(varRef.parent(), varName, randomPrefixes); + ExpressionNode valueExpr = valueExprOpt.get(); + if (valueExpr.kind().equals(SyntaxKind.STRING_LITERAL)) { + // String literal values + String stringValue = ((BasicLiteralNode) valueExpr).literalToken().text(); + // Remove the leading and trailing double quotes + stringValue = stringValue.substring(1, stringValue.length() - 1); + return Optional.of(stringValue); + } else if (valueExpr instanceof NameReferenceNode refNode) { + // Checking for constant values + Optional refSymbol = context.semanticModel().symbol(refNode); + if (refSymbol.isPresent() && refSymbol.get() instanceof ConstantSymbol constantRef && + constantRef.constValue() instanceof ConstantValue constantValue && + constantValue.value() instanceof String constString) { + return Optional.of(constString); + } + } + return Optional.empty(); } /** - * Checks if a variable with the given name uses unsecure random within a - * specific scope. + * Processes statements to collect variable declarations and assignments + * up to the specified target statement. * - * @param startNode the node to start searching from - * @param varName the name of the variable - * @param randomPrefixes set of prefixes used for the random module - * @return true if unsecure random usage is found, false otherwise + * @param statements the list of statements + * @param targetStatement the target statement to stop at + * @param varExpressions the map to store variable names and their expressions */ - private static boolean hasUnsecureRandomInScope(Node startNode, String varName, Set randomPrefixes) { - Node current = startNode; - while (current != null) { - if (current instanceof FunctionBodyBlockNode functionBodyBlock) { - if (checkStatementsForUnsecureRandom(functionBodyBlock.statements(), varName, randomPrefixes)) { - return true; + public static void processStatementsForVariableExpressions(NodeList statements, + StatementNode targetStatement, + Map varExpressions) { + for (StatementNode statement : statements) { + boolean isBlockStatement = isBlockStatementNode(statement); + + // Stop processing if we reach the target statement or found a block statement + if (statement.equals(targetStatement) || isBlockStatement) { + if (isBlockStatement) { + // If we find any block nodes, we cannot verify variable declarations or assignments + // since they may be changed within those blocks. Clear collected expressions and stop. + varExpressions.clear(); } - } else if (current instanceof ModulePartNode modulePart - && checkModuleMembersForUnsecureRandom(modulePart.members(), varName, randomPrefixes)) { - return true; + break; } - current = current.parent(); - } - return false; - } - - /** - * Checks a list of statements for unsecure random usage. - * - * @param statements the statements to check - * @param varName the name of the variable - * @param randomPrefixes set of prefixes used for the random module - * @return true if unsecure random usage is found, false otherwise - */ - private static boolean checkStatementsForUnsecureRandom(NodeList statements, String varName, - Set randomPrefixes) { - for (StatementNode stmt : statements) { - if (containsUnsecureRandomUsage(stmt.toSourceCode(), varName, randomPrefixes)) { - return true; + // Process assignment statements + if (statement instanceof AssignmentStatementNode assignmentNode) { + processAssignmentStatement(assignmentNode, varExpressions); + } else if (statement instanceof VariableDeclarationNode variableDeclarationNode) { + processVariableDeclaration(variableDeclarationNode, varExpressions); } } - return false; } /** - * Checks a list of module members for unsecure random usage. + * Processes an assignment statement and adds it to the variable expressions map. * - * @param members the module members to check - * @param varName the name of the variable - * @param randomPrefixes set of prefixes used for the random module - * @return true if unsecure random usage is found, false otherwise + * @param assignmentNode the assignment statement node + * @param varExpressions the map to store variable names and their expressions */ - private static boolean checkModuleMembersForUnsecureRandom(NodeList members, - String varName, Set randomPrefixes) { - for (ModuleMemberDeclarationNode member : members) { - if (containsUnsecureRandomUsage(member.toSourceCode(), varName, randomPrefixes)) { - return true; - } + private static void processAssignmentStatement(AssignmentStatementNode assignmentNode, + Map varExpressions) { + Node varRef = assignmentNode.varRef(); + if (varRef instanceof SimpleNameReferenceNode simpleNameRef) { + String varName = unescapeIdentifier(simpleNameRef.name().text()); + ExpressionNode expression = assignmentNode.expression(); + varExpressions.put(varName, expression); } - return false; } /** - * Checks if the source code contains unsecure random usage for a specific - * variable. + * Processes a variable declaration statement and adds it to the variable expressions map. * - * @param sourceCode the source code to check - * @param varName the name of the variable - * @param randomPrefixes set of prefixes used for the random module - * @return true if unsecure random usage is found, false otherwise + * @param variableDeclarationNode the variable declaration node + * @param varExpressions the map to store variable names and their expressions */ - private static boolean containsUnsecureRandomUsage(String sourceCode, String varName, Set randomPrefixes) { - for (String prefix : randomPrefixes) { - String randomCallPattern = prefix + ":" + CREATE_INT_IN_RANGE; - if (sourceCode.contains(varName) && sourceCode.contains(randomCallPattern)) { - return true; - } + private static void processVariableDeclaration(VariableDeclarationNode variableDeclarationNode, + Map varExpressions) { + BindingPatternNode bindingPatternNode = variableDeclarationNode.typedBindingPattern().bindingPattern(); + + // Only supporting capture binding patterns for variable declarations + if (variableDeclarationNode.initializer().isEmpty() || + !(bindingPatternNode instanceof CaptureBindingPatternNode captureBindingPattern)) { + return; } - return false; - } - /** - * Retrieves the Document corresponding to the given module and document ID. - * - * @param module the module - * @param documentId the document ID - * @return the Document for the given module and document ID - */ - public static Document getDocument(Module module, DocumentId documentId) { - return module.document(documentId); + String varName = unescapeIdentifier(captureBindingPattern.variableName().text()); + ExpressionNode initializer = variableDeclarationNode.initializer().get(); + varExpressions.put(varName, initializer); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoCipherAlgorithmAnalyzer.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoCipherAlgorithmAnalyzer.java index a963e4c7..b68fdcf2 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoCipherAlgorithmAnalyzer.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoCipherAlgorithmAnalyzer.java @@ -18,42 +18,30 @@ package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer; -import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.FunctionSymbol; import io.ballerina.compiler.syntax.tree.FunctionCallExpressionNode; -import io.ballerina.compiler.syntax.tree.ImportOrgNameNode; -import io.ballerina.compiler.syntax.tree.ImportPrefixNode; -import io.ballerina.compiler.syntax.tree.ModulePartNode; -import io.ballerina.compiler.syntax.tree.NamedArgumentNode; -import io.ballerina.compiler.syntax.tree.Node; -import io.ballerina.compiler.syntax.tree.PositionalArgumentNode; -import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.projects.Document; import io.ballerina.projects.plugins.AnalysisTask; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.scan.Reporter; -import java.util.HashSet; -import java.util.Set; +import java.util.Optional; -import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_FAST_HASH_ALGORITHMS; -import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_REUSING_COUNTER_MODE_VECTORS; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getCryptoFunctionSymbol; /** * Analyzer to detect the usage of weak cipher algorithms and insecure practices in the Ballerina crypto module. */ public class CryptoCipherAlgorithmAnalyzer implements AnalysisTask { + private final Reporter reporter; - private static final String BALLERINA_ORG = "ballerina"; - private static final String CRYPTO = "crypto"; - private static final String IV = "iv"; - private static final String RANDOM = "random"; - private final Set cryptoPrefixes = new HashSet<>(); - private final Set randomPrefixes = new HashSet<>(); + private final CryptoFunctionRulesEngine rulesEngine; public CryptoCipherAlgorithmAnalyzer(Reporter reporter) { this.reporter = reporter; - this.cryptoPrefixes.add(CRYPTO); - this.randomPrefixes.add(RANDOM); + this.rulesEngine = new CryptoFunctionRulesEngine(); } /** @@ -63,263 +51,21 @@ public CryptoCipherAlgorithmAnalyzer(Reporter reporter) { */ @Override public void perform(SyntaxNodeAnalysisContext context) { - analyzeImports(context); - FunctionCallExpressionNode functionCall = (FunctionCallExpressionNode) context.node(); - - if (!(functionCall.functionName() instanceof QualifiedNameReferenceNode qualifiedName)) { - return; - } - - String modulePrefix = qualifiedName.modulePrefix().text(); - - if (!cryptoPrefixes.contains(modulePrefix)) { - return; - } - - String functionName = qualifiedName.identifier().text(); - - if (CryptoAnalyzerUtils.isWeakCipherFunction(functionName)) { - report(context, CryptoRule.AVOID_WEAK_CIPHER_ALGORITHMS.getId()); - } - - if (CryptoAnalyzerUtils.requiresSecureIV(functionName)) { - checkHardcodedIVUsage(functionCall, context); - checkUnsecureRandomUsage(functionCall, context); - } - - if (CryptoAnalyzerUtils.HASH_BCRYPT.equals(functionName)) { - checkWeakBcryptUsage(functionCall, context); - } else if (CryptoAnalyzerUtils.HASH_ARGON2.equals(functionName)) { - checkWeakArgon2Usage(functionCall, context); - } else if (CryptoAnalyzerUtils.HASH_PBKDF2.equals(functionName)) { - checkWeakPbkdf2Usage(functionCall, context); - } - } - - /** - * Checks if the bcrypt hash function is used with weak parameters. - * - * @param functionCall the function call node - * @param context the syntax node analysis context - */ - private void checkWeakBcryptUsage(FunctionCallExpressionNode functionCall, SyntaxNodeAnalysisContext context) { - if (functionCall.arguments().stream().count() < 2) { - return; - } - - Node workFactor = functionCall.arguments().get(1); - - if (workFactor instanceof PositionalArgumentNode positional) { - ExpressionNode expr = positional.expression(); - if (CryptoAnalyzerUtils.isWeakBcryptParameter(expr)) { - report(context, AVOID_FAST_HASH_ALGORITHMS.getId()); - } - } else if (workFactor instanceof NamedArgumentNode named) { - ExpressionNode expr = named.expression(); - if (CryptoAnalyzerUtils.isWeakBcryptParameter(expr)) { - report(context, AVOID_FAST_HASH_ALGORITHMS.getId()); - } - } - } - - /** - * Checks if the Argon2 hash function is used with weak parameters. - * - * @param functionCall the function call node - * @param context the syntax node analysis context - */ - private void checkWeakArgon2Usage(FunctionCallExpressionNode functionCall, SyntaxNodeAnalysisContext context) { - int argsCount = (int) functionCall.arguments().stream().count(); - if (argsCount < 2) { - return; - } - - boolean hasWeakParameters = false; - - for (Node arg : functionCall.arguments()) { - if (arg instanceof NamedArgumentNode named) { - String paramName = named.argumentName().name().text(); - ExpressionNode expr = named.expression(); - - if (CryptoAnalyzerUtils.ITERATIONS.equals(paramName)) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakArgon2Parameter(expr, - CryptoAnalyzerUtils.ArgonParameter.ITERATIONS); - } else if (CryptoAnalyzerUtils.MEMORY.equals(paramName)) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakArgon2Parameter(expr, - CryptoAnalyzerUtils.ArgonParameter.MEMORY); - } else if (CryptoAnalyzerUtils.PARALLELISM.equals(paramName)) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakArgon2Parameter(expr, - CryptoAnalyzerUtils.ArgonParameter.PARALLELISM); - } - } - } - - // Check for positional arguments - if (functionCall.arguments().get(1) instanceof PositionalArgumentNode iterationsArg) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakArgon2Parameter(iterationsArg.expression(), - CryptoAnalyzerUtils.ArgonParameter.ITERATIONS); - } - - if (argsCount >= 3 && functionCall.arguments().get(2) instanceof PositionalArgumentNode memoryArg) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakArgon2Parameter(memoryArg.expression(), - CryptoAnalyzerUtils.ArgonParameter.MEMORY); - } - - if (argsCount >= 4 && functionCall.arguments().get(3) instanceof PositionalArgumentNode parallelismArg) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakArgon2Parameter(parallelismArg.expression(), - CryptoAnalyzerUtils.ArgonParameter.PARALLELISM); - } - - if (hasWeakParameters) { - report(context, AVOID_FAST_HASH_ALGORITHMS.getId()); - } - } - - /** - * Checks if the PBKDF2 hash function is used with weak parameters. - * - * @param functionCall the function call node - * @param context the syntax node analysis context - */ - private void checkWeakPbkdf2Usage(FunctionCallExpressionNode functionCall, SyntaxNodeAnalysisContext context) { - int argsCount = (int) functionCall.arguments().stream().count(); - if (argsCount < 2) { - report(context, AVOID_FAST_HASH_ALGORITHMS.getId()); - return; - } - - boolean hasWeakParameters = false; - - for (Node arg : functionCall.arguments()) { - if (arg instanceof NamedArgumentNode named) { - String paramName = named.argumentName().name().text(); - if (CryptoAnalyzerUtils.ITERATIONS.equals(paramName)) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakPbkdf2Parameter(named.expression()); - } - } - } - - if (functionCall.arguments().get(1) instanceof PositionalArgumentNode iterationsArg) { - hasWeakParameters |= CryptoAnalyzerUtils.isWeakPbkdf2Parameter(iterationsArg.expression()); - } - - if (hasWeakParameters) { - report(context, AVOID_FAST_HASH_ALGORITHMS.getId()); - } - } - - /** - * Checks if the AES-GCM/AES-ECB functions use hardcoded initialization vectors. - * For encryptAesGcm(input, key, iv, padding, tagSize), the third parameter (iv) - * is checked. - * - * @param functionCall the function call node - * @param context the syntax node analysis context - */ - private void checkHardcodedIVUsage(FunctionCallExpressionNode functionCall, SyntaxNodeAnalysisContext context) { - if (functionCall.arguments().stream().count() < 3) { + SemanticModel semanticModel = context.semanticModel(); + Document document = CryptoAnalyzerUtils.getDocument(context.currentPackage().module(context.moduleId()), + context.documentId()); + if (!(functionCall.functionName().kind().equals(SyntaxKind.QUALIFIED_NAME_REFERENCE))) { return; } - Node ivArgument = functionCall.arguments().get(2); - - if (ivArgument instanceof PositionalArgumentNode positional) { - ExpressionNode expr = positional.expression(); - if (CryptoAnalyzerUtils.isHardcodedIV(expr, context)) { - report(context, AVOID_REUSING_COUNTER_MODE_VECTORS.getId()); - } - } else if (ivArgument instanceof NamedArgumentNode named) { - // Check for named arguments - String paramName = named.argumentName().name().text(); - if (IV.equals(paramName)) { - ExpressionNode expr = named.expression(); - if (CryptoAnalyzerUtils.isHardcodedIV(expr, context)) { - report(context, AVOID_REUSING_COUNTER_MODE_VECTORS.getId()); - } - } - } - } - - /** - * Checks if the AES-GCM/AES-ECB functions use unsecure random number generation for - * initialization vectors. - * For encryptAes(input, key, iv, padding, tagSize), the third parameter (iv) - * is checked for usage of random:createIntInRange(). - * - * @param functionCall the function call node - * @param context the syntax node analysis context - */ - private void checkUnsecureRandomUsage(FunctionCallExpressionNode functionCall, SyntaxNodeAnalysisContext context) { - if (functionCall.arguments().stream().count() < 3) { + Optional functionSymbolOpt = getCryptoFunctionSymbol(functionCall, semanticModel); + if (functionSymbolOpt.isEmpty()) { return; } - Node ivArgument = functionCall.arguments().get(2); - - if (ivArgument instanceof PositionalArgumentNode positional) { - ExpressionNode expr = positional.expression(); - if (CryptoAnalyzerUtils.usesUnsecureRandom(expr, randomPrefixes)) { - report(context, CryptoRule.AVOID_USING_UNSECURE_RANDOM_NUMBER_GENERATORS.getId()); - } - } else if (ivArgument instanceof NamedArgumentNode named) { - String paramName = named.argumentName().name().text(); - if ("iv".equals(paramName)) { - ExpressionNode expr = named.expression(); - if (CryptoAnalyzerUtils.usesUnsecureRandom(expr, randomPrefixes)) { - report(context, CryptoRule.AVOID_USING_UNSECURE_RANDOM_NUMBER_GENERATORS.getId()); - } - } - } - } - - /** - * Reports an issue for the given context and rule ID. - * - * @param context the syntax node analysis context - * @param ruleId the ID of the rule to report - */ - private void report(SyntaxNodeAnalysisContext context, int ruleId) { - reporter.reportIssue( - CryptoAnalyzerUtils.getDocument(context.currentPackage().module(context.moduleId()), - context.documentId()), - context.node().location(), - ruleId); - } - - /** - * Analyzes imports to identify all prefixes used for the crypto module. - * - * @param context the syntax node analysis context - */ - private void analyzeImports(SyntaxNodeAnalysisContext context) { - Document document = CryptoAnalyzerUtils.getDocument(context.currentPackage().module(context.moduleId()), - context.documentId()); - - if (document.syntaxTree().rootNode() instanceof ModulePartNode modulePartNode) { - modulePartNode.imports().forEach(importDeclarationNode -> { - ImportOrgNameNode importOrgNameNode = importDeclarationNode.orgName().orElse(null); - - if (importOrgNameNode != null && BALLERINA_ORG.equals(importOrgNameNode.orgName().text())) { - if (importDeclarationNode.moduleName().stream() - .anyMatch(moduleNameNode -> CRYPTO.equals(moduleNameNode.text()))) { - - ImportPrefixNode importPrefixNode = importDeclarationNode.prefix().orElse(null); - String prefix = importPrefixNode != null ? importPrefixNode.prefix().text() : CRYPTO; - - cryptoPrefixes.add(prefix); - } - - if (importDeclarationNode.moduleName().stream() - .anyMatch(moduleNameNode -> RANDOM.equals(moduleNameNode.text()))) { - - ImportPrefixNode importPrefixNode = importDeclarationNode.prefix().orElse(null); - String prefix = importPrefixNode != null ? importPrefixNode.prefix().text() : RANDOM; - - randomPrefixes.add(prefix); - } - } - }); - } + FunctionContext functionContext = FunctionContext.getInstance(semanticModel, this.reporter, document, + functionCall, functionSymbolOpt.get()); + rulesEngine.executeRules(functionContext); } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoFunctionRulesEngine.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoFunctionRulesEngine.java new file mode 100644 index 00000000..f9b46ea8 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoFunctionRulesEngine.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://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. + */ +package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer; + +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules.AvoidFastHashAlgorithmsRule; +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules.AvoidReusingCounterModeVectorsRule; +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules.AvoidWeakCipherAlgorithmsRule; +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules.CryptoFunctionRule; + +import java.util.ArrayList; +import java.util.List; + +/** + * Engine to execute crypto function rules. + * + * @since 2.9.1 + */ +public class CryptoFunctionRulesEngine { + + private final List rules; + + public CryptoFunctionRulesEngine() { + this.rules = new ArrayList<>(); + initializeDefaultRules(); + } + + public void executeRules(FunctionContext context) { + for (CryptoFunctionRule rule : rules) { + if (rule.isApplicable(context)) { + rule.analyze(context); + } + } + } + + public void addRule(CryptoFunctionRule rule) { + if (rule != null && !rules.contains(rule)) { + rules.add(rule); + } + } + + private void initializeDefaultRules() { + addRule(new AvoidWeakCipherAlgorithmsRule()); + addRule(new AvoidFastHashAlgorithmsRule()); + addRule(new AvoidReusingCounterModeVectorsRule()); + // Add more default rules here as needed + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoRule.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoRule.java index ca2aa96a..60f0d899 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoRule.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/CryptoRule.java @@ -29,9 +29,7 @@ public enum CryptoRule { AVOID_FAST_HASH_ALGORITHMS(createRule(2, "Avoid using fast hashing algorithms", VULNERABILITY)), AVOID_REUSING_COUNTER_MODE_VECTORS(createRule(3, - "Avoid reusing counter mode initialization vectors", VULNERABILITY)), - AVOID_USING_UNSECURE_RANDOM_NUMBER_GENERATORS(createRule(4, - "Secure random number generators should not output predictable values", VULNERABILITY)); + "Avoid reusing counter mode initialization vectors", VULNERABILITY)); private final Rule rule; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/FunctionContext.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/FunctionContext.java new file mode 100644 index 00000000..a6c7d77e --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/FunctionContext.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://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. + */ +package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.FunctionSymbol; +import io.ballerina.compiler.api.symbols.ParameterSymbol; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.FunctionArgumentNode; +import io.ballerina.compiler.syntax.tree.FunctionCallExpressionNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.SeparatedNodeList; +import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode; +import io.ballerina.compiler.syntax.tree.StatementNode; +import io.ballerina.projects.Document; +import io.ballerina.scan.Reporter; +import io.ballerina.tools.diagnostics.Location; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.collectVariableExpressionsUntilStatement; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getModuleLevelVarExpressions; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getModulePartNode; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getParamExpressions; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getParentBlockNode; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getStatementNode; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.unescapeIdentifier; + +/** + * Represents the context of a function being analyzed. + * + * @since 2.9.1 + */ +public class FunctionContext { + private final SemanticModel semanticModel; + private final Reporter reporter; + private final Document document; + private final String functionName; + private final Location functionLocation; + private Map paramExpressions = Map.of(); + private Map varExpressions = Map.of(); + + /** + * Creates a FunctionContext instance for the given function call and symbol. + * + * @param semanticModel The semantic model + * @param reporter The reporter for diagnostics + * @param document The document containing the function call + * @param functionCall The function call expression node + * @param functionSymbol The function symbol + * @return A FunctionContext instance + */ + public static FunctionContext getInstance(SemanticModel semanticModel, Reporter reporter, Document document, + FunctionCallExpressionNode functionCall, + FunctionSymbol functionSymbol) { + Location location = functionCall.location(); + SeparatedNodeList arguments = functionCall.arguments(); + Optional functionNameOpt = functionSymbol.getName(); + if (functionNameOpt.isEmpty()) { + // This should not happen as function symbols always have name in this context + throw new IllegalStateException("Function name is not available for the function symbol"); + } + + String functionName = functionNameOpt.get(); + Optional> params = functionSymbol.typeDescriptor().params(); + // Create a default FunctionContext in case of missing parameters or arguments + FunctionContext defaultFunctionContext = new FunctionContext(semanticModel, reporter, document, functionName, + location); + if (params.isEmpty() || arguments.isEmpty()) { + return defaultFunctionContext; + } + + Map paramExpressions = getParamExpressions(params.get(), arguments); + + Optional statementNode = getStatementNode(functionCall); + if (statementNode.isEmpty()) { + return defaultFunctionContext; + } + + // A map is used to hold variable expressions in global and function block scope. + // The key is the variable name and the value is the expression node assigned to it. + // Processing happens in an order using the NodeList. Hence, expressions will be overridden + // if there are reassignments with the same variable name + Map varExpressions = new HashMap<>(); + + // Collect module level variable expressions + Optional modulePartNode = getModulePartNode(statementNode.get()); + if (modulePartNode.isPresent()) { + varExpressions = getModuleLevelVarExpressions(modulePartNode.get()); + } + + Optional functionBodyOpt = getParentBlockNode(statementNode.get()); + if (functionBodyOpt.isEmpty()) { + return defaultFunctionContext; + } + + // Add variable declarations up to the function call statement + collectVariableExpressionsUntilStatement(functionBodyOpt.get(), statementNode.get(), varExpressions); + return new FunctionContext(semanticModel, reporter, document, functionName, location, paramExpressions, + varExpressions); + } + + // Private constructor to enforce the use of the instance method + private FunctionContext(SemanticModel semanticModel, Reporter reporter, Document document, String functionName, + Location functionLocation) { + this.semanticModel = semanticModel; + this.reporter = reporter; + this.document = document; + this.functionName = functionName; + this.functionLocation = functionLocation; + } + + // Private constructor to enforce the use of the instance method + private FunctionContext(SemanticModel semanticModel, Reporter reporter, Document document, String functionName, + Location functionLocation, Map paramExpressions, + Map varExpressions) { + this(semanticModel, reporter, document, functionName, functionLocation); + this.paramExpressions = paramExpressions; + this.varExpressions = varExpressions; + } + + /** + * Returns the semantic model. + * + * @return the semantic model + */ + public SemanticModel semanticModel() { + return semanticModel; + } + + /** + * Returns the reporter. + * + * @return the reporter + */ + public Reporter reporter() { + return reporter; + } + + /** + * Returns the document. + * + * @return the document + */ + public Document document() { + return document; + } + + /** + * Returns the function name. + * + * @return the function name + */ + public String functionName() { + return functionName; + } + + /** + * Returns the function location. + * + * @return the function location + */ + public Location functionLocation() { + return functionLocation; + } + + /** + * Retrieves the expression node for a given parameter name. + * If the parameter expression is a simple name reference, it resolves + * the reference to get the actual expression. + * + * @param paramName The name of the parameter + * @return An Optional containing the expression node if found, otherwise empty + */ + public Optional getParamExpression(String paramName) { + paramName = unescapeIdentifier(paramName); + if (!paramExpressions.containsKey(paramName)) { + return Optional.empty(); + } + ExpressionNode paramExpr = paramExpressions.get(paramName); + if (paramExpr instanceof SimpleNameReferenceNode simpleNameRef) { + String varName = simpleNameRef.name().text(); + // Retrying to get the expression from variable expressions + // collected with the block scope and global scope + Optional varExprOpt = getVarExpression(varName); + return varExprOpt.isPresent() ? varExprOpt : Optional.of(paramExpr); + } + return Optional.of(paramExpr); + } + + /** + * Retrieves the expression node for a given variable name. + * If the variable expression is a simple name reference, it resolves + * the reference to get the actual expression. + * + * @param varName The name of the variable + * @return An Optional containing the expression node if found, otherwise empty + */ + public Optional getVarExpression(String varName) { + varName = unescapeIdentifier(varName); + if (!varExpressions.containsKey(varName)) { + return Optional.empty(); + } + ExpressionNode varExpr = varExpressions.get(varName); + if (varExpr instanceof SimpleNameReferenceNode simpleNameRef) { + String innerVarName = simpleNameRef.name().text(); + return getVarExpression(innerVarName); + } + return Optional.of(varExpr); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidFastHashAlgorithmsRule.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidFastHashAlgorithmsRule.java new file mode 100644 index 00000000..57f3b059 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidFastHashAlgorithmsRule.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://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. + */ +package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.ConstantSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.values.ConstantValue; +import io.ballerina.compiler.syntax.tree.BasicLiteralNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.NameReferenceNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.FunctionContext; + +import java.util.Optional; + +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getStringValue; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_FAST_HASH_ALGORITHMS; + +/** + * Rule to avoid fast hash algorithms. + * Analyzes the usage of bcrypt, argon2, and pbkdf2 hashing functions with weak parameters. + * + * @since 2.9.1 + */ +public class AvoidFastHashAlgorithmsRule implements CryptoFunctionRule { + + public static final String HASH_BCRYPT = "hashBcrypt"; + public static final String HASH_ARGON2 = "hashArgon2"; + public static final String HASH_PBKDF2 = "hashPbkdf2"; + public static final String WORK_FACTOR = "workFactor"; + public static final String ITERATIONS = "iterations"; + public static final String MEMORY = "memory"; + public static final String PARALLELISM = "parallelism"; + public static final String ALGORITHM = "algorithm"; + public static final String HMAC_SHA1 = "SHA1"; + public static final String HMAC_SHA256 = "SHA256"; + public static final String HMAC_SHA512 = "SHA512"; + + public static final int BCRYPT_RECOMMENDED_WORK_FACTOR = 10; + public static final int ARGON2_RECOMMENDED_ITERATIONS = 2; + public static final int ARGON2_RECOMMENDED_MEMORY = 19456; + public static final int PBKDF2_RECOMMENDED_ITERATIONS_FOR_SHA1 = 1300000; + public static final int PBKDF2_RECOMMENDED_ITERATIONS_FOR_SHA256 = 600000; + public static final int PBKDF2_RECOMMENDED_ITERATIONS_FOR_SHA512 = 210000; + + @Override + public void analyze(FunctionContext context) { + if (isBCryptWithLowWorkFactor(context) || isArgon2WithWeakParams(context) + || isPBKDF2WithLowIterations(context)) { + context.reporter().reportIssue(context.document(), context.functionLocation(), getRuleId()); + } + } + + @Override + public int getRuleId() { + return AVOID_FAST_HASH_ALGORITHMS.getId(); + } + + @Override + public boolean isApplicable(FunctionContext context) { + String functionName = context.functionName(); + return functionName.equals(HASH_BCRYPT) || functionName.equals(HASH_ARGON2) + || functionName.equals(HASH_PBKDF2); + } + + private boolean isBCryptWithLowWorkFactor(FunctionContext context) { + if (!context.functionName().equals(HASH_BCRYPT)) { + return false; + } + SemanticModel semanticModel = context.semanticModel(); + Optional workFactorOpt = context.getParamExpression(WORK_FACTOR); + // If work factor is not provided, default is 12 which is considered secure + return workFactorOpt + .filter(expr -> hasLowerIntegerValue(expr, BCRYPT_RECOMMENDED_WORK_FACTOR, semanticModel)) + .isPresent(); + + } + + private boolean isArgon2WithWeakParams(FunctionContext context) { + if (!context.functionName().equals(HASH_ARGON2)) { + return false; + } + SemanticModel semanticModel = context.semanticModel(); + // Check if any parameter is below the recommended threshold + // Parallelism should be a positive integer value and the minimum recommended value is 1 + // So, we do not need to check for parallelism here + return isArgon2ParamBelowThreshold(context, ITERATIONS, ARGON2_RECOMMENDED_ITERATIONS, semanticModel) || + isArgon2ParamBelowThreshold(context, MEMORY, ARGON2_RECOMMENDED_MEMORY, semanticModel); + } + + private boolean isArgon2ParamBelowThreshold(FunctionContext context, String paramName, + int recommendedValue, SemanticModel semanticModel) { + Optional paramOpt = context.getParamExpression(paramName); + // If parameter is not provided, defaults are used which are considered secure + // Default iterations: 3, Default memory: 65536, Default parallelism: 4 + return paramOpt + .filter(expr -> hasLowerIntegerValue(expr, recommendedValue, semanticModel)) + .isPresent(); + } + + private boolean isPBKDF2WithLowIterations(FunctionContext context) { + if (!context.functionName().equals(HASH_PBKDF2)) { + return false; + } + + // If algorithm is not provided, default is HMAC_SHA256 + String algorithm = getStringValue(ALGORITHM, context).orElse(HMAC_SHA256); + + SemanticModel semanticModel = context.semanticModel(); + Optional iterationsOpt = context.getParamExpression(ITERATIONS); + if (iterationsOpt.isEmpty()) { + // Default iterations for is 10000 which is lower than recommended for all algorithms + return true; + } + + int recommendedIterations = getRecommendedIterationsForPBKDF2(algorithm); + return hasLowerIntegerValue(iterationsOpt.get(), recommendedIterations, semanticModel); + } + + private static int getRecommendedIterationsForPBKDF2(String algorithm) { + return switch (algorithm) { + case HMAC_SHA1 -> PBKDF2_RECOMMENDED_ITERATIONS_FOR_SHA1; + case HMAC_SHA256 -> PBKDF2_RECOMMENDED_ITERATIONS_FOR_SHA256; + case HMAC_SHA512 -> PBKDF2_RECOMMENDED_ITERATIONS_FOR_SHA512; + default -> throw new IllegalArgumentException("Unsupported HMAC algorithm: " + algorithm + + " found for PBKDF2 hashing function"); + }; + } + + private static boolean hasLowerIntegerValue(ExpressionNode valueExpr, Integer targetValue, + SemanticModel semanticModel) { + if (valueExpr.kind().equals(SyntaxKind.NUMERIC_LITERAL)) { + String iterationsValue = ((BasicLiteralNode) valueExpr).literalToken().text(); + try { + int iterationsInt = Integer.parseInt(iterationsValue); + return iterationsInt < targetValue; + } catch (NumberFormatException e) { + // Ignore and continue + } + } else if (valueExpr instanceof NameReferenceNode refNode) { + Optional refSymbol = semanticModel.symbol(refNode); + if (refSymbol.isPresent() && refSymbol.get() instanceof ConstantSymbol constantRef && + constantRef.constValue() instanceof ConstantValue constantValue && + constantValue.value() instanceof Long longValue) { + return longValue < targetValue; + } + } + return false; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidReusingCounterModeVectorsRule.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidReusingCounterModeVectorsRule.java new file mode 100644 index 00000000..ce323e0c --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidReusingCounterModeVectorsRule.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://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. + */ +package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules; + +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.SymbolKind; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.ListConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.MethodCallExpressionNode; +import io.ballerina.compiler.syntax.tree.NameReferenceNode; +import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.FunctionContext; + +import java.util.Optional; + +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.unescapeIdentifier; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_REUSING_COUNTER_MODE_VECTORS; + +/** + * Rule to avoid reusing initialization vectors (IVs) in counter mode encryption algorithms such as AES-CBC + * and AES-GCM. + * + * @since 2.9.1 + */ +public class AvoidReusingCounterModeVectorsRule implements CryptoFunctionRule { + + public static final String ENCRYPT_AES_CBC = "encryptAesCbc"; + public static final String ENCRYPT_AES_GCM = "encryptAesGcm"; + public static final String INITIALIZATION_VECTOR = "iv"; + public static final String TO_BYTES_METHOD = "toBytes"; + + @Override + public void analyze(FunctionContext context) { + Optional paramExpression = context.getParamExpression(INITIALIZATION_VECTOR); + if (paramExpression.isEmpty()) { + // The IV is a required parameter for these functions, so this case should not occur. + throw new IllegalStateException("Initialization vector parameter is missing for function: " + + context.functionName()); + } + + if (hasHardCodedIV(paramExpression.get(), context)) { + context.reporter().reportIssue(context.document(), context.functionLocation(), getRuleId()); + } + } + + @Override + public int getRuleId() { + return AVOID_REUSING_COUNTER_MODE_VECTORS.getId(); + + } + + @Override + public boolean isApplicable(FunctionContext context) { + String functionName = context.functionName(); + return functionName.equals(ENCRYPT_AES_CBC) || functionName.equals(ENCRYPT_AES_GCM); + } + + private boolean hasHardCodedIV(ExpressionNode ivExpression, FunctionContext context) { + // Check for list constructor with numeric literals (e.g., [1, 2, 3, ...]) + if (ivExpression instanceof ListConstructorExpressionNode listExpression) { + return listExpression.expressions().stream() + .allMatch(expr -> expr.kind().equals(SyntaxKind.NUMERIC_LITERAL)); + } + + // Check for toBytes() method called on a string literal or name reference referring to a constant + if (ivExpression instanceof MethodCallExpressionNode methodCallExpression) { + ExpressionNode expression = methodCallExpression.expression(); + if (!isMethodCallOnConstantExpr(expression, context)) { + return false; + } + NameReferenceNode nameReferenceNode = methodCallExpression.methodName(); + if (nameReferenceNode instanceof SimpleNameReferenceNode simpleNameRef) { + return simpleNameRef.name().text().equals(TO_BYTES_METHOD); + } + } + + return false; + } + + private boolean isMethodCallOnConstantExpr(ExpressionNode expression, FunctionContext context) { + if (expression.kind().equals(SyntaxKind.STRING_LITERAL)) { + return true; + } + if (expression.kind().equals(SyntaxKind.SIMPLE_NAME_REFERENCE) || + expression.kind().equals(SyntaxKind.QUALIFIED_NAME_REFERENCE)) { + Optional symbol = context.semanticModel().symbol(expression); + if (symbol.isPresent() && symbol.get().kind().equals(SymbolKind.CONSTANT)) { + return true; + } + } + + // If the value is not constant, check if it's a parameter referring to a constant where the value can be + // determined at compile time + if (expression instanceof SimpleNameReferenceNode simpleNameRef) { + String varName = unescapeIdentifier(simpleNameRef.name().text()); + Optional paramExpression = context.getVarExpression(varName); + return paramExpression.isPresent() && + isMethodCallOnConstantExpr(paramExpression.get(), context); + } + return false; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidWeakCipherAlgorithmsRule.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidWeakCipherAlgorithmsRule.java new file mode 100644 index 00000000..144f1936 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/AvoidWeakCipherAlgorithmsRule.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://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. + */ +package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules; + +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.FunctionContext; + +import java.util.Optional; + +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoAnalyzerUtils.getStringValue; +import static io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.CryptoRule.AVOID_WEAK_CIPHER_ALGORITHMS; + +/** + * Rule to avoid weak cipher algorithms in crypto functions. + * Analyzes the usage of AES in ECB/CBC modes and RSA with PKCS1 padding. + * + * @since 2.9.1 + */ +public class AvoidWeakCipherAlgorithmsRule implements CryptoFunctionRule { + + public static final String ENCRYPT_AES_ECB = "encryptAesEcb"; + public static final String ENCRYPT_AES_CBC = "encryptAesCbc"; + public static final String ENCRYPT_RSA_ECB = "encryptRsaEcb"; + public static final String PADDING_PARAM = "padding"; + public static final String PKCS1_PADDING = "PKCS1"; + + @Override + public void analyze(FunctionContext context) { + String functionName = context.functionName(); + if (functionName.equals(ENCRYPT_AES_CBC) || functionName.equals(ENCRYPT_AES_ECB) + || (functionName.equals(ENCRYPT_RSA_ECB) && isRsbEcbWithPKCS1Padding(context))) { + context.reporter().reportIssue(context.document(), context.functionLocation(), getRuleId()); + } + } + + @Override + public int getRuleId() { + return AVOID_WEAK_CIPHER_ALGORITHMS.getId(); + } + + @Override + public boolean isApplicable(FunctionContext context) { + String functionName = context.functionName(); + return functionName.equals(ENCRYPT_AES_CBC) || functionName.equals(ENCRYPT_AES_ECB) + || functionName.equals(ENCRYPT_RSA_ECB); + } + + private boolean isRsbEcbWithPKCS1Padding(FunctionContext context) { + Optional paddingExprOpt = getStringValue(PADDING_PARAM, context); + // If padding parameter is not provided, it defaults to PKCS1 padding which is considered weak + return paddingExprOpt + .map(s -> s.equals(PKCS1_PADDING)) + .orElse(true); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/CryptoFunctionRule.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/CryptoFunctionRule.java new file mode 100644 index 00000000..2cd65d40 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/crypto/compiler/staticcodeanalyzer/functionrules/CryptoFunctionRule.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://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. + */ +package io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.functionrules; + +import io.ballerina.stdlib.crypto.compiler.staticcodeanalyzer.FunctionContext; + +/** + * Interface for crypto function analysis rules. + * + * @since 2.9.1 + */ +public interface CryptoFunctionRule { + + /** + * Analyze the given crypto function and report issues if any. + * + * @param context Context information required to analyze the function + */ + void analyze(FunctionContext context); + + /** + * Get the unique rule ID for this rule. + * + * @return Unique rule ID + */ + int getRuleId(); + + /** + * Check whether this rule is applicable for the given context. + * + * @param context Context information required to analyze the function + * @return true if the rule is applicable, false otherwise + */ + boolean isApplicable(FunctionContext context); +} diff --git a/compiler-plugin/src/main/resources/rules.json b/compiler-plugin/src/main/resources/rules.json index 6fb391a4..567d9129 100644 --- a/compiler-plugin/src/main/resources/rules.json +++ b/compiler-plugin/src/main/resources/rules.json @@ -13,10 +13,5 @@ "id": 3, "kind": "VULNERABILITY", "description": "Avoid reusing counter mode initialization vectors" - }, - { - "id": 4, - "kind": "VULNERABILITY", - "description": "Secure random number generators should not output predictable values" } ] diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 28830500..4d5ab359 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -4,7 +4,7 @@ _Owners_: @shafreenAnfar @bhashinee _Reviewers_: @shafreenAnfar _Created_: 2022/08/23 _Updated_: 2025/01/20 -_Edition_: Swan Lake +_Edition_: Swan Lake ## Introduction @@ -17,173 +17,184 @@ If you have any feedback or suggestions about the library, start a discussion vi The conforming implementation of the specification is released and included in the distribution. Any deviation from the specification is considered a bug. ## Contents + 1. [Overview](#1-overview) 2. [Hash](#2-hash) - * 2.1. [MD5](#21-md5) - * 2.2. [SHA1](#22-sha1) - * 2.3. [SHA256](#23-sha256) - * 2.4. [SHA384](#24-sha384) - * 2.5. [SHA512](#25-sha512) - * 2.6. [CRC32B](#26-crc32b) - * 2.7. [KECCAK256](#27-keccak256) + - 2.1. [MD5](#21-md5) + - 2.2. [SHA1](#22-sha1) + - 2.3. [SHA256](#23-sha256) + - 2.4. [SHA384](#24-sha384) + - 2.5. [SHA512](#25-sha512) + - 2.6. [CRC32B](#26-crc32b) + - 2.7. [KECCAK256](#27-keccak256) 3. [HMAC](#3-hmac) - * 3.1. [MD5](#31-md5) - * 3.2. [SHA1](#32-sha1) - * 3.3. [SHA256](#33-sha256) - * 3.4. [SHA384](#34-sha384) - * 3.5. [SHA512](#35-sha512) -4. [Decode private/public key](#4-decode-private-public-keys) - * 4.1. [Decode RSA Private key from PKCS12 file](#41-rsa-decode-private-key-from-pkcs12-file) - * 4.2. [Decode RSA Private key using Private key and Password](#42-decode-rsa-private-key-using-private-key-and-password) - * 4.3. [Decode RSA Private key using Private key content and Password](#43-decode-rsa-private-key-using-private-key-content-and-password) - * 4.4. [Decode RSA Public key from PKCS12 file](#44-decode-rsa-public-key-from-pkcs12-file) - * 4.5. [Decode RSA Public key from the certificate file](#45-decode-rsa-public-key-from-the-certificate-file) - * 4.6. [Decode RSA Public key from the certificate content](#46-decode-rsa-public-key-from-the-certificate-content) - * 4.7. [Decode EC Private key from PKCS12 file](#47-decode-ec-private-key-from-pkcs12-file) - * 4.8. [Decode EC Private key using Private key and Password](#48-decode-ec-private-key-using-private-key-and-password) - * 4.9. [Decode EC Public key from PKCS12 file](#49-decode-ec-public-key-from-pkcs12-file) - * 4.10. [Decode EC Public key from the certificate file](#410-decode-ec-public-key-from-the-certificate-file) - * 4.11. [Build RSA Public key from modulus and exponent parameters](#411-build-rsa-public-key-from-modulus-and-exponent-parameters) - * 4.12. [Decode ML-DSA-65 Private key from PKCS12 file](#412-decode-ml-dsa-65-private-key-from-pkcs12-file) - * 4.13. [Decode ML-DSA-65 Private key using Private key and Password](#413-decode-ml-dsa-65-private-key-using-private-key-and-password) - * 4.14. [Decode ML-DSA-65 Public key from PKCS12 file](#414-decode-ml-dsa-65-public-key-from-pkcs12-file) - * 4.15. [Decode ML-DSA-65 Public key from the certificate file](#415-decode-ml-dsa-65-public-key-from-the-certificate-file) - * 4.16. [Decode ML-KEM-768 Private key from PKCS12 file](#416-decode-ml-kem-768-private-key-from-pkcs12-file) - * 4.17. [Decode ML-KEM-768 Private key using Private key and Password](#417-decode-ml-kem-768-private-key-using-private-key-and-password) - * 4.18. [Decode ML-KEM-768 Public key from PKCS12 file](#418-decode-ml-kem-768-public-key-from-pkcs12-file) - * 4.19. [Decode ML-KEM-768 Public key from the certificate file](#419-decode-ml-kem-768-public-key-from-the-certificate-file) -5. [Encrypt-Decrypt](#5-encrypt-decrypt) - * 5.1. [Encryption](#51-encryption) - * 5.1.1. [RSA](#511-rsa) - * 5.1.2. [AES-CBC](#512-aes-cbc) - * 5.1.3. [AES-ECB](#513-aes-ecb) - * 5.1.4. [AES-GCM](#514-aes-gcm) - * 5.1.5. [PGP](#515-pgp) - * 5.2. [Decryption](#52-decryption) - * 5.2.1. [RSA-ECB](#521-rsa-ecb) - * 5.2.2. [AES-CBC](#522-aes-cbc) - * 5.2.3. [AES-ECB](#523-aes-ecb) - * 5.2.4. [AES-GCM](#524-aes-gcm) - * 5.2.5. [PGP](#525-pgp) + - 3.1. [MD5](#31-md5) + - 3.2. [SHA1](#32-sha1) + - 3.3. [SHA256](#33-sha256) + - 3.4. [SHA384](#34-sha384) + - 3.5. [SHA512](#35-sha512) +4. [Decode private/public key](#4-decode-privatepublic-key) + - 4.1. [Decode RSA Private key from PKCS12 file](#41-decode-rsa-private-key-from-pkcs12-file) + - 4.2. [Decode RSA Private key using Private key and Password](#42-decode-rsa-private-key-using-private-key-and-password) + - 4.3. [Decode RSA Private key using Private key content and Password](#43-decode-rsa-private-key-using-private-key-content-and-password) + - 4.4. [Decode RSA Public key from PKCS12 file](#44-decode-rsa-public-key-from-pkcs12-file) + - 4.5. [Decode RSA Public key from the certificate file](#45-decode-rsa-public-key-from-the-certificate-file) + - 4.6. [Decode RSA Public key from the certificate content](#46-decode-rsa-public-key-from-the-certificate-content) + - 4.7. [Decode EC Private key from PKCS12 file](#47-decode-ec-private-key-from-pkcs12-file) + - 4.8. [Decode EC Private key using Private key and Password](#48-decode-ec-private-key-using-private-key-and-password) + - 4.9. [Decode EC Public key from PKCS12 file](#49-decode-ec-public-key-from-pkcs12-file) + - 4.10. [Decode EC Public key from the certificate file](#410-decode-ec-public-key-from-the-certificate-file) + - 4.11. [Build RSA Public key from modulus and exponent parameters](#411-build-rsa-public-key-from-modulus-and-exponent-parameters) + - 4.12. [Decode ML-DSA-65 Private key from PKCS12 file](#412-decode-ml-dsa-65-private-key-from-pkcs12-file) + - 4.13. [Decode ML-DSA-65 Private key using Private key and Password](#413-decode-ml-dsa-65-private-key-using-private-key-and-password) + - 4.14. [Decode ML-DSA-65 Public key from PKCS12 file](#414-decode-ml-dsa-65-public-key-from-pkcs12-file) + - 4.15. [Decode ML-DSA-65 Public key from the certificate file](#415-decode-ml-dsa-65-public-key-from-the-certificate-file) + - 4.16. [Decode ML-KEM-768 Private key from PKCS12 file](#416-decode-ml-kem-768-private-key-from-pkcs12-file) + - 4.17. [Decode ML-KEM-768 Private key using Private key and Password](#417-decode-ml-kem-768-private-key-using-private-key-and-password) + - 4.18. [Decode ML-KEM-768 Public key from PKCS12 file](#418-decode-ml-kem-768-public-key-from-pkcs12-file) + - 4.19. [Decode ML-KEM-768 Public key from the certificate file](#419-decode-ml-kem-768-public-key-from-the-certificate-file) +5. [Encrypt-Decrypt](#5-encrypt-decrypt) + - 5.1. [Encryption](#51-encryption) + - 5.1.1. [RSA](#511-rsa) + - 5.1.2. [AES-CBC](#512-aes-cbc) + - 5.1.3. [AES-ECB](#513-aes-ecb) + - 5.1.4. [AES-GCM](#514-aes-gcm) + - 5.1.5. [PGP](#515-pgp) + - 5.2. [Decryption](#52-decryption) + - 5.2.1. [RSA-ECB](#521-rsa-ecb) + - 5.2.2. [AES-CBC](#522-aes-cbc) + - 5.2.3. [AES-ECB](#523-aes-ecb) + - 5.2.4. [AES-GCM](#524-aes-gcm) + - 5.2.5. [PGP](#525-pgp) 6. [Sign and Verify](#6-sign-and-verify) - * 6.1. [Sign messages](#61-sign-messages) - * 6.1.1. [RSA-MD5](#611-rsa-md5) - * 6.1.2. [RSA-SHA1](#612-rsa-sha1) - * 6.1.3. [RSA-SHA256](#613-rsa-sha256) - * 6.1.4. [RSA-SHA384](#614-rsa-sha384) - * 6.1.5. [RSA-SHA512](#615-rsa-sha512) - * 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) - * 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) - * 6.1.8. [ML-DSA-65](#618-mldsa65) - * 6.2. [Verify signature](#62-verify-signature) - * 6.2.1. [RSA-MD5](#621-rsa-md5) - * 6.2.2. [RSA-SHA1](#622-rsa-sha1) - * 6.2.3. [RSA-SHA256](#623-rsa-sha256) - * 6.2.4. [RSA-SHA384](#624-rsa-sha384) - * 6.2.5. [RSA-SHA512](#625-rsa-sha512) - * 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) - * 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) - * 6.2.8. [ML-DSA-65](#618-mldsa65) + - 6.1. [Sign messages](#61-sign-messages) + - 6.1.1. [RSA-MD5](#611-rsa-md5) + - 6.1.2. [RSA-SHA1](#612-rsa-sha1) + - 6.1.3. [RSA-SHA256](#613-rsa-sha256) + - 6.1.4. [RSA-SHA384](#614-rsa-sha384) + - 6.1.5. [RSA-SHA512](#615-rsa-sha512) + - 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) + - 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) + - 6.1.8. [ML-DSA-65](#618-ml-dsa-65) + - 6.2. [Verify signature](#62-verify-signature) + - 6.2.1. [RSA-MD5](#621-rsa-md5) + - 6.2.2. [RSA-SHA1](#622-rsa-sha1) + - 6.2.3. [RSA-SHA256](#623-rsa-sha256) + - 6.2.4. [RSA-SHA384](#624-rsa-sha384) + - 6.2.5. [RSA-SHA512](#625-rsa-sha512) + - 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) + - 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) + - 6.2.8. [ML-DSA-65](#628-ml-dsa-65) 7. [Key Derivation Function (KDF)](#7-key-derivation-function-kdf) - * 7.1. [HKDF-SHA256](#71-hkdf-sha256) + - 7.1. [HKDF-SHA256](#71-hkdf-sha256) 8. [Key Exchange Mechanism (KEM)](#8-key-exchange-mechanism-kem) - * 8.1 [Encapsulation](#81-encapsulation) - * 8.1.1 [RSA-KEM](#811-rsa-kem) - * 8.1.2 [ML-KEM-768](#812-ml-kem-768) - * 8.1.3 [RSA-KEM-ML-KEM-768](#813-rsa-kem-ml-kem-768) - * 8.2 [Decapsulation](#81-encapsulation) - * 8.2.1 [RSA-KEM](#821-rsa-kem) - * 8.2.2 [ML-KEM-768](#822-ml-kem-768) - * 8.2.3 [RSA-KEM-ML-KEM-768](#823-rsa-kem-ml-kem-768) + - 8.1 [Encapsulation](#81-encapsulation) + - 8.1.1 [RSA-KEM](#811-rsa-kem) + - 8.1.2 [ML-KEM-768](#812-ml-kem-768) + - 8.1.3 [RSA-KEM-ML-KEM-768](#813-rsa-kem-ml-kem-768) + - 8.2 [Decapsulation](#81-encapsulation) + - 8.2.1 [RSA-KEM](#821-rsa-kem) + - 8.2.2 [ML-KEM-768](#822-ml-kem-768) + - 8.2.3 [RSA-KEM-ML-KEM-768](#823-rsa-kem-ml-kem-768) 9. [Hybrid Public Key Encryption (HPKE)](#9-hybrid-public-key-encryption-hpke) - * 9.1 [Encrypt](#91-encrypt) - * 9.1.1 [ML-KEM-768-HPKE](#911-ml-kem-768-hpke) - * 9.1.2 [RSA-KEM-ML-KEM-768-HPKE](#912-rsa-kem-ml-kem-768-hpke) - * 9.2 [Decrypt](#92-decrypt) - * 9.2.1 [ML-KEM-768-HPKE](#921-ml-kem-768-hpke) - * 9.2.2 [RSA-KEM-ML-KEM-768-HPKE](#922-rsa-kem-ml-kem-768-hpke) + - 9.1 [Encrypt](#91-encrypt) + - 9.1.1 [ML-KEM-768-HPKE](#911-ml-kem-768-hpke) + - 9.1.2 [RSA-KEM-ML-KEM-768-HPKE](#912-rsa-kem-ml-kem-768-hpke) + - 9.2 [Decrypt](#92-decrypt) + - 9.2.1 [ML-KEM-768-HPKE](#921-ml-kem-768-hpke) + - 9.2.2 [RSA-KEM-ML-KEM-768-HPKE](#922-rsa-kem-ml-kem-768-hpke) 10. [Password hashing](#10-password-hashing) - * 10.1 [BCrypt](#101-bcrypt) - * 10.2 [Argon2](#102-argon2) - * 10.3 [PBKDF2](#103-pbkdf2) + - 10.1 [BCrypt](#101-bcrypt) + - 10.2 [Argon2](#102-argon2) + - 10.3 [PBKDF2](#103-pbkdf2) +11. [Static Code Rules](#11-static-code-rules) + - 11.1 [Avoid using insecure cipher modes or padding schemes](#111-avoid-using-insecure-cipher-modes-or-padding-schemes) + - 11.2 [Avoid using fast hashing algorithms](#112-avoid-using-fast-hashing-algorithms) + - 11.3 [Avoid reusing counter mode initialization vectors](#113-avoid-reusing-counter-mode-initialization-vectors) - -## 1. [Overview](#1-overview) +## 1. Overview The Ballerina `crypto` library facilitates APIs to do operations like hashing, HMAC generation, checksum generation, encryption, decryption, signing data digitally, verifying digitally signed data, etc., with different cryptographic algorithms. -## 2. [Hash](#2-hash) +## 2. Hash The `crypto` library supports generating hashes with 5 different hash algorithms MD5, SHA1, SHA256, SHA384, and SHA512. Also, it supports generating the CRC32B checksum. -### 2.1. [MD5](#21-md) +### 2.1. MD5 This API can be used to create the MD5 hash of the given data. + ```ballerina string dataString = "Hello Ballerina"; byte[] data = dataString.toBytes(); byte[] hash = crypto:hashMd5(data); ``` -### 2.2. [SHA1](#22-sha1) +### 2.2. SHA1 This API can be used to create the SHA-1 hash of the given data. + ```ballerina string dataString = "Hello Ballerina"; byte[] data = dataString.toBytes(); byte[] hash = crypto:hashSha1(data); ``` -### 2.3. [SHA256](#23-sha256) +### 2.3. SHA256 This API can be used to create the SHA-256 hash of the given data. + ```ballerina string dataString = "Hello Ballerina"; byte[] data = dataString.toBytes(); byte[] hash = crypto:hashSha256(data); ``` -### 2.4. [SHA384](#24-sha384) +### 2.4. SHA384 This API can be used to create the SHA-384 hash of the given data. + ```ballerina string dataString = "Hello Ballerina"; byte[] data = dataString.toBytes(); byte[] hash = crypto:hashSha384(data); ``` -### 2.5. [SHA512](#25-sha512) +### 2.5. SHA512 This API can be used to create the SHA-512 hash of the given data. + ```ballerina string dataString = "Hello Ballerina"; byte[] data = dataString.toBytes(); byte[] hash = crypto:hashSha512(data); ``` -### 2.6. [CRC32B](#26-crc32b) +### 2.6. CRC32B This API can be used to create the Hex-encoded CRC32B value of the given data. + ```ballerina string stringData = "Hello Ballerina"; byte[] data = stringData.toBytes(); string checksum = crypto:crc32b(data); ``` -### 2.7. [KECCAK256](#27-keccak256) +### 2.7. KECCAK256 This API can be used to create the Hex-encoded KECCAK-256 value of the given data. + ```ballerina string stringData = "Hello Ballerina"; byte[] data = stringData.toBytes(); string checksum = crypto:hashKeccak256(data); ``` -## 3. [HMAC](#3-hmac) +## 3. HMAC The `crypto` library supports generating HMAC with 5 different hash algorithms: MD5, SHA1, SHA256, SHA384, and SHA512. -### 3.1. [MD5](#31-md5) +### 3.1. MD5 This API can be used to create HMAC using the MD5 hash function of the given data. @@ -195,7 +206,7 @@ byte[] key = secret.toBytes(); byte[] hmac = check crypto:hmacMd5(data, key); ``` -### 3.2. [SHA1](#32-sha1) +### 3.2. SHA1 This API can be used to create HMAC using the SHA-1 hash function of the given data. @@ -207,7 +218,7 @@ byte[] key = secret.toBytes(); byte[] hmac = crypto:hmacSha1(data, key); ``` -### 3.3. [SHA256](#33-sha256) +### 3.3. SHA256 This API can be used to create HMAC using the SHA-256 hash function of the given data. @@ -219,7 +230,7 @@ byte[] key = secret.toBytes(); byte[] hmac = check crypto:hmacSha256(data, key); ``` -### 3.4. [SHA384](#34-sha384) +### 3.4. SHA384 This API can be used to create HMAC using the SHA-384 hash function of the given data. @@ -231,7 +242,7 @@ byte[] key = secret.toBytes(); byte[] hmac = check crypto:hmacSha384(data, key); ``` -### 3.5. [SHA512](#35-sha512) +### 3.5. SHA512 This API can be used to create HMAC using the SHA-512 hash function of the given data. @@ -243,11 +254,11 @@ byte[] key = secret.toBytes(); byte[] hmac = check crypto:hmacSha512(data, key); ``` -## 4. [Decode private/public key](#4-decode-private-public-keys) +## 4. Decode private/public key The `crypto` library supports decoding the RSA private key from a `.p12` file and a key file in the `PEM` format. Also, it supports decoding a public key from a `.p12` file and a certificate file in the `X509` format. Additionally, this supports building an RSA public key with the modulus and exponent parameters. -### 4.1. [Decode RSA Private key from PKCS12 file](#41-rsa-decode-private-key-from-pkcs12-file) +### 4.1. Decode RSA Private key from PKCS12 file This API can be used to decode the RSA private key from the given PKCS#12 file. @@ -259,7 +270,7 @@ crypto:KeyStore keyStore = { crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); ``` -### 4.2. [Decode RSA Private key using Private key and Password](#42-decode-rsa-private-key-using-private-key-and-password) +### 4.2. Decode RSA Private key using Private key and Password This API can be used to decode the RSA private key from the given private key and private key password. @@ -268,7 +279,7 @@ string keyFile = "/path/to/private.key"; crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyFile(keyFile, "keyPassword"); ``` -4.3. [Decode RSA Private key using Private key content and Password](#43-decode-rsa-private-key-using-private-key-content-and-password) +## 4.3. Decode RSA Private key using Private key content and Password This API can be used to decode the RSA public key from the given public certificate content as a byte array. @@ -277,7 +288,7 @@ byte[] keyContent = [45,45,45,45,45,66,69,71,73,78,...]; crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromContent(keyContent); ``` -### 4.4. [Decode RSA Public key from PKCS12 file](#44-decode-rsa-public-key-from-pkcs12-file) +### 4.4. Decode RSA Public key from PKCS12 file This API can be used to decode the RSA public key from the given PKCS#12 archive file. @@ -289,7 +300,7 @@ crypto:TrustStore trustStore = { crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(trustStore, "keyAlias"); ``` -### 4.5. [Decode RSA Public key from the certificate file](#45-decode-rsa-public-key-from-the-certificate-file) +### 4.5. Decode RSA Public key from the certificate file This API can be used to decode the RSA public key from the given public certificate file. @@ -298,7 +309,7 @@ string certFile = "/path/to/public.cert"; crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromCertFile(certFile); ``` -### 4.6. [Decode RSA Public key from the certificate content](#46-decode-rsa-public-key-from-the-certificate-content) +### 4.6. Decode RSA Public key from the certificate content This API can be used to decode the RSA public key from the given public certificate content as a byte array. @@ -307,7 +318,7 @@ byte[] certFileContent = [45,45,45,45,45,66,69,71,73,78,...]; crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromContent(certFileContent); ``` -### 4.7. [Decode EC Private key from PKCS12 file](#47-decode-ec-private-key-from-pkcs12-file) +### 4.7. Decode EC Private key from PKCS12 file This API can be used to decode the EC private key from the given PKCS#12 file. @@ -319,7 +330,7 @@ crypto:KeyStore keyStore = { crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); ``` -### 4.8. [Decode EC Private key using Private key and Password](#48-decode-ec-private-key-using-private-key-and-password) +### 4.8. Decode EC Private key using Private key and Password This API can be used to decode the EC private key from the given private key and private key password. @@ -328,7 +339,7 @@ string keyFile = "/path/to/private.key"; crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyFile(keyFile, "keyPassword"); ``` -### 4.9. [Decode EC Public key from PKCS12 file](#49-decode-ec-public-key-from-pkcs12-file) +### 4.9. Decode EC Public key from PKCS12 file This API can be used to decode the RSA public key from the given PKCS#12 archive file. @@ -340,7 +351,7 @@ crypto:TrustStore trustStore = { crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(trustStore, "keyAlias"); ``` -### 4.10. [Decode EC Public key from the certificate file](#410-decode-ec-public-key-from-the-certificate-file) +### 4.10. Decode EC Public key from the certificate file This API can be used to decode the EC public key from the given public certificate file. @@ -349,7 +360,7 @@ string certFile = "/path/to/public.cert"; crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromCertFile(certFile); ``` -### 4.11. [Build RSA Public key from modulus and exponent parameters](#411-build-rsa-public-key-from-modulus-and-exponent-parameters) +### 4.11. Build RSA Public key from modulus and exponent parameters This API can be used to build the RSA public key from the given modulus and exponent parameters. @@ -362,7 +373,7 @@ string exponent = "AQAB"; crypto:PublicKey publicKey = check crypto:buildRsaPublicKey(modulus, exponent); ``` -### 4.12. [Decode ML-DSA-65 Private key from PKCS12 file](#412-decode-ml-dsa-65-private-key-from-pkcs12-file) +### 4.12. Decode ML-DSA-65 Private key from PKCS12 file This API can be used to decode the ML-DSA-65 private key from the given PKCS#12 file. @@ -374,7 +385,7 @@ crypto:KeyStore keyStore = { crypto:PrivateKey privateKey = check crypto:decodeMlDsa65PrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); ``` -### 4.13. [Decode ML-DSA-65 Private key using Private key and Password](#413-decode-ml-dsa-65-private-key-using-private-key-and-password) +### 4.13. Decode ML-DSA-65 Private key using Private key and Password This API can be used to decode the ML-DSA-65 private key from the given private key and private key password. @@ -383,7 +394,7 @@ string keyFile = "/path/to/private.key"; crypto:PrivateKey privateKey = check crypto:decodeMlDsa65PrivateKeyFromKeyFile(keyFile, "keyPassword"); ``` -### 4.14. [Decode ML-DSA-65 Public key from PKCS12 file](#414-decode-ml-dsa-65-public-key-from-pkcs12-file) +### 4.14. Decode ML-DSA-65 Public key from PKCS12 file This API can be used to decode the ML-DSA-65 public key from the given PKCS#12 archive file. @@ -395,7 +406,7 @@ crypto:TrustStore trustStore = { crypto:PublicKey publicKey = check crypto:decodeMlDsa65PublicKeyFromTrustStore(trustStore, "keyAlias"); ``` -### 4.15. [Decode ML-DSA-65 Public key from the certificate file](#415-decode-ml-dsa-65-public-key-from-the-certificate-file) +### 4.15. Decode ML-DSA-65 Public key from the certificate file This API can be used to decode the ML-DSA-65 public key from the given public certificate file. @@ -404,7 +415,7 @@ string certFile = "/path/to/public.cert"; crypto:PublicKey publicKey = check crypto:decodeMlDsa65PublicKeyFromCertFile(certFile); ``` -### 4.16. [Decode ML-KEM-768 Private key from PKCS12 file](#416-decode-ml-kem-768-private-key-from-pkcs12-file) +### 4.16. Decode ML-KEM-768 Private key from PKCS12 file This API can be used to decode the ML-KEM-768 private key from the given PKCS#12 file. @@ -416,7 +427,7 @@ crypto:KeyStore keyStore = { crypto:PrivateKey privateKey = check crypto:decodeMlKem768PrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); ``` -### 4.17. [Decode ML-KEM-768 Private key using Private key and Password](#417-decode-ml-kem-768-private-key-using-private-key-and-password) +### 4.17. Decode ML-KEM-768 Private key using Private key and Password This API can be used to decode the ML-KEM-768 private key from the given private key and private key password. @@ -425,7 +436,7 @@ string keyFile = "/path/to/private.key"; crypto:PrivateKey privateKey = check crypto:decodeMlKem768PrivateKeyFromKeyFile(keyFile, "keyPassword"); ``` -### 4.18. [Decode ML-KEM-768 Public key from PKCS12 file](#418-decode-ml-kem-768-public-key-from-pkcs12-file) +### 4.18. Decode ML-KEM-768 Public key from PKCS12 file This API can be used to decode the ML-KEM-768 public key from the given PKCS#12 archive file. @@ -437,7 +448,7 @@ crypto:TrustStore trustStore = { crypto:PublicKey publicKey = check crypto:decodeMlKem768PublicKeyFromTrustStore(trustStore, "keyAlias"); ``` -### 4.19. [Decode ML-KEM-768 Public key from the certificate file](#419-decode-ml-kem-768-public-key-from-the-certificate-file) +### 4.19. Decode ML-KEM-768 Public key from the certificate file This API can be used to decode the ML-KEM-768 public key from the given public certificate file. @@ -446,13 +457,13 @@ string certFile = "/path/to/public.cert"; crypto:PublicKey publicKey = check crypto:decodeMlKem768PublicKeyFromCertFile(certFile); ``` -## 5. [Encrypt-Decrypt](#5-encrypt-decrypt) +## 5. Encrypt-Decrypt The `crypto` library supports both symmetric key encryption/decryption and asymmetric key encryption/decryption. The RSA algorithm can be used for asymmetric-key encryption/decryption with the use of private and public keys. The AES algorithm can be used for symmetric-key encryption/decryption with the use of a shared key. -### 5.1. [Encryption](#51-encryption) +### 5.1. Encryption -#### 5.1.1. [RSA](#511-rsa) +#### 5.1.1. RSA This API can be used to create the RSA-encrypted value of the given data. @@ -467,7 +478,7 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt byte[] cipherText = check crypto:encryptRsaEcb(data, publicKey); ``` -#### 5.1.2. [AES-CBC](#512-aes-cbc) +#### 5.1.2. AES-CBC This API can be used to create the AES-CBC-encrypted value for the given data. @@ -485,7 +496,7 @@ foreach int i in 0...15 { byte[] cipherText = check crypto:encryptAesCbc(data, key, initialVector); ``` -#### 5.1.3. [AES-ECB](#513-aes-ecb) +#### 5.1.3. AES-ECB This API can be used to create the AES-ECB-encrypted value for the given data. @@ -499,7 +510,7 @@ foreach int i in 0...15 { byte[] cipherText = check crypto:encryptAesEcb(data, key); ``` -#### 5.1.4. [AES-GCM](#514-aes-gcm) +#### 5.1.4. AES-GCM This API can be used to create the AES-GCM-encrypted value for the given data. @@ -517,7 +528,7 @@ foreach int i in 0...15 { byte[] cipherText = check crypto:encryptAesGcm(data, key, initialVector); ``` -#### 5.1.5. [PGP](#515-pgp) +#### 5.1.5. PGP This API can be used to create the PGP-encrypted value for the given data. @@ -554,9 +565,9 @@ stream inputStream = check io:fileReadBlocksAsStream("input.txt" stream|crypto:Error encryptedStream = crypto:encryptStreamAsPgp(inputStream, "public_key.asc"); ``` -### 5.2. [Decryption](#52-decryption) +### 5.2. Decryption -#### 5.2.1. [RSA-ECB](#521-rsa-ecb) +#### 5.2.1. RSA-ECB This API can be used to create the RSA-decrypted value for the given RSA-encrypted data. @@ -573,7 +584,7 @@ byte[] cipherText = check crypto:encryptRsaEcb(data, publicKey); byte[] plainText = check crypto:decryptRsaEcb(cipherText, privateKey); ``` -#### 5.2.2. [AES-CBC](#522-aes-cbc) +#### 5.2.2. AES-CBC This API can be used to create the AES-CBC-decrypted value for the given AES-CBC-encrypted data. @@ -592,7 +603,7 @@ byte[] cipherText = check crypto:encryptAesCbc(data, key, initialVector); byte[] plainText = check crypto:decryptAesCbc(cipherText, key, initialVector); ``` -#### 5.2.3. [AES-ECB](#523-aes-ecb) +#### 5.2.3. AES-ECB This API can be used to create the AES-ECB-decrypted value for the given AES-ECB-encrypted data. @@ -607,7 +618,7 @@ byte[] cipherText = check crypto:encryptAesEcb(data, key); byte[] plainText = check crypto:decryptAesEcb(cipherText, key); ``` -#### 5.2.4. [AES-GCM](#524-aes-gcm) +#### 5.2.4. AES-GCM This API can be used to create the AES-GCM-decrypted value for the given AES-GCM-encrypted data. @@ -626,7 +637,7 @@ byte[] cipherText = check crypto:encryptAesGcm(data, key, initialVector); byte[] plainText = check crypto:decryptAesGcm(cipherText, key, initialVector); ``` -#### 5.2.5. [PGP](#525-pgp) +#### 5.2.5. PGP This API can be used to create the PGP-decrypted value for the given PGP-encrypted data. @@ -649,13 +660,13 @@ stream inputStream = check io:fileReadBlocksAsStream("pgb_encryp stream|crypto:Error decryptedStream = crypto:decryptStreamFromPgp(inputStream, "private_key.asc", passphrase); ``` -## 6. [Sign and Verify](#6-sign-and-verify) +## 6. Sign and Verify The `crypto` library supports signing data using the RSA private key and verification of the signature using the RSA public key. This supports MD5, SHA1, SHA256, SHA384, and SHA512 digesting algorithms, and ML-DSA-65 post-quantum signature algorithm as well. -### 6.1. [Sign messages](#51-sign-messages) +### 6.1. Sign messages -#### 6.1.1. [RSA-MD5](#611-rsa-md5) +#### 6.1.1. RSA-MD5 This API can be used to create the RSA-MD5 based signature value for the given data. @@ -670,7 +681,7 @@ crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyS byte[] signature = check crypto:signRsaMd5(data, privateKey); ``` -#### 6.1.2. [RSA-SHA1](#612-rsa-sha1) +#### 6.1.2. RSA-SHA1 This API can be used to create the RSA-SHA1 based signature value for the given data. @@ -685,7 +696,7 @@ crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyS byte[] signature = check crypto:signRsaSha1(data, privateKey); ``` -#### 6.1.3. [RSA-SHA256](#613-rsa-sha256) +#### 6.1.3. RSA-SHA256 This API can be used to create the RSA-SHA256 based signature value for the given data. @@ -700,7 +711,7 @@ crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyS byte[] signature = check crypto:signRsaSha256(data, privateKey); ``` -#### 6.1.4. [RSA-SHA384](#614-rsa-sha384) +#### 6.1.4. RSA-SHA384 This API can be used to create the RSA-SHA384 based signature value for the given data. @@ -715,7 +726,7 @@ crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyS byte[] signature = check crypto:signRsaSha384(data, privateKey); ``` -#### 6.1.5. [RSA-SHA512](#615-rsa-sha512) +#### 6.1.5. RSA-SHA512 This API can be used to create the RSA-SHA512 based signature value for the given data. @@ -730,7 +741,7 @@ crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyS byte[] signature = check crypto:signRsaSha512(data, privateKey); ``` -#### 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) +#### 6.1.6. SHA384withECDSA This API can be used to create the SHA384withECDSA based signature value for the given data. @@ -745,7 +756,7 @@ crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keySt byte[] signature = check crypto:signSha384withEcdsa(data, privateKey); ``` -#### 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) +#### 6.1.7. SHA256withECDSA This API can be used to create the SHA256withECDSA based signature value for the given data. @@ -760,7 +771,7 @@ crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keySt byte[] signature = check crypto:signSha256withEcdsa(data, privateKey); ``` -#### 6.1.8. [ML-DSA-65](#618-mldsa65) +#### 6.1.8. ML-DSA-65 This API can be used to create the ML-DSA-65 based signature value for the given data. @@ -775,9 +786,9 @@ crypto:PrivateKey privateKey = check crypto:decodeMlDsa65PrivateKeyFromKeyStore( byte[] signature = check crypto:signMlDsa65(data, privateKey); ``` -### 6.2. [Verify signature](#62-verify-signature) +### 6.2. Verify signature -#### 6.2.1. [RSA-MD5](#621-rsa-md5) +#### 6.2.1. RSA-MD5 This API can be used to verify the RSA-MD5 based signature. @@ -794,7 +805,7 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt boolean validity = check crypto:verifyRsaMd5Signature(data, signature, publicKey); ``` -#### 6.2.2. [RSA-SHA1](#622-rsa-sha1) +#### 6.2.2. RSA-SHA1 This API can be used to verify the RSA-SHA1 based signature. @@ -811,7 +822,7 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt boolean validity = check crypto:verifyRsaSha1Signature(data, signature, publicKey); ``` -#### 6.2.3. [RSA-SHA256](#623-rsa-sha256) +#### 6.2.3. RSA-SHA256 This API can be used to verify the RSA-SHA256 based signature. @@ -828,7 +839,7 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt boolean validity = check crypto:verifyRsaSha256Signature(data, signature, publicKey); ``` -#### 6.2.4. [RSA-SHA384](#624-rsa-sha384) +#### 6.2.4. RSA-SHA384 This API can be used to verify the RSA-SHA384 based signature. @@ -845,7 +856,7 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt boolean validity = check crypto:verifyRsaSha384Signature(data, signature, publicKey); ``` -#### 6.2.5. [RSA-SHA512](#625-rsa-sha512) +#### 6.2.5. RSA-SHA512 This API can be used to verify the RSA-SHA512 based signature. @@ -862,7 +873,7 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt boolean validity = check crypto:verifyRsaSha512Signature(data, signature, publicKey); ``` -#### 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) +#### 6.2.6. SHA384withECDSA This API can be used to verify the SHA384withECDSA based signature. @@ -879,7 +890,7 @@ crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(keySto boolean validity = check crypto:verifySha384withEcdsaSignature(data, signature, publicKey); ``` -#### 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) +#### 6.2.7. SHA256withECDSA This API can be used to verify the SHA256withECDSA based signature. @@ -896,7 +907,7 @@ crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(keySto boolean validity = check crypto:verifySha256withEcdsaSignature(data, signature, publicKey); ``` -#### 6.2.8. [ML-DSA-65](#628-mldsa65) +#### 6.2.8. ML-DSA-65 This API can be used to verify the ML-DSA-65 based signature. @@ -913,12 +924,11 @@ crypto:PublicKey publicKey = check crypto:decodeMlDsa65PublicKeyFromTrustStore(k boolean validity = check crypto:verifyMlDsa65Signature(data, signature, publicKey); ``` - -## 7. [Key Derivation Function (KDF)](#7-key-derivation-function-kdf) +## 7. Key Derivation Function (KDF) The `crypto` module supports HMAC-based Key Derivation Function (HKDF). HKDF is a key derivation function that uses a Hash-based Message Authentication Code (HMAC) to derive keys. -### 7.1. [HKDF-SHA256](#71-hkdf-sha256) +### 7.1. HKDF-SHA256 This API can be used to create HKDF using the SHA256 hash function of the given data. @@ -928,15 +938,15 @@ byte[] key = secret.toBytes(); byte[] hash = crypto:hkdfSha256(key, 32); ``` -## 8. [Key Exchange Mechanism (KEM)](#8-key-exchange-mechanism-kem) +## 8. Key Exchange Mechanism (KEM) The `crypto` module supports Key Exchange Mechanisms (KEM). It includes RSA-KEM and post-quantum ML-KEM-768 for both encapsulation and decapsulation. -### 8.1. [Encapsulation](#81-encapsulation) - -#### 8.1.1. [RSA-KEM](#811-rsa-kem) +### 8.1. Encapsulation -This API can be used to create shared secret and its encapsulation using RSA-KEM function. +#### 8.1.1. RSA-KEM + +This API can be used to create shared secret and its encapsulation using RSA-KEM function. ```ballerina crypto:KeyStore keyStore = { @@ -946,10 +956,10 @@ crypto:KeyStore keyStore = { crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keyStore, "keyAlias"); crypto:EncapsulationResult encapsulationResult = check crypto:encapsulateRsaKem(publicKey); ``` - -#### 8.1.2. [ML-KEM-768](#812-ml-kem-768) -This API can be used to create shared secret and its encapsulation using ML-KEM-768 function. +#### 8.1.2. ML-KEM-768 + +This API can be used to create shared secret and its encapsulation using ML-KEM-768 function. ```ballerina crypto:KeyStore keyStore = { @@ -959,10 +969,10 @@ crypto:KeyStore keyStore = { crypto:PublicKey publicKey = check crypto:decodeMlKem768PublicKeyFromTrustStore(keyStore, "keyAlias"); crypto:EncapsulationResult encapsulationResult = check crypto:encapsulateMlKem768(publicKey); ``` - -#### 8.1.3. [RSA-KEM-ML-KEM-768](#813-rsa-kem-ml-kem-768) -This API can be used to create shared secret and its encapsulation using RSA-KEM-ML-KEM-768 function. +#### 8.1.3. RSA-KEM-ML-KEM-768 + +This API can be used to create shared secret and its encapsulation using RSA-KEM-ML-KEM-768 function. ```ballerina crypto:KeyStore mlkemKeyStore = { @@ -982,11 +992,11 @@ crypto:PrivateKey rsaPrivateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(r byte[] sharedSecret = check crypto:decapsulateRsaKemMlKem768(encapsulatedSecret, rsaPrivateKey, mlkemPrivateKey); ``` -### 8.2. [Decapsulation](#81-encapsulation) - -#### 8.2.1. [RSA-KEM](#821-rsa-kem) +### 8.2. Decapsulation + +#### 8.2.1. RSA-KEM -This API can be used to decapsulate shared secret using RSA-KEM function of the given data. +This API can be used to decapsulate shared secret using RSA-KEM function of the given data. ```ballerina crypto:KeyStore keyStore = { @@ -999,10 +1009,10 @@ byte[] encapsulatedSecret = encapsulationResult.encapsulatedSecret; crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyStorePassword"); byte[] sharedSecret = check crypto:decapsulateRsaKem(encapsulatedSecret, privateKey); ``` - -#### 8.2.2. [ML-KEM-768](#822-ml-kem-768) -This API can be used to decapsulate shared secret using ML-KEM-768 function of the given data. +#### 8.2.2. ML-KEM-768 + +This API can be used to decapsulate shared secret using ML-KEM-768 function of the given data. ```ballerina crypto:KeyStore keyStore = { @@ -1015,10 +1025,10 @@ byte[] encapsulatedSecret = encapsulationResult.encapsulatedSecret; crypto:PrivateKey privateKey = check crypto:decodeMlKem768PrivateKeyFromKeyStore(keyStore, "keyAlias", "keyStorePassword"); byte[] sharedSecret = check crypto:decapsulateMlKem768(encapsulatedSecret, privateKey); ``` - -#### 8.2.3. [RSA-KEM-ML-KEM-768](#823-rsa-kem-ml-kem-768) -This API can be used to decapsulate shared secret using RSA-KEM-ML-KEM-768 function of the given data. +#### 8.2.3. RSA-KEM-ML-KEM-768 + +This API can be used to decapsulate shared secret using RSA-KEM-ML-KEM-768 function of the given data. ```ballerina crypto:KeyStore mlkemKeyStore = { @@ -1038,13 +1048,13 @@ crypto:PrivateKey rsaPrivateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(r byte[] sharedSecret = check crypto:decapsulateRsaKemMlKem768(encapsulatedSecret, rsaPrivateKey, mlkemPrivateKey); ``` -## 9. [Hybrid Public Key Encryption (HPKE)](#9-hybrid-public-key-encryption-hpke) +## 9. Hybrid Public Key Encryption (HPKE) The `crypto` module supports Hybrid Public Key Encryption (HPKE). It supports post-quantum ML-KEM-768-HPKE and RSA-KEM-ML-KEM-768-HPKE for encryption and decryption. -### 9.1. [Encrypt](#91-encrypt) - -#### 9.1.1. [ML-KEM-768-HPKE](#911-ml-kem-768-hpke) +### 9.1. Encrypt + +#### 9.1.1. ML-KEM-768-HPKE This API can be used to create the ML-KEM-768-hybrid-encrypted value of the given data. @@ -1058,8 +1068,8 @@ crypto:KeyStore keyStore = { crypto:PublicKey publicKey = check crypto:decodeMlKem768PublicKeyFromTrustStore(keyStore, "keyAlias"); crypto:HybridEncryptionResult encryptionResult = check crypto:encryptMlKem768Hpke(data, publicKey); ``` - -#### 9.1.2. [RSA-KEM-ML-KEM-768-HPKE](#912-rsa-kem-ml-kem-768-hpke) + +#### 9.1.2. RSA-KEM-ML-KEM-768-HPKE This API can be used to create the RSA-KEM-ML-KEM-768-hybrid-encrypted value of the given data. @@ -1079,9 +1089,9 @@ crypto:PublicKey rsaPublicKey = check crypto:decodeRsaPublicKeyFromTrustStore(rs crypto:HybridEncryptionResult encryptionResult = check crypto:encryptRsaKemMlKem768Hpke(data, rsaPublicKey, mlkemPublicKey); ``` -### 9.2. [Decrypt](#92-decrypt) - -#### 9.2.1. [ML-KEM-768-HPKE](#921-ml-kem-768-hpke) +### 9.2. Decrypt + +#### 9.2.1. ML-KEM-768-HPKE This API can be used to create the ML-KEM-768-hybrid-decrypted value of the given data. @@ -1099,8 +1109,8 @@ byte[] encapsulatedKey = encryptionResult.encapsulatedSecret; crypto:PrivateKey privateKey = check crypto:decodeMlKem768PrivateKeyFromKeyStore(keyStore, "keyAlias", "keyStorePassword"); byte[] decryptedData = check crypto:decryptMlKem768Hpke(cipherText, encapsulatedKey, privateKey); ``` - -#### 9.2.2. [RSA-KEM-ML-KEM-768-HPKE](#922-rsa-kem-ml-kem-768-hpke) + +#### 9.2.2. RSA-KEM-ML-KEM-768-HPKE This API can be used to create the RSA-KEM-ML-KEM-768-hybrid-decrypted value of the given data. @@ -1125,11 +1135,11 @@ crypto:PrivateKey rsaPrivateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(r byte[] decryptedData = check crypto:decryptRsaKemMlKem768Hpke(cipherText, encapsulatedKey, rsaPrivateKey, mlkemPrivateKey); ``` -## 10. [Password Hashing](#10-password-hashing) +## 10. Password Hashing The `crypto` module provides password hashing using BCrypt and Argon2id algorithms for secure password storage. -### 10.1 [BCrypt](#101-bcrypt) +### 10.1 BCrypt Implements the BCrypt password hashing algorithm based on the Blowfish cipher. @@ -1138,6 +1148,7 @@ public isolated function hashBcrypt(string password, int workFactor = 12) return ``` Parameters: + - `password`: The plain text password to hash - `workFactor`: Computational complexity factor (4-31, default: 12) @@ -1146,6 +1157,7 @@ public isolated function verifyBcrypt(string password, string hashedPassword) re ``` Example: + ```ballerina string password = "your-password"; // Hash with default work factor (12) @@ -1155,7 +1167,7 @@ string hashedPassword2 = check crypto:hashBcrypt(password, 14); boolean isValid = check crypto:verifyBcrypt(password, hashedPassword1); ``` -### 10.2 [Argon2](#102-argon2) +### 10.2 Argon2 Implements the Argon2id variant of the Argon2 password hashing algorithm, optimized for both high memory usage and GPU resistance. @@ -1165,6 +1177,7 @@ public isolated function hashArgon2(string password, int iterations = 3, ``` Parameters: + - `password`: The plain text password to hash - `iterations`: Number of iterations (default: 3) - `memory`: Memory usage in KB (minimum: 8192, default: 65536) @@ -1177,6 +1190,7 @@ public isolated function verifyArgon2(string password, string hashedPassword) re ``` Example: + ```ballerina string password = "your-password"; // Hash with default parameters @@ -1186,7 +1200,7 @@ string hashedPassword2 = check crypto:hashArgon2(password, iterations = 4, memor boolean isValid = check crypto:verifyArgon2(password, hashedPassword1); ``` -### 10.3 [PBKDF2](#103-pbkdf2) +### 10.3 PBKDF2 Implements the PBKDF2 (Password-Based Key Derivation Function 2) algorithm for password hashing. @@ -1232,4 +1246,349 @@ string password = "mySecurePassword123"; string hashedPassword = "$pbkdf2-sha256$i=10000$salt$hash"; // Verify the hashed password boolean isValid = check crypto:verifyPbkdf2(password, hashedPassword); -``` \ No newline at end of file +``` + +## 11. Static Code Rules + +The following static code rules are applied to the Crypto module. + +| Id | Kind | Description | +|--------------------|---------------|-------------------------------------------------------------------------------------------------------------------| +| ballerina/crypto:1 | VULNERABILITY | [Avoid using insecure cipher modes or padding schemes](#111-avoid-using-insecure-cipher-modes-or-padding-schemes) | +| ballerina/crypto:2 | VULNERABILITY | [Avoid using fast hashing algorithms](#112-avoid-using-fast-hashing-algorithms) | +| ballerina/crypto:3 | VULNERABILITY | [Avoid reusing counter mode initialization vectors](#113-avoid-reusing-counter-mode-initialization-vectors) | + +### 11.1 Avoid using insecure cipher modes or padding schemes + +Using weak or outdated encryption modes and padding schemes can compromise the security of encrypted data, even when strong algorithms are used. + +## 11.1.1. Why this is an issue? + +Encryption algorithms are essential for protecting sensitive information and ensuring secure communications. When implementing encryption, it's critical to select not only strong algorithms but also secure modes of operation and padding schemes. Using weak or outdated encryption modes can compromise the security of otherwise strong algorithms. + +The security risks of using weak encryption modes include: + +- Data confidentiality breaches where encrypted content becomes readable +- Modification of encrypted data without detection +- Pattern recognition in encrypted data that reveals information about the plaintext +- Replay attacks where valid encrypted data is reused maliciously +- Known-plaintext attacks that can reveal encryption keys + +## 11.1.2. What is the potential impact? + +Common vulnerable patterns include: + +- Using ECB (Electronic Codebook) mode which doesn't hide data patterns +- Implementing CBC (Cipher Block Chaining) without integrity checks +- Using RSA encryption without proper padding schemes +- Relying on outdated padding methods like PKCS1v1.5 +- Using stream ciphers with insufficient initialization vectors + +## 11.1.3. How can I fix this? + +Choose secure encryption modes and padding schemes that provide both confidentiality and integrity protection. + +### 11.1.3.1 AES Encryption Example + +**Non-compliant code :** + +```ballerina +byte[] cipherText = check crypto:encryptAesEcb(data, key); +``` + +For AES, the weakest mode is ECB (Electronic Codebook). Repeated blocks of data are encrypted to the same value, making them easy to identify and reducing the difficulty of recovering the original cleartext. + +```ballerina +byte[] cipherText = check crypto:encryptAesCbc(data, key, initialVector); +``` + +Unauthenticated modes such as CBC (Cipher Block Chaining) may be used but are prone to attacks that manipulate the ciphertext (like padding oracle attacks). They must be used with caution and additional integrity checks. + +**Compliant code:** + +```ballerina +byte[] cipherText = check crypto:encryptAesGcm(data, key, initialVector); +``` + +AES-GCM (Galois/Counter Mode) provides authenticated encryption, ensuring both confidentiality and integrity of the encrypted data. + +### 11.1.3.2 RSA Encryption Example + +**Non-compliant code :** + +```ballerina +// Default padding is PKCS1 +byte[] cipherText = check crypto:encryptRsaEcb(data, publicKey); + +cipherText = check crypto:encryptRsaEcb(data, publicKey, crypto:PKCS1); +``` + +For RSA, avoid using PKCS1v1.5 padding as it is vulnerable to various attacks. Instead, use OAEP (Optimal Asymmetric Encryption Padding) which provides better security. + +**Compliant code:** + +```ballerina +byte[] cipherText = check crypto:encryptRsaEcb(data, publicKey, crypto:OAEPwithMD5andMGF1); +``` + +OAEP such as OAEPwithMD5andMGF1, OAEPWithSHA1AndMGF1, OAEPWithSHA256AndMGF1, OAEPwithSHA384andMGF1, and OAEPwithSHA512andMGF1 should be used for RSA encryption to enhance security. + +## Additional Resources + +- OWASP - [Top 10 2021 Category A2 - Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/) +- OWASP - [Top 10 2017 Category A3 - Sensitive Data Exposure](https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure) +- OWASP - [Top 10 2017 Category A6 - Security Misconfiguration](https://owasp.org/www-project-top-ten/2017/A6_2017-Security_Misconfiguration) +- OWASP - [Mobile AppSec Verification Standard - Cryptography Requirements](https://mas.owasp.org/checklists/MASVS-CRYPTO/) +- OWASP - [Mobile Top 10 2016 Category M5 - Insufficient Cryptography](https://owasp.org/www-project-mobile-top-10/2016-risks/m5-insufficient-cryptography) +- OWASP - [Mobile Top 10 2024 Category M10 - Insufficient Cryptography](https://owasp.org/www-project-mobile-top-10/2023-risks/m10-insufficient-cryptography) +- CWE - [CWE-327 - Use of a Broken or Risky Cryptographic Algorithm](https://cwe.mitre.org/data/definitions/327) +- CWE - [CWE-780 - Use of RSA Algorithm without OAEP](https://cwe.mitre.org/data/definitions/780) +- [CERT, MSC61-J.](https://wiki.sei.cmu.edu/confluence/x/hDdGBQ) - Do not use insecure or weak cryptographic algorithms + +### 11.2 Avoid using fast hashing algorithms + +Storing passwords in plaintext or using fast hashing algorithms creates significant security vulnerabilities. If an attacker gains access to your database, plaintext passwords are immediately compromised. Similarly, passwords hashed with fast algorithms (like MD5, SHA-1, or SHA-256 without sufficient iterations) can be rapidly cracked using modern hardware. + +## 11.2.1. Why this is an issue? + +When using PBKDF2 (Password-Based Key Derivation Function 2), the iteration count is critical for security. Higher iteration counts increase the computational effort required to hash passwords, making brute-force attacks more difficult and time-consuming. + +Following are the OWASP recommended parameters: + +For BCrypt: + +- Use a work factor of 10 or more +- Only use BCrypt for password storage in legacy systems where Argon2 and scrypt are not available +- Be aware of BCrypt's 72-byte password length limit + +For Argon2: + +- Use the Argon2id variant (which Ballerina implements) +- Minimum configuration of 19 MiB (19,456 KB) of memory +- An iteration count of at least 2 +- At least 1 degree of parallelism (this is enforced by Ballerina) + +For PBKDF2: + +- PBKDF2-HMAC-SHA1: 1,300,000 iterations +- PBKDF2-HMAC-SHA256: 600,000 iterations (recommended by NIST) +- PBKDF2-HMAC-SHA512: 210,000 iterations + +If performance constraints make these recommendations impractical, the iteration count should never be lower than 100,000. + +## 11.2.2. What is the potential impact? + +The security risks of using fast hashing algorithms include: + +- Password databases become vulnerable to brute-force attacks +- Dictionary attacks can quickly test common passwords +- Rainbow table attacks can reverse hash values +- GPU-accelerated cracking tools can process billions of hashes per second +- Credential stuffing attacks using compromised password lists + +## 11.2.3. How can I fix this? + +Use secure password hashing algorithms with appropriate parameters that provide sufficient computational cost to resist brute-force attacks. + +### 11.2.3.1 BCrypt Hashing Example + +**Non-compliant code:** + +```ballerina +public function main() returns error? { + string password = "mySecurePassword123"; + // Using insufficient work factor + string hashedPassword = check crypto:hashBcrypt(password, 4); + io:println("Hashed Password: ", hashedPassword); +} +``` + +Using BCrypt with a work factor below 10 is insufficient and vulnerable to brute-force attacks. + +**Compliant code:** + +```ballerina +public function hashPassword() returns error? { + string password = "mySecurePassword123"; + // Using sufficient work factor (14 or higher for better security) + string hashedPassword = check crypto:hashBcrypt(password, 14); + io:println("Hashed Password: ", hashedPassword); +} +``` + +### 11.2.3.2 Argon2 Hashing Example + +**Non-compliant code:** + +```ballerina +public function main() returns error? { + string password = "mySecurePassword123"; + // Using insufficient memory configuration + string hashedPassword = check crypto:hashArgon2(password, memory = 4096); + io:println("Hashed Password: ", hashedPassword); +} +``` + +Using Argon2 with insufficient memory (less than 19,456 KB) makes it vulnerable to attacks. + +**Compliant code:** + +```ballerina +public function hashPassword() returns error? { + string password = "mySecurePassword123"; + // Using recommended parameters: sufficient memory, iterations, and parallelism + string hashedPassword = check crypto:hashArgon2(password, iterations = 3, memory = 65536, parallelism = 4); + io:println("Hashed Password: ", hashedPassword); +} +``` + +### 11.2.3.3 PBKDF2 Hashing Example + +**Non-compliant code:** + +```ballerina +public function main() returns error? { + string password = "mySecurePassword123"; + // Using default settings with insufficient iterations + string hashedPassword = check crypto:hashPbkdf2(password); + io:println("Hashed Password: ", hashedPassword); +} +``` + +Using PBKDF2 with insufficient iterations (default 10,000) is vulnerable to brute-force attacks. + +**Compliant code:** + +```ballerina +public function hashPassword() returns error? { + string password = "mySecurePassword123"; + // Using sufficient iterations as recommended by NIST + string hashedPassword = check crypto:hashPbkdf2(password, iterations = 600000, algorithm = crypto:SHA256); + io:println("Hashed Password: ", hashedPassword); +} +``` + +## 11.2.4 Additional Resources + +- OWASP - [Top 10 2021 Category A2 - Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/) +- OWASP - [Top 10 2021 Category A4 - Insecure Design](https://owasp.org/Top10/A04_2021-Insecure_Design/) +- OWASP - [Top 10 2017 Category A3 - Sensitive Data Exposure](https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure) +- OWASP - [Mobile Top 10 2024 Category M10 - Insufficient Cryptography](https://owasp.org/www-project-mobile-top-10/2023-risks/m10-insufficient-cryptography) +- CWE - [CWE-256 - Plaintext Storage of a Password](https://cwe.mitre.org/data/definitions/256) +- CWE - [CWE-916 - Use of Password Hash With Insufficient Computational Effort](https://cwe.mitre.org/data/definitions/916) +- STIG Viewer - [Application Security and Development: V-222542](https://stigviewer.com/stigs/application_security_and_development/2024-12-06/finding/V-222542) - The application must only store cryptographic representations of passwords. + +### 11.3 Avoid reusing counter mode initialization vectors + +When using encryption algorithms in counter mode (such as AES-GCM, AES-CCM, or AES-CTR), initialization vectors (IVs) or nonces should never be reused with the same encryption key. Reusing IVs with the same key can completely compromise the security of the encryption. + +## 11.3.1. Why this is an issue? + +Counter mode encryption relies on unique initialization vectors to ensure security. When the same IV is used with the same encryption key for different plaintexts, it creates serious vulnerabilities that can lead to: + +- Exposure of encrypted data +- Ability for attackers to forge authenticated messages +- Recovery of the authentication key in some cases +- Disclosure of plaintext by XORing two ciphertexts created with the same IV and key + +In modes like GCM (Galois Counter Mode), the initialization vector must be unique for each encryption operation. When an IV is reused, an attacker who observes multiple encrypted messages can perform cryptanalysis to recover the plaintext or even the encryption key. + +The security risks of reusing IVs in counter mode include: + +- Complete compromise of confidentiality +- Potential loss of message authentication +- Violation of the security guarantees provided by the encryption algorithm +- Exposure of sensitive data even when using strong encryption algorithms + +## 11.3.2. What is the potential impact? + +Reusing initialization vectors in counter mode encryption creates critical security vulnerabilities: + +- **Confidentiality breach**: Attackers can XOR two ciphertexts encrypted with the same IV and key to reveal patterns in the plaintext +- **Authentication forgery**: In authenticated encryption modes like GCM, IV reuse can allow attackers to create valid forged messages +- **Key recovery**: In some scenarios, repeated IV usage can lead to recovery of the encryption key itself +- **Complete system compromise**: Once the encryption is broken, all data encrypted with that key becomes vulnerable + +## 11.3.3. How can I fix this? + +Generate cryptographically secure random initialization vectors for each encryption operation and ensure they are never reused with the same key. + +### 11.3.3.1 AES-GCM Encryption Example + +**Non-compliant code:** + +```ballerina +public function encryptData(string data) returns byte[]|error { + byte[16] initialVector = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + byte[16] key = [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; + byte[] dataBytes = data.toBytes(); + return crypto:encryptAesGcm(dataBytes, key, initialVector); +} +``` + +In this non-compliant example, the initialization vector is hardcoded, meaning every encryption operation uses the same IV. This completely undermines the security of AES-GCM encryption, regardless of key strength. + +**Compliant code:** + +```ballerina +import ballerina/crypto; +import ballerina/random; + +public function encryptData(string data) returns [byte[], byte[16]]|error { + byte[16] initialVector = []; + foreach int i in 0...15 { + initialVector[i] = (check random:createIntInRange(0, 255)); + } + byte[16] key = [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]; + byte[] dataBytes = data.toBytes(); + byte[] encryptedData = check crypto:encryptAesGcm(dataBytes, key, initialVector); + return [encryptedData, initialVector]; +} +``` + +This compliant approach generates a cryptographically secure random initialization vector for each encryption operation and returns it along with the encrypted data. The IV must be stored alongside the encrypted data (but doesn't need to be kept secret) to allow for decryption later. + +### 11.3.3.2 AES-CBC Encryption Example + +**Non-compliant code:** + +```ballerina +public function encryptMessage(string message) returns byte[]|error { + // Static nonce - this is vulnerable! + byte[12] nonce = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; + byte[16] key = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + byte[] messageBytes = message.toBytes(); + return crypto:encryptAesCbc(messageBytes, key, nonce); +} +``` + +**Compliant code:** + +```ballerina +import ballerina/crypto; +import ballerina/random; + +public function encryptMessage(string message) returns [byte[], byte[12]]|error { + // Generate unique nonce for each encryption + byte[12] nonce = []; + foreach int i in 0...11 { + nonce[i] = (check random:createIntInRange(0, 255)); + } + byte[16] key = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + byte[] messageBytes = message.toBytes(); + byte[] encryptedData = check crypto:encryptAesCbc(messageBytes, key, nonce); + return [encryptedData, nonce]; +} +``` + +## 11.3.4 Additional Resources + +- OWASP - [Top 10 2021 Category A2 - Cryptographic Failures](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/) +- OWASP - [Top 10 2017 Category A3 - Sensitive Data Exposure](https://owasp.org/www-project-top-ten/2017/A3_2017-Sensitive_Data_Exposure) +- OWASP - [Mobile AppSec Verification Standard - Cryptography Requirements](https://mas.owasp.org/checklists/MASVS-CRYPTO/) +- OWASP - [Mobile Top 10 2016 Category M5 - Insufficient Cryptography](https://owasp.org/www-project-mobile-top-10/2016-risks/m5-insufficient-cryptography) +- OWASP - [Mobile Top 10 2024 Category M10 - Insufficient Cryptography](https://owasp.org/www-project-mobile-top-10/2023-risks/m10-insufficient-cryptography) +- CWE - [CWE-323 - Reusing a Nonce, Key Pair in Encryption](https://cwe.mitre.org/data/definitions/323) +- [NIST, SP-800-38A](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf) - Recommendation for Block Cipher Modes of Operation +- [NIST, SP-800-38C](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38c.pdf) - Recommendation for Block Cipher Modes of Operation: The CCM Mode for Authentication and Confidentiality +- [NIST, SP-800-38D](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) - Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC diff --git a/gradle.properties b/gradle.properties index 02accd8e..30dc7192 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=2.9.1-SNAPSHOT +version=2.10.0-SNAPSHOT checkstylePluginVersion=10.12.0 bouncycastleVersion=1.80 spotbugsPluginVersion=6.0.18 From 8c50f1ec6e4758360903d4950fb6d20a452c75cf Mon Sep 17 00:00:00 2001 From: Randil Tharusha <51049280+randilt@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:14:18 +0530 Subject: [PATCH 06/15] Update ballerina/sign_verify.bal Co-authored-by: Thisaru Guruge --- ballerina/sign_verify.bal | 1 - 1 file changed, 1 deletion(-) diff --git a/ballerina/sign_verify.bal b/ballerina/sign_verify.bal index 65171ac9..251462cb 100644 --- a/ballerina/sign_verify.bal +++ b/ballerina/sign_verify.bal @@ -152,7 +152,6 @@ public isolated function signRsaSha512(byte[] input, PrivateKey privateKey) retu # + privateKey - Private key used for signing # + return - The generated signature or else a `crypto:Error` if the private key is invalid public isolated function signRsaSsaPss256(byte[] input, PrivateKey privateKey) returns byte[]|Error = @java:Method { - name: "signRsaSsaPss256", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; From 9de770c8ce1b70f0783461e88382d79d8a91b79b Mon Sep 17 00:00:00 2001 From: Randil Tharusha <51049280+randilt@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:40:01 +0530 Subject: [PATCH 07/15] Update ballerina/sign_verify.bal Co-authored-by: DimuthuMadushan <35717653+DimuthuMadushan@users.noreply.github.com> --- ballerina/sign_verify.bal | 1 - 1 file changed, 1 deletion(-) diff --git a/ballerina/sign_verify.bal b/ballerina/sign_verify.bal index 251462cb..69d74ed4 100644 --- a/ballerina/sign_verify.bal +++ b/ballerina/sign_verify.bal @@ -359,7 +359,6 @@ public isolated function verifyRsaSha512Signature(byte[] data, byte[] signature, # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSsaPss256Signature(byte[] data, byte[] signature, PublicKey publicKey) returns boolean|Error = @java:Method { - name: "verifyRsaSsaPss256Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; From 1fbd9f45b506e2b9051f74495b04ddebc66d75a4 Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:08:28 +0530 Subject: [PATCH 08/15] Add RSASSA-PSS with SHA-256 signing and verification functions --- ballerina/sign_verify.bal | 58 ++++++++++++++++--- ballerina/tests/sign_verify_test.bal | 47 +++++++++++++-- .../stdlib/crypto/nativeimpl/Sign.java | 14 +++++ 3 files changed, 107 insertions(+), 12 deletions(-) diff --git a/ballerina/sign_verify.bal b/ballerina/sign_verify.bal index 8ded90e2..65171ac9 100644 --- a/ballerina/sign_verify.bal +++ b/ballerina/sign_verify.bal @@ -136,6 +136,26 @@ public isolated function signRsaSha512(byte[] input, PrivateKey privateKey) retu 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; +# Returns the RSASSA-PSS with SHA-256 based signature value for the given data. +# ```ballerina +# string input = "Hello Ballerina"; +# byte[] data = input.toBytes(); +# crypto:KeyStore keyStore = { +# path: "/path/to/keyStore.p12", +# password: "keyStorePassword" +# }; +# crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +# byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +# ``` +# +# + input - The content to be signed +# + privateKey - Private key used for signing +# + return - The generated signature or else a `crypto:Error` if the private key is invalid +public isolated function signRsaSsaPss256(byte[] input, PrivateKey privateKey) returns byte[]|Error = @java:Method { + name: "signRsaSsaPss256", + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" +} external; + # Returns the SHA384withECDSA based signature value for the given data. # ```ballerina # string input = "Hello Ballerina"; @@ -195,7 +215,7 @@ public isolated function signSha256withEcdsa(byte[] input, PrivateKey privateKey # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaMd5Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaMd5Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -219,7 +239,7 @@ public isolated function verifyRsaMd5Signature(byte[] data, byte[] signature, Pu # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyMlDsa65Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyMlDsa65Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -267,7 +287,7 @@ public isolated function verifyRsaSha1Signature(byte[] data, byte[] signature, P # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSha256Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaSha256Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -291,7 +311,7 @@ public isolated function verifyRsaSha256Signature(byte[] data, byte[] signature, # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSha384Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaSha384Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -315,11 +335,35 @@ public isolated function verifyRsaSha384Signature(byte[] data, byte[] signature, # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSha512Signature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifyRsaSha512Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; +# Verifies the RSASSA-PSS with SHA-256 based signature. +# ```ballerina +# string input = "Hello Ballerina"; +# byte[] data = input.toBytes(); +# crypto:KeyStore keyStore = { +# path: "/path/to/keyStore.p12", +# password: "keyStorePassword" +# }; +# crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +# byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +# crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keyStore, "keyAlias"); +# boolean validity = check crypto:verifyRsaSsaPss256Signature(data, signature, publicKey); +# ``` +# +# + data - The content to be verified +# + signature - Signature value +# + publicKey - Public key used for verification +# + return - Validity of the signature or else a `crypto:Error` if the public key is invalid +public isolated function verifyRsaSsaPss256Signature(byte[] data, byte[] signature, PublicKey publicKey) + returns boolean|Error = @java:Method { + name: "verifyRsaSsaPss256Signature", + 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" +} external; + # Verifies the SHA384withECDSA based signature. # ```ballerina # string input = "Hello Ballerina"; @@ -339,7 +383,7 @@ public isolated function verifyRsaSha512Signature(byte[] data, byte[] signature, # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifySha384withEcdsaSignature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifySha384withEcdsaSignature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; @@ -363,7 +407,7 @@ public isolated function verifySha384withEcdsaSignature(byte[] data, byte[] sign # + publicKey - Public key used for verification # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifySha256withEcdsaSignature(byte[] data, byte[] signature, PublicKey publicKey) - returns boolean|Error = @java:Method { + returns boolean|Error = @java:Method { name: "verifySha256withEcdsaSignature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; diff --git a/ballerina/tests/sign_verify_test.bal b/ballerina/tests/sign_verify_test.bal index 46ea7c4a..21b3baf0 100644 --- a/ballerina/tests/sign_verify_test.bal +++ b/ballerina/tests/sign_verify_test.bal @@ -106,6 +106,18 @@ isolated function testSignRsaSha512() returns Error? { test:assertEquals(sha512Signature.toBase16(), expectedSha512Signature); } +@test:Config {} +isolated function testSignRsaSsaPss256() returns Error? { + byte[] payload = "Ballerina test".toBytes(); + KeyStore keyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PrivateKey privateKey = check decodeRsaPrivateKeyFromKeyStore(keyStore, "ballerina", "ballerina"); + byte[] pssSignature = check signRsaSsaPss256(payload, privateKey); + test:assertTrue(pssSignature.length() > 0); +} + @test:Config {} isolated function testSignMlDsa65() returns Error? { byte[] payload = "Ballerina test".toBytes(); @@ -204,7 +216,7 @@ isolated function testSignMlDsa65() returns Error? { @test:Config {} isolated function testSignRsaMd5WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaMd5(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -216,7 +228,7 @@ isolated function testSignRsaMd5WithInvalidKey() { @test:Config {} isolated function testSignRsaSha1WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha1(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -228,7 +240,7 @@ isolated function testSignRsaSha1WithInvalidKey() { @test:Config {} isolated function testSignRsaSha256WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha256(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -240,7 +252,7 @@ isolated function testSignRsaSha256WithInvalidKey() { @test:Config {} isolated function testSignRsaSha384WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha384(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -252,7 +264,7 @@ isolated function testSignRsaSha384WithInvalidKey() { @test:Config {} isolated function testSignRsaSha512WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); - PrivateKey privateKey = {algorithm:"RSA"}; + PrivateKey privateKey = {algorithm: "RSA"}; byte[]|Error result = signRsaSha512(payload, privateKey); if result is Error { test:assertTrue(result.message().includes("Uninitialized private key:")); @@ -261,6 +273,18 @@ isolated function testSignRsaSha512WithInvalidKey() { } } +@test:Config {} +isolated function testSignRsaSsaPss256WithInvalidKey() { + byte[] payload = "Ballerina test".toBytes(); + PrivateKey privateKey = {algorithm: "RSA"}; + byte[]|Error result = signRsaSsaPss256(payload, privateKey); + if result is Error { + test:assertTrue(result.message().includes("Uninitialized private key:")); + } else { + test:assertFail("Expected error not found."); + } +} + @test:Config {} isolated function testSignMlDsa65WithInvalidKey() { byte[] payload = "Ballerina test".toBytes(); @@ -338,6 +362,19 @@ isolated function testVerifyRsaSha512() returns Error? { test:assertTrue(check verifyRsaSha512Signature(payload, sha512Signature, publicKey)); } +@test:Config {} +isolated function testVerifyRsaSsaPss256() returns Error? { + byte[] payload = "Ballerina test".toBytes(); + KeyStore keyStore = { + path: KEYSTORE_PATH, + password: "ballerina" + }; + PrivateKey privateKey = check decodeRsaPrivateKeyFromKeyStore(keyStore, "ballerina", "ballerina"); + PublicKey publicKey = check decodeRsaPublicKeyFromTrustStore(keyStore, "ballerina"); + byte[] pssSignature = check signRsaSsaPss256(payload, privateKey); + test:assertTrue(check verifyRsaSsaPss256Signature(payload, pssSignature, publicKey)); +} + @test:Config {} isolated function testVerifySha384withEcdsa() returns Error? { byte[] payload = "Ballerina test".toBytes(); diff --git a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java index 35a9ed7d..815a5152 100644 --- a/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java +++ b/native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Sign.java @@ -83,6 +83,12 @@ public static Object signRsaSha512(BArray inputValue, BMap privateKey) { return CryptoUtils.sign("SHA512withRSA", key, input); } + public static Object signRsaSsaPss256(BArray inputValue, BMap privateKey) { + byte[] input = inputValue.getBytes(); + PrivateKey key = (PrivateKey) privateKey.getNativeData(Constants.NATIVE_DATA_PRIVATE_KEY); + return CryptoUtils.sign("SHA256withRSAandMGF1", key, input); + } + public static Object verifyMlDsa65Signature(BArray dataValue, BArray signatureValue, BMap publicKey) { byte[] data = dataValue.getBytes(); @@ -131,6 +137,14 @@ public static Object verifyRsaSha512Signature(BArray dataValue, BArray signature return CryptoUtils.verify("SHA512withRSA", key, data, signature); } + public static Object verifyRsaSsaPss256Signature(BArray dataValue, BArray signatureValue, + BMap publicKey) { + byte[] data = dataValue.getBytes(); + byte[] signature = signatureValue.getBytes(); + PublicKey key = (PublicKey) publicKey.getNativeData(Constants.NATIVE_DATA_PUBLIC_KEY); + return CryptoUtils.verify("SHA256withRSAandMGF1", key, data, signature); + } + public static Object verifySha384withEcdsaSignature(BArray dataValue, BArray signatureValue, BMap publicKey) { byte[] data = dataValue.getBytes(); byte[] signature = signatureValue.getBytes(); From c2847681257e57f6f9d98e338529792532de05f3 Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:18:33 +0530 Subject: [PATCH 09/15] Add RSASSA-PSS (PS256) signature support proposal --- .../proposals/rsassa-pss-signature-support.md | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/proposals/rsassa-pss-signature-support.md diff --git a/docs/proposals/rsassa-pss-signature-support.md b/docs/proposals/rsassa-pss-signature-support.md new file mode 100644 index 00000000..ad3030f1 --- /dev/null +++ b/docs/proposals/rsassa-pss-signature-support.md @@ -0,0 +1,94 @@ +# Proposal: Introduce RSASSA-PSS (PS256) Signature Support to Ballerina Crypto Module + +_Authors_: @randilt +_Reviewers_: +_Created_: 2025/10/01 +_Updated_: 2025/10/01 +_Issue_: [#8292](https://github.com/ballerina-platform/ballerina-library/issues/8292) + +## Summary + +This proposal introduces support for the RSASSA-PSS signature scheme with SHA-256 (PS256) to the Ballerina Crypto Module. Currently, only classic RSA signatures (PKCS#1 v1.5) are available, but RSASSA-PSS provides enhanced security properties and is increasingly required by modern cryptographic standards and protocols. + +## Goals + +- Provide support for RSASSA-PSS signature generation with SHA-256 +- Provide support for RSASSA-PSS signature verification with SHA-256 +- Enable higher-level modules like JWT to utilize PS256 signatures + +## Motivation + +The Ballerina Crypto Module currently supports several RSA signature algorithms using PKCS#1 v1.5 padding (RSA-MD5, RSA-SHA1, RSA-SHA256, RSA-SHA384, RSA-SHA512). However, RSASSA-PSS, which is defined in RFC 8017 and provides enhanced security properties, is not yet supported. + +RSASSA-PSS offers several advantages over PKCS#1 v1.5: + +1. **Provable Security**: RSASSA-PSS has a security proof in the random oracle model +2. **Probabilistic Signatures**: Uses random salt generation, making signatures non-deterministic +3. **Modern Standard**: Required by many modern protocols and standards, including JWT RS256 alternative (PS256) +4. **Recommended by Standards**: NIST and other standards bodies recommend RSASSA-PSS over PKCS#1 v1.5 + +The lack of native support for RSASSA-PSS limits the cryptographic capabilities of Ballerina applications, particularly those implementing modern security protocols that require or prefer PS256 signatures. + +## Description + +This proposal adds RSASSA-PSS signature generation and verification with SHA-256 to the Ballerina Crypto Module, following the same architectural patterns as existing RSA signature functions. + +The key functionalities expected from this change are: + +- API to generate RSASSA-PSS signatures with `crypto:signRsaSsaPss256` +- API to verify RSASSA-PSS signatures with `crypto:verifyRsaSsaPss256Signature` + +### API additions + +Two new APIs will be added following the existing RSA signature function patterns: + +```ballerina +# Returns the RSASSA-PSS based signature value for the given data. +# +# + input - The content to be signed +# + privateKey - Private key used for signing +# + return - The generated signature or else a `crypto:Error` if the private key is invalid +public isolated function signRsaSsaPss256(byte[] input, PrivateKey privateKey) returns byte[]|Error; +``` + +```ballerina +# Verifies the RSASSA-PSS based signature. +# +# + data - The content to be verified +# + signature - Signature value +# + publicKey - Public key used for verification +# + return - Validity of the signature or else a `crypto:Error` if the key is invalid +public isolated function verifyRsaSsaPss256Signature(byte[] data, byte[] signature, PublicKey publicKey) returns boolean|Error; +``` + +### Implementation Details + +The implementation follows the existing architecture: + +1. **Java Native Implementation**: Uses Java's `Signature.getInstance("SHA256withRSAandMGF1")` with default PSS parameters +2. **Algorithm String**: Uses `"SHA256withRSAandMGF1"` directly as string literal, consistent with other algorithms +3. **Method Naming**: Follows the pattern `signRsaSsaPss256` and `verifyRsaSsaPss256Signature` +4. **Documentation**: Matches the style and format of existing signature functions + +### PSS Parameters + +The implementation uses Java's default RSASSA-PSS parameters: + +- **Hash Algorithm**: SHA-256 +- **MGF**: MGF1 with SHA-256 +- **Salt Length**: Same as hash length (32 bytes for SHA-256) +- **Trailer Field**: 1 (standard value) + +These parameters align with common RSASSA-PSS usage and provide strong security guarantees. + +## Testing + +The implementation includes comprehensive test coverage following the existing test patterns: + +1. **Basic Signing Test**: Verifies signature generation returns valid length (probabilistic nature prevents deterministic comparison) +2. **Invalid Key Test**: Verifies proper error handling with invalid private keys +3. **Sign-Verify Test**: Tests complete workflow of signing with private key and verifying with corresponding public key + +## Backward Compatibility + +This addition is fully backward compatible as it only adds new functions without modifying existing APIs or behavior. From 3acc044666cd1af1f194782e0daf1b7d1f2aeecd Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:22:48 +0530 Subject: [PATCH 10/15] Add RSASSA-PSS-SHA256 signing and verification APIs to the specification --- docs/spec/spec.md | 204 +++++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 85 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 4d5ab359..05bc191b 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -20,89 +20,91 @@ The conforming implementation of the specification is released and included in t 1. [Overview](#1-overview) 2. [Hash](#2-hash) - - 2.1. [MD5](#21-md5) - - 2.2. [SHA1](#22-sha1) - - 2.3. [SHA256](#23-sha256) - - 2.4. [SHA384](#24-sha384) - - 2.5. [SHA512](#25-sha512) - - 2.6. [CRC32B](#26-crc32b) - - 2.7. [KECCAK256](#27-keccak256) + - 2.1. [MD5](#21-md5) + - 2.2. [SHA1](#22-sha1) + - 2.3. [SHA256](#23-sha256) + - 2.4. [SHA384](#24-sha384) + - 2.5. [SHA512](#25-sha512) + - 2.6. [CRC32B](#26-crc32b) + - 2.7. [KECCAK256](#27-keccak256) 3. [HMAC](#3-hmac) - - 3.1. [MD5](#31-md5) - - 3.2. [SHA1](#32-sha1) - - 3.3. [SHA256](#33-sha256) - - 3.4. [SHA384](#34-sha384) - - 3.5. [SHA512](#35-sha512) + - 3.1. [MD5](#31-md5) + - 3.2. [SHA1](#32-sha1) + - 3.3. [SHA256](#33-sha256) + - 3.4. [SHA384](#34-sha384) + - 3.5. [SHA512](#35-sha512) 4. [Decode private/public key](#4-decode-privatepublic-key) - - 4.1. [Decode RSA Private key from PKCS12 file](#41-decode-rsa-private-key-from-pkcs12-file) - - 4.2. [Decode RSA Private key using Private key and Password](#42-decode-rsa-private-key-using-private-key-and-password) - - 4.3. [Decode RSA Private key using Private key content and Password](#43-decode-rsa-private-key-using-private-key-content-and-password) - - 4.4. [Decode RSA Public key from PKCS12 file](#44-decode-rsa-public-key-from-pkcs12-file) - - 4.5. [Decode RSA Public key from the certificate file](#45-decode-rsa-public-key-from-the-certificate-file) - - 4.6. [Decode RSA Public key from the certificate content](#46-decode-rsa-public-key-from-the-certificate-content) - - 4.7. [Decode EC Private key from PKCS12 file](#47-decode-ec-private-key-from-pkcs12-file) - - 4.8. [Decode EC Private key using Private key and Password](#48-decode-ec-private-key-using-private-key-and-password) - - 4.9. [Decode EC Public key from PKCS12 file](#49-decode-ec-public-key-from-pkcs12-file) - - 4.10. [Decode EC Public key from the certificate file](#410-decode-ec-public-key-from-the-certificate-file) - - 4.11. [Build RSA Public key from modulus and exponent parameters](#411-build-rsa-public-key-from-modulus-and-exponent-parameters) - - 4.12. [Decode ML-DSA-65 Private key from PKCS12 file](#412-decode-ml-dsa-65-private-key-from-pkcs12-file) - - 4.13. [Decode ML-DSA-65 Private key using Private key and Password](#413-decode-ml-dsa-65-private-key-using-private-key-and-password) - - 4.14. [Decode ML-DSA-65 Public key from PKCS12 file](#414-decode-ml-dsa-65-public-key-from-pkcs12-file) - - 4.15. [Decode ML-DSA-65 Public key from the certificate file](#415-decode-ml-dsa-65-public-key-from-the-certificate-file) - - 4.16. [Decode ML-KEM-768 Private key from PKCS12 file](#416-decode-ml-kem-768-private-key-from-pkcs12-file) - - 4.17. [Decode ML-KEM-768 Private key using Private key and Password](#417-decode-ml-kem-768-private-key-using-private-key-and-password) - - 4.18. [Decode ML-KEM-768 Public key from PKCS12 file](#418-decode-ml-kem-768-public-key-from-pkcs12-file) - - 4.19. [Decode ML-KEM-768 Public key from the certificate file](#419-decode-ml-kem-768-public-key-from-the-certificate-file) + - 4.1. [Decode RSA Private key from PKCS12 file](#41-decode-rsa-private-key-from-pkcs12-file) + - 4.2. [Decode RSA Private key using Private key and Password](#42-decode-rsa-private-key-using-private-key-and-password) + - 4.3. [Decode RSA Private key using Private key content and Password](#43-decode-rsa-private-key-using-private-key-content-and-password) + - 4.4. [Decode RSA Public key from PKCS12 file](#44-decode-rsa-public-key-from-pkcs12-file) + - 4.5. [Decode RSA Public key from the certificate file](#45-decode-rsa-public-key-from-the-certificate-file) + - 4.6. [Decode RSA Public key from the certificate content](#46-decode-rsa-public-key-from-the-certificate-content) + - 4.7. [Decode EC Private key from PKCS12 file](#47-decode-ec-private-key-from-pkcs12-file) + - 4.8. [Decode EC Private key using Private key and Password](#48-decode-ec-private-key-using-private-key-and-password) + - 4.9. [Decode EC Public key from PKCS12 file](#49-decode-ec-public-key-from-pkcs12-file) + - 4.10. [Decode EC Public key from the certificate file](#410-decode-ec-public-key-from-the-certificate-file) + - 4.11. [Build RSA Public key from modulus and exponent parameters](#411-build-rsa-public-key-from-modulus-and-exponent-parameters) + - 4.12. [Decode ML-DSA-65 Private key from PKCS12 file](#412-decode-ml-dsa-65-private-key-from-pkcs12-file) + - 4.13. [Decode ML-DSA-65 Private key using Private key and Password](#413-decode-ml-dsa-65-private-key-using-private-key-and-password) + - 4.14. [Decode ML-DSA-65 Public key from PKCS12 file](#414-decode-ml-dsa-65-public-key-from-pkcs12-file) + - 4.15. [Decode ML-DSA-65 Public key from the certificate file](#415-decode-ml-dsa-65-public-key-from-the-certificate-file) + - 4.16. [Decode ML-KEM-768 Private key from PKCS12 file](#416-decode-ml-kem-768-private-key-from-pkcs12-file) + - 4.17. [Decode ML-KEM-768 Private key using Private key and Password](#417-decode-ml-kem-768-private-key-using-private-key-and-password) + - 4.18. [Decode ML-KEM-768 Public key from PKCS12 file](#418-decode-ml-kem-768-public-key-from-pkcs12-file) + - 4.19. [Decode ML-KEM-768 Public key from the certificate file](#419-decode-ml-kem-768-public-key-from-the-certificate-file) 5. [Encrypt-Decrypt](#5-encrypt-decrypt) - - 5.1. [Encryption](#51-encryption) - - 5.1.1. [RSA](#511-rsa) - - 5.1.2. [AES-CBC](#512-aes-cbc) - - 5.1.3. [AES-ECB](#513-aes-ecb) - - 5.1.4. [AES-GCM](#514-aes-gcm) - - 5.1.5. [PGP](#515-pgp) - - 5.2. [Decryption](#52-decryption) - - 5.2.1. [RSA-ECB](#521-rsa-ecb) - - 5.2.2. [AES-CBC](#522-aes-cbc) - - 5.2.3. [AES-ECB](#523-aes-ecb) - - 5.2.4. [AES-GCM](#524-aes-gcm) - - 5.2.5. [PGP](#525-pgp) + - 5.1. [Encryption](#51-encryption) + - 5.1.1. [RSA](#511-rsa) + - 5.1.2. [AES-CBC](#512-aes-cbc) + - 5.1.3. [AES-ECB](#513-aes-ecb) + - 5.1.4. [AES-GCM](#514-aes-gcm) + - 5.1.5. [PGP](#515-pgp) + - 5.2. [Decryption](#52-decryption) + - 5.2.1. [RSA-ECB](#521-rsa-ecb) + - 5.2.2. [AES-CBC](#522-aes-cbc) + - 5.2.3. [AES-ECB](#523-aes-ecb) + - 5.2.4. [AES-GCM](#524-aes-gcm) + - 5.2.5. [PGP](#525-pgp) 6. [Sign and Verify](#6-sign-and-verify) - - 6.1. [Sign messages](#61-sign-messages) - - 6.1.1. [RSA-MD5](#611-rsa-md5) - - 6.1.2. [RSA-SHA1](#612-rsa-sha1) - - 6.1.3. [RSA-SHA256](#613-rsa-sha256) - - 6.1.4. [RSA-SHA384](#614-rsa-sha384) - - 6.1.5. [RSA-SHA512](#615-rsa-sha512) - - 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) - - 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) - - 6.1.8. [ML-DSA-65](#618-ml-dsa-65) - - 6.2. [Verify signature](#62-verify-signature) - - 6.2.1. [RSA-MD5](#621-rsa-md5) - - 6.2.2. [RSA-SHA1](#622-rsa-sha1) - - 6.2.3. [RSA-SHA256](#623-rsa-sha256) - - 6.2.4. [RSA-SHA384](#624-rsa-sha384) - - 6.2.5. [RSA-SHA512](#625-rsa-sha512) - - 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) - - 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) - - 6.2.8. [ML-DSA-65](#628-ml-dsa-65) + - 6.1. [Sign messages](#61-sign-messages) + - 6.1.1. [RSA-MD5](#611-rsa-md5) + - 6.1.2. [RSA-SHA1](#612-rsa-sha1) + - 6.1.3. [RSA-SHA256](#613-rsa-sha256) + - 6.1.4. [RSA-SHA384](#614-rsa-sha384) + - 6.1.5. [RSA-SHA512](#615-rsa-sha512) + - 6.1.6. [RSASSA-PSS-SHA256](#616-rsassa-pss-sha256) + - 6.1.7. [SHA384withECDSA](#617-sha384withecdsa) + - 6.1.8. [SHA256withECDSA](#618-sha256withecdsa) + - 6.1.9. [ML-DSA-65](#619-mldsa65) + - 6.2. [Verify signature](#62-verify-signature) + - 6.2.1. [RSA-MD5](#621-rsa-md5) + - 6.2.2. [RSA-SHA1](#622-rsa-sha1) + - 6.2.3. [RSA-SHA256](#623-rsa-sha256) + - 6.2.4. [RSA-SHA384](#624-rsa-sha384) + - 6.2.5. [RSA-SHA512](#625-rsa-sha512) + - 6.2.6. [RSASSA-PSS-SHA256](#626-rsassa-pss-sha256) + - 6.2.7. [SHA384withECDSA](#627-sha384withecdsa) + - 6.2.8. [SHA256withECDSA](#628-sha256withecdsa) + - 6.2.9. [ML-DSA-65](#629-mldsa65) 7. [Key Derivation Function (KDF)](#7-key-derivation-function-kdf) - - 7.1. [HKDF-SHA256](#71-hkdf-sha256) + - 7.1. [HKDF-SHA256](#71-hkdf-sha256) 8. [Key Exchange Mechanism (KEM)](#8-key-exchange-mechanism-kem) - - 8.1 [Encapsulation](#81-encapsulation) - - 8.1.1 [RSA-KEM](#811-rsa-kem) - - 8.1.2 [ML-KEM-768](#812-ml-kem-768) - - 8.1.3 [RSA-KEM-ML-KEM-768](#813-rsa-kem-ml-kem-768) - - 8.2 [Decapsulation](#81-encapsulation) - - 8.2.1 [RSA-KEM](#821-rsa-kem) - - 8.2.2 [ML-KEM-768](#822-ml-kem-768) - - 8.2.3 [RSA-KEM-ML-KEM-768](#823-rsa-kem-ml-kem-768) + - 8.1 [Encapsulation](#81-encapsulation) + - 8.1.1 [RSA-KEM](#811-rsa-kem) + - 8.1.2 [ML-KEM-768](#812-ml-kem-768) + - 8.1.3 [RSA-KEM-ML-KEM-768](#813-rsa-kem-ml-kem-768) + - 8.2 [Decapsulation](#81-encapsulation) + - 8.2.1 [RSA-KEM](#821-rsa-kem) + - 8.2.2 [ML-KEM-768](#822-ml-kem-768) + - 8.2.3 [RSA-KEM-ML-KEM-768](#823-rsa-kem-ml-kem-768) 9. [Hybrid Public Key Encryption (HPKE)](#9-hybrid-public-key-encryption-hpke) - - 9.1 [Encrypt](#91-encrypt) - - 9.1.1 [ML-KEM-768-HPKE](#911-ml-kem-768-hpke) - - 9.1.2 [RSA-KEM-ML-KEM-768-HPKE](#912-rsa-kem-ml-kem-768-hpke) - - 9.2 [Decrypt](#92-decrypt) - - 9.2.1 [ML-KEM-768-HPKE](#921-ml-kem-768-hpke) - - 9.2.2 [RSA-KEM-ML-KEM-768-HPKE](#922-rsa-kem-ml-kem-768-hpke) + - 9.1 [Encrypt](#91-encrypt) + - 9.1.1 [ML-KEM-768-HPKE](#911-ml-kem-768-hpke) + - 9.1.2 [RSA-KEM-ML-KEM-768-HPKE](#912-rsa-kem-ml-kem-768-hpke) + - 9.2 [Decrypt](#92-decrypt) + - 9.2.1 [ML-KEM-768-HPKE](#921-ml-kem-768-hpke) + - 9.2.2 [RSA-KEM-ML-KEM-768-HPKE](#922-rsa-kem-ml-kem-768-hpke) 10. [Password hashing](#10-password-hashing) - 10.1 [BCrypt](#101-bcrypt) - 10.2 [Argon2](#102-argon2) @@ -543,7 +545,7 @@ byte[] cipherText = check crypto:encryptPgp(data, publicKeyPath); The following encryption options can be configured in the PGP encryption. | Option | Description | Default Value | -|-----------------------|-------------------------------------------------------------------|---------------| +| --------------------- | ----------------------------------------------------------------- | ------------- | | compressionAlgorithm | Specifies the compression algorithm used for PGP encryption | ZIP | | symmetricKeyAlgorithm | Specifies the symmetric key algorithm used for encryption | AES_256 | | armor | Indicates whether ASCII armor is enabled for the encrypted output | true | @@ -741,7 +743,22 @@ crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyS byte[] signature = check crypto:signRsaSha512(data, privateKey); ``` -#### 6.1.6. SHA384withECDSA +#### 6.1.6. [RSASSA-PSS-SHA256](#616-rsassa-pss-sha256) + +This API can be used to create the RSASSA-PSS based signature value for the given data. + +```ballerina +string input = "Hello Ballerina"; +byte[] data = input.toBytes(); +crypto:KeyStore keyStore = { + path: "/path/to/keyStore.p12", + password: "keyStorePassword" +}; +crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +``` + +#### 6.1.7. [SHA384withECDSA](#617-sha384withecdsa) This API can be used to create the SHA384withECDSA based signature value for the given data. @@ -756,7 +773,7 @@ crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keySt byte[] signature = check crypto:signSha384withEcdsa(data, privateKey); ``` -#### 6.1.7. SHA256withECDSA +#### 6.1.8. [SHA256withECDSA](#618-sha256withecdsa) This API can be used to create the SHA256withECDSA based signature value for the given data. @@ -771,7 +788,7 @@ crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keySt byte[] signature = check crypto:signSha256withEcdsa(data, privateKey); ``` -#### 6.1.8. ML-DSA-65 +#### 6.1.9. [ML-DSA-65](#619-mldsa65) This API can be used to create the ML-DSA-65 based signature value for the given data. @@ -873,7 +890,24 @@ crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keySt boolean validity = check crypto:verifyRsaSha512Signature(data, signature, publicKey); ``` -#### 6.2.6. SHA384withECDSA +#### 6.2.6. [RSASSA-PSS-SHA256](#626-rsassa-pss-sha256) + +This API can be used to verify the RSASSA-PSS based signature. + +```ballerina +string input = "Hello Ballerina"; +byte[] data = input.toBytes(); +crypto:KeyStore keyStore = { + path: "/path/to/keyStore.p12", + password: "keyStorePassword" +}; +crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword"); +byte[] signature = check crypto:signRsaSsaPss256(data, privateKey); +crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromTrustStore(keyStore, "keyAlias"); +boolean validity = check crypto:verifyRsaSsaPss256Signature(data, signature, publicKey); +``` + +#### 6.2.7. [SHA384withECDSA](#627-sha384withecdsa) This API can be used to verify the SHA384withECDSA based signature. @@ -890,7 +924,7 @@ crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(keySto boolean validity = check crypto:verifySha384withEcdsaSignature(data, signature, publicKey); ``` -#### 6.2.7. SHA256withECDSA +#### 6.2.8. [SHA256withECDSA](#628-sha256withecdsa) This API can be used to verify the SHA256withECDSA based signature. @@ -907,7 +941,7 @@ crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(keySto boolean validity = check crypto:verifySha256withEcdsaSignature(data, signature, publicKey); ``` -#### 6.2.8. ML-DSA-65 +#### 6.2.9. [ML-DSA-65](#629-mldsa65) This API can be used to verify the ML-DSA-65 based signature. @@ -1172,7 +1206,7 @@ boolean isValid = check crypto:verifyBcrypt(password, hashedPassword1); Implements the Argon2id variant of the Argon2 password hashing algorithm, optimized for both high memory usage and GPU resistance. ```ballerina -public isolated function hashArgon2(string password, int iterations = 3, +public isolated function hashArgon2(string password, int iterations = 3, int memory = 65536, int parallelism = 4) returns string|Error ``` @@ -1253,7 +1287,7 @@ boolean isValid = check crypto:verifyPbkdf2(password, hashedPassword); The following static code rules are applied to the Crypto module. | Id | Kind | Description | -|--------------------|---------------|-------------------------------------------------------------------------------------------------------------------| +| ------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------- | | ballerina/crypto:1 | VULNERABILITY | [Avoid using insecure cipher modes or padding schemes](#111-avoid-using-insecure-cipher-modes-or-padding-schemes) | | ballerina/crypto:2 | VULNERABILITY | [Avoid using fast hashing algorithms](#112-avoid-using-fast-hashing-algorithms) | | ballerina/crypto:3 | VULNERABILITY | [Avoid reusing counter mode initialization vectors](#113-avoid-reusing-counter-mode-initialization-vectors) | From 597c290cbc4d90ca3b1abc0652e0ede2ee5800a2 Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 1 Oct 2025 13:30:45 +0530 Subject: [PATCH 11/15] Update changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 9ddba451..3bf5c5bc 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added +- [Introduce support for RSASSA-PSS (PS256) signature algorithm](https://github.com/ballerina-platform/ballerina-library/issues/8292) - [Introduce support for PBKDF2 password hashing and verification](https://github.com/ballerina-platform/ballerina-lang/issues/43926) - [Add static analysis rule - Encryption algorithms should be used with secure mode and padding scheme](https://github.com/ballerina-platform/ballerina-library/issues/7940) - [Add static analysis rule - Passwords should not be stored in plaintext or with a fast hashing algorithm](https://github.com/ballerina-platform/ballerina-library/issues/7950) From bec1a7daec03fecc3028a6a5b08d243111702405 Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 3 Dec 2025 12:52:42 +0530 Subject: [PATCH 12/15] Refactor signature section formatting in the specification --- docs/spec/spec.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 05bc191b..c7c1dd39 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -67,26 +67,24 @@ The conforming implementation of the specification is released and included in t - 5.2.4. [AES-GCM](#524-aes-gcm) - 5.2.5. [PGP](#525-pgp) 6. [Sign and Verify](#6-sign-and-verify) - - 6.1. [Sign messages](#61-sign-messages) - - 6.1.1. [RSA-MD5](#611-rsa-md5) - - 6.1.2. [RSA-SHA1](#612-rsa-sha1) - - 6.1.3. [RSA-SHA256](#613-rsa-sha256) - - 6.1.4. [RSA-SHA384](#614-rsa-sha384) - - 6.1.5. [RSA-SHA512](#615-rsa-sha512) - - 6.1.6. [RSASSA-PSS-SHA256](#616-rsassa-pss-sha256) - - 6.1.7. [SHA384withECDSA](#617-sha384withecdsa) - - 6.1.8. [SHA256withECDSA](#618-sha256withecdsa) - - 6.1.9. [ML-DSA-65](#619-mldsa65) - - 6.2. [Verify signature](#62-verify-signature) - - 6.2.1. [RSA-MD5](#621-rsa-md5) - - 6.2.2. [RSA-SHA1](#622-rsa-sha1) - - 6.2.3. [RSA-SHA256](#623-rsa-sha256) - - 6.2.4. [RSA-SHA384](#624-rsa-sha384) - - 6.2.5. [RSA-SHA512](#625-rsa-sha512) - - 6.2.6. [RSASSA-PSS-SHA256](#626-rsassa-pss-sha256) - - 6.2.7. [SHA384withECDSA](#627-sha384withecdsa) - - 6.2.8. [SHA256withECDSA](#628-sha256withecdsa) - - 6.2.9. [ML-DSA-65](#629-mldsa65) + * 6.1. [Sign messages](#61-sign-messages) + * 6.1.1. [RSA-MD5](#611-rsa-md5) + * 6.1.2. [RSA-SHA1](#612-rsa-sha1) + * 6.1.3. [RSA-SHA256](#613-rsa-sha256) + * 6.1.4. [RSA-SHA384](#614-rsa-sha384) + * 6.1.5. [RSA-SHA512](#615-rsa-sha512) + * 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) + * 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) + * 6.1.8. [ML-DSA-65](#618-mldsa65) + * 6.2. [Verify signature](#62-verify-signature) + * 6.2.1. [RSA-MD5](#621-rsa-md5) + * 6.2.2. [RSA-SHA1](#622-rsa-sha1) + * 6.2.3. [RSA-SHA256](#623-rsa-sha256) + * 6.2.4. [RSA-SHA384](#624-rsa-sha384) + * 6.2.5. [RSA-SHA512](#625-rsa-sha512) + * 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) + * 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) + * 6.2.8. [ML-DSA-65](#618-mldsa65) 7. [Key Derivation Function (KDF)](#7-key-derivation-function-kdf) - 7.1. [HKDF-SHA256](#71-hkdf-sha256) 8. [Key Exchange Mechanism (KEM)](#8-key-exchange-mechanism-kem) From 322347ee86f187e1eeeaaf133882855aa08fe332 Mon Sep 17 00:00:00 2001 From: Randil Tharusha <51049280+randilt@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:14:18 +0530 Subject: [PATCH 13/15] Update ballerina/sign_verify.bal Co-authored-by: Thisaru Guruge --- ballerina/sign_verify.bal | 1 - 1 file changed, 1 deletion(-) diff --git a/ballerina/sign_verify.bal b/ballerina/sign_verify.bal index 65171ac9..251462cb 100644 --- a/ballerina/sign_verify.bal +++ b/ballerina/sign_verify.bal @@ -152,7 +152,6 @@ public isolated function signRsaSha512(byte[] input, PrivateKey privateKey) retu # + privateKey - Private key used for signing # + return - The generated signature or else a `crypto:Error` if the private key is invalid public isolated function signRsaSsaPss256(byte[] input, PrivateKey privateKey) returns byte[]|Error = @java:Method { - name: "signRsaSsaPss256", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; From e64f9951c35bf9a16bbfe88bcb3dfd2db3fd8f27 Mon Sep 17 00:00:00 2001 From: Randil Tharusha <51049280+randilt@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:40:01 +0530 Subject: [PATCH 14/15] Update ballerina/sign_verify.bal Co-authored-by: DimuthuMadushan <35717653+DimuthuMadushan@users.noreply.github.com> --- ballerina/sign_verify.bal | 1 - 1 file changed, 1 deletion(-) diff --git a/ballerina/sign_verify.bal b/ballerina/sign_verify.bal index 251462cb..69d74ed4 100644 --- a/ballerina/sign_verify.bal +++ b/ballerina/sign_verify.bal @@ -359,7 +359,6 @@ public isolated function verifyRsaSha512Signature(byte[] data, byte[] signature, # + return - Validity of the signature or else a `crypto:Error` if the public key is invalid public isolated function verifyRsaSsaPss256Signature(byte[] data, byte[] signature, PublicKey publicKey) returns boolean|Error = @java:Method { - name: "verifyRsaSsaPss256Signature", 'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign" } external; From 1d6bcdd6b80f782c819f9317844a116a137f6c0a Mon Sep 17 00:00:00 2001 From: Randil Date: Wed, 3 Dec 2025 12:52:42 +0530 Subject: [PATCH 15/15] Refactor signature section formatting in the specification --- docs/spec/spec.md | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 05bc191b..c7c1dd39 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -67,26 +67,24 @@ The conforming implementation of the specification is released and included in t - 5.2.4. [AES-GCM](#524-aes-gcm) - 5.2.5. [PGP](#525-pgp) 6. [Sign and Verify](#6-sign-and-verify) - - 6.1. [Sign messages](#61-sign-messages) - - 6.1.1. [RSA-MD5](#611-rsa-md5) - - 6.1.2. [RSA-SHA1](#612-rsa-sha1) - - 6.1.3. [RSA-SHA256](#613-rsa-sha256) - - 6.1.4. [RSA-SHA384](#614-rsa-sha384) - - 6.1.5. [RSA-SHA512](#615-rsa-sha512) - - 6.1.6. [RSASSA-PSS-SHA256](#616-rsassa-pss-sha256) - - 6.1.7. [SHA384withECDSA](#617-sha384withecdsa) - - 6.1.8. [SHA256withECDSA](#618-sha256withecdsa) - - 6.1.9. [ML-DSA-65](#619-mldsa65) - - 6.2. [Verify signature](#62-verify-signature) - - 6.2.1. [RSA-MD5](#621-rsa-md5) - - 6.2.2. [RSA-SHA1](#622-rsa-sha1) - - 6.2.3. [RSA-SHA256](#623-rsa-sha256) - - 6.2.4. [RSA-SHA384](#624-rsa-sha384) - - 6.2.5. [RSA-SHA512](#625-rsa-sha512) - - 6.2.6. [RSASSA-PSS-SHA256](#626-rsassa-pss-sha256) - - 6.2.7. [SHA384withECDSA](#627-sha384withecdsa) - - 6.2.8. [SHA256withECDSA](#628-sha256withecdsa) - - 6.2.9. [ML-DSA-65](#629-mldsa65) + * 6.1. [Sign messages](#61-sign-messages) + * 6.1.1. [RSA-MD5](#611-rsa-md5) + * 6.1.2. [RSA-SHA1](#612-rsa-sha1) + * 6.1.3. [RSA-SHA256](#613-rsa-sha256) + * 6.1.4. [RSA-SHA384](#614-rsa-sha384) + * 6.1.5. [RSA-SHA512](#615-rsa-sha512) + * 6.1.6. [SHA384withECDSA](#616-sha384withecdsa) + * 6.1.7. [SHA256withECDSA](#617-sha256withecdsa) + * 6.1.8. [ML-DSA-65](#618-mldsa65) + * 6.2. [Verify signature](#62-verify-signature) + * 6.2.1. [RSA-MD5](#621-rsa-md5) + * 6.2.2. [RSA-SHA1](#622-rsa-sha1) + * 6.2.3. [RSA-SHA256](#623-rsa-sha256) + * 6.2.4. [RSA-SHA384](#624-rsa-sha384) + * 6.2.5. [RSA-SHA512](#625-rsa-sha512) + * 6.2.6. [SHA384withECDSA](#626-sha384withecdsa) + * 6.2.7. [SHA256withECDSA](#627-sha256withecdsa) + * 6.2.8. [ML-DSA-65](#618-mldsa65) 7. [Key Derivation Function (KDF)](#7-key-derivation-function-kdf) - 7.1. [HKDF-SHA256](#71-hkdf-sha256) 8. [Key Exchange Mechanism (KEM)](#8-key-exchange-mechanism-kem)