blob: 3e15c67261bcaa5723fec9d5c0ed243446e3dd81 [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
21from typing import Optional, Union
22import unittest
23
24
25class Expr:
26 """Representation of a C expression with a known or knowable numerical value."""
27 def __init__(self, content: Union[int, str]):
28 if isinstance(content, int):
29 digits = 8 if content > 0xffff else 4
30 self.string = '{0:#0{1}x}'.format(content, digits + 2)
31 self.value_if_known = content #type: Optional[int]
32 else:
33 self.string = content
34 self.value_if_known = None
35
36 value_cache = {
37 # Hard-coded for initial testing
38 'PSA_KEY_LIFETIME_PERSISTENT': 0x00000001,
39 'PSA_KEY_TYPE_RAW_DATA': 0x1001,
40 }
41
42 def update_cache(self) -> None:
43 pass #not implemented yet
44
45 @staticmethod
46 def normalize(string: str) -> str:
47 """Put the given C expression in a canonical form.
48
49 This function is only intended to give correct results for the
50 relatively simple kind of C expression typically used with this
51 module.
52 """
53 return re.sub(r'\s+', r'', string)
54
55 def value(self) -> int:
56 """Return the numerical value of the expression."""
57 if self.value_if_known is None:
58 if re.match(r'([0-9]+|0x[0-9a-f]+)\Z', self.string, re.I):
59 return int(self.string, 0)
60 normalized = self.normalize(self.string)
61 if normalized not in self.value_cache:
62 self.update_cache()
63 self.value_if_known = self.value_cache[normalized]
64 return self.value_if_known
65
66Exprable = Union[str, int, Expr]
67"""Something that can be converted to a C expression with a known numerical value."""
68
69def as_expr(thing: Exprable) -> Expr:
70 """Return an `Expr` object for `thing`.
71
72 If `thing` is already an `Expr` object, return it. Otherwise build a new
73 `Expr` object from `thing`. `thing` can be an integer or a string that
74 contains a C expression.
75 """
76 if isinstance(thing, Expr):
77 return thing
78 else:
79 return Expr(thing)
80
81
82class Key:
83 """Representation of a PSA crypto key object and its storage encoding.
84 """
85
86 LATEST_VERSION = 0
87 """The latest version of the storage format."""
88
89 def __init__(self, *,
90 version: Optional[int] = None,
91 id: Optional[int] = None, #pylint: disable=redefined-builtin
92 lifetime: Exprable = 'PSA_KEY_LIFETIME_PERSISTENT',
93 type: Exprable, #pylint: disable=redefined-builtin
94 bits: int,
95 usage: Exprable, alg: Exprable, alg2: Exprable,
96 material: bytes #pylint: disable=used-before-assignment
97 ) -> None:
98 self.version = self.LATEST_VERSION if version is None else version
99 self.id = id #pylint: disable=invalid-name #type: Optional[int]
100 self.lifetime = as_expr(lifetime) #type: Expr
101 self.type = as_expr(type) #type: Expr
102 self.bits = bits #type: int
103 self.usage = as_expr(usage) #type: Expr
104 self.alg = as_expr(alg) #type: Expr
105 self.alg2 = as_expr(alg2) #type: Expr
106 self.material = material #type: bytes
107
108 MAGIC = b'PSA\000KEY\000'
109
110 @staticmethod
111 def pack(
112 fmt: str,
113 *args: Union[int, Expr]
114 ) -> bytes: #pylint: disable=used-before-assignment
115 """Pack the given arguments into a byte string according to the given format.
116
117 This function is similar to `struct.pack`, but with the following differences:
118 * All integer values are encoded with standard sizes and in
119 little-endian representation. `fmt` must not include an endianness
120 prefix.
121 * Arguments can be `Expr` objects instead of integers.
122 * Only integer-valued elements are supported.
123 """
124 return struct.pack('<' + fmt, # little-endian, standard sizes
125 *[arg.value() if isinstance(arg, Expr) else arg
126 for arg in args])
127
128 def bytes(self) -> bytes:
129 """Return the representation of the key in storage as a byte array.
130
131 This is the content of the PSA storage file. When PSA storage is
132 implemented over stdio files, this does not include any wrapping made
133 by the PSA-storage-over-stdio-file implementation.
134 """
135 header = self.MAGIC + self.pack('L', self.version)
136 if self.version == 0:
137 attributes = self.pack('LHHLLL',
138 self.lifetime, self.type, self.bits,
139 self.usage, self.alg, self.alg2)
140 material = self.pack('L', len(self.material)) + self.material
141 else:
142 raise NotImplementedError
143 return header + attributes + material
144
145 def hex(self) -> str:
146 """Return the representation of the key as a hexadecimal string.
147
148 This is the hexadecimal representation of `self.bytes`.
149 """
150 return self.bytes().hex()
151
152
153class TestKey(unittest.TestCase):
154 # pylint: disable=line-too-long
155 """A few smoke tests for the functionality of the `Key` class."""
156
157 def test_numerical(self):
158 key = Key(version=0,
159 id=1, lifetime=0x00000001,
160 type=0x2400, bits=128,
161 usage=0x00000300, alg=0x05500200, alg2=0x04c01000,
162 material=b'@ABCDEFGHIJKLMNO')
163 expected_hex = '505341004b45590000000000010000000024800000030000000250050010c00410000000404142434445464748494a4b4c4d4e4f'
164 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
165 self.assertEqual(key.hex(), expected_hex)
166
167 def test_names(self):
168 length = 0xfff8 // 8 # PSA_MAX_KEY_BITS in bytes
169 key = Key(version=0,
170 id=1, lifetime='PSA_KEY_LIFETIME_PERSISTENT',
171 type='PSA_KEY_TYPE_RAW_DATA', bits=length*8,
172 usage=0, alg=0, alg2=0,
173 material=b'\x00' * length)
174 expected_hex = '505341004b45590000000000010000000110f8ff000000000000000000000000ff1f0000' + '00' * length
175 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
176 self.assertEqual(key.hex(), expected_hex)
177
178 def test_defaults(self):
179 key = Key(type=0x1001, bits=8,
180 usage=0, alg=0, alg2=0,
181 material=b'\x2a')
182 expected_hex = '505341004b455900000000000100000001100800000000000000000000000000010000002a'
183 self.assertEqual(key.bytes(), bytes.fromhex(expected_hex))
184 self.assertEqual(key.hex(), expected_hex)