Pārlūkot izejas kodu

Refactored network config chunking to sign every chunk to prevent stupid DOS attack potential, and implement network config fast propagate (though we probably will not use this for a bit).

Adam Ierymenko 9 gadi atpakaļ
vecāks
revīzija
15c07c58b6
5 mainītis faili ar 214 papildinājumiem un 138 dzēšanām
  1. 3 39
      node/Dictionary.hpp
  2. 31 23
      node/IncomingPacket.cpp
  3. 119 48
      node/Network.cpp
  4. 25 15
      node/Network.hpp
  5. 36 13
      node/Packet.hpp

+ 3 - 39
node/Dictionary.hpp

@@ -23,7 +23,6 @@
 #include "Utils.hpp"
 #include "Buffer.hpp"
 #include "Address.hpp"
-#include "C25519.hpp"
 
 #include <stdint.h>
 
@@ -444,49 +443,14 @@ public:
 		return found;
 	}
 
-	/**
-	 * Sign this Dictionary, replacing any previous signature
-	 *
-	 * @param sigKey Key to use for signature in dictionary
-	 * @param kp Key pair to sign with
-	 */
-	inline void wrapWithSignature(const char *sigKey,const C25519::Pair &kp)
-	{
-		this->erase(sigKey);
-		C25519::Signature sig(C25519::sign(kp,this->data(),this->sizeBytes()));
-		this->add(sigKey,reinterpret_cast<const char *>(sig.data),ZT_C25519_SIGNATURE_LEN);
-	}
-
-	/**
-	 * Verify signature (and erase signature key)
-	 *
-	 * This erases this Dictionary's signature key (if present) and verifies
-	 * the signature. The key is erased to render the Dictionary into the
-	 * original unsigned form it was signed in for verification purposes.
-	 *
-	 * @param sigKey Key to use for signature in dictionary
-	 * @param pk Public key to check against
-	 * @return True if signature was present and valid
-	 */
-	inline bool unwrapAndVerify(const char *sigKey,const C25519::Public &pk)
-	{
-		char sig[ZT_C25519_SIGNATURE_LEN+1];
-		if (this->get(sigKey,sig,sizeof(sig)) != ZT_C25519_SIGNATURE_LEN)
-			return false;
-		this->erase(sigKey);
-		return C25519::verify(pk,this->data(),this->sizeBytes(),sig);
-	}
-
-	/**
-	 * @return Dictionary data as a 0-terminated C-string
-	 */
-	inline const char *data() const { return _d; }
-
 	/**
 	 * @return Value of C template parameter
 	 */
 	inline unsigned int capacity() const { return C; }
 
+	inline const char *data() const { return _d; }
+	inline char *unsafeData() { return _d; }
+
 private:
 	char _d[C];
 };

+ 31 - 23
node/IncomingPacket.cpp

@@ -433,21 +433,9 @@ bool IncomingPacket::_doOK(const RuntimeEnvironment *RR,const SharedPtr<Peer> &p
 			} break;
 
 			case Packet::VERB_NETWORK_CONFIG_REQUEST: {
-				const uint64_t nwid = at<uint64_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_NETWORK_ID);
-				const SharedPtr<Network> network(RR->node->network(nwid));
-				if ((network)&&(network->controller() == peer->address())) {
-					trustEstablished = true;
-					const unsigned int chunkLen = at<uint16_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT_LEN);
-					const void *chunkData = field(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT,chunkLen);
-					unsigned int chunkIndex = 0;
-					unsigned int totalSize = chunkLen;
-					if ((ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT + chunkLen) < size()) {
-						totalSize = at<uint32_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT + chunkLen);
-						chunkIndex = at<uint32_t>(ZT_PROTO_VERB_NETWORK_CONFIG_REQUEST__OK__IDX_DICT + chunkLen + 4);
-					}
-					TRACE("%s(%s): OK(NETWORK_CONFIG_REQUEST) chunkLen==%u chunkIndex==%u totalSize==%u",source().toString().c_str(),_path->address().toString().c_str(),chunkLen,chunkIndex,totalSize);
-					network->handleInboundConfigChunk(inRePacketId,chunkData,chunkLen,chunkIndex,totalSize);
-				}
+				const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PROTO_VERB_OK_IDX_PAYLOAD)));
+				if (network)
+					network->handleConfigChunk(*this,ZT_PROTO_VERB_OK_IDX_PAYLOAD);
 			}	break;
 
 			//case Packet::VERB_ECHO: {
