summaryrefslogtreecommitdiff
path: root/src/main/java/com/android/apkzlib/sign/SigningExtension.java
blob: 2685aa179f4f9f1d35aaea361a7ca7a1b7f943ad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.apkzlib.sign;

import com.android.apksig.ApkSignerEngine;
import com.android.apksig.ApkVerifier;
import com.android.apksig.DefaultApkSignerEngine;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apkzlib.utils.IOExceptionRunnable;
import com.android.apkzlib.zip.StoredEntry;
import com.android.apkzlib.zip.ZFile;
import com.android.apkzlib.zip.ZFileExtension;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * {@link ZFile} extension which signs the APK.
 *
 * <p>
 * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature
 * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this
 * extension's constructor.
 */
public class SigningExtension {
    // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive
    // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.
    // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile
    // extension events/callbacks.
    //
    // The main issue leading to additional complexity in this class is that the current build
    // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that
    // matter) for incremental builds. Thus:
    // * ZFile extension receives no events for JAR entries already in the APK whereas
    //   ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this
    //   class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries
    //   already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR
    //   entries which the incremental build session did not touch.
    // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed
    //   from it whereas ApkSignerEngine produces no output only if it has already produced a signed
    //   APK and no changes have since been made to it. This class addresses this issue by checking
    //   in its "register" method whether the APK is correctly signed and, only if that's the case,
    //   doesn't modify the APK unless a JAR entry is added to it or removed from it after
    //   "register".

    /**
     * Minimum API Level on which this APK is supposed to run.
     */
    private final int minSdkVersion;

    /**
     * Whether JAR signing (aka v1 signing) is enabled.
     */
    private final boolean v1SigningEnabled;

    /**
     * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled.
     */
    private final boolean v2SigningEnabled;

    /**
     * Certificate of the signer, to be embedded into the APK's signature.
     */
    @Nonnull
    private final X509Certificate certificate;

    /**
     * APK signer which performs most of the heavy lifting.
     */
    @Nonnull
    private final ApkSignerEngine signer;

    /**
     * Names of APK entries which have been processed by {@link #signer}.
     */
    private final Set<String> signerProcessedOutputEntryNames = new HashSet<>();

    /**
     * Cached contents of the most recently output APK Signing Block or {@code null} if the block
     * hasn't yet been output.
     */
    @Nullable
    private byte[] cachedApkSigningBlock;

    /**
     * {@code true} if signatures may need to be output, {@code false} if there's no need to output
     * signatures. This is used in an optimization where we don't modify the APK if it's already
     * signed and if no JAR entries have been added to or removed from the file.
     */
    private boolean dirty;

    /**
     * The extension registered with the {@link ZFile}. {@code null} if not registered.
     */
    @Nullable
    private ZFileExtension extension;

    /**
     * The file this extension is attached to. {@code null} if not yet registered.
     */
    @Nullable
    private ZFile zFile;

