Add a bit of hardening in the network certificate of membership by incorporating a full hash of the identity to which it is issued. This means the recipient need not depend entirely on the root verifying identities properly to make sure impersonation is not occurring.

This commit is contained in:
Adam Ierymenko 2021-09-20 18:26:49 -04:00 committed by Grant Limberg
parent 9cfb807fcb
commit 134d33c218
No known key found for this signature in database
GPG key ID: 2BA62CCABBB4095A
7 changed files with 78 additions and 42 deletions

View file

@ -1801,7 +1801,7 @@ void EmbeddedNetworkController::_request(
nc->certificateOfOwnershipCount = 1; nc->certificateOfOwnershipCount = 1;
} }
CertificateOfMembership com(now,credentialtmd,nwid,identity.address()); CertificateOfMembership com(now,credentialtmd,nwid,identity);
if (com.sign(_signingId)) { if (com.sign(_signingId)) {
nc->com = com; nc->com = com;
} else { } else {

View file

@ -148,37 +148,43 @@ void CertificateOfMembership::fromString(const char *s)
#endif // ZT_SUPPORT_OLD_STYLE_NETCONF #endif // ZT_SUPPORT_OLD_STYLE_NETCONF
bool CertificateOfMembership::agreesWith(const CertificateOfMembership &other) const bool CertificateOfMembership::agreesWith(const CertificateOfMembership &other, const Identity &otherIdentity) const
{ {
unsigned int myidx = 0;
unsigned int otheridx = 0;
if ((_qualifierCount == 0)||(other._qualifierCount == 0)) if ((_qualifierCount == 0)||(other._qualifierCount == 0))
return false; return false;
while (myidx < _qualifierCount) { std::map< uint64_t, uint64_t > otherFields;
// Fail if we're at the end of other, since this means the field is for(unsigned int i=0;i<other._qualifierCount;++i)
// missing. otherFields[other._qualifiers[i].id] = other._qualifiers[i].value;
if (otheridx >= other._qualifierCount)
return false;
// Seek to corresponding tuple in other, ignoring tuples that bool fullIdentityVerification = false;
// we may not have. If we run off the end of other, the tuple is for(unsigned int i=0;i<_qualifierCount;++i) {
// missing. This works because tuples are sorted by ID. const uint64_t qid = _qualifiers[i].id;
while (other._qualifiers[otheridx].id != _qualifiers[myidx].id) { if ((qid >= 3)&&(qid <= 6)) {
++otheridx; fullIdentityVerification = true;
if (otheridx >= other._qualifierCount) } else {
std::map< uint64_t, uint64_t >::iterator otherQ(otherFields.find(qid));
if (otherQ == otherFields.end())
return false;
const uint64_t a = _qualifiers[i].value;
const uint64_t b = otherQ->second;
if (((a >= b) ? (a - b) : (b - a)) > _qualifiers[i].maxDelta)
return false; return false;
} }
}
// Compare to determine if the absolute value of the difference // If this COM has a full hash of its identity, assume the other must have this as well.
// between these two parameters is within our maxDelta. // Otherwise we are on a controller that does not incorporate these.
const uint64_t a = _qualifiers[myidx].value; if (fullIdentityVerification) {
const uint64_t b = other._qualifiers[myidx].value; uint64_t idHash[6];
if (((a >= b) ? (a - b) : (b - a)) > _qualifiers[myidx].maxDelta) otherIdentity.publicKeyHash(idHash);
return false; for(unsigned long i=0;i<4;++i) {
std::map< uint64_t, uint64_t >::iterator otherQ(otherFields.find((uint64_t)(i + 3)));
++myidx; if (otherQ == otherFields.end())
return false;
if (otherQ->second != Utils::ntoh(idHash[i]))
return false;
}
} }
return true; return true;

View file

@ -94,6 +94,8 @@ public:
* ZeroTier address to whom certificate was issued * ZeroTier address to whom certificate was issued
*/ */
COM_RESERVED_ID_ISSUED_TO = 2 COM_RESERVED_ID_ISSUED_TO = 2
// IDs 3-6 reserved for full hash of identity to which this COM was issued.
}; };
/** /**
@ -110,7 +112,7 @@ public:
* @param nwid Network ID * @param nwid Network ID
* @param issuedTo Certificate recipient * @param issuedTo Certificate recipient
*/ */
CertificateOfMembership(uint64_t timestamp,uint64_t timestampMaxDelta,uint64_t nwid,const Address &issuedTo) CertificateOfMembership(uint64_t timestamp,uint64_t timestampMaxDelta,uint64_t nwid,const Identity &issuedTo)
{ {
_qualifiers[0].id = COM_RESERVED_ID_TIMESTAMP; _qualifiers[0].id = COM_RESERVED_ID_TIMESTAMP;
_qualifiers[0].value = timestamp; _qualifiers[0].value = timestamp;
@ -119,9 +121,20 @@ public:
_qualifiers[1].value = nwid; _qualifiers[1].value = nwid;
_qualifiers[1].maxDelta = 0; _qualifiers[1].maxDelta = 0;
_qualifiers[2].id = COM_RESERVED_ID_ISSUED_TO; _qualifiers[2].id = COM_RESERVED_ID_ISSUED_TO;
_qualifiers[2].value = issuedTo.toInt(); _qualifiers[2].value = issuedTo.address().toInt();
_qualifiers[2].maxDelta = 0xffffffffffffffffULL; _qualifiers[2].maxDelta = 0xffffffffffffffffULL;
_qualifierCount = 3;
// Include hash of full identity public key in COM for hardening purposes. Pack it in
// using the original COM format. Format may be revised in the future to make this cleaner.
uint64_t idHash[6];
issuedTo.publicKeyHash(idHash);
for(unsigned long i=0;i<4;++i) {
_qualifiers[i + 3].id = (uint64_t)(i + 3);
_qualifiers[i + 3].value = Utils::ntoh(idHash[i]);
_qualifiers[i + 3].maxDelta = 0xffffffffffffffffULL;
}
_qualifierCount = 7;
memset(_signature.data,0,ZT_C25519_SIGNATURE_LEN); memset(_signature.data,0,ZT_C25519_SIGNATURE_LEN);
} }
@ -224,9 +237,10 @@ public:
* tuples present in this cert but not in other result in 'false'. * tuples present in this cert but not in other result in 'false'.
* *
* @param other Cert to compare with * @param other Cert to compare with
* @param otherIdentity Identity of other node
* @return True if certs agree and 'other' may be communicated with * @return True if certs agree and 'other' may be communicated with
*/ */
bool agreesWith(const CertificateOfMembership &other) const; bool agreesWith(const CertificateOfMembership &other, const Identity &otherIdentity) const;
/** /**
* Sign this certificate * Sign this certificate

View file

@ -109,6 +109,18 @@ public:
*/ */
inline bool hasPrivate() const { return (_privateKey != (C25519::Private *)0); } inline bool hasPrivate() const { return (_privateKey != (C25519::Private *)0); }
/**
* Compute a SHA384 hash of this identity's address and public key(s).
*
* @param sha384buf Buffer with 48 bytes of space to receive hash
*/
inline void publicKeyHash(void *sha384buf) const
{
uint8_t address[ZT_ADDRESS_LENGTH];
_address.copyTo(address, ZT_ADDRESS_LENGTH);
SHA384(sha384buf, address, ZT_ADDRESS_LENGTH, _publicKey.data, ZT_C25519_PUBLIC_KEY_LEN);
}
/** /**
* Compute the SHA512 hash of our private key (if we have one) * Compute the SHA512 hash of our private key (if we have one)
* *

View file

@ -91,13 +91,14 @@ public:
* Check whether the peer represented by this Membership should be allowed on this network at all * Check whether the peer represented by this Membership should be allowed on this network at all
* *
* @param nconf Our network config * @param nconf Our network config
* @param otherNodeIdentity Identity of remote node
* @return True if this peer is allowed on this network at all * @return True if this peer is allowed on this network at all
*/ */
inline bool isAllowedOnNetwork(const NetworkConfig &nconf) const inline bool isAllowedOnNetwork(const NetworkConfig &thisNodeNetworkConfig, const Identity &otherNodeIdentity) const
{ {
if (nconf.isPublic()) return true; if (thisNodeNetworkConfig.isPublic()) return true;
if (_com.timestamp() <= _comRevocationThreshold) return false; if (_com.timestamp() <= _comRevocationThreshold) return false;
return nconf.com.agreesWith(_com); return thisNodeNetworkConfig.com.agreesWith(_com, otherNodeIdentity);
} }
inline bool recentlyAssociated(const int64_t now) const inline bool recentlyAssociated(const int64_t now) const

View file

@ -1227,7 +1227,7 @@ bool Network::gate(void *tPtr,const SharedPtr<Peer> &peer)
try { try {
if (_config) { if (_config) {
Membership *m = _memberships.get(peer->address()); Membership *m = _memberships.get(peer->address());
if ( (_config.isPublic()) || ((m)&&(m->isAllowedOnNetwork(_config))) ) { if ( (_config.isPublic()) || ((m)&&(m->isAllowedOnNetwork(_config, peer->identity()))) ) {
if (!m) if (!m)
m = &(_membership(peer->address())); m = &(_membership(peer->address()));
if (m->multicastLikeGate(now)) { if (m->multicastLikeGate(now)) {
@ -1487,8 +1487,11 @@ void Network::_sendUpdatesToMembers(void *tPtr,const MulticastGroup *const newMu
Membership *m = (Membership *)0; Membership *m = (Membership *)0;
Hashtable<Address,Membership>::Iterator i(_memberships); Hashtable<Address,Membership>::Iterator i(_memberships);
while (i.next(a,m)) { while (i.next(a,m)) {
if ( ( m->multicastLikeGate(now) || (newMulticastGroup) ) && (m->isAllowedOnNetwork(_config)) && (!std::binary_search(alwaysAnnounceTo.begin(),alwaysAnnounceTo.end(),*a)) ) const Identity remoteIdentity(RR->topology->getIdentity(tPtr, *a));
_announceMulticastGroupsTo(tPtr,*a,groups); if (remoteIdentity) {
if ( ( m->multicastLikeGate(now) || (newMulticastGroup) ) && (m->isAllowedOnNetwork(_config, remoteIdentity)) && (!std::binary_search(alwaysAnnounceTo.begin(),alwaysAnnounceTo.end(),*a)) )
_announceMulticastGroupsTo(tPtr,*a,groups);
}
} }
} }
} }

View file

@ -561,8 +561,8 @@ static int testCertificate()
std::cout << idA.address().toString(buf) << ", " << idB.address().toString(buf) << std::endl; std::cout << idA.address().toString(buf) << ", " << idB.address().toString(buf) << std::endl;
std::cout << "[certificate] Generating certificates A and B..."; std::cout << "[certificate] Generating certificates A and B...";
CertificateOfMembership cA(10000,100,1,idA.address()); CertificateOfMembership cA(10000,100,1,idA);
CertificateOfMembership cB(10099,100,1,idB.address()); CertificateOfMembership cB(10099,100,1,idB);
std::cout << std::endl; std::cout << std::endl;
std::cout << "[certificate] Signing certificates A and B with authority..."; std::cout << "[certificate] Signing certificates A and B with authority...";
@ -574,13 +574,13 @@ static int testCertificate()
//std::cout << "[certificate] B: " << cB.toString() << std::endl; //std::cout << "[certificate] B: " << cB.toString() << std::endl;
std::cout << "[certificate] A agrees with B and B with A... "; std::cout << "[certificate] A agrees with B and B with A... ";
if (cA.agreesWith(cB)) if (cA.agreesWith(cB, idB))
std::cout << "yes, "; std::cout << "yes, ";
else { else {
std::cout << "FAIL" << std::endl; std::cout << "FAIL" << std::endl;
return -1; return -1;
} }
if (cB.agreesWith(cA)) if (cB.agreesWith(cA, idA))
std::cout << "yes." << std::endl; std::cout << "yes." << std::endl;
else { else {
std::cout << "FAIL" << std::endl; std::cout << "FAIL" << std::endl;
@ -588,18 +588,18 @@ static int testCertificate()
} }
std::cout << "[certificate] Generating two certificates that should not agree..."; std::cout << "[certificate] Generating two certificates that should not agree...";
cA = CertificateOfMembership(10000,100,1,idA.address()); cA = CertificateOfMembership(10000,100,1,idA);
cB = CertificateOfMembership(10101,100,1,idB.address()); cB = CertificateOfMembership(10101,100,1,idB);
std::cout << std::endl; std::cout << std::endl;
std::cout << "[certificate] A agrees with B and B with A... "; std::cout << "[certificate] A agrees with B and B with A... ";
if (!cA.agreesWith(cB)) if (!cA.agreesWith(cB, idB))
std::cout << "no, "; std::cout << "no, ";
else { else {
std::cout << "FAIL" << std::endl; std::cout << "FAIL" << std::endl;
return -1; return -1;
} }
if (!cB.agreesWith(cA)) if (!cB.agreesWith(cA, idA))
std::cout << "no." << std::endl; std::cout << "no." << std::endl;
else { else {
std::cout << "FAIL" << std::endl; std::cout << "FAIL" << std::endl;