10. 09. 2026 Francesco Belacca Development

Measuring Post-Quantum Security in Go and .NET

TL;DR

Results: GitHub Actions run 32970330873 at commit 1846eb4a863015cbc0a3c75431b42d138b8706c3

This experiment measures two different workloads in Go 1.27 and .NET 10/.NET 11(preview):

  • Raw crypto: one ML-KEM-768 encapsulation/decapsulation round trip versus one P-256 ECDH shared-secret round trip.
  • TLS: a fresh authenticated TLS 1.3 loopback connection using X25519 versus hybrid X25519MLKEM768, with ECDSA P-256 authentication, the same payload, and the same connection flow.

It used one ubuntu-24.04 runner, 20 independent process samples, and 100 measured operations per sample. For TLS, the cleanest comparison is classical versus hybrid within the same implementation and, for .NET, the same OpenSSL provider row. Absolute times across Go and .NET are implementation-specific measurements, not a general language speed ranking.

The clearest result is Go’s TLS overhead. Go’s hybrid median was 1.049 ms versus 0.789 ms for X25519: a +33.0% increase (1.33x). Its low relative standard deviation (1.2% hybrid, 2.0% classical) makes this a clear signal for this Go stack. This is the most relevant result for the cost of accepting fresh post-quantum TLS connections.

The .NET TLS result is mainly a support result, not a performance verdict. All four .NET/OpenSSL combinations supported raw ML-KEM-768 and hybrid X25519MLKEM768 TLS. Their hybrid/classical medians changed by -1.4% to +3.2%, but observed variability was larger (1.7% to 11.8%), so this run does not establish a .NET hybrid speedup or slowdown.

Raw crypto is informative but narrower. Go ML-KEM was +12.7% over P-256 ECDH (0.159 versus 0.141 ms). In .NET, ML-KEM was faster than ECDH in these operation-level measurements (0.082 to 0.085 versus 0.169 to 0.214 ms), but some .NET raw samples were highly variable. These timings do not predict full TLS cost, and ML-KEM and ECDH are different primitives.

The hybrid Go handshake also allocated about 71.4% more bytes and 2.7% more allocations than classical TLS. Build times and the Go 1.27-versus-1.26 cross-run differences are useful pipeline observations, but not language or release rankings.

The TLS measurement still uses classical ECDSA authentication and loopback networking, so it is evidence about session-key establishment in this setup, not a fully post-quantum deployment or a prediction of WAN performance.

Also I’m open to critics but please, stay classy.
This is not my bread and butter.

Why Measure Both Raw Crypto and TLS?

ML-KEM and ECDH establish shared secrets, but they are not interchangeable primitives. A raw benchmark locates operation cost; it does not predict the cost of a full TLS handshake. TLS adds certificate processing, transport setup, key scheduling, record protection, and the larger wire values associated with hybrid post-quantum key exchange.

The workloads therefore answer different questions:

  • The raw workload compares one complete ML-KEM-768 encapsulation/decapsulation round trip with one complete P-256 ECDH round trip.
  • The TLS workload compares fresh authenticated TLS 1.3 connections using X25519 against X25519MLKEM768.

The raw result is a workload-specific implementation signal. The TLS result is the more relevant migration signal for a service accepting fresh connections.

I wanted to know which language was faster at this, and try to understand why.

Nothing more.

How the two measured cases work

Raw ML-KEM-768 and ECDH P-256 round trips

Both raw paths end with two matching 32-byte shared secrets, but they reach that result differently. ML-KEM-768 is a post-quantum key-encapsulation mechanism (KEM): a client uses the server’s public encapsulation key to create a ciphertext and a secret, then the server uses its private decapsulation key and that ciphertext to recover the same secret. ECDH P-256 is classical key agreement: each side combines its own private key with the other side’s public key to derive the same secret.

These outputs are key material, not encrypted messages: neither raw path authenticates a peer, derives traffic keys, or protects application data. ECDH is a mature and widely deployed baseline, but a sufficiently capable quantum computer could break it. ML-KEM is designed around different mathematical problems for which no efficient classical or quantum attack is known, making it useful for establishing secrets that should remain confidential in a post-quantum future.

Full TLS 1.3 classical and hybrid connections

TLS 1.3 turns key establishment into an authenticated, encrypted channel. It negotiates algorithms, binds the handshake into an HKDF key schedule, authenticates the server with a certificate and CertificateVerify, confirms the transcript and keys with Finished, and protects application records with authenticated encryption (AEAD). The benchmark holds that surrounding work constant and changes the key-establishment group from classical X25519 to hybrid X25519MLKEM768.

The hybrid group combines a classical and a post-quantum contribution, reducing reliance on either family alone while deployments transition.

It changes key establishment, not every TLS algorithm: these runs still use an ECDSA P-256 certificate. They therefore measure protection of session-key confidentiality against harvest-now-decrypt-later attacks, not a fully post-quantum-authenticated TLS stack.

