Windows
Is PQC enabled? — quick check
Windows — already-installed Edge or Chrome
1. Open https://your-host.example.com in Edge or Chrome.
2. Press F12.
3. Open the Security tab.
4. Select the main origin.
5. Read the connection details.
PQC works if the key exchange group is X25519MLKEM768.
Not PQC if the key exchange group is X25519, P-256, P-384, or another classical group. Expected when PQC is ON
Connection - protocol: TLS 1.3,
key exchange group: X25519MLKEM768,
cipher: AES_256_GCM What you'll see when PQC is OFF
Connection - protocol: TLS 1.3,
key exchange group: X25519,
cipher: AES_256_GCM This is the lowest-dependency Windows check: no PSGallery, Node.js, npm, OpenSSL, or downloads. It works for any HTTPS site that Edge or Chrome can reach.
Windows PowerShell — no local OpenSSL needed
# Simplest hosted CheckPQC probe. Requires only PowerShell + HTTPS.
iwr -useb https://checkpqc.com/test.ps1 | iex
# Manual equivalent. This also fixes stale older CheckPQC modules.
Remove-Module CheckPQC -Force -ErrorAction SilentlyContinue
Install-Module -Name CheckPQC -Scope CurrentUser -MinimumVersion 0.2.8 -Force -AllowClobber
Import-Module CheckPQC -MinimumVersion 0.2.8 -Force
$result = Test-PQC -HostName checkpqc.app
$result | Select-Object verdict,ExitCode,@{Name='HybridGroup';Expression={$_.hybrid.namedGroup}} Expected when PQC is ON
Status: WORKS
Host: checkpqc.app
Verdict: HYBRID_ENABLED
ExitCode: 0
HybridGroup: X25519MLKEM768
verdict ExitCode HybridGroup
------- -------- -----------
HYBRID_ENABLED 0 X25519MLKEM768 What you'll see when PQC is OFF
verdict ExitCode HybridGroup
------- -------- -----------
CLIENT_ONLY 2 X25519 This path uses the hosted scanner, so Windows does not need Node.js, npm, OpenSSL, or PATH changes. Use the local OpenSSL script below only when you need an offline/private-host proof from the Windows machine itself.
Windows PowerShell — local/offline OpenSSL proof
# 1) No-dependency check — identify this machine first.
[System.Environment]::OSVersion.Version
Get-ComputerInfo | Select-Object OsName,OsVersion,OsBuildNumber
# 2) Dependency check — prompt before installing anything.
function Confirm-Install($Message) {
$answer = Read-Host "$Message Install now? [y/N]"
return ($answer -match '^[Yy]')
}
function Get-OpenSSLPath {
$cmd = Get-Command openssl.exe -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$programFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')
$currentPath = (Get-Location).Path
$candidateDirs = @(
$(if ($env:ProgramFiles) { Join-Path (Join-Path $env:ProgramFiles 'OpenSSL-Win64') 'bin' }),
$(if ($programFilesX86) { Join-Path (Join-Path $programFilesX86 'OpenSSL-Win32') 'bin' }),
$(if ($env:LOCALAPPDATA) { Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'Programs') 'OpenSSL-Win64') 'bin' }),
$currentPath,
$(if ($currentPath) { Join-Path (Join-Path $currentPath 'openssl') 'bin' })
)
foreach ($dir in $candidateDirs) {
if (-not $dir) { continue }
$candidate = Join-Path $dir 'openssl.exe'
if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { return $candidate }
}
return $null
}
$openssl = Get-OpenSSLPath
if (-not $openssl) {
Write-Warning 'OpenSSL 3.5+ was not found. Built-in Windows PowerShell/Schannel cannot prove the PQC named group.'
if ((Get-Command winget -ErrorAction SilentlyContinue) -and (Confirm-Install 'Install ShiningLight OpenSSL with winget?')) {
winget install ShiningLight.OpenSSL.Light
$openssl = Get-OpenSSLPath
}
if (-not $openssl) { 'Install or pre-stage OpenSSL 3.5+ and retry.'; return }
}
& $openssl version
$groups = (& $openssl list -tls-groups 2>$null) -join [Environment]::NewLine
if ($groups -notmatch 'X25519MLKEM768|MLKEM|Kyber') {
'Local OpenSSL does not advertise ML-KEM groups. Install or pre-stage OpenSSL 3.5+ and retry.'
return
}
& $openssl list -kem-algorithms | Select-String -Pattern 'mlkem|kyber'
# 3) Live handshake — fully local. checkpqc.app is a known-PQ target;
# swap it for whatever you want to test.
& $openssl s_client -connect checkpqc.app:443 -tls1_3 `
-groups X25519MLKEM768 2>&1 |
Select-String -Pattern 'Negotiated TLS1\.3 group|Cipher is|alert' Expected when PQC is ON
OsName : Microsoft Windows 11 Insider 25H2
OpenSSL 3.6.2 7 Apr 2026
X25519MLKEM768 @ default
Negotiated TLS1.3 group: X25519MLKEM768
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 What you'll see when PQC is OFF
OsName : Microsoft Windows 11 24H2
OpenSSL 1.1.1w 11 Sep 2023
# (no mlkem line)
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
# (no group line — 1.1.1 cannot offer ML-KEM) Schannel itself never exposes the negotiated TLS named group, so a local OpenSSL 3.5+ binary (winget ShiningLight.OpenSSL.Light) is the most reliable offline answer on Windows. Build 26100+ (Insider 25H2) is starting to surface ML-KEM cipher suites under HKLM\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\SSL\00010003.
Windows uses Schannel for system TLS — anything that calls
WinHTTP, HttpClient on .NET Framework, IIS, or the legacy
SSPI API. Microsoft began rolling out hybrid post-quantum TLS to Schannel
via Windows Insider builds during 2025. Production Windows 11 23H2/24H2 does not
yet negotiate X25519MLKEM768 on Schannel.
What's PQ-ready on Windows today
- Edge / Chrome / Firefox — bundle their own TLS stack with hybrid PQC. Edge, Chrome, Firefox.
- OpenSSH for Windows 9.6+ — hybrid SSH KEX. OpenSSH.
- WSL2 (Ubuntu 24.04+) — full Linux OpenSSL story applies; install OpenSSL 3.5+ inside WSL.
- Signal / WhatsApp / Teams calls — protocol-level PQ where supported.
- Go / Rust / Node binaries built with PQC-aware TLS — same as Linux.
What's not yet
- IIS — Schannel-bound; depends on the Schannel rollout.
- RDP — uses CredSSP over TLS (Schannel) — same gate.
- SMB over QUIC — Schannel-gated.
- .NET Framework / .NET on Windows — Schannel-gated unless you swap
to OpenSSL via a custom
SslStreambackend (rare). .NET.
Track Schannel rollout
# Schannel ML-KEM cipher suite registry path (Insider builds)
HKLM\SYSTEM\CurrentControlSet\Control\Cryptography\Configuration\Local\SSL\00010003 Microsoft's SymCrypt repo is the upstream crypto library; PQ algorithms land there first.
Start with the no-download check
If Edge or Chrome is already installed, start there. It is the simplest offline-friendly Windows check because it does not need PSGallery, Node.js, npm, OpenSSL, or any downloaded module.
1. Open https://your-host.example.com in Edge or Chrome.
2. Press F12.
3. Open the Security tab.
4. Select the main origin.
5. Read the connection details.
PQC works if the key exchange group is X25519MLKEM768.
Not PQC if the key exchange group is X25519, P-256, P-384, or another classical group. Get PQC TLS into PowerShell scripts now
The simplest path on Windows is the official CheckPQC
PowerShell module. For public HTTPS checks it only needs PowerShell
and outbound HTTPS to api.checkpqc.app:
# Simplest hosted check. No Node.js, npm, local OpenSSL, or PATH changes.
iwr -useb https://checkpqc.com/test.ps1 | iex
# Manual PSGallery path. This also fixes stale older CheckPQC modules.
Remove-Module CheckPQC -Force -ErrorAction SilentlyContinue
Install-Module -Name CheckPQC -Scope CurrentUser -MinimumVersion 0.2.8 -Force -AllowClobber
Import-Module CheckPQC -MinimumVersion 0.2.8 -Force
Test-PQC -HostName checkpqc.app
# Local CLI path (adds Node.js via winget if needed)
winget install aegyrix.check-pqc
check-pqc checkpqc.app For raw OpenSSL handshakes (e.g. when pinning a specific named group from a script), you can also install standalone OpenSSL 3.5+:
function Confirm-Install($Message) {
$answer = Read-Host "$Message Install now? [y/N]"
return ($answer -match '^[Yy]')
}
function Get-OpenSSLPath {
$cmd = Get-Command openssl.exe -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$programFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')
$candidateDirs = @(
$(if ($env:ProgramFiles) { Join-Path (Join-Path $env:ProgramFiles 'OpenSSL-Win64') 'bin' }),
$(if ($programFilesX86) { Join-Path (Join-Path $programFilesX86 'OpenSSL-Win32') 'bin' }),
$(if ($env:LOCALAPPDATA) { Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'Programs') 'OpenSSL-Win64') 'bin' })
)
foreach ($dir in $candidateDirs) {
if (-not $dir) { continue }
$candidate = Join-Path $dir 'openssl.exe'
if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { return $candidate }
}
return $null
}
$openssl = Get-OpenSSLPath
if (-not $openssl) {
Write-Warning 'OpenSSL 3.5+ was not found. Built-in Windows PowerShell/Schannel cannot prove the PQC named group.'
if ((Get-Command winget -ErrorAction SilentlyContinue) -and (Confirm-Install 'Install ShiningLight OpenSSL with winget?')) {
winget install ShiningLight.OpenSSL.Light
$openssl = Get-OpenSSLPath
}
if (-not $openssl) { 'Install or pre-stage OpenSSL 3.5+ and retry.'; return }
}
& $openssl version
& $openssl s_client -connect checkpqc.app:443 -tls1_3 -groups X25519MLKEM768 2>&1 |
Select-String -Pattern 'Negotiated TLS1.3 group|Server Temp Key|Cipher is|alert' Use the standalone OpenSSL 3.5+ binary instead of Invoke-WebRequest when
you need to pin the named group.
No internet, no PSGallery, no module downloads
If the Windows machine cannot reach the internet or cannot download
PowerShell modules, do not use Install-Module or the hosted
API path. You need a checker that is already on the machine, or a
pre-staged offline tool copied in through your normal software process.
Option 1: already-installed Edge or Chrome
This is the lowest-dependency visual check. It works for HTTPS sites the browser can reach, including internal sites, and it does not need PSGallery, Node.js, npm, or OpenSSL.
1. Open https://your-host.example.com in Edge or Chrome.
2. Press F12.
3. Open the Security tab.
4. Select the main origin.
5. Read the connection details.
PQC works if the key exchange group is X25519MLKEM768.
Not PQC if the key exchange group is X25519, P-256, P-384, or another classical group. Option 2: local or pre-staged OpenSSL 3.5+
For a command-line proof with no network dependency beyond the target
host itself, copy an OpenSSL 3.5+ Windows build onto the machine ahead
of time, then run this script. It checks local ML-KEM support and then
forces a TLS 1.3 handshake that offers X25519MLKEM768.
$HostName = 'checkpqc.app' # change to the host you want to test
$Port = 443
function Confirm-Install($Message) {
$answer = Read-Host "$Message Install now? [y/N]"
return ($answer -match '^[Yy]')
}
function Get-OpenSSLPath {
$cmd = Get-Command openssl.exe -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$programFilesX86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')
$currentPath = (Get-Location).Path
$candidateDirs = @(
$(if ($env:ProgramFiles) { Join-Path (Join-Path $env:ProgramFiles 'OpenSSL-Win64') 'bin' }),
$(if ($programFilesX86) { Join-Path (Join-Path $programFilesX86 'OpenSSL-Win32') 'bin' }),
$(if ($env:LOCALAPPDATA) { Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'Programs') 'OpenSSL-Win64') 'bin' }),
$currentPath,
$(if ($currentPath) { Join-Path (Join-Path $currentPath 'openssl') 'bin' })
)
foreach ($dir in $candidateDirs) {
if (-not $dir) { continue }
$candidate = Join-Path $dir 'openssl.exe'
if ($candidate -and (Test-Path -LiteralPath $candidate -PathType Leaf)) { return $candidate }
}
return $null
}
$openssl = Get-OpenSSLPath
if (-not $openssl) {
'No local OpenSSL 3.5+ binary found. Built-in Windows PowerShell/Schannel cannot prove the PQC named group.'
if ((Get-Command winget -ErrorAction SilentlyContinue) -and (Confirm-Install 'Install ShiningLight OpenSSL with winget?')) {
winget install ShiningLight.OpenSSL.Light
$openssl = Get-OpenSSLPath
}
if (-not $openssl) { 'Install or pre-stage OpenSSL 3.5+ and retry.'; return }
}
& $openssl version
$groups = (& $openssl list -tls-groups 2>$null) -join [Environment]::NewLine
if ($groups -notmatch 'X25519MLKEM768|MLKEM|Kyber') {
'Local OpenSSL does not advertise ML-KEM groups. Copy in OpenSSL 3.5+ and retry.'
return
}
$connect = "$($HostName):$Port"
$probe = & $openssl s_client -connect $connect -servername $HostName -tls1_3 -groups X25519MLKEM768 2>&1
$probe | Select-String -Pattern 'Negotiated TLS1.3 group|Server Temp Key|Cipher is|alert'
if ($probe -match 'X25519MLKEM768') {
'PQC: WORKS'
} else {
'PQC: NOT PROVEN - check whether the server has X25519MLKEM768 enabled.'
} If neither Edge/Chrome connection details nor a local OpenSSL 3.5+ binary is available, Windows built-ins can prove that TLS works, but they cannot prove that the negotiated key exchange was PQC. In that case, pre-stage OpenSSL, the CheckPQC CLI bundle, or a Docker image through your offline software distribution process.
Raw SslStream handshake from PowerShell
A common one-liner you'll see online uses [Net.Sockets.TcpClient]
plus SslStream to confirm a TLS handshake works. There's a sharp
edge: Windows PowerShell 5.1 (the bundled ISE) runs on
.NET Framework 4.x, where
SslStream.AuthenticateAsClient(string) defaults to
TLS 1.0 only. Any modern site will fail with
"A call to SSPI failed, see inner exception."
This version works in both Windows PowerShell 5.1 and PowerShell 7+:
# Force modern protocols (PS 5.1 needs this; PS 7 ignores the legacy switch)
[Net.ServicePointManager]::SecurityProtocol =
[Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
$tcp = [Net.Sockets.TcpClient]::new('checkpqc.app', 443)
$ssl = [Net.Security.SslStream]::new($tcp.GetStream(), $false)
$protocols = [System.Security.Authentication.SslProtocols]::Tls12 -bor `
[System.Security.Authentication.SslProtocols]::Tls13
try {
$ssl.AuthenticateAsClient('checkpqc.app', $null, $protocols, $false)
"Negotiated: $($ssl.SslProtocol) cipher=$($ssl.NegotiatedCipherSuite)"
} finally {
$ssl.Dispose(); $tcp.Dispose()
} Important: this only proves the host accepts TLS 1.2/1.3
and reports the cipher. SslStream uses
Schannel, which has no public ML-KEM support yet, so the
NamedGroup field will always be a classical group
(X25519 / P-256) regardless of what the server
can offer. To detect actual PQ negotiation from Windows
today, use check-pqc (which calls our scanner over the API)
or openssl s_client -groups X25519MLKEM768 with the
OpenSSL 3.5+ binary above.