Go to home page
 

Hybrid Post-Quantum Digital Signatures with Ballerina

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

  1. The original message is signed with an ML-DSA private key, generating Sig_MLDSA.
  2. The same message is signed with an RSA private key, producing Sig_RSA.
  3. The final hybrid signature is {Sig_MLDSA, Sig_RSA}.


Figure 1: Parallel Hybrid Signing

 

Verification Process

  1. The received hybrid signature contains {Sig_MLDSA, Sig_RSA}.
  2. The verifier extracts and verifies Sig_MLDSA using the ML-DSA public key.
  3. The verifier extracts and verifies Sig_RSA using the RSA public key.
  4. 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

  1. The original message is first signed with ML-DSA, generating Sig_MLDSA.
  2. The original message along with the generated Sig_MLDSA is then signed using RSA, producing Sig_RSA(Sig_MLDSA).
  3. The final hybrid signature is {Sig_RSA(Sig_MLDSA), Sig_MLDSA}.


Figure 3: Nested Hybrid Signing

 

Verification Process

  1. The received hybrid signature contains {Sig_MLDSA(RSA), Sig_RSA}.
  2. The verifier first checks Sig_MLDSA(RSA) using the ML-DSA public key.
  3. The verifier then checks Sig_RSA using the RSA public key.
  4. 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.