Offer an ApkSignerEngine implementation.
This adds an implementation of ApkSignerEngine to the apksigner-core library. Bug: 27461702 Change-Id: I5f977b98555ca507a0dfcd3e92eecb9758aa8370
This commit is contained in:
parent
afd3d55261
commit
e305f45f00
|
@ -0,0 +1,870 @@
|
||||||
|
/*
|
||||||
|
* 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.apksigner.core;
|
||||||
|
|
||||||
|
import com.android.apksigner.core.internal.apk.v1.DigestAlgorithm;
|
||||||
|
import com.android.apksigner.core.internal.apk.v1.V1SchemeSigner;
|
||||||
|
import com.android.apksigner.core.internal.apk.v2.MessageDigestSink;
|
||||||
|
import com.android.apksigner.core.internal.apk.v2.V2SchemeSigner;
|
||||||
|
import com.android.apksigner.core.internal.util.ByteArrayOutputStreamSink;
|
||||||
|
import com.android.apksigner.core.internal.util.Pair;
|
||||||
|
import com.android.apksigner.core.util.DataSink;
|
||||||
|
import com.android.apksigner.core.util.DataSource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
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.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link ApkSignerEngine}.
|
||||||
|
*
|
||||||
|
* <p>Use {@link Builder} to obtain instances of this engine.
|
||||||
|
*/
|
||||||
|
public class DefaultApkSignerEngine implements ApkSignerEngine {
|
||||||
|
|
||||||
|
// IMPLEMENTATION NOTE: This engine generates a signed APK as follows:
|
||||||
|
// 1. The engine asks its client to output input JAR entries which are not part of JAR
|
||||||
|
// signature.
|
||||||
|
// 2. If JAR signing (v1 signing) is enabled, the engine inspects the output JAR entries to
|
||||||
|
// compute their digests, to be placed into output META-INF/MANIFEST.MF. It also inspects
|
||||||
|
// the contents of input and output META-INF/MANIFEST.MF to borrow the main section of the
|
||||||
|
// file. It does not care about individual (i.e., JAR entry-specific) sections. It then
|
||||||
|
// emits the v1 signature (a set of JAR entries) and asks the client to output them.
|
||||||
|
// 3. If APK Signature Scheme v2 (v2 signing) is enabled, the engine emits an APK Signing Block
|
||||||
|
// from outputZipSections() and asks its client to insert this block into the output.
|
||||||
|
|
||||||
|
private final boolean mV1SigningEnabled;
|
||||||
|
private final boolean mV2SigningEnabled;
|
||||||
|
private final boolean mOtherSignersSignaturesPreserved;
|
||||||
|
private final List<V1SchemeSigner.SignerConfig> mV1SignerConfigs;
|
||||||
|
private final DigestAlgorithm mV1ContentDigestAlgorithm;
|
||||||
|
private final List<V2SchemeSigner.SignerConfig> mV2SignerConfigs;
|
||||||
|
|
||||||
|
private boolean mClosed;
|
||||||
|
|
||||||
|
private boolean mV1SignaturePending;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Names of JAR entries which this engine is expected to output as part of v1 signing.
|
||||||
|
*/
|
||||||
|
private final Set<String> mSignatureExpectedOutputJarEntryNames;
|
||||||
|
|
||||||
|
/** Requests for digests of output JAR entries. */
|
||||||
|
private final Map<String, GetJarEntryDataDigestRequest> mOutputJarEntryDigestRequests =
|
||||||
|
new HashMap<>();
|
||||||
|
|
||||||
|
/** Digests of output JAR entries. */
|
||||||
|
private final Map<String, byte[]> mOutputJarEntryDigests = new HashMap<>();
|
||||||
|
|
||||||
|
/** Data of JAR entries emitted by this engine as v1 signature. */
|
||||||
|
private final Map<String, byte[]> mEmittedSignatureJarEntryData = new HashMap<>();
|
||||||
|
|
||||||
|
/** Requests for data of output JAR entries which comprise the v1 signature. */
|
||||||
|
private final Map<String, GetJarEntryDataRequest> mOutputSignatureJarEntryDataRequests =
|
||||||
|
new HashMap<>();
|
||||||
|
/**
|
||||||
|
* Request to obtain the data of MANIFEST.MF or {@code null} if the request hasn't been issued.
|
||||||
|
*/
|
||||||
|
private GetJarEntryDataRequest mInputJarManifestEntryDataRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to output the emitted v1 signature or {@code null} if the request hasn't been issued.
|
||||||
|
*/
|
||||||
|
private OutputJarSignatureRequestImpl mAddV1SignatureRequest;
|
||||||
|
|
||||||
|
private boolean mV2SignaturePending;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to output the emitted v2 signature or {@code null} if the request hasn't been issued.
|
||||||
|
*/
|
||||||
|
private OutputApkSigningBlockRequestImpl mAddV2SignatureRequest;
|
||||||
|
|
||||||
|
private DefaultApkSignerEngine(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
int minSdkVersion,
|
||||||
|
boolean v1SigningEnabled,
|
||||||
|
boolean v2SigningEnabled,
|
||||||
|
boolean otherSignersSignaturesPreserved) throws InvalidKeyException {
|
||||||
|
if (signerConfigs.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||||
|
}
|
||||||
|
if (otherSignersSignaturesPreserved) {
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"Preserving other signer's signatures is not yet implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
mV1SigningEnabled = v1SigningEnabled;
|
||||||
|
mV2SigningEnabled = v2SigningEnabled;
|
||||||
|
mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved;
|
||||||
|
mV1SignerConfigs =
|
||||||
|
(v1SigningEnabled)
|
||||||
|
? new ArrayList<>(signerConfigs.size()) : Collections.emptyList();
|
||||||
|
mV2SignerConfigs =
|
||||||
|
(v2SigningEnabled)
|
||||||
|
? new ArrayList<>(signerConfigs.size()) : Collections.emptyList();
|
||||||
|
mV1ContentDigestAlgorithm =
|
||||||
|
(v1SigningEnabled)
|
||||||
|
? V1SchemeSigner.getSuggestedContentDigestAlgorithm(minSdkVersion) : null;
|
||||||
|
for (SignerConfig signerConfig : signerConfigs) {
|
||||||
|
List<X509Certificate> certificates = signerConfig.getCertificates();
|
||||||
|
PublicKey publicKey = certificates.get(0).getPublicKey();
|
||||||
|
|
||||||
|
if (v1SigningEnabled) {
|
||||||
|
DigestAlgorithm v1SignatureDigestAlgorithm =
|
||||||
|
V1SchemeSigner.getSuggestedSignatureDigestAlgorithm(
|
||||||
|
publicKey, minSdkVersion);
|
||||||
|
V1SchemeSigner.SignerConfig v1SignerConfig = new V1SchemeSigner.SignerConfig();
|
||||||
|
v1SignerConfig.name = signerConfig.getName();
|
||||||
|
v1SignerConfig.privateKey = signerConfig.getPrivateKey();
|
||||||
|
v1SignerConfig.certificates = certificates;
|
||||||
|
v1SignerConfig.contentDigestAlgorithm = mV1ContentDigestAlgorithm;
|
||||||
|
v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm;
|
||||||
|
mV1SignerConfigs.add(v1SignerConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v2SigningEnabled) {
|
||||||
|
V2SchemeSigner.SignerConfig v2SignerConfig = new V2SchemeSigner.SignerConfig();
|
||||||
|
v2SignerConfig.privateKey = signerConfig.getPrivateKey();
|
||||||
|
v2SignerConfig.certificates = certificates;
|
||||||
|
v2SignerConfig.signatureAlgorithms =
|
||||||
|
V2SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, minSdkVersion);
|
||||||
|
mV2SignerConfigs.add(v2SignerConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mSignatureExpectedOutputJarEntryNames =
|
||||||
|
(v1SigningEnabled)
|
||||||
|
? V1SchemeSigner.getOutputEntryNames(mV1SignerConfigs)
|
||||||
|
: Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void inputApkSigningBlock(DataSource apkSigningBlock) {
|
||||||
|
checkNotClosed();
|
||||||
|
|
||||||
|
if ((apkSigningBlock == null) || (apkSigningBlock.size() == 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOtherSignersSignaturesPreserved) {
|
||||||
|
// TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured
|
||||||
|
// in this engine.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: Preserve blocks other than APK Signature Scheme v2 blocks.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputJarEntryInstructions inputJarEntry(String entryName) {
|
||||||
|
checkNotClosed();
|
||||||
|
|
||||||
|
InputJarEntryInstructions.OutputPolicy outputPolicy =
|
||||||
|
getInputJarEntryOutputPolicy(entryName);
|
||||||
|
switch (outputPolicy) {
|
||||||
|
case SKIP:
|
||||||
|
return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.SKIP);
|
||||||
|
case OUTPUT:
|
||||||
|
return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT);
|
||||||
|
case OUTPUT_BY_ENGINE:
|
||||||
|
if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
|
||||||
|
// We copy the main section of the JAR manifest from input to output. Thus, this
|
||||||
|
// invalidates v1 signature and we need to see the entry's data.
|
||||||
|
mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName);
|
||||||
|
return new InputJarEntryInstructions(
|
||||||
|
InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE,
|
||||||
|
mInputJarManifestEntryDataRequest);
|
||||||
|
}
|
||||||
|
return new InputJarEntryInstructions(
|
||||||
|
InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE);
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unsupported output policy: " + outputPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InspectJarEntryRequest outputJarEntry(String entryName) {
|
||||||
|
checkNotClosed();
|
||||||
|
invalidateV2Signature();
|
||||||
|
if (!mV1SigningEnabled) {
|
||||||
|
// No need to inspect JAR entries when v1 signing is not enabled.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// v1 signing is enabled
|
||||||
|
|
||||||
|
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
|
||||||
|
// This entry is covered by v1 signature. We thus need to inspect the entry's data to
|
||||||
|
// compute its digest(s) for v1 signature.
|
||||||
|
|
||||||
|
// TODO: Handle the case where other signer's v1 signatures are present and need to be
|
||||||
|
// preserved. In that scenario we can't modify MANIFEST.MF and add/remove JAR entries
|
||||||
|
// covered by v1 signature.
|
||||||
|
invalidateV1Signature();
|
||||||
|
GetJarEntryDataDigestRequest dataDigestRequest =
|
||||||
|
new GetJarEntryDataDigestRequest(
|
||||||
|
entryName,
|
||||||
|
V1SchemeSigner.getMessageDigestInstance(mV1ContentDigestAlgorithm));
|
||||||
|
mOutputJarEntryDigestRequests.put(entryName, dataDigestRequest);
|
||||||
|
mOutputJarEntryDigests.remove(entryName);
|
||||||
|
return dataDigestRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
|
||||||
|
// This entry is part of v1 signature generated by this engine. We need to check whether
|
||||||
|
// the entry's data is as output by the engine.
|
||||||
|
invalidateV1Signature();
|
||||||
|
GetJarEntryDataRequest dataRequest;
|
||||||
|
if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) {
|
||||||
|
dataRequest = new GetJarEntryDataRequest(entryName);
|
||||||
|
mInputJarManifestEntryDataRequest = dataRequest;
|
||||||
|
} else {
|
||||||
|
// If this entry is part of v1 signature which has been emitted by this engine,
|
||||||
|
// check whether the output entry's data matches what the engine emitted.
|
||||||
|
dataRequest =
|
||||||
|
(mEmittedSignatureJarEntryData.containsKey(entryName))
|
||||||
|
? new GetJarEntryDataRequest(entryName) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataRequest != null) {
|
||||||
|
mOutputSignatureJarEntryDataRequests.put(entryName, dataRequest);
|
||||||
|
}
|
||||||
|
return dataRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This entry is not covered by v1 signature and isn't part of v1 signature.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName) {
|
||||||
|
checkNotClosed();
|
||||||
|
return getInputJarEntryOutputPolicy(entryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void outputJarEntryRemoved(String entryName) {
|
||||||
|
checkNotClosed();
|
||||||
|
invalidateV2Signature();
|
||||||
|
if (!mV1SigningEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName)) {
|
||||||
|
// This entry is covered by v1 signature.
|
||||||
|
invalidateV1Signature();
|
||||||
|
mOutputJarEntryDigests.remove(entryName);
|
||||||
|
mOutputJarEntryDigestRequests.remove(entryName);
|
||||||
|
mOutputSignatureJarEntryDataRequests.remove(entryName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
|
||||||
|
// This entry is part of the v1 signature generated by this engine.
|
||||||
|
invalidateV1Signature();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputJarSignatureRequest outputJarEntries()
|
||||||
|
throws InvalidKeyException, SignatureException {
|
||||||
|
checkNotClosed();
|
||||||
|
|
||||||
|
if (!mV1SignaturePending) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mInputJarManifestEntryDataRequest != null)
|
||||||
|
&& (!mInputJarManifestEntryDataRequest.isDone())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Still waiting to inspect input APK's "
|
||||||
|
+ mInputJarManifestEntryDataRequest.getEntryName());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (GetJarEntryDataDigestRequest digestRequest
|
||||||
|
: mOutputJarEntryDigestRequests.values()) {
|
||||||
|
String entryName = digestRequest.getEntryName();
|
||||||
|
if (!digestRequest.isDone()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Still waiting to inspect output APK's " + entryName);
|
||||||
|
}
|
||||||
|
mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
|
||||||
|
}
|
||||||
|
mOutputJarEntryDigestRequests.clear();
|
||||||
|
|
||||||
|
for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
|
||||||
|
if (!dataRequest.isDone()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Still waiting to inspect output APK's " + dataRequest.getEntryName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> apkSigningSchemeIds =
|
||||||
|
(mV2SigningEnabled) ? Collections.singletonList(2) : Collections.emptyList();
|
||||||
|
byte[] inputJarManifest =
|
||||||
|
(mInputJarManifestEntryDataRequest != null)
|
||||||
|
? mInputJarManifestEntryDataRequest.getData() : null;
|
||||||
|
|
||||||
|
// Check whether the most recently used signature (if present) is still fine.
|
||||||
|
List<Pair<String, byte[]>> signatureZipEntries;
|
||||||
|
if ((mAddV1SignatureRequest == null) || (!mAddV1SignatureRequest.isDone())) {
|
||||||
|
try {
|
||||||
|
signatureZipEntries =
|
||||||
|
V1SchemeSigner.sign(
|
||||||
|
mV1SignerConfigs,
|
||||||
|
mV1ContentDigestAlgorithm,
|
||||||
|
mOutputJarEntryDigests,
|
||||||
|
apkSigningSchemeIds,
|
||||||
|
inputJarManifest);
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new SignatureException("Failed to generate v1 signature", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
V1SchemeSigner.OutputManifestFile newManifest =
|
||||||
|
V1SchemeSigner.generateManifestFile(
|
||||||
|
mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest);
|
||||||
|
byte[] emittedSignatureManifest =
|
||||||
|
mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME);
|
||||||
|
if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) {
|
||||||
|
// Emitted v1 signature is no longer valid.
|
||||||
|
try {
|
||||||
|
signatureZipEntries =
|
||||||
|
V1SchemeSigner.signManifest(
|
||||||
|
mV1SignerConfigs,
|
||||||
|
mV1ContentDigestAlgorithm,
|
||||||
|
apkSigningSchemeIds,
|
||||||
|
newManifest);
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new SignatureException("Failed to generate v1 signature", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Emitted v1 signature is still valid. Check whether the signature is there in the
|
||||||
|
// output.
|
||||||
|
signatureZipEntries = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, byte[]> expectedOutputEntry
|
||||||
|
: mEmittedSignatureJarEntryData.entrySet()) {
|
||||||
|
String entryName = expectedOutputEntry.getKey();
|
||||||
|
byte[] expectedData = expectedOutputEntry.getValue();
|
||||||
|
GetJarEntryDataRequest actualDataRequest =
|
||||||
|
mOutputSignatureJarEntryDataRequests.get(entryName);
|
||||||
|
if (actualDataRequest == null) {
|
||||||
|
// This signature entry hasn't been output.
|
||||||
|
signatureZipEntries.add(Pair.of(entryName, expectedData));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byte[] actualData = actualDataRequest.getData();
|
||||||
|
if (!Arrays.equals(expectedData, actualData)) {
|
||||||
|
signatureZipEntries.add(Pair.of(entryName, expectedData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (signatureZipEntries.isEmpty()) {
|
||||||
|
// v1 signature in the output is valid
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// v1 signature in the output is not valid.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signatureZipEntries.isEmpty()) {
|
||||||
|
// v1 signature in the output is valid
|
||||||
|
mV1SignaturePending = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OutputJarSignatureRequest.JarEntry> sigEntries =
|
||||||
|
new ArrayList<>(signatureZipEntries.size());
|
||||||
|
for (Pair<String, byte[]> entry : signatureZipEntries) {
|
||||||
|
String entryName = entry.getFirst();
|
||||||
|
byte[] entryData = entry.getSecond();
|
||||||
|
sigEntries.add(new OutputJarSignatureRequest.JarEntry(entryName, entryData));
|
||||||
|
mEmittedSignatureJarEntryData.put(entryName, entryData);
|
||||||
|
}
|
||||||
|
mAddV1SignatureRequest = new OutputJarSignatureRequestImpl(sigEntries);
|
||||||
|
return mAddV1SignatureRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OutputApkSigningBlockRequest outputZipSections(
|
||||||
|
DataSource zipEntries,
|
||||||
|
DataSource zipCentralDirectory,
|
||||||
|
DataSource zipEocd) throws IOException, InvalidKeyException, SignatureException {
|
||||||
|
checkNotClosed();
|
||||||
|
checkV1SigningDoneIfEnabled();
|
||||||
|
if (!mV2SigningEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
invalidateV2Signature();
|
||||||
|
|
||||||
|
byte[] apkSigningBlock =
|
||||||
|
V2SchemeSigner.generateApkSigningBlock(
|
||||||
|
zipEntries, zipCentralDirectory, zipEocd, mV2SignerConfigs);
|
||||||
|
|
||||||
|
mAddV2SignatureRequest = new OutputApkSigningBlockRequestImpl(apkSigningBlock);
|
||||||
|
return mAddV2SignatureRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void outputDone() {
|
||||||
|
checkNotClosed();
|
||||||
|
checkV1SigningDoneIfEnabled();
|
||||||
|
checkV2SigningDoneIfEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
mClosed = true;
|
||||||
|
|
||||||
|
mAddV1SignatureRequest = null;
|
||||||
|
mInputJarManifestEntryDataRequest = null;
|
||||||
|
mOutputJarEntryDigestRequests.clear();
|
||||||
|
mOutputJarEntryDigests.clear();
|
||||||
|
mEmittedSignatureJarEntryData.clear();
|
||||||
|
mOutputSignatureJarEntryDataRequests.clear();
|
||||||
|
|
||||||
|
mAddV2SignatureRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateV1Signature() {
|
||||||
|
if (mV1SigningEnabled) {
|
||||||
|
mV1SignaturePending = true;
|
||||||
|
}
|
||||||
|
invalidateV2Signature();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invalidateV2Signature() {
|
||||||
|
if (mV2SigningEnabled) {
|
||||||
|
mV2SignaturePending = true;
|
||||||
|
mAddV2SignatureRequest = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotClosed() {
|
||||||
|
if (mClosed) {
|
||||||
|
throw new IllegalStateException("Engine closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkV1SigningDoneIfEnabled() {
|
||||||
|
if (!mV1SignaturePending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mAddV1SignatureRequest == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"v1 signature (JAR signature) not yet generated. Skipped outputJarEntries()?");
|
||||||
|
}
|
||||||
|
if (!mAddV1SignatureRequest.isDone()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"v1 signature (JAR signature) addition requested by outputJarEntries() hasn't"
|
||||||
|
+ " been fulfilled");
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, byte[]> expectedOutputEntry
|
||||||
|
: mEmittedSignatureJarEntryData.entrySet()) {
|
||||||
|
String entryName = expectedOutputEntry.getKey();
|
||||||
|
byte[] expectedData = expectedOutputEntry.getValue();
|
||||||
|
GetJarEntryDataRequest actualDataRequest =
|
||||||
|
mOutputSignatureJarEntryDataRequests.get(entryName);
|
||||||
|
if (actualDataRequest == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"APK entry " + entryName + " not yet output despite this having been"
|
||||||
|
+ " requested");
|
||||||
|
} else if (!actualDataRequest.isDone()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Still waiting to inspect output APK's " + entryName);
|
||||||
|
}
|
||||||
|
byte[] actualData = actualDataRequest.getData();
|
||||||
|
if (!Arrays.equals(expectedData, actualData)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Output APK entry " + entryName + " data differs from what was requested");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mV1SignaturePending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkV2SigningDoneIfEnabled() {
|
||||||
|
if (!mV2SignaturePending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mAddV2SignatureRequest == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"v2 signature (APK Signature Scheme v2 signature) not yet generated."
|
||||||
|
+ " Skipped outputZipSections()?");
|
||||||
|
}
|
||||||
|
if (!mAddV2SignatureRequest.isDone()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"v2 signature (APK Signature Scheme v2 signature) addition requested by"
|
||||||
|
+ " outputZipSections() hasn't been fulfilled yet");
|
||||||
|
}
|
||||||
|
mAddV2SignatureRequest = null;
|
||||||
|
mV2SignaturePending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the output policy for the provided input JAR entry.
|
||||||
|
*/
|
||||||
|
private InputJarEntryInstructions.OutputPolicy getInputJarEntryOutputPolicy(String entryName) {
|
||||||
|
if (mSignatureExpectedOutputJarEntryNames.contains(entryName)) {
|
||||||
|
return InputJarEntryInstructions.OutputPolicy.OUTPUT_BY_ENGINE;
|
||||||
|
}
|
||||||
|
if ((mOtherSignersSignaturesPreserved)
|
||||||
|
|| (V1SchemeSigner.isJarEntryDigestNeededInManifest(entryName))) {
|
||||||
|
return InputJarEntryInstructions.OutputPolicy.OUTPUT;
|
||||||
|
}
|
||||||
|
return InputJarEntryInstructions.OutputPolicy.SKIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
|
||||||
|
private final List<JarEntry> mAdditionalJarEntries;
|
||||||
|
private volatile boolean mDone;
|
||||||
|
|
||||||
|
private OutputJarSignatureRequestImpl(List<JarEntry> additionalZipEntries) {
|
||||||
|
mAdditionalJarEntries =
|
||||||
|
Collections.unmodifiableList(new ArrayList<>(additionalZipEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<JarEntry> getAdditionalJarEntries() {
|
||||||
|
return mAdditionalJarEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done() {
|
||||||
|
mDone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDone() {
|
||||||
|
return mDone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OutputApkSigningBlockRequestImpl implements OutputApkSigningBlockRequest {
|
||||||
|
private final byte[] mApkSigningBlock;
|
||||||
|
private volatile boolean mDone;
|
||||||
|
|
||||||
|
private OutputApkSigningBlockRequestImpl(byte[] apkSigingBlock) {
|
||||||
|
mApkSigningBlock = apkSigingBlock.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getApkSigningBlock() {
|
||||||
|
return mApkSigningBlock.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done() {
|
||||||
|
mDone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDone() {
|
||||||
|
return mDone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR entry inspection request which obtain the entry's uncompressed data.
|
||||||
|
*/
|
||||||
|
private static class GetJarEntryDataRequest implements InspectJarEntryRequest {
|
||||||
|
private final String mEntryName;
|
||||||
|
private final Object mLock = new Object();
|
||||||
|
private final ByteArrayOutputStreamSink mBuf = new ByteArrayOutputStreamSink();
|
||||||
|
|
||||||
|
private boolean mDone;
|
||||||
|
|
||||||
|
private GetJarEntryDataRequest(String entryName) {
|
||||||
|
mEntryName = entryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEntryName() {
|
||||||
|
return mEntryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSink getDataSink() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
checkNotDone();
|
||||||
|
return mBuf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (mDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDone() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
return mDone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotDone() throws IllegalStateException {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (mDone) {
|
||||||
|
throw new IllegalStateException("Already done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getData() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (!mDone) {
|
||||||
|
throw new IllegalStateException("Not yet done");
|
||||||
|
}
|
||||||
|
return mBuf.getData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR entry inspection request which obtains the digest of the entry's uncompressed data.
|
||||||
|
*/
|
||||||
|
private static class GetJarEntryDataDigestRequest implements InspectJarEntryRequest {
|
||||||
|
private final String mEntryName;
|
||||||
|
private final MessageDigest mMessageDigest;
|
||||||
|
private final DataSink mDataSink;
|
||||||
|
private final Object mLock = new Object();
|
||||||
|
|
||||||
|
private boolean mDone;
|
||||||
|
private byte[] mDigest;
|
||||||
|
|
||||||
|
private GetJarEntryDataDigestRequest(String entryName, MessageDigest digest) {
|
||||||
|
mEntryName = entryName;
|
||||||
|
mMessageDigest = digest;
|
||||||
|
mDataSink = new MessageDigestSink(new MessageDigest[] {mMessageDigest});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEntryName() {
|
||||||
|
return mEntryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSink getDataSink() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
checkNotDone();
|
||||||
|
return mDataSink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void done() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (mDone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mDone = true;
|
||||||
|
mDigest = mMessageDigest.digest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDone() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
return mDone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkNotDone() throws IllegalStateException {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (mDone) {
|
||||||
|
throw new IllegalStateException("Already done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getDigest() {
|
||||||
|
synchronized (mLock) {
|
||||||
|
if (!mDone) {
|
||||||
|
throw new IllegalStateException("Not yet done");
|
||||||
|
}
|
||||||
|
return mDigest.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration of a signer.
|
||||||
|
*
|
||||||
|
* <p>Use {@link Builder} to obtain configuration instances.
|
||||||
|
*/
|
||||||
|
public static class SignerConfig {
|
||||||
|
private final String mName;
|
||||||
|
private final PrivateKey mPrivateKey;
|
||||||
|
private final List<X509Certificate> mCertificates;
|
||||||
|
|
||||||
|
private SignerConfig(
|
||||||
|
String name,
|
||||||
|
PrivateKey privateKey,
|
||||||
|
List<X509Certificate> certificates) {
|
||||||
|
mName = name;
|
||||||
|
mPrivateKey = privateKey;
|
||||||
|
mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of this signer.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the signing key of this signer.
|
||||||
|
*/
|
||||||
|
public PrivateKey getPrivateKey() {
|
||||||
|
return mPrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the certificate(s) of this signer. The first certificate's public key corresponds
|
||||||
|
* to this signer's private key.
|
||||||
|
*/
|
||||||
|
public List<X509Certificate> getCertificates() {
|
||||||
|
return mCertificates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder of {@link SignerConfig} instances.
|
||||||
|
*/
|
||||||
|
public static class Builder {
|
||||||
|
private final String mName;
|
||||||
|
private final PrivateKey mPrivateKey;
|
||||||
|
private final List<X509Certificate> mCertificates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Builder}.
|
||||||
|
*
|
||||||
|
* @param name signer's name. The name is reflected in the name of files comprising the
|
||||||
|
* JAR signature of the APK.
|
||||||
|
* @param privateKey signing key
|
||||||
|
* @param certificates list of one or more X.509 certificates. The subject public key of
|
||||||
|
* the first certificate must correspond to the {@code privateKey}.
|
||||||
|
*/
|
||||||
|
public Builder(
|
||||||
|
String name,
|
||||||
|
PrivateKey privateKey,
|
||||||
|
List<X509Certificate> certificates) {
|
||||||
|
mName = name;
|
||||||
|
mPrivateKey = privateKey;
|
||||||
|
mCertificates = new ArrayList<>(certificates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@code SignerConfig} instance configured based on the configuration of
|
||||||
|
* this builder.
|
||||||
|
*/
|
||||||
|
public SignerConfig build() {
|
||||||
|
return new SignerConfig(
|
||||||
|
mName,
|
||||||
|
mPrivateKey,
|
||||||
|
mCertificates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder of {@link DefaultApkSignerEngine} instances.
|
||||||
|
*/
|
||||||
|
public static class Builder {
|
||||||
|
private final List<SignerConfig> mSignerConfigs;
|
||||||
|
private final int mMinSdkVersion;
|
||||||
|
|
||||||
|
private boolean mV1SigningEnabled = true;
|
||||||
|
private boolean mV2SigningEnabled = true;
|
||||||
|
private boolean mOtherSignersSignaturesPreserved;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Builder}.
|
||||||
|
*
|
||||||
|
* @param signerConfigs information about signers with which the APK will be signed. At
|
||||||
|
* least one signer configuration must be provided.
|
||||||
|
* @param minSdkVersion API Level of the oldest Android platform on which the APK is
|
||||||
|
* supposed to be installed. See {@code minSdkVersion} attribute in the APK's
|
||||||
|
* {@code AndroidManifest.xml}. The higher the version, the stronger signing features
|
||||||
|
* will be enabled.
|
||||||
|
*/
|
||||||
|
public Builder(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
int minSdkVersion) {
|
||||||
|
if (signerConfigs.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||||
|
}
|
||||||
|
mSignerConfigs = new ArrayList<>(signerConfigs);
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@code DefaultApkSignerEngine} instance configured based on the
|
||||||
|
* configuration of this builder.
|
||||||
|
*/
|
||||||
|
public DefaultApkSignerEngine build() throws InvalidKeyException {
|
||||||
|
return new DefaultApkSignerEngine(
|
||||||
|
mSignerConfigs,
|
||||||
|
mMinSdkVersion,
|
||||||
|
mV1SigningEnabled,
|
||||||
|
mV2SigningEnabled,
|
||||||
|
mOtherSignersSignaturesPreserved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the APK should be signed using JAR signing (aka v1 signature scheme).
|
||||||
|
*
|
||||||
|
* <p>By default, the APK will be signed using this scheme.
|
||||||
|
*/
|
||||||
|
public Builder setV1SigningEnabled(boolean enabled) {
|
||||||
|
mV1SigningEnabled = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the APK should be signed using APK Signature Scheme v2 (aka v2 signature
|
||||||
|
* scheme).
|
||||||
|
*
|
||||||
|
* <p>By default, the APK will be signed using this scheme.
|
||||||
|
*/
|
||||||
|
public Builder setV2SigningEnabled(boolean enabled) {
|
||||||
|
mV2SigningEnabled = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether signatures produced by signers other than the ones configured in this engine
|
||||||
|
* should be copied from the input APK to the output APK.
|
||||||
|
*
|
||||||
|
* <p>By default, signatures of other signers are omitted from the output APK.
|
||||||
|
*/
|
||||||
|
public Builder setOtherSignersSignaturesPreserved(boolean preserved) {
|
||||||
|
mOtherSignersSignaturesPreserved = preserved;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,11 +24,11 @@ import java.security.MessageDigest;
|
||||||
* Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
|
* Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
|
||||||
* {@code MessageDigest} instance receives the same data.
|
* {@code MessageDigest} instance receives the same data.
|
||||||
*/
|
*/
|
||||||
class MessageDigestSink implements DataSink {
|
public class MessageDigestSink implements DataSink {
|
||||||
|
|
||||||
private final MessageDigest[] mMessageDigests;
|
private final MessageDigest[] mMessageDigests;
|
||||||
|
|
||||||
MessageDigestSink(MessageDigest[] digests) {
|
public MessageDigestSink(MessageDigest[] digests) {
|
||||||
mMessageDigests = digests;
|
mMessageDigests = digests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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.apksigner.core.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksigner.core.util.DataSink;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sink which stores all input data into an internal {@link ByteArrayOutputStream}, thus
|
||||||
|
* accepting an arbitrary amount of data.
|
||||||
|
*/
|
||||||
|
public class ByteArrayOutputStreamSink implements DataSink {
|
||||||
|
|
||||||
|
private final ByteArrayOutputStream mBuf = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) {
|
||||||
|
mBuf.write(buf, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) {
|
||||||
|
if (!buf.hasRemaining()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.hasArray()) {
|
||||||
|
mBuf.write(
|
||||||
|
buf.array(),
|
||||||
|
buf.arrayOffset() + buf.position(),
|
||||||
|
buf.remaining());
|
||||||
|
buf.position(buf.limit());
|
||||||
|
} else {
|
||||||
|
byte[] tmp = new byte[buf.remaining()];
|
||||||
|
buf.get(tmp);
|
||||||
|
mBuf.write(tmp, 0, tmp.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data received so far.
|
||||||
|
*/
|
||||||
|
public byte[] getData() {
|
||||||
|
return mBuf.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue