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.