blob: 8f9dd6a9ae244f4e91e221e1a6e58755b49d668a [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.builder.internal.packaging.sign;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.internal.packaging.sign.v2.ApkSignerV2;
import com.android.builder.internal.packaging.sign.v2.ByteArrayDigestSource;
import com.android.builder.internal.packaging.sign.v2.DigestSource;
import com.android.builder.internal.packaging.sign.v2.SignatureAlgorithm;
import com.android.builder.internal.packaging.sign.v2.ZFileDigestSource;
import com.android.builder.internal.packaging.zip.StoredEntry;
import com.android.builder.internal.packaging.zip.ZFile;
import com.android.builder.internal.packaging.zip.ZFileExtension;
import com.android.builder.internal.utils.IOExceptionRunnable;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.List;
/**
* Extension that adds full APK signing. This extension will:
* <ul>
* <li>Generate a new signature if the zip is modified after the extension is added.</li>
* <li>Generate a new signature even if the zip is not modified after the extension is
* added, but a valid signature is not found.</li>
* </ul>
* <p>
* The extension computes the signature, if needed, at the {@link ZFileExtension#entriesWritten()}
* event, after all zip entries have been written, but before the central directory or EOCD have.
* This allows the extension to set the central directory offset parameter in the zip file
* using {@link ZFile#setExtraDirectoryOffset(long)} adding enough space in the zip file for the
* signature block.
* <p>
* The signature block is written before the central directory, allowing the zip file to be written
* in sequential order.
*/
public class FullApkSignExtension {
/**
* The zip file this extension is registered with.
*/
@NonNull
private final ZFile mFile;
/**
* Signer certificate.
*/
@NonNull
private final X509Certificate mCertificate;
/**
* Signer private key.
*/
@NonNull
private final PrivateKey mPrivateKey;
/**
* APK Signature Scheme v2 algorithms to use for signing the APK.
*/
private final List<SignatureAlgorithm> mV2SignatureAlgorithms;
/**
* {@code true} if the zip needs its signature to be updated.
*/
private boolean mNeedsSignatureUpdate = true;
/**
* The extension to register with the {@link ZFile}. {@code null} if not registered.
*/
@Nullable
private ZFileExtension mExtension;
/**
* Creates a new extension. This will not register the extension with the provided
* {@link ZFile}. Until {@link #register()} is invoked, this extension is not used.
*
* @param file the zip file to register the extension with
* @param minSdkVersion minSdkVersion of the package
* @param certificate sign certificate
* @param privateKey the private key to sign the jar
*
* @throws InvalidKeyException if the signing key is not suitable for signing this APK.
*/
public FullApkSignExtension(@NonNull ZFile file,
int minSdkVersion,
@NonNull X509Certificate certificate,
@NonNull PrivateKey privateKey) throws InvalidKeyException {
mFile = file;
mCertificate = certificate;
mPrivateKey = privateKey;
mV2SignatureAlgorithms =
ApkSignerV2.getSuggestedSignatureAlgorithms(
certificate.getPublicKey(), minSdkVersion);
}
/**
* Registers the extension with the {@link ZFile} provided in the constructor.
*/
public void register() {
Preconditions.checkState(mExtension == null, "register() has already been invoked.");
mExtension = new ZFileExtension() {
@Nullable
@Override
public IOExceptionRunnable beforeUpdate() throws IOException {
mFile.setExtraDirectoryOffset(0);
return null;
}
@Nullable
@Override
public IOExceptionRunnable added(@NonNull StoredEntry entry,
@Nullable StoredEntry replaced) {
onZipChanged();
return null;
}
@Nullable
@Override
public IOExceptionRunnable removed(@NonNull StoredEntry entry) {
onZipChanged();
return null;
}
@Override
public void entriesWritten() throws IOException {
onEntriesWritten();
}
};
mFile.addZFileExtension(mExtension);
}
/**
* Invoked when the zip file has been changed.
*/
private void onZipChanged() {
mNeedsSignatureUpdate = true;
}
/**
* Invoked before the zip file has been updated.
*
* @throws IOException failed to perform the update
*/
private void onEntriesWritten() throws IOException {
if (!mNeedsSignatureUpdate) {
return;
}
mNeedsSignatureUpdate = false;
byte[] apkSigningBlock = generateApkSigningBlock();
Verify.verify(apkSigningBlock.length > 0, "apkSigningBlock.length == 0");
mFile.setExtraDirectoryOffset(apkSigningBlock.length);
long apkSigningBlockOffset =
mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset();
mFile.directWrite(apkSigningBlockOffset, apkSigningBlock);
}
/**
* Generates a signature for the APK.
*
* @return the signature data block
* @throws IOException failed to generate a signature
*/
@NonNull
private byte[] generateApkSigningBlock() throws IOException {
byte[] centralDirectoryData = mFile.getCentralDirectoryBytes();
byte[] eocdData = mFile.getEocdBytes();
ApkSignerV2.SignerConfig signerConfig = new ApkSignerV2.SignerConfig();
signerConfig.privateKey = mPrivateKey;
signerConfig.certificates = ImmutableList.of(mCertificate);
signerConfig.signatureAlgorithms = mV2SignatureAlgorithms;
DigestSource centralDir = new ByteArrayDigestSource(centralDirectoryData);
DigestSource eocd = new ByteArrayDigestSource(eocdData);
DigestSource zipEntries =
new ZFileDigestSource(
mFile,
0,
mFile.getCentralDirectoryOffset() - mFile.getExtraDirectoryOffset());
try {
return ApkSignerV2.generateApkSigningBlock(
zipEntries,
centralDir,
eocd,
ImmutableList.of(signerConfig));
} catch (InvalidKeyException | SignatureException e) {
throw new IOException("Failed to sign APK using APK Signature Scheme v2", e);
}
}
}