chiark / gitweb /
t/: Add a test suite.
[catacomb-python] / t / t-key.py
diff --git a/t/t-key.py b/t/t-key.py
new file mode 100644 (file)
index 0000000..e81217e
--- /dev/null
@@ -0,0 +1,343 @@
+### -*-python-*-
+###
+### Testing key-management functionality
+###
+### (c) 2019 Straylight/Edgeware
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This file is part of the Python interface to Catacomb.
+###
+### Catacomb/Python is free software: you can redistribute it and/or
+### modify it under the terms of the GNU General Public License as
+### published by the Free Software Foundation; either version 2 of the
+### License, or (at your option) any later version.
+###
+### Catacomb/Python is distributed in the hope that it will be useful, but
+### WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+### General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with Catacomb/Python.  If not, write to the Free Software
+### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+### USA.
+
+###--------------------------------------------------------------------------
+### Imported modules.
+
+import catacomb as C
+import sys as SYS
+import unittest as U
+import testutils as T
+import time as TM
+
+###--------------------------------------------------------------------------
+class TestKeyError (U.TestCase):
+
+  def test_keyerror(me):
+
+    try: C.KeyFile("notexist", C.KOPEN_NOFILE).newkey(1, "foo")
+    except C.KeyError: e = SYS.exc_info()[1]
+    else: me.fail("expected `catacomb.KeyError'")
+    me.assertEqual(e.err, C.KERR_READONLY)
+    me.assertEqual(e.errstring, "Key file is read-only")
+    me.assertEqual(e.args, (C.KERR_READONLY,))
+    me.assertEqual(str(e),
+                   "KERR_READONLY (%d): Key file is read-only" %
+                     C.KERR_READONLY)
+
+    me.assertRaises(TypeError, C.KeyError)
+    token = ["TOKEN"]
+    e = C.KeyError(C.KERR_DUPID, token)
+    me.assertEqual(e.err, C.KERR_DUPID)
+    me.assertEqual(e.errstring, "Key id already exists")
+    me.assertEqual(e.args, (C.KERR_DUPID, token))
+
+###--------------------------------------------------------------------------
+class TestKeyFile (U.TestCase):
+
+  def test_keyring(me):
+
+    kf = C.KeyFile("t/keyring")
+
+    ## Check basic attributes.
+    me.assertEqual(kf.name, "t/keyring")
+    me.assertEqual(kf.modifiedp, False)
+    me.assertEqual(kf.writep, False)
+    me.assertEqual(kf.filep, False)
+
+    ## Check enumeration.
+    me.assertEqual(set(k.type for k in T.itervalues(kf)),
+                   set(["rsa", "ec", "ec-param", "twofish"]))
+    me.assertEqual(len(kf), 4)
+
+    ## Start with `rsa'.
+    k = kf.bytag("ron")
+    me.assertEqual(k.type, "rsa")
+    me.assertEqual(k.id, 0x8599dbab)
+    me.assertEqual(type(k.data), C.KeyDataStructured)
+    me.assertEqual(set(k.data), set(["e", "n", "private"]))
+    priv = k.data["private"]
+    me.assertEqual(type(priv), C.KeyDataEncrypted)
+    me.assertRaises(C.KeyError, priv.unlock, T.bin("wrong secret"))
+    priv = priv.unlock(T.bin("very secret"))
+    me.assertEqual(type(priv), C.KeyDataStructured)
+    me.assertEqual(set(priv),
+                   set(["p", "q", "d", "d-mod-p", "d-mod-q", "q-inv"]))
+    me.assertEqual(k.data["n"].mp, priv["p"].mp*priv["q"].mp)
+
+    ## This key has an attribute.  Poke about at them.
+    a = k.attr
+    me.assertEqual(len(a), 1)
+    me.assertEqual(set(a), set(["attr"]))
+    me.assertEqual(a["attr"], "value")
+    me.assertRaises(KeyError, lambda: a["notexist"])
+    me.assertEqual(a.get("attr"), "value")
+    me.assertEqual(a.get("notexist"), None)
+
+    ## Check fingerprinting while we're here.
+    for filter in ["-secret", "none"]:
+      h = C.sha256(); me.assertTrue(k.fingerprint(h, filter)); fp0 = h.done()
+      h = C.sha256()
+      h.hash(T.bin("catacomb-key-fingerprint:")) \
+       .hashu32(k.id) \
+       .hashbuf8(T.bin(k.type))
+      h.hash(k.data.encode(filter))
+      for a in sorted(T.iterkeys(k.attr)):
+        h.hashbuf8(T.bin(a)).hashbuf16(T.bin(k.attr[a]))
+      fp1 = h.done()
+      me.assertEqual(fp0, fp1)
+
+    ## Try `ec-param'.  This should be fairly easy.
+    k = kf["ec-param"]
+    me.assertEqual(k.tag, None)
+    me.assertEqual(k.id, 0x4a4e1ee7)
+    me.assertEqual(type(k.data), C.KeyDataStructured)
+    me.assertEqual(set(k.data), set(["curve"]))
+    curve = k.data["curve"]
+    me.assertEqual(type(curve), C.KeyDataString)
+    me.assertEqual(curve.str, "nist-p256")
+
+    ## Check qualified-tag lookups.
+    me.assertRaises(C.KeyError, kf.qtag, "notexist.curve")
+    me.assertRaises(C.KeyError, kf.qtag, "ec-param.notexist")
+    t, k, kd = kf.qtag("ec-param.curve")
+    me.assertEqual(t, "4a4e1ee7:ec-param.curve")
+    me.assertEqual(k.type, "ec-param")
+    me.assertEqual(type(kd), C.KeyDataString)
+    me.assertEqual(kd.str, "nist-p256")
+
+    ## Try `ec'.  A little trickier.
+    k = kf.bytype("ec")
+    me.assertEqual(k.tag, None)
+    me.assertEqual(k.id, 0xbd761d35)
+    me.assertEqual(type(k.data), C.KeyDataStructured)
+    me.assertEqual(set(k.data), set(["curve", "p", "private"]))
+    curve = k.data["curve"]
+    me.assertEqual(type(curve), C.KeyDataString)
+    me.assertEqual(curve.str, "nist-p256")
+    einfo = C.eccurves[curve.str]
+    me.assertEqual(type(k.data["p"]), C.KeyDataECPt)
+    X = k.data["p"].ecpt
+    priv = k.data["private"]
+    me.assertEqual(type(priv), C.KeyDataEncrypted)
+    me.assertRaises(C.KeyError, priv.unlock, T.bin("wrong secret"))
+    priv = priv.unlock(T.bin("super secret"))
+    me.assertEqual(type(priv), C.KeyDataStructured)
+    me.assertEqual(set(priv), set(["x"]))
+    x = priv["x"].mp
+    me.assertEqual(x*einfo.G, X)
+
+    ## Finish with `twofish'.
+    k = kf.byid(0x60090be2)
+    me.assertEqual(k.tag, None)
+    me.assertEqual(k.type, "twofish")
+    me.assertEqual(type(k.data), C.KeyDataEncrypted)
+    me.assertRaises(C.KeyError, k.data.unlock, T.bin("wrong secret"))
+    kd = k.data.unlock(T.bin("not secret"))
+    me.assertEqual(type(kd), C.KeyDataBinary)
+    me.assertEqual(kd.bin, C.bytes("d337b98eea24425826df202a6a3d1ef8"
+                                   "377b71923fe1179451564776da29bb84"))
+
+    ## Check unsuccessful searches.
+    me.assertRaises(KeyError, lambda: kf["notexist"])
+    me.assertEqual(kf.bytag("notexist"), None)
+    me.assertEqual(kf.bytag(12345), None)
+    me.assertEqual(kf.bytype("notexist"), None)
+    me.assertRaises(TypeError, kf.bytype, 12345)
+    me.assertRaises(C.KeyError, kf.byid, 0x12345678)
+
+    ## The keyring should be readonly.
+    me.assertRaises(C.KeyError, kf.newkey, 0x12345678, "fail")
+    me.assertRaises(C.KeyError, setattr, k, "tag", "foo")
+    me.assertRaises(C.KeyError, delattr, k, "tag")
+    me.assertRaises(C.KeyError, setattr, k, "data", C.KeyDataString("foo"))
+
+  def test_keywrite(me):
+    kf = C.KeyFile("test", C.KOPEN_WRITE | C.KOPEN_NOFILE)
+    me.assertEqual(kf.modifiedp, False)
+    now = int(TM.time())
+    exp = now + 86400
+
+    k = kf.newkey(0x11111111, "first", exp)
+    me.assertEqual(kf.modifiedp, True)
+
+    me.assertEqual(kf[0x11111111].id, 0x11111111)
+    me.assertEqual(k.exptime, exp)
+    me.assertEqual(k.deltime, exp)
+    me.assertRaises(ValueError, setattr, k, "deltime", C.KEXP_FOREVER)
+    k.exptime = exp + 5
+    me.assertEqual(k.data.str, "<unset>")
+    n = 9876543210
+    k.data = C.KeyDataMP(n)
+    me.assertEqual(k.data.mp, n)
+    me.assertEqual(k.comment, None)
+    c = ";; just a test"
+    k.comment = c
+    me.assertEqual(k.comment, c)
+    k.comment = None
+    me.assertEqual(k.comment, None)
+    k.comment = c
+    me.assertEqual(k.comment, c)
+    del k.comment
+    me.assertEqual(k.comment, None)
+
+###--------------------------------------------------------------------------
+
+def keydata_equalp(kd0, kd1):
+  if type(kd0) is not type(kd1): return False
+  elif type(kd0) is C.KeyDataBinary: return kd0.bin == kd1.bin
+  elif type(kd0) is C.KeyDataMP: return kd0.mp == kd1.mp
+  elif type(kd0) is C.KeyDataEncrypted: return kd0.ct == kd1.ct
+  elif type(kd0) is C.KeyDataECPt: return kd0.ecpt == kd1.ecpt
+  elif type(kd0) is C.KeyDataString: return kd0.str == kd1.str
+  elif type(kd0) is C.KeyDataStructured:
+    if len(kd0) != len(kd1): return False
+    for t, v0 in T.iteritems(kd0):
+      try: v1 = kd1[t]
+      except KeyError: return False
+      if not keydata_equalp(v0, v1): return False
+    return True
+  else:
+    raise SystemError("unexpected keydata type")
+
+class TestKeyData (U.TestCase):
+
+  def test_flags(me):
+    me.assertEqual(C.KeyData.readflags("none"), (0, 0, ""))
+    me.assertEqual(C.KeyData.readflags("ec,public:..."),
+                   (C.KENC_EC | C.KCAT_PUB,
+                    C.KF_ENCMASK | C.KF_CATMASK,
+                    ":..."))
+    me.assertEqual(C.KeyData.readflags("int,burn"),
+                   (C.KENC_MP | C.KF_BURN, C.KF_ENCMASK | C.KF_BURN, ""))
+    me.assertRaises(C.KeyError, C.KeyData.readflags, "int,burn?")
+    me.assertRaises(C.KeyError, C.KeyData.readflags, "int,ec")
+    me.assertRaises(C.KeyError, C.KeyData.readflags, "snork")
+    me.assertEqual(C.KeyData.writeflags(0), "binary,symmetric")
+    me.assertEqual(C.KeyData.writeflags(C.KENC_EC | C.KCAT_PUB), "ec,public")
+
+  def test_misc(me):
+    kd = C.KeyDataStructured({ "a": C.KeyDataString("foo", "public"),
+                               "b": C.KeyDataMP(12345, "private"),
+                               "c": C.KeyDataString("bar", "public") })
+
+    kd2 = kd.copy()
+    me.assertEqual(type(kd2), C.KeyDataStructured)
+    me.assertEqual(set(T.iterkeys(kd2)), set(["a", "b", "c"]))
+
+    kd2 = C.KeyDataMP(12345, C.KCAT_PRIV).copy("private")
+
+    kd2 = kd.copy("-secret")
+    me.assertEqual(type(kd2), C.KeyDataStructured)
+    me.assertEqual(set(T.iterkeys(kd2)), set(["a", "c"]))
+
+    kd2 = kd.copy((0, C.KF_NONSECRET))
+    me.assertEqual(type(kd2), C.KeyDataStructured)
+    me.assertEqual(set(T.iterkeys(kd2)), set(["b"]))
+
+  def check_encode(me, kd):
+    me.assertTrue(keydata_equalp(C.KeyData.decode(kd.encode()), kd))
+    kd1, tail = C.KeyData.read(kd.write())
+    me.assertEqual(tail, "")
+    me.assertTrue(keydata_equalp(kd, kd1))
+
+  def test_bin(me):
+    rng = T.detrand("kd-bin")
+    by = rng.block(16)
+    kd = C.KeyDataBinary(by, "symm,burn")
+    me.assertEqual(kd.bin, by)
+    me.check_encode(kd)
+
+  def test_mp(me):
+    rng = T.detrand("kd-mp")
+    x = rng.mp(128)
+    kd = C.KeyDataMP(x, "symm,burn")
+    me.assertEqual(kd.mp, x)
+    me.check_encode(kd)
+
+  def test_string(me):
+    s = "some random string"
+    kd = C.KeyDataString(s, "symm,burn")
+    me.assertEqual(kd.str, s)
+    me.check_encode(kd)
+
+  def test_enc(me):
+    rng = T.detrand("kd-enc")
+    ct = rng.block(16)
+    kd = C.KeyDataEncrypted(ct, "symm")
+    me.assertEqual(kd.ct, ct)
+    me.check_encode(kd)
+
+  def test_ecpt(me):
+    rng = T.detrand("kd-ec")
+    Q = C.ECPt(rng.mp(128), rng.mp(128))
+    kd = C.KeyDataECPt(Q, "symm,burn")
+    me.assertEqual(kd.ecpt, Q)
+    me.check_encode(kd)
+
+  def test_struct(me):
+    rng = T.detrand("kd-struct")
+    kd = C.KeyDataStructured({ "a": C.KeyDataString("a"),
+                               "b": C.KeyDataString("b"),
+                               "c": C.KeyDataString("c"),
+                               "d": C.KeyDataString("d") })
+    for i in ["a", "b", "c", "d"]: me.assertEqual(kd[i].str, i)
+    me.assertEqual(len(kd), 4)
+    me.check_encode(kd)
+    me.assertRaises(TypeError, C.KeyDataStructured, { "a": "a" })
+
+###--------------------------------------------------------------------------
+### Mappings.
+
+class TestKeyFileMapping (T.ImmutableMappingTextMixin):
+  def _mkkey(me, i): return i
+  def _getkey(me, k): return k
+  def _getvalue(me, v): return v.data.mp
+
+  def test_keyfile(me):
+    kf = C.KeyFile("test", C.KOPEN_WRITE | C.KOPEN_NOFILE)
+    model = {}
+    for i in [1, 2, 3]:
+      model[i] = 100 + i
+      kf.newkey(i, "k#%d" % i).data = C.KeyDataMP(100 + i)
+
+    me.check_immutable_mapping(kf, model)
+
+class TestKeyAttrMapping (T.MutableMappingTestMixin):
+
+  def test_attrmap(me):
+    def mkmap():
+      kf = C.KeyFile("test", C.KOPEN_WRITE | C.KOPEN_NOFILE)
+      k = kf.newkey(0x12345678, "test-key")
+      return k.attr
+    me.check_mapping(mkmap)
+
+    a = mkmap()
+    me.assertRaises(TypeError, a.update, { 3: 3, 4: 5 })
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()