浏览代码

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 8 年之前
父节点
当前提交
15c07c58b6
共有 5 个文件被更改,包括 214 次插入138 次删除
  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,