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 + "&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) {