Search by

langacorp / kemproof

LucaPRATA

Attest that an ML-KEM-768 key exchange really happened, with key confirmation, and record it with an expiry. It does not protect a channel.

Package info

github.com/langacorp/kemproof

pkg:composer/langacorp/kemproof

Statistics

Installs: 7

Dependents: 0

Suggesters: 0

Stars: 3

Open Issues: 0

v2.0.0 2026-10-04 18:18 UTC

This package is auto-updated.

Last update: 2026-10-04 18:58:41 UTC


README

self-test packagist DOI

Attest that an ML-KEM-768 key exchange really happened — and record it in a way anyone can check later.

Post-quantum key exchange in production is easy to claim and hard to prove. This is the smallest thing that turns the claim into a record: an exchange took place, with this algorithm, at this time, valid until then.

What this proves

  • A party completed an ML-KEM-768 encapsulation against a fresh public key
  • Both sides reached the same shared secret. The prover shows it with a key confirmation, and the verifier refuses the exchange — and stores nothing — when the confirmation does not match the secret it decapsulated
  • The exchange belongs to this subject and this session: the confirmation is bound to both, and to the public key and the ciphertext, so it cannot be moved to another subject or replayed against another keypair
  • It happened at a known time, and the record expires

What this does not prove

Read this part twice. It is the reason the project exists.

  • It does not encrypt anything. The shared secret is used to derive a fingerprint and is then discarded. No traffic is protected by it.
  • It is not a tunnel, and not a replacement for TLS. Your transport security is whatever it was before.
  • It is not hybrid. One KEM, not a KEM combined with a classical exchange.
  • It says nothing about today's traffic. It says an exchange was possible.
  • It does not say who the prover is. Anyone who can reach your handshake endpoint can run an exchange for the subject they name. Deciding who may attest a subject is your endpoint's job; the reference verifier in examples/ does not do it.

Why version 2 exists

Version 1 could not tell a real exchange from a forged one. ML-KEM decapsulation uses implicit rejection: a ciphertext that was never produced against the public key decapsulates without error to an unrelated secret. Measured with liboqs on 1.x: 1088 random bytes were attested, and the record read valid => true. The verifier had no way to tell, because the prover's secret never came back to it.

Version 2 makes the prover show it. Next to the ciphertext it sends

confirmation = HMAC-SHA256(key = shared secret,
                           "kemproof/2 key confirmation" | session id | subject
                           | SHA-256(public key) | SHA-256(ciphertext))

with every field length-prefixed (4 bytes, big-endian). The verifier recomputes it from the secret it decapsulated and compares in constant time. A random ciphertext, a flipped bit, a confirmation made for another subject, session or keypair: each is refused, and each case is a test that runs against real liboqs in CI.

If you need protected traffic, you need a protocol, not an attestation. If you need to show an auditor that post-quantum key exchange runs in your estate and when it last ran, that is this.

How it works

verifier                             prover
   |  handshake                         |
   |----- session id, public key ------>|
   |                                    |  encapsulate (liboqs)
   |                                    |  confirmation = HMAC(secret, transcript)
   |<--- ciphertext, confirmation ------|
   |  decapsulate                       |
   |  check confirmation, or refuse     |
   |  fingerprint = HMAC(secret, sid)   |
   |  store: alg, protocol, time, expiry|
   |  discard the secret                |

Three steps, and the secret survives none of them. The secret key of the handshake stays on the verifier and is used once: delete it after attest(), whatever the outcome. examples/verifier.php shows one way to do that.

Requirements

  • liboqs — the crypto is Open Quantum Safe, not ours. This project is the record around it.
  • PHP 8.0+ with the ffi extension, for the verifier — the same floor composer.json declares, and the self-test runs on it. 8.0 is a floor, not a recommendation: it no longer receives security fixes upstream. The code runs on it. Run something newer.
  • Python 3.8+ for the reference client

Install

composer require langacorp/kemproof

The package is the PHP verifier under src/. liboqs is a system library and is not installed by Composer: it has to be present on the machine already.

Use

