Skip to content
56 changes: 49 additions & 7 deletions ballerina/sign_verify.bal
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,25 @@ 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 {
'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign"
} external;

# Returns the SHA384withECDSA based signature value for the given data.
# ```ballerina
# string input = "Hello Ballerina";
Expand Down Expand Up @@ -195,7 +214,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;
Expand All @@ -219,7 +238,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;
Expand Down Expand Up @@ -267,7 +286,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;
Expand All @@ -291,7 +310,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;
Expand All @@ -315,11 +334,34 @@ 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 {
'class: "io.ballerina.stdlib.crypto.nativeimpl.Sign"
} external;

# Verifies the SHA384withECDSA based signature.
# ```ballerina
# string input = "Hello Ballerina";
Expand All @@ -339,7 +381,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;
Expand All @@ -363,7 +405,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;
47 changes: 42 additions & 5 deletions ballerina/tests/sign_verify_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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:"));
Expand All @@ -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:"));
Expand All @@ -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:"));
Expand All @@ -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:"));
Expand All @@ -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:"));
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
94 changes: 94 additions & 0 deletions docs/proposals/rsassa-pss-signature-support.md
Original file line number Diff line number Diff line change
@@ -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.
Loading