blob: 3a740072e6d490d108a7b7d27931b3bb3eb4aecf [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
104 def __init__(self, *,
105 version: Optional[int] = None,
106 id: Optional[int] = None, #pylint: disable=redefined-builtin
107 lifetime: Exprable = 'PSA_KEY_LIFETIME_PERSISTENT',
108 type: Exprable, #pylint: disable=redefined-builtin
109 bits: int,
110 usage: Exprable, alg: Exprable, alg2: Exprable,
111 material: bytes #pylint: disable=used-before-assignment
112 ) -> None:
113 self.version = self.LATEST_VERSION if version is None else version
114 self.id = id #pylint: disable=invalid-name #type: Optional[int]
115 self.lifetime = as_expr(lifetime) #type: Expr
116 self.type = as_expr(type) #type: Expr
117 self.bits = bits #type: int
118 self.usage = as_expr(usage) #type: Expr
119 self.alg = as_expr(alg) #type: Expr
120 self.alg2 = as_expr(alg2) #type: Expr
121 self.material = material #type: bytes
122
123 MAGIC = b'PSA\000KEY\000'
124
125 @staticmethod
126 def pack(
127 fmt: str,
128 *args: Union[int, Expr]
129 ) -> bytes: #pylint: disable=used-before-assignment
130 """Pack the given arguments into a byte string according to the given format.
131
132 This function is similar to `struct.pack`, but with the following differences:
133 * All integer values are encoded with standard sizes and in
134 little-endian representation. `fmt` must not include an endianness
135 prefix.
136 * Arguments can be `Expr` objects instead of integers.
137 * Only integer-valued elements are supported.
138 """
139 return struct.pack('<' + fmt, # little-endian, standard sizes
140 *[arg.value() if isinstance(arg, Expr) else arg
141 for arg in args])
142
143 def bytes(self) -> bytes:
144 """Return the representation of the key in storage as a byte array.
145
146 This is the content of the PSA storage file. When PSA storage is
147 implemented over stdio files, this does not include any wrapping made
148 by the PSA-storage-over-stdio-file implementation.
149 """
150 header = self.MAGIC + self.pack('L', self.version)
151 if self.version == 0:
152 attributes = self.pack('LHHLLL',
153 self.lifetime, self.type, self.bits,
154 self.usage, self.alg, self.alg2)
155 material = self.pack('L', len(self.material)) + self.material
156 else:
157 raise NotImplementedError
158 return header + attributes + material
159
160 def hex(self) -> str:
161 """Return the representation of the key as a hexadecimal string.
162
163 This is the hexadecimal representation of `self.bytes`.
164 """
165 return self.bytes().hex()
166
167
168class TestKey(unittest.TestCase):
169 # pylint: disable=line-too-long
170 """A few smoke tests for the functionality of the `Key` class."""
171
172 def test_numerical(self):
173 key = Key(version=0,
174 id=1, lifetime=0x00000001,
175 type=0x2400, bits=128,
176 usage=0x00000300, alg=0x05500200, alg2=0x04c01000,
177 material=b'@ABCDEFGHIJKLMNO')
178 expected_hex = '505341004b45590000000000010000000024800000030000000250050010c00410000000404142434445464748494a4b4c4d4e4f'
179 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
180 self.assertEqual(key.hex(), expected_hex)
181
182 def test_names(self):
183 length = 0xfff8 // 8 # PSA_MAX_KEY_BITS in bytes
184 key = Key(version=0,
185 id=1, lifetime='PSA_KEY_LIFETIME_PERSISTENT',
186 type='PSA_KEY_TYPE_RAW_DATA', bits=length*8,
187 usage=0, alg=0, alg2=0,
188 material=b'\x00' * length)
189 expected_hex = '505341004b45590000000000010000000110f8ff000000000000000000000000ff1f0000' + '00' * length
190 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
191 self.assertEqual(key.hex(), expected_hex)
192
193 def test_defaults(self):
194 key = Key(type=0x1001, bits=8,
195 usage=0, alg=0, alg2=0,
196 material=b'\x2a')
197 expected_hex = '505341004b455900000000000100000001100800000000000000000000000000010000002a'
198 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
199 self.assertEqual(key.hex(), expected_hex)