Merge \"Use Builder pattern for ApkVerifier parameters.\"
am: 744e7746c4
Change-Id: I36d18044ea627ff9f5f83ef8463476de07ce45ff
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.util.AndroidSdkVersion;
|
||||
import com.android.apksigner.core.util.DataSource;
|
||||
import com.android.apksigner.core.util.DataSources;
|
||||
import com.android.apksigner.core.zip.ZipFormatException;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
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
|
||||
* 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 {
|
||||
|
||||
@@ -49,6 +55,57 @@ public class ApkVerifier {
|
||||
private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES =
|
||||
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
|
||||
* 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
|
||||
* 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 {
|
||||
if (minSdkVersion < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
@@ -1050,17 +1107,16 @@ public class ApkVerifier {
|
||||
*/
|
||||
private static class ByteArray {
|
||||
private final byte[] mArray;
|
||||
private final int mHashCode;
|
||||
|
||||
private ByteArray(byte[] arr) {
|
||||
mArray = arr;
|
||||
mHashCode = Arrays.hashCode(mArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + Arrays.hashCode(mArray);
|
||||
return result;
|
||||
return mHashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1075,10 +1131,103 @@ public class ApkVerifier {
|
||||
return false;
|
||||
}
|
||||
ByteArray other = (ByteArray) obj;
|
||||
if (hashCode() != other.hashCode()) {
|
||||
return false;
|
||||
}
|
||||
if (!Arrays.equals(mArray, other.mArray)) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
|
||||
import com.android.apksigner.core.internal.util.ByteBufferDataSource;
|
||||
import com.android.apksigner.core.internal.util.RandomAccessFileDataSource;
|
||||
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
@@ -21,4 +23,26 @@ public abstract class DataSources {
|
||||
}
|
||||
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