# Copyright (c) 2015-2016, 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: # - Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # - 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. # # - 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. # Number of static hash and HMAC state blocks to allocate. # Numbers pulled out of a hat, just testing. STATIC_HASH_STATE_BLOCKS = 10 STATIC_HMAC_STATE_BLOCKS = 4 STATIC_PKEY_STATE_BLOCKS = 6 INC = hal.h hal_internal.h LIB = libhal.a # Error checking on known control options, some of which allow the user entirely too much rope. USAGE := "usage: ${MAKE} [IO_BUS=eim|i2c|fmc] [RPC_MODE=none|server|client-simple|client-mixed] [KS=volatile|mmap|flash] [RPC_TRANSPORT=none|loopback|serial|daemon] [MODEXP_CORE=no|yes]" IO_BUS ?= eim KS ?= volatile RPC_MODE ?= none RPC_TRANSPORT ?= daemon MODEXP_CORE ?= no ifeq (,$(and \ $(filter none eim i2c fmc ,${IO_BUS}),\ $(filter none server client-simple client-mixed ,${RPC_MODE}),\ $(filter volatile mmap flash ,${KS}),\ $(filter none loopback serial daemon ,${RPC_TRANSPORT}),\ $(filter no yes ,${MODEXP_CORE}))) $(error ${USAGE}) endif $(info Building libhal with configuration IO_BUS=${IO_BUS} RPC_MODE=${RPC_MODE} KS=${KS} RPC_TRANSPORT=${RPC_TRANSPORT} MODEXP_CORE=${MODEXP_CORE}) # Whether the RSA code should use the ModExp | ModExpS6 | ModExpA7 core. ifeq "${MODEXP_CORE}" "yes" RSA_USE_MODEXP_CORE := 1 else RSA_USE_MODEXP_CORE := 0 endif # Object files to build, initialized with ones we always want. # There's a balance here between skipping files we don't strictly # need and reducing the number of unnecessary conditionals in this # makefile, so the working definition of "always want" is sometimes # just "building this is harmless even if we don't use it." OBJ = errorstrings.o hash.o asn1.o ecdsa.o rsa.o ${KS_OBJ} # Object files to build when we're on a platform with direct access # to our hardware (Verilog) cores. CORE_OBJ = core.o csprng.o pbkdf2.o aes_keywrap.o modexp.o mkmif.o ${IO_OBJ} # I/O bus to the FPGA # # IO_BUS = none | eim | i2c | fmc # none: No FPGA I/O bus # eim: EIM bus from Novena # i2c: Older I2C bus from Novena # fmc: FMC bus from dev-bridge and alpha boards ifeq "${IO_BUS}" "eim" IO_OBJ = hal_io_eim.o novena-eim.o else ifeq "${IO_BUS}" "i2c" IO_OBJ = hal_io_i2c.o else ifeq "${IO_BUS}" "fmc" IO_OBJ = hal_io_fmc.o endif # If we're building for STM32, position-independent code leads to some # hard-to-debug function pointer errors. OTOH, if we're building for Linux # (even on the Novena), we want to make it possible to build a shared library. ifneq "${IO_BUS}" "fmc" CFLAGS += -fPIC endif # The mmap and flash keystore implementations are both server code. # # The volatile keystore (conventional memory) is client code, to # support using the same API for things like PKCS #11 "session" objects. # # Default at the moment is mmap, since that should work on the Novena # and we haven't yet written the flash code for the bridge board. KS_OBJ = ks.o ifeq "${KS}" "mmap" KS_OBJ += ks_mmap.o else ifeq "${KS}" "volatile" KS_OBJ += ks_volatile.o else ifeq "${KS}" "flash" KS_OBJ += ks_flash.o endif # RPC_MODE = none | server | client-simple | client-mixed # none: Build without RPC client, use cores directly. # server: Build for server side of RPC (HSM), use cores directly. # client-simple: Build for other host, communicate with cores via RPC server. # client-mixed: Like client-simple but do hashing locally in software and # support a local keystore (for PKCS #11 public keys, etc) # # RPC_TRANSPORT = loopback | serial | daemon # loopback: Communicate over loopback socket on Novena # serial: Communicate over USB in serial pass-through mode # daemon: Communicate over USB via a daemon, to arbitrate multiple clients # # Note that RPC_MODE setting also controls the RPC_CLIENT setting passed to the C # preprocessor via CFLAGS. Whatever we pass here must evaluate to an integer in # the C preprocessor: we can use symbolic names so long as they're defined as macros # in the C code, but we can't use things like C enum symbols. ifneq "${RPC_MODE}" "none" OBJ += rpc_api.o xdr.o endif ifeq "${RPC_TRANSPORT}" "serial" OBJ += slip.o endif RPC_CLIENT_OBJ = rpc_client.o ifeq "${RPC_TRANSPORT}" "loopback" RPC_CLIENT_OBJ += rpc_client_loopback.o else ifeq "${RPC_TRANSPORT}" "serial" RPC_CLIENT_OBJ += rpc_client_serial.o else ifeq "${RPC_TRANSPORT}" "daemon" RPC_CLIENT_OBJ += rpc_client_daemon.o endif RPC_DISPATCH_OBJ = rpc_hash.o rpc_misc.o rpc_pkey.o RPC_SERVER_OBJ = rpc_server.o ifeq "${RPC_TRANSPORT}" "loopback" RPC_SERVER_OBJ += rpc_server_loopback.o else ifeq "${RPC_TRANSPORT}" "serial" RPC_SERVER_OBJ += rpc_server_serial.o endif ifeq "${RPC_MODE}" "none" OBJ += ${CORE_OBJ} CFLAGS += -DHAL_RSA_USE_MODEXP=${RSA_USE_MODEXP_CORE} else ifeq "${RPC_MODE}" "server" OBJ += ${CORE_OBJ} ${RPC_SERVER_OBJ} ${RPC_DISPATCH_OBJ} CFLAGS += -DRPC_CLIENT=RPC_CLIENT_LOCAL -DHAL_RSA_USE_MODEXP=${RSA_USE_MODEXP_CORE} else ifeq "${RPC_MODE}" "client-simple" OBJ += ${RPC_CLIENT_OBJ} CFLAGS += -DRPC_CLIENT=RPC_CLIENT_REMOTE -DHAL_RSA_USE_MODEXP=0 else ifeq "${RPC_MODE}" "client-mixed" OBJ += ${RPC_CLIENT_OBJ} ${RPC_DISPATCH_OBJ} CFLAGS += -DRPC_CLIENT=RPC_CLIENT_MIXED -DHAL_RSA_USE_MODEXP=0 KS = volatile endif TFMDIR := $(abspath ../thirdparty/libtfm) CFLAGS += -g3 -Wall -std=c99 -Wno-strict-aliasing -I${TFMDIR} LDFLAGS := -g3 -L${TFMDIR} -ltfm CFLAGS += -DHAL_STATIC_HASH_STATE_BLOCKS=${STATIC_HASH_STATE_BLOCKS} CFLAGS += -DHAL_STATIC_HMAC_STATE_BLOCKS=${STATIC_HMAC_STATE_BLOCKS} CFLAGS += -DHAL_STATIC_PKEY_STATE_BLOCKS=${STATIC_PKEY_STATE_BLOCKS} all: ${LIB} cd tests; ${MAKE} CFLAGS='${CFLAGS} -I..' LDFLAGS='${LDFLAGS}' $@ cd utils; ${MAKE} CFLAGS='${CFLAGS} -I..' LDFLAGS='${LDFLAGS}' $@ client: ${MAKE} RPC_MODE=client-simple mixed: ${MAKE} RPC_MODE=client-mixed server: ${MAKE} RPC_MODE=server daemon: cryptech_rpcd ${MAKE} RPC_MODE=client-mixed RPC_TRANSPORT=daemon cryptech_rpcd: daemon.o slip.o rpc_serial.o xdr.o ${CC} ${CFLAGS} -o $@ $^ ${LDFLAGS} ${OBJ}: ${INC} ${LIB}: ${OBJ} ${AR} rcs $@ $^ asn1.o rsa.o ecdsa.o: asn1_internal.h ecdsa.o: ecdsa_curves.h novena-eim.o hal_io_eim.o: novena-eim.h slip.o rpc_client_serial.o rpc_server_serial.o: slip_internal.h ks.o: last_gasp_pin_internal.h last_gasp_pin_internal.h: ./utils/last_gasp_default_pin >$@ test: all export RPC_MODE cd tests; ${MAKE} -k $@ clean: rm -f *.o ${LIB} cd tests; ${MAKE} $@ cd utils; ${MAKE} $@ distclean: clean rm -f TAGS tags: TAGS TAGS: *.[ch] tests/*.[ch] utils/*.[ch] etags $^ help usage: @echo ${USAGE} span class='sha1'>0c9048b
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
#!/usr/bin/env python
#
# Copyright (c) 2016-2017, 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:
# - Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# - 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.
#
# - 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.
"""
Implementation of Cryptech RPC protocol multiplexer in Python.
Unlike the original C implementation, this uses SLIP encapsulation
over a SOCK_STREAM channel, because support for SOCK_SEQPACKET is not
what we might wish. We outsource all the heavy lifting for serial and
network I/O to the PySerial and Tornado libraries, respectively.
"""
import os
import sys
import time
import struct
import atexit
import weakref
import logging
import argparse
import logging.handlers
import serial
import serial.tools.list_ports_posix
import tornado.tcpserver
import tornado.iostream
import tornado.netutil
import tornado.ioloop
import tornado.queues
import tornado.locks
import tornado.gen
logger = logging.getLogger("cryptech_muxd")
SLIP_END = chr(0300) # Indicates end of SLIP packet
SLIP_ESC = chr(0333) # Indicates byte stuffing
SLIP_ESC_END = chr(0334) # ESC ESC_END means END data byte
SLIP_ESC_ESC = chr(0335) # ESC ESC_ESC means ESC data byte
Control_U = chr(0025) # Console: clear line
Control_M = chr(0015) # Console: end of line
def slip_encode(buffer):
"Encode a buffer using SLIP encapsulation."
return SLIP_END + buffer.replace(SLIP_ESC, SLIP_ESC + SLIP_ESC_ESC).replace(SLIP_END, SLIP_ESC + SLIP_ESC_END) + SLIP_END
def slip_decode(buffer):
"Decode a SLIP-encapsulated buffer."
return buffer.strip(SLIP_END).replace(SLIP_ESC + SLIP_ESC_END, SLIP_END).replace(SLIP_ESC + SLIP_ESC_ESC, SLIP_ESC)
def client_handle_get(msg):
"Extract client_handle field from a Cryptech RPC message."
return struct.unpack(">L", msg[4:8])[0]
def client_handle_set(msg, handle):
"Replace client_handle field in a Cryptech RPC message."
return msg[:4] + struct.pack(">L", handle) + msg[8:]
class SerialIOStream(tornado.iostream.BaseIOStream):
"""
Implementation of a Tornado IOStream over a PySerial device.
"""
def __init__(self, device):
self.serial = serial.Serial(device, 921600, timeout = 0, write_timeout = 0)
self.serial_device = device
super(SerialIOStream, self).__init__()
def fileno(self):
return self.serial.fileno()
def close_fd(self):
self.serial.close()
def write_to_fd(self, data):
return self.serial.write(data)
def read_from_fd(self):
return self.serial.read(self.read_chunk_size) or None
class PFUnixServer(tornado.tcpserver.TCPServer):
"""
Variant on tornado.tcpserver.TCPServer, listening on a PF_UNIX
(aka PF_LOCAL) socket instead of a TCP socket.
"""
def __init__(self, serial_stream, socket_filename, mode = 0600):
super(PFUnixServer, self).__init__()
self.serial = serial_stream
self.socket_filename = socket_filename
self.add_socket(tornado.netutil.bind_unix_socket(socket_filename, mode))
atexit.register(self.atexit_unlink)
def atexit_unlink(self):
try:
os.unlink(self.socket_filename)
except:
pass
class RPCIOStream(SerialIOStream):
"""
Tornado IOStream for a serial RPC channel.
"""
def __init__(self, device):
super(RPCIOStream, self).__init__(device)
self.queues = weakref.WeakValueDictionary()
self.rpc_input_lock = tornado.locks.Lock()
@tornado.gen.coroutine
def rpc_input(self, query, handle, queue):
"Send a query to the HSM."
logger.debug("RPC send: %s", ":".join("{:02x}".format(ord(c)) for c in query))
self.queues[handle] = queue
with (yield self.rpc_input_lock.acquire()):
yield self.write(query)
@tornado.gen.coroutine
def rpc_output_loop(self):
"Handle reply stream HSM -> network."
while True:
try:
reply = yield self.read_until(SLIP_END)
except tornado.iostream.StreamClosedError:
logger.info("RPC UART closed")
for q in self.queues.itervalues():
q.put_nowait(None)
return
logger.debug("RPC recv: %s", ":".join("{:02x}".format(ord(c)) for c in reply))
try:
handle = client_handle_get(slip_decode(reply))
except:
continue
self.queues[handle].put_nowait(reply)
class QueuedStreamClosedError(tornado.iostream.StreamClosedError):
"Deferred StreamClosedError passed throught a Queue."
class RPCServer(PFUnixServer):
"""
Serve multiplexed Cryptech RPC over a PF_UNIX socket.
"""
@tornado.gen.coroutine
def handle_stream(self, stream, address):
"Handle one network connection."
logger.info("RPC connected %r", stream)
handle = stream.socket.fileno()
queue = tornado.queues.Queue()
while True:
try:
query = yield stream.read_until(SLIP_END)
if len(query) < 9:
continue
query = slip_encode(client_handle_set(slip_decode(query), handle))
yield self.serial.rpc_input(query, handle, queue)
reply = yield queue.get()
if reply is None:
raise QueuedStreamClosedError()
yield stream.write(SLIP_END + reply)
except tornado.iostream.StreamClosedError:
logger.info("RPC closing %r", stream)
stream.close()
return
class CTYIOStream(SerialIOStream):
"""
Tornado IOStream for a serial console channel.
"""
def __init__(self, device):
super(CTYIOStream, self).__init__(device)
self.attached_cty = None
@tornado.gen.coroutine
def cty_output_loop(self):
while True:
try:
buffer = yield self.read_bytes(self.read_chunk_size, partial = True)
except tornado.iostream.StreamClosedError:
logger.info("CTY UART closed")
if self.attached_cty is not None:
self.attached_cty.close()
return
try:
if self.attached_cty is not None:
yield self.attached_cty.write(buffer)
except tornado.iostream.StreamClosedError:
pass
class CTYServer(PFUnixServer):
"""
Serve Cryptech console over a PF_UNIX socket.
"""
@tornado.gen.coroutine
def handle_stream(self, stream, address):
"Handle one network connection."
if self.serial.attached_cty is not None:
yield stream.write("[Console already in use, sorry]\n")
stream.close()
return
logger.info("CTY connected to %r", stream)
try:
self.serial.attached_cty = stream
while self.serial.attached_cty is stream:
yield self.serial.write((yield stream.read_bytes(1024, partial = True)))
except tornado.iostream.StreamClosedError:
stream.close()
finally:
logger.info("CTY disconnected from %r", stream)
if self.serial.attached_cty is stream:
self.serial.attached_cty = None
class ProbeIOStream(SerialIOStream):
"""
Tornado IOStream for probing a serial port. This is nasty.
"""
def __init__(self, device):
super(ProbeIOStream, self).__init__(device)
@classmethod
@tornado.gen.coroutine
def run_probes(cls, args):
if args.rpc_device is not None and args.cty_device is not None:
return
if args.probe:
devs = set(args.probe)
else:
devs = set(str(port)
for port, desc, hwid in serial.tools.list_ports_posix.comports()
if "VID:PID=0403:6014" in hwid)
devs.discard(args.rpc_device)
devs.discard(args.cty_device)
if not devs:
return
logging.debug("Probing candidate devices %s", " ".join(devs))
results = yield dict((dev, ProbeIOStream(dev).run_probe()) for dev in devs)
for dev, result in results.iteritems():
if result == "cty" and args.cty_device is None:
logger.info("Selecting %s as CTY device", dev)
args.cty_device = dev
if result == "rpc" and args.rpc_device is None:
logger.info("Selecting %s as RPC device", dev)
args.rpc_device = dev
@tornado.gen.coroutine
def run_probe(self):
RPC_query = chr(0) * 8 # client_handle = 0, function code = RPC_FUNC_GET_VERSION
RPC_reply = chr(0) * 12 # opcode = RPC_FUNC_GET_VERSION, client_handle = 0, valret = HAL_OK
probe_string = SLIP_END + Control_U + SLIP_END + RPC_query + SLIP_END + Control_U + Control_M
yield self.write(probe_string)
yield tornado.gen.sleep(0.5)
response = yield self.read_bytes(self.read_chunk_size, partial = True)
logger.debug("Probing %s: %r %s", self.serial_device, response, ":".join("{:02x}".format(ord(c)) for c in response))
is_cty = any(prompt in response for prompt in ("Username:", "Password:", "cryptech>"))
try:
is_rpc = response[response.index(SLIP_END + RPC_reply) + len(SLIP_END + RPC_reply) + 4] == SLIP_END
except ValueError:
is_rpc = False
except IndexError:
is_rpc = False
assert not is_cty or not is_rpc
result = None
if is_cty:
result = "cty"
yield self.write(Control_U)
if is_rpc:
result = "rpc"
yield self.write(SLIP_END)
self.close()
raise tornado.gen.Return(result)
@tornado.gen.coroutine
def main():
parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("-v", "--verbose",
action = "count",
help = "blather about what we're doing")
parser.add_argument("-l", "--log-file",
help = "log to file instead of stderr")
parser.add_argument("-p", "--probe",
nargs = "*",
metavar = "DEVICE",
help = "probe for device UARTs")
parser.add_argument("--rpc-device",
help = "RPC serial device name",
default = os.getenv("CRYPTECH_RPC_CLIENT_SERIAL_DEVICE"))
parser.add_argument("--rpc-socket",
help = "RPC PF_UNIX socket name",
default = os.getenv("CRYPTECH_RPC_CLIENT_SOCKET_NAME",
"/tmp/.cryptech_muxd.rpc"))
parser.add_argument("--cty-device",
help = "CTY serial device name",
default = os.getenv("CRYPTECH_CTY_CLIENT_SERIAL_DEVICE"))
parser.add_argument("--cty-socket",
help = "CTY PF_UNIX socket name",
default = os.getenv("CRYPTECH_CTY_CLIENT_SOCKET_NAME",
"/tmp/.cryptech_muxd.cty"))
args = parser.parse_args()
if args.log_file is not None:
logging.getLogger().handlers[:] = [logging.handlers.WatchedFileHandler(args.log_file)]
logging.getLogger().handlers[0].setFormatter(
logging.Formatter("%(asctime)-15s %(name)s[%(process)d]:%(levelname)s: %(message)s",
"%Y-%m-%d %H:%M:%S"))
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG if args.verbose > 1 else logging.INFO)
if args.probe is not None:
yield ProbeIOStream.run_probes(args)
futures = []
if args.rpc_device is None:
logger.warn("No RPC device found")
else:
rpc_stream = RPCIOStream(device = args.rpc_device)
rpc_server = RPCServer(rpc_stream, args.rpc_socket)
futures.append(rpc_stream.rpc_output_loop())
if args.cty_device is None:
logger.warn("No CTY device found")
else:
cty_stream = CTYIOStream(device = args.cty_device)
cty_server = CTYServer(cty_stream, args.cty_socket)
futures.append(cty_stream.cty_output_loop())
# Might want to use WaitIterator(dict(...)) here so we can
# diagnose and restart output loops if they fail?
if futures:
yield futures
if __name__ == "__main__":
try:
tornado.ioloop.IOLoop.current().run_sync(main)
except KeyboardInterrupt:
pass