From c520dcb0f65ab5eac5d2975cb0dfcd6ba5bbeae3 Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Sun, 20 Mar 2022 07:41:49 -0400
Subject: [PATCH] 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
---
 .../i2p/router/web/helpers/NetDbRenderer.java |  15 +-
 .../certificates/family/stormycloud.crt       |  14 ++
 .../i2p/router/crypto/FamilyKeyCrypto.java    | 170 ++++++++++++------
 .../KademliaNetworkDatabaseFacade.java        |  15 +-
 .../src/net/i2p/router/sybil/Analysis.java    |  61 +++++--
 5 files changed, 196 insertions(+), 79 deletions(-)
 create mode 100644 installer/resources/certificates/family/stormycloud.crt

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 bcea57b323..9ef915f209 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 0000000000..4ec4765a3e
--- /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 275f61d351..5830aa9600 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 4878f3942f..b04b3ddded 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 6d50aa038b..6483617c68 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 + "&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) {
-- 
GitLab