Browse Source

Key-pinning back-end for directory authorities.

This module implements a key-pinning mechanism to ensure that it's
safe to use RSA keys as identitifers even as we migrate to Ed25519
keys.  It remembers, for every Ed25519 key we've seen, what the
associated Ed25519 key is.  This way, if we see a different Ed25519
key with that RSA key, we'll know that there's a mismatch.

We persist these entries to disk using a simple format, where each
line has a base64-encoded RSA SHA1 hash, then a base64-endoded
Ed25519 key.  Empty lines, misformed lines, and lines beginning with
a # are ignored. Lines beginning with @ are reserved for future
extensions.
Nick Mathewson 9 years ago
parent
commit
eacbe03c71
6 changed files with 695 additions and 0 deletions
  1. 2 0
      src/or/include.am
  2. 391 0
      src/or/keypin.c
  3. 44 0
      src/or/keypin.h
  4. 1 0
      src/test/include.am
  5. 2 0
      src/test/test.c
  6. 255 0
      src/test/test_keypin.c

+ 2 - 0
src/or/include.am

@@ -53,6 +53,7 @@ LIBTOR_A_SOURCES = \
 	src/or/entrynodes.c				\
 	src/or/ext_orport.c				\
 	src/or/hibernate.c				\
+	src/or/keypin.c					\
 	src/or/main.c					\
 	src/or/microdesc.c				\
 	src/or/networkstatus.c				\
@@ -161,6 +162,7 @@ ORHEADERS = \
 	src/or/geoip.h					\
 	src/or/entrynodes.h				\
 	src/or/hibernate.h				\
+	src/or/keypin.h					\
 	src/or/main.h					\
 	src/or/microdesc.h				\
 	src/or/networkstatus.h				\

+ 391 - 0
src/or/keypin.c