$kem = new KemProof\KemAttestation('/path/to/liboqs.so', $yourStore);

$hs = $kem->handshake();      // hand out session_id and public_key, keep secret_key
// ... the prover returns $ct and $confirmation (32 raw bytes)
$record = $kem->attest($subject, $hs['session_id'], $hs['secret_key'], $ct, $confirmation);
// throws RuntimeException('key confirmation failed: ...') and stores nothing
// when the prover did not reach the same secret
$status = $kem->status($subject);         // null if never attested

status() returns null when a subject was never attested, and a record with valid => false when it expired. Those are different answers and the caller should not collapse them: never measured is not the same as measured and stale.

Storage is yours. Implement StoreInterface over a table, a file, a cache.

python3 client/attest.py https://example.org/kem/v1 my-subject /path/to/liboqs.so

A reference endpoint for that client, with single-use sessions, a session lifetime and the subject checked against the handshake:

KEMPROOF_LIBOQS=/path/to/liboqs.so KEMPROOF_DATA=/var/lib/kemproof \
  php -d ffi.enable=true -S 127.0.0.1:8080 examples/verifier.php

Outside the CLI, PHP's default ffi.enable=preload forbids loading liboqs at run time: set ffi.enable=true for the pool that runs the verifier, or preload the library. kemproof says so in its error instead of failing inside FFI.

Upgrading from 1.x

  • attest() takes a fifth argument, the 32-byte confirmation. A 1.x client sends none and is refused: update the client and the verifier together.
  • Records carry protocol => 'kemproof/2' and confirmed => true. A 1.x record has neither; it was never confirmed, and should be read that way.
  • The fingerprint is computed as before.

Tests

php tests/sizes.php      # constants match FIPS 203
php tests/autoload.php   # every public name resolves through PSR-4
php tests/lengths.php    # wrong key, ciphertext and confirmation lengths are refused
php tests/vector.php     # the confirmation is pinned, the same bytes as the client
KEMPROOF_LIBOQS=/path/to/liboqs.so php tests/exchange.php
KEMPROOF_LIBOQS=/path/to/liboqs.so python3 -m unittest discover -s tests -v

The client tests include an end-to-end run: the Python client against examples/verifier.php under php -S, with real liboqs on both sides.

Without KEMPROOF_LIBOQS, the real-exchange tests say they did not run; they do not pass in its place. The self-test builds liboqs from a pinned tag and runs all of them.

The other tools

Each came out of a defect measured on our own estate. Each one is standalone and depends on none of the others.

  • realroute — checks that a route really exists, by content and not by status code.
  • leakform — finds secrets in a git repository by shape, across every ref.
  • samecheck — measures whether the copies that should be identical still are, and never says which one is right.
  • provenreal — compares what a system claims with what can be measured, from independent sources.
  • countdrift — finds numbers written by hand that no longer match their source.

The set is kept on the organisation profile. It is not written here as a count, because a number typed by hand is the thing countdrift exists to find.

Where this comes from

LANGA runs an ecosystem of digital services on its own infrastructure, and this is the part that records ML-KEM-768 exchanges within it.

Two layers are involved and they are not the same. Connections to the sites served on our Galaxy infrastructure negotiate ML-KEM-768 at the TLS layer: 65 hosts out of 65, measured on 3 September 2026 by reading the negotiated group in the ServerHello, querying the server address rather than the public name. Not yet enabled on LINK, our client-hosting infrastructure, at that layer. This tool records something else — an application-level exchange, run on top of whatever transport is already in place. Anyone checking with openssl is measuring the first and never the second, which is why each claim here says which layer it means.

Writing the honest version of the claim was the harder half: our own public pages said encryption where the code did key exchange, and the two are not the same promise. That is written up here: Post-quantum key exchange in production, and what it is not.

The ecosystem it runs in:

  • LANGA — the ecosystem
  • LINK — monitoring and security for any website
  • easy LANGA — client management, reports, support
  • LANGA Tools — WordPress toolkit for developers

See How we work.

Licence

MIT — see LICENSE. Copyright LANGA Corporation S.r.l.