Skip to content

Commit

Permalink
Basic TPM 2.0 bindings in botan3.py
Browse files Browse the repository at this point in the history
Enough to set up a TPM context, enable Botan's crypto backend and
instantiate a TPM-backed RNG with parameter encryption via an
unauthenticated Session object.
  • Loading branch information
reneme committed Oct 11, 2024
1 parent 109d000 commit 4cb6970
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 2 deletions.
30 changes: 30 additions & 0 deletions doc/api_ref/python.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ Random Number Generators
no matter how many 'system' rng instances are created. Thus it is
easy to use the RNG in a one-off way, with `botan.RandomNumberGenerator().get(32)`.

When Botan is configured with TPM 2.0 support, also 'tpm2' is allowed
to instantiate a TPM-backed RNG. Note that this requires passing
additional named arguments ``tpm2_context=`` with a ``TPM2Context`` and
(optionally) ``tpm2_sessions=`` with one or more ``TPM2Session`` objects.

.. py:method:: get(length)
Return some bytes
Expand Down Expand Up @@ -461,6 +466,31 @@ Public Key Operations
Returns a key derived by the KDF.
TPM 2.0 Bindings
-------------------------------------
.. versionadded:: 3.6.0
.. py:class:: TPM2Context(tcti_nameconf = None, tcti_conf = None)
Create a TPM 2.0 context optionally with a TCTI name and configuration,
separated by a colon, or as separate parameters.
.. py:method:: supports_botan_crypto_backend()
Returns True if the TPM adapter can use Botan-based crypto primitives
to communicate with the TPM
.. py:method:: enable_botan_crypto_backend(rng)
Enables the TPM adapter to use Botan-based crypto primitives. The passed
RNG must not depend on the TPM itself.
.. py:class:: TPM2UnauthenticatedSession(ctx)
Creates a TPM 2.0 session that is not bound to any authentication credential
but provides basic parameter encryption between the TPM and the application.
Multiple Precision Integers (MPI)
-------------------------------------
.. versionadded:: 2.8.0
Expand Down
118 changes: 116 additions & 2 deletions src/python/botan3.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
(C) 2015,2017,2018,2019,2023 Jack Lloyd
(C) 2015 Uri Blumenthal (extensions and patches)
(C) 2024 Amos Treiber, René Meusel - Rohde & Schwarz Cybersecurity
Botan is released under the Simplified BSD License (see license.txt)
Expand All @@ -20,11 +21,13 @@

from ctypes import CDLL, CFUNCTYPE, POINTER, byref, create_string_buffer, \
c_void_p, c_size_t, c_uint8, c_uint32, c_uint64, c_int, c_uint, c_char, c_char_p, addressof
from typing import Callable

from sys import platform
from time import strptime, mktime, time as system_time
from binascii import hexlify
from datetime import datetime
from collections.abc import Iterable

BOTAN_FFI_VERSION = 20240408

Expand Down Expand Up @@ -502,6 +505,16 @@ def ffi_api(fn, args, allowed_errors=None):
ffi_api(dll.botan_zfec_decode,
[c_size_t, c_size_t, POINTER(c_size_t), POINTER(c_char_p), c_size_t, POINTER(c_char_p)])

# TPM2
ffi_api(dll.botan_tpm2_supports_crypto_backend, [])
ffi_api(dll.botan_tpm2_ctx_init, [c_void_p, c_char_p], [-40])
ffi_api(dll.botan_tpm2_ctx_init_ex, [c_void_p, c_char_p, c_char_p], [-40])
ffi_api(dll.botan_tpm2_ctx_enable_crypto_backend, [c_void_p, c_void_p])
ffi_api(dll.botan_tpm2_ctx_destroy, [c_void_p], [-40])
ffi_api(dll.botan_tpm2_rng_init, [c_void_p, c_void_p, c_void_p, c_void_p, c_void_p])
ffi_api(dll.botan_tpm2_unauthenticated_session_init, [c_void_p, c_void_p])
ffi_api(dll.botan_tpm2_session_destroy, [c_void_p])

return dll