Why the TLS results differ between implementations

Similar raw ML-KEM results do not imply similar hybrid TLS results because the raw operation is only one component of a connection. The raw classical comparison uses P-256 ECDH, while the TLS comparison uses X25519 versus X25519MLKEM768. TLS also includes socket setup, certificate authentication and signing, transcript hashing, HKDF key scheduling, record protection, message processing, and allocation or marshaling work.

The fixed part of that work matters when reporting percentages. In GitHub Actions run 32970330873, Go’s classical TLS median was 0.789 ms, so the hybrid median of 1.049 ms exposed a 33.0% increase. The .NET classical medians were 2.145 to 2.370 ms, making the ML-KEM addition a much smaller share of the total connection time. The Go standard-library TLS implementation and .NET’s SslStream over OpenSSL also have different protocol and memory-management paths, so they can add and schedule the hybrid work differently even when the underlying ML-KEM operation is comparable.

The results therefore compare complete implementation stacks, not just cryptographic primitives. They show a clear hybrid-TLS cost signal for this Go path, while the small .NET differences are within the variability observed in those measurements.

What Go 1.27 changes here

Against the previous Go 1.26.6 run on the same reported CPU model, the Go 1.27.0 medians were 3.1% higher for raw ML-KEM, 0.8% higher for ECDH, 3.5% higher for classical TLS, and 2.8% higher for hybrid TLS. Clean and cached builds were 0.3% and 0.7% lower. These are separate GitHub-hosted runner samples, so the small differences combine toolchain effects with normal run-to-run noise and should not be attributed to Go 1.27 alone. The more reliable within-run result remains the hybrid/classical comparison.

No library replacement is warranted for the existing workload. The Go documentation still recommends crypto/mlkem‘s ML-KEM-768 parameter set for most applications, and retaining it preserves comparison with the .NET/OpenSSL rows. Go 1.27 also adds an opt-in pure ML-KEM-1024 TLS group and the new crypto/mldsa package with ML-DSA support in crypto/x509 and TLS 1.3. Those are useful candidates for separate future scenarios, but substituting either here would change the key-exchange parameter set or certificate authentication and break the current like-for-like contract. The benchmark should continue using the standard library without adding a third-party cryptography dependency.

Workload Contract

Raw operations

Each measured raw sample performs 100 successful round trips.

  • ML-KEM-768 reuses the server decapsulation key, imports the server encapsulation key for each client operation, encapsulates, decapsulates, and compares the returned shared secrets directly.
  • ECDH P-256 reuses the server private key, creates a fresh client key, derives raw shared secrets on both sides, and compares them directly.

Neither path includes a KDF, hash, build, restore, process startup, or reporting step in its measured operation. This makes the Go crypto/ecdh shared-secret operation match .NET’s ECDiffieHellman.DeriveRawSecretAgreement operation.

TLS 1.3

Each measured TLS operation creates a fresh loopback TCP connection, completes a TLS 1.3 handshake, sends a 32-byte payload, verifies the echoed payload, and closes the connection. The client and server run in separate processes for both implementations.

Before timing begins, each scenario performs one unmeasured warmup connection. It verifies TLS 1.3, an ECDSA P-256 server certificate, disabled resumption, a selected cipher suite, and the intended key-exchange evidence.

The classical scenario uses X25519. The hybrid scenario uses X25519MLKEM768. Both use TLS 1.3, the same 32-byte echo path, and an ECDSA P-256 certificate. Go records the negotiated group from tls.ConnectionState.CurveID. Public SslStream does not expose the named group, so .NET records OpenSSL group preflight plus a one-group OPENSSL_CONF restriction; that is configured-and-preflight evidence, not a direct post-handshake group field.

Execution and Reporting

The workflow runs Go 1.27.0, .NET 10, and .NET 11 preview serially on one ubuntu-24.04 runner. The .NET rows cover OpenSSL 3.5.0 and 4.0.0. The final job completed successfully in 3 minutes 19 seconds.

Every .NET invocation receives an explicit versioned LD_LIBRARY_PATH prefix and verifies that prefix’s openssl binary before execution. This prevents a global dynamic-loader cache from silently selecting the other provider version.

CI uses 20 independent process samples and 100 measured operations per sample. Successive samples alternate raw and TLS scenario order so that elapsed time, thermal state, and runner drift do not always favor the same case. The report retains every sample, sample count, iteration count, median, and relative standard deviation.

The recorded environment was Linux on GitHub’s Ubuntu 24.04 image, kernel 6.17.0-1022-azure, Go 1.27.0, .NET SDK 11.0.100-preview.7.26381.103, and an observed AMD EPYC 7763 host model from the Go output.

Results From Run 32970330873

