-
Notifications
You must be signed in to change notification settings - Fork 742
/
countermeasure.py
221 lines (193 loc) · 7.47 KB
/
countermeasure.py
1
2
3
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# Copyright lowRISC contributors (OpenTitan project).
# 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 reggen.lib import check_keys, check_str, check_list
# The documentation of assets and cm_types can be found here
# https://opentitan.org/book/doc/contributing/hw/comportability/#security-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',
'ADDR_INFECTION',
'INTEGRITY',
'READBACK',
'ADDR_INFECTION',
'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(block_name: str, hjson_path: str,
rtl_names: Dict[str, List[Tuple[str, int]]],
hjson_list: List['CounterMeasure']) -> bool:
"""Compare RTL to Hjson countermeasures.
This compares a dictionary of countermeasure names extracted from the
RTL against the list defined in the IP Hjson and checks that:
- every name in the RTL should match one in Hjson
- every entry in Hjson should have some matching name in the RTL
Any mismatch logs an error showing the file and line of the problem.
Returns True if there are no errors.
"""
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?
rtl_only_countermeasures = rtl_set - hjson_set
for name in rtl_only_countermeasures:
for rtl_path, line in rtl_names[name]:
log.error(f"No Hjson countermeasure {name} exists for RTL "
f"reference at {rtl_path}, line {line}.")
missing_countermeasures = hjson_set - rtl_set
for name in missing_countermeasures:
log.error(f"Hjson countermeasure {name} declared in Hjson file "
f"{hjson_path} doesn't appear in the RTL.")
if len(rtl_only_countermeasures) > 0:
if len(missing_countermeasures) > 0:
log.error(f"Block {block_name}: Some unknown and some missing "
"countermeasures in RTL")
else:
log.error(f"Block {block_name}: Some unknown countermeasures "
"referenced in RTL.")
return False
elif len(missing_countermeasures) > 0:
log.error(f"Block {block_name}: Some countermeasures in Hjson "
"don't appear in RTL.")
return False
return True