Repository navigation
nuntius is an experimental library, published for research and study. It is not recommended for
production environments. No external security review or audit has been performed — not of this
code, and not of the specification it implements. It was built for learning, and for the fun of
it. See Read this first.
⚠ Breaking: 1.0.0 is not interoperable with any 0.0.x release
There is no migration path and no dual-stack mode. A v4 receiver rejects a first byte that is
not 0x04 with ERR_UNSUPPORTED_VERSION, and existing identities must be re-registered because the
identity key type itself changed.
That is not a gratuitous break. Correcting the key agreement changes every key derived downstream of
it, so no version of the fix stays wire-compatible.
Why
v3 was cryptographically broken, not merely dated. Two of the defects were reported by users
years ago and went unfixed:
- #13 — @digal
showed in May 2019 that X3DH collapsed to a single Diffie-Hellman.
crypto_kdf_derive_from_keyreads exactly 32 bytes from its key parameter; the implementation
handed it a 96- or 128-byteDH1 ‖ DH2 ‖ DH3 [‖ DH4]. DH2, DH3 and DH4 were computed,
concatenated, and discarded. DH1 isX25519(IK_A, SPK_B)— both long-lived — so the handshake had
no forward secrecy and the one-time prekey contributed nothing. - #12 —
@NoVoLuMe and @raojunbo reported that
the library could not agree a key with standard X25519 implementations. It couldn't, for two
independent reasons: the API took Ed25519 public keys and ran_pk_to_curve25519over whatever
it was given, discarding the return value so a rejected input left an uninitialized stack buffer in
use as a private scalar; and the result wasBLAKE2b(q ‖ pk_s ‖ pk_r)rather thanq.
SPEC.md §14 has the full table: thirteen confirmed defects, plus nine more found while writing the
specification.
Every one of them passed the v3 test suite. In each case both parties agreed — and agreement was
never the property in question. The old IRTripleDHServiceSpec asserted that Alice's and Bob's
shared keys were equal and 32 bytes long, which is exactly as true of a single DH as of a triple one.
That is the organising constraint of this release: positive round-trip tests certify nothing about
this protocol.
What 1.0.0 is
A specification first. SPEC.md is
normative — 19 sections covering the wire format to the byte, the ordered validation gates, the
zeroization schedule, the per-platform trap catalogue, and the decisions taken under adversarial
review. The Java, Kotlin and Swift ports are written against that document, not against this code.
A frozen conformance corpus. spec/vectors/*.json — 88 vectors covering every id §15.3 and §15.4
require. The RFC-anchored vectors are transcribed from RFC 5869, 7748, 8032 and 8439, never generated
here; a corpus produced entirely by the implementation under test proves self-consistency and nothing
else. The suite rebuilds the corpus in memory each run and compares byte-for-byte, so drift fails a
test rather than rewriting the contract, and the whole suite runs again with the clock ten years
forward.
And the protocol itself:
- HKDF-SHA256 (RFC 5869) everywhere a key is derived — its
(ikm, ikm_len)signature makes #13's
truncation inexpressible rather than merely fixed - ChaCha20-Poly1305 IETF (RFC 8439) replacing AES-256-CBC + HMAC-SHA256 via CommonCrypto
- A two-key identity: Ed25519 for signing (pure, never prehashed) and X25519 for ECDH, as separate
key pairs, with the Ed25519→X25519 conversion banned outright - Nominal key types the compiler enforces; ordered parsing gates where no offset is ever derived from
a received byte; atomic decryption that fails closed; every libsodium andSec*return value
checked - One injectable clock and CSPRNG, because a frozen corpus that reads the wall clock has a shelf life
of seven days
libsodium moves 1.0.13 → 1.0.22, vendored as an XCFramework. CocoaPods support is removed in favour
of SwiftPM. Deployment target is iOS 13.0.
529 tests, 0 failures. Including property-based tests for the invariants a round-trip cannot
see: that altering any single DH input changes SK, that every message key moves when SK moves,
that every bit flip fails to decrypt, and that a failed authentication leaves session state
byte-identical.
Ports
nuntius-java ·
nuntius-kotlin ·
nuntius-swift
All three are early and deliberately incomplete — the substrate, key types, crypto seam and
protocol KDFs are implemented and verified against the RFCs; X3DH, the Double Ratchet, the wire codec
and state serialization are honest stubs. Each currently runs 14 of the 88 conformance vectors and
names the unimplemented section for each of the 74 it skips.
Two specification defects this release found in itself
Both were caught while building the corpus, and both would have been frozen into the contract:
- §3.4 claimed Ed25519 signing is byte-reproducible across libsodium, the JDK and CryptoKit. It
is not — RFC 8032 §8.2 permits added randomness and CryptoKit takes it, so signing one message
three times under one seed gives three different valid signatures. The mandatory
RFC8032-ED25519vector was therefore unpassable by a conformant Swift port. Signatures are now
asserted verify-side only. - §4.4 claimed CryptoKit performs no small-order check. It does. The MUST is unchanged and now
says why: an implementation may not skip its own check because it trusts the library underneath,
and that trust is exactly what was wrong here — in the direction that sounds safe.
Full changelog:
CHANGELOG.md ·
0.0.9...1.0.0