NetDB: Refactor family validation

Return a result code from verify()
Load all known certs at startup rather than continually reloading them
Only give full verified status to known keys
Enforce signatures in netdb store when key is available
Show family verification status on /netdb
Export our cert to disk if missing
Add stormycloud family cert
Bypass /24 Sybil penalty
This commit is contained in:
zzz
2022-03-20 07:41:49 -04:00
parent fc88d672c5
commit c520dcb0f6
5 changed files with 196 additions and 79 deletions

View File

@@ -41,6 +41,7 @@ import net.i2p.data.router.RouterInfo;
import net.i2p.router.JobImpl;
import net.i2p.router.RouterContext;
import net.i2p.router.TunnelPoolSettings;
import net.i2p.router.crypto.FamilyKeyCrypto;
import net.i2p.router.util.HashDistance; // debug
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import static net.i2p.router.sybil.Util.biLog2;
@@ -985,7 +986,6 @@ class NetDbRenderer {
* Be careful to use stripHTML for any displayed routerInfo data
* to prevent vulnerabilities
*/
private void renderRouterInfo(StringBuilder buf, RouterInfo info, boolean isUs, boolean full) {
String hash = info.getIdentity().getHash().toBase64();
buf.append("<table class=\"netdbentry\">" +
@@ -1065,7 +1065,7 @@ class NetDbRenderer {
}
buf.append("</td></tr>\n");
if (full) {
buf.append("<tr><td><b>" + _t("Stats") + ":</b><td colspan=\"2\"><code>");
buf.append("<tr><td><b>").append(_t("Stats")).append(":</b><td colspan=\"2\"><code>");
Map<Object, Object> p = info.getOptionsMap();
for (Map.Entry<Object, Object> e : p.entrySet()) {
String key = (String) e.getKey();
@@ -1073,6 +1073,17 @@ class NetDbRenderer {
buf.append(DataHelper.stripHTML(key)).append(" = ").append(DataHelper.stripHTML(val)).append("<br>\n");
}
buf.append("</code></td></tr>\n");
String family = info.getOption("family");
if (family != null) {
FamilyKeyCrypto fkc = _context.router().getFamilyKeyCrypto();
if (fkc != null) {
buf.append("<tr><td><b>").append(_t("Family"))
.append(":</b><td colspan=\"2\"><span class=\"netdb_info\">")
.append(fkc.verify(info) == FamilyKeyCrypto.Result.STORED_KEY ? "Verified" : "Unverified")
.append(' ').append(DataHelper.stripHTML(family))
.append("</span></td></tr>\n");
}
}
}
buf.append("</table>\n");
}

View File

@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICKDCCAc6gAwIBAgIUcPHZXtYSqGNRCD6z8gp79WUFtI0wCgYIKoZIzj0EAwIw
gZMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGlu
MRgwFgYDVQQKDA9TdG9ybXlDbG91ZCBJbmMxIzAhBgNVBAMMGnN0b3JteWNsb3Vk
LmZhbWlseS5pMnAubmV0MSQwIgYJKoZIhvcNAQkBFhVhZG1pbkBzdG9ybXljbG91
ZC5vcmcwHhcNMjIwMzE5MTU1MjU2WhcNMzIwMzE2MTU1MjU2WjCBkzELMAkGA1UE
BhMCVVMxDjAMBgNVBAgMBVRleGFzMQ8wDQYDVQQHDAZBdXN0aW4xGDAWBgNVBAoM
D1N0b3JteUNsb3VkIEluYzEjMCEGA1UEAwwac3Rvcm15Y2xvdWQuZmFtaWx5Lmky
cC5uZXQxJDAiBgkqhkiG9w0BCQEWFWFkbWluQHN0b3JteWNsb3VkLm9yZzBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABFUli0hvJEmowNjJVjbKEIWBJhqe973S4VdL
cJuA5yY3dC4Y998abWEox7/Y1BhnBbpJuiodA341bXKkLMXQy/kwCgYIKoZIzj0E
AwIDSAAwRQIgD12F/TfY3iV1/WDF7BSKgbD5g2MfELUIy1dtUlJQuJUCIQD69mZw
V1Z9j2x0ZsuirS3i6AMfVyTDj0RFS3U1jeHzIQ==
-----END CERTIFICATE-----

View File

@@ -29,6 +29,7 @@ import net.i2p.data.SigningPublicKey;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.RouterContext;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.FileSuffixFilter;
import net.i2p.util.Log;
import net.i2p.util.SecureDirectory;
@@ -42,8 +43,9 @@ public class FamilyKeyCrypto {
private final RouterContext _context;
private final Log _log;
private final Map<Hash, String> _verified;
private final Set<Hash> _negativeCache;
private final Map<Hash, Verified> _verified;
private final Map<String, SigningPublicKey> _knownKeys;
private final Map<Hash, Result> _negativeCache;
private final Set<Hash> _ourFamily;
// following for verification only, otherwise null
private final String _fname;
@@ -91,9 +93,11 @@ public class FamilyKeyCrypto {
}
_privkey = (_fname != null) ? initialize() : null;
_pubkey = (_privkey != null) ? _privkey.toPublic() : null;
_verified = new ConcurrentHashMap<Hash, String>(4);
_negativeCache = new ConcurrentHashSet<Hash>(4);
_verified = new ConcurrentHashMap<Hash, Verified>(16);
_negativeCache = new ConcurrentHashMap<Hash, Result>(4);
_ourFamily = (_privkey != null) ? new ConcurrentHashSet<Hash>(4) : Collections.<Hash>emptySet();
_knownKeys = new HashMap<String, SigningPublicKey>(8);
loadCerts();
}
/**
@@ -178,15 +182,45 @@ public class FamilyKeyCrypto {
return _fname;
}
/**
* Only STORED_KEY is fully trusted.
* RI_KEY is Java with key in the RI.
* NO_KEY is i2pd without a key in the RI.
*
* @since 0.9.54
*/
public enum Result { NO_FAMILY, NO_KEY, NO_SIG, NAME_CHANGED, SIG_CHANGED, INVALID_SIG,
UNSUPPORTED_SIG, BAD_KEY, BAD_SIG, RI_KEY, STORED_KEY }
/**
* Cached name/sig/result.
*
* @since 0.9.54
*/
private static class Verified {
public final String name, sig;
public final Result result;
public Verified(String n, String s, Result r) {
name = n; sig = s; result = r;
}
}
/**
* Verify the family signature in a RouterInfo.
* @return true if good sig or if no family specified at all
* This requires a family key in the RI,
* or a certificate file for the family
* in certificates/family.
*
* @return Result
*/
public boolean verify(RouterInfo ri) {
public Result verify(RouterInfo ri) {
String name = ri.getOption(OPT_NAME);
if (name == null)
return true;
return verify(ri, name);
return Result.NO_FAMILY;
Result rv = verify(ri, name);
if (_log.shouldInfo())
_log.info("Result: " + rv + " for " + name + ' ' + ri.getHash());
return rv;
}
/**
@@ -208,7 +242,7 @@ public class FamilyKeyCrypto {
return true;
if (h.equals(_context.routerHash()))
return false;
boolean rv = verify(ri, name);
boolean rv = verify(ri, name) == Result.STORED_KEY;
if (rv) {
_ourFamily.add(h);
_log.logAlways(Log.INFO, "Found and verified member of our family (" + _fname + "): " + h);
@@ -223,31 +257,34 @@ public class FamilyKeyCrypto {
* Verify the family in a RouterInfo, name already retrieved
* @since 0.9.28
*/
private boolean verify(RouterInfo ri, String name) {
private Result verify(RouterInfo ri, String name) {
Hash h = ri.getHash();
String ssig = ri.getOption(OPT_SIG);
if (ssig == null) {
if (_log.shouldInfo())
_log.info("No sig for " + h + ' ' + name);
return false;
return Result.NO_SIG;
}
String nameAndSig = _verified.get(h);
String riNameAndSig = name + ssig;
if (nameAndSig != null) {
if (nameAndSig.equals(riNameAndSig))
return true;
// name or sig changed
Verified v = _verified.get(h);
if (v != null) {
if (!v.name.equals(name))
return Result.NAME_CHANGED;
if (v.sig.equals(ssig))
return v.result;
// sig changed, fall thru to re-check
_verified.remove(h);
}
SigningPublicKey spk;
boolean isKnownKey;
if (name.equals(_fname)) {
// us
spk = _pubkey;
isKnownKey = true;
} else {
if (_negativeCache.contains(h))
return false;
spk = loadCert(name);
if (spk == null) {
Result r = _negativeCache.get(h);
if (r != null)
return r;
spk = _knownKeys.get(name);
isKnownKey = spk != null;
if (!isKnownKey) {
// look for a b64 key in the RI
String skey = ri.getOption(OPT_KEY);
if (skey != null) {
@@ -268,57 +305,57 @@ public class FamilyKeyCrypto {
} catch (NumberFormatException e) {
if (_log.shouldInfo())
_log.info("Bad b64 family key: " + ri, e);
_negativeCache.put(h, Result.BAD_KEY);
return Result.BAD_KEY;
} catch (IllegalArgumentException e) {
if (_log.shouldInfo())
_log.info("Bad b64 family key: " + ri, e);
_negativeCache.put(h, Result.BAD_KEY);
return Result.BAD_KEY;
} catch (ArrayIndexOutOfBoundsException e) {
if (_log.shouldInfo())
_log.info("Bad b64 family key: " + ri, e);
_negativeCache.put(h, Result.BAD_KEY);
return Result.BAD_KEY;
}
}
}
if (spk == null) {
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("No cert or valid key for " + h + ' ' + name);
return false;
_negativeCache.put(h, Result.NO_KEY);
return Result.NO_KEY;
}
}
}
if (!spk.getType().isAvailable()) {
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Unsupported crypto for sig for " + h);
return false;
_negativeCache.put(h, Result.UNSUPPORTED_SIG);
return Result.UNSUPPORTED_SIG;
}
byte[] bsig = Base64.decode(ssig);
if (bsig == null) {
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Bad sig for " + h + ' ' + name + ' ' + ssig);
return false;
_negativeCache.put(h, Result.INVALID_SIG);
return Result.INVALID_SIG;
}
Signature sig;
try {
sig = new Signature(spk.getType(), bsig);
} catch (IllegalArgumentException iae) {
// wrong size (type mismatch)
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Bad sig for " + ri, iae);
return false;
_negativeCache.put(h, Result.INVALID_SIG);
return Result.INVALID_SIG;
}
byte[] nb = DataHelper.getUTF8(name);
byte[] b = new byte[nb.length + Hash.HASH_LENGTH];
System.arraycopy(nb, 0, b, 0, nb.length);
System.arraycopy(ri.getHash().getData(), 0, b, nb.length, Hash.HASH_LENGTH);
boolean rv = _context.dsa().verifySignature(sig, b, spk);
if (rv)
_verified.put(h, riNameAndSig);
else
_negativeCache.add(h);
if (_log.shouldInfo())
_log.info("Verified? " + rv + " for " + h + ' ' + name + ' ' + ssig);
boolean ok = _context.dsa().verifySignature(sig, b, spk);
Result rv;
if (ok) {
rv = isKnownKey ? Result.STORED_KEY : Result.RI_KEY;
_verified.put(h, new Verified(name, ssig, rv));
} else {
rv = Result.BAD_SIG;
_negativeCache.put(h, rv);
}
return rv;
}
@@ -441,27 +478,40 @@ public class FamilyKeyCrypto {
}
}
/**
* Load all the certs.
*
* @since 0.9.54
*/
private void loadCerts() {
File dir = new File(_context.getBaseDir(), CERT_DIR);
File[] files = dir.listFiles(new FileSuffixFilter(CERT_SUFFIX));
if (files == null)
return;
for (File file : files) {
String name = file.getName();
name = name.substring(0, name.length() - CERT_SUFFIX.length());
SigningPublicKey spk = loadCert(file);
if (spk != null)
_knownKeys.put(name, spk);
}
if (_log.shouldInfo())
_log.info("Loaded " + _knownKeys.size() + " keys");
}
/**
* Load a public key from a cert.
*
* @return null on all errors
*/
private SigningPublicKey loadCert(String familyName) {
if (familyName.contains("/") || familyName.contains("\\") ||
familyName.contains("..") || (new File(familyName)).isAbsolute())
return null;
familyName = familyName.replace("@", "_at_");
File dir = new File(_context.getBaseDir(), CERT_DIR);
File file = new File(dir, familyName + CERT_SUFFIX);
if (!file.exists())
return null;
private SigningPublicKey loadCert(File file) {
try {
PublicKey pk = CertUtil.loadKey(file);
return SigUtil.fromJavaKey(pk);
} catch (GeneralSecurityException gse) {
_log.error("Error loading family key " + familyName, gse);
_log.error("Error loading family key " + file, gse);
} catch (IOException ioe) {
_log.error("Error loading family key " + familyName, ioe);
_log.error("Error loading family key " + file, ioe);
}
return null;
}
@@ -480,6 +530,12 @@ public class FamilyKeyCrypto {
PrivateKey pk = KeyStoreUtil.getPrivateKey(ks, ksPass, _fname, keyPass);
if (pk == null)
throw new GeneralSecurityException("Family key not found: " + _fname);
// ensure the cert is there in case it needs to be exported
String familyName = _fname.replace("@", "_at_");
File dir = new File(_context.getBaseDir(), CERT_DIR);
File file = new File(dir, familyName + CERT_SUFFIX);
if (!file.exists())
KeyStoreUtil.exportCert(ks, ksPass, _fname, file);
return SigUtil.fromJavaKey(pk);
} catch (IOException ioe) {
throw new GeneralSecurityException("Error loading family key " + _fname, ioe);

View File

@@ -1125,12 +1125,17 @@ public abstract class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacad
}
FamilyKeyCrypto fkc = _context.router().getFamilyKeyCrypto();
if (fkc != null) {
boolean validFamily = fkc.verify(routerInfo);
if (!validFamily) {
if (_log.shouldInfo())
_log.info("Bad family sig: " + routerInfo.getHash());
FamilyKeyCrypto.Result r = fkc.verify(routerInfo);
switch (r) {
case BAD_KEY:
case INVALID_SIG:
case NO_SIG:
return "Bad family " + r + ' ' + routerInfo.getHash();
case BAD_SIG:
// To be investigated
break;
}
// todo store in RI
}
return validate(routerInfo);
}

View File

@@ -59,6 +59,7 @@ public class Analysis extends JobImpl implements RouterApp {
private volatile ClientAppState _state = UNINITIALIZED;
private final DecimalFormat fmt = new DecimalFormat("#0.00");
private boolean _wasRun;
private final List<String> _familyExemptPoints24 = new ArrayList<String>(2);
/**
* The name we register with the ClientAppManager
@@ -87,6 +88,7 @@ public class Analysis extends JobImpl implements RouterApp {
private static final double POINTS_FAMILY = -10.0;
private static final double POINTS_FAMILY_VERIFIED = POINTS_FAMILY * 2;
private static final double POINTS_NONFF = -5.0;
private static final double POINTS_BAD_FAMILY = 20.0;
private static final double POINTS_BAD_OUR_FAMILY = 100.0;
private static final double POINTS_OUR_FAMILY = -100.0;
public static final double MIN_CLOSE = 242.0;
@@ -107,6 +109,7 @@ public class Analysis extends JobImpl implements RouterApp {
public static final long DEFAULT_FREQUENCY = 24*60*60*1000L;
public static final float MIN_BLOCK_POINTS = 12.01f;
/** Get via getInstance() */
private Analysis(RouterContext ctx, ClientAppManager mgr, String[] args) {
super(ctx);
@@ -114,6 +117,8 @@ public class Analysis extends JobImpl implements RouterApp {
_log = ctx.logManager().getLog(Analysis.class);
_cmgr = mgr;
_persister = new PersistSybil(ctx);
_familyExemptPoints24.add("SUNYSB");
_familyExemptPoints24.add("stormycloud");
}
/**
@@ -600,7 +605,7 @@ public class Analysis extends JobImpl implements RouterApp {
for (Integer ii : oc.objects()) {
int count = oc.count(ii);
if (count >= 2)
rv.put(ii, new ArrayList<RouterInfo>(4));
rv.put(ii, new ArrayList<RouterInfo>(count));
}
for (Map.Entry<Integer, List<RouterInfo>> e : rv.entrySet()) {
Integer ii = e.getKey();
@@ -649,8 +654,9 @@ public class Analysis extends JobImpl implements RouterApp {
for (Integer ii : oc.objects()) {
int count = oc.count(ii);
if (count >= 2)
rv.put(ii, new ArrayList<RouterInfo>(4));
rv.put(ii, new ArrayList<RouterInfo>(count));
}
FamilyKeyCrypto fkc = _context.router().getFamilyKeyCrypto();
for (Map.Entry<Integer, List<RouterInfo>> e : rv.entrySet()) {
Integer ii = e.getKey();
int count = oc.count(ii);
@@ -672,6 +678,12 @@ public class Analysis extends JobImpl implements RouterApp {
continue;
if ((ip[2] & 0xff) != i2)
continue;
if (fkc != null) {
String f = info.getOption("family");
if (f != null && _familyExemptPoints24.contains(f) &&
fkc.verify(info) == FamilyKeyCrypto.Result.STORED_KEY)
continue;
}
e.getValue().add(info);
addPoints(points, info.getHash(), point, reason);
}
@@ -695,7 +707,7 @@ public class Analysis extends JobImpl implements RouterApp {
for (Integer ii : oc.objects()) {
int count = oc.count(ii);
if (count >= 4)
rv.put(ii, new ArrayList<RouterInfo>(8));
rv.put(ii, new ArrayList<RouterInfo>(count));
}
for (Map.Entry<Integer, List<RouterInfo>> e : rv.entrySet()) {
Integer ii = e.getKey();
@@ -759,18 +771,37 @@ public class Analysis extends JobImpl implements RouterApp {
reason = "Spoofed our family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
}
} else {
if (fkc.verify(info)) {
point = POINTS_FAMILY_VERIFIED;
if (count > 1)
reason = "In verified family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
else
reason = "In verified family \"" + ss + '"';
} else {
point = POINTS_FAMILY;
if (count > 1)
reason = "In unverified family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
else
reason = "In unverified family \"" + ss + '"';
FamilyKeyCrypto.Result r = fkc.verify(info);
switch (r) {
case BAD_KEY:
case BAD_SIG:
case INVALID_SIG:
case NO_SIG:
point = POINTS_BAD_FAMILY;
reason = "Bad family config \"" + ss + '"';
break;
case STORED_KEY:
point = POINTS_FAMILY_VERIFIED;
if (count > 1)
reason = "In verified family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
else
reason = "In verified family \"" + ss + '"';
break;
case NO_KEY:
case RI_KEY:
case UNSUPPORTED_SIG:
case NAME_CHANGED:
case SIG_CHANGED:
case NO_FAMILY: // won't happen
default:
point = POINTS_FAMILY;
if (count > 1)
reason = "In unverified family \"" + ss + "\" with <a href=\"/netdb?fam=" + ss + "&amp;sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>";
else
reason = "In unverified family \"" + ss + '"';
break;
}
}
} else if (count > 1) {