Post-quantum cryptography is moving from research into real software stacks.
That matters for Python developers because cryptographic libraries, TLS implementations, APIs, certificates, and application-level signatures will eventually need to deal with algorithms designed to resist attacks from large-scale quantum computers.
One of the important algorithms in this area is ML-DSA, the post-quantum digital signature algorithm standardized in FIPS 204.
Python 3.15 is interesting here because its SSL layer adds better controls for TLS signature algorithms and key-exchange groups, including support for post-quantum groups when the underlying OpenSSL version provides them. At the same time, application-level ML-DSA signing can be tested through libraries such as cryptography, which provides MLDSA44, MLDSA65, and MLDSA87 key types.
There is an important distinction, though.
Python 3.15 does not mean that this works:
import ssl
ssl.mldsa_sign(...)
There is no such standard-library API.
Instead, a Python application can have two separate post-quantum concerns:
TLS communication
↓
Python ssl + OpenSSL
Application signatures
↓
cryptography + OpenSSL
Understanding that separation is important before testing ML-DSA in a real application.
What Is ML-DSA?
ML-DSA is a post-quantum digital signature algorithm based on module lattices.
It is standardized as part of FIPS 204 and is intended to provide digital signatures that remain secure against attackers with sufficiently powerful quantum computers.
Digital signatures are commonly used for:
Authentication
Software signing
Document signing
API request verification
Certificates
Secure update systems
Identity systems
A simplified signature workflow looks like this:
Private key
↓
Sign message
↓
Signature
↓
Message + Signature
↓
Public key
↓
Verify
ML-DSA follows the same high-level idea.
The difference is the underlying cryptographic construction.
ML-DSA Has Three Parameter Sets
ML-DSA defines three main parameter sets:
ML-DSA-44
ML-DSA-65
ML-DSA-87
OpenSSL 3.5 implements all three. They correspond to different security categories.
A simplified comparison is:
Algorithm | Security Category | Signature Size |
|---|---|---|
ML-DSA-44 | 2 | ~2.4 KB |
ML-DSA-65 | 3 | ~3.3 KB |
ML-DSA-87 | 5 | ~4.6 KB |
The exact sizes depend on the format and implementation details, but the important point is that ML-DSA signatures are substantially larger than many traditional signatures.
That can matter for APIs, tokens, certificates, network traffic, and storage.
Why Python Developers Should Care
Suppose an application currently uses:
RSA
ECDSA
Ed25519
for digital signatures.
Moving to post-quantum signatures is not simply a matter of changing:
algorithm = "RSA"
to:
algorithm = "ML-DSA"
You need to think about:
Key generation
Key storage
Serialization
Signature size
Verification cost
Protocol compatibility
TLS support
Library support
Certificate support
Client and server interoperability
Hardware security modules
Deployment environments
The Python application is only one part of the system.
Python 3.15 and OpenSSL
Python's ssl module is built around OpenSSL on supported CPython builds.
Python 3.15 expands the SSL API with methods such as:
SSLContext.set_groups()
SSLContext.get_groups()
SSLSocket.group()
These APIs allow applications to inspect and configure the groups used during TLS key agreement.
Python's documentation notes that these groups can include post-quantum groups when the underlying OpenSSL version supports them. Some of the APIs require OpenSSL 3.2 or later, while get_groups() requires OpenSSL 3.5 or later.
Python 3.15 also adds APIs for controlling TLS signature algorithms:
ssl.get_sigalgs()
SSLContext.set_client_sigalgs()
SSLContext.set_server_sigalgs()
SSLSocket.client_sigalg()
SSLSocket.server_sigalg()
These APIs make it easier to inspect and control which signature algorithms can be used during TLS authentication.
This is useful for post-quantum migration testing.
Check Your OpenSSL Version First
Before testing anything, check the OpenSSL version used by Python.
Run:
import ssl
print(ssl.OPENSSL_VERSION)
You can also inspect:
import ssl
print(ssl.OPENSSL_VERSION_INFO)
For command-line testing:
openssl version
This matters because Python's capabilities depend partly on the OpenSSL library available to the interpreter.
For example, OpenSSL 3.5 provides native ML-DSA key generation and signing support.
A Python installation linked against a different OpenSSL version may not expose the same capabilities.
Testing ML-DSA at the Application Level
For application-level signatures, the cryptography package currently provides ML-DSA support.
For example:
from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA65PrivateKey
private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()
message = b"Hello from Python"
signature = private_key.sign(message)
public_key.verify(signature, message)
print("Signature verified")
This is a normal sign-and-verify workflow.
The private key creates the signature.
The public key verifies it.
The verification call raises an exception if the signature is invalid.
Why ML-DSA-65 Is a Useful Test Starting Point
If you are experimenting with the three parameter sets, ML-DSA-65 is a reasonable starting point because it sits between ML-DSA-44 and ML-DSA-87.
For example:
from cryptography.hazmat.primitives.asymmetric.mldsa import (
MLDSA65PrivateKey
)
private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()
message = b"production test message"
signature = private_key.sign(message)
print("Signature bytes:", len(signature))
public_key.verify(signature, message)
print("Verification successful")
This gives you two useful measurements:
Signature generation
Signature size
You should also measure verification time.
Test All Three Parameter Sets
If your application needs to choose an ML-DSA security level, test all three.
For example:
from cryptography.hazmat.primitives.asymmetric.mldsa import (
MLDSA44PrivateKey,
MLDSA65PrivateKey,
MLDSA87PrivateKey,
)
for key_type in (
MLDSA44PrivateKey,
MLDSA65PrivateKey,
MLDSA87PrivateKey,
):
private_key = key_type.generate()
public_key = private_key.public_key()
message = b"test message"
signature = private_key.sign(message)
public_key.verify(signature, message)
print(
key_type.__name__,
len(signature)
)
This is a much better starting point than choosing an algorithm solely because its name sounds stronger.
Measure the behavior that matters to your application.
Signature Size Matters
One of the biggest practical differences with post-quantum signatures is size.
A traditional Ed25519 signature is only 64 bytes.
ML-DSA signatures are much larger.
For example, current cryptography documentation lists approximately:
ML-DSA-44: 2420 bytes
ML-DSA-65: 3309 bytes
ML-DSA-87: 4627 bytes
for the signature sizes.
That difference becomes important if signatures are included in:
JWT-like tokens
HTTP headers
API requests
Database records
Messages
Certificates
Signed URLs
Software packages
A design that works comfortably with a 64-byte signature may behave very differently with a signature several kilobytes long.
Benchmark Signing and Verification
Do not benchmark just one operation.
Measure:
Key generation
Signing
Verification
Serialization
Deserialization
For example:
import time
from cryptography.hazmat.primitives.asymmetric.mldsa import (
MLDSA65PrivateKey
)
private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()
message = b"benchmark message"
start = time.perf_counter()
for _ in range(1000):
signature = private_key.sign(message)
sign_time = time.perf_counter() - start
start = time.perf_counter()
for _ in range(1000):
public_key.verify(signature, message)
verify_time = time.perf_counter() - start
print(f"Signing: {sign_time:.4f}s")
print(f"Verification: {verify_time:.4f}s")
This is only a basic benchmark.
For production decisions, run multiple iterations and test realistic message sizes.
Test Different Message Sizes
A signature system should not be benchmarked using only:
message = b"hello"
Test several sizes:
1 KB
10 KB
100 KB
1 MB
10 MB
For ML-DSA, the signature operation internally handles the message according to the algorithm's defined encoding rules. OpenSSL also exposes context and message-encoding controls.
For large application data, you should also consider whether signing the entire payload is actually necessary.
A common architecture is:
Large document
↓
Hash
↓
Sign digest or protocol-defined representation
But do not invent your own ML-DSA pre-hashing scheme. Follow the API and protocol specification you are implementing.
Context Strings
ML-DSA supports an optional context string.
For example:
from cryptography.hazmat.primitives.asymmetric.mldsa import (
MLDSA65PrivateKey
)
private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()
message = b"payment request"
context = b"payment-api-v1"
signature = private_key.sign(
message,
context
)
public_key.verify(
signature,
message,
context
)
The same context must be used during verification.
The current cryptography API limits the context string to 255 bytes.
Context strings are useful when the same key is used for different protocol purposes.
For example:
payment-api-v1
document-signing-v1
software-update-v1
This can help prevent a signature created for one purpose from being accidentally accepted in another context.
The exact context design should come from the protocol being implemented.
What Happens If the Message Changes?
A digital signature should fail verification if the signed message is changed.
For example:
from cryptography.hazmat.primitives.asymmetric.mldsa import (
MLDSA65PrivateKey
)
private_key = MLDSA65PrivateKey.generate()
public_key = private_key.public_key()
message = b"amount=100"
signature = private_key.sign(message)
public_key.verify(
signature,
b"amount=100"
)
print("Valid")
But:
public_key.verify(
signature,
b"amount=1000"
)
should fail.
This is one of the first negative tests you should add to an application.
Test Corrupted Signatures
Do not test only successful verification.
Test failure cases too.
For example:
from cryptography.exceptions import InvalidSignature
try:
public_key.verify(
signature,
b"tampered message"
)
except InvalidSignature:
print("Tampering detected")
Also test:
Modified signature
Wrong public key
Wrong context
Modified message
Truncated signature
Empty message
Large message
These tests are more useful than simply proving that a valid signature works.
Test Key Serialization
Applications rarely keep a private key as a Python object forever.
Keys usually need to be stored or transferred using a standard encoding.
The cryptography package supports key serialization, including PEM and DER representations for supported key types.
A production test should therefore include:
Generate key
↓
Serialize key
↓
Store / load
↓
Deserialize
↓
Sign
↓
Verify
Do not assume that because signing works in memory, your complete key-management workflow works.
OpenSSL Can Test ML-DSA Independently
It is useful to test ML-DSA outside Python as well.
OpenSSL 3.5 provides ML-DSA key generation:
openssl genpkey -algorithm ML-DSA-65 -out mldsa65.pem
You can then sign:
openssl pkeyutl \
-sign \
-in message.txt \
-inkey mldsa65.pem \
-out signature.bin
And verify:
openssl pkeyutl \
-verify \
-in message.txt \
-inkey mldsa65.pem \
-sigfile signature.bin
OpenSSL documents ML-DSA-44, ML-DSA-65, and ML-DSA-87 support through its EVP interfaces and pkeyutl command.
This gives you a useful interoperability test:
Python
↓
Generate/sign
↓
OpenSSL
↓
Verify
and then:
OpenSSL
↓
Generate/sign
↓
Python
↓
Verify
For a real migration, cross-implementation testing is extremely valuable.
Testing TLS Is a Separate Problem
Application-level ML-DSA signatures and TLS authentication are related, but they are not the same thing.
For application signing:
Python application
↓
ML-DSA
↓
Signature
For TLS:
Python application
↓
ssl
↓
OpenSSL
↓
TLS handshake
↓
Negotiated groups / signature algorithms
Python 3.15 adds APIs that help inspect and control the TLS negotiation.
For example:
import ssl
print(ssl.get_sigalgs())
The available results depend on the OpenSSL version and enabled providers.
Inspect TLS Groups
Python 3.15 adds:
context.get_groups()
when supported by the underlying OpenSSL version.
For example:
import ssl
context = ssl.create_default_context()
print(context.get_groups())
If the OpenSSL version does not provide the required functionality, the method may not be available in the expected form.
The key point is that Python is exposing the TLS controls while OpenSSL supplies the cryptographic implementation.
Python's documentation specifically notes OpenSSL version requirements for these APIs.
Configure TLS Groups Carefully
Python 3.15 provides:
context.set_groups(...)
This allows an application to control the groups used for TLS key agreement.
For example:
import ssl
context = ssl.create_default_context()
context.set_groups([
"X25519",
"secp256r1",
])
The exact group names available depend on the OpenSSL version and configuration.
For post-quantum testing, do not copy a group name from one environment and assume it exists everywhere.
First inspect the available groups:
print(context.get_groups())
Then select a configuration supported by both endpoints.
TLS Signature Algorithms Are Different From TLS Groups
This distinction is easy to miss.
A TLS group is related to key agreement.
A TLS signature algorithm is related to authentication signatures.
Python 3.15 exposes separate APIs for these.
For example:
context.set_groups(...)
controls groups.
While:
context.set_client_sigalgs(...)
context.set_server_sigalgs(...)
control signature algorithms used for certificate-based authentication.
A post-quantum TLS deployment may involve both categories.
Do not treat them as interchangeable.
Hybrid TLS Is an Important Migration Strategy
Post-quantum migration does not necessarily mean replacing every classical algorithm immediately.
A practical transition can use hybrid mechanisms where classical and post-quantum cryptography are used together.
For example:
Classical mechanism
+
Post-quantum mechanism
↓
Hybrid protection
This allows organizations to test post-quantum interoperability while retaining established algorithms during the transition.
The exact hybrid groups and protocol combinations depend on the TLS implementation and the current standards supported by the endpoints.
Python 3.15's expanded group APIs make this area easier to inspect and configure, but the actual available post-quantum groups come from the underlying TLS implementation.
Check Both Ends of the TLS Connection
A common testing mistake is to update only the client.
For example:
Python 3.15 client
↓
Old TLS server
If the server does not support the selected group or signature algorithm, negotiation can fail.
A proper test uses:
Modern client
↓
Modern server
and then tests compatibility against:
Modern client
↓
Existing server
This tells you whether the new cryptographic configuration works in both controlled and mixed environments.
Test TLS Negotiation Instead of Assuming It
After a TLS connection succeeds, Python 3.15 provides methods for inspecting the negotiated group and signature algorithm.
For example:
with socket.create_connection(
("example.internal", 443)
) as sock:
with context.wrap_socket(
sock,
server_hostname="example.internal"
) as tls:
print("Cipher:", tls.cipher())
print("Group:", tls.group())
print("Server signature:", tls.server_sigalg())
The exact availability of these methods depends on the Python/OpenSSL combination. The Python 3.15 SSL documentation specifies OpenSSL requirements for them.
This is much better than assuming that because the handshake succeeded, the expected cryptographic mechanism was actually negotiated.
Why Certificate Compatibility Matters
TLS authentication normally involves certificates.
Post-quantum signatures create a much larger compatibility question:
Client
↓
TLS handshake
↓
Server certificate
↓
Certificate signature
↓
Signature algorithm
Not every existing certificate-management system supports post-quantum public-key algorithms.
Even if OpenSSL can perform ML-DSA operations, your certificate authority, load balancer, reverse proxy, cloud service, or hardware security module may not support the same workflow.
That is why a post-quantum migration should be tested across the complete infrastructure rather than only inside Python.
ML-DSA Is Not ML-KEM
Another common mistake is mixing up signature and key-establishment algorithms.
ML-DSA is used for:
Digital signatures
Authentication
ML-KEM is used for:
Key encapsulation
Key establishment
They solve different problems.
A simplified TLS picture is:
ML-KEM
↓
Establish shared secret
ML-DSA
↓
Authenticate signatures
Do not replace one with the other.
A post-quantum TLS design may need both types of cryptographic mechanisms.
Testing Application APIs
Suppose your API currently signs requests.
A traditional design might look like:
Client
↓
Request body
↓
Signature
↓
Server
↓
Verify signature
To test ML-DSA, keep the protocol structure the same:
Client
↓
Request body
↓
ML-DSA signature
↓
Server
↓
ML-DSA verification
Then measure:
Request size
Signature size
Signing time
Verification time
Latency
Throughput
This tells you whether the algorithm is practical for the API rather than simply proving that the cryptographic operation works.
Be Careful With Headers
Large signatures can create practical problems when placed in HTTP headers.
For example:
X-Signature: <large signature>
may work in a test environment.
But real systems have limits around:
Reverse proxies
Load balancers
Web servers
API gateways
Frameworks
Header sizes
A signature that is several kilobytes long should therefore be tested through the complete HTTP stack.
In many systems, placing the signature in the request body may be a better protocol design.
The correct choice depends on the API.
Test Storage Requirements
Post-quantum keys and signatures can be larger than their classical counterparts.
This can affect:
Database columns
Cache entries
Message queues
Object storage
Audit logs
Token size
Network traffic
For example, if your database currently stores:
signature VARBINARY(256)
that schema may be too small for ML-DSA signatures.
Do not change the schema based only on the algorithm name.
Measure the actual serialized key and signature sizes for the exact format your application uses.
Do Not Benchmark Only Cryptographic Operations
A microbenchmark may show:
ML-DSA signing = X ms
but that is not enough.
The actual application cost may include:
Serialize payload
↓
Generate signature
↓
Encode signature
↓
Build HTTP request
↓
Network transfer
↓
Server parsing
↓
Decode signature
↓
Verify
For APIs, benchmark the complete workflow.
This is where larger post-quantum signatures can become noticeable.
Security Testing Checklist
When evaluating ML-DSA, test more than performance.
Valid Signature
Correct key
Correct message
Correct context
must verify successfully.
Modified Message
Original signature
Modified message
must fail.
Modified Signature
Modified signature
Original message
must fail.
Wrong Public Key
The signature must not verify with another key.
Wrong Context
A signature created with one context must not verify under another context.
Serialization Round Trip
Generate
→ Serialize
→ Load
→ Sign
→ Verify
must work.
Cross-Implementation
Test:
Python → OpenSSL
OpenSSL → Python
when your chosen formats and libraries support the required interoperability.
Common Mistakes
Assuming Python 3.15 Has a Built-In ML-DSA API
It does not provide a high-level MLDSA... signing API in the standard library.
For application-level signing, use a cryptographic library that supports ML-DSA.
Ignoring the OpenSSL Version
Python's SSL capabilities depend heavily on the underlying OpenSSL version.
Confusing ML-DSA With ML-KEM
They solve different cryptographic problems.
Testing Only Key Generation
A generated key does not prove that your complete application workflow works.
Ignoring Signature Size
Several-kilobyte signatures can affect APIs, storage, and network traffic.
Testing Only One TLS Endpoint
Both sides need compatible cryptographic capabilities.
Hard-Coding Algorithm Names
Available algorithms depend on the Python build, OpenSSL version, providers, and configuration.
Inspect capabilities at runtime where appropriate.
Designing Your Own Cryptographic Protocol
Do not invent a custom signature format or hybrid scheme simply to experiment with ML-DSA.
Use established standards and well-reviewed libraries.
A Practical Migration Plan
If you are considering post-quantum signatures for an existing Python application, start with a small test environment.
Phase 1: Inventory
List where your application currently uses public-key cryptography:
TLS
API authentication
JWT-like tokens
Document signing
Software updates
Certificates
Identity systems
Phase 2: Dependency Check
Record:
Python version
OpenSSL version
cryptography version
TLS proxy
Load balancer
Certificate authority
HSM/KMS
Phase 3: Application Test
Implement ML-DSA signing and verification using a supported library.
Measure:
Signing
Verification
Key generation
Signature size
Serialization
Phase 4: TLS Test
Check:
Available groups
Available signature algorithms
Negotiated group
Negotiated signature algorithm
Certificate compatibility
Phase 5: Interoperability
Test multiple clients and servers.
Do not stop after one successful connection.
Phase 6: Production-Like Load Test
Measure:
Latency
Throughput
CPU
Memory
Network bandwidth
Request size
Failure rate
Phase 7: Gradual Rollout
If the results are good, introduce the new cryptographic configuration gradually.
Keep a fallback path until interoperability is proven.
Final Thoughts
Post-quantum cryptography is not something that can be adopted by changing one line of Python code.
ML-DSA is a good example of why.
At the application layer, you need a library that exposes the algorithm and a secure key-management strategy. At the TLS layer, you need compatible OpenSSL capabilities, supported TLS groups and signature algorithms, compatible certificates, and endpoints that can actually negotiate the configuration.
Python 3.15 improves the TLS side by exposing more control over groups and signature algorithms. That is useful because developers can inspect what the TLS stack is doing instead of treating the handshake as a black box.
For application-level signatures, current cryptography releases provide direct ML-DSA APIs for the three standardized parameter sets.
The safest approach is to treat ML-DSA as an engineering migration rather than simply a cryptographic experiment.
Start with:
Can I generate a key?
Can I sign?
Can I verify?
Can I serialize the key?
Can another implementation verify it?
Can my TLS stack negotiate the required algorithms?
Can my infrastructure handle the larger keys and signatures?
Can my application handle the performance and network cost?
Once those questions have good answers, you have something much more valuable than a successful demo: a realistic understanding of whether post-quantum signatures fit your application.
Summary
ML-DSA is a post-quantum digital signature algorithm standardized in FIPS 204. Python 3.15 is relevant to post-quantum testing because its ssl module provides more control over TLS groups and signature algorithms, while the underlying OpenSSL version determines which cryptographic capabilities are actually available.
For application-level signing, a library such as cryptography can provide ML-DSA-44, ML-DSA-65, and ML-DSA-87.
Before using ML-DSA in production, test more than signing and verification. Check key serialization, signature sizes, API payloads, TLS negotiation, certificates, interoperability, CPU usage, network overhead, and infrastructure compatibility.
The main lesson is simple: post-quantum readiness is a system-level problem, not just a Python code change.

Join the conversation! Your thoughts help the community grow.