#
Expand Down Expand Up @@ -631,14 +644,115 @@ def const_time_compare(x, y):
rc = _DLL.botan_constant_time_compare(xbits, ybits, c_size_t(len_x))
return rc == 0

#
# TPM2
#

class TPM2Object:
def __init__(self, obj: c_void_p, destroyer: Callable[[c_void_p], None]):
self.__obj = obj
self.__destroyer = destroyer

def __del__(self):
if hasattr(self, '__obj') and hasattr(self, '__destroyer'):
self.__destroyer(self.__obj)

def handle_(self):
return self.__obj

class TPM2Context(TPM2Object):
"""TPM 2.0 Context object"""

def __init__(self, tcti_name_maybe_with_conf: str = None, tcti_conf: str = None):
"""Construct a TPM2Context object with optional TCTI name and configuration."""

obj = c_void_p(0)
if tcti_conf is not None:
rc = _DLL.botan_tpm2_ctx_init_ex(byref(obj), _ctype_str(tcti_name_maybe_with_conf), _ctype_str(tcti_conf))
else:
rc = _DLL.botan_tpm2_ctx_init(byref(obj), _ctype_str(tcti_name_maybe_with_conf))
if rc == -40: # 'Not Implemented'
raise BotanException("TPM2 is not implemented in this build configuration", rc)
self.rng_ = None
super().__init__(obj, _DLL.botan_tpm2_ctx_destroy)

@staticmethod
def supports_botan_crypto_backend() -> bool:
"""Returns True if the given build supports the Botan-based crypto backend."""
rc = _DLL.botan_tpm2_supports_crypto_backend()
return rc == 1

def enable_botan_crypto_backend(self, rng):
"""Enables the Botan-based crypto backend.
The passed rng MUST NOT be dependent on the TPM."""
# By keeping a reference to the passed-in RNG object, we make sure
# that the underlying object lives at least as long as this context.
self.rng_ = rng
_DLL.botan_tpm2_ctx_enable_crypto_backend(self.handle_(), self.rng_.handle_())

class TPM2Session(TPM2Object):
"""Basic TPM 2.0 Session object, typically users will instantiate a derived class."""

def __init__(self, obj: c_void_p):
super().__init__(obj, _DLL.botan_tpm2_session_destroy)

@staticmethod
def session_bundle_(*args):
"""Transforms a session bundle passed by the downstream user into a 3-tuple of session handles.
Users might pass a bare TPM2Session object or an iterable list of such objects."""
if len(args) == 1:
if isinstance(args[0], Iterable):
args = list(args[0])
elif args[0] is None:
args = []

if len(args) <= 3 and all(isinstance(s, TPM2Session) for s in args):
sessions = list(args)
while len(sessions) < 3:
sessions.append(None)
return (s.handle_() if isinstance(s, TPM2Session) else None for s in sessions)
else:
raise BotanException("session bundle arguments must be 0 to 3 TPM2Session objects")


class TPM2UnauthenticatedSession(TPM2Session):
"""Session object that is not bound to any authenication credential.
It provides basic parameter encryption between the application and the TPM."""
def __init__(self, ctx: TPM2Context):
obj = c_void_p(0)
_DLL.botan_tpm2_unauthenticated_session_init(byref(obj), ctx.handle_())
super().__init__(obj)

