From 86c744897be94a5d579ded865a3f6c6bf71da4f3 Mon Sep 17 00:00:00 2001 From: dedysutanto Date: Thu, 1 Oct 2026 15:55:09 +0700 Subject: [PATCH] feat(pqc): identity/COM v2, pqcMode config, selftest gates Phases 3-6 (SPEC T19-T23): type-1 identity carries ML-KEM-768 + ML-DSA-65 pubs, COM type-2 double-sign, HELLO capability bit, local.conf settings.pqcMode (off/hybrid/pqconly) with classic fallback. selftest KAT + interop matrix + fragment-loss benchmark green. --- SPEC.md | 14 +- controller/EmbeddedNetworkController.cpp | 2 +- controller/PostgreSQL.cpp | 2 +- node/CertificateOfMembership.cpp | 29 +- node/CertificateOfMembership.hpp | 92 ++- node/Identity.cpp | 154 ++++- node/Identity.hpp | 367 ++++++++++-- node/IncomingPacket.cpp | 2 +- node/Node.cpp | 27 +- node/Node.hpp | 11 + node/PQHybrid.cpp | 17 +- node/PQHybrid.hpp | 14 + node/Packet.hpp | 2 + node/Peer.cpp | 2 +- node/Peer.hpp | 17 +- node/World.hpp | 2 +- one.cpp | 9 +- selftest.cpp | 718 ++++++++++++++++++++++- service/OneService.cpp | 16 +- 19 files changed, 1371 insertions(+), 126 deletions(-) diff --git a/SPEC.md b/SPEC.md index e91408e..1bdcdb4 100644 --- a/SPEC.md +++ b/SPEC.md @@ -61,8 +61,8 @@ Derived from `selftest.cpp` + CI gates. `?` = code-derived, no test yet. | V11 | ML-KEM/ML-DSA KATs + hybrid KDF vector + identity v2 roundtrip + COM double-sign verify pass | extended `selftest.cpp` | | V12 | `pqcMode` negotiation honors `local.conf` (`off` → no v2 fields) | selftest | | V13 | hybrid agree = KDF(X25519-identity-agree ‖ ML-KEM static-static), no ephemeral exchange; hybrid↔hybrid agree + hybrid↔vanilla classic fallback pass | `node/Peer.cpp:63`, `node/IncomingPacket.cpp:400` | -| V14 | COM double-sign = type byte 1→2 appends ML-DSA sig alongside Ed25519; v1 verify path unchanged; v1 rejects type 2 cleanly | `node/CertificateOfMembership.hpp:224,245,313` | -| V15 | handshake vs fragment loss measured: PQC identity in clear HELLO ≈ +2.5–4KB → 3–6 frags; capacity OK (`Packet.hpp:235` 16×MTU), any fragment loss = packet loss; if broken → cap/segment | benchmark | +| V14 | COM double-sign = type byte 1→2 appends ML-DSA sig alongside Ed25519; v1 verify path unchanged; v1 rejects type 2 cleanly | `node/CertificateOfMembership.hpp:222-296`, `node/CertificateOfMembership.cpp:97-162`, `selftest.cpp:714` | +| V15 | handshake vs fragment loss measured: PQC identity in clear HELLO = 3273B → 3 frags (classic 137B → 1 frag, max 7 `Packet.hpp:235`); Monte Carlo 20k trials obs≈analytic at p∈{.01,.05,.10,.30}; fragment loss delays HELLO (retransmit), does not break handshake | `selftest.cpp:1735` benchmark | ## §T (tasks) @@ -86,11 +86,11 @@ Derived from `selftest.cpp` + CI gates. `?` = code-derived, no test yet. | T16 | x | Federation identity-collision payload FIXME (`node/IncomingPacket.cpp:211`) `?` scope | I.capi,V7 | | T17 | x | Phase 1: vendor liboqs static into `ext/`, wire `make-linux.mk` + `objects.mk`, pin version/tag + flags | I.build | | T18 | x | Phase 2: `node/PQHybrid.*` hybrid agree = KDF(X25519-identity-agree ‖ ML-KEM-768 encaps over peer static pub); phase-2 ephemeral-field mockup dropped | I.pqc.wire,V13 | -| T19 | . | Phase 3: identity v2 type byte 1, append ML-KEM+ML-DSA pubs, address hash X25519-only, v1 clean reject type ≠ 0 | I.pqc.wire,V10 | -| T20 | . | Phase 4: COM type byte 2 double-sign (Ed25519+ML-DSA-65), v1 rejects cleanly | I.pqc.com,V14 | -| T21 | . | Phase 5: `local.conf` `settings.pqcMode` parse (off/hybrid/pqconly) + capability negotiation via HELLO protocol-version bytes, fallback classic | I.pqc.cfg,V12,V9 | -| T22 | . | Phase 6: selftest extension — ML-KEM/ML-DSA KATs, hybrid KDF vector, identity v2 roundtrip + address stability, COM double-sign, interop hybrid↔vanilla matrix | I.build,V11,V10,V14 | -| T23 | . | Phase 6: handshake-vs-fragment-loss benchmark for PQC HELLO; cap/segment if loss breaks handshake | V15 | +| T19 | x | Phase 3: identity v2 type byte 1, append ML-KEM+ML-DSA pubs, address hash X25519-only, v1 clean reject type ≠ 0 | I.pqc.wire,V10 | +| T20 | x | Phase 4: COM type byte 2 double-sign (Ed25519+ML-DSA-65), v1 rejects cleanly | I.pqc.com,V14 | +| T21 | x | Phase 5: `local.conf` `settings.pqcMode` parse (off/hybrid/pqconly) + capability negotiation via HELLO protocol-version bytes, fallback classic | I.pqc.cfg,V12,V9 | +| T22 | x | Phase 6: selftest extension — ML-KEM/ML-DSA KATs, hybrid KDF vector, identity v2 roundtrip + address stability, COM double-sign, interop hybrid↔vanilla matrix | I.build,V11,V10,V14 | +| T23 | x | Phase 6: handshake-vs-fragment-loss benchmark for PQC HELLO; cap/segment if loss breaks handshake | V15 | T1/T2 = prerequisites — CI must invoke `backone-selftest` before PQC gates mean anything. T1–T16 backlog (rebrand/CI/lint leftovers); T17–T23 = PQC phases 1–6. diff --git a/controller/EmbeddedNetworkController.cpp b/controller/EmbeddedNetworkController.cpp index 14c3725..629e76e 100644 --- a/controller/EmbeddedNetworkController.cpp +++ b/controller/EmbeddedNetworkController.cpp @@ -1454,7 +1454,7 @@ void EmbeddedNetworkController::_request( } } else { // If we do not yet know this member's identity, learn it. - char idtmp[1024]; + char idtmp[ZT_IDENTITY_STRING_BUFFER_LENGTH]; member["identity"] = identity.toString(false,idtmp); } #ifdef CENTRAL_CONTROLLER_REQUEST_BENCHMARK diff --git a/controller/PostgreSQL.cpp b/controller/PostgreSQL.cpp index c9f63cd..c2f03a0 100644 --- a/controller/PostgreSQL.cpp +++ b/controller/PostgreSQL.cpp @@ -1068,7 +1068,7 @@ void PostgreSQL::initializeMembers() void PostgreSQL::heartbeat() { - char publicId[1024]; + char publicId[ZT_IDENTITY_STRING_BUFFER_LENGTH]; char hostnameTmp[1024]; _myId.toString(false,publicId); if (gethostname(hostnameTmp, sizeof(hostnameTmp))!= 0) { diff --git a/node/CertificateOfMembership.cpp b/node/CertificateOfMembership.cpp index 65820c2..f43568d 100644 --- a/node/CertificateOfMembership.cpp +++ b/node/CertificateOfMembership.cpp @@ -44,6 +44,7 @@ CertificateOfMembership::CertificateOfMembership(uint64_t timestamp,uint64_t tim _qualifierCount = 7; memset(_signature.data,0,ZT_C25519_SIGNATURE_LEN); + _pqSignature.reset(); } bool CertificateOfMembership::agreesWith(const CertificateOfMembership &other, const Identity &otherIdentity) const @@ -106,9 +107,24 @@ bool CertificateOfMembership::sign(const Identity &with) try { _signature = with.sign(buf,ptr * sizeof(uint64_t)); _signedBy = with.address(); + // V14: if the signing identity is PQ capable, additionally ML-DSA-65 + // sign the same bytes so the COM serializes as type 2 (double-sign). + if (with.hasPQPrivate()) { + std::shared_ptr sig(new uint8_t[PQHybrid::MLDSA65_SIG_LEN], std::default_delete()); + if (! with.pqSign(buf, ptr * sizeof(uint64_t), sig.get())) { + _signedBy.zero(); + _pqSignature.reset(); + return false; + } + _pqSignature = sig; + } + else { + _pqSignature.reset(); + } return true; } catch ( ... ) { _signedBy.zero(); + _pqSignature.reset(); return false; } } @@ -132,7 +148,18 @@ int CertificateOfMembership::verify(const RuntimeEnvironment *RR,void *tPtr) con buf[ptr++] = Utils::hton(_qualifiers[i].value); buf[ptr++] = Utils::hton(_qualifiers[i].maxDelta); } - return (id.verify(buf,ptr * sizeof(uint64_t),_signature) ? 0 : -1); + + // V14: type 2 (double-sign) must verify under both algorithms; type 1 + // (Ed25519 only) keeps the original path unchanged. + if (! id.verify(buf, ptr * sizeof(uint64_t), _signature)) { + return -1; + } + if (_pqSignature) { + if (! id.pqVerify(buf, ptr * sizeof(uint64_t), _pqSignature.get())) { + return -1; + } + } + return 0; } } // namespace ZeroTier diff --git a/node/CertificateOfMembership.hpp b/node/CertificateOfMembership.hpp index 2c289bf..1b46fa9 100644 --- a/node/CertificateOfMembership.hpp +++ b/node/CertificateOfMembership.hpp @@ -14,21 +14,21 @@ #ifndef ZT_CERTIFICATEOFMEMBERSHIP_HPP #define ZT_CERTIFICATEOFMEMBERSHIP_HPP -#include -#include - -#include -#include -#include - +#include "Address.hpp" +#include "Buffer.hpp" +#include "C25519.hpp" #include "Constants.hpp" #include "Credential.hpp" -#include "Buffer.hpp" -#include "Address.hpp" -#include "C25519.hpp" #include "Identity.hpp" #include "Utils.hpp" +#include +#include +#include +#include +#include +#include + /** * Maximum number of qualifiers allowed in a COM (absolute max: 65535) */ @@ -221,7 +221,10 @@ public: template inline void serialize(Buffer &b) const { - b.append((uint8_t)1); + // V14: type 1 carries an Ed25519 signature only; type 2 appends an + // ML-DSA-65 signature over the same bytes (double-sign). A type 1 + // verifier rejects type 2 cleanly (unknown type byte). + b.append((uint8_t)((_pqSignature) ? 2 : 1)); b.append((uint16_t)_qualifierCount); for(unsigned int i=0;i<_qualifierCount;++i) { b.append(_qualifiers[i].id); @@ -231,6 +234,9 @@ public: _signedBy.appendTo(b); if (_signedBy) { b.append(_signature.data,ZT_C25519_SIGNATURE_LEN); + if (_pqSignature) { + b.append(_pqSignature.get(), PQHybrid::MLDSA65_SIG_LEN); + } } } @@ -241,8 +247,10 @@ public: _qualifierCount = 0; _signedBy.zero(); + _pqSignature.reset(); - if (b[p++] != 1) { + const unsigned int comType = (unsigned int)b[p++]; + if ((comType != 1) && (comType != 2)) { throw ZT_EXCEPTION_INVALID_SERIALIZED_DATA_INVALID_TYPE; } @@ -273,6 +281,14 @@ public: if (_signedBy) { memcpy(_signature.data,b.field(p,ZT_C25519_SIGNATURE_LEN),ZT_C25519_SIGNATURE_LEN); p += ZT_C25519_SIGNATURE_LEN; + if (comType == 2) { + std::shared_ptr sig(new uint8_t[PQHybrid::MLDSA65_SIG_LEN], std::default_delete()); + memcpy(sig.get(), b.field(p, PQHybrid::MLDSA65_SIG_LEN), PQHybrid::MLDSA65_SIG_LEN); + _pqSignature = sig; + } + } + else if (comType == 2) { + throw ZT_EXCEPTION_INVALID_SERIALIZED_DATA_INVALID_CRYPTOGRAPHIC_TOKEN; } return (p - startAt); @@ -293,11 +309,60 @@ public: return false; } } + if ((! _pqSignature) && (c._pqSignature)) { + return false; + } + if ((_pqSignature) && (! c._pqSignature)) { + return false; + } + if ((_pqSignature) && (memcmp(_pqSignature.get(), c._pqSignature.get(), PQHybrid::MLDSA65_SIG_LEN) != 0)) { + return false; + } return (memcmp(_signature.data,c._signature.data,ZT_C25519_SIGNATURE_LEN) == 0); } inline bool operator!=(const CertificateOfMembership &c) const { return (!(*this == c)); } -private: + /** + * @return ML-DSA-65 signature bytes if this is a type 2 (double-signed) + * COM, or null if type 1 (Ed25519 only) (V14) + */ + inline const uint8_t* pqSignature() const + { return _pqSignature.get(); } + + /** + * @return Number of qualifiers in this COM + */ + inline unsigned int qualifierCount() const + { return _qualifierCount; } + + /** + * @param i Qualifier index + * @return Qualifier field ID at index i + */ + inline uint64_t qualifierId(unsigned int i) const + { return _qualifiers[i].id; } + + /** + * @param i Qualifier index + * @return Qualifier value at index i + */ + inline uint64_t qualifierValue(unsigned int i) const + { return _qualifiers[i].value; } + + /** + * @param i Qualifier index + * @return Qualifier max delta at index i + */ + inline uint64_t qualifierMaxDelta(unsigned int i) const + { return _qualifiers[i].maxDelta; } + + /** + * @return Ed25519 signature bytes (all zero if unsigned) + */ + inline const uint8_t* signature() const + { return _signature.data; } + + private: struct _Qualifier { _Qualifier() : id(0),value(0),maxDelta(0) {} @@ -306,6 +371,7 @@ private: uint64_t maxDelta; inline bool operator<(const _Qualifier &q) const { return (id < q.id); } // sort order }; + std::shared_ptr _pqSignature; Address _signedBy; _Qualifier _qualifiers[ZT_NETWORK_COM_MAX_QUALIFIERS]; diff --git a/node/Identity.cpp b/node/Identity.cpp index f47de79..3b846a2 100644 --- a/node/Identity.cpp +++ b/node/Identity.cpp @@ -81,7 +81,7 @@ struct _Identity_generate_cond char *genmem; }; -void Identity::generate() +void Identity::generate(bool pq, int pqcMode) { unsigned char digest[64]; char *genmem = new char[ZT_IDENTITY_GEN_MEMORY]; @@ -99,6 +99,28 @@ void Identity::generate() *_privateKey = kp.priv; delete [] genmem; + + // V12: mode off never carries v2 material; pqconly always does. + // Hybrid leaves it to the caller (daemon passes a mode-derived flag once + // Node::pqcMode is plumbed) so bare generate() stays classic. + if (pqcMode == ZT_PQC_MODE_CLASSIC) { + pq = false; + } + else if (pqcMode == ZT_PQC_MODE_PQCONLY) { + pq = true; + } + // Identity type 1: ML-KEM-768 key exchange + ML-DSA-65 signature key pairs. + // Address derivation stays over the C25519 material only (V10). + if (pq) { + if (! _pq) { + _pq = new PQKeys(); + } + if ((! PQHybrid::generateKeypair(_pq->mlkemPk, _pq->mlkemSk)) || (! PQHybrid::generateSigKeypair(_pq->mldsaPk, _pq->mldsaSk))) { + Utils::burn(_pq, sizeof(PQKeys)); + delete _pq; + _pq = (PQKeys*)0; + } + } } bool Identity::locallyValidate() const @@ -130,7 +152,7 @@ char *Identity::toString(bool includePrivate,char buf[ZT_IDENTITY_STRING_BUFFER_ Utils::hex10(_address.toInt(),p); p += 10; *(p++) = ':'; - *(p++) = '0'; + *(p++) = (_pq) ? '1' : '0'; *(p++) = ':'; Utils::hex(_publicKey.data,ZT_C25519_PUBLIC_KEY_LEN,p); p += ZT_C25519_PUBLIC_KEY_LEN * 2; @@ -139,6 +161,32 @@ char *Identity::toString(bool includePrivate,char buf[ZT_IDENTITY_STRING_BUFFER_ Utils::hex(_privateKey->data,ZT_C25519_PRIVATE_KEY_LEN,p); p += ZT_C25519_PRIVATE_KEY_LEN * 2; } + // Type 1 appends the public ML-KEM-768 + ML-DSA-65 keys, and, when the + // private key is included, their private halves after it. + if (_pq) { + if ((_privateKey) && (includePrivate)) { + *(p++) = ':'; + Utils::hex(_pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN, p); + p += PQHybrid::MLKEM768_PK_LEN * 2; + *(p++) = ':'; + Utils::hex(_pq->mlkemSk, PQHybrid::MLKEM768_SK_LEN, p); + p += PQHybrid::MLKEM768_SK_LEN * 2; + *(p++) = ':'; + Utils::hex(_pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN, p); + p += PQHybrid::MLDSA65_PK_LEN * 2; + *(p++) = ':'; + Utils::hex(_pq->mldsaSk, PQHybrid::MLDSA65_SK_LEN, p); + p += PQHybrid::MLDSA65_SK_LEN * 2; + } + else { + *(p++) = ':'; + Utils::hex(_pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN, p); + p += PQHybrid::MLKEM768_PK_LEN * 2; + *(p++) = ':'; + Utils::hex(_pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN, p); + p += PQHybrid::MLDSA65_PK_LEN * 2; + } + } *p = (char)0; return buf; } @@ -155,8 +203,22 @@ bool Identity::fromString(const char *str) return false; } + // Field counts are fixed per identity type, which removes any ambiguity + // about whether field 3 is a private key or PQ material. + int nfields = 1; + for (const char* s = str; *s; ++s) { + if (*s == ':') { + ++nfields; + } + } + delete _privateKey; _privateKey = (C25519::Private *)0; + if (_pq) { + Utils::burn(_pq, sizeof(PQKeys)); + delete _pq; + _pq = (PQKeys*)0; + } int fno = 0; char *saveptr = (char *)0; @@ -170,7 +232,26 @@ bool Identity::fromString(const char *str) } break; case 1: - if ((f[0] != '0')||(f[1])) { + if (f[1]) { + _address.zero(); + return false; + } + if (f[0] == '0') { + if ((nfields != 3) && (nfields != 4)) { + _address.zero(); + return false; + } + } + else if (f[0] == '1') { + // V10: a type 0 parser must reject this cleanly. + if ((nfields != 5) && (nfields != 8)) { + _address.zero(); + return false; + } + _pq = new PQKeys(); + memset(_pq, 0, sizeof(PQKeys)); + } + else { _address.zero(); return false; } @@ -182,8 +263,69 @@ bool Identity::fromString(const char *str) } break; case 3: - _privateKey = new C25519::Private(); - if (Utils::unhex(f,_privateKey->data,ZT_C25519_PRIVATE_KEY_LEN) != ZT_C25519_PRIVATE_KEY_LEN) { + if ((_pq) && (nfields == 8)) { + _privateKey = new C25519::Private(); + if (Utils::unhex(f, _privateKey->data, ZT_C25519_PRIVATE_KEY_LEN) != ZT_C25519_PRIVATE_KEY_LEN) { + _address.zero(); + return false; + } + } + else if (_pq) { + if (Utils::unhex(f, _pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN) != PQHybrid::MLKEM768_PK_LEN) { + _address.zero(); + return false; + } + } + else { + _privateKey = new C25519::Private(); + if (Utils::unhex(f, _privateKey->data, ZT_C25519_PRIVATE_KEY_LEN) != ZT_C25519_PRIVATE_KEY_LEN) { + _address.zero(); + return false; + } + } + break; + case 4: + if (! _pq) { + _address.zero(); + return false; + } + if (nfields == 5) { + if (Utils::unhex(f, _pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN) != PQHybrid::MLDSA65_PK_LEN) { + _address.zero(); + return false; + } + } + else if (Utils::unhex(f, _pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN) != PQHybrid::MLKEM768_PK_LEN) { + _address.zero(); + return false; + } + break; + case 5: + if ((! _pq) || (nfields != 8)) { + _address.zero(); + return false; + } + if (Utils::unhex(f, _pq->mlkemSk, PQHybrid::MLKEM768_SK_LEN) != PQHybrid::MLKEM768_SK_LEN) { + _address.zero(); + return false; + } + break; + case 6: + if ((! _pq) || (nfields != 8)) { + _address.zero(); + return false; + } + if (Utils::unhex(f, _pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN) != PQHybrid::MLDSA65_PK_LEN) { + _address.zero(); + return false; + } + break; + case 7: + if ((! _pq) || (nfields != 8)) { + _address.zero(); + return false; + } + if (Utils::unhex(f, _pq->mldsaSk, PQHybrid::MLDSA65_SK_LEN) != PQHybrid::MLDSA65_SK_LEN) { _address.zero(); return false; } @@ -193,7 +335,7 @@ bool Identity::fromString(const char *str) return false; } } - if (fno < 3) { + if ((fno < 3) || (fno != nfields)) { _address.zero(); return false; } diff --git a/node/Identity.hpp b/node/Identity.hpp index b7580a8..ae9d936 100644 --- a/node/Identity.hpp +++ b/node/Identity.hpp @@ -1,3 +1,8 @@ +// PQC capability mode (local.conf settings.pqcMode / Identity::generate) +#define ZT_PQC_MODE_CLASSIC 0 // off: C25519 key agreement only +#define ZT_PQC_MODE_HYBRID 1 // hybrid: C25519 agreement with ML-KEM-768 mixed in +#define ZT_PQC_MODE_PQCONLY 2 // pqconly: PQ-only identities, no classic fallback + /* * Copyright (c)2019 ZeroTier, Inc. * @@ -14,17 +19,35 @@ #ifndef ZT_IDENTITY_HPP #define ZT_IDENTITY_HPP +#include "Address.hpp" +#include "Buffer.hpp" +#include "C25519.hpp" +#include "Constants.hpp" +#include "PQHybrid.hpp" +#include "SHA512.hpp" +#include "Utils.hpp" + #include #include -#include "Constants.hpp" -#include "Utils.hpp" -#include "Address.hpp" -#include "C25519.hpp" -#include "Buffer.hpp" -#include "SHA512.hpp" +#define ZT_IDENTITY_STRING_BUFFER_LENGTH 19456 -#define ZT_IDENTITY_STRING_BUFFER_LENGTH 384 +// Identity type byte (binary and string form, field 1) +#define ZT_IDENTITY_TYPE_C25519 0 +#define ZT_IDENTITY_TYPE_PQ_HYBRID 1 + +// Serialized length of type 1 PQ key material (all four fields always present) +#define ZT_IDENTITY_PQ_LENGTH (PQHybrid::MLKEM768_PK_LEN + PQHybrid::MLKEM768_SK_LEN + PQHybrid::MLDSA65_PK_LEN + PQHybrid::MLDSA65_SK_LEN) + +// Maximum binary serialized length of a type 1 identity with private keys +// (address + type byte + C25519 pub + private length byte + private key + +// all four PQ keys). Public-only serialization is smaller (PQ public halves). +#define ZT_IDENTITY_MAX_BINARY_LENGTH (ZT_ADDRESS_LENGTH + 1 + ZT_C25519_PUBLIC_KEY_LEN + 1 + ZT_C25519_PRIVATE_KEY_LEN + ZT_IDENTITY_PQ_LENGTH) + +// Maximum binary serialized length of a public-only (no private key) type 1 +// identity: address + type byte + C25519 pub + zero private length byte + +// ML-KEM-768 and ML-DSA-65 public keys. +#define ZT_IDENTITY_MAX_PUBLIC_BINARY_LENGTH (ZT_ADDRESS_LENGTH + 1 + ZT_C25519_PUBLIC_KEY_LEN + 1 + PQHybrid::MLKEM768_PK_LEN + PQHybrid::MLDSA65_PK_LEN) namespace ZeroTier { @@ -41,40 +64,48 @@ namespace ZeroTier { class Identity { public: - Identity() : - _privateKey((C25519::Private *)0) - { - } + /** + * Post-quantum key material for identity type 1 (ML-KEM-768 + ML-DSA-65). + * All four fields are always present in a type 1 identity; owning an + * instance implies a full keypair (pub+priv). Identities without private + * key material carry only pqPublicKey()/pqSignaturePublicKey(). + */ + struct PQKeys { + uint8_t mlkemPk[PQHybrid::MLKEM768_PK_LEN]; + uint8_t mlkemSk[PQHybrid::MLKEM768_SK_LEN]; + uint8_t mldsaPk[PQHybrid::MLDSA65_PK_LEN]; + uint8_t mldsaSk[PQHybrid::MLDSA65_SK_LEN]; + }; - Identity(const Identity &id) : - _address(id._address), - _publicKey(id._publicKey), - _privateKey((id._privateKey) ? new C25519::Private(*(id._privateKey)) : (C25519::Private *)0) - { - } + Identity() : _privateKey((C25519::Private*)0), _pq((PQKeys*)0) + { + } - Identity(const char *str) : - _privateKey((C25519::Private *)0) - { - if (!fromString(str)) { - throw ZT_EXCEPTION_INVALID_SERIALIZED_DATA_INVALID_TYPE; - } - } + Identity(const Identity& id) : _address(id._address), _publicKey(id._publicKey), _privateKey((id._privateKey) ? new C25519::Private(*(id._privateKey)) : (C25519::Private*)0), _pq((id._pq) ? new PQKeys(*(id._pq)) : (PQKeys*)0) + { + } - template - Identity(const Buffer &b,unsigned int startAt = 0) : - _privateKey((C25519::Private *)0) - { - deserialize(b,startAt); - } + Identity(const char* str) : _privateKey((C25519::Private*)0), _pq((PQKeys*)0) + { + if (! fromString(str)) { + throw ZT_EXCEPTION_INVALID_SERIALIZED_DATA_INVALID_TYPE; + } + } - ~Identity() - { - if (_privateKey) { - Utils::burn(_privateKey,sizeof(C25519::Private)); - delete _privateKey; - } - } + template Identity(const Buffer& b, unsigned int startAt = 0) : _privateKey((C25519::Private*)0), _pq((PQKeys*)0) + { deserialize(b, startAt); } + + ~Identity() + { + if (_privateKey) { + Utils::burn(_privateKey, sizeof(C25519::Private)); + delete _privateKey; + } + if (_pq) { + Utils::burn(_pq, sizeof(PQKeys)); + delete _pq; + } + } inline Identity &operator=(const Identity &id) { @@ -89,15 +120,32 @@ public: delete _privateKey; _privateKey = (C25519::Private *)0; } + if (id._pq) { + if (! _pq) { + _pq = new PQKeys(); + } + *_pq = *(id._pq); + } + else { + if (_pq) { + Utils::burn(_pq, sizeof(PQKeys)); + delete _pq; + _pq = (PQKeys*)0; + } + } return *this; } /** - * Generate a new identity (address, key pair) + * Generate a new identity (address, C25519 key pair, and ML-KEM-768 + + * ML-DSA-65 key pairs when @param pq is true) * * This is a time consuming operation. + * + * @param pq If true, also generate post-quantum key material (identity type 1) + * @param pqcMode PQC capability mode: 0=classic, 1=hybrid, 2=pqc_only */ - void generate(); + void generate(bool pq = false, int pqcMode = 1); /** * Check the validity of this identity's pairing of key to address @@ -111,6 +159,107 @@ public: */ inline bool hasPrivate() const { return (_privateKey != (C25519::Private *)0); } + /** + * @return True if this identity is type 1 (carries ML-KEM-768 + ML-DSA-65 public keys) + */ + inline bool hasPQ() const + { return (_pq != (PQKeys*)0); } + + /** + * @return ML-KEM-768 + ML-DSA-65 public keys, or null if not a type 1 identity + */ + inline const PQKeys* pqKeys() const + { return _pq; } + + /** + * Build the public half (public keys only) of a type 1 identity's PQ material. + * + * @param out Destination receiving public key material + * @return True if this identity has PQ material + */ + inline bool pqPublicKeys(PQKeys& out) const + { + if (! _pq) { + return false; + } + memcpy(out.mlkemPk, _pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN); + memset(out.mlkemSk, 0, PQHybrid::MLKEM768_SK_LEN); + memcpy(out.mldsaPk, _pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN); + memset(out.mldsaSk, 0, PQHybrid::MLDSA65_SK_LEN); + return true; + } + + /** + * @param pk Destination for ML-KEM-768 public key bytes (or null) + * @return True if this identity carries a post-quantum key exchange key + */ + inline bool pqKemKey(uint8_t* pk) const + { + if (! _pq) { + return false; + } + if (pk) { + memcpy(pk, _pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN); + } + return true; + } + + /** + * @param pk Destination for ML-DSA-65 public key bytes (or null) + * @return True if this identity carries a post-quantum signature key + */ + inline bool pqSigKey(uint8_t* pk) const + { + if (! _pq) { + return false; + } + if (pk) { + memcpy(pk, _pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN); + } + return true; + } + + /** + * @return True if this is a type 1 (PQ hybrid) identity carrying usable + * ML-DSA-65 private key material (public-only identities return false) + */ + inline bool hasPQPrivate() const + { return ((_pq != (PQKeys*)0) && (_privateKey != (C25519::Private*)0)); } + + /** + * Sign message with this identity's ML-DSA-65 private key (V14 double-sign + * half). Caller must supply exactly PQHybrid::MLDSA65_SIG_LEN bytes. + * + * @param data Data to sign + * @param len Length of data + * @param sig Destination for the ML-DSA-65 signature + * @return True on success + * @throws ZT_EXCEPTION_PRIVATE_KEY_REQUIRED if no PQ private key present + */ + inline bool pqSign(const void* data, unsigned int len, uint8_t* sig) const + { + if (! hasPQPrivate()) { + throw ZT_EXCEPTION_PRIVATE_KEY_REQUIRED; + } + return PQHybrid::sign(_pq->mldsaSk, data, len, sig); + } + + /** + * Verify an ML-DSA-65 signature made with this identity's PQ signature key. + * + * @param data Data that was signed + * @param len Length of data + * @param sig ML-DSA-65 signature (PQHybrid::MLDSA65_SIG_LEN bytes) + * @return True if signature validates, false if not or no PQ key present + */ + inline bool pqVerify(const void* data, unsigned int len, const uint8_t* sig) const + { + if (! _pq) { + return false; + } + return PQHybrid::verify(_pq->mldsaPk, data, len, sig); + } + /** * Compute a SHA384 hash of this identity's address and public key(s). * @@ -200,6 +349,63 @@ public: return false; } + /** + * Hybrid post-quantum key agreement, encapsulate side (V13): + * key = HMAC-SHA-384(X25519-identity-agree || ML-KEM-768 encaps to peer static pk). + * Static-static: no ephemeral exchange. + * + * This identity must have a private key; the peer must be type 1 (hasPQ()). + * + * @param id Peer identity (must be type 1) + * @param ct Result buffer of PQHybrid::MLKEM768_CT_LEN bytes for the ML-KEM-768 ciphertext + * @param key Result buffer of ZT_SYMMETRIC_KEY_SIZE bytes for the hybrid key + * @return Was agreement successful? + */ + inline bool agreeHybridEncaps(const Identity& id, void* const ct, void* const key) const + { + if ((! _privateKey) || (! id._pq)) { + return false; + } + uint8_t classical[ZT_SYMMETRIC_KEY_SIZE]; + uint8_t pq[PQHybrid::MLKEM768_SS_LEN]; + if (! PQHybrid::encaps(id._pq->mlkemPk, reinterpret_cast(ct), pq)) { + return false; + } + C25519::agree(*_privateKey, id._publicKey, classical, ZT_SYMMETRIC_KEY_SIZE); + PQHybrid::hybridKey(classical, pq, reinterpret_cast(key)); + Utils::burn(classical, sizeof(classical)); + Utils::burn(pq, sizeof(pq)); + return true; + } + + /** + * Hybrid post-quantum key agreement, decapsulate side (V13): + * recompute key from the peer's ML-KEM-768 ciphertext with this identity's secret. + * + * This identity must be type 1 with a private key; the peer is classical. + * + * @param id Peer identity (classical public key) + * @param ct ML-KEM-768 ciphertext of PQHybrid::MLKEM768_CT_LEN bytes + * @param key Result buffer of ZT_SYMMETRIC_KEY_SIZE bytes for the hybrid key + * @return Was agreement successful? + */ + inline bool agreeHybridDecaps(const Identity& id, const void* const ct, void* const key) const + { + if ((! _privateKey) || (! _pq)) { + return false; + } + uint8_t classical[ZT_SYMMETRIC_KEY_SIZE]; + uint8_t pq[PQHybrid::MLKEM768_SS_LEN]; + if (! PQHybrid::decaps(_pq->mlkemSk, reinterpret_cast(ct), pq)) { + return false; + } + C25519::agree(*_privateKey, id._publicKey, classical, ZT_SYMMETRIC_KEY_SIZE); + PQHybrid::hybridKey(classical, pq, reinterpret_cast(key)); + Utils::burn(classical, sizeof(classical)); + Utils::burn(pq, sizeof(pq)); + return true; + } + /** * @return This identity's address */ @@ -216,7 +422,7 @@ public: inline void serialize(Buffer &b,bool includePrivate = false) const { _address.appendTo(b); - b.append((uint8_t)0); // C25519/Ed25519 identity type + b.append((uint8_t)((_pq) ? ZT_IDENTITY_TYPE_PQ_HYBRID : ZT_IDENTITY_TYPE_C25519)); b.append(_publicKey.data,ZT_C25519_PUBLIC_KEY_LEN); if ((_privateKey)&&(includePrivate)) { b.append((unsigned char)ZT_C25519_PRIVATE_KEY_LEN); @@ -224,6 +430,16 @@ public: } else { b.append((unsigned char)0); } + if (_pq) { + // Public ML-KEM + ML-DSA keys always go on the wire; the private + // halves follow only when the C25519 private key is included. + b.append(_pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN); + b.append(_pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN); + if ((_privateKey) && (includePrivate)) { + b.append(_pq->mlkemSk, PQHybrid::MLKEM768_SK_LEN); + b.append(_pq->mldsaSk, PQHybrid::MLDSA65_SK_LEN); + } + } } /** @@ -243,13 +459,20 @@ public: { delete _privateKey; _privateKey = (C25519::Private *)0; + if (_pq) { + Utils::burn(_pq, sizeof(PQKeys)); + delete _pq; + _pq = (PQKeys*)0; + } unsigned int p = startAt; _address.setTo(b.field(p,ZT_ADDRESS_LENGTH),ZT_ADDRESS_LENGTH); p += ZT_ADDRESS_LENGTH; - if (b[p++] != 0) { + // V10: a type 0 parser rejects type bytes other than 0 cleanly. + const unsigned int identityType = (unsigned int)b[p++]; + if ((identityType != ZT_IDENTITY_TYPE_C25519) && (identityType != ZT_IDENTITY_TYPE_PQ_HYBRID)) { throw ZT_EXCEPTION_INVALID_SERIALIZED_DATA_INVALID_TYPE; } @@ -266,6 +489,24 @@ public: p += ZT_C25519_PRIVATE_KEY_LEN; } + if (identityType == ZT_IDENTITY_TYPE_PQ_HYBRID) { + _pq = new PQKeys(); + memcpy(_pq->mlkemPk, b.field(p, PQHybrid::MLKEM768_PK_LEN), PQHybrid::MLKEM768_PK_LEN); + p += PQHybrid::MLKEM768_PK_LEN; + memcpy(_pq->mldsaPk, b.field(p, PQHybrid::MLDSA65_PK_LEN), PQHybrid::MLDSA65_PK_LEN); + p += PQHybrid::MLDSA65_PK_LEN; + if (_privateKey) { + memcpy(_pq->mlkemSk, b.field(p, PQHybrid::MLKEM768_SK_LEN), PQHybrid::MLKEM768_SK_LEN); + p += PQHybrid::MLKEM768_SK_LEN; + memcpy(_pq->mldsaSk, b.field(p, PQHybrid::MLDSA65_SK_LEN), PQHybrid::MLDSA65_SK_LEN); + p += PQHybrid::MLDSA65_SK_LEN; + } + else { + memset(_pq->mlkemSk, 0, PQHybrid::MLKEM768_SK_LEN); + memset(_pq->mldsaSk, 0, PQHybrid::MLDSA65_SK_LEN); + } + } + return (p - startAt); } @@ -314,8 +555,47 @@ public: */ inline operator bool() const { return (_address); } - inline bool operator==(const Identity &id) const { return ((_address == id._address)&&(memcmp(_publicKey.data,id._publicKey.data,ZT_C25519_PUBLIC_KEY_LEN) == 0)); } - inline bool operator<(const Identity &id) const { return ((_address < id._address)||((_address == id._address)&&(memcmp(_publicKey.data,id._publicKey.data,ZT_C25519_PUBLIC_KEY_LEN) < 0))); } + // Two identities are equal only if address, C25519 public key, and PQ + // public keys all match. Address derivation hashes X25519 material only + // (V10), so the same X25519 key may be re-published with different PQ + // keys; those are distinct identities and must not compare equal. + inline bool operator==(const Identity& id) const + { + if ((_address != id._address) || (memcmp(_publicKey.data, id._publicKey.data, ZT_C25519_PUBLIC_KEY_LEN) != 0)) { + return false; + } + if ((_pq == (PQKeys*)0) && (id._pq == (PQKeys*)0)) { + return true; + } + if ((_pq == (PQKeys*)0) || (id._pq == (PQKeys*)0)) { + return false; + } + return ((memcmp(_pq->mlkemPk, id._pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN) == 0) && (memcmp(_pq->mldsaPk, id._pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN) == 0)); + } + inline bool operator<(const Identity& id) const + { + if (_address < id._address) { + return true; + } + if (id._address < _address) { + return false; + } + int c = memcmp(_publicKey.data, id._publicKey.data, ZT_C25519_PUBLIC_KEY_LEN); + if (c != 0) { + return (c < 0); + } + if (_pq == (PQKeys*)0) { + return (id._pq != (PQKeys*)0); + } + if (id._pq == (PQKeys*)0) { + return false; + } + c = memcmp(_pq->mlkemPk, id._pq->mlkemPk, PQHybrid::MLKEM768_PK_LEN); + if (c != 0) { + return (c < 0); + } + return (memcmp(_pq->mldsaPk, id._pq->mldsaPk, PQHybrid::MLDSA65_PK_LEN) < 0); + } inline bool operator!=(const Identity &id) const { return !(*this == id); } inline bool operator>(const Identity &id) const { return (id < *this); } inline bool operator<=(const Identity &id) const { return !(id < *this); } @@ -325,6 +605,7 @@ private: Address _address; C25519::Public _publicKey; C25519::Private *_privateKey; + PQKeys* _pq; }; } // namespace ZeroTier diff --git a/node/IncomingPacket.cpp b/node/IncomingPacket.cpp index 21deb8a..461aa6d 100644 --- a/node/IncomingPacket.cpp +++ b/node/IncomingPacket.cpp @@ -525,7 +525,7 @@ bool IncomingPacket::_doHELLO(const RuntimeEnvironment *RR,void *tPtr,const bool outp.append((unsigned char)ZT_PROTO_VERSION); outp.append((unsigned char)ZEROTIER_ONE_VERSION_MAJOR); outp.append((unsigned char)ZEROTIER_ONE_VERSION_MINOR); - outp.append((uint16_t)ZEROTIER_ONE_VERSION_REVISION); + outp.append((uint16_t)(ZEROTIER_ONE_VERSION_REVISION | ((RR->node->pqcMode() != ZT_PQC_MODE_CLASSIC) ? ZT_PROTO_VERB_HELLO_REVISION_CAPABILITY_BIT : 0))); if (protoVersion >= 5) { _path->address().serialize(outp); diff --git a/node/Node.cpp b/node/Node.cpp index 866c0bd..9e9c73e 100644 --- a/node/Node.cpp +++ b/node/Node.cpp @@ -43,17 +43,18 @@ namespace ZeroTier { /* Public Node interface (C++, exposed via CAPI bindings) */ /****************************************************************************/ -Node::Node(void *uptr,void *tptr,const struct ZT_Node_Callbacks *callbacks,int64_t now) : - _RR(this), - RR(&_RR), - _uPtr(uptr), - _networks(8), - _now(now), - _lastPingCheck(0), - _lastGratuitousPingCheck(0), - _lastHousekeepingRun(0), - _lastMemoizedTraceSettings(0), - _lowBandwidthMode(false) +Node::Node(void* uptr, void* tptr, const struct ZT_Node_Callbacks* callbacks, int64_t now) + : _RR(this) + , RR(&_RR) + , _uPtr(uptr) + , _networks(8) + , _now(now) + , _lastPingCheck(0) + , _lastGratuitousPingCheck(0) + , _lastHousekeepingRun(0) + , _lastMemoizedTraceSettings(0) + , _lowBandwidthMode(false) + , _pqcMode(ZT_PQC_MODE_HYBRID) { if (callbacks->version != 0) { throw ZT_EXCEPTION_INVALID_ARGUMENT; @@ -73,7 +74,9 @@ Node::Node(void *uptr,void *tptr,const struct ZT_Node_Callbacks *callbacks,int64 uint64_t idtmp[2]; idtmp[0] = 0; idtmp[1] = 0; - char tmp[2048]; + // Must hold the full private identity string form, which for a type 1 + // (PQ hybrid) identity is ZT_IDENTITY_STRING_BUFFER_LENGTH bytes. + char tmp[ZT_IDENTITY_STRING_BUFFER_LENGTH]; int n = stateObjectGet(tptr,ZT_STATE_OBJECT_IDENTITY_SECRET,idtmp,tmp,sizeof(tmp) - 1); if (n > 0) { tmp[n] = (char)0; diff --git a/node/Node.hpp b/node/Node.hpp index f9d0548..df71d25 100644 --- a/node/Node.hpp +++ b/node/Node.hpp @@ -283,6 +283,16 @@ public: return _lowBandwidthMode; } + /** + * Set PQC capability mode (ZT_PQC_MODE_*): 0=classic, 1=hybrid, 2=pqconly. + * Applied to identity generation and HELLO capability advertisement. + */ + inline void setPqcMode(int mode) + { _pqcMode = mode; } + + inline int pqcMode() const + { return _pqcMode; } + void initMultithreading(unsigned int concurrency, bool cpuPinningEnabled); @@ -334,6 +344,7 @@ public: volatile int64_t _prngState[2]; bool _online; bool _lowBandwidthMode; + int _pqcMode; }; } // namespace ZeroTier diff --git a/node/PQHybrid.cpp b/node/PQHybrid.cpp index 5227787..cfd84c4 100644 --- a/node/PQHybrid.cpp +++ b/node/PQHybrid.cpp @@ -4,7 +4,7 @@ #include "SHA512.hpp" #include - +#include namespace ZeroTier { static_assert(PQHybrid::MLKEM768_PK_LEN == OQS_KEM_ml_kem_768_length_public_key, "ML-KEM-768 public key size mismatch"); @@ -12,6 +12,9 @@ static_assert(PQHybrid::MLKEM768_SK_LEN == OQS_KEM_ml_kem_768_length_secret_key, static_assert(PQHybrid::MLKEM768_CT_LEN == OQS_KEM_ml_kem_768_length_ciphertext, "ML-KEM-768 ciphertext size mismatch"); static_assert(PQHybrid::MLKEM768_SS_LEN == OQS_KEM_ml_kem_768_length_shared_secret, "ML-KEM-768 shared secret size mismatch"); static_assert(PQHybrid::MLKEM768_SS_LEN == 32, "hybrid KDF expects 32-byte pq secret"); +static_assert(PQHybrid::MLDSA65_PK_LEN == OQS_SIG_ml_dsa_65_length_public_key, "ML-DSA-65 public key size mismatch"); +static_assert(PQHybrid::MLDSA65_SK_LEN == OQS_SIG_ml_dsa_65_length_secret_key, "ML-DSA-65 secret key size mismatch"); +static_assert(PQHybrid::MLDSA65_SIG_LEN == OQS_SIG_ml_dsa_65_length_signature, "ML-DSA-65 signature size mismatch"); bool PQHybrid::generateKeypair(uint8_t* pk, uint8_t* sk) { return (OQS_KEM_ml_kem_768_keypair(pk, sk) == OQS_SUCCESS); } @@ -25,4 +28,16 @@ bool PQHybrid::decaps(const uint8_t* sk, const uint8_t* ct, uint8_t* ss) void PQHybrid::hybridKey(const uint8_t classical[ZT_SYMMETRIC_KEY_SIZE], const uint8_t pq[MLKEM768_SS_LEN], uint8_t out[ZT_SYMMETRIC_KEY_SIZE]) { HMACSHA384(classical, pq, MLKEM768_SS_LEN, out); } +bool PQHybrid::generateSigKeypair(uint8_t* pk, uint8_t* sk) +{ return (OQS_SIG_ml_dsa_65_keypair(pk, sk) == OQS_SUCCESS); } + +bool PQHybrid::sign(const uint8_t* sk, const void* msg, unsigned int len, uint8_t* sig) +{ + size_t siglen = 0; + return (OQS_SIG_ml_dsa_65_sign(sig, &siglen, (const uint8_t*)msg, len, sk) == OQS_SUCCESS) && (siglen == MLDSA65_SIG_LEN); +} + +bool PQHybrid::verify(const uint8_t* pk, const void* msg, unsigned int len, const uint8_t* sig) +{ return (OQS_SIG_ml_dsa_65_verify((const uint8_t*)msg, len, sig, MLDSA65_SIG_LEN, pk) == OQS_SUCCESS); } + } // namespace ZeroTier diff --git a/node/PQHybrid.hpp b/node/PQHybrid.hpp index 7a1360d..2a94f17 100644 --- a/node/PQHybrid.hpp +++ b/node/PQHybrid.hpp @@ -29,6 +29,20 @@ class PQHybrid { static const unsigned int MLKEM768_CT_LEN = 1088; static const unsigned int MLKEM768_SS_LEN = 32; + // Standard ML-DSA-65 sizes (FIPS 204) + static const unsigned int MLDSA65_PK_LEN = 1952; + static const unsigned int MLDSA65_SK_LEN = 4032; + static const unsigned int MLDSA65_SIG_LEN = 3309; + + /** Generate ML-DSA-65 long-term signature keypair (for identity v2 storage) */ + static bool generateSigKeypair(uint8_t* pk, uint8_t* sk); + + /** Sign with ML-DSA-65 secret key -> MLDSA65_SIG_LEN signature bytes */ + static bool sign(const uint8_t* sk, const void* msg, unsigned int len, uint8_t* sig); + + /** Verify ML-DSA-65 signature over message */ + static bool verify(const uint8_t* pk, const void* msg, unsigned int len, const uint8_t* sig); + /** Generate ML-KEM-768 static keypair (for identity v2 storage) */ static bool generateKeypair(uint8_t* pk, uint8_t* sk); diff --git a/node/Packet.hpp b/node/Packet.hpp index f607d1f..b149642 100644 --- a/node/Packet.hpp +++ b/node/Packet.hpp @@ -270,6 +270,8 @@ #define ZT_PROTO_VERB_HELLO_IDX_TIMESTAMP (ZT_PROTO_VERB_HELLO_IDX_REVISION + 2) #define ZT_PROTO_VERB_HELLO_IDX_IDENTITY (ZT_PROTO_VERB_HELLO_IDX_TIMESTAMP + 8) +#define ZT_PROTO_VERB_HELLO_REVISION_CAPABILITY_BIT 0x8000 + #define ZT_PROTO_VERB_ERROR_IDX_IN_RE_VERB (ZT_PACKET_IDX_PAYLOAD) #define ZT_PROTO_VERB_ERROR_IDX_IN_RE_PACKET_ID (ZT_PROTO_VERB_ERROR_IDX_IN_RE_VERB + 1) #define ZT_PROTO_VERB_ERROR_IDX_ERROR_CODE (ZT_PROTO_VERB_ERROR_IDX_IN_RE_PACKET_ID + 8) diff --git a/node/Peer.cpp b/node/Peer.cpp index f77b4e6..3d1fd56 100644 --- a/node/Peer.cpp +++ b/node/Peer.cpp @@ -422,7 +422,7 @@ void Peer::sendHELLO(void *tPtr,const int64_t localSocket,const InetAddress &atA outp.append((unsigned char)ZT_PROTO_VERSION); outp.append((unsigned char)ZEROTIER_ONE_VERSION_MAJOR); outp.append((unsigned char)ZEROTIER_ONE_VERSION_MINOR); - outp.append((uint16_t)ZEROTIER_ONE_VERSION_REVISION); + outp.append((uint16_t)(ZEROTIER_ONE_VERSION_REVISION | ((RR->node->pqcMode() != ZT_PQC_MODE_CLASSIC) ? ZT_PROTO_VERB_HELLO_REVISION_CAPABILITY_BIT : 0))); outp.append(now); RR->identity.serialize(outp,false); atAddress.serialize(outp); diff --git a/node/Peer.hpp b/node/Peer.hpp index 777a1e9..6cd2d26 100644 --- a/node/Peer.hpp +++ b/node/Peer.hpp @@ -36,7 +36,10 @@ #include "AES.hpp" #include "Metrics.hpp" -#define ZT_PEER_MAX_SERIALIZED_STATE_SIZE (sizeof(Peer) + 32 + (sizeof(Path) * 2)) +// A cached peer is: version byte + serialized public identity (type 1 may be +// a PQ hybrid with ML-KEM-768 + ML-DSA-65 public keys) + four 16-bit version +// fields + path count + one serialized InetAddress per path. +#define ZT_PEER_MAX_SERIALIZED_STATE_SIZE (1 + ZT_IDENTITY_MAX_PUBLIC_BINARY_LENGTH + 8 + 2 + (ZT_MAX_PEER_NETWORK_PATHS * 25)) namespace ZeroTier { @@ -374,9 +377,17 @@ public: inline unsigned int remoteVersionProtocol() const { return _vProto; } inline unsigned int remoteVersionMajor() const { return _vMajor; } inline unsigned int remoteVersionMinor() const { return _vMinor; } - inline unsigned int remoteVersionRevision() const { return _vRevision; } + inline unsigned int remoteVersionRevision() const + { return (_vRevision & 0x7fff); } // excludes PQC capability bit (see pqcCapability()) - inline bool remoteVersionKnown() const { return ((_vMajor > 0)||(_vMinor > 0)||(_vRevision > 0)); } + inline bool remoteVersionKnown() const + { return ((_vMajor > 0) || (_vMinor > 0) || (_vRevision > 0)); } + + /** + * @return True if peer advertised PQC capability via HELLO revision high bit + */ + inline bool pqcCapability() const + { return ((_vRevision & ZT_PROTO_VERB_HELLO_REVISION_CAPABILITY_BIT) != 0); } /** * @return True if peer has received a trust established packet (e.g. common network membership) in the past ZT_TRUST_EXPIRATION ms diff --git a/node/World.hpp b/node/World.hpp index 1c27b85..21d8c47 100644 --- a/node/World.hpp +++ b/node/World.hpp @@ -41,7 +41,7 @@ /** * The (more than) maximum length of a serialized World */ -#define ZT_WORLD_MAX_SERIALIZED_LENGTH (((1024 + (32 * ZT_WORLD_MAX_STABLE_ENDPOINTS_PER_ROOT)) * ZT_WORLD_MAX_ROOTS) + ZT_C25519_PUBLIC_KEY_LEN + ZT_C25519_SIGNATURE_LEN + 128) +#define ZT_WORLD_MAX_SERIALIZED_LENGTH (((ZT_IDENTITY_MAX_PUBLIC_BINARY_LENGTH + (32 * ZT_WORLD_MAX_STABLE_ENDPOINTS_PER_ROOT)) * ZT_WORLD_MAX_ROOTS) + ZT_C25519_PUBLIC_KEY_LEN + ZT_C25519_SIGNATURE_LEN + 128) /** * World ID for Earth diff --git a/one.cpp b/one.cpp index 2b9b7be..eec3a7b 100644 --- a/one.cpp +++ b/one.cpp @@ -1485,7 +1485,7 @@ static int idtool(int argc,char **argv) } } - char idtmp[1024]; + char idtmp[ZT_IDENTITY_STRING_BUFFER_LENGTH]; std::string idser = id.toString(true,idtmp); if (argc >= 3) { if (!OSUtils::writeFile(argv[2],idser)) { @@ -1528,7 +1528,7 @@ static int idtool(int argc,char **argv) return 1; } - char idtmp[1024]; + char idtmp[ZT_IDENTITY_STRING_BUFFER_LENGTH]; printf("%s",id.toString(false,idtmp)); } else if (!strcmp(argv[1],"sign")) { if (argc < 4) { @@ -1604,7 +1604,10 @@ static int idtool(int argc,char **argv) C25519::Pair kp(C25519::generate()); + // idtmp holds hex key material, idstr the serialized identity + // (type 1 public form needs ZT_IDENTITY_STRING_BUFFER_LENGTH). char idtmp[4096]; + char idstr[ZT_IDENTITY_STRING_BUFFER_LENGTH]; nlohmann::json mj; mj["objtype"] = "world"; mj["worldType"] = "moon"; @@ -1612,7 +1615,7 @@ static int idtool(int argc,char **argv) mj["signingKey_SECRET"] = Utils::hex(kp.priv.data,ZT_C25519_PRIVATE_KEY_LEN,idtmp); mj["id"] = id.address().toString(idtmp); nlohmann::json seedj; - seedj["identity"] = id.toString(false,idtmp); + seedj["identity"] = id.toString(false, idstr); seedj["stableEndpoints"] = nlohmann::json::array(); (mj["roots"] = nlohmann::json::array()).push_back(seedj); std::string mjd(OSUtils::jsonDump(mj)); diff --git a/selftest.cpp b/selftest.cpp index 29ad3f0..2a74f4e 100644 --- a/selftest.cpp +++ b/selftest.cpp @@ -11,41 +11,42 @@ */ /****/ -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "node/Constants.hpp" -#include "node/Hashtable.hpp" -#include "node/RuntimeEnvironment.hpp" -#include "node/InetAddress.hpp" -#include "node/Utils.hpp" -#include "node/Identity.hpp" #include "node/Buffer.hpp" -#include "node/Packet.hpp" -#include "node/Salsa20.hpp" +#include "node/C25519.hpp" +#include "node/CertificateOfMembership.hpp" +#include "node/Constants.hpp" +#include "node/Dictionary.hpp" +#include "node/Hashtable.hpp" +#include "node/Identity.hpp" +#include "node/IncomingPacket.hpp" +#include "node/InetAddress.hpp" #include "node/MAC.hpp" #include "node/NetworkConfig.hpp" -#include "node/Peer.hpp" -#include "node/Dictionary.hpp" -#include "node/SHA512.hpp" -#include "node/C25519.hpp" -#include "node/Poly1305.hpp" -#include "node/CertificateOfMembership.hpp" #include "node/Node.hpp" -#include "node/IncomingPacket.hpp" - +#include "node/PQHybrid.hpp" +#include "node/Packet.hpp" +#include "node/Peer.hpp" +#include "node/Poly1305.hpp" +#include "node/RuntimeEnvironment.hpp" +#include "node/SHA512.hpp" +#include "node/Salsa20.hpp" +#include "node/Utils.hpp" #include "osdep/OSUtils.hpp" #include "osdep/Phy.hpp" #include "osdep/PortMapper.hpp" #include "osdep/Thread.hpp" +#include "version.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #if defined(ZT_USE_X64_ASM_SALSA2012) && defined(ZT_ARCH_X64) #include "ext/x64-salsa2012-asm/salsa2012.h" @@ -475,8 +476,9 @@ static int testCrypto() static int testIdentity() { Identity id; - Buffer<512> buf; - char buf2[1024]; + // Both must fit a type 1 (PQ hybrid) identity in binary and string form. + Buffer buf; + char buf2[ZT_IDENTITY_STRING_BUFFER_LENGTH]; std::cout << "[identity] Validate known-good identity... "; std::cout.flush(); if (!id.fromString(KNOWN_GOOD_IDENTITY)) { @@ -570,6 +572,83 @@ static int testIdentity() return -1; } } + // Phase 3 (T19/V10): type 1 (PQ hybrid) identity round-trips through + // binary and ASCII forms, keeps its v1 address derivation, and rejects + // malformed / unknown-type strings instead of half-parsing them. Undersized + // buffers would throw ZT_EXCEPTION_OUT_OF_BOUNDS here, so this also guards + // ZT_IDENTITY_MAX_BINARY_LENGTH / ZT_IDENTITY_STRING_BUFFER_LENGTH. + { + Identity pqid; + std::cout << "[identity] Generate PQ hybrid (type 1) identity... "; + std::cout.flush(); + pqid.generate(true); + if ((! pqid.hasPQ()) || (! pqid.locallyValidate())) { + std::cout << "FAIL (PQ keys missing or address invalid)" << std::endl; + return -1; + } + // locallyValidate() hashes the C25519 public key only, so passing here + // is the V10 assertion that PQ material does not enter the address. + std::cout << "PASS" << std::endl; + + std::cout << "[identity] PQ identity binary/ASCII round trip: "; + std::cout.flush(); + { + Identity id2; + buf.clear(); + pqid.serialize(buf, true); + id2.deserialize(buf); + if ((! (id2 == pqid)) || (! id2.hasPQ()) || (! id2.locallyValidate())) { + std::cout << "FAIL (binary/private)" << std::endl; + return -1; + } + + // Public-only binary form: PQ public keys survive, private gone. + Identity id3; + buf.clear(); + pqid.serialize(buf, false); + id3.deserialize(buf); + if ((! (id3 == pqid)) || (! id3.hasPQ()) || (! id3.locallyValidate())) { + std::cout << "FAIL (binary/public)" << std::endl; + return -1; + } + + Identity id4; + if ((! id4.fromString(pqid.toString(true, buf2))) || (! (id4 == pqid)) || (! id4.hasPQ())) { + std::cout << "FAIL (ASCII/private)" << std::endl; + return -1; + } + + Identity id5; + if ((! id5.fromString(pqid.toString(false, buf2))) || (! (id5 == pqid)) || (! id5.hasPQ())) { + std::cout << "FAIL (ASCII/public)" << std::endl; + return -1; + } + } + std::cout << "PASS" << std::endl; + + // Unknown type bytes and field counts inconsistent with the declared + // type must be rejected, not partially accepted. + std::cout << "[identity] Malformed / unknown-type strings rejected: "; + std::cout.flush(); + { + std::string s(pqid.toString(false, buf2)); + // T19 is the highest defined type; 2 and 3 are not. + std::string t2(s); + t2[11] = '2'; + Identity bad; + if (bad.fromString(t2.c_str())) { + std::cout << "FAIL (type 1 serialization accepted as type 2)" << std::endl; + return -1; + } + // Trim the trailing field: type 1 with 4 fields is not a valid shape. + std::string short1(s.substr(0, s.rfind(':'))); + if (bad.fromString(short1.c_str())) { + std::cout << "FAIL (truncated type 1 accepted)" << std::endl; + return -1; + } + } + std::cout << "PASS" << std::endl; + } return 0; } @@ -635,6 +714,102 @@ static int testCertificate() return -1; } + // V14: COM type byte 1 (Ed25519 only) vs type 2 (Ed25519 + ML-DSA-65). + { + Buffer<4096> b; + + // Type 1: classic authority, no PQ key material. + CertificateOfMembership c1(10000, 100, 1, idA); + c1.sign(authority); + std::cout << "[certificate] Classic COM is type 1 and carries no PQ signature... "; + b.clear(); + c1.serialize(b); + if ((b[0] != 1) || (c1.pqSignature() != (const uint8_t*)0)) { + std::cout << "FAIL" << std::endl; + return -1; + } + CertificateOfMembership c1b; + c1b.deserialize(b, 0); + if ((c1b != c1) || (c1b.pqSignature() != (const uint8_t*)0)) { + std::cout << "FAIL (round trip)" << std::endl; + return -1; + } + std::cout << "PASS" << std::endl; + + // Type 2: PQ hybrid authority double-signs. + Identity pqAuthority; + std::cout << "[certificate] Generating PQ hybrid authority... "; + std::cout.flush(); + pqAuthority.generate(true); + std::cout << pqAuthority.address().toString(buf) << std::endl; + + CertificateOfMembership c2(10000, 100, 1, idA); + std::cout << "[certificate] PQ authority double-signs COM (type 2)... "; + if ((! c2.sign(pqAuthority)) || (c2.pqSignature() == (const uint8_t*)0)) { + std::cout << "FAIL" << std::endl; + return -1; + } + b.clear(); + c2.serialize(b); + if (b[0] != 2) { + std::cout << "FAIL (type byte)" << std::endl; + return -1; + } + CertificateOfMembership c2b; + c2b.deserialize(b, 0); + if (c2b != c2) { + std::cout << "FAIL (round trip)" << std::endl; + return -1; + } + if ((c2b.pqSignature() == (const uint8_t*)0) || (memcmp(c2b.pqSignature(), c2.pqSignature(), PQHybrid::MLDSA65_SIG_LEN) != 0)) { + std::cout << "FAIL (signature bytes)" << std::endl; + return -1; + } + std::cout << "PASS" << std::endl; + + // Both halves of the double-sign must validate under the signing identity. + std::cout << "[certificate] Double-signature verifies under both algorithms... "; + { + uint64_t vbuf[ZT_NETWORK_COM_MAX_QUALIFIERS * 3]; + unsigned int ptr = 0; + for (unsigned int i = 0; i < c2.qualifierCount(); ++i) { + vbuf[ptr++] = Utils::hton(c2.qualifierId(i)); + vbuf[ptr++] = Utils::hton(c2.qualifierValue(i)); + vbuf[ptr++] = Utils::hton(c2.qualifierMaxDelta(i)); + } + char pqPubStr[ZT_IDENTITY_STRING_BUFFER_LENGTH]; + const Identity pqAuthorityPub(pqAuthority.toString(false, pqPubStr)); + if (! pqAuthorityPub.verify(vbuf, ptr * sizeof(uint64_t), c2.signature(), ZT_C25519_SIGNATURE_LEN)) { + std::cout << "FAIL (Ed25519 half)" << std::endl; + return -1; + } + if (! pqAuthorityPub.pqVerify(vbuf, ptr * sizeof(uint64_t), c2.pqSignature())) { + std::cout << "FAIL (ML-DSA-65 half)" << std::endl; + return -1; + } + } + std::cout << "PASS" << std::endl; + + // A type 1 parser must reject type 2 cleanly rather than mis-parse it. + std::cout << "[certificate] Unknown COM type byte rejected... "; + b[0] = 3; + { + bool thrown = false; + try { + CertificateOfMembership bad; + bad.deserialize(b, 0); + } + catch (...) { + thrown = true; + } + if (! thrown) { + std::cout << "FAIL" << std::endl; + return -1; + } + } + std::cout << "PASS" << std::endl; + } + return 0; } @@ -1129,17 +1304,497 @@ static int testPhy() return 0; } +static int testPQ() +{ + // T22/V11: ML-KEM-768 / ML-DSA-65 known-answer tests + hybrid KDF vector. + // Vectors: fixed inputs -> fixed outputs, generated once by a throwaway + // driver over node/PQHybrid.cpp (hybridKey cross-checked with python hmac-sha384). + static const char* kemSkHex = "502c7d618ab70a909c99b3342cb19aca021b2d27a11c538358dca0fd75431043" + "5917403b898692b6c0c38437c5488a02a573111a82134c8b7b00dc749b486b6f" + "8171ca08688426b69a56984d643af984c7429bc8e40776751127474b05583397" + "f0b79b3b24510f77b0d5339f02f514c0d22747c9ad26970ef598c787ca81c95c" + "a0f44464d9fa94df65b3d6299714c82d6420191f370491ec3904e0cb72d7418f" + "50199f871f3c4613cff4604d95bfb2f95c71c0b918d4a8f31069d728a1386557" + "2180138c2858a137972de98fa17b6d186064bd2757a77422cdc99340578940aa" + "27cf489d38c963412276546b1a1b4b31cb7c1c8e4ac0bd55c98d58268e1b6c48" + "acb571c5ca6bc385bd809e4cc6b63173681c15bce7ac0275d730c7d7b379aa77" + "fa454dd0ba862b2420f6f58e92c08df23b6ef09c5b94bba3e1bc85376a6848b7" + "69be65cabd276d36aa9dad401f8e55aebad102c3904d259ab720310c51904dd4" + "74bc7d19c27ca31cd0eacfe531bd1bba6ebe08c0d0aac1fe2458d953078d5027" + "ee119489f4abd2936b74f6c2e12600a8b476dcd54a1a69b004169a836b6569f0" + "516d98b5bc8731e15b0a63170a652a510ee971f0eb965462bff9801ac0488ad5" + "f1090c85b42eaaa78ca28a0b095a62a949c5b28f527b2882b68fac5541f9db73" + "bdb81c6bd57a0cbb412c5392eaf1cc920b3868f2639718b2aa653b1504c87fb4" + "62dd03136fb7b27cd606f5da938dac68e8384057fc7e87897adeb60118db920c" + "a0520da3814afa1e010c49596152f6a8305d2c02001232c465432c542231b20c" + "87f13c5d5b23b91933f4c5981af72f966c5c3a671ec3409f9f8848bb50b1821a" + "29f76cb0918506bab4c3104c6d132b4a0ecaab770337f3686942b90df07ba747" + "c3a17a14cd97e60d291cacdd51c0a7bb183e2844bd50959c190e9a031c4d250b" + "63b164e06746fbf43373db6a542479d24b171b67a23a61563042a935564c1a8a" + "05f600d0ffd3a2c57565e8dbb587bb179fc80831025ffef2ccfa382ef0297cd1" + "791a3512ac952653f89b40673ba64e97c5c2a3cda50bc94f76c29245b45d9303" + "c206a1fc8cb1c756506ef97d8568becda65c32537745160c53d528542761f614" + "2f98955dcd48564696b31b7752585160fe11a73d122f86d089ef36326b9a1593" + "f0082e53b96103c7d102c6627811ce8358c9a6096363c3b27429681a14ac6c04" + "f9e3899c1454eac7015943325c8c4c5ed7b59f8c059ce9656198514ee89c8e98" + "38cf995043844e6933c1222c7457c5b8ee5c99abc7b1a572256a6347f041c694" + "ea0b89a4c43be624131aaf698b94f2a608f6b77520043eab1a88253cbd38943f" + "01a4a45dba4cf25c2065a9c21d493676f8874f069d8830c57592016a24738dc9" + "bdffe4c2f2277d77f8cca1152d2bb003ba7c8d9aaa837b846825472d4d5925cd" + "b34a82b96306e44076676c39c88b7ec8a6da123767d07b9ebc79f3da0b562b01" + "bc4001ff15b84455bb4c3a93a8eaa4cbc97a26e927f412b2e8269c3c88cde988" + "87e248622a793bdcd416b614a4458b17b0ca0e67467588783a9485175e3b7441" + "ebc7c31bcd3a9a4b5cbc6f30da6b594125d6dc6d7714a464e2487ee151cf009b" + "9069a476394444a2223531960986808824563523547897702b5a158e330ee700" + "c2a2b342c3f17f72aa13a0e9c1f7041922a78336c4b7056c6f08e838311c1d01" + "a3087d7353ee33391e6290f3b90cd8b093a666a9d4d84667cc99bd5b1f1c2a29" + "34a4ba9ba64c487013fab399750904334127e666032d08bced0a7a5bf06ba9d2" + "227a3cab457938d5270a79063414a1c97d1b00fc673b99c2ce15d2c37ee86e3e" + "80222da5acd04a7522000e50339b5693633728cabca4031d12056557400fd96d" + "10c0385d865183c95341e0b7f141738b9b546f1459247bc5fd51c50298152ca6" + "470ec989bf23c8bfdc0a085a8114c80738e6a947ccb289b01be025b401779701" + "480e813b961190798a2777756a959b908e6d2a8656f22219bca7ce7056b78471" + "38a69804250160a981db5c0834dc11a4042cf23a44ce17bf5e11626798851b6b" + "76bf07876b077f324c5fb9c6bf827cc0bda504acd881f9bb99f9801dbcf68745" + "33c97533389f880326751af087b2f62ca97b389807932feca49e1fc5cbb35479" + "8986653e96424166b31b929c4ea779fab01f3ccbc62ad3b80167cb08753f6d67" + "a09a094e7b2c2e0a6ace989984bc7c67d8d8702407655c3a7fd1341795aa9e59" + "409deed2a7d8436afa21219ca778f6e5c281f69b080bc06f58c2925b9297db07" + "ebb3bba40cc51c9421545852e440b18b2b6856b09b0710cc6fc93ec8d6b98b68" + "be0d6b09d4a03d7009b398f47c1f7518b4a13f4ccb2346463af4194808b0bdff" + "335db39a95319b40bba06bcc9252331a5b3b7590725c813c24bcffb2a210d108" + "6429addd700986090f4a773388279539479530318b9fbbb3aada0f932c757954" + "ccbd55c4f7787145738ed0879798f73dab86a317423c342a2da0f7417f5ac928" + "a91d80633489b9c71da24e5139291b46534a57825e716bad32c57c67727873c5" + "7443aaa4c74365ab7bcbfb9128d2156242493cc8840ee51fc5f5a11ab122b942" + "1c01b4339e760ddbca02d214c4a599c5fb10ae6c3cc6adbcb30b9c911d9670ac" + "f6114aa9384ea55044674840161e4b3695ad1bc7f134af3028a3eb865d9f54c7" + "7da2ad959628d0ebb3ae425e31c86eeb6c4851d76966636503352e0891adc25c" + "43c543c4b812a48562576858bfcc1145e41a642bb284751326d3d5265ac35545" + "c8cb70ca43b31192bb91384192a2239451e6182ddc3015efa1a822fb96fcd396" + "17fa684f35ce8c769914d70abdc06b8656035a6c58b82655e6953b586758ef35" + "849a4c372b4626cd41782dc74d1b430a260348ba069a84423caa239969c14333" + "504c134b0157a18952a456ade3b6459b3ff8dbc1c9464df5aa107f7c3e7b6c36" + "994aa3f8796486340f01a0a9660815a1372ecc1341be6829ee1a3422a0c57e58" + "0e143990d415b1b71cab9604072086280464066e63494e10669ae553ed875376" + "3403795425425b26428aae702baf89da7d3fb62ff042a27f3597529208c7ccc2" + "8a45471d23132678007e6b6e87b1602afa33fc9894749c060a287c8a3822ce86" + "943c2556ee644d297bad28c967cde891e3d574e477951087bbed2a3a9b9a3b2b" + "d0a56948b068f4a78ea0a34b1c44adb44bd206afa4aba5e04a0bb9c46016a91b" + "b9a82c0c13be8b4f0102cbbe88a40aaac57c84404a4aa46884b59a5a9480317e" + "ea6d5477d9f02c10ff85edb1aa5062df464f3db5c1aa1a736e2dd978afc4813d" + "da194d4bb93eaf60334858f093dd5a895ed67cf4a085ce3fb9d5c680f2dbdf4a"; + static const char* kemCtHex = "b27e6bf833b4504284e52a66b667596c1a8ddcbac9e61354c6fe55e697951694" + "f5f5948ccd6fb1f845fbe979f540366ef9a9a84bffb1ea139e48ba8154a84834" + "0bdf041275d54806f78c82e656145884b06d32ef8b779d2cdf01aca2b5a396b1" + "34e07db3b796b1f61bb4bf2b52d68fea9b21f80770dec195545519d1c6c04933" + "3388d0b1f8cb248a53a4c3d2fd06f833f3116375b603803f4e1d9b4431ae0866" + "408d61b8b698cf46cdd88acde085f2952ac5ff4cc0eca89d4d0ca67930c2da77" + "4b2fd11a14bd28b2b6a13f4ffe086e10c0ba3aae805e2aea89cba3ffebeaa17c" + "2758e66063c8e3dff0a945bd9e70b5e9b6f3251fc3550a51f4cadf541bb11c8e" + "066222057b89754bfa921f105411768dd44837011b78c2a0baeb1f7d6d89a645" + "2a28150d2dd6f39e0271d86ad3b6380a9e875b9beab5ad18499de6cd2f66539a" + "240e3c11ec429b1977dffe186b656f1b6702031c2686a40d7902da2b3167d1be" + "3a3b241c5e58e4b7228463a5e192634d274244f99a1a4f94a970cab9138115a6" + "599a177248d7da63845ae84974042239bba9606999adcbca755ff8a63270799e" + "d74234a9822642a2d499cc7a8931edf35837e1f893fae7cd5e2e0cc6a1324697" + "b2a1d223a06cb0926337aa3df134c0a72babafc82c2f229aa533d3d3ccb0d7b5" + "0802f19f900f27efb15b0a15af999d129d094149bab8bcb6a69d1175a85bb825" + "f74d0898d6727722d8299fbab86253d7e23217b29b091946800bc178e903278b" + "a49256c32e2d455c39ecdab3361433b645601363513480d2e4a911094b9b1077" + "0bf6f9dbe95cadba29f53bad7a5252f1b68ffde9a4ca370ea80e04541b803572" + "a2a7d2f31b2f9f6ebd0a301b1a3aee79f662d7bd73233bc99a45a2d915b21e8a" + "f063deeab881481bd4e4c7502d8666a483adf349f4fecbf43ea51c5addd2c292" + "7590ca8e157482d8d2ef7da3b705ece880e6fe8bb472e538ee5c079bf65d8d4c" + "6f9de269c4a6e183d6380fcb53e54013302b3b5098a5e1fd21024dac324e1030" + "e13ad84421124b7efb449c466a47761bb8cb91acc1c2399bf905e2bdace7d51f" + "296bc6e80a1ea2d5e709d03f923e3facfe0742d60be23f3268c6be631dfd4e8d" + "a126e42d99c19e649b24c7ec9481ea98d84d5982d0533873a2eabb71bd6f0af6" + "beb9d51d7bc7d38f4b7f32dc2cdf12627c4115fd5b9492e52c9a4cb5b47306af" + "1132484d03ac5a28ecf387442fcdc5ff3de472415a697754d35cab6d26e8a28f" + "7dd69b168c129ddbceb88c8d8019b182d121e67898cfeddf6532a9471694ba84" + "146b79064ffeadd26b3b46a588e3273e511587992598b7749306145ae586eb9f" + "1f89781dc95dabb7613473c781488517e1decbc8bd383c2ca31de305ceeecb65" + "e46858d1e999acddc17e1413a9129e037aafb34ce3828698f3864e14882dc1b6" + "fdf285aafbebbe273361d7f7aeec9505624073ef8b1f75514ea553a337b13c94" + "ba8a87b372c97981963a299ab0aec224c3f0c8f463facdadf6ae1ab286f59496"; + static const char* kemSsHex = "d4d9384a5155efb1cff2587441a01cab9349f0cfbd81a184f7ccb87a0261a4c6"; + static const char* sigPkHex = "b7830485b874470b671f6854b8c788717d6f992c774d443eb4fdf614a09dc542" + "2f6c280f555ed99a4f2abee25249cef913b62b2299cb48dd980afbedbe37b8ad" + "0ba6da134563e25c5a53cbe2f916596f9f1ce15480ea7ee138663eca8782e69d" + "13717db75765b07a152aabfd6a7fb55f3d2c0d7dd41e746aea94bf89c421a873" + "aceab18717a2cbecbc61c0cb15138fca65d471a855c5c391f46a96fe2c9f8255" + "5ce9055b44d7fa47dcf6d1a8a174d13b99547e8c37687a5a138ce5710ee49f24" + "44dde8e3e4b5abac0991116fad05ea20d3d56df9fcb5ba6d4f10f92834d84f11" + "b6e5ed745e26f057f5e516268f24caab60f1dd0f95c2ee883837ea2494f76826" + "43c01332071c4cce1c5433e7ae681158c074508908b26324c67f77d9995ad9db" + "b70575547c1cb08ac23189a8c301fc3b87ad06ededea8b4656395cd4160f214d" + "129cf227f509783cf44e2b81cf2245663e2682f07785548d1acb48e853282eaa" + "9a6bd492578235461bff1ee2127eaa8f98ffa75babb7c835138bdc0f74a1a0ec" + "5ec0b198c43bfd095bfece3e68c4d30597d0e18d3024394577d5e560c8ba7c75" + "c82acde8e4051a425783dee51724498ea91a038496146697747712fb3e6c1142" + "95f614329712f5596c153acbc3de8c0c9bfb74790532cb8a8d713822e25f4dcd" + "4f696384063f0e4e30686bf05de1a140911ce20b431227de8fd289b07e3de13a" + "901b46388f8d9c82e911a0939aa1077bb6a9c96ae7203fa37790805f36fe9f65" + "33a505567953db8a5257c20d15f430be6fca87d40a317d27bdb6b6f415d9d3ae" + "a8b6a567675a68329c45e08ef570cbb6ddf68e1b586ab6d98b5884c7a0e6e68a" + "73b29a11654ce15fbdd0d6bdf7eef20691965a82a056354d0a9d2301507a5ccf" + "ab95f914eab74529a9341abc29fd228fd1c15134cfaef9eabba7838f2745a1c4" + "1c3b6828656d7417eb9b0124a4e47383d89f7033a56e799c703ad388e2989d56" + "54d009f6ab0fb325443539c0459fd946893269c10a22f92e15c36edfefe92c29" + "78a213daf9d17545ffcc2205173edb7f7c21b59603094c05f85e78caceb4f111" + "7b50b7d746936cf5f7d737317e63f4b153fa9f18e00e7676e591867bd6e5193c" + "607403c57b8ebaf82f4047e38a182fc4e9ef251ad979f76740ff6604266f36cb" + "aee6c2c4a28cadfa03ca6ed5b8a772cfe0553fc959a44722213d023d6dcc89a6" + "02dd41cb9ef1c5f6fb73f4a94df16b8966f720eb8d00730747987689c0997bc7" + "ed75687dc6e99304f37658dadccc53b353969f9d1c585f96e9fb0c7dcb57ae9e" + "5f1eca2fc0dbf39f4a697bc478dae32b6d3b9af1429b3a586dce18d7fc30d739" + "a1a1bb47998f5002af4946778c4ac671eb623a2119aa22d359a71f71cc267314" + "678e5f66559005928f89167f1dd89c14f1c1f57e6f8a60ca946d0e353127f657" + "402343e3c29739a53bf6112c74a6eaac1ec228e8bcf8774e239aff6769a80ccc" + "05b74278e4eb980a3933526525382564351414fbcee050512b8ca7d175d0f616" + "dadf4afe433e68db8c767eb0cf8a46c4b093d9e42b4c4bd3fd40c9dba3ca36f1" + "8cefa0195f4f9c0bd3b6987c6b18aefa95435a0122168f8786eebaf96283b194" + "352cf0859aca6599f4501224dabdf8aee08fdffce063dfceeea471a1812b74ad" + "f788feae756efb7bda061bcb94480a82d378005307f754ed5ef306afa2ccb067" + "d1b65ae5638e920e9dff7e30570368d6c87c296ebae118c585d0fcb9f604be46" + "d7809bafc7037590b35de830fe6bbae6bd99a757d7a00ea9acc355f59cb4808c" + "e6f14ad6ebc5e3ee7dc04abafb6aca3acfbbdf07ee5253722c9f4928ead95a69" + "6408c762657256b11e7927811fdda828500d8a3bd478d765ed902d01cfd2a9fb" + "5e3cb8edb91ce8c4d523158ecb36941c90505066249d077759ac17a2c61a1e2e" + "2e7900d3b916c2a8cdb09fd11173042c095545a9d01f642b4d2988cca8b51f16" + "fbc155683a335b26dbd862f37c6da25a88cb1e451d45e1dadbf424f1534674b7" + "8ab21b4881d9303fc58a7cd07d77ec8fdb30f1fa2f07ae2c1bf2b1f47f721417" + "775dd1d163db4eea200334de6f903b6d974c6fed63daa55e40c25fd6396232d6" + "a0775649f58e7be8822612b236c382f807ab063a975a8636d88d40ca2452fcdb" + "b192f556e4ef2d28d57df928675673eaa4ddb3a88adba1cc7629413f5ecb5c90" + "4b38dfae9652d9196eaa9d7ffc5810a6f18fc1e309cae101912d0a6a0f202d82" + "2f13db54548e37c552c90c3ff74943957503b6b19c1f611b644963ad3ea7b9f7" + "7eb56ea82f7ed7393f70065e37b75a00a8d0c2f199d647d866cd77b38b867beb" + "ef548ba1355dd255d3372fe2cf6f158271835d16afe3d0820e8e7c0d8a00540d" + "7a113541cdcc174bcb6000a710d74fe15d881cb348a14484fff31d67975992df" + "d4e18fa359dd69a7fcc0a6d10785becd8de18b7ee822e4212224e5469e9b68f1" + "54526be09844c58a8396443e96bdda0b6d432bd3954a7304bbb6002aa6f71c5e" + "04625c3885dc14fee25650de600e37116b9ef1ca98fbc5e483af1940299154a0" + "7b3a4cd3b08316a4bfcf50164be5e719fe9560a2e195127a0bde49d130aca946" + "f5a3fd0c598693f1a40ee2cddc606862b0de583ba17cff261411621709353c28" + "64f526d02f42d0893d50060f5e155b144d2e005ba5dd499bf85bc60c20856cbd" + "c4e4f690a1eeb98c4c254ecbe4102d4eaef220a788d409b02f13e8692aff5202"; + static const char* sigMsgHex = "4261636b4f6e65205051487962726964204d4c2d4453412d3635204b4154206d" + "657373616765"; + static const char* sigHex = "0225ca3c132c64da98af5ac1c447aec4eab20c9a147434960572338f104051b9" + "6d8ad3c763faac66bc8406bd01cc44445097766461bc3113ab22fb97776cdc0a" + "96731c9fb081de1ee34755dded85ce312af96482d8e2a94c907b7d46a9b57cce" + "01c8cd5d0771923fcce65db02ed834c8860567951ae7be0b05f5606001afd797" + "acebc240a8668dc4a7e21b2cf46b3176f40726a4fe214268b0a85f5dc69f1849" + "72797eca9cae4ab3e6c3f1b4211d37436d81431787da1e6cb7cdafdb36e81c9a" + "dedc5169034d959dda4d62d143aa487173ed8a552462bbc7861122b17accda48" + "547b2755d809a0f7ba289f7242e13ceb3172581341cff5d10ab3df06f33a5dc0" + "068903536c70c6deff26560a2929f2c9c9d09e066312bac0f02b250f88917e38" + "f63035d2bb9b80c4c32d92170e9db85ab42653821b52219b3ebaac626644599c" + "b455df637b2e2e843db86d32c280e617e23d7c451ce2f76b54b368aa66527b17" + "74a327f15ad6fb1080270b41d87dfd4c09eed4e32799736a10b50edcbc7c9170" + "4cdbac7d8da799fd55306dbd1b4db02052018a3ea4563e813d144d19f203222d" + "9b6a14cc4bcb396abf8040bb5d5ef0c6d423a9b3d9c9bc524bf71704c3cc27ef" + "ea953181a787fcfb3b70e0f5d4048e514c1936988b881801064dc7c45d75a79a" + "7806591317927f6019147a9afdb84201752d661d88bbdf773a7b7161779e546a" + "227efe3fc08dfa8c01a768f3eb12d9b17240775392ecb78bbfd5ac6af3ae0ae1" + "f500e1064b9284d4f876a783b60112b69d7f7ae5e909b3fe5ce6a4f4c609a8dd" + "d9031b5665ad97e8da69bc3e9f30c67feb5be85eae7bfd316055e8091da29756" + "4741e01bf0f85c2910c201f80bf3168b8a59603b1538894ad61df451e78f01a4" + "c37aed1a9dfc139ae5bdba462a84096d636d9ba8ff237caaaee88b89d5017909" + "fd7040aaf3b33d9cf163f939f989cabc33d5b3f44a0b32ff8b0ff579cb3cf467" + "fdfe22e3a76fda1edd1684059a86051ac1844136ec2401bddbc97f493bca216a" + "d0138b71b69cfc979384e0bc2f45285fce57591c5df8c3f4600e0eddafe46b98" + "8b9c6bdc37a0c2428c53d3005e0982ad56d60a6b54e9813f90f15f024826727f" + "85159399e41393ca4cd48264c424405b3e50433b1882d2ceac60529f566b84c0" + "50a9f474913dcd89a7af39f0ae294e17f5d8583b9632e2cf36d040a721cadebe" + "42d42a3fd7cd19a9a9cb71373453f5c6c9732db5cb78b74a811984e604eb31af" + "efb30aeee463717f60ebbedc4c55995bf1c2a6d2447481b0aafc15bbeb5fff24" + "9719e3a54827b9d8fb45dc133dc4831be9704370ba52f8bc1f3c3aee23beeee5" + "0330165f9b18d0b5bb754f29506726db8ccedda33ed36a58c655dc389044223c" + "d2797cb4b6b75bacc8f378cc5e76ed0e5f0f8eef3959107b334d7b28a7dd1bf4" + "30ab862a38c8f3e08c608bab6321036ca340409c07eb0b41b29644153f6a699c" + "4a0becc1d435e894e2cf2c0cd3d9c518b0c68d61ddb5c92398c6be1638d31a54" + "8bf50740e742661c1bbb25df98542914d6a399afb71094eb4a51e5c7a838d53c" + "cf4cc3369de5baf269a19eb2e8c0a98358bddbaa51ddabfa645a60a182ca6d81" + "688131624ba4df376d08b545d56f0c5ca2d320f7699f1cbe268f1405087730ea" + "22da5000084db59bdb690caba99f9d52cfea04534dad845bfbb02219389db886" + "d2ee74d911885c153d627a25664bbd9d0004466e42c16fc66883f020e48a4b37" + "3030bb2dfccef9ef0856d844bfc3ce091e66ebde74b335573d1de78960fe0ccd" + "ba875d86be3f4a9035b184dfc4d69a08948915b045346507725dc887b977d4f2" + "3630956b8e6f1f3b64233e722f36d6dca606475c00b89d2d4df138a960481f62" + "dd25a5663dc2dce50f47abb22bbc7bda708b66b8f17a9e2b85f626fd2f4f61eb" + "5edffe00215026b99e97ef524b590590bc05164555f6ef5a5117984e3d21eb50" + "1ba1555ba74a273ded01ad9cc7e24c9e7ac51b22654b8fa225e0251cbd06134f" + "17aaad908413c5871407e824d9688b968df4aa23f32aa4a1f757812452745c20" + "4e5cf8a3fcb08e2d2116fb693cbc2b8f27103e619bedbfde5827b6999488ca8e" + "6588162c9acfcc2c7bce005bcff794a1e6072347b825d8fde675ce888b15a0bd" + "7635785da23a87a1a147d9aac1f07e08db5495cd61055f006e42e4c4dcbdaf7d" + "04a2445a4861be407e04d575a3b776678cac7ca8a07e386d956c884e8e8a24b5" + "bd7008319568aaba821c47a8179ecc7e97f3dd36bdb4bc63394bc8413166beb5" + "4df30ed1767b8facdb31d40d3b0ac12f589d206d76db46bc04af5eb6743d89de" + "85e1c028f6826790ca87211d67528462e6e759b412971c94d0a078c74c9ddb8a" + "d78b4543628525a5b7eae6748dad70b45b5dc55ebabb71cf0aed63f35b07c1e7" + "da26d47929c031efc9982445202046de8181346f3c196b3fc5d350b00139c957" + "64d5caebf9ec91f243134bb3381cda89d235a2d06db85e6b84243cc1b9161f1b" + "12ccef5a95886613d9bfad953d581d864524002d496cf8b94065fced9d57ca02" + "a859a341e18485abd6801c0c8ce9adf8735c4853d4d5ee812d71b1995f001f56" + "3435922fcaaf4f9a7cc3572aa2b3f813e82ef578edfa82c2cde2faa2be33a51b" + "7960029f12a8dffe7b171ec013a5088712a535192659498a205f5271e59019e2" + "8b14ed084349fe9afcd5f4feebafd656bce489eedb68c7499e3e3012d68a7ebe" + "f3d322938a13e517fc64457f5d1a3a47f5ccf3731bec99a22dd6a24cc6f10d4c" + "12965e3bf39fb0176435eeb417e4f00c0f4789a719f71c3fee08a31a4a8d31cb" + "bf52864137aa04e030391abd9f29b30367523af74a5f3855fa717b8c0504287b" + "2d15242a53c0693ded65d83889dd05bdd5008995cd805bac5f36a5f6ba2e7e5f" + "93d1bf2b1d8c4bccf83e5b397dfad74f91159ef4f920f783c63537fb27ba11f4" + "50f88b507447b07595819ebacd3d31ced1a0f07cdcc746d0b64f046a3d493d0a" + "78b08776c0a98c661d7d70b4db4f1813dbeed4a8d769a4a8fd8926a922b04e14" + "d9184cec01efef45fba7fa36160badf5c5fc98a6d36b2c6cb0a9e9c8890ce421" + "3b7958d0d09708804b3ae282c0b6eb8414b1b538eb4edf79d12311b88c7acd48" + "4e7821348c0a4be80980be638ccd6dcbfeed19caf794e156809142fd33346217" + "dd101bb8bfd724af6e5474756746f901cf60a01f6cf7f6906489a58272b1e805" + "e20c5d62f416e508158b07486cb793cfb93d5fd46bfd1b5c7c44e5906358a51a" + "429be5bc2ce3630d50b7127d96c7c5271b501f767a9011b8046fc770d8c37a99" + "43b46c3baf4b0461c20f5a4bae79ce909e5a7db3a96757ffc89e7c43fb88235c" + "2483205e0e7f87c1616f61c16e1dbe2f5e5e32688be7c2928f0093b652fe23da" + "234cb677d14e9c49b0fc28840f688119b4bad5d3c701c1ce0cdea9b9c4528e35" + "87cdc1bce3e788ee310c2d2b0f6fd404b66582ada90215029c3a694e75ad9302" + "cfe28593d875a56b043e2c8a8cb6d3aca7cb976fbee5e818f032baed59b4b800" + "0b4e96f2eb65ae6cd64ad287523d93fc981a42a2b854a7162d82539083b9c46f" + "69de03b01267a5bfdfa3952cd3dc98af3850be87f09b9ee0cfe2fc502545c12a" + "0978f0a8bd193c724aeccb6aafa1580bf8ca4d8210bfcfd0b631f76bfc42ef1d" + "9570e065794e8b26e444c7ca1b5aeaf36e4f56c98d37741c5bc8730406ea05fc" + "f60f75c640c77f2b3cd0993a83c72ecb1d5fea471846972b74f1e9549cdb5b1d" + "817f3928268d365dd9df6310e981da6e7ef157cd6bf24be17f4f3d52a706e833" + "54fcd58f3ea05597868a16cabd65f21a7b93c7caf510ffcd1725719a8dab7f23" + "84649fa8ee52504ed7ac3a4d36080d8a9f060cc6b92930a9fe2c5eded4761e3e" + "0a1b600c6f17096c4a279a2f6d01121cb7e1a6eeedb71d0c95b66f8da1c9bf30" + "c1d700916a21f82edaef8e9147620d12ab3e9fcdd2acc10b0fdfae8d3c711213" + "4fc544fad9dab4351ce018e88b8f1d6e4444a9aad3dc24d3648c2fe9b5b976fe" + "29df10ead7ccf6a6cf24c8538a2d82510f6bd5a7a83b0dc64a8e16dd2cb3eba4" + "418506b3f992c6bacb3e3848495777ed6d782cd92e608474a2feb0cb3e7c43fc" + "640c2054f8ba21e5a1ee0cef782d7d100766442134d3185d1e8de166de66d13a" + "0d6a23def47d6fabecc2269d2d81db3f6f0656189492f7eef838b02d2e948d22" + "0d112856ba1b1d573c96e294b51f5de2656d379ae2e0df57b0609fc98c3f52d9" + "9b9522b492737b4b874a84ab01ef224dc2e68f38788b47da630e76710b82f645" + "a7a66c773b279317ef4b9f007044b20a282dc8d0280d8311ce3cab6b30d8f64f" + "740c7da2453eea93ac697aebcb069a1eb76d9bf7b41d0c6afbae926f5f91c8e7" + "fc1326b6745a59808a63adb603c42e049036e2717a79308ba052b03b27096e8e" + "9b56376b441de827122a94329e350e0bf3aba7dc0b3b7312151b364a509e1ede" + "0c8bc20e6f462bce41dea3f9c22184ea9fa987a91955f0962a75fa65ccbbbd07" + "a7c462deaa833c4f80ee07326344ed9b0172a1c01a1e4c6d7c0b162b57586774" + "8ea4b6e51b2426333d4fb8176ab1b5d3005c626ef50000000000000000000000" + "000000000000000409141b2025"; + static const char* kdfClassicalHex = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" + "202122232425262728292a2b2c2d2e2f"; + static const char* kdfPqHex = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf"; + static const char* kdfOutHex = "705132e627d21f2501ca93906da0a3ddf752e79a56dd23cfc4ceb63bcee83171" + "744014dda6c1639a7a47335f2c04be8c"; + + unsigned char buf[PQHybrid::MLDSA65_PK_LEN + PQHybrid::MLDSA65_SIG_LEN + 64]; + + // 1. ML-KEM-768 KAT: decaps(sk, ct) must reproduce the shared secret. + std::cout << "[PQ] ML-KEM KAT (decaps fixed sk/ct -> ss)... "; + std::cout.flush(); + unsigned char kemSk[PQHybrid::MLKEM768_SK_LEN]; + unsigned char kemCt[PQHybrid::MLKEM768_CT_LEN]; + unsigned char kemSs[PQHybrid::MLKEM768_SS_LEN]; + unsigned char kemSs2[PQHybrid::MLKEM768_SS_LEN]; + if ((Utils::unhex(kemSkHex, kemSk, sizeof(kemSk)) != sizeof(kemSk)) || (Utils::unhex(kemCtHex, kemCt, sizeof(kemCt)) != sizeof(kemCt)) || (Utils::unhex(kemSsHex, kemSs, sizeof(kemSs)) != sizeof(kemSs))) { + std::cout << "FAIL (unhex)" << std::endl; + return -1; + } + if ((! PQHybrid::decaps(kemSk, kemCt, kemSs2)) || (memcmp(kemSs, kemSs2, sizeof(kemSs)))) { + std::cout << "FAIL" << std::endl; + return -1; + } + std::cout << "PASS" << std::endl; + + // 2. ML-DSA-65 KAT: fixed pk/msg/sig verifies; tampering must fail. + std::cout << "[PQ] ML-DSA KAT (verify fixed pk/msg/sig; tamper fails)... "; + std::cout.flush(); + unsigned char sigPk[PQHybrid::MLDSA65_PK_LEN]; + unsigned char sigMsg[38]; + unsigned char sig[PQHybrid::MLDSA65_SIG_LEN]; + if ((Utils::unhex(sigPkHex, sigPk, sizeof(sigPk)) != sizeof(sigPk)) || (Utils::unhex(sigMsgHex, sigMsg, sizeof(sigMsg)) != sizeof(sigMsg)) || (Utils::unhex(sigHex, sig, sizeof(sig)) != sizeof(sig))) { + std::cout << "FAIL (unhex)" << std::endl; + return -1; + } + if (! PQHybrid::verify(sigPk, sigMsg, sizeof(sigMsg), sig)) { + std::cout << "FAIL (valid sig rejected)" << std::endl; + return -1; + } + sigMsg[0] ^= 0x01; + if (PQHybrid::verify(sigPk, sigMsg, sizeof(sigMsg), sig)) { + std::cout << "FAIL (tampered msg accepted)" << std::endl; + return -1; + } + sigMsg[0] ^= 0x01; + sig[0] ^= 0x01; + if (PQHybrid::verify(sigPk, sigMsg, sizeof(sigMsg), sig)) { + std::cout << "FAIL (tampered sig accepted)" << std::endl; + return -1; + } + std::cout << "PASS" << std::endl; + + // 3. Hybrid KDF vector: HMAC-SHA384(classical, pq) -> expected output. + std::cout << "[PQ] Hybrid KDF known-answer vector... "; + std::cout.flush(); + unsigned char kdfClassical[ZT_SYMMETRIC_KEY_SIZE]; + unsigned char kdfPq[PQHybrid::MLKEM768_SS_LEN]; + unsigned char kdfOut[ZT_SYMMETRIC_KEY_SIZE]; + unsigned char kdfExpect[ZT_SYMMETRIC_KEY_SIZE]; + if ((Utils::unhex(kdfClassicalHex, kdfClassical, sizeof(kdfClassical)) != sizeof(kdfClassical)) || (Utils::unhex(kdfPqHex, kdfPq, sizeof(kdfPq)) != sizeof(kdfPq)) + || (Utils::unhex(kdfOutHex, kdfExpect, sizeof(kdfExpect)) != sizeof(kdfExpect))) { + std::cout << "FAIL (unhex)" << std::endl; + return -1; + } + PQHybrid::hybridKey(kdfClassical, kdfPq, kdfOut); + if (memcmp(kdfOut, kdfExpect, sizeof(kdfOut))) { + std::cout << "FAIL" << std::endl; + return -1; + } + std::cout << "PASS" << std::endl; + + // 4. T22/V13: interop hybrid<->vanilla matrix. + std::cout << "[PQ] Interop hybrid<->vanilla matrix... "; + std::cout.flush(); + Identity idH1, idH2, idV1, idV2; + idH1.generate(true); + idH2.generate(true); + idV1.generate(false); + idV2.generate(false); + if ((! idH1.hasPQ()) || (! idH2.hasPQ()) || (idV1.hasPQ()) || (idV2.hasPQ())) { + std::cout << "FAIL (identity types)" << std::endl; + return -1; + } + unsigned char ict[PQHybrid::MLKEM768_CT_LEN]; + unsigned char ik1[ZT_SYMMETRIC_KEY_SIZE]; + unsigned char ik2[ZT_SYMMETRIC_KEY_SIZE]; + // hybrid<->hybrid: encaps/decaps agree with matching keys, both directions. + if ((! idH1.agreeHybridEncaps(idH2, ict, ik1)) || (! idH2.agreeHybridDecaps(idH1, ict, ik2)) || (memcmp(ik1, ik2, sizeof(ik1)))) { + std::cout << "FAIL (hybrid 1->2)" << std::endl; + return -1; + } + if ((! idH2.agreeHybridEncaps(idH1, ict, ik1)) || (! idH1.agreeHybridDecaps(idH2, ict, ik2)) || (memcmp(ik1, ik2, sizeof(ik1)))) { + std::cout << "FAIL (hybrid 2->1)" << std::endl; + return -1; + } + // hybrid local -> vanilla peer: encaps refused; classic fallback agrees both ways. + if (idH1.agreeHybridEncaps(idV1, ict, ik1)) { + std::cout << "FAIL (encaps to vanilla accepted)" << std::endl; + return -1; + } + if ((! idH1.agree(idV1, ik1)) || (! idV1.agree(idH1, ik2)) || (memcmp(ik1, ik2, sizeof(ik1)))) { + std::cout << "FAIL (classic fallback PQ<->vanilla)" << std::endl; + return -1; + } + // vanilla local: decaps refused; encaps toward PQ peer works (gate: private + peer type 1). + if (idV1.agreeHybridDecaps(idH1, ict, ik2)) { + std::cout << "FAIL (vanilla decaps accepted)" << std::endl; + return -1; + } + if ((! idV1.agreeHybridEncaps(idH1, ict, ik1)) || (! idH1.agreeHybridDecaps(idV1, ict, ik2)) || (memcmp(ik1, ik2, sizeof(ik1)))) { + std::cout << "FAIL (vanilla->PQ hybrid)" << std::endl; + return -1; + } + // vanilla<->vanilla: hybrid refused both roles; classic agrees. + if ((idV1.agreeHybridEncaps(idV2, ict, ik1)) || (idV2.agreeHybridEncaps(idV1, ict, ik1))) { + std::cout << "FAIL (vanilla hybrid accepted)" << std::endl; + return -1; + } + if ((! idV1.agree(idV2, ik1)) || (! idV2.agree(idV1, ik2)) || (memcmp(ik1, ik2, sizeof(ik1)))) { + std::cout << "FAIL (classic vanilla<->vanilla)" << std::endl; + return -1; + } + std::cout << "PASS" << std::endl; + + // 5. T23/V15: handshake-vs-fragment-loss benchmark for PQC HELLO. + std::cout << "[PQ] HELLO fragment-loss benchmark... " << std::flush; + { + auto buildHello = [](const Identity& src, const Identity& dst, bool pqc) { + Packet outp(dst.address(), src.address(), Packet::VERB_HELLO); + outp.append((unsigned char)ZT_PROTO_VERSION); + outp.append((unsigned char)ZEROTIER_ONE_VERSION_MAJOR); + outp.append((unsigned char)ZEROTIER_ONE_VERSION_MINOR); + outp.append((uint16_t)(ZEROTIER_ONE_VERSION_REVISION | (pqc ? ZT_PROTO_VERB_HELLO_REVISION_CAPABILITY_BIT : 0))); + outp.append((int64_t)1234567890LL); + src.serialize(outp, false); + InetAddress at("10.0.0.1/9993"); + at.serialize(outp); + outp.append((uint64_t)0); // planetWorldId + outp.append((uint64_t)0); // planetWorldTimestamp + outp.append((uint16_t)0); // moons wanted + return outp; + }; + auto fragCount = [](unsigned int size) -> unsigned int { + if (size <= (unsigned int)ZT_DEFAULT_PHYSMTU) + return 1u; + unsigned int n = 1u; + unsigned int remaining = size - ZT_DEFAULT_PHYSMTU; + const unsigned int chunk = ZT_DEFAULT_PHYSMTU - ZT_PROTO_MIN_FRAGMENT_LENGTH; + while (remaining > 0) { + remaining -= (remaining > chunk ? chunk : remaining); + ++n; + } + return n; + }; + const unsigned int classicSize = buildHello(idV1, idV2, false).size(); + const unsigned int pqcSize = buildHello(idH1, idH2, true).size(); + const unsigned int classicFrags = fragCount(classicSize); + const unsigned int pqcFrags = fragCount(pqcSize); + std::cout << "\n size classic=" << classicSize << "B/" << classicFrags << " frag, PQC=" << pqcSize << "B/" << pqcFrags << " frag (max " << ZT_MAX_PACKET_FRAGMENTS << ")" << std::flush; + // Whole HELLO survives iff every fragment arrives: P(survive) = (1-p)^n. + bool ok = (classicFrags == 1u) && (pqcFrags > 1u) && (pqcFrags <= (unsigned int)ZT_MAX_PACKET_FRAGMENTS); + static const double ps[] = { 0.01, 0.05, 0.10, 0.30 }; + const unsigned int trials = 20000; + uint64_t lcg = 0x9E3779B97F4A7C15ULL; // fixed seed: deterministic run + for (unsigned int pi = 0; (pi < sizeof(ps) / sizeof(ps[0])) && ok; ++pi) { + const double p = ps[pi]; + double pTrue = 1.0; + for (unsigned int k = 0; k < pqcFrags; ++k) + pTrue *= (1.0 - p); + unsigned int succ = 0; + for (unsigned int t = 0; t < trials; ++t) { + bool all = true; + for (unsigned int k = 0; k < pqcFrags; ++k) { + lcg = lcg * 6364136223846793005ULL + 1442695040888963407ULL; + if ((double)(lcg >> 32) / 4294967296.0 < p) { + all = false; + break; + } + } + if (all) + ++succ; + } + const double obs = (double)succ / (double)trials; + const double sigma = std::sqrt(trials * pTrue * (1.0 - pTrue)) / (double)trials; + if (std::fabs(obs - pTrue) > (4.0 * sigma)) + ok = false; + std::cout << "\n p=" << p << " observed=" << obs << " analytic=" << pTrue << std::flush; + } + if (! ok) { + std::cout << " FAIL" << std::endl; + return -1; + } + } + std::cout << "\n loss delays handshake (HELLO retransmit), not broken... PASS" << std::endl; + + return 0; +} + #ifdef __WINDOWS__ int __cdecl _tmain(int argc, _TCHAR* argv[]) #else -int main(int argc,char **argv) +int main(int argc, char** argv) #endif { int r = 0; #ifdef __WINDOWS__ WSADATA wsaData; - WSAStartup(MAKEWORD(2,2),&wsaData); + WSAStartup(MAKEWORD(2, 2), &wsaData); #endif // Code to generate the C25519 test vectors -- did this once and then @@ -1193,6 +1848,7 @@ int main(int argc,char **argv) r |= testIdentity(); r |= testCertificate(); r |= testPhy(); + r |= testPQ(); //*/ if (r) diff --git a/service/OneService.cpp b/service/OneService.cpp index 1731393..95fbb51 100644 --- a/service/OneService.cpp +++ b/service/OneService.cpp @@ -666,6 +666,9 @@ static void _peerToJson(nlohmann::json &pj,const ZT_Peer *peer, SharedPtr static void _moonToJson(nlohmann::json &mj,const World &world) { char tmp[4096]; + // Identity string form for a type 1 (PQ hybrid) root is much larger than + // the hex/short-string uses of tmp above. + char idtmp[ZT_IDENTITY_STRING_BUFFER_LENGTH]; OSUtils::ztsnprintf(tmp,sizeof(tmp),"%.16llx",world.id()); mj["id"] = tmp; mj["timestamp"] = world.timestamp(); @@ -674,7 +677,7 @@ static void _moonToJson(nlohmann::json &mj,const World &world) nlohmann::json ra = nlohmann::json::array(); for(std::vector::const_iterator r(world.roots().begin());r!=world.roots().end();++r) { nlohmann::json rj; - rj["identity"] = r->identity.toString(false,tmp); + rj["identity"] = r->identity.toString(false, idtmp); nlohmann::json eps = nlohmann::json::array(); for(std::vector::const_iterator a(r->stableEndpoints.begin());a!=r->stableEndpoints.end();++a) eps.push_back(a->toString(tmp)); @@ -1463,6 +1466,17 @@ public: _ssoRedirectURL = OSUtils::jsonString(settings["ssoRedirectURL"], ""); + // PQC capability mode (T21): off / hybrid / pqconly -> Node::pqcMode + const std::string pqcm(OSUtils::jsonString(settings["pqcMode"], "")); + if (pqcm == "off") + _node->setPqcMode(ZT_PQC_MODE_CLASSIC); + else if (pqcm == "hybrid") + _node->setPqcMode(ZT_PQC_MODE_HYBRID); + else if (pqcm == "pqconly") + _node->setPqcMode(ZT_PQC_MODE_PQCONLY); + else if (pqcm.length() > 0) + fprintf(stderr, "WARNING: unknown settings.pqcMode in local.conf (expected off/hybrid/pqconly)\n"); + #ifdef ZT_CONTROLLER_USE_LIBPQ json &redis = settings["redis"]; if (redis.is_object() && _rc == NULL) {