t/: Add a test suite.
authorMark Wooding <mdw@distorted.org.uk>
Sat, 16 Nov 2019 18:53:24 +0000 (18:53 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Wed, 27 Nov 2019 15:10:44 +0000 (15:10 +0000)
It's fairly substantial, but far from complete.  It's also a little
strange in places because it's been sent backwards in time from the
future.

When building the Debian package, run the tests verbosely, so that if
the test crashes I stand a chance of figuring out where.

22 files changed:
.gitignore
MANIFEST.in
debian/rules
setup.py
t/keyring [new file with mode: 0644]
t/t-algorithms.py [new file with mode: 0644]
t/t-buffer.py [new file with mode: 0644]
t/t-bytes.py [new file with mode: 0644]
t/t-convert.py [new file with mode: 0644]
t/t-ec.py [new file with mode: 0644]
t/t-field.py [new file with mode: 0644]
t/t-group.py [new file with mode: 0644]
t/t-key.py [new file with mode: 0644]
t/t-misc.py [new file with mode: 0644]
t/t-mp.py [new file with mode: 0644]
t/t-passphrase.py [new file with mode: 0644]
t/t-pgen.py [new file with mode: 0644]
t/t-pubkey.py [new file with mode: 0644]
t/t-rand.py [new file with mode: 0644]
t/t-rat.py [new file with mode: 0644]
t/t-share.py [new file with mode: 0644]
t/testutils.py [new file with mode: 0644]

index 98bd1e8..a3d0fc5 100644 (file)
@@ -11,3 +11,4 @@ auto-version
 mdwsetup.py
 *.pyc
 pysetup.mk
+/t/keyring.old
index e9531eb..c09417b 100644 (file)
@@ -8,6 +8,7 @@ include MANIFEST.in setup.py Makefile
 
 ## C extension code.
 include *.c *.h
+include t/*.py t/keyring
 include algorithms.py
 exclude algorithms.h
 include pwsafe
index f718ebd..04d6f4e 100755 (executable)
@@ -3,5 +3,8 @@
 
 export PYTHONS := $(shell pyversions -r)
 
+override_dh_auto_test:
+       dh_auto_test -- OPTS-check=-V
+
 override_dh_auto_install:
        dh_auto_install -- prefix=/usr
index 1f45f69..80a1547 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -25,4 +25,9 @@ MS.setup(name = 'catacomb-python',
          packages = ['catacomb'],
          scripts = ['pwsafe'],
          genfiles = [MS.Generate('algorithms.h')],
+         unittest_dir = "t",
+         unittests = ["t-misc", "t-algorithms", "t-bytes", "t-buffer",
+                      "t-convert", "t-ec", "t-field", "t-group", "t-key",
+                      "t-mp", "t-passphrase", "t-pgen", "t-pubkey",
+                      "t-rand", "t-rat", "t-share"],
          ext_modules = [cat])
diff --git a/t/keyring b/t/keyring
new file mode 100644 (file)
index 0000000..65fcca4
--- /dev/null
+++ b/t/keyring
@@ -0,0 +1,4 @@
+60090be2:twofish encrypt,secret:w6Qmlm8zVc0Y6O4PwXfOrl4ru7+DTCrDD9pkbnh5xhsZc2CSRb3NKu+ihi9hykSkyXdSgn9G7E52xQUA/wyp4Nsp7X+Jg2X6bdvLag== forever forever -
+bd761d35:ec struct:[p=ec,public:0x42d95404921b2d8f19c761211753196901d7695049a96db2f0d7e00bd1dbf8ec,0x8a959136f7ca7e75718b9bc2d2ac3b0680daf4030288324816c8028cf8ebc8c4,private=encrypt,secret:/ddlDkc0+W5cfukYUHBxQoNNwxLmYnjRWYW+GnUwxmqdgv+8NTNIWN/GdahdsMT1GSnWKKGObd0Abz+tEiyJJa/hElI6JaxX/UirHahChpWbKTr/,curve=string,shared:nist%2dp256] forever forever -
+4a4e1ee7:ec-param struct:[curve=string,shared:nist%2dp256] forever forever -
+8599dbab:rsa:ron struct:[private=encrypt,secret:JER0qvxt53DtgAl03m+Rf4KggCkzT3YZ0JkrhJsuuvFVMYXK1gBjp4bk/zGMhWYjRKPVtNujiox3oSL933HwJLL7r0MVhUtZl+3zaS9DQ3c/lpGOMS8/VYDV84Ny1oU7IJtZakdTbRNd7YJapMq44SX5xamERBNEe1dslxVdLNVLn/Cc54fDNbhMVx/S3MrkuQaxF/wNFiL5nOezlbCVal5ln0RF7AXulIwnYkIeeC2P0h4oUfmhXWuiRAvk9bPFc5fMES1JruBk3YaQ7FzErIYV4vTMxotclwcAJvQDo5reUHQzAaaLXY80b3VlxhPQaKCcnZNcAJTDJRYh/OcSDt4AfVcTDqGToTXDfw/PL89KNI4R0vxF9Vqtyes4iPCwr6WJh4ZDBbT9r4WdIoAhO+2gFO1yHii3YbdAwPM4WrFE6caIkmgZHmkbc4v2rMz4DRGNOj1cegfz0xGTBFjvy/haTHy/tzV9qPb8N4VFgTvEroN7OwgS+l6GExlPH/JOff+hsOwcwfTUC0vJ/Cm8tOgL7guivADPL3ThGnqdt0/9S11MIoiYF51+nFH4JvcyJ7kjOI4EQbc50YcoGYVEhatUeUzOwMV4zxymlMGVUVZH509JC5ABCJLM4+qmklRT4G6K2kMHo9w32kJwBcAg5u2RR/2v0ALYWZ7FqAwjP40u3t2LrMeXStiAKs2YzkEqB/20kqlv1D+pasGVUUE3MvfHLm8JyrXjYG2erHs8BZRuXwz+DfZhmC9rZ6v7WaJFu2joIBcLMWouIU3WL5vpaNklDzBXfynZyPSwsgfmUUsi1xXWJPvUJH71IY5UAcHiWaaiBLIuJ3eCpqxR33LA6S8tJmnAC+VgwT9ATutVVW1CS++Q7mGdkgPWAJ3AkJL2lPMfousldvvknoS9Ybx1jJtpRyInFlgdjHoYpfe4DWXjoRDLQbkDemQNHHudog3n9GQ/kM1N/3xRcIxIBp0hzTFg+axMJU9pz+105/ymYQXIRkT9R3kglIa0l7G+uy760+kvh5VZp6wA4qmdqbvEQOl//pJ2hl52bntHd9w0Q61je8ByXSFxZeKqSkJkl1KSX8Z/JMiXWq1ea5enwWk5d9UX4aBgEtQdv2NCkirVSofrXIFvR3+VWfLwlPd2VcS3FAXU4LyjATiNZRp/0t0X81H/ozY6XzSXMmU8xs/FPnbVWL4fmXhspNbfK+uuib/Xd9lpzrxrNu+YbaNbtG97OIUwbtSakD+TbS3QTWvFwGNyIiuBm2t4R/M+vN1/qB+eTupGFmB/bFRv3H0h8fl4SaNSXqsZ6LTjWUQ+oQZ6Hu0Ri/qLtM62Dm3cou0yOASSMINDhScMOlxCcCONwpANefmMmx+HevmdGxSY8oKPMXysU7ZPFNYYPnqPhDg/4YW8Rga305ZwoWWV3kFcfeS5CxjOOtlsLDDsmDT5OYWCZczzseXS8Q2B5a8ZKJRmw7eAKW14yJmdk3HlLnBqGUAdtYlqXuEKR3c9QeIDJJsijEOxOyaTEL/Zp+mKQ9YRfmj/mPHbFJ1kJI9mkgosGdc9LUqCQpE5vsBCgmFljlD3Br7V651TtvnCeuOZGmlhuXz/79+0aEYfJ9/vjXA7S/W+AEdovbSGemATuuXIC792x6Mmy8hWt/U8YTB2kakbMWlJK/zWWFOn5gggc0c7n5D/BagZc6MwD6z8KmlLszgiBhBD904V66nzKQKoc5QJypK52SALhqfBQik8S5Ss4K/XEf7hgR5HNiZUNe3+Y07ru+zp3hejqgT8qJZg9YfZ6H/rkJ+41YdOf0UknyY8+Oeub0BQJquk3kLXWywCMtOYMaN391n/PP6T3GUVJlkhNk7LbrbjadwzLmB12CIgfI4AYDrCcR+/sLPTHOUXHne4F1xqEe1jb8/FWhxBOk+WqbBdgzr0nviHWWI=,n=integer,public:4588518152824847065809864017017980752300172722911691501294534688567191677622293877055275853619101138154946767929897875236974063360161364832318034181230762058282548891215880925776054212542819092223319882319288851559780722078087931927390589564076952023872334935403192026988691452623591714907960325366105508662452732895343767851573374920807255520857672360953324211494673508138823790217244758900285029386826393365768875455330392381958999387168679887837434942105961538360802941784841026202770984642852469173489052850527654973593434682838716079442275717555051916006906119658853435209224439445648915415615440379170113919377940230580258468751352164223236121841848896011526918868701912075848851443130268164520732142338177464314742042248866304723629029407667423868594096042267348555827683577338882853478253550493572887955844539064781262100314281062281504561302389580828642599153244753523052134755591791992951760425215644414834705106809,e=integer,public:65537] forever forever attr=value
diff --git a/t/t-algorithms.py b/t/t-algorithms.py
new file mode 100644 (file)
index 0000000..8e073f2
--- /dev/null
@@ -0,0 +1,576 @@
+### -*- mode: python, coding: utf-8 -*-
+###
+### Test symmetric algorithms
+###
+### (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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+### Utilities.
+
+def bad_key_size(ksz):
+  if isinstance(ksz, C.KeySZAny): return None
+  elif isinstance(ksz, C.KeySZRange):
+    if ksz.mod != 1: return ksz.min + 1
+    elif ksz.max != 0: return ksz.max + 1
+    elif ksz.min != 0: return ksz.min - 1
+    else: return None
+  elif isinstance(ksz, C.KeySZSet):
+    for sz in sorted(ksz.set):
+      if sz + 1 not in ksz.set: return sz + 1
+    assert False, "That should have worked."
+  else:
+    return None
+
+def different_key_size(ksz, sz):
+  if isinstance(ksz, C.KeySZAny): return sz + 1
+  elif isinstance(ksz, C.KeySZRange):
+    if sz > ksz.min: return sz - ksz.mod
+    elif ksz.max == 0 or sz < ksz.max: return sz + ksz.mod
+    else: return None
+  elif isinstance(ksz, C.KeySZSet):
+    for sz1 in sorted(ksz.set):
+      if sz != sz1: return sz1
+    return None
+  else:
+    return None
+
+class HashBufferTestMixin (U.TestCase):
+  """Mixin class for testing all of the various `hash...' methods."""
+
+  def check_hashbuffer_hashn(me, w, bigendp, makefn, hashfn):
+    """Check `hashuN'."""
+
+    ## Check encoding an integer.
+    h0, donefn0 = makefn(w + 2)
+    hashfn(h0.hashu8(0x00), T.bytes_as_int(w, bigendp)).hashu8(w + 1)
+    h1, donefn1 = makefn(w + 2)
+    h1.hash(T.span(w + 2))
+    me.assertEqual(donefn0(), donefn1())
+
+    ## Check overflow detection.
+    h0, _ = makefn(w)
+    me.assertRaises((OverflowError, ValueError),
+                    hashfn, h0, 1 << 8*w)
+
+  def check_hashbuffer_bufn(me, w, bigendp, makefn, hashfn):
+    """Check `hashbufN'."""
+
+    ## Go through a number of different sizes.
+    for n in [0, 1, 7, 8, 19, 255, 12345, 65535, 123456]:
+      if n >= 1 << 8*w: continue
+      h0, donefn0 = makefn(2 + w + n)
+      hashfn(h0.hashu8(0x00), T.span(n)).hashu8(0xff)
+      h1, donefn1 = makefn(2 + w + n)
+      h1.hash(T.prep_lenseq(w, n, bigendp, True))
+      me.assertEqual(donefn0(), donefn1())
+
+    ## Check blocks which are too large for the length prefix.
+    if w <= 3:
+      n = 1 << 8*w
+      h0, _ = makefn(w + n)
+      me.assertRaises((ValueError, OverflowError, TypeError),
+                      hashfn, h0, C.ByteString.zero(n))
+
+  def check_hashbuffer(me, makefn):
+    """Test the various `hash...' methods."""
+
+    ## Check `hashuN'.
+    me.check_hashbuffer_hashn(1, True, makefn, lambda h, n: h.hashu8(n))
+    me.check_hashbuffer_hashn(2, True, makefn, lambda h, n: h.hashu16(n))
+    me.check_hashbuffer_hashn(2, True, makefn, lambda h, n: h.hashu16b(n))
+    me.check_hashbuffer_hashn(2, False, makefn, lambda h, n: h.hashu16l(n))
+    if hasattr(makefn(0)[0], "hashu24"):
+      me.check_hashbuffer_hashn(3, True, makefn, lambda h, n: h.hashu24(n))
+      me.check_hashbuffer_hashn(3, True, makefn, lambda h, n: h.hashu24b(n))
+      me.check_hashbuffer_hashn(3, False, makefn, lambda h, n: h.hashu24l(n))
+    me.check_hashbuffer_hashn(4, True, makefn, lambda h, n: h.hashu32(n))
+    me.check_hashbuffer_hashn(4, True, makefn, lambda h, n: h.hashu32b(n))
+    me.check_hashbuffer_hashn(4, False, makefn, lambda h, n: h.hashu32l(n))
+    if hasattr(makefn(0)[0], "hashu64"):
+      me.check_hashbuffer_hashn(8, True, makefn, lambda h, n: h.hashu64(n))
+      me.check_hashbuffer_hashn(8, True, makefn, lambda h, n: h.hashu64b(n))
+      me.check_hashbuffer_hashn(8, False, makefn, lambda h, n: h.hashu64l(n))
+
+    ## Check `hashbufN'.
+    me.check_hashbuffer_bufn(1, True, makefn, lambda h, x: h.hashbuf8(x))
+    me.check_hashbuffer_bufn(2, True, makefn, lambda h, x: h.hashbuf16(x))
+    me.check_hashbuffer_bufn(2, True, makefn, lambda h, x: h.hashbuf16b(x))
+    me.check_hashbuffer_bufn(2, False, makefn, lambda h, x: h.hashbuf16l(x))
+    if hasattr(makefn(0)[0], "hashbuf24"):
+      me.check_hashbuffer_bufn(3, True, makefn, lambda h, x: h.hashbuf24(x))
+      me.check_hashbuffer_bufn(3, True, makefn, lambda h, x: h.hashbuf24b(x))
+      me.check_hashbuffer_bufn(3, False, makefn, lambda h, x: h.hashbuf24l(x))
+    me.check_hashbuffer_bufn(4, True, makefn, lambda h, x: h.hashbuf32(x))
+    me.check_hashbuffer_bufn(4, True, makefn, lambda h, x: h.hashbuf32b(x))
+    me.check_hashbuffer_bufn(4, False, makefn, lambda h, x: h.hashbuf32l(x))
+    if hasattr(makefn(0)[0], "hashbuf64"):
+      me.check_hashbuffer_bufn(8, True, makefn, lambda h, x: h.hashbuf64(x))
+      me.check_hashbuffer_bufn(8, True, makefn, lambda h, x: h.hashbuf64b(x))
+      me.check_hashbuffer_bufn(8, False, makefn, lambda h, x: h.hashbuf64l(x))
+
+###--------------------------------------------------------------------------
+class TestKeysize (U.TestCase):
+
+  def test_any(me):
+
+    ## A typical one-byte spec.
+    ksz = C.seal.keysz
+    me.assertEqual(type(ksz), C.KeySZAny)
+    me.assertEqual(ksz.default, 20)
+    me.assertEqual(ksz.min, 0)
+    me.assertEqual(ksz.max, 0)
+    for n in [0, 12, 20, 5000]:
+      me.assertTrue(ksz.check(n))
+      me.assertEqual(ksz.best(n), n)
+
+    ## A typical two-byte spec.  (No published algorithms actually /need/ a
+    ## two-byte key-size spec, but all of the HMAC variants use one anyway.)
+    ksz = C.sha256_hmac.keysz
+    me.assertEqual(type(ksz), C.KeySZAny)
+    me.assertEqual(ksz.default, 32)
+    me.assertEqual(ksz.min, 0)
+    me.assertEqual(ksz.max, 0)
+    for n in [0, 12, 20, 5000]:
+      me.assertTrue(ksz.check(n))
+      me.assertEqual(ksz.best(n), n)
+
+    ## Check construction.
+    ksz = C.KeySZAny(15)
+    me.assertEqual(ksz.default, 15)
+    me.assertEqual(ksz.min, 0)
+    me.assertEqual(ksz.max, 0)
+    me.assertRaises(ValueError, lambda: C.KeySZAny(-8))
+    me.assertEqual(C.KeySZAny(0).default, 0)
+
+  def test_set(me):
+    ## Note that no published algorithm uses a 16-bit `set' spec.
+
+    ## A typical spec.
+    ksz = C.salsa20.keysz
+    me.assertEqual(type(ksz), C.KeySZSet)
+    me.assertEqual(ksz.default, 32)
+    me.assertEqual(ksz.min, 10)
+    me.assertEqual(ksz.max, 32)
+    me.assertEqual(set(ksz.set), set([10, 16, 32]))
+    for x, best, pad in [(9, None, 10), (10, 10, 10), (11, 10, 16),
+                         (15, 10, 16), (16, 16, 16), (17, 16, 32),
+                         (31, 16, 32), (32, 32, 32), (33, 32, None)]:
+      if x == best == pad: me.assertTrue(ksz.check(x))
+      else: me.assertFalse(ksz.check(x))
+      if best is None: me.assertRaises(ValueError, ksz.best, x)
+      else: me.assertEqual(ksz.best(x), best)
+
+    ## Check construction.
+    ksz = C.KeySZSet(7)
+    me.assertEqual(ksz.default, 7)
+    me.assertEqual(set(ksz.set), set([7]))
+    me.assertEqual(ksz.min, 7)
+    me.assertEqual(ksz.max, 7)
+    ksz = C.KeySZSet(7, [3, 6, 9])
+    me.assertEqual(ksz.default, 7)
+    me.assertEqual(set(ksz.set), set([3, 6, 7, 9]))
+    me.assertEqual(ksz.min, 3)
+    me.assertEqual(ksz.max, 9)
+
+  def test_range(me):
+    ## Note that no published algorithm uses a 16-bit `range' spec, or an
+    ## unbounded `range'.
+
+    ## A typical spec.
+    ksz = C.rijndael.keysz
+    me.assertEqual(type(ksz), C.KeySZRange)
+    me.assertEqual(ksz.default, 32)
+    me.assertEqual(ksz.min, 4)
+    me.assertEqual(ksz.max, 32)
+    me.assertEqual(ksz.mod, 4)
+    for x, best in [(3, None), (4, 4), (5, 4),
+                    (15, 12), (16, 16), (17, 16),
+                    (31, 28), (32, 32), (33, 32)]:
+      if x == best: me.assertTrue(ksz.check(x))
+      else: me.assertFalse(ksz.check(x))
+      if best is None: me.assertRaises(ValueError, ksz.best, x)
+      else: me.assertEqual(ksz.best(x), best)
+
+    ## Check construction.
+    ksz = C.KeySZRange(28, 21, 35, 7)
+    me.assertEqual(ksz.default, 28)
+    me.assertEqual(ksz.min, 21)
+    me.assertEqual(ksz.max, 35)
+    me.assertEqual(ksz.mod, 7)
+    me.assertRaises(ValueError, C.KeySZRange, 29, 21, 35, 7)
+    me.assertRaises(ValueError, C.KeySZRange, 28, 20, 35, 7)
+    me.assertRaises(ValueError, C.KeySZRange, 28, 21, 34, 7)
+    me.assertRaises(ValueError, C.KeySZRange, 28, -7, 35, 7)
+    me.assertRaises(ValueError, C.KeySZRange, 28, 35, 21, 7)
+    me.assertRaises(ValueError, C.KeySZRange, 35, 21, 28, 7)
+    me.assertRaises(ValueError, C.KeySZRange, 21, 28, 35, 7)
+
+  def test_conversions(me):
+    me.assertEqual(C.KeySZ.fromec(256), 128)
+    me.assertEqual(C.KeySZ.fromschnorr(256), 128)
+    me.assertEqual(round(C.KeySZ.fromdl(2958.6875)), 128)
+    me.assertEqual(round(C.KeySZ.fromif(2958.6875)), 128)
+    me.assertEqual(C.KeySZ.toec(128), 256)
+    me.assertEqual(C.KeySZ.toschnorr(128), 256)
+    me.assertEqual(C.KeySZ.todl(128), 2958.6875)
+    me.assertEqual(C.KeySZ.toif(128), 2958.6875)
+
+###--------------------------------------------------------------------------
+class TestCipher (T.GenericTestMixin):
+  """Test basic symmetric ciphers."""
+
+  def _test_cipher(me, ccls):
+
+    ## Check the class properties.
+    me.assertEqual(type(ccls.name), str)
+    me.assertTrue(isinstance(ccls.keysz, C.KeySZ))
+    me.assertEqual(type(ccls.blksz), int)
+
+    ## Check round-tripping.
+    k = T.span(ccls.keysz.default)
+    iv = T.span(ccls.blksz)
+    m = T.span(253)
+    enc = ccls(k)
+    dec = ccls(k)
+    try: enc.setiv(iv)
+    except ValueError: can_setiv = False
+    else:
+      can_setiv = True
+      dec.setiv(iv)
+    c0 = enc.encrypt(m[0:57])
+    m0 = dec.decrypt(c0)
+    c1 = enc.encrypt(m[57:189])
+    m1 = dec.decrypt(c1)
+    try: enc.bdry()
+    except ValueError: can_bdry = False
+    else:
+      dec.bdry()
+      can_bdry = True
+    c2 = enc.encrypt(m[189:253])
+    m2 = dec.decrypt(c2)
+    me.assertEqual(len(c0) + len(c1) + len(c2), len(m))
+    me.assertEqual(m0, m[0:57])
+    me.assertEqual(m1, m[57:189])
+    me.assertEqual(m2, m[189:253])
+
+    ## Check the `enczero' and `deczero' methods.
+    c3 = enc.enczero(32)
+    me.assertEqual(dec.decrypt(c3), C.ByteString.zero(32))
+    m4 = dec.deczero(32)
+    me.assertEqual(enc.encrypt(m4), C.ByteString.zero(32))
+
+    ## Check that ciphers which support a `boundary' operation actually
+    ## need it.
+    if can_bdry:
+      dec = ccls(k)
+      if can_setiv: dec.setiv(iv)
+      m01 = dec.decrypt(c0 + c1)
+      me.assertEqual(m01, m[0:189])
+
+    ## Check that the boundary actually does something.
+    if can_bdry:
+      dec = ccls(k)
+      if can_setiv: dec.setiv(iv)
+      m012 = dec.decrypt(c0 + c1 + c2)
+      me.assertNotEqual(m012, m)
+
+    ## Check that bad key lengths are rejected.
+    badlen = bad_key_size(ccls.keysz)
+    if badlen is not None: me.assertRaises(ValueError, ccls, T.span(badlen))
+
+TestCipher.generate_testcases((name, C.gcciphers[name]) for name in
+  ["des-ecb", "rijndael-cbc", "twofish-cfb", "serpent-ofb",
+   "blowfish-counter", "rc4", "seal", "salsa20/8", "shake128-xof"])
+
+###--------------------------------------------------------------------------
+class BaseTestHash (HashBufferTestMixin):
+  """Base class for testing hash functions."""
+
+  def check_hash(me, hcls, need_bufsz = True):
+    """
+    Check hash class HCLS.
+
+    If NEED_BUFSZ is false, then don't insist that HCLS have working `bufsz',
+    `name', or `hashsz' attributes.  This test is mostly reused for MACs,
+    which don't have these attributes.
+    """
+    ## Check the class properties.
+    if need_bufsz:
+      me.assertEqual(type(hcls.name), str)
+      me.assertEqual(type(hcls.bufsz), int)
+      me.assertEqual(type(hcls.hashsz), int)
+
+    ## Set some initial values.
+    m = T.span(131)
+    h = hcls().hash(m).done()
+
+    ## Check that hash length comes out right.
+    if need_bufsz: me.assertEqual(len(h), hcls.hashsz)
+
+    ## Check that we get the same answer if we split the message up.
+    me.assertEqual(h, hcls().hash(m[0:73]).hash(m[73:131]).done())
+
+    ## Check the `check' method.
+    me.assertTrue(hcls().hash(m).check(h))
+    me.assertFalse(hcls().hash(m).check(h ^ len(h)*C.bytes("aa")))
+
+    ## Check the menagerie of random hashing methods.
+    def mkhash(_):
+      h = hcls()
+      return h, h.done
+    me.check_hashbuffer(mkhash)
+
+class TestHash (BaseTestHash, T.GenericTestMixin):
+  """Test hash functions."""
+  def _test_hash(me, hcls):    me.check_hash(hcls, need_bufsz = True)
+
+TestHash.generate_testcases((name, C.gchashes[name]) for name in
+  ["md5", "sha", "whirlpool", "sha256", "sha512/224", "sha3-384", "shake256",
+   "crc32"])
+
+###--------------------------------------------------------------------------
+class TestMessageAuthentication (BaseTestHash, T.GenericTestMixin):
+  """Test message authentication codes."""
+
+  def _test_mac(me, mcls):
+
+    ## Check the MAC properties.
+    me.assertEqual(type(mcls.name), str)
+    me.assertTrue(isinstance(mcls.keysz, C.KeySZ))
+    me.assertEqual(type(mcls.tagsz), int)
+
+    ## Test hashing.
+    k = T.span(mcls.keysz.default)
+    key = mcls(k)
+    me.check_hash(key, need_bufsz = False)
+
+    ## Check that bad key lengths are rejected.
+    badlen = bad_key_size(mcls.keysz)
+    if badlen is not None: me.assertRaises(ValueError, mcls, T.span(badlen))
+
+TestMessageAuthentication.generate_testcases \
+  ((name, C.gcmacs[name]) for name in
+   ["sha-hmac", "rijndael-cmac", "twofish-pmac1", "kmac128"])
+
+class TestPoly1305 (HashBufferTestMixin):
+  """Check the Poly1305 one-time message authentication function."""
+
+  def test_poly1305(me):
+
+    ## Check the MAC properties.
+    me.assertEqual(C.poly1305.name, "poly1305")
+    me.assertEqual(type(C.poly1305.keysz), C.KeySZSet)
+    me.assertEqual(C.poly1305.keysz.default, 16)
+    me.assertEqual(set(C.poly1305.keysz.set), set([16]))
+    me.assertEqual(C.poly1305.tagsz, 16)
+    me.assertEqual(C.poly1305.masksz, 16)
+
+    ## Set some initial values.
+    k = T.span(16)
+    u = T.span(64)[-16:]
+    m = T.span(149)
+    key = C.poly1305(k)
+    t = key(u).hash(m).done()
+
+    ## Check the key properties.
+    me.assertEqual(len(t), 16)
+
+    ## Check that we get the same answer if we split the message up.
+    me.assertEqual(t, key(u).hash(m[0:86]).hash(m[86:149]).done())
+
+    ## Check the `check' method.
+    me.assertTrue(key(u).hash(m).check(t))
+    me.assertFalse(key(u).hash(m).check(t ^ 16*C.bytes("cc")))
+
+    ## Check the menagerie of random hashing methods.
+    def mkhash(_):
+      h = key(u)
+      return h, h.done
+    me.check_hashbuffer(mkhash)
+
+    ## Check that we can't complete hashing without a mask.
+    me.assertRaises(ValueError, key().hash(m).done)
+
+    ## Check `concat'.
+    h0 = key().hash(m[0:96])
+    h1 = key().hash(m[96:117])
+    me.assertEqual(t, key(u).concat(h0, h1).hash(m[117:149]).done())
+    key1 = C.poly1305(k)
+    me.assertRaises(TypeError, key().concat, key1().hash(m[0:96]), h1)
+    me.assertRaises(TypeError, key().concat, h0, key1().hash(m[96:117]))
+    me.assertRaises(ValueError, key().concat, key().hash(m[0:93]), h1)
+
+###--------------------------------------------------------------------------
+class TestHLatin (U.TestCase):
+  """Test the `hsalsa20' and `hchacha20' functions."""
+
+  def test_hlatin(me):
+    kk = [T.span(sz) for sz in [32]]
+    n = T.span(16)
+    bad_k = T.span(18)
+    bad_n = T.span(13)
+    for fn in [C.hsalsa208_prf, C.hsalsa2012_prf, C.hsalsa20_prf,
+               C.hchacha8_prf, C.hchacha12_prf, C.hchacha20_prf]:
+      for k in kk:
+        h = fn(k, n)
+        me.assertEqual(len(h), 32)
+      me.assertRaises(ValueError, fn, bad_k, n)
+      me.assertRaises(ValueError, fn, k, bad_n)
+
+###--------------------------------------------------------------------------
+class TestKeccak (HashBufferTestMixin):
+  """Test the Keccak-p[1600, n] sponge function."""
+
+  def test_keccak(me):
+
+    ## Make a state and feed some stuff into it.
+    m0 = T.bin("some initial string")
+    m1 = T.bin("awesome follow-up string")
+    st0 = C.Keccak1600()
+    me.assertEqual(st0.nround, 24)
+    st0.mix(m0).step()
+
+    ## Make another step with a different round count.
+    st1 = C.Keccak1600(23)
+    st1.mix(m0).step()
+    me.assertNotEqual(st0.extract(32), st1.extract(32))
+
+    ## Check error conditions.
+    _ = st0.extract(200)
+    me.assertRaises(ValueError, st0.extract, 201)
+    st0.mix(T.span(200))
+    me.assertRaises(ValueError, st0.mix, T.span(201))
+
+  def check_shake(me, xcls, c, done_matches_xof = True):
+    """
+    Test the SHAKE and cSHAKE XOFs.
+
+    This is also used for testing KMAC, but that sets DONE_MATCHES_XOF false
+    to indicate that the XOF output is range-separated from the fixed-length
+    outputs (unlike the basic SHAKE functions).
+    """
+
+    ## Check the hash attributes.
+    x = xcls()
+    me.assertEqual(x.rate, 200 - c)
+    me.assertEqual(x.buffered, 0)
+    me.assertEqual(x.state, "absorb")
+
+    ## Set some initial values.
+    func = T.bin("TESTXOF")
+    perso = T.bin("catacomb-python test")
+    m = T.span(167)
+    h0 = xcls().hash(m).done(193)
+    me.assertEqual(len(h0), 193)
+    h1 = xcls(func = func, perso = perso).hash(m).done(193)
+    me.assertEqual(len(h1), 193)
+    me.assertNotEqual(h0, h1)
+
+    ## Check input and output in pieces, and the state machine.
+    if done_matches_xof: h = h0
+    else: h = xcls().hash(m).xof().get(len(h0))
+    x = xcls().hash(m[0:76]).hash(m[76:167]).xof()
+    me.assertEqual(h, x.get(98) + x.get(95))
+
+    ## Check masking.
+    x = xcls().hash(m).xof()
+    me.assertEqual(x.mask(m), C.ByteString(m) ^ C.ByteString(h[0:len(m)]))
+
+    ## Check the `check' method.
+    me.assertTrue(xcls().hash(m).check(h0))
+    me.assertFalse(xcls().hash(m).check(h1))
+
+    ## Check the menagerie of random hashing methods.
+    def mkhash(_):
+      x = xcls(func = func, perso = perso)
+      return x, lambda: x.done(100 - x.rate//2)
+    me.check_hashbuffer(mkhash)
+
+    ## Check the state machine tracking.
+    x = xcls(); me.assertEqual(x.state, "absorb")
+    x.hash(m); me.assertEqual(x.state, "absorb")
+    xx = x.copy()
+    h = xx.done(100 - x.rate//2)
+    me.assertEqual(xx.state, "dead")
+    me.assertRaises(ValueError, xx.done, 1)
+    me.assertRaises(ValueError, xx.get, 1)
+    me.assertEqual(x.state, "absorb")
+    me.assertRaises(ValueError, x.get, 1)
+    x.xof(); me.assertEqual(x.state, "squeeze")
+    me.assertRaises(ValueError, x.done, 1)
+    _ = x.get(1)
+    yy = x.copy(); me.assertEqual(yy.state, "squeeze")
+
+  def test_shake128(me): me.check_shake(C.Shake128, 32)
+  def test_shake256(me): me.check_shake(C.Shake256, 64)
+
+  def check_kmac(me, mcls, c):
+    k = T.span(32)
+    me.check_shake(lambda func = None, perso = T.bin(""):
+                     mcls(k, perso = perso),
+                   c, done_matches_xof = False)
+
+  def test_kmac128(me): me.check_kmac(C.KMAC128, 32)
+  def test_kmac256(me): me.check_kmac(C.KMAC256, 64)
+
+###--------------------------------------------------------------------------
+class TestPRP (T.GenericTestMixin):
+  """Test pseudorandom permutations (PRPs)."""
+
+  def _test_prp(me, pcls):
+
+    ## Check the PRP properties.
+    me.assertEqual(type(pcls.name), str)
+    me.assertTrue(isinstance(pcls.keysz, C.KeySZ))
+    me.assertEqual(type(pcls.blksz), int)
+
+    ## Check round-tripping.
+    k = T.span(pcls.keysz.default)
+    key = pcls(k)
+    m = T.span(pcls.blksz)
+    c = key.encrypt(m)
+    me.assertEqual(len(c), pcls.blksz)
+    me.assertEqual(m, key.decrypt(c))
+
+    ## Check that bad key lengths are rejected.
+    badlen = bad_key_size(pcls.keysz)
+    if badlen is not None: me.assertRaises(ValueError, pcls, T.span(badlen))
+
+    ## Check that bad blocks are rejected.
+    badblk = T.span(pcls.blksz + 1)
+    me.assertRaises(ValueError, key.encrypt, badblk)
+    me.assertRaises(ValueError, key.decrypt, badblk)
+
+TestPRP.generate_testcases((name, C.gcprps[name]) for name in
+  ["desx", "blowfish", "rijndael"])
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-buffer.py b/t/t-buffer.py
new file mode 100644 (file)
index 0000000..24823c6
--- /dev/null
@@ -0,0 +1,251 @@
+### -*- mode: python, coding: utf-8 -*-
+###
+### Test read and write buffers
+###
+### (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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestReadBuffer (U.TestCase):
+
+  def check_getn(me, w, bigendp, getfn):
+    """Check that `getuN' works."""
+    buf = C.ReadBuffer(T.span(w + 2))
+    me.assertEqual(buf.getu8(), 0x00)
+    me.assertEqual(getfn(buf), T.bytes_as_int(w, bigendp))
+    me.assertEqual(buf.getu8(), w + 1)
+    me.assertTrue(buf.endp)
+    me.assertRaises(C.BufferError, getfn, C.ReadBuffer(T.span(w - 1)))
+    me.assertEqual(getfn(C.ReadBuffer(w*C.bytes("00"))), 0)
+    me.assertEqual(getfn(C.ReadBuffer(w*C.bytes("ff"))), (1 << 8*w) - 1)
+
+  def check_getbufn(me, w, bigendp, blkfn, buffn):
+    """Check that `getblkN' and `getbufN' work."""
+
+    ## Run tests for several different data sizes.
+    for n in [0, 1, 7, 8, 19, 255, 12345, 65535, 123456]:
+
+      ## Make a sequence to parse.  If it's too large, then skip.
+      if n >= 1 << 8*w: continue
+      seq = T.prep_lenseq(w, n, bigendp, True)
+
+      ## Check `getblkN'.
+      buf = C.ReadBuffer(seq)
+      me.assertEqual(buf.getu8(), 0)
+      me.assertEqual(blkfn(buf), T.span(n))
+      me.assertEqual(buf.getu8(), 0xff)
+      me.assertTrue(buf.endp)
+
+      ## Check `getbufN'.  Delete the outside buffer early, to make sure that
+      ## the subbuffer keeps it alive.
+      buf = C.ReadBuffer(seq)
+      me.assertEqual(buf.getu8(), 0)
+      b = buffn(buf)
+      me.assertEqual(buf.getu8(), 0xff)
+      me.assertTrue(buf.endp)
+      del buf
+      me.assertEqual(b.offset, 0)
+      me.assertEqual(b.size, n)
+      if n > 0:
+        me.assertEqual(b.getu8(), 0x00)
+        b.offset = n - 1
+        me.assertEqual(b.getu8(), (n - 1)&0xff)
+      me.assertTrue(b.endp)
+
+      ## Test invalid lengths.  This is going to work by setting the top bit
+      ## of the length, so if it's already set, then that won't be any good.
+      if n >= 1 << 8*w - 1: continue
+      seq = T.prep_lenseq(w, n, bigendp, False)
+
+      ## Check `getblkN'.
+      me.assertRaises(C.BufferError, blkfn, C.ReadBuffer(T.span(w - 1)))
+      b = C.ReadBuffer(seq)
+      me.assertEqual(b.getu8(), 0)
+      me.assertRaises(C.BufferError, blkfn, b)
+
+      ## Check `getbufN'.
+      me.assertRaises(C.BufferError, buffn, C.ReadBuffer(T.span(w - 1)))
+      b = C.ReadBuffer(seq)
+      me.assertEqual(b.getu8(), 0)
+      me.assertRaises(C.BufferError, buffn, b)
+
+  def test_readbuffer(me):
+
+    ## Check `getuN'.
+    me.check_getn(1, True, lambda buf: buf.getu8())
+    me.check_getn(2, True, lambda buf: buf.getu16())
+    me.check_getn(2, True, lambda buf: buf.getu16b())
+    me.check_getn(2, False, lambda buf: buf.getu16l())
+    me.check_getn(3, True, lambda buf: buf.getu24())
+    me.check_getn(3, True, lambda buf: buf.getu24b())
+    me.check_getn(3, False, lambda buf: buf.getu24l())
+    me.check_getn(4, True, lambda buf: buf.getu32())
+    me.check_getn(4, True, lambda buf: buf.getu32b())
+    me.check_getn(4, False, lambda buf: buf.getu32l())
+    if hasattr(C.ReadBuffer, "getu64"):
+      me.check_getn(8, True, lambda buf: buf.getu64())
+      me.check_getn(8, True, lambda buf: buf.getu64b())
+      me.check_getn(8, False, lambda buf: buf.getu64l())
+
+    ## Check `getblkN' and `getbufN'.
+    me.check_getbufn(1, True,
+                     lambda buf: buf.getblk8(),
+                     lambda buf: buf.getbuf8())
+    me.check_getbufn(2, True,
+                     lambda buf: buf.getblk16(),
+                     lambda buf: buf.getbuf16())
+    me.check_getbufn(2, True,
+                     lambda buf: buf.getblk16b(),
+                     lambda buf: buf.getbuf16b())
+    me.check_getbufn(2, False,
+                     lambda buf: buf.getblk16l(),
+                     lambda buf: buf.getbuf16l())
+    me.check_getbufn(3, True,
+                     lambda buf: buf.getblk24(),
+                     lambda buf: buf.getbuf24())
+    me.check_getbufn(3, True,
+                     lambda buf: buf.getblk24b(),
+                     lambda buf: buf.getbuf24b())
+    me.check_getbufn(3, False,
+                     lambda buf: buf.getblk24l(),
+                     lambda buf: buf.getbuf24l())
+    me.check_getbufn(4, True,
+                     lambda buf: buf.getblk32(),
+                     lambda buf: buf.getbuf32())
+    me.check_getbufn(4, True,
+                     lambda buf: buf.getblk32b(),
+                     lambda buf: buf.getbuf32b())
+    me.check_getbufn(4, False,
+                     lambda buf: buf.getblk32l(),
+                     lambda buf: buf.getbuf32l())
+    if hasattr(C.ReadBuffer, "getu64"):
+      me.check_getbufn(8, True,
+                       lambda buf: buf.getblk64(),
+                       lambda buf: buf.getbuf64())
+      me.check_getbufn(8, True,
+                       lambda buf: buf.getblk64b(),
+                       lambda buf: buf.getbuf64b())
+      me.check_getbufn(8, False,
+                       lambda buf: buf.getblk64l(),
+                       lambda buf: buf.getbuf64l())
+
+    ## Check other `ReadBuffer' methods and properties.
+    buf = C.ReadBuffer(T.span(256))
+    me.assertEqual(buf.size, 256)
+    me.assertEqual(buf.left, 256)
+    me.assertEqual(buf.offset, 0)
+    buf.offset = 52
+    me.assertEqual(buf.left, 204)
+    buf.skip(7)
+    me.assertEqual(buf.offset, 59)
+    me.assertEqual(buf.left, 197)
+    me.assertRaises(C.BufferError, C.ReadBuffer(T.span(6)).skip, 7)
+    me.assertEqual(buf.get(5), C.bytes("3b3c3d3e3f"))
+    me.assertRaises(C.BufferError, C.ReadBuffer(T.span(4)).get, 5)
+
+###--------------------------------------------------------------------------
+class TestWriteBuffer (U.TestCase):
+
+  def check_putn(me, w, bigendp, putfn):
+    """Check `putuN'."""
+
+    ## Check encoding an integer.
+    buf = C.WriteBuffer()
+    buf.putu8(0x00)
+    putfn(buf, T.bytes_as_int(w, bigendp))
+    buf.putu8(w + 1)
+    me.assertEqual(C.ByteString(buf), T.span(w + 2))
+    me.assertEqual(C.ByteString(putfn(C.WriteBuffer(), (1 << 8*w) - 1)),
+                   w*C.bytes("ff"))
+    me.assertEqual(C.ByteString(putfn(C.WriteBuffer(), C.MP(0))),
+                   w*C.bytes("00"))
+
+    ## Check overflow detection.
+    me.assertRaises((OverflowError, ValueError),
+                    putfn, C.WriteBuffer(), 1 << 8*w)
+
+  def check_putbufn(me, w, bigendp, putfn):
+    """Check `putblkN'."""
+
+    ## Go through a number of different sizes.
+    for n in [0, 1, 7, 8, 19, 255, 12345, 65535, 123456]:
+      if n >= 1 << 8*w: continue
+      me.assertEqual(C.ByteString(putfn(C.WriteBuffer().putu8(0x00),
+                                        T.span(n)).putu8(0xff)),
+                     T.prep_lenseq(w, n, bigendp, True))
+
+    ## Check blocks which are too large for the length prefix.
+    if w <= 3:
+      me.assertRaises(ValueError, putfn,
+                      C.WriteBuffer(), C.ByteString.zero(1 << 8*w))
+
+  def test_writebuffer(me):
+
+    ## Check `putuN'.
+    me.check_putn(1, True, lambda buf, n: buf.putu8(n))
+    me.check_putn(2, True, lambda buf, n: buf.putu16(n))
+    me.check_putn(2, True, lambda buf, n: buf.putu16b(n))
+    me.check_putn(2, False, lambda buf, n: buf.putu16l(n))
+    me.check_putn(3, True, lambda buf, n: buf.putu24(n))
+    me.check_putn(3, True, lambda buf, n: buf.putu24b(n))
+    me.check_putn(3, False, lambda buf, n: buf.putu24l(n))
+    me.check_putn(4, True, lambda buf, n: buf.putu32(n))
+    me.check_putn(4, True, lambda buf, n: buf.putu32b(n))
+    me.check_putn(4, False, lambda buf, n: buf.putu32l(n))
+    if hasattr(C.WriteBuffer, "putu64"):
+      me.check_putn(8, True, lambda buf, n: buf.putu64(n))
+      me.check_putn(8, True, lambda buf, n: buf.putu64b(n))
+      me.check_putn(8, False, lambda buf, n: buf.putu64l(n))
+
+    ## Check `putblkN".
+    me.check_putbufn(1, True, lambda buf, x: buf.putblk8(x))
+    me.check_putbufn(2, True, lambda buf, x: buf.putblk16(x))
+    me.check_putbufn(2, True, lambda buf, x: buf.putblk16b(x))
+    me.check_putbufn(2, False, lambda buf, x: buf.putblk16l(x))
+    me.check_putbufn(3, True, lambda buf, x: buf.putblk24(x))
+    me.check_putbufn(3, True, lambda buf, x: buf.putblk24b(x))
+    me.check_putbufn(3, False, lambda buf, x: buf.putblk24l(x))
+    me.check_putbufn(4, True, lambda buf, x: buf.putblk32(x))
+    me.check_putbufn(4, True, lambda buf, x: buf.putblk32b(x))
+    me.check_putbufn(4, False, lambda buf, x: buf.putblk32l(x))
+    if hasattr(C.WriteBuffer, "putu64"):
+      me.check_putbufn(8, True, lambda buf, x: buf.putblk64(x))
+      me.check_putbufn(8, True, lambda buf, x: buf.putblk64b(x))
+      me.check_putbufn(8, False, lambda buf, x: buf.putblk64l(x))
+
+    ## Check other methods and properties.
+    buf = C.WriteBuffer()
+    buf.zero(17)
+    buf.put(T.span(23))
+    me.assertEqual(buf.size, 40)
+    me.assertEqual(C.ByteString(buf), C.ByteString.zero(17) + T.span(23))
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-bytes.py b/t/t-bytes.py
new file mode 100644 (file)
index 0000000..6755e96
--- /dev/null
@@ -0,0 +1,162 @@
+### -*- mode: python, coding: utf-8 -*-
+###
+### Test `ByteString'
+###
+### (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 sys as SYS
+import catacomb as C
+import unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestByteString (U.TestCase):
+
+  def test_create(me):
+
+    ## Create a string and make sure it looks right.
+    x = C.ByteString(T.bin("abcde"))
+    me.assertEqual(x, T.bin("abcde"))
+    me.assertEqual(x, C.bytes("6162636465"))
+    me.assertEqual(len(x), 5)
+
+  def test_index(me):
+
+    x = C.ByteString(T.bin("once upon a time there was a string"))
+
+    ## Check that simple indexing works.
+    me.assertEqual(x[3], 'e')
+    me.assertEqual(x[-5], 't')
+
+    ## Check out-of-range detection.
+    x[34]; me.assertRaises(IndexError, lambda: x[35])
+    x[-35]; me.assertRaises(IndexError, lambda: x[-36])
+
+    ## Check slicing.
+    me.assertEqual(x[7:17], T.bin("on a time "))
+
+    ## Complex slicing is also supported.
+    me.assertEqual(x[5:23:3], C.bytes("756e206d7472"))
+
+  def test_compare(me):
+    """
+    Test byte string comparison.
+
+    This is rather important, since we override it and many of the other
+    tests assume that comparison works.
+    """
+
+    def check(big, small):
+      """Check comparisons between BIG and SMALL strings."""
+
+      ## Equality.
+      me.assertTrue(big == big)
+      me.assertFalse(big == small)
+
+      ## Inequality.
+      me.assertFalse(big != big)
+      me.assertTrue(big != small)
+
+      ## Strict less-than.
+      me.assertFalse(big < big)
+      me.assertFalse(big < small)
+      me.assertTrue(small < big)
+
+      ## Non-strict less-than.
+      me.assertTrue(big <= big)
+      me.assertFalse(big <= small)
+      me.assertTrue(small <= big)
+
+      ## Non-strict greater-than.
+      me.assertTrue(big >= big)
+      me.assertTrue(big >= small)
+      me.assertFalse(small >= big)
+
+      ## Strict greater-than.
+      me.assertFalse(big > big)
+      me.assertTrue(big > small)
+      me.assertFalse(small > big)
+
+    ## Strings with equal length.
+    check(C.ByteString(T.bin("a string which is unlike the second")),
+          C.ByteString(T.bin("a string that is not like the first")))
+
+    ## A string and a prefix of it.
+    check(C.ByteString(T.bin("short strings order before longer ones")),
+          C.ByteString(T.bin("short string")))
+
+    ## The `ctstreq' function.
+    x = T.bin("special test string")
+    y = T.bin("my different string")
+    me.assertTrue(C.ctstreq(x, x))
+    me.assertFalse(C.ctstreq(x, y))
+
+  def test_operators(me):
+
+    ## Some example strings.
+    x = C.bytes("03a5fc")
+    y = C.bytes("5fac30")
+    z = C.bytes("00000000")
+
+    ## Operands of a binary operator must have equal lengths.
+    me.assertRaises(ValueError, lambda: x&z)
+    me.assertRaises(ValueError, lambda: x|z)
+    me.assertRaises(ValueError, lambda: x^z)
+
+    ## Bitwise AND.
+    me.assertEqual(type(x&y), C.ByteString)
+    me.assertEqual(x&y, C.bytes("03a430"))
+
+    ## Bitwise OR.
+    me.assertEqual(type(x | y), C.ByteString)
+    me.assertEqual(x | y, C.bytes("5fadfc"))
+
+    # Bitwise XOR.
+    me.assertEqual(type(x ^ y), C.ByteString)
+    me.assertEqual(x ^ y, C.bytes("5c09cc"))
+
+    # Bitwise NOT.
+    me.assertEqual(type(~x), C.ByteString)
+    me.assertEqual(~x, C.bytes("fc5a03"))
+
+    ## Concatenation.
+    me.assertEqual(x + y, C.bytes("03a5fc5fac30"))
+
+    ## Replication (asymmetric but commutative).
+    me.assertEqual(3*x, C.bytes("03a5fc03a5fc03a5fc"))
+    me.assertEqual(x*3, C.bytes("03a5fc03a5fc03a5fc"))
+
+    ## Replication by zero (regression test).
+    me.assertEqual(0*x, C.ByteString(T.bin("")))
+    me.assertEqual(x*0, C.ByteString(T.bin("")))
+
+  def test_zero(me):
+    me.assertEqual(C.ByteString.zero(7), T.bin(7*"\0"))
+    me.assertEqual(C.ByteString.zero(0), T.bin(""))
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-convert.py b/t/t-convert.py
new file mode 100644 (file)
index 0000000..08c660f
--- /dev/null
@@ -0,0 +1,114 @@
+### -*- mode: python, coding: utf-8 -*-
+###
+### Testing implicit conversions
+###
+### (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
+
+###--------------------------------------------------------------------------
+class TestConvert (U.TestCase):
+  """Test implicit conversions between various mathematical types."""
+
+  def test(me):
+
+    k = C.PrimeField(17)
+    E = k.ec(-3, 4)
+    P = E(1) # order 15
+    kk = C.BinPolyField(0x13)
+    EE = kk.ec(1, 2)
+    PP = E(9) # order 16
+
+    ## `MP' asymmetric operations.
+    me.assertEqual(pow(C.MP(5), 2), 25)
+    me.assertEqual(pow(5, C.MP(2)), 25)
+    me.assertEqual(pow(C.MP(5), 2, 7), 4)
+    me.assertEqual(pow(5, C.MP(2), 7), 4)
+    me.assertEqual(pow(5, 2, C.MP(7)), 4)
+    for bad in [lambda x: [x]]:
+      me.assertRaises(TypeError, pow, C.MP(5), bad(2))
+      me.assertRaises(TypeError, pow, C.MP(5), bad(2), 7)
+      if not T.DEBUGP:
+        ## Debug builds of Python 2 crash here, and it's not our fault.  Run
+        ##
+        ## $ python2.7-dbg -c 'pow(long(5), 2, [7])'
+        ##
+        ## to confirm.  The `[7]' causes coercion to occur.  The first and
+        ## second operands are coerced first, and successfully replaced by
+        ## the results: the first operand (in this case) is unchanged, but
+        ## has its refcount bumped, and the second operand is replaced by the
+        ## result of coercion, which now has a refcount of 1.  Then the first
+        ## and third operands are coerced, which fails.  Python decrements
+        ## the refcounts of the results of the first coercion, so the second
+        ## operand is now freed and, in debug builds, clobbered.  Python then
+        ## tries to format an error message, quoting the types of the
+        ## operands, but one of them has been lost.
+        me.assertRaises(TypeError, pow, C.MP(5), 2, bad(7))
+      me.assertRaises(TypeError, T.lsl, C.MP(5), bad(2))
+
+    ## `GF' asymmetric operations.
+    me.assertEqual(pow(C.GF(0x5), 2), C.GF(0x11))
+    me.assertEqual(pow(C.GF(0x5), C.MP(2)), C.GF(0x11))
+    me.assertEqual(pow(C.GF(5), 2, C.GF(0x13)), C.GF(0x2))
+    for bad in [lambda x: [x]]:
+      me.assertRaises(TypeError, pow, C.GF(5), bad(2))
+      me.assertRaises(TypeError, T.lsl, C.GF(5), bad(2))
+    for bad in [lambda x: [x]]:
+      me.assertRaises(TypeError, pow, bad(5), C.GF(2))
+      me.assertRaises(TypeError, pow, bad(5), C.GF(2), bad(7))
+      me.assertRaises(TypeError, pow, bad(5), bad(2), C.GF(7))
+      me.assertRaises(TypeError, pow, C.GF(5), bad(2), bad(7))
+      if not T.DEBUGP:
+        ## Python bug: see above.
+        me.assertRaises(TypeError, pow, C.GF(5), 2, bad(7))
+
+    ## `MP' and `GF'.
+    me.assertEqual(C.MP(5), 5)
+    me.assertEqual(5, C.MP(5))
+
+    me.assertEqual(C.MP(5) + 3, 8)
+    me.assertEqual(3 + C.MP(5), 8)
+    me.assertRaises(TypeError, T.add, C.MP(5), C.GF(3))
+    me.assertRaises(TypeError, T.add, C.GF(3), C.MP(5))
+
+    ## Field elements.
+    me.assertEqual(k(3) + 4, 7)
+    me.assertEqual(4 + k(3), 7)
+    me.assertEqual(k(3) + C.MP(4), 7)
+    me.assertEqual(C.MP(4) + k(3), 7)
+    me.assertEqual(k(3) + 4, 7)
+    me.assertEqual(C.GF(7) + kk(3), C.GF(4))
+    me.assertEqual(kk(3) + C.GF(7), C.GF(4))
+
+    me.assertRaises(TypeError, T.add, k(3), kk(3))
+    me.assertRaises(TypeError, T.add, kk(3), k(3))
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-ec.py b/t/t-ec.py
new file mode 100644 (file)
index 0000000..ef80b90
--- /dev/null
+++ b/t/t-ec.py
@@ -0,0 +1,250 @@
+### -*- mode: python, coding: utf-8 -*-
+###
+### Testing elliptic curve 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 unittest as U
+import testutils as T
+
+k = C.PrimeField(19)
+E = k.ec(-3, 6)
+P = E(0) # order 26
+
+kk = C.BinPolyField(0x13)
+EE = kk.ec(1, 8)
+PP = EE(8) # order 20
+
+###--------------------------------------------------------------------------
+class TestCurvelessPoints (U.TestCase):
+  """Test handling of points without an explicit curve."""
+
+  def test(me):
+
+    ## Construction.
+    O = C.ECPt(); me.assertFalse(O)
+    P = C.ECPt(12345, 67890); me.assertTrue(P)
+    Q = C.ECPt(23456, 78901); me.assertTrue(Q); me.assertNotEqual(P, Q)
+    R = C.ECPt(O); me.assertFalse(R); me.assertEqual(O, R); me.assertNotEqual(P, R)
+    R = C.ECPt(None); me.assertFalse(R); me.assertEqual(O, R)
+    me.assertEqual(C.ECPt("12345, 67890"), P)
+    me.assertEqual(C.ECPt((12345, 67890)), P)
+    me.assertRaises(TypeError, C.ECPt, ())
+    me.assertRaises(TypeError, C.ECPt, (1234,))
+    me.assertRaises(TypeError, C.ECPt, (1, 2, 3, 4))
+    me.assertRaises(ValueError, C.ECPt, "12345")
+    me.assertRaises(ValueError, C.ECPt, "12345,")
+    me.assertRaises(ValueError, C.ECPt, "12345, xyzzy")
+    me.assertRaises(TypeError, C.ECPt, (1, 2, 3))
+    me.assertRaises(TypeError, C.ECPt, 1, 2, 3)
+    me.assertRaises(TypeError, C.ECPt, 1234)
+    me.assertRaises(TypeError, C.ECPt, object())
+    me.assertRaises(TypeError, C.ECPt, 1, None)
+    #me.assertRaises(TypeError, C.ECPt, (1, None))
+
+    ## Arithmetic shouldn't work.
+    me.assertRaises(TypeError, T.neg, P)
+    me.assertRaises(TypeError, T.add, P, Q)
+
+    ## Attributes.  We only have raw integer access.
+    me.assertTrue(P.point is P)
+    me.assertEqual(P.ix, 12345)
+    me.assertEqual(P.iy, 67890)
+    me.assertEqual(P.tobuf(), C.bytes("000230390003010932"))
+    me.assertRaises(AttributeError, lambda: P.curve)
+    me.assertRaises(AttributeError, lambda: P.x)
+    me.assertRaises(AttributeError, lambda: P.y)
+    me.assertRaises(AttributeError, lambda: P._x)
+    me.assertRaises(AttributeError, lambda: P._y)
+    me.assertRaises(AttributeError, lambda: P._z)
+
+    ## Encoding and decoding.
+    P = C.ECPt(254, 291)
+    me.assertEqual(O.tobuf(), C.bytes("0000"))
+    me.assertEqual(C.ECPt(0, 0).tobuf(), C.bytes("000100000100"))
+    me.assertEqual(P.tobuf(), C.bytes("0001fe00020123"))
+    me.assertEqual(C.ECPt.frombuf(C.bytes("0001fe000201233f")),
+                   (P, C.bytes("3f")))
+    me.assertRaises(ValueError, C.ECPt.frombuf, C.bytes("0001fe000201"))
+
+    ## String conversion and parsing.
+    me.assertEqual(C.ECPt.parse("254, 291)"), (P, ")"))
+    me.assertRaises(ValueError, C.ECPt.parse, "(254, 291")
+
+###--------------------------------------------------------------------------
+class TestCurves (T.GenericTestMixin):
+  """Test elliptic curves."""
+
+  def test_compare(me):
+    me.assertEqual(E, E)
+    E1 = k.ec(-3, 6)
+    me.assertFalse(E is E1)
+    me.assertEqual(E, E1)
+    me.assertNotEqual(E, EE)
+    me.assertNotEqual(E, [])
+
+  def _test_curve(me, einfo, checkfail = False):
+
+    ## Some useful values.
+    E = einfo.curve
+    P = einfo.G
+    O = E()
+    n = einfo.r
+    h = einfo.h
+    k = E.field
+    me.assertTrue(n.primep()); l = C.NicePrimeField(n)
+
+    ## Check that things are basically sane.
+    me.assertFalse(O)
+    me.assertTrue(P)
+    me.assertTrue(n)
+    nP = n*P; me.assertFalse(nP); me.assertEqual(nP, O)
+
+    ## Check point construction.
+    me.assertEqual(type(P.ix), C.MP)
+    me.assertEqual(type(P.iy), C.MP)
+    me.assertTrue(isinstance(P.x, C.FE))
+    me.assertTrue(isinstance(P.y, C.FE))
+    me.assertTrue(isinstance(P._x, C.FE))
+    me.assertTrue(isinstance(P._y, C.FE))
+    if isinstance(E, C.ECPrimeProjCurve) or isinstance(E, C.ECBinProjCurve):
+      me.assertTrue(isinstance(P._z, C.FE))
+    else:
+      me.assertEqual(P._z, None)
+    me.assertEqual(E(None), O)
+    me.assertEqual(E(P.x, P.y), P)
+    me.assertEqual(E((P.x, P.y)), P)
+    me.assertEqual(E(P._x, P._y, P._z), P)
+    me.assertEqual(E((P._x, P._y, P._z)), P)
+    Q = E(P.point); me.assertEqual(type(Q), E); me.assertEqual(Q, P)
+    me.assertEqual(E("%s, %s" % (P.ix, P.iy)), P)
+    me.assertRaises(ValueError, E, "1234")
+    me.assertRaises(ValueError, E, "1234,")
+    me.assertRaises(TypeError, E, 1, None)
+    Q = E(P.ix); me.assertTrue(Q == P or Q == -P)
+    for i in T.range(128):
+      x = P.ix + i
+      try: E(x)
+      except ValueError: badx = x; break
+    else:
+      me.fail("no off-curve point found")
+
+    ## Attributes.
+    me.assertEqual(P.ix, P.point.ix)
+    me.assertEqual(P.iy, P.point.iy)
+    me.assertEqual(P.x, k(P.point.ix))
+    me.assertEqual(P.y, k(P.point.iy))
+    R = 6*P
+    if isinstance(E, C.ECPrimeProjCurve) or isinstance(E, C.ECBinProjCurve):
+      me.assertEqual(P._z, k.one)
+      me.assertEqual(R._x, R.x*R._z**2)
+      me.assertEqual(R._y, R.y*R._z**3)
+      me.assertNotEqual(R._z, k.one)
+    else:
+      me.assertEqual(P._z, None)
+      me.assertEqual(R._x, R.x)
+      me.assertEqual(R._y, R.y)
+      me.assertEqual(R._z, None)
+    me.assertEqual(R.curve, E)
+
+    ## Arithmetic.
+    Q = 17*P
+    me.assertEqual(Q, P*17)
+    me.assertEqual(-Q, (n - 17)*P)
+    me.assertEqual(Q + R, 23*P)
+    me.assertEqual(Q + R.point, 23*P)
+    me.assertRaises(TypeError, T.add, Q.point, R.point)
+    me.assertEqual(Q - R, 11*P)
+    me.assertEqual(P*l(17), Q)
+
+    ## Ordering.
+    me.assertTrue(P == P)
+    me.assertTrue(P != Q)
+    me.assertRaises(TypeError, T.lt, P, Q)
+    me.assertRaises(TypeError, T.le, P, Q)
+    me.assertRaises(TypeError, T.ge, P, Q)
+    me.assertRaises(TypeError, T.gt, P, Q)
+
+    ## Encoding.
+    Z0 = C.ByteString.zero(0)
+    Z1 = C.ByteString.zero(1)
+    me.assertEqual(O.ec2osp(), Z1)
+    me.assertEqual(E.os2ecp(Z1), (O, Z0))
+    t = C.ByteString(C.WriteBuffer()
+                       .putu8(0x04)
+                       .put(P.ix.storeb(k.noctets))
+                       .put(P.iy.storeb(k.noctets)))
+    me.assertEqual(P.ec2osp(), t)
+    me.assertEqual(C.ByteString(C.WriteBuffer().putecptraw(P)), t)
+    me.assertEqual(E.os2ecp(t), (P, Z0))
+    me.assertEqual(C.ReadBuffer(t).getecptraw(E), P)
+    if isinstance(k, C.PrimeField): ybit = int(P.iy&1)
+    else:
+      try: ybit = int((P.y/P.x).value&C.GF(1))
+      except ZeroDivisionError: ybit = 0
+    t = C.ByteString(C.WriteBuffer()
+                       .putu8(0x02 | ybit)
+                       .put(P.ix.storeb(k.noctets)))
+    me.assertEqual(P.ec2osp(C.EC_LSB), t)
+    me.assertEqual(E.os2ecp(t, C.EC_LSB), (P, Z0))
+
+    ## Curve methods.
+    Q = E.find(P.x); me.assertTrue(Q == P or Q == -P)
+    Q = E.find(P.ix); me.assertTrue(Q == P or Q == -P)
+    me.assertRaises(ValueError, E.find, badx)
+    for i in T.range(128):
+      if E.rand() != P: break
+    else:
+      me.fail("random point always gives me P")
+    for i in T.range(128):
+      R = E.rand(C.LCRand(i))
+      if R != P: break
+    else:
+      me.fail("random point always gives me P")
+    me.assertEqual(R, E.rand(C.LCRand(i)))
+    me.assertEqual(E.parse("%s, %s!xyzzy" % (P.ix, P.iy)), (P, "!xyzzy"))
+
+    ## Simultaneous multiplication.
+    Q, R, S = 5*P, 7*P, 11*P
+    me.assertEqual(E.mmul([Q, 9, R, 8, S, 5]), 156*P)
+    me.assertEqual(E.mmul(Q, 9, R, 8, S, 5), 156*P)
+
+    ## Test other curve info things while we're here.
+    if not checkfail: einfo.check()
+    else: me.assertRaises(ValueError, einfo.check)
+
+  def test_tinycurves(me):
+    me._test_curve(C.ECInfo(E, 2*P, 13, 2), checkfail = True)
+    ei, _ = C.ECInfo.parse("binpoly: 0x13; bin: 0x01, 0x08; 0x02, 0x0c: 5*4")
+    me._test_curve(ei, checkfail = True)
+
+TestCurves.generate_testcases((name, C.eccurves[name]) for name in
+  ["nist-p256", "nist-k233", "nist-b163", "nist-b283n"])
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-field.py b/t/t-field.py
new file mode 100644 (file)
index 0000000..3871b45
--- /dev/null
@@ -0,0 +1,150 @@
+### -*-python-*-
+###
+### Testing finite-field 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 itertools as I
+
+import catacomb as C
+import unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestFields (T.GenericTestMixin):
+  """Test finite fields."""
+
+  def _test_field(me, k):
+
+    ## Some initial values.
+    zero = k.zero
+    one = k.one
+    x = k(2760820866)
+    y = k(3757175895)
+    z = k(920571320)
+
+    ## Check that they're all different.
+    v = [zero, one, x, y, z]
+    for i in T.range(len(v)):
+      for j in T.range(len(v)):
+        if i == j: me.assertEqual(v[i], v[j])
+        else: me.assertNotEqual(v[i], v[j])
+
+    ## Basic arithmetic.  Knowing the answers is too hard.  For now, just
+    ## check that the field laws hold.
+    t = x + y; me.assertEqual(t - y, x); me.assertEqual(t - x, y)
+    t = x - y; me.assertEqual(t + y, x)
+    t = x*y; me.assertEqual(t/x, y); me.assertEqual(t/y, x)
+    me.assertEqual(+x, x)
+    me.assertEqual(x + -x, zero)
+    me.assertEqual(x - x, zero)
+    me.assertEqual(x*x.inv(), one)
+    me.assertEqual(x/x, one)
+    me.assertRaises(ZeroDivisionError, k.inv, zero)
+
+    ## Exponentiation.  At least we know the group exponent.
+    me.assertEqual(x**(k.q - 1), one)
+
+    ## Comparisons.  We've already done equality and inequality, and nothing
+    ## else should work.
+    me.assertRaises(TypeError, T.lt, x, y)
+    me.assertRaises(TypeError, T.le, x, y)
+    me.assertRaises(TypeError, T.ge, x, y)
+    me.assertRaises(TypeError, T.gt, x, y)
+
+    ## Conversion back to integer.
+    me.assertEqual(int(x), 2760820866)
+
+    ## Square and square-root.  In a prime field, we may need to search
+    ## around to find a quadratic residue.  In binary fields, squaring is
+    ## linear, and every element has a unique square root.
+    me.assertEqual(x*x, x.sqr())
+    for i in T.range(128):
+      t = k(int(x) + i)
+      q = t.sqrt()
+      if q is not None: break
+    else:
+      me.fail("no quadratic residue found")
+    me.assertEqual(q.sqr(), t)
+
+    ## Hex and octal conversions.
+    me.assertEqual(hex(x), hex(T.long(x.value)).rstrip("L"))
+    me.assertEqual(oct(x), oct(T.long(x.value)).rstrip("L"))
+
+    if isinstance(k, C.PrimeField):
+
+      ## Representation.
+      me.assertEqual(type(x.value), C.MP)
+      me.assertEqual(k.type, C.FTY_PRIME)
+
+      ## Properties.
+      me.assertEqual(k.p, k.q)
+
+      ## Operations.
+      me.assertEqual(x.dbl(), 2*x)
+      me.assertEqual(x.tpl(), 3*x)
+      me.assertEqual(x.qdl(), 4*x)
+      me.assertEqual(x.hlv(), x/2)
+
+    else:
+
+      ## Representation.
+      me.assertEqual(type(x.value), C.GF)
+      me.assertEqual(k.type, C.FTY_BINARY)
+
+      ## Properties.
+      me.assertEqual(k.m, k.nbits)
+      me.assertEqual(k.p.degree, k.m)
+      if isinstance(k, C.BinNormField):
+        l = C.BinPolyField(k.p)
+        a, b = l.zero, l(k.beta)
+        for i in T.range(k.m):
+          a += b
+          b = b.sqr()
+        me.assertEqual(a, l.one)
+
+      ## Operations.
+      for i in T.range(128):
+        t = k(int(x) + i)
+        u = t.quadsolve()
+        if u is not None: break
+      else:
+        me.fail("no quadratic solution found")
+      me.assertEqual(u*u + u, t)
+
+    ## Encoding.
+    me.assertEqual(k.nbits, (k.q - 1).nbits)
+    me.assertEqual(k.noctets, (k.q - 1).noctets)
+
+TestFields.generate_testcases \
+  (("%s-%s" % (name, suffix), getfn(C.eccurves[name]))
+   for suffix, getfn in [("coords", lambda einfo: einfo.curve.field),
+                         ("scalars", lambda einfo: C.PrimeField(einfo.r))]
+   for name in ["nist-p256", "nist-k233", "nist-b163", "nist-b283n"])
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-group.py b/t/t-group.py
new file mode 100644 (file)
index 0000000..203ca16
--- /dev/null
@@ -0,0 +1,255 @@
+### -*-python-*-
+###
+### Testing cyclic-group 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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestGroups (T.GenericTestMixin):
+  """Test cyclic groups."""
+
+  def _test_group(me, G):
+
+    ## Some initial values.
+    id = G.i
+    g = G.g
+    x = g**2014319630
+    y = g**721326623
+
+    ## Check that they're all different.
+    v = [id, g, x, y]
+    for i in T.range(len(v)):
+      for j in T.range(len(v)):
+        if i == j: me.assertEqual(v[i], v[j])
+        else: me.assertNotEqual(v[i], v[j])
+
+    ## Basic arithmetic.  Knowing the answers is too hard.  For now, just
+    ## check that the field laws hold.
+    t = x*y; me.assertEqual(t/x, y); me.assertEqual(t/y, x)
+    me.assertEqual(x*x.inv(), id)
+    me.assertEqual(x.sqr(), x*x)
+    me.assertEqual(x/x, id)
+
+    ## Exponentiation.
+    me.assertEqual(x**G.r, id)
+    me.assertEqual(G.mexp((x, 5), (y, 7)), g**15120884511)
+
+    ## Comparisons.  We've already done equality and inequality, and nothing
+    ## else should work.
+    me.assertRaises(TypeError, T.lt, x, y)
+    me.assertRaises(TypeError, T.le, x, y)
+    me.assertRaises(TypeError, T.ge, x, y)
+    me.assertRaises(TypeError, T.gt, x, y)
+
+    if isinstance(G, C.ECGroup):
+
+      ## Properties.
+      me.assertEqual(G.noctets, 2*G.info.curve.field.noctets + 1)
+      me.assertEqual(G.nbits, 2*G.info.curve.field.nbits)
+
+      ## Conversion to integer.
+      i = y.toint(); me.assertEqual(type(i), C.MP)
+      t = G(i); me.assertTrue(t == y or t == y.inv())
+
+      ## Conversion to elliptic curve.
+      Q = y.toec()
+      me.assertTrue(isinstance(Q, C.ECPtCurve))
+      me.assertEqual(G(Q), y)
+
+      ## Encoding.
+      t = y.tobuf()
+      me.assertEqual(t, Q.tobuf())
+      me.assertEqual(G.frombuf(t)[0], y)
+      me.assertEqual(id.tobuf(), C.ByteString.zero(2))
+      t = y.toraw()
+      me.assertEqual(t, Q.ec2osp())
+      me.assertEqual(G.fromraw(t)[0], y)
+      me.assertEqual(id.toraw(), C.ByteString.zero(1))
+
+    else:
+
+      ## Properties.
+      me.assertEqual(G.noctets, G.info.p.noctets)
+      if isinstance(G.info, C.BinDHInfo):
+        me.assertEqual(G.nbits, G.info.p.degree)
+      else:
+        me.assertEqual(G.nbits, G.info.p.nbits)
+
+      ## Conversion to integer.
+      i = y.toint(); me.assertEqual(type(i), C.MP)
+      me.assertTrue(G(i) == y)
+
+      ## Conversion to elliptic curve.
+      me.assertRaises(TypeError, G.toec, x)
+
+      ## Encoding.
+      t = y.tobuf()
+      me.assertEqual(t, C.ByteString(C.WriteBuffer().putmp(i)))
+      me.assertEqual(G.frombuf(t)[0], y)
+      me.assertEqual(id.tobuf(), C.bytes("000101"))
+      t = y.toraw()
+      me.assertEqual(t, i.storeb(G.noctets))
+      me.assertEqual(G.fromraw(t)[0], y)
+      me.assertEqual(id.toraw(),
+                     C.ByteString(C.WriteBuffer().zero(G.noctets - 1).putu8(1)))
+
+    ## String conversion.
+    ystr = str(y)
+    me.assertEqual(G.fromstring(ystr), (y, ""))
+
+    ## Checking operations.
+    me.assertRaises(ValueError, id.check)
+    x.check()
+    y.check()
+    G.checkgroup()
+
+  def test_dhinfo(me):
+    dhinfo = C.DHInfo.parse("127, 7, 2")
+    me.assertEqual(dhinfo.p, 127)
+    me.assertEqual(dhinfo.r, 7)
+    me.assertEqual(dhinfo.g, 2)
+    dhinfo.group().checkgroup()
+
+  def test_bindhinfo(me):
+    bindhinfo = C.BinDHInfo.parse("0x805, 89, 0x22")
+    me.assertEqual(bindhinfo.p, C.GF(0x805))
+    me.assertEqual(bindhinfo.r, 89)
+    me.assertEqual(bindhinfo.g, C.GF(0x22))
+    bindhinfo.group().checkgroup()
+
+  def test_parse(me):
+
+    G = C.Group.parse("prime 127, 7, 2")
+    me.assertEqual(G.info.p, 127)
+    me.assertEqual(G.r, 7)
+    me.assertEqual(G.info.g, 2)
+    G.checkgroup()
+
+    G = C.Group.parse("bin 0x805, 89, 0x22")
+    me.assertEqual(G.info.p, C.GF(0x805))
+    me.assertEqual(G.r, 89)
+    me.assertEqual(G.info.g, C.GF(0x22))
+    G.checkgroup()
+
+  def test_gen_schnorr(me):
+    ev = T.EventRecorder()
+    dhinfo = C.DHInfo.generate(512, 64, event = ev,
+                               rng = T.detrand("schnorr"))
+    me.assertEqual(dhinfo.p.nbits, 512)
+    me.assertEqual(dhinfo.r.nbits, 64)
+    me.assertTrue(dhinfo.p.primep())
+    me.assertTrue(dhinfo.r.primep())
+    me.assertEqual(dhinfo.p%dhinfo.r, 1)
+    me._test_group(dhinfo.group())
+    me.assertEqual(ev.events, "[q:F4/P26/D][p:F5/P5/D][g:D]")
+
+  def test_gen_limlee(me):
+    ev = T.EventRecorder()
+    dhinfo, ff = C.DHInfo.genlimlee(512, 64, event = ev, ievent = ev,
+                                    rng = T.detrand("limlee"))
+    me.assertEqual(dhinfo.p.nbits, 512)
+    me.assertEqual(dhinfo.r.nbits, 64)
+    me.assertTrue(dhinfo.p.primep())
+    me.assertTrue(dhinfo.r.primep())
+    for f in ff:
+      me.assertTrue(f.primep())
+      me.assertTrue(f.nbits >= 64)
+    me.assertEqual(C.MPMul().factor(2).factor(ff).done() + 1, dhinfo.p)
+    me._test_group(dhinfo.group())
+    me.assertEqual(ev.events,
+                   "[p:"
+                   "[p_0:F8/P26/D]"
+                   "[p_1:P26/D]"
+                   "[p_2:F4/P26/D]"
+                   "[p_3:P26/D]"
+                   "[p_4:F1/P26/D]"
+                   "[p_5:F1/P26/D]"
+                   "[p_6:P26/D]"
+                   "[p*_7:P26/D]"
+                   "[p_8:F1/P26/D]"
+                   "[p_9:F1/P26/D]"
+                   "[p*_10:P26/D]"
+                   "[p_11:F6/P26/D]"
+                   "[p_12:P26/D]"
+                   "[p_13:P26/D]"
+                   "[p_14:F4/P26/D]"
+                   "[p_15:F1/P26/D]"
+                   "[p_16:F1/P26/D]"
+                   "[p_17:F1/P26/D]"
+                   "[p_18:F6/P26/D]"
+                   "[p_19:F1/P26/D]"
+                   "[p_20:F3/P26/D]"
+                   "[p_21:P26/D]"
+                   "[p_22:F2/P26/D]"
+                   "[p_23:F4/P26/D]"
+                   "[p_24:F7/P26/D]"
+                   "[p_25:F2/P26/D]"
+                   "[p_26:F9/P26/D]"
+                   "[p_27:F4/P26/D]"
+                   "[p*_28:F11/P26/D]"
+                   "[p*_29:F4/P26/D]"
+                   "[p*_30:F1/P26/D]"
+                   "[p*_31:F6/P26/D]"
+                   "[p*_32:F4/P26/D]"
+                   "[p*_33:F3/P26/D]"
+                   "[p*_34:P26/D]"
+                   "[p*_35:F3/P26/D]"
+                   "[p*_36:F1/P26/D]"
+                   "[p*_37:F1/P26/D]"
+                   "[p*_38:F4/P26/D]"
+                   "[p*_39:P26/D]"
+                   "[p*_40:P26/D]"
+                   "[p*_41:F2/P26/D]"
+                   "[p*_42:F1/P26/D]"
+                   "F22/P5/D]"
+                   "[g:D]")
+
+  def test_gen_kcdsa(me):
+    ev = T.EventRecorder()
+    dhinfo, h = C.DHInfo.genkcdsa(512, 64, event = ev,
+                                  rng = T.detrand("kcdsa"))
+    me.assertEqual(dhinfo.p.nbits, 512)
+    me.assertEqual(dhinfo.r.nbits, 64)
+    me.assertTrue(dhinfo.p.primep())
+    me.assertTrue(dhinfo.r.primep())
+    me.assertTrue(h.primep())
+    me.assertEqual(2*h*dhinfo.r + 1, dhinfo.p)
+    me._test_group(dhinfo.group())
+    me.assertEqual(ev.events, "[v:F23/P6/D][p:F86/P26/D][g:D]")
+
+TestGroups.generate_testcases((name, map[name].group()) for name, map in
+  [("nist-p256", C.eccurves),
+   ("nist-b283", C.eccurves),
+   ("catacomb-ll-128-512", C.primegroups),
+   ("p1363-64", C.bingroups)])
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
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()
diff --git a/t/t-misc.py b/t/t-misc.py
new file mode 100644 (file)
index 0000000..18a3170
--- /dev/null
@@ -0,0 +1,41 @@
+### -*- mode: python, coding: utf-8 -*-
+###
+### Miscellaneous tests
+###
+### (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 unittest as U
+
+###--------------------------------------------------------------------------
+class Tests (U.TestCase):
+
+  def test_path(me):
+    me.assertTrue(C.__file__.startswith("build"))
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-mp.py b/t/t-mp.py
new file mode 100644 (file)
index 0000000..ff373ca
--- /dev/null
+++ b/t/t-mp.py
@@ -0,0 +1,561 @@
+### -*-python-*-
+###
+### Testing multiprecision integer (and related) 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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestMP (U.TestCase):
+
+  def test_make(me):
+    x = C.MP(5)
+    k = C.PrimeField(17)
+    kk = C.BinPolyField(C.GF(0x13))
+    E = k.ec(-3, 1)
+    me.assertEqual(x, 5)
+    me.assertTrue(C.MP(x) is x)
+    me.assertEqual(C.MP(k(8)), 8)
+    me.assertEqual(C.MP(kk(8)), 8)
+    me.assertEqual(C.MP(E(1, 4)), 1)
+    me.assertRaises(TypeError, C.MP, E())
+
+    me.assertEqual(int(x), 5)
+    big = 6556380541834372447694561492436749633
+    me.assertEqual(type(big), T.long)
+    y = C.MP(big)
+    me.assertEqual(y, big)
+    me.assertEqual(int(y), big)
+
+    me.assertEqual(C.MP(str(big)), big)
+    me.assertEqual(C.MP('0x4eeb684a0954ec4ceb255e3e9778d41'), big)
+    me.assertEqual(C.MP('4eeb684a0954ec4ceb255e3e9778d41', 16), big)
+    me.assertEqual(C.MP('0b0', 16), 176) # not 0
+
+    me.assertEqual(C.MP('047353320450112516611472622536175135706501'), big)
+    me.assertEqual(C.MP('0o47353320450112516611472622536175135706501'), big)
+    me.assertEqual(C.MP('047353320450112516611472622536175135706501', 8), big)
+    me.assertEqual(C.MP('47353320450112516611472622536175135706501', 8), big)
+
+    me.assertEqual(C.MP('0b100111011011001100000010001011'), 661438603)
+    me.assertEqual(C.MP('100111011011001100000010001011', 2), 661438603)
+
+  def test_string(me):
+    y = C.MP(6556380541834372447694561492436749633)
+    me.assertEqual(str(y), '6556380541834372447694561492436749633')
+    me.assertEqual(repr(y), 'MP(6556380541834372447694561492436749633L)')
+    me.assertEqual(hex(y), '0x4eeb684a0954ec4ceb255e3e9778d41')
+    me.assertEqual(oct(y), '047353320450112516611472622536175135706501')
+
+  def test_number(me):
+    x, y, m, zero = C.MP(169), C.MP(24), C.MP(205), C.MP(0)
+
+    me.assertEqual(-x, -169)
+    me.assertEqual(~x, -170)
+    me.assertEqual(abs(x), 169)
+    me.assertEqual(abs(-x), 169)
+
+    me.assertEqual(x + y, 193)
+    me.assertEqual(x - y, 145)
+    me.assertEqual(x*y, 4056)
+    me.assertEqual(x&y, 8)
+    me.assertEqual(x&-y, 168)
+    me.assertEqual(x | y, 185)
+    me.assertEqual(x | -y, -23)
+    me.assertEqual(x ^ y, 177)
+    me.assertEqual(x ^ -y, -191)
+
+    me.assertEqual(x << 3, 1352)
+    me.assertEqual(x << -2, 42)
+    me.assertEqual(x >> 2, 42)
+    me.assertEqual(x >> -3, 1352)
+    me.assertEqual(-x << 3, -1352)
+    me.assertEqual(-x >> 2, -43)
+
+    u = x/y; me.assertEqual((u.numer, u.denom), (169, 24))
+    me.assertEqual(x//y, 7)
+    me.assertEqual(x%y, 1)
+    me.assertEqual(divmod(x, y), (7, 1))
+    me.assertRaises(ZeroDivisionError, lambda: x/zero)
+    me.assertRaises(ZeroDivisionError, lambda: x//zero)
+    me.assertRaises(ZeroDivisionError, lambda: x%zero)
+    me.assertRaises(ZeroDivisionError, divmod, x, zero)
+
+    me.assertEqual(pow(x, y), 294632676319010105335586872991323185304149065116720321)
+    me.assertEqual(pow(x, y, m), 51)
+    me.assertRaises(ValueError, pow, x, -y)
+    me.assertEqual(pow(x, -y, m), 201)
+    me.assertRaises(ZeroDivisionError, pow, x, -y, 208)
+
+    me.assertTrue(x)
+    me.assertFalse(zero)
+
+  def test_order(me):
+    x, y = C.MP(169), C.MP(24)
+    me.assertTrue(x == x)
+    me.assertFalse(x != x)
+    me.assertFalse(x == y)
+    me.assertTrue(x != y)
+    me.assertTrue(x > y)
+    me.assertFalse(y > x)
+    me.assertFalse(x > x)
+    me.assertTrue(x >= y)
+    me.assertFalse(y >= x)
+    me.assertTrue(x >= x)
+    me.assertFalse(x <= y)
+    me.assertTrue(y <= x)
+    me.assertTrue(x <= x)
+    me.assertFalse(x < y)
+    me.assertTrue(y < x)
+    me.assertFalse(x < x)
+
+  def test_bits(me):
+    x, y, zero = C.MP(169), C.MP(-24), C.MP(0)
+    me.assertTrue(x.testbit(0))
+    me.assertFalse(x.testbit(1))
+    me.assertFalse(x.testbit(1000))
+    me.assertFalse(y.testbit(0))
+    me.assertTrue(y.testbit(3))
+    me.assertTrue(y.testbit(1000))
+
+    me.assertEqual(x.setbit(0), x)
+    me.assertEqual(x.clearbit(0), 168)
+    me.assertEqual(x.setbit(1), 171)
+    me.assertEqual(x.clearbit(1), x)
+    me.assertEqual(y.setbit(0), -23)
+    me.assertEqual(y.clearbit(0), y)
+    me.assertEqual(y.setbit(3), y)
+    me.assertEqual(y.clearbit(3), -32)
+    me.assertEqual(y.setbit(1000), y)
+
+    me.assertEqual(x.nbits, 8)
+    me.assertEqual(y.nbits, 5)
+    me.assertEqual(zero.nbits, 0)
+
+  def test_loadstore(me):
+    x = C.MP(0x0123456789ab)
+    y = -x
+    u = C.MP(0xfedcba9876)
+
+    me.assertEqual(x.noctets, 6)
+    me.assertEqual(x.noctets2c, 6)
+    me.assertEqual(y.noctets, 6)
+    me.assertEqual(y.noctets2c, 6)
+    me.assertEqual(u.noctets, 5)
+    me.assertEqual(u.noctets2c, 6)
+
+    me.assertEqual(x, C.MP.loadb(C.bytes("0123456789ab")))
+    me.assertEqual(x, C.MP.loadb2c(C.bytes("0123456789ab")))
+    me.assertEqual(y, C.MP.loadb2c(C.bytes("fedcba987655")))
+
+    me.assertEqual(x.storeb(), C.bytes("0123456789ab"))
+    me.assertEqual(x.storeb(3), C.bytes("6789ab"))
+    me.assertEqual(x.storeb(8), C.bytes("00000123456789ab"))
+    me.assertEqual(x.storeb2c(), C.bytes("0123456789ab"))
+    me.assertEqual(x.storeb2c(3), C.bytes("6789ab"))
+    me.assertEqual(x.storeb2c(8), C.bytes("00000123456789ab"))
+    me.assertEqual(u.storeb2c(), C.bytes("00fedcba9876"))
+    me.assertEqual(y.storeb2c(), C.bytes("fedcba987655"))
+    me.assertEqual(y.storeb2c(3), C.bytes("987655"))
+    me.assertEqual(y.storeb2c(8), C.bytes("fffffedcba987655"))
+
+    me.assertEqual(x, C.MP.loadl(C.bytes("ab8967452301")))
+    me.assertEqual(x, C.MP.loadl2c(C.bytes("ab8967452301")))
+    me.assertEqual(y, C.MP.loadl2c(C.bytes("557698badcfe")))
+
+    me.assertEqual(x.storel(), C.bytes("ab8967452301"))
+    me.assertEqual(x.storel(3), C.bytes("ab8967"))
+    me.assertEqual(x.storel(8), C.bytes("ab89674523010000"))
+    me.assertEqual(x.storel2c(), C.bytes("ab8967452301"))
+    me.assertEqual(x.storel2c(3), C.bytes("ab8967"))
+    me.assertEqual(x.storel2c(8), C.bytes("ab89674523010000"))
+    me.assertEqual(u.storel2c(), C.bytes("7698badcfe00"))
+    me.assertEqual(y.storel2c(), C.bytes("557698badcfe"))
+    me.assertEqual(y.storel2c(3), C.bytes("557698"))
+    me.assertEqual(y.storel2c(8), C.bytes("557698badcfeffff"))
+
+    me.assertEqual(x.tobuf(), C.bytes("00060123456789ab"))
+    me.assertEqual((x, T.bin("abcd")),
+                   C.MP.frombuf(C.bytes("00060123456789ab61626364")))
+
+  def test_numbertheory(me):
+    p, x, y, z = C.MP(173), C.MP(169), C.MP(24), C.MP(20)
+
+    me.assertEqual(x.odd(), (0, x))
+    me.assertEqual(y.odd(), (3, 3))
+
+    me.assertEqual(x.sqr(), 28561)
+    me.assertEqual(x.sqrt(), 13)
+    me.assertEqual(y.sqrt(), 4)
+
+    me.assertEqual(x.gcd(y), 1)
+    me.assertEqual(x.gcdx(y), (1, -23, 162))
+    me.assertEqual(y.gcdx(x), (1, -7, 1))
+    me.assertEqual(x.modinv(y), 162)
+    me.assertEqual(y.modinv(x), 1)
+
+    me.assertEqual(x.jacobi(y), 1)
+    me.assertEqual(x.jacobi(13), 0)
+    me.assertEqual(y.jacobi(x), 1)
+    me.assertEqual(p.jacobi(y), 1)
+    me.assertEqual(p.jacobi(z), -1)
+    me.assertEqual(p.modsqrt(y), 71)
+    me.assertRaises(ValueError, p.modsqrt, z)
+
+    me.assertEqual(y.leastcongruent(x, 32), 184)
+
+    me.assertTrue(p.primep())
+    me.assertFalse(x.primep())
+
+  def test_bang(me):
+    me.assertEqual(C.MP.factorial(0), 1)
+    me.assertEqual \
+      (C.MP.factorial(50),
+       30414093201713378043612608166064768844377641568960512000000000000)
+    me.assertRaises((ValueError, OverflowError), C.MP.factorial, -1)
+
+  def test_fib(me):
+    me.assertEqual(C.MP.fibonacci(-2), -1)
+    me.assertEqual(C.MP.fibonacci(-1), +1)
+    me.assertEqual(C.MP.fibonacci( 0),  0)
+    me.assertEqual(C.MP.fibonacci(+1), +1)
+    me.assertEqual(C.MP.fibonacci(+2), +1)
+    me.assertEqual(C.MP.fibonacci(50), 12586269025)
+
+###--------------------------------------------------------------------------
+class TestMPMul (U.TestCase):
+
+  def test(me):
+    m = C.MPMul()
+    me.assertTrue(m.livep)
+    m.factor(1, 2, 3)
+    m.factor([4, 5, 6])
+    me.assertEqual(m.done(), 720)
+    me.assertFalse(m.livep)
+
+    me.assertEqual(C.MPMul(T.range(1, 7)).done(), 720)
+    me.assertEqual(C.MP.factorial(6), 720)
+
+###--------------------------------------------------------------------------
+class TestMPMont (U.TestCase):
+
+  def test(me):
+
+    me.assertRaises(ValueError,
+                    C.MPMont, 35315021952044908656941308411353985942)
+    me.assertRaises(ValueError, C.MPMont, -9)
+
+    p = C.MP(269464705320809171350781605680038324101)
+    g = C.MP(2) # lucky chance
+    x = C.MP(211184293914316080585277908844600399612)
+    y = C.MP(154454671298730680774195646814344206562)
+    xy = C.MP(209444562478584646216087606217820187655)
+    me.assertTrue(p.primep())
+    m = C.MPMont(p)
+    me.assertEqual(m.m, p)
+
+    ## The precise values of m.r and m.r2 are dependent on the internal
+    ## bignum representation.  But we expect m.r to be congruent to some
+    ## power of two.  (It should be 2^128.)
+    t = p.modinv(m.r)
+    for i in T.range(1025):
+      if t == 1: break
+      t *= 2
+      if t >= p: t -= p
+    else:
+      me.fail("m.r is not a small-ish power of 2")
+    me.assertEqual(m.r2, pow(2, 2*i, p))
+    me.assertEqual(m.ext(m.r), 1)
+    me.assertEqual(m.reduce(m.r), 1)
+
+    me.assertEqual(m.ext(m.int(x)), x)
+    me.assertEqual(m.int(x), m.mul(x, m.r2))
+    me.assertEqual(m.mul(m.int(x), y), xy)
+    me.assertEqual(m.ext(m.mul(m.int(x), m.int(y))), xy)
+
+    me.assertEqual(m.exp(2, p - 1), 1)
+    me.assertEqual(m.expr(m.int(2), p - 1), m.r)
+
+    q, r, s, z = 32, 128, 2048, pow(g, 156, p)
+    me.assertEqual(m.mexp([(q, 9), (r, 8), (s, 5)]), z)
+    me.assertEqual(m.mexp(q, 9, r, 8, s, 5), z)
+
+    q, r, s, z = T.imap(m.int, [32, 128, 2048, pow(g, 156, p)])
+    me.assertEqual(m.mexpr([(q, 9), (r, 8), (s, 5)]), z)
+    me.assertEqual(m.mexpr(q, 9, r, 8, s, 5), z)
+
+###--------------------------------------------------------------------------
+class TestMPBarrett (U.TestCase):
+
+  def test(me):
+
+    p = C.MP(269464705320809171350781605680038324101)
+    g = C.MP(2) # lucky chance
+    x = C.MP(211184293914316080585277908844600399612)
+    y = C.MP(154454671298730680774195646814344206562)
+    xy = C.MP(209444562478584646216087606217820187655)
+    me.assertTrue(p.primep())
+    m = C.MPBarrett(p)
+    me.assertEqual(m.m, p)
+
+    me.assertEqual(m.reduce(x*y), xy)
+
+    me.assertEqual(m.exp(2, p - 1), 1)
+
+    q, r, s, z = 32, 128, 2048, pow(g, 156, p)
+    me.assertEqual(m.mexp([(q, 9), (r, 8), (s, 5)]), z)
+    me.assertEqual(m.mexp(q, 9, r, 8, s, 5), z)
+
+###--------------------------------------------------------------------------
+class TestMPReduce (U.TestCase):
+
+  def test(me):
+
+    p = C.MP(2)**127 - 1
+    g = C.MP(2) # lucky chance
+    x = C.MP(94827182170881245766374991987593163418)
+    y = C.MP(106025009945795266831396608563402138277)
+    xy = C.MP(80027041045616838298103413933629021123)
+    me.assertTrue(p.primep())
+    m = C.MPReduce(p)
+    me.assertEqual(m.m, p)
+
+    me.assertEqual(m.reduce(x*y), xy)
+
+    me.assertEqual(m.exp(2, 127), 1)
+
+###--------------------------------------------------------------------------
+class TestMPCRT (U.TestCase):
+
+  def test(me):
+
+    c = C.MPCRT(5, 7, 11)
+    me.assertEqual(c.moduli, [5, 7, 11])
+    me.assertEqual(c.product, 385)
+    me.assertEqual(c.solve([2, 3, 4]), 367)
+    me.assertEqual(c.solve([2, -4, -7]), 367)
+
+    me.assertRaises(ValueError, C.MPCRT, [6, 15, 35])
+
+###--------------------------------------------------------------------------
+class TestGF (U.TestCase):
+
+  def test_make(me):
+    x = C.GF(5)
+    k = C.PrimeField(17)
+    kk = C.BinPolyField(C.GF(0x13))
+    E = k.ec(-3, 1)
+    me.assertTrue(C.GF(x) is x)
+    me.assertEqual(C.GF(k(8)), C.GF(8))
+    me.assertEqual(C.GF(kk(8)), C.GF(8))
+    me.assertEqual(C.GF(E(1, 4)), C.GF(1))
+    me.assertRaises(TypeError, C.GF, E())
+
+    me.assertEqual(int(x), 5)
+    y = C.GF(0x4eeb684a0954ec4ceb255e3e9778d41)
+    me.assertEqual(type(int(y)), T.long)
+
+    me.assertEqual(C.GF('0x4eeb684a0954ec4ceb255e3e9778d41'), y)
+    me.assertEqual(C.GF('4eeb684a0954ec4ceb255e3e9778d41', 16), y)
+    me.assertEqual(C.GF('0b0', 16), C.GF(176)) # not 0
+
+    me.assertEqual(C.GF('047353320450112516611472622536175135706501'), y)
+    me.assertEqual(C.GF('0o47353320450112516611472622536175135706501'), y)
+    me.assertEqual(C.GF('047353320450112516611472622536175135706501', 8), y)
+    me.assertEqual(C.GF('47353320450112516611472622536175135706501', 8), y)
+
+    t = C.GF(661438603)
+    me.assertEqual(C.GF('0b100111011011001100000010001011'), t)
+    me.assertEqual(C.GF('100111011011001100000010001011', 2), t)
+
+  def test_string(me):
+    y = C.GF(0x4eeb684a0954ec4ceb255e3e9778d41)
+    me.assertEqual(str(y), '0x4eeb684a0954ec4ceb255e3e9778d41')
+    me.assertEqual(repr(y), 'GF(0x4eeb684a0954ec4ceb255e3e9778d41L)')
+    me.assertEqual(hex(y), '0x4eeb684a0954ec4ceb255e3e9778d41')
+    me.assertEqual(oct(y), '047353320450112516611472622536175135706501')
+
+  def test_number(me):
+    x, y, m, zero = C.GF(0xa9), C.GF(0x18), C.GF(0x11b), C.GF(0)
+
+    me.assertEqual(x, -x)
+    me.assertEqual(abs(x), x)
+
+    me.assertEqual(x + y, C.GF(0xb1))
+    me.assertEqual(x - y, C.GF(0xb1))
+    me.assertEqual(x*y, C.GF(0xfd8))
+    me.assertEqual(x&y, C.GF(0x8))
+    me.assertEqual(x | y, C.GF(0xb9))
+    me.assertEqual(x ^ y, C.GF(0xb1))
+
+    me.assertEqual(x << 3, C.GF(0x548))
+    me.assertEqual(x << -2, C.GF(0x2a))
+    me.assertEqual(x >> 2, C.GF(0x2a))
+    me.assertEqual(x >> -3, C.GF(0x548))
+
+    u = x/y; me.assertEqual((u.numer, u.denom), (C.GF(0x67), C.GF(0x8)))
+    me.assertEqual(x//y, C.GF(0xc))
+    me.assertEqual(x%y, C.GF(0x9))
+    me.assertEqual(divmod(x, y), (C.GF(0xc), C.GF(0x9)))
+    me.assertRaises(ZeroDivisionError, lambda: x/zero)
+    me.assertRaises(ZeroDivisionError, lambda: x//zero)
+    me.assertRaises(ZeroDivisionError, lambda: x%zero)
+    me.assertRaises(ZeroDivisionError, divmod, x, zero)
+
+    me.assertEqual(pow(x, 24),
+                   C.GF(0x1000100000001010000010101000101010001000001))
+    me.assertEqual(pow(x, 24, m), C.GF(0x78))
+    me.assertEqual(pow(x, -24, m), C.GF(0xb6))
+    me.assertRaises(ZeroDivisionError, pow, x, -24, C.GF(0x18))
+
+    me.assertTrue(x)
+    me.assertFalse(zero)
+
+  def test_order(me):
+    x, y, z = C.GF(0xa9), C.GF(0x18), C.GF(0xb3)
+    me.assertTrue(x == x)
+    me.assertFalse(x != x)
+    me.assertFalse(x == y)
+    me.assertTrue(x != y)
+    me.assertTrue(x > y)
+    me.assertFalse(y > x)
+    me.assertFalse(x > x)
+    me.assertFalse(x > z)
+    me.assertFalse(z > x)
+    me.assertTrue(x >= y)
+    me.assertFalse(y >= x)
+    me.assertTrue(x >= x)
+    me.assertTrue(x >= z)
+    me.assertTrue(z >= x)
+    me.assertFalse(x <= y)
+    me.assertTrue(y <= x)
+    me.assertTrue(x <= x)
+    me.assertTrue(x <= z)
+    me.assertTrue(z <= x)
+    me.assertFalse(x < y)
+    me.assertTrue(y < x)
+    me.assertFalse(x < x)
+    me.assertFalse(x < z)
+    me.assertFalse(z < x)
+
+  def test_bits(me):
+    x, zero = C.GF(0xa9), C.GF(0)
+    me.assertTrue(x.testbit(0))
+    me.assertFalse(x.testbit(1))
+    me.assertFalse(x.testbit(1000))
+
+    me.assertEqual(x.setbit(0), x)
+    me.assertEqual(x.clearbit(0), C.GF(0xa8))
+    me.assertEqual(x.setbit(1), C.GF(0xab))
+    me.assertEqual(x.clearbit(1), x)
+
+    me.assertEqual(x.nbits, 8)
+    me.assertEqual(x.degree, 7)
+    me.assertEqual(zero.nbits, 0)
+    me.assertEqual(zero.degree, -1)
+
+  def test_loadstore(me):
+    x = C.GF(0x0123456789ab)
+
+    me.assertEqual(x.noctets, 6)
+
+    me.assertEqual(x, C.GF.loadb(C.bytes("0123456789ab")))
+
+    me.assertEqual(x.storeb(), C.bytes("0123456789ab"))
+    me.assertEqual(x.storeb(3), C.bytes("6789ab"))
+    me.assertEqual(x.storeb(8), C.bytes("00000123456789ab"))
+
+    me.assertEqual(x, C.GF.loadl(C.bytes("ab8967452301")))
+
+    me.assertEqual(x.storel(), C.bytes("ab8967452301"))
+    me.assertEqual(x.storel(3), C.bytes("ab8967"))
+    me.assertEqual(x.storel(8), C.bytes("ab89674523010000"))
+
+    me.assertEqual(x.tobuf(), C.bytes("00060123456789ab"))
+    me.assertEqual((x, T.bin("abcd")),
+                   C.GF.frombuf(C.bytes("00060123456789ab61626364")))
+
+  def test_numbertheory(me):
+    p, x, y = C.GF(0x11b), C.GF(0xa9), C.GF(0x18)
+
+    me.assertEqual(x.sqr(), C.GF(0x4441))
+
+    me.assertEqual(x.gcd(y), C.GF(0x3))
+    me.assertEqual(x.gcdx(y), (C.GF(0x3), C.GF(0x3), C.GF(0x15)))
+    me.assertEqual(p.modinv(x), C.GF(0xc8))
+
+    me.assertTrue(p.irreduciblep())
+    me.assertFalse(x.irreduciblep())
+
+###--------------------------------------------------------------------------
+class TestGFReduce (U.TestCase):
+
+  def test(me):
+    p = C.GF(0x87).setbit(128)
+    me.assertTrue(p.irreduciblep())
+    m = C.GFReduce(p)
+
+    x = C.GF(0xce46b4c1d3a1523520b1bb6eb5c61883)
+    y = C.GF(0xb5b0b3566b8e03f4b4a2b1ac413f8566)
+    xy = C.GF(0x28e5b895c11b08edc2fe7e1be5694c64)
+
+    me.assertEqual(m.reduce(x*y), xy)
+    me.assertEqual(m.trace(x), 0)
+    me.assertEqual(m.trace(y), 1)
+    me.assertEqual(m.sqrt(x), C.GF(0xa277ee4bf770e5974cf1e31b1ccb54a1))
+    me.assertEqual(m.halftrace(y), C.GF(0x9cea73e79ffd190dd3c81d33e58d8e6f))
+    me.assertEqual(m.quadsolve(x), C.GF(0x9664c09d23d168147a438de6a813c784))
+
+###--------------------------------------------------------------------------
+class TestGFN (U.TestCase):
+
+  def test(me):
+    p = C.GF(0x87).setbit(128)
+    beta = C.GF(0xc50f387e37194d4a4b41e157a3e2b5e1)
+    y = C.GF(0xdaca76dc2578a63c788a2ce0fc7878f6)
+    yy = C.GF(0x298a98f955100f054fcee3433f96b00e)
+    zero, one, fff = C.GF(0), C.GF(1), C.GF(T.long(2)**128 - 1)
+    me.assertTrue(p.irreduciblep())
+
+    gfn = C.GFN(p, beta)
+    me.assertEqual(gfn.p, p)
+    me.assertEqual(gfn.beta, beta)
+    me.assertEqual(gfn.pton(zero), zero)
+    me.assertEqual(gfn.ntop(zero), zero)
+    me.assertEqual(gfn.pton(one), fff)
+    me.assertEqual(gfn.ntop(fff), one)
+    me.assertEqual(gfn.pton(y), yy)
+    me.assertEqual(gfn.ntop(yy), y)
+
+    ## Doesn't generate a normal basis.
+    me.assertRaises(ValueError, C.GFN, p, y)
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-passphrase.py b/t/t-passphrase.py
new file mode 100644 (file)
index 0000000..9cc80a9
--- /dev/null
@@ -0,0 +1,90 @@
+### -*-python-*-
+###
+### Testing (some) passsword 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.
+
+from __future__ import with_statement
+
+###--------------------------------------------------------------------------
+### Imported modules.
+
+import catacomb as C
+import errno as E
+import unittest as U
+import os as OS
+import subprocess as SUB
+import sys as SYS
+import testutils as T
+import time as TM
+
+###--------------------------------------------------------------------------
+### Running the pixie.
+
+class PixieTestCase (U.TestCase):
+  def setUp(me):
+    pix = "pixie-py%d.%d%s" % (SYS.version_info[0],
+                               SYS.version_info[1],
+                               T.DEBUGP and "dbg" or "")
+    me.token = hex(C.rand.block(8))
+    try: OS.mkdir(OS.path.join("build", pix), int("700", 8))
+    except OSError:
+      if SYS.exc_info()[1].errno == E.EEXIST: pass
+      else: raise
+    me.sock = OS.path.join("build", pix, "sock")
+    OS.environ["CATACOMB_PIXIE_SOCKET"] = me.sock
+    with open(OS.path.join("build", pix, "log"), "a") as logf:
+      SUB.check_call(["pixie", "-dr", "-s" + me.sock,
+                      "-c", "echo 'pp.%%t.%s'" % me.token], stderr = logf)
+  def tearDown(me):
+    SUB.check_call(["pixie", "-s" + me.sock, "-C", "quit"])
+
+###--------------------------------------------------------------------------
+class TestPixie (PixieTestCase):
+
+  def test(me):
+    px = C.Pixie(socket = me.sock)
+
+    pp = T.bin("super secret")
+    pp1 = T.bin("pp.test1.%s" % me.token)
+    pp2 = T.bin("pp.test2.%s" % me.token)
+    px.set("test1", pp)
+    me.assertEqual(px.read("test1"), pp)
+    me.assertEqual(px.read("test1", C.PMODE_READ), pp)
+    me.assertEqual(px.read("test1", C.PMODE_VERIFY), pp1)
+    me.assertEqual(px.read("test1", C.PMODE_READ), pp1)
+    px.set("test1", pp)
+    me.assertEqual(px.read("test1", C.PMODE_READ), pp)
+    me.assertEqual(px.read("test2"), pp2)
+    px.cancel("test1")
+    me.assertEqual(px.read("test1"), pp1)
+
+    px.set("test1", pp)
+    me.assertEqual(C.ppread("test1"), pp)
+    C.ppcancel("test1")
+    me.assertEqual(px.read("test1"), pp1)
+    C.ppcancel("test1")
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-pgen.py b/t/t-pgen.py
new file mode 100644 (file)
index 0000000..dd66903
--- /dev/null
@@ -0,0 +1,228 @@
+### -*-python-*-
+###
+### Testing prime-generation 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 itertools as I
+import unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestPrimeFilter (U.TestCase):
+
+  def test(me):
+    n0 = 55614384877957400296344428606761687241
+    pf = C.PrimeFilter(n0)
+    me.assertEqual(pf.status, C.PGEN_FAIL)
+    me.assertFalse(pf)
+    me.assertEqual(pf.x, n0)
+    me.assertEqual(int(pf), n0)
+    while not pf: pf.step(2)
+    me.assertEqual(pf.status, C.PGEN_TRY)
+    me.assertEqual(pf.x, n0 + 6)
+
+    n1 = 111228769755914800592688857213523374495
+    pf1 = pf.muladd(2, 1)
+    me.assertEqual(pf1.x, n1)
+    me.assertEqual(pf1.status, C.PGEN_FAIL)
+
+    step = 2275063338
+    pf0 = C.PrimeFilter(step)
+    me.assertEqual(pf0.status, C.PGEN_FAIL)
+    while not pf1: pf1.jump(pf0)
+    me.assertEqual(pf1.x, n1 + 6*step)
+
+###--------------------------------------------------------------------------
+class TestRabinMiller (U.TestCase):
+
+  def test(me):
+    ## See `Prime and Prejudice' by Martin R. Albrecht, Jake Massimo,
+    ## Kenneth G. Paterson, and Juraj Somorovsky.
+    p1 = C.MP(142445387161415482404826365418175962266689133006163)
+    p2 = C.MP(5840260873618034778597880982145214452934254453252643)
+    p3 = C.MP(14386984103302963722887462907235772188935602433622363)
+    n = p1*p2*p3
+
+    rm = C.RabinMiller(n)
+    me.assertEqual(rm.test(2), C.PGEN_PASS)
+    me.assertEqual(rm.test(41), C.PGEN_FAIL)
+    me.assertEqual(rm.niters, 6)
+
+  def test_iters(me):
+    me.assertEqual(C.RabinMiller.iters(50), 27)
+    me.assertEqual(C.RabinMiller.iters(100), 27)
+    me.assertEqual(C.RabinMiller.iters(500), 6)
+    me.assertEqual(C.RabinMiller.iters(1000), 3)
+    me.assertEqual(C.RabinMiller.iters(2000), 2)
+
+###--------------------------------------------------------------------------
+
+class FailingHandler (object):
+  def pg_begin(me, ev): return C.PGEN_TRY
+  def pg_try(me, ev): raise T.Explosion
+  def pg_done(me, ev): raise ValueError("pg_done")
+
+class TestPGen (U.TestCase):
+
+  def test_pgen(me):
+    ev = T.EventRecorder()
+    p0 = T.detrand("pgen").mp(256, 1)
+    p = C.pgen(p0, name = "p", event = ev)
+    me.assertEqual(p, p0 + 54)
+    me.assertTrue(p.primep())
+    me.assertEqual(ev.events, "[p:F4/P11/D]")
+
+  def test_pgen_exc(me):
+    rng = T.detrand("pgen_exc")
+    exc = [None]
+    def hook(why, ty, val, tb): exc[0] = val
+    C.lostexchook = hook
+    me.assertRaises(T.Explosion, C.pgen, rng.mp(256, 1), name = "p",
+                    event = T.EventRecorder(explode_after = 19))
+    me.assertEqual(exc[0], None)
+    me.assertRaises(T.Explosion, C.pgen, rng.mp(256, 1), name = "p",
+                    event = FailingHandler(), stepper = FailingHandler())
+    me.assertEqual(exc[0].args[0], "pg_done")
+    exc = [None]
+    me.assertRaises(T.Explosion, C.limlee, 512, 96, name = "p", rng = rng,
+                    event = T.EventRecorder(explode_after = 8))
+    ev = T.EventRecorder(explode_after = 19)
+    me.assertRaises(T.Explosion, C.limlee, 512, 96, name = "p", rng = rng,
+                    ievent = ev)
+    me.assertRaises(ValueError, ev.rng.byte)
+    me.assertEqual(exc[0], None)
+    C.lostexchook = C.default_lostexchook
+
+  def test_strongprime_setup(me):
+    ev = T.EventRecorder()
+    p0, delta = C.strongprime_setup(256, name = "p", event = ev,
+                                    rng = T.detrand("strongprime_setup"))
+    p = C.pgen(p0, name = "p", event = ev,
+               stepper = C.PrimeGenJumper(delta))
+    me.assertTrue(p.primep())
+    me.assertEqual((p - p0)%delta, 0)
+    me.assertEqual(p.nbits, 256)
+    me.assertEqual(ev.events,
+                   "[p [s]:F3/P26/D]"
+                   "[p [t]:F6/P26/D]"
+                   "[p [r]:F5/P26/D]"
+                   "[p:F7/P11/D]")
+
+  def test_strongprime(me):
+    ev = T.EventRecorder()
+    p = C.strongprime(256, name = "p", event = ev,
+                      rng = T.detrand("strongprime"))
+    me.assertTrue(p.primep())
+    me.assertEqual(p.nbits, 256)
+    me.assertEqual(ev.events,
+                   "[p [s]:F5/P26/D]"
+                   "[p [t]:F13/P26/D]"
+                   "[p [r]:F7/P26/D]"
+                   "[p:F13/P11/D]")
+
+  def test_limlee(me):
+    ev = T.EventRecorder()
+    p, qq = C.limlee(512, 96, name = "p", event = ev, ievent = ev,
+                     rng = T.detrand("limlee"))
+    me.assertTrue(p.primep())
+    me.assertEqual(p.nbits, 512)
+    qbig = qq.pop()
+    me.assertTrue(qbig.primep())
+    me.assertTrue(qbig.nbits >= 96)
+    for q in qq:
+      me.assertTrue(q.primep())
+      me.assertEqual(q.nbits, 96)
+    me.assertEqual(p, C.MPMul(qq).factor(qbig).factor(2).done() + 1)
+    me.assertEqual(ev.events,
+                   "[p:"
+                   "[p_0:P26/D]"
+                   "[p_1:P26/D]"
+                   "[p_2:P26/D]"
+                   "[p_3:F5/P26/D]"
+                   "[p*_4:P26/D]"
+                   "[p_5:F6/P26/D]"
+                   "[p_6:F3/P26/D]"
+                   "[p*_7:P26/D]"
+                   "[p_8:F5/P26/D]"
+                   "[p*_9:F3/P26/D]"
+                   "[p_10:F3/P26/D]"
+                   "[p_11:F1/P26/D]"
+                   "[p_12:F2/P26/D]"
+                   "[p_13:F4/P26/D]"
+                   "[p_14:F15/P26/D]"
+                   "[p_15:P26/D]"
+                   "[p_16:F3/P26/D]"
+                   "[p_17:P26/D]"
+                   "[p_18:F1/P26/D]"
+                   "[p_19:F8/P26/D]"
+                   "[p_20:F12/P26/D]"
+                   "[p_21:F2/P26/D]"
+                   "[p_22:F2/P26/D]"
+                   "[p_23:F2/P26/D]"
+                   "[p_24:F1/P26/D]"
+                   "[p_25:F9/P26/D]"
+                   "[p_26:P26/D]"
+                   "[p_27:F2/P26/D]"
+                   "[p*_28:F8/P26/D]"
+                   "[p*_29:F14/P26/D]"
+                   "[p*_30:F4/P26/D]"
+                   "[p*_31:F6/P26/D]"
+                   "[p*_32:P26/D]"
+                   "[p*_33:F1/P26/D]"
+                   "[p*_34:F6/P26/D]"
+                   "[p*_35:F5/P26/D]"
+                   "[p*_36:F6/P26/D]"
+                   "[p*_37:P26/D]"
+                   "[p*_38:F3/P26/D]"
+                   "[p*_39:P26/D]"
+                   "[p*_40:P26/D]"
+                   "F41/P5/D]")
+
+  def test_sgprime(me):
+    ev = T.EventRecorder()
+    q0 = T.detrand("sgprime").mp(255, 1)
+    q = C.sgprime(q0, event = ev)
+    me.assertTrue(q.primep())
+    p = 2*q + 1
+    me.assertEqual(p.nbits, 256)
+    me.assertTrue(p.primep())
+
+  def test_kcdsa(me):
+    ev = T.EventRecorder()
+    p, q, h = C.kcdsaprime(512, 96, event = ev, rng = T.detrand("kcdsa"))
+    me.assertTrue(q.primep())
+    me.assertEqual(q.nbits, 96)
+    me.assertTrue(h.primep())
+    me.assertEqual(p, 2*q*h + 1)
+    me.assertTrue(p.primep())
+    me.assertEqual(p.nbits, 512)
+    me.assertEqual(ev.events, "[p [h]:F17/P6/D][p:F60/P26/D]")
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-pubkey.py b/t/t-pubkey.py
new file mode 100644 (file)
index 0000000..5b930b0
--- /dev/null
@@ -0,0 +1,192 @@
+### -*-python-*-
+###
+### Testing public-key crypto 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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestDSA (U.TestCase):
+
+  def check(me, privcls, pubcls, rng, H, G):
+    msg = T.bin("A simple test message")
+    wrong = T.bin("This is not the message you're looking for")
+    a = privcls(G, rng.range(G.r), hash = H, rng = rng)
+    me.assertEqual(a.hash, H)
+    me.assertEqual(a.rng, rng)
+    A = pubcls(G, a.p, hash = H, rng = rng)
+    h = a.endhash(a.beginhash().hash(msg))
+    me.assertEqual(h, A.endhash(a.beginhash().hash(msg)))
+    sig = a.sign(h)
+    me.assertTrue(A.verify(h, sig))
+    me.assertFalse(A.verify(A.endhash(A.beginhash().hash(wrong)), sig))
+
+    me.assertRaises(ValueError, a.sign, h + C.bytes("00"))
+    me.assertRaises(ValueError, a.verify, h + C.bytes("00"), sig)
+
+  def test_dsa(me):
+    me.check(C.DSAPriv, C.DSAPub, T.detrand("dsa"), C.sha256,
+             C.primegroups["catacomb-ll-256-3072"].group())
+  def test_ecdsa(me):
+    me.check(C.DSAPriv, C.DSAPub, T.detrand("ecdsa"), C.sha256,
+             C.eccurves["nist-p256"].group())
+  def test_kcdsa(me):
+    me.check(C.KCDSAPriv, C.KCDSAPub, T.detrand("kcdsa"), C.has160,
+             C.primegroups["catacomb-ll-160-1024"].group())
+  def test_eckcdsa(me):
+    me.check(C.KCDSAPriv, C.KCDSAPub, T.detrand("eckcdsa"), C.sha256,
+             C.eccurves["nist-p256"].group())
+
+###--------------------------------------------------------------------------
+class TestRSA (U.TestCase):
+
+  def test(me):
+    rng = T.detrand("rsa")
+    ev = T.EventRecorder()
+    msg = T.bin("A simple test message")
+    h = C.sha256().hash(msg).done()
+    wrong = T.bin("This is not the message you're looking for")
+    hh = C.sha256().hash(wrong).done()
+
+    a = C.RSAPriv.generate(1536, event = ev, rng = rng)
+    A = C.RSAPub(n = a.n, e = a.e)
+    me.assertEqual(a.n.nbits, 1536)
+    me.assertEqual(a.rng, None)
+    a.rng = rng
+    me.assertEqual(a.rng, rng)
+    me.assertEqual(ev.events,
+                   "[p [s]:F6/P7/D]"
+                   "[p [t]:F68/P7/D]"
+                   "[p [r]:F20/P7/D]"
+                   "[p:F35/P3/D]"
+                   "[q [s]:F47/P7/D]"
+                   "[q [t]:F4/P7/D]"
+                   "[q [r]:F8/P7/D]"
+                   "[q:F22/P3/D]")
+
+    C.RSAPriv(n = a.n, e = a.e, d = a.d)
+    C.RSAPriv(n = a.n, p = a.p, d = a.d)
+    C.RSAPriv(n = a.n, q = a.q, e = a.e)
+    C.RSAPriv(p = a.p, q = a.q, e = a.e)
+    me.assertRaises(ValueError, C.RSAPriv, p = a.p, q = a.q, dp = a.dp)
+
+    me.assertTrue(a.p.primep())
+    me.assertTrue(a.q.primep())
+    me.assertEqual(a.n, a.p*a.q)
+    me.assertEqual(a.dp, a.d%(a.p - 1))
+    me.assertEqual(a.dq, a.d%(a.q - 1))
+    me.assertEqual((a.e*a.dp)%(a.p - 1), 1)
+    me.assertEqual((a.e*a.dq)%(a.q - 1), 1)
+    me.assertEqual((a.q*a.q_inv)%a.p, 1)
+
+    x = rng.range(a.n)
+    y = a.privop(x)
+    me.assertEqual(x, a.pubop(y))
+    me.assertEqual(x, A.pubop(y))
+    z = a.pubop(x)
+    me.assertEqual(z, A.pubop(x))
+    me.assertEqual(x, a.privop(z))
+
+    pad = C.PKCS1Crypt(rng = rng)
+    ct = A.encrypt(msg, pad)
+    me.assertEqual(a.decrypt(ct, pad), msg)
+    me.assertRaises(ValueError, a.decrypt, ct ^ 1, pad)
+
+    pad = C.PKCS1Sig(ep = C.bytes("3031300d060960864801650304020105000420"))
+    sig = a.sign(h, pad)
+    me.assertTrue(A.verify(h, sig, pad))
+    me.assertFalse(A.verify(hh, sig, pad))
+
+    pad = C.OAEP(mgf = C.sha256_mgf, hash = C.sha256, rng = rng)
+    ct = A.encrypt(msg, pad)
+    me.assertEqual(a.decrypt(ct, pad), msg)
+    me.assertRaises(ValueError, a.decrypt, ct ^ 1, pad)
+
+    pad = C.PSS(mgf = C.sha256_mgf, hash = C.sha256, rng = rng)
+    sig = a.sign(h, pad)
+    me.assertTrue(A.verify(h, sig, pad))
+    me.assertFalse(A.verify(hh, sig, pad))
+
+###--------------------------------------------------------------------------
+class TestXDH (U.TestCase):
+
+  def check(me, privcls, pubcls, rng):
+    msg = T.bin("A simple test message")
+    n = rng.block(24)
+    n1 = rng.block(24)
+
+    a = privcls.generate(rng)
+    A = pubcls(a.pub)
+    b = privcls.generate(rng)
+    B = pubcls(b.pub)
+    Z = a.agree(B)
+    me.assertEqual(b.agree(A), Z)
+    me.assertEqual(b.agree(a), Z)
+
+    ct = a.box(B, n, msg)
+    me.assertEqual(b.unbox(A, n, ct), msg)
+    me.assertRaises(ValueError, b.unbox, A, n1, ct)
+
+  def test_x25519(me):
+    me.check(C.X25519Priv, C.X25519Pub, T.detrand("x25519"))
+  def test_x448(me):
+    me.check(C.X448Priv, C.X448Pub, T.detrand("x448"))
+
+###--------------------------------------------------------------------------
+class TestEdDSA (U.TestCase):
+
+  def check(me, privcls, pubcls, rng):
+    msg = T.bin("A simple test message")
+    wrong = T.bin("This is not the message you're looking for")
+    perso = T.bin("Catacomb/Python test")
+    a = privcls.generate(rng)
+    A = pubcls(a.pub)
+
+    sig = a.sign(msg)
+    me.assertTrue(a.verify(msg, sig))
+    me.assertTrue(A.verify(msg, sig))
+    me.assertFalse(A.verify(wrong, sig))
+
+    sig = a.sign(msg, perso = perso)
+    me.assertTrue(a.verify(msg, sig, perso = perso))
+    me.assertFalse(A.verify(msg, sig))
+
+    h = a.endhash(a.beginhash().hash(msg))
+    sig = a.sign(h, phflag = True)
+    me.assertTrue(a.verify(h, sig, phflag = True))
+    me.assertFalse(A.verify(h, sig))
+
+  def test_ed25519(me):
+    me.check(C.Ed25519Priv, C.Ed25519Pub, T.detrand("ed25519"))
+  def test_ed448(me):
+    me.check(C.Ed448Priv, C.Ed448Pub, T.detrand("ed448"))
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-rand.py b/t/t-rand.py
new file mode 100644 (file)
index 0000000..d8d7b00
--- /dev/null
@@ -0,0 +1,165 @@
+### -*-python-*-
+###
+### Testing random-generator 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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestRandomGenerator (U.TestCase):
+
+  def check_rand(me, rng):
+
+    for i in T.range(50):
+      x = rng.byte()
+      me.assertEqual(type(x), int)
+      me.assertTrue(0 <= x < 256)
+
+    x = rng.word()
+    me.assertTrue(0 <= x <= 0xffffffff)
+
+    for i in T.range(50):
+      x = rng.range(10)
+      me.assertEqual(type(x), int)
+      me.assertTrue(0 <= x < 10)
+    x = rng.range(T.MAXFIXNUM + 1)
+    me.assertEqual(type(x), C.MP)
+
+    for i in T.range(50):
+      x = rng.mp(123, 7)
+      me.assertEqual(type(x), C.MP)
+      me.assertEqual(x.nbits, 123)
+      me.assertEqual(x&7, 7)
+    me.assertRaises((OverflowError, ValueError), rng.mp, 128, -1)
+    me.assertRaises((OverflowError, ValueError), rng.mp, 128, C.MPW_MAX + 1)
+
+    x = rng.block(17)
+    me.assertEqual(type(x), C.ByteString)
+    me.assertEqual(len(x), 17)
+
+  def test_lcrand(me):
+    rng = C.LCRand(0)
+    me.assertFalse(rng.cryptop)
+    w0 = rng.word()
+    rng.seedint(0)
+    me.assertEqual(rng.word(), w0)
+
+  def test_firand(me):
+    rng = C.FibRand(0)
+    me.assertFalse(rng.cryptop)
+    w0 = rng.word()
+    rng.seedint(0)
+    me.assertEqual(rng.word(), w0)
+
+  def test_truerand(me):
+    rng = C.TrueRand()
+    me.assertTrue(rng.cryptop)
+    me.check_rand(rng)
+    rng.key(T.span(23))
+    rng.seed(256)
+    me.assertRaises(ValueError, rng.seed, C.RAND_IBITS + 1)
+    rng.seedint(-314)
+    rng.seedword(0x12345678)
+    rng.seedrand(T.detrand("seed-truerand"))
+    rng.seedblock(T.span(123))
+    rng.add(T.span(123), 978)
+    rng.gate()
+    rng.stretch()
+    rng.timer()
+    me.check_rand(rng)
+
+  def test_cryptorand(me):
+    for r, kw in [("rijndael-counter", {}),
+                  ("rc4", {}),
+                  ("xchacha20", { "nonce": T.span(24) }),
+                  ("seal", { "i": 12345678 }),
+                  ("shake128", { "func": T.bin("TEST"),
+                                 "perso": T.bin("Catacomb/Python test") }),
+                  ("kmac256", { "perso": T.bin("Catacomb/Python test") })]:
+      rcls = C.gccrands[r]
+      rng = rcls(T.span(rcls.keysz.default), **kw)
+      me.assertTrue(rng.cryptop)
+
+  def test_sslrand(me):
+    rng = C.SSLRand(T.span(16), T.span(32), C.md5, C.sha)
+    me.check_rand(rng)
+  def test_tlsdx(me):
+    rng = C.TLSDataExpansion(T.span(16), T.span(32), C.sha256_hmac)
+    me.check_rand(rng)
+  def test_tlsprf(me):
+    rng = C.TLSPRF(T.span(16), T.span(32), C.md5_hmac, C.sha_hmac)
+    me.check_rand(rng)
+
+  def test_dsarand(me):
+    seed = T.span(16)
+    n = C.MP.loadb(seed)
+    rng = C.DSARand(seed)
+    me.check_rand(rng)
+    me.assertEqual(rng.seed, (n + 153 + 3).storeb(16))
+
+  def test_bbs(me):
+    ev = T.EventRecorder()
+    drng = T.detrand("bbs")
+    rngpriv = C.BBSPriv.generate(1536, event = ev, rng = drng)
+    me.assertEqual(rngpriv.n.nbits, 1536)
+    me.assertEqual(rngpriv.p&3, 3)
+    me.assertEqual(rngpriv.q&3, 3)
+    me.assertTrue(rngpriv.p.primep())
+    me.assertTrue(rngpriv.q.primep())
+    me.assertEqual(rngpriv.n, rngpriv.p*rngpriv.q)
+    me.assertEqual(ev.events,
+                   "[p [s]:F10/P7/D]"
+                   "[p [t]:F2/P7/D]"
+                   "[p [r]:F7/P7/D]"
+                   "[p:F6/P3/D]"
+                   "[q [s]:P7/D]"
+                   "[q [t]:F33/P7/D]"
+                   "[q [r]:F33/P7/D]"
+                   "[q:F55/P3/D]")
+
+    x0 = drng.range(rngpriv.n)
+    rngpriv.seedmp(x0)
+    rng = C.BlumBlumShub(rngpriv.n, x0)
+    me.check_rand(rngpriv)
+    me.check_rand(rng)
+    me.assertEqual(rngpriv.x, rng.x)
+
+    msg = T.span(123)
+    rng.wrap()
+    ct = rng.mask(msg)
+    rng.wrap()
+
+    rngpriv.x = rng.x
+    nsteps = (123*8 + 7)//(rng.stepsz)
+    rngpriv.rew(nsteps + 1)
+    me.assertEqual(rngpriv.mask(ct), msg)
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-rat.py b/t/t-rat.py
new file mode 100644 (file)
index 0000000..9fd15d4
--- /dev/null
@@ -0,0 +1,94 @@
+### -*-python-*-
+###
+### Testing rational arithmetic 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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestRat (U.TestCase):
+
+  def check_rat(me, k):
+    R = k.RING
+
+    ## Check that exact true division in the base ring gives base-ring
+    ## elements.
+    a, b, c = R(19), R(5), R(8)
+    m = a*b
+    q = m/b
+    me.assertEqual(type(q), R)
+    me.assertEqual(q, a)
+
+    ## Check that inexact division gives a fraction.
+    f = (m + c)/b
+    me.assertNotEqual(type(f), R)
+    me.assertNotEqual(f, a)
+    r = f - q
+    me.assertEqual(b*r, c)
+
+    ## More complicated arithmetic.
+    u, v = a/b, a/c
+    me.assertEqual((u + v)*(b*c), a*(b + c))
+    me.assertEqual((u - v)*(b*c), a*(c - b))
+
+    ## Ordering.
+    me.assertTrue(b < c)
+    me.assertTrue(b/a < c/a)
+    me.assertFalse(c/a < b/a)
+    me.assertTrue(b/a <= c/a)
+    me.assertFalse(c/a <= b/a)
+    me.assertFalse(b/a >= c/a)
+    me.assertTrue(c/a >= b/a)
+    me.assertFalse(b/a > c/a)
+    me.assertTrue(c/a > b/a)
+
+  def test_intrat(me):
+    me.check_rat(C.IntRat)
+
+    ## Check string conversions.
+    u = C.MP(5)/C.MP(4)
+    me.assertEqual(str(u), "5/4")
+    me.assertEqual(repr(u), "IntRat(5, 4)")
+
+  def test_gfrat(me):
+    me.check_rat(C.GFRat)
+
+    ## Check string conversions.
+    u = C.GF(5)/C.GF(4)
+    me.assertEqual(str(u), "0x5/0x4")
+    me.assertEqual(repr(u), "GFRat(0x5, 0x4)")
+
+  def test_cross(me):
+    u = C.MP(5)/C.MP(3)
+    v = C.GF(5)/C.GF(3)
+    me.assertRaises(TypeError, T.add, u, v)
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/t-share.py b/t/t-share.py
new file mode 100644 (file)
index 0000000..089d2ff
--- /dev/null
@@ -0,0 +1,68 @@
+### -*-python-*-
+###
+### Testing (some) passsword 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 unittest as U
+import testutils as T
+
+###--------------------------------------------------------------------------
+class TestShare (U.TestCase):
+
+  def check_share(me, splitcls, splitargs, joincls, joinargs, secret):
+
+    split = splitcls(3, secret, *splitargs)
+    me.assertEqual(split.threshold, 3)
+    shares = [split.get(i) for i in T.range(5)]
+
+    join = joincls(3, *joinargs)
+    me.assertEqual(join.threshold, 3)
+    me.assertFalse(join.addedp(1))
+    me.assertEqual(join.remain, 3)
+    join.add(1, shares[1])
+    me.assertTrue(join.addedp(1))
+    me.assertEqual(join.remain, 2)
+    me.assertRaises(ValueError, join.combine)
+    join.add(0, shares[0])
+    join.add(3, shares[3])
+    me.assertEqual(join.remain, 0)
+    me.assertEqual(join.combine(), secret)
+
+  def test_share(me):
+    rng = T.detrand("share")
+    p = C.MP(2)**127 - 1
+    me.check_share(C.ShareSplit, [p, rng], C.ShareJoin, [p], rng.range(p))
+
+  def test_gfshare(me):
+    rng = T.detrand("gfshare")
+    me.check_share(C.GFShareSplit, [rng], C.GFShareJoin, [123],
+                   rng.block(123))
+
+###----- That's all, folks --------------------------------------------------
+
+if __name__ == "__main__": U.main()
diff --git a/t/testutils.py b/t/testutils.py
new file mode 100644 (file)
index 0000000..42bbd76
--- /dev/null
@@ -0,0 +1,275 @@
+### -*- mode: python, coding: utf-8 -*-
+###
+### Test utilities
+###
+### (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
+if SYS.version_info >= (3,): import builtins as B
+else: import __builtin__ as B
+import unittest as U
+
+###--------------------------------------------------------------------------
+### Main code.
+
+## Some compatibility hacks.
+import itertools as I
+def bin(x): return x
+range = xrange
+long = long
+imap = I.imap
+def byteseq(seq): return "".join(map(chr, seq))
+def iterkeys(m): return m.iterkeys()
+def itervalues(m): return m.itervalues()
+def iteritems(m): return m.iteritems()
+from cStringIO import StringIO
+MAXFIXNUM = SYS.maxint
+
+DEBUGP = hasattr(SYS, "gettotalrefcount")
+
+FULLSPAN = byteseq(range(256))
+def span(n):
+  """A string `00 01 .. NN'."""
+  return (n >> 8)*FULLSPAN + FULLSPAN[:n&255]
+
+def bytes_as_int(w, bigendp):
+  """Convert the byte-sequence `01 02 ... WW' to an integer."""
+  x = 0
+  if bigendp:
+    for i in range(w): x = x << 8 | i + 1
+  else:
+    for i in range(w): x |= i + 1 << 8*i
+  return x
+
+def prep_lenseq(w, n, bigendp, goodp):
+  """
+  Return a reference buffer containing `00 LL .. LL 00 01 02 .. NN ff'.
+
+  Here, LL .. LL is the length of following sequence, not including the final
+  `ff', as a W-byte integer.  If GOODP is false, then the most significant
+  bit of LL .. LL is set, to provoke an overflow.
+  """
+  if goodp: l = n
+  else: l = n + (1 << 8*w - 1)
+  lenbyte = bigendp \
+    and (lambda i: (l >> 8*(w - i - 1))&0xff) \
+    or (lambda i: (l >> 8*i)&0xff)
+  return byteseq([0x00]) + \
+    byteseq([lenbyte(i) for i in range(w)]) + \
+    span(n) + \
+    byteseq([0xff])
+
+Z64 = C.ByteString.zero(8)
+def detrand(seed):
+  """Return a fast deterministic random generator with the given SEED."""
+  return C.chacha8rand(C.sha256().hash(bin(seed)).done(), Z64)
+
+class GenericTestMixin (U.TestCase):
+  """
+  A mixin class to generate test-case functions for all similar things.
+  """
+
+  @classmethod
+  def generate_testcases(cls, things):
+    testfns = dict()
+    checkfns = []
+    for k, v in iteritems(cls.__dict__):
+      if k.startswith("_test_"): checkfns.append((k[6:], v))
+    for name, thing in things:
+      for test, checkfn in checkfns:
+        testfn = lambda me, thing = thing: checkfn(me, thing)
+        doc = getattr(checkfn, "__doc__", None)
+        if doc is not None: testfn.__doc__ = doc % name
+        testfns["test_%s%%%s" % (test, name)] = testfn
+    tmpcls =  type("_tmp", (cls,), testfns)
+    for k, v in iteritems(tmpcls.__dict__):
+      if k.startswith("test_"): setattr(cls, k, v)
+
+class ImmutableMappingTextMixin (U.TestCase):
+
+  ## Subclass stubs.
+  def _mkkey(me, i): return "k#%d" % i
+  def _getkey(me, k): return int(k[2:])
+  def _getvalue(me, v): return int(v[2:])
+  def _getitem(me, it): k, v = it; return me._getkey(k), me._getvalue(v)
+
+  def check_immutable_mapping(me, map, model):
+
+    ## Lookup.
+    limk = 0
+    any = False
+    me.assertEqual(len(map), len(model))
+    for k, v in iteritems(model):
+      any = True
+      if k >= limk: limk = k + 1
+      me.assertTrue(me._mkkey(k) in map)
+      me.assertTrue(map.has_key(me._mkkey(k)))
+      me.assertEqual(me._getvalue(map[me._mkkey(k)]), v)
+      me.assertEqual(me._getvalue(map.get(me._mkkey(k))), v)
+    if any: me.assertTrue(me._mkkey(k) in map)
+    me.assertFalse(map.has_key(me._mkkey(limk)))
+    me.assertRaises(KeyError, lambda: map[me._mkkey(limk)])
+    me.assertEqual(map.get(me._mkkey(limk)), None)
+    for listfn, getfn in [(lambda x: x.keys(), me._getkey),
+                          (lambda x: x.values(), me._getvalue),
+                          (lambda x: x.items(), me._getitem)]:
+      rlist, mlist = listfn(map), listfn(model)
+      me.assertEqual(type(rlist), list)
+      rlist = B.map(getfn, rlist)
+      rlist.sort(); mlist.sort(); me.assertEqual(rlist, mlist)
+    for iterfn, getfn in [(lambda x: x.iterkeys(), me._getkey),
+                          (lambda x: x.itervalues(), me._getvalue),
+                          (lambda x: x.iteritems(), me._getitem)]:
+      me.assertEqual(set(imap(getfn, iterfn(map))), set(iterfn(model)))
+
+class MutableMappingTestMixin (ImmutableMappingTextMixin):
+
+  ## Subclass stubs.
+  def _mkvalue(me, i): return "v#%d" % i
+
+  def check_mapping(me, emptymapfn):
+
+    map = emptymapfn()
+    me.assertEqual(len(map), 0)
+
+    def check_views():
+      me.check_immutable_mapping(map, model)
+
+    model = { 1: 101, 2: 202, 4: 404 }
+    for k, v in iteritems(model): map[me._mkkey(k)] = me._mkvalue(v)
+    check_views()
+
+    model.update({ 2: 212, 6: 606, 7: 707 })
+    map.update({ me._mkkey(2): me._mkvalue(212),
+                 me._mkkey(6): me._mkvalue(606),
+                 me._mkkey(7): me._mkvalue(707) })
+    check_views()
+
+    model[9] = 909
+    map[me._mkkey(9)] = me._mkvalue(909)
+    check_views()
+
+    model[9] = 919
+    map[me._mkkey(9)] = me._mkvalue(919)
+    check_views()
+
+    map.setdefault(me._mkkey(9), me._mkvalue(929))
+    check_views()
+
+    model[8] = 808
+    map.setdefault(me._mkkey(8), me._mkvalue(808))
+    check_views()
+
+    me.assertRaises(KeyError, map.pop, me._mkkey(5))
+    obj = object()
+    me.assertEqual(map.pop(me._mkkey(5), obj), obj)
+    me.assertEqual(me._getvalue(map.pop(me._mkkey(8))), 808)
+    del model[8]
+    check_views()
+
+    del model[9]
+    del map[me._mkkey(9)]
+    check_views()
+
+    k, v = map.popitem()
+    mk, mv = me._getkey(k), me._getvalue(v)
+    me.assertEqual(model[mk], mv)
+    del model[mk]
+    check_views()
+
+    map.clear()
+    model = {}
+    check_views()
+
+class Explosion (Exception): pass
+
+class EventRecorder (C.PrimeGenEventHandler):
+  def __init__(me, parent = None, explode_after = None, *args, **kw):
+    super(EventRecorder, me).__init__(*args, **kw)
+    me._streak = 0
+    me._op = None
+    me._parent = parent
+    me._countdown = explode_after
+    me.rng = None
+    if parent is None: me._buf = StringIO()
+    else: me._buf = parent._buf
+  def _event_common(me, ev):
+    if me.rng is None: me.rng = ev.rng
+    if me._countdown is None: pass
+    elif me._countdown == 0: raise Explosion()
+    else: me._countdown -= 1
+  def _put(me, op):
+    if op == me._op:
+      me._streak += 1
+    else:
+      if me._op is not None: me._buf.write("%s%d/" % (me._op, me._streak))
+      me._op = op
+      me._streak = 1
+  def pg_begin(me, ev):
+    me._event_common(ev)
+    me._buf.write("[%s:" % ev.name)
+  def pg_try(me, ev):
+    me._event_common(ev)
+  def pg_fail(me, ev):
+    me._event_common(ev)
+    me._put("F")
+  def pg_pass(me, ev):
+    me._event_common(ev)
+    me._put("P")
+  def pg_done(me, ev):
+    me._event_common(ev)
+    me._put(None); me._buf.write("D]")
+  def pg_abort(me, ev):
+    me._event_common(ev)
+    me._put(None); me._buf.write("A]")
+  @property
+  def events(me):
+    return me._buf.getvalue()
+
+## Functions for operators.
+neg = lambda x: -x
+pos = lambda x: +x
+add = lambda x, y: x + y
+sub = lambda x, y: x - y
+mul = lambda x, y: x*y
+div = lambda x, y: x/y
+mod = lambda x, y: x%y
+floordiv = lambda x, y: x//y
+bitand = lambda x, y: x&y
+bitor = lambda x, y: x | y
+bitxor = lambda x, y: x ^ y
+bitnot = lambda x: ~x
+lsl = lambda x, y: x << y
+lsr = lambda x, y: x >> y
+eq = lambda x, y: x == y
+ne = lambda x, y: x != y
+lt = lambda x, y: x < y
+le = lambda x, y: x <= y
+ge = lambda x, y: x >= y
+gt = lambda x, y: x > y
+
+###----- That's all, folks --------------------------------------------------