diff --git a/.github/pylintrc b/.github/pylintrc new file mode 100644 index 00000000..318226b6 --- /dev/null +++ b/.github/pylintrc @@ -0,0 +1,624 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. +ignore-paths=cnap/core/generated + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=15 + +# Maximum number of attributes for a class (see R0902). +max-attributes=15 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=15 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException, + builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + bare-except, + missing-class-docstring, + unused-argument, + too-few-public-methods, + invalid-name, + fixme, + useless-parent-delegation + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io \ No newline at end of file diff --git a/.github/workflows/pylint.yaml b/.github/workflows/pylint.yaml new file mode 100644 index 00000000..323d9be9 --- /dev/null +++ b/.github/workflows/pylint.yaml @@ -0,0 +1,42 @@ +name: Python Code Scan + +on: + push: + branches: + - main + paths: + - 'common/**/*.py' + - 'vmsdk/**/*.py' + pull_request: + paths: + - 'common/**/*.py' + - 'vmsdk/**/*.py' + workflow_dispatch: + +jobs: + codescan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install pylint pydocstyle + python3 -m pip install -r ./common/python/requirements.txt + python3 -m pip install -r ./vmsdk/python/requirements.txt + sudo apt update + + - name: Analyze python code + run: | + set -ex + source setupenv.sh + python_files=$(find ./ -name "*.py" -print) + if [[ -n "$python_files" ]]; then + echo "$python_files" | xargs -n 1 python3 -m pylint --rcfile=.github/pylintrc + #echo "$python_files" | xargs -n 1 python3 -m pydocstyle --convention=google + else + echo "No python files found." + fi diff --git a/.gitignore b/.gitignore index c6127b38..eea1d4d0 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ modules.order Module.symvers Mkfile.old dkms.conf + +__pycache__/ \ No newline at end of file diff --git a/common/python/cctrusted_base/binaryblob.py b/common/python/cctrusted_base/binaryblob.py new file mode 100644 index 00000000..6ce1f701 --- /dev/null +++ b/common/python/cctrusted_base/binaryblob.py @@ -0,0 +1,113 @@ +""" +Manage the binary blob +""" +import logging +import string +import struct + +LOG = logging.getLogger(__name__) + +__author__ = "" + + +class BinaryBlob: + """ + Manage the binary blob. + """ + + def __init__(self, data, base=0): + self._data = data + self._base_address = base + + @property + def length(self): + """ + Length of binary in bytes + """ + return len(self._data) + + @property + def data(self): + """ + Raw data of binary blob + """ + return self._data + + def to_hex_string(self): + """ + To hex string + """ + return "".join(f"{b:02x}" % b for b in self._data) + + def get_uint16(self, pos): + """ + Get UINT16 integer + """ + assert pos + 2 <= self.length + return (struct.unpack(" str: + """ + Return algorithms name from ID + """ + if alg_id in TcgAlgorithmRegistry.TPM_ALG_TABLE: + return TcgAlgorithmRegistry.TPM_ALG_TABLE[alg_id] + return "UNKNOWN" + + def __init__(self, alg_id: int) -> None: + assert alg_id in TcgAlgorithmRegistry.TPM_ALG_TABLE, \ + "invalid parameter alg_id" + self._alg_id = alg_id + +class TcgDigest: + """ + TCG Digest + """ + + def __init__(self, alg_id=TcgAlgorithmRegistry.TPM_ALG_SHA384): + self._algorithms = TcgAlgorithmRegistry(alg_id) + self._hash = [] + + @property + def algorithms(self) -> TcgAlgorithmRegistry: + """ + Algorithms for the hash of digest + """ + return self._algorithms + +class TcgIMR(ABC): + """ + Common Integrated Measurement Register class + """ + + _INVALID_IMR_INDEX = -1 + + def __init__(self): + self._index = -1 + self._digest = [] + + @property + def index(self) -> int: + """ + The index of IMR register + """ + return self._index + + @property + def digest(self): + """ + The digest value of IMR + """ + return self._digest + + @property + @abstractmethod + def count(self): + """ + The total account of IMR + """ + raise NotImplementedError("Need implemented in different arch") + + def is_valid(self): + """ + Check whether IMR is valid or not + """ + return self._index != TcgIMR._INVALID_IMR_INDEX and \ + self._index < self.count + +class TdxRTMR(TcgIMR): + """ + RTMR class defined for Intel TDX + """ + + @property + def count(self): + return 4 + +class TpmPCR(TcgIMR): + """ + PCR class defined for TPM + """ + + @property + def count(self): + return 24 diff --git a/common/python/cctrusted_base/utility.py b/common/python/cctrusted_base/utility.py new file mode 100644 index 00000000..c6b9a273 --- /dev/null +++ b/common/python/cctrusted_base/utility.py @@ -0,0 +1,434 @@ +''' +Module utility provides constant variables defined in outer +references and helper classes. +''' +import os +import logging +import ctypes +import struct +import fcntl +from typing import List + +__author__ = "" + +LOG = logging.getLogger(__name__) + +# The name of the device in different kernel version +DEVICE_NODE_NAME_DEPRECATED = "/dev/tdx-attest" # deprecated +DEVICE_NODE_NAME_1_0 = "/dev/tdx-guest" +DEVICE_NODE_NAME_1_5 = "/dev/tdx_guest" + +# The device operators for tdx v1.0 +# +# Reference: arch/x86/include/uapi/asm/tdx.h in kernel source +# Layout: dir(2bit) size(14bit) type(8bit) nr(8bit) value +# TDX_CMD_GET_REPORT 11 00,0000,0000,1000 b'T' 0000,0001 0xc008 +# TDX_CMD_GET_QUOTE 10 00,0000,0000,1000 b'T' 0000,0002 0x8008 +# big-endian -> little-endian +# 0xc008 0x08c0 +# 0x8008 0x0880 +TDX_CMD_GET_REPORT_V1_0 = int.from_bytes(struct.pack('Hcb', 0x08c0, b'T', 1), 'big') +TDX_CMD_GET_QUOTE_V1_0 = int.from_bytes(struct.pack('Hcb', 0x0880, b'T', 2), 'big') + +# The devic operators for tdx v1.5 +# Reference: TDX_CMD_GET_REPORT0 +# defined in include/uapi/linux/tdx-guest.h in kernel source +# Layout: dir(2bit) size(14bit) type(8bit) nr(8bit) +# 11 00,0100,0100,0000 b'T' 0000,0001 +# The higher 16bit is standed by 0xc440 in big-endian, +# 0x40c4 in little-endian. +TDX_CMD_GET_REPORT0_V1_5 = int.from_bytes(struct.pack('Hcb', 0x40c4, b'T', 1),'big') + +# The valid value of tcb_info_valid +# Note: +# 1. In tdx 1.0, the valid(8 bytes), tee_tcb_svn(16 bytes), mrseam(48 bytes) are +# ready. The valid structure can be written in little-endian as +# '1111,1111 1000,0000 0..0', transformed to '1111,1111 0000,0001 0..0' or +# '0xff 01 0..0' in human-readable format. +# 2. In tdx 1.5, tee_tcb_svn2(16 bytes) is introdued and placed the previously +# reserved section closet to the attributes structure. The valid structure is +# written as '1111,1111 1000,0000 1100,0000 0..0' in little-enditan, or +# '1111,1111 0000,0001 0000,0011 0..0' or '0xff 01 03 0..0' in human-readable +# format. +# Ref: IntelĀ® CPU Architectural Extensions Specification +# in https://www.intel.com/content/www/us/en/developer/articles/technical +# /intel-trust-domain-extensions.html +# FIXME: tee_tcb_svn2 info update # pylint: disable=fixme +TCB_INFO_VALID_VAL_1_0 = b"\xff\x01\x00\x00\x00\x00\x00\x00" +TCB_INFO_VALID_VAL_1_5 = b"\xff\x01\x03\x00\x00\x00\x00\x00" + +# The length of the reportdata +TDX_REPORTDATA_LEN = 64 +# The length of the tdreport +TDX_REPORT_LEN = 1024 +# The length of the tdquote 4 pages +TDX_QUOTE_LEN = 4 * 4096 + +class DeviceNode: + """ + DeviceNode manager operation on tdx device in guest + Support devices: + * DEVICE_NODE_NAME_1_0 + * DEVICE_NODE_NAME_1_5 + Support operation: + * GET_TDREPORT + * GET_TDQUOTE + """ + GET_TDREPORT = "get tdreport" + GET_TDQUOTE = "get tdquote" + + class DeviceOperatorsMap: + ''' + Class DeviceOperatorsMap contains the name of a device node + and corresponding opertors on it. + ''' + def __init__(self, device:str, operators: map): + self.device_node = device + self.operators = operators + + DEVICE_OPERATOR_MAPS = [ + DeviceOperatorsMap(DEVICE_NODE_NAME_1_0, { + GET_TDREPORT: TDX_CMD_GET_REPORT_V1_0, + GET_TDQUOTE: TDX_CMD_GET_QUOTE_V1_0 + }), + DeviceOperatorsMap(DEVICE_NODE_NAME_1_5, { + GET_TDREPORT: TDX_CMD_GET_REPORT0_V1_5 + }) + ] + + def __init__(self): + self.device_node_name = None + self.operators = None + self._determine_device_node() + self.tdreport = None + self.reportdata = None + self.tdquote = None + + def _determine_device_node(self): + if os.path.exists(DEVICE_NODE_NAME_DEPRECATED): + LOG.error("Deprecated device node %s, please upgrade to use %s or %s", + DEVICE_NODE_NAME_DEPRECATED, DEVICE_NODE_NAME_1_0, DEVICE_NODE_NAME_1_5) + return + + for dom in self.DEVICE_OPERATOR_MAPS: + if os.path.exists(dom.device_node): + self.device_node_name = dom.device_node + self.operators = dom.operators + break + + if self.device_node_name is None: + for dom in self.DEVICE_OPERATOR_MAPS: + if os.path.exists(dom.device_node): + self.device_node_name = dom.device_node + self.operators = dom.operators + break + + def get_tdreport_bytes(self, report_data=None): + ''' + Method get_tdreport_bytes requests the tdx device to retrive + the tdreport in bytes format. + ''' + if self.device_node_name is None or self.operators is None: + LOG.error("Invalid device node: %s", self.device_node_name) + return None + + # 1. Get the operator + operator = self.operators[self.GET_TDREPORT] + if operator is None: + LOG.error("Device %s not support operation %s", + self.device_node_name, self.GET_TDREPORT) + return None + + # 2. Get device file descriptor + try: + fd_tdx_device = os.open(self.device_node_name, os.O_RDWR) + except (PermissionError, IOError, OSError): + LOG.error("Fail to open file %s", self.device_node_name) + return None + + # 3. Create the request + req = self.create_tdx_report_req(report_data) + + # 4. Retrieve tdreport + try: + fcntl.ioctl(fd_tdx_device, + operator, + req) + except OSError: + LOG.error("Fail to execute ioctl for file %s", self.device_node_name) + os.close(fd_tdx_device) + return None + os.close(fd_tdx_device) + + # 5. Get tdreport bytes form tdx_report_req + tdreport_bytes = self.get_tdreport_bytes_from_req(req) + return tdreport_bytes + + def create_tdx_report_req(self, report_data=None): + ''' + Method create_tdx_report_req creates a tdx_report_req struct + with report_data. + ''' + length = 0 + if report_data is not None: + length = len(report_data) + if length > TDX_REPORTDATA_LEN: + LOG.error("Input report_data is longer than TDX_REPORTDATA_LEN") + return None + + if self.device_node_name == DEVICE_NODE_NAME_1_0: + self.reportdata = ctypes.create_string_buffer(TDX_REPORTDATA_LEN) + for index in range(length): + self.reportdata[index] = report_data[index] + self.tdreport = ctypes.create_string_buffer(TDX_REPORT_LEN) + req = struct.pack("BQLQL", 0, ctypes.addressof(self.reportdata), TDX_REPORTDATA_LEN, + ctypes.addressof(self.tdreport), TDX_REPORT_LEN) + return req + + if self.device_node_name == DEVICE_NODE_NAME_1_5: + req = bytearray(TDX_REPORTDATA_LEN + TDX_REPORT_LEN) + for index in range(length): + req[index] = report_data[index] + return req + return None + + def get_tdreport_bytes_from_req(self, req): + ''' + Method get_tdreport_bytes_from_req retrieves the tdreprot in bytes + format from the tdx_report_req struct. + ''' + if self.device_node_name == DEVICE_NODE_NAME_1_0: + parts = struct.unpack("BQLQL", req) + buffer = (ctypes.c_char * TDX_REPORT_LEN).from_address(parts[3]) + return bytearray(buffer) + if self.device_node_name == DEVICE_NODE_NAME_1_5: + tdreport_bytes = req[TDX_REPORTDATA_LEN:] + return tdreport_bytes + return None + + def qgs_msg_quote_req(self, tdreport): + ''' + Method qgs_msg_quote_req generates QGS messages for tdquote + Refer: https://github.com/intel/SGXDataCenterAttestationPrimitives + qgs_msg_header_t & qgs_msg_get_quote_req_t + uint16_t major_version = 1; + uint16_t minor_version = 0; + uint32_t type = 0 (GET_QUOTE_REQ); + // size of the whole message, include this header, in byte + uint32_t size = header + report_size + id_list_size; + uint32_t error_code = 0; // used in response only + + uint32_t report_size = report_size; // cannot be 0 + uint32_t id_list_size = 0; // length of id_list, in byte, can be 0 + uint8_t report_id_list[] = NULL; + ''' + major_version = 1 + minor_version = 0 + msg_type = 0 + error_code = 0 + msg_size = 0 + report_size = 0 + id_list_size = 0 + + if tdreport is not None: + report_size = len(tdreport) + # sizeof(qgs_msg_get_quote_req_t) + msg_size = 16 + 8 + report_size + + qgs_msg = struct.pack(f"2H5I{report_size}s", major_version, minor_version, msg_type, + msg_size, error_code, report_size, id_list_size, tdreport) + return qgs_msg + + def qgs_msg_quote_resp(self, buf): + ''' + Method qgs_msg_quote_resp parse tdquote from the response of QGS + Refer: https://github.com/intel/SGXDataCenterAttestationPrimitives + qgs_msg_header_t & qgs_msg_get_quote_resp_t + uint16_t major_version = 1; + uint16_t minor_version = 0; + uint32_t type = 1 (GET_QUOTE_RESP); + uint32_t size = header + quote; + uint32_t error_code = 0; // used in response only + + uint32_t selected_id_size; // can be 0 in case only one id is sent in request + uint32_t quote_size; // length of quote_data, in byte + uint8_t id_quote[]; // selected id followed by quote + ''' + msg_size = len(buf) - (16 + 8) + major_version, minor_version, msg_type, msg_size, error_code, _, quote_size, quote = \ + struct.unpack(f"2H5I{msg_size}s", buf) + if major_version != 1 and minor_version != 0 and msg_type != 1 and error_code != 0: + return None + return quote[:quote_size] + + def get_tdquote_bytes(self, report_data=None): + ''' + Method get_tdquote_bytes requests the tdx device to retrive + the tdquote in bytes format. + ''' + tdreport_bytes = self.get_tdreport_bytes(report_data) + if tdreport_bytes is None: + LOG.error("Get TD report failed") + return None + + tdquote_req = self.create_tdx_quote_req(tdreport_bytes) + + try: + fd_tdx_device = os.open(self.device_node_name, os.O_RDWR) + except (PermissionError, IOError, OSError): + LOG.error("Fail to open file %s", self.device_node_name) + return None + + operator = self.operators[self.GET_TDQUOTE] + if operator is None: + LOG.error("Device %s not support operation %s", + self.device_node_name, self.GET_TDQUOTE) + return None + + try: + fcntl.ioctl(fd_tdx_device, + operator, + tdquote_req) + except OSError: + LOG.error("Fail to execute tdquote ioctl for file %s", self.device_node_name) + os.close(fd_tdx_device) + return None + os.close(fd_tdx_device) + + # # # 7. Get tdreport bytes form tdx_quote_req + tdquote_bytes = self.get_tdquote_bytes_from_req(tdquote_req) + return tdquote_bytes + + def create_tdx_quote_req(self, tdreport): + ''' + Method create_tdx_quote_req creates a tdx_quote_req struct + with report_data. Refer TDX Guest Hypervisor Communication + Interface (GHCI) specification. + struct tdx_quote_hdr { + /* Quote version, filled by TD */ + __u64 version; + /* Status code of Quote request, filled by VMM */ + __u64 status; + /* Length of TDREPORT, filled by TD */ + __u32 in_len; + /* Length of Quote, filled by VMM */ + __u32 out_len; + /* Actual Quote data or TDREPORT on input */ + __u64 data[0]; + }; + ''' + report_size = len(tdreport) + version = 1 + status = 0 + in_len = 0 + out_len = 0 + + qgs_msg = self.qgs_msg_quote_req(tdreport) + in_len = len(qgs_msg) + 4 + + if self.device_node_name == DEVICE_NODE_NAME_1_0: + tdquote_hdr = struct.pack(f"QQII4s{report_size}s", version, status, in_len, out_len, + len(qgs_msg).to_bytes(4, "big"), qgs_msg) + self.tdquote = ctypes.create_string_buffer(TDX_QUOTE_LEN) + self.tdquote[:len(tdquote_hdr)] = tdquote_hdr + req = struct.pack("QQ", ctypes.addressof(self.tdquote),TDX_QUOTE_LEN) + return req + + if self.device_node_name == DEVICE_NODE_NAME_1_5: + LOG.error("TDX 1.5 is not supported yet") + return None + return None + + def get_tdquote_bytes_from_req(self, req): + ''' + Method get_tdquote_bytes_from_req retrieves the tdquote in bytes + format from the tdx_report_req struct. + struct tdx_quote_req { + __u64 buf; + __u64 len; + }; + ''' + if self.device_node_name == DEVICE_NODE_NAME_1_0: + buf_addr, buf_len = struct.unpack("QQ", req) + buf = (ctypes.c_char * buf_len).from_address(buf_addr) + # sizeof(version + status + in_len + out_len) = 24 + data_len = buf_len - 24 + _, _, _, out_len, data = struct.unpack(f"QQII{data_len}s", buf) + data_len = int.from_bytes(data[:4], "big") + if data_len != out_len - 4: + LOG.error("TD Quote data length sanity check failed") + return None + tdquote = self.qgs_msg_quote_resp(data[4:]) + return tdquote + if self.device_node_name == DEVICE_NODE_NAME_1_5: + LOG.error("TDX 1.5 is not supported yet") + return None + return None + + def get_tee_tcb_info_valid_val(self): + ''' + Method get_tee_tcb_info_valid_val helps get the valid value + of the field tdreport.tee_tcb_info.valid. + ''' + val = b"\x00" + if self.device_node_name == DEVICE_NODE_NAME_DEPRECATED: + LOG.error("Deprecated device node %s, please upgrade to use %s or %s", + DEVICE_NODE_NAME_DEPRECATED, DEVICE_NODE_NAME_1_0, DEVICE_NODE_NAME_1_5) + elif self.device_node_name == DEVICE_NODE_NAME_1_0: + val = TCB_INFO_VALID_VAL_1_0 + elif self.device_node_name == DEVICE_NODE_NAME_1_5: + val = TCB_INFO_VALID_VAL_1_5 + return val + +class ModuleVersion: + ''' + class ModuleVersion contains version infomation of tdx module + ''' + + VALID_SVN_LENGTH = 16 + + def __init__(self, release_names: List[str], major: int, minor: int, is_debug: bool = False): + self.release_names = release_names + self.major = major + self.minor = minor + self.is_debug = is_debug + + @staticmethod + def from_bytes(tee_tcb_svn: bytes): + ''' + Method from_bytes parses bytes of the svn, if it + is valid, return the instance of the module version. + ''' + if len(tee_tcb_svn) != ModuleVersion.VALID_SVN_LENGTH: + return None, False + version_bytes = tee_tcb_svn[0:2] + for version in VALID_MODULE_VERSIONS: + if version.to_hex().to_bytes(2, byteorder='little') == version_bytes: + return version, True + return None, False + + def to_hex(self): + ''' + Method to_hex converts the module version to the svn in hex. + ''' + return self.major * 16 * 16 + self.minor + + def __str__(self): + sep = " or " + names = sep.join(self.release_names) + return (f'module version: {{ ' + f'release_names: {names},' + f'major: {self.major},' + f'minor: {self.minor},' + f'is_debug: {self.is_debug}' + f' }}' + ) + +VALID_MODULE_VERSIONS = [ + ModuleVersion(["1.0"], 0, 0, True), + ModuleVersion(["1.0"], 0, 3), + ModuleVersion(["1.4", "1.5"], 1, 0, True), + ModuleVersion(["1.4", "1.5"], 1, 3), + ModuleVersion(["2.0"], 2, 0, True), + ModuleVersion(["2.0"], 2, 3), +] diff --git a/common/python/requirements.txt b/common/python/requirements.txt new file mode 100644 index 00000000..e69de29b diff --git a/samples/CCTrustedInspect/README.md b/samples/CCTrustedInspect/README.md index 49652a77..3b192bf9 100644 --- a/samples/CCTrustedInspect/README.md +++ b/samples/CCTrustedInspect/README.md @@ -1,4 +1,5 @@ # Inspect Tool Get and dump the trusted primitives like measurement, event log, quote. With different -back-end SDK, the inspected TCB could be VM base, container or cluster. \ No newline at end of file +back-end SDK, the inspected TCB could be VM base, container or cluster. + diff --git a/setupenv.sh b/setupenv.sh new file mode 100644 index 00000000..84e9e6ba --- /dev/null +++ b/setupenv.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +CURR_DIR=$(pwd) + +# setup PYTHONPATH +export PYTHONPATH=$PYTHONPATH:$CURR_DIR/common/python/:$CURR_DIR/vmsdk/python diff --git a/vmsdk/python/cctrusted/__init__.py b/vmsdk/python/cctrusted/__init__.py index 6f311a23..ec743861 100644 --- a/vmsdk/python/cctrusted/__init__.py +++ b/vmsdk/python/cctrusted/__init__.py @@ -2,11 +2,5 @@ Package to provide CC trusted API for confidential VM """ -def get_measurements(): - pass - -def get_eventlog(): - pass - -def get_quote(): - pass +# pylint: disable=syntax-error +from .api import* diff --git a/vmsdk/python/cctrusted/api.py b/vmsdk/python/cctrusted/api.py new file mode 100644 index 00000000..b9b938a2 --- /dev/null +++ b/vmsdk/python/cctrusted/api.py @@ -0,0 +1,11 @@ +""" +CC Trusted API Implementation. +""" + +# pylint: disable=unused-import +from cctrusted_base.imr import TcgIMR + +def get_measurement(imr_select_index:int) -> TcgIMR: + """ + Get IMR register value according to given index + """ diff --git a/vmsdk/python/cctrusted/cc_linux.py b/vmsdk/python/cctrusted/cc_linux.py new file mode 100644 index 00000000..2c34bc25 --- /dev/null +++ b/vmsdk/python/cctrusted/cc_linux.py @@ -0,0 +1,77 @@ +""" +The CC stub in Linux. + +If it is device node, it can be /dev/tdx-guest or /dev/sev-guest +If also can be sysfs +""" + +from abc import ABC, abstractmethod + +class CcDeviceNode: + + @property + @abstractmethod + def device_node_path(self) -> str: + """ + Return the name of device node + """ + raise NotImplementedError("Need implement in inherited class") + +class TdxDeviceNode(CcDeviceNode): + + DEVICE_NODE_NAME_1_0 = "/dev/tdx-guest" + DEVICE_NODE_NAME_1_5 = "/dev/tdx_guest" + + @property + def device_node_path(self) -> str: + return TdxDeviceNode.DEVICE_NODE_NAME_1_5 + +class CcStub: + + TYPE_CC_NONE = -1 + TYPE_CC_TDX = 0 + TYPE_CC_SEV = 1 + TYPE_CC_CCA = 1 + + _inst = None + + def __init__(self): + self._device_node:CcDeviceNode = None + self._cc_type = CcStub.TYPE_CC_NONE + self._is_init = False + + @property + def cc_type(self) -> int: + """ + CC type like TYPE_CC_TDX, TYPE_CC_SEV etc + """ + return self._cc_type + + def init(self): + """ + Initialize the CC stub and environment + """ + if self._cc_type is not CcStub.TYPE_CC_NONE: + return + + cc_type = self._detect_cc_type() + if not self._process_device_node(): + return False + self._cc_type = cc_type + return True + + def _detect_cc_type(self): + # TODO: add code to detect CC type + return CcStub.TYPE_CC_NONE + + def _process_device_node(self): + # TODO: add code to process device node + return True + + @staticmethod + def inst() -> CcStub: + if CcStub._inst is None: + inst = CcStub() + if inst.init(): + CcStub._inst = inst + return CcStub._inst diff --git a/vmsdk/python/requirements.txt b/vmsdk/python/requirements.txt new file mode 100644 index 00000000..e69de29b