Hybrid Post-Quantum Digital Signatures with Ballerina
- Udara Pathum
- Senior Software Engineer, WSO2
As highlighted in the Quantum-Safeness of WSO2 Products blog, the need for alternative digital signature algorithms has become increasingly urgent. While post-quantum (Q) algorithms are gaining traction, they require time to build industry-wide trust. To address this, hybrid post-quantum digital signatures have emerged, combining classical (C) cryptographic methods with post-quantum (Q) algorithms to ensure resilience against both classical and quantum threats.
The Ballerina Swan Lake Update 12, support for FIPS-standardized post-quantum algorithms, including ML-DSA for digital signatures, has been introduced. This update enables the implementation of hybrid post-quantum digital signatures in Ballerina, strengthening security in cryptographic applications. In this article, we will delve into the concept of hybrid post-quantum digital signatures and demonstrate how Ballerina simplifies their adoption.
Hybrid Post-Quantum and Classical Signatures
Hybrid post-quantum digital signatures integrate traditional cryptographic algorithms such as RSA or ECDSA with post-quantum alternatives like ML-DSA, Falcon, or SPHINCS+. This approach ensures security against both classical computers and potential quantum threats.
Hybrid post-quantum digital signatures can be implemented in two primary ways:
- Parallel Hybrid Signatures (C + Q) – The payload is signed independently using both a classical and a post-quantum algorithm, and the resulting signatures are combined.
- Nested Hybrid Signatures (C & Q) – The payload is signed first using a post-quantum algorithm, and then the resulting signature is signed again using a classical algorithm.
Each approach has distinct advantages and trade-offs in terms of security and performance. The following sections provide a detailed breakdown of both, using RSA and ML-DSA as examples, along with sample implementations in Ballerina.
Key Pair Generation
Separate key pairs need to be generated for RSA and ML-DSA.
RSA: You can use either OpenSSL or keytool to generate an RSA key pair. Below is a common method using OpenSSL:
# Generate an RSA private key and a self-signed certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem
# Export the key and certificate into a PKCS#12 keystore
openssl pkcs12 -export -out rsa_keystore.p12 -inkey key.pem -in cert.pem -name "alias"MLDSA: To generate an ML-DSA key pair, you need OpenSSL 3.x with the OQS provider installed.
# Generate an ML-DSA private key openssl genpkey -algorithm mldsa65 -out mldsa_key.pem -aes256 # Generate a self-signed certificate openssl req -new -x509 -key mldsa_key.pem -out mldsa-cert.crt -days 365 # Export the key and certificate into a PKCS#12 keystore openssl pkcs12 -export -inkey mldsa_key.pem -in mldsa-cert.crt -out mldsa-keystore.pkcs12 -name alias
Parallel Hybrid Signatures (C + Q)
In the parallel hybrid approach, the payload is signed separately using RSA and ML-DSA. Both signatures are generated independently and then combined into a single hybrid signature.
Signing Process
- The original message is signed with an ML-DSA private key, generating Sig_MLDSA.
- The same message is signed with an RSA private key, producing Sig_RSA.
- The final hybrid signature is {Sig_MLDSA, Sig_RSA}.

Figure 1: Parallel Hybrid Signing
Verification Process
- The received hybrid signature contains {Sig_MLDSA, Sig_RSA}.
- The verifier extracts and verifies Sig_MLDSA using the ML-DSA public key.
- The verifier extracts and verifies Sig_RSA using the RSA public key.
- If both verifications pass, the signature is valid.

