--- /dev/null
+### -*-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()