Home | History | Annotate | Download | only in sign
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.tools.build.apkzlib.sign;
     17 
     18 import com.android.apksig.ApkSignerEngine;
     19 import com.android.apksig.ApkVerifier;
     20 import com.android.apksig.DefaultApkSignerEngine;
     21 import com.android.apksig.apk.ApkFormatException;
     22 import com.android.apksig.util.DataSource;
     23 import com.android.apksig.util.DataSources;
     24 import com.android.tools.build.apkzlib.utils.IOExceptionRunnable;
     25 import com.android.tools.build.apkzlib.zip.StoredEntry;
     26 import com.android.tools.build.apkzlib.zip.ZFile;
     27 import com.android.tools.build.apkzlib.zip.ZFileExtension;
     28 import com.google.common.base.Preconditions;
     29 import com.google.common.collect.ImmutableList;
     30 import java.io.ByteArrayInputStream;
     31 import java.io.IOException;
     32 import java.nio.ByteBuffer;
     33 import java.security.InvalidKeyException;
     34 import java.security.NoSuchAlgorithmException;
     35 import java.security.PrivateKey;
     36 import java.security.SignatureException;
     37 import java.security.cert.CertificateEncodingException;
     38 import java.security.cert.X509Certificate;
     39 import java.util.ArrayList;
     40 import java.util.Arrays;
     41 import java.util.HashSet;
     42 import java.util.List;
     43 import java.util.Set;
     44 import javax.annotation.Nonnull;
     45 import javax.annotation.Nullable;
     46 
     47 /**
     48  * {@link ZFile} extension which signs the APK.
     49  *
     50  * <p>
     51  * This extension is capable of signing the APK using JAR signing (aka v1 scheme) and APK Signature
     52  * Scheme v2 (aka v2 scheme). Which schemes are actually used is specified by parameters to this
     53  * extension's constructor.
     54  */
     55 public class SigningExtension {
     56     // IMPLEMENTATION NOTE: Most of the heavy lifting is performed by the ApkSignerEngine primitive
     57     // from apksig library. This class is an adapter between ZFile extension and ApkSignerEngine.
     58     // This class takes care of invoking the right methods on ApkSignerEngine in response to ZFile
     59     // extension events/callbacks.
     60     //
     61     // The main issue leading to additional complexity in this class is that the current build
     62     // pipeline does not reuse ApkSignerEngine instances (or ZFile extension instances for that
     63     // matter) for incremental builds. Thus:
     64     // * ZFile extension receives no events for JAR entries already in the APK whereas
     65     //   ApkSignerEngine needs to know about all JAR entries to be covered by signature. Thus, this
     66     //   class, during "beforeUpdate" ZFile event, notifies ApkSignerEngine about JAR entries
     67     //   already in the APK which ApkSignerEngine hasn't yet been told about -- these are the JAR
     68     //   entries which the incremental build session did not touch.
     69     // * The build pipeline expects the APK not to change if no JAR entry was added to it or removed
     70     //   from it whereas ApkSignerEngine produces no output only if it has already produced a signed
     71     //   APK and no changes have since been made to it. This class addresses this issue by checking
     72     //   in its "register" method whether the APK is correctly signed and, only if that's the case,
     73     //   doesn't modify the APK unless a JAR entry is added to it or removed from it after
     74     //   "register".
     75 
     76     /**
     77      * Minimum API Level on which this APK is supposed to run.
     78      */
     79     private final int minSdkVersion;
     80 
     81     /**
     82      * Whether JAR signing (aka v1 signing) is enabled.
     83      */
     84     private final boolean v1SigningEnabled;
     85 
     86     /**
     87      * Whether APK Signature Scheme v2 sining (aka v2 signing) is enabled.
     88      */
     89     private final boolean v2SigningEnabled;
     90 
     91     /**
     92      * Certificate of the signer, to be embedded into the APK's signature.
     93      */
     94     @Nonnull
     95     private final X509Certificate certificate;
     96 
     97     /**
     98      * APK signer which performs most of the heavy lifting.
     99      */
    100     @Nonnull
    101     private final ApkSignerEngine signer;
    102 
    103     /**
    104      * Names of APK entries which have been processed by {@link #signer}.
    105      */
    106     private final Set<String> signerProcessedOutputEntryNames = new HashSet<>();
    107 
    108     /**
    109      * Cached contents of the most recently output APK Signing Block or {@code null} if the block
    110      * hasn't yet been output.
    111      */
    112     @Nullable
    113     private byte[] cachedApkSigningBlock;
    114 
    115     /**
    116      * {@code true} if signatures may need to be output, {@code false} if there's no need to output
    117      * signatures. This is used in an optimization where we don't modify the APK if it's already
    118      * signed and if no JAR entries have been added to or removed from the file.
    119      */
    120     private boolean dirty;
    121 
    122     /**
    123      * The extension registered with the {@link ZFile}. {@code null} if not registered.
    124      */
    125     @Nullable
    126     private ZFileExtension extension;
    127 
    128     /**
    129      * The file this extension is attached to. {@code null} if not yet registered.
    130      */
    131     @Nullable
    132     private ZFile zFile;
    133 
    134     public SigningExtension(
    135             int minSdkVersion,
    136             @Nonnull X509Certificate certificate,
    137             @Nonnull PrivateKey privateKey,
    138             boolean v1SigningEnabled,
    139             boolean v2SigningEnabled) throws InvalidKeyException {
    140         DefaultApkSignerEngine.SignerConfig signerConfig =
    141                 new DefaultApkSignerEngine.SignerConfig.Builder(
    142                         "CERT", privateKey, ImmutableList.of(certificate)).build();
    143         signer =
    144                 new DefaultApkSignerEngine.Builder(ImmutableList.of(signerConfig), minSdkVersion)
    145                         .setOtherSignersSignaturesPreserved(false)
    146                         .setV1SigningEnabled(v1SigningEnabled)
    147                         .setV2SigningEnabled(v2SigningEnabled)
    148                         .setCreatedBy("1.0 (Android)")
    149                         .build();
    150         this.minSdkVersion = minSdkVersion;
    151         this.v1SigningEnabled = v1SigningEnabled;
    152         this.v2SigningEnabled = v2SigningEnabled;
    153         this.certificate = certificate;
    154     }
    155 
    156     public void register(@Nonnull ZFile zFile) throws NoSuchAlgorithmException, IOException {
    157         Preconditions.checkState(extension == null, "register() already invoked");
    158         this.zFile = zFile;
    159         dirty = !isCurrentSignatureAsRequested();
    160         extension = new ZFileExtension() {
    161             @Override
    162             public IOExceptionRunnable added(
    163                     @Nonnull StoredEntry entry, @Nullable StoredEntry replaced) {
    164                 return () -> onZipEntryOutput(entry);
    165             }
    166 
    167             @Override
    168             public IOExceptionRunnable removed(@Nonnull StoredEntry entry) {
    169                 String entryName = entry.getCentralDirectoryHeader().getName();
    170                 return () -> onZipEntryRemovedFromOutput(entryName);
    171             }
    172 
    173             @Override
    174             public IOExceptionRunnable beforeUpdate() throws IOException {
    175                 return () -> onOutputZipReadyForUpdate();
    176             }
    177 
    178             @Override
    179             public void entriesWritten() throws IOException {
    180                 onOutputZipEntriesWritten();
    181             }
    182 
    183             @Override
    184             public void closed() {
    185                 onOutputClosed();
    186             }
    187         };
    188         this.zFile.addZFileExtension(extension);
    189     }
    190 
    191     /**
    192      * Returns {@code true} if the APK's signatures are as requested by parameters to this signing
    193      * extension.
    194      */
    195     private boolean isCurrentSignatureAsRequested() throws IOException, NoSuchAlgorithmException {
    196         ApkVerifier.Result result;
    197         try {
    198             result =
    199                     new ApkVerifier.Builder(new ZFileDataSource(zFile))
    200                             .setMinCheckedPlatformVersion(minSdkVersion)
    201                             .build()
    202                             .verify();
    203         } catch (ApkFormatException e) {
    204             // Malformed APK
    205             return false;
    206         }
    207 
    208         if (!result.isVerified()) {
    209             // Signature(s) did not verify
    210             return false;
    211         }
    212 
    213         if ((result.isVerifiedUsingV1Scheme() != v1SigningEnabled)
    214                 || (result.isVerifiedUsingV2Scheme() != v2SigningEnabled)) {
    215             // APK isn't signed with exactly the schemes we want it to be signed
    216             return false;
    217         }
    218 
    219         List<X509Certificate> verifiedSignerCerts = result.getSignerCertificates();
    220         if (verifiedSignerCerts.size() != 1) {
    221             // APK is not signed by exactly one signer
    222             return false;
    223         }
    224 
    225         byte[] expectedEncodedCert;
    226         byte[] actualEncodedCert;
    227         try {
    228             expectedEncodedCert = certificate.getEncoded();
    229             actualEncodedCert = verifiedSignerCerts.get(0).getEncoded();
    230         } catch (CertificateEncodingException e) {
    231             // Failed to encode signing certificates
    232             return false;
    233         }
    234 
    235         if (!Arrays.equals(expectedEncodedCert, actualEncodedCert)) {
    236             // APK is signed by a wrong signer
    237             return false;
    238         }
    239 
    240         // APK is signed the way we want it to be signed
    241         return true;
    242     }
    243 
    244     private void onZipEntryOutput(@Nonnull StoredEntry entry) throws IOException {
    245         setDirty();
    246         String entryName = entry.getCentralDirectoryHeader().getName();
    247         // This event may arrive after the entry has already been deleted. In that case, we don't
    248         // report the addition of the entry to ApkSignerEngine.
    249         if (entry.isDeleted()) {
    250             return;
    251         }
    252         ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest =
    253                 signer.outputJarEntry(entryName);
    254         signerProcessedOutputEntryNames.add(entryName);
    255         if (inspectEntryRequest != null) {
    256             byte[] entryContents = entry.read();
    257             inspectEntryRequest.getDataSink().consume(entryContents, 0, entryContents.length);
    258             inspectEntryRequest.done();
    259         }
    260     }
    261 
    262     private void onZipEntryRemovedFromOutput(@Nonnull String entryName) {
    263         setDirty();
    264         signer.outputJarEntryRemoved(entryName);
    265         signerProcessedOutputEntryNames.remove(entryName);
    266     }
    267 
    268     private void onOutputZipReadyForUpdate() throws IOException {
    269         if (!dirty) {
    270             return;
    271         }
    272 
    273         // Notify signer engine about ZIP entries that have appeared in the output without the
    274         // engine knowing. Also identify ZIP entries which disappeared from the output without the
    275         // engine knowing.
    276         Set<String> unprocessedRemovedEntryNames = new HashSet<>(signerProcessedOutputEntryNames);
    277         for (StoredEntry entry : zFile.entries()) {
    278             String entryName = entry.getCentralDirectoryHeader().getName();
    279             unprocessedRemovedEntryNames.remove(entryName);
    280             if (!signerProcessedOutputEntryNames.contains(entryName)) {
    281                 // Signer engine is not yet aware that this entry is in the output
    282                 onZipEntryOutput(entry);
    283             }
    284         }
    285 
    286         // Notify signer engine about entries which disappeared from the output without the engine
    287         // knowing
    288         for (String entryName : unprocessedRemovedEntryNames) {
    289             onZipEntryRemovedFromOutput(entryName);
    290         }
    291 
    292         // Check whether we need to output additional JAR entries which comprise the v1 signature
    293         ApkSignerEngine.OutputJarSignatureRequest addV1SignatureRequest;
    294         try {
    295             addV1SignatureRequest = signer.outputJarEntries();
    296         } catch (Exception e) {
    297             throw new IOException("Failed to generate v1 signature", e);
    298         }
    299         if (addV1SignatureRequest == null) {
    300             return;
    301         }
    302 
    303         // We need to output additional JAR entries which comprise the v1 signature
    304         List<ApkSignerEngine.OutputJarSignatureRequest.JarEntry> v1SignatureEntries =
    305                 new ArrayList<>(addV1SignatureRequest.getAdditionalJarEntries());
    306 
    307         // Reorder the JAR entries comprising the v1 signature so that MANIFEST.MF is the first
    308         // entry. This ensures that it cleanly overwrites the existing MANIFEST.MF output by
    309         // ManifestGenerationExtension.
    310         for (int i = 0; i < v1SignatureEntries.size(); i++) {
    311             ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry = v1SignatureEntries.get(i);
    312             String name = entry.getName();
    313             if (!ManifestGenerationExtension.MANIFEST_NAME.equals(name)) {
    314                 continue;
    315             }
    316             if (i != 0) {
    317                 v1SignatureEntries.remove(i);
    318                 v1SignatureEntries.add(0, entry);
    319             }
    320             break;
    321         }
    322 
    323         // Output the JAR entries comprising the v1 signature
    324         for (ApkSignerEngine.OutputJarSignatureRequest.JarEntry entry : v1SignatureEntries) {
    325             String name = entry.getName();
    326             byte[] data = entry.getData();
    327             zFile.add(name, new ByteArrayInputStream(data));
    328         }
    329 
    330         addV1SignatureRequest.done();
    331     }
    332 
    333     private void onOutputZipEntriesWritten() throws IOException {
    334         if (!dirty) {
    335             return;
    336         }
    337 
    338         // Check whether we should output an APK Signing Block which contains v2 signatures
    339         byte[] apkSigningBlock;
    340         byte[] centralDirBytes = zFile.getCentralDirectoryBytes();
    341         byte[] eocdBytes = zFile.getEocdBytes();
    342         ApkSignerEngine.OutputApkSigningBlockRequest addV2SignatureRequest;
    343         // This event may arrive a second time -- after we write out the APK Signing Block. Thus, we
    344         // cache the block to speed things up. The cached block is invalidated by any changes to the
    345         // file (as reported to this extension).
    346         if (cachedApkSigningBlock != null) {
    347             apkSigningBlock = cachedApkSigningBlock;
    348             addV2SignatureRequest = null;
    349         } else {
    350             DataSource centralDir = DataSources.asDataSource(ByteBuffer.wrap(centralDirBytes));
    351             DataSource eocd = DataSources.asDataSource(ByteBuffer.wrap(eocdBytes));
    352             long zipEntriesSizeBytes =
    353                     zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset();
    354             DataSource zipEntries = new ZFileDataSource(zFile, 0, zipEntriesSizeBytes);
    355             try {
    356                 addV2SignatureRequest = signer.outputZipSections(zipEntries, centralDir, eocd);
    357             } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException
    358                     | ApkFormatException | IOException e) {
    359                 throw new IOException("Failed to generate v2 signature", e);
    360             }
    361             apkSigningBlock =
    362                     (addV2SignatureRequest != null)
    363                             ? addV2SignatureRequest.getApkSigningBlock() : new byte[0];
    364             cachedApkSigningBlock = apkSigningBlock;
    365         }
    366 
    367         // Insert the APK Signing Block into the output right before the ZIP Central Directory and
    368         // accordingly update the start offset of ZIP Central Directory in ZIP End of Central
    369         // Directory.
    370         zFile.directWrite(
    371                 zFile.getCentralDirectoryOffset() - zFile.getExtraDirectoryOffset(),
    372                 apkSigningBlock);
    373         zFile.setExtraDirectoryOffset(apkSigningBlock.length);
    374 
    375         if (addV2SignatureRequest != null) {
    376             addV2SignatureRequest.done();
    377         }
    378     }
    379 
    380     private void onOutputClosed() {
    381         if (!dirty) {
    382             return;
    383         }
    384         signer.outputDone();
    385         dirty = false;
    386     }
    387 
    388     private void setDirty() {
    389         dirty = true;
    390         cachedApkSigningBlock = null;
    391     }
    392 }