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 }