xref: /optee_os/scripts/sign_encrypt.py (revision 27f5d377bf681bf647d55755d3a34004f71831a4)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2015, 2017, 2019, Linaro Limited
5#
6
7import sys
8import math
9
10
11algo = {'TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256': 0x70414930,
12        'TEE_ALG_RSASSA_PKCS1_V1_5_SHA256': 0x70004830}
13
14enc_key_type = {'SHDR_ENC_KEY_DEV_SPECIFIC': 0x0,
15                'SHDR_ENC_KEY_CLASS_WIDE': 0x1}
16
17SHDR_BOOTSTRAP_TA = 1
18SHDR_ENCRYPTED_TA = 2
19SHDR_MAGIC = 0x4f545348
20SHDR_SIZE = 20
21
22
23def uuid_parse(s):
24    from uuid import UUID
25    return UUID(s)
26
27
28def int_parse(str):
29    return int(str, 0)
30
31
32def get_args(logger):
33    from argparse import ArgumentParser, RawDescriptionHelpFormatter
34    import textwrap
35    command_base = ['sign-enc', 'digest', 'stitch', 'verify']
36    command_aliases_digest = ['generate-digest']
37    command_aliases_stitch = ['stitch-ta']
38    command_aliases = command_aliases_digest + command_aliases_stitch
39    command_choices = command_base + command_aliases
40
41    dat = '[' + ', '.join(command_aliases_digest) + ']'
42    sat = '[' + ', '.join(command_aliases_stitch) + ']'
43
44    parser = ArgumentParser(
45        description='Sign and encrypt (optional) a Trusted Application for' +
46        ' OP-TEE.',
47        usage='\n   %(prog)s command [ arguments ]\n\n'
48
49        '   command:\n' +
50        '     sign-enc    Generate signed and optionally encrypted loadable' +
51        ' TA image file.\n' +
52        '                 Takes arguments --uuid, --ta-version, --in, --out,' +
53        ' --key,\n' +
54        '                 --enc-key (optional) and' +
55        ' --enc-key-type (optional).\n' +
56        '     digest      Generate loadable TA binary image digest' +
57        ' for offline\n' +
58        '                 signing. Takes arguments --uuid, --ta-version,' +
59        ' --in, --key,\n'
60        '                 --enc-key (optional), --enc-key-type (optional),' +
61        ' --algo (optional) and --dig.\n' +
62        '     stitch      Generate loadable signed and encrypted TA binary' +
63        ' image file from\n' +
64        '                 TA raw image and its signature. Takes' +
65        ' arguments --uuid, --in, --key, --out,\n' +
66        '                 --enc-key (optional), --enc-key-type (optional),\n' +
67        '                 --algo (optional) and --sig.\n' +
68        '     verify      Verify signed TA binary\n' +
69        '                 Takes arguments --uuid, --in, --key\n\n' +
70        '   %(prog)s --help  show available commands and arguments\n\n',
71        formatter_class=RawDescriptionHelpFormatter,
72        epilog=textwrap.dedent('''\
73            If no command is given, the script will default to "sign-enc".
74
75            command aliases:
76              The command \'digest\' can be aliased by ''' + dat + '''
77              The command \'stitch\' can be aliased by ''' + sat + '\n' + '''
78            example offline signing command using OpenSSL for algorithm
79            TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256:
80              base64 -d <UUID>.dig | \\
81              openssl pkeyutl -sign -inkey <KEYFILE>.pem \\
82                  -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pss \\
83                  -pkeyopt rsa_pss_saltlen:digest \\
84                  -pkeyopt rsa_mgf1_md:sha256 | \\
85              base64 > <UUID>.sig\n
86            example offline signing command using OpenSSL for algorithm
87            TEE_ALG_RSASSA_PKCS1_V1_5_SHA256:
88              base64 -d <UUID>.dig | \\
89              openssl pkeyutl -sign -inkey <KEYFILE>.pem \\
90                  -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pkcs1 | \\
91              base64 > <UUID>.sig
92            '''))
93
94    parser.add_argument(
95        'command', choices=command_choices, nargs='?',
96        default='sign-enc',
97        help='Command, one of [' + ', '.join(command_base) + ']')
98    parser.add_argument('--uuid', required=True,
99                        type=uuid_parse, help='String UUID of the TA')
100    parser.add_argument('--key', required=True,
101                        help='Name of signing key file (PEM format)')
102    parser.add_argument('--enc-key', required=False,
103                        help='Encryption key string')
104    parser.add_argument(
105        '--enc-key-type', required=False, default='SHDR_ENC_KEY_DEV_SPECIFIC',
106        choices=list(enc_key_type.keys()),
107        help='Encryption key type.\n' +
108        '(SHDR_ENC_KEY_DEV_SPECIFIC or SHDR_ENC_KEY_CLASS_WIDE).\n' +
109        'Defaults to SHDR_ENC_KEY_DEV_SPECIFIC.')
110    parser.add_argument(
111        '--ta-version', required=False, type=int_parse, default=0,
112        help='TA version stored as a 32-bit unsigned integer and used for\n' +
113        'rollback protection of TA install in the secure database.\n' +
114        'Defaults to 0.')
115    parser.add_argument(
116        '--sig', required=False, dest='sigf',
117        help='Name of signature input file, defaults to <UUID>.sig')
118    parser.add_argument(
119        '--dig', required=False, dest='digf',
120        help='Name of digest output file, defaults to <UUID>.dig')
121    parser.add_argument(
122        '--in', required=True, dest='inf',
123        help='Name of application input file, defaults to <UUID>.stripped.elf')
124    parser.add_argument(
125        '--out', required=False, dest='outf',
126        help='Name of application output file, defaults to <UUID>.ta')
127    parser.add_argument('--algo', required=False, choices=list(algo.keys()),
128                        default='TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256',
129                        help='The hash and signature algorithm, ' +
130                        'defaults to TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256. ' +
131                        'Allowed values are: ' +
132                        ', '.join(list(algo.keys())), metavar='')
133
134    parsed = parser.parse_args()
135
136    # Check parameter combinations
137
138    if parsed.digf is None and \
139       parsed.outf is not None and \
140       parsed.command in ['digest'] + command_aliases_digest:
141        logger.error('A digest was requested, but argument --out was given.' +
142                     '  Did you mean:\n  ' +
143                     parser.prog+' --dig ' + parsed.outf + ' ...')
144        sys.exit(1)
145
146    if parsed.digf is not None \
147       and parsed.outf is not None \
148       and parsed.command in ['digest'] + command_aliases_digest:
149        logger.warn('A digest was requested, but arguments --dig and ' +
150                    '--out were given.\n' +
151                    '  --out will be ignored.')
152
153    # Set defaults for optional arguments.
154
155    if parsed.sigf is None:
156        parsed.sigf = str(parsed.uuid)+'.sig'
157    if parsed.digf is None:
158        parsed.digf = str(parsed.uuid)+'.dig'
159    if parsed.inf is None:
160        parsed.inf = str(parsed.uuid)+'.stripped.elf'
161    if parsed.outf is None:
162        parsed.outf = str(parsed.uuid)+'.ta'
163
164    return parsed
165
166
167def main():
168    from cryptography import exceptions
169    from cryptography.hazmat.backends import default_backend
170    from cryptography.hazmat.primitives import serialization
171    from cryptography.hazmat.primitives import hashes
172    from cryptography.hazmat.primitives.asymmetric import padding
173    from cryptography.hazmat.primitives.asymmetric import rsa
174    from cryptography.hazmat.primitives.asymmetric import utils
175    import base64
176    import logging
177    import os
178    import struct
179
180    logging.basicConfig()
181    logger = logging.getLogger(os.path.basename(__file__))
182
183    args = get_args(logger)
184
185    with open(args.key, 'rb') as f:
186        data = f.read()
187
188        try:
189            key = serialization.load_pem_private_key(data, password=None,
190                                                     backend=default_backend())
191        except ValueError:
192            key = serialization.load_pem_public_key(data,
193                                                    backend=default_backend())
194
195    with open(args.inf, 'rb') as f:
196        img = f.read()
197
198    chosen_hash = hashes.SHA256()
199    h = hashes.Hash(chosen_hash, default_backend())
200
201    digest_len = chosen_hash.digest_size
202    sig_len = math.ceil(key.key_size / 8)
203
204    img_size = len(img)
205
206    hdr_version = args.ta_version  # struct shdr_bootstrap_ta::ta_version
207
208    magic = SHDR_MAGIC
209    if args.enc_key:
210        img_type = SHDR_ENCRYPTED_TA
211    else:
212        img_type = SHDR_BOOTSTRAP_TA
213
214    shdr = struct.pack('<IIIIHH',
215                       magic, img_type, img_size, algo[args.algo],
216                       digest_len, sig_len)
217    shdr_uuid = args.uuid.bytes
218    shdr_version = struct.pack('<I', hdr_version)
219
220    if args.enc_key:
221        from cryptography.hazmat.primitives.ciphers.aead import AESGCM
222        cipher = AESGCM(bytes.fromhex(args.enc_key))
223        # Use 12 bytes for nonce per recommendation
224        nonce = os.urandom(12)
225        out = cipher.encrypt(nonce, img, None)
226        ciphertext = out[:-16]
227        # Authentication Tag is always the last 16 bytes
228        tag = out[-16:]
229
230        enc_algo = 0x40000810      # TEE_ALG_AES_GCM
231        flags = enc_key_type[args.enc_key_type]
232        ehdr = struct.pack('<IIHH',
233                           enc_algo, flags, len(nonce), len(tag))
234
235    h.update(shdr)
236    h.update(shdr_uuid)
237    h.update(shdr_version)
238    if args.enc_key:
239        h.update(ehdr)
240        h.update(nonce)
241        h.update(tag)
242    h.update(img)
243    img_digest = h.finalize()
244
245    def write_image_with_signature(sig):
246        with open(args.outf, 'wb') as f:
247            f.write(shdr)
248            f.write(img_digest)
249            f.write(sig)
250            f.write(shdr_uuid)
251            f.write(shdr_version)
252            if args.enc_key:
253                f.write(ehdr)
254                f.write(nonce)
255                f.write(tag)
256                f.write(ciphertext)
257            else:
258                f.write(img)
259
260    def sign_encrypt_ta():
261        if not isinstance(key, rsa.RSAPrivateKey):
262            logger.error('Provided key cannot be used for signing, ' +
263                         'please use offline-signing mode.')
264            sys.exit(1)
265        else:
266            if args.algo == 'TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256':
267                sig = key.sign(
268                    img_digest,
269                    padding.PSS(
270                        mgf=padding.MGF1(chosen_hash),
271                        salt_length=digest_len
272                    ),
273                    utils.Prehashed(chosen_hash)
274                )
275            elif args.algo == 'TEE_ALG_RSASSA_PKCS1_V1_5_SHA256':
276                sig = key.sign(
277                    img_digest,
278                    padding.PKCS1v15(),
279                    utils.Prehashed(chosen_hash)
280                )
281
282            if len(sig) != sig_len:
283                raise Exception(("Actual signature length is not equal to ",
284                                 "the computed one: {} != {}").
285                                format(len(sig), sig_len))
286            write_image_with_signature(sig)
287            logger.info('Successfully signed application.')
288
289    def generate_digest():
290        with open(args.digf, 'wb+') as digfile:
291            digfile.write(base64.b64encode(img_digest))
292
293    def stitch_ta():
294        try:
295            with open(args.sigf, 'r') as sigfile:
296                sig = base64.b64decode(sigfile.read())
297        except IOError:
298            if not os.path.exists(args.digf):
299                generate_digest()
300            logger.error('No signature file found. Please sign\n %s\n' +
301                         'offline and place the signature at \n %s\n' +
302                         'or pass a different location ' +
303                         'using the --sig argument.\n',
304                         args.digf, args.sigf)
305            sys.exit(1)
306        else:
307            try:
308                if args.algo == 'TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256':
309                    key.verify(
310                        sig,
311                        img_digest,
312                        padding.PSS(
313                            mgf=padding.MGF1(chosen_hash),
314                            salt_length=digest_len
315                        ),
316                        utils.Prehashed(chosen_hash)
317                    )
318                elif args.algo == 'TEE_ALG_RSASSA_PKCS1_V1_5_SHA256':
319                    key.verify(
320                        sig,
321                        img_digest,
322                        padding.PKCS1v15(),
323                        utils.Prehashed(chosen_hash)
324                    )
325            except exceptions.InvalidSignature:
326                logger.error('Verification failed, ignoring given signature.')
327                sys.exit(1)
328
329            write_image_with_signature(sig)
330            logger.info('Successfully applied signature.')
331
332    def verify_ta():
333        # Extract header
334        [magic,
335         img_type,
336         img_size,
337         algo_value,
338         digest_len,
339         sig_len] = struct.unpack('<IIIIHH', img[:SHDR_SIZE])
340
341        # Extract digest and signature
342        start, end = SHDR_SIZE, SHDR_SIZE + digest_len
343        digest = img[start:end]
344
345        start, end = end, SHDR_SIZE + digest_len + sig_len
346        signature = img[start:end]
347
348        # Extract UUID and TA version
349        start, end = end, end + 16 + 4
350        [uuid, ta_version] = struct.unpack('<16sI', img[start:end])
351
352        if magic != SHDR_MAGIC:
353            raise Exception("Unexpected magic: 0x{:08x}".format(magic))
354
355        if img_type != SHDR_BOOTSTRAP_TA:
356            raise Exception("Unsupported image type: {}".format(img_type))
357
358        if algo_value not in algo.values():
359            raise Exception('Unrecognized algorithm: 0x{:08x}'
360                            .format(algo_value))
361
362        # Verify signature against hash digest
363        if algo_value == 0x70414930:
364            key.verify(
365                signature,
366                digest,
367                padding.PSS(
368                    mgf=padding.MGF1(chosen_hash),
369                    salt_length=digest_len
370                ),
371                utils.Prehashed(chosen_hash)
372            )
373        else:
374            key.verify(
375                signature,
376                digest,
377                padding.PKCS1v15(),
378                utils.Prehashed(chosen_hash)
379            )
380
381        h = hashes.Hash(chosen_hash, default_backend())
382
383        # sizeof(struct shdr)
384        h.update(img[:SHDR_SIZE])
385
386        # sizeof(struct shdr_bootstrap_ta)
387        h.update(img[start:end])
388
389        # raw image
390        start = end
391        end += img_size
392        h.update(img[start:end])
393
394        if digest != h.finalize():
395            raise Exception('Hash digest does not match')
396
397        logger.info('Trusted application is correctly verified.')
398
399    # dispatch command
400    {
401        'sign-enc': sign_encrypt_ta,
402        'digest': generate_digest,
403        'generate-digest': generate_digest,
404        'stitch': stitch_ta,
405        'stitch-ta': stitch_ta,
406        'verify': verify_ta,
407    }.get(args.command, 'sign_encrypt_ta')()
408
409
410if __name__ == "__main__":
411    main()
412