cr50: test: move crypto test into its own module

This is a no-op change moving some common code out of tpmtest.py,
preparing it to support different testing modes.

BRANCH=none
BUG=chrome-os-partner:43025
TEST=the AES test still succeeds:
  $ test/tpm_test/tpmtest.py
  Starting MPSSE at 800 kHz
  Connected to device vid:did:rid of 1ae0:0028:00
  SUCCESS: AES:ECB common
  SUCCESS: AES:ECB128 1
  SUCCESS: AES:ECB192 1
  SUCCESS: AES:ECB256 1
  SUCCESS: AES:ECB256 2
  SUCCESS: AES:CTR128I 1
  SUCCESS: AES:CTR256I 1

Change-Id: Ia6e0e3e89f99875297da0a4f6137de5901c8ca08
Signed-off-by: Vadim Bendebury <vbendeb@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/314691
Reviewed-by: Randall Spangler <rspangler@chromium.org>
This commit is contained in:
Vadim Bendebury
2015-11-27 07:31:00 -08:00
committed by chrome-bot
parent 0e9cd95664
commit e1be8e179c
3 changed files with 267 additions and 230 deletions

View File

@@ -0,0 +1,220 @@
#!/usr/bin/python
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module for testing cryptography functions using extended commands."""
from __future__ import print_function
import struct
import xml.etree.ElementTree as ET
import utils
# Extension 'cryptography' subcommand codes:
AES = 0
# Basic crypto operations
DECRYPT = 0
ENCRYPT = 1
class CryptoError(Exception):
pass
def get_attribute(tdesc, attr_name, required=True):
"""Retrieve an attribute value from an XML node.
Args:
tdesc: an Element of the ElementTree, a test descriptor containing
necessary information to run a single encryption/description
session.
attr_name: a string, the name of the attribute to retrieve.
required: a Boolean, if True - the attribute must be present in the
descriptor, otherwise it is considered optional
Returns:
The attribute value as a string (ascii or binary)
Raises:
CryptoError: on various format errors, or in case a required attribute is
not found, the error message describes the problem.
"""
# Fields stored in hex format by default.
default_hex = ('cipher_text', 'iv', 'key')
data = tdesc.find(attr_name)
if data is None:
if required:
raise CryptoError('node "%s" does not have attribute "%s"' %
(tdesc.get('name'), attr_name))
return ''
# Attribute is present, does it have to be decoded from hex?
cell_format = data.get('format')
if not cell_format:
if attr_name in default_hex:
cell_format = 'hex'
else:
cell_format = 'ascii'
elif cell_format not in ('hex', 'ascii'):
raise CryptoError('%s:%s, unrecognizable format "%s"' %
(tdesc.get('name'), attr_name, cell_format))
text = ' '.join(x.strip() for x in data.text.splitlines() if x)
if cell_format == 'ascii':
return text
# Drop spaces from hex representation.
text = text.replace(' ', '')
if len(text) & 3:
raise CryptoError('%s:%s %swrong hex number size' %
(tdesc.get('name'), attr_name, utils.hex_dump(text)))
# Convert text to binary
value = ''
for x in range(len(text)/8):
try:
value += struct.pack('<I', int('0x%s' % text[8*x:8*(x+1)], 16))
except ValueError:
raise CryptoError('%s:%s %swrong hex value' %
(tdesc.get('name'), attr_name, utils.hex_dump(text)))
return value
class CryptoD(object):
"""A helper object to contain an encryption scheme description.
Attributes:
subcmd: a 16 bit max integer, the extension subcommand to be used with
this encryption scheme.
sumbodes: an optional dictionary, the keys are strings, names of the
encryption scheme submodes, the values are integers to be included in
the appropriate subcommand fields to communicat the submode to the
device.
"""
def __init__(self, subcommand, submodes=None):
self.subcmd = subcommand
if not submodes:
submodes = {}
self.submodes = submodes
SUPPORTED_MODES = {
'AES': CryptoD(AES, {
'ECB': 0,
'CTR': 1,
'CBC': 2,
'GCM': 3
}),
}
def crypto_run(node_name, op_type, key, iv, in_text, out_text, tpm):
"""Perform a basic operation(encrypt or decrypt).
This function creates an extended command with the requested parameters,
sends it to the device, and then compares the response to the expected
value.
Args:
node_name: a string, the name of the XML node this data comes from. The
format of the name is "<enc type>:<submode> ....", where <enc type> is
the major encryption mode (say AED or DES) and submode - a variant of
the major scheme, if exists.
op_type: an int, encodes the operation to perform (encrypt/decrypt), passed
directly to the device as a field in the extended command
key: a binary string
iv: a binary string, might be empty
in_text: a binary string, the input of the encrypt/decrypt operation
out_text: a binary string, might be empty, the expected output of the
operation. Note that it could be shorter than actual output (padded to
integer number of blocks), in which case only its length of bytes is
compared debug_mode: a Boolean, if True - enables tracing on the console
tpm: a TPM object to send extended commands to an initialized TPM
Returns:
The actual binary string, result of the operation, if the
comparison with the expected value was successful.
Raises:
CryptoError: in case there were problems parsing the node name, or verifying
the operation results.
"""
mode_name, submode_name = node_name.split(':')
submode_name = submode_name[:3].upper()
mode = SUPPORTED_MODES.get(mode_name.upper())
if not mode:
raise CryptoError('unrecognizable mode in node "%s"' % node_name)
submode = mode.submodes.get(submode_name, 0)
cmd = '%c' % op_type # Encrypt or decrypt
cmd += '%c' % submode # A particular type of a generic algorithm.
cmd += '%c' % len(key)
cmd += key
cmd += '%c' % len(iv)
if iv:
cmd += iv
cmd += struct.pack('>H', len(in_text))
cmd += in_text
if tpm.debug_enabled():
print('%d:%d cmd size' % (op_type, mode.subcmd),
len(cmd), utils.hex_dump(cmd))
wrapped_response = tpm.command(tpm.wrap_ext_command(mode.subcmd, cmd))
real_out_text = tpm.unwrap_ext_response(mode.subcmd, wrapped_response)
if out_text:
if len(real_out_text) > len(out_text):
real_out_text = real_out_text[:len(out_text)] # Ignore padding
if real_out_text != out_text:
if tpm.debug_enabled():
print('Out text mismatch in node %s:\n' % node_name)
else:
raise CryptoError('Out text mismatch in node %s, operation %d:\n'
'In text:%sExpected out text:%sReal out text:%s' % (
node_name, op_type,
utils.hex_dump(in_text),
utils.hex_dump(out_text),
utils.hex_dump(real_out_text)))
return real_out_text
def crypto_test(tdesc, tpm):
"""Perform a single test described in the xml file.
The xml node contains all pertinent information about the test inputs and
outputs.
Args:
tdesc: an Element of the ElementTree, a test descriptor containing
necessary information to run a single encryption/description
session.
tpm: a TPM object to send extended commands to an initialized TPM
Raises:
CryptoError: on various execution errors, the details are included in the
error message.
"""
node_name = tdesc.get('name')
key = get_attribute(tdesc, 'key')
if len(key) not in (16, 24, 32):
raise CryptoError('wrong key size "%s:%s"' % (
node_name,
''.join('%2.2x' % ord(x) for x in key)))
iv = get_attribute(tdesc, 'iv', required=False)
if iv and len(iv) != 16:
raise CryptoError('wrong iv size "%s:%s"' % (
node_name,
''.join('%2.2x' % ord(x) for x in iv)))
clear_text = get_attribute(tdesc, 'clear_text')
if tpm.debug_enabled():
print('clear text size', len(clear_text))
cipher_text = get_attribute(tdesc, 'cipher_text', required=False)
real_cipher_text = crypto_run(node_name, ENCRYPT, key, iv,
clear_text, cipher_text, tpm)
crypto_run(node_name, DECRYPT, key, iv, real_cipher_text,
clear_text, tpm)
print(utils.cursor_back() + 'SUCCESS: %s' % node_name)
def crypto_tests(tpm, xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
for child in root:
crypto_test(child, tpm)

View File

@@ -3,7 +3,7 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module for testing TPM, using both conventional and extended commands."""
"""Module for initializing and driving a SPI TPM."""
from __future__ import print_function
@@ -11,31 +11,19 @@ import os
import struct
import sys
import traceback
import xml.etree.ElementTree as ET
# Suppressing pylint warning about an import not at the top of the file. The
# path needs to be set *before* the last import.
# pylint: disable=C6204
root_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
sys.path.append(os.path.join(root_dir, '..', '..', 'build', 'tpm_test'))
import ftdi_spi_tpm
# Basic crypto operations
DECRYPT = 0
ENCRYPT = 1
import crypto_test
import ftdi_spi_tpm
# Extension command for dcypto testing
EXT_CMD = 0xbaccd00a
# Extension subcommands for encryption types
AES = 0
if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
cursor_back = '\x1b[1D' # Move one space to the left.
else:
cursor_back = ''
class TpmError(Exception):
pass
@@ -59,6 +47,7 @@ class TPM(object):
'80 01 00 00 00 0a 00 00 01 00')
def __init__(self, freq=800*1000, debug_mode=False):
self._debug_enabled = debug_mode
self._handle = ftdi_spi_tpm
if not self._handle.FtdiSpiInit(freq, debug_mode):
raise TpmError()
@@ -132,229 +121,19 @@ class TPM(object):
size, len(response)))
return response[header_size:]
def hex_dump(binstr):
"""Convert string into its hex representation."""
dump_lines = ['',]
i = 0
while i < len(binstr):
strsize = min(16, len(binstr) - i)
hexstr = ' '.join('%2.2x' % ord(x) for x in binstr[i:i+strsize])
dump_lines.append(hexstr)
i += strsize
dump_lines.append('')
return '\n'.join(dump_lines)
def get_attribute(tdesc, attr_name, required=True):
"""Retrieve an attribute value from an XML node.
Args:
tdesc: an Element of the ElementTree, a test descriptor containing
necessary information to run a single encryption/description
session.
attr_name: a string, the name of the attribute to retrieve.
required: a Boolean, if True - the attribute must be present in the
descriptor, otherwise it is considered optional
Returns:
The attribute value as a string (ascii or binary)
Raises:
TpmError: on various format errors, or in case a required attribute is not
found, the error message describes the problem.
"""
# Fields stored in hex format by default.
default_hex = ('cipher_text', 'iv', 'key')
data = tdesc.find(attr_name)
if data is None:
if required:
raise TpmError('node "%s" does not have attribute "%s"' %
(tdesc.get('name'), attr_name))
return ''
# Attribute is present, does it have to be decoded from hex?
cell_format = data.get('format')
if not cell_format:
if attr_name in default_hex:
cell_format = 'hex'
else:
cell_format = 'ascii'
elif cell_format not in ('hex', 'ascii'):
raise TpmError('%s:%s, unrecognizable format "%s"' %
(tdesc.get('name'), attr_name, cell_format))
text = ' '.join(x.strip() for x in data.text.splitlines() if x)
if cell_format == 'ascii':
return text
# Drop spaces from hex representation.
text = text.replace(' ', '')
if len(text) & 3:
raise TpmError('%s:%s %swrong hex number size' %
(tdesc.get('name'), attr_name, hex_dump(text)))
# Convert text to binary
value = ''
for x in range(len(text)/8):
try:
value += struct.pack('<I', int('0x%s' % text[8*x:8*(x+1)], 16))
except ValueError:
raise TpmError('%s:%s %swrong hex value' %
(tdesc.get('name'), attr_name, hex_dump(text)))
return value
class CryptoD(object):
"""A helper object to contain an encryption scheme description.
Attributes:
subcmd: a 16 bit max integer, the extension subcommand to be used with
this encryption scheme.
sumbodes: an optional dictionary, the keys are strings, names of the
encryption scheme submodes, the values are integers to be included in
the appropriate subcommand fields to communicat the submode to the
device.
"""
def __init__(self, subcommand, submodes=None):
self.subcmd = subcommand
if not submodes:
submodes = {}
self.submodes = submodes
SUPPORTED_MODES = {
'AES': CryptoD(AES, {
'ECB': 0,
'CTR': 1,
'CBC': 2,
'GCM': 3
}),
}
def crypto_run(node_name, op_type, key, iv, in_text, out_text, tpm, debug_mode):
"""Perform a basic operation(encrypt or decrypt).
This function creates an extended command with the requested parameters,
sends it to the device, and then compares the response to the expected
value.
Args:
node_name: a string, the name of the XML node this data comes from. The
format of the name is "<enc type>:<submode> ....", where <enc type> is
the major encryption mode (say AED or DES) and submode - a variant of
the major scheme, if exists.
op_type: an int, encodes the operation to perform (encrypt/decrypt), passed
directly to the device as a field in the extended command
key: a binary string
iv: a binary string, might be empty
in_text: a binary string, the input of the encrypt/decrypt operation
out_text: a binary string, might be empty, the expected output of the
operation. Note that it could be shorter than actual output (padded to
integer number of blocks), in which case only its length of bytes is
compared debug_mode: a Boolean, if True - enables tracing on the console
tpm: a TPM object to send extended commands to an initialized TPM
debug_mode: a Boolean, if True - this function and the FTDI driver
generate debug messated on the console.
Returns:
The actual binary string, result of the operation, if the
comparison with the expected value was successful.
Raises:
TpmError: in case there were problems parsing the node name, or verifying
the operation results.
"""
mode_name, submode_name = node_name.split(':')
submode_name = submode_name[:3].upper()
mode = SUPPORTED_MODES.get(mode_name.upper())
if not mode:
raise TpmError('unrecognizable mode in node "%s"' % node_name)
submode = mode.submodes.get(submode_name, 0)
cmd = '%c' % op_type # Encrypt or decrypt
cmd += '%c' % submode # A particular type of a generic algorithm.
cmd += '%c' % len(key)
cmd += key
cmd += '%c' % len(iv)
if iv:
cmd += iv
cmd += struct.pack('>H', len(in_text))
cmd += in_text
if debug_mode:
print('%d:%d cmd size' % (op_type, mode.subcmd), len(cmd), hex_dump(cmd))
wrapped_response = tpm.command(tpm.wrap_ext_command(mode.subcmd, cmd))
real_out_text = tpm.unwrap_ext_response(mode.subcmd, wrapped_response)
if out_text:
if len(real_out_text) > len(out_text):
real_out_text = real_out_text[:len(out_text)] # Ignore padding
if real_out_text != out_text:
if debug_mode:
print('Out text mismatch in node %s:\n' % node_name)
else:
raise TpmError('Out text mismatch in node %s, operation %d:\n'
'In text:%sExpected out text:%sReal out text:%s' % (
node_name, op_type,
hex_dump(in_text),
hex_dump(out_text),
hex_dump(real_out_text)))
return real_out_text
def crypto_test(tdesc, tpm, debug_mode):
"""Perform a single test described in the xml file.
The xml node contains all pertinent information about the test inputs and
outputs.
Args:
tdesc: an Element of the ElementTree, a test descriptor containing
necessary information to run a single encryption/description
session.
tpm: a TPM object to send extended commands to an initialized TPM
debug_mode: a Boolean, if True - this function and the FTDI driver
generate debug messated on the console.
Raises:
TpmError: on various execution errors, the details are included in the
error message.
"""
node_name = tdesc.get('name')
key = get_attribute(tdesc, 'key')
if len(key) not in (16, 24, 32):
raise TpmError('wrong key size "%s:%s"' % (
node_name,
''.join('%2.2x' % ord(x) for x in key)))
iv = get_attribute(tdesc, 'iv', required=False)
if iv and len(iv) != 16:
raise TpmError('wrong iv size "%s:%s"' % (
node_name,
''.join('%2.2x' % ord(x) for x in iv)))
clear_text = get_attribute(tdesc, 'clear_text')
if debug_mode:
print('clear text size', len(clear_text))
cipher_text = get_attribute(tdesc, 'cipher_text', required=False)
real_cipher_text = crypto_run(node_name, ENCRYPT, key, iv,
clear_text, cipher_text, tpm, debug_mode)
crypto_run(node_name, DECRYPT, key, iv, real_cipher_text,
clear_text, tpm, debug_mode)
print(cursor_back + 'SUCCESS: %s' % node_name)
def debug_enabled(self):
return self._debug_enabled
if __name__ == '__main__':
tree = ET.parse(os.path.join(root_dir, 'crypto_test.xml'))
root = tree.getroot()
try:
debug_needed = len(sys.argv) == 2 and sys.argv[1] == '-d'
t = TPM(debug_mode=debug_needed)
for child in root:
crypto_test(child, t, debug_needed)
except TpmError as e:
crypto_test.crypto_tests(t, os.path.join(root_dir, 'crypto_test.xml'))
except (TpmError, crypto_test.CryptoError) as e:
print()
print(e)
print('Error:', e)
if debug_needed:
traceback.print_exc()
sys.exit(1)

38
test/tpm_test/utils.py Normal file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/python
# Copyright 2015 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Support functions for extended command based testing."""
import sys
if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
cursor_back_cmd = '\x1b[1D' # Move one space to the left.
else:
cursor_back_cmd = ''
def cursor_back():
"""Return a string which would move cursor one space left, if available.
This is used to remove the remaining 'spinner' character after the test
completes and its result is printed on the same line where the 'spinner' was
spinning.
"""
return cursor_back_cmd
def hex_dump(binstr):
"""Convert binary string into its multiline hex representation."""
dump_lines = ['',]
i = 0
while i < len(binstr):
strsize = min(16, len(binstr) - i)
hexstr = ' '.join('%2.2x' % ord(x) for x in binstr[i:i+strsize])
dump_lines.append(hexstr)
i += strsize
dump_lines.append('')
return '\n'.join(dump_lines)