@@ -0,0 +1,391 @@
+/* Copyright (c) 2014, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+#define KEYPIN_PRIVATE
+
+#include "orconfig.h"
+#include "compat.h"
+#include "crypto.h"
+#include "di_ops.h"
+#include "ht.h"
+#include "keypin.h"
+#include "siphash.h"
+#include "torint.h"
+#include "torlog.h"
+#include "util.h"
+
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#ifdef HAVE_FCNTL_H
+#include <fcntl.h>
+#endif
+
+#ifdef _WIN32
+#include <io.h>
+#endif
+
+/**
+ * @file keypin.c
+ * @brief Key-pinning for RSA and Ed25519 identity keys at directory
+ *  authorities.
+ *
+ * This module implements a key-pinning mechanism to ensure that it's safe
+ * to use RSA keys as identitifers even as we migrate to Ed25519 keys.  It
+ * remembers, for every Ed25519 key we've seen, what the associated Ed25519
+ * key is.  This way, if we see a different Ed25519 key with that RSA key,
+ * we'll know that there's a mismatch.
+ *
+ * We persist these entries to disk using a simple format, where each line
+ * has a base64-encoded RSA SHA1 hash, then a base64-endoded Ed25519 key.
+ * Empty lines, misformed lines, and lines beginning with # are
+ * ignored. Lines beginning with @ are reserved for future extensions.
+ */
+
+static int keypin_journal_append_entry(const uint8_t *rsa_id_digest,
+                                       const uint8_t *ed25519_id_key);
+
+static HT_HEAD(rsamap, keypin_ent_st) the_rsa_map = HT_INITIALIZER();
+static HT_HEAD(edmap, keypin_ent_st) the_ed_map = HT_INITIALIZER();
+
+/** Hashtable helper: compare two keypin table entries and return true iff
+ * they have the same RSA key IDs. */
+static INLINE int
+keypin_ents_eq_rsa(const keypin_ent_t *a, const keypin_ent_t *b)
+{
+  return tor_memeq(a->rsa_id, b->rsa_id, sizeof(a->rsa_id));
+}
+
+/** Hashtable helper: hash a keypin table entries based on its RSA key ID */
+static INLINE unsigned
+keypin_ent_hash_rsa(const keypin_ent_t *a)
+{
+return (unsigned) siphash24g(a->rsa_id, sizeof(a->rsa_id));
+}
+
+/** Hashtable helper: compare two keypin table entries and return true iff
+ * they have the same ed25519 keys */
+static INLINE int
+keypin_ents_eq_ed(const keypin_ent_t *a, const keypin_ent_t *b)
+{
+  return tor_memeq(a->ed25519_key, b->ed25519_key, sizeof(a->ed25519_key));
+}
+
+/** Hashtable helper: hash a keypin table entries based on its ed25519 key */
+static INLINE unsigned
+keypin_ent_hash_ed(const keypin_ent_t *a)
+{
+return (unsigned) siphash24g(a->ed25519_key, sizeof(a->ed25519_key));
+}
+
+HT_PROTOTYPE(rsamap, keypin_ent_st, rsamap_node, keypin_ent_hash_rsa,
+               keypin_ents_eq_rsa);
+HT_GENERATE2(rsamap, keypin_ent_st, rsamap_node, keypin_ent_hash_rsa,
+               keypin_ents_eq_rsa, 0.6, tor_reallocarray, tor_free_);
+
+HT_PROTOTYPE(edmap, keypin_ent_st, edmap_node, keypin_ent_hash_ed,
+               keypin_ents_eq_ed);
+HT_GENERATE2(edmap, keypin_ent_st, edmap_node, keypin_ent_hash_ed,
+               keypin_ents_eq_ed, 0.6, tor_reallocarray, tor_free_);
+
+/**
+ * Check whether we already have an entry in the key pinning table for a
+ * router with RSA ID digest <b>rsa_id_digest</b> or for ed25519 key
+ * <b>ed25519_id_key</b>.  If we have an entry that matches both keys,
+ * return KEYPIN_FOUND. If we find an entry that matches one key but
+ * not the other, return KEYPIN_MISMATCH.  If we have no entry for either
+ * key, add such an entry to the table and return KEYPIN_ADDED.
+ */
+int
+keypin_check_and_add(const uint8_t *rsa_id_digest,
+                     const uint8_t *ed25519_id_key)
+{
+  keypin_ent_t search, *ent;
+  memset(&search, 0, sizeof(search));
+  memcpy(search.rsa_id, rsa_id_digest, sizeof(search.rsa_id));
+  memcpy(search.ed25519_key, ed25519_id_key, sizeof(search.ed25519_key));
+
+  /* Search by RSA key digest first */
+  ent = HT_FIND(rsamap, &the_rsa_map, &search);
+  if (ent) {
+    tor_assert(fast_memeq(ent->rsa_id, rsa_id_digest, sizeof(ent->rsa_id)));
+    if (tor_memeq(ent->ed25519_key, ed25519_id_key,sizeof(ent->ed25519_key))) {
+      return KEYPIN_FOUND; /* Match on both keys. Great. */
+    } else {
+      return KEYPIN_MISMATCH; /* Found RSA with different Ed key */
+    }
+  }
+
+  /* See if we know a different RSA key for this ed key */
+  ent = HT_FIND(edmap, &the_ed_map, &search);
+  if (ent) {
+    /* If we got here, then the ed key matches and the RSA doesn't */
+    tor_assert(fast_memeq(ent->ed25519_key, ed25519_id_key,
+                          sizeof(ent->ed25519_key)));
+    tor_assert(fast_memneq(ent->rsa_id, rsa_id_digest, sizeof(ent->rsa_id)));
+    return KEYPIN_MISMATCH;
+  }
+
+  /* Okay, this one is new to us. */
+  ent = tor_memdup(&search, sizeof(search));
+  keypin_add_entry_to_map(ent);
+  keypin_journal_append_entry(rsa_id_digest, ed25519_id_key);
+  return KEYPIN_ADDED;
+}
+
+/**
+ * Helper: add <b>ent</b> to the hash tables.
+ */
+MOCK_IMPL(STATIC void,
+keypin_add_entry_to_map, (keypin_ent_t *ent))
+{
+  HT_INSERT(rsamap, &the_rsa_map, ent);
+  HT_INSERT(edmap, &the_ed_map, ent);
+}
+
+/**
+ * Check whether we already have an entry in the key pinning table for a
+ * router with RSA ID digest <b>rsa_id_digest</b>.  If we have no such entry,
+ * return KEYPIN_NOT_FOUND.  If we find an entry that matches the RSA key but
+ * which has an ed25519 key, return KEYPIN_MISMATCH.
+ */
+int
+keypin_check_lone_rsa(const uint8_t *rsa_id_digest)
+{
+  keypin_ent_t search, *ent;
+  memset(&search, 0, sizeof(search));
+  memcpy(search.rsa_id, rsa_id_digest, sizeof(search.rsa_id));
+
+  /* Search by RSA key digest first */
+  ent = HT_FIND(rsamap, &the_rsa_map, &search);
+  if (ent) {
+    return KEYPIN_MISMATCH;
+  } else {
+    return KEYPIN_NOT_FOUND;
+  }
+}
+
+/** Open fd to the keypinning journal file. */
+static int keypin_journal_fd = -1;
+
+/** Open the key-pinning journal to append to <b>fname</b>.  Return 0 on
+ * success, -1 on failure. */
+int
+keypin_open_journal(const char *fname)
+{
+  /* O_SYNC ??*/
+  int fd = tor_open_cloexec(fname, O_WRONLY|O_CREAT, 0600);
+  if (fd < 0)
+    goto err;
+
+  if (tor_fd_seekend(fd) < 0)
+    goto err;
+
+  /* Add a newline in case the last line was only partially written */
+  if (write(fd, "\n", 1) < 1)
+    goto err;
+
+  /* Add something about when we opened this file. */
+  char buf[80];
+  char tbuf[ISO_TIME_LEN+1];
+  format_iso_time(tbuf, approx_time());
+  tor_snprintf(buf, sizeof(buf), "@opened-at %s\n", tbuf);
+  if (write_all(fd, buf, strlen(buf), 0) < 0)
+    goto err;
+
+  keypin_journal_fd = fd;
+  return 0;
+ err:
+  if (fd >= 0)
+    close(fd);
+  return -1;
+}
+
+/** Close the keypinning journal file. */
+int
+keypin_close_journal(void)
+{
+  if (keypin_journal_fd >= 0)
+    close(keypin_journal_fd);
+  keypin_journal_fd = -1;
+  return 0;
+}
+
+/** Length of a keypinning journal line, including terminating newline. */
+#define JOURNAL_LINE_LEN (BASE64_DIGEST_LEN + BASE64_DIGEST256_LEN + 2)
+
+/** Add an entry to the keypinning journal to map <b>rsa_id_digest</b> and
+ * <b>ed25519_id_key</b>. */
+static int
+keypin_journal_append_entry(const uint8_t *rsa_id_digest,
+                            const uint8_t *ed25519_id_key)
+{
+  if (keypin_journal_fd == -1)
+    return -1;
+  char line[JOURNAL_LINE_LEN];
+  digest_to_base64(line, (const char*)rsa_id_digest);
+  line[BASE64_DIGEST_LEN] = ' ';
+  digest256_to_base64(line + BASE64_DIGEST_LEN + 1,
+                      (const char*)ed25519_id_key);
+  line[BASE64_DIGEST_LEN+1+BASE64_DIGEST256_LEN] = '\n';
+
+  if (write_all(keypin_journal_fd, line, JOURNAL_LINE_LEN, 0)<0) {
+    log_warn(LD_DIRSERV, "Error while adding a line to the key-pinning "
+             "journal: %s", strerror(errno));
+    keypin_close_journal();
+    return -1;
+  }
+
+  return 0;
+}
+
+/** Load a journal from the <b>size</b>-byte region at <b>data</b>.  Return 0
+ * on success, -1 on failure. */
+STATIC int
+keypin_load_journal_impl(const char *data, size_t size)
+{
+  const char *start = data, *end = data + size, *next;
+
+  int n_corrupt_lines = 0;
+  int n_entries = 0;
+  int n_duplicates = 0;
+  int n_conflicts = 0;
+
+  for (const char *cp = start; cp < end; cp = next) {
+    const char *eol = memchr(cp, '\n', end-cp);
+    const char *eos = eol ? eol : end;
+    const size_t len = eos - cp;
+
+    next = eol ? eol + 1 : end;
+
+    if (len == 0) {
+      continue;
+    }
+
+    if (*cp == '@') {
+      /* Lines that start with @ are reserved. Ignore for now. */
+      continue;
+    }
+    if (*cp == '#') {
+      /* Lines that start with # are comments. */
+      continue;
+    }
+
+    /* Is it the right length?  (The -1 here is for the newline.) */
+    if (len != JOURNAL_LINE_LEN - 1) {
+      /* Lines with a bad length are corrupt unless they are empty.
+       * Ignore them either way */
+      for (const char *s = cp; s < eos; ++s) {
+        if (! TOR_ISSPACE(*s)) {
+          ++n_corrupt_lines;
+          break;
+        }
+      }
+      continue;
+    }
+
+    keypin_ent_t *ent = keypin_parse_journal_line(cp);
+
+    if (ent == NULL) {
+      ++n_corrupt_lines;
+      continue;
+    }
+
+    const keypin_ent_t *ent2;
+    if ((ent2 = HT_FIND(rsamap, &the_rsa_map, ent))) {
+      if (fast_memeq(ent2->ed25519_key, ent->ed25519_key, DIGEST256_LEN)) {
+        ++n_duplicates;
+      } else {
+        ++n_conflicts;
+      }
+      tor_free(ent);
+      continue;
+    } else if (HT_FIND(edmap, &the_ed_map, ent)) {
+      tor_free(ent);
+      ++n_conflicts;
+      continue;
+    }
+
+    keypin_add_entry_to_map(ent);
+    ++n_entries;
+  }
+
+  int severity = (n_corrupt_lines || n_duplicates) ? LOG_WARN : LOG_INFO;
+  tor_log(severity, LD_DIRSERV,
+          "Loaded %d entries from keypin journal. "
+          "Found %d corrupt lines, %d duplicates, and %d conflicts.",
+          n_entries, n_corrupt_lines, n_duplicates, n_conflicts);
+
+  return 0;
+}
+
+/**
+ * Load a journal from the file called <b>fname</b>. Return 0 on success,
+ * -1 on failure.
+ */
+int
+keypin_load_journal(const char *fname)
+{
+  tor_mmap_t *map = tor_mmap_file(fname);
+  if (!map) {
+    if (errno == ENOENT)
+      return 0;
+    else
+      return -1;
+  }
+  int r = keypin_load_journal_impl(map->data, map->size);
+  tor_munmap_file(map);
+  return r;
+}
+
+/** Parse a single keypinning journal line entry from <b>cp</b>.  The input
+ * does not need to be NUL-terminated, but it <em>does</em> need to have
+ * KEYPIN_JOURNAL_LINE_LEN -1 bytes available to read.  Return a new entry
+ * on success, and NULL on failure.
+ */
+STATIC keypin_ent_t *
+keypin_parse_journal_line(const char *cp)
+{
+  /* XXXX assumes !USE_OPENSSL_BASE64 */
+  keypin_ent_t *ent = tor_malloc_zero(sizeof(keypin_ent_t));
+
+  if (base64_decode((char*)ent->rsa_id, sizeof(ent->rsa_id),
+             cp, BASE64_DIGEST_LEN) != DIGEST_LEN ||
+      cp[BASE64_DIGEST_LEN] != ' ' ||
+      base64_decode((char*)ent->ed25519_key, sizeof(ent->ed25519_key),
+             cp+BASE64_DIGEST_LEN+1, BASE64_DIGEST256_LEN) != DIGEST256_LEN) {
+    tor_free(ent);
+    return NULL;
+  } else {
+    return ent;
+  }
+}
+
+/** Remove all entries from the keypinning table.*/
+void
+keypin_clear(void)
+{
+  int bad_entries = 0;
+  {
+    keypin_ent_t **ent, **next, *this;
+    for (ent = HT_START(rsamap, &the_rsa_map); ent != NULL; ent = next) {
+      this = *ent;
+      next = HT_NEXT_RMV(rsamap, &the_rsa_map, ent);
+
+      keypin_ent_t *other_ent = HT_REMOVE(edmap, &the_ed_map, this);
+      bad_entries += (other_ent != this);
+
+      tor_free(this);
+    }
+  }
+  bad_entries += HT_SIZE(&the_ed_map);
+
+  HT_CLEAR(edmap,&the_ed_map);
+  HT_CLEAR(rsamap,&the_rsa_map);
+
+  if (bad_entries) {
+    log_warn(LD_BUG, "Found %d discrepencies in the the keypin database.",
+             bad_entries);
+  }
+}
+

