diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java index bcea57b323099d01d52a48e1d41613947af1e3ed..9ef915f20997914dadfcff68bd5b2652cc718e56 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/NetDbRenderer.java @@ -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"); } diff --git a/installer/resources/certificates/family/stormycloud.crt b/installer/resources/certificates/family/stormycloud.crt new file mode 100644 index 0000000000000000000000000000000000000000..4ec4765a3e0704f84b9fde3c816e03589b5d7cc9 --- /dev/null +++ b/installer/resources/certificates/family/stormycloud.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICKDCCAc6gAwIBAgIUcPHZXtYSqGNRCD6z8gp79WUFtI0wCgYIKoZIzj0EAwIw +gZMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEPMA0GA1UEBwwGQXVzdGlu +MRgwFgYDVQQKDA9TdG9ybXlDbG91ZCBJbmMxIzAhBgNVBAMMGnN0b3JteWNsb3Vk +LmZhbWlseS5pMnAubmV0MSQwIgYJKoZIhvcNAQkBFhVhZG1pbkBzdG9ybXljbG91 +ZC5vcmcwHhcNMjIwMzE5MTU1MjU2WhcNMzIwMzE2MTU1MjU2WjCBkzELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMQ8wDQYDVQQHDAZBdXN0aW4xGDAWBgNVBAoM +D1N0b3JteUNsb3VkIEluYzEjMCEGA1UEAwwac3Rvcm15Y2xvdWQuZmFtaWx5Lmky +cC5uZXQxJDAiBgkqhkiG9w0BCQEWFWFkbWluQHN0b3JteWNsb3VkLm9yZzBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABFUli0hvJEmowNjJVjbKEIWBJhqe973S4VdL +cJuA5yY3dC4Y998abWEox7/Y1BhnBbpJuiodA341bXKkLMXQy/kwCgYIKoZIzj0E +AwIDSAAwRQIgD12F/TfY3iV1/WDF7BSKgbD5g2MfELUIy1dtUlJQuJUCIQD69mZw +V1Z9j2x0ZsuirS3i6AMfVyTDj0RFS3U1jeHzIQ== +-----END CERTIFICATE----- diff --git a/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java b/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java index 275f61d35112efa46244f1afd591df5ecbe77bb1..5830aa96002c1d5f482f2788e3695442059a8a0e 100644 --- a/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java +++ b/router/java/src/net/i2p/router/crypto/FamilyKeyCrypto.java @@ -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); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java index 4878f3942fce281584974a751ce3724fa4f88bf9..b04b3ddded4691d910fbc264922c5a3b54c0ca9e 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -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); } diff --git a/router/java/src/net/i2p/router/sybil/Analysis.java b/router/java/src/net/i2p/router/sybil/Analysis.java index 6d50aa038b3d11f3916d3486ef02c41b45e307f3..6483617c685d42168ce847829c867f99008cbfbe 100644 --- a/router/java/src/net/i2p/router/sybil/Analysis.java +++ b/router/java/src/net/i2p/router/sybil/Analysis.java @@ -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 + "&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 + "&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 + "&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 + "&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 + "&sybil\">" + (count - 1) + " other" + (( count > 2) ? "s" : "") + "</a>"; + else + reason = "In unverified family \"" + ss + '"'; + break; } } } else if (count > 1) {