Use Builder pattern for ApkVerifier parameters.
This should make it easier to add parameters/options without breaking existing clients. Bug: 27461702 Change-Id: Ia4577f78d703a6b91828dd08492c78d5e9afb110
This commit is contained in:
@@ -23,9 +23,13 @@ import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm;
|
|||||||
import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
|
import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
|
||||||
import com.android.apksigner.core.internal.util.AndroidSdkVersion;
|
import com.android.apksigner.core.internal.util.AndroidSdkVersion;
|
||||||
import com.android.apksigner.core.util.DataSource;
|
import com.android.apksigner.core.util.DataSource;
|
||||||
|
import com.android.apksigner.core.util.DataSources;
|
||||||
import com.android.apksigner.core.zip.ZipFormatException;
|
import com.android.apksigner.core.zip.ZipFormatException;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -42,6 +46,8 @@ import java.util.Set;
|
|||||||
*
|
*
|
||||||
* <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
|
* <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
|
||||||
* the verifier to be used for checking whether an APK's signatures will verify on Android.
|
* the verifier to be used for checking whether an APK's signatures will verify on Android.
|
||||||
|
*
|
||||||
|
* <p>Use {@link Builder} to obtain instances of this verifier.
|
||||||
*/
|
*/
|
||||||
public class ApkVerifier {
|
public class ApkVerifier {
|
||||||
|
|
||||||
@@ -49,6 +55,57 @@ public class ApkVerifier {
|
|||||||
private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
|
private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
|
||||||
Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2");
|
Collections.singletonMap(APK_SIGNATURE_SCHEME_V2_ID, "APK Signature Scheme v2");
|
||||||
|
|
||||||
|
private final File mApkFile;
|
||||||
|
private final DataSource mApkDataSource;
|
||||||
|
|
||||||
|
private final int mMinSdkVersion;
|
||||||
|
private final int mMaxSdkVersion;
|
||||||
|
|
||||||
|
private ApkVerifier(
|
||||||
|
File apkFile,
|
||||||
|
DataSource apkDataSource,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion) {
|
||||||
|
mApkFile = apkFile;
|
||||||
|
mApkDataSource = apkDataSource;
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
mMaxSdkVersion = maxSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the APK's signatures and returns the result of verification. The APK can be
|
||||||
|
* considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
|
||||||
|
* The verification result also includes errors, warnings, and information about signers.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error is encountered while reading the APK
|
||||||
|
* @throws ZipFormatException if the APK is malformed at ZIP format level
|
||||||
|
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||||
|
* required cryptographic algorithm implementation is missing
|
||||||
|
* @throws IllegalStateException if this verifier's configuration is missing required
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
public Result verify() throws IOException, ZipFormatException, NoSuchAlgorithmException,
|
||||||
|
IllegalStateException {
|
||||||
|
Closeable in = null;
|
||||||
|
try {
|
||||||
|
DataSource apk;
|
||||||
|
if (mApkDataSource != null) {
|
||||||
|
apk = mApkDataSource;
|
||||||
|
} else if (mApkFile != null) {
|
||||||
|
RandomAccessFile f = new RandomAccessFile(mApkFile, "r");
|
||||||
|
in = f;
|
||||||
|
apk = DataSources.asDataSource(f, 0, f.length());
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("APK not provided");
|
||||||
|
}
|
||||||
|
return verify(apk, mMinSdkVersion, mMaxSdkVersion);
|
||||||
|
} finally {
|
||||||
|
if (in != null) {
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies the APK's signatures and returns the result of verification. The APK can be
|
* Verifies the APK's signatures and returns the result of verification. The APK can be
|
||||||
* considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
|
* considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
|
||||||
@@ -65,7 +122,7 @@ public class ApkVerifier {
|
|||||||
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||||
* required cryptographic algorithm implementation is missing
|
* required cryptographic algorithm implementation is missing
|
||||||
*/
|
*/
|
||||||
public Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion)
|
private static Result verify(DataSource apk, int minSdkVersion, int maxSdkVersion)
|
||||||
throws IOException, ZipFormatException, NoSuchAlgorithmException {
|
throws IOException, ZipFormatException, NoSuchAlgorithmException {
|
||||||
if (minSdkVersion < 0) {
|
if (minSdkVersion < 0) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
@@ -1050,17 +1107,16 @@ public class ApkVerifier {
|
|||||||
*/
|
*/
|
||||||
private static class ByteArray {
|
private static class ByteArray {
|
||||||
private final byte[] mArray;
|
private final byte[] mArray;
|
||||||
|
private final int mHashCode;
|
||||||
|
|
||||||
private ByteArray(byte[] arr) {
|
private ByteArray(byte[] arr) {
|
||||||
mArray = arr;
|
mArray = arr;
|
||||||
|
mHashCode = Arrays.hashCode(mArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
final int prime = 31;
|
return mHashCode;
|
||||||
int result = 1;
|
|
||||||
result = prime * result + Arrays.hashCode(mArray);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1075,10 +1131,103 @@ public class ApkVerifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ByteArray other = (ByteArray) obj;
|
ByteArray other = (ByteArray) obj;
|
||||||
|
if (hashCode() != other.hashCode()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!Arrays.equals(mArray, other.mArray)) {
|
if (!Arrays.equals(mArray, other.mArray)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder of {@link ApkVerifier} instances.
|
||||||
|
*
|
||||||
|
* <p>Although not required, it is best to provide the SDK version (API Level) of the oldest
|
||||||
|
* Android platform on which the APK is supposed to be installed -- see
|
||||||
|
* {@link #setMinCheckedPlatformVersion(int)}. Without this information, APKs which use security
|
||||||
|
* features not supported on ancient Android platforms (e.g., SHA-256 digests or ECDSA
|
||||||
|
* signatures) will not verify.
|
||||||
|
*/
|
||||||
|
public static class Builder {
|
||||||
|
private final File mApkFile;
|
||||||
|
private final DataSource mApkDataSource;
|
||||||
|
|
||||||
|
private int mMinSdkVersion = 1;
|
||||||
|
private int mMaxSdkVersion = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Builder} for verifying the provided APK file.
|
||||||
|
*/
|
||||||
|
public Builder(File apk) {
|
||||||
|
if (apk == null) {
|
||||||
|
throw new NullPointerException("apk == null");
|
||||||
|
}
|
||||||
|
mApkFile = apk;
|
||||||
|
mApkDataSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Builder} for verifying the provided APK.
|
||||||
|
*/
|
||||||
|
public Builder(DataSource apk) {
|
||||||
|
if (apk == null) {
|
||||||
|
throw new NullPointerException("apk == null");
|
||||||
|
}
|
||||||
|
mApkDataSource = apk;
|
||||||
|
mApkFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the oldest Android platform version for which the APK is verified. APK verification
|
||||||
|
* will confirm that the APK is expected to install successfully on all known Android
|
||||||
|
* platforms starting from the platform version with the provided API Level.
|
||||||
|
*
|
||||||
|
* <p>By default, the APK is checked for all platform versions. Thus, APKs which use
|
||||||
|
* security features not supported on ancient Android platforms (e.g., SHA-256 digests or
|
||||||
|
* ECDSA signatures) will not verify by default.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
|
||||||
|
*
|
||||||
|
* @see #setCheckedPlatformVersions(int, int)
|
||||||
|
*/
|
||||||
|
public Builder setMinCheckedPlatformVersion(int minSdkVersion) {
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
mMaxSdkVersion = Integer.MAX_VALUE;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the range of Android platform versions for which the APK is verified. APK
|
||||||
|
* verification will confirm that the APK is expected to install successfully on Android
|
||||||
|
* platforms whose API Levels fall into this inclusive range.
|
||||||
|
*
|
||||||
|
* <p>By default, the APK is checked for all platform versions. Thus, APKs which use
|
||||||
|
* security features not supported on ancient Android platforms (e.g., SHA-256 digests or
|
||||||
|
* ECDSA signatures) will not verify by default.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
|
||||||
|
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
|
||||||
|
*
|
||||||
|
* @see #setMinCheckedPlatformVersion(int)
|
||||||
|
*/
|
||||||
|
public Builder setCheckedPlatformVersions(int minSdkVersion, int maxSdkVersion) {
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
mMaxSdkVersion = maxSdkVersion;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an {@link ApkVerifier} initialized according to the configuration of this
|
||||||
|
* builder.
|
||||||
|
*/
|
||||||
|
public ApkVerifier build() {
|
||||||
|
return new ApkVerifier(
|
||||||
|
mApkFile,
|
||||||
|
mApkDataSource,
|
||||||
|
mMinSdkVersion,
|
||||||
|
mMaxSdkVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
* 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 java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
|
||||||
|
import com.android.apksigner.core.util.DataSink;
|
||||||
|
import com.android.apksigner.core.util.DataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSource} backed by a {@link RandomAccessFile}.
|
||||||
|
*/
|
||||||
|
public class RandomAccessFileDataSource implements DataSource {
|
||||||
|
|
||||||
|
private static final int MAX_READ_CHUNK_SIZE = 65536;
|
||||||
|
|
||||||
|
private final RandomAccessFile mFile;
|
||||||
|
private final long mOffset;
|
||||||
|
private final long mSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
|
||||||
|
* specified the whole file. Changes to the contents of the file, including the size of the
|
||||||
|
* file, will be visible in this data source.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileDataSource(RandomAccessFile file) {
|
||||||
|
mFile = file;
|
||||||
|
mOffset = 0;
|
||||||
|
mSize = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code RandomAccessFileDataSource} based on the data contained in the
|
||||||
|
* specified region of the provided file. Changes to the contents of the file will be visible in
|
||||||
|
* this data source.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileDataSource(RandomAccessFile file, long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IllegalArgumentException("offset: " + size);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IllegalArgumentException("size: " + size);
|
||||||
|
}
|
||||||
|
mFile = file;
|
||||||
|
mOffset = offset;
|
||||||
|
mSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
if (mSize == -1) {
|
||||||
|
try {
|
||||||
|
return mFile.length();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RandomAccessFileDataSource slice(long offset, long size) {
|
||||||
|
long sourceSize = size();
|
||||||
|
checkChunkValid(offset, size, sourceSize);
|
||||||
|
if ((offset == 0) && (size == sourceSize)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RandomAccessFileDataSource(mFile, mOffset + offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
long sourceSize = size();
|
||||||
|
checkChunkValid(offset, size, sourceSize);
|
||||||
|
if (size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long chunkOffsetInFile = mOffset + offset;
|
||||||
|
long remaining = size;
|
||||||
|
byte[] buf = new byte[(int) Math.min(remaining, MAX_READ_CHUNK_SIZE)];
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkSize = (int) Math.min(remaining, buf.length);
|
||||||
|
synchronized (mFile) {
|
||||||
|
mFile.seek(chunkOffsetInFile);
|
||||||
|
mFile.readFully(buf, 0, chunkSize);
|
||||||
|
}
|
||||||
|
sink.consume(buf, 0, chunkSize);
|
||||||
|
chunkOffsetInFile += chunkSize;
|
||||||
|
remaining -= chunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
long sourceSize = size();
|
||||||
|
checkChunkValid(offset, size, sourceSize);
|
||||||
|
if (size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long offsetInFile = mOffset + offset;
|
||||||
|
int remaining = size;
|
||||||
|
FileChannel fileChannel = mFile.getChannel();
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkSize;
|
||||||
|
synchronized (mFile) {
|
||||||
|
fileChannel.position(offsetInFile);
|
||||||
|
chunkSize = fileChannel.read(dest);
|
||||||
|
}
|
||||||
|
offsetInFile += chunkSize;
|
||||||
|
remaining -= chunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(size);
|
||||||
|
copyTo(offset, size, result);
|
||||||
|
result.flip();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkChunkValid(long offset, long size, long sourceSize) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IllegalArgumentException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IllegalArgumentException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > sourceSize) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"offset (" + offset + ") > source size (" + sourceSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > sourceSize) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"offset (" + offset + ") + size (" + size
|
||||||
|
+ ") > source size (" + sourceSize +")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,9 @@
|
|||||||
package com.android.apksigner.core.util;
|
package com.android.apksigner.core.util;
|
||||||
|
|
||||||
import com.android.apksigner.core.internal.util.ByteBufferDataSource;
|
import com.android.apksigner.core.internal.util.ByteBufferDataSource;
|
||||||
|
import com.android.apksigner.core.internal.util.RandomAccessFileDataSource;
|
||||||
|
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -21,4 +23,26 @@ public abstract class DataSources {
|
|||||||
}
|
}
|
||||||
return new ByteBufferDataSource(buffer);
|
return new ByteBufferDataSource(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link DataSource} backed by the provided {@link RandomAccessFile}. Changes to the
|
||||||
|
* file, including changes to size of file, will be visible in the data source.
|
||||||
|
*/
|
||||||
|
public static DataSource asDataSource(RandomAccessFile file) {
|
||||||
|
if (file == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
return new RandomAccessFileDataSource(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link DataSource} backed by the provided region of the {@link RandomAccessFile}.
|
||||||
|
* Changes to the file will be visible in the data source.
|
||||||
|
*/
|
||||||
|
public static DataSource asDataSource(RandomAccessFile file, long offset, long size) {
|
||||||
|
if (file == null) {
|
||||||
|
throw new NullPointerException();
|
||||||
|
}
|
||||||
|
return new RandomAccessFileDataSource(file, offset, size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user