Figure 2: Parallel Hybrid Verification
import ballerina/crypto;
import ballerina/io;
configurable string mlDsaKeystore = "../keystores/mldsa-keystore.p12";
configurable string rsaKeystore = "../keystores/rsa-keystore.p12";
configurable string mlDsaCertFile = "../keystores/mldsa-cert.pem";
configurable string rsaCertFile = "../keystores/rsa-cert.pem";
configurable string keystorePassword = ?;
configurable string alias = "alias";
function signWithMlDsa65(byte[] input) returns byte[]|error {
crypto:KeyStore keyStore = {
path: mlDsaKeystore,
password: keystorePassword
};
crypto:PrivateKey privateKey = check crypto:decodeMlDsa65PrivateKeyFromKeyStore(keyStore, alias, keystorePassword);
return check crypto:signMlDsa65(input, privateKey);}
function verifyWithMlDsa65(byte[] input, byte[] signature) returns boolean|error {
crypto:PublicKey publicKey = check crypto:decodeMlDsa65PublicKeyFromCertFile(mlDsaCertFile);
return crypto:verifyMlDsa65Signature(input, signature, publicKey);
}
function signWithRsa(byte[] input) returns byte[]|error {
crypto:KeyStore keyStore = {
path: rsaKeystore,
password: keystorePassword
};
crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, alias, keystorePassword);
return check crypto:signRsaSha256(input, privateKey);
}
function verifyWithRsa(byte[] input, byte[] signature) returns boolean|error {
crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromCertFile(rsaCertFile);
return crypto:verifyRsaSha256Signature(input, signature, publicKey);
}
public function main() returns error? {
byte[] input = "Hello Ballerina".toBytes();
byte[] sigMlDsa = check signWithMlDsa65(input);
io:println("ML DSA Signature: ", sigMlDsa.toBase64());
byte[] sigRsa = check signWithRsa(input);
io:println("RSA Signature: ", sigRsa.toBase64());
boolean isVerified = check verifyWithMlDsa65(input, sigMlDsa);
io:println("ML DSA Signature Verification: ", isVerified);
isVerified = check verifyWithRsa(input, sigRsa);
io:println("RSA Signature Verification: ", isVerified);}Sample Code for Parallel Signing and Verification
Nested Hybrid Signatures (C & Q)
In the nested hybrid approach, ML-DSA is signed first, and then the resulting signature is signed again using RSA.
Signing Process
- The original message is first signed with ML-DSA, generating Sig_MLDSA.
- The original message along with the generated Sig_MLDSA is then signed using RSA, producing Sig_RSA(Sig_MLDSA).
- The final hybrid signature is {Sig_RSA(Sig_MLDSA), Sig_MLDSA}.

Figure 3: Nested Hybrid Signing
Verification Process
- The received hybrid signature contains {Sig_MLDSA(RSA), Sig_RSA}.
- The verifier first checks Sig_MLDSA(RSA) using the ML-DSA public key.
- The verifier then checks Sig_RSA using the RSA public key.
- If both verifications pass, the signature is valid.

Figure 4: Nested Hybrid Verification
import ballerina/crypto;
import ballerina/io;
configurable string mlDsaKeystore = "../keystores/mldsa-keystore.p12";
configurable string rsaKeystore = "../keystores/rsa-keystore.p12";
configurable string mlDsaCertFile = "../keystores/mldsa-cert.pem";
configurable string rsaCertFile = "../keystores/rsa-cert.pem";
configurable string keystorePassword = ?;
configurable string alias = "alias";
function signWithMlDsa65(byte[] input) returns byte[]|error {
crypto:KeyStore keyStore = {
path: mlDsaKeystore,
password: keystorePassword
};
crypto:PrivateKey privateKey = check crypto:decodeMlDsa65PrivateKeyFromKeyStore(keyStore, alias, keystorePassword);
return check crypto:signMlDsa65(input, privateKey);
}
function verifyWithMlDsa65(byte[] input, byte[] signature) returns boolean|error {
crypto:PublicKey publicKey = check crypto:decodeMlDsa65PublicKeyFromCertFile(mlDsaCertFile);
return crypto:verifyMlDsa65Signature(input, signature, publicKey);
}
function signWithRsa(byte[] input) returns byte[]|error {
crypto:KeyStore keyStore = {
path: rsaKeystore,
password: keystorePassword
};
byte[] data = input;
crypto:PrivateKey privateKey = check crypto:decodeRsaPrivateKeyFromKeyStore(keyStore, alias, keystorePassword);
return check crypto:signRsaSha256(data, privateKey);}
function verifyWithRsa(byte[] input, byte[] signature) returns boolean|error {
crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromCertFile(rsaCertFile);
return crypto:verifyRsaSha256Signature(input, signature, publicKey);
}
public function main() returns error? {
byte[] input = "Hello Ballerina".toBytes();
byte[] sigMlDsa = check signWithMlDsa65(input);
io:println("ML DSA Signature: ", sigMlDsa.toBase64());
byte[] sigRsaMlDsa = check signWithRsa([...input, ...sigMlDsa]);
io:println("RSA Signature: ", sigRsaMlDsa.toBase64());
boolean isVerified = check verifyWithMlDsa65(input, sigMlDsa);
io:println("ML DSA Signature Verification: ", isVerified);
isVerified = check verifyWithRsa([...input, ...sigMlDsa], sigRsaMlDsa);
io:println("RSA Signature Verification: ", isVerified);
}Sample Code for Nested Signing and Verification
Hybrid post-quantum digital signatures provide a robust solution for securing data against both classical and quantum threats. Ballerina simplifies their implementation, enabling developers to seamlessly adopt future-proof cryptography while maintaining compatibility with existing systems.
For further details on the code implementation, visit the GitHub repository.