+ 44 - 0
src/or/keypin.h

@@ -0,0 +1,44 @@
+/* Copyright (c) 2014, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+#ifndef TOR_KEYPIN_H
+#define TOR_KEYPIN_H
+
+#include "testsupport.h"
+
+int keypin_check_and_add(const uint8_t *rsa_id_digest,
+                         const uint8_t *ed25519_id_key);
+
+int keypin_open_journal(const char *fname);
+int keypin_close_journal(void);
+int keypin_load_journal(const char *fname);
+void keypin_clear(void);
+int keypin_check_lone_rsa(const uint8_t *rsa_id_digest);
+
+#define KEYPIN_FOUND 0
+#define KEYPIN_ADDED 1
+#define KEYPIN_MISMATCH -1
+#define KEYPIN_NOT_FOUND -2
+
+#ifdef KEYPIN_PRIVATE
+
+/**
+ * In-memory representation of a key-pinning table entry.
+ */
+typedef struct keypin_ent_st {
+  HT_ENTRY(keypin_ent_st) rsamap_node;
+  HT_ENTRY(keypin_ent_st) edmap_node;
+  /** SHA1 hash of the RSA key */
+  uint8_t rsa_id[DIGEST_LEN];
+  /** Ed2219 key. */
+  uint8_t ed25519_key[DIGEST256_LEN];
+} keypin_ent_t;
+
+STATIC keypin_ent_t * keypin_parse_journal_line(const char *cp);
+STATIC int keypin_load_journal_impl(const char *data, size_t size);
+
+MOCK_DECL(STATIC void, keypin_add_entry_to_map, (keypin_ent_t *ent));
+#endif
+
+#endif
+

