diff --git a/test/agenttest.py b/test/agenttest.py new file mode 100755 index 00000000..9e44d556 --- /dev/null +++ b/test/agenttest.py @@ -0,0 +1,252 @@ +#!/usr/bin/python3 + +import sys +import os +import socket +import base64 +import itertools +import collections + +from ssh import * +import agenttestdata + +test_session_id = b'Test16ByteSessId' +assert len(test_session_id) == 16 +test_message_to_sign = b'test message to sign' + +TestSig2 = collections.namedtuple("TestSig2", "flags sig") + +class Key2(collections.namedtuple("Key2", "comment public sigs openssh")): + def public_only(self): + return Key2(self.comment, self.public, None, None) + + def Add(self): + alg = ssh_decode_string(self.public) + msg = (ssh_byte(SSH2_AGENTC_ADD_IDENTITY) + + ssh_string(alg) + + self.openssh + + ssh_string(self.comment)) + return agent_query(msg) + + verb = "sign" + def Use(self, flags): + msg = (ssh_byte(SSH2_AGENTC_SIGN_REQUEST) + + ssh_string(self.public) + + ssh_string(test_message_to_sign)) + if flags is not None: + msg += ssh_uint32(flags) + rsp = agent_query(msg) + t, rsp = ssh_decode_byte(rsp, True) + assert t == SSH2_AGENT_SIGN_RESPONSE + sig, rsp = ssh_decode_string(rsp, True) + assert len(rsp) == 0 + return sig + + def Del(self): + msg = (ssh_byte(SSH2_AGENTC_REMOVE_IDENTITY) + + ssh_string(self.public)) + return agent_query(msg) + + @staticmethod + def DelAll(): + msg = (ssh_byte(SSH2_AGENTC_REMOVE_ALL_IDENTITIES)) + return agent_query(msg) + + @staticmethod + def List(): + msg = (ssh_byte(SSH2_AGENTC_REQUEST_IDENTITIES)) + rsp = agent_query(msg) + t, rsp = ssh_decode_byte(rsp, True) + assert t == SSH2_AGENT_IDENTITIES_ANSWER + nk, rsp = ssh_decode_uint32(rsp, True) + keylist = [] + for _ in range(nk): + p, rsp = ssh_decode_string(rsp, True) + c, rsp = ssh_decode_string(rsp, True) + keylist.append(Key2(c, p, None, None)) + assert len(rsp) == 0 + return keylist + + @classmethod + def make_examples(cls): + cls.examples = agenttestdata.key2examples(cls, TestSig2) + + def iter_testsigs(self): + for testsig in self.sigs: + if testsig.flags == 0: + yield testsig._replace(flags=None) + yield testsig + + def iter_tests(self): + for testsig in self.iter_testsigs(): + yield ([testsig.flags], + " (flags={})".format(testsig.flags), + testsig.sig) + +class Key1(collections.namedtuple( + "Key1", "comment public challenge response private")): + def public_only(self): + return Key1(self.comment, self.public, None, None, None) + + def Add(self): + msg = (ssh_byte(SSH1_AGENTC_ADD_RSA_IDENTITY) + + self.private + + ssh_string(self.comment)) + return agent_query(msg) + + verb = "decrypt" + def Use(self, challenge): + msg = (ssh_byte(SSH1_AGENTC_RSA_CHALLENGE) + + self.public + + ssh1_mpint(challenge) + + test_session_id + + ssh_uint32(1)) + rsp = agent_query(msg) + t, rsp = ssh_decode_byte(rsp, True) + assert t == SSH1_AGENT_RSA_RESPONSE + assert len(rsp) == 16 + return rsp + + def Del(self): + msg = (ssh_byte(SSH1_AGENTC_REMOVE_RSA_IDENTITY) + + self.public) + return agent_query(msg) + + @staticmethod + def DelAll(): + msg = (ssh_byte(SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES)) + return agent_query(msg) + + @staticmethod + def List(): + msg = (ssh_byte(SSH1_AGENTC_REQUEST_RSA_IDENTITIES)) + rsp = agent_query(msg) + t, rsp = ssh_decode_byte(rsp, True) + assert t == SSH1_AGENT_RSA_IDENTITIES_ANSWER + nk, rsp = ssh_decode_uint32(rsp, True) + keylist = [] + for _ in range(nk): + b, rsp = ssh_decode_uint32(rsp, True) + e, rsp = ssh1_get_mpint(rsp, True) + m, rsp = ssh1_get_mpint(rsp, True) + c, rsp = ssh_decode_string(rsp, True) + keylist.append(Key1(c, ssh_uint32(b)+e+m, None, None, None)) + assert len(rsp) == 0 + return keylist + + @classmethod + def make_examples(cls): + cls.examples = agenttestdata.key1examples(cls) + + def iter_tests(self): + yield [self.challenge], "", self.response + +def agent_query(msg): + msg = ssh_string(msg) + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(os.environ["SSH_AUTH_SOCK"]) + s.send(msg) + length = ssh_decode_uint32(s.recv(4)) + assert length < AGENT_MAX_MSGLEN + return s.recv(length) + +def enumerate_bits(iterable): + return ((1<>1) + diff = new ^ old + assert diff != 0 and (diff & (diff-1)) == 0 + yield old, new, diff + old = new + assert old == 0 + +class TestRunner: + def __init__(self): + self.ok = True + + @staticmethod + def fmt_response(response): + return "'{}'".format( + base64.encodebytes(response).decode("ASCII").replace("\n","")) + + @staticmethod + def fmt_keylist(keys): + return "{{{}}}".format( + ",".join(key.comment.decode("ASCII") for key in sorted(keys))) + + def expect_success(self, text, response): + if response == ssh_byte(SSH_AGENT_SUCCESS): + print(text, "=> success") + elif response == ssh_byte(SSH_AGENT_FAILURE): + print("FAIL!", text, "=> failure") + self.ok = False + else: + print("FAIL!", text, "=>", self.fmt_response(response)) + self.ok = False + + def check_keylist(self, K, expected_keys): + keys = K.List() + print("list keys =>", self.fmt_keylist(keys)) + if set(keys) != set(expected_keys): + print("FAIL! Should have been", self.fmt_keylist(expected_keys)) + self.ok = False + + def gray_code_test(self, K): + bks = list(enumerate_bits(K.examples)) + + self.check_keylist(K, {}) + + for old, new, diff in gray_code(len(K.examples)): + bit, key = next((bit, key) for bit, key in bks if diff & bit) + + if new & bit: + self.expect_success("insert " + key.comment.decode("ASCII"), + key.Add()) + else: + self.expect_success("delete " + key.comment.decode("ASCII"), + key.Del()) + + self.check_keylist(K, [key.public_only() for bit, key in bks + if new & bit]) + + def sign_test(self, K): + for key in K.examples: + for params, message, expected_answer in key.iter_tests(): + key.Add() + actual_answer = key.Use(*params) + key.Del() + record = "{} with {}{}".format( + K.verb, key.comment.decode("ASCII"), message) + if actual_answer == expected_answer: + print(record, "=> success") + else: + print("FAIL!", record, "=> {} but expected {}".format( + self.fmt_response(actual_answer), + self.fmt_response(expected_answer))) + self.ok = False + + def run(self): + self.expect_success("init: delete all ssh2 keys", Key2.DelAll()) + + for K in [Key2, Key1]: + self.gray_code_test(K) + self.sign_test(K) + + # TODO: negative tests of all kinds. + +def main(): + Key2.make_examples() + Key1.make_examples() + + tr = TestRunner() + tr.run() + if tr.ok: + print("Test run passed") + else: + sys.exit("Test run failed!") + +if __name__ == "__main__": + main() diff --git a/test/agenttestdata.py b/test/agenttestdata.py new file mode 100644 index 00000000..596c069b --- /dev/null +++ b/test/agenttestdata.py @@ -0,0 +1,14 @@ +# DO NOT EDIT DIRECTLY! Autogenerated by agenttestgen.py +# +# To regenerate, run +# python3 agenttestgen.py > agenttestdata.py +# +# agenttestgen.py depends on the testcrypt system, so you must also +# have built testcrypt in the parent directory, or else set +# PUTTY_TESTCRYPT to point at a working implementation of it. + + +def key2examples(Key2, TestSig2): + return [Key2(comment=b'RSA-1024', public=b'\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01%\x00\x00\x00\x81\x00\x87B\x16\x99\x8c96\x92\xe7\x00-\xc5\xf0%\x13:\xe5a?_\x14\xb5\x15{%\xed\xd4\rB\x98\x02~\xb0\xfdWc\xa6\x8fSz\xa7\xfd\x94\xe1\xcegx\xe3\x14\xba\x87A4\xef\xb0\x056\x9c\x80r\x18\xd7Q\xb69\xed\x9a5\xba\x8b\xf8\xee\x84F\xceD\xfa\xccn\xd6\x9ba8\x8f\xb5\x9dz\x0b\xf1\xa3\xe9vH\x1dr\r[x\xbb\xd9\xd6\xf3\xcb~W\x8fYu\xd5|G)\xa9\xa8_\x91A\x1f\xef\x80\x83\xb3jp)\xef\xe8\x05', sigs=[TestSig2(flags=0, sig=b'\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x801\xaa\xb4t\xe10\x83Q\xe4\x18\x84\x1e\xdeN$\xde;\t\xf9\xae"H\r>\xa9\x91B"\xfd\x01\\19\xee*\xb9\xc1\x8a\x1b*:\xc3\t\x91\x85\xae\x7f\xf8\x84\x08\xbd\x89P\xa9\xdb\t\x8fc\x95\xa4\xcb\xda\x19<\x14\xc4\x1a|\n\xef)\xf4\xc8\xfb\xc1\x04"\xf6\x8a9\xac\xec\xa6x>\xd3-\xb1\xf7\x1e\x04\x8a\xd4k\xcb\x12\xf9\xc1\xaa\x1aV\xc3\xb8\xdd\xf8\x01\x10Z\xdd\x8c\xcd\x12w\x83pJOr\xb8\xed\x84\xa5\xf5&\r:\xd7H'), TestSig2(flags=2, sig=b"\x00\x00\x00\x0crsa-sha2-256\x00\x00\x00\x80J)H\xfe\x18t\xc8\xa9$\x07*\xc9\x16\xb3\\\x8cK\x7f\xdd\xd8\xb0g\xed\x10o\xac\xe8\xd3\xefQj\xe2\x9fi\x13\x9b\x93\x07`}\x12\x9f\xc1Y\x19{\xb8\xc0\x8c\xe6\x03\xfd\x8d\xc1\xfat\xf0T\x055\x02r:AOM\x18x\xb6:\xb7g\xe5k\x12$\xabX)\xf8\xe9\x12\xa2\x04\xff\xfa^\xc7 G\x9c7\x92\x03>^\x00,\x1e\x063\x16\x9b\xd4.'\x01\xa4Lv\xd2\xae\xf0\xc0\xed\x8d\xf6'aj\x1aq`\xc3\x85\x08\xc2\x8a"), TestSig2(flags=4, sig=b'\x00\x00\x00\x0crsa-sha2-512\x00\x00\x00\x80;k\xd5\xf4\xe8\xec\xeb\x8b\xe13L}\x96\x918?\xd9\x90\t\x9dN\xec+|<\xc1\xc6\x10\xf6]y\x8c\xf9a\xd5\x07c:\x90\x7fzBQ\xe49\x87s\x1d\x81Kz\xe9\xee\n\xf3|\xee6\x84A\xd0\xec\xc0\xf9h\xc5$\x13\xd8r\xa2j;\xb6$?*\xd59\xcb\xdf\x85\x19U\x1f\x10\xb4\xd8C\xbb"`\x8a)\x0fy`|\xd7\xa3\xec`tw\x8a>(\x0fj\x08\xbc\x92\xd8K\xb0qP\xbe\xd9\xaf\x91\x16\xd7\x9a\x9a\r\x9d\xe5')], openssh=b'\x00\x00\x00\x81\x00\x87B\x16\x99\x8c96\x92\xe7\x00-\xc5\xf0%\x13:\xe5a?_\x14\xb5\x15{%\xed\xd4\rB\x98\x02~\xb0\xfdWc\xa6\x8fSz\xa7\xfd\x94\xe1\xcegx\xe3\x14\xba\x87A4\xef\xb0\x056\x9c\x80r\x18\xd7Q\xb69\xed\x9a5\xba\x8b\xf8\xee\x84F\xceD\xfa\xccn\xd6\x9ba8\x8f\xb5\x9dz\x0b\xf1\xa3\xe9vH\x1dr\r[x\xbb\xd9\xd6\xf3\xcb~W\x8fYu\xd5|G)\xa9\xa8_\x91A\x1f\xef\x80\x83\xb3jp)\xef\xe8\x05\x00\x00\x00\x01%\x00\x00\x00\x80 \xe6\x8f\xe0)\x06\xffo\xd7S\x12\r\x8dp\xcdS\x83\xe78\xed\x9d@\xcd\xdf\xafG\xb0\x1e\xe6\xafZ\x8d\x84\xffZr/n\xf8\xa1Ku\x08\x89\xf3\xef\xa7\xc8\x88\x80f\x16\xc7\xaf\xec\x8b\xa5\x80\x03\x91_\xfd\x06\t_\xc5\xbf\xc1\xcb\xe3\r12k\xaeY\xe4RA\xab\xf1\xba\x8a\x99\xcf\xaf\x06\xf1\x82\xc6\x9d\xd5g[\xb2\xfb\xd8\xbc\x9b]\xb9l\t\xb2\x0b\xc9\x98JK\xfe\x8a\xd6\xfc[\xd19\xe7N|\xa6m\x9ei\xce&\xc6\xfcM\x00\x00\x00A\x00\x95\xc13\x16\xd2O*\xbc8\xc6{D\xb2\xe5\x85\x9ao\xf9\xfc.\xea\xe7\x9d\xca4}\x9c7\xca\xe1\x1e\xdb`o\x88w\x0c\xa3\xba\xde\nF\xb7\xf2x}\xd9\x00\x00\x00A\x00\xa3h\x11\x1a\x99\xc6\xa4\x0eAj\x93\xff\xa0&D(\x05#MoE\xdb\x8d\xf4\x99\xc8\xd2\xffv\xf1\x90C~\xe4\xed\xce\x1f\x85\x9f\x92\xce\xacMR\xd7\x0c\xb9z\x87\xea\xe97/\x97\xbd\x19q:TLB\xb7$\r'), Key2(comment=b'DSA-1024', public=b'\x00\x00\x00\x07ssh-dss\x00\x00\x00\x81\x00\xbc\x0c\xb0L\xfc<\x03TyZQ\xe1\xef\xd4\xd5\xe4\xa2\xb3\xaf\x14t\x0f\\,!E\xdbf\x01\x9e\x95\xddr\xeb\xab\xae;\xc1\xe3\x0c\xe9\xd9\x15\xc2\xa9\xc3g\x04\xa5\xf1\x965\xf1\x81\x9dS\x9c\x83en\x93\x11\xe0p\n)\xdaZ\x17y\xff\xf2\xbf\x9b[;"E1\xf0\xde\xbd\xe1;\x9a6Xnc\x8f\xd3\x1dg\xd1\x80\xa9\x8em\x86t\xc8\xa9!\xcd\xb3\xe4mx\xd5\x93%R\xbb9u\xd2\x99p\xe2\xbe\xf3\xfb%\xebd\xc4\x86\xe3\x00\x00\x00\x15\x00\xec^\x98\x84x\xc1\xa8`\xcfB\xc7\x1e\xf0\x8d\xd3\x89\xa3\xa8\xec\xc7\x00\x00\x00\x80Q\xecf\xfd\xcc\x9a+\xcclxu\x1b\x0b\xd7\xfd\xedDP\xd1\x82~H\x0eqn\x1e\xed\xd8\xad\xe9\xe3\xf8!\x1d\\\xb4\xde\xc1e\xd7\xc0(\x1dpQ\xee\xad\xeez\xe1\xb3\xa4\x12d\x92\x1a\x89\x82u\x99\xa3$\x85\x9c\xb840\xe7\xd6&O\x85~\xd6\xac\x1eq\xa1\x06\xa2\xd1ro\xd0}>\xc0O\xaf\x8a\xf8@B\r7\xf6\x89\xda\xd0\xb9\x0e\xb6\xae\xcdh\x1a\x86%\xdcN\x8cE\xd8\xcd(\x19\xa6y\x9a\xc0\xc2\xd6;\xc3\xc9{\x104\x00\x00\x00\x80`\xc1\xe8\x18\xe4\xd0\x16\xf9[l\xce\\L*\x19\x14"20K\xc6\x18*\xe5\x91\x80\xbf+\xec\xfe\xd3D\xf7\xa6\xf9Y0x#sl\x88BU\x7f\x1c\r\xb3\x08EL\x86\xa2\x8c\x81\x15pD\xac\x9c\xa3\x8e\x02\x89\xb9\xb6\x9f\xaa\xd0\xc8\x89o\x81Qm\x18f0\\\x92\x1f\xbf\xa1\x8d8\x8a\xb1\xec\xb8G\xbd9b\x8d\x7f\x9bk\xb9x\xe5\xde\xce/\'f\xc4\x8bmX\xd9 \xb4\xd5\xe8\xd1\xe1\xc8\xeb\xe7\xbc2LG\x90]\\%\x9f', sigs=[TestSig2(flags=0, sig=b"\x00\x00\x00\x07ssh-dss\x00\x00\x00(\xeb\xf2\xb0 2(\x93a\xfc\x0f\xad\x1al\xd5\xd0n\xd5\x10\x9d\\G\x18]?\xf4h5D\x12WL\xe6#\xa0\x89'\x17\xd3;\xb7")], openssh=b'\x00\x00\x00\x81\x00\xbc\x0c\xb0L\xfc<\x03TyZQ\xe1\xef\xd4\xd5\xe4\xa2\xb3\xaf\x14t\x0f\\,!E\xdbf\x01\x9e\x95\xddr\xeb\xab\xae;\xc1\xe3\x0c\xe9\xd9\x15\xc2\xa9\xc3g\x04\xa5\xf1\x965\xf1\x81\x9dS\x9c\x83en\x93\x11\xe0p\n)\xdaZ\x17y\xff\xf2\xbf\x9b[;"E1\xf0\xde\xbd\xe1;\x9a6Xnc\x8f\xd3\x1dg\xd1\x80\xa9\x8em\x86t\xc8\xa9!\xcd\xb3\xe4mx\xd5\x93%R\xbb9u\xd2\x99p\xe2\xbe\xf3\xfb%\xebd\xc4\x86\xe3\x00\x00\x00\x15\x00\xec^\x98\x84x\xc1\xa8`\xcfB\xc7\x1e\xf0\x8d\xd3\x89\xa3\xa8\xec\xc7\x00\x00\x00\x80Q\xecf\xfd\xcc\x9a+\xcclxu\x1b\x0b\xd7\xfd\xedDP\xd1\x82~H\x0eqn\x1e\xed\xd8\xad\xe9\xe3\xf8!\x1d\\\xb4\xde\xc1e\xd7\xc0(\x1dpQ\xee\xad\xeez\xe1\xb3\xa4\x12d\x92\x1a\x89\x82u\x99\xa3$\x85\x9c\xb840\xe7\xd6&O\x85~\xd6\xac\x1eq\xa1\x06\xa2\xd1ro\xd0}>\xc0O\xaf\x8a\xf8@B\r7\xf6\x89\xda\xd0\xb9\x0e\xb6\xae\xcdh\x1a\x86%\xdcN\x8cE\xd8\xcd(\x19\xa6y\x9a\xc0\xc2\xd6;\xc3\xc9{\x104\x00\x00\x00\x80`\xc1\xe8\x18\xe4\xd0\x16\xf9[l\xce\\L*\x19\x14"20K\xc6\x18*\xe5\x91\x80\xbf+\xec\xfe\xd3D\xf7\xa6\xf9Y0x#sl\x88BU\x7f\x1c\r\xb3\x08EL\x86\xa2\x8c\x81\x15pD\xac\x9c\xa3\x8e\x02\x89\xb9\xb6\x9f\xaa\xd0\xc8\x89o\x81Qm\x18f0\\\x92\x1f\xbf\xa1\x8d8\x8a\xb1\xec\xb8G\xbd9b\x8d\x7f\x9bk\xb9x\xe5\xde\xce/\'f\xc4\x8bmX\xd9 \xb4\xd5\xe8\xd1\xe1\xc8\xeb\xe7\xbc2LG\x90]\\%\x9f\x00\x00\x00\x15\x00\x85mio\xa4\xa2\xcc\xadnC\x94\x84I*;\xf40\xc9\xd7\xa9'), Key2(comment=b'ECDSA-p256', public=b'\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00\x08nistp256\x00\x00\x00A\x04D\x01\'\xac\xa9\xeaJ#\x1e\x80\x1e\xd2R"xq\xb2h\x128c\x11\xf5\xe8\x19,;\x1d\xcf\xf4h\x8c\xaeQ\xee\x15\xc2\xdb\xc77\x80\xc4\xc2\x15\x1d0s\xe1\xbfa\xd9}pz\xc6af4d\xbd\xc6\xc6\x1as', sigs=[TestSig2(flags=0, sig=b'\x00\x00\x00\x13ecdsa-sha2-nistp256\x00\x00\x00J\x00\x00\x00!\x00\xe0\x0b\xa1\x01\xc1\xc6A\x0c\x0c\x02\x9f\xc2B\x18G=\xfa+\xde\x8a/)x\xea\xc0R\xc0\x92\xe9\t?\xc7\x00\x00\x00!\x00\xa3\xcd^!\xa5\x0f"\xcd\x9c\x82\xe0\x01\x9c\xf1\xcaa\x83\x83\x848\xe4\xfb\xd9p]\xe1\xcc<\xdb\xde\x99\xe8')], openssh=b'\x00\x00\x00\x08nistp256\x00\x00\x00A\x04D\x01\'\xac\xa9\xeaJ#\x1e\x80\x1e\xd2R"xq\xb2h\x128c\x11\xf5\xe8\x19,;\x1d\xcf\xf4h\x8c\xaeQ\xee\x15\xc2\xdb\xc77\x80\xc4\xc2\x15\x1d0s\xe1\xbfa\xd9}pz\xc6af4d\xbd\xc6\xc6\x1as\x00\x00\x00!\x00\xc3\x8b\xc3A\xde\xfd\xd4\xcb\xf7\x9c\xa0\xc7L\xd1\xb0\xfe\x8e\xf2\xf6o\xe4"\x88K\x15p0\xbc\x0b\x19\xa7\xad'), Key2(comment=b'Ed25519', public=b'\x00\x00\x00\x0bssh-ed25519\x00\x00\x00 \xd6\x9aC\xb8\xa4\x1b\x95\x8a\x97\x9a\x9d\x95\xaa5\xd5\x9b\xc3B\xd2\xd1*\x85\xde.E"\x1c\xe9\xef\xc0\x06\xdb', sigs=[TestSig2(flags=0, sig=b'\x00\x00\x00\x0bssh-ed25519\x00\x00\x00@\xda\xcbw6\xff\xb9\xc1)\x1e\xfa\xef/s!u\xe1\xc0%\xd9-;\x97<\xbc2o\x1a\xef\x17\xd3\x8dvU\xbf\x8d,\xed\xe8S\xe8\xc27\xdb\x1d\xe7\xda\\\xce\xdc\x00\xad\x97BpC\x9a\xec\xd3\r1\x9f\xc0n\x0b')], openssh=b'\x00\x00\x00 \xd6\x9aC\xb8\xa4\x1b\x95\x8a\x97\x9a\x9d\x95\xaa5\xd5\x9b\xc3B\xd2\xd1*\x85\xde.E"\x1c\xe9\xef\xc0\x06\xdb\x00\x00\x00@\rV\xff\xf36D\xe3\xb2\xcc\xda\xa1\x11\x9d\xa2\xa7\xc0\'}C\xcb"\xf0x\rlL\xfc\xcc\x99\x10\x91\xc8\xd6\x9aC\xb8\xa4\x1b\x95\x8a\x97\x9a\x9d\x95\xaa5\xd5\x9b\xc3B\xd2\xd1*\x85\xde.E"\x1c\xe9\xef\xc0\x06\xdb')] +def key1examples(Key1): + return [Key1(comment=b'RSA-1024a', public=b"\x00\x00\x04\x00\x00\x06%\x04\x00\x98\x8d\xac\xa1\xff\xd1\x05\xd4@\x93\x11\xfc\xd8\xb5\x8c\x18\xa8/\x9ePh\x06y,\xc1\xdd\xc2q\x90\xe0g\xebIgl\x12\xacs.\xc1\xd7\xd0,\x8d\xd4\xa4\xd1\x88F\x1dW\xa6\xb9\x808+0u`B\xa8\xd2z\x0c>}\xaeA\xa7\x945\x91\x0c\xd5@5s\xa8R\xc31\xc5\x8e'\xec6\x00\x98\xdd\x0b\x93\xa8\x8e\xe6\xa9\x19\xa2\xbaf\xd6\xa8@\x1b\x82\xf4\xf5j\xc4\x06\xdd\x08\x7f\xcce\xcdc%\xc4W\xd7k\xd2\xe3\xcf\xa2\xbaI=", challenge=105855771610781217219148893240371739231586890974005582821084910669621353003219053895226458126049169949952763904000891276244889049724351186264170655451406153989624471223276671179692313150356649135789040179224142632652879598695018927369451625737003799565133274183885945180682771324880529165922297762364545903323, response=b'U\xb2\xdf(\xb8\xc7\xf5\r\x16\xa0%O9l\xdb\xf0', private=b'\x00\x00\x04\x00\x04\x00\x98\x8d\xac\xa1\xff\xd1\x05\xd4@\x93\x11\xfc\xd8\xb5\x8c\x18\xa8/\x9ePh\x06y,\xc1\xdd\xc2q\x90\xe0g\xebIgl\x12\xacs.\xc1\xd7\xd0,\x8d\xd4\xa4\xd1\x88F\x1dW\xa6\xb9\x808+0u`B\xa8\xd2z\x0c>}\xaeA\xa7\x945\x91\x0c\xd5@5s\xa8R\xc31\xc5\x8e\'\xec6\x00\x98\xdd\x0b\x93\xa8\x8e\xe6\xa9\x19\xa2\xbaf\xd6\xa8@\x1b\x82\xf4\xf5j\xc4\x06\xdd\x08\x7f\xcce\xcdc%\xc4W\xd7k\xd2\xe3\xcf\xa2\xbaI=\x00\x06%\x04\x00\x94n+m0@\xfe\xc0\xad\x88--];\x04\xd9\xb8e\xae\xca\xc6\x14"\xdfp\x84\xbd09\xef\x19\x00\x9arv\xfdi\x84\xd3\x8c+\xed$nR[,\xbb\xf11N]\x07\x83\xacE\xb2\x9b\xb7\x9a\xcd\xc5\xde\x86\xe7\xf9O\xf9\xa0\xce\xca\x085\xe7\xb7En\x94\x1e;T)SH\xff\x85\xe5\x8d\x1f\xa6,\xc7\xffV\x80\xf2E=\x08\x8e\xe6\x9e\xb2}py\xad\xa55Z\xf4\xed\xc8^+tnIN)vE\xbd\x82\xb665\xdd\x01\xfb\x04d\xa4\xb80\x98l\xd0!l\x99[\x12\x1c\x85\xda\xa5\xd0#\xcc\x7f\x80\xcdE~\x8bF \xf1;\x9d\xfb\xc0\xa3\x93\xa2\xb89^\x91D\xb7\xd3\x18\xbdx\x93U\xc3{\xa4\t,F\xb5\xdd\xadk\xc9@\xd0$\xc6\x03\x02\x00\x9a\xa4\\\xb4\xe5\x05\xe0&\xb0\x06\xc3\x9dQ`\xe4w\xe3-6N "\xa2\x9a%\xf16T\x92%\x16\xf9\xfc\xb5\xc6\xc4\xbb\xa8\xf7?\xa7"\xa1\x9do\x10A3\x14\x12\x18Uj\x19k\x19\x93\x99\xc9\xab\xfa\xa7\x15\xcb\x02\x00\xfc\x8a\xdb\xcc2\x9d[\x1a\xd0\x12\x1c\xad7\xbdk\xaa\xc6Ql\xeb7;\x87f\x8fv\xafM\x8b\xa8\xaa\n40\x90)\xb8t\tBaU\xba<\xcb\xa1\x12\xad\xaad\xb3\x0e\xf4\xfc\x07\x13;\x1c\x17]P[|\x17'), Key1(comment=b'RSA-1024b', public=b'\x00\x00\x04\x00\x00\x06%\x04\x00\x97\xe4sJ\xf8i\x83\x9f\xe8k%\xc6\xb7\xcbm!\xd7\xdd\xd5!N\xad<8\x0e\x1f\xa15yV\xbcr\xec\x8c\xca\x94\xed\x0c\xdbDC\x9e\xe1\xf5\xe4_\xb6>\x19\xe0\xdf\xb1te\xc7n\x86\xf7\xd15\x9e\xfc\x81\x90V\x92\xae\x1cb\xcc\xde\x05k\x8eNIa\x87\x1a\x8aG\xdd\xc9\xc9K\xfe\xb3W\xb0%\xe2\x10bs\x18\xe2\x07I\xf8\x88P\x04i!\xa9\xdd5\x12\x12\xdbp\x06\x03\xbb\x0e(\x82\x0e\xe7\xe7r\x17\xdbN\x91\x141Q', challenge=106369452810277819728395930691403679528939443121481728310811020449278450935158409081846069415863931371431145424909167586350456375465223628112328681772147389155179853401808803792809242283837570604791348990555643874877574733757512944970974196016255159184273573080435875970788050018918135917906633435695051286334, response=b'U\xb2\xdf(\xb8\xc7\xf5\r\x16\xa0%O9l\xdb\xf0', private=b'\x00\x00\x04\x00\x04\x00\x97\xe4sJ\xf8i\x83\x9f\xe8k%\xc6\xb7\xcbm!\xd7\xdd\xd5!N\xad<8\x0e\x1f\xa15yV\xbcr\xec\x8c\xca\x94\xed\x0c\xdbDC\x9e\xe1\xf5\xe4_\xb6>\x19\xe0\xdf\xb1te\xc7n\x86\xf7\xd15\x9e\xfc\x81\x90V\x92\xae\x1cb\xcc\xde\x05k\x8eNIa\x87\x1a\x8aG\xdd\xc9\xc9K\xfe\xb3W\xb0%\xe2\x10bs\x18\xe2\x07I\xf8\x88P\x04i!\xa9\xdd5\x12\x12\xdbp\x06\x03\xbb\x0e(\x82\x0e\xe7\xe7r\x17\xdbN\x91\x141Q\x00\x06%\x03\xfe1C,O\xaa\x83\x15\xee\xac>m\x1d\xda\xbe\x84BS\xd9>4P\xde=\x0bB\xd9\xd3kI\xf2\x9d\xfb\xc2W,\xf2\x07\xb1$\x84\xd7\xa9&\xb0\x9d\x18\x1fn\x16;\x18\x1d\xe0\x8f\xb6M\\4\xb2\x8d\xee_\xbbP\xe7 j!\xc7W\xcb\xc9\x19\nM\x90\xfe4\xe5U\xef[\xdc&A\xde\xd9\x84\x02\xdek\xec\xaf\xb4\xd0I\xaaR\xa6\xc1\x8b\xbc\x13\xf1?,\xc6{\n\x02p\xa7\'\xa1\xb9\xf8\x1f\xeb\x99\xe2\xcf\xc4%"+Mu\x9d\x02\x00\xc4\x89W\x05\x8f\xff\xabX6\x8f\x9fQ\x19\xb8\xc2\xc2Y|\xa90g\xa9\xa7\xa9\x17 \xeb\xbbSMd\xf4YW\xa7\x93\xfcn\xc3AI\x04tK\x1a\xd74`\xec]+\xd8\x91`W@\xc7\xa6G\x82\x99\xac\x8c\x9b\x02\x00\xacs\x1e6\xa5\x10\xb2\xd1\xc9|\x87\x15\xb6\xd9*\x05O\x9e\x95\xec\x1f\xac\xbc/2\xc1\xdb\xa7\x97w@\xfe?d\xb2|\xd4\x96\x02\xc8y\xdf; \x89\x0b'), Key1(comment=b'RSA-768d', public=b'\x00\x00\x03\x00\x00\x06%\x03\x00\x9e4w\xb6C\x1c\xb1\xcdV\x96\t\x14\x04T\xb5\xca\x0ct?a8\xfd-\xb1l\x83/\xc3\x95\x97\x8b \xcdZW\x15\x87G\xa8\x1d\xea(\x1d\x03V\xe8\xe8/M.\xe6\xd6\x8d,\xf3>$"R\xcciYwp*\xc7z\x0c\xc3/k\x87\xc1{4\x1bw\xf1\x00\xda~\x84\x0e\xb0)\'\x84\x9e<\xd1\x19\x18\x81\xcb\xffo', challenge=600986133602165113984107876330614577281000470334319933978738264340033662158902949574073710382789301043986608302703563989903269508211841666685953915670687064469873711668788199922957307325206107166514327102609966835942325119838536897, response=b'U\xb2\xdf(\xb8\xc7\xf5\r\x16\xa0%O9l\xdb\xf0', private=b'\x00\x00\x03\x00\x03\x00\x9e4w\xb6C\x1c\xb1\xcdV\x96\t\x14\x04T\xb5\xca\x0ct?a8\xfd-\xb1l\x83/\xc3\x95\x97\x8b \xcdZW\x15\x87G\xa8\x1d\xea(\x1d\x03V\xe8\xe8/M.\xe6\xd6\x8d,\xf3>$"R\xcciYwp*\xc7z\x0c\xc3/k\x87\xc1{4\x1bw\xf1\x00\xda~\x84\x0e\xb0)\'\x84\x9e<\xd1\x19\x18\x81\xcb\xffo\x00\x06%\x02\xfe"4\xdb\x9d\x07\x97\x80c\xbf\xb1\xbc\xc6\x0e\xc65$\xc4l)a!\x14%\x8e%L\xcc\x0e\x9c\xe2~\xf2U\xea\x04\xfd\xbcb\x856\xe6\x856\xb4\x9d+px\x97\x0c\x17\xde\x93\xd8zOAO\xea\xab\xa2p\x14\x87\xf5\x1c\x00\xb2\xa3A\x08\x14\xe9v\xb8\xd2\xbf\x03C\xf2\xa3\xfeV/BZ\x11\x82#\xff)\xf8\x93\x0f\xf0m\x01\x80\x85\xde\xf4\x1b\xd2Pu\xf3\xd8\xf8Z\xabZ\x92q\xef\xab\x06\x85\xcd\xc3\x85\xd6?j\xd4\xaa\x96l\xeeRZ\xab8\x05\x84xT\xdd\x15j\xea9O_\xf4`R\x01\x80\xc4\xeb\x12\x92\xdc\x05\\b\x8c\xec\x11\x10\xc5\xaa\xdf\x97\x1eD\x92\x06_\xb3\xc28C\xceH\xa7\x8a\xf2F\xe6+\x01\xbf\xad\x81\x99&2\xcc\xff\xa2\xaad\x04\x08\x8d\x01\x80\xcd\xab\xe5\xdeE^a-\t$\xa4a\xd4h8\xe4>\xe1d\xcc0n\xe3\xee\xc5\xe7\xd4\xa59\x8f\x9f\xb2\x1d\n\x00h\x14\xad\xcdq\x89UTPu\x9e>\xeb')] diff --git a/test/agenttestgen.py b/test/agenttestgen.py new file mode 100755 index 00000000..bc2068bc --- /dev/null +++ b/test/agenttestgen.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +def generate(): + import hashlib + + print("""\ +# DO NOT EDIT DIRECTLY! Autogenerated by agenttestgen.py +# +# To regenerate, run +# python3 agenttestgen.py > agenttestdata.py +# +# agenttestgen.py depends on the testcrypt system, so you must also +# have built testcrypt in the parent directory, or else set +# PUTTY_TESTCRYPT to point at a working implementation of it. + +""") + + from testcrypt import (rsa_generate, dsa_generate, ecdsa_generate, + eddsa_generate, random_clear, random_queue, + ssh_key_public_blob, ssh_key_openssh_blob, + ssh_key_sign, rsa1_generate, rsa_ssh1_encrypt, + rsa_ssh1_public_blob, rsa_ssh1_private_blob_agent, + mp_from_bytes_be) + from agenttest import (Key2, TestSig2, test_message_to_sign, + Key1, test_session_id) + import ssh + + keygen2 = [ + ('RSA-1024', lambda: rsa_generate(1024), + (ssh.SSH_AGENT_RSA_SHA2_256, ssh.SSH_AGENT_RSA_SHA2_512)), + ('DSA-1024', lambda: dsa_generate(1024)), + ('ECDSA-p256', lambda: ecdsa_generate(256)), + ('Ed25519', lambda: eddsa_generate(256)), + ] + + keys2 = [] + + for record in keygen2: + if len(record) == 2: + record += ((),) + comment, genfn, flaglist = record + flaglist = (0,) + flaglist + + random_clear() + random_queue(b''.join(hashlib.sha512('{}{:d}'.format(comment, j) + .encode('ASCII')).digest() + for j in range(1000))) + key = genfn() + sigs = [TestSig2(flags, ssh_key_sign(key, test_message_to_sign, flags)) + for flags in flaglist] + + keys2.append(Key2(comment.encode("ASCII"), + ssh_key_public_blob(key), + sigs, + ssh_key_openssh_blob(key))) + + print("def key2examples(Key2, TestSig2):\n return {!r}".format(keys2)) + + keygen1 = [ + ('RSA-1024a', 1024), + ('RSA-1024b', 1024), + ('RSA-768c', 768), + ('RSA-768d', 768), + ] + + keys1 = [] + + for comment, bits in keygen1: + random_clear() + random_queue(b''.join(hashlib.sha512('{}{:d}'.format(comment, j) + .encode('ASCII')).digest() + for j in range(1000))) + key = rsa1_generate(bits) + preimage = b'Test128BitRSA1ChallengeCleartext' + assert len(preimage) == 32 + challenge_bytes = rsa_ssh1_encrypt(preimage, key) + assert len(challenge_bytes) > 0 + challenge = int(mp_from_bytes_be(challenge_bytes)) + response = hashlib.md5(preimage + test_session_id).digest() + + keys1.append(Key1(comment.encode("ASCII"), + rsa_ssh1_public_blob(key, 'exponent_first'), + challenge, response, + rsa_ssh1_private_blob_agent(key))) + + print("def key1examples(Key1):\n return {!r}".format(keys1)) + +if __name__ == "__main__": + generate() diff --git a/test/cryptsuite.py b/test/cryptsuite.py index 6d77e685..2bf9428e 100755 --- a/test/cryptsuite.py +++ b/test/cryptsuite.py @@ -15,41 +15,16 @@ except ImportError: from eccref import * from testcrypt import * +from ssh import * try: base64decode = base64.decodebytes except AttributeError: base64decode = base64.decodestring -def nbits(n): - # Mimic mp_get_nbits for ordinary Python integers. - assert 0 <= n - smax = next(s for s in itertools.count() if (n >> (1 << s)) == 0) - toret = 0 - for shift in reversed([1 << s for s in range(smax)]): - if n >> shift != 0: - n >>= shift - toret += shift - assert n <= 1 - if n == 1: - toret += 1 - return toret - def unhex(s): return binascii.unhexlify(s.replace(" ", "").replace("\n", "")) -def ssh_uint32(n): - return struct.pack(">L", n) -def ssh_string(s): - return ssh_uint32(len(s)) + s -def ssh1_mpint(x): - bits = nbits(x) - bytevals = [0xFF & (x >> (8*n)) for n in range((bits-1)//8, -1, -1)] - return struct.pack(">H" + "B" * len(bytevals), bits, *bytevals) -def ssh2_mpint(x): - bytevals = [0xFF & (x >> (8*n)) for n in range(nbits(x)//8, -1, -1)] - return struct.pack(">L" + "B" * len(bytevals), len(bytevals), *bytevals) - def rsa_bare(e, n): rsa = rsa_new() get_rsa_ssh1_pub(ssh_uint32(nbits(n)) + ssh1_mpint(e) + ssh1_mpint(n), diff --git a/test/ssh.py b/test/ssh.py new file mode 100644 index 00000000..2fe9f9a3 --- /dev/null +++ b/test/ssh.py @@ -0,0 +1,98 @@ +import struct +import itertools + +def nbits(n): + # Mimic mp_get_nbits for ordinary Python integers. + assert 0 <= n + smax = next(s for s in itertools.count() if (n >> (1 << s)) == 0) + toret = 0 + for shift in reversed([1 << s for s in range(smax)]): + if n >> shift != 0: + n >>= shift + toret += shift + assert n <= 1 + if n == 1: + toret += 1 + return toret + +def ssh_byte(n): + return struct.pack("B", n) + +def ssh_uint32(n): + return struct.pack(">L", n) + +def ssh_string(s): + return ssh_uint32(len(s)) + s + +def ssh1_mpint(x): + bits = nbits(x) + bytevals = [0xFF & (x >> (8*n)) for n in range((bits-1)//8, -1, -1)] + return struct.pack(">H" + "B" * len(bytevals), bits, *bytevals) + +def ssh2_mpint(x): + bytevals = [0xFF & (x >> (8*n)) for n in range(nbits(x)//8, -1, -1)] + return struct.pack(">L" + "B" * len(bytevals), len(bytevals), *bytevals) + +def decoder(fn): + def decode(s, return_rest = False): + item, length_consumed = fn(s) + if return_rest: + return item, s[length_consumed:] + else: + return item + return decode + +@decoder +def ssh_decode_byte(s): + return struct.unpack_from("B", s, 0)[0], 1 + +@decoder +def ssh_decode_uint32(s): + return struct.unpack_from(">L", s, 0)[0], 4 + +@decoder +def ssh_decode_string(s): + length = ssh_decode_uint32(s) + assert length + 4 <= len(s) + return s[4:length+4], length+4 + +@decoder +def ssh1_get_mpint(s): # returns it unconsumed, still in wire encoding + nbits = struct.unpack_from(">H", s, 0)[0] + nbytes = (nbits + 7) // 8 + assert nbytes + 2 <= len(s) + return s[:nbytes+2], nbytes+2 + +@decoder +def ssh1_decode_mpint(s): + nbits = struct.unpack_from(">H", s, 0)[0] + nbytes = (nbits + 7) // 8 + assert nbytes + 2 <= len(s) + data = s[2:nbytes+2] + v = 0 + for b in struct.unpack("B" * len(data), data): + v = (v << 8) | b + return v, nbytes+2 + +AGENT_MAX_MSGLEN = 262144 + +SSH1_AGENTC_REQUEST_RSA_IDENTITIES = 1 +SSH1_AGENT_RSA_IDENTITIES_ANSWER = 2 +SSH1_AGENTC_RSA_CHALLENGE = 3 +SSH1_AGENT_RSA_RESPONSE = 4 +SSH1_AGENTC_ADD_RSA_IDENTITY = 7 +SSH1_AGENTC_REMOVE_RSA_IDENTITY = 8 +SSH1_AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9 +SSH_AGENT_FAILURE = 5 +SSH_AGENT_SUCCESS = 6 +SSH2_AGENTC_REQUEST_IDENTITIES = 11 +SSH2_AGENT_IDENTITIES_ANSWER = 12 +SSH2_AGENTC_SIGN_REQUEST = 13 +SSH2_AGENT_SIGN_RESPONSE = 14 +SSH2_AGENTC_ADD_IDENTITY = 17 +SSH2_AGENTC_REMOVE_IDENTITY = 18 +SSH2_AGENTC_REMOVE_ALL_IDENTITIES = 19 +SSH2_AGENTC_EXTENSION = 27 + +SSH_AGENT_RSA_SHA2_256 = 2 +SSH_AGENT_RSA_SHA2_512 = 4