Call the Windows service
This guide shows how a program uses the named pipe to generate keys, sign and verify, encrypt and decrypt, and run an ML-KEM key exchange. The examples use PowerShell; any language that can open a named pipe and write JSON works the same way.
Before you begin, start the service on \\.\pipe\wvds_crypto as described in
Build and test.
Connect and define a helper
$pipe = [System.IO.Pipes.NamedPipeClientStream]::new('.', 'wvds_crypto', 'InOut') $pipe.Connect(5000) $utf8 = [System.Text.UTF8Encoding]::new($false) $reader = [System.IO.StreamReader]::new($pipe, $utf8) $writer = [System.IO.StreamWriter]::new($pipe, $utf8) $writer.NewLine = "`n"; $writer.AutoFlush = $true function Invoke-Crypto([hashtable] $request) { $writer.WriteLine(($request | ConvertTo-Json -Compress)) $reader.ReadLine() | ConvertFrom-Json }
Each request is one line, and so is each response. The connection stays open for further requests.
Sign and verify
$key = Invoke-Crypto @{ operation = 'keygen'; request_id = 'k1'; algorithm = 'ML-DSA'; level = 'ML-DSA-65' } $message = [Convert]::ToBase64String($utf8.GetBytes('reading 42')) $sig = Invoke-Crypto @{ operation = 'ml_dsa_sign'; request_id = 's1'; key_id = $key.key_id; message = $message } $check = Invoke-Crypto @{ operation = 'ml_dsa_verify'; request_id = 'v1'; level = 'ML-DSA-65' public_key = $key.public_key; message = $message; signature = $sig.signature } $check.valid # True
keygen returns key_id and the public key. Keep the public key yourself if signatures need to remain
verifiable after the service restarts. An ML-DSA-65 signature is 3309 bytes long (signature_size).
For ml_dsa_verify, check the valid field. A bad signature isn't an error: the response has
success: true and valid: false.
Encrypt and decrypt
$aes = Invoke-Crypto @{ operation = 'keygen'; request_id = 'k2'; algorithm = 'AES-256-GCM' } $enc = Invoke-Crypto @{ operation = 'aes_encrypt'; request_id = 'e1'; key_id = $aes.key_id; plaintext = $message } $dec = Invoke-Crypto @{ operation = 'aes_decrypt'; request_id = 'd1'; key_id = $aes.key_id ciphertext = $enc.ciphertext; nonce = $enc.nonce; tag = $enc.tag } $utf8.GetString([Convert]::FromBase64String($dec.plaintext)) # reading 42
The service picks the nonce. Store or transmit ciphertext, nonce, and tag together; if any one is
missing, the message can't be decrypted. Optionally, aad binds additional data to the ciphertext that
stays unencrypted; if you use it, pass the same aad when you decrypt.
Agree on a key with ML-KEM
The recipient generates a key pair and hands out the public key:
$kem = Invoke-Crypto @{ operation = 'keygen'; request_id = 'k3'; algorithm = 'ML-KEM-768' }
The sender encapsulates with that public key and sends the ciphertext to the recipient:
$cap = Invoke-Crypto @{ operation = 'ml_kem_encapsulate'; request_id = 'c1'; public_key = $kem.public_key; level = 'ML-KEM-768' }
The recipient decapsulates:
$dcp = Invoke-Crypto @{ operation = 'ml_kem_decapsulate'; request_id = 'c2'; key_id = $kem.key_id; ciphertext = $cap.ciphertext } $cap.shared_secret -eq $dcp.shared_secret # True
Both sides now hold the same 32-byte secret. It leaves the service in the clear, as Base64 in the
response. Use hkdf to derive a key for the actual purpose, and treat the secret on the client side
like a key. For a hybrid scheme per BSI TR-02102-1, also combine it with a classical algorithm; see
Algorithms and parameter sets.
Detect errors
$r = Invoke-Crypto @{ operation = 'keygen'; request_id = 'k4'; algorithm = 'ML-KEM'; level = 'ML-KEM-999' } if (-not $r.success) { "$($r.error.code): $($r.error.message)" }
Output: UNSUPPORTED_ALGORITHM: Unsupported ML-KEM level: ML-KEM-999
Check success on every response. For what the codes mean, see Error codes.
Close the connection
$pipe.Dispose()