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.
This commit is contained in:
dedysutanto committed 2026-10-01 15:55:09 +07:00
1 parent d4568326af
commit 86c744897b
19 files changed
+1371 -126

No files matched your search

+7 -7
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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) {
+28 -1
View File
@@ -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<uint8_t> sig(new uint8_t[PQHybrid::MLDSA65_SIG_LEN], std::default_delete<uint8_t[]>());
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
+79 -13
View File
@@ -14,21 +14,21 @@
#ifndef ZT_CERTIFICATEOFMEMBERSHIP_HPP
#define ZT_CERTIFICATEOFMEMBERSHIP_HPP
#include <stdint.h>
#include <string.h>
#include <string>
#include <stdexcept>
#include <algorithm>
#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 <algorithm>
#include <memory>
#include <stdexcept>
#include <stdint.h>
#include <string.h>
#include <string>
/**
* Maximum number of qualifiers allowed in a COM (absolute max: 65535)
*/
@@ -221,7 +221,10 @@ public:
template<unsigned int C>
inline void serialize(Buffer<C> &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<uint8_t> sig(new uint8_t[PQHybrid::MLDSA65_SIG_LEN], std::default_delete<uint8_t[]>());
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<uint8_t> _pqSignature;
Address _signedBy;
_Qualifier _qualifiers[ZT_NETWORK_COM_MAX_QUALIFIERS];
+148 -6
View File
@@ -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;
}
+324 -43
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#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<unsigned int C>
Identity(const Buffer<C> &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 <unsigned int C> Identity(const Buffer<C>& 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<uint8_t*>(ct), pq)) {
return false;
}
C25519::agree(*_privateKey, id._publicKey, classical, ZT_SYMMETRIC_KEY_SIZE);
PQHybrid::hybridKey(classical, pq, reinterpret_cast<uint8_t*>(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<const uint8_t*>(ct), pq)) {
return false;
}
C25519::agree(*_privateKey, id._publicKey, classical, ZT_SYMMETRIC_KEY_SIZE);
PQHybrid::hybridKey(classical, pq, reinterpret_cast<uint8_t*>(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<C> &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
+1 -1
View File
@@ -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);
+15 -12
View File
@@ -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;
+11
View File
@@ -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
+16 -1
View File
@@ -4,7 +4,7 @@
#include "SHA512.hpp"
#include <oqs/kem_ml_kem.h>
#include <oqs/sig_ml_dsa.h>
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
+14
View File
@@ -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);
+2
View File
@@ -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)
+1 -1
View File
@@ -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);
+14 -3
View File
@@ -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
+1 -1
View File
@@ -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
+6 -3
View File
@@ -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));
+687 -31
View File
@@ -11,41 +11,42 @@
*/
/****/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdexcept>
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#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 <cmath>
#include <iostream>
#include <stdexcept>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <thread>
#include <time.h>
#include <vector>
#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<ZT_IDENTITY_MAX_BINARY_LENGTH> 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)
+15 -1
View File
@@ -666,6 +666,9 @@ static void _peerToJson(nlohmann::json &pj,const ZT_Peer *peer, SharedPtr<Bond>
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<World::Root>::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<InetAddress>::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) {