1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-07-01 11:32:48 -05:00

Expose CRC32 to testcrypt, and add tests for it.

Finding even semi-official test vectors for this CRC implementation
was hard, because it turns out not to _quite_ match any of the well
known ones catalogued on the web. Its _polynomial_ is well known, but
the combination of details that go alongside it (starting state,
post-hashing transformation) are not quite the same as any other hash
I know of.

After trawling catalogue websites for a while I finally worked out
that SSH-1's CRC and RFC 1662's CRC are basically the same except for
different choices of starting value and final adjustment. And RFC
1662's CRC is common enough that there _are_ test vectors.

So I've renamed the previous crc32_compute function to crc32_ssh1,
reflecting that it seems to be its own thing unlike any other CRC;
implemented the RFC 1662 CRC as well, as an alternative tiny wrapper
on the inner crc32_update function; and exposed all three functions to
testcrypt. That lets me run standard test vectors _and_ directed tests
of the internal update routine, plus one check that crc32_ssh1 itself
does what I expect.

While I'm here, I've also modernised the code to use uint32_t in place
of unsigned long, and ptrlen instead of separate pointer,length
arguments. And I've removed the general primer on CRC theory from the
header comment, in favour of the more specifically useful information
about _which_ CRC this is and how it matches up to anything else out
there.

(I've bowed to inevitability and put the directed CRC tests in the
'crypt' class in cryptsuite.py. Of course this is a misnomer, since
CRC isn't cryptography, but it falls into the same category in terms
of the role it plays in SSH-1, and I didn't feel like making a new
pointedly-named 'notreallycrypt' container class just for this :-)
This commit is contained in:
Simon Tatham
2019-01-14 20:45:19 +00:00
parent f71dce662e
commit c330156259
7 changed files with 141 additions and 83 deletions

View File

@ -3,6 +3,7 @@
import unittest
import struct
import itertools
import functools
import contextlib
import hashlib
import binascii
@ -935,6 +936,36 @@ class crypt(MyTestBase):
for d in decryptions:
self.assertEqualBin(d, decryptions[0])
def testCRC32(self):
# Check the effect of every possible single-byte input to
# crc32_update. In the traditional implementation with a
# 256-word lookup table, this exercises every table entry; in
# _any_ implementation which iterates over the input one byte
# at a time, it should be a similarly exhaustive test. (But if
# a more optimised implementation absorbed _more_ than 8 bits
# at a time, then perhaps this test wouldn't be enough...)
# It would be nice if there was a functools.iterate() which
# would apply a function n times. Failing that, making shift1
# accept and ignore a second argument allows me to iterate it
# 8 times using functools.reduce.
shift1 = lambda x, dummy=None: (x >> 1) ^ (0xEDB88320 * (x & 1))
shift8 = lambda x: functools.reduce(shift1, [None]*8, x)
# A small selection of choices for the other input to
# crc32_update, just to check linearity.
test_prior_values = [0, 0xFFFFFFFF, 0x45CC1F6A, 0xA0C4ADCF, 0xD482CDF1]
for prior in test_prior_values:
prior_shifted = shift8(prior)
for i in range(256):
exp = shift8(i) ^ prior_shifted
self.assertEqual(crc32_update(prior, struct.pack("B", i)), exp)
# Check linearity of the _reference_ implementation, while
# we're at it!
self.assertEqual(shift8(i ^ prior), exp)
class standard_test_vectors(MyTestBase):
def testAES(self):
def vector(cipher, key, plaintext, ciphertext):
@ -1367,6 +1398,50 @@ class standard_test_vectors(MyTestBase):
signature = unhex(words[3])[:64]
vector(privkey, pubkey, message, signature)
def testCRC32(self):
self.assertEqual(crc32_rfc1662("123456789"), 0xCBF43926)
self.assertEqual(crc32_ssh1("123456789"), 0x2DFD2D88)
# Source:
# http://reveng.sourceforge.net/crc-catalogue/17plus.htm#crc.cat.crc-32-iso-hdlc
# which collected these from various sources.
reveng_tests = [
'000000001CDF4421',
'F20183779DAB24',
'0FAA005587B2C9B6',
'00FF55111262A032',
'332255AABBCCDDEEFF3D86AEB0',
'926B559BA2DE9C',
'FFFFFFFFFFFFFFFF',
'C008300028CFE9521D3B08EA449900E808EA449900E8300102007E649416',
'6173640ACEDE2D15',
]
for vec in map(unhex, reveng_tests):
# Each of these test vectors can be read two ways. One
# interpretation is that the last four bytes are the
# little-endian encoding of the CRC of the rest. (Because
# that's how the CRC is attached to a string at the
# sending end.)
#
# The other interpretation is that if you CRC the whole
# string, _including_ the final four bytes, you expect to
# get the same value for any correct string (because the
# little-endian encoding matches the way the rest of the
# string was interpreted as a polynomial in the first
# place). That's how a receiver is intended to check
# things.
#
# The expected output value is listed in RFC 1662, and in
# the reveng.sourceforge.net catalogue, as 0xDEBB20E3. But
# that's because their checking procedure omits the final
# complement step that the construction procedure
# includes. Our crc32_rfc1662 function does do the final
# complement, so we expect the bitwise NOT of that value,
# namely 0x2144DF1C.
expected = struct.unpack("<L", vec[-4:])[0]
self.assertEqual(crc32_rfc1662(vec[:-4]), expected)
self.assertEqual(crc32_rfc1662(vec), 0x2144DF1C)
if __name__ == "__main__":
try:
unittest.main()