aere-proof-of-software/sdk-pq-sign/README.md
Aere Network 065f84e34a Aere Proof of Software 1.4.0: an attestation of what was built, from what, by whom and when, verifiable without trusting Aere Network
Every artifact's SHA-256 and size, the SBOM's digest, the git commit and tree of the source, a hybrid signature (classical + ML-DSA,
both required) and, when notarized, a first-seen time on Aere Network covered by the validators' post-quantum certificate. verify
recomputes everything from the files; --rebuild-from repeats an npm pack build from a clone the verifier chose; the SBOM is
re-derived from the committed package-lock.json or, new in 1.4.0, from the committed go.mod and go.sum; --signer requires the
signing keys you expect (1.4.0); a developer credential binds the keys to a person, judged against a trust root you choose. The
builder's declared date can only accuse, never acquit (1.4.0). Includes the GitHub Action and the hybrid signature library it uses.

Laid out as in the development repository (tools/proof-of-software/, sdk-pq-sign/, sdk/) so that nothing is rewritten for
publication. Tests and negative controls measured on 2026-09-29 are listed in README.md.
2026-09-29 22:46:57 +03:00

89 lines
4.5 KiB
Markdown

# @aere/pq-sign
Hybrid signatures for documents, payloads, receipts and API calls: a classical scheme (**secp256k1** or **Ed25519**) and a
NIST post-quantum scheme (**ML-DSA-44 / 65 / 87**, FIPS 204) sign the same digest, and a signature is valid only when
**both** halves verify. Keys are generated and used only on your side; nothing is sent anywhere. The post-quantum half
can be re-verified independently by the Aere Network chain precompile, so a third party does not have to trust this
library, or you, to check it.
```
npm install @aere/pq-sign
```
Pure JavaScript (the audited noble libraries; no native code): Node 18+, and any bundler for the browser.
## Why hybrid
A signature made with two independent schemes stays unforgeable while **either** scheme holds. If large quantum
computers break secp256k1 and Ed25519, the ML-DSA half still binds the signer; if a flaw is found in the young lattice
scheme, the classical half still does. This is the migration shape recommended by NIST, the German BSI and the French
ANSSI for the transition years: not "replace", but "add, and require both".
## Use
```js
import { generateKeyPair, sign, verify, serialize, parse, envelopeHash } from '@aere/pq-sign';
const keys = generateKeyPair({ alg: 'secp256k1+ml-dsa-65' }); // or 'ed25519+ml-dsa-65', '...+ml-dsa-44', '...+ml-dsa-87'
const envelope = sign('release notes of myapp 1.4.2', keys); // string or Uint8Array
const r = verify('release notes of myapp 1.4.2', envelope);
// r = { valid: true, classical: true, pq: true, messageHashMatches: true, toSignMatches: true, reason: null }
const text = serialize(envelope); // JSON, all byte fields 0x-hex; parse(text) gives it back
```
Keep `keys.classical.secretKey` and `keys.pq.secretKey` where you keep secrets. Publish `keys.classical.publicKey` and
`keys.pq.publicKey`; they travel inside every envelope anyway. `generateKeyPair({ seed })` makes the ML-DSA key
deterministic (32-byte seed) for test vectors; classical keys are always fresh unless `classicalSecretKey` is given.
## What exactly is signed
```
messageHash = sha256(message)
toSign = sha256("aere-hybrid-signature-v1" || alg || messageHash)
```
Both schemes sign the 32 bytes of `toSign`: secp256k1 as a digest (RFC 6979, compact 64-byte signature, compressed
33-byte key), Ed25519 as a message (64-byte signature, 32-byte key), ML-DSA in the pure FIPS 204 interface with an
empty context. The domain string and the algorithm label are under the hash, so an envelope cannot be replayed as a
different algorithm pair or for another message. The envelope:
```json
{ "v": 1, "kind": "aere-hybrid-signature", "alg": "secp256k1+ml-dsa-65", "hash": "sha256",
"messageHash": "0x…", "toSign": "0x…",
"classicalPublicKey": "0x…", "classicalSignature": "0x…",
"pqPublicKey": "0x…", "pqSignature": "0x…" }
```
Sizes: ML-DSA-44 key 1,312 B, signature 2,420 B; ML-DSA-65 1,952 / 3,309 B; ML-DSA-87 2,592 / 4,627 B.
## Independent verification on chain
The Aere Network chain (2800) runs the NIST post-quantum verifiers as native precompiles. With an
[Aere Cloud](https://aere.network/cloud.html) key, the ML-DSA half of any envelope can be judged by the chain itself:
```js
import { AereCloud } from '@aere/cloud';
import { verifyOnChain } from '@aere/pq-sign';
const r = await verifyOnChain(envelope, new AereCloud({ apiKey }));
// r = { pqValidOnChain: true, precompile: '0x…0ae3', block: 19018096, chainId: 2800 }
```
That answer does not depend on this library's code. One FIPS 204 detail, measured on chain 2800 on 2026-09-17: the
ML-DSA precompile implements the internal verify (Algorithm 8), and a standard external signature is made over
`M' = 0x00 || len(ctx) || ctx || M`; `verifyOnChain` asks the gateway for the external interface (`interface:
"external"`), which builds `M'` for an empty context. Callers of the precompile directly should pass
`precompileMessage(envelope)` as the message. And if you want a first-seen time for the signature that cannot be
backdated and is covered by the validators' post-quantum certificate, notarize `envelopeHash(envelope)` with
`POST /v1/notarize` and read `GET /v1/proof/{hash}`: first appearance, covering anchor, finality, verification steps.
## Tests
`npm test` runs every claim with its negative pair (a flipped bit in either half, another message, another key, a
foreign envelope). With `AERE_CHEIE` (or `AERE_CHEIE_FISIER`) in the environment it also asks the chain precompile to
confirm a fresh post-quantum signature and to refuse a corrupted one.
## License
MIT. Aere Network, 2026.