xref: /optee_os/scripts/sign_encrypt.py (revision 2de17fda55d2d902123e25b1036cd6ae00b7ea77)
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
8
9
10def uuid_parse(s):
11    from uuid import UUID
12    return UUID(s)
13
14
15def int_parse(str):
16    return int(str, 0)
17
18
19def get_args(logger):
20    from argparse import ArgumentParser, RawDescriptionHelpFormatter
21    import textwrap
22    command_base = ['sign-enc', 'digest', 'stitch']
23    command_aliases_digest = ['generate-digest']
24    command_aliases_stitch = ['stitch-ta']
25    command_aliases = command_aliases_digest + command_aliases_stitch
26    command_choices = command_base + command_aliases
27
28    dat = '[' + ', '.join(command_aliases_digest) + ']'
29    sat = '[' + ', '.join(command_aliases_stitch) + ']'
30
31    parser = ArgumentParser(
32        description='Sign and encrypt (optional) a Tusted Application for' +
33        ' OP-TEE.',
34        usage='\n   %(prog)s command [ arguments ]\n\n'
35
36        '   command:\n' +
37        '     sign-enc    Generate signed and optionally encrypted loadable' +
38        ' TA image file.\n' +
39        '                 Takes arguments --uuid, --ta-version, --in, --out,' +
40        ' --key\n' +
41        '                 and --enc-key (optional).\n' +
42        '     digest      Generate loadable TA binary image digest' +
43        ' for offline\n' +
44        '                 signing. Takes arguments --uuid, --ta-version,' +
45        ' --in, --key,\n'
46        '                 --enc-key (optional) and --dig.\n' +
47        '     stitch      Generate loadable signed and encrypted TA binary' +
48        ' image file from\n' +
49        '                 TA raw image and its signature. Takes' +
50        ' arguments\n' +
51        '                 --uuid, --in, --key, --enc-key (optional), --out,' +
52        ' and --sig.\n\n' +
53        '   %(prog)s --help  show available commands and arguments\n\n',
54        formatter_class=RawDescriptionHelpFormatter,
55        epilog=textwrap.dedent('''\
56            If no command is given, the script will default to "sign-enc".
57
58            command aliases:
59              The command \'digest\' can be aliased by ''' + dat + '''
60              The command \'stitch\' can be aliased by ''' + sat + '\n' + '''
61            example offline signing command using OpenSSL:
62              base64 -d <UUID>.dig | \\
63              openssl pkeyutl -sign -inkey <KEYFILE>.pem \\
64                  -pkeyopt digest:sha256 \\
65                  -pkeyopt rsa_padding_mode:pkcs1 | \\
66              base64 > <UUID>.sig
67            '''))
68
69    parser.add_argument(
70        'command', choices=command_choices, nargs='?',
71        default='sign-enc',
72        help='Command, one of [' + ', '.join(command_base) + ']')
73    parser.add_argument('--uuid', required=True,
74                        type=uuid_parse, help='String UUID of the TA')
75    parser.add_argument('--key', required=True,
76                        help='Name of signing key file (PEM format)')
77    parser.add_argument('--enc-key', required=False,
78                        help='Encryption key string')
79    parser.add_argument(
80        '--ta-version', required=False, type=int_parse, default=0,
81        help='TA version stored as a 32-bit unsigned integer and used for\n' +
82        'rollback protection of TA install in the secure database.\n' +
83        'Defaults to 0.')
84    parser.add_argument(
85        '--sig', required=False, dest='sigf',
86        help='Name of signature input file, defaults to <UUID>.sig')
87    parser.add_argument(
88        '--dig', required=False, dest='digf',
89        help='Name of digest output file, defaults to <UUID>.dig')
90    parser.add_argument(
91        '--in', required=True, dest='inf',
92        help='Name of application input file, defaults to <UUID>.stripped.elf')
93    parser.add_argument(
94        '--out', required=False, dest='outf',
95        help='Name of application output file, defaults to <UUID>.ta')
96
97    parsed = parser.parse_args()
98
99    # Check parameter combinations
100
101    if parsed.digf is None and \
102       parsed.outf is not None and \
103       parsed.command in ['digest'] + command_aliases_digest:
104        logger.error('A digest was requested, but argument --out was given.' +
105                     '  Did you mean:\n  ' +
106                     parser.prog+' --dig ' + parsed.outf + ' ...')
107        sys.exit(1)
108
109    if parsed.digf is not None \
110       and parsed.outf is not None \
111       and parsed.command in ['digest'] + command_aliases_digest:
112        logger.warn('A digest was requested, but arguments --dig and ' +
113                    '--out were given.\n' +
114                    '  --out will be ignored.')
115
116    # Set defaults for optional arguments.
117
118    if parsed.sigf is None:
119        parsed.sigf = str(parsed.uuid)+'.sig'
120    if parsed.digf is None:
121        parsed.digf = str(parsed.uuid)+'.dig'
122    if parsed.inf is None:
123        parsed.inf = str(parsed.uuid)+'.stripped.elf'
124    if parsed.outf is None:
125        parsed.outf = str(parsed.uuid)+'.ta'
126
127    return parsed
128
129
130def main():
131    from Crypto.Signature import PKCS1_v1_5
132    from Crypto.Hash import SHA256
133    from Crypto.PublicKey import RSA
134    from Crypto.Util.number import ceil_div
135    import base64
136    import logging
137    import os
138    import struct
139
140    logging.basicConfig()
141    logger = logging.getLogger(os.path.basename(__file__))
142
143    args = get_args(logger)
144
145    with open(args.key, 'rb') as f:
146        key = RSA.importKey(f.read())
147
148    with open(args.inf, 'rb') as f:
149        img = f.read()
150
151    h = SHA256.new()
152
153    digest_len = h.digest_size
154    try:
155        # This works in pycrypto
156        sig_len = ceil_div(key.size() + 1, 8)
157    except NotImplementedError:
158        # ... and this one - in pycryptodome
159        sig_len = key.size_in_bytes()
160
161    img_size = len(img)
162
163    hdr_version = args.ta_version  # struct shdr_bootstrap_ta::ta_version
164
165    magic = 0x4f545348   # SHDR_MAGIC
166    if args.enc_key:
167        img_type = 2         # SHDR_ENCRYPTED_TA
168    else:
169        img_type = 1         # SHDR_BOOTSTRAP_TA
170    algo = 0x70004830    # TEE_ALG_RSASSA_PKCS1_V1_5_SHA256
171
172    shdr = struct.pack('<IIIIHH',
173                       magic, img_type, img_size, algo, digest_len, sig_len)
174    shdr_uuid = args.uuid.bytes
175    shdr_version = struct.pack('<I', hdr_version)
176
177    if args.enc_key:
178        from Cryptodome.Cipher import AES
179        cipher = AES.new(bytearray.fromhex(args.enc_key), AES.MODE_GCM)
180        ciphertext, tag = cipher.encrypt_and_digest(img)
181
182        enc_algo = 0x40000810  # TEE_ALG_AES_GCM
183        flags = 0              # SHDR_ENC_KEY_DEV_SPECIFIC
184        ehdr = struct.pack('<IIHH',
185                           enc_algo, flags, len(cipher.nonce), len(tag))
186
187    h.update(shdr)
188    h.update(shdr_uuid)
189    h.update(shdr_version)
190    if args.enc_key:
191        h.update(ehdr)
192        h.update(cipher.nonce)
193        h.update(tag)
194    h.update(img)
195    img_digest = h.digest()
196
197    def write_image_with_signature(sig):
198        with open(args.outf, 'wb') as f:
199            f.write(shdr)
200            f.write(img_digest)
201            f.write(sig)
202            f.write(shdr_uuid)
203            f.write(shdr_version)
204            if args.enc_key:
205                f.write(ehdr)
206                f.write(cipher.nonce)
207                f.write(tag)
208                f.write(ciphertext)
209            else:
210                f.write(img)
211
212    def sign_encrypt_ta():
213        if not key.has_private():
214            logger.error('Provided key cannot be used for signing, ' +
215                         'please use offline-signing mode.')
216            sys.exit(1)
217        else:
218            signer = PKCS1_v1_5.new(key)
219            sig = signer.sign(h)
220            if len(sig) != sig_len:
221                raise Exception(("Actual signature length is not equal to ",
222                                 "the computed one: {} != {}").
223                                format(len(sig), sig_len))
224            write_image_with_signature(sig)
225            logger.info('Successfully signed application.')
226
227    def generate_digest():
228        with open(args.digf, 'wb+') as digfile:
229            digfile.write(base64.b64encode(img_digest))
230
231    def stitch_ta():
232        try:
233            with open(args.sigf, 'r') as sigfile:
234                sig = base64.b64decode(sigfile.read())
235        except IOError:
236            if not os.path.exists(args.digf):
237                generate_digest()
238            logger.error('No signature file found. Please sign\n %s\n' +
239                         'offline and place the signature at \n %s\n' +
240                         'or pass a different location ' +
241                         'using the --sig argument.\n',
242                         args.digf, args.sigf)
243            sys.exit(1)
244        else:
245            verifier = PKCS1_v1_5.new(key)
246            if verifier.verify(h, sig):
247                write_image_with_signature(sig)
248                logger.info('Successfully applied signature.')
249            else:
250                logger.error('Verification failed, ignoring given signature.')
251                sys.exit(1)
252
253    # dispatch command
254    {
255        'sign-enc': sign_encrypt_ta,
256        'digest': generate_digest,
257        'generate-digest': generate_digest,
258        'stitch': stitch_ta,
259        'stitch-ta': stitch_ta
260    }.get(args.command, 'sign_encrypt_ta')()
261
262
263if __name__ == "__main__":
264    main()
265