t/t-algorithms.py: Add AEAD tests.
authorMark Wooding <mdw@distorted.org.uk>
Sun, 17 Nov 2019 23:41:36 +0000 (23:41 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Wed, 27 Nov 2019 15:11:29 +0000 (15:11 +0000)
t/t-algorithms.py

index f63565a..d9f94df 100644 (file)
@@ -316,6 +316,280 @@ TestCipher.generate_testcases((name, C.gcciphers[name]) for name in
    "blowfish-counter", "rc4", "seal", "salsa20/8", "shake128-xof"])
 
 ###--------------------------------------------------------------------------
+class TestAuthenticatedEncryption \
+        (HashBufferTestMixin, T.GenericTestMixin):
+  """Test authenticated encryption schemes."""
+
+  def _test_aead(me, aecls):
+
+    ## Check the class properties.
+    me.assertEqual(type(aecls.name), str)
+    me.assertTrue(isinstance(aecls.keysz, C.KeySZ))
+    me.assertTrue(isinstance(aecls.noncesz, C.KeySZ))
+    me.assertTrue(isinstance(aecls.tagsz, C.KeySZ))
+    me.assertEqual(type(aecls.blksz), int)
+    me.assertEqual(type(aecls.bufsz), int)
+    me.assertEqual(type(aecls.ohd), int)
+    me.assertEqual(type(aecls.flags), int)
+
+    ## Check round-tripping, with full precommitment.  First, select some
+    ## parameters.  (It's conceivable that some AEAD schemes are more
+    ## restrictive than advertised by the various properties, but this works
+    ## out OK in practice.)
+    k = T.span(aecls.keysz.default)
+    n = T.span(aecls.noncesz.default)
+    if aecls.flags&C.AEADF_NOAAD: h = T.span(0)
+    else: h = T.span(131)
+    m = T.span(253)
+    tsz = aecls.tagsz.default
+    key = aecls(k)
+
+    ## Next, encrypt a message, checking that things are proper as we go.
+    enc = key.enc(nonce = n, hsz = len(h), msz = len(m), tsz = tsz)
+    me.assertEqual(enc.hsz, len(h))
+    me.assertEqual(enc.msz, len(m))
+    me.assertEqual(enc.mlen, 0)
+    me.assertEqual(enc.tsz, tsz)
+    aad = enc.aad()
+    if aecls.flags&C.AEADF_AADNDEP: me.assertEqual(aad.hsz, len(h))
+    else: me.assertEqual(aad.hsz, None)
+    me.assertEqual(aad.hlen, 0)
+    if not aecls.flags&C.AEADF_NOAAD:
+      aad.hash(h[0:83])
+      me.assertEqual(aad.hlen, 83)
+      aad.hash(h[83:131])
+      me.assertEqual(aad.hlen, 131)
+    c0 = enc.encrypt(m[0:57])
+    me.assertEqual(enc.mlen, 57)
+    me.assertTrue(57 - aecls.bufsz <= len(c0) <= 57 + aecls.ohd)
+    c1 = enc.encrypt(m[57:189])
+    me.assertEqual(enc.mlen, 189)
+    me.assertTrue(132 - aecls.bufsz <= len(c1) <=
+                  132 + aecls.bufsz + aecls.ohd)
+    c2 = enc.encrypt(m[189:253])
+    me.assertEqual(enc.mlen, 253)
+    me.assertTrue(64 - aecls.bufsz <= len(c2) <=
+                  64 + aecls.bufsz + aecls.ohd)
+    c3, t = enc.done(aad = aad)
+    me.assertTrue(len(c3) <= aecls.bufsz + aecls.ohd)
+    c = c0 + c1 + c2 + c3
+    me.assertTrue(len(m) <= len(c) <= len(m) + aecls.ohd)
+    me.assertEqual(len(t), tsz)
+
+    ## And now decrypt it again, with different record boundaries.
+    dec = key.dec(nonce = n, hsz = len(h), csz = len(c), tsz = tsz)
+    me.assertEqual(dec.hsz, len(h))
+    me.assertEqual(dec.csz, len(c))
+    me.assertEqual(dec.clen, 0)
+    me.assertEqual(dec.tsz, tsz)
+    aad = dec.aad()
+    if aecls.flags&C.AEADF_AADNDEP: me.assertEqual(aad.hsz, len(h))
+    else: me.assertEqual(aad.hsz, None)
+    me.assertEqual(aad.hlen, 0)
+    aad.hash(h)
+    m0 = dec.decrypt(c[0:156])
+    me.assertTrue(156 - aecls.bufsz <= len(m0) <= 156)
+    m1 = dec.decrypt(c[156:])
+    me.assertTrue(len(c) - 156 - aecls.bufsz <= len(m1) <=
+                  len(c) - 156 + aecls.bufsz)
+    m2 = dec.done(tag = t, aad = aad)
+    me.assertEqual(m0 + m1 + m2, m)
+
+    ## And again, with the wrong tag.
+    dec = key.dec(nonce = n, hsz = len(h), csz = len(c), tsz = tsz)
+    aad = dec.aad(); aad.hash(h)
+    _ = dec.decrypt(c)
+    me.assertRaises(ValueError, dec.done, tag = t ^ tsz*C.bytes("55"))
+
+    ## Check that the all-in-one methods work.
+    me.assertEqual((c, t),
+                   key.encrypt(n = n, h = h, m = m, tsz = tsz))
+    me.assertEqual(m,
+                   key.decrypt(n = n, h = h, c = c, t = t))
+
+    ## Check that bad key, nonce, and tag lengths are rejected.
+    badlen = bad_key_size(aecls.keysz)
+    if badlen is not None: me.assertRaises(ValueError, aecls, T.span(badlen))
+    badlen = bad_key_size(aecls.noncesz)
+    if badlen is not None:
+      me.assertRaises(ValueError, key.enc, nonce = T.span(badlen),
+                      hsz = len(h), msz = len(m), tsz = tsz)
+      me.assertRaises(ValueError, key.dec, nonce = T.span(badlen),
+                      hsz = len(h), csz = len(c), tsz = tsz)
+      if not aecls.flags&C.AEADF_PCTSZ:
+        enc = key.enc(nonce = n, hsz = 0, msz = len(m))
+        _ = enc.encrypt(m)
+        me.assertRaises(ValueError, enc.done, tsz = badlen)
+    badlen = bad_key_size(aecls.tagsz)
+    if badlen is not None:
+      me.assertRaises(ValueError, key.enc, nonce = n,
+                      hsz = len(h), msz = len(m), tsz = badlen)
+      me.assertRaises(ValueError, key.dec, nonce = n,
+                      hsz = len(h), csz = len(c), tsz = badlen)
+
+    ## Check that we can't get a loose `aad' object from a scheme which has
+    ## nonce-dependent AAD processing.
+    if aecls.flags&C.AEADF_AADNDEP: me.assertRaises(ValueError, key.aad)
+
+    ## Check the menagerie of AAD hashing methods.
+    if not aecls.flags&C.AEADF_NOAAD:
+      def mkhash(hsz):
+        enc = key.enc(nonce = n, hsz = hsz, msz = 0, tsz = tsz)
+        aad = enc.aad()
+        return aad, lambda: enc.done(aad = aad)[1]
+      me.check_hashbuffer(mkhash)
+
+    ## Check that encryption/decryption works with the given precommitments.
+    def quick_enc_check(**kw):
+      enc = key.enc(**kw)
+      aad = enc.aad().hash(h)
+      c0 = enc.encrypt(m); c1, tt = enc.done(aad = aad, tsz = tsz)
+      me.assertEqual((c, t), (c0 + c1, tt))
+    def quick_dec_check(**kw):
+      dec = key.dec(**kw)
+      aad = dec.aad().hash(h)
+      m0 = dec.decrypt(c); m1 = dec.done(aad = aad, tag = t)
+      me.assertEqual(m, m0 + m1)
+
+    ## Check that we can get away without precommitting to the header length
+    ## if and only if the AEAD scheme says it will let us.
+    if aecls.flags&C.AEADF_PCHSZ:
+      me.assertRaises(ValueError, key.enc, nonce = n,
+                      msz = len(m), tsz = tsz)
+      me.assertRaises(ValueError, key.dec, nonce = n,
+                      csz = len(c), tsz = tsz)
+    else:
+      quick_enc_check(nonce = n, msz = len(m), tsz = tsz)
+      quick_dec_check(nonce = n, csz = len(c), tsz = tsz)
+
+    ## Check that we can get away without precommitting to the message/
+    ## ciphertext length if and only if the AEAD scheme says it will let us.
+    if aecls.flags&C.AEADF_PCMSZ:
+      me.assertRaises(ValueError, key.enc, nonce = n,
+                      hsz = len(h), tsz = tsz)
+      me.assertRaises(ValueError, key.dec, nonce = n,
+                      hsz = len(h), tsz = tsz)
+    else:
+      quick_enc_check(nonce = n, hsz = len(h), tsz = tsz)
+      quick_dec_check(nonce = n, hsz = len(h), tsz = tsz)
+
+    ## Check that we can get away without precommitting to the tag length if
+    ## and only if the AEAD scheme says it will let us.
+    if aecls.flags&C.AEADF_PCTSZ:
+      me.assertRaises(ValueError, key.enc, nonce = n,
+                      hsz = len(h), msz = len(m))
+      me.assertRaises(ValueError, key.dec, nonce = n,
+                      hsz = len(h), csz = len(c))
+    else:
+      quick_enc_check(nonce = n, hsz = len(h), msz = len(m))
+      quick_dec_check(nonce = n, hsz = len(h), csz = len(c))
+
+    ## Check that if we precommit to the header length, we're properly held
+    ## to the commitment.
+    if not aecls.flags&C.AEADF_NOAAD:
+
+      ## First, check encryption with underrun.  If we must supply AAD first,
+      ## then the underrun will be reported when we start trying to encrypt;
+      ## otherwise, checking is delayed until `done'.
+      enc = key.enc(nonce = n, hsz = len(h), msz = len(m), tsz = tsz)
+      aad = enc.aad().hash(h[0:83])
+      if aecls.flags&C.AEADF_AADFIRST:
+        me.assertRaises(ValueError, enc.encrypt, m)
+      else:
+        _ = enc.encrypt(m)
+        me.assertRaises(ValueError, enc.done, aad = aad)
+
+      ## Next, check decryption with underrun.  If we must supply AAD first,
+      ## then the underrun will be reported when we start trying to encrypt;
+      ## otherwise, checking is delayed until `done'.
+      dec = key.dec(nonce = n, hsz = len(h), csz = len(c), tsz = tsz)
+      aad = dec.aad().hash(h[0:83])
+      if aecls.flags&C.AEADF_AADFIRST:
+        me.assertRaises(ValueError, dec.decrypt, c)
+      else:
+        _ = dec.decrypt(c)
+        me.assertRaises(ValueError, dec.done, tag = t, aad = aad)
+
+      ## If AAD processing is nonce-dependent then an overrun will be
+      ## detected imediately.
+      if aecls.flags&C.AEADF_AADNDEP:
+        enc = key.enc(nonce = n, hsz = len(h), msz = len(m), tsz = tsz)
+        aad = enc.aad().hash(h[0:83])
+        me.assertRaises(ValueError, aad.hash, h[82:131])
+        dec = key.dec(nonce = n, hsz = len(h), csz = len(c), tsz = tsz)
+        aad = dec.aad().hash(h[0:83])
+        me.assertRaises(ValueError, aad.hash, h[82:131])
+
+    ## Some additional tests for nonce-dependent `aad' objects.
+    if aecls.flags&C.AEADF_AADNDEP:
+
+      ## Check that `aad' objects can't be used once their parents are gone.
+      enc = key.enc(nonce = n, hsz = len(h), msz = len(m), tsz = tsz)
+      aad = enc.aad()
+      del enc
+      me.assertRaises(ValueError, aad.hash, h)
+
+      ## Check that they can't be crossed over.
+      enc0 = key.enc(nonce = n, hsz = len(h), msz = len(m), tsz = tsz)
+      enc1 = key.enc(nonce = n, hsz = len(h), msz = len(m), tsz = tsz)
+      enc0.aad().hash(h)
+      aad1 = enc1.aad().hash(h)
+      _ = enc0.encrypt(m)
+      me.assertRaises(ValueError, enc0.done, tsz = tsz, aad = aad1)
+
+    ## Test copying AAD.
+    if not aecls.flags&C.AEADF_AADNDEP and not aecls.flags&C.AEADF_NOAAD:
+      aad0 = key.aad()
+      aad0.hash(h[0:83])
+      aad1 = aad0.copy()
+      aad2 = aad1.copy()
+      aad0.hash(h[83:131])
+      aad1.hash(h[83:131])
+      aad2.hash(h[83:131] ^ 48*C.bytes("ff"))
+      me.assertEqual(key.enc(nonce = n, hsz = len(h),
+                             msz = 0, tsz = tsz).done(aad = aad0),
+                     key.enc(nonce = n, hsz = len(h),
+                             msz = 0, tsz = tsz).done(aad = aad1))
+      me.assertNotEqual(key.enc(nonce = n, hsz = len(h),
+                                msz = 0, tsz = tsz).done(aad = aad0),
+                        key.enc(nonce = n, hsz = len(h),
+                                msz = 0, tsz = tsz).done(aad = aad2))
+
+    ## Check that if we precommit to the message length, we're properly held
+    ## to the commitment.  (Fortunately, this is way simpler than the AAD
+    ## case above.)  First, try an underrun.
+    enc = key.enc(nonce = n, hsz = 0, msz = len(m), tsz = tsz)
+    _ = enc.encrypt(m[0:183])
+    me.assertRaises(ValueError, enc.done, tsz = tsz)
+    dec = key.dec(nonce = n, hsz = 0, csz = len(c), tsz = tsz)
+    _ = dec.decrypt(c[0:183])
+    me.assertRaises(ValueError, dec.done, tag = t)
+
+    ## And now an overrun.
+    enc = key.enc(nonce = n, hsz = 0, msz = 183, tsz = tsz)
+    me.assertRaises(ValueError, enc.encrypt, m)
+    dec = key.dec(nonce = n, hsz = 0, csz = 183, tsz = tsz)
+    me.assertRaises(ValueError, dec.decrypt, c)
+
+    ## Finally, check that if we precommit to a tag length, we're properly
+    ## held to the commitment.  This depends on being able to find a tag size
+    ## which isn't the default.
+    tsz1 = different_key_size(aecls.tagsz, tsz)
+    if tsz1 is not None:
+      enc = key.enc(nonce = n, hsz = 0, msz = len(m), tsz = tsz1)
+      _ = enc.encrypt(m)
+      me.assertRaises(ValueError, enc.done, tsz = tsz)
+      dec = key.dec(nonce = n, hsz = len(h), csz = len(c), tsz = tsz1)
+      aad = dec.aad().hash(h)
+      _ = dec.decrypt(c)
+      me.assertRaises(ValueError, enc.done, tsz = tsz, aad = aad)
+
+TestAuthenticatedEncryption.generate_testcases \
+  ((name, C.gcaeads[name]) for name in
+   ["des3-ccm", "blowfish-ocb1", "square-ocb3", "rijndael-gcm",
+    "serpent-eax", "salsa20-naclbox", "chacha20-poly1305"])
+
+###--------------------------------------------------------------------------
 class BaseTestHash (HashBufferTestMixin):
   """Base class for testing hash functions."""