blob: 5ff173831beb9e39ae9e5e800e3389710cbb5650 [file] [log] [blame]
Gilles Peskinee0094482021-02-17 14:34:37 +01001"""Knowledge about the PSA key store as implemented in Mbed TLS.
2"""
3
4# Copyright The Mbed TLS Contributors
5# SPDX-License-Identifier: Apache-2.0
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
19import re
20import struct
Gilles Peskine23523962021-03-10 01:32:38 +010021from typing import Dict, List, Optional, Set, Union
Gilles Peskinee0094482021-02-17 14:34:37 +010022import unittest
23
Gilles Peskine23523962021-03-10 01:32:38 +010024from mbedtls_dev import c_build_helper
25
Gilles Peskinee0094482021-02-17 14:34:37 +010026
27class Expr:
28 """Representation of a C expression with a known or knowable numerical value."""
Gilles Peskine23523962021-03-10 01:32:38 +010029
Gilles Peskinee0094482021-02-17 14:34:37 +010030 def __init__(self, content: Union[int, str]):
31 if isinstance(content, int):
32 digits = 8 if content > 0xffff else 4
33 self.string = '{0:#0{1}x}'.format(content, digits + 2)
34 self.value_if_known = content #type: Optional[int]
35 else:
36 self.string = content
Gilles Peskine23523962021-03-10 01:32:38 +010037 self.unknown_values.add(self.normalize(content))
Gilles Peskinee0094482021-02-17 14:34:37 +010038 self.value_if_known = None
39
Gilles Peskine23523962021-03-10 01:32:38 +010040 value_cache = {} #type: Dict[str, int]
41 """Cache of known values of expressions."""
42
43 unknown_values = set() #type: Set[str]
44 """Expressions whose values are not present in `value_cache` yet."""
Gilles Peskinee0094482021-02-17 14:34:37 +010045
46 def update_cache(self) -> None:
Gilles Peskine23523962021-03-10 01:32:38 +010047 """Update `value_cache` for expressions registered in `unknown_values`."""
48 expressions = sorted(self.unknown_values)
49 values = c_build_helper.get_c_expression_values(
50 'unsigned long', '%lu',
51 expressions,
52 header="""
53 #include <psa/crypto.h>
54 """,
55 include_path=['include']) #type: List[str]
56 for e, v in zip(expressions, values):
57 self.value_cache[e] = int(v, 0)
58 self.unknown_values.clear()
Gilles Peskinee0094482021-02-17 14:34:37 +010059
60 @staticmethod
61 def normalize(string: str) -> str:
62 """Put the given C expression in a canonical form.
63
64 This function is only intended to give correct results for the
65 relatively simple kind of C expression typically used with this
66 module.
67 """
68 return re.sub(r'\s+', r'', string)
69
70 def value(self) -> int:
71 """Return the numerical value of the expression."""
72 if self.value_if_known is None:
73 if re.match(r'([0-9]+|0x[0-9a-f]+)\Z', self.string, re.I):
74 return int(self.string, 0)
75 normalized = self.normalize(self.string)
76 if normalized not in self.value_cache:
77 self.update_cache()
78 self.value_if_known = self.value_cache[normalized]
79 return self.value_if_known
80
81Exprable = Union[str, int, Expr]
82"""Something that can be converted to a C expression with a known numerical value."""
83
84def as_expr(thing: Exprable) -> Expr:
85 """Return an `Expr` object for `thing`.
86
87 If `thing` is already an `Expr` object, return it. Otherwise build a new
88 `Expr` object from `thing`. `thing` can be an integer or a string that
89 contains a C expression.
90 """
91 if isinstance(thing, Expr):
92 return thing
93 else:
94 return Expr(thing)
95
96
97class Key:
98 """Representation of a PSA crypto key object and its storage encoding.
99 """
100
101 LATEST_VERSION = 0
102 """The latest version of the storage format."""
103
gabor-mezei-arm927742e2021-06-28 16:27:29 +0200104 IMPLICIT_USAGE_FLAGS = {
gabor-mezei-arme84d3212021-06-28 16:54:11 +0200105 'PSA_KEY_USAGE_SIGN_HASH': 'PSA_KEY_USAGE_SIGN_MESSAGE',
106 'PSA_KEY_USAGE_VERIFY_HASH': 'PSA_KEY_USAGE_VERIFY_MESSAGE'
107 } #type: Dict[str, str]
gabor-mezei-arm927742e2021-06-28 16:27:29 +0200108 """Mapping of usage flags to the flags that they imply."""
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200109
gabor-mezei-arm927742e2021-06-28 16:27:29 +0200110 IMPLICIT_USAGE_FLAGS_KEY_RESTRICTION = {
gabor-mezei-arm044fefc2021-06-24 10:16:44 +0200111 'PSA_KEY_USAGE_SIGN_HASH': '.*KEY_PAIR',
112 'PSA_KEY_USAGE_VERIFY_HASH': '.*KEY.*'
113 } #type: Dict[str, str]
gabor-mezei-arm927742e2021-06-28 16:27:29 +0200114 """Use a regexp to determine key types for which signature is possible
115 when using the actual usage flag.
gabor-mezei-arm044fefc2021-06-24 10:16:44 +0200116 """
117
Gilles Peskinee0094482021-02-17 14:34:37 +0100118 def __init__(self, *,
119 version: Optional[int] = None,
120 id: Optional[int] = None, #pylint: disable=redefined-builtin
121 lifetime: Exprable = 'PSA_KEY_LIFETIME_PERSISTENT',
122 type: Exprable, #pylint: disable=redefined-builtin
123 bits: int,
124 usage: Exprable, alg: Exprable, alg2: Exprable,
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200125 material: bytes, #pylint: disable=used-before-assignment
gabor-mezei-armacfcc182021-06-28 17:40:32 +0200126 implicit_usage: bool = True
Gilles Peskinee0094482021-02-17 14:34:37 +0100127 ) -> None:
128 self.version = self.LATEST_VERSION if version is None else version
129 self.id = id #pylint: disable=invalid-name #type: Optional[int]
130 self.lifetime = as_expr(lifetime) #type: Expr
131 self.type = as_expr(type) #type: Expr
132 self.bits = bits #type: int
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200133 self.original_usage = as_expr(usage) #type: Expr
134 self.updated_usage = self.original_usage #type: Expr
Gilles Peskinee0094482021-02-17 14:34:37 +0100135 self.alg = as_expr(alg) #type: Expr
136 self.alg2 = as_expr(alg2) #type: Expr
137 self.material = material #type: bytes
138
gabor-mezei-armacfcc182021-06-28 17:40:32 +0200139 if implicit_usage:
gabor-mezei-arm927742e2021-06-28 16:27:29 +0200140 for flag, extension in self.IMPLICIT_USAGE_FLAGS.items():
gabor-mezei-arme84d3212021-06-28 16:54:11 +0200141 if self.original_usage.value() & Expr(flag).value() and \
142 self.original_usage.value() & Expr(extension).value() == 0:
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200143 self.updated_usage = Expr(self.updated_usage.string +
gabor-mezei-arme84d3212021-06-28 16:54:11 +0200144 ' | ' + extension)
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200145
Gilles Peskinee0094482021-02-17 14:34:37 +0100146 MAGIC = b'PSA\000KEY\000'
147
148 @staticmethod
149 def pack(
150 fmt: str,
151 *args: Union[int, Expr]
152 ) -> bytes: #pylint: disable=used-before-assignment
153 """Pack the given arguments into a byte string according to the given format.
154
155 This function is similar to `struct.pack`, but with the following differences:
156 * All integer values are encoded with standard sizes and in
157 little-endian representation. `fmt` must not include an endianness
158 prefix.
159 * Arguments can be `Expr` objects instead of integers.
160 * Only integer-valued elements are supported.
161 """
162 return struct.pack('<' + fmt, # little-endian, standard sizes
163 *[arg.value() if isinstance(arg, Expr) else arg
164 for arg in args])
165
166 def bytes(self) -> bytes:
167 """Return the representation of the key in storage as a byte array.
168
169 This is the content of the PSA storage file. When PSA storage is
170 implemented over stdio files, this does not include any wrapping made
171 by the PSA-storage-over-stdio-file implementation.
172 """
173 header = self.MAGIC + self.pack('L', self.version)
174 if self.version == 0:
175 attributes = self.pack('LHHLLL',
176 self.lifetime, self.type, self.bits,
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200177 self.updated_usage, self.alg, self.alg2)
Gilles Peskinee0094482021-02-17 14:34:37 +0100178 material = self.pack('L', len(self.material)) + self.material
179 else:
180 raise NotImplementedError
181 return header + attributes + material
182
183 def hex(self) -> str:
184 """Return the representation of the key as a hexadecimal string.
185
186 This is the hexadecimal representation of `self.bytes`.
187 """
188 return self.bytes().hex()
189
Gilles Peskineefb584d2021-04-21 22:05:34 +0200190 def location_value(self) -> int:
191 """The numerical value of the location encoded in the key's lifetime."""
192 return self.lifetime.value() >> 8
193
Gilles Peskinee0094482021-02-17 14:34:37 +0100194
195class TestKey(unittest.TestCase):
196 # pylint: disable=line-too-long
197 """A few smoke tests for the functionality of the `Key` class."""
198
199 def test_numerical(self):
200 key = Key(version=0,
201 id=1, lifetime=0x00000001,
202 type=0x2400, bits=128,
203 usage=0x00000300, alg=0x05500200, alg2=0x04c01000,
204 material=b'@ABCDEFGHIJKLMNO')
205 expected_hex = '505341004b45590000000000010000000024800000030000000250050010c00410000000404142434445464748494a4b4c4d4e4f'
206 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
207 self.assertEqual(key.hex(), expected_hex)
208
209 def test_names(self):
210 length = 0xfff8 // 8 # PSA_MAX_KEY_BITS in bytes
211 key = Key(version=0,
212 id=1, lifetime='PSA_KEY_LIFETIME_PERSISTENT',
213 type='PSA_KEY_TYPE_RAW_DATA', bits=length*8,
214 usage=0, alg=0, alg2=0,
215 material=b'\x00' * length)
216 expected_hex = '505341004b45590000000000010000000110f8ff000000000000000000000000ff1f0000' + '00' * length
217 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
218 self.assertEqual(key.hex(), expected_hex)
219
220 def test_defaults(self):
221 key = Key(type=0x1001, bits=8,
222 usage=0, alg=0, alg2=0,
223 material=b'\x2a')
224 expected_hex = '505341004b455900000000000100000001100800000000000000000000000000010000002a'
225 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
226 self.assertEqual(key.hex(), expected_hex)