blob: 88992a6fc5161ea385a63c6005393944f0f38ce6 [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
Gilles Peskinee0094482021-02-17 14:34:37 +0100110 def __init__(self, *,
111 version: Optional[int] = None,
112 id: Optional[int] = None, #pylint: disable=redefined-builtin
113 lifetime: Exprable = 'PSA_KEY_LIFETIME_PERSISTENT',
114 type: Exprable, #pylint: disable=redefined-builtin
115 bits: int,
116 usage: Exprable, alg: Exprable, alg2: Exprable,
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200117 material: bytes, #pylint: disable=used-before-assignment
gabor-mezei-armacfcc182021-06-28 17:40:32 +0200118 implicit_usage: bool = True
Gilles Peskinee0094482021-02-17 14:34:37 +0100119 ) -> None:
120 self.version = self.LATEST_VERSION if version is None else version
121 self.id = id #pylint: disable=invalid-name #type: Optional[int]
122 self.lifetime = as_expr(lifetime) #type: Expr
123 self.type = as_expr(type) #type: Expr
124 self.bits = bits #type: int
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200125 self.original_usage = as_expr(usage) #type: Expr
126 self.updated_usage = self.original_usage #type: Expr
Gilles Peskinee0094482021-02-17 14:34:37 +0100127 self.alg = as_expr(alg) #type: Expr
128 self.alg2 = as_expr(alg2) #type: Expr
129 self.material = material #type: bytes
130
gabor-mezei-armacfcc182021-06-28 17:40:32 +0200131 if implicit_usage:
gabor-mezei-arm927742e2021-06-28 16:27:29 +0200132 for flag, extension in self.IMPLICIT_USAGE_FLAGS.items():
gabor-mezei-arme84d3212021-06-28 16:54:11 +0200133 if self.original_usage.value() & Expr(flag).value() and \
134 self.original_usage.value() & Expr(extension).value() == 0:
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200135 self.updated_usage = Expr(self.updated_usage.string +
gabor-mezei-arme84d3212021-06-28 16:54:11 +0200136 ' | ' + extension)
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200137
Gilles Peskinee0094482021-02-17 14:34:37 +0100138 MAGIC = b'PSA\000KEY\000'
139
140 @staticmethod
141 def pack(
142 fmt: str,
143 *args: Union[int, Expr]
144 ) -> bytes: #pylint: disable=used-before-assignment
145 """Pack the given arguments into a byte string according to the given format.
146
147 This function is similar to `struct.pack`, but with the following differences:
148 * All integer values are encoded with standard sizes and in
149 little-endian representation. `fmt` must not include an endianness
150 prefix.
151 * Arguments can be `Expr` objects instead of integers.
152 * Only integer-valued elements are supported.
153 """
154 return struct.pack('<' + fmt, # little-endian, standard sizes
155 *[arg.value() if isinstance(arg, Expr) else arg
156 for arg in args])
157
158 def bytes(self) -> bytes:
159 """Return the representation of the key in storage as a byte array.
160
161 This is the content of the PSA storage file. When PSA storage is
162 implemented over stdio files, this does not include any wrapping made
163 by the PSA-storage-over-stdio-file implementation.
164 """
165 header = self.MAGIC + self.pack('L', self.version)
166 if self.version == 0:
167 attributes = self.pack('LHHLLL',
168 self.lifetime, self.type, self.bits,
gabor-mezei-arm15c1f032021-06-24 10:04:38 +0200169 self.updated_usage, self.alg, self.alg2)
Gilles Peskinee0094482021-02-17 14:34:37 +0100170 material = self.pack('L', len(self.material)) + self.material
171 else:
172 raise NotImplementedError
173 return header + attributes + material
174
175 def hex(self) -> str:
176 """Return the representation of the key as a hexadecimal string.
177
178 This is the hexadecimal representation of `self.bytes`.
179 """
180 return self.bytes().hex()
181
Gilles Peskineefb584d2021-04-21 22:05:34 +0200182 def location_value(self) -> int:
183 """The numerical value of the location encoded in the key's lifetime."""
184 return self.lifetime.value() >> 8
185
Gilles Peskinee0094482021-02-17 14:34:37 +0100186
187class TestKey(unittest.TestCase):
188 # pylint: disable=line-too-long
189 """A few smoke tests for the functionality of the `Key` class."""
190
191 def test_numerical(self):
192 key = Key(version=0,
193 id=1, lifetime=0x00000001,
194 type=0x2400, bits=128,
195 usage=0x00000300, alg=0x05500200, alg2=0x04c01000,
196 material=b'@ABCDEFGHIJKLMNO')
197 expected_hex = '505341004b45590000000000010000000024800000030000000250050010c00410000000404142434445464748494a4b4c4d4e4f'
198 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
199 self.assertEqual(key.hex(), expected_hex)
200
201 def test_names(self):
202 length = 0xfff8 // 8 # PSA_MAX_KEY_BITS in bytes
203 key = Key(version=0,
204 id=1, lifetime='PSA_KEY_LIFETIME_PERSISTENT',
205 type='PSA_KEY_TYPE_RAW_DATA', bits=length*8,
206 usage=0, alg=0, alg2=0,
207 material=b'\x00' * length)
208 expected_hex = '505341004b45590000000000010000000110f8ff000000000000000000000000ff1f0000' + '00' * length
209 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
210 self.assertEqual(key.hex(), expected_hex)
211
212 def test_defaults(self):
213 key = Key(type=0x1001, bits=8,
214 usage=0, alg=0, alg2=0,
215 material=b'\x2a')
216 expected_hex = '505341004b455900000000000100000001100800000000000000000000000000010000002a'
217 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
218 self.assertEqual(key.hex(), expected_hex)