Metadata-Version: 2.5
Name: q-armor-pqc
Version: 0.1.1
Summary: INTERIM: ML-DSA-65 over liboqs for QArmor until the L2 generator emits it (SAD D-7, ADR-22). ML-KEM-768 is delegated to iden-q-post-quantum.
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: cryptography>=42
Requires-Dist: iden-q-post-quantum
Provides-Extra: pqc
Requires-Dist: liboqs-python>=0.10; extra == 'pqc'
Description-Content-Type: text/markdown

# q-armor-pqc

**ML-DSA-65 (FIPS 204) for QArmor, and an interim package by design: it exists to be deleted.**

[![PyPI](https://img.shields.io/pypi/v/q-armor-pqc.svg)](https://pypi.org/project/q-armor-pqc/)
[![Python](https://img.shields.io/pypi/pyversions/q-armor-pqc.svg)](https://pypi.org/project/q-armor-pqc/)
[![Licence](https://img.shields.io/badge/licence-proprietary-1f6feb.svg)](https://idenq.io)
[![FIPS 204](https://img.shields.io/badge/NIST-ML--DSA--65-000000.svg)](https://csrc.nist.gov/pubs/fips/204/final)

> A cryptographic primitive with two implementations has two behaviours, and you find out which one you were running during the incident. This package exists so that QArmor has exactly one, until the place it belongs can hold it.

`iden-q-post-quantum` is the L2 generator's emission (`iqcgen`) and the only
place in the estate where a cryptographic primitive has one source and one KAT
gate. It carries ML-KEM-768 and ML-DSA-**44**, in Python and in TypeScript.
The DVN's on-chain attestations sign with ML-DSA-**65** (SAD §6.4.2), and no
language emits that primitive. Per ADR-22 it is a **spec missing from the
generator** (SAD D-7), not a second library, and the generator is private.
While that gate stays closed, this package is where ML-DSA-65 lives.

**When the emission publishes ML-DSA-65, this package is deleted.** It is not
versioned in parallel, not migrated, not maintained: `q_armor_lib` and
`q_armor_chain` change the import to the emission and `packages/q-armor-pqc/`
disappears from the workspace. It is published to PyPI only because
[`q-armor-lib`](https://pypi.org/project/q-armor-lib/) pins it, and a
published library cannot depend on an editable.

## Install

```bash
pip install q-armor-pqc            # ML-DSA-65 with a development Ed25519 fallback
pip install 'q-armor-pqc[pqc]'     # + liboqs-python: real ML-DSA-65
```

> **Read this before you deploy it.** `QARMOR_REQUIRE_PQC=1` demands liboqs and
> fails if it is absent. Without that variable, ML-DSA-65 falls back to
> **Ed25519**, which is classical, and says so with a `RuntimeWarning` at
> import. It is the only live exception to "NIST algorithms only"
> (`CLAUDE.md`), and it is stated rather than hidden. The KEM has no fallback.

## What is here, and what is delegated

- **`MLDSA65`**: liboqs, or Ed25519 in development. The only thing that
  justifies the package.
- **`MLKEM768`**: delegates to the emission. ML-KEM-768 has not been
  implemented here since 2026-08-23: a second implementation over liboqs meant
  the same primitive on two backends, one inside the KAT gate and one outside
  (ADR-13, the SAD's S-6 scenario). The wrapper stays because `encapsulate`
  never returned the raw secret but `HKDF-SHA3-256(ss)`, while the emission
  derives with HKDF-SHA256: changing the import alone would have changed the
  AES key, a cryptographic change dressed up as a refactor. The emission's
  keygen is deterministic from a seed, and the seed comes from `secrets`.
- **`aead_encrypt` / `aead_decrypt`**: AES-256-GCM from `cryptography`.

**The KEM's X25519 fallback is gone**, and that went with the delegation: the
emission is pure Python and a hard dependency, so the "liboqs is absent" case
no longer exists for the KEM.

## About the name

In this estate "crypto" means **blockchain**, not cryptography:
`quantum-armor-crypto` is the DVN (SAD §4.2, §6.4, ADR-18). And `q-crypto-*`
is the prefix of the L2 generator (`q-crypto-gen`) and of what it emits. This
distribution's previous name, `q-crypto-py`, broke both rules and was also the
one the SAD used for the Python emission; the directory
`packages/q-armor-crypto/` inverted ADR-18 in the tree itself. Both were
renamed on 2026-09-02, before the first publication, so that name would never
reach an index. `pqc` is what it contains; `armor`, who it belongs to.

## A dependency on a private module, said out loud

`pqc_engine` imports `iden_q_post_quantum._runtime`, not the emission's public
API. Checked against `iden-q-post-quantum` 1.2.0: what is public is
`seal`/`unseal` (a hybrid X25519 and ML-KEM-768 envelope), `encrypt`/`decrypt`
by passphrase, `derive_recipient_keys` and the ML-DSA-44 signature. **There is
no public raw KEM**: `mlkem768_keygen`, `encapsulate` and `decapsulate` live
only in `_runtime`. QArmor's session needs the raw KEM to derive its own key,
so this dependency contradicts "consumers depend on published outputs"
(ADR-22) and is recorded as what it is: a gap in the generator with the same
shape as D-7, a missing public KEM API spec, to be closed upstream rather than
wrapped here.

## Tests

`tests/` carries the package's own suite: that the KEM's AES key is
HKDF-SHA3-256 of the emission's secret (and not the raw secret, nor
HKDF-SHA256), that encapsulate and decapsulate converge, that a signature
verifies only its own message, and that AES-GCM rejects a tampered tag.

**What is still open is an ML-DSA-65 KAT against the FIPS 204 vectors**: with
`QARMOR_REQUIRE_PQC` undefined the signature runs over Ed25519, and a test
that has only passed there has not seen ML-DSA-65. It is exercised as well
from `q-armor-lib`'s session and proofs tests, from the DVN worker's, and from
`api/`.

## The QArmor family

Five packages, one version, published together from a single tag.

| Package | What it is |
|---|---|
| [`q-armor-lib`](https://pypi.org/project/q-armor-lib/) | Detection, scoring and the control-plane client |
| [`q-armor-cli`](https://pypi.org/project/q-armor-cli/) | `q-armor`, the operator's command line |
| [`q-armor-proxy`](https://pypi.org/project/q-armor-proxy/) | Enterprise runtime: protection proxy, firewall, SIEM webhook |
| [`q-armor-chain`](https://pypi.org/project/q-armor-chain/) | DVN worker and `QArmorDVN.sol` |
| [`q-armor-pqc`](https://pypi.org/project/q-armor-pqc/) | This one, and it is interim |

## Next

[idenq.io](https://idenq.io)