@@ -894,20 +882,31 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons
 						Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY> *dconf = new Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY>();
 						try {
 							if (netconf->toDictionary(*dconf,metaData.getUI(ZT_NETWORKCONFIG_REQUEST_METADATA_KEY_VERSION,0) < 6)) {
-								dconf->wrapWithSignature(ZT_NETWORKCONFIG_DICT_KEY_SIGNATURE,RR->identity.privateKeyPair());
-
+								uint64_t configUpdateId = RR->node->prng();
+								if (!configUpdateId) ++configUpdateId;
 								const unsigned int totalSize = dconf->sizeBytes();
 								unsigned int chunkIndex = 0;
 								while (chunkIndex < totalSize) {
-									const unsigned int chunkLen = std::min(totalSize - chunkIndex,(unsigned int)(ZT_PROTO_MAX_PACKET_LENGTH - (ZT_PACKET_IDX_PAYLOAD + 32)));
+									const unsigned int chunkLen = std::min(totalSize - chunkIndex,(unsigned int)(ZT_UDP_DEFAULT_PAYLOAD_MTU - (ZT_PACKET_IDX_PAYLOAD + 256)));
 									Packet outp(peer->address(),RR->identity.address(),Packet::VERB_OK);
 									outp.append((unsigned char)Packet::VERB_NETWORK_CONFIG_REQUEST);
 									outp.append(requestPacketId);
+
+									const unsigned int sigStart = outp.size();
 									outp.append(nwid);
 									outp.append((uint16_t)chunkLen);
 									outp.append((const void *)(dconf->data() + chunkIndex),chunkLen);
+
+									outp.append((uint8_t)0); // no flags
+									outp.append((uint64_t)configUpdateId);
 									outp.append((uint32_t)totalSize);
 									outp.append((uint32_t)chunkIndex);
+
+									C25519::Signature sig(RR->identity.sign(reinterpret_cast<const uint8_t *>(outp.data()) + sigStart,outp.size() - sigStart));
+									outp.append((uint8_t)1);
+									outp.append((uint16_t)ZT_C25519_SIGNATURE_LEN);
+									outp.append(sig.data,ZT_C25519_SIGNATURE_LEN);
+
 									outp.compress();
 									RR->sw->send(outp,true);
 									chunkIndex += chunkLen;
@@ -977,12 +976,21 @@ bool IncomingPacket::_doNETWORK_CONFIG_REQUEST(const RuntimeEnvironment *RR,cons
 bool IncomingPacket::_doNETWORK_CONFIG(const RuntimeEnvironment *RR,const SharedPtr<Peer> &peer)
 {
 	try {
-		const uint64_t nwid = at<uint64_t>(ZT_PACKET_IDX_PAYLOAD);
-		bool trustEstablished = false;
-
-
+		const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PACKET_IDX_PAYLOAD)));
+		if (network) {
+			const uint64_t configUpdateId = network->handleConfigChunk(*this,ZT_PACKET_IDX_PAYLOAD);
+			if (configUpdateId) {
+				Packet outp(peer->address(),RR->identity.address(),Packet::VERB_OK);
+				outp.append((uint8_t)Packet::VERB_ECHO);
+				outp.append((uint64_t)packetId());
+				outp.append((uint64_t)network->id());
+				outp.append((uint64_t)configUpdateId);
+				outp.armor(peer->key(),true);
+				_path->send(RR,outp.data(),outp.size(),RR->node->now());
+			}
+		}
 
-		peer->received(_path,hops(),packetId(),Packet::VERB_NETWORK_CONFIG,0,Packet::VERB_NOP,trustEstablished);
+		peer->received(_path,hops(),packetId(),Packet::VERB_NETWORK_CONFIG,0,Packet::VERB_NOP,false);
 	} catch ( ... ) {
 		TRACE("dropped NETWORK_CONFIG_REFRESH from %s(%s): unexpected exception",source().toString().c_str(),_path->address().toString().c_str());
 	}

+ 119 - 48
node/Network.cpp

@@ -569,12 +569,14 @@ Network::Network(const RuntimeEnvironment *renv,uint64_t nwid,void *uptr) :
 	_lastAnnouncedMulticastGroupsUpstream(0),
 	_mac(renv->identity.address(),nwid),
 	_portInitialized(false),
-	_inboundConfigPacketId(0),
 	_lastConfigUpdate(0),
 	_destroyed(false),
 	_netconfFailure(NETCONF_FAILURE_NONE),
 	_portError(0)
 {
+	for(int i=0;i<ZT_NETWORK_MAX_INCOMING_UPDATES;++i)
+		_incomingConfigChunks[i].ts = 0;
+
 	char confn[128];
 	Utils::snprintf(confn,sizeof(confn),"networks.d/%.16llx.conf",_id);
 
@@ -875,54 +877,133 @@ void Network::multicastUnsubscribe(const MulticastGroup &mg)
 		_myMulticastGroups.erase(i);
 }
 
-void Network::handleInboundConfigChunk(const uint64_t inRePacketId,const void *data,unsigned int chunkSize,unsigned int chunkIndex,unsigned int totalSize)
+uint64_t Network::handleConfigChunk(const Packet &chunk,unsigned int ptr)
 {
-	std::string newConfig;
-	if ((_inboundConfigPacketId == inRePacketId)&&(totalSize < ZT_NETWORKCONFIG_DICT_CAPACITY)&&((chunkIndex + chunkSize) <= totalSize)) {
-		Mutex::Lock _l(_lock);
-
-		_inboundConfigChunks[chunkIndex].append((const char *)data,chunkSize);
-
-		unsigned int totalWeHave = 0;
-		for(std::map<unsigned int,std::string>::iterator c(_inboundConfigChunks.begin());c!=_inboundConfigChunks.end();++c)
-			totalWeHave += (unsigned int)c->second.length();
-
-		if (totalWeHave == totalSize) {
-			TRACE("have all chunks for network config request %.16llx, assembling...",inRePacketId);
-			for(std::map<unsigned int,std::string>::iterator c(_inboundConfigChunks.begin());c!=_inboundConfigChunks.end();++c)
-				newConfig.append(c->second);
-			_inboundConfigPacketId = 0;
-			_inboundConfigChunks.clear();
-		} else if (totalWeHave > totalSize) {
-			_inboundConfigPacketId = 0;
-			_inboundConfigChunks.clear();
+	const unsigned int start = ptr;
+
+	ptr += 8; // skip network ID, which is already obviously known
+	const uint16_t chunkLen = chunk.at<uint16_t>(ptr); ptr += 2;
+	const void *chunkData = chunk.field(ptr,chunkLen); ptr += chunkLen;
+
+	Mutex::Lock _l(_lock);
+
+	_IncomingConfigChunk *c = (_IncomingConfigChunk *)0;
+	uint64_t chunkId = 0;
+	uint64_t configUpdateId;
+	unsigned long totalLength,chunkIndex;
+	if (ptr < chunk.size()) {
+		const bool fastPropagate = ((chunk[ptr++] & 0x01) != 0);
+		configUpdateId = chunk.at<uint64_t>(ptr); ptr += 8;
+		totalLength = chunk.at<uint32_t>(ptr); ptr += 4;
+		chunkIndex = chunk.at<uint32_t>(ptr); ptr += 4;
+
+		if (((chunkIndex + chunkLen) > totalLength)||(totalLength >= ZT_NETWORKCONFIG_DICT_CAPACITY)) { // >= since we need room for a null at the end
+			TRACE("discarded chunk from %s: invalid length or length overflow",chunk.source().toString().c_str());
+			return 0;
+		}
+
+		if ((chunk[ptr] != 1)||(chunk.at<uint16_t>(ptr + 1) != ZT_C25519_SIGNATURE_LEN)) {
+			TRACE("discarded chunk from %s: unrecognized signature type",chunk.source().toString().c_str());
+			return 0;
+		}
+		const uint8_t *sig = reinterpret_cast<const uint8_t *>(chunk.field(ptr + 3,ZT_C25519_SIGNATURE_LEN));
+
+		// We can use the signature, which is unique per chunk, to get a per-chunk ID for local deduplication use
+		for(unsigned int i=0;i<16;++i)
+			reinterpret_cast<uint8_t *>(&chunkId)[i & 7] ^= sig[i];
+
+		// Find existing or new slot for this update and check if this is a duplicate chunk
+		for(int i=0;i<ZT_NETWORK_MAX_INCOMING_UPDATES;++i) {
+			if (_incomingConfigChunks[i].updateId == configUpdateId) {
+				c = &(_incomingConfigChunks[i]);
+
+				for(unsigned long j=0;j<c->haveChunks;++j) {
+					if (c->haveChunkIds[j] == chunkId)
+						return 0;
+				}
+
+				break;
+			} else if ((!c)||(_incomingConfigChunks[i].ts < c->ts)) {
+				c = &(_incomingConfigChunks[i]);
+			}
+		}
+
+		// If it's not a duplicate, check chunk signature
+		const Identity controllerId(RR->topology->getIdentity(controller()));
+		if (!controllerId) { // we should always have the controller identity by now, otherwise how would we have queried it the first time?
+			TRACE("unable to verify chunk from %s: don't have controller identity",chunk.source().toString().c_str());
+			return 0;
+		}
+		if (!controllerId.verify(chunk.field(start,ptr - start),ptr - start,sig,ZT_C25519_SIGNATURE_LEN)) {
+			TRACE("discarded chunk from %s: signature check failed",chunk.source().toString().c_str());
+			return 0;
+		}
+
+		// New properly verified chunks can be flooded "virally" through the network
+		if (fastPropagate) {
+			Address *a = (Address *)0;
+			Membership *m = (Membership *)0;
+			Hashtable<Address,Membership>::Iterator i(_memberships);
+			while (i.next(a,m)) {
+				if ((*a != chunk.source())&&(*a != controller())) {
+					Packet outp(*a,RR->identity.address(),Packet::VERB_NETWORK_CONFIG);
+					outp.append(reinterpret_cast<const uint8_t *>(chunk.data()) + start,chunk.size() - start);
+					RR->sw->send(outp,true);
+				}
+			}
+		}
+	} else if (chunk.source() == controller()) {
+		// Legacy support for OK(NETWORK_CONFIG_REQUEST) from older controllers
+		chunkId = chunk.packetId();
+		configUpdateId = chunkId;
+		totalLength = chunkLen;
+		chunkIndex = 0;
+
+		if (totalLength >= ZT_NETWORKCONFIG_DICT_CAPACITY)
+			return 0;
+
+		// Find oldest slot for this udpate to use buffer space
+		for(int i=0;i<ZT_NETWORK_MAX_INCOMING_UPDATES;++i) {
+			if ((!c)||(_incomingConfigChunks[i].ts < c->ts))
+				c = &(_incomingConfigChunks[i]);
 		}
 	} else {
-		return;
+		TRACE("discarded single-chunk unsigned legacy config: this is only allowed if the sender is the controller itself");
+		return 0;
+	}
+
+	++c->ts; // newer is higher, that's all we need
+
+	if (c->updateId != configUpdateId) {
+		c->updateId = configUpdateId;
+		for(int i=0;i<ZT_NETWORK_MAX_UPDATE_CHUNKS;++i)
+			c->haveChunkIds[i] = 0;
+		c->haveChunks = 0;
+		c->haveBytes = 0;
 	}
+	if (c->haveChunks >= ZT_NETWORK_MAX_UPDATE_CHUNKS)
+		return false;
+	c->haveChunkIds[c->haveChunks++] = chunkId;
+
+	memcpy(c->data.unsafeData() + chunkIndex,chunkData,chunkLen);
+	c->haveBytes += chunkLen;
 
-	if ((newConfig.length() > 0)&&(newConfig.length() < ZT_NETWORKCONFIG_DICT_CAPACITY)) {
-		Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY> *dict = new Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY>(newConfig.c_str());
-		NetworkConfig *nc = new NetworkConfig();
+	if (c->haveBytes == totalLength) {
+		c->data.unsafeData()[c->haveBytes] = (char)0; // ensure null terminated
+
+		NetworkConfig *const nc = new NetworkConfig();
 		try {
-			Identity controllerId(RR->topology->getIdentity(this->controller()));
-			if (controllerId) {
-				if (nc->fromDictionary(*dict)) {
-					Mutex::Lock _l(_lock);
-					this->_setConfiguration(*nc,true);
-				} else {
-					TRACE("error parsing new config with length %u: deserialization of NetworkConfig failed (certificate error?)",(unsigned int)newConfig.length());
-				}
+			if (nc->fromDictionary(c->data)) {
+				this->_setConfiguration(*nc,true);
+				return configUpdateId;
 			}
 			delete nc;
-			delete dict;
 		} catch ( ... ) {
-			TRACE("error parsing new config with length %u: unexpected exception",(unsigned int)newConfig.length());
 			delete nc;
-			delete dict;
-			throw;
 		}
 	}
+
+	return 0;
 }
 
 void Network::requestConfiguration()
@@ -980,10 +1061,7 @@ void Network::requestConfiguration()
 	} else {
 		outp.append((unsigned char)0,16);
 	}
-
-	RR->node->expectReplyTo(_inboundConfigPacketId = outp.packetId());
-	_inboundConfigChunks.clear();
-
+	RR->node->expectReplyTo(outp.packetId());
 	outp.compress();
 	RR->sw->send(outp,true);
 }
@@ -1127,13 +1205,6 @@ Membership::AddCredentialResult Network::addCredential(const Address &sentFrom,c
 	const Membership::AddCredentialResult result = m.addCredential(RR,_config,rev);
 
 	if ((result == Membership::ADD_ACCEPTED_NEW)&&(rev.fastPropagate())) {
-		/* Fast propagation is done by using a very aggressive rumor mill
-		 * propagation algorithm. When we see a Revocation that we haven't
-		 * seen before we blast it to every known member. This leads to
-		 * a huge number of redundant messages, but eventually everybody
-		 * will get it. This helps revocation speed and also helps in cases
-		 * where the controller is under attack. It need only get one
-		 * revocation out and the rest is history. */
 		Address *a = (Address *)0;
 		Membership *m = (Membership *)0;
 		Hashtable<Address,Membership>::Iterator i(_memberships);

+ 25 - 15
node/Network.hpp

@@ -44,6 +44,9 @@
 #include "NetworkConfig.hpp"
 #include "CertificateOfMembership.hpp"
 
+#define ZT_NETWORK_MAX_INCOMING_UPDATES 3
+#define ZT_NETWORK_MAX_UPDATE_CHUNKS ((ZT_NETWORKCONFIG_DICT_CAPACITY / 1024) + 1)
+
 namespace ZeroTier {
 
 class RuntimeEnvironment;
@@ -174,16 +177,15 @@ public:
 	/**
 	 * Handle an inbound network config chunk
 	 *
-	 * This is called from IncomingPacket when we receive a chunk from a network
-	 * controller.
+	 * This is called from IncomingPacket to handle incoming network config
+	 * chunks via OK(NETWORK_CONFIG_REQUEST) or NETWORK_CONFIG. It verifies
+	 * each chunk and once assembled applies the configuration.
 	 *
-	 * @param requestId An ID for grouping chunks, e.g. in-re packet ID for OK(NETWORK_CONFIG_REQUEST)
-	 * @param data Chunk data
-	 * @param chunkSize Size of data[]
-	 * @param chunkIndex Index of chunk in full config
-	 * @param totalSize Total size of network config
+	 * @param chunk Packet containing chunk
+	 * @param ptr Index of chunk and related fields in packet
+	 * @return Update ID if update was fully assembled and accepted or 0 otherwise
 	 */
-	void handleInboundConfigChunk(const uint64_t requestId,const void *data,unsigned int chunkSize,unsigned int chunkIndex,unsigned int totalSize);
+	uint64_t handleConfigChunk(const Packet &chunk,unsigned int ptr);
 
 	/**
 	 * Set netconf failure to 'access denied' -- called in IncomingPacket when controller reports this
@@ -353,19 +355,27 @@ private:
 	const uint64_t _id;
 	uint64_t _lastAnnouncedMulticastGroupsUpstream;
 	MAC _mac; // local MAC address
-	volatile bool _portInitialized;
+	bool _portInitialized;
 
 	std::vector< MulticastGroup > _myMulticastGroups; // multicast groups that we belong to (according to tap)
 	Hashtable< MulticastGroup,uint64_t > _multicastGroupsBehindMe; // multicast groups that seem to be behind us and when we last saw them (if we are a bridge)
 	Hashtable< MAC,Address > _remoteBridgeRoutes; // remote addresses where given MACs are reachable (for tracking devices behind remote bridges)
 
-	uint64_t _inboundConfigPacketId;
-	std::map<unsigned int,std::string> _inboundConfigChunks;
-
 	NetworkConfig _config;
-	volatile uint64_t _lastConfigUpdate;
+	uint64_t _lastConfigUpdate;
+
+	struct _IncomingConfigChunk
+	{
+		uint64_t ts;
+		uint64_t updateId;
+		uint64_t haveChunkIds[ZT_NETWORK_MAX_UPDATE_CHUNKS];
+		unsigned long haveChunks;
+		unsigned long haveBytes;
+		Dictionary<ZT_NETWORKCONFIG_DICT_CAPACITY> data;
+	};
+	_IncomingConfigChunk _incomingConfigChunks[ZT_NETWORK_MAX_INCOMING_UPDATES];
 
-	volatile bool _destroyed;
+	bool _destroyed;
 
 	enum {
 		NETCONF_FAILURE_NONE,
@@ -373,7 +383,7 @@ private:
 		NETCONF_FAILURE_NOT_FOUND,
 		NETCONF_FAILURE_INIT_FAILED
 	} _netconfFailure;
-	volatile int _portError; // return value from port config callback
+	int _portError; // return value from port config callback
 
 	Hashtable<Address,Membership> _memberships;
 

+ 36 - 13
node/Packet.hpp

@@ -755,8 +755,26 @@ public:
 		 *   <[8] 64-bit network ID>
 		 *   <[2] 16-bit length of network configuration dictionary chunk>
 		 *   <[...] network configuration dictionary (may be incomplete)>
+		 *   [ ... end of legacy single chunk response ... ]
+		 *   <[1] 8-bit flags>
+		 *   <[8] 64-bit config update ID (should never be 0)>
 		 *   <[4] 32-bit total length of assembled dictionary>
-		 *   <[4] 32-bit index of chunk in this reply>
+		 *   <[4] 32-bit index of chunk>
+		 *   [ ... end signed portion ... ]
+		 *   <[1] 8-bit chunk signature type>
+		 *   <[2] 16-bit length of chunk signature>
+		 *   <[...] chunk signature>
+		 *
+		 * The chunk signature signs the entire payload of the OK response.
+		 * Currently only one signature type is supported: ed25519 (1).
+		 *
+		 * Each config chunk is signed to prevent memory exhaustion or
+		 * traffic crowding DOS attacks against config fragment assembly.
+		 *
+		 * If the packet is from the network controller it is permitted to end
+		 * before the config update ID or other chunking related or signature
+		 * fields. This is to support older controllers that don't include
+		 * these fields and may be removed in the future.
 		 *
 		 * ERROR response payload:
 		 *   <[8] 64-bit network ID>
@@ -766,25 +784,30 @@ public:
 		/**
 		 * Network configuration data push:
 		 *   <[8] 64-bit network ID>
-		 *   <[8] 64-bit config update ID (token to identify this update)>
-		 *   <[1] flags>
 		 *   <[2] 16-bit length of network configuration dictionary chunk>
 		 *   <[...] network configuration dictionary (may be incomplete)>
+		 *   <[1] 8-bit flags>
+		 *   <[8] 64-bit config update ID (should never be 0)>
 		 *   <[4] 32-bit total length of assembled dictionary>
-		 *   <[4] 32-bit index of chunk in this reply>
+		 *   <[4] 32-bit index of chunk>
+		 *   [ ... end signed portion ... ]
+		 *   <[1] 8-bit chunk signature type>
+		 *   <[2] 16-bit length of chunk signature>
+		 *   <[...] chunk signature>
 		 *
 		 * This is a direct push variant for network config updates. It otherwise
-		 * carries the same payload as OK(NETWORK_CONFIG_REQUEST). There is an
-		 * extra number after network ID in this version that is used in place of
-		 * the in-re packet ID sent with OKs to group chunks together.
-		 *
-		 * Unlike OK(NETWORK_CONFIG_REQUEST) this can be sent by peers other than
-		 * network controllers. In that case the certificate inside the Dictionary
-		 * is used for verification purposes.
+		 * carries the same payload as OK(NETWORK_CONFIG_REQUEST) and has the same
+		 * semantics.
 		 *
 		 * Flags:
-		 *   0x01 - Patch, not whole config
-		 *   0x02 - Use fast P2P propagation
+		 *   0x01 - Use fast propagation
+		 *
+		 * An OK should be sent if the config is successfully received and
+		 * accepted.
+		 *
+		 * OK payload:
+		 *   <[8] 64-bit network ID>
+		 *   <[8] 64-bit config update ID>
 		 */
 		VERB_NETWORK_CONFIG = 0x0c,