| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import re |
| import logging as log |
| from typing import Dict, List, Sequence, Tuple |
| |
| from .lib import check_keys, check_str, check_list |
| |
| # The documentation of assets and cm_types can be found here |
| # https://docs.opentitan.org/doc/rm/comportability_specification/#countermeasures |
| CM_ASSETS = [ |
| 'KEY', |
| 'ADDR', |
| 'DATA_REG', |
| 'DATA_REG_SW', |
| 'CTRL_FLOW', |
| 'CTRL', |
| 'CONFIG', |
| 'LFSR', |
| 'RNG', |
| 'CTR', |
| 'FSM', |
| 'MEM', |
| 'CLK', |
| 'RST', |
| 'BUS', |
| 'INTERSIG', |
| 'MUX', |
| 'CONSTANTS', |
| 'STATE', |
| 'TOKEN', |
| 'LOGIC' |
| ] |
| |
| CM_TYPES = [ |
| 'MUBI', |
| 'SPARSE', |
| 'DIFF', |
| 'REDUN', |
| 'REGWEN', |
| 'REGREN', |
| 'SHADOW', |
| 'SCRAMBLE', |
| 'INTEGRITY', |
| 'CONSISTENCY', |
| 'DIGEST', |
| 'LC_GATED', |
| 'BKGN_CHK', |
| 'GLITCH_DETECT', |
| 'SW_UNREADABLE', |
| 'SW_UNWRITABLE', |
| 'SW_NOACCESS', |
| 'SIDELOAD', |
| 'SEC_WIPE', |
| 'SCA', |
| 'MASKING', |
| 'LOCAL_ESC', |
| 'GLOBAL_ESC', |
| 'UNPREDICTABLE', |
| 'TERMINAL', |
| 'COUNT', |
| 'CM' |
| ] |
| |
| # Tag to look for when extracting RTL annotations. |
| CM_RTL_TAG = 'SEC_CM:' |
| |
| |
| class CounterMeasure: |
| """Object holding details for one countermeasure within an IP block.""" |
| |
| def __init__(self, instance: str, asset: str, cm_type: str, desc: str): |
| self.instance = instance |
| self.asset = asset |
| self.cm_type = cm_type |
| self.desc = desc |
| |
| @staticmethod |
| def from_raw(what: str, raw: object) -> 'CounterMeasure': |
| """ |
| Create a CounterMeasure object from a dict. |
| |
| The 'raw' dict must have the keys 'name' and 'desc', where 'name' has |
| to follow the canonical countermeasure naming convention. |
| """ |
| rd = check_keys(raw, what, ['name', 'desc'], []) |
| |
| name = check_str(rd['name'], f'name field of {what}') |
| desc = check_str(rd['desc'], f'desc field of {what}') |
| |
| try: |
| # the format is [CM_INST_NAME].<CM_ASSET>.<CM_TYPE> |
| # i.e., the countermeasure instance name is optional. |
| fields = name.split('.') |
| if len(fields) == 3: |
| instance, asset, cm_type = fields |
| else: |
| asset, cm_type = fields |
| instance = "" |
| except ValueError: |
| raise ValueError( |
| f'Invalid countermeasure ID format: {name} ({what}).') |
| if not re.fullmatch(r'^([a-zA-Z0-9_]*)', instance): |
| raise ValueError( |
| f'Invalid countermeasure instance name: {instance} ({what}).') |
| if asset not in CM_ASSETS: |
| raise ValueError( |
| f'Invalid countermeasure asset: {asset} ({what}).') |
| if cm_type not in CM_TYPES: |
| raise ValueError( |
| f'Invalid countermeasure type: {cm_type} ({what}).') |
| return CounterMeasure(instance, asset, cm_type, desc) |
| |
| @staticmethod |
| def from_raw_list(what: str, |
| raw: object) -> List['CounterMeasure']: |
| """ |
| Create a list of CounterMeasure objects from a list of dicts. |
| |
| The dicts in 'raw' must have the keys 'name' and 'desc', where 'name' |
| has to follow the canonical countermeasure naming convention. |
| """ |
| ret = [] |
| for idx, entry in enumerate(check_list(raw, what)): |
| entry_what = f'entry {idx} of {what}' |
| cm = CounterMeasure.from_raw(entry_what, entry) |
| ret.append(cm) |
| return ret |
| |
| def _asdict(self) -> Dict[str, object]: |
| """Returns a dict with 'name' and 'desc' fields""" |
| return { |
| 'name': str(self), |
| 'desc': self.desc |
| } |
| |
| def __str__(self) -> str: |
| namestr = self.asset + '.' + self.cm_type |
| return self.instance + '.' + namestr if self.instance else namestr |
| |
| @staticmethod |
| def search_rtl_files(paths: Sequence[str]) -> Dict[str, |
| List[Tuple[str, int]]]: |
| """Find countermeasures in the given list of RTL files. |
| |
| The return value is a dictionary mapping countermeasure name to where |
| that countermeasure is found (as a (path, line_number) pair). |
| """ |
| ret: Dict[str, List[Tuple[str, int]]] = {} |
| # Match things like "// SEC_CM: FOO" or "// SEC_CM: FOO, BAR" |
| regex = re.compile(r'//\s*' + CM_RTL_TAG + r'\s*(.*)') |
| good_name = re.compile(r'[A-Za-z0-9_.]+$') |
| |
| for path in paths: |
| with open(path) as fd: |
| for idx, line in enumerate(fd): |
| match = regex.search(line) |
| if not match: |
| continue |
| |
| # We've got a hit. The regex above is intentionally lax |
| # because we want to see an error if someone writes |
| # something horrible, rather than silently ignoring the |
| # line. Split the match on commas to allow multiple entries |
| # on a line. |
| entries = match.group(1).split(',') |
| for entry in entries: |
| entry = entry.strip() |
| |
| if not good_name.match(entry): |
| raise ValueError('Malformed countermeasure name, ' |
| '{!r}, at {}, line {}.' |
| .format(entry, path, idx + 1)) |
| ret.setdefault(entry, []).append((path, idx + 1)) |
| |
| return ret |
| |
| @staticmethod |
| def check_annotation_list(what: str, |
| rtl_names: Dict[str, List[Tuple[str, int]]], |
| hjson_list: List['CounterMeasure']) -> None: |
| """Compare found list of countermeasures against list from Hjson |
| |
| This compares a dictionary of countermeasure names extracted from the |
| RTL against the list defined in the IP Hjson and checks that they |
| match: every name in the RTL should correspond to an entry in the Hjson |
| and every entry in the Hjson should have at least one matching name in |
| the RTL. |
| |
| """ |
| hjson_set = {str(cm) for cm in hjson_list} |
| rtl_set = set(rtl_names.keys()) |
| |
| # Is there anything in the RTL that doesn't correspond to an Hjson |
| # entry? |
| for name in rtl_set - hjson_set: |
| # Print out an error message for each hit and then raise a |
| # RuntimeError. |
| for path, line in rtl_names[name]: |
| log.error('Unknown countermeasure {!r} ' |
| 'referenced at {}, line {}.' |
| .format(name, path, line)) |
| raise RuntimeError('One or more unknown countermeasures ' |
| 'referenced in RTL.') |
| |
| # Is there anything in the Hjson that isn't described in the RTL? |
| for name in hjson_set - rtl_set: |
| log.warning("Countermeasure {} is referenced in the Hjson of the " |
| "{}, but doesn't appear in the RTL." |
| .format(name, what)) |
| |
| # TODO(#10071): Once all designs are annotated, generate a RuntimeError |
| # if we saw anything in the loop above. |