#
# RNG
#
class RandomNumberGenerator:
# Can also use type "system"
def __init__(self, rng_type='system'):
def __init__(self, rng_type='system', **kwargs):
"""Constructs a RandomNumberGenerator of type rng_type
Available RNG types are::
* 'system': Adapter to the operating system's RNG
* 'user': Software-PRNG that is auto-seeded by the system RNG
* 'null': Mock-RNG that fails if randomness is pulled from it
* 'hwrng': Adapter to an available hardware RNG (platform dependent)
* 'tpm2': Adapter to a TPM 2.0 RNG
(needs additional named arguments tpm2_context= and, optionally, tpm2_sessions=)
"""
self.__obj = c_void_p(0)
_DLL.botan_rng_init(byref(self.__obj), _ctype_str(rng_type))
if rng_type == 'tpm2':
ctx = kwargs.pop("tpm2_context", None)
if not ctx or not isinstance(ctx, TPM2Context):
raise BotanException("Cannot instantiate a TPM2-based RNG without a TPM2 context, pass tpm2_context= argument?")
sessions = TPM2Session.session_bundle_(kwargs.pop("tpm2_sessions", None))
if kwargs:
raise BotanException("Unexpected arguments for TPM2 RNG: %s" % (", ".join(kwargs.keys())))
_DLL.botan_tpm2_rng_init(byref(self.__obj), ctx.handle_(), *sessions)
else:
if kwargs:
raise BotanException("Unexpected arguments for RNG type %s: %s" % (rng_type, ", ".join(kwargs.keys())))
_DLL.botan_rng_init(byref(self.__obj), _ctype_str(rng_type))

def __del__(self):
_DLL.botan_rng_destroy(self.__obj)
Expand Down
58 changes: 58 additions & 0 deletions src/scripts/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,70 @@ def test_rng(self):
output3 = user_rng.get(1021)
self.assertEqual(len(output3), 1021)

with self.assertRaisesRegex(botan.BotanException, r".*Unexpected arguments for RNG.*"):
botan.RandomNumberGenerator("user", unexpected="unexpected")

system_rng = botan.RandomNumberGenerator('system')

user_rng.reseed_from_rng(system_rng, 256)

user_rng.add_entropy('seed material...')

def test_tpm2_rng(self):
if ARGS.tpm2_tcti_name is None or ARGS.tpm2_tcti_name == "disabled":
self.skipTest("TPM2 runtime tests are disabled")

try:
tpm2_ctx = botan.TPM2Context(ARGS.tpm2_tcti_name, ARGS.tpm2_tcti_conf)
except botan.BotanException as ex:
if ex.error_code() == -40: # Not Implemented
self.skipTest("No TPM2 support in this build")
else:
raise ex

if tpm2_ctx.supports_botan_crypto_backend():
user_rng = botan.RandomNumberGenerator("user")
tpm2_ctx.enable_botan_crypto_backend(user_rng)

session = botan.TPM2UnauthenticatedSession(tpm2_ctx)

# no TPM context provided
with self.assertRaisesRegex(botan.BotanException, r".*without a TPM2 context.*"):
botan.RandomNumberGenerator("tpm2")

# invalid session object provided
with self.assertRaisesRegex(botan.BotanException, r".*0 to 3 TPM2Session objects.*"):
botan.RandomNumberGenerator("tpm2", tpm2_context=tpm2_ctx,
tpm2_sessions=int(0))

# too many "sessions" provided
with self.assertRaisesRegex(botan.BotanException, r".*0 to 3 TPM2Session objects.*"):
botan.RandomNumberGenerator("tpm2", tpm2_context=tpm2_ctx,
tpm2_sessions=[session, None, None, None])

# unexpected kwarg provided
with self.assertRaisesRegex(botan.BotanException, r".*Unexpected arguments for TPM2.*"):
botan.RandomNumberGenerator("tpm2", tpm2_context=tpm2_ctx,
unexpected_arg="unexpected")

# session provided as a single session (not wrapped into an Iterable)
tpm2_rng = botan.RandomNumberGenerator("tpm2", tpm2_context=tpm2_ctx, tpm2_sessions=session)

output1 = tpm2_rng.get(32)
output2 = tpm2_rng.get(32)

self.assertEqual(len(output1), 32)
self.assertEqual(len(output2), 32)
self.assertNotEqual(output1, output2)
tpm2_rng.add_entropy('xkcd #221: 4 - chosen by fair dice roll')

# session provided wrapped into an Iterable
tpm2_rng2 = botan.RandomNumberGenerator("tpm2", tpm2_context=tpm2_ctx, tpm2_sessions=[session])
output3 = tpm2_rng2.get(32)

self.assertEqual(len(output3), 32)
self.assertNotEqual(output2, output3)

def test_hash(self):

try:
Expand Down

0 comments on commit 4cb6970

Please sign in to comment.