blob: 2685aa179f4f9f1d35aaea361a7ca7a1b7f943ad [file] [log] [blame]
/*
* 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;
}
}