+ 1 - 0
src/test/include.am

@@ -44,6 +44,7 @@ src_test_test_SOURCES = \
 	src/test/test_extorport.c \
 	src/test/test_hs.c \
 	src/test/test_introduce.c \
+	src/test/test_keypin.c \
 	src/test/test_logging.c \
 	src/test/test_microdesc.c \
 	src/test/test_nodelist.c \

+ 2 - 0
src/test/test.c

@@ -1136,6 +1136,7 @@ extern struct testcase_t guardfraction_tests[];
 extern struct testcase_t extorport_tests[];
 extern struct testcase_t hs_tests[];
 extern struct testcase_t introduce_tests[];
+extern struct testcase_t keypin_tests[];
 extern struct testcase_t logging_tests[];
 extern struct testcase_t microdesc_tests[];
 extern struct testcase_t nodelist_tests[];
@@ -1181,6 +1182,7 @@ struct testgroup_t testgroups[] = {
   { "extorport/", extorport_tests },
   { "hs/", hs_tests },
   { "introduce/", introduce_tests },
+  { "keypin/", keypin_tests },
   { "nodelist/", nodelist_tests },
   { "oom/", oom_tests },
   { "options/", options_tests },

+ 255 - 0
src/test/test_keypin.c

@@ -0,0 +1,255 @@
+/* Copyright (c) 2014, The Tor Project, Inc. */
+/* See LICENSE for licensing information */
+
+#include "orconfig.h"
+#define KEYPIN_PRIVATE
+#include "or.h"
+#include "keypin.h"
+#include "util.h"
+
+#include "test.h"
+
+static void
+test_keypin_parse_line(void *arg)
+{
+  (void)arg;
+  keypin_ent_t *ent = NULL;
+
+  /* Good line */
+  ent = keypin_parse_journal_line(
+                "aGVyZSBpcyBhIGdvb2Qgc2hhMSE "
+                "VGhpcyBlZDI1NTE5IHNjb2ZmcyBhdCB0aGUgc2hhMS4");
+  tt_assert(ent);
+  tt_mem_op(ent->rsa_id, ==, "here is a good sha1!", 20);
+  tt_mem_op(ent->ed25519_key, ==, "This ed25519 scoffs at the sha1.", 32);
+  tor_free(ent); ent = NULL;
+
+  /* Good line with extra stuff we will ignore. */
+  ent = keypin_parse_journal_line(
+                "aGVyZSBpcyBhIGdvb2Qgc2hhMSE "
+                "VGhpcyBlZDI1NTE5IHNjb2ZmcyBhdCB0aGUgc2hhMS4helloworld");
+  tt_assert(ent);
+  tt_mem_op(ent->rsa_id, ==, "here is a good sha1!", 20);
+  tt_mem_op(ent->ed25519_key, ==, "This ed25519 scoffs at the sha1.", 32);
+  tor_free(ent); ent = NULL;
+
+  /* Bad line: no space in the middle. */
+  ent = keypin_parse_journal_line(
+                "aGVyZSBpcyBhIGdvb2Qgc2hhMSE?"
+                "VGhpcyBlZDI1NTE5IHNjb2ZmcyBhdCB0aGUgc2hhMS4");
+  tt_assert(! ent);
+
+  /* Bad line: bad base64 in RSA ID */
+  ent = keypin_parse_journal_line(
+                "aGVyZSBpcyBhIGdv!2Qgc2hhMSE "
+                "VGhpcyBlZDI1NTE5IHNjb2ZmcyBhdCB0aGUgc2hhMS4");
+  tt_assert(! ent);
+
+  /* Bad line: bad base64 in Ed25519 */
+  ent = keypin_parse_journal_line(
+                "aGVyZSBpcyBhIGdvb2Qgc2hhMSE "
+                "VGhpcyBlZDI1NTE5IHNjb2ZmcyB!dCB0aGUgc2hhMS4");
+  tt_assert(! ent);
+
+ done:
+  tor_free(ent);
+}
+
+static smartlist_t *mock_addent_got = NULL;
+static void
+mock_addent(keypin_ent_t *ent)
+{
+  smartlist_add(mock_addent_got, ent);
+  keypin_add_entry_to_map__real(ent);
+}
+
+static void
+test_keypin_parse_file(void *arg)
+{
+  (void)arg;
+
+  mock_addent_got = smartlist_new();
+  MOCK(keypin_add_entry_to_map, mock_addent);
+
+  /* Simple, minimal, correct example. */
+  const char data1[] =
+"PT09PT09PT09PT09PT09PT09PT0 PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0\n"
+"TG9yYXggaXBzdW0gZ3J1dnZ1bHU cyB0aG5lZWQgYW1ldCwgc25lcmdlbGx5IG9uY2UtbGU\n"
+"ciBsZXJraW0sIHNlZCBkbyBiYXI YmFsb290IHRlbXBvciBnbHVwcGl0dXMgdXQgbGFib3I\n"
+"ZSBldCB0cnVmZnVsYSBtYWduYSA YWxpcXVhLiBVdCBlbmltIGFkIGdyaWNrbGUtZ3Jhc3M\n"
+"dmVuaWFtLCBxdWlzIG1pZmYtbXU ZmZlcmVkIGdhLXp1bXBjbyBsYWJvcmlzIG5pc2kgdXQ\n"
+"Y3J1ZmZ1bHVzIGV4IGVhIHNjaGw b3BwaXR5IGNvbnNlcXVhdC4gRHVpcyBhdXRlIHNuYXI\n"
+"Z2dsZSBpbiBzd29tZWVzd2FucyA aW4gdm9sdXB0YXRlIGF4ZS1oYWNrZXIgZXNzZSByaXA\n"
+"cHVsdXMgY3J1bW1paSBldSBtb28 ZiBudWxsYSBzbnV2di5QTFVHSFBMT1ZFUlhZWlpZLi4\n";
+
+  tt_int_op(0, ==, keypin_load_journal_impl(data1, strlen(data1)));
+  tt_int_op(8, ==, smartlist_len(mock_addent_got));
+  keypin_ent_t *ent = smartlist_get(mock_addent_got, 2);
+  tt_mem_op(ent->rsa_id, ==, "r lerkim, sed do bar", 20);
+  tt_mem_op(ent->ed25519_key, ==, "baloot tempor gluppitus ut labor", 32);
+
+  /* More complex example: weird lines, bogus lines,
+     duplicate/conflicting lines */
+  const char data2[] =
+    "PT09PT09PT09PT09PT09PT09PT0 PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0\n"
+    "# This is a comment.\n"
+    "     \n"
+    "QXQgdGhlIGVuZCBvZiB0aGUgeWU YXIgS3VycmVta2FybWVycnVrIHNhaWQgdG8gaGltLCA\n"
+    "IllvdSBoYXZlIG1hZGUgYSBnb28 ZCBiZWdpbm5pbmcuIiBCdXQgbm8gbW9yZS4gV2l6YXI\n"
+    "\n"
+    "ZHMgc3BlYWsgdHJ1dGgsIGFuZCA aXQgd2FzIHRydWUgdGhhdCBhbGwgdGhlIG1hc3Rlcgo\n"
+    "@reserved for a future extension \n"
+    "eSBvZiBOYW1lcyB0aGF0IEdlZCA aGFkIHRvaWxlZCbyB3aW4gdGhhdCB5ZWFyIHdhcyA\n"
+    "eSBvZiBOYW1lcyB0aGF0IEdlZCA aGFkIHRvaWxlZCbyB3aW4gdGhhdCB5ZWFyIHdhcy"
+              "A line too long\n"
+    "dGhlIG1lcmUgc3RhcnQgb2Ygd2g YXQgaGUgbXVzdCBnbyBvb!BsZWFybmluZy4uLi4uLi4\n"
+    "ZHMgc3BlYWsgdaJ1dGgsIGFuZCA aXQgd2FzIHRydWUgdGhhdCBhbGwgdGhlIG1hc3Rlcgo\n"
+    "ZHMgc3BlYWsgdHJ1dGgsIGFuZCA aXQgd2FzIHRydaUgdGhhdCBhbGwgdGhlIG1hc3Rlcgo\n"
+    ;
+
+  tt_int_op(0, ==, keypin_load_journal_impl(data2, strlen(data2)));
+  tt_int_op(11, ==, smartlist_len(mock_addent_got));
+  ent = smartlist_get(mock_addent_got, 9);
+  tt_mem_op(ent->rsa_id, ==, "\"You have made a goo", 20);
+  tt_mem_op(ent->ed25519_key, ==, "d beginning.\" But no more. Wizar", 32);
+
+  ent = smartlist_get(mock_addent_got, 10);
+  tt_mem_op(ent->rsa_id, ==, "ds speak truth, and ", 20);
+  tt_mem_op(ent->ed25519_key, ==, "it was true that all the master\n", 32);
+
+  /* File truncated before NL */
+  const char data3[] =
+    "Tm8gZHJhZ29uIGNhbiByZXNpc3Q IHRoZSBmYXNjaW5hdGlvbiBvZiByaWRkbGluZyB0YWw";
+  tt_int_op(0, ==, keypin_load_journal_impl(data3, strlen(data3)));
+  tt_int_op(12, ==, smartlist_len(mock_addent_got));
+  ent = smartlist_get(mock_addent_got, 11);
+  tt_mem_op(ent->rsa_id, ==, "No dragon can resist", 20);
+  tt_mem_op(ent->ed25519_key, ==, " the fascination of riddling tal", 32);
+
+ done:
+  keypin_clear();
+  smartlist_free(mock_addent_got);
+}
+
+#define ADD(a,b) keypin_check_and_add((const uint8_t*)(a),(const uint8_t*)(b))
+#define LONE_RSA(a) keypin_check_lone_rsa((const uint8_t*)(a))
+
+static void
+test_keypin_add_entry(void *arg)
+{
+  (void)arg;
+  keypin_clear();
+
+  tt_int_op(KEYPIN_ADDED, ==, ADD("ambassadors-at-large",
+                                  "bread-and-butter thing-in-itself"));
+  tt_int_op(KEYPIN_ADDED, ==, ADD("gentleman-adventurer",
+                                  "cloak-and-dagger what's-his-face"));
+
+  tt_int_op(KEYPIN_FOUND, ==, ADD("ambassadors-at-large",
+                                  "bread-and-butter thing-in-itself"));
+  tt_int_op(KEYPIN_FOUND, ==, ADD("ambassadors-at-large",
+                                  "bread-and-butter thing-in-itself"));
+  tt_int_op(KEYPIN_FOUND, ==, ADD("gentleman-adventurer",
+                                  "cloak-and-dagger what's-his-face"));
+
+  tt_int_op(KEYPIN_ADDED, ==, ADD("Johnnies-come-lately",
+                                  "run-of-the-mill root-mean-square"));
+
+  tt_int_op(KEYPIN_MISMATCH, ==, ADD("gentleman-adventurer",
+                                     "hypersentimental closefistedness"));
+
+  tt_int_op(KEYPIN_MISMATCH, ==, ADD("disestablismentarian",
+                                     "cloak-and-dagger what's-his-face"));
+
+  tt_int_op(KEYPIN_FOUND, ==, ADD("gentleman-adventurer",
+                                  "cloak-and-dagger what's-his-face"));
+
+  tt_int_op(KEYPIN_NOT_FOUND, ==, LONE_RSA("Llanfairpwllgwyngyll"));
+  tt_int_op(KEYPIN_MISMATCH, ==, LONE_RSA("Johnnies-come-lately"));
+
+ done:
+  keypin_clear();
+}
+
+static void
+test_keypin_journal(void *arg)
+{
+  (void)arg;
+  char *contents = NULL;
+  const char *fname = get_fname("keypin-journal");
+
+  tt_int_op(0, ==, keypin_load_journal(fname)); /* ENOENT is okay */
+  update_approx_time(1217709000);
+  tt_int_op(0, ==, keypin_open_journal(fname));
+
+  tt_int_op(KEYPIN_ADDED, ==, ADD("king-of-the-herrings",
+                                  "good-for-nothing attorney-at-law"));
+  tt_int_op(KEYPIN_ADDED, ==, ADD("yellowish-red-yellow",
+                                  "salt-and-pepper high-muck-a-muck"));
+  tt_int_op(KEYPIN_FOUND, ==, ADD("yellowish-red-yellow",
+                                  "salt-and-pepper high-muck-a-muck"));
+  keypin_close_journal();
+  keypin_clear();
+
+  tt_int_op(0, ==, keypin_load_journal(fname));
+  update_approx_time(1231041600);
+  tt_int_op(0, ==, keypin_open_journal(fname));
+  tt_int_op(KEYPIN_FOUND, ==, ADD("yellowish-red-yellow",
+                                  "salt-and-pepper high-muck-a-muck"));
+  tt_int_op(KEYPIN_ADDED, ==, ADD("theatre-in-the-round",
+                                  "holier-than-thou jack-in-the-box"));
+  tt_int_op(KEYPIN_ADDED, ==, ADD("no-deposit-no-return",
+                                  "across-the-board will-o-the-wisp"));
+  tt_int_op(KEYPIN_MISMATCH, ==, ADD("intellectualizations",
+                                     "salt-and-pepper high-muck-a-muck"));
+  keypin_close_journal();
+  keypin_clear();
+
+  tt_int_op(0, ==, keypin_load_journal(fname));
+  update_approx_time(1412278354);
+  tt_int_op(0, ==, keypin_open_journal(fname));
+  tt_int_op(KEYPIN_FOUND, ==, ADD("yellowish-red-yellow",
+                                  "salt-and-pepper high-muck-a-muck"));
+  tt_int_op(KEYPIN_MISMATCH, ==, ADD("intellectualizations",
+                                     "salt-and-pepper high-muck-a-muck"));
+  tt_int_op(KEYPIN_FOUND, ==, ADD("theatre-in-the-round",
+                                  "holier-than-thou jack-in-the-box"));
+  tt_int_op(KEYPIN_MISMATCH, ==, ADD("counterrevolutionary",
+                                     "holier-than-thou jack-in-the-box"));
+  tt_int_op(KEYPIN_MISMATCH, ==, ADD("no-deposit-no-return",
+                                     "floccinaucinihilipilificationism"));
+  keypin_close_journal();
+
+  contents = read_file_to_str(fname, RFTS_BIN, NULL);
+  tt_assert(contents);
+  tt_str_op(contents,==,
+    "\n"
+    "@opened-at 2008-08-02 20:30:00\n"
+    "a2luZy1vZi10aGUtaGVycmluZ3M Z29vZC1mb3Itbm90aGluZyBhdHRvcm5leS1hdC1sYXc\n"
+    "eWVsbG93aXNoLXJlZC15ZWxsb3c c2FsdC1hbmQtcGVwcGVyIGhpZ2gtbXVjay1hLW11Y2s\n"
+    "\n"
+    "@opened-at 2009-01-04 04:00:00\n"
+    "dGhlYXRyZS1pbi10aGUtcm91bmQ aG9saWVyLXRoYW4tdGhvdSBqYWNrLWluLXRoZS1ib3g\n"
+    "bm8tZGVwb3NpdC1uby1yZXR1cm4 YWNyb3NzLXRoZS1ib2FyZCB3aWxsLW8tdGhlLXdpc3A\n"
+    "\n"
+    "@opened-at 2014-10-02 19:32:34\n");
+
+ done:
+  tor_free(contents);
+  keypin_clear();
+}
+
+#undef ADD
+#undef LONE_RSA
+
+#define TEST(name, flags)                                       \
+  { #name , test_keypin_ ## name, (flags), NULL, NULL }
+
+struct testcase_t keypin_tests[] = {
+  TEST( parse_line, 0 ),
+  TEST( parse_file, TT_FORK ),
+  TEST( add_entry, TT_FORK ),
+  TEST( journal, TT_FORK ),
+  END_OF_TESTCASES
+};
+