xref: /optee_os/scripts/sign_encrypt.py (revision 5b25c76ac40f830867e3d60800120ffd7874e8dc)
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 Cryptodome.Signature import pss
132    from Cryptodome.Hash import SHA256
133    from Cryptodome.PublicKey import RSA
134    import base64
135    import logging
136    import os
137    import struct
138
139    logging.basicConfig()
140    logger = logging.getLogger(os.path.basename(__file__))
141
142    args = get_args(logger)
143
144    with open(args.key, 'rb') as f:
145        key = RSA.importKey(f.read())
146
147    with open(args.inf, 'rb') as f:
148        img = f.read()
149
150    h = SHA256.new()
151
152    digest_len = h.digest_size
153    sig_len = key.size_in_bytes()
154
155    img_size = len(img)
156
157    hdr_version = args.ta_version  # struct shdr_bootstrap_ta::ta_version
158
159    magic = 0x4f545348   # SHDR_MAGIC
160    if args.enc_key:
161        img_type = 2         # SHDR_ENCRYPTED_TA
162    else:
163        img_type = 1         # SHDR_BOOTSTRAP_TA
164    algo = 0x70414930    # TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256
165
166    shdr = struct.pack('<IIIIHH',
167                       magic, img_type, img_size, algo, digest_len, sig_len)
168    shdr_uuid = args.uuid.bytes
169    shdr_version = struct.pack('<I', hdr_version)
170
171    if args.enc_key:
172        from Cryptodome.Cipher import AES
173        cipher = AES.new(bytearray.fromhex(args.enc_key), AES.MODE_GCM)
174        ciphertext, tag = cipher.encrypt_and_digest(img)
175
176        enc_algo = 0x40000810  # TEE_ALG_AES_GCM
177        flags = 0              # SHDR_ENC_KEY_DEV_SPECIFIC
178        ehdr = struct.pack('<IIHH',
179                           enc_algo, flags, len(cipher.nonce), len(tag))
180
181    h.update(shdr)
182    h.update(shdr_uuid)
183    h.update(shdr_version)
184    if args.enc_key:
185        h.update(ehdr)
186        h.update(cipher.nonce)
187        h.update(tag)
188    h.update(img)
189    img_digest = h.digest()
190
191    def write_image_with_signature(sig):
192        with open(args.outf, 'wb') as f:
193            f.write(shdr)
194            f.write(img_digest)
195            f.write(sig)
196            f.write(shdr_uuid)
197            f.write(shdr_version)
198            if args.enc_key:
199                f.write(ehdr)
200                f.write(cipher.nonce)
201                f.write(tag)
202                f.write(ciphertext)
203            else:
204                f.write(img)
205
206    def sign_encrypt_ta():
207        if not key.has_private():
208            logger.error('Provided key cannot be used for signing, ' +
209                         'please use offline-signing mode.')
210            sys.exit(1)
211        else:
212            signer = pss.new(key)
213            sig = signer.sign(h)
214            if len(sig) != sig_len:
215                raise Exception(("Actual signature length is not equal to ",
216                                 "the computed one: {} != {}").
217                                format(len(sig), sig_len))
218            write_image_with_signature(sig)
219            logger.info('Successfully signed application.')
220
221    def generate_digest():
222        with open(args.digf, 'wb+') as digfile:
223            digfile.write(base64.b64encode(img_digest))
224
225    def stitch_ta():
226        try:
227            with open(args.sigf, 'r') as sigfile:
228                sig = base64.b64decode(sigfile.read())
229        except IOError:
230            if not os.path.exists(args.digf):
231                generate_digest()
232            logger.error('No signature file found. Please sign\n %s\n' +
233                         'offline and place the signature at \n %s\n' +
234                         'or pass a different location ' +
235                         'using the --sig argument.\n',
236                         args.digf, args.sigf)
237            sys.exit(1)
238        else:
239            verifier = pss.new(key)
240            if verifier.verify(h, sig):
241                write_image_with_signature(sig)
242                logger.info('Successfully applied signature.')
243            else:
244                logger.error('Verification failed, ignoring given signature.')
245                sys.exit(1)
246
247    # dispatch command
248    {
249        'sign-enc': sign_encrypt_ta,
250        'digest': generate_digest,
251        'generate-digest': generate_digest,
252        'stitch': stitch_ta,
253        'stitch-ta': stitch_ta
254    }.get(args.command, 'sign_encrypt_ta')()
255
256
257if __name__ == "__main__":
258    main()
259