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