    public SigningExtension(
            int minSdkVersion,
            @Nonnull X509Certificate certificate,
            @Nonnull PrivateKey privateKey,
            boolean v1SigningEnabled,
            boolean v2SigningEnabled) throws InvalidKeyException {
        DefaultApkSignerEngine.SignerConfig signerConfig =
                new DefaultApkSignerEngine.SignerConfig.Builder(
                        "CERT", privateKey, ImmutableList.of(certificate)).build();
        signer =
                new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion)
                        .setOtherSignersSignaturesPreserved(false)
                        .setV1SigningEnabled(v1SigningEnabled)
                        .setV2SigningEnabled(v2SigningEnabled)
                        .setCreatedBy("1.0 (Android)")
                        .build();
        this.minSdkVersion = minSdkVersion;
        this.v1SigningEnabled = v1SigningEnabled;
        this.v2SigningEnabled = v2SigningEnabled;
        this.certificate = certificate;
    }

    public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException {
        Preconditions.checkState(extension == null, "register() already invoked");
        this.zFile = zFile;
        dirty = !isCurrentSignatureAsRequested();
        extension = new ZFileExtension() {
            @Override
            public IOExceptionRunnable added(
                    @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
                return () -> onZipEntryOutput(entry);
            }

            @Override
            public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
                String entryName = entry.getCentralDirectoryHeader().getName();
                return () -> onZipEntryRemovedFromOutput(entryName);
            }

            @Override
            public IOExceptionRunnable beforeUpdate() throws IOException {
                return () -> onOutputZipReadyForUpdate();
            }

            @Override
            public void entriesWritten() throws IOException {
                onOutputZipEntriesWritten();
            }

            @Override
            public void closed() {
                onOutputClosed();
            }
        };
        this.zFile.addZFileExtension(extension);
    }

    /**
     * Returns {@code true} if the APK's signatures are as requested by parameters to this signing
     * extension.
     */
    private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {
        ApkVerifier.Result result;
        try {
            result =
                    new ApkVerifier.Builder(new ZFileDataSource(zFile))
                            .setMinCheckedPlatformVersion(minSdkVersion)
                            .build()
                            .verify();
        } catch (ApkFormatException e) {
            // Malformed APK
            return false;
        }

        if (!result.isVerified()) {
            // Signature(s) did not verify
            return false;
        }

        if ((result.isVerifiedUsingV1Scheme() != v1SigningEnabled)
                || (result.isVerifiedUsingV2Scheme() != v2SigningEnabled)) {
            // APK isn't signed with exactly the schemes we want it to be signed
            return false;
        }

        List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();
        if (verifiedSignerCerts.size() != 1) {
            // APK is not signed by exactly one signer
            return false;
        }

        byte[] expectedEncodedCert;
        byte[] actualEncodedCert;
        try {
            expectedEncodedCert = certificate.getEncoded();
            actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();
        } catch (CertificateEncodingException e) {
            // Failed to encode signing certificates
            return false;
        }

        if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {
            // APK is signed by a wrong signer
            return false;
        }

        // APK is signed the way we want it to be signed
        return true;
    }

    private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException {
        setDirty();
        String entryName = entry.getCentralDirectoryHeader().getName();
        // This event may arrive after the entry has already been deleted. In that case, we don't
        // report the addition of the entry to ApkSignerEngine.
        if (entry.isDeleted()) {
            return;
        }
        ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
                signer.outputJarEntry(entryName);
        signerProcessedOutputEntryNames.add(entryName);
        if (inspectEntryRequest != null) {
            byte[] entryContents = entry.read();
            inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length);
            inspectEntryRequest.done();
        }
    }

    private void onZipEntryRemovedFromOutput(@Nonnull String entryName) {
        setDirty();
        signer.outputJarEntryRemoved(entryName);
        signerProcessedOutputEntryNames.remove(entryName);
    }

    private void onOutputZipReadyForUpdate() throws IOException {
        if (!dirty) {
            return;
        }

        // Notify signer engine about ZIP entries that have appeared in the output without the
        // engine knowing. Also identify ZIP entries which disappeared from the output without the
        // engine knowing.
        Set<String> unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames);
        for (StoredEntry entry : zFile.entries()) {
            String entryName = entry.getCentralDirectoryHeader().getName();
            unprocessedRemovedEntryNames.remove(entryName);
            if (!signerProcessedOutputEntryNames.contains(entryName)) {
                // Signer engine is not yet aware that this entry is in the output
                onZipEntryOutput(entry);
            }
        }

        // Notify signer engine about entries which disappeared from the output without the engine
        // knowing
        for (String entryName : unprocessedRemovedEntryNames) {
            onZipEntryRemovedFromOutput(entryName);
        }

        // Check whether we need to output additional JAR entries which comprise the v1 signature
        ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;
        try {
            addV1SignatureRequest = signer.outputJarEntries();
        } catch (Exception e) {
            throw new IOException("Failed to generate v1 signature", e);
        }
        if (addV1SignatureRequest == null) {
            return;
        }

        // We need to output additional JAR entries which comprise the v1 signature
        List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =
                new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());

        // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first
        // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by
        // ManifestGenerationExtension.
        for (int i = 0; i < v1SignatureEntries.size(); i++) {
            ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);
            String name = entry.getName();
            if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {
                continue;
            }
            if (i != 0) {
                v1SignatureEntries.remove(i);
                v1SignatureEntries.add(0, entry);
            }
            break;
        }

        // Output the JAR entries comprising the v1 signature
        for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {
            String name = entry.getName();
            byte[] data = entry.getData();
            zFile.add(name, new ByteArrayInputStream(data));
        }

        addV1SignatureRequest.done();
    }

    private void onOutputZipEntriesWritten() throws IOException {
        if (!dirty) {
            return;
        }

        // Check whether we should output an APK Signing Block which contains v2 signatures
        byte[] apkSigningBlock;
        byte[] centralDirBytes = zFile.getCentralDirectoryBytes();
        byte[] eocdBytes = zFile.getEocdBytes();
        ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest;
        // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we
        // cache the block to speed things up. The cached block is invalidated by any changes to the
        // file (as reported to this extension).
        if (cachedApkSigningBlock != null) {
            apkSigningBlock = cachedApkSigningBlock;
            addV2SignatureRequest = null;
        } else {
            DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));
            DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));
            long zipEntriesSizeBytes =
                    zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset();
            DataSource zipEntries = new ZFileDataSource(zFile, 0, zipEntriesSizeBytes);
            try {
                addV2SignatureRequest = signer.outputZipSections(zipEntries, centralDir, eocd);
            } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException
                    | ApkFormatException | IOException e) {
                throw new IOException("Failed to generate v2 signature", e);
            }
            apkSigningBlock =
                    (addV2SignatureRequest != null)
                            ? addV2SignatureRequest.getApkSigningBlock() : new byte[0];
            cachedApkSigningBlock = apkSigningBlock;
        }

        // Insert the APK Signing Block into the output right before the ZIP Central Directory and
        // accordingly update the start offset of ZIP Central Directory in ZIP End of Central
        // Directory.
        zFile.directWrite(
                zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(),
                apkSigningBlock);
        zFile.setExtraDirectoryOffset(apkSigningBlock.length);

        if (addV2SignatureRequest != null) {
            addV2SignatureRequest.done();
        }
    }

    private void onOutputClosed() {
        if (!dirty) {
            return;
        }
        signer.outputDone();
        dirty = false;
    }

    private void setDirty() {
        dirty = true;
        cachedApkSigningBlock = null;
    }
}