diff --git a/p11proxy-mitm b/p11proxy-mitm new file mode 100755 index 0000000000000000000000000000000000000000..013f161baac7e3ec37d62c1e2421bfb6936c9df9 --- /dev/null +++ b/p11proxy-mitm @@ -0,0 +1,528 @@ +#!/usr/bin/python +# +# Copyright (c) 2012, 2013, NORDUnet A/S +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# 3. Neither the name of the NORDUnet nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Author : Fredrik Thulin <fredrik@thulin.net> +# +""" +MITM (man in the middle) proxy of pkcs11-proxy. Written while learning +how the (not very documented) wire format worked, as well as to help +diagnose (and fix) a couple of bugs in the C implementation. + +Usage : + + On console 1 : + + $ PKCS11_DAEMON_SOCKET="tcp://127.0.0.1:2345" pkcs11-daemon /usr/lib/libsofthsm.so + + On console 2 : + + $ ./p11proxy-mitm --debug + + On console 3 : + + $ PKCS11_PROXY_SOCKET="tcp://127.0.0.1:2344" pkcs11-tool --show-info --module libpkcs11-proxy.so + +""" + +import sys +import struct +import socket +import logging +import argparse +import SocketServer +import PyKCS11 # used to get all the PKCS#11 defines + +default_host = "localhost" +default_port_in = 2344 +default_port_out = 2345 +default_timeout = 3 +default_debug = False + +args = None +yhsm = None +logger = None + +# Extracted from gck-rpc-private.h. The last two elements of the tuples describe +# the requests/responses. +# +# /* +# * a_ = prefix denotes array of _ +# * A = CK_ATTRIBUTE +# * f_ = prefix denotes buffer for _ +# * M = CK_MECHANISM +# * u = CK_ULONG +# * s = space padded string +# * v = CK_VERSION +# * y = CK_BYTE +# * z = null terminated string +# */ +request_list = [ + (0, "ERROR", None, None), + (1, "C_Initialize", "ay", ""), + (2, "C_Finalize", "", ""), + (3, "C_GetInfo", "", "vsusv"), + (4, "C_GetSlotList", "yfu", "au"), + (5, "C_GetSlotInfo", "u", "ssuvv"), + (6, "C_GetTokenInfo", "u", "ssssuuuuuuuuuuuvvs"), + (7, "C_GetMechanismList", "ufu", "au"), + (8, "C_GetMechanismInfo", "uu", "uuu"), + (9, "C_InitToken", "uayz", ""), + (10, "C_WaitForSlotEvent", "u", "u"), + (11, "C_OpenSession", "uu", "u"), + (12, "C_CloseSession", "u", ""), + (13, "C_CloseAllSessions", "u", ""), + (14, "C_GetFunctionStatus", "u", ""), + (15, "C_CancelFunction", "u", ""), + (16, "C_GetSessionInfo", "u", "uuuu"), + (17, "C_InitPIN", "uay", ""), + (18, "C_SetPIN", "uayay", ""), + (19, "C_GetOperationState", "ufy", "ay"), + (20, "C_SetOperationState", "uayuu", ""), + (21, "C_Login", "uuay", ""), + (22, "C_Logout", "u", ""), + (23, "C_CreateObject", "uaA", "u"), + (24, "C_CopyObject", "uuaA", "u"), + (25, "C_DestroyObject", "uu", ""), + (26, "C_GetObjectSize", "uu", "u"), + (27, "C_GetAttributeValue", "uufA", "aAu"), + (28, "C_SetAttributeValue", "uuaA", ""), + (29, "C_FindObjectsInit", "uaA", ""), + (30, "C_FindObjects", "ufu", "au"), + (31, "C_FindObjectsFinal", "u", ""), + (32, "C_EncryptInit", "uMu", ""), + (33, "C_Encrypt", "uayfy", "ay"), + (34, "C_EncryptUpdate", "uayfy", "ay"), + (35, "C_EncryptFinal", "ufy", "ay"), + (36, "C_DecryptInit", "uMu", ""), + (37, "C_Decrypt", "uayfy", "ay"), + (38, "C_DecryptUpdate", "uayfy", "ay"), + (39, "C_DecryptFinal", "ufy", "ay"), + (40, "C_DigestInit", "uM", ""), + (41, "C_Digest", "uayfy", "ay"), + (42, "C_DigestUpdate", "uay", ""), + (43, "C_DigestKey", "uu", ""), + (44, "C_DigestFinal", "ufy", "ay"), + (45, "C_SignInit", "uMu", ""), + (46, "C_Sign", "uayfy", "ay"), + (47, "C_SignUpdate", "uay", ""), + (48, "C_SignFinal", "ufy", "ay"), + (49, "C_SignRecoverInit", "uMu", ""), + (50, "C_SignRecover", "uayfy", "ay"), + (51, "C_VerifyInit", "uMu", ""), + (52, "C_Verify", "uayay", ""), + (53, "C_VerifyUpdate", "uay", ""), + (54, "C_VerifyFinal", "uay", ""), + (55, "C_VerifyRecoverInit", "uMu", ""), + (56, "C_VerifyRecover", "uayfy", "ay"), + (57, "C_DigestEncryptUpdate", "uayfy", "ay"), + (58, "C_DecryptDigestUpdate", "uayfy", "ay"), + (59, "C_SignEncryptUpdate", "uayfy", "ay"), + (60, "C_DecryptVerifyUpdate", "uayfy", "ay"), + (61, "C_GenerateKey", "uMaA", "u"), + (62, "C_GenerateKeyPair", "uMaAaA", "uu"), + (63, "C_WrapKey", "uMuufy", "ay"), + (64, "C_UnwrapKey", "uMuayaA", "u"), + (65, "C_DeriveKey", "uMuaA", "u"), + (66, "C_SeedRandom", "uay", ""), + (67, "C_GenerateRandom", "ufy", "ay"), + ] + +def parse_args(): + """ + Parse the command line arguments. + """ + parser = argparse.ArgumentParser(description = "pkcs11-proxy man-in-the-middle", + add_help = True, + formatter_class = argparse.ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument('-H', '--host', + dest='listen_host', + default=default_host, + help='Host address to listen on', + metavar='HOST', + ) + + parser.add_argument('--host-out', + dest='connect_to_host', + default=default_host, + help='Host address to connect to', + metavar='HOST', + ) + + parser.add_argument('--port-in', + dest='listen_port', + type=int, + default=default_port_in, + help='Port to listen on', + metavar='PORT', + ) + + parser.add_argument('--port-out', + dest='connect_to_port', + type=int, + default=default_port_out, + help='Port to connect to', + metavar='PORT', + ) + + parser.add_argument('--timeout', + dest='timeout', + type=int, default=default_timeout, + required=False, + help='Request timeout in seconds', + metavar='SECONDS', + ) + + parser.add_argument('--debug', + dest='debug', + action='store_true', default=default_debug, + help='Enable debug operation', + ) + + return parser.parse_args() + +class ProxyHandler(SocketServer.StreamRequestHandler): + """ + The RequestHandler class for our server. + + It is instantiated once per connection to the server, and must + override the handle() method to implement communication to the + client. + """ + + def __init__(self, *other_args, **kwargs): + self.timeout = args.timeout + + # Outgoing connection to the real server (pkcs11-daemon) + self.p11proxy = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.p11proxy.connect((args.connect_to_host, args.connect_to_port)) + + self.logger = logger + SocketServer.BaseRequestHandler.__init__(self, *other_args, **kwargs) + + def handle(self): + """ + Handle an incoming connection. Read requests/responses and decode them after passing + them on to the real pkcs11-proxy client/server. + """ + # self.request is the TCP socket connected to the client + try: + # 64 bits app id. Randomized on the client, and not really used in the server. + self.appid = self.request.recv(8) + self.p11proxy.send(self.appid) + + while True: + # Read request. First 32 bits are length. + length = self.request.recv(4) + if not length: + break + # Pass length to real server. + self.p11proxy.send(length) + self.logger.debug("\n\nRequest length is {}".format(length.encode('hex'))) + self._handle_request(struct.unpack('>L', length)[0]) + + # Read response. Again, first 32 bits are length. + length = self.p11proxy.recv(4) + # Pass length to client. + self.request.sendall(length) + self.logger.debug("\n\nAnswer length is {}".format(length.encode('hex'))) + self._handle_answer(struct.unpack('>L', length)[0]) + except Exception, e: + self.logger.exception("Got exception handling request from {}".format(self.client_address[0])) + return None + + def _handle_request(self, length): + """ + Given expected length, read the rest of a request, pass it on and decode whatever we can. + """ + self.logger.debug("Request({})".format(length)) + data = self.request.recv(length) + self.logger.debug(" R: {}".format(data.encode('hex'))) + self.p11proxy.send(data) + # parse request + (call_id, rest) = self._get_uint32(data) + if call_id > len(request_list): + return None + self.logger.debug(" R: call_id {}".format(call_id)) + rinfo = request_list[call_id] + self.logger.debug(" R: name {}".format(rinfo[1])) + fmt = rinfo[2] + if fmt: + self.logger.debug(" R: fmt {} ({})".format(fmt, fmt.encode('hex'))) + return self._parse_message('R', fmt, rest) + + def _handle_answer(self, length): + """ + Given expected length, read the rest of a response, pass it on and decode whatever we can. + """ + self.logger.debug("Answer({})".format(length)) + data = self.p11proxy.recv(length) + self.logger.debug(" A: {}".format(data.encode('hex'))) + self.request.sendall(data) + (call_id, rest) = self._get_uint32(data) + if call_id > len(request_list): + return None + # parse answer + (call_id, rest) = self._get_uint32(data) + if call_id > len(request_list): + return None + self.logger.debug(" A: call_id {}".format(call_id)) + rinfo = request_list[call_id] + self.logger.debug(" A: name {}".format(rinfo[1])) + if not call_id: + # This is ERROR + (ckr, _) = self._get_uint32(data[-4:]) + self.logger.debug(" A: {}".format(PyKCS11.CKR[ckr])) + fmt = rinfo[3] + if fmt: + self.logger.debug(" A: fmt {} ({})".format(fmt, fmt.encode('hex'))) + self._parse_message('A', fmt, rest) + + def _parse_message(self, msgtype, fmt, data): + """ + Parse the data in the request/response, which share format. + """ + # Check that the data is of the format we expect it to be. The format of every request/answer + # is redundantly included in the data sent over the network, although we already know what + # it should be. I guess this is a way to reduce the likeliness of undetected stream de-sync, + # because it would be wasteful if it was just used to make developers forget to update the + # handshake string when changing the format for a request/response. + (parsed_fmt, rest) = self._get_byte_array(data) + if parsed_fmt != fmt: + self.logger.error(" {}: Format mismatch, mine is {} and received is {}".format(msgtype, fmt, parsed_fmt)) + return None + self.logger.debug(" {}: format match".format(msgtype)) + try: + res = self._value_dump(msgtype, parsed_fmt, rest) + return res + except Exception: + self.logger.exception("Got exception trying to dump all data") + return [] + + def _value_dump(self, msgtype, fmt, data, res = []): + """ + Decode the data in a request/answer according to fmt. + + Will output the decoded data using self.logger.debug(). + """ + if not fmt: + if data: + self.logger.warning("{} bytes left after processing : {}".format(len(data), data.encode('hex'))) + return [] + #self.logger.debug("PARSING {} FROM {}".format(fmt[0], data.encode('hex'))) + if fmt[0] == 'u': + (this, rest) = self._get_uint64(data) + res.append(this) + self.logger.debug(" {}: uint64 {}".format(msgtype, hex(this))) + elif fmt[0] == 'a': + #self.logger.debug("PARSING {} FROM {}".format(fmt[0], data.encode('hex'))) + if fmt[1] == 'A': + valid = 1 + rest = data + else: + (valid, rest) = self._get_uint8(data) + self.logger.debug(" {}: Byte-Array valid : {}".format(msgtype, valid)) + array_num = 0 + (array_num, rest) = self._get_uint32(rest) + self.logger.debug(" {}: Byte-Array length : {}/{}".format(msgtype, array_num, hex(array_num))) + if array_num > 256: + raise Exception("Unreasonable array length : {}".format(hex(array_num))) + if not valid: + # /* If not valid, then just the length is encoded, this can signify CKR_BUFFER_TOO_SMALL */ + self.logger.debug(" {}: Array length {}/{}, not valid - maybe CKR_BUFFER_TOO_SMALL".format(msgtype, array_num, array_num)) + # array is now fully handled, remove next element of fmt + fmt = fmt[1:] + if rest: + self.logger.debug(" {}: CKR_BUFFER_TOO_SMALL leaving data {} ({} bytes) for fmt {}".format(msgtype, rest.encode('hex'), len(rest), fmt[1:])) + else: + if fmt[1] == 'y': + # simple byte array (i.e. string to us) + rest = struct.pack('>L', array_num) + rest # restore length before calling _get_byte_array + (this, rest) = self._get_byte_array(rest) + self.logger.debug(" {}: Byte-Array({}) {}".format(msgtype, len(this), repr(this))) + res.append(this) + fmt = fmt[1:] + else: + if array_num > 0: + # modify fmt to match the array length + placeholder = 'X' + array_elements = fmt[1] * (array_num - 1) + fmt_rest = fmt[1:] + fmt = placeholder + array_elements + fmt_rest + else: + # chomp the next fmt too + fmt = fmt[1:] + self.logger.debug(" {}: Array length {}, new fmt {}".format(msgtype, array_num, fmt)) + elif fmt[0] == 's': + (this, rest) = self._get_byte_array(data) + res.append(this) + self.logger.debug(" {}: string({}/{}) {}".format(msgtype, len(this), hex(len(this)), repr(this))) + elif fmt[0] == 'v': + # version, struct ck_version from pkcs11.h + (major, rest) = self._get_uint8(data) + (minor, rest) = self._get_uint8(rest) + self.logger.debug(" {}: CK_Version {}.{}".format(msgtype, major, minor)) + elif fmt[0] == 'y': + (this, rest) = self._get_uint8(data) + self.logger.debug(" {}: CK_Byte {}".format(msgtype, hex(this))) + elif fmt[0] == 'z': + # NULL-terminated string, len == -1 for empty string + (length, rest) = self._get_uint32(data) + if length == 0xffffffff: + res.append('') + else: + (this, rest) = self._get_byte_array(data) + res.append(this) + self.logger.debug(" {}: NULL-string({}/{}) {}".format(msgtype, len(this), hex(len(this)), repr(this))) + elif fmt[0] == 'M': + # Mechanism + #self.logger.debug("PARSING {} FROM {}".format(fmt[0], data.encode('hex'))) + (mech_type, rest) = self._get_uint32(data) + self.logger.debug(" {}: Mechanism {}/{}".format(msgtype, repr(mech_type), PyKCS11.CKM.get(mech_type))) + (this, rest) = self._get_byte_array(rest) + self.logger.debug(" {}: Mechanism parameter {}".format(msgtype, repr(this))) + res.append((mech_type, this)) + elif fmt[0] == 'A': + # Attribute + (this, rest) = self._get_attribute(data) + res.append(this) + #self.logger.debug(" {}: Attribute {}".format(msgtype, this)) + vl = "None" + if this['uValueLen'] is not None: + vl = hex(this['uValueLen']) + self.logger.debug(" {}: Attribute type {}/{}, valid {}, len {}, data {}".format(\ + msgtype, hex(this['type']), PyKCS11.CKA.get(this['type']), this['valid'], vl, this['data'].encode('hex'))) + elif fmt[0] == 'f': + # array buffer + if fmt[1] == 'y': + (flags, rest) = self._get_uint8(data) + else: + rest = data + (num_items, rest) = self._get_uint32(rest) + if fmt[1] == 'A': + self.logger.debug(" {}: Attribute buffer, {} elements".format(msgtype, hex(num_items))) + while num_items: + (attr_type, rest) = self._get_uint32(rest) + (buffer_len, rest) = self._get_uint32(rest) + self.logger.debug(" {}: type {}/{} len {}".format(msgtype, hex(attr_type), PyKCS11.CKA.get(attr_type), hex(buffer_len))) + res.append(('attribute buffer', attr_type, buffer_len)) + num_items -= 1 + elif fmt[1] == 'y': + self.logger.debug(" {}: Byte buffer, length {} (flags {})".format(msgtype, hex(num_items), flags)) + # just a number of bytes to alloc + pass + elif fmt[1] == 'u': + # ulong buffer, just an uint32 (?) + self.logger.debug("PARSING {} FROM {}".format(fmt[0], data.encode('hex'))) + (this, rest) = self._get_uint32(data) + self.logger.debug(" {}: ulong buffer {}".format(msgtype, hex(this))) + else: + raise Exception("Unknown array buffer fmt '{}'".format(fmt[1])) + # need to munch an extra fmt + fmt = fmt[1:] + else: + self.logger.warn(" {}: STOPPING at fmt {}, data {}".format(msgtype, fmt[0], data.encode('hex'))) + return [] + return self._value_dump(msgtype, fmt[1:], rest, res) + + def _get_uint64(self, data): + (a, rest) = self._get_uint32(data) + (b, rest) = self._get_uint32(rest) + return (a << 32 | b, data[8:]) + + def _get_uint32(self, data): + res = struct.unpack('>L', data[:4])[0] + return (res, data[4:]) + + def _get_uint8(self, data): + res = ord(data[0]) + return (res, data[1:]) + + def _get_byte_array(self, data): + (length, rest) = self._get_uint32(data) + if length == 0xffffffff: + return('', rest) + if length > len(rest): + raise Exception('Parse error, not enough bytes left for byte-array') + res = rest[:length] + return (res, rest[length:]) + + def _get_attribute(self, data): + attr = {} + (attr_type, rest) = self._get_uint32(data) + (attr_valid, rest) = self._get_uint8(rest) + attr_vallen = None + data = '' + if attr_valid: + (attr_vallen, rest) = self._get_uint32(rest) + # length doubly included, in byte array again? + (data, rest) = self._get_byte_array(rest) + # read data here! + attr['type'] = attr_type + attr['valid'] = attr_valid + attr['uValueLen'] = attr_vallen + attr['data'] = data + return (attr, rest) + +def main(): + global args + args = parse_args() + + level = logging.INFO + if args.debug: + level = logging.DEBUG + logging.basicConfig(level=level) + global logger + logger = logging.getLogger('p11proxy-mitm') + + # Create the server + server = SocketServer.TCPServer((args.listen_host, args.listen_port), \ + ProxyHandler, \ + bind_and_activate=False, \ + ) + server.allow_reuse_address = True + server.timeout = args.timeout + server.server_bind() # Manually bind, to support allow_reuse_address + server.server_activate() # (see above comment) + + # Activate the server; this will keep running until you + # interrupt the program with Ctrl-C + server.serve_forever() + +if __name__ == '__main__': + try: + if main(): + sys.exit(0) + sys.exit(1) + except KeyboardInterrupt: + pass