All supported measurements below are medians across 20 samples of 100 operations. Benchmark ms covers prebuilt benchmark execution only. Clean and cached build metrics cover the same two targets and exclude benchmark execution.

RuntimeOpenSSLBenchmark msClean build msCached build msML-KEM-768 msECDH P-256 msTLS classical msTLS hybrid msPQ support
Go 1.27.0/4813.7512773.57147.120.1590.1410.7891.049Yes
.NET 103.5.031489.953992.741261.890.0840.2142.3452.420Yes
.NET 104.0.030423.271608.991233.970.0850.2132.3702.385Yes
.NET 11 preview3.5.026207.871542.661022.460.0820.1692.1702.140Yes
.NET 11 preview4.0.025983.411253.411062.060.0820.1692.1452.150Yes

TLS findings

Go’s hybrid TLS median was 1.33x its classical median, a 33.0% increase. Its relative standard deviation was 2.0% classically and 1.2% for hybrid TLS, so this is a clear cost signal for this Go standard-library path.

The .NET TLS medians differed by +3.2% (.NET 10/OpenSSL 3.5), +0.6% (.NET 10/OpenSSL 4.0), -1.4% (.NET 11/OpenSSL 3.5), and +0.2% (.NET 11/OpenSSL 4.0). Those changes are smaller than the recorded relative standard deviations: 8.8% to 11.8% for .NET 10 and 1.7% to 6.1% for .NET 11. The run does not support a .NET hybrid speedup or slowdown claim at that scale.

Raw findings

Go ML-KEM-768 was 1.13x its P-256 ECDH median.

The .NET ML-KEM/ECDH ratios were 0.39x to 0.40x for .NET 10 and 0.48x for .NET 11 preview. The .NET 10 raw measurements had substantial variability, especially ML-KEM (21.5% to 22.7% RSD), so the raw values should be used as scoped implementation measurements rather than broad performance claims.

The Go client-side allocation medians also show the hybrid handshake’s larger footprint: about 36,329 B/op and 415 allocs/op classically versus about 62,285 B/op and 426 allocs/op for hybrid TLS. That is roughly 71.4% more allocated bytes and 2.7% more allocations in this Go path.

Negotiation evidence

RuntimeOpenSSLClassical groupHybrid groupClassical cipherHybrid cipherClassical certificateHybrid certificate
Go 1.27.0Not usedX25519X25519MLKEM768n/an/aECDSA P-256ECDSA P-256
.NET 103.5.0X25519X25519MLKEM768TLS_AES_256_GCM_SHA384TLS_AES_256_GCM_SHA384ECDSA P-256ECDSA P-256
.NET 104.0.0X25519X25519MLKEM768TLS_AES_256_GCM_SHA384TLS_AES_256_GCM_SHA384ECDSA P-256ECDSA P-256
.NET 11 preview3.5.0X25519X25519MLKEM768TLS_AES_256_GCM_SHA384TLS_AES_256_GCM_SHA384ECDSA P-256ECDSA P-256
.NET 11 preview4.0.0X25519X25519MLKEM768TLS_AES_256_GCM_SHA384TLS_AES_256_GCM_SHA384ECDSA P-256ECDSA P-256

What This Run Supports

This benchmark supports narrowly scoped claims about these exact implementations, provider versions, and shared runner:

  1. The declared raw ML-KEM and ECDH round-trip costs.
  2. The hybrid TLS cost relative to classical TLS within each implementation/provider row.
  3. ML-KEM and hybrid TLS availability for the four tested .NET/OpenSSL combinations.

It does not establish that one language is universally faster, that loopback predicts WAN behavior, or that a tiny .NET TLS median difference is meaningful when its variability is larger.

Clean and cached build timings are build-pipeline observations, just additional infos, under the declared two-target contract.

They must not be treated as a universal language ranking.

Scope of the Results

The measurements describe the declared runtime, provider, TLS, and runner configurations. The four tested .NET/OpenSSL combinations all support raw ML-KEM-768 and hybrid X25519MLKEM768 TLS under this benchmark’s provider configuration.

Known Limitations

  • The shared GitHub-hosted runner still has virtualization, scheduler, and background-load noise. Running serially removes cross-VM variation but does not create a dedicated benchmark machine.
  • The Go standard library and .NET’s managed/OpenSSL path are intentionally different implementations. The benchmark measures those complete stacks.
  • .NET group evidence is indirect because SslStream does not surface the negotiated named group.
  • Loopback emphasizes connection establishment. It omits WAN latency, congestion, packet loss, session resumption, connection pooling, and sustained application throughput.
  • Performance measurements do not establish cryptographic suitability, compliance, interoperability with every peer, or freedom from implementation defects.

References

Francesco Belacca

Francesco Belacca

Senior Site Reliability Engineer at Wuerth IT Italy

Author

Francesco Belacca

Leave a Reply

Your email address will not be published. Required fields are marked *

Archive