You've already forked godot
mirror of
https://github.com/godotengine/godot.git
synced 2025-11-04 12:00:25 +00:00
Add support to the Android editor for signing and verifying Android apks
- Apk signing and verification is enabled using the apksig library from ac5cbb07d8
This commit is contained in:
@@ -17,6 +17,7 @@ repos:
|
|||||||
exclude: |
|
exclude: |
|
||||||
(?x)^(
|
(?x)^(
|
||||||
tests/python_build/.*|
|
tests/python_build/.*|
|
||||||
|
platform/android/java/editor/src/main/java/com/android/.*|
|
||||||
platform/android/java/lib/src/com/.*
|
platform/android/java/lib/src/com/.*
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ repos:
|
|||||||
exclude: |
|
exclude: |
|
||||||
(?x)^(
|
(?x)^(
|
||||||
tests/python_build/.*|
|
tests/python_build/.*|
|
||||||
|
platform/android/java/editor/src/main/java/com/android/.*|
|
||||||
platform/android/java/lib/src/com/.*
|
platform/android/java/lib/src/com/.*
|
||||||
)
|
)
|
||||||
additional_dependencies: [clang-tidy==18.1.1]
|
additional_dependencies: [clang-tidy==18.1.1]
|
||||||
@@ -54,6 +56,11 @@ repos:
|
|||||||
rev: v2.3.0
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
platform/android/java/editor/src/main/java/com/android/.*|
|
||||||
|
platform/android/java/lib/src/com/.*
|
||||||
|
)
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
|
|
||||||
### Requires Docker; look into alternative implementation.
|
### Requires Docker; look into alternative implementation.
|
||||||
@@ -135,6 +142,7 @@ repos:
|
|||||||
(?x)^(
|
(?x)^(
|
||||||
core/math/bvh_.*\.inc$|
|
core/math/bvh_.*\.inc$|
|
||||||
platform/(?!android|ios|linuxbsd|macos|web|windows)\w+/.*|
|
platform/(?!android|ios|linuxbsd|macos|web|windows)\w+/.*|
|
||||||
|
platform/android/java/editor/src/main/java/com/android/.*|
|
||||||
platform/android/java/lib/src/com/.*|
|
platform/android/java/lib/src/com/.*|
|
||||||
platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView\.java$|
|
platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView\.java$|
|
||||||
platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper\.java$|
|
platform/android/java/lib/src/org/godotengine/godot/gl/EGLLogWrapper\.java$|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ dependencies {
|
|||||||
implementation "androidx.window:window:1.3.0"
|
implementation "androidx.window:window:1.3.0"
|
||||||
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||||
|
implementation "org.bouncycastle:bcprov-jdk15to18:1.77"
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,550 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK signing logic which is independent of how input and output APKs are stored, parsed, and
|
||||||
|
* generated.
|
||||||
|
*
|
||||||
|
* <p><h3>Operating Model</h3>
|
||||||
|
*
|
||||||
|
* The abstract operating model is that there is an input APK which is being signed, thus producing
|
||||||
|
* an output APK. In reality, there may be just an output APK being built from scratch, or the input
|
||||||
|
* APK and the output APK may be the same file. Because this engine does not deal with reading and
|
||||||
|
* writing files, it can handle all of these scenarios.
|
||||||
|
*
|
||||||
|
* <p>The engine is stateful and thus cannot be used for signing multiple APKs. However, once
|
||||||
|
* the engine signed an APK, the engine can be used to re-sign the APK after it has been modified.
|
||||||
|
* This may be more efficient than signing the APK using a new instance of the engine. See
|
||||||
|
* <a href="#incremental">Incremental Operation</a>.
|
||||||
|
*
|
||||||
|
* <p>In the engine's operating model, a signed APK is produced as follows.
|
||||||
|
* <ol>
|
||||||
|
* <li>JAR entries to be signed are output,</li>
|
||||||
|
* <li>JAR archive is signed using JAR signing, thus adding the so-called v1 signature to the
|
||||||
|
* output,</li>
|
||||||
|
* <li>JAR archive is signed using APK Signature Scheme v2, thus adding the so-called v2 signature
|
||||||
|
* to the output.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>The input APK may contain JAR entries which, depending on the engine's configuration, may or
|
||||||
|
* may not be output (e.g., existing signatures may need to be preserved or stripped) or which the
|
||||||
|
* engine will overwrite as part of signing. The engine thus offers {@link #inputJarEntry(String)}
|
||||||
|
* which tells the client whether the input JAR entry needs to be output. This avoids the need for
|
||||||
|
* the client to hard-code the aspects of APK signing which determine which parts of input must be
|
||||||
|
* ignored. Similarly, the engine offers {@link #inputApkSigningBlock(DataSource)} to help the
|
||||||
|
* client avoid dealing with preserving or stripping APK Signature Scheme v2 signature of the input
|
||||||
|
* APK.
|
||||||
|
*
|
||||||
|
* <p>To use the engine to sign an input APK (or a collection of JAR entries), follow these
|
||||||
|
* steps:
|
||||||
|
* <ol>
|
||||||
|
* <li>Obtain a new instance of the engine -- engine instances are stateful and thus cannot be used
|
||||||
|
* for signing multiple APKs.</li>
|
||||||
|
* <li>Locate the input APK's APK Signing Block and provide it to
|
||||||
|
* {@link #inputApkSigningBlock(DataSource)}.</li>
|
||||||
|
* <li>For each JAR entry in the input APK, invoke {@link #inputJarEntry(String)} to determine
|
||||||
|
* whether this entry should be output. The engine may request to inspect the entry.</li>
|
||||||
|
* <li>For each output JAR entry, invoke {@link #outputJarEntry(String)} which may request to
|
||||||
|
* inspect the entry.</li>
|
||||||
|
* <li>Once all JAR entries have been output, invoke {@link #outputJarEntries()} which may request
|
||||||
|
* that additional JAR entries are output. These entries comprise the output APK's JAR
|
||||||
|
* signature.</li>
|
||||||
|
* <li>Locate the ZIP Central Directory and ZIP End of Central Directory sections in the output and
|
||||||
|
* invoke {@link #outputZipSections2(DataSource, DataSource, DataSource)} which may request that
|
||||||
|
* an APK Signature Block is inserted before the ZIP Central Directory. The block contains the
|
||||||
|
* output APK's APK Signature Scheme v2 signature.</li>
|
||||||
|
* <li>Invoke {@link #outputDone()} to signal that the APK was output in full. The engine will
|
||||||
|
* confirm that the output APK is signed.</li>
|
||||||
|
* <li>Invoke {@link #close()} to signal that the engine will no longer be used. This lets the
|
||||||
|
* engine free any resources it no longer needs.
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>Some invocations of the engine may provide the client with a task to perform. The client is
|
||||||
|
* expected to perform all requested tasks before proceeding to the next stage of signing. See
|
||||||
|
* documentation of each method about the deadlines for performing the tasks requested by the
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* <p><h3 id="incremental">Incremental Operation</h3></a>
|
||||||
|
*
|
||||||
|
* The engine supports incremental operation where a signed APK is produced, then modified and
|
||||||
|
* re-signed. This may be useful for IDEs, where an app is frequently re-signed after small changes
|
||||||
|
* by the developer. Re-signing may be more efficient than signing from scratch.
|
||||||
|
*
|
||||||
|
* <p>To use the engine in incremental mode, keep notifying the engine of changes to the APK through
|
||||||
|
* {@link #inputApkSigningBlock(DataSource)}, {@link #inputJarEntry(String)},
|
||||||
|
* {@link #inputJarEntryRemoved(String)}, {@link #outputJarEntry(String)},
|
||||||
|
* and {@link #outputJarEntryRemoved(String)}, perform the tasks requested by the engine through
|
||||||
|
* these methods, and, when a new signed APK is desired, run through steps 5 onwards to re-sign the
|
||||||
|
* APK.
|
||||||
|
*
|
||||||
|
* <p><h3>Output-only Operation</h3>
|
||||||
|
*
|
||||||
|
* The engine's abstract operating model consists of an input APK and an output APK. However, it is
|
||||||
|
* possible to use the engine in output-only mode where the engine's {@code input...} methods are
|
||||||
|
* not invoked. In this mode, the engine has less control over output because it cannot request that
|
||||||
|
* some JAR entries are not output. Nevertheless, the engine will attempt to make the output APK
|
||||||
|
* signed and will report an error if cannot do so.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
||||||
|
*/
|
||||||
|
public interface ApkSignerEngine extends Closeable {
|
||||||
|
|
||||||
|
default void setExecutor(RunnablesExecutor executor) {
|
||||||
|
throw new UnsupportedOperationException("setExecutor method is not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the signer engine with the data already present in the apk (if any). There
|
||||||
|
* might already be data that can be reused if the entries has not been changed.
|
||||||
|
*
|
||||||
|
* @param manifestBytes
|
||||||
|
* @param entryNames
|
||||||
|
* @return set of entry names which were processed by the engine during the initialization, a
|
||||||
|
* subset of entryNames
|
||||||
|
*/
|
||||||
|
default Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) {
|
||||||
|
throw new UnsupportedOperationException("initWith method is not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the input APK contains the provided APK Signing Block. The
|
||||||
|
* block may contain signatures of the input APK, such as APK Signature Scheme v2 signatures.
|
||||||
|
*
|
||||||
|
* @param apkSigningBlock APK signing block of the input APK. The provided data source is
|
||||||
|
* guaranteed to not be used by the engine after this method terminates.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs while reading the APK Signing Block
|
||||||
|
* @throws ApkFormatException if the APK Signing Block is malformed
|
||||||
|
* @throws IllegalStateException if this engine is closed
|
||||||
|
*/
|
||||||
|
void inputApkSigningBlock(DataSource apkSigningBlock)
|
||||||
|
throws IOException, ApkFormatException, IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the specified JAR entry was encountered in the input APK.
|
||||||
|
*
|
||||||
|
* <p>When an input entry is updated/changed, it's OK to not invoke
|
||||||
|
* {@link #inputJarEntryRemoved(String)} before invoking this method.
|
||||||
|
*
|
||||||
|
* @return instructions about how to proceed with this entry
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if this engine is closed
|
||||||
|
*/
|
||||||
|
InputJarEntryInstructions inputJarEntry(String entryName) throws IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the specified JAR entry was output.
|
||||||
|
*
|
||||||
|
* <p>It is unnecessary to invoke this method for entries added to output by this engine (e.g.,
|
||||||
|
* requested by {@link #outputJarEntries()}) provided the entries were output with exactly the
|
||||||
|
* data requested by the engine.
|
||||||
|
*
|
||||||
|
* <p>When an already output entry is updated/changed, it's OK to not invoke
|
||||||
|
* {@link #outputJarEntryRemoved(String)} before invoking this method.
|
||||||
|
*
|
||||||
|
* @return request to inspect the entry or {@code null} if the engine does not need to inspect
|
||||||
|
* the entry. The request must be fulfilled before {@link #outputJarEntries()} is
|
||||||
|
* invoked.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if this engine is closed
|
||||||
|
*/
|
||||||
|
InspectJarEntryRequest outputJarEntry(String entryName) throws IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the specified JAR entry was removed from the input. It's safe
|
||||||
|
* to invoke this for entries for which {@link #inputJarEntry(String)} hasn't been invoked.
|
||||||
|
*
|
||||||
|
* @return output policy of this JAR entry. The policy indicates how this input entry affects
|
||||||
|
* the output APK. The client of this engine should use this information to determine
|
||||||
|
* how the removal of this input APK's JAR entry affects the output APK.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if this engine is closed
|
||||||
|
*/
|
||||||
|
InputJarEntryInstructions.OutputPolicy inputJarEntryRemoved(String entryName)
|
||||||
|
throws IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the specified JAR entry was removed from the output. It's safe
|
||||||
|
* to invoke this for entries for which {@link #outputJarEntry(String)} hasn't been invoked.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if this engine is closed
|
||||||
|
*/
|
||||||
|
void outputJarEntryRemoved(String entryName) throws IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that all JAR entries have been output.
|
||||||
|
*
|
||||||
|
* @return request to add JAR signature to the output or {@code null} if there is no need to add
|
||||||
|
* a JAR signature. The request will contain additional JAR entries to be output. The
|
||||||
|
* request must be fulfilled before
|
||||||
|
* {@link #outputZipSections2(DataSource, DataSource, DataSource)} is invoked.
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the APK is malformed in a way which is preventing this engine
|
||||||
|
* from producing a valid signature. For example, if the engine uses the provided
|
||||||
|
* {@code META-INF/MANIFEST.MF} as a template and the file is malformed.
|
||||||
|
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||||
|
* cryptographic algorithm implementation is missing
|
||||||
|
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||||
|
* not suitable for generating the signature
|
||||||
|
* @throws SignatureException if an error occurred while generating a signature
|
||||||
|
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||||
|
* entries, or if the engine is closed
|
||||||
|
*/
|
||||||
|
OutputJarSignatureRequest outputJarEntries()
|
||||||
|
throws ApkFormatException, NoSuchAlgorithmException, InvalidKeyException,
|
||||||
|
SignatureException, IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||||
|
*
|
||||||
|
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||||
|
* terminates.
|
||||||
|
*
|
||||||
|
* @deprecated This is now superseded by {@link #outputZipSections2(DataSource, DataSource,
|
||||||
|
* DataSource)}.
|
||||||
|
*
|
||||||
|
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||||
|
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||||
|
* archive and extends all the way to the ZIP Central Directory.
|
||||||
|
* @param zipCentralDirectory ZIP Central Directory section
|
||||||
|
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||||
|
*
|
||||||
|
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||||
|
* not contain an APK Signing Block. The request must be fulfilled before
|
||||||
|
* {@link #outputDone()} is invoked.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||||
|
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||||
|
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||||
|
* provided to the engine is malformed.
|
||||||
|
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||||
|
* cryptographic algorithm implementation is missing
|
||||||
|
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||||
|
* not suitable for generating the signature
|
||||||
|
* @throws SignatureException if an error occurred while generating a signature
|
||||||
|
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||||
|
* entries or to output JAR signature, or if the engine is closed
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
OutputApkSigningBlockRequest outputZipSections(
|
||||||
|
DataSource zipEntries,
|
||||||
|
DataSource zipCentralDirectory,
|
||||||
|
DataSource zipEocd)
|
||||||
|
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||||
|
InvalidKeyException, SignatureException, IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the ZIP sections comprising the output APK have been output.
|
||||||
|
*
|
||||||
|
* <p>The provided data sources are guaranteed to not be used by the engine after this method
|
||||||
|
* terminates.
|
||||||
|
*
|
||||||
|
* @param zipEntries the section of ZIP archive containing Local File Header records and data of
|
||||||
|
* the ZIP entries. In a well-formed archive, this section starts at the start of the
|
||||||
|
* archive and extends all the way to the ZIP Central Directory.
|
||||||
|
* @param zipCentralDirectory ZIP Central Directory section
|
||||||
|
* @param zipEocd ZIP End of Central Directory (EoCD) record
|
||||||
|
*
|
||||||
|
* @return request to add an APK Signing Block to the output or {@code null} if the output must
|
||||||
|
* not contain an APK Signing Block. The request must be fulfilled before
|
||||||
|
* {@link #outputDone()} is invoked.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs while reading the provided ZIP sections
|
||||||
|
* @throws ApkFormatException if the provided APK is malformed in a way which prevents this
|
||||||
|
* engine from producing a valid signature. For example, if the APK Signing Block
|
||||||
|
* provided to the engine is malformed.
|
||||||
|
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||||
|
* cryptographic algorithm implementation is missing
|
||||||
|
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||||
|
* not suitable for generating the signature
|
||||||
|
* @throws SignatureException if an error occurred while generating a signature
|
||||||
|
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||||
|
* entries or to output JAR signature, or if the engine is closed
|
||||||
|
*/
|
||||||
|
OutputApkSigningBlockRequest2 outputZipSections2(
|
||||||
|
DataSource zipEntries,
|
||||||
|
DataSource zipCentralDirectory,
|
||||||
|
DataSource zipEocd)
|
||||||
|
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||||
|
InvalidKeyException, SignatureException, IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that the signed APK was output.
|
||||||
|
*
|
||||||
|
* <p>This does not change the output APK. The method helps the client confirm that the current
|
||||||
|
* output is signed.
|
||||||
|
*
|
||||||
|
* @throws IllegalStateException if there are unfulfilled requests, such as to inspect some JAR
|
||||||
|
* entries or to output signatures, or if the engine is closed
|
||||||
|
*/
|
||||||
|
void outputDone() throws IllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a V4 signature proto and write to output file.
|
||||||
|
*
|
||||||
|
* @param data Input data to calculate a verity hash tree and hash root
|
||||||
|
* @param outputFile To store the serialized V4 Signature.
|
||||||
|
* @param ignoreFailures Whether any failures will be silently ignored.
|
||||||
|
* @throws InvalidKeyException if a signature could not be generated because a signing key is
|
||||||
|
* not suitable for generating the signature
|
||||||
|
* @throws NoSuchAlgorithmException if a signature could not be generated because a required
|
||||||
|
* cryptographic algorithm implementation is missing
|
||||||
|
* @throws SignatureException if an error occurred while generating a signature
|
||||||
|
* @throws IOException if protobuf fails to be serialized and written to file
|
||||||
|
*/
|
||||||
|
void signV4(DataSource data, File outputFile, boolean ignoreFailures)
|
||||||
|
throws InvalidKeyException, NoSuchAlgorithmException, SignatureException, IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the signing configuration provided to the engine is capable of creating a
|
||||||
|
* SourceStamp.
|
||||||
|
*/
|
||||||
|
default boolean isEligibleForSourceStamp() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generates the digest of the certificate used to sign the source stamp. */
|
||||||
|
default byte[] generateSourceStampCertificateDigest() throws SignatureException {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates to this engine that it will no longer be used. Invoking this on an already closed
|
||||||
|
* engine is OK.
|
||||||
|
*
|
||||||
|
* <p>This does not change the output APK. For example, if the output APK is not yet fully
|
||||||
|
* signed, it will remain so after this method terminates.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void close();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instructions about how to handle an input APK's JAR entry.
|
||||||
|
*
|
||||||
|
* <p>The instructions indicate whether to output the entry (see {@link #getOutputPolicy()}) and
|
||||||
|
* may contain a request to inspect the entry (see {@link #getInspectJarEntryRequest()}), in
|
||||||
|
* which case the request must be fulfilled before {@link ApkSignerEngine#outputJarEntries()} is
|
||||||
|
* invoked.
|
||||||
|
*/
|
||||||
|
public static class InputJarEntryInstructions {
|
||||||
|
private final OutputPolicy mOutputPolicy;
|
||||||
|
private final InspectJarEntryRequest mInspectJarEntryRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||||
|
* output policy and without a request to inspect the entry.
|
||||||
|
*/
|
||||||
|
public InputJarEntryInstructions(OutputPolicy outputPolicy) {
|
||||||
|
this(outputPolicy, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code InputJarEntryInstructions} instance with the provided entry
|
||||||
|
* output mode and with the provided request to inspect the entry.
|
||||||
|
*
|
||||||
|
* @param inspectJarEntryRequest request to inspect the entry or {@code null} if there's no
|
||||||
|
* need to inspect the entry.
|
||||||
|
*/
|
||||||
|
public InputJarEntryInstructions(
|
||||||
|
OutputPolicy outputPolicy,
|
||||||
|
InspectJarEntryRequest inspectJarEntryRequest) {
|
||||||
|
mOutputPolicy = outputPolicy;
|
||||||
|
mInspectJarEntryRequest = inspectJarEntryRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the output policy for this entry.
|
||||||
|
*/
|
||||||
|
public OutputPolicy getOutputPolicy() {
|
||||||
|
return mOutputPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request to inspect the JAR entry or {@code null} if there is no need to
|
||||||
|
* inspect the entry.
|
||||||
|
*/
|
||||||
|
public InspectJarEntryRequest getInspectJarEntryRequest() {
|
||||||
|
return mInspectJarEntryRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output policy for an input APK's JAR entry.
|
||||||
|
*/
|
||||||
|
public static enum OutputPolicy {
|
||||||
|
/** Entry must not be output. */
|
||||||
|
SKIP,
|
||||||
|
|
||||||
|
/** Entry should be output. */
|
||||||
|
OUTPUT,
|
||||||
|
|
||||||
|
/** Entry will be output by the engine. The client can thus ignore this input entry. */
|
||||||
|
OUTPUT_BY_ENGINE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to inspect the specified JAR entry.
|
||||||
|
*
|
||||||
|
* <p>The entry's uncompressed data must be provided to the data sink returned by
|
||||||
|
* {@link #getDataSink()}. Once the entry's data has been provided to the sink, {@link #done()}
|
||||||
|
* must be invoked.
|
||||||
|
*/
|
||||||
|
interface InspectJarEntryRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data sink into which the entry's uncompressed data should be sent.
|
||||||
|
*/
|
||||||
|
DataSink getDataSink();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that entry's data has been provided in full.
|
||||||
|
*/
|
||||||
|
void done();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the JAR entry.
|
||||||
|
*/
|
||||||
|
String getEntryName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to add JAR signature (aka v1 signature) to the output APK.
|
||||||
|
*
|
||||||
|
* <p>Entries listed in {@link #getAdditionalJarEntries()} must be added to the output APK after
|
||||||
|
* which {@link #done()} must be invoked.
|
||||||
|
*/
|
||||||
|
interface OutputJarSignatureRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns JAR entries that must be added to the output APK.
|
||||||
|
*/
|
||||||
|
List<JarEntry> getAdditionalJarEntries();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the JAR entries contained in this request were added to the output APK.
|
||||||
|
*/
|
||||||
|
void done();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR entry.
|
||||||
|
*/
|
||||||
|
public static class JarEntry {
|
||||||
|
private final String mName;
|
||||||
|
private final byte[] mData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code JarEntry} with the provided name and data.
|
||||||
|
*
|
||||||
|
* @param data uncompressed data of the entry. Changes to this array will not be
|
||||||
|
* reflected in {@link #getData()}.
|
||||||
|
*/
|
||||||
|
public JarEntry(String name, byte[] data) {
|
||||||
|
mName = name;
|
||||||
|
mData = data.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of this ZIP entry.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the uncompressed data of this JAR entry.
|
||||||
|
*/
|
||||||
|
public byte[] getData() {
|
||||||
|
return mData.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||||
|
* signature(s) of the APK are contained in this block.
|
||||||
|
*
|
||||||
|
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||||
|
* output APK such that the block is immediately before the ZIP Central Directory, the offset of
|
||||||
|
* ZIP Central Directory in the ZIP End of Central Directory record must be adjusted
|
||||||
|
* accordingly, and then {@link #done()} must be invoked.
|
||||||
|
*
|
||||||
|
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||||
|
* contained in this request.
|
||||||
|
*
|
||||||
|
* @deprecated This is now superseded by {@link OutputApkSigningBlockRequest2}.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
interface OutputApkSigningBlockRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the APK Signing Block.
|
||||||
|
*/
|
||||||
|
byte[] getApkSigningBlock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the APK Signing Block was output as requested.
|
||||||
|
*/
|
||||||
|
void done();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to add the specified APK Signing Block to the output APK. APK Signature Scheme v2
|
||||||
|
* signature(s) of the APK are contained in this block.
|
||||||
|
*
|
||||||
|
* <p>The APK Signing Block returned by {@link #getApkSigningBlock()} must be placed into the
|
||||||
|
* output APK such that the block is immediately before the ZIP Central Directory. Immediately
|
||||||
|
* before the APK Signing Block must be padding consists of the number of 0x00 bytes returned by
|
||||||
|
* {@link #getPaddingSizeBeforeApkSigningBlock()}. The offset of ZIP Central Directory in the
|
||||||
|
* ZIP End of Central Directory record must be adjusted accordingly, and then {@link #done()}
|
||||||
|
* must be invoked.
|
||||||
|
*
|
||||||
|
* <p>If the output contains an APK Signing Block, that block must be replaced by the block
|
||||||
|
* contained in this request.
|
||||||
|
*/
|
||||||
|
interface OutputApkSigningBlockRequest2 {
|
||||||
|
/**
|
||||||
|
* Returns the APK Signing Block.
|
||||||
|
*/
|
||||||
|
byte[] getApkSigningBlock();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the APK Signing Block was output as requested.
|
||||||
|
*/
|
||||||
|
void done();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of 0x00 bytes the caller must place immediately before APK Signing
|
||||||
|
* Block.
|
||||||
|
*/
|
||||||
|
int getPaddingSizeBeforeApkSigningBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is intended as a lightweight representation of an APK signature verification issue
|
||||||
|
* where the client does not require the additional textual details provided by a subclass.
|
||||||
|
*/
|
||||||
|
public class ApkVerificationIssue {
|
||||||
|
/* The V2 signer(s) could not be read from the V2 signature block */
|
||||||
|
public static final int V2_SIG_MALFORMED_SIGNERS = 1;
|
||||||
|
/* A V2 signature block exists without any V2 signers */
|
||||||
|
public static final int V2_SIG_NO_SIGNERS = 2;
|
||||||
|
/* Failed to parse a signer's block in the V2 signature block */
|
||||||
|
public static final int V2_SIG_MALFORMED_SIGNER = 3;
|
||||||
|
/* Failed to parse the signer's signature record in the V2 signature block */
|
||||||
|
public static final int V2_SIG_MALFORMED_SIGNATURE = 4;
|
||||||
|
/* The V2 signer contained no signatures */
|
||||||
|
public static final int V2_SIG_NO_SIGNATURES = 5;
|
||||||
|
/* The V2 signer's certificate could not be parsed */
|
||||||
|
public static final int V2_SIG_MALFORMED_CERTIFICATE = 6;
|
||||||
|
/* No signing certificates exist for the V2 signer */
|
||||||
|
public static final int V2_SIG_NO_CERTIFICATES = 7;
|
||||||
|
/* Failed to parse the V2 signer's digest record */
|
||||||
|
public static final int V2_SIG_MALFORMED_DIGEST = 8;
|
||||||
|
/* The V3 signer(s) could not be read from the V3 signature block */
|
||||||
|
public static final int V3_SIG_MALFORMED_SIGNERS = 9;
|
||||||
|
/* A V3 signature block exists without any V3 signers */
|
||||||
|
public static final int V3_SIG_NO_SIGNERS = 10;
|
||||||
|
/* Failed to parse a signer's block in the V3 signature block */
|
||||||
|
public static final int V3_SIG_MALFORMED_SIGNER = 11;
|
||||||
|
/* Failed to parse the signer's signature record in the V3 signature block */
|
||||||
|
public static final int V3_SIG_MALFORMED_SIGNATURE = 12;
|
||||||
|
/* The V3 signer contained no signatures */
|
||||||
|
public static final int V3_SIG_NO_SIGNATURES = 13;
|
||||||
|
/* The V3 signer's certificate could not be parsed */
|
||||||
|
public static final int V3_SIG_MALFORMED_CERTIFICATE = 14;
|
||||||
|
/* No signing certificates exist for the V3 signer */
|
||||||
|
public static final int V3_SIG_NO_CERTIFICATES = 15;
|
||||||
|
/* Failed to parse the V3 signer's digest record */
|
||||||
|
public static final int V3_SIG_MALFORMED_DIGEST = 16;
|
||||||
|
/* The source stamp signer contained no signatures */
|
||||||
|
public static final int SOURCE_STAMP_NO_SIGNATURE = 17;
|
||||||
|
/* The source stamp signer's certificate could not be parsed */
|
||||||
|
public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18;
|
||||||
|
/* The source stamp contains a signature produced using an unknown algorithm */
|
||||||
|
public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19;
|
||||||
|
/* Failed to parse the signer's signature in the source stamp signature block */
|
||||||
|
public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20;
|
||||||
|
/* The source stamp's signature block failed verification */
|
||||||
|
public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21;
|
||||||
|
/* An exception was encountered when verifying the source stamp */
|
||||||
|
public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22;
|
||||||
|
/* The certificate digest in the APK does not match the expected digest */
|
||||||
|
public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23;
|
||||||
|
/*
|
||||||
|
* The APK contains a source stamp signature block without a corresponding stamp certificate
|
||||||
|
* digest in the APK contents.
|
||||||
|
*/
|
||||||
|
public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24;
|
||||||
|
/*
|
||||||
|
* The APK does not contain the source stamp certificate digest file nor the source stamp
|
||||||
|
* signature block.
|
||||||
|
*/
|
||||||
|
public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25;
|
||||||
|
/*
|
||||||
|
* None of the signatures provided by the source stamp were produced with a known signature
|
||||||
|
* algorithm.
|
||||||
|
*/
|
||||||
|
public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26;
|
||||||
|
/*
|
||||||
|
* The source stamp signer's certificate in the signing block does not match the certificate in
|
||||||
|
* the APK.
|
||||||
|
*/
|
||||||
|
public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27;
|
||||||
|
/* The APK could not be properly parsed due to a ZIP or APK format exception */
|
||||||
|
public static final int MALFORMED_APK = 28;
|
||||||
|
/* An unexpected exception was caught when attempting to verify the APK's signatures */
|
||||||
|
public static final int UNEXPECTED_EXCEPTION = 29;
|
||||||
|
/* The APK contains the certificate digest file but does not contain a stamp signature block */
|
||||||
|
public static final int SOURCE_STAMP_SIG_MISSING = 30;
|
||||||
|
/* Source stamp block contains a malformed attribute. */
|
||||||
|
public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31;
|
||||||
|
/* Source stamp block contains an unknown attribute. */
|
||||||
|
public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32;
|
||||||
|
/**
|
||||||
|
* Failed to parse the SigningCertificateLineage structure in the source stamp
|
||||||
|
* attributes section.
|
||||||
|
*/
|
||||||
|
public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33;
|
||||||
|
/**
|
||||||
|
* The source stamp certificate does not match the terminal node in the provided
|
||||||
|
* proof-of-rotation structure describing the stamp certificate history.
|
||||||
|
*/
|
||||||
|
public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34;
|
||||||
|
/**
|
||||||
|
* The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record
|
||||||
|
* with signature(s) that did not verify.
|
||||||
|
*/
|
||||||
|
public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
|
||||||
|
/** No V1 / jar signing signature blocks were found in the APK. */
|
||||||
|
public static final int JAR_SIG_NO_SIGNATURES = 36;
|
||||||
|
/** An exception was encountered when parsing the V1 / jar signer in the signature block. */
|
||||||
|
public static final int JAR_SIG_PARSE_EXCEPTION = 37;
|
||||||
|
/** The source stamp timestamp attribute has an invalid value. */
|
||||||
|
public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38;
|
||||||
|
|
||||||
|
private final int mIssueId;
|
||||||
|
private final String mFormat;
|
||||||
|
private final Object[] mParams;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and
|
||||||
|
* {@code params}.
|
||||||
|
*/
|
||||||
|
public ApkVerificationIssue(String format, Object... params) {
|
||||||
|
mIssueId = -1;
|
||||||
|
mFormat = format;
|
||||||
|
mParams = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code
|
||||||
|
* params}.
|
||||||
|
*/
|
||||||
|
public ApkVerificationIssue(int issueId, Object... params) {
|
||||||
|
mIssueId = issueId;
|
||||||
|
mFormat = null;
|
||||||
|
mParams = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the numeric ID for this issue.
|
||||||
|
*/
|
||||||
|
public int getIssueId() {
|
||||||
|
return mIssueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the optional parameters for this issue.
|
||||||
|
*/
|
||||||
|
public Object[] getParams() {
|
||||||
|
return mParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// If this instance was created by a subclass with a format string then return the same
|
||||||
|
// formatted String as the subclass.
|
||||||
|
if (mFormat != null) {
|
||||||
|
return String.format(mFormat, mParams);
|
||||||
|
}
|
||||||
|
StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId);
|
||||||
|
for (Object param : mParams) {
|
||||||
|
result.append(", ").append(param.toString());
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||||
|
import com.android.apksig.internal.apk.v1.V1SchemeConstants;
|
||||||
|
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||||
|
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports internally defined constants to allow clients to reference these values without relying
|
||||||
|
* on internal code.
|
||||||
|
*/
|
||||||
|
public class Constants {
|
||||||
|
private Constants() {}
|
||||||
|
|
||||||
|
public static final int VERSION_SOURCE_STAMP = 0;
|
||||||
|
public static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
|
||||||
|
public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
|
||||||
|
public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3;
|
||||||
|
public static final int VERSION_APK_SIGNATURE_SCHEME_V31 = 31;
|
||||||
|
public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of signers supported by the v1 and v2 APK Signature Schemes.
|
||||||
|
*/
|
||||||
|
public static final int MAX_APK_SIGNERS = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default page alignment for native library files in bytes.
|
||||||
|
*/
|
||||||
|
public static final short LIBRARY_PAGE_ALIGNMENT_BYTES = 16384;
|
||||||
|
|
||||||
|
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||||
|
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
|
||||||
|
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||||
|
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
|
||||||
|
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID =
|
||||||
|
V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||||
|
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
|
||||||
|
|
||||||
|
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||||
|
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||||
|
public static final int V2_SOURCE_STAMP_BLOCK_ID =
|
||||||
|
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||||
|
|
||||||
|
public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1";
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public final class Hints {
|
||||||
|
/**
|
||||||
|
* Name of hint pattern asset file in APK.
|
||||||
|
*/
|
||||||
|
public static final String PIN_HINT_ASSET_ZIP_ENTRY_NAME = "assets/com.android.hints.pins.txt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of hint byte range data file in APK. Keep in sync with PinnerService.java.
|
||||||
|
*/
|
||||||
|
public static final String PIN_BYTE_RANGE_ZIP_ENTRY_NAME = "pinlist.meta";
|
||||||
|
|
||||||
|
private static int clampToInt(long value) {
|
||||||
|
return (int) Math.max(0, Math.min(value, Integer.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ByteRange {
|
||||||
|
final long start;
|
||||||
|
final long end;
|
||||||
|
|
||||||
|
public ByteRange(long start, long end) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class PatternWithRange {
|
||||||
|
final Pattern pattern;
|
||||||
|
final long offset;
|
||||||
|
final long size;
|
||||||
|
|
||||||
|
public PatternWithRange(String pattern) {
|
||||||
|
this.pattern = Pattern.compile(pattern);
|
||||||
|
this.offset= 0;
|
||||||
|
this.size = Long.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PatternWithRange(String pattern, long offset, long size) {
|
||||||
|
this.pattern = Pattern.compile(pattern);
|
||||||
|
this.offset = offset;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Matcher matcher(CharSequence input) {
|
||||||
|
return this.pattern.matcher(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteRange ClampToAbsoluteByteRange(ByteRange rangeIn) {
|
||||||
|
if (rangeIn.end - rangeIn.start < this.offset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long rangeOutStart = rangeIn.start + this.offset;
|
||||||
|
long rangeOutSize = Math.min(rangeIn.end - rangeOutStart,
|
||||||
|
this.size);
|
||||||
|
return new ByteRange(rangeOutStart,
|
||||||
|
rangeOutStart + rangeOutSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a blob of bytes that PinnerService understands as a
|
||||||
|
* sequence of byte ranges to pin.
|
||||||
|
*/
|
||||||
|
public static byte[] encodeByteRangeList(List<ByteRange> pinByteRanges) {
|
||||||
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(pinByteRanges.size() * 8);
|
||||||
|
DataOutputStream out = new DataOutputStream(bos);
|
||||||
|
try {
|
||||||
|
for (ByteRange pinByteRange : pinByteRanges) {
|
||||||
|
out.writeInt(clampToInt(pinByteRange.start));
|
||||||
|
out.writeInt(clampToInt(pinByteRange.end - pinByteRange.start));
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AssertionError("impossible", ex);
|
||||||
|
}
|
||||||
|
return bos.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ArrayList<PatternWithRange> parsePinPatterns(byte[] patternBlob) {
|
||||||
|
ArrayList<PatternWithRange> pinPatterns = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (String rawLine : new String(patternBlob, "UTF-8").split("\n")) {
|
||||||
|
String line = rawLine.replaceFirst("#.*", ""); // # starts a comment
|
||||||
|
String[] fields = line.split(" ");
|
||||||
|
if (fields.length == 1) {
|
||||||
|
pinPatterns.add(new PatternWithRange(fields[0]));
|
||||||
|
} else if (fields.length == 3) {
|
||||||
|
long start = Long.parseLong(fields[1]);
|
||||||
|
long end = Long.parseLong(fields[2]);
|
||||||
|
pinPatterns.add(new PatternWithRange(fields[0], start, end - start));
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("bad pin pattern line " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (UnsupportedEncodingException ex) {
|
||||||
|
throw new RuntimeException("UTF-8 must be supported", ex);
|
||||||
|
}
|
||||||
|
return pinPatterns;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# apksig ([commit ac5cbb07d87cc342fcf07715857a812305d69888](https://android.googlesource.com/platform/tools/apksig/+/ac5cbb07d87cc342fcf07715857a812305d69888))
|
||||||
|
|
||||||
|
apksig is a project which aims to simplify APK signing and checking whether APK signatures are
|
||||||
|
expected to verify on Android. apksig supports
|
||||||
|
[JAR signing](https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File)
|
||||||
|
(used by Android since day one) and
|
||||||
|
[APK Signature Scheme v2](https://source.android.com/security/apksigning/v2.html) (supported since
|
||||||
|
Android Nougat, API Level 24). apksig is meant to be used outside of Android devices.
|
||||||
|
|
||||||
|
The key feature of apksig is that it knows about differences in APK signature verification logic
|
||||||
|
between different versions of the Android platform. apksig thus thoroughly checks whether an APK's
|
||||||
|
signature is expected to verify on all Android platform versions supported by the APK. When signing
|
||||||
|
an APK, apksig chooses the most appropriate cryptographic algorithms based on the Android platform
|
||||||
|
versions supported by the APK being signed.
|
||||||
|
|
||||||
|
## apksig library
|
||||||
|
|
||||||
|
apksig library offers three primitives:
|
||||||
|
|
||||||
|
* `ApkSigner` which signs the provided APK so that it verifies on all Android platform versions
|
||||||
|
supported by the APK. The range of platform versions can be customized.
|
||||||
|
* `ApkVerifier` which checks whether the provided APK is expected to verify on all Android
|
||||||
|
platform versions supported by the APK. The range of platform versions can be customized.
|
||||||
|
* `(Default)ApkSignerEngine` which abstracts away signing APKs from parsing and building APKs.
|
||||||
|
This is useful in optimized APK building pipelines, such as in Android Plugin for Gradle,
|
||||||
|
which need to perform signing while building an APK, instead of after. For simpler use cases
|
||||||
|
where the APK to be signed is available upfront, the `ApkSigner` above is easier to use.
|
||||||
|
|
||||||
|
_NOTE: Some public classes of the library are in packages having the word "internal" in their name.
|
||||||
|
These are not public API of the library. Do not use \*.internal.\* classes directly because these
|
||||||
|
classes may change any time without regard to existing clients outside of `apksig` and `apksigner`._
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,911 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig;
|
||||||
|
|
||||||
|
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||||
|
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||||
|
import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
|
||||||
|
import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
|
||||||
|
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
||||||
|
import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.apk.ApkUtilsLite;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigResult;
|
||||||
|
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
||||||
|
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||||
|
import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier;
|
||||||
|
import com.android.apksig.internal.apk.v2.V2SchemeConstants;
|
||||||
|
import com.android.apksig.internal.apk.v3.V3SchemeConstants;
|
||||||
|
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||||
|
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||||
|
import com.android.apksig.internal.zip.ZipUtils;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.DataSources;
|
||||||
|
import com.android.apksig.zip.ZipFormatException;
|
||||||
|
import com.android.apksig.zip.ZipSections;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK source stamp verifier intended only to verify the validity of the stamp signature.
|
||||||
|
*
|
||||||
|
* <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks
|
||||||
|
* when obtaining the digests for verification. This verifier should only be used in cases where
|
||||||
|
* another mechanism has already been used to verify the APK signatures.
|
||||||
|
*/
|
||||||
|
public class SourceStampVerifier {
|
||||||
|
private final File mApkFile;
|
||||||
|
private final DataSource mApkDataSource;
|
||||||
|
|
||||||
|
private final int mMinSdkVersion;
|
||||||
|
private final int mMaxSdkVersion;
|
||||||
|
|
||||||
|
private SourceStampVerifier(
|
||||||
|
File apkFile,
|
||||||
|
DataSource apkDataSource,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion) {
|
||||||
|
mApkFile = apkFile;
|
||||||
|
mApkDataSource = apkDataSource;
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
mMaxSdkVersion = maxSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the APK's source stamp signature and returns the result of the verification.
|
||||||
|
*
|
||||||
|
* <p>The APK's source stamp can be considered verified if the result's {@link
|
||||||
|
* Result#isVerified()} returns {@code true}. If source stamp verification fails all of the
|
||||||
|
* resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors
|
||||||
|
* can be obtained as follows:
|
||||||
|
* <ul>
|
||||||
|
* <li>Obtain the generic errors via {@link Result#getErrors()}
|
||||||
|
* <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer
|
||||||
|
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||||
|
* <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer
|
||||||
|
* query for any errors with {@link Result.SignerInfo#getErrors()}
|
||||||
|
* <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query
|
||||||
|
* for any stamp errors with {@link Result.SourceStampInfo#getErrors()}
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public SourceStampVerifier.Result verifySourceStamp() {
|
||||||
|
return verifySourceStamp(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the APK's source stamp signature, including verification that the SHA-256 digest of
|
||||||
|
* the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result
|
||||||
|
* of the verification.
|
||||||
|
*
|
||||||
|
* <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp,
|
||||||
|
* if present, without verifying the actual source stamp certificate used to sign the source
|
||||||
|
* stamp. This can be used to verify an APK contains a properly signed source stamp without
|
||||||
|
* verifying a particular signer.
|
||||||
|
*
|
||||||
|
* @see #verifySourceStamp()
|
||||||
|
*/
|
||||||
|
public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) {
|
||||||
|
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 verifySourceStamp(apk, expectedCertDigest);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Result result = new Result();
|
||||||
|
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
if (in != null) {
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided {@code apk}'s source stamp signature, including verification of the
|
||||||
|
* SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and
|
||||||
|
* returns the result of the verification.
|
||||||
|
*
|
||||||
|
* @see #verifySourceStamp(String)
|
||||||
|
*/
|
||||||
|
private SourceStampVerifier.Result verifySourceStamp(DataSource apk,
|
||||||
|
String expectedCertDigest) {
|
||||||
|
Result result = new Result();
|
||||||
|
try {
|
||||||
|
ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
||||||
|
// Attempt to obtain the source stamp's certificate digest from the APK.
|
||||||
|
List<CentralDirectoryRecord> cdRecords =
|
||||||
|
ZipUtils.parseZipCentralDirectory(apk, zipSections);
|
||||||
|
CentralDirectoryRecord sourceStampCdRecord = null;
|
||||||
|
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||||
|
if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
||||||
|
sourceStampCdRecord = cdRecord;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the source stamp's certificate digest is not available within the APK then the
|
||||||
|
// source stamp cannot be verified; check if a source stamp signing block is in the
|
||||||
|
// APK's signature block to determine the appropriate status to return.
|
||||||
|
if (sourceStampCdRecord == null) {
|
||||||
|
boolean stampSigningBlockFound;
|
||||||
|
try {
|
||||||
|
ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||||
|
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
||||||
|
stampSigningBlockFound = true;
|
||||||
|
} catch (SignatureNotFoundException e) {
|
||||||
|
stampSigningBlockFound = false;
|
||||||
|
}
|
||||||
|
result.addVerificationError(stampSigningBlockFound
|
||||||
|
? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST
|
||||||
|
: ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the contents of the source stamp certificate digest match the expected
|
||||||
|
// value, if provided.
|
||||||
|
byte[] sourceStampCertificateDigest =
|
||||||
|
LocalFileRecord.getUncompressedData(
|
||||||
|
apk,
|
||||||
|
sourceStampCdRecord,
|
||||||
|
zipSections.getZipCentralDirectoryOffset());
|
||||||
|
if (expectedCertDigest != null) {
|
||||||
|
String actualCertDigest = ApkSigningBlockUtilsLite.toHex(
|
||||||
|
sourceStampCertificateDigest);
|
||||||
|
if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) {
|
||||||
|
result.addVerificationError(
|
||||||
|
ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH,
|
||||||
|
actualCertDigest, expectedCertDigest);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests =
|
||||||
|
new HashMap<>();
|
||||||
|
if (mMaxSdkVersion >= AndroidSdkVersion.P) {
|
||||||
|
SignatureInfo signatureInfo;
|
||||||
|
try {
|
||||||
|
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||||
|
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
|
||||||
|
} catch (SignatureNotFoundException e) {
|
||||||
|
signatureInfo = null;
|
||||||
|
}
|
||||||
|
if (signatureInfo != null) {
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||||
|
ContentDigestAlgorithm.class);
|
||||||
|
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3,
|
||||||
|
apkContentDigests, result);
|
||||||
|
signatureSchemeApkContentDigests.put(
|
||||||
|
VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P ||
|
||||||
|
signatureSchemeApkContentDigests.isEmpty())) {
|
||||||
|
SignatureInfo signatureInfo;
|
||||||
|
try {
|
||||||
|
signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections,
|
||||||
|
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||||
|
} catch (SignatureNotFoundException e) {
|
||||||
|
signatureInfo = null;
|
||||||
|
}
|
||||||
|
if (signatureInfo != null) {
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>(
|
||||||
|
ContentDigestAlgorithm.class);
|
||||||
|
parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2,
|
||||||
|
apkContentDigests, result);
|
||||||
|
signatureSchemeApkContentDigests.put(
|
||||||
|
VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mMinSdkVersion < AndroidSdkVersion.N
|
||||||
|
|| signatureSchemeApkContentDigests.isEmpty()) {
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
|
||||||
|
getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
|
||||||
|
signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
|
||||||
|
apkContentDigests);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApkSigResult sourceStampResult =
|
||||||
|
V2SourceStampVerifier.verify(
|
||||||
|
apk,
|
||||||
|
zipSections,
|
||||||
|
sourceStampCertificateDigest,
|
||||||
|
signatureSchemeApkContentDigests,
|
||||||
|
mMinSdkVersion,
|
||||||
|
mMaxSdkVersion);
|
||||||
|
result.mergeFrom(sourceStampResult);
|
||||||
|
return result;
|
||||||
|
} catch (ApkFormatException | IOException | ZipFormatException e) {
|
||||||
|
result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e);
|
||||||
|
} catch (SignatureNotFoundException e) {
|
||||||
|
result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses each signer in the provided APK V2 / V3 signature block and populates corresponding
|
||||||
|
* {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
public static void parseSigners(
|
||||||
|
ByteBuffer apkSignatureSchemeBlock,
|
||||||
|
int apkSigSchemeVersion,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||||
|
Result result) {
|
||||||
|
boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||||
|
// Both the V2 and V3 signature blocks contain the following:
|
||||||
|
// * length-prefixed sequence of length-prefixed signers
|
||||||
|
ByteBuffer signers;
|
||||||
|
try {
|
||||||
|
signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
|
||||||
|
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!signers.hasRemaining()) {
|
||||||
|
result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
|
||||||
|
: ApkVerificationIssue.V3_SIG_NO_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateFactory certFactory;
|
||||||
|
try {
|
||||||
|
certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
while (signers.hasRemaining()) {
|
||||||
|
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||||
|
if (isV2Block) {
|
||||||
|
result.addV2Signer(signerInfo);
|
||||||
|
} else {
|
||||||
|
result.addV3Signer(signerInfo);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers);
|
||||||
|
parseSigner(
|
||||||
|
signer,
|
||||||
|
apkSigSchemeVersion,
|
||||||
|
certFactory,
|
||||||
|
apkContentDigests,
|
||||||
|
signerInfo);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addVerificationWarning(
|
||||||
|
isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
|
||||||
|
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided signer block and populates the {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||||
|
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
||||||
|
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
||||||
|
* integrity of the APK.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
private static void parseSigner(
|
||||||
|
ByteBuffer signerBlock,
|
||||||
|
int apkSigSchemeVersion,
|
||||||
|
CertificateFactory certFactory,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||||
|
Result.SignerInfo signerInfo)
|
||||||
|
throws ApkFormatException {
|
||||||
|
boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||||
|
// Both the V2 and V3 signer blocks contain the following:
|
||||||
|
// * length-prefixed signed data
|
||||||
|
// * length-prefixed sequence of length-prefixed digests:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: digest of contents
|
||||||
|
// * length-prefixed sequence of certificates:
|
||||||
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||||
|
ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock);
|
||||||
|
ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||||
|
ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData);
|
||||||
|
|
||||||
|
// Parse the digests block
|
||||||
|
while (digests.hasRemaining()) {
|
||||||
|
try {
|
||||||
|
ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests);
|
||||||
|
int sigAlgorithmId = digest.getInt();
|
||||||
|
byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest);
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||||
|
if (signatureAlgorithm == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addVerificationWarning(
|
||||||
|
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
|
||||||
|
: ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the certificates block
|
||||||
|
if (certificates.hasRemaining()) {
|
||||||
|
byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates);
|
||||||
|
X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(encodedCert));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
signerInfo.addVerificationWarning(
|
||||||
|
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
|
||||||
|
: ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||||
|
// form. Without this, getEncoded may return a different form from what was stored in
|
||||||
|
// the signature. This is because some X509Certificate(Factory) implementations
|
||||||
|
// re-encode certificates.
|
||||||
|
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||||
|
signerInfo.setSigningCertificate(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signerInfo.getSigningCertificate() == null) {
|
||||||
|
signerInfo.addVerificationWarning(
|
||||||
|
isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
|
||||||
|
: ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
|
||||||
|
* V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
|
||||||
|
* will be updated to include a warning, but the source stamp verification can still proceed.
|
||||||
|
*/
|
||||||
|
private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
|
||||||
|
List<CentralDirectoryRecord> cdRecords,
|
||||||
|
DataSource apk,
|
||||||
|
ZipSections zipSections,
|
||||||
|
Result result)
|
||||||
|
throws IOException, ApkFormatException {
|
||||||
|
CentralDirectoryRecord manifestCdRecord = null;
|
||||||
|
List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
|
||||||
|
ContentDigestAlgorithm.class);
|
||||||
|
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||||
|
String cdRecordName = cdRecord.getName();
|
||||||
|
if (cdRecordName == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
|
||||||
|
manifestCdRecord = cdRecord;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cdRecordName.startsWith("META-INF/")
|
||||||
|
&& (cdRecordName.endsWith(".RSA")
|
||||||
|
|| cdRecordName.endsWith(".DSA")
|
||||||
|
|| cdRecordName.endsWith(".EC"))) {
|
||||||
|
signatureBlockRecords.add(cdRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (manifestCdRecord == null) {
|
||||||
|
// No JAR signing manifest file found. For SourceStamp verification, returning an empty
|
||||||
|
// digest is enough since this would affect the final digest signed by the stamp, and
|
||||||
|
// thus an empty digest will invalidate that signature.
|
||||||
|
return v1ContentDigest;
|
||||||
|
}
|
||||||
|
if (signatureBlockRecords.isEmpty()) {
|
||||||
|
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
|
||||||
|
} else {
|
||||||
|
for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
|
||||||
|
signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
|
||||||
|
for (Certificate certificate : certFactory.generateCertificates(
|
||||||
|
new ByteArrayInputStream(signatureBlockBytes))) {
|
||||||
|
// If multiple certificates are found within the signature block only the
|
||||||
|
// first is used as the signer of this block.
|
||||||
|
if (certificate instanceof X509Certificate) {
|
||||||
|
Result.SignerInfo signerInfo = new Result.SignerInfo();
|
||||||
|
signerInfo.setSigningCertificate((X509Certificate) certificate);
|
||||||
|
result.addV1Signer(signerInfo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// Log a warning for the parsing exception but still proceed with the stamp
|
||||||
|
// verification.
|
||||||
|
result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
|
||||||
|
signatureBlockRecord.getName(), e);
|
||||||
|
break;
|
||||||
|
} catch (ZipFormatException e) {
|
||||||
|
throw new ApkFormatException("Failed to read APK", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] manifestBytes =
|
||||||
|
LocalFileRecord.getUncompressedData(
|
||||||
|
apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset());
|
||||||
|
v1ContentDigest.put(
|
||||||
|
ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes));
|
||||||
|
return v1ContentDigest;
|
||||||
|
} catch (ZipFormatException e) {
|
||||||
|
throw new ApkFormatException("Failed to read APK", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of verifying the APK's source stamp signature; this signature can only be considered
|
||||||
|
* verified if {@link #isVerified()} returns true.
|
||||||
|
*/
|
||||||
|
public static class Result {
|
||||||
|
private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
|
||||||
|
private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
|
||||||
|
private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
|
||||||
|
private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
|
||||||
|
mV2SchemeSigners, mV3SchemeSigners);
|
||||||
|
private SourceStampInfo mSourceStampInfo;
|
||||||
|
|
||||||
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||||
|
|
||||||
|
private boolean mVerified;
|
||||||
|
|
||||||
|
void addVerificationError(int errorId, Object... params) {
|
||||||
|
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
void addVerificationWarning(int warningId, Object... params) {
|
||||||
|
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addV1Signer(SignerInfo signerInfo) {
|
||||||
|
mV1SchemeSigners.add(signerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addV2Signer(SignerInfo signerInfo) {
|
||||||
|
mV2SchemeSigners.add(signerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addV3Signer(SignerInfo signerInfo) {
|
||||||
|
mV3SchemeSigners.add(signerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the APK's source stamp signature
|
||||||
|
*/
|
||||||
|
public boolean isVerified() {
|
||||||
|
return mVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mergeFrom(ApkSigResult source) {
|
||||||
|
switch (source.signatureSchemeVersion) {
|
||||||
|
case Constants.VERSION_SOURCE_STAMP:
|
||||||
|
mVerified = source.verified;
|
||||||
|
if (!source.mSigners.isEmpty()) {
|
||||||
|
mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unknown ApkSigResult Signing Block Scheme Id "
|
||||||
|
+ source.signatureSchemeVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
|
||||||
|
* provided APK.
|
||||||
|
*/
|
||||||
|
public List<SignerInfo> getV1SchemeSigners() {
|
||||||
|
return mV1SchemeSigners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
|
||||||
|
* provided APK.
|
||||||
|
*/
|
||||||
|
public List<SignerInfo> getV2SchemeSigners() {
|
||||||
|
return mV2SchemeSigners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the
|
||||||
|
* provided APK.
|
||||||
|
*/
|
||||||
|
public List<SignerInfo> getV3SchemeSigners() {
|
||||||
|
return mV3SchemeSigners;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link SourceStampInfo} instance representing the source stamp signer for the
|
||||||
|
* APK, or null if the source stamp signature verification failed before the stamp signature
|
||||||
|
* block could be fully parsed.
|
||||||
|
*/
|
||||||
|
public SourceStampInfo getSourceStampInfo() {
|
||||||
|
return mSourceStampInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if an error was encountered while verifying the APK.
|
||||||
|
*
|
||||||
|
* <p>Any error prevents the APK from being considered verified.
|
||||||
|
*/
|
||||||
|
public boolean containsErrors() {
|
||||||
|
if (!mErrors.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||||
|
for (SignerInfo signer : signers) {
|
||||||
|
if (signer.containsErrors()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mSourceStampInfo != null) {
|
||||||
|
if (mSourceStampInfo.containsErrors()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the errors encountered while verifying the APK's source stamp.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getErrors() {
|
||||||
|
return mErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the warnings encountered while verifying the APK's source stamp.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getWarnings() {
|
||||||
|
return mWarnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all errors for this result, including any errors from signature scheme signers
|
||||||
|
* and the source stamp.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getAllErrors() {
|
||||||
|
List<ApkVerificationIssue> errors = new ArrayList<>();
|
||||||
|
errors.addAll(mErrors);
|
||||||
|
|
||||||
|
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||||
|
for (SignerInfo signer : signers) {
|
||||||
|
errors.addAll(signer.getErrors());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mSourceStampInfo != null) {
|
||||||
|
errors.addAll(mSourceStampInfo.getErrors());
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all warnings for this result, including any warnings from signature scheme
|
||||||
|
* signers and the source stamp.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getAllWarnings() {
|
||||||
|
List<ApkVerificationIssue> warnings = new ArrayList<>();
|
||||||
|
warnings.addAll(mWarnings);
|
||||||
|
|
||||||
|
for (List<SignerInfo> signers : mAllSchemeSigners) {
|
||||||
|
for (SignerInfo signer : signers) {
|
||||||
|
warnings.addAll(signer.getWarnings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mSourceStampInfo != null) {
|
||||||
|
warnings.addAll(mSourceStampInfo.getWarnings());
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information about an APK's signer and any errors encountered while parsing the
|
||||||
|
* corresponding signature block.
|
||||||
|
*/
|
||||||
|
public static class SignerInfo {
|
||||||
|
private X509Certificate mSigningCertificate;
|
||||||
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||||
|
|
||||||
|
void setSigningCertificate(X509Certificate signingCertificate) {
|
||||||
|
mSigningCertificate = signingCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addVerificationError(int errorId, Object... params) {
|
||||||
|
mErrors.add(new ApkVerificationIssue(errorId, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
void addVerificationWarning(int warningId, Object... params) {
|
||||||
|
mWarnings.add(new ApkVerificationIssue(warningId, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current signing certificate used by this signer.
|
||||||
|
*/
|
||||||
|
public X509Certificate getSigningCertificate() {
|
||||||
|
return mSigningCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors
|
||||||
|
* encountered during processing of this signer's signature block.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getErrors() {
|
||||||
|
return mErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
|
||||||
|
* encountered during processing of this signer's signature block.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getWarnings() {
|
||||||
|
return mWarnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if any errors were encountered while parsing this signer's
|
||||||
|
* signature block.
|
||||||
|
*/
|
||||||
|
public boolean containsErrors() {
|
||||||
|
return !mErrors.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains information about an APK's source stamp and any errors encountered while
|
||||||
|
* parsing the stamp signature block.
|
||||||
|
*/
|
||||||
|
public static class SourceStampInfo {
|
||||||
|
private final List<X509Certificate> mCertificates;
|
||||||
|
private final List<X509Certificate> mCertificateLineage;
|
||||||
|
|
||||||
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
|
||||||
|
|
||||||
|
private final long mTimestamp;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Since this utility is intended just to verify the source stamp, and the source stamp
|
||||||
|
* currently only logs warnings to prevent failing the APK signature verification, treat
|
||||||
|
* all warnings as errors. If the stamp verification is updated to log errors this
|
||||||
|
* should be set to false to ensure only errors trigger a failure verifying the source
|
||||||
|
* stamp.
|
||||||
|
*/
|
||||||
|
private static final boolean mWarningsAsErrors = true;
|
||||||
|
|
||||||
|
private SourceStampInfo(ApkSignerInfo result) {
|
||||||
|
mCertificates = result.certs;
|
||||||
|
mCertificateLineage = result.certificateLineage;
|
||||||
|
mErrors.addAll(result.getErrors());
|
||||||
|
mWarnings.addAll(result.getWarnings());
|
||||||
|
mInfoMessages.addAll(result.getInfoMessages());
|
||||||
|
mTimestamp = result.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SourceStamp's signing certificate or {@code null} if not available. The
|
||||||
|
* certificate is guaranteed to be available if no errors were encountered during
|
||||||
|
* verification (see {@link #containsErrors()}.
|
||||||
|
*
|
||||||
|
* <p>This certificate contains the SourceStamp's public key.
|
||||||
|
*/
|
||||||
|
public X509Certificate getCertificate() {
|
||||||
|
return mCertificates.isEmpty() ? null : mCertificates.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code List} of {@link X509Certificate} instances representing the source
|
||||||
|
* stamp signer's lineage with the oldest signer at element 0, or an empty {@code List}
|
||||||
|
* if the stamp's signing certificate has not been rotated.
|
||||||
|
*/
|
||||||
|
public List<X509Certificate> getCertificatesInLineage() {
|
||||||
|
return mCertificateLineage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether any errors were encountered during the source stamp verification.
|
||||||
|
*/
|
||||||
|
public boolean containsErrors() {
|
||||||
|
return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if any info messages were encountered during verification of
|
||||||
|
* this source stamp.
|
||||||
|
*/
|
||||||
|
public boolean containsInfoMessages() {
|
||||||
|
return !mInfoMessages.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were
|
||||||
|
* encountered during source stamp verification.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getErrors() {
|
||||||
|
if (!mWarningsAsErrors) {
|
||||||
|
return mErrors;
|
||||||
|
}
|
||||||
|
List<ApkVerificationIssue> result = new ArrayList<>();
|
||||||
|
result.addAll(mErrors);
|
||||||
|
result.addAll(mWarnings);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that
|
||||||
|
* were encountered during source stamp verification.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getWarnings() {
|
||||||
|
return mWarnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code List} of {@link ApkVerificationIssue} representing info messages
|
||||||
|
* that were encountered during source stamp verification.
|
||||||
|
*/
|
||||||
|
public List<ApkVerificationIssue> getInfoMessages() {
|
||||||
|
return mInfoMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the epoch timestamp in seconds representing the time this source stamp block
|
||||||
|
* was signed, or 0 if the timestamp is not available.
|
||||||
|
*/
|
||||||
|
public long getTimestampEpochSeconds() {
|
||||||
|
return mTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder of {@link SourceStampVerifier} instances.
|
||||||
|
*
|
||||||
|
* <p> The resulting verifier, by default, checks whether the APK's source stamp signature will
|
||||||
|
* verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not
|
||||||
|
* queried to determine the APK's minimum supported level, so the caller should specify a lower
|
||||||
|
* bound with {@link #setMinCheckedPlatformVersion(int)}.
|
||||||
|
*/
|
||||||
|
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 source stamp verification of the provided {@code
|
||||||
|
* apk}.
|
||||||
|
*/
|
||||||
|
public Builder(File apk) {
|
||||||
|
if (apk == null) {
|
||||||
|
throw new NullPointerException("apk == null");
|
||||||
|
}
|
||||||
|
mApkFile = apk;
|
||||||
|
mApkDataSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Builder} for source stamp verification of the provided {@code
|
||||||
|
* 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's source stamp is verified.
|
||||||
|
*
|
||||||
|
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||||
|
* on all Android platforms starting from the platform version with the provided {@code
|
||||||
|
* minSdkVersion}. The upper end of the platform versions range can be modified via
|
||||||
|
* {@link #setMaxCheckedPlatformVersion(int)}.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion API Level of the oldest platform for which to verify the APK
|
||||||
|
*/
|
||||||
|
public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) {
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the newest Android platform version for which the APK's source stamp is verified.
|
||||||
|
*
|
||||||
|
* <p>APK source stamp verification will confirm that the APK's stamp is expected to verify
|
||||||
|
* on all platform versions up to and including the proviced {@code maxSdkVersion}. The
|
||||||
|
* lower end of the platform versions range can be modified via {@link
|
||||||
|
* #setMinCheckedPlatformVersion(int)}.
|
||||||
|
*
|
||||||
|
* @param maxSdkVersion API Level of the newest platform for which to verify the APK
|
||||||
|
* @see #setMinCheckedPlatformVersion(int)
|
||||||
|
*/
|
||||||
|
public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) {
|
||||||
|
mMaxSdkVersion = maxSdkVersion;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@link SourceStampVerifier} initialized according to the configuration of this
|
||||||
|
* builder.
|
||||||
|
*/
|
||||||
|
public SourceStampVerifier build() {
|
||||||
|
return new SourceStampVerifier(
|
||||||
|
mApkFile,
|
||||||
|
mApkDataSource,
|
||||||
|
mMinSdkVersion,
|
||||||
|
mMaxSdkVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an APK is not well-formed. For example, this may indicate that the APK is not a
|
||||||
|
* well-formed ZIP archive, in which case {@link #getCause()} will return a
|
||||||
|
* {@link com.android.apksig.zip.ZipFormatException ZipFormatException}, or that the APK contains
|
||||||
|
* multiple ZIP entries with the same name.
|
||||||
|
*/
|
||||||
|
public class ApkFormatException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public ApkFormatException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApkFormatException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that no APK Signing Block was found in an APK.
|
||||||
|
*/
|
||||||
|
public class ApkSigningBlockNotFoundException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public ApkSigningBlockNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApkSigningBlockNotFoundException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,670 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.apk.AndroidBinXmlParser;
|
||||||
|
import com.android.apksig.internal.apk.stamp.SourceStampConstants;
|
||||||
|
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.internal.zip.CentralDirectoryRecord;
|
||||||
|
import com.android.apksig.internal.zip.LocalFileRecord;
|
||||||
|
import com.android.apksig.internal.zip.ZipUtils;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.zip.ZipFormatException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK utilities.
|
||||||
|
*/
|
||||||
|
public abstract class ApkUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the Android manifest ZIP entry in APKs.
|
||||||
|
*/
|
||||||
|
public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml";
|
||||||
|
|
||||||
|
/** Name of the SourceStamp certificate hash ZIP entry in APKs. */
|
||||||
|
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME =
|
||||||
|
SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
|
||||||
|
|
||||||
|
private ApkUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the main ZIP sections of the provided APK.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurred while reading the APK
|
||||||
|
* @throws ZipFormatException if the APK is malformed
|
||||||
|
*/
|
||||||
|
public static ZipSections findZipSections(DataSource apk)
|
||||||
|
throws IOException, ZipFormatException {
|
||||||
|
com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk);
|
||||||
|
return new ZipSections(
|
||||||
|
zipSections.getZipCentralDirectoryOffset(),
|
||||||
|
zipSections.getZipCentralDirectorySizeBytes(),
|
||||||
|
zipSections.getZipCentralDirectoryRecordCount(),
|
||||||
|
zipSections.getZipEndOfCentralDirectoryOffset(),
|
||||||
|
zipSections.getZipEndOfCentralDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the ZIP sections of an APK.
|
||||||
|
*/
|
||||||
|
public static class ZipSections extends com.android.apksig.zip.ZipSections {
|
||||||
|
public ZipSections(
|
||||||
|
long centralDirectoryOffset,
|
||||||
|
long centralDirectorySizeBytes,
|
||||||
|
int centralDirectoryRecordCount,
|
||||||
|
long eocdOffset,
|
||||||
|
ByteBuffer eocd) {
|
||||||
|
super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount,
|
||||||
|
eocdOffset, eocd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the offset of the start of the ZIP Central Directory in the APK's ZIP End of Central
|
||||||
|
* Directory record.
|
||||||
|
*
|
||||||
|
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||||
|
* @param offset offset of the ZIP Central Directory relative to the start of the archive. Must
|
||||||
|
* be between {@code 0} and {@code 2^32 - 1} inclusive.
|
||||||
|
*/
|
||||||
|
public static void setZipEocdCentralDirectoryOffset(
|
||||||
|
ByteBuffer zipEndOfCentralDirectory, long offset) {
|
||||||
|
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||||
|
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the length of EOCD comment.
|
||||||
|
*
|
||||||
|
* @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record
|
||||||
|
*/
|
||||||
|
public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
|
||||||
|
ByteBuffer eocd = zipEndOfCentralDirectory.slice();
|
||||||
|
eocd.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
ZipUtils.updateZipEocdCommentLen(eocd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the APK Signing Block of the provided {@code apk}.
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the APK is not a valid ZIP archive
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlock findApkSigningBlock(DataSource apk)
|
||||||
|
throws ApkFormatException, IOException, ApkSigningBlockNotFoundException {
|
||||||
|
ApkUtils.ZipSections inputZipSections;
|
||||||
|
try {
|
||||||
|
inputZipSections = ApkUtils.findZipSections(apk);
|
||||||
|
} catch (ZipFormatException e) {
|
||||||
|
throw new ApkFormatException("Malformed APK: not a ZIP archive", e);
|
||||||
|
}
|
||||||
|
return findApkSigningBlock(apk, inputZipSections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the APK Signing Block of the provided APK.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
|
||||||
|
throws IOException, ApkSigningBlockNotFoundException {
|
||||||
|
ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk,
|
||||||
|
zipSections);
|
||||||
|
return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the location of the APK Signing Block inside an APK.
|
||||||
|
*/
|
||||||
|
public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock {
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ApkSigningBlock}.
|
||||||
|
*
|
||||||
|
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
|
||||||
|
* Signing Block inside the APK file
|
||||||
|
* @param contents contents of the APK Signing Block
|
||||||
|
*/
|
||||||
|
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
|
||||||
|
super(startOffsetInApk, contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the contents of the APK's {@code AndroidManifest.xml}.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs while reading the APK
|
||||||
|
* @throws ApkFormatException if the APK is malformed
|
||||||
|
*/
|
||||||
|
public static ByteBuffer getAndroidManifest(DataSource apk)
|
||||||
|
throws IOException, ApkFormatException {
|
||||||
|
ZipSections zipSections;
|
||||||
|
try {
|
||||||
|
zipSections = findZipSections(apk);
|
||||||
|
} catch (ZipFormatException e) {
|
||||||
|
throw new ApkFormatException("Not a valid ZIP archive", e);
|
||||||
|
}
|
||||||
|
List<CentralDirectoryRecord> cdRecords =
|
||||||
|
V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
|
||||||
|
CentralDirectoryRecord androidManifestCdRecord = null;
|
||||||
|
for (CentralDirectoryRecord cdRecord : cdRecords) {
|
||||||
|
if (ANDROID_MANIFEST_ZIP_ENTRY_NAME.equals(cdRecord.getName())) {
|
||||||
|
androidManifestCdRecord = cdRecord;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (androidManifestCdRecord == null) {
|
||||||
|
throw new ApkFormatException("Missing " + ANDROID_MANIFEST_ZIP_ENTRY_NAME);
|
||||||
|
}
|
||||||
|
DataSource lfhSection = apk.slice(0, zipSections.getZipCentralDirectoryOffset());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ByteBuffer.wrap(
|
||||||
|
LocalFileRecord.getUncompressedData(
|
||||||
|
lfhSection, androidManifestCdRecord, lfhSection.size()));
|
||||||
|
} catch (ZipFormatException e) {
|
||||||
|
throw new ApkFormatException("Failed to read " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android resource ID of the {@code android:minSdkVersion} attribute in AndroidManifest.xml.
|
||||||
|
*/
|
||||||
|
private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android resource ID of the {@code android:debuggable} attribute in AndroidManifest.xml.
|
||||||
|
*/
|
||||||
|
private static final int DEBUGGABLE_ATTR_ID = 0x0101000f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android resource ID of the {@code android:targetSandboxVersion} attribute in
|
||||||
|
* AndroidManifest.xml.
|
||||||
|
*/
|
||||||
|
private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android resource ID of the {@code android:targetSdkVersion} attribute in
|
||||||
|
* AndroidManifest.xml.
|
||||||
|
*/
|
||||||
|
private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270;
|
||||||
|
private static final String USES_SDK_ELEMENT_TAG = "uses-sdk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml.
|
||||||
|
*/
|
||||||
|
private static final int VERSION_CODE_ATTR_ID = 0x0101021b;
|
||||||
|
private static final String MANIFEST_ELEMENT_TAG = "manifest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml.
|
||||||
|
*/
|
||||||
|
private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the lowest Android platform version (API Level) supported by an APK with the
|
||||||
|
* provided {@code AndroidManifest.xml}.
|
||||||
|
*
|
||||||
|
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||||
|
* resource format
|
||||||
|
*
|
||||||
|
* @throws MinSdkVersionException if an error occurred while determining the API Level
|
||||||
|
*/
|
||||||
|
public static int getMinSdkVersionFromBinaryAndroidManifest(
|
||||||
|
ByteBuffer androidManifestContents) throws MinSdkVersionException {
|
||||||
|
// IMPLEMENTATION NOTE: Minimum supported Android platform version number is declared using
|
||||||
|
// uses-sdk elements which are children of the top-level manifest element. uses-sdk element
|
||||||
|
// declares the minimum supported platform version using the android:minSdkVersion attribute
|
||||||
|
// whose default value is 1.
|
||||||
|
// For each encountered uses-sdk element, the Android runtime checks that its minSdkVersion
|
||||||
|
// is not higher than the runtime's API Level and rejects APKs if it is higher. Thus, the
|
||||||
|
// effective minSdkVersion value is the maximum over the encountered minSdkVersion values.
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If no uses-sdk elements are encountered, Android accepts the APK. We treat this
|
||||||
|
// scenario as though the minimum supported API Level is 1.
|
||||||
|
int result = 1;
|
||||||
|
|
||||||
|
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||||
|
int eventType = parser.getEventType();
|
||||||
|
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||||
|
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||||
|
&& (parser.getDepth() == 2)
|
||||||
|
&& ("uses-sdk".equals(parser.getName()))
|
||||||
|
&& (parser.getNamespace().isEmpty())) {
|
||||||
|
// In each uses-sdk element, minSdkVersion defaults to 1
|
||||||
|
int minSdkVersion = 1;
|
||||||
|
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||||
|
if (parser.getAttributeNameResourceId(i) == MIN_SDK_VERSION_ATTR_ID) {
|
||||||
|
int valueType = parser.getAttributeValueType(i);
|
||||||
|
switch (valueType) {
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||||
|
minSdkVersion = parser.getAttributeIntValue(i);
|
||||||
|
break;
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||||
|
minSdkVersion =
|
||||||
|
getMinSdkVersionForCodename(
|
||||||
|
parser.getAttributeStringValue(i));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new MinSdkVersionException(
|
||||||
|
"Unable to determine APK's minimum supported Android"
|
||||||
|
+ ": unsupported value type in "
|
||||||
|
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||||
|
+ " minSdkVersion"
|
||||||
|
+ ". Only integer values supported.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = Math.max(result, minSdkVersion);
|
||||||
|
}
|
||||||
|
eventType = parser.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||||
|
throw new MinSdkVersionException(
|
||||||
|
"Unable to determine APK's minimum supported Android platform version"
|
||||||
|
+ ": malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CodenamesLazyInitializer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of platform codename (first letter of) to API Level mappings. The list must be
|
||||||
|
* sorted by the first letter. For codenames not in the list, the assumption is that the API
|
||||||
|
* Level is incremented by one for every increase in the codename's first letter.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private static final Pair<Character, Integer>[] SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL =
|
||||||
|
new Pair[] {
|
||||||
|
Pair.of('C', 2),
|
||||||
|
Pair.of('D', 3),
|
||||||
|
Pair.of('E', 4),
|
||||||
|
Pair.of('F', 7),
|
||||||
|
Pair.of('G', 8),
|
||||||
|
Pair.of('H', 10),
|
||||||
|
Pair.of('I', 13),
|
||||||
|
Pair.of('J', 15),
|
||||||
|
Pair.of('K', 18),
|
||||||
|
Pair.of('L', 20),
|
||||||
|
Pair.of('M', 22),
|
||||||
|
Pair.of('N', 23),
|
||||||
|
Pair.of('O', 25),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final Comparator<Pair<Character, Integer>> CODENAME_FIRST_CHAR_COMPARATOR =
|
||||||
|
new ByFirstComparator();
|
||||||
|
|
||||||
|
private static class ByFirstComparator implements Comparator<Pair<Character, Integer>> {
|
||||||
|
@Override
|
||||||
|
public int compare(Pair<Character, Integer> o1, Pair<Character, Integer> o2) {
|
||||||
|
char c1 = o1.getFirst();
|
||||||
|
char c2 = o2.getFirst();
|
||||||
|
return c1 - c2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the API Level corresponding to the provided platform codename.
|
||||||
|
*
|
||||||
|
* <p>This method is pessimistic. It returns a value one lower than the API Level with which the
|
||||||
|
* platform is actually released (e.g., 23 for N which was released as API Level 24). This is
|
||||||
|
* because new features which first appear in an API Level are not available in the early days
|
||||||
|
* of that platform version's existence, when the platform only has a codename. Moreover, this
|
||||||
|
* method currently doesn't differentiate between initial and MR releases, meaning API Level
|
||||||
|
* returned for MR releases may be more than one lower than the API Level with which the
|
||||||
|
* platform version is actually released.
|
||||||
|
*
|
||||||
|
* @throws CodenameMinSdkVersionException if the {@code codename} is not supported
|
||||||
|
*/
|
||||||
|
static int getMinSdkVersionForCodename(String codename) throws CodenameMinSdkVersionException {
|
||||||
|
char firstChar = codename.isEmpty() ? ' ' : codename.charAt(0);
|
||||||
|
// Codenames are case-sensitive. Only codenames starting with A-Z are supported for now.
|
||||||
|
// We only look at the first letter of the codename as this is the most important letter.
|
||||||
|
if ((firstChar >= 'A') && (firstChar <= 'Z')) {
|
||||||
|
Pair<Character, Integer>[] sortedCodenamesFirstCharToApiLevel =
|
||||||
|
CodenamesLazyInitializer.SORTED_CODENAMES_FIRST_CHAR_TO_API_LEVEL;
|
||||||
|
int searchResult =
|
||||||
|
Arrays.binarySearch(
|
||||||
|
sortedCodenamesFirstCharToApiLevel,
|
||||||
|
Pair.of(firstChar, null), // second element of the pair is ignored here
|
||||||
|
CodenamesLazyInitializer.CODENAME_FIRST_CHAR_COMPARATOR);
|
||||||
|
if (searchResult >= 0) {
|
||||||
|
// Exact match -- searchResult is the index of the matching element
|
||||||
|
return sortedCodenamesFirstCharToApiLevel[searchResult].getSecond();
|
||||||
|
}
|
||||||
|
// Not an exact match -- searchResult is negative and is -(insertion index) - 1.
|
||||||
|
// The element at insertionIndex - 1 (if present) is smaller than firstChar and the
|
||||||
|
// element at insertionIndex (if present) is greater than firstChar.
|
||||||
|
int insertionIndex = -1 - searchResult; // insertionIndex is in [0; array length]
|
||||||
|
if (insertionIndex == 0) {
|
||||||
|
// 'A' or 'B' -- never released to public
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
// The element at insertionIndex - 1 is the newest older codename.
|
||||||
|
// API Level bumped by at least 1 for every change in the first letter of codename
|
||||||
|
Pair<Character, Integer> newestOlderCodenameMapping =
|
||||||
|
sortedCodenamesFirstCharToApiLevel[insertionIndex - 1];
|
||||||
|
char newestOlderCodenameFirstChar = newestOlderCodenameMapping.getFirst();
|
||||||
|
int newestOlderCodenameApiLevel = newestOlderCodenameMapping.getSecond();
|
||||||
|
return newestOlderCodenameApiLevel + (firstChar - newestOlderCodenameFirstChar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CodenameMinSdkVersionException(
|
||||||
|
"Unable to determine APK's minimum supported Android platform version"
|
||||||
|
+ " : Unsupported codename in " + ANDROID_MANIFEST_ZIP_ENTRY_NAME
|
||||||
|
+ "'s minSdkVersion: \"" + codename + "\"",
|
||||||
|
codename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the APK is debuggable according to its {@code AndroidManifest.xml}.
|
||||||
|
* See the {@code android:debuggable} attribute of the {@code application} element.
|
||||||
|
*
|
||||||
|
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||||
|
* resource format
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the manifest is malformed
|
||||||
|
*/
|
||||||
|
public static boolean getDebuggableFromBinaryAndroidManifest(
|
||||||
|
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||||
|
// IMPLEMENTATION NOTE: Whether the package is debuggable is declared using the first
|
||||||
|
// "application" element which is a child of the top-level manifest element. The debuggable
|
||||||
|
// attribute of this application element is coerced to a boolean value. If there is no
|
||||||
|
// application element or if it doesn't declare the debuggable attribute, the package is
|
||||||
|
// considered not debuggable.
|
||||||
|
|
||||||
|
try {
|
||||||
|
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||||
|
int eventType = parser.getEventType();
|
||||||
|
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||||
|
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||||
|
&& (parser.getDepth() == 2)
|
||||||
|
&& ("application".equals(parser.getName()))
|
||||||
|
&& (parser.getNamespace().isEmpty())) {
|
||||||
|
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||||
|
if (parser.getAttributeNameResourceId(i) == DEBUGGABLE_ATTR_ID) {
|
||||||
|
int valueType = parser.getAttributeValueType(i);
|
||||||
|
switch (valueType) {
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_BOOLEAN:
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||||
|
String value = parser.getAttributeStringValue(i);
|
||||||
|
return ("true".equals(value))
|
||||||
|
|| ("TRUE".equals(value))
|
||||||
|
|| ("1".equals(value));
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_REFERENCE:
|
||||||
|
// References to resources are not supported on purpose. The
|
||||||
|
// reason is that the resolved value depends on the resource
|
||||||
|
// configuration (e.g, MNC/MCC, locale, screen density) used
|
||||||
|
// at resolution time. As a result, the same APK may appear as
|
||||||
|
// debuggable in one situation and as non-debuggable in another
|
||||||
|
// situation. Such APKs may put users at risk.
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Unable to determine whether APK is debuggable"
|
||||||
|
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||||
|
+ " android:debuggable attribute references a"
|
||||||
|
+ " resource. References are not supported for"
|
||||||
|
+ " security reasons. Only constant boolean,"
|
||||||
|
+ " string and int values are supported.");
|
||||||
|
default:
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Unable to determine whether APK is debuggable"
|
||||||
|
+ ": " + ANDROID_MANIFEST_ZIP_ENTRY_NAME + "'s"
|
||||||
|
+ " android:debuggable attribute uses"
|
||||||
|
+ " unsupported value type. Only boolean,"
|
||||||
|
+ " string and int values are supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This application element does not declare the debuggable attribute
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
eventType = parser.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// No application element found
|
||||||
|
return false;
|
||||||
|
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Unable to determine whether APK is debuggable: malformed binary resource: "
|
||||||
|
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the package name of the APK according to its {@code AndroidManifest.xml} or
|
||||||
|
* {@code null} if package name is not declared. See the {@code package} attribute of the
|
||||||
|
* {@code manifest} element.
|
||||||
|
*
|
||||||
|
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||||
|
* resource format
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the manifest is malformed
|
||||||
|
*/
|
||||||
|
public static String getPackageNameFromBinaryAndroidManifest(
|
||||||
|
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||||
|
// IMPLEMENTATION NOTE: Package name is declared as the "package" attribute of the top-level
|
||||||
|
// manifest element. Interestingly, as opposed to most other attributes, Android Package
|
||||||
|
// Manager looks up this attribute by its name rather than by its resource ID.
|
||||||
|
|
||||||
|
try {
|
||||||
|
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||||
|
int eventType = parser.getEventType();
|
||||||
|
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||||
|
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||||
|
&& (parser.getDepth() == 1)
|
||||||
|
&& ("manifest".equals(parser.getName()))
|
||||||
|
&& (parser.getNamespace().isEmpty())) {
|
||||||
|
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||||
|
if ("package".equals(parser.getAttributeName(i))
|
||||||
|
&& (parser.getNamespace().isEmpty())) {
|
||||||
|
return parser.getAttributeStringValue(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No "package" attribute found
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
eventType = parser.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// No manifest element found
|
||||||
|
return null;
|
||||||
|
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Unable to determine APK package name: malformed binary resource: "
|
||||||
|
+ ANDROID_MANIFEST_ZIP_ENTRY_NAME,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the security sandbox version targeted by an APK with the provided
|
||||||
|
* {@code AndroidManifest.xml}.
|
||||||
|
*
|
||||||
|
* <p>If the security sandbox version is not specified in the manifest a default value of 1 is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||||
|
* resource format
|
||||||
|
*/
|
||||||
|
public static int getTargetSandboxVersionFromBinaryAndroidManifest(
|
||||||
|
ByteBuffer androidManifestContents) {
|
||||||
|
try {
|
||||||
|
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||||
|
MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
// An ApkFormatException indicates the target sandbox is not specified in the manifest;
|
||||||
|
// return a default value of 1.
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}.
|
||||||
|
*
|
||||||
|
* <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither
|
||||||
|
* value is specified then a value of 1 is returned.
|
||||||
|
*
|
||||||
|
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||||
|
* resource format
|
||||||
|
*/
|
||||||
|
public static int getTargetSdkVersionFromBinaryAndroidManifest(
|
||||||
|
ByteBuffer androidManifestContents) {
|
||||||
|
// If the targetSdkVersion is not specified then the platform will use the value of the
|
||||||
|
// minSdkVersion; if neither is specified then the platform will use a value of 1.
|
||||||
|
int minSdkVersion = 1;
|
||||||
|
try {
|
||||||
|
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||||
|
USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
// Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk
|
||||||
|
// element is not specified at all.
|
||||||
|
}
|
||||||
|
androidManifestContents.rewind();
|
||||||
|
try {
|
||||||
|
minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
// Similar to above, expected if the APK does not contain a minSdkVersion attribute, or
|
||||||
|
// the uses-sdk element is not specified at all.
|
||||||
|
}
|
||||||
|
return minSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the versionCode of the APK according to its {@code AndroidManifest.xml}.
|
||||||
|
*
|
||||||
|
* <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid
|
||||||
|
* integer an ApkFormatException is thrown.
|
||||||
|
*
|
||||||
|
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||||
|
* resource format
|
||||||
|
* @throws ApkFormatException if an error occurred while determining the versionCode, or if the
|
||||||
|
* versionCode attribute value is not available.
|
||||||
|
*/
|
||||||
|
public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents)
|
||||||
|
throws ApkFormatException {
|
||||||
|
return getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||||
|
MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the versionCode and versionCodeMajor of the APK according to its {@code
|
||||||
|
* AndroidManifest.xml} combined together as a single long value.
|
||||||
|
*
|
||||||
|
* <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower
|
||||||
|
* 32 bits. If the versionCodeMajor is not specified then the versionCode is returned.
|
||||||
|
*
|
||||||
|
* @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android
|
||||||
|
* resource format
|
||||||
|
* @throws ApkFormatException if an error occurred while determining the version, or if the
|
||||||
|
* versionCode attribute value is not available.
|
||||||
|
*/
|
||||||
|
public static long getLongVersionCodeFromBinaryAndroidManifest(
|
||||||
|
ByteBuffer androidManifestContents) throws ApkFormatException {
|
||||||
|
// If the versionCode is not found then allow the ApkFormatException to be thrown to notify
|
||||||
|
// the caller that the versionCode is not available.
|
||||||
|
int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents);
|
||||||
|
long versionCodeMajor = 0;
|
||||||
|
try {
|
||||||
|
androidManifestContents.rewind();
|
||||||
|
versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents,
|
||||||
|
MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
// This is expected if the versionCodeMajor has not been defined for the APK; in this
|
||||||
|
// case the return value is just the versionCode.
|
||||||
|
}
|
||||||
|
return (versionCodeMajor << 32) | versionCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the integer value of the requested {@code attributeId} in the specified {@code
|
||||||
|
* elementName} from the provided {@code androidManifestContents} in binary Android resource
|
||||||
|
* format.
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or
|
||||||
|
* if the requested attribute is not found.
|
||||||
|
*/
|
||||||
|
private static int getAttributeValueFromBinaryAndroidManifest(
|
||||||
|
ByteBuffer androidManifestContents, String elementName, int attributeId)
|
||||||
|
throws ApkFormatException {
|
||||||
|
if (elementName == null) {
|
||||||
|
throw new NullPointerException("elementName cannot be null");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents);
|
||||||
|
int eventType = parser.getEventType();
|
||||||
|
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||||
|
if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT)
|
||||||
|
&& (elementName.equals(parser.getName()))) {
|
||||||
|
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||||
|
if (parser.getAttributeNameResourceId(i) == attributeId) {
|
||||||
|
int valueType = parser.getAttributeValueType(i);
|
||||||
|
switch (valueType) {
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_INT:
|
||||||
|
case AndroidBinXmlParser.VALUE_TYPE_STRING:
|
||||||
|
return parser.getAttributeIntValue(i);
|
||||||
|
default:
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Unsupported value type, " + valueType
|
||||||
|
+ ", for attribute " + String.format("0x%08X",
|
||||||
|
attributeId) + " under element " + elementName);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eventType = parser.next();
|
||||||
|
}
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Failed to determine APK's " + elementName + " attribute "
|
||||||
|
+ String.format("0x%08X", attributeId) + " value");
|
||||||
|
} catch (AndroidBinXmlParser.XmlParserException e) {
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Unable to determine value for attribute " + String.format("0x%08X",
|
||||||
|
attributeId) + " under element " + elementName
|
||||||
|
+ "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] computeSha256DigestBytes(byte[] data) {
|
||||||
|
return ApkUtilsLite.computeSha256DigestBytes(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.internal.zip.ZipUtils;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.zip.ZipFormatException;
|
||||||
|
import com.android.apksig.zip.ZipSections;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight version of the ApkUtils for clients that only require a subset of the utility
|
||||||
|
* functionality.
|
||||||
|
*/
|
||||||
|
public class ApkUtilsLite {
|
||||||
|
private ApkUtilsLite() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the main ZIP sections of the provided APK.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurred while reading the APK
|
||||||
|
* @throws ZipFormatException if the APK is malformed
|
||||||
|
*/
|
||||||
|
public static ZipSections findZipSections(DataSource apk)
|
||||||
|
throws IOException, ZipFormatException {
|
||||||
|
Pair<ByteBuffer, Long> eocdAndOffsetInFile =
|
||||||
|
ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
|
||||||
|
if (eocdAndOffsetInFile == null) {
|
||||||
|
throw new ZipFormatException("ZIP End of Central Directory record not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst();
|
||||||
|
long eocdOffset = eocdAndOffsetInFile.getSecond();
|
||||||
|
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf);
|
||||||
|
if (cdStartOffset > eocdOffset) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"ZIP Central Directory start offset out of range: " + cdStartOffset
|
||||||
|
+ ". ZIP End of Central Directory offset: " + eocdOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf);
|
||||||
|
long cdEndOffset = cdStartOffset + cdSizeBytes;
|
||||||
|
if (cdEndOffset > eocdOffset) {
|
||||||
|
throw new ZipFormatException(
|
||||||
|
"ZIP Central Directory overlaps with End of Central Directory"
|
||||||
|
+ ". CD end: " + cdEndOffset
|
||||||
|
+ ", EoCD start: " + eocdOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf);
|
||||||
|
|
||||||
|
return new ZipSections(
|
||||||
|
cdStartOffset,
|
||||||
|
cdSizeBytes,
|
||||||
|
cdRecordCount,
|
||||||
|
eocdOffset,
|
||||||
|
eocdBuf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://source.android.com/security/apksigning/v2.html
|
||||||
|
private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
|
||||||
|
private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
|
||||||
|
private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the APK Signing Block of the provided APK.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
* @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections)
|
||||||
|
throws IOException, ApkSigningBlockNotFoundException {
|
||||||
|
// FORMAT (see https://source.android.com/security/apksigning/v2.html):
|
||||||
|
// OFFSET DATA TYPE DESCRIPTION
|
||||||
|
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||||
|
// * @+8 bytes payload
|
||||||
|
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||||
|
// * @-16 bytes uint128: magic
|
||||||
|
|
||||||
|
long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
|
||||||
|
long centralDirEndOffset =
|
||||||
|
centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
|
||||||
|
long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
|
||||||
|
if (centralDirEndOffset != eocdStartOffset) {
|
||||||
|
throw new ApkSigningBlockNotFoundException(
|
||||||
|
"ZIP Central Directory is not immediately followed by End of Central Directory"
|
||||||
|
+ ". CD end: " + centralDirEndOffset
|
||||||
|
+ ", EoCD start: " + eocdStartOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) {
|
||||||
|
throw new ApkSigningBlockNotFoundException(
|
||||||
|
"APK too small for APK Signing Block. ZIP Central Directory offset: "
|
||||||
|
+ centralDirStartOffset);
|
||||||
|
}
|
||||||
|
// Read the magic and offset in file from the footer section of the block:
|
||||||
|
// * uint64: size of block
|
||||||
|
// * 16 bytes: magic
|
||||||
|
ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24);
|
||||||
|
footer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
|
||||||
|
|| (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
|
||||||
|
throw new ApkSigningBlockNotFoundException(
|
||||||
|
"No APK Signing Block before ZIP Central Directory");
|
||||||
|
}
|
||||||
|
// Read and compare size fields
|
||||||
|
long apkSigBlockSizeInFooter = footer.getLong(0);
|
||||||
|
if ((apkSigBlockSizeInFooter < footer.capacity())
|
||||||
|
|| (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
|
||||||
|
throw new ApkSigningBlockNotFoundException(
|
||||||
|
"APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
|
||||||
|
}
|
||||||
|
int totalSize = (int) (apkSigBlockSizeInFooter + 8);
|
||||||
|
long apkSigBlockOffset = centralDirStartOffset - totalSize;
|
||||||
|
if (apkSigBlockOffset < 0) {
|
||||||
|
throw new ApkSigningBlockNotFoundException(
|
||||||
|
"APK Signing Block offset out of range: " + apkSigBlockOffset);
|
||||||
|
}
|
||||||
|
ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8);
|
||||||
|
apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
|
||||||
|
if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
|
||||||
|
throw new ApkSigningBlockNotFoundException(
|
||||||
|
"APK Signing Block sizes in header and footer do not match: "
|
||||||
|
+ apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
|
||||||
|
}
|
||||||
|
return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about the location of the APK Signing Block inside an APK.
|
||||||
|
*/
|
||||||
|
public static class ApkSigningBlock {
|
||||||
|
private final long mStartOffsetInApk;
|
||||||
|
private final DataSource mContents;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ApkSigningBlock}.
|
||||||
|
*
|
||||||
|
* @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK
|
||||||
|
* Signing Block inside the APK file
|
||||||
|
* @param contents contents of the APK Signing Block
|
||||||
|
*/
|
||||||
|
public ApkSigningBlock(long startOffsetInApk, DataSource contents) {
|
||||||
|
mStartOffsetInApk = startOffsetInApk;
|
||||||
|
mContents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the start offset (in bytes, relative to start of file) of the APK Signing Block.
|
||||||
|
*/
|
||||||
|
public long getStartOffset() {
|
||||||
|
return mStartOffsetInApk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data source which provides the full contents of the APK Signing Block,
|
||||||
|
* including its footer.
|
||||||
|
*/
|
||||||
|
public DataSource getContents() {
|
||||||
|
return mContents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] computeSha256DigestBytes(byte[] data) {
|
||||||
|
MessageDigest messageDigest;
|
||||||
|
try {
|
||||||
|
messageDigest = MessageDigest.getInstance("SHA-256");
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException("SHA-256 is not found", e);
|
||||||
|
}
|
||||||
|
messageDigest.update(data);
|
||||||
|
return messageDigest.digest();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||||
|
* an APK because the version is specified as a codename, rather than as API Level number, and the
|
||||||
|
* codename is in an unexpected format.
|
||||||
|
*/
|
||||||
|
public class CodenameMinSdkVersionException extends MinSdkVersionException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** Encountered codename. */
|
||||||
|
private final String mCodename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code MinSdkVersionCodenameException} with the provided message and
|
||||||
|
* codename.
|
||||||
|
*/
|
||||||
|
public CodenameMinSdkVersionException(String message, String codename) {
|
||||||
|
super(message);
|
||||||
|
mCodename = codename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the codename.
|
||||||
|
*/
|
||||||
|
public String getCodename() {
|
||||||
|
return mCodename;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that there was an issue determining the minimum Android platform version supported by
|
||||||
|
* an APK.
|
||||||
|
*/
|
||||||
|
public class MinSdkVersionException extends ApkFormatException {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code MinSdkVersionException} with the provided message.
|
||||||
|
*/
|
||||||
|
public MinSdkVersionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code MinSdkVersionException} with the provided message and cause.
|
||||||
|
*/
|
||||||
|
public MinSdkVersionException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,869 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML pull style parser of Android binary XML resources, such as {@code AndroidManifest.xml}.
|
||||||
|
*
|
||||||
|
* <p>For an input document, the parser outputs an event stream (see {@code EVENT_... constants} via
|
||||||
|
* {@link #getEventType()} and {@link #next()} methods. Additional information about the current
|
||||||
|
* event can be obtained via an assortment of getters, for example, {@link #getName()} or
|
||||||
|
* {@link #getAttributeNameResourceId(int)}.
|
||||||
|
*/
|
||||||
|
public class AndroidBinXmlParser {
|
||||||
|
|
||||||
|
/** Event: start of document. */
|
||||||
|
public static final int EVENT_START_DOCUMENT = 1;
|
||||||
|
|
||||||
|
/** Event: end of document. */
|
||||||
|
public static final int EVENT_END_DOCUMENT = 2;
|
||||||
|
|
||||||
|
/** Event: start of an element. */
|
||||||
|
public static final int EVENT_START_ELEMENT = 3;
|
||||||
|
|
||||||
|
/** Event: end of an document. */
|
||||||
|
public static final int EVENT_END_ELEMENT = 4;
|
||||||
|
|
||||||
|
/** Attribute value type is not supported by this parser. */
|
||||||
|
public static final int VALUE_TYPE_UNSUPPORTED = 0;
|
||||||
|
|
||||||
|
/** Attribute value is a string. Use {@link #getAttributeStringValue(int)} to obtain it. */
|
||||||
|
public static final int VALUE_TYPE_STRING = 1;
|
||||||
|
|
||||||
|
/** Attribute value is an integer. Use {@link #getAttributeIntValue(int)} to obtain it. */
|
||||||
|
public static final int VALUE_TYPE_INT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute value is a resource reference. Use {@link #getAttributeIntValue(int)} to obtain it.
|
||||||
|
*/
|
||||||
|
public static final int VALUE_TYPE_REFERENCE = 3;
|
||||||
|
|
||||||
|
/** Attribute value is a boolean. Use {@link #getAttributeBooleanValue(int)} to obtain it. */
|
||||||
|
public static final int VALUE_TYPE_BOOLEAN = 4;
|
||||||
|
|
||||||
|
private static final long NO_NAMESPACE = 0xffffffffL;
|
||||||
|
|
||||||
|
private final ByteBuffer mXml;
|
||||||
|
|
||||||
|
private StringPool mStringPool;
|
||||||
|
private ResourceMap mResourceMap;
|
||||||
|
private int mDepth;
|
||||||
|
private int mCurrentEvent = EVENT_START_DOCUMENT;
|
||||||
|
|
||||||
|
private String mCurrentElementName;
|
||||||
|
private String mCurrentElementNamespace;
|
||||||
|
private int mCurrentElementAttributeCount;
|
||||||
|
private List<Attribute> mCurrentElementAttributes;
|
||||||
|
private ByteBuffer mCurrentElementAttributesContents;
|
||||||
|
private int mCurrentElementAttrSizeBytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new parser for the provided document.
|
||||||
|
*/
|
||||||
|
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
|
||||||
|
xml.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
Chunk resXmlChunk = null;
|
||||||
|
while (xml.hasRemaining()) {
|
||||||
|
Chunk chunk = Chunk.get(xml);
|
||||||
|
if (chunk == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (chunk.getType() == Chunk.TYPE_RES_XML) {
|
||||||
|
resXmlChunk = chunk;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resXmlChunk == null) {
|
||||||
|
throw new XmlParserException("No XML chunk in file");
|
||||||
|
}
|
||||||
|
mXml = resXmlChunk.getContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the depth of the current element. Outside of the root of the document the depth is
|
||||||
|
* {@code 0}. The depth is incremented by {@code 1} before each {@code start element} event and
|
||||||
|
* is decremented by {@code 1} after each {@code end element} event.
|
||||||
|
*/
|
||||||
|
public int getDepth() {
|
||||||
|
return mDepth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of the current event. See {@code EVENT_...} constants.
|
||||||
|
*/
|
||||||
|
public int getEventType() {
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the local name of the current element or {@code null} if the current event does not
|
||||||
|
* pertain to an element.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mCurrentElementName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the namespace of the current element or {@code null} if the current event does not
|
||||||
|
* pertain to an element. Returns an empty string if the element is not associated with a
|
||||||
|
* namespace.
|
||||||
|
*/
|
||||||
|
public String getNamespace() {
|
||||||
|
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return mCurrentElementNamespace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of attributes of the element associated with the current event or
|
||||||
|
* {@code -1} if no element is associated with the current event.
|
||||||
|
*/
|
||||||
|
public int getAttributeCount() {
|
||||||
|
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mCurrentElementAttributeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID corresponding to the name of the specified attribute of the current
|
||||||
|
* element or {@code 0} if the name is not associated with a resource ID.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public int getAttributeNameResourceId(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getNameResourceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the specified attribute of the current element.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public String getAttributeName(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the specified attribute of the current element or an empty string if
|
||||||
|
* the attribute is not associated with a namespace.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public String getAttributeNamespace(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getNamespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value type of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public int getAttributeValueType(int index) throws XmlParserException {
|
||||||
|
int type = getAttribute(index).getValueType();
|
||||||
|
switch (type) {
|
||||||
|
case Attribute.TYPE_STRING:
|
||||||
|
return VALUE_TYPE_STRING;
|
||||||
|
case Attribute.TYPE_INT_DEC:
|
||||||
|
case Attribute.TYPE_INT_HEX:
|
||||||
|
return VALUE_TYPE_INT;
|
||||||
|
case Attribute.TYPE_REFERENCE:
|
||||||
|
return VALUE_TYPE_REFERENCE;
|
||||||
|
case Attribute.TYPE_INT_BOOLEAN:
|
||||||
|
return VALUE_TYPE_BOOLEAN;
|
||||||
|
default:
|
||||||
|
return VALUE_TYPE_UNSUPPORTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the integer value of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event.
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public int getAttributeIntValue(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getIntValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the boolean value of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event.
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getBooleanValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string value of the specified attribute of the current element. See
|
||||||
|
* {@code VALUE_TYPE_...} constants.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if the index is out of range or the current event is not a
|
||||||
|
* {@code start element} event.
|
||||||
|
* @throws XmlParserException if a parsing error is occurred
|
||||||
|
*/
|
||||||
|
public String getAttributeStringValue(int index) throws XmlParserException {
|
||||||
|
return getAttribute(index).getStringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Attribute getAttribute(int index) {
|
||||||
|
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||||
|
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("index must be >= 0");
|
||||||
|
}
|
||||||
|
if (index >= mCurrentElementAttributeCount) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
|
||||||
|
}
|
||||||
|
parseCurrentElementAttributesIfNotParsed();
|
||||||
|
return mCurrentElementAttributes.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advances to the next parsing event and returns its type. See {@code EVENT_...} constants.
|
||||||
|
*/
|
||||||
|
public int next() throws XmlParserException {
|
||||||
|
// Decrement depth if the previous event was "end element".
|
||||||
|
if (mCurrentEvent == EVENT_END_ELEMENT) {
|
||||||
|
mDepth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read events from document, ignoring events that we don't report to caller. Stop at the
|
||||||
|
// earliest event which we report to caller.
|
||||||
|
while (mXml.hasRemaining()) {
|
||||||
|
Chunk chunk = Chunk.get(mXml);
|
||||||
|
if (chunk == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
switch (chunk.getType()) {
|
||||||
|
case Chunk.TYPE_STRING_POOL:
|
||||||
|
if (mStringPool != null) {
|
||||||
|
throw new XmlParserException("Multiple string pools not supported");
|
||||||
|
}
|
||||||
|
mStringPool = new StringPool(chunk);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Chunk.RES_XML_TYPE_START_ELEMENT:
|
||||||
|
{
|
||||||
|
if (mStringPool == null) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Named element encountered before string pool");
|
||||||
|
}
|
||||||
|
ByteBuffer contents = chunk.getContents();
|
||||||
|
if (contents.remaining() < 20) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Start element chunk too short. Need at least 20 bytes. Available: "
|
||||||
|
+ contents.remaining() + " bytes");
|
||||||
|
}
|
||||||
|
long nsId = getUnsignedInt32(contents);
|
||||||
|
long nameId = getUnsignedInt32(contents);
|
||||||
|
int attrStartOffset = getUnsignedInt16(contents);
|
||||||
|
int attrSizeBytes = getUnsignedInt16(contents);
|
||||||
|
int attrCount = getUnsignedInt16(contents);
|
||||||
|
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
|
||||||
|
contents.position(0);
|
||||||
|
if (attrStartOffset > contents.remaining()) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Attributes start offset out of bounds: " + attrStartOffset
|
||||||
|
+ ", max: " + contents.remaining());
|
||||||
|
}
|
||||||
|
if (attrEndOffset > contents.remaining()) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Attributes end offset out of bounds: " + attrEndOffset
|
||||||
|
+ ", max: " + contents.remaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentElementName = mStringPool.getString(nameId);
|
||||||
|
mCurrentElementNamespace =
|
||||||
|
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||||
|
mCurrentElementAttributeCount = attrCount;
|
||||||
|
mCurrentElementAttributes = null;
|
||||||
|
mCurrentElementAttrSizeBytes = attrSizeBytes;
|
||||||
|
mCurrentElementAttributesContents =
|
||||||
|
sliceFromTo(contents, attrStartOffset, attrEndOffset);
|
||||||
|
|
||||||
|
mDepth++;
|
||||||
|
mCurrentEvent = EVENT_START_ELEMENT;
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Chunk.RES_XML_TYPE_END_ELEMENT:
|
||||||
|
{
|
||||||
|
if (mStringPool == null) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Named element encountered before string pool");
|
||||||
|
}
|
||||||
|
ByteBuffer contents = chunk.getContents();
|
||||||
|
if (contents.remaining() < 8) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"End element chunk too short. Need at least 8 bytes. Available: "
|
||||||
|
+ contents.remaining() + " bytes");
|
||||||
|
}
|
||||||
|
long nsId = getUnsignedInt32(contents);
|
||||||
|
long nameId = getUnsignedInt32(contents);
|
||||||
|
mCurrentElementName = mStringPool.getString(nameId);
|
||||||
|
mCurrentElementNamespace =
|
||||||
|
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||||
|
mCurrentEvent = EVENT_END_ELEMENT;
|
||||||
|
mCurrentElementAttributes = null;
|
||||||
|
mCurrentElementAttributesContents = null;
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
|
||||||
|
if (mResourceMap != null) {
|
||||||
|
throw new XmlParserException("Multiple resource maps not supported");
|
||||||
|
}
|
||||||
|
mResourceMap = new ResourceMap(chunk);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unknown chunk type -- ignore
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentEvent = EVENT_END_DOCUMENT;
|
||||||
|
return mCurrentEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseCurrentElementAttributesIfNotParsed() {
|
||||||
|
if (mCurrentElementAttributes != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
|
||||||
|
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
|
||||||
|
int startPosition = i * mCurrentElementAttrSizeBytes;
|
||||||
|
ByteBuffer attr =
|
||||||
|
sliceFromTo(
|
||||||
|
mCurrentElementAttributesContents,
|
||||||
|
startPosition,
|
||||||
|
startPosition + mCurrentElementAttrSizeBytes);
|
||||||
|
long nsId = getUnsignedInt32(attr);
|
||||||
|
long nameId = getUnsignedInt32(attr);
|
||||||
|
attr.position(attr.position() + 7); // skip ignored fields
|
||||||
|
int valueType = getUnsignedInt8(attr);
|
||||||
|
long valueData = getUnsignedInt32(attr);
|
||||||
|
mCurrentElementAttributes.add(
|
||||||
|
new Attribute(
|
||||||
|
nsId,
|
||||||
|
nameId,
|
||||||
|
valueType,
|
||||||
|
(int) valueData,
|
||||||
|
mStringPool,
|
||||||
|
mResourceMap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Attribute {
|
||||||
|
private static final int TYPE_REFERENCE = 1;
|
||||||
|
private static final int TYPE_STRING = 3;
|
||||||
|
private static final int TYPE_INT_DEC = 0x10;
|
||||||
|
private static final int TYPE_INT_HEX = 0x11;
|
||||||
|
private static final int TYPE_INT_BOOLEAN = 0x12;
|
||||||
|
|
||||||
|
private final long mNsId;
|
||||||
|
private final long mNameId;
|
||||||
|
private final int mValueType;
|
||||||
|
private final int mValueData;
|
||||||
|
private final StringPool mStringPool;
|
||||||
|
private final ResourceMap mResourceMap;
|
||||||
|
|
||||||
|
private Attribute(
|
||||||
|
long nsId,
|
||||||
|
long nameId,
|
||||||
|
int valueType,
|
||||||
|
int valueData,
|
||||||
|
StringPool stringPool,
|
||||||
|
ResourceMap resourceMap) {
|
||||||
|
mNsId = nsId;
|
||||||
|
mNameId = nameId;
|
||||||
|
mValueType = valueType;
|
||||||
|
mValueData = valueData;
|
||||||
|
mStringPool = stringPool;
|
||||||
|
mResourceMap = resourceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNameResourceId() {
|
||||||
|
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() throws XmlParserException {
|
||||||
|
return mStringPool.getString(mNameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNamespace() throws XmlParserException {
|
||||||
|
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValueType() {
|
||||||
|
return mValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIntValue() throws XmlParserException {
|
||||||
|
switch (mValueType) {
|
||||||
|
case TYPE_REFERENCE:
|
||||||
|
case TYPE_INT_DEC:
|
||||||
|
case TYPE_INT_HEX:
|
||||||
|
case TYPE_INT_BOOLEAN:
|
||||||
|
return mValueData;
|
||||||
|
default:
|
||||||
|
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getBooleanValue() throws XmlParserException {
|
||||||
|
switch (mValueType) {
|
||||||
|
case TYPE_INT_BOOLEAN:
|
||||||
|
return mValueData != 0;
|
||||||
|
default:
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Cannot coerce to boolean: value type " + mValueType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStringValue() throws XmlParserException {
|
||||||
|
switch (mValueType) {
|
||||||
|
case TYPE_STRING:
|
||||||
|
return mStringPool.getString(mValueData & 0xffffffffL);
|
||||||
|
case TYPE_INT_DEC:
|
||||||
|
return Integer.toString(mValueData);
|
||||||
|
case TYPE_INT_HEX:
|
||||||
|
return "0x" + Integer.toHexString(mValueData);
|
||||||
|
case TYPE_INT_BOOLEAN:
|
||||||
|
return Boolean.toString(mValueData != 0);
|
||||||
|
case TYPE_REFERENCE:
|
||||||
|
return "@" + Integer.toHexString(mValueData);
|
||||||
|
default:
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Cannot coerce to string: value type " + mValueType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chunk of a document. Each chunk is tagged with a type and consists of a header followed by
|
||||||
|
* contents.
|
||||||
|
*/
|
||||||
|
private static class Chunk {
|
||||||
|
public static final int TYPE_STRING_POOL = 1;
|
||||||
|
public static final int TYPE_RES_XML = 3;
|
||||||
|
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
|
||||||
|
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
|
||||||
|
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
|
||||||
|
|
||||||
|
static final int HEADER_MIN_SIZE_BYTES = 8;
|
||||||
|
|
||||||
|
private final int mType;
|
||||||
|
private final ByteBuffer mHeader;
|
||||||
|
private final ByteBuffer mContents;
|
||||||
|
|
||||||
|
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
|
||||||
|
mType = type;
|
||||||
|
mHeader = header;
|
||||||
|
mContents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getContents() {
|
||||||
|
ByteBuffer result = mContents.slice();
|
||||||
|
result.order(mContents.order());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getHeader() {
|
||||||
|
ByteBuffer result = mHeader.slice();
|
||||||
|
result.order(mHeader.order());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType() {
|
||||||
|
return mType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumes the chunk located at the current position of the input and returns the chunk
|
||||||
|
* or {@code null} if there is no chunk left in the input.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if the chunk is malformed
|
||||||
|
*/
|
||||||
|
public static Chunk get(ByteBuffer input) throws XmlParserException {
|
||||||
|
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
|
||||||
|
// Android ignores the last chunk if its header is too big to fit into the file
|
||||||
|
input.position(input.limit());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int originalPosition = input.position();
|
||||||
|
int type = getUnsignedInt16(input);
|
||||||
|
int headerSize = getUnsignedInt16(input);
|
||||||
|
long chunkSize = getUnsignedInt32(input);
|
||||||
|
long chunkRemaining = chunkSize - 8;
|
||||||
|
if (chunkRemaining > input.remaining()) {
|
||||||
|
// Android ignores the last chunk if it's too big to fit into the file
|
||||||
|
input.position(input.limit());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (headerSize < HEADER_MIN_SIZE_BYTES) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Malformed chunk: header too short: " + headerSize + " bytes");
|
||||||
|
} else if (headerSize > chunkSize) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
|
||||||
|
+ chunkSize + " bytes");
|
||||||
|
}
|
||||||
|
int contentStartPosition = originalPosition + headerSize;
|
||||||
|
long chunkEndPosition = originalPosition + chunkSize;
|
||||||
|
Chunk chunk =
|
||||||
|
new Chunk(
|
||||||
|
type,
|
||||||
|
sliceFromTo(input, originalPosition, contentStartPosition),
|
||||||
|
sliceFromTo(input, contentStartPosition, chunkEndPosition));
|
||||||
|
input.position((int) chunkEndPosition);
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String pool of a document. Strings are referenced by their {@code 0}-based index in the pool.
|
||||||
|
*/
|
||||||
|
private static class StringPool {
|
||||||
|
private static final int FLAG_UTF8 = 1 << 8;
|
||||||
|
|
||||||
|
private final ByteBuffer mChunkContents;
|
||||||
|
private final ByteBuffer mStringsSection;
|
||||||
|
private final int mStringCount;
|
||||||
|
private final boolean mUtf8Encoded;
|
||||||
|
private final Map<Integer, String> mCachedStrings = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new string pool from the provided chunk.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if a parsing error occurred
|
||||||
|
*/
|
||||||
|
public StringPool(Chunk chunk) throws XmlParserException {
|
||||||
|
ByteBuffer header = chunk.getHeader();
|
||||||
|
int headerSizeBytes = header.remaining();
|
||||||
|
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
|
||||||
|
if (header.remaining() < 20) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"XML chunk's header too short. Required at least 20 bytes. Available: "
|
||||||
|
+ header.remaining() + " bytes");
|
||||||
|
}
|
||||||
|
long stringCount = getUnsignedInt32(header);
|
||||||
|
if (stringCount > Integer.MAX_VALUE) {
|
||||||
|
throw new XmlParserException("Too many strings: " + stringCount);
|
||||||
|
}
|
||||||
|
mStringCount = (int) stringCount;
|
||||||
|
long styleCount = getUnsignedInt32(header);
|
||||||
|
if (styleCount > Integer.MAX_VALUE) {
|
||||||
|
throw new XmlParserException("Too many styles: " + styleCount);
|
||||||
|
}
|
||||||
|
long flags = getUnsignedInt32(header);
|
||||||
|
long stringsStartOffset = getUnsignedInt32(header);
|
||||||
|
long stylesStartOffset = getUnsignedInt32(header);
|
||||||
|
|
||||||
|
ByteBuffer contents = chunk.getContents();
|
||||||
|
if (mStringCount > 0) {
|
||||||
|
int stringsSectionStartOffsetInContents =
|
||||||
|
(int) (stringsStartOffset - headerSizeBytes);
|
||||||
|
int stringsSectionEndOffsetInContents;
|
||||||
|
if (styleCount > 0) {
|
||||||
|
// Styles section follows the strings section
|
||||||
|
if (stylesStartOffset < stringsStartOffset) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Styles offset (" + stylesStartOffset + ") < strings offset ("
|
||||||
|
+ stringsStartOffset + ")");
|
||||||
|
}
|
||||||
|
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
|
||||||
|
} else {
|
||||||
|
stringsSectionEndOffsetInContents = contents.remaining();
|
||||||
|
}
|
||||||
|
mStringsSection =
|
||||||
|
sliceFromTo(
|
||||||
|
contents,
|
||||||
|
stringsSectionStartOffsetInContents,
|
||||||
|
stringsSectionEndOffsetInContents);
|
||||||
|
} else {
|
||||||
|
mStringsSection = ByteBuffer.allocate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
|
||||||
|
mChunkContents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the string located at the specified {@code 0}-based index in this pool.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if the string does not exist or cannot be decoded
|
||||||
|
*/
|
||||||
|
public String getString(long index) throws XmlParserException {
|
||||||
|
if (index < 0) {
|
||||||
|
throw new XmlParserException("Unsuported string index: " + index);
|
||||||
|
} else if (index >= mStringCount) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx = (int) index;
|
||||||
|
String result = mCachedStrings.get(idx);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
|
||||||
|
if (offsetInStringsSection >= mStringsSection.capacity()) {
|
||||||
|
throw new XmlParserException(
|
||||||
|
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
|
||||||
|
+ ", max: " + (mStringsSection.capacity() - 1));
|
||||||
|
}
|
||||||
|
mStringsSection.position((int) offsetInStringsSection);
|
||||||
|
result =
|
||||||
|
(mUtf8Encoded)
|
||||||
|
? getLengthPrefixedUtf8EncodedString(mStringsSection)
|
||||||
|
: getLengthPrefixedUtf16EncodedString(mStringsSection);
|
||||||
|
mCachedStrings.put(idx, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
|
||||||
|
throws XmlParserException {
|
||||||
|
// If the length (in uint16s) is 0x7fff or lower, it is stored as a single uint16.
|
||||||
|
// Otherwise, it is stored as a big-endian uint32 with highest bit set. Thus, the range
|
||||||
|
// of supported values is 0 to 0x7fffffff inclusive.
|
||||||
|
int lengthChars = getUnsignedInt16(encoded);
|
||||||
|
if ((lengthChars & 0x8000) != 0) {
|
||||||
|
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
|
||||||
|
}
|
||||||
|
if (lengthChars > Integer.MAX_VALUE / 2) {
|
||||||
|
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
|
||||||
|
}
|
||||||
|
int lengthBytes = lengthChars * 2;
|
||||||
|
|
||||||
|
byte[] arr;
|
||||||
|
int arrOffset;
|
||||||
|
if (encoded.hasArray()) {
|
||||||
|
arr = encoded.array();
|
||||||
|
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||||
|
encoded.position(encoded.position() + lengthBytes);
|
||||||
|
} else {
|
||||||
|
arr = new byte[lengthBytes];
|
||||||
|
arrOffset = 0;
|
||||||
|
encoded.get(arr);
|
||||||
|
}
|
||||||
|
// Reproduce the behavior of Android runtime which requires that the UTF-16 encoded
|
||||||
|
// array of bytes is NULL terminated.
|
||||||
|
if ((arr[arrOffset + lengthBytes] != 0)
|
||||||
|
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
|
||||||
|
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new String(arr, arrOffset, lengthBytes, "UTF-16LE");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("UTF-16LE character encoding not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
|
||||||
|
throws XmlParserException {
|
||||||
|
// If the length (in bytes) is 0x7f or lower, it is stored as a single uint8. Otherwise,
|
||||||
|
// it is stored as a big-endian uint16 with highest bit set. Thus, the range of
|
||||||
|
// supported values is 0 to 0x7fff inclusive.
|
||||||
|
|
||||||
|
// Skip UTF-16 encoded length (in uint16s)
|
||||||
|
int lengthBytes = getUnsignedInt8(encoded);
|
||||||
|
if ((lengthBytes & 0x80) != 0) {
|
||||||
|
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read UTF-8 encoded length (in bytes)
|
||||||
|
lengthBytes = getUnsignedInt8(encoded);
|
||||||
|
if ((lengthBytes & 0x80) != 0) {
|
||||||
|
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] arr;
|
||||||
|
int arrOffset;
|
||||||
|
if (encoded.hasArray()) {
|
||||||
|
arr = encoded.array();
|
||||||
|
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||||
|
encoded.position(encoded.position() + lengthBytes);
|
||||||
|
} else {
|
||||||
|
arr = new byte[lengthBytes];
|
||||||
|
arrOffset = 0;
|
||||||
|
encoded.get(arr);
|
||||||
|
}
|
||||||
|
// Reproduce the behavior of Android runtime which requires that the UTF-8 encoded array
|
||||||
|
// of bytes is NULL terminated.
|
||||||
|
if (arr[arrOffset + lengthBytes] != 0) {
|
||||||
|
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new String(arr, arrOffset, lengthBytes, "UTF-8");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException("UTF-8 character encoding not supported", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource map of a document. Resource IDs are referenced by their {@code 0}-based index in the
|
||||||
|
* map.
|
||||||
|
*/
|
||||||
|
private static class ResourceMap {
|
||||||
|
private final ByteBuffer mChunkContents;
|
||||||
|
private final int mEntryCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new resource map from the provided chunk.
|
||||||
|
*
|
||||||
|
* @throws XmlParserException if a parsing error occurred
|
||||||
|
*/
|
||||||
|
public ResourceMap(Chunk chunk) throws XmlParserException {
|
||||||
|
mChunkContents = chunk.getContents().slice();
|
||||||
|
mChunkContents.order(chunk.getContents().order());
|
||||||
|
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||||
|
mEntryCount = mChunkContents.remaining() / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID located at the specified {@code 0}-based index in this pool or
|
||||||
|
* {@code 0} if the index is out of range.
|
||||||
|
*/
|
||||||
|
public int getResourceId(long index) {
|
||||||
|
if ((index < 0) || (index >= mEntryCount)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int idx = (int) index;
|
||||||
|
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||||
|
return mChunkContents.getInt(idx * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||||
|
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||||
|
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||||
|
* buffer's byte order.
|
||||||
|
*/
|
||||||
|
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
|
||||||
|
if (start < 0) {
|
||||||
|
throw new IllegalArgumentException("start: " + start);
|
||||||
|
}
|
||||||
|
if (end < start) {
|
||||||
|
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||||
|
}
|
||||||
|
int capacity = source.capacity();
|
||||||
|
if (end > source.capacity()) {
|
||||||
|
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||||
|
}
|
||||||
|
return sliceFromTo(source, (int) start, (int) end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||||
|
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||||
|
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||||
|
* buffer's byte order.
|
||||||
|
*/
|
||||||
|
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||||
|
if (start < 0) {
|
||||||
|
throw new IllegalArgumentException("start: " + start);
|
||||||
|
}
|
||||||
|
if (end < start) {
|
||||||
|
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||||
|
}
|
||||||
|
int capacity = source.capacity();
|
||||||
|
if (end > source.capacity()) {
|
||||||
|
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||||
|
}
|
||||||
|
int originalLimit = source.limit();
|
||||||
|
int originalPosition = source.position();
|
||||||
|
try {
|
||||||
|
source.position(0);
|
||||||
|
source.limit(end);
|
||||||
|
source.position(start);
|
||||||
|
ByteBuffer result = source.slice();
|
||||||
|
result.order(source.order());
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
source.position(0);
|
||||||
|
source.limit(originalLimit);
|
||||||
|
source.position(originalPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getUnsignedInt8(ByteBuffer buffer) {
|
||||||
|
return buffer.get() & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getUnsignedInt16(ByteBuffer buffer) {
|
||||||
|
return buffer.getShort() & 0xffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getUnsignedInt32(ByteBuffer buffer) {
|
||||||
|
return buffer.getInt() & 0xffffffffL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
|
||||||
|
return buffer.getInt(position) & 0xffffffffL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an error occurred while parsing a document.
|
||||||
|
*/
|
||||||
|
public static class XmlParserException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public XmlParserException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public XmlParserException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerificationIssue;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of an APK signature verification result.
|
||||||
|
*/
|
||||||
|
public class ApkSigResult {
|
||||||
|
public final int signatureSchemeVersion;
|
||||||
|
|
||||||
|
/** Whether the APK's Signature Scheme signature verifies. */
|
||||||
|
public boolean verified;
|
||||||
|
|
||||||
|
public final List<ApkSignerInfo> mSigners = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
public ApkSigResult(int signatureSchemeVersion) {
|
||||||
|
this.signatureSchemeVersion = signatureSchemeVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if this result encountered errors during verification.
|
||||||
|
*/
|
||||||
|
public boolean containsErrors() {
|
||||||
|
if (!mErrors.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!mSigners.isEmpty()) {
|
||||||
|
for (ApkSignerInfo signer : mSigners) {
|
||||||
|
if (signer.containsErrors()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if this result encountered warnings during verification.
|
||||||
|
*/
|
||||||
|
public boolean containsWarnings() {
|
||||||
|
if (!mWarnings.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!mSigners.isEmpty()) {
|
||||||
|
for (ApkSignerInfo signer : mSigners) {
|
||||||
|
if (signer.containsWarnings()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code
|
||||||
|
* issueId} and {@code params}.
|
||||||
|
*/
|
||||||
|
public void addError(int issueId, Object... parameters) {
|
||||||
|
mErrors.add(new ApkVerificationIssue(issueId, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code
|
||||||
|
* issueId} and {@code params}.
|
||||||
|
*/
|
||||||
|
public void addWarning(int issueId, Object... parameters) {
|
||||||
|
mWarnings.add(new ApkVerificationIssue(issueId, parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the errors encountered during verification.
|
||||||
|
*/
|
||||||
|
public List<? extends ApkVerificationIssue> getErrors() {
|
||||||
|
return mErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the warnings encountered during verification.
|
||||||
|
*/
|
||||||
|
public List<? extends ApkVerificationIssue> getWarnings() {
|
||||||
|
return mWarnings;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerificationIssue;
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of an APK signer.
|
||||||
|
*/
|
||||||
|
public class ApkSignerInfo {
|
||||||
|
public int index;
|
||||||
|
public long timestamp;
|
||||||
|
public List<X509Certificate> certs = new ArrayList<>();
|
||||||
|
public List<X509Certificate> certificateLineage = new ArrayList<>();
|
||||||
|
|
||||||
|
private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
|
||||||
|
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code
|
||||||
|
* issueId} and {@code params}.
|
||||||
|
*/
|
||||||
|
public void addError(int issueId, Object... params) {
|
||||||
|
mErrors.add(new ApkVerificationIssue(issueId, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code
|
||||||
|
* issueId} and {@code params}.
|
||||||
|
*/
|
||||||
|
public void addWarning(int issueId, Object... params) {
|
||||||
|
mWarnings.add(new ApkVerificationIssue(issueId, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the
|
||||||
|
* provided {@code issueId} and {@code params}.
|
||||||
|
*/
|
||||||
|
public void addInfoMessage(int issueId, Object... params) {
|
||||||
|
mInfoMessages.add(new ApkVerificationIssue(issueId, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if any errors were encountered during verification for this signer.
|
||||||
|
*/
|
||||||
|
public boolean containsErrors() {
|
||||||
|
return !mErrors.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if any warnings were encountered during verification for this signer.
|
||||||
|
*/
|
||||||
|
public boolean containsWarnings() {
|
||||||
|
return !mWarnings.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if any info messages were encountered during verification of this
|
||||||
|
* signer.
|
||||||
|
*/
|
||||||
|
public boolean containsInfoMessages() {
|
||||||
|
return !mInfoMessages.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the errors encountered during verification for this signer.
|
||||||
|
*/
|
||||||
|
public List<? extends ApkVerificationIssue> getErrors() {
|
||||||
|
return mErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the warnings encountered during verification for this signer.
|
||||||
|
*/
|
||||||
|
public List<? extends ApkVerificationIssue> getWarnings() {
|
||||||
|
return mWarnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the info messages encountered during verification of this signer.
|
||||||
|
*/
|
||||||
|
public List<? extends ApkVerificationIssue> getInfoMessages() {
|
||||||
|
return mInfoMessages;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
|
||||||
|
import com.android.apksig.apk.ApkUtilsLite;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.zip.ZipSections;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the
|
||||||
|
* utility functionality.
|
||||||
|
*/
|
||||||
|
public class ApkSigningBlockUtilsLite {
|
||||||
|
private ApkSigningBlockUtilsLite() {}
|
||||||
|
|
||||||
|
private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray();
|
||||||
|
/**
|
||||||
|
* Returns the APK Signature Scheme block contained in the provided APK file for the given ID
|
||||||
|
* and the additional information relevant for verifying the block against the file.
|
||||||
|
*
|
||||||
|
* @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
|
||||||
|
* identifying the appropriate block to find, e.g. the APK Signature Scheme v2
|
||||||
|
* block ID.
|
||||||
|
*
|
||||||
|
* @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme
|
||||||
|
* @throws IOException if an I/O error occurs while reading the APK
|
||||||
|
*/
|
||||||
|
public static SignatureInfo findSignature(
|
||||||
|
DataSource apk, ZipSections zipSections, int blockId)
|
||||||
|
throws IOException, SignatureNotFoundException {
|
||||||
|
// Find the APK Signing Block.
|
||||||
|
DataSource apkSigningBlock;
|
||||||
|
long apkSigningBlockOffset;
|
||||||
|
try {
|
||||||
|
ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo =
|
||||||
|
ApkUtilsLite.findApkSigningBlock(apk, zipSections);
|
||||||
|
apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset();
|
||||||
|
apkSigningBlock = apkSigningBlockInfo.getContents();
|
||||||
|
} catch (ApkSigningBlockNotFoundException e) {
|
||||||
|
throw new SignatureNotFoundException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
ByteBuffer apkSigningBlockBuf =
|
||||||
|
apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size());
|
||||||
|
apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
|
// Find the APK Signature Scheme Block inside the APK Signing Block.
|
||||||
|
ByteBuffer apkSignatureSchemeBlock =
|
||||||
|
findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId);
|
||||||
|
return new SignatureInfo(
|
||||||
|
apkSignatureSchemeBlock,
|
||||||
|
apkSigningBlockOffset,
|
||||||
|
zipSections.getZipCentralDirectoryOffset(),
|
||||||
|
zipSections.getZipEndOfCentralDirectoryOffset(),
|
||||||
|
zipSections.getZipEndOfCentralDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteBuffer findApkSignatureSchemeBlock(
|
||||||
|
ByteBuffer apkSigningBlock,
|
||||||
|
int blockId) throws SignatureNotFoundException {
|
||||||
|
checkByteOrderLittleEndian(apkSigningBlock);
|
||||||
|
// FORMAT:
|
||||||
|
// OFFSET DATA TYPE DESCRIPTION
|
||||||
|
// * @+0 bytes uint64: size in bytes (excluding this field)
|
||||||
|
// * @+8 bytes pairs
|
||||||
|
// * @-24 bytes uint64: size in bytes (same as the one above)
|
||||||
|
// * @-16 bytes uint128: magic
|
||||||
|
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
|
||||||
|
|
||||||
|
int entryCount = 0;
|
||||||
|
while (pairs.hasRemaining()) {
|
||||||
|
entryCount++;
|
||||||
|
if (pairs.remaining() < 8) {
|
||||||
|
throw new SignatureNotFoundException(
|
||||||
|
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
|
||||||
|
}
|
||||||
|
long lenLong = pairs.getLong();
|
||||||
|
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
|
||||||
|
throw new SignatureNotFoundException(
|
||||||
|
"APK Signing Block entry #" + entryCount
|
||||||
|
+ " size out of range: " + lenLong);
|
||||||
|
}
|
||||||
|
int len = (int) lenLong;
|
||||||
|
int nextEntryPos = pairs.position() + len;
|
||||||
|
if (len > pairs.remaining()) {
|
||||||
|
throw new SignatureNotFoundException(
|
||||||
|
"APK Signing Block entry #" + entryCount + " size out of range: " + len
|
||||||
|
+ ", available: " + pairs.remaining());
|
||||||
|
}
|
||||||
|
int id = pairs.getInt();
|
||||||
|
if (id == blockId) {
|
||||||
|
return getByteBuffer(pairs, len - 4);
|
||||||
|
}
|
||||||
|
pairs.position(nextEntryPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new SignatureNotFoundException(
|
||||||
|
"No APK Signature Scheme block in APK Signing Block with ID: " + blockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkByteOrderLittleEndian(ByteBuffer buffer) {
|
||||||
|
if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
|
||||||
|
throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||||
|
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||||
|
* guaranteed to contain at least one signature.
|
||||||
|
*
|
||||||
|
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||||
|
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||||
|
* requested platform versions. As a result, the result may contain more than one signature.
|
||||||
|
*
|
||||||
|
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||||
|
* found for an Android platform version in the range.
|
||||||
|
*/
|
||||||
|
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||||
|
List<T> signatures, int minSdkVersion, int maxSdkVersion)
|
||||||
|
throws NoApkSupportedSignaturesException {
|
||||||
|
return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the subset of signatures which are expected to be verified by at least one Android
|
||||||
|
* platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is
|
||||||
|
* guaranteed to contain at least one signature.
|
||||||
|
*
|
||||||
|
* <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a
|
||||||
|
* signature within the signing block using the standard JCA.
|
||||||
|
*
|
||||||
|
* <p>Each Android platform version typically verifies exactly one signature from the provided
|
||||||
|
* {@code signatures} set. This method returns the set of these signatures collected over all
|
||||||
|
* requested platform versions. As a result, the result may contain more than one signature.
|
||||||
|
*
|
||||||
|
* @throws NoApkSupportedSignaturesException if no supported signatures were
|
||||||
|
* found for an Android platform version in the range.
|
||||||
|
*/
|
||||||
|
public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify(
|
||||||
|
List<T> signatures, int minSdkVersion, int maxSdkVersion,
|
||||||
|
boolean onlyRequireJcaSupport) throws
|
||||||
|
NoApkSupportedSignaturesException {
|
||||||
|
// Pick the signature with the strongest algorithm at all required SDK versions, to mimic
|
||||||
|
// Android's behavior on those versions.
|
||||||
|
//
|
||||||
|
// Here we assume that, once introduced, a signature algorithm continues to be supported in
|
||||||
|
// all future Android versions. We also assume that the better-than relationship between
|
||||||
|
// algorithms is exactly the same on all Android platform versions (except that older
|
||||||
|
// platforms might support fewer algorithms). If these assumption are no longer true, the
|
||||||
|
// logic here will need to change accordingly.
|
||||||
|
Map<Integer, T>
|
||||||
|
bestSigAlgorithmOnSdkVersion = new HashMap<>();
|
||||||
|
int minProvidedSignaturesVersion = Integer.MAX_VALUE;
|
||||||
|
for (T sig : signatures) {
|
||||||
|
SignatureAlgorithm sigAlgorithm = sig.algorithm;
|
||||||
|
int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion()
|
||||||
|
: sigAlgorithm.getMinSdkVersion();
|
||||||
|
if (sigMinSdkVersion > maxSdkVersion) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sigMinSdkVersion < minProvidedSignaturesVersion) {
|
||||||
|
minProvidedSignaturesVersion = sigMinSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion);
|
||||||
|
if ((candidate == null)
|
||||||
|
|| (compareSignatureAlgorithm(
|
||||||
|
sigAlgorithm, candidate.algorithm) > 0)) {
|
||||||
|
bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must have some supported signature algorithms for minSdkVersion.
|
||||||
|
if (minSdkVersion < minProvidedSignaturesVersion) {
|
||||||
|
throw new NoApkSupportedSignaturesException(
|
||||||
|
"Minimum provided signature version " + minProvidedSignaturesVersion +
|
||||||
|
" > minSdkVersion " + minSdkVersion);
|
||||||
|
}
|
||||||
|
if (bestSigAlgorithmOnSdkVersion.isEmpty()) {
|
||||||
|
throw new NoApkSupportedSignaturesException("No supported signature");
|
||||||
|
}
|
||||||
|
List<T> signaturesToVerify =
|
||||||
|
new ArrayList<>(bestSigAlgorithmOnSdkVersion.values());
|
||||||
|
Collections.sort(
|
||||||
|
signaturesToVerify,
|
||||||
|
(sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId()));
|
||||||
|
return signaturesToVerify;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if
|
||||||
|
* {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference.
|
||||||
|
*/
|
||||||
|
public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
|
||||||
|
ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
|
||||||
|
ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
|
||||||
|
return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number
|
||||||
|
* if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference.
|
||||||
|
*/
|
||||||
|
private static int compareContentDigestAlgorithm(
|
||||||
|
ContentDigestAlgorithm alg1,
|
||||||
|
ContentDigestAlgorithm alg2) {
|
||||||
|
switch (alg1) {
|
||||||
|
case CHUNKED_SHA256:
|
||||||
|
switch (alg2) {
|
||||||
|
case CHUNKED_SHA256:
|
||||||
|
return 0;
|
||||||
|
case CHUNKED_SHA512:
|
||||||
|
case VERITY_CHUNKED_SHA256:
|
||||||
|
return -1;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||||
|
}
|
||||||
|
case CHUNKED_SHA512:
|
||||||
|
switch (alg2) {
|
||||||
|
case CHUNKED_SHA256:
|
||||||
|
case VERITY_CHUNKED_SHA256:
|
||||||
|
return 1;
|
||||||
|
case CHUNKED_SHA512:
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||||
|
}
|
||||||
|
case VERITY_CHUNKED_SHA256:
|
||||||
|
switch (alg2) {
|
||||||
|
case CHUNKED_SHA256:
|
||||||
|
return 1;
|
||||||
|
case VERITY_CHUNKED_SHA256:
|
||||||
|
return 0;
|
||||||
|
case CHUNKED_SHA512:
|
||||||
|
return -1;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown alg2: " + alg2);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unknown alg1: " + alg1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns new byte buffer whose content is a shared subsequence of this buffer's content
|
||||||
|
* between the specified start (inclusive) and end (exclusive) positions. As opposed to
|
||||||
|
* {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
|
||||||
|
* buffer's byte order.
|
||||||
|
*/
|
||||||
|
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||||
|
if (start < 0) {
|
||||||
|
throw new IllegalArgumentException("start: " + start);
|
||||||
|
}
|
||||||
|
if (end < start) {
|
||||||
|
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||||
|
}
|
||||||
|
int capacity = source.capacity();
|
||||||
|
if (end > source.capacity()) {
|
||||||
|
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||||
|
}
|
||||||
|
int originalLimit = source.limit();
|
||||||
|
int originalPosition = source.position();
|
||||||
|
try {
|
||||||
|
source.position(0);
|
||||||
|
source.limit(end);
|
||||||
|
source.position(start);
|
||||||
|
ByteBuffer result = source.slice();
|
||||||
|
result.order(source.order());
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
source.position(0);
|
||||||
|
source.limit(originalLimit);
|
||||||
|
source.position(originalPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative <em>get</em> method for reading {@code size} number of bytes from the current
|
||||||
|
* position of this buffer.
|
||||||
|
*
|
||||||
|
* <p>This method reads the next {@code size} bytes at this buffer's current position,
|
||||||
|
* returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
|
||||||
|
* {@code size}, byte order set to this buffer's byte order; and then increments the position by
|
||||||
|
* {@code size}.
|
||||||
|
*/
|
||||||
|
private static ByteBuffer getByteBuffer(ByteBuffer source, int size) {
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IllegalArgumentException("size: " + size);
|
||||||
|
}
|
||||||
|
int originalLimit = source.limit();
|
||||||
|
int position = source.position();
|
||||||
|
int limit = position + size;
|
||||||
|
if ((limit < position) || (limit > originalLimit)) {
|
||||||
|
throw new BufferUnderflowException();
|
||||||
|
}
|
||||||
|
source.limit(limit);
|
||||||
|
try {
|
||||||
|
ByteBuffer result = source.slice();
|
||||||
|
result.order(source.order());
|
||||||
|
source.position(limit);
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
source.limit(originalLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toHex(byte[] value) {
|
||||||
|
StringBuilder sb = new StringBuilder(value.length * 2);
|
||||||
|
int len = value.length;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
int hi = (value[i] & 0xff) >>> 4;
|
||||||
|
int lo = value[i] & 0x0f;
|
||||||
|
sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException {
|
||||||
|
if (source.remaining() < 4) {
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Remaining buffer too short to contain length of length-prefixed field"
|
||||||
|
+ ". Remaining: " + source.remaining());
|
||||||
|
}
|
||||||
|
int len = source.getInt();
|
||||||
|
if (len < 0) {
|
||||||
|
throw new IllegalArgumentException("Negative length");
|
||||||
|
} else if (len > source.remaining()) {
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Length-prefixed field longer than remaining buffer"
|
||||||
|
+ ". Field length: " + len + ", remaining: " + source.remaining());
|
||||||
|
}
|
||||||
|
return getByteBuffer(source, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException {
|
||||||
|
int len = buf.getInt();
|
||||||
|
if (len < 0) {
|
||||||
|
throw new ApkFormatException("Negative length");
|
||||||
|
} else if (len > buf.remaining()) {
|
||||||
|
throw new ApkFormatException(
|
||||||
|
"Underflow while reading length-prefixed value. Length: " + len
|
||||||
|
+ ", available: " + buf.remaining());
|
||||||
|
}
|
||||||
|
byte[] result = new byte[len];
|
||||||
|
buf.get(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
List<Pair<Integer, byte[]>> sequence) {
|
||||||
|
int resultSize = 0;
|
||||||
|
for (Pair<Integer, byte[]> element : sequence) {
|
||||||
|
resultSize += 12 + element.getSecond().length;
|
||||||
|
}
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(resultSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
for (Pair<Integer, byte[]> element : sequence) {
|
||||||
|
byte[] second = element.getSecond();
|
||||||
|
result.putInt(8 + second.length);
|
||||||
|
result.putInt(element.getFirst());
|
||||||
|
result.putInt(second.length);
|
||||||
|
result.put(second);
|
||||||
|
}
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of a supported signature for an APK.
|
||||||
|
*/
|
||||||
|
public class ApkSupportedSignature {
|
||||||
|
public final SignatureAlgorithm algorithm;
|
||||||
|
public final byte[] signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new supported signature using the provided {@code algorithm} and {@code
|
||||||
|
* signature} bytes.
|
||||||
|
*/
|
||||||
|
public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
/** APK Signature Scheme v2 content digest algorithm. */
|
||||||
|
public enum ContentDigestAlgorithm {
|
||||||
|
/** SHA2-256 over 1 MB chunks. */
|
||||||
|
CHUNKED_SHA256(1, "SHA-256", 256 / 8),
|
||||||
|
|
||||||
|
/** SHA2-512 over 1 MB chunks. */
|
||||||
|
CHUNKED_SHA512(2, "SHA-512", 512 / 8),
|
||||||
|
|
||||||
|
/** SHA2-256 over 4 KB chunks for APK verity. */
|
||||||
|
VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8),
|
||||||
|
|
||||||
|
/** Non-chunk SHA2-256. */
|
||||||
|
SHA256(4, "SHA-256", 256 / 8);
|
||||||
|
|
||||||
|
private final int mId;
|
||||||
|
private final String mJcaMessageDigestAlgorithm;
|
||||||
|
private final int mChunkDigestOutputSizeBytes;
|
||||||
|
|
||||||
|
private ContentDigestAlgorithm(
|
||||||
|
int id, String jcaMessageDigestAlgorithm, int chunkDigestOutputSizeBytes) {
|
||||||
|
mId = id;
|
||||||
|
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgorithm;
|
||||||
|
mChunkDigestOutputSizeBytes = chunkDigestOutputSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the ID of the digest algorithm used on the APK. */
|
||||||
|
public int getId() {
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link java.security.MessageDigest} algorithm used for computing digests of
|
||||||
|
* chunks by this content digest algorithm.
|
||||||
|
*/
|
||||||
|
String getJcaMessageDigestAlgorithm() {
|
||||||
|
return mJcaMessageDigestAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the size (in bytes) of the digest of a chunk of content. */
|
||||||
|
int getChunkDigestOutputSizeBytes() {
|
||||||
|
return mChunkDigestOutputSizeBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base exception that is thrown when there are no signatures that support the full range of
|
||||||
|
* requested platform versions.
|
||||||
|
*/
|
||||||
|
public class NoApkSupportedSignaturesException extends Exception {
|
||||||
|
public NoApkSupportedSignaturesException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.security.spec.MGF1ParameterSpec;
|
||||||
|
import java.security.spec.PSSParameterSpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signing Block signature algorithm.
|
||||||
|
*/
|
||||||
|
public enum SignatureAlgorithm {
|
||||||
|
// TODO reserve the 0x0000 ID to mean null
|
||||||
|
/**
|
||||||
|
* RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc, content
|
||||||
|
* digested using SHA2-256 in 1 MB chunks.
|
||||||
|
*/
|
||||||
|
RSA_PSS_WITH_SHA256(
|
||||||
|
0x0101,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA256withRSA/PSS",
|
||||||
|
new PSSParameterSpec(
|
||||||
|
"SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.M),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content
|
||||||
|
* digested using SHA2-512 in 1 MB chunks.
|
||||||
|
*/
|
||||||
|
RSA_PSS_WITH_SHA512(
|
||||||
|
0x0102,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||||
|
"RSA",
|
||||||
|
Pair.of(
|
||||||
|
"SHA512withRSA/PSS",
|
||||||
|
new PSSParameterSpec(
|
||||||
|
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.M),
|
||||||
|
|
||||||
|
/** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||||
|
RSA_PKCS1_V1_5_WITH_SHA256(
|
||||||
|
0x0103,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA256withRSA", null),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.INITIAL_RELEASE),
|
||||||
|
|
||||||
|
/** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||||
|
RSA_PKCS1_V1_5_WITH_SHA512(
|
||||||
|
0x0104,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA512withRSA", null),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.INITIAL_RELEASE),
|
||||||
|
|
||||||
|
/** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||||
|
ECDSA_WITH_SHA256(
|
||||||
|
0x0201,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"EC",
|
||||||
|
Pair.of("SHA256withECDSA", null),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.HONEYCOMB),
|
||||||
|
|
||||||
|
/** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */
|
||||||
|
ECDSA_WITH_SHA512(
|
||||||
|
0x0202,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA512,
|
||||||
|
"EC",
|
||||||
|
Pair.of("SHA512withECDSA", null),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.HONEYCOMB),
|
||||||
|
|
||||||
|
/** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */
|
||||||
|
DSA_WITH_SHA256(
|
||||||
|
0x0301,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"DSA",
|
||||||
|
Pair.of("SHA256withDSA", null),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.INITIAL_RELEASE),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done
|
||||||
|
* deterministically according to RFC 6979.
|
||||||
|
*/
|
||||||
|
DETDSA_WITH_SHA256(
|
||||||
|
0x0301,
|
||||||
|
ContentDigestAlgorithm.CHUNKED_SHA256,
|
||||||
|
"DSA",
|
||||||
|
Pair.of("SHA256withDetDSA", null),
|
||||||
|
AndroidSdkVersion.N,
|
||||||
|
AndroidSdkVersion.INITIAL_RELEASE),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in
|
||||||
|
* the same way fsverity operates. This digest and the content length (before digestion, 8 bytes
|
||||||
|
* in little endian) construct the final digest.
|
||||||
|
*/
|
||||||
|
VERITY_RSA_PKCS1_V1_5_WITH_SHA256(
|
||||||
|
0x0421,
|
||||||
|
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||||
|
"RSA",
|
||||||
|
Pair.of("SHA256withRSA", null),
|
||||||
|
AndroidSdkVersion.P,
|
||||||
|
AndroidSdkVersion.INITIAL_RELEASE),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||||
|
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||||
|
* endian) construct the final digest.
|
||||||
|
*/
|
||||||
|
VERITY_ECDSA_WITH_SHA256(
|
||||||
|
0x0423,
|
||||||
|
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||||
|
"EC",
|
||||||
|
Pair.of("SHA256withECDSA", null),
|
||||||
|
AndroidSdkVersion.P,
|
||||||
|
AndroidSdkVersion.HONEYCOMB),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way
|
||||||
|
* fsverity operates. This digest and the content length (before digestion, 8 bytes in little
|
||||||
|
* endian) construct the final digest.
|
||||||
|
*/
|
||||||
|
VERITY_DSA_WITH_SHA256(
|
||||||
|
0x0425,
|
||||||
|
ContentDigestAlgorithm.VERITY_CHUNKED_SHA256,
|
||||||
|
"DSA",
|
||||||
|
Pair.of("SHA256withDSA", null),
|
||||||
|
AndroidSdkVersion.P,
|
||||||
|
AndroidSdkVersion.INITIAL_RELEASE);
|
||||||
|
|
||||||
|
private final int mId;
|
||||||
|
private final String mJcaKeyAlgorithm;
|
||||||
|
private final ContentDigestAlgorithm mContentDigestAlgorithm;
|
||||||
|
private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams;
|
||||||
|
private final int mMinSdkVersion;
|
||||||
|
private final int mJcaSigAlgMinSdkVersion;
|
||||||
|
|
||||||
|
SignatureAlgorithm(int id,
|
||||||
|
ContentDigestAlgorithm contentDigestAlgorithm,
|
||||||
|
String jcaKeyAlgorithm,
|
||||||
|
Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams,
|
||||||
|
int minSdkVersion,
|
||||||
|
int jcaSigAlgMinSdkVersion) {
|
||||||
|
mId = id;
|
||||||
|
mContentDigestAlgorithm = contentDigestAlgorithm;
|
||||||
|
mJcaKeyAlgorithm = jcaKeyAlgorithm;
|
||||||
|
mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams;
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of this signature algorithm as used in APK Signature Scheme v2 wire format.
|
||||||
|
*/
|
||||||
|
public int getId() {
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content digest algorithm associated with this signature algorithm.
|
||||||
|
*/
|
||||||
|
public ContentDigestAlgorithm getContentDigestAlgorithm() {
|
||||||
|
return mContentDigestAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JCA {@link java.security.Key} algorithm used by this signature scheme.
|
||||||
|
*/
|
||||||
|
public String getJcaKeyAlgorithm() {
|
||||||
|
return mJcaKeyAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link java.security.Signature} algorithm and the {@link AlgorithmParameterSpec}
|
||||||
|
* (or null if not needed) to parameterize the {@code Signature}.
|
||||||
|
*/
|
||||||
|
public Pair<String, ? extends AlgorithmParameterSpec> getJcaSignatureAlgorithmAndParams() {
|
||||||
|
return mJcaSignatureAlgAndParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinSdkVersion() {
|
||||||
|
return mMinSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the minimum SDK version that supports the JCA signature algorithm.
|
||||||
|
*/
|
||||||
|
public int getJcaSigAlgMinSdkVersion() {
|
||||||
|
return mJcaSigAlgMinSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SignatureAlgorithm findById(int id) {
|
||||||
|
for (SignatureAlgorithm alg : SignatureAlgorithm.values()) {
|
||||||
|
if (alg.getId() == id) {
|
||||||
|
return alg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme block and additional information relevant to verifying the signatures
|
||||||
|
* contained in the block against the file.
|
||||||
|
*/
|
||||||
|
public class SignatureInfo {
|
||||||
|
/** Contents of APK Signature Scheme block. */
|
||||||
|
public final ByteBuffer signatureBlock;
|
||||||
|
|
||||||
|
/** Position of the APK Signing Block in the file. */
|
||||||
|
public final long apkSigningBlockOffset;
|
||||||
|
|
||||||
|
/** Position of the ZIP Central Directory in the file. */
|
||||||
|
public final long centralDirOffset;
|
||||||
|
|
||||||
|
/** Position of the ZIP End of Central Directory (EoCD) in the file. */
|
||||||
|
public final long eocdOffset;
|
||||||
|
|
||||||
|
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
|
||||||
|
public final ByteBuffer eocd;
|
||||||
|
|
||||||
|
public SignatureInfo(
|
||||||
|
ByteBuffer signatureBlock,
|
||||||
|
long apkSigningBlockOffset,
|
||||||
|
long centralDirOffset,
|
||||||
|
long eocdOffset,
|
||||||
|
ByteBuffer eocd) {
|
||||||
|
this.signatureBlock = signatureBlock;
|
||||||
|
this.apkSigningBlockOffset = apkSigningBlockOffset;
|
||||||
|
this.centralDirOffset = centralDirOffset;
|
||||||
|
this.eocdOffset = eocdOffset;
|
||||||
|
this.eocd = eocd;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base exception that is thrown when the APK is not signed with the requested signature scheme.
|
||||||
|
*/
|
||||||
|
public class SignatureNotFoundException extends Exception {
|
||||||
|
public SignatureNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SignatureNotFoundException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.stamp;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */
|
||||||
|
public class SourceStampCertificateLineage {
|
||||||
|
|
||||||
|
private final static int FIRST_VERSION = 1;
|
||||||
|
private final static int CURRENT_VERSION = FIRST_VERSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes the binary representation of a SourceStampCertificateLineage. Also
|
||||||
|
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
|
||||||
|
* parent.
|
||||||
|
*/
|
||||||
|
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
|
||||||
|
throws IOException {
|
||||||
|
List<SigningCertificateNode> result = new ArrayList<>();
|
||||||
|
int nodeCount = 0;
|
||||||
|
if (inputBytes == null || !inputBytes.hasRemaining()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes);
|
||||||
|
|
||||||
|
CertificateFactory certFactory;
|
||||||
|
try {
|
||||||
|
certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * uint32: version code
|
||||||
|
// * sequence of length-prefixed (uint32): nodes
|
||||||
|
// * length-prefixed bytes: signed data
|
||||||
|
// * length-prefixed bytes: certificate
|
||||||
|
// * uint32: signature algorithm id
|
||||||
|
// * uint32: flags
|
||||||
|
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||||
|
// * length-prefixed bytes: signature over above signed data
|
||||||
|
|
||||||
|
X509Certificate lastCert = null;
|
||||||
|
int lastSigAlgorithmId = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int version = inputBytes.getInt();
|
||||||
|
if (version != CURRENT_VERSION) {
|
||||||
|
// we only have one version to worry about right now, so just check it
|
||||||
|
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
|
||||||
|
+ " different than any of which we are aware");
|
||||||
|
}
|
||||||
|
HashSet<X509Certificate> certHistorySet = new HashSet<>();
|
||||||
|
while (inputBytes.hasRemaining()) {
|
||||||
|
nodeCount++;
|
||||||
|
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
|
||||||
|
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
|
||||||
|
int flags = nodeBytes.getInt();
|
||||||
|
int sigAlgorithmId = nodeBytes.getInt();
|
||||||
|
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
|
||||||
|
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
|
||||||
|
|
||||||
|
if (lastCert != null) {
|
||||||
|
// Use previous level cert to verify current level
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
PublicKey publicKey = lastCert.getPublicKey();
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
sig.update(signedData);
|
||||||
|
if (!sig.verify(signature)) {
|
||||||
|
throw new SecurityException("Unable to verify signature of certificate #"
|
||||||
|
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
|
||||||
|
+ " SourceStampCertificateLineage object");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData.rewind();
|
||||||
|
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
|
||||||
|
int signedSigAlgorithm = signedData.getInt();
|
||||||
|
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
|
||||||
|
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
|
||||||
|
+ nodeBytes + " when verifying SourceStampCertificateLineage object");
|
||||||
|
}
|
||||||
|
lastCert = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(encodedCert));
|
||||||
|
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
|
||||||
|
if (certHistorySet.contains(lastCert)) {
|
||||||
|
throw new SecurityException("Encountered duplicate entries in "
|
||||||
|
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
|
||||||
|
+ "signing certificates should be unique");
|
||||||
|
}
|
||||||
|
certHistorySet.add(lastCert);
|
||||||
|
lastSigAlgorithmId = sigAlgorithmId;
|
||||||
|
result.add(new SigningCertificateNode(
|
||||||
|
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
|
||||||
|
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
|
||||||
|
}
|
||||||
|
} catch(ApkFormatException | BufferUnderflowException e){
|
||||||
|
throw new IOException("Failed to parse SourceStampCertificateLineage object", e);
|
||||||
|
} catch(NoSuchAlgorithmException | InvalidKeyException
|
||||||
|
| InvalidAlgorithmParameterException | SignatureException e){
|
||||||
|
throw new SecurityException(
|
||||||
|
"Failed to verify signature over signed data for certificate #" + nodeCount
|
||||||
|
+ " when parsing SourceStampCertificateLineage object", e);
|
||||||
|
} catch(CertificateException e){
|
||||||
|
throw new SecurityException("Failed to decode certificate #" + nodeCount
|
||||||
|
+ " when parsing SourceStampCertificateLineage object", e);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents one signing certificate in the SourceStampCertificateLineage, which
|
||||||
|
* generally means it is/was used at some point to sign source stamps.
|
||||||
|
*/
|
||||||
|
public static class SigningCertificateNode {
|
||||||
|
|
||||||
|
public SigningCertificateNode(
|
||||||
|
X509Certificate signingCert,
|
||||||
|
SignatureAlgorithm parentSigAlgorithm,
|
||||||
|
SignatureAlgorithm sigAlgorithm,
|
||||||
|
byte[] signature,
|
||||||
|
int flags) {
|
||||||
|
this.signingCert = signingCert;
|
||||||
|
this.parentSigAlgorithm = parentSigAlgorithm;
|
||||||
|
this.sigAlgorithm = sigAlgorithm;
|
||||||
|
this.signature = signature;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof SigningCertificateNode)) return false;
|
||||||
|
|
||||||
|
SigningCertificateNode that = (SigningCertificateNode) o;
|
||||||
|
if (!signingCert.equals(that.signingCert)) return false;
|
||||||
|
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
|
||||||
|
if (sigAlgorithm != that.sigAlgorithm) return false;
|
||||||
|
if (!Arrays.equals(signature, that.signature)) return false;
|
||||||
|
if (flags != that.flags) return false;
|
||||||
|
|
||||||
|
// we made it
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode());
|
||||||
|
result = prime * result +
|
||||||
|
((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode());
|
||||||
|
result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode());
|
||||||
|
result = prime * result + Arrays.hashCode(signature);
|
||||||
|
result = prime * result + flags;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the signing cert for this node. This is part of the data signed by the parent node.
|
||||||
|
*/
|
||||||
|
public final X509Certificate signingCert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the algorithm used by this node's parent to bless this data. Its ID value is part of
|
||||||
|
* the data signed by the parent node. {@code null} for first node.
|
||||||
|
*/
|
||||||
|
public final SignatureAlgorithm parentSigAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the algorithm used by this node to bless the next node's data. Its ID value is part
|
||||||
|
* of the signed data of the next node. {@code null} for the last node.
|
||||||
|
*/
|
||||||
|
public SignatureAlgorithm sigAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* signature over the signed data (above). The signature is from this node's parent
|
||||||
|
* signing certificate, which should correspond to the signing certificate used to sign an
|
||||||
|
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
|
||||||
|
*/
|
||||||
|
public final byte[] signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the flags detailing how the platform should treat this signing cert
|
||||||
|
*/
|
||||||
|
public int flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.stamp;
|
||||||
|
|
||||||
|
/** Constants used for source stamp signing and verification. */
|
||||||
|
public class SourceStampConstants {
|
||||||
|
private SourceStampConstants() {}
|
||||||
|
|
||||||
|
public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
|
||||||
|
public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d;
|
||||||
|
public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
|
||||||
|
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
|
||||||
|
/**
|
||||||
|
* The source stamp timestamp attribute value is an 8-byte little-endian encoded long
|
||||||
|
* representing the epoch time in seconds when the stamp block was signed. The first 8 bytes
|
||||||
|
* of the attribute value buffer will be used to read the timestamp, and any additional buffer
|
||||||
|
* space will be ignored.
|
||||||
|
*/
|
||||||
|
public static final int STAMP_TIME_ATTR_ID = 0xe43c5946;
|
||||||
|
}
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.stamp;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerificationIssue;
|
||||||
|
import com.android.apksig.Constants;
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||||
|
import com.android.apksig.internal.apk.ApkSupportedSignature;
|
||||||
|
import com.android.apksig.internal.apk.NoApkSupportedSignaturesException;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source Stamp verifier.
|
||||||
|
*
|
||||||
|
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||||
|
*
|
||||||
|
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||||
|
*
|
||||||
|
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||||
|
* block.
|
||||||
|
*/
|
||||||
|
class SourceStampVerifier {
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private SourceStampVerifier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the SourceStamp block and populates the {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over digest provided.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
|
||||||
|
* maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
public static void verifyV1SourceStamp(
|
||||||
|
ByteBuffer sourceStampBlockData,
|
||||||
|
CertificateFactory certFactory,
|
||||||
|
ApkSignerInfo result,
|
||||||
|
byte[] apkDigest,
|
||||||
|
byte[] sourceStampCertificateDigest,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws ApkFormatException, NoSuchAlgorithmException {
|
||||||
|
X509Certificate sourceStampCertificate =
|
||||||
|
verifySourceStampCertificate(
|
||||||
|
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
|
||||||
|
if (result.containsWarnings() || result.containsErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData);
|
||||||
|
verifySourceStampSignature(
|
||||||
|
apkDigest,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion,
|
||||||
|
sourceStampCertificate,
|
||||||
|
apkDigestSignatures,
|
||||||
|
result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the SourceStamp block and populates the {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over digest of multiple signature schemes provided.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the {@code [minSdkVersion,
|
||||||
|
* maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
public static void verifyV2SourceStamp(
|
||||||
|
ByteBuffer sourceStampBlockData,
|
||||||
|
CertificateFactory certFactory,
|
||||||
|
ApkSignerInfo result,
|
||||||
|
Map<Integer, byte[]> signatureSchemeApkDigests,
|
||||||
|
byte[] sourceStampCertificateDigest,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws ApkFormatException, NoSuchAlgorithmException {
|
||||||
|
X509Certificate sourceStampCertificate =
|
||||||
|
verifySourceStampCertificate(
|
||||||
|
sourceStampBlockData, certFactory, sourceStampCertificateDigest, result);
|
||||||
|
if (result.containsWarnings() || result.containsErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse signed signature schemes block.
|
||||||
|
ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData);
|
||||||
|
Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>();
|
||||||
|
while (signedSignatureSchemes.hasRemaining()) {
|
||||||
|
ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes);
|
||||||
|
int signatureSchemeId = signedSignatureScheme.getInt();
|
||||||
|
ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme);
|
||||||
|
signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest :
|
||||||
|
signatureSchemeApkDigests.entrySet()) {
|
||||||
|
// TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a
|
||||||
|
// v3.0 block must always be present with a v3.1 block is it sufficient to just use the
|
||||||
|
// v3.0 block?
|
||||||
|
if (signatureSchemeApkDigest.getKey()
|
||||||
|
== Constants.VERSION_APK_SIGNATURE_SCHEME_V31) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verifySourceStampSignature(
|
||||||
|
signatureSchemeApkDigest.getValue(),
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion,
|
||||||
|
sourceStampCertificate,
|
||||||
|
signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()),
|
||||||
|
result);
|
||||||
|
if (result.containsWarnings() || result.containsErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceStampBlockData.hasRemaining()) {
|
||||||
|
// The stamp block contains some additional attributes.
|
||||||
|
ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
|
||||||
|
ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
|
||||||
|
|
||||||
|
byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
|
||||||
|
stampAttributeData.get(stampAttributeBytes);
|
||||||
|
stampAttributeData.flip();
|
||||||
|
|
||||||
|
verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion,
|
||||||
|
sourceStampCertificate, stampAttributeDataSignatures, result);
|
||||||
|
if (result.containsErrors() || result.containsWarnings()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parseStampAttributes(stampAttributeData, sourceStampCertificate, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate verifySourceStampCertificate(
|
||||||
|
ByteBuffer sourceStampBlockData,
|
||||||
|
CertificateFactory certFactory,
|
||||||
|
byte[] sourceStampCertificateDigest,
|
||||||
|
ApkSignerInfo result)
|
||||||
|
throws NoSuchAlgorithmException, ApkFormatException {
|
||||||
|
// Parse the SourceStamp certificate.
|
||||||
|
byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData);
|
||||||
|
X509Certificate sourceStampCertificate;
|
||||||
|
try {
|
||||||
|
sourceStampCertificate = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(sourceStampEncodedCertificate));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||||
|
// form. Without this, getEncoded may return a different form from what was stored in
|
||||||
|
// the signature. This is because some X509Certificate(Factory) implementations
|
||||||
|
// re-encode certificates.
|
||||||
|
sourceStampCertificate =
|
||||||
|
new GuaranteedEncodedFormX509Certificate(
|
||||||
|
sourceStampCertificate, sourceStampEncodedCertificate);
|
||||||
|
result.certs.add(sourceStampCertificate);
|
||||||
|
// Verify the SourceStamp certificate found in the signing block is the same as the
|
||||||
|
// SourceStamp certificate found in the APK.
|
||||||
|
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||||
|
messageDigest.update(sourceStampEncodedCertificate);
|
||||||
|
byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
|
||||||
|
if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
|
||||||
|
result.addWarning(
|
||||||
|
ApkVerificationIssue
|
||||||
|
.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
|
||||||
|
toHex(sourceStampBlockCertificateDigest),
|
||||||
|
toHex(sourceStampCertificateDigest));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sourceStampCertificate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifySourceStampSignature(
|
||||||
|
byte[] data,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
X509Certificate sourceStampCertificate,
|
||||||
|
ByteBuffer signatures,
|
||||||
|
ApkSignerInfo result) {
|
||||||
|
// Parse the signatures block and identify supported signatures
|
||||||
|
int signatureCount = 0;
|
||||||
|
List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||||
|
while (signatures.hasRemaining()) {
|
||||||
|
signatureCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
||||||
|
int sigAlgorithmId = signature.getInt();
|
||||||
|
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||||
|
if (signatureAlgorithm == null) {
|
||||||
|
result.addInfoMessage(
|
||||||
|
ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM,
|
||||||
|
sigAlgorithmId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
supportedSignatures.add(
|
||||||
|
new ApkSupportedSignature(signatureAlgorithm, sigBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addWarning(
|
||||||
|
ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (supportedSignatures.isEmpty()) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Verify signatures over digests using the SourceStamp's certificate.
|
||||||
|
List<ApkSupportedSignature> signaturesToVerify;
|
||||||
|
try {
|
||||||
|
signaturesToVerify =
|
||||||
|
getSignaturesToVerify(
|
||||||
|
supportedSignatures, minSdkVersion, maxSdkVersion, true);
|
||||||
|
} catch (NoApkSupportedSignaturesException e) {
|
||||||
|
// To facilitate debugging capture the signature algorithms and resulting exception in
|
||||||
|
// the warning.
|
||||||
|
StringBuilder signatureAlgorithms = new StringBuilder();
|
||||||
|
for (ApkSupportedSignature supportedSignature : supportedSignatures) {
|
||||||
|
if (signatureAlgorithms.length() > 0) {
|
||||||
|
signatureAlgorithms.append(", ");
|
||||||
|
}
|
||||||
|
signatureAlgorithms.append(supportedSignature.algorithm);
|
||||||
|
}
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE,
|
||||||
|
signatureAlgorithms.toString(), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ApkSupportedSignature signature : signaturesToVerify) {
|
||||||
|
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
PublicKey publicKey = sourceStampCertificate.getPublicKey();
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
sig.update(data);
|
||||||
|
byte[] sigBytes = signature.signature;
|
||||||
|
if (!sig.verify(sigBytes)) {
|
||||||
|
result.addWarning(
|
||||||
|
ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (InvalidKeyException
|
||||||
|
| InvalidAlgorithmParameterException
|
||||||
|
| SignatureException
|
||||||
|
| NoSuchAlgorithmException e) {
|
||||||
|
result.addWarning(
|
||||||
|
ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseStampAttributes(ByteBuffer stampAttributeData,
|
||||||
|
X509Certificate sourceStampCertificate, ApkSignerInfo result)
|
||||||
|
throws ApkFormatException {
|
||||||
|
ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
|
||||||
|
int stampAttributeCount = 0;
|
||||||
|
while (stampAttributes.hasRemaining()) {
|
||||||
|
stampAttributeCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
|
||||||
|
int id = attribute.getInt();
|
||||||
|
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||||
|
if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) {
|
||||||
|
readStampCertificateLineage(value, sourceStampCertificate, result);
|
||||||
|
} else if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) {
|
||||||
|
long timestamp = ByteBuffer.wrap(value).order(
|
||||||
|
ByteOrder.LITTLE_ENDIAN).getLong();
|
||||||
|
if (timestamp > 0) {
|
||||||
|
result.timestamp = timestamp;
|
||||||
|
} else {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP,
|
||||||
|
timestamp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.addInfoMessage(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id);
|
||||||
|
}
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE,
|
||||||
|
stampAttributeCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void readStampCertificateLineage(byte[] lineageBytes,
|
||||||
|
X509Certificate sourceStampCertificate, ApkSignerInfo result) {
|
||||||
|
try {
|
||||||
|
// SourceStampCertificateLineage is verified when built
|
||||||
|
List<SourceStampCertificateLineage.SigningCertificateNode> nodes =
|
||||||
|
SourceStampCertificateLineage.readSigningCertificateLineage(
|
||||||
|
ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN));
|
||||||
|
for (int i = 0; i < nodes.size(); i++) {
|
||||||
|
result.certificateLineage.add(nodes.get(i).signingCert);
|
||||||
|
}
|
||||||
|
// Make sure that the last cert in the chain matches this signer cert
|
||||||
|
if (!sourceStampCertificate.equals(
|
||||||
|
result.certificateLineage.get(result.certificateLineage.size() - 1))) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.stamp;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SourceStamp signer.
|
||||||
|
*
|
||||||
|
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||||
|
*
|
||||||
|
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||||
|
*
|
||||||
|
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||||
|
* block.
|
||||||
|
*
|
||||||
|
* <p>V1 of the source stamp allows signing the digest of at most one signature scheme only.
|
||||||
|
*/
|
||||||
|
public abstract class V1SourceStampSigner {
|
||||||
|
public static final int V1_SOURCE_STAMP_BLOCK_ID =
|
||||||
|
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V1SourceStampSigner() {}
|
||||||
|
|
||||||
|
public static Pair<byte[], Integer> generateSourceStampBlock(
|
||||||
|
SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo)
|
||||||
|
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
if (sourceStampSignerConfig.certificates.isEmpty()) {
|
||||||
|
throw new SignatureException("No certificates configured for signer");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||||
|
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
|
||||||
|
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
|
||||||
|
}
|
||||||
|
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||||
|
|
||||||
|
SourceStampBlock sourceStampBlock = new SourceStampBlock();
|
||||||
|
|
||||||
|
try {
|
||||||
|
sourceStampBlock.stampCertificate =
|
||||||
|
sourceStampSignerConfig.certificates.get(0).getEncoded();
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new SignatureException(
|
||||||
|
"Retrieving the encoded form of the stamp certificate failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] digestBytes =
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
|
||||||
|
sourceStampBlock.signedDigests =
|
||||||
|
ApkSigningBlockUtils.generateSignaturesOverData(
|
||||||
|
sourceStampSignerConfig, digestBytes);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
|
||||||
|
// * length-prefixed sequence of length-prefixed signatures:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: signature of signed data
|
||||||
|
byte[] sourceStampSignerBlock =
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][] {
|
||||||
|
sourceStampBlock.stampCertificate,
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
sourceStampBlock.signedDigests),
|
||||||
|
});
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed stamp block.
|
||||||
|
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
|
||||||
|
SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SourceStampBlock {
|
||||||
|
public byte[] stampCertificate;
|
||||||
|
public List<Pair<Integer, byte[]>> signedDigests;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.stamp;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerifier;
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.apk.ApkUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source Stamp verifier.
|
||||||
|
*
|
||||||
|
* <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme.
|
||||||
|
*/
|
||||||
|
public abstract class V1SourceStampVerifier {
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V1SourceStampVerifier() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
|
||||||
|
* The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is
|
||||||
|
* {@code true}. If verification fails, the result will contain errors -- see {@link
|
||||||
|
* ApkSigningBlockUtils.Result#getErrors()}.
|
||||||
|
*
|
||||||
|
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||||
|
* required cryptographic algorithm implementation is missing
|
||||||
|
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are
|
||||||
|
* found
|
||||||
|
* @throws IOException if an I/O error occurs when reading the APK
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlockUtils.Result verify(
|
||||||
|
DataSource apk,
|
||||||
|
ApkUtils.ZipSections zipSections,
|
||||||
|
byte[] sourceStampCertificateDigest,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws IOException, NoSuchAlgorithmException,
|
||||||
|
ApkSigningBlockUtils.SignatureNotFoundException {
|
||||||
|
ApkSigningBlockUtils.Result result =
|
||||||
|
new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP);
|
||||||
|
SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtils.findSignature(
|
||||||
|
apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
signatureInfo.signatureBlock,
|
||||||
|
sourceStampCertificateDigest,
|
||||||
|
apkContentDigests,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion,
|
||||||
|
result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
|
||||||
|
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
|
||||||
|
* result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for
|
||||||
|
* more information about the contract of this method.
|
||||||
|
*/
|
||||||
|
private static void verify(
|
||||||
|
ByteBuffer sourceStampBlock,
|
||||||
|
byte[] sourceStampCertificateDigest,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
ApkSigningBlockUtils.Result result)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||||
|
result.signers.add(signerInfo);
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
ByteBuffer sourceStampBlockData =
|
||||||
|
ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
|
||||||
|
byte[] digestBytes =
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
getApkDigests(apkContentDigests));
|
||||||
|
SourceStampVerifier.verifyV1SourceStamp(
|
||||||
|
sourceStampBlockData,
|
||||||
|
certFactory,
|
||||||
|
signerInfo,
|
||||||
|
digestBytes,
|
||||||
|
sourceStampCertificateDigest,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion);
|
||||||
|
result.verified = !result.containsErrors() && !result.containsWarnings();
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Pair<Integer, byte[]>> getApkDigests(
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
|
||||||
|
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||||
|
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
|
||||||
|
apkContentDigests.entrySet()) {
|
||||||
|
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
|
||||||
|
}
|
||||||
|
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||||
|
return digests;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.stamp;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
|
||||||
|
import com.android.apksig.SigningCertificateLineage;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SourceStamp signer.
|
||||||
|
*
|
||||||
|
* <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
|
||||||
|
*
|
||||||
|
* <p>The stamp is part of the APK that is protected by the signing block.
|
||||||
|
*
|
||||||
|
* <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
|
||||||
|
* block.
|
||||||
|
*
|
||||||
|
* <p>V2 of the source stamp allows signing the digests of more than one signature schemes.
|
||||||
|
*/
|
||||||
|
public class V2SourceStampSigner {
|
||||||
|
public static final int V2_SOURCE_STAMP_BLOCK_ID =
|
||||||
|
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||||
|
|
||||||
|
private final SignerConfig mSourceStampSignerConfig;
|
||||||
|
private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
|
||||||
|
private final boolean mSourceStampTimestampEnabled;
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V2SourceStampSigner(Builder builder) {
|
||||||
|
mSourceStampSignerConfig = builder.mSourceStampSignerConfig;
|
||||||
|
mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos;
|
||||||
|
mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Pair<byte[], Integer> generateSourceStampBlock(
|
||||||
|
SignerConfig sourceStampSignerConfig,
|
||||||
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos)
|
||||||
|
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
return new Builder(sourceStampSignerConfig,
|
||||||
|
signatureSchemeDigestInfos).build().generateSourceStampBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pair<byte[], Integer> generateSourceStampBlock()
|
||||||
|
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
if (mSourceStampSignerConfig.certificates.isEmpty()) {
|
||||||
|
throw new SignatureException("No certificates configured for signer");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the digests for signature schemes.
|
||||||
|
List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>();
|
||||||
|
getSignedDigestsFor(
|
||||||
|
VERSION_APK_SIGNATURE_SCHEME_V3,
|
||||||
|
mSignatureSchemeDigestInfos,
|
||||||
|
mSourceStampSignerConfig,
|
||||||
|
signatureSchemeDigests);
|
||||||
|
getSignedDigestsFor(
|
||||||
|
VERSION_APK_SIGNATURE_SCHEME_V2,
|
||||||
|
mSignatureSchemeDigestInfos,
|
||||||
|
mSourceStampSignerConfig,
|
||||||
|
signatureSchemeDigests);
|
||||||
|
getSignedDigestsFor(
|
||||||
|
VERSION_JAR_SIGNATURE_SCHEME,
|
||||||
|
mSignatureSchemeDigestInfos,
|
||||||
|
mSourceStampSignerConfig,
|
||||||
|
signatureSchemeDigests);
|
||||||
|
Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst));
|
||||||
|
|
||||||
|
SourceStampBlock sourceStampBlock = new SourceStampBlock();
|
||||||
|
|
||||||
|
try {
|
||||||
|
sourceStampBlock.stampCertificate =
|
||||||
|
mSourceStampSignerConfig.certificates.get(0).getEncoded();
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new SignatureException(
|
||||||
|
"Retrieving the encoded form of the stamp certificate failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceStampBlock.signedDigests = signatureSchemeDigests;
|
||||||
|
|
||||||
|
sourceStampBlock.stampAttributes = encodeStampAttributes(
|
||||||
|
generateStampAttributes(mSourceStampSignerConfig.signingCertificateLineage));
|
||||||
|
sourceStampBlock.signedStampAttributes =
|
||||||
|
ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig,
|
||||||
|
sourceStampBlock.stampAttributes);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
|
||||||
|
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
|
||||||
|
// * uint32: signature scheme id
|
||||||
|
// * length-prefixed bytes: signed digests for the respective signature scheme
|
||||||
|
// * length-prefixed bytes: encoded stamp attributes
|
||||||
|
// * length-prefixed sequence of length-prefixed signed stamp attributes:
|
||||||
|
// * uint32: signature algorithm id
|
||||||
|
// * length-prefixed bytes: signed stamp attributes for the respective signature algorithm
|
||||||
|
byte[] sourceStampSignerBlock =
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][]{
|
||||||
|
sourceStampBlock.stampCertificate,
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
sourceStampBlock.signedDigests),
|
||||||
|
sourceStampBlock.stampAttributes,
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
sourceStampBlock.signedStampAttributes),
|
||||||
|
});
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed stamp block.
|
||||||
|
return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock),
|
||||||
|
SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void getSignedDigestsFor(
|
||||||
|
int signatureSchemeVersion,
|
||||||
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos,
|
||||||
|
SignerConfig mSourceStampSignerConfig,
|
||||||
|
List<Pair<Integer, byte[]>> signatureSchemeDigests)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> digestInfo =
|
||||||
|
mSignatureSchemeDigestInfos.get(signatureSchemeVersion);
|
||||||
|
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||||
|
for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) {
|
||||||
|
digests.add(Pair.of(digest.getKey().getId(), digest.getValue()));
|
||||||
|
}
|
||||||
|
Collections.sort(digests, Comparator.comparing(Pair::getFirst));
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed digests:
|
||||||
|
// * uint32: digest algorithm id
|
||||||
|
// * length-prefixed bytes: digest of the respective digest algorithm
|
||||||
|
byte[] digestBytes =
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed signed digests:
|
||||||
|
// * uint32: signature algorithm id
|
||||||
|
// * length-prefixed bytes: signed digest for the respective signature algorithm
|
||||||
|
List<Pair<Integer, byte[]>> signedDigest =
|
||||||
|
ApkSigningBlockUtils.generateSignaturesOverData(
|
||||||
|
mSourceStampSignerConfig, digestBytes);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed signed signature scheme digests:
|
||||||
|
// * uint32: signature scheme id
|
||||||
|
// * length-prefixed bytes: signed digests for the respective signature scheme
|
||||||
|
signatureSchemeDigests.add(
|
||||||
|
Pair.of(
|
||||||
|
signatureSchemeVersion,
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
signedDigest)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) {
|
||||||
|
int payloadSize = 0;
|
||||||
|
for (byte[] attributeValue : stampAttributes.values()) {
|
||||||
|
// Pair size + Attribute ID + Attribute value
|
||||||
|
payloadSize += 4 + 4 + attributeValue.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * length-prefixed bytes: pair
|
||||||
|
// * uint32: ID
|
||||||
|
// * bytes: value
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(4 + payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.putInt(payloadSize);
|
||||||
|
for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) {
|
||||||
|
// Pair size
|
||||||
|
result.putInt(4 + stampAttribute.getValue().length);
|
||||||
|
result.putInt(stampAttribute.getKey());
|
||||||
|
result.put(stampAttribute.getValue());
|
||||||
|
}
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) {
|
||||||
|
HashMap<Integer, byte[]> stampAttributes = new HashMap<>();
|
||||||
|
|
||||||
|
if (mSourceStampTimestampEnabled) {
|
||||||
|
// Write the current epoch time as the timestamp for the source stamp.
|
||||||
|
long timestamp = Instant.now().getEpochSecond();
|
||||||
|
if (timestamp > 0) {
|
||||||
|
ByteBuffer attributeBuffer = ByteBuffer.allocate(8);
|
||||||
|
attributeBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
attributeBuffer.putLong(timestamp);
|
||||||
|
stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID,
|
||||||
|
attributeBuffer.array());
|
||||||
|
} else {
|
||||||
|
// The epoch time should never be <= 0, and since security decisions can potentially
|
||||||
|
// be made based on the value in the timestamp, throw an Exception to ensure the
|
||||||
|
// issues with the environment are resolved before allowing the signing.
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Received an invalid value from Instant#getTimestamp: " + timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineage != null) {
|
||||||
|
stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID,
|
||||||
|
lineage.encodeSigningCertificateLineage());
|
||||||
|
}
|
||||||
|
return stampAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SourceStampBlock {
|
||||||
|
public byte[] stampCertificate;
|
||||||
|
public List<Pair<Integer, byte[]>> signedDigests;
|
||||||
|
// Optional stamp attributes that are not required for verification.
|
||||||
|
public byte[] stampAttributes;
|
||||||
|
public List<Pair<Integer, byte[]>> signedStampAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builder of {@link V2SourceStampSigner} instances. */
|
||||||
|
public static class Builder {
|
||||||
|
private final SignerConfig mSourceStampSignerConfig;
|
||||||
|
private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos;
|
||||||
|
private boolean mSourceStampTimestampEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig}
|
||||||
|
* and the {@code signatureSchemeDigestInfos}.
|
||||||
|
*/
|
||||||
|
public Builder(SignerConfig sourceStampSignerConfig,
|
||||||
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) {
|
||||||
|
mSourceStampSignerConfig = sourceStampSignerConfig;
|
||||||
|
mSignatureSchemeDigestInfos = signatureSchemeDigestInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the source stamp should contain the timestamp attribute with the time
|
||||||
|
* at which the source stamp was signed.
|
||||||
|
*/
|
||||||
|
public Builder setSourceStampTimestampEnabled(boolean value) {
|
||||||
|
mSourceStampTimestampEnabled = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a new V2SourceStampSigner that can be used to generate a new source stamp
|
||||||
|
* block signed with the specified signing config.
|
||||||
|
*/
|
||||||
|
public V2SourceStampSigner build() {
|
||||||
|
return new V2SourceStampSigner(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.stamp;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerificationIssue;
|
||||||
|
import com.android.apksig.Constants;
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigResult;
|
||||||
|
import com.android.apksig.internal.apk.ApkSignerInfo;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.apk.SignatureNotFoundException;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.zip.ZipSections;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source Stamp verifier.
|
||||||
|
*
|
||||||
|
* <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes.
|
||||||
|
*/
|
||||||
|
public abstract class V2SourceStampVerifier {
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V2SourceStampVerifier() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's SourceStamp signatures and returns the result of verification.
|
||||||
|
* The APK must be considered verified only if {@link ApkSigResult#verified} is
|
||||||
|
* {@code true}. If verification fails, the result will contain errors -- see {@link
|
||||||
|
* ApkSigResult#getErrors()}.
|
||||||
|
*
|
||||||
|
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||||
|
* required cryptographic algorithm implementation is missing
|
||||||
|
* @throws SignatureNotFoundException if no SourceStamp signatures are
|
||||||
|
* found
|
||||||
|
* @throws IOException if an I/O error occurs when reading the APK
|
||||||
|
*/
|
||||||
|
public static ApkSigResult verify(
|
||||||
|
DataSource apk,
|
||||||
|
ZipSections zipSections,
|
||||||
|
byte[] sourceStampCertificateDigest,
|
||||||
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||||
|
ApkSigResult result =
|
||||||
|
new ApkSigResult(Constants.VERSION_SOURCE_STAMP);
|
||||||
|
SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtilsLite.findSignature(
|
||||||
|
apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
signatureInfo.signatureBlock,
|
||||||
|
sourceStampCertificateDigest,
|
||||||
|
signatureSchemeApkContentDigests,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion,
|
||||||
|
result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's SourceStamp signatures and outputs the results into the provided
|
||||||
|
* {@code result}. APK is considered verified only if there are no errors reported in the {@code
|
||||||
|
* result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for
|
||||||
|
* more information about the contract of this method.
|
||||||
|
*/
|
||||||
|
private static void verify(
|
||||||
|
ByteBuffer sourceStampBlock,
|
||||||
|
byte[] sourceStampCertificateDigest,
|
||||||
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
ApkSigResult result)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
ApkSignerInfo signerInfo = new ApkSignerInfo();
|
||||||
|
result.mSigners.add(signerInfo);
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
ByteBuffer sourceStampBlockData =
|
||||||
|
ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock);
|
||||||
|
SourceStampVerifier.verifyV2SourceStamp(
|
||||||
|
sourceStampBlockData,
|
||||||
|
certFactory,
|
||||||
|
signerInfo,
|
||||||
|
getSignatureSchemeDigests(signatureSchemeApkContentDigests),
|
||||||
|
sourceStampCertificateDigest,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion);
|
||||||
|
result.verified = !result.containsErrors() && !result.containsWarnings();
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Integer, byte[]> getSignatureSchemeDigests(
|
||||||
|
Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) {
|
||||||
|
Map<Integer, byte[]> digests = new HashMap<>();
|
||||||
|
for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>>
|
||||||
|
signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) {
|
||||||
|
List<Pair<Integer, byte[]>> apkDigests =
|
||||||
|
getApkDigests(signatureSchemeApkContentDigest.getValue());
|
||||||
|
digests.put(
|
||||||
|
signatureSchemeApkContentDigest.getKey(),
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests));
|
||||||
|
}
|
||||||
|
return digests;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Pair<Integer, byte[]>> getApkDigests(
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> apkContentDigests) {
|
||||||
|
List<Pair<Integer, byte[]>> digests = new ArrayList<>();
|
||||||
|
for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest :
|
||||||
|
apkContentDigests.entrySet()) {
|
||||||
|
digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue()));
|
||||||
|
}
|
||||||
|
Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() {
|
||||||
|
@Override
|
||||||
|
public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) {
|
||||||
|
return pair1.getFirst() - pair2.getFirst();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return digests;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.apk.v1;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest algorithm used with JAR signing (aka v1 signing scheme).
|
||||||
|
*/
|
||||||
|
public enum DigestAlgorithm {
|
||||||
|
/** SHA-1 */
|
||||||
|
SHA1("SHA-1"),
|
||||||
|
|
||||||
|
/** SHA2-256 */
|
||||||
|
SHA256("SHA-256");
|
||||||
|
|
||||||
|
private final String mJcaMessageDigestAlgorithm;
|
||||||
|
|
||||||
|
private DigestAlgorithm(String jcaMessageDigestAlgoritm) {
|
||||||
|
mJcaMessageDigestAlgorithm = jcaMessageDigestAlgoritm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link java.security.MessageDigest} algorithm represented by this digest
|
||||||
|
* algorithm.
|
||||||
|
*/
|
||||||
|
String getJcaMessageDigestAlgorithm() {
|
||||||
|
return mJcaMessageDigestAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Comparator<DigestAlgorithm> BY_STRENGTH_COMPARATOR = new StrengthComparator();
|
||||||
|
|
||||||
|
private static class StrengthComparator implements Comparator<DigestAlgorithm> {
|
||||||
|
@Override
|
||||||
|
public int compare(DigestAlgorithm a1, DigestAlgorithm a2) {
|
||||||
|
switch (a1) {
|
||||||
|
case SHA1:
|
||||||
|
switch (a2) {
|
||||||
|
case SHA1:
|
||||||
|
return 0;
|
||||||
|
case SHA256:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||||
|
|
||||||
|
case SHA256:
|
||||||
|
switch (a2) {
|
||||||
|
case SHA1:
|
||||||
|
return 1;
|
||||||
|
case SHA256:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Unsupported algorithm: " + a2);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unsupported algorithm: " + a1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.v1;
|
||||||
|
|
||||||
|
/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */
|
||||||
|
public class V1SchemeConstants {
|
||||||
|
private V1SchemeConstants() {}
|
||||||
|
|
||||||
|
public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
|
||||||
|
public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR =
|
||||||
|
"X-Android-APK-Signed";
|
||||||
|
}
|
||||||
@@ -0,0 +1,586 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.apk.v1;
|
||||||
|
|
||||||
|
import static com.android.apksig.Constants.MAX_APK_SIGNERS;
|
||||||
|
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
|
||||||
|
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid;
|
||||||
|
import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1EncodingException;
|
||||||
|
import com.android.apksig.internal.jar.ManifestWriter;
|
||||||
|
import com.android.apksig.internal.jar.SignatureFileWriter;
|
||||||
|
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK signer which uses JAR signing (aka v1 signing scheme).
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a>
|
||||||
|
*/
|
||||||
|
public abstract class V1SchemeSigner {
|
||||||
|
public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME;
|
||||||
|
|
||||||
|
private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY =
|
||||||
|
new Attributes.Name("Created-By");
|
||||||
|
private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0";
|
||||||
|
private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0";
|
||||||
|
|
||||||
|
private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME =
|
||||||
|
new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signer configuration.
|
||||||
|
*/
|
||||||
|
public static class SignerConfig {
|
||||||
|
/** Name. */
|
||||||
|
public String name;
|
||||||
|
|
||||||
|
/** Private key. */
|
||||||
|
public PrivateKey privateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Certificates, with the first certificate containing the public key corresponding to
|
||||||
|
* {@link #privateKey}.
|
||||||
|
*/
|
||||||
|
public List<X509Certificate> certificates;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest algorithm used for the signature.
|
||||||
|
*/
|
||||||
|
public DigestAlgorithm signatureDigestAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If DSA is the signing algorithm, whether or not deterministic DSA signing should be used.
|
||||||
|
*/
|
||||||
|
public boolean deterministicDsaSigning;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V1SchemeSigner() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the JAR signing digest algorithm to be used for signing an APK using the provided key.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||||
|
* AndroidManifest.xml minSdkVersion attribute)
|
||||||
|
*
|
||||||
|
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using
|
||||||
|
* JAR signing (aka v1 signature scheme)
|
||||||
|
*/
|
||||||
|
public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm(
|
||||||
|
PublicKey signingKey, int minSdkVersion) throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = signingKey.getAlgorithm();
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) {
|
||||||
|
// Prior to API Level 18, only SHA-1 can be used with RSA.
|
||||||
|
if (minSdkVersion < 18) {
|
||||||
|
return DigestAlgorithm.SHA1;
|
||||||
|
}
|
||||||
|
return DigestAlgorithm.SHA256;
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Prior to API Level 21, only SHA-1 can be used with DSA
|
||||||
|
if (minSdkVersion < 21) {
|
||||||
|
return DigestAlgorithm.SHA1;
|
||||||
|
} else {
|
||||||
|
return DigestAlgorithm.SHA256;
|
||||||
|
}
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
if (minSdkVersion < 18) {
|
||||||
|
throw new InvalidKeyException(
|
||||||
|
"ECDSA signatures only supported for minSdkVersion 18 and higher");
|
||||||
|
}
|
||||||
|
return DigestAlgorithm.SHA256;
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a safe version of the provided signer name.
|
||||||
|
*/
|
||||||
|
public static String getSafeSignerName(String name) {
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Empty name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// According to https://docs.oracle.com/javase/tutorial/deployment/jar/signing.html, the
|
||||||
|
// name must not be longer than 8 characters and may contain only A-Z, 0-9, _, and -.
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
char[] nameCharsUpperCase = name.toUpperCase(Locale.US).toCharArray();
|
||||||
|
for (int i = 0; i < Math.min(nameCharsUpperCase.length, 8); i++) {
|
||||||
|
char c = nameCharsUpperCase[i];
|
||||||
|
if (((c >= 'A') && (c <= 'Z'))
|
||||||
|
|| ((c >= '0') && (c <= '9'))
|
||||||
|
|| (c == '-')
|
||||||
|
|| (c == '_')) {
|
||||||
|
result.append(c);
|
||||||
|
} else {
|
||||||
|
result.append('_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link MessageDigest} instance corresponding to the provided digest algorithm.
|
||||||
|
*/
|
||||||
|
private static MessageDigest getMessageDigestInstance(DigestAlgorithm digestAlgorithm)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
String jcaAlgorithm = digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||||
|
return MessageDigest.getInstance(jcaAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JCA {@link MessageDigest} algorithm corresponding to the provided digest
|
||||||
|
* algorithm.
|
||||||
|
*/
|
||||||
|
public static String getJcaMessageDigestAlgorithm(DigestAlgorithm digestAlgorithm) {
|
||||||
|
return digestAlgorithm.getJcaMessageDigestAlgorithm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's
|
||||||
|
* manifest.
|
||||||
|
*/
|
||||||
|
public static boolean isJarEntryDigestNeededInManifest(String entryName) {
|
||||||
|
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File
|
||||||
|
|
||||||
|
// Entries which represent directories sould not be listed in the manifest.
|
||||||
|
if (entryName.endsWith("/")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entries outside of META-INF must be listed in the manifest.
|
||||||
|
if (!entryName.startsWith("META-INF/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Entries in subdirectories of META-INF must be listed in the manifest.
|
||||||
|
if (entryName.indexOf('/', "META-INF/".length()) != -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignored file names (case-insensitive) in META-INF directory:
|
||||||
|
// MANIFEST.MF
|
||||||
|
// *.SF
|
||||||
|
// *.RSA
|
||||||
|
// *.DSA
|
||||||
|
// *.EC
|
||||||
|
// SIG-*
|
||||||
|
String fileNameLowerCase =
|
||||||
|
entryName.substring("META-INF/".length()).toLowerCase(Locale.US);
|
||||||
|
if (("manifest.mf".equals(fileNameLowerCase))
|
||||||
|
|| (fileNameLowerCase.endsWith(".sf"))
|
||||||
|
|| (fileNameLowerCase.endsWith(".rsa"))
|
||||||
|
|| (fileNameLowerCase.endsWith(".dsa"))
|
||||||
|
|| (fileNameLowerCase.endsWith(".ec"))
|
||||||
|
|| (fileNameLowerCase.startsWith("sig-"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||||
|
* JAR entries which need to be added to the APK as part of the signature.
|
||||||
|
*
|
||||||
|
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||||
|
* must be provided.
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the source manifest is malformed
|
||||||
|
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
||||||
|
* missing
|
||||||
|
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||||
|
* cannot be used in general
|
||||||
|
* @throws SignatureException if an error occurs when computing digests of generating
|
||||||
|
* signatures
|
||||||
|
*/
|
||||||
|
public static List<Pair<String, byte[]>> sign(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
DigestAlgorithm jarEntryDigestAlgorithm,
|
||||||
|
Map<String, byte[]> jarEntryDigests,
|
||||||
|
List<Integer> apkSigningSchemeIds,
|
||||||
|
byte[] sourceManifestBytes,
|
||||||
|
String createdBy)
|
||||||
|
throws NoSuchAlgorithmException, ApkFormatException, InvalidKeyException,
|
||||||
|
CertificateException, SignatureException {
|
||||||
|
if (signerConfigs.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||||
|
}
|
||||||
|
if (signerConfigs.size() > MAX_APK_SIGNERS) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"APK Signature Scheme v1 only supports a maximum of " + MAX_APK_SIGNERS + ", "
|
||||||
|
+ signerConfigs.size() + " provided");
|
||||||
|
}
|
||||||
|
OutputManifestFile manifest =
|
||||||
|
generateManifestFile(
|
||||||
|
jarEntryDigestAlgorithm, jarEntryDigests, sourceManifestBytes);
|
||||||
|
|
||||||
|
return signManifest(
|
||||||
|
signerConfigs, jarEntryDigestAlgorithm, apkSigningSchemeIds, createdBy, manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the provided APK using JAR signing (aka v1 signature scheme) and returns the list of
|
||||||
|
* JAR entries which need to be added to the APK as part of the signature.
|
||||||
|
*
|
||||||
|
* @param signerConfigs signer configurations, one for each signer. At least one signer config
|
||||||
|
* must be provided.
|
||||||
|
*
|
||||||
|
* @throws InvalidKeyException if a signing key is not suitable for this signature scheme or
|
||||||
|
* cannot be used in general
|
||||||
|
* @throws SignatureException if an error occurs when computing digests of generating
|
||||||
|
* signatures
|
||||||
|
*/
|
||||||
|
public static List<Pair<String, byte[]>> signManifest(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
DigestAlgorithm digestAlgorithm,
|
||||||
|
List<Integer> apkSigningSchemeIds,
|
||||||
|
String createdBy,
|
||||||
|
OutputManifestFile manifest)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||||
|
SignatureException {
|
||||||
|
if (signerConfigs.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("At least one signer config must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each signer output .SF and .(RSA|DSA|EC) file, then output MANIFEST.MF.
|
||||||
|
List<Pair<String, byte[]>> signatureJarEntries =
|
||||||
|
new ArrayList<>(2 * signerConfigs.size() + 1);
|
||||||
|
byte[] sfBytes =
|
||||||
|
generateSignatureFile(apkSigningSchemeIds, digestAlgorithm, createdBy, manifest);
|
||||||
|
for (SignerConfig signerConfig : signerConfigs) {
|
||||||
|
String signerName = signerConfig.name;
|
||||||
|
byte[] signatureBlock;
|
||||||
|
try {
|
||||||
|
signatureBlock = generateSignatureBlock(signerConfig, sfBytes);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException(
|
||||||
|
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new CertificateException(
|
||||||
|
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException(
|
||||||
|
"Failed to sign using signer \"" + signerName + "\"", e);
|
||||||
|
}
|
||||||
|
signatureJarEntries.add(Pair.of("META-INF/" + signerName + ".SF", sfBytes));
|
||||||
|
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||||
|
String signatureBlockFileName =
|
||||||
|
"META-INF/" + signerName + "."
|
||||||
|
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
||||||
|
signatureJarEntries.add(
|
||||||
|
Pair.of(signatureBlockFileName, signatureBlock));
|
||||||
|
}
|
||||||
|
signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents));
|
||||||
|
return signatureJarEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the names of JAR entries which this signer will produce as part of v1 signature.
|
||||||
|
*/
|
||||||
|
public static Set<String> getOutputEntryNames(List<SignerConfig> signerConfigs) {
|
||||||
|
Set<String> result = new HashSet<>(2 * signerConfigs.size() + 1);
|
||||||
|
for (SignerConfig signerConfig : signerConfigs) {
|
||||||
|
String signerName = signerConfig.name;
|
||||||
|
result.add("META-INF/" + signerName + ".SF");
|
||||||
|
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||||
|
String signatureBlockFileName =
|
||||||
|
"META-INF/" + signerName + "."
|
||||||
|
+ publicKey.getAlgorithm().toUpperCase(Locale.US);
|
||||||
|
result.add(signatureBlockFileName);
|
||||||
|
}
|
||||||
|
result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generated and returns the {@code META-INF/MANIFEST.MF} file based on the provided (optional)
|
||||||
|
* input {@code MANIFEST.MF} and digests of JAR entries covered by the manifest.
|
||||||
|
*/
|
||||||
|
public static OutputManifestFile generateManifestFile(
|
||||||
|
DigestAlgorithm jarEntryDigestAlgorithm,
|
||||||
|
Map<String, byte[]> jarEntryDigests,
|
||||||
|
byte[] sourceManifestBytes) throws ApkFormatException {
|
||||||
|
Manifest sourceManifest = null;
|
||||||
|
if (sourceManifestBytes != null) {
|
||||||
|
try {
|
||||||
|
sourceManifest = new Manifest(new ByteArrayInputStream(sourceManifestBytes));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ApkFormatException("Malformed source META-INF/MANIFEST.MF", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ByteArrayOutputStream manifestOut = new ByteArrayOutputStream();
|
||||||
|
Attributes mainAttrs = new Attributes();
|
||||||
|
// Copy the main section from the source manifest (if provided). Otherwise use defaults.
|
||||||
|
// NOTE: We don't output our own Created-By header because this signer did not create the
|
||||||
|
// JAR/APK being signed -- the signer only adds signatures to the already existing
|
||||||
|
// JAR/APK.
|
||||||
|
if (sourceManifest != null) {
|
||||||
|
mainAttrs.putAll(sourceManifest.getMainAttributes());
|
||||||
|
} else {
|
||||||
|
mainAttrs.put(Attributes.Name.MANIFEST_VERSION, ATTRIBUTE_VALUE_MANIFEST_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ManifestWriter.writeMainSection(manifestOut, mainAttrs);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> sortedEntryNames = new ArrayList<>(jarEntryDigests.keySet());
|
||||||
|
Collections.sort(sortedEntryNames);
|
||||||
|
SortedMap<String, byte[]> invidualSectionsContents = new TreeMap<>();
|
||||||
|
String entryDigestAttributeName = getEntryDigestAttributeName(jarEntryDigestAlgorithm);
|
||||||
|
for (String entryName : sortedEntryNames) {
|
||||||
|
checkEntryNameValid(entryName);
|
||||||
|
byte[] entryDigest = jarEntryDigests.get(entryName);
|
||||||
|
Attributes entryAttrs = new Attributes();
|
||||||
|
entryAttrs.putValue(
|
||||||
|
entryDigestAttributeName,
|
||||||
|
Base64.getEncoder().encodeToString(entryDigest));
|
||||||
|
ByteArrayOutputStream sectionOut = new ByteArrayOutputStream();
|
||||||
|
byte[] sectionBytes;
|
||||||
|
try {
|
||||||
|
ManifestWriter.writeIndividualSection(sectionOut, entryName, entryAttrs);
|
||||||
|
sectionBytes = sectionOut.toByteArray();
|
||||||
|
manifestOut.write(sectionBytes);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to write in-memory MANIFEST.MF", e);
|
||||||
|
}
|
||||||
|
invidualSectionsContents.put(entryName, sectionBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputManifestFile result = new OutputManifestFile();
|
||||||
|
result.contents = manifestOut.toByteArray();
|
||||||
|
result.mainSectionAttributes = mainAttrs;
|
||||||
|
result.individualSectionsContents = invidualSectionsContents;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkEntryNameValid(String name) throws ApkFormatException {
|
||||||
|
// JAR signing spec says CR, LF, and NUL are not permitted in entry names
|
||||||
|
// CR or LF in entry names will result in malformed MANIFEST.MF and .SF files because there
|
||||||
|
// is no way to escape characters in MANIFEST.MF and .SF files. NUL can, presumably, cause
|
||||||
|
// issues when parsing using C and C++ like languages.
|
||||||
|
for (char c : name.toCharArray()) {
|
||||||
|
if ((c == '\r') || (c == '\n') || (c == 0)) {
|
||||||
|
throw new ApkFormatException(
|
||||||
|
String.format(
|
||||||
|
"Unsupported character 0x%1$02x in ZIP entry name \"%2$s\"",
|
||||||
|
(int) c,
|
||||||
|
name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class OutputManifestFile {
|
||||||
|
public byte[] contents;
|
||||||
|
public SortedMap<String, byte[]> individualSectionsContents;
|
||||||
|
public Attributes mainSectionAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateSignatureFile(
|
||||||
|
List<Integer> apkSignatureSchemeIds,
|
||||||
|
DigestAlgorithm manifestDigestAlgorithm,
|
||||||
|
String createdBy,
|
||||||
|
OutputManifestFile manifest) throws NoSuchAlgorithmException {
|
||||||
|
Manifest sf = new Manifest();
|
||||||
|
Attributes mainAttrs = sf.getMainAttributes();
|
||||||
|
mainAttrs.put(Attributes.Name.SIGNATURE_VERSION, ATTRIBUTE_VALUE_SIGNATURE_VERSION);
|
||||||
|
mainAttrs.put(ATTRIBUTE_NAME_CREATED_BY, createdBy);
|
||||||
|
if (!apkSignatureSchemeIds.isEmpty()) {
|
||||||
|
// Add APK Signature Scheme v2 (and newer) signature stripping protection.
|
||||||
|
// This attribute indicates that this APK is supposed to have been signed using one or
|
||||||
|
// more APK-specific signature schemes in addition to the standard JAR signature scheme
|
||||||
|
// used by this code. APK signature verifier should reject the APK if it does not
|
||||||
|
// contain a signature for the signature scheme the verifier prefers out of this set.
|
||||||
|
StringBuilder attrValue = new StringBuilder();
|
||||||
|
for (int id : apkSignatureSchemeIds) {
|
||||||
|
if (attrValue.length() > 0) {
|
||||||
|
attrValue.append(", ");
|
||||||
|
}
|
||||||
|
attrValue.append(String.valueOf(id));
|
||||||
|
}
|
||||||
|
mainAttrs.put(
|
||||||
|
SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME,
|
||||||
|
attrValue.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add main attribute containing the digest of MANIFEST.MF.
|
||||||
|
MessageDigest md = getMessageDigestInstance(manifestDigestAlgorithm);
|
||||||
|
mainAttrs.putValue(
|
||||||
|
getManifestDigestAttributeName(manifestDigestAlgorithm),
|
||||||
|
Base64.getEncoder().encodeToString(md.digest(manifest.contents)));
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
SignatureFileWriter.writeMainSection(out, mainAttrs);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
||||||
|
}
|
||||||
|
String entryDigestAttributeName = getEntryDigestAttributeName(manifestDigestAlgorithm);
|
||||||
|
for (Map.Entry<String, byte[]> manifestSection
|
||||||
|
: manifest.individualSectionsContents.entrySet()) {
|
||||||
|
String sectionName = manifestSection.getKey();
|
||||||
|
byte[] sectionContents = manifestSection.getValue();
|
||||||
|
byte[] sectionDigest = md.digest(sectionContents);
|
||||||
|
Attributes attrs = new Attributes();
|
||||||
|
attrs.putValue(
|
||||||
|
entryDigestAttributeName,
|
||||||
|
Base64.getEncoder().encodeToString(sectionDigest));
|
||||||
|
|
||||||
|
try {
|
||||||
|
SignatureFileWriter.writeIndividualSection(out, sectionName, attrs);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to write in-memory .SF file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A bug in the java.util.jar implementation of Android platforms up to version 1.6 will
|
||||||
|
// cause a spurious IOException to be thrown if the length of the signature file is a
|
||||||
|
// multiple of 1024 bytes. As a workaround, add an extra CRLF in this case.
|
||||||
|
if ((out.size() > 0) && ((out.size() % 1024) == 0)) {
|
||||||
|
try {
|
||||||
|
SignatureFileWriter.writeSectionDelimiter(out);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to write to ByteArrayOutputStream", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the CMS PKCS #7 signature block corresponding to the provided signature file and
|
||||||
|
* signing configuration.
|
||||||
|
*/
|
||||||
|
private static byte[] generateSignatureBlock(
|
||||||
|
SignerConfig signerConfig, byte[] signatureFileBytes)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, CertificateException,
|
||||||
|
SignatureException {
|
||||||
|
// Obtain relevant bits of signing configuration
|
||||||
|
List<X509Certificate> signerCerts = signerConfig.certificates;
|
||||||
|
X509Certificate signingCert = signerCerts.get(0);
|
||||||
|
PublicKey publicKey = signingCert.getPublicKey();
|
||||||
|
DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm;
|
||||||
|
Pair<String, AlgorithmIdentifier> signatureAlgs =
|
||||||
|
getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm,
|
||||||
|
signerConfig.deterministicDsaSigning);
|
||||||
|
String jcaSignatureAlgorithm = signatureAlgs.getFirst();
|
||||||
|
|
||||||
|
// Generate the cryptographic signature of the signature file
|
||||||
|
byte[] signatureBytes;
|
||||||
|
try {
|
||||||
|
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
signature.initSign(signerConfig.privateKey);
|
||||||
|
signature.update(signatureFileBytes);
|
||||||
|
signatureBytes = signature.sign();
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException("Failed to sign using " + jcaSignatureAlgorithm, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the signature against the public key in the signing certificate
|
||||||
|
try {
|
||||||
|
Signature signature = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
signature.initVerify(publicKey);
|
||||||
|
signature.update(signatureFileBytes);
|
||||||
|
if (!signature.verify(signatureBytes)) {
|
||||||
|
throw new SignatureException("Signature did not verify");
|
||||||
|
}
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException(
|
||||||
|
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||||
|
+ " public key from certificate",
|
||||||
|
e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException(
|
||||||
|
"Failed to verify generated " + jcaSignatureAlgorithm + " signature using"
|
||||||
|
+ " public key from certificate",
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
|
||||||
|
AlgorithmIdentifier digestAlgorithmId =
|
||||||
|
getSignerInfoDigestAlgorithmOid(digestAlgorithm);
|
||||||
|
AlgorithmIdentifier signatureAlgorithmId = signatureAlgs.getSecond();
|
||||||
|
try {
|
||||||
|
return ApkSigningBlockUtils.generatePkcs7DerEncodedMessage(
|
||||||
|
signatureBytes,
|
||||||
|
null,
|
||||||
|
signerCerts, digestAlgorithmId,
|
||||||
|
signatureAlgorithmId);
|
||||||
|
} catch (Asn1EncodingException | CertificateEncodingException ex) {
|
||||||
|
throw new SignatureException("Failed to encode signature block");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
return "SHA1-Digest";
|
||||||
|
case SHA256:
|
||||||
|
return "SHA-256-Digest";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getManifestDigestAttributeName(DigestAlgorithm digestAlgorithm) {
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
return "SHA1-Digest-Manifest";
|
||||||
|
case SHA256:
|
||||||
|
return "SHA-256-Digest-Manifest";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected content digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.v2;
|
||||||
|
|
||||||
|
/** Constants used by the V2 Signature Scheme signing and verification. */
|
||||||
|
public class V2SchemeConstants {
|
||||||
|
private V2SchemeConstants() {}
|
||||||
|
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
|
||||||
|
public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
|
||||||
|
}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.apk.v2;
|
||||||
|
|
||||||
|
import static com.android.apksig.Constants.MAX_APK_SIGNERS;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.interfaces.ECKey;
|
||||||
|
import java.security.interfaces.RSAKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v2 signer.
|
||||||
|
*
|
||||||
|
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
||||||
|
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||||
|
* uncompressed contents of ZIP entries.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||||
|
*/
|
||||||
|
public abstract class V2SchemeSigner {
|
||||||
|
/*
|
||||||
|
* The two main goals of APK Signature Scheme v2 are:
|
||||||
|
* 1. Detect any unauthorized modifications to the APK. This is achieved by making the signature
|
||||||
|
* cover every byte of the APK being signed.
|
||||||
|
* 2. Enable much faster signature and integrity verification. This is achieved by requiring
|
||||||
|
* only a minimal amount of APK parsing before the signature is verified, thus completely
|
||||||
|
* bypassing ZIP entry decompression and by making integrity verification parallelizable by
|
||||||
|
* employing a hash tree.
|
||||||
|
*
|
||||||
|
* The generated signature block is wrapped into an APK Signing Block and inserted into the
|
||||||
|
* original APK immediately before the start of ZIP Central Directory. This is to ensure that
|
||||||
|
* JAR and ZIP parsers continue to work on the signed APK. The APK Signing Block is designed for
|
||||||
|
* extensibility. For example, a future signature scheme could insert its signatures there as
|
||||||
|
* well. The contract of the APK Signing Block is that all contents outside of the block must be
|
||||||
|
* protected by signatures inside the block.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID =
|
||||||
|
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V2SchemeSigner() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the APK Signature Scheme v2 signature algorithms to be used for signing an APK using the
|
||||||
|
* provided key.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||||
|
* AndroidManifest.xml minSdkVersion attribute).
|
||||||
|
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
|
||||||
|
* Signature Scheme v2
|
||||||
|
*/
|
||||||
|
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
|
||||||
|
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = signingKey.getAlgorithm();
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||||
|
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||||
|
// changed when deterministic signature schemes are used).
|
||||||
|
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
|
||||||
|
if (modulusLengthBits <= 3072) {
|
||||||
|
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
if (verityEnabled) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// DSA is supported only with SHA-256.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(
|
||||||
|
deterministicDsaSigning ?
|
||||||
|
SignatureAlgorithm.DETDSA_WITH_SHA256 :
|
||||||
|
SignatureAlgorithm.DSA_WITH_SHA256);
|
||||||
|
if (verityEnabled) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
|
||||||
|
if (keySizeBits <= 256) {
|
||||||
|
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
|
||||||
|
if (verityEnabled) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
|
||||||
|
generateApkSignatureSchemeV2Block(RunnablesExecutor executor,
|
||||||
|
DataSource beforeCentralDir,
|
||||||
|
DataSource centralDir,
|
||||||
|
DataSource eocd,
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
boolean v3SigningEnabled)
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
|
||||||
|
SignatureException {
|
||||||
|
return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd,
|
||||||
|
signerConfigs, v3SigningEnabled, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests
|
||||||
|
generateApkSignatureSchemeV2Block(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource beforeCentralDir,
|
||||||
|
DataSource centralDir,
|
||||||
|
DataSource eocd,
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
boolean v3SigningEnabled,
|
||||||
|
List<byte[]> preservedV2SignerBlocks)
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException,
|
||||||
|
SignatureException {
|
||||||
|
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
|
||||||
|
ApkSigningBlockUtils.computeContentDigests(
|
||||||
|
executor, beforeCentralDir, centralDir, eocd, signerConfigs);
|
||||||
|
return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests(
|
||||||
|
generateApkSignatureSchemeV2Block(
|
||||||
|
digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled,
|
||||||
|
preservedV2SignerBlocks),
|
||||||
|
digestInfo.getSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block(
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> contentDigests,
|
||||||
|
boolean v3SigningEnabled,
|
||||||
|
List<byte[]> preservedV2SignerBlocks)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||||
|
|
||||||
|
if (signerConfigs.size() > MAX_APK_SIGNERS) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"APK Signature Scheme v2 only supports a maximum of " + MAX_APK_SIGNERS + ", "
|
||||||
|
+ signerConfigs.size() + " provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size());
|
||||||
|
if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) {
|
||||||
|
signerBlocks.addAll(preservedV2SignerBlocks);
|
||||||
|
}
|
||||||
|
int signerNumber = 0;
|
||||||
|
for (SignerConfig signerConfig : signerConfigs) {
|
||||||
|
signerNumber++;
|
||||||
|
byte[] signerBlock;
|
||||||
|
try {
|
||||||
|
signerBlock = generateSignerBlock(signerConfig, contentDigests, v3SigningEnabled);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||||
|
}
|
||||||
|
signerBlocks.add(signerBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair.of(
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][] {
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||||
|
}),
|
||||||
|
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateSignerBlock(
|
||||||
|
SignerConfig signerConfig,
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> contentDigests,
|
||||||
|
boolean v3SigningEnabled)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
if (signerConfig.certificates.isEmpty()) {
|
||||||
|
throw new SignatureException("No certificates configured for signer");
|
||||||
|
}
|
||||||
|
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||||
|
|
||||||
|
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
||||||
|
|
||||||
|
V2SignatureSchemeBlock.SignedData signedData = new V2SignatureSchemeBlock.SignedData();
|
||||||
|
try {
|
||||||
|
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new SignatureException("Failed to encode certificates", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<Integer, byte[]>> digests =
|
||||||
|
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
||||||
|
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||||
|
ContentDigestAlgorithm contentDigestAlgorithm =
|
||||||
|
signatureAlgorithm.getContentDigestAlgorithm();
|
||||||
|
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
||||||
|
if (contentDigest == null) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
contentDigestAlgorithm
|
||||||
|
+ " content digest for "
|
||||||
|
+ signatureAlgorithm
|
||||||
|
+ " not computed");
|
||||||
|
}
|
||||||
|
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
|
||||||
|
}
|
||||||
|
signedData.digests = digests;
|
||||||
|
signedData.additionalAttributes = generateAdditionalAttributes(v3SigningEnabled);
|
||||||
|
|
||||||
|
V2SignatureSchemeBlock.Signer signer = new V2SignatureSchemeBlock.Signer();
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed digests:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: digest of contents
|
||||||
|
// * length-prefixed sequence of certificates:
|
||||||
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||||
|
// * length-prefixed sequence of length-prefixed additional attributes:
|
||||||
|
// * uint32: ID
|
||||||
|
// * (length - 4) bytes: value
|
||||||
|
|
||||||
|
signer.signedData =
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][] {
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
signedData.digests),
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates),
|
||||||
|
signedData.additionalAttributes,
|
||||||
|
new byte[0],
|
||||||
|
});
|
||||||
|
signer.publicKey = encodedPublicKey;
|
||||||
|
signer.signatures = new ArrayList<>();
|
||||||
|
signer.signatures =
|
||||||
|
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed signed data
|
||||||
|
// * length-prefixed sequence of length-prefixed signatures:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: signature of signed data
|
||||||
|
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
||||||
|
return encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][] {
|
||||||
|
signer.signedData,
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
signer.signatures),
|
||||||
|
signer.publicKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) {
|
||||||
|
if (v3SigningEnabled) {
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * length-prefixed bytes: attribute pair
|
||||||
|
// * uint32: ID - STRIPPING_PROTECTION_ATTR_ID in this case
|
||||||
|
// * uint32: value - 3 (v3 signature scheme id) in this case
|
||||||
|
int payloadSize = 4 + 4 + 4;
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.putInt(payloadSize - 4);
|
||||||
|
result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID);
|
||||||
|
result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
|
||||||
|
return result.array();
|
||||||
|
} else {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class V2SignatureSchemeBlock {
|
||||||
|
private static final class Signer {
|
||||||
|
public byte[] signedData;
|
||||||
|
public List<Pair<Integer, byte[]>> signatures;
|
||||||
|
public byte[] publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SignedData {
|
||||||
|
public List<Pair<Integer, byte[]>> digests;
|
||||||
|
public List<byte[]> certificates;
|
||||||
|
public byte[] additionalAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,471 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.apk.v2;
|
||||||
|
|
||||||
|
import static com.android.apksig.Constants.MAX_APK_SIGNERS;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerifier.Issue;
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.apk.ApkUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v2 verifier.
|
||||||
|
*
|
||||||
|
* <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
|
||||||
|
* bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||||
|
* uncompressed contents of ZIP entries.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||||
|
*/
|
||||||
|
public abstract class V2SchemeVerifier {
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private V2SchemeVerifier() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
|
||||||
|
* verification. The APK must be considered verified only if
|
||||||
|
* {@link ApkSigningBlockUtils.Result#verified} is
|
||||||
|
* {@code true}. If verification fails, the result will contain errors -- see
|
||||||
|
* {@link ApkSigningBlockUtils.Result#getErrors()}.
|
||||||
|
*
|
||||||
|
* <p>Verification succeeds iff the APK's APK Signature Scheme v2 signatures are expected to
|
||||||
|
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
* If the APK's signature is expected to not verify on any of the specified platform versions,
|
||||||
|
* this method returns a result with one or more errors and whose
|
||||||
|
* {@code Result.verified == false}, or this method throws an exception.
|
||||||
|
*
|
||||||
|
* @throws ApkFormatException if the APK is malformed
|
||||||
|
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||||
|
* required cryptographic algorithm implementation is missing
|
||||||
|
* @throws ApkSigningBlockUtils.SignatureNotFoundException if no APK Signature Scheme v2
|
||||||
|
* signatures are found
|
||||||
|
* @throws IOException if an I/O error occurs when reading the APK
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlockUtils.Result verify(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource apk,
|
||||||
|
ApkUtils.ZipSections zipSections,
|
||||||
|
Map<Integer, String> supportedApkSigSchemeNames,
|
||||||
|
Set<Integer> foundSigSchemeIds,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws IOException, ApkFormatException, NoSuchAlgorithmException,
|
||||||
|
ApkSigningBlockUtils.SignatureNotFoundException {
|
||||||
|
ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||||
|
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||||
|
SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtils.findSignature(apk, zipSections,
|
||||||
|
V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result);
|
||||||
|
|
||||||
|
DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
|
||||||
|
DataSource centralDir =
|
||||||
|
apk.slice(
|
||||||
|
signatureInfo.centralDirOffset,
|
||||||
|
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
|
||||||
|
ByteBuffer eocd = signatureInfo.eocd;
|
||||||
|
|
||||||
|
verify(executor,
|
||||||
|
beforeApkSigningBlock,
|
||||||
|
signatureInfo.signatureBlock,
|
||||||
|
centralDir,
|
||||||
|
eocd,
|
||||||
|
supportedApkSigSchemeNames,
|
||||||
|
foundSigSchemeIds,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion,
|
||||||
|
result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's v2 signatures and outputs the results into the provided
|
||||||
|
* {@code result}. APK is considered verified only if there are no errors reported in the
|
||||||
|
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, Map,
|
||||||
|
* Set, int, int)} for more information about the contract of this method.
|
||||||
|
*
|
||||||
|
* @param result result populated by this method with interesting information about the APK,
|
||||||
|
* such as information about signers, and verification errors and warnings.
|
||||||
|
*/
|
||||||
|
private static void verify(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource beforeApkSigningBlock,
|
||||||
|
ByteBuffer apkSignatureSchemeV2Block,
|
||||||
|
DataSource centralDir,
|
||||||
|
ByteBuffer eocd,
|
||||||
|
Map<Integer, String> supportedApkSigSchemeNames,
|
||||||
|
Set<Integer> foundSigSchemeIds,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
ApkSigningBlockUtils.Result result)
|
||||||
|
throws IOException, NoSuchAlgorithmException {
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||||
|
parseSigners(
|
||||||
|
apkSignatureSchemeV2Block,
|
||||||
|
contentDigestsToVerify,
|
||||||
|
supportedApkSigSchemeNames,
|
||||||
|
foundSigSchemeIds,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion,
|
||||||
|
result);
|
||||||
|
if (result.containsErrors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ApkSigningBlockUtils.verifyIntegrity(
|
||||||
|
executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
|
||||||
|
if (!result.containsErrors()) {
|
||||||
|
result.verified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses each signer in the provided APK Signature Scheme v2 block and populates corresponding
|
||||||
|
* {@code signerInfos} of the provided {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||||
|
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||||
|
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
public static void parseSigners(
|
||||||
|
ByteBuffer apkSignatureSchemeV2Block,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||||
|
Map<Integer, String> supportedApkSigSchemeNames,
|
||||||
|
Set<Integer> foundApkSigSchemeIds,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
|
||||||
|
ByteBuffer signers;
|
||||||
|
try {
|
||||||
|
signers = ApkSigningBlockUtils.getLengthPrefixedSlice(apkSignatureSchemeV2Block);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!signers.hasRemaining()) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_SIGNERS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateFactory certFactory;
|
||||||
|
try {
|
||||||
|
certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
int signerCount = 0;
|
||||||
|
while (signers.hasRemaining()) {
|
||||||
|
int signerIndex = signerCount;
|
||||||
|
signerCount++;
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||||
|
signerInfo.index = signerIndex;
|
||||||
|
result.signers.add(signerInfo);
|
||||||
|
try {
|
||||||
|
ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers);
|
||||||
|
parseSigner(
|
||||||
|
signer,
|
||||||
|
certFactory,
|
||||||
|
signerInfo,
|
||||||
|
contentDigestsToVerify,
|
||||||
|
supportedApkSigSchemeNames,
|
||||||
|
foundApkSigSchemeIds,
|
||||||
|
minSdkVersion,
|
||||||
|
maxSdkVersion);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addError(Issue.V2_SIG_MALFORMED_SIGNER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (signerCount > MAX_APK_SIGNERS) {
|
||||||
|
result.addError(Issue.V2_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, signerCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided signer block and populates the {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} contained in this block but does not
|
||||||
|
* verify the integrity of the rest of the APK. To facilitate APK integrity verification, this
|
||||||
|
* method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the
|
||||||
|
* integrity of the APK.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
private static void parseSigner(
|
||||||
|
ByteBuffer signerBlock,
|
||||||
|
CertificateFactory certFactory,
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo result,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||||
|
Map<Integer, String> supportedApkSigSchemeNames,
|
||||||
|
Set<Integer> foundApkSigSchemeIds,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException {
|
||||||
|
ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] signedDataBytes = new byte[signedData.remaining()];
|
||||||
|
signedData.get(signedDataBytes);
|
||||||
|
signedData.flip();
|
||||||
|
result.signedData = signedDataBytes;
|
||||||
|
|
||||||
|
ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] publicKeyBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signerBlock);
|
||||||
|
|
||||||
|
// Parse the signatures block and identify supported signatures
|
||||||
|
int signatureCount = 0;
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||||
|
while (signatures.hasRemaining()) {
|
||||||
|
signatureCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures);
|
||||||
|
int sigAlgorithmId = signature.getInt();
|
||||||
|
byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature);
|
||||||
|
result.signatures.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
|
||||||
|
sigAlgorithmId, sigBytes));
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||||
|
if (signatureAlgorithm == null) {
|
||||||
|
result.addWarning(Issue.V2_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
supportedSignatures.add(
|
||||||
|
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_SIGNATURE, signatureCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.signatures.isEmpty()) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_SIGNATURES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signatures over signed-data block using the public key
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
|
||||||
|
try {
|
||||||
|
signaturesToVerify =
|
||||||
|
ApkSigningBlockUtils.getSignaturesToVerify(
|
||||||
|
supportedSignatures, minSdkVersion, maxSdkVersion);
|
||||||
|
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
|
||||||
|
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||||
|
PublicKey publicKey;
|
||||||
|
try {
|
||||||
|
publicKey =
|
||||||
|
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||||
|
new X509EncodedKeySpec(publicKeyBytes));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
signedData.position(0);
|
||||||
|
sig.update(signedData);
|
||||||
|
byte[] sigBytes = signature.signature;
|
||||||
|
if (!sig.verify(sigBytes)) {
|
||||||
|
result.addError(Issue.V2_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||||
|
contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
||||||
|
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||||
|
| SignatureException e) {
|
||||||
|
result.addError(Issue.V2_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one signature over signedData has verified. We can now parse signed-data.
|
||||||
|
signedData.position(0);
|
||||||
|
ByteBuffer digests = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||||
|
ByteBuffer certificates = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||||
|
ByteBuffer additionalAttributes = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData);
|
||||||
|
|
||||||
|
// Parse the certificates block
|
||||||
|
int certificateIndex = -1;
|
||||||
|
while (certificates.hasRemaining()) {
|
||||||
|
certificateIndex++;
|
||||||
|
byte[] encodedCert = ApkSigningBlockUtils.readLengthPrefixedByteArray(certificates);
|
||||||
|
X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_MALFORMED_CERTIFICATE,
|
||||||
|
certificateIndex,
|
||||||
|
certificateIndex + 1,
|
||||||
|
e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||||
|
// form. Without this, getEncoded may return a different form from what was stored in
|
||||||
|
// the signature. This is because some X509Certificate(Factory) implementations
|
||||||
|
// re-encode certificates.
|
||||||
|
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||||
|
result.certs.add(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.certs.isEmpty()) {
|
||||||
|
result.addError(Issue.V2_SIG_NO_CERTIFICATES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
X509Certificate mainCertificate = result.certs.get(0);
|
||||||
|
byte[] certificatePublicKeyBytes;
|
||||||
|
try {
|
||||||
|
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
|
||||||
|
mainCertificate.getPublicKey());
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
System.out.println("Caught an exception encoding the public key: " + e);
|
||||||
|
e.printStackTrace();
|
||||||
|
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||||
|
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||||
|
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the digests block
|
||||||
|
int digestCount = 0;
|
||||||
|
while (digests.hasRemaining()) {
|
||||||
|
digestCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer digest = ApkSigningBlockUtils.getLengthPrefixedSlice(digests);
|
||||||
|
int sigAlgorithmId = digest.getInt();
|
||||||
|
byte[] digestBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(digest);
|
||||||
|
result.contentDigests.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||||
|
sigAlgorithmId, digestBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
|
||||||
|
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
|
||||||
|
}
|
||||||
|
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
|
||||||
|
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
|
||||||
|
sigAlgsFromSignaturesRecord,
|
||||||
|
sigAlgsFromDigestsRecord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the additional attributes block.
|
||||||
|
int additionalAttributeCount = 0;
|
||||||
|
Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet();
|
||||||
|
Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1);
|
||||||
|
while (additionalAttributes.hasRemaining()) {
|
||||||
|
additionalAttributeCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer attribute =
|
||||||
|
ApkSigningBlockUtils.getLengthPrefixedSlice(additionalAttributes);
|
||||||
|
int id = attribute.getInt();
|
||||||
|
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||||
|
result.additionalAttributes.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
|
||||||
|
switch (id) {
|
||||||
|
case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID:
|
||||||
|
// stripping protection added when signing with a newer scheme
|
||||||
|
int foundId = ByteBuffer.wrap(value).order(
|
||||||
|
ByteOrder.LITTLE_ENDIAN).getInt();
|
||||||
|
if (supportedApkSigSchemeIds.contains(foundId)) {
|
||||||
|
supportedExpectedApkSigSchemeIds.add(foundId);
|
||||||
|
} else {
|
||||||
|
result.addWarning(
|
||||||
|
Issue.V2_SIG_UNKNOWN_APK_SIG_SCHEME_ID, result.index, foundId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
||||||
|
}
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that all known IDs indicated in stripping protection have already verified
|
||||||
|
for (int id : supportedExpectedApkSigSchemeIds) {
|
||||||
|
if (!foundApkSigSchemeIds.contains(id)) {
|
||||||
|
String apkSigSchemeName = supportedApkSigSchemeNames.get(id);
|
||||||
|
result.addError(
|
||||||
|
Issue.V2_SIG_MISSING_APK_SIG_REFERENCED,
|
||||||
|
result.index,
|
||||||
|
apkSigSchemeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.v3;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.AndroidSdkVersion;
|
||||||
|
|
||||||
|
/** Constants used by the V3 Signature Scheme signing and verification. */
|
||||||
|
public class V3SchemeConstants {
|
||||||
|
private V3SchemeConstants() {}
|
||||||
|
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
|
||||||
|
public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
|
||||||
|
|
||||||
|
public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P;
|
||||||
|
public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T;
|
||||||
|
/**
|
||||||
|
* By default, APK signing key rotation will target T, but packages that have previously
|
||||||
|
* rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the
|
||||||
|
* --rotation-min-sdk-version parameter when using apksigner or when invoking
|
||||||
|
* {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}.
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This attribute is intended to be written to the V3.0 signer block as an additional attribute
|
||||||
|
* whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If
|
||||||
|
* this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version
|
||||||
|
* for rotation in the v3.1 signing block is not X, then the APK should be rejected.
|
||||||
|
*/
|
||||||
|
public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This attribute is written to the V3.1 signer block as an additional attribute to signify that
|
||||||
|
* the rotation-min-sdk-version is targeting a development release. This is required to support
|
||||||
|
* testing rotation on new development releases as the previous platform release SDK version
|
||||||
|
* is used as the development release SDK version until the development release SDK is
|
||||||
|
* finalized.
|
||||||
|
*/
|
||||||
|
public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current development release; rotation / signing configs targeting this release should
|
||||||
|
* be written with the {@link #PROD_RELEASE} SDK version and the dev release attribute.
|
||||||
|
*/
|
||||||
|
public static final int DEV_RELEASE = AndroidSdkVersion.U;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current production release.
|
||||||
|
*/
|
||||||
|
public static final int PROD_RELEASE = AndroidSdkVersion.T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig.internal.apk.v3;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicKey;
|
||||||
|
|
||||||
|
import com.android.apksig.SigningCertificateLineage;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.interfaces.ECKey;
|
||||||
|
import java.security.interfaces.RSAKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v3 signer.
|
||||||
|
*
|
||||||
|
* <p>APK Signature Scheme v3 builds upon APK Signature Scheme v3, and maintains all of the APK
|
||||||
|
* Signature Scheme v2 goals.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||||
|
* <p>The main contribution of APK Signature Scheme v3 is the introduction of the {@link
|
||||||
|
* SigningCertificateLineage}, which enables an APK to change its signing certificate as long as
|
||||||
|
* it can prove the new siging certificate was signed by the old.
|
||||||
|
*/
|
||||||
|
public class V3SchemeSigner {
|
||||||
|
public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID =
|
||||||
|
V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||||
|
public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID;
|
||||||
|
|
||||||
|
private final RunnablesExecutor mExecutor;
|
||||||
|
private final DataSource mBeforeCentralDir;
|
||||||
|
private final DataSource mCentralDir;
|
||||||
|
private final DataSource mEocd;
|
||||||
|
private final List<SignerConfig> mSignerConfigs;
|
||||||
|
private final int mBlockId;
|
||||||
|
private final OptionalInt mOptionalV31MinSdkVersion;
|
||||||
|
private final boolean mRotationTargetsDevRelease;
|
||||||
|
|
||||||
|
private V3SchemeSigner(DataSource beforeCentralDir,
|
||||||
|
DataSource centralDir,
|
||||||
|
DataSource eocd,
|
||||||
|
List<SignerConfig> signerConfigs,
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
int blockId,
|
||||||
|
OptionalInt optionalV31MinSdkVersion,
|
||||||
|
boolean rotationTargetsDevRelease) {
|
||||||
|
mBeforeCentralDir = beforeCentralDir;
|
||||||
|
mCentralDir = centralDir;
|
||||||
|
mEocd = eocd;
|
||||||
|
mSignerConfigs = signerConfigs;
|
||||||
|
mExecutor = executor;
|
||||||
|
mBlockId = blockId;
|
||||||
|
mOptionalV31MinSdkVersion = optionalV31MinSdkVersion;
|
||||||
|
mRotationTargetsDevRelease = rotationTargetsDevRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the
|
||||||
|
* provided key.
|
||||||
|
*
|
||||||
|
* @param minSdkVersion minimum API Level of the platform on which the APK may be installed (see
|
||||||
|
* AndroidManifest.xml minSdkVersion attribute).
|
||||||
|
* @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK
|
||||||
|
* Signature Scheme v3
|
||||||
|
*/
|
||||||
|
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
|
||||||
|
int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = signingKey.getAlgorithm();
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Use RSASSA-PKCS1-v1_5 signature scheme instead of RSASSA-PSS to guarantee
|
||||||
|
// deterministic signatures which make life easier for OTA updates (fewer files
|
||||||
|
// changed when deterministic signature schemes are used).
|
||||||
|
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int modulusLengthBits = ((RSAKey) signingKey).getModulus().bitLength();
|
||||||
|
if (modulusLengthBits <= 3072) {
|
||||||
|
// 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
if (verityEnabled) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 3072 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// DSA is supported only with SHA-256.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(
|
||||||
|
deterministicDsaSigning ?
|
||||||
|
SignatureAlgorithm.DETDSA_WITH_SHA256 :
|
||||||
|
SignatureAlgorithm.DSA_WITH_SHA256);
|
||||||
|
if (verityEnabled) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
// Pick a digest which is no weaker than the key.
|
||||||
|
int keySizeBits = ((ECKey) signingKey).getParams().getOrder().bitLength();
|
||||||
|
if (keySizeBits <= 256) {
|
||||||
|
// 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit.
|
||||||
|
List<SignatureAlgorithm> algorithms = new ArrayList<>();
|
||||||
|
algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256);
|
||||||
|
if (verityEnabled) {
|
||||||
|
algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256);
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
} else {
|
||||||
|
// Keys longer than 256 bit need to be paired with a stronger digest to avoid the
|
||||||
|
// digest being the weak link. SHA-512 is the next strongest supported digest.
|
||||||
|
return Collections.singletonList(SignatureAlgorithm.ECDSA_WITH_SHA512);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource beforeCentralDir,
|
||||||
|
DataSource centralDir,
|
||||||
|
DataSource eocd,
|
||||||
|
List<SignerConfig> signerConfigs)
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
|
||||||
|
return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs)
|
||||||
|
.setRunnablesExecutor(executor)
|
||||||
|
.setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
|
||||||
|
.build()
|
||||||
|
.generateApkSignatureSchemeV3BlockAndDigests();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] generateV3SignerAttribute(
|
||||||
|
SigningCertificateLineage signingCertificateLineage) {
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * length-prefixed bytes: attribute pair
|
||||||
|
// * uint32: ID
|
||||||
|
// * bytes: value - encoded V3 SigningCertificateLineage
|
||||||
|
byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage();
|
||||||
|
int payloadSize = 4 + 4 + encodedLineage.length;
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.putInt(4 + encodedLineage.length);
|
||||||
|
result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID);
|
||||||
|
result.put(encodedLineage);
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute(
|
||||||
|
int rotationMinSdkVersion) {
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * length-prefixed bytes: attribute pair
|
||||||
|
// * uint32: ID
|
||||||
|
// * bytes: value - int32 representing minimum SDK version for rotation
|
||||||
|
int payloadSize = 4 + 4 + 4;
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.putInt(payloadSize - 4);
|
||||||
|
result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID);
|
||||||
|
result.putInt(rotationMinSdkVersion);
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] generateV31RotationTargetsDevReleaseAttribute() {
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * length-prefixed bytes: attribute pair
|
||||||
|
// * uint32: ID
|
||||||
|
// * bytes: value - No value is used for this attribute
|
||||||
|
int payloadSize = 4 + 4;
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.putInt(payloadSize - 4);
|
||||||
|
result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x
|
||||||
|
* signing scheme block and digests based on the parameters provided to the {@link Builder}.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
|
* @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is
|
||||||
|
* missing
|
||||||
|
* @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained
|
||||||
|
* @throws SignatureException if an error occurs when computing digests or generating
|
||||||
|
* signatures
|
||||||
|
*/
|
||||||
|
public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests()
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException {
|
||||||
|
Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo =
|
||||||
|
ApkSigningBlockUtils.computeContentDigests(
|
||||||
|
mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs);
|
||||||
|
return new SigningSchemeBlockAndDigests(
|
||||||
|
generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pair<byte[], Integer> generateApkSignatureSchemeV3Block(
|
||||||
|
Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed signer blocks.
|
||||||
|
List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size());
|
||||||
|
int signerNumber = 0;
|
||||||
|
for (SignerConfig signerConfig : mSignerConfigs) {
|
||||||
|
signerNumber++;
|
||||||
|
byte[] signerBlock;
|
||||||
|
try {
|
||||||
|
signerBlock = generateSignerBlock(signerConfig, contentDigests);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
throw new InvalidKeyException("Signer #" + signerNumber + " failed", e);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
throw new SignatureException("Signer #" + signerNumber + " failed", e);
|
||||||
|
}
|
||||||
|
signerBlocks.add(signerBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair.of(
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(
|
||||||
|
new byte[][] {
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(signerBlocks),
|
||||||
|
}),
|
||||||
|
mBlockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateSignerBlock(
|
||||||
|
SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
if (signerConfig.certificates.isEmpty()) {
|
||||||
|
throw new SignatureException("No certificates configured for signer");
|
||||||
|
}
|
||||||
|
PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||||
|
|
||||||
|
byte[] encodedPublicKey = encodePublicKey(publicKey);
|
||||||
|
|
||||||
|
V3SignatureSchemeBlock.SignedData signedData = new V3SignatureSchemeBlock.SignedData();
|
||||||
|
try {
|
||||||
|
signedData.certificates = encodeCertificates(signerConfig.certificates);
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new SignatureException("Failed to encode certificates", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<Integer, byte[]>> digests =
|
||||||
|
new ArrayList<>(signerConfig.signatureAlgorithms.size());
|
||||||
|
for (SignatureAlgorithm signatureAlgorithm : signerConfig.signatureAlgorithms) {
|
||||||
|
ContentDigestAlgorithm contentDigestAlgorithm =
|
||||||
|
signatureAlgorithm.getContentDigestAlgorithm();
|
||||||
|
byte[] contentDigest = contentDigests.get(contentDigestAlgorithm);
|
||||||
|
if (contentDigest == null) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
contentDigestAlgorithm
|
||||||
|
+ " content digest for "
|
||||||
|
+ signatureAlgorithm
|
||||||
|
+ " not computed");
|
||||||
|
}
|
||||||
|
digests.add(Pair.of(signatureAlgorithm.getId(), contentDigest));
|
||||||
|
}
|
||||||
|
signedData.digests = digests;
|
||||||
|
signedData.minSdkVersion = signerConfig.minSdkVersion;
|
||||||
|
signedData.maxSdkVersion = signerConfig.maxSdkVersion;
|
||||||
|
signedData.additionalAttributes = generateAdditionalAttributes(signerConfig);
|
||||||
|
|
||||||
|
V3SignatureSchemeBlock.Signer signer = new V3SignatureSchemeBlock.Signer();
|
||||||
|
|
||||||
|
signer.signedData = encodeSignedData(signedData);
|
||||||
|
|
||||||
|
signer.minSdkVersion = signerConfig.minSdkVersion;
|
||||||
|
signer.maxSdkVersion = signerConfig.maxSdkVersion;
|
||||||
|
signer.publicKey = encodedPublicKey;
|
||||||
|
signer.signatures =
|
||||||
|
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, signer.signedData);
|
||||||
|
|
||||||
|
return encodeSigner(signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) {
|
||||||
|
byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData);
|
||||||
|
byte[] signatures =
|
||||||
|
encodeAsLengthPrefixedElement(
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
signer.signatures));
|
||||||
|
byte[] publicKey = encodeAsLengthPrefixedElement(signer.publicKey);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed signed data
|
||||||
|
// * uint32: minSdkVersion
|
||||||
|
// * uint32: maxSdkVersion
|
||||||
|
// * length-prefixed sequence of length-prefixed signatures:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: signature of signed data
|
||||||
|
// * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded)
|
||||||
|
int payloadSize = signedData.length + 4 + 4 + signatures.length + publicKey.length;
|
||||||
|
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.put(signedData);
|
||||||
|
result.putInt(signer.minSdkVersion);
|
||||||
|
result.putInt(signer.maxSdkVersion);
|
||||||
|
result.put(signatures);
|
||||||
|
result.put(publicKey);
|
||||||
|
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) {
|
||||||
|
byte[] digests =
|
||||||
|
encodeAsLengthPrefixedElement(
|
||||||
|
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(
|
||||||
|
signedData.digests));
|
||||||
|
byte[] certs =
|
||||||
|
encodeAsLengthPrefixedElement(
|
||||||
|
encodeAsSequenceOfLengthPrefixedElements(signedData.certificates));
|
||||||
|
byte[] attributes = encodeAsLengthPrefixedElement(signedData.additionalAttributes);
|
||||||
|
|
||||||
|
// FORMAT:
|
||||||
|
// * length-prefixed sequence of length-prefixed digests:
|
||||||
|
// * uint32: signature algorithm ID
|
||||||
|
// * length-prefixed bytes: digest of contents
|
||||||
|
// * length-prefixed sequence of certificates:
|
||||||
|
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded).
|
||||||
|
// * uint-32: minSdkVersion
|
||||||
|
// * uint-32: maxSdkVersion
|
||||||
|
// * length-prefixed sequence of length-prefixed additional attributes:
|
||||||
|
// * uint32: ID
|
||||||
|
// * (length - 4) bytes: value
|
||||||
|
// * uint32: Proof-of-rotation ID: 0x3ba06f8c
|
||||||
|
// * length-prefixed roof-of-rotation structure
|
||||||
|
int payloadSize = digests.length + certs.length + 4 + 4 + attributes.length;
|
||||||
|
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.put(digests);
|
||||||
|
result.put(certs);
|
||||||
|
result.putInt(signedData.minSdkVersion);
|
||||||
|
result.putInt(signedData.maxSdkVersion);
|
||||||
|
result.put(attributes);
|
||||||
|
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] generateAdditionalAttributes(SignerConfig signerConfig) {
|
||||||
|
List<byte[]> attributes = new ArrayList<>();
|
||||||
|
if (signerConfig.signingCertificateLineage != null) {
|
||||||
|
attributes.add(generateV3SignerAttribute(signerConfig.signingCertificateLineage));
|
||||||
|
}
|
||||||
|
if ((mRotationTargetsDevRelease || signerConfig.signerTargetsDevRelease)
|
||||||
|
&& mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
|
||||||
|
attributes.add(generateV31RotationTargetsDevReleaseAttribute());
|
||||||
|
}
|
||||||
|
if (mOptionalV31MinSdkVersion.isPresent()
|
||||||
|
&& mBlockId == V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
|
||||||
|
attributes.add(generateV3RotationMinSdkVersionStrippingProtectionAttribute(
|
||||||
|
mOptionalV31MinSdkVersion.getAsInt()));
|
||||||
|
}
|
||||||
|
int attributesSize = attributes.stream().mapToInt(attribute -> attribute.length).sum();
|
||||||
|
byte[] attributesBuffer = new byte[attributesSize];
|
||||||
|
if (attributesSize == 0) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
int index = 0;
|
||||||
|
for (byte[] attribute : attributes) {
|
||||||
|
System.arraycopy(attribute, 0, attributesBuffer, index, attribute.length);
|
||||||
|
index += attribute.length;
|
||||||
|
}
|
||||||
|
return attributesBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class V3SignatureSchemeBlock {
|
||||||
|
private static final class Signer {
|
||||||
|
public byte[] signedData;
|
||||||
|
public int minSdkVersion;
|
||||||
|
public int maxSdkVersion;
|
||||||
|
public List<Pair<Integer, byte[]>> signatures;
|
||||||
|
public byte[] publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SignedData {
|
||||||
|
public List<Pair<Integer, byte[]>> digests;
|
||||||
|
public List<byte[]> certificates;
|
||||||
|
public int minSdkVersion;
|
||||||
|
public int maxSdkVersion;
|
||||||
|
public byte[] additionalAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builder of {@link V3SchemeSigner} instances. */
|
||||||
|
public static class Builder {
|
||||||
|
private final DataSource mBeforeCentralDir;
|
||||||
|
private final DataSource mCentralDir;
|
||||||
|
private final DataSource mEocd;
|
||||||
|
private final List<SignerConfig> mSignerConfigs;
|
||||||
|
|
||||||
|
private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED;
|
||||||
|
private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||||
|
private OptionalInt mOptionalV31MinSdkVersion = OptionalInt.empty();
|
||||||
|
private boolean mRotationTargetsDevRelease = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code
|
||||||
|
* centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to
|
||||||
|
* be used to sign the APK.
|
||||||
|
*/
|
||||||
|
public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd,
|
||||||
|
List<SignerConfig> signerConfigs) {
|
||||||
|
mBeforeCentralDir = beforeCentralDir;
|
||||||
|
mCentralDir = centralDir;
|
||||||
|
mEocd = eocd;
|
||||||
|
mSignerConfigs = signerConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests.
|
||||||
|
*/
|
||||||
|
public Builder setRunnablesExecutor(RunnablesExecutor executor) {
|
||||||
|
mExecutor = executor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code blockId} to be used for the V3 signature block.
|
||||||
|
*
|
||||||
|
* <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link
|
||||||
|
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
|
||||||
|
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
|
||||||
|
*/
|
||||||
|
public Builder setBlockId(int blockId) {
|
||||||
|
mBlockId = blockId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each
|
||||||
|
* signer's block.
|
||||||
|
*
|
||||||
|
* <p>This value provides stripping protection to ensure a v3.1 signing block with rotation
|
||||||
|
* is not modified or removed from the APK's signature block.
|
||||||
|
*/
|
||||||
|
public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
|
||||||
|
return setMinSdkVersionForV31(rotationMinSdkVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code minSdkVersion} to be written as an additional attribute in each
|
||||||
|
* signer's block.
|
||||||
|
*
|
||||||
|
* <p>This value provides the stripping protection to ensure a v3.1 signing block is not
|
||||||
|
* modified or removed from the APK's signature block.
|
||||||
|
*/
|
||||||
|
public Builder setMinSdkVersionForV31(int minSdkVersion) {
|
||||||
|
if (minSdkVersion == V3SchemeConstants.DEV_RELEASE) {
|
||||||
|
minSdkVersion = V3SchemeConstants.PROD_RELEASE;
|
||||||
|
}
|
||||||
|
mOptionalV31MinSdkVersion = OptionalInt.of(minSdkVersion);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the minimum SDK version of a signer is intended to target a development
|
||||||
|
* release; this is primarily required after the T SDK is finalized, and an APK needs to
|
||||||
|
* target U during its development cycle for rotation.
|
||||||
|
*
|
||||||
|
* <p>This is only required after the T SDK is finalized since S and earlier releases do
|
||||||
|
* not know about the V3.1 block ID, but once T is released and work begins on U, U will
|
||||||
|
* use the SDK version of T during development. A signer with a minimum SDK version of T's
|
||||||
|
* SDK version along with setting {@code enabled} to true will allow an APK to use the
|
||||||
|
* rotated key on a device running U while causing this to be bypassed for T.
|
||||||
|
*
|
||||||
|
* <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android
|
||||||
|
* Sv2), then the rotated signing key will be used in the v3.0 signing block and this call
|
||||||
|
* will be a noop.
|
||||||
|
*/
|
||||||
|
public Builder setRotationTargetsDevRelease(boolean enabled) {
|
||||||
|
mRotationTargetsDevRelease = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link V3SchemeSigner} built with the configuration provided to this
|
||||||
|
* {@code Builder}.
|
||||||
|
*/
|
||||||
|
public V3SchemeSigner build() {
|
||||||
|
return new V3SchemeSigner(mBeforeCentralDir,
|
||||||
|
mCentralDir,
|
||||||
|
mEocd,
|
||||||
|
mSignerConfigs,
|
||||||
|
mExecutor,
|
||||||
|
mBlockId,
|
||||||
|
mOptionalV31MinSdkVersion,
|
||||||
|
mRotationTargetsDevRelease);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,783 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig.internal.apk.v3;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerificationIssue;
|
||||||
|
import com.android.apksig.ApkVerifier.Issue;
|
||||||
|
import com.android.apksig.SigningCertificateLineage;
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.apk.ApkUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundException;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.RunnablesExecutor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.OptionalInt;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme v3 verifier.
|
||||||
|
*
|
||||||
|
* <p>APK Signature Scheme v3, like v2 is a whole-file signature scheme which aims to protect every
|
||||||
|
* single bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
|
||||||
|
* uncompressed contents of ZIP entries.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
|
||||||
|
*/
|
||||||
|
public class V3SchemeVerifier {
|
||||||
|
private final RunnablesExecutor mExecutor;
|
||||||
|
private final DataSource mApk;
|
||||||
|
private final ApkUtils.ZipSections mZipSections;
|
||||||
|
private final ApkSigningBlockUtils.Result mResult;
|
||||||
|
private final Set<ContentDigestAlgorithm> mContentDigestsToVerify;
|
||||||
|
private final int mMinSdkVersion;
|
||||||
|
private final int mMaxSdkVersion;
|
||||||
|
private final int mBlockId;
|
||||||
|
private final OptionalInt mOptionalRotationMinSdkVersion;
|
||||||
|
private final boolean mFullVerification;
|
||||||
|
|
||||||
|
private ByteBuffer mApkSignatureSchemeV3Block;
|
||||||
|
|
||||||
|
private V3SchemeVerifier(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource apk,
|
||||||
|
ApkUtils.ZipSections zipSections,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||||
|
ApkSigningBlockUtils.Result result,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion,
|
||||||
|
int blockId,
|
||||||
|
OptionalInt optionalRotationMinSdkVersion,
|
||||||
|
boolean fullVerification) {
|
||||||
|
mExecutor = executor;
|
||||||
|
mApk = apk;
|
||||||
|
mZipSections = zipSections;
|
||||||
|
mContentDigestsToVerify = contentDigestsToVerify;
|
||||||
|
mResult = result;
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
mMaxSdkVersion = maxSdkVersion;
|
||||||
|
mBlockId = blockId;
|
||||||
|
mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion;
|
||||||
|
mFullVerification = fullVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of
|
||||||
|
* verification. The APK must be considered verified only if
|
||||||
|
* {@link ApkSigningBlockUtils.Result#verified} is
|
||||||
|
* {@code true}. If verification fails, the result will contain errors -- see
|
||||||
|
* {@link ApkSigningBlockUtils.Result#getErrors()}.
|
||||||
|
*
|
||||||
|
* <p>Verification succeeds iff the APK's APK Signature Scheme v3 signatures are expected to
|
||||||
|
* verify on all Android platform versions in the {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
* If the APK's signature is expected to not verify on any of the specified platform versions,
|
||||||
|
* this method returns a result with one or more errors and whose
|
||||||
|
* {@code Result.verified == false}, or this method throws an exception.
|
||||||
|
*
|
||||||
|
* <p>This method only verifies the v3.0 signing block without platform targeted rotation from
|
||||||
|
* a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence
|
||||||
|
* of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}.
|
||||||
|
*
|
||||||
|
* @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a
|
||||||
|
* required cryptographic algorithm implementation is missing
|
||||||
|
* @throws SignatureNotFoundException if no APK Signature Scheme v3
|
||||||
|
* signatures are found
|
||||||
|
* @throws IOException if an I/O error occurs when reading the APK
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlockUtils.Result verify(
|
||||||
|
RunnablesExecutor executor,
|
||||||
|
DataSource apk,
|
||||||
|
ApkUtils.ZipSections zipSections,
|
||||||
|
int minSdkVersion,
|
||||||
|
int maxSdkVersion)
|
||||||
|
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||||
|
return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion)
|
||||||
|
.setRunnablesExecutor(executor)
|
||||||
|
.setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
|
||||||
|
.build()
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided APK's v3 signatures and outputs the results into the provided
|
||||||
|
* {@code result}. APK is considered verified only if there are no errors reported in the
|
||||||
|
* {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int,
|
||||||
|
* int)} for more information about the contract of this method.
|
||||||
|
*
|
||||||
|
* @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the
|
||||||
|
* APK, such as information about signers, and verification errors and warnings
|
||||||
|
*/
|
||||||
|
public ApkSigningBlockUtils.Result verify()
|
||||||
|
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||||
|
if (mApk == null || mZipSections == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"A non-null apk and zip sections must be specified to verify an APK's v3 "
|
||||||
|
+ "signatures");
|
||||||
|
}
|
||||||
|
SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
|
||||||
|
mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
|
||||||
|
|
||||||
|
DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset);
|
||||||
|
DataSource centralDir =
|
||||||
|
mApk.slice(
|
||||||
|
signatureInfo.centralDirOffset,
|
||||||
|
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
|
||||||
|
ByteBuffer eocd = signatureInfo.eocd;
|
||||||
|
|
||||||
|
parseSigners();
|
||||||
|
|
||||||
|
if (mResult.containsErrors()) {
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd,
|
||||||
|
mContentDigestsToVerify, mResult);
|
||||||
|
|
||||||
|
// make sure that the v3 signers cover the entire targeted sdk version ranges and that the
|
||||||
|
// longest SigningCertificateHistory, if present, corresponds to the newest platform
|
||||||
|
// versions
|
||||||
|
SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>();
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) {
|
||||||
|
sortedSigners.put(signer.maxSdkVersion, signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// first make sure there is neither overlap nor holes
|
||||||
|
int firstMin = 0;
|
||||||
|
int lastMax = 0;
|
||||||
|
int lastLineageSize = 0;
|
||||||
|
|
||||||
|
// while we're iterating through the signers, build up the list of lineages
|
||||||
|
List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size());
|
||||||
|
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) {
|
||||||
|
int currentMin = signer.minSdkVersion;
|
||||||
|
int currentMax = signer.maxSdkVersion;
|
||||||
|
if (firstMin == 0) {
|
||||||
|
// first round sets up our basis
|
||||||
|
firstMin = currentMin;
|
||||||
|
} else {
|
||||||
|
// A signer's minimum SDK can equal the previous signer's maximum SDK if this signer
|
||||||
|
// is targeting a development release.
|
||||||
|
if (currentMin != (lastMax + 1)
|
||||||
|
&& !(currentMin == lastMax && signerTargetsDevRelease(signer))) {
|
||||||
|
mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastMax = currentMax;
|
||||||
|
|
||||||
|
// also, while we're here, make sure that the lineage sizes only increase
|
||||||
|
if (signer.signingCertificateLineage != null) {
|
||||||
|
int currLineageSize = signer.signingCertificateLineage.size();
|
||||||
|
if (currLineageSize < lastLineageSize) {
|
||||||
|
mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastLineageSize = currLineageSize;
|
||||||
|
lineages.add(signer.signingCertificateLineage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we support our desired sdk ranges; if rotation is present in a v3.1 block
|
||||||
|
// then the max level only needs to support up to that sdk version for rotation.
|
||||||
|
if (firstMin > mMinSdkVersion
|
||||||
|
|| lastMax < (mOptionalRotationMinSdkVersion.isPresent()
|
||||||
|
? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) {
|
||||||
|
mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
mResult.signingCertificateLineage =
|
||||||
|
SigningCertificateLineage.consolidateLineages(lineages);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
mResult.addError(Issue.V3_INCONSISTENT_LINEAGES);
|
||||||
|
}
|
||||||
|
if (!mResult.containsErrors()) {
|
||||||
|
mResult.verified = true;
|
||||||
|
}
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses each signer in the provided APK Signature Scheme v3 block and populates corresponding
|
||||||
|
* {@code signerInfos} of the provided {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||||
|
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||||
|
* the expected digests of the rest of the APK (see {@code contentDigestsToVerify}).
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
public static void parseSigners(
|
||||||
|
ByteBuffer apkSignatureSchemeV3Block,
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify,
|
||||||
|
ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException {
|
||||||
|
try {
|
||||||
|
new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block)
|
||||||
|
.setResult(result)
|
||||||
|
.setContentDigestsToVerify(contentDigestsToVerify)
|
||||||
|
.setFullVerification(false)
|
||||||
|
.build()
|
||||||
|
.parseSigners();
|
||||||
|
} catch (IOException | SignatureNotFoundException e) {
|
||||||
|
// This should never occur since the apkSignatureSchemeV3Block was already provided.
|
||||||
|
throw new IllegalStateException("An exception was encountered when attempting to parse"
|
||||||
|
+ " the signers from the provided APK Signature Scheme v3 block", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses each signer in the APK Signature Scheme v3 block and populates corresponding
|
||||||
|
* {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the
|
||||||
|
* returned {@link ApkSigningBlockUtils.Result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} block contained in each signer block.
|
||||||
|
* However, this does not verify the integrity of the rest of the APK but rather simply reports
|
||||||
|
* the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}).
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the returned {@code Result} if a verification error
|
||||||
|
* is encountered when parsing the signers.
|
||||||
|
*/
|
||||||
|
public ApkSigningBlockUtils.Result parseSigners()
|
||||||
|
throws IOException, NoSuchAlgorithmException, SignatureNotFoundException {
|
||||||
|
ByteBuffer signers;
|
||||||
|
try {
|
||||||
|
if (mApkSignatureSchemeV3Block == null) {
|
||||||
|
SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult);
|
||||||
|
mApkSignatureSchemeV3Block = signatureInfo.signatureBlock;
|
||||||
|
}
|
||||||
|
signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block);
|
||||||
|
} catch (ApkFormatException e) {
|
||||||
|
mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS);
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
if (!signers.hasRemaining()) {
|
||||||
|
mResult.addError(Issue.V3_SIG_NO_SIGNERS);
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateFactory certFactory;
|
||||||
|
try {
|
||||||
|
certFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
int signerCount = 0;
|
||||||
|
while (signers.hasRemaining()) {
|
||||||
|
int signerIndex = signerCount;
|
||||||
|
signerCount++;
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signerInfo =
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||||
|
signerInfo.index = signerIndex;
|
||||||
|
mResult.signers.add(signerInfo);
|
||||||
|
try {
|
||||||
|
ByteBuffer signer = getLengthPrefixedSlice(signers);
|
||||||
|
parseSigner(signer, certFactory, signerInfo);
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER);
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided signer block and populates the {@code result}.
|
||||||
|
*
|
||||||
|
* <p>This verifies signatures over {@code signed-data} contained in this block, as well as
|
||||||
|
* the data contained therein, but does not verify the integrity of the rest of the APK. To
|
||||||
|
* facilitate APK integrity verification, this method adds the {@code contentDigestsToVerify}.
|
||||||
|
* These digests can then be used to verify the integrity of the APK.
|
||||||
|
*
|
||||||
|
* <p>This method adds one or more errors to the {@code result} if a verification error is
|
||||||
|
* expected to be encountered on an Android platform version in the
|
||||||
|
* {@code [minSdkVersion, maxSdkVersion]} range.
|
||||||
|
*/
|
||||||
|
private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory,
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo result)
|
||||||
|
throws ApkFormatException, NoSuchAlgorithmException {
|
||||||
|
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] signedDataBytes = new byte[signedData.remaining()];
|
||||||
|
signedData.get(signedDataBytes);
|
||||||
|
signedData.flip();
|
||||||
|
result.signedData = signedDataBytes;
|
||||||
|
|
||||||
|
int parsedMinSdkVersion = signerBlock.getInt();
|
||||||
|
int parsedMaxSdkVersion = signerBlock.getInt();
|
||||||
|
result.minSdkVersion = parsedMinSdkVersion;
|
||||||
|
result.maxSdkVersion = parsedMaxSdkVersion;
|
||||||
|
if (parsedMinSdkVersion < 0 || parsedMinSdkVersion > parsedMaxSdkVersion) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_INVALID_SDK_VERSIONS, parsedMinSdkVersion, parsedMaxSdkVersion);
|
||||||
|
}
|
||||||
|
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
|
||||||
|
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
|
||||||
|
|
||||||
|
// Parse the signatures block and identify supported signatures
|
||||||
|
int signatureCount = 0;
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1);
|
||||||
|
while (signatures.hasRemaining()) {
|
||||||
|
signatureCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer signature = getLengthPrefixedSlice(signatures);
|
||||||
|
int sigAlgorithmId = signature.getInt();
|
||||||
|
byte[] sigBytes = readLengthPrefixedByteArray(signature);
|
||||||
|
result.signatures.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.Signature(
|
||||||
|
sigAlgorithmId, sigBytes));
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||||
|
if (signatureAlgorithm == null) {
|
||||||
|
result.addWarning(Issue.V3_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// TODO consider dropping deprecated signatures for v3 or modifying
|
||||||
|
// getSignaturesToVerify (called below)
|
||||||
|
supportedSignatures.add(
|
||||||
|
new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_SIGNATURE, signatureCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.signatures.isEmpty()) {
|
||||||
|
result.addError(Issue.V3_SIG_NO_SIGNATURES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signatures over signed-data block using the public key
|
||||||
|
List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify = null;
|
||||||
|
try {
|
||||||
|
signaturesToVerify =
|
||||||
|
ApkSigningBlockUtils.getSignaturesToVerify(
|
||||||
|
supportedSignatures, result.minSdkVersion, result.maxSdkVersion);
|
||||||
|
} catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
|
||||||
|
result.addError(Issue.V3_SIG_NO_SUPPORTED_SIGNATURES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) {
|
||||||
|
SignatureAlgorithm signatureAlgorithm = signature.algorithm;
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||||
|
PublicKey publicKey;
|
||||||
|
try {
|
||||||
|
publicKey =
|
||||||
|
KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||||
|
new X509EncodedKeySpec(publicKeyBytes));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
signedData.position(0);
|
||||||
|
sig.update(signedData);
|
||||||
|
byte[] sigBytes = signature.signature;
|
||||||
|
if (!sig.verify(sigBytes)) {
|
||||||
|
result.addError(Issue.V3_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||||
|
mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm());
|
||||||
|
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||||
|
| SignatureException e) {
|
||||||
|
result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least one signature over signedData has verified. We can now parse signed-data.
|
||||||
|
signedData.position(0);
|
||||||
|
ByteBuffer digests = getLengthPrefixedSlice(signedData);
|
||||||
|
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
|
||||||
|
|
||||||
|
int signedMinSdkVersion = signedData.getInt();
|
||||||
|
if (signedMinSdkVersion != parsedMinSdkVersion) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_MIN_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
|
||||||
|
parsedMinSdkVersion,
|
||||||
|
signedMinSdkVersion);
|
||||||
|
}
|
||||||
|
int signedMaxSdkVersion = signedData.getInt();
|
||||||
|
if (signedMaxSdkVersion != parsedMaxSdkVersion) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_MAX_SDK_VERSION_MISMATCH_BETWEEN_SIGNER_AND_SIGNED_DATA_RECORD,
|
||||||
|
parsedMaxSdkVersion,
|
||||||
|
signedMaxSdkVersion);
|
||||||
|
}
|
||||||
|
ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
|
||||||
|
|
||||||
|
// Parse the certificates block
|
||||||
|
int certificateIndex = -1;
|
||||||
|
while (certificates.hasRemaining()) {
|
||||||
|
certificateIndex++;
|
||||||
|
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
|
||||||
|
X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
certificate = X509CertificateUtils.generateCertificate(encodedCert, certFactory);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_MALFORMED_CERTIFICATE,
|
||||||
|
certificateIndex,
|
||||||
|
certificateIndex + 1,
|
||||||
|
e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||||
|
// form. Without this, getEncoded may return a different form from what was stored in
|
||||||
|
// the signature. This is because some X509Certificate(Factory) implementations
|
||||||
|
// re-encode certificates.
|
||||||
|
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert);
|
||||||
|
result.certs.add(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.certs.isEmpty()) {
|
||||||
|
result.addError(Issue.V3_SIG_NO_CERTIFICATES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
X509Certificate mainCertificate = result.certs.get(0);
|
||||||
|
byte[] certificatePublicKeyBytes;
|
||||||
|
try {
|
||||||
|
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
|
||||||
|
mainCertificate.getPublicKey());
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
System.out.println("Caught an exception encoding the public key: " + e);
|
||||||
|
e.printStackTrace();
|
||||||
|
certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||||
|
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||||
|
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the digests block
|
||||||
|
int digestCount = 0;
|
||||||
|
while (digests.hasRemaining()) {
|
||||||
|
digestCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer digest = getLengthPrefixedSlice(digests);
|
||||||
|
int sigAlgorithmId = digest.getInt();
|
||||||
|
byte[] digestBytes = readLengthPrefixedByteArray(digest);
|
||||||
|
result.contentDigests.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||||
|
sigAlgorithmId, digestBytes));
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_DIGEST, digestCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo.Signature signature : result.signatures) {
|
||||||
|
sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
|
||||||
|
}
|
||||||
|
List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest digest : result.contentDigests) {
|
||||||
|
sigAlgsFromDigestsRecord.add(digest.getSignatureAlgorithmId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sigAlgsFromSignaturesRecord.equals(sigAlgsFromDigestsRecord)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS,
|
||||||
|
sigAlgsFromSignaturesRecord,
|
||||||
|
sigAlgsFromDigestsRecord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the additional attributes block.
|
||||||
|
int additionalAttributeCount = 0;
|
||||||
|
boolean rotationAttrFound = false;
|
||||||
|
while (additionalAttributes.hasRemaining()) {
|
||||||
|
additionalAttributeCount++;
|
||||||
|
try {
|
||||||
|
ByteBuffer attribute =
|
||||||
|
getLengthPrefixedSlice(additionalAttributes);
|
||||||
|
int id = attribute.getInt();
|
||||||
|
byte[] value = ByteBufferUtils.toByteArray(attribute);
|
||||||
|
result.additionalAttributes.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value));
|
||||||
|
if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) {
|
||||||
|
try {
|
||||||
|
// SigningCertificateLineage is verified when built
|
||||||
|
result.signingCertificateLineage =
|
||||||
|
SigningCertificateLineage.readFromV3AttributeValue(value);
|
||||||
|
// make sure that the last cert in the chain matches this signer cert
|
||||||
|
SigningCertificateLineage subLineage =
|
||||||
|
result.signingCertificateLineage.getSubLineage(result.certs.get(0));
|
||||||
|
if (result.signingCertificateLineage.size() != subLineage.size()) {
|
||||||
|
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
result.addError(Issue.V3_SIG_POR_DID_NOT_VERIFY);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
result.addError(Issue.V3_SIG_POR_CERT_MISMATCH);
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(Issue.V3_SIG_MALFORMED_LINEAGE);
|
||||||
|
}
|
||||||
|
} else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) {
|
||||||
|
rotationAttrFound = true;
|
||||||
|
// API targeting for rotation was added with V3.1; if the maxSdkVersion
|
||||||
|
// does not support v3.1 then ignore this attribute.
|
||||||
|
if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT
|
||||||
|
&& mFullVerification) {
|
||||||
|
int attrRotationMinSdkVersion = ByteBuffer.wrap(value)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN).getInt();
|
||||||
|
if (mOptionalRotationMinSdkVersion.isPresent()) {
|
||||||
|
int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
|
||||||
|
if (attrRotationMinSdkVersion != rotationMinSdkVersion) {
|
||||||
|
result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH,
|
||||||
|
attrRotationMinSdkVersion, rotationMinSdkVersion);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) {
|
||||||
|
// This attribute should only be used by a v3.1 signer to indicate rotation
|
||||||
|
// is targeting the development release that is using the SDK version of the
|
||||||
|
// previously released platform version.
|
||||||
|
if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
|
||||||
|
result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
|
||||||
|
}
|
||||||
|
} catch (ApkFormatException | BufferUnderflowException e) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V3_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) {
|
||||||
|
result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING,
|
||||||
|
mOptionalRotationMinSdkVersion.getAsInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the specified {@code signerInfo} is targeting a development release.
|
||||||
|
*/
|
||||||
|
public static boolean signerTargetsDevRelease(
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signerInfo) {
|
||||||
|
boolean result = signerInfo.additionalAttributes.stream()
|
||||||
|
.mapToInt(attribute -> attribute.getId())
|
||||||
|
.anyMatch(attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builder of {@link V3SchemeVerifier} instances. */
|
||||||
|
public static class Builder {
|
||||||
|
private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED;
|
||||||
|
private DataSource mApk;
|
||||||
|
private ApkUtils.ZipSections mZipSections;
|
||||||
|
private ByteBuffer mApkSignatureSchemeV3Block;
|
||||||
|
private Set<ContentDigestAlgorithm> mContentDigestsToVerify;
|
||||||
|
private ApkSigningBlockUtils.Result mResult;
|
||||||
|
private int mMinSdkVersion;
|
||||||
|
private int mMaxSdkVersion;
|
||||||
|
private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||||
|
private boolean mFullVerification = true;
|
||||||
|
private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
|
||||||
|
* verify the V3 signing block of the provided {@code apk} with the specified {@code
|
||||||
|
* zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}.
|
||||||
|
*/
|
||||||
|
public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion,
|
||||||
|
int maxSdkVersion) {
|
||||||
|
mApk = apk;
|
||||||
|
mZipSections = zipSections;
|
||||||
|
mMinSdkVersion = minSdkVersion;
|
||||||
|
mMaxSdkVersion = maxSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to
|
||||||
|
* parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code
|
||||||
|
* apkSignatureSchemeV3Block}.
|
||||||
|
*
|
||||||
|
* <note>Full verification of the v3 signature is not possible when instantiating a new
|
||||||
|
* {@code V3SchemeVerifier} with this method.</note>
|
||||||
|
*/
|
||||||
|
public Builder(ByteBuffer apkSignatureSchemeV3Block) {
|
||||||
|
mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests.
|
||||||
|
*/
|
||||||
|
public Builder setRunnablesExecutor(RunnablesExecutor executor) {
|
||||||
|
mExecutor = executor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the V3 {code blockId} to be verified in the provided APK.
|
||||||
|
*
|
||||||
|
* <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link
|
||||||
|
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link
|
||||||
|
* V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes.
|
||||||
|
*/
|
||||||
|
public Builder setBlockId(int blockId) {
|
||||||
|
mBlockId = blockId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional
|
||||||
|
* attribute.
|
||||||
|
*
|
||||||
|
* <p>This value can be obtained from the signers returned when verifying the v3.1 signing
|
||||||
|
* block of an APK; in the case of multiple signers targeting different SDK versions in the
|
||||||
|
* v3.1 signing block, the minimum SDK version from all the signers should be used.
|
||||||
|
*/
|
||||||
|
public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) {
|
||||||
|
mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@code result} instance to be used when returning verification results.
|
||||||
|
*
|
||||||
|
* <p>This method can be used when the caller already has a {@link
|
||||||
|
* ApkSigningBlockUtils.Result} and wants to store the verification results in this
|
||||||
|
* instance.
|
||||||
|
*/
|
||||||
|
public Builder setResult(ApkSigningBlockUtils.Result result) {
|
||||||
|
mResult = result;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the instance to be used to store the {@code contentDigestsToVerify}.
|
||||||
|
*
|
||||||
|
* <p>This method can be used when the caller needs access to the {@code
|
||||||
|
* contentDigestsToVerify} computed by this {@code V3SchemeVerifier}.
|
||||||
|
*/
|
||||||
|
public Builder setContentDigestsToVerify(
|
||||||
|
Set<ContentDigestAlgorithm> contentDigestsToVerify) {
|
||||||
|
mContentDigestsToVerify = contentDigestsToVerify;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether full verification should be performed by the {@code V3SchemeVerifier} built
|
||||||
|
* from this instance.
|
||||||
|
*
|
||||||
|
* <note>{@link #verify()} will always verify the content digests for the APK, but this
|
||||||
|
* allows verification of the rotation minimum SDK version stripping attribute to be skipped
|
||||||
|
* for scenarios where this value may not have been parsed from a V3.1 signing block (such
|
||||||
|
* as when only {@link #parseSigners()} will be invoked.</note>
|
||||||
|
*/
|
||||||
|
public Builder setFullVerification(boolean fullVerification) {
|
||||||
|
mFullVerification = fullVerification;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link V3SchemeVerifier} built with the configuration provided to this
|
||||||
|
* {@code Builder}.
|
||||||
|
*/
|
||||||
|
public V3SchemeVerifier build() {
|
||||||
|
int sigSchemeVersion;
|
||||||
|
switch (mBlockId) {
|
||||||
|
case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID:
|
||||||
|
sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||||
|
mMinSdkVersion = Math.max(mMinSdkVersion,
|
||||||
|
V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT);
|
||||||
|
break;
|
||||||
|
case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID:
|
||||||
|
sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
|
||||||
|
// V3.1 supports targeting an SDK version later than that of the initial release
|
||||||
|
// in which it is supported; allow any range for V3.1 as long as V3.0 covers the
|
||||||
|
// rest of the range.
|
||||||
|
mMinSdkVersion = mMaxSdkVersion;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x",
|
||||||
|
mBlockId));
|
||||||
|
}
|
||||||
|
if (mResult == null) {
|
||||||
|
mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion);
|
||||||
|
}
|
||||||
|
if (mContentDigestsToVerify == null) {
|
||||||
|
mContentDigestsToVerify = new HashSet<>(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
V3SchemeVerifier verifier = new V3SchemeVerifier(
|
||||||
|
mExecutor,
|
||||||
|
mApk,
|
||||||
|
mZipSections,
|
||||||
|
mContentDigestsToVerify,
|
||||||
|
mResult,
|
||||||
|
mMinSdkVersion,
|
||||||
|
mMaxSdkVersion,
|
||||||
|
mBlockId,
|
||||||
|
mOptionalRotationMinSdkVersion,
|
||||||
|
mFullVerification);
|
||||||
|
if (mApkSignatureSchemeV3Block != null) {
|
||||||
|
verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block;
|
||||||
|
}
|
||||||
|
return verifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig.internal.apk.v3;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengthPrefixedElement;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkFormatException;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signer Lineage.
|
||||||
|
*
|
||||||
|
* <p>The signer lineage contains a history of signing certificates with each ancestor attesting to
|
||||||
|
* the validity of its descendant. Each additional descendant represents a new identity that can be
|
||||||
|
* used to sign an APK, and each generation has accompanying attributes which represent how the
|
||||||
|
* APK would like to view the older signing certificates, specifically how they should be trusted in
|
||||||
|
* certain situations.
|
||||||
|
*
|
||||||
|
* <p> Its primary use is to enable APK Signing Certificate Rotation. The Android platform verifies
|
||||||
|
* the APK Signer Lineage, and if the current signing certificate for the APK is in the Signer
|
||||||
|
* Lineage, and the Lineage contains the certificate the platform associates with the APK, it will
|
||||||
|
* allow upgrades to the new certificate.
|
||||||
|
*
|
||||||
|
* @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a>
|
||||||
|
*/
|
||||||
|
public class V3SigningCertificateLineage {
|
||||||
|
|
||||||
|
private final static int FIRST_VERSION = 1;
|
||||||
|
private final static int CURRENT_VERSION = FIRST_VERSION;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes the binary representation of an {@link V3SigningCertificateLineage}. Also
|
||||||
|
* verifies that the structure is well-formed, e.g. that the signature for each node is from its
|
||||||
|
* parent.
|
||||||
|
*/
|
||||||
|
public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes)
|
||||||
|
throws IOException {
|
||||||
|
List<SigningCertificateNode> result = new ArrayList<>();
|
||||||
|
int nodeCount = 0;
|
||||||
|
if (inputBytes == null || !inputBytes.hasRemaining()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApkSigningBlockUtils.checkByteOrderLittleEndian(inputBytes);
|
||||||
|
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * uint32: version code
|
||||||
|
// * sequence of length-prefixed (uint32): nodes
|
||||||
|
// * length-prefixed bytes: signed data
|
||||||
|
// * length-prefixed bytes: certificate
|
||||||
|
// * uint32: signature algorithm id
|
||||||
|
// * uint32: flags
|
||||||
|
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||||
|
// * length-prefixed bytes: signature over above signed data
|
||||||
|
|
||||||
|
X509Certificate lastCert = null;
|
||||||
|
int lastSigAlgorithmId = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
int version = inputBytes.getInt();
|
||||||
|
if (version != CURRENT_VERSION) {
|
||||||
|
// we only have one version to worry about right now, so just check it
|
||||||
|
throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version"
|
||||||
|
+ " different than any of which we are aware");
|
||||||
|
}
|
||||||
|
HashSet<X509Certificate> certHistorySet = new HashSet<>();
|
||||||
|
while (inputBytes.hasRemaining()) {
|
||||||
|
nodeCount++;
|
||||||
|
ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes);
|
||||||
|
ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes);
|
||||||
|
int flags = nodeBytes.getInt();
|
||||||
|
int sigAlgorithmId = nodeBytes.getInt();
|
||||||
|
SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId);
|
||||||
|
byte[] signature = readLengthPrefixedByteArray(nodeBytes);
|
||||||
|
|
||||||
|
if (lastCert != null) {
|
||||||
|
// Use previous level cert to verify current level
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
PublicKey publicKey = lastCert.getPublicKey();
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
sig.update(signedData);
|
||||||
|
if (!sig.verify(signature)) {
|
||||||
|
throw new SecurityException("Unable to verify signature of certificate #"
|
||||||
|
+ nodeCount + " using " + jcaSignatureAlgorithm + " when verifying"
|
||||||
|
+ " V3SigningCertificateLineage object");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData.rewind();
|
||||||
|
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
|
||||||
|
int signedSigAlgorithm = signedData.getInt();
|
||||||
|
if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) {
|
||||||
|
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
|
||||||
|
+ nodeBytes + " when verifying V3SigningCertificateLineage object");
|
||||||
|
}
|
||||||
|
lastCert = X509CertificateUtils.generateCertificate(encodedCert);
|
||||||
|
lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert);
|
||||||
|
if (certHistorySet.contains(lastCert)) {
|
||||||
|
throw new SecurityException("Encountered duplicate entries in "
|
||||||
|
+ "SigningCertificateLineage at certificate #" + nodeCount + ". All "
|
||||||
|
+ "signing certificates should be unique");
|
||||||
|
}
|
||||||
|
certHistorySet.add(lastCert);
|
||||||
|
lastSigAlgorithmId = sigAlgorithmId;
|
||||||
|
result.add(new SigningCertificateNode(
|
||||||
|
lastCert, SignatureAlgorithm.findById(signedSigAlgorithm),
|
||||||
|
SignatureAlgorithm.findById(sigAlgorithmId), signature, flags));
|
||||||
|
}
|
||||||
|
} catch(ApkFormatException | BufferUnderflowException e){
|
||||||
|
throw new IOException("Failed to parse V3SigningCertificateLineage object", e);
|
||||||
|
} catch(NoSuchAlgorithmException | InvalidKeyException
|
||||||
|
| InvalidAlgorithmParameterException | SignatureException e){
|
||||||
|
throw new SecurityException(
|
||||||
|
"Failed to verify signature over signed data for certificate #" + nodeCount
|
||||||
|
+ " when parsing V3SigningCertificateLineage object", e);
|
||||||
|
} catch(CertificateException e){
|
||||||
|
throw new SecurityException("Failed to decode certificate #" + nodeCount
|
||||||
|
+ " when parsing V3SigningCertificateLineage object", e);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* encode the in-memory representation of this {@code V3SigningCertificateLineage}
|
||||||
|
*/
|
||||||
|
public static byte[] encodeSigningCertificateLineage(
|
||||||
|
List<SigningCertificateNode> signingCertificateLineage) {
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * version code
|
||||||
|
// * sequence of length-prefixed (uint32): nodes
|
||||||
|
// * length-prefixed bytes: signed data
|
||||||
|
// * length-prefixed bytes: certificate
|
||||||
|
// * uint32: signature algorithm id
|
||||||
|
// * uint32: flags
|
||||||
|
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||||
|
|
||||||
|
List<byte[]> nodes = new ArrayList<>();
|
||||||
|
for (SigningCertificateNode node : signingCertificateLineage) {
|
||||||
|
nodes.add(encodeSigningCertificateNode(node));
|
||||||
|
}
|
||||||
|
byte [] encodedSigningCertificateLineage = encodeAsSequenceOfLengthPrefixedElements(nodes);
|
||||||
|
|
||||||
|
// add the version code (uint32) on top of the encoded nodes
|
||||||
|
int payloadSize = 4 + encodedSigningCertificateLineage.length;
|
||||||
|
ByteBuffer encodedWithVersion = ByteBuffer.allocate(payloadSize);
|
||||||
|
encodedWithVersion.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
encodedWithVersion.putInt(CURRENT_VERSION);
|
||||||
|
encodedWithVersion.put(encodedSigningCertificateLineage);
|
||||||
|
return encodedWithVersion.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] encodeSigningCertificateNode(SigningCertificateNode node) {
|
||||||
|
// FORMAT (little endian):
|
||||||
|
// * length-prefixed bytes: signed data
|
||||||
|
// * length-prefixed bytes: certificate
|
||||||
|
// * uint32: signature algorithm id
|
||||||
|
// * uint32: flags
|
||||||
|
// * uint32: signature algorithm id (used by to sign next cert in lineage)
|
||||||
|
// * length-prefixed bytes: signature over signed data
|
||||||
|
int parentSigAlgorithmId = 0;
|
||||||
|
if (node.parentSigAlgorithm != null) {
|
||||||
|
parentSigAlgorithmId = node.parentSigAlgorithm.getId();
|
||||||
|
}
|
||||||
|
int sigAlgorithmId = 0;
|
||||||
|
if (node.sigAlgorithm != null) {
|
||||||
|
sigAlgorithmId = node.sigAlgorithm.getId();
|
||||||
|
}
|
||||||
|
byte[] prefixedSignedData = encodeSignedData(node.signingCert, parentSigAlgorithmId);
|
||||||
|
byte[] prefixedSignature = encodeAsLengthPrefixedElement(node.signature);
|
||||||
|
int payloadSize = prefixedSignedData.length + 4 + 4 + prefixedSignature.length;
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.put(prefixedSignedData);
|
||||||
|
result.putInt(node.flags);
|
||||||
|
result.putInt(sigAlgorithmId);
|
||||||
|
result.put(prefixedSignature);
|
||||||
|
return result.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] encodeSignedData(X509Certificate certificate, int flags) {
|
||||||
|
try {
|
||||||
|
byte[] prefixedCertificate = encodeAsLengthPrefixedElement(certificate.getEncoded());
|
||||||
|
int payloadSize = 4 + prefixedCertificate.length;
|
||||||
|
ByteBuffer result = ByteBuffer.allocate(payloadSize);
|
||||||
|
result.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
result.put(prefixedCertificate);
|
||||||
|
result.putInt(flags);
|
||||||
|
return encodeAsLengthPrefixedElement(result.array());
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to encode V3SigningCertificateLineage certificate", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents one signing certificate in the {@link V3SigningCertificateLineage}, which
|
||||||
|
* generally means it is/was used at some point to sign the same APK of the others in the
|
||||||
|
* lineage.
|
||||||
|
*/
|
||||||
|
public static class SigningCertificateNode {
|
||||||
|
|
||||||
|
public SigningCertificateNode(
|
||||||
|
X509Certificate signingCert,
|
||||||
|
SignatureAlgorithm parentSigAlgorithm,
|
||||||
|
SignatureAlgorithm sigAlgorithm,
|
||||||
|
byte[] signature,
|
||||||
|
int flags) {
|
||||||
|
this.signingCert = signingCert;
|
||||||
|
this.parentSigAlgorithm = parentSigAlgorithm;
|
||||||
|
this.sigAlgorithm = sigAlgorithm;
|
||||||
|
this.signature = signature;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof SigningCertificateNode)) return false;
|
||||||
|
|
||||||
|
SigningCertificateNode that = (SigningCertificateNode) o;
|
||||||
|
if (!signingCert.equals(that.signingCert)) return false;
|
||||||
|
if (parentSigAlgorithm != that.parentSigAlgorithm) return false;
|
||||||
|
if (sigAlgorithm != that.sigAlgorithm) return false;
|
||||||
|
if (!Arrays.equals(signature, that.signature)) return false;
|
||||||
|
if (flags != that.flags) return false;
|
||||||
|
|
||||||
|
// we made it
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = Objects.hash(signingCert, parentSigAlgorithm, sigAlgorithm, flags);
|
||||||
|
result = 31 * result + Arrays.hashCode(signature);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the signing cert for this node. This is part of the data signed by the parent node.
|
||||||
|
*/
|
||||||
|
public final X509Certificate signingCert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the algorithm used by the this node's parent to bless this data. Its ID value is part of
|
||||||
|
* the data signed by the parent node. {@code null} for first node.
|
||||||
|
*/
|
||||||
|
public final SignatureAlgorithm parentSigAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the algorithm used by the this nodeto bless the next node's data. Its ID value is part
|
||||||
|
* of the signed data of the next node. {@code null} for the last node.
|
||||||
|
*/
|
||||||
|
public SignatureAlgorithm sigAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* signature over the signed data (above). The signature is from this node's parent
|
||||||
|
* signing certificate, which should correspond to the signing certificate used to sign an
|
||||||
|
* APK before rotating to this one, and is formed using {@code signatureAlgorithm}.
|
||||||
|
*/
|
||||||
|
public final byte[] signature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the flags detailing how the platform should treat this signing cert
|
||||||
|
*/
|
||||||
|
public int flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.v4;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31;
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates;
|
||||||
|
import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID;
|
||||||
|
import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||||
|
import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||||
|
|
||||||
|
import com.android.apksig.apk.ApkUtils;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureInfo;
|
||||||
|
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
|
||||||
|
import com.android.apksig.internal.apk.v3.V3SchemeSigner;
|
||||||
|
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.zip.ZipFormatException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme V4 signer. V4 scheme file contains 2 mandatory fields - used during
|
||||||
|
* installation. And optional verity tree - has to be present during session commit.
|
||||||
|
* <p>
|
||||||
|
* The fields:
|
||||||
|
* <p>
|
||||||
|
* 1. hashingInfo - verity root hash and hashing info,
|
||||||
|
* 2. signingInfo - certificate, public key and signature,
|
||||||
|
* For more details see V4Signature.
|
||||||
|
* </p>
|
||||||
|
* (optional) verityTree: integer size prepended bytes of the verity hash tree.
|
||||||
|
* <p>
|
||||||
|
*/
|
||||||
|
public abstract class V4SchemeSigner {
|
||||||
|
/**
|
||||||
|
* Hidden constructor to prevent instantiation.
|
||||||
|
*/
|
||||||
|
private V4SchemeSigner() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SignerConfig {
|
||||||
|
final public ApkSigningBlockUtils.SignerConfig v4Config;
|
||||||
|
final public ApkSigningBlockUtils.SignerConfig v41Config;
|
||||||
|
|
||||||
|
public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs,
|
||||||
|
List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException {
|
||||||
|
if (v4Configs == null || v4Configs.size() != 1) {
|
||||||
|
throw new InvalidKeyException("Only accepting one signer config for V4 Signature.");
|
||||||
|
}
|
||||||
|
if (v41Configs != null && v41Configs.size() != 1) {
|
||||||
|
throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature.");
|
||||||
|
}
|
||||||
|
this.v4Config = v4Configs.get(0);
|
||||||
|
this.v41Config = v41Configs != null ? v41Configs.get(0) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on a public key, return a signing algorithm that supports verity.
|
||||||
|
*/
|
||||||
|
public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey,
|
||||||
|
int minSdkVersion, boolean apkSigningBlockPaddingSupported,
|
||||||
|
boolean deterministicDsaSigning)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms(
|
||||||
|
signingKey, minSdkVersion,
|
||||||
|
apkSigningBlockPaddingSupported, deterministicDsaSigning);
|
||||||
|
// Keeping only supported algorithms.
|
||||||
|
for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) {
|
||||||
|
final SignatureAlgorithm algorithm = iter.next();
|
||||||
|
if (!isSupported(algorithm.getContentDigestAlgorithm(), false)) {
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return algorithms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute hash tree and generate v4 signature for a given APK. Write the serialized data to
|
||||||
|
* output file.
|
||||||
|
*/
|
||||||
|
public static void generateV4Signature(
|
||||||
|
DataSource apkContent, SignerConfig signerConfig, File outputFile)
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
|
Pair<V4Signature, byte[]> pair = generateV4Signature(apkContent, signerConfig);
|
||||||
|
try (final OutputStream output = new FileOutputStream(outputFile)) {
|
||||||
|
pair.getFirst().writeTo(output);
|
||||||
|
V4Signature.writeBytes(output, pair.getSecond());
|
||||||
|
} catch (IOException e) {
|
||||||
|
outputFile.delete();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generate v4 signature and hash tree for a given APK. */
|
||||||
|
public static Pair<V4Signature, byte[]> generateV4Signature(
|
||||||
|
DataSource apkContent,
|
||||||
|
SignerConfig signerConfig)
|
||||||
|
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
|
// Salt has to stay empty for fs-verity compatibility.
|
||||||
|
final byte[] salt = null;
|
||||||
|
// Not used by apksigner.
|
||||||
|
final byte[] additionalData = null;
|
||||||
|
|
||||||
|
final long fileSize = apkContent.size();
|
||||||
|
|
||||||
|
// Obtaining the strongest supported digest for each of the v2/v3/v3.1 blocks
|
||||||
|
// (CHUNKED_SHA256 or CHUNKED_SHA512).
|
||||||
|
final Map<Integer, byte[]> apkDigests = getApkDigests(apkContent);
|
||||||
|
|
||||||
|
// Obtaining the merkle tree and the root hash in verity format.
|
||||||
|
ApkSigningBlockUtils.VerityTreeAndDigest verityContentDigestInfo =
|
||||||
|
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
|
||||||
|
|
||||||
|
final ContentDigestAlgorithm verityContentDigestAlgorithm =
|
||||||
|
verityContentDigestInfo.contentDigestAlgorithm;
|
||||||
|
final byte[] rootHash = verityContentDigestInfo.rootHash;
|
||||||
|
final byte[] tree = verityContentDigestInfo.tree;
|
||||||
|
|
||||||
|
final Pair<Integer, Byte> hashingAlgorithmBlockSizePair = convertToV4HashingInfo(
|
||||||
|
verityContentDigestAlgorithm);
|
||||||
|
final V4Signature.HashingInfo hashingInfo = new V4Signature.HashingInfo(
|
||||||
|
hashingAlgorithmBlockSizePair.getFirst(), hashingAlgorithmBlockSizePair.getSecond(),
|
||||||
|
salt, rootHash);
|
||||||
|
|
||||||
|
// Generating SigningInfo and combining everything into V4Signature.
|
||||||
|
final V4Signature signature;
|
||||||
|
try {
|
||||||
|
signature = generateSignature(signerConfig, hashingInfo, apkDigests, additionalData,
|
||||||
|
fileSize);
|
||||||
|
} catch (InvalidKeyException | SignatureException | CertificateEncodingException e) {
|
||||||
|
throw new InvalidKeyException("Signer failed", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair.of(signature, tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static V4Signature.SigningInfo generateSigningInfo(
|
||||||
|
ApkSigningBlockUtils.SignerConfig signerConfig,
|
||||||
|
V4Signature.HashingInfo hashingInfo,
|
||||||
|
byte[] apkDigest, byte[] additionalData, long fileSize)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
|
||||||
|
CertificateEncodingException {
|
||||||
|
if (signerConfig.certificates.isEmpty()) {
|
||||||
|
throw new SignatureException("No certificates configured for signer");
|
||||||
|
}
|
||||||
|
if (signerConfig.certificates.size() != 1) {
|
||||||
|
throw new CertificateEncodingException("Should only have one certificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collecting data for signing.
|
||||||
|
final PublicKey publicKey = signerConfig.certificates.get(0).getPublicKey();
|
||||||
|
|
||||||
|
final List<byte[]> encodedCertificates = encodeCertificates(signerConfig.certificates);
|
||||||
|
final byte[] encodedCertificate = encodedCertificates.get(0);
|
||||||
|
|
||||||
|
final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest,
|
||||||
|
encodedCertificate, additionalData, publicKey.getEncoded(), -1, null);
|
||||||
|
|
||||||
|
final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo,
|
||||||
|
signingInfoNoSignature);
|
||||||
|
|
||||||
|
// Signing.
|
||||||
|
final List<Pair<Integer, byte[]>> signatures =
|
||||||
|
ApkSigningBlockUtils.generateSignaturesOverData(signerConfig, data);
|
||||||
|
if (signatures.size() != 1) {
|
||||||
|
throw new SignatureException("Should only be one signature generated");
|
||||||
|
}
|
||||||
|
|
||||||
|
final int signatureAlgorithmId = signatures.get(0).getFirst();
|
||||||
|
final byte[] signature = signatures.get(0).getSecond();
|
||||||
|
|
||||||
|
return new V4Signature.SigningInfo(apkDigest,
|
||||||
|
encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId,
|
||||||
|
signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static V4Signature generateSignature(
|
||||||
|
SignerConfig signerConfig,
|
||||||
|
V4Signature.HashingInfo hashingInfo,
|
||||||
|
Map<Integer, byte[]> apkDigests, byte[] additionalData, long fileSize)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, SignatureException,
|
||||||
|
CertificateEncodingException {
|
||||||
|
byte[] apkDigest = apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V3)
|
||||||
|
? apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V3)
|
||||||
|
: apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||||
|
final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config,
|
||||||
|
hashingInfo, apkDigest, additionalData, fileSize);
|
||||||
|
|
||||||
|
final V4Signature.SigningInfos signingInfos;
|
||||||
|
if (signerConfig.v41Config != null) {
|
||||||
|
if (!apkDigests.containsKey(VERSION_APK_SIGNATURE_SCHEME_V31)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"V4.1 cannot be signed without a V3.1 content digest");
|
||||||
|
}
|
||||||
|
apkDigest = apkDigests.get(VERSION_APK_SIGNATURE_SCHEME_V31);
|
||||||
|
final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock(
|
||||||
|
APK_SIGNATURE_SCHEME_V31_BLOCK_ID,
|
||||||
|
generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest,
|
||||||
|
additionalData, fileSize).toByteArray());
|
||||||
|
signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock);
|
||||||
|
} else {
|
||||||
|
signingInfos = new V4Signature.SigningInfos(signingInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(),
|
||||||
|
signingInfos.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a {@code Map} from the APK signature scheme version to a {@code byte[]} of the
|
||||||
|
* strongest supported content digest found in that version's signature block for the V2,
|
||||||
|
* V3, and V3.1 signatures in the provided {@code apk}.
|
||||||
|
*
|
||||||
|
* <p>If a supported content digest algorithm is not found in any of the signature blocks,
|
||||||
|
* or if the APK is not signed by any of these signature schemes, then an {@code IOException}
|
||||||
|
* is thrown.
|
||||||
|
*/
|
||||||
|
private static Map<Integer, byte[]> getApkDigests(DataSource apk) throws IOException {
|
||||||
|
ApkUtils.ZipSections zipSections;
|
||||||
|
try {
|
||||||
|
zipSections = ApkUtils.findZipSections(apk);
|
||||||
|
} catch (ZipFormatException e) {
|
||||||
|
throw new IOException("Malformed APK: not a ZIP archive", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, byte[]> sigSchemeToDigest = new HashMap<>(1);
|
||||||
|
try {
|
||||||
|
byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V31);
|
||||||
|
sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V31, digest);
|
||||||
|
} catch (SignatureException expected) {
|
||||||
|
// It is expected to catch a SignatureException if the APK does not have a v3.1
|
||||||
|
// signature.
|
||||||
|
}
|
||||||
|
|
||||||
|
SignatureException v3Exception = null;
|
||||||
|
try {
|
||||||
|
byte[] digest = getBestV3Digest(apk, zipSections, VERSION_APK_SIGNATURE_SCHEME_V3);
|
||||||
|
sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V3, digest);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
v3Exception = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignatureException v2Exception = null;
|
||||||
|
try {
|
||||||
|
byte[] digest = getBestV2Digest(apk, zipSections);
|
||||||
|
sigSchemeToDigest.put(VERSION_APK_SIGNATURE_SCHEME_V2, digest);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
v2Exception = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sigSchemeToDigest.size() > 0) {
|
||||||
|
return sigSchemeToDigest;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException(
|
||||||
|
"Failed to obtain v2/v3 digest, v3 exception: " + v3Exception + ", v2 exception: "
|
||||||
|
+ v2Exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getBestV3Digest(DataSource apk, ApkUtils.ZipSections zipSections,
|
||||||
|
int v3SchemeVersion) throws SignatureException {
|
||||||
|
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||||
|
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||||
|
v3SchemeVersion);
|
||||||
|
final int blockId;
|
||||||
|
switch (v3SchemeVersion) {
|
||||||
|
case VERSION_APK_SIGNATURE_SCHEME_V31:
|
||||||
|
blockId = APK_SIGNATURE_SCHEME_V31_BLOCK_ID;
|
||||||
|
break;
|
||||||
|
case VERSION_APK_SIGNATURE_SCHEME_V3:
|
||||||
|
blockId = APK_SIGNATURE_SCHEME_V3_BLOCK_ID;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid V3 scheme provided: " + v3SchemeVersion);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtils.findSignature(apk, zipSections, blockId, result);
|
||||||
|
final ByteBuffer apkSignatureSchemeV3Block = signatureInfo.signatureBlock;
|
||||||
|
V3SchemeVerifier.parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify,
|
||||||
|
result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new SignatureException("Failed to extract and parse v3 block", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.signers.size() != 1) {
|
||||||
|
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
|
||||||
|
if (signer.containsErrors()) {
|
||||||
|
throw new SignatureException("Parsing failed: " + signer.getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
|
||||||
|
result.signers.get(0).contentDigests;
|
||||||
|
return pickBestDigest(contentDigests);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] getBestV2Digest(DataSource apk, ApkUtils.ZipSections zipSections)
|
||||||
|
throws SignatureException {
|
||||||
|
final Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
|
||||||
|
final Set<Integer> foundApkSigSchemeIds = new HashSet<>(1);
|
||||||
|
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||||
|
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
|
||||||
|
try {
|
||||||
|
final SignatureInfo signatureInfo =
|
||||||
|
ApkSigningBlockUtils.findSignature(apk, zipSections,
|
||||||
|
APK_SIGNATURE_SCHEME_V2_BLOCK_ID, result);
|
||||||
|
final ByteBuffer apkSignatureSchemeV2Block = signatureInfo.signatureBlock;
|
||||||
|
V2SchemeVerifier.parseSigners(
|
||||||
|
apkSignatureSchemeV2Block,
|
||||||
|
contentDigestsToVerify,
|
||||||
|
Collections.emptyMap(),
|
||||||
|
foundApkSigSchemeIds,
|
||||||
|
Integer.MAX_VALUE,
|
||||||
|
Integer.MAX_VALUE,
|
||||||
|
result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new SignatureException("Failed to extract and parse v2 block", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.signers.size() != 1) {
|
||||||
|
throw new SignatureException("Should only have one signer, errors: " + result.getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signer = result.signers.get(0);
|
||||||
|
if (signer.containsErrors()) {
|
||||||
|
throw new SignatureException("Parsing failed: " + signer.getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests =
|
||||||
|
signer.contentDigests;
|
||||||
|
return pickBestDigest(contentDigests);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] pickBestDigest(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) throws SignatureException {
|
||||||
|
if (contentDigests == null || contentDigests.isEmpty()) {
|
||||||
|
throw new SignatureException("Should have at least one digest");
|
||||||
|
}
|
||||||
|
|
||||||
|
int bestAlgorithmOrder = -1;
|
||||||
|
byte[] bestDigest = null;
|
||||||
|
for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) {
|
||||||
|
final SignatureAlgorithm signatureAlgorithm =
|
||||||
|
SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
|
||||||
|
final ContentDigestAlgorithm contentDigestAlgorithm =
|
||||||
|
signatureAlgorithm.getContentDigestAlgorithm();
|
||||||
|
if (!isSupported(contentDigestAlgorithm, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int algorithmOrder = digestAlgorithmSortingOrder(contentDigestAlgorithm);
|
||||||
|
if (bestAlgorithmOrder < algorithmOrder) {
|
||||||
|
bestAlgorithmOrder = algorithmOrder;
|
||||||
|
bestDigest = contentDigest.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestDigest == null) {
|
||||||
|
throw new SignatureException("Failed to find a supported digest in the source APK");
|
||||||
|
}
|
||||||
|
return bestDigest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) {
|
||||||
|
switch (contentDigestAlgorithm) {
|
||||||
|
case CHUNKED_SHA256:
|
||||||
|
return 0;
|
||||||
|
case VERITY_CHUNKED_SHA256:
|
||||||
|
return 1;
|
||||||
|
case CHUNKED_SHA512:
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm,
|
||||||
|
boolean forV3Digest) {
|
||||||
|
if (contentDigestAlgorithm == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA256
|
||||||
|
|| contentDigestAlgorithm == ContentDigestAlgorithm.CHUNKED_SHA512
|
||||||
|
|| (forV3Digest
|
||||||
|
&& contentDigestAlgorithm == ContentDigestAlgorithm.VERITY_CHUNKED_SHA256)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Pair<Integer, Byte> convertToV4HashingInfo(ContentDigestAlgorithm algorithm)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
switch (algorithm) {
|
||||||
|
case VERITY_CHUNKED_SHA256:
|
||||||
|
return Pair.of(V4Signature.HASHING_ALGORITHM_SHA256,
|
||||||
|
V4Signature.LOG2_BLOCK_SIZE_4096_BYTES);
|
||||||
|
default:
|
||||||
|
throw new NoSuchAlgorithmException(
|
||||||
|
"Invalid hash algorithm, only SHA2-256 over 4 KB chunks supported.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.v4;
|
||||||
|
|
||||||
|
import static com.android.apksig.internal.apk.ApkSigningBlockUtils.toHex;
|
||||||
|
|
||||||
|
import com.android.apksig.ApkVerifier;
|
||||||
|
import com.android.apksig.ApkVerifier.Issue;
|
||||||
|
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
|
||||||
|
import com.android.apksig.internal.apk.ContentDigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.apk.SignatureAlgorithm;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.AlgorithmParameterSpec;
|
||||||
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* APK Signature Scheme V4 verifier.
|
||||||
|
* <p>
|
||||||
|
* Verifies the serialized V4Signature file against an APK.
|
||||||
|
*/
|
||||||
|
public abstract class V4SchemeVerifier {
|
||||||
|
/**
|
||||||
|
* Hidden constructor to prevent instantiation.
|
||||||
|
*/
|
||||||
|
private V4SchemeVerifier() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* The main goals of the verifier are: 1) parse V4Signature file fields 2) verifies the PKCS7
|
||||||
|
* signature block against the raw root hash bytes in the proto field 3) verifies that the raw
|
||||||
|
* root hash matches with the actual hash tree root of the give APK 4) if the file contains a
|
||||||
|
* verity tree, verifies that it matches with the actual verity tree computed from the given
|
||||||
|
* APK.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
public static ApkSigningBlockUtils.Result verify(DataSource apk, File v4SignatureFile)
|
||||||
|
throws IOException, NoSuchAlgorithmException {
|
||||||
|
final V4Signature signature;
|
||||||
|
final byte[] tree;
|
||||||
|
try (InputStream input = new FileInputStream(v4SignatureFile)) {
|
||||||
|
signature = V4Signature.readFrom(input);
|
||||||
|
tree = V4Signature.readBytes(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(
|
||||||
|
ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4);
|
||||||
|
|
||||||
|
if (signature == null) {
|
||||||
|
result.addError(Issue.V4_SIG_NO_SIGNATURES,
|
||||||
|
"Signature file does not contain a v4 signature.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signature.version != V4Signature.CURRENT_VERSION) {
|
||||||
|
result.addWarning(Issue.V4_SIG_VERSION_NOT_CURRENT, signature.version,
|
||||||
|
V4Signature.CURRENT_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
|
||||||
|
signature.hashingInfo);
|
||||||
|
|
||||||
|
V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
|
||||||
|
signature.signingInfos);
|
||||||
|
|
||||||
|
final ApkSigningBlockUtils.Result.SignerInfo signerInfo;
|
||||||
|
|
||||||
|
// Verify the primary signature over signedData.
|
||||||
|
{
|
||||||
|
V4Signature.SigningInfo signingInfo = signingInfos.signingInfo;
|
||||||
|
final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
|
||||||
|
signingInfo);
|
||||||
|
signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData);
|
||||||
|
result.signers.add(signerInfo);
|
||||||
|
if (result.containsErrors()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all subsequent signatures.
|
||||||
|
for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) {
|
||||||
|
V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
|
||||||
|
signingInfoBlock.signingInfo);
|
||||||
|
final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo,
|
||||||
|
signingInfo);
|
||||||
|
result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData));
|
||||||
|
if (result.containsErrors()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the root hash and the tree are correct.
|
||||||
|
verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree);
|
||||||
|
if (!result.containsErrors()) {
|
||||||
|
result.verified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided signature block and populates the {@code result}.
|
||||||
|
* <p>
|
||||||
|
* This verifies {@signingInfo} over {@code signedData}, as well as parsing the certificate
|
||||||
|
* contained in the signature block. This method adds one or more errors to the {@code result}.
|
||||||
|
*/
|
||||||
|
private static ApkSigningBlockUtils.Result.SignerInfo parseAndVerifySignatureBlock(
|
||||||
|
V4Signature.SigningInfo signingInfo,
|
||||||
|
final byte[] signedData) throws NoSuchAlgorithmException {
|
||||||
|
final ApkSigningBlockUtils.Result.SignerInfo result =
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo();
|
||||||
|
result.index = 0;
|
||||||
|
|
||||||
|
final int sigAlgorithmId = signingInfo.signatureAlgorithmId;
|
||||||
|
final byte[] sigBytes = signingInfo.signature;
|
||||||
|
result.signatures.add(
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.Signature(sigAlgorithmId, sigBytes));
|
||||||
|
|
||||||
|
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId);
|
||||||
|
if (signatureAlgorithm == null) {
|
||||||
|
result.addError(Issue.V4_SIG_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String jcaSignatureAlgorithm =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst();
|
||||||
|
AlgorithmParameterSpec jcaSignatureAlgorithmParams =
|
||||||
|
signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond();
|
||||||
|
|
||||||
|
String keyAlgorithm = signatureAlgorithm.getJcaKeyAlgorithm();
|
||||||
|
|
||||||
|
final byte[] publicKeyBytes = signingInfo.publicKey;
|
||||||
|
PublicKey publicKey;
|
||||||
|
try {
|
||||||
|
publicKey = KeyFactory.getInstance(keyAlgorithm).generatePublic(
|
||||||
|
new X509EncodedKeySpec(publicKeyBytes));
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.addError(Issue.V4_SIG_MALFORMED_PUBLIC_KEY, e);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
|
||||||
|
sig.initVerify(publicKey);
|
||||||
|
if (jcaSignatureAlgorithmParams != null) {
|
||||||
|
sig.setParameter(jcaSignatureAlgorithmParams);
|
||||||
|
}
|
||||||
|
sig.update(signedData);
|
||||||
|
if (!sig.verify(sigBytes)) {
|
||||||
|
result.addError(Issue.V4_SIG_DID_NOT_VERIFY, signatureAlgorithm);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
|
||||||
|
} catch (InvalidKeyException | InvalidAlgorithmParameterException
|
||||||
|
| SignatureException e) {
|
||||||
|
result.addError(Issue.V4_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signingInfo.certificate == null) {
|
||||||
|
result.addError(Issue.V4_SIG_NO_CERTIFICATE);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original encoded
|
||||||
|
// form. Without this, getEncoded may return a different form from what was stored in
|
||||||
|
// the signature. This is because some X509Certificate(Factory) implementations
|
||||||
|
// re-encode certificates.
|
||||||
|
certificate = new GuaranteedEncodedFormX509Certificate(
|
||||||
|
X509CertificateUtils.generateCertificate(signingInfo.certificate),
|
||||||
|
signingInfo.certificate);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
result.addError(Issue.V4_SIG_MALFORMED_CERTIFICATE, e);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.certs.add(certificate);
|
||||||
|
|
||||||
|
byte[] certificatePublicKeyBytes;
|
||||||
|
try {
|
||||||
|
certificatePublicKeyBytes = ApkSigningBlockUtils.encodePublicKey(
|
||||||
|
certificate.getPublicKey());
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
System.out.println("Caught an exception encoding the public key: " + e);
|
||||||
|
e.printStackTrace();
|
||||||
|
certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
|
||||||
|
result.addError(
|
||||||
|
Issue.V4_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
|
||||||
|
ApkSigningBlockUtils.toHex(certificatePublicKeyBytes),
|
||||||
|
ApkSigningBlockUtils.toHex(publicKeyBytes));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add apk digest from the file to the result.
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest =
|
||||||
|
new ApkSigningBlockUtils.Result.SignerInfo.ContentDigest(
|
||||||
|
0 /* signature algorithm id doesn't matter here */, signingInfo.apkDigest);
|
||||||
|
result.contentDigests.add(contentDigest);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyRootHashAndTree(DataSource apkContent,
|
||||||
|
ApkSigningBlockUtils.Result.SignerInfo signerInfo, byte[] expectedDigest,
|
||||||
|
byte[] expectedTree) throws IOException, NoSuchAlgorithmException {
|
||||||
|
ApkSigningBlockUtils.VerityTreeAndDigest actualContentDigestInfo =
|
||||||
|
ApkSigningBlockUtils.computeChunkVerityTreeAndDigest(apkContent);
|
||||||
|
|
||||||
|
ContentDigestAlgorithm algorithm = actualContentDigestInfo.contentDigestAlgorithm;
|
||||||
|
final byte[] actualDigest = actualContentDigestInfo.rootHash;
|
||||||
|
final byte[] actualTree = actualContentDigestInfo.tree;
|
||||||
|
|
||||||
|
if (!Arrays.equals(expectedDigest, actualDigest)) {
|
||||||
|
signerInfo.addError(
|
||||||
|
ApkVerifier.Issue.V4_SIG_APK_ROOT_DID_NOT_VERIFY,
|
||||||
|
algorithm,
|
||||||
|
toHex(expectedDigest),
|
||||||
|
toHex(actualDigest));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only check verity tree if it is not empty
|
||||||
|
if (expectedTree != null && !Arrays.equals(expectedTree, actualTree)) {
|
||||||
|
signerInfo.addError(
|
||||||
|
ApkVerifier.Issue.V4_SIG_APK_TREE_DID_NOT_VERIFY,
|
||||||
|
algorithm,
|
||||||
|
toHex(expectedDigest),
|
||||||
|
toHex(actualDigest));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
signerInfo.verifiedContentDigests.put(algorithm, actualDigest);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.apk.v4;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class V4Signature {
|
||||||
|
public static final int CURRENT_VERSION = 2;
|
||||||
|
|
||||||
|
public static final int HASHING_ALGORITHM_SHA256 = 1;
|
||||||
|
public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12;
|
||||||
|
|
||||||
|
public static final int MAX_SIGNING_INFOS_SIZE = 7168;
|
||||||
|
|
||||||
|
public static class HashingInfo {
|
||||||
|
public final int hashAlgorithm; // only 1 == SHA256 supported
|
||||||
|
public final byte log2BlockSize; // only 12 (block size 4096) supported now
|
||||||
|
public final byte[] salt; // used exactly as in fs-verity, 32 bytes max
|
||||||
|
public final byte[] rawRootHash; // salted digest of the first Merkle tree page
|
||||||
|
|
||||||
|
HashingInfo(int hashAlgorithm, byte log2BlockSize, byte[] salt, byte[] rawRootHash) {
|
||||||
|
this.hashAlgorithm = hashAlgorithm;
|
||||||
|
this.log2BlockSize = log2BlockSize;
|
||||||
|
this.salt = salt;
|
||||||
|
this.rawRootHash = rawRootHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HashingInfo fromByteArray(byte[] bytes) throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
final int hashAlgorithm = buffer.getInt();
|
||||||
|
final byte log2BlockSize = buffer.get();
|
||||||
|
byte[] salt = readBytes(buffer);
|
||||||
|
byte[] rawRootHash = readBytes(buffer);
|
||||||
|
return new HashingInfo(hashAlgorithm, log2BlockSize, salt, rawRootHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] toByteArray() {
|
||||||
|
final int size = 4/*hashAlgorithm*/ + 1/*log2BlockSize*/ + bytesSize(this.salt)
|
||||||
|
+ bytesSize(this.rawRootHash);
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
buffer.putInt(this.hashAlgorithm);
|
||||||
|
buffer.put(this.log2BlockSize);
|
||||||
|
writeBytes(buffer, this.salt);
|
||||||
|
writeBytes(buffer, this.rawRootHash);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SigningInfo {
|
||||||
|
public final byte[] apkDigest; // used to match with the corresponding APK
|
||||||
|
public final byte[] certificate; // ASN.1 DER form
|
||||||
|
public final byte[] additionalData; // a free-form binary data blob
|
||||||
|
public final byte[] publicKey; // ASN.1 DER, must match the certificate
|
||||||
|
public final int signatureAlgorithmId; // see the APK v2 doc for the list
|
||||||
|
public final byte[] signature;
|
||||||
|
|
||||||
|
SigningInfo(byte[] apkDigest, byte[] certificate, byte[] additionalData,
|
||||||
|
byte[] publicKey, int signatureAlgorithmId, byte[] signature) {
|
||||||
|
this.apkDigest = apkDigest;
|
||||||
|
this.certificate = certificate;
|
||||||
|
this.additionalData = additionalData;
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
this.signatureAlgorithmId = signatureAlgorithmId;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SigningInfo fromByteArray(byte[] bytes) throws IOException {
|
||||||
|
return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN));
|
||||||
|
}
|
||||||
|
|
||||||
|
static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException {
|
||||||
|
byte[] apkDigest = readBytes(buffer);
|
||||||
|
byte[] certificate = readBytes(buffer);
|
||||||
|
byte[] additionalData = readBytes(buffer);
|
||||||
|
byte[] publicKey = readBytes(buffer);
|
||||||
|
int signatureAlgorithmId = buffer.getInt();
|
||||||
|
byte[] signature = readBytes(buffer);
|
||||||
|
return new SigningInfo(apkDigest, certificate, additionalData, publicKey,
|
||||||
|
signatureAlgorithmId, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] toByteArray() {
|
||||||
|
final int size = bytesSize(this.apkDigest) + bytesSize(this.certificate) + bytesSize(
|
||||||
|
this.additionalData) + bytesSize(this.publicKey) + 4/*signatureAlgorithmId*/
|
||||||
|
+ bytesSize(this.signature);
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
writeBytes(buffer, this.apkDigest);
|
||||||
|
writeBytes(buffer, this.certificate);
|
||||||
|
writeBytes(buffer, this.additionalData);
|
||||||
|
writeBytes(buffer, this.publicKey);
|
||||||
|
buffer.putInt(this.signatureAlgorithmId);
|
||||||
|
writeBytes(buffer, this.signature);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SigningInfoBlock {
|
||||||
|
public final int blockId;
|
||||||
|
public final byte[] signingInfo;
|
||||||
|
|
||||||
|
public SigningInfoBlock(int blockId, byte[] signingInfo) {
|
||||||
|
this.blockId = blockId;
|
||||||
|
this.signingInfo = signingInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException {
|
||||||
|
int blockId = buffer.getInt();
|
||||||
|
byte[] signingInfo = readBytes(buffer);
|
||||||
|
return new SigningInfoBlock(blockId, signingInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] toByteArray() {
|
||||||
|
final int size = 4/*blockId*/ + bytesSize(this.signingInfo);
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
buffer.putInt(this.blockId);
|
||||||
|
writeBytes(buffer, this.signingInfo);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SigningInfos {
|
||||||
|
public final SigningInfo signingInfo;
|
||||||
|
public final SigningInfoBlock[] signingInfoBlocks;
|
||||||
|
|
||||||
|
public SigningInfos(SigningInfo signingInfo) {
|
||||||
|
this.signingInfo = signingInfo;
|
||||||
|
this.signingInfoBlocks = new SigningInfoBlock[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) {
|
||||||
|
this.signingInfo = signingInfo;
|
||||||
|
this.signingInfoBlocks = signingInfoBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SigningInfos fromByteArray(byte[] bytes) throws IOException {
|
||||||
|
ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer);
|
||||||
|
if (!buffer.hasRemaining()) {
|
||||||
|
return new SigningInfos(signingInfo);
|
||||||
|
}
|
||||||
|
ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1);
|
||||||
|
while (buffer.hasRemaining()) {
|
||||||
|
signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer));
|
||||||
|
}
|
||||||
|
return new SigningInfos(signingInfo,
|
||||||
|
signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] toByteArray() {
|
||||||
|
byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][];
|
||||||
|
arrays[0] = this.signingInfo.toByteArray();
|
||||||
|
int size = arrays[0].length;
|
||||||
|
for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
|
||||||
|
arrays[i + 1] = this.signingInfoBlocks[i].toByteArray();
|
||||||
|
size += arrays[i + 1].length;
|
||||||
|
}
|
||||||
|
if (size > MAX_SIGNING_INFOS_SIZE) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Combined SigningInfos length exceeded limit of 7K: " + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all arrays into one.
|
||||||
|
byte[] result = Arrays.copyOf(arrays[0], size);
|
||||||
|
int offset = arrays[0].length;
|
||||||
|
for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) {
|
||||||
|
System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length);
|
||||||
|
offset += arrays[i + 1].length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always 2 for now.
|
||||||
|
public final int version;
|
||||||
|
public final byte[] hashingInfo;
|
||||||
|
// Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock.
|
||||||
|
// Passed as-is to the kernel. Can be retrieved later.
|
||||||
|
public final byte[] signingInfos;
|
||||||
|
|
||||||
|
V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) {
|
||||||
|
this.version = version;
|
||||||
|
this.hashingInfo = hashingInfo;
|
||||||
|
this.signingInfos = signingInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static V4Signature readFrom(InputStream stream) throws IOException {
|
||||||
|
final int version = readIntLE(stream);
|
||||||
|
if (version != CURRENT_VERSION) {
|
||||||
|
throw new IOException("Invalid signature version.");
|
||||||
|
}
|
||||||
|
final byte[] hashingInfo = readBytes(stream);
|
||||||
|
final byte[] signingInfo = readBytes(stream);
|
||||||
|
return new V4Signature(version, hashingInfo, signingInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeTo(OutputStream stream) throws IOException {
|
||||||
|
writeIntLE(stream, this.version);
|
||||||
|
writeBytes(stream, this.hashingInfo);
|
||||||
|
writeBytes(stream, this.signingInfos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) {
|
||||||
|
final int size =
|
||||||
|
4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize(
|
||||||
|
hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize(
|
||||||
|
signingInfo.apkDigest) + bytesSize(signingInfo.certificate) + bytesSize(
|
||||||
|
signingInfo.additionalData);
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
buffer.putInt(size);
|
||||||
|
buffer.putLong(fileSize);
|
||||||
|
buffer.putInt(hashingInfo.hashAlgorithm);
|
||||||
|
buffer.put(hashingInfo.log2BlockSize);
|
||||||
|
writeBytes(buffer, hashingInfo.salt);
|
||||||
|
writeBytes(buffer, hashingInfo.rawRootHash);
|
||||||
|
writeBytes(buffer, signingInfo.apkDigest);
|
||||||
|
writeBytes(buffer, signingInfo.certificate);
|
||||||
|
writeBytes(buffer, signingInfo.additionalData);
|
||||||
|
return buffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility methods.
|
||||||
|
static int bytesSize(byte[] bytes) {
|
||||||
|
return 4/*length*/ + (bytes == null ? 0 : bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void readFully(InputStream stream, byte[] buffer) throws IOException {
|
||||||
|
int len = buffer.length;
|
||||||
|
int n = 0;
|
||||||
|
while (n < len) {
|
||||||
|
int count = stream.read(buffer, n, len - n);
|
||||||
|
if (count < 0) {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
n += count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int readIntLE(InputStream stream) throws IOException {
|
||||||
|
final byte[] buffer = new byte[4];
|
||||||
|
readFully(stream, buffer);
|
||||||
|
return ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeIntLE(OutputStream stream, int v) throws IOException {
|
||||||
|
final byte[] buffer = ByteBuffer.wrap(new byte[4]).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
|
||||||
|
stream.write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] readBytes(InputStream stream) throws IOException {
|
||||||
|
try {
|
||||||
|
final int size = readIntLE(stream);
|
||||||
|
final byte[] bytes = new byte[size];
|
||||||
|
readFully(stream, bytes);
|
||||||
|
return bytes;
|
||||||
|
} catch (EOFException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] readBytes(ByteBuffer buffer) throws IOException {
|
||||||
|
if (buffer.remaining() < 4) {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
final int size = buffer.getInt();
|
||||||
|
if (buffer.remaining() < size) {
|
||||||
|
throw new EOFException();
|
||||||
|
}
|
||||||
|
final byte[] bytes = new byte[size];
|
||||||
|
buffer.get(bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBytes(OutputStream stream, byte[] bytes) throws IOException {
|
||||||
|
if (bytes == null) {
|
||||||
|
writeIntLE(stream, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writeIntLE(stream, bytes.length);
|
||||||
|
stream.write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeBytes(ByteBuffer buffer, byte[] bytes) {
|
||||||
|
if (bytes == null) {
|
||||||
|
buffer.putInt(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
buffer.putInt(bytes.length);
|
||||||
|
buffer.put(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,673 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerDataValue;
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerDataValueFormatException;
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerDataValueReader;
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerEncoding;
|
||||||
|
import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser of ASN.1 BER-encoded structures.
|
||||||
|
*
|
||||||
|
* <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class},
|
||||||
|
* containing fields annotated with {@link Asn1Field}.
|
||||||
|
*/
|
||||||
|
public final class Asn1BerParser {
|
||||||
|
private Asn1BerParser() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ASN.1 structure contained in the BER encoded input.
|
||||||
|
*
|
||||||
|
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
|
||||||
|
* is advanced to the first position following the end of the consumed structure.
|
||||||
|
* @param containerClass class describing the structure of the input. The class must meet the
|
||||||
|
* following requirements:
|
||||||
|
* <ul>
|
||||||
|
* <li>The class must be annotated with {@link Asn1Class}.</li>
|
||||||
|
* <li>The class must expose a public no-arg constructor.</li>
|
||||||
|
* <li>Member fields of the class which are populated with parsed input must be
|
||||||
|
* annotated with {@link Asn1Field} and be public and non-final.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
|
||||||
|
* object
|
||||||
|
*/
|
||||||
|
public static <T> T parse(ByteBuffer encoded, Class<T> containerClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
BerDataValue containerDataValue;
|
||||||
|
try {
|
||||||
|
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
|
||||||
|
} catch (BerDataValueFormatException e) {
|
||||||
|
throw new Asn1DecodingException("Failed to decode top-level data value", e);
|
||||||
|
}
|
||||||
|
if (containerDataValue == null) {
|
||||||
|
throw new Asn1DecodingException("Empty input");
|
||||||
|
}
|
||||||
|
return parse(containerDataValue, containerClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means
|
||||||
|
* that this method does not care whether the tag number of this data structure is
|
||||||
|
* {@code SET OF} and whether the tag class is {@code UNIVERSAL}.
|
||||||
|
*
|
||||||
|
* <p>Note: The returned type is {@link List} rather than {@link java.util.Set} because ASN.1
|
||||||
|
* SET may contain duplicate elements.
|
||||||
|
*
|
||||||
|
* @param encoded encoded input. If the decoding operation succeeds, the position of this buffer
|
||||||
|
* is advanced to the first position following the end of the consumed structure.
|
||||||
|
* @param elementClass class describing the structure of the values/elements contained in this
|
||||||
|
* container. The class must meet the following requirements:
|
||||||
|
* <ul>
|
||||||
|
* <li>The class must be annotated with {@link Asn1Class}.</li>
|
||||||
|
* <li>The class must expose a public no-arg constructor.</li>
|
||||||
|
* <li>Member fields of the class which are populated with parsed input must be
|
||||||
|
* annotated with {@link Asn1Field} and be public and non-final.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @throws Asn1DecodingException if the input could not be decoded into the specified Java
|
||||||
|
* object
|
||||||
|
*/
|
||||||
|
public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
BerDataValue containerDataValue;
|
||||||
|
try {
|
||||||
|
containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue();
|
||||||
|
} catch (BerDataValueFormatException e) {
|
||||||
|
throw new Asn1DecodingException("Failed to decode top-level data value", e);
|
||||||
|
}
|
||||||
|
if (containerDataValue == null) {
|
||||||
|
throw new Asn1DecodingException("Empty input");
|
||||||
|
}
|
||||||
|
return parseSetOf(containerDataValue, elementClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T parse(BerDataValue container, Class<T> containerClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
if (container == null) {
|
||||||
|
throw new NullPointerException("container == null");
|
||||||
|
}
|
||||||
|
if (containerClass == null) {
|
||||||
|
throw new NullPointerException("containerClass == null");
|
||||||
|
}
|
||||||
|
|
||||||
|
Asn1Type dataType = getContainerAsn1Type(containerClass);
|
||||||
|
switch (dataType) {
|
||||||
|
case CHOICE:
|
||||||
|
return parseChoice(container, containerClass);
|
||||||
|
|
||||||
|
case SEQUENCE:
|
||||||
|
{
|
||||||
|
int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL;
|
||||||
|
int expectedTagNumber = BerEncoding.getTagNumber(dataType);
|
||||||
|
if ((container.getTagClass() != expectedTagClass)
|
||||||
|
|| (container.getTagNumber() != expectedTagNumber)) {
|
||||||
|
throw new Asn1UnexpectedTagException(
|
||||||
|
"Unexpected data value read as " + containerClass.getName()
|
||||||
|
+ ". Expected " + BerEncoding.tagClassAndNumberToString(
|
||||||
|
expectedTagClass, expectedTagNumber)
|
||||||
|
+ ", but read: " + BerEncoding.tagClassAndNumberToString(
|
||||||
|
container.getTagClass(), container.getTagNumber()));
|
||||||
|
}
|
||||||
|
return parseSequence(container, containerClass);
|
||||||
|
}
|
||||||
|
case UNENCODED_CONTAINER:
|
||||||
|
return parseSequence(container, containerClass, true);
|
||||||
|
default:
|
||||||
|
throw new Asn1DecodingException("Parsing container " + dataType + " not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
|
||||||
|
if (fields.isEmpty()) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"No fields annotated with " + Asn1Field.class.getName()
|
||||||
|
+ " in CHOICE class " + containerClass.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that class + tagNumber don't clash between the choices
|
||||||
|
for (int i = 0; i < fields.size() - 1; i++) {
|
||||||
|
AnnotatedField f1 = fields.get(i);
|
||||||
|
int tagNumber1 = f1.getBerTagNumber();
|
||||||
|
int tagClass1 = f1.getBerTagClass();
|
||||||
|
for (int j = i + 1; j < fields.size(); j++) {
|
||||||
|
AnnotatedField f2 = fields.get(j);
|
||||||
|
int tagNumber2 = f2.getBerTagNumber();
|
||||||
|
int tagClass2 = f2.getBerTagClass();
|
||||||
|
if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"CHOICE fields are indistinguishable because they have the same tag"
|
||||||
|
+ " class and number: " + containerClass.getName()
|
||||||
|
+ "." + f1.getField().getName()
|
||||||
|
+ " and ." + f2.getField().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate the container object / result
|
||||||
|
T obj;
|
||||||
|
try {
|
||||||
|
obj = containerClass.getConstructor().newInstance();
|
||||||
|
} catch (IllegalArgumentException | ReflectiveOperationException e) {
|
||||||
|
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
|
||||||
|
}
|
||||||
|
// Set the matching field's value from the data value
|
||||||
|
for (AnnotatedField field : fields) {
|
||||||
|
try {
|
||||||
|
field.setValueFrom(dataValue, obj);
|
||||||
|
return obj;
|
||||||
|
} catch (Asn1UnexpectedTagException expected) {
|
||||||
|
// not a match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"No options of CHOICE " + containerClass.getName() + " matched");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
return parseSequence(container, containerClass, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T parseSequence(BerDataValue container, Class<T> containerClass,
|
||||||
|
boolean isUnencodedContainer) throws Asn1DecodingException {
|
||||||
|
List<AnnotatedField> fields = getAnnotatedFields(containerClass);
|
||||||
|
Collections.sort(
|
||||||
|
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
|
||||||
|
// Check that there are no fields with the same index
|
||||||
|
if (fields.size() > 1) {
|
||||||
|
AnnotatedField lastField = null;
|
||||||
|
for (AnnotatedField field : fields) {
|
||||||
|
if ((lastField != null)
|
||||||
|
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Fields have the same index: " + containerClass.getName()
|
||||||
|
+ "." + lastField.getField().getName()
|
||||||
|
+ " and ." + field.getField().getName());
|
||||||
|
}
|
||||||
|
lastField = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate the container object / result
|
||||||
|
T t;
|
||||||
|
try {
|
||||||
|
t = containerClass.getConstructor().newInstance();
|
||||||
|
} catch (IllegalArgumentException | ReflectiveOperationException e) {
|
||||||
|
throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse fields one by one. A complication is that there may be optional fields.
|
||||||
|
int nextUnreadFieldIndex = 0;
|
||||||
|
BerDataValueReader elementsReader = container.contentsReader();
|
||||||
|
while (nextUnreadFieldIndex < fields.size()) {
|
||||||
|
BerDataValue dataValue;
|
||||||
|
try {
|
||||||
|
// if this is the first field of an unencoded container then the entire contents of
|
||||||
|
// the container should be used when assigning to this field.
|
||||||
|
if (isUnencodedContainer && nextUnreadFieldIndex == 0) {
|
||||||
|
dataValue = container;
|
||||||
|
} else {
|
||||||
|
dataValue = elementsReader.readDataValue();
|
||||||
|
}
|
||||||
|
} catch (BerDataValueFormatException e) {
|
||||||
|
throw new Asn1DecodingException("Malformed data value", e);
|
||||||
|
}
|
||||||
|
if (dataValue == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = nextUnreadFieldIndex; i < fields.size(); i++) {
|
||||||
|
AnnotatedField field = fields.get(i);
|
||||||
|
try {
|
||||||
|
if (field.isOptional()) {
|
||||||
|
// Optional field -- might not be present and we may thus be trying to set
|
||||||
|
// it from the wrong tag.
|
||||||
|
try {
|
||||||
|
field.setValueFrom(dataValue, t);
|
||||||
|
nextUnreadFieldIndex = i + 1;
|
||||||
|
break;
|
||||||
|
} catch (Asn1UnexpectedTagException e) {
|
||||||
|
// This field is not present, attempt to use this data value for the
|
||||||
|
// next / iteration of the loop
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Mandatory field -- if we can't set its value from this data value, then
|
||||||
|
// it's an error
|
||||||
|
field.setValueFrom(dataValue, t);
|
||||||
|
nextUnreadFieldIndex = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Asn1DecodingException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Failed to parse " + containerClass.getName()
|
||||||
|
+ "." + field.getField().getName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness
|
||||||
|
// of elements -- it's an unordered collection.
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
List<T> result = new ArrayList<>();
|
||||||
|
BerDataValueReader elementsReader = container.contentsReader();
|
||||||
|
while (true) {
|
||||||
|
BerDataValue dataValue;
|
||||||
|
try {
|
||||||
|
dataValue = elementsReader.readDataValue();
|
||||||
|
} catch (BerDataValueFormatException e) {
|
||||||
|
throw new Asn1DecodingException("Malformed data value", e);
|
||||||
|
}
|
||||||
|
if (dataValue == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
T element;
|
||||||
|
if (ByteBuffer.class.equals(elementClass)) {
|
||||||
|
element = (T) dataValue.getEncodedContents();
|
||||||
|
} else if (Asn1OpaqueObject.class.equals(elementClass)) {
|
||||||
|
element = (T) new Asn1OpaqueObject(dataValue.getEncoded());
|
||||||
|
} else {
|
||||||
|
element = parse(dataValue, elementClass);
|
||||||
|
}
|
||||||
|
result.add(element);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Asn1Type getContainerAsn1Type(Class<?> containerClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if (containerAnnotation == null) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
containerClass.getName() + " is not annotated with "
|
||||||
|
+ Asn1Class.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (containerAnnotation.type()) {
|
||||||
|
case CHOICE:
|
||||||
|
case SEQUENCE:
|
||||||
|
case UNENCODED_CONTAINER:
|
||||||
|
return containerAnnotation.type();
|
||||||
|
default:
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Unsupported ASN.1 container annotation type: "
|
||||||
|
+ containerAnnotation.type());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> getElementType(Field field)
|
||||||
|
throws Asn1DecodingException, ClassNotFoundException {
|
||||||
|
String type = field.getGenericType().getTypeName();
|
||||||
|
int delimiterIndex = type.indexOf('<');
|
||||||
|
if (delimiterIndex == -1) {
|
||||||
|
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
|
||||||
|
}
|
||||||
|
int startIndex = delimiterIndex + 1;
|
||||||
|
int endIndex = type.indexOf('>', startIndex);
|
||||||
|
// TODO: handle comma?
|
||||||
|
if (endIndex == -1) {
|
||||||
|
throw new Asn1DecodingException("Not a container type: " + field.getGenericType());
|
||||||
|
}
|
||||||
|
String elementClassName = type.substring(startIndex, endIndex);
|
||||||
|
return Class.forName(elementClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AnnotatedField {
|
||||||
|
private final Field mField;
|
||||||
|
private final Asn1Field mAnnotation;
|
||||||
|
private final Asn1Type mDataType;
|
||||||
|
private final Asn1TagClass mTagClass;
|
||||||
|
private final int mBerTagClass;
|
||||||
|
private final int mBerTagNumber;
|
||||||
|
private final Asn1Tagging mTagging;
|
||||||
|
private final boolean mOptional;
|
||||||
|
|
||||||
|
public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException {
|
||||||
|
mField = field;
|
||||||
|
mAnnotation = annotation;
|
||||||
|
mDataType = annotation.type();
|
||||||
|
|
||||||
|
Asn1TagClass tagClass = annotation.cls();
|
||||||
|
if (tagClass == Asn1TagClass.AUTOMATIC) {
|
||||||
|
if (annotation.tagNumber() != -1) {
|
||||||
|
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
|
||||||
|
} else {
|
||||||
|
tagClass = Asn1TagClass.UNIVERSAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTagClass = tagClass;
|
||||||
|
mBerTagClass = BerEncoding.getTagClass(mTagClass);
|
||||||
|
|
||||||
|
int tagNumber;
|
||||||
|
if (annotation.tagNumber() != -1) {
|
||||||
|
tagNumber = annotation.tagNumber();
|
||||||
|
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
|
||||||
|
tagNumber = -1;
|
||||||
|
} else {
|
||||||
|
tagNumber = BerEncoding.getTagNumber(mDataType);
|
||||||
|
}
|
||||||
|
mBerTagNumber = tagNumber;
|
||||||
|
|
||||||
|
mTagging = annotation.tagging();
|
||||||
|
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
|
||||||
|
&& (annotation.tagNumber() == -1)) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Tag number must be specified when tagging mode is " + mTagging);
|
||||||
|
}
|
||||||
|
|
||||||
|
mOptional = annotation.optional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field getField() {
|
||||||
|
return mField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1Field getAnnotation() {
|
||||||
|
return mAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOptional() {
|
||||||
|
return mOptional;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBerTagClass() {
|
||||||
|
return mBerTagClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBerTagNumber() {
|
||||||
|
return mBerTagNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException {
|
||||||
|
int readTagClass = dataValue.getTagClass();
|
||||||
|
if (mBerTagNumber != -1) {
|
||||||
|
int readTagNumber = dataValue.getTagNumber();
|
||||||
|
if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) {
|
||||||
|
throw new Asn1UnexpectedTagException(
|
||||||
|
"Tag mismatch. Expected: "
|
||||||
|
+ BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber)
|
||||||
|
+ ", but found "
|
||||||
|
+ BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (readTagClass != mBerTagClass) {
|
||||||
|
throw new Asn1UnexpectedTagException(
|
||||||
|
"Tag mismatch. Expected class: "
|
||||||
|
+ BerEncoding.tagClassToString(mBerTagClass)
|
||||||
|
+ ", but found "
|
||||||
|
+ BerEncoding.tagClassToString(readTagClass));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mTagging == Asn1Tagging.EXPLICIT) {
|
||||||
|
try {
|
||||||
|
dataValue = dataValue.contentsReader().readDataValue();
|
||||||
|
} catch (BerDataValueFormatException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Failed to read contents of EXPLICIT data value", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Asn1UnexpectedTagException extends Asn1DecodingException {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public Asn1UnexpectedTagException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException {
|
||||||
|
if (!encodedOid.hasRemaining()) {
|
||||||
|
throw new Asn1DecodingException("Empty OBJECT IDENTIFIER");
|
||||||
|
}
|
||||||
|
|
||||||
|
// First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2
|
||||||
|
long firstComponent = decodeBase128UnsignedLong(encodedOid);
|
||||||
|
int firstNode = (int) Math.min(firstComponent / 40, 2);
|
||||||
|
long secondNode = firstComponent - firstNode * 40;
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.append(Long.toString(firstNode)).append('.')
|
||||||
|
.append(Long.toString(secondNode));
|
||||||
|
|
||||||
|
// Each consecutive node is encoded as a separate component
|
||||||
|
while (encodedOid.hasRemaining()) {
|
||||||
|
long node = decodeBase128UnsignedLong(encodedOid);
|
||||||
|
result.append('.').append(Long.toString(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException {
|
||||||
|
if (!encoded.hasRemaining()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long result = 0;
|
||||||
|
while (encoded.hasRemaining()) {
|
||||||
|
if (result > Long.MAX_VALUE >>> 7) {
|
||||||
|
throw new Asn1DecodingException("Base-128 number too large");
|
||||||
|
}
|
||||||
|
int b = encoded.get() & 0xff;
|
||||||
|
result <<= 7;
|
||||||
|
result |= b & 0x7f;
|
||||||
|
if ((b & 0x80) == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Truncated base-128 encoded input: missing terminating byte, with highest bit not"
|
||||||
|
+ " set");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigInteger integerToBigInteger(ByteBuffer encoded) {
|
||||||
|
if (!encoded.hasRemaining()) {
|
||||||
|
return BigInteger.ZERO;
|
||||||
|
}
|
||||||
|
return new BigInteger(ByteBufferUtils.toByteArray(encoded));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException {
|
||||||
|
BigInteger value = integerToBigInteger(encoded);
|
||||||
|
if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0
|
||||||
|
|| value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value));
|
||||||
|
}
|
||||||
|
return value.intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException {
|
||||||
|
BigInteger value = integerToBigInteger(encoded);
|
||||||
|
if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0
|
||||||
|
|| value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value));
|
||||||
|
}
|
||||||
|
return value.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
Field[] declaredFields = containerClass.getDeclaredFields();
|
||||||
|
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
|
||||||
|
for (Field field : declaredFields) {
|
||||||
|
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
|
||||||
|
if (annotation == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Modifier.isStatic(field.getModifiers())) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
Asn1Field.class.getName() + " used on a static field: "
|
||||||
|
+ containerClass.getName() + "." + field.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
AnnotatedField annotatedField;
|
||||||
|
try {
|
||||||
|
annotatedField = new AnnotatedField(field, annotation);
|
||||||
|
} catch (Asn1DecodingException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Invalid ASN.1 annotation on "
|
||||||
|
+ containerClass.getName() + "." + field.getName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
result.add(annotatedField);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BerToJavaConverter {
|
||||||
|
private BerToJavaConverter() {}
|
||||||
|
|
||||||
|
public static void setFieldValue(
|
||||||
|
Object obj, Field field, Asn1Type type, BerDataValue dataValue)
|
||||||
|
throws Asn1DecodingException {
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case SET_OF:
|
||||||
|
case SEQUENCE_OF:
|
||||||
|
if (Asn1OpaqueObject.class.equals(field.getType())) {
|
||||||
|
field.set(obj, convert(type, dataValue, field.getType()));
|
||||||
|
} else {
|
||||||
|
field.set(obj, parseSetOf(dataValue, getElementType(field)));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
field.set(obj, convert(type, dataValue, field.getType()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Failed to set value of " + obj.getClass().getName()
|
||||||
|
+ "." + field.getName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T convert(
|
||||||
|
Asn1Type sourceType,
|
||||||
|
BerDataValue dataValue,
|
||||||
|
Class<T> targetType) throws Asn1DecodingException {
|
||||||
|
if (ByteBuffer.class.equals(targetType)) {
|
||||||
|
return (T) dataValue.getEncodedContents();
|
||||||
|
} else if (byte[].class.equals(targetType)) {
|
||||||
|
ByteBuffer resultBuf = dataValue.getEncodedContents();
|
||||||
|
if (!resultBuf.hasRemaining()) {
|
||||||
|
return (T) EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
byte[] result = new byte[resultBuf.remaining()];
|
||||||
|
resultBuf.get(result);
|
||||||
|
return (T) result;
|
||||||
|
} else if (Asn1OpaqueObject.class.equals(targetType)) {
|
||||||
|
return (T) new Asn1OpaqueObject(dataValue.getEncoded());
|
||||||
|
}
|
||||||
|
ByteBuffer encodedContents = dataValue.getEncodedContents();
|
||||||
|
switch (sourceType) {
|
||||||
|
case INTEGER:
|
||||||
|
if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) {
|
||||||
|
return (T) Integer.valueOf(integerToInt(encodedContents));
|
||||||
|
} else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) {
|
||||||
|
return (T) Long.valueOf(integerToLong(encodedContents));
|
||||||
|
} else if (BigInteger.class.equals(targetType)) {
|
||||||
|
return (T) integerToBigInteger(encodedContents);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OBJECT_IDENTIFIER:
|
||||||
|
if (String.class.equals(targetType)) {
|
||||||
|
return (T) oidToString(encodedContents);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UTC_TIME:
|
||||||
|
case GENERALIZED_TIME:
|
||||||
|
if (String.class.equals(targetType)) {
|
||||||
|
return (T) new String(ByteBufferUtils.toByteArray(encodedContents));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BOOLEAN:
|
||||||
|
// A boolean should be encoded in a single byte with a value of 0 for false and
|
||||||
|
// any non-zero value for true.
|
||||||
|
if (boolean.class.equals(targetType)) {
|
||||||
|
if (encodedContents.remaining() != 1) {
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Incorrect encoded size of boolean value: "
|
||||||
|
+ encodedContents.remaining());
|
||||||
|
}
|
||||||
|
boolean result;
|
||||||
|
if (encodedContents.get() == 0) {
|
||||||
|
result = false;
|
||||||
|
} else {
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
return (T) new Boolean(result);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SEQUENCE:
|
||||||
|
{
|
||||||
|
Asn1Class containerAnnotation =
|
||||||
|
targetType.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if ((containerAnnotation != null)
|
||||||
|
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
|
||||||
|
return parseSequence(dataValue, targetType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CHOICE:
|
||||||
|
{
|
||||||
|
Asn1Class containerAnnotation =
|
||||||
|
targetType.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if ((containerAnnotation != null)
|
||||||
|
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
|
||||||
|
return parseChoice(dataValue, targetType);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Asn1DecodingException(
|
||||||
|
"Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Asn1Class {
|
||||||
|
public Asn1Type type();
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that input could not be decoded into intended ASN.1 structure.
|
||||||
|
*/
|
||||||
|
public class Asn1DecodingException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public Asn1DecodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1DecodingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,596 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.ber.BerEncoding;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encoder of ASN.1 structures into DER-encoded form.
|
||||||
|
*
|
||||||
|
* <p>Structure is described to the encoder by providing a class annotated with {@link Asn1Class},
|
||||||
|
* containing fields annotated with {@link Asn1Field}.
|
||||||
|
*/
|
||||||
|
public final class Asn1DerEncoder {
|
||||||
|
private Asn1DerEncoder() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the DER-encoded form of the provided ASN.1 structure.
|
||||||
|
*
|
||||||
|
* @param container container to be encoded. The container's class must meet the following
|
||||||
|
* requirements:
|
||||||
|
* <ul>
|
||||||
|
* <li>The class must be annotated with {@link Asn1Class}.</li>
|
||||||
|
* <li>Member fields of the class which are to be encoded must be annotated with
|
||||||
|
* {@link Asn1Field} and be public.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @throws Asn1EncodingException if the input could not be encoded
|
||||||
|
*/
|
||||||
|
public static byte[] encode(Object container) throws Asn1EncodingException {
|
||||||
|
Class<?> containerClass = container.getClass();
|
||||||
|
Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if (containerAnnotation == null) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
containerClass.getName() + " not annotated with " + Asn1Class.class.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
Asn1Type containerType = containerAnnotation.type();
|
||||||
|
switch (containerType) {
|
||||||
|
case CHOICE:
|
||||||
|
return toChoice(container);
|
||||||
|
case SEQUENCE:
|
||||||
|
return toSequence(container);
|
||||||
|
case UNENCODED_CONTAINER:
|
||||||
|
return toSequence(container, true);
|
||||||
|
default:
|
||||||
|
throw new Asn1EncodingException("Unsupported container type: " + containerType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toChoice(Object container) throws Asn1EncodingException {
|
||||||
|
Class<?> containerClass = container.getClass();
|
||||||
|
List<AnnotatedField> fields = getAnnotatedFields(container);
|
||||||
|
if (fields.isEmpty()) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"No fields annotated with " + Asn1Field.class.getName()
|
||||||
|
+ " in CHOICE class " + containerClass.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
AnnotatedField resultField = null;
|
||||||
|
for (AnnotatedField field : fields) {
|
||||||
|
Object fieldValue = getMemberFieldValue(container, field.getField());
|
||||||
|
if (fieldValue != null) {
|
||||||
|
if (resultField != null) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Multiple non-null fields in CHOICE class " + containerClass.getName()
|
||||||
|
+ ": " + resultField.getField().getName()
|
||||||
|
+ ", " + field.getField().getName());
|
||||||
|
}
|
||||||
|
resultField = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultField == null) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"No non-null fields in CHOICE class " + containerClass.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultField.toDer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toSequence(Object container) throws Asn1EncodingException {
|
||||||
|
return toSequence(container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toSequence(Object container, boolean omitTag)
|
||||||
|
throws Asn1EncodingException {
|
||||||
|
Class<?> containerClass = container.getClass();
|
||||||
|
List<AnnotatedField> fields = getAnnotatedFields(container);
|
||||||
|
Collections.sort(
|
||||||
|
fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index());
|
||||||
|
if (fields.size() > 1) {
|
||||||
|
AnnotatedField lastField = null;
|
||||||
|
for (AnnotatedField field : fields) {
|
||||||
|
if ((lastField != null)
|
||||||
|
&& (lastField.getAnnotation().index() == field.getAnnotation().index())) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Fields have the same index: " + containerClass.getName()
|
||||||
|
+ "." + lastField.getField().getName()
|
||||||
|
+ " and ." + field.getField().getName());
|
||||||
|
}
|
||||||
|
lastField = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<byte[]> serializedFields = new ArrayList<>(fields.size());
|
||||||
|
int contentLen = 0;
|
||||||
|
for (AnnotatedField field : fields) {
|
||||||
|
byte[] serializedField;
|
||||||
|
try {
|
||||||
|
serializedField = field.toDer();
|
||||||
|
} catch (Asn1EncodingException e) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Failed to encode " + containerClass.getName()
|
||||||
|
+ "." + field.getField().getName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
if (serializedField != null) {
|
||||||
|
serializedFields.add(serializedField);
|
||||||
|
contentLen += serializedField.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (omitTag) {
|
||||||
|
byte[] unencodedResult = new byte[contentLen];
|
||||||
|
int index = 0;
|
||||||
|
for (byte[] serializedField : serializedFields) {
|
||||||
|
System.arraycopy(serializedField, 0, unencodedResult, index, serializedField.length);
|
||||||
|
index += serializedField.length;
|
||||||
|
}
|
||||||
|
return unencodedResult;
|
||||||
|
} else {
|
||||||
|
return createTag(
|
||||||
|
BerEncoding.TAG_CLASS_UNIVERSAL, true, BerEncoding.TAG_NUMBER_SEQUENCE,
|
||||||
|
serializedFields.toArray(new byte[0][]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toSetOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
|
||||||
|
return toSequenceOrSetOf(values, elementType, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toSequenceOf(Collection<?> values, Asn1Type elementType) throws Asn1EncodingException {
|
||||||
|
return toSequenceOrSetOf(values, elementType, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toSequenceOrSetOf(Collection<?> values, Asn1Type elementType, boolean toSet)
|
||||||
|
throws Asn1EncodingException {
|
||||||
|
List<byte[]> serializedValues = new ArrayList<>(values.size());
|
||||||
|
for (Object value : values) {
|
||||||
|
serializedValues.add(JavaToDerConverter.toDer(value, elementType, null));
|
||||||
|
}
|
||||||
|
int tagNumber;
|
||||||
|
if (toSet) {
|
||||||
|
if (serializedValues.size() > 1) {
|
||||||
|
Collections.sort(serializedValues, ByteArrayLexicographicComparator.INSTANCE);
|
||||||
|
}
|
||||||
|
tagNumber = BerEncoding.TAG_NUMBER_SET;
|
||||||
|
} else {
|
||||||
|
tagNumber = BerEncoding.TAG_NUMBER_SEQUENCE;
|
||||||
|
}
|
||||||
|
return createTag(
|
||||||
|
BerEncoding.TAG_CLASS_UNIVERSAL, true, tagNumber,
|
||||||
|
serializedValues.toArray(new byte[0][]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares two bytes arrays based on their lexicographic order. Corresponding elements of the
|
||||||
|
* two arrays are compared in ascending order. Elements at out of range indices are assumed to
|
||||||
|
* be smaller than the smallest possible value for an element.
|
||||||
|
*/
|
||||||
|
private static class ByteArrayLexicographicComparator implements Comparator<byte[]> {
|
||||||
|
private static final ByteArrayLexicographicComparator INSTANCE =
|
||||||
|
new ByteArrayLexicographicComparator();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(byte[] arr1, byte[] arr2) {
|
||||||
|
int commonLength = Math.min(arr1.length, arr2.length);
|
||||||
|
for (int i = 0; i < commonLength; i++) {
|
||||||
|
int diff = (arr1[i] & 0xff) - (arr2[i] & 0xff);
|
||||||
|
if (diff != 0) {
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return arr1.length - arr2.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<AnnotatedField> getAnnotatedFields(Object container)
|
||||||
|
throws Asn1EncodingException {
|
||||||
|
Class<?> containerClass = container.getClass();
|
||||||
|
Field[] declaredFields = containerClass.getDeclaredFields();
|
||||||
|
List<AnnotatedField> result = new ArrayList<>(declaredFields.length);
|
||||||
|
for (Field field : declaredFields) {
|
||||||
|
Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class);
|
||||||
|
if (annotation == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Modifier.isStatic(field.getModifiers())) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
Asn1Field.class.getName() + " used on a static field: "
|
||||||
|
+ containerClass.getName() + "." + field.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
AnnotatedField annotatedField;
|
||||||
|
try {
|
||||||
|
annotatedField = new AnnotatedField(container, field, annotation);
|
||||||
|
} catch (Asn1EncodingException e) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Invalid ASN.1 annotation on "
|
||||||
|
+ containerClass.getName() + "." + field.getName(),
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
result.add(annotatedField);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toInteger(int value) {
|
||||||
|
return toInteger((long) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toInteger(long value) {
|
||||||
|
return toInteger(BigInteger.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toInteger(BigInteger value) {
|
||||||
|
return createTag(
|
||||||
|
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_INTEGER,
|
||||||
|
value.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toBoolean(boolean value) {
|
||||||
|
// A boolean should be encoded in a single byte with a value of 0 for false and any non-zero
|
||||||
|
// value for true.
|
||||||
|
byte[] result = new byte[1];
|
||||||
|
if (value == false) {
|
||||||
|
result[0] = 0;
|
||||||
|
} else {
|
||||||
|
result[0] = 1;
|
||||||
|
}
|
||||||
|
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_BOOLEAN, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] toOid(String oid) throws Asn1EncodingException {
|
||||||
|
ByteArrayOutputStream encodedValue = new ByteArrayOutputStream();
|
||||||
|
String[] nodes = oid.split("\\.");
|
||||||
|
if (nodes.length < 2) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"OBJECT IDENTIFIER must contain at least two nodes: " + oid);
|
||||||
|
}
|
||||||
|
int firstNode;
|
||||||
|
try {
|
||||||
|
firstNode = Integer.parseInt(nodes[0]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new Asn1EncodingException("Node #1 not numeric: " + nodes[0]);
|
||||||
|
}
|
||||||
|
if ((firstNode > 6) || (firstNode < 0)) {
|
||||||
|
throw new Asn1EncodingException("Invalid value for node #1: " + firstNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
int secondNode;
|
||||||
|
try {
|
||||||
|
secondNode = Integer.parseInt(nodes[1]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new Asn1EncodingException("Node #2 not numeric: " + nodes[1]);
|
||||||
|
}
|
||||||
|
if ((secondNode >= 40) || (secondNode < 0)) {
|
||||||
|
throw new Asn1EncodingException("Invalid value for node #2: " + secondNode);
|
||||||
|
}
|
||||||
|
int firstByte = firstNode * 40 + secondNode;
|
||||||
|
if (firstByte > 0xff) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"First two nodes out of range: " + firstNode + "." + secondNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedValue.write(firstByte);
|
||||||
|
for (int i = 2; i < nodes.length; i++) {
|
||||||
|
String nodeString = nodes[i];
|
||||||
|
int node;
|
||||||
|
try {
|
||||||
|
node = Integer.parseInt(nodeString);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new Asn1EncodingException("Node #" + (i + 1) + " not numeric: " + nodeString);
|
||||||
|
}
|
||||||
|
if (node < 0) {
|
||||||
|
throw new Asn1EncodingException("Invalid value for node #" + (i + 1) + ": " + node);
|
||||||
|
}
|
||||||
|
if (node <= 0x7f) {
|
||||||
|
encodedValue.write(node);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node < 1 << 14) {
|
||||||
|
encodedValue.write(0x80 | (node >> 7));
|
||||||
|
encodedValue.write(node & 0x7f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node < 1 << 21) {
|
||||||
|
encodedValue.write(0x80 | (node >> 14));
|
||||||
|
encodedValue.write(0x80 | ((node >> 7) & 0x7f));
|
||||||
|
encodedValue.write(node & 0x7f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Asn1EncodingException("Node #" + (i + 1) + " too large: " + node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return createTag(
|
||||||
|
BerEncoding.TAG_CLASS_UNIVERSAL, false, BerEncoding.TAG_NUMBER_OBJECT_IDENTIFIER,
|
||||||
|
encodedValue.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getMemberFieldValue(Object obj, Field field)
|
||||||
|
throws Asn1EncodingException {
|
||||||
|
try {
|
||||||
|
return field.get(obj);
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Failed to read " + obj.getClass().getName() + "." + field.getName(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class AnnotatedField {
|
||||||
|
private final Field mField;
|
||||||
|
private final Object mObject;
|
||||||
|
private final Asn1Field mAnnotation;
|
||||||
|
private final Asn1Type mDataType;
|
||||||
|
private final Asn1Type mElementDataType;
|
||||||
|
private final Asn1TagClass mTagClass;
|
||||||
|
private final int mDerTagClass;
|
||||||
|
private final int mDerTagNumber;
|
||||||
|
private final Asn1Tagging mTagging;
|
||||||
|
private final boolean mOptional;
|
||||||
|
|
||||||
|
public AnnotatedField(Object obj, Field field, Asn1Field annotation)
|
||||||
|
throws Asn1EncodingException {
|
||||||
|
mObject = obj;
|
||||||
|
mField = field;
|
||||||
|
mAnnotation = annotation;
|
||||||
|
mDataType = annotation.type();
|
||||||
|
mElementDataType = annotation.elementType();
|
||||||
|
|
||||||
|
Asn1TagClass tagClass = annotation.cls();
|
||||||
|
if (tagClass == Asn1TagClass.AUTOMATIC) {
|
||||||
|
if (annotation.tagNumber() != -1) {
|
||||||
|
tagClass = Asn1TagClass.CONTEXT_SPECIFIC;
|
||||||
|
} else {
|
||||||
|
tagClass = Asn1TagClass.UNIVERSAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mTagClass = tagClass;
|
||||||
|
mDerTagClass = BerEncoding.getTagClass(mTagClass);
|
||||||
|
|
||||||
|
int tagNumber;
|
||||||
|
if (annotation.tagNumber() != -1) {
|
||||||
|
tagNumber = annotation.tagNumber();
|
||||||
|
} else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) {
|
||||||
|
tagNumber = -1;
|
||||||
|
} else {
|
||||||
|
tagNumber = BerEncoding.getTagNumber(mDataType);
|
||||||
|
}
|
||||||
|
mDerTagNumber = tagNumber;
|
||||||
|
|
||||||
|
mTagging = annotation.tagging();
|
||||||
|
if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT))
|
||||||
|
&& (annotation.tagNumber() == -1)) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Tag number must be specified when tagging mode is " + mTagging);
|
||||||
|
}
|
||||||
|
|
||||||
|
mOptional = annotation.optional();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Field getField() {
|
||||||
|
return mField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1Field getAnnotation() {
|
||||||
|
return mAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] toDer() throws Asn1EncodingException {
|
||||||
|
Object fieldValue = getMemberFieldValue(mObject, mField);
|
||||||
|
if (fieldValue == null) {
|
||||||
|
if (mOptional) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new Asn1EncodingException("Required field not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encoded = JavaToDerConverter.toDer(fieldValue, mDataType, mElementDataType);
|
||||||
|
switch (mTagging) {
|
||||||
|
case NORMAL:
|
||||||
|
return encoded;
|
||||||
|
case EXPLICIT:
|
||||||
|
return createTag(mDerTagClass, true, mDerTagNumber, encoded);
|
||||||
|
case IMPLICIT:
|
||||||
|
int originalTagNumber = BerEncoding.getTagNumber(encoded[0]);
|
||||||
|
if (originalTagNumber == 0x1f) {
|
||||||
|
throw new Asn1EncodingException("High-tag-number form not supported");
|
||||||
|
}
|
||||||
|
if (mDerTagNumber >= 0x1f) {
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Unsupported high tag number: " + mDerTagNumber);
|
||||||
|
}
|
||||||
|
encoded[0] = BerEncoding.setTagNumber(encoded[0], mDerTagNumber);
|
||||||
|
encoded[0] = BerEncoding.setTagClass(encoded[0], mDerTagClass);
|
||||||
|
return encoded;
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unknown tagging mode: " + mTagging);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] createTag(
|
||||||
|
int tagClass, boolean constructed, int tagNumber, byte[]... contents) {
|
||||||
|
if (tagNumber >= 0x1f) {
|
||||||
|
throw new IllegalArgumentException("High tag numbers not supported: " + tagNumber);
|
||||||
|
}
|
||||||
|
// tag class & number fit into the first byte
|
||||||
|
byte firstIdentifierByte =
|
||||||
|
(byte) ((tagClass << 6) | (constructed ? 1 << 5 : 0) | tagNumber);
|
||||||
|
|
||||||
|
int contentsLength = 0;
|
||||||
|
for (byte[] c : contents) {
|
||||||
|
contentsLength += c.length;
|
||||||
|
}
|
||||||
|
int contentsPosInResult;
|
||||||
|
byte[] result;
|
||||||
|
if (contentsLength < 0x80) {
|
||||||
|
// Length fits into one byte
|
||||||
|
contentsPosInResult = 2;
|
||||||
|
result = new byte[contentsPosInResult + contentsLength];
|
||||||
|
result[0] = firstIdentifierByte;
|
||||||
|
result[1] = (byte) contentsLength;
|
||||||
|
} else {
|
||||||
|
// Length is represented as multiple bytes
|
||||||
|
// The low 7 bits of the first byte represent the number of length bytes (following the
|
||||||
|
// first byte) in which the length is in big-endian base-256 form
|
||||||
|
if (contentsLength <= 0xff) {
|
||||||
|
contentsPosInResult = 3;
|
||||||
|
result = new byte[contentsPosInResult + contentsLength];
|
||||||
|
result[1] = (byte) 0x81; // 1 length byte
|
||||||
|
result[2] = (byte) contentsLength;
|
||||||
|
} else if (contentsLength <= 0xffff) {
|
||||||
|
contentsPosInResult = 4;
|
||||||
|
result = new byte[contentsPosInResult + contentsLength];
|
||||||
|
result[1] = (byte) 0x82; // 2 length bytes
|
||||||
|
result[2] = (byte) (contentsLength >> 8);
|
||||||
|
result[3] = (byte) (contentsLength & 0xff);
|
||||||
|
} else if (contentsLength <= 0xffffff) {
|
||||||
|
contentsPosInResult = 5;
|
||||||
|
result = new byte[contentsPosInResult + contentsLength];
|
||||||
|
result[1] = (byte) 0x83; // 3 length bytes
|
||||||
|
result[2] = (byte) (contentsLength >> 16);
|
||||||
|
result[3] = (byte) ((contentsLength >> 8) & 0xff);
|
||||||
|
result[4] = (byte) (contentsLength & 0xff);
|
||||||
|
} else {
|
||||||
|
contentsPosInResult = 6;
|
||||||
|
result = new byte[contentsPosInResult + contentsLength];
|
||||||
|
result[1] = (byte) 0x84; // 4 length bytes
|
||||||
|
result[2] = (byte) (contentsLength >> 24);
|
||||||
|
result[3] = (byte) ((contentsLength >> 16) & 0xff);
|
||||||
|
result[4] = (byte) ((contentsLength >> 8) & 0xff);
|
||||||
|
result[5] = (byte) (contentsLength & 0xff);
|
||||||
|
}
|
||||||
|
result[0] = firstIdentifierByte;
|
||||||
|
}
|
||||||
|
for (byte[] c : contents) {
|
||||||
|
System.arraycopy(c, 0, result, contentsPosInResult, c.length);
|
||||||
|
contentsPosInResult += c.length;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class JavaToDerConverter {
|
||||||
|
private JavaToDerConverter() {}
|
||||||
|
|
||||||
|
public static byte[] toDer(Object source, Asn1Type targetType, Asn1Type targetElementType)
|
||||||
|
throws Asn1EncodingException {
|
||||||
|
Class<?> sourceType = source.getClass();
|
||||||
|
if (Asn1OpaqueObject.class.equals(sourceType)) {
|
||||||
|
ByteBuffer buf = ((Asn1OpaqueObject) source).getEncoded();
|
||||||
|
byte[] result = new byte[buf.remaining()];
|
||||||
|
buf.get(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((targetType == null) || (targetType == Asn1Type.ANY)) {
|
||||||
|
return encode(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (targetType) {
|
||||||
|
case OCTET_STRING:
|
||||||
|
case BIT_STRING:
|
||||||
|
byte[] value = null;
|
||||||
|
if (source instanceof ByteBuffer) {
|
||||||
|
ByteBuffer buf = (ByteBuffer) source;
|
||||||
|
value = new byte[buf.remaining()];
|
||||||
|
buf.slice().get(value);
|
||||||
|
} else if (source instanceof byte[]) {
|
||||||
|
value = (byte[]) source;
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
return createTag(
|
||||||
|
BerEncoding.TAG_CLASS_UNIVERSAL,
|
||||||
|
false,
|
||||||
|
BerEncoding.getTagNumber(targetType),
|
||||||
|
value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case INTEGER:
|
||||||
|
if (source instanceof Integer) {
|
||||||
|
return toInteger((Integer) source);
|
||||||
|
} else if (source instanceof Long) {
|
||||||
|
return toInteger((Long) source);
|
||||||
|
} else if (source instanceof BigInteger) {
|
||||||
|
return toInteger((BigInteger) source);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BOOLEAN:
|
||||||
|
if (source instanceof Boolean) {
|
||||||
|
return toBoolean((Boolean) (source));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UTC_TIME:
|
||||||
|
case GENERALIZED_TIME:
|
||||||
|
if (source instanceof String) {
|
||||||
|
return createTag(BerEncoding.TAG_CLASS_UNIVERSAL, false,
|
||||||
|
BerEncoding.getTagNumber(targetType), ((String) source).getBytes());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OBJECT_IDENTIFIER:
|
||||||
|
if (source instanceof String) {
|
||||||
|
return toOid((String) source);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SEQUENCE:
|
||||||
|
{
|
||||||
|
Asn1Class containerAnnotation =
|
||||||
|
sourceType.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if ((containerAnnotation != null)
|
||||||
|
&& (containerAnnotation.type() == Asn1Type.SEQUENCE)) {
|
||||||
|
return toSequence(source);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CHOICE:
|
||||||
|
{
|
||||||
|
Asn1Class containerAnnotation =
|
||||||
|
sourceType.getDeclaredAnnotation(Asn1Class.class);
|
||||||
|
if ((containerAnnotation != null)
|
||||||
|
&& (containerAnnotation.type() == Asn1Type.CHOICE)) {
|
||||||
|
return toChoice(source);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SET_OF:
|
||||||
|
return toSetOf((Collection<?>) source, targetElementType);
|
||||||
|
case SEQUENCE_OF:
|
||||||
|
return toSequenceOf((Collection<?>) source, targetElementType);
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Asn1EncodingException(
|
||||||
|
"Unsupported conversion: " + sourceType.getName() + " to ASN.1 " + targetType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** ASN.1 DER-encoded {@code NULL}. */
|
||||||
|
public static final Asn1OpaqueObject ASN1_DER_NULL =
|
||||||
|
new Asn1OpaqueObject(new byte[] {BerEncoding.TAG_NUMBER_NULL, 0});
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an ASN.1 structure could not be encoded.
|
||||||
|
*/
|
||||||
|
public class Asn1EncodingException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public Asn1EncodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1EncodingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target({ElementType.FIELD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Asn1Field {
|
||||||
|
/** Index used to order fields in a container. Required for fields of SEQUENCE containers. */
|
||||||
|
public int index() default 0;
|
||||||
|
|
||||||
|
public Asn1TagClass cls() default Asn1TagClass.AUTOMATIC;
|
||||||
|
|
||||||
|
public Asn1Type type();
|
||||||
|
|
||||||
|
/** Tagging mode. Default: NORMAL. */
|
||||||
|
public Asn1Tagging tagging() default Asn1Tagging.NORMAL;
|
||||||
|
|
||||||
|
/** Tag number. Required when IMPLICIT and EXPLICIT tagging mode is used.*/
|
||||||
|
public int tagNumber() default -1;
|
||||||
|
|
||||||
|
/** {@code true} if this field is optional. Ignored for fields of CHOICE containers. */
|
||||||
|
public boolean optional() default false;
|
||||||
|
|
||||||
|
/** Type of elements. Used only for SET_OF or SEQUENCE_OF. */
|
||||||
|
public Asn1Type elementType() default Asn1Type.ANY;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opaque holder of encoded ASN.1 stuff.
|
||||||
|
*/
|
||||||
|
public class Asn1OpaqueObject {
|
||||||
|
private final ByteBuffer mEncoded;
|
||||||
|
|
||||||
|
public Asn1OpaqueObject(ByteBuffer encoded) {
|
||||||
|
mEncoded = encoded.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Asn1OpaqueObject(byte[] encoded) {
|
||||||
|
mEncoded = ByteBuffer.wrap(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getEncoded() {
|
||||||
|
return mEncoded.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
public enum Asn1TagClass {
|
||||||
|
UNIVERSAL,
|
||||||
|
APPLICATION,
|
||||||
|
CONTEXT_SPECIFIC,
|
||||||
|
PRIVATE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not really an actual tag class: decoder/encoder will attempt to deduce the correct tag class
|
||||||
|
* automatically.
|
||||||
|
*/
|
||||||
|
AUTOMATIC,
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
public enum Asn1Tagging {
|
||||||
|
NORMAL,
|
||||||
|
EXPLICIT,
|
||||||
|
IMPLICIT,
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1;
|
||||||
|
|
||||||
|
public enum Asn1Type {
|
||||||
|
ANY,
|
||||||
|
CHOICE,
|
||||||
|
INTEGER,
|
||||||
|
OBJECT_IDENTIFIER,
|
||||||
|
OCTET_STRING,
|
||||||
|
SEQUENCE,
|
||||||
|
SEQUENCE_OF,
|
||||||
|
SET_OF,
|
||||||
|
BIT_STRING,
|
||||||
|
UTC_TIME,
|
||||||
|
GENERALIZED_TIME,
|
||||||
|
BOOLEAN,
|
||||||
|
// This type can be used to annotate classes that encapsulate ASN.1 structures that are not
|
||||||
|
// classified as a SEQUENCE or SET.
|
||||||
|
UNENCODED_CONTAINER
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASN.1 Basic Encoding Rules (BER) data value -- see {@code X.690}.
|
||||||
|
*/
|
||||||
|
public class BerDataValue {
|
||||||
|
private final ByteBuffer mEncoded;
|
||||||
|
private final ByteBuffer mEncodedContents;
|
||||||
|
private final int mTagClass;
|
||||||
|
private final boolean mConstructed;
|
||||||
|
private final int mTagNumber;
|
||||||
|
|
||||||
|
BerDataValue(
|
||||||
|
ByteBuffer encoded,
|
||||||
|
ByteBuffer encodedContents,
|
||||||
|
int tagClass,
|
||||||
|
boolean constructed,
|
||||||
|
int tagNumber) {
|
||||||
|
mEncoded = encoded;
|
||||||
|
mEncodedContents = encodedContents;
|
||||||
|
mTagClass = tagClass;
|
||||||
|
mConstructed = constructed;
|
||||||
|
mTagNumber = tagNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag class of this data value. See {@link BerEncoding} {@code TAG_CLASS}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public int getTagClass() {
|
||||||
|
return mTagClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the content octets of this data value are the complete BER encoding
|
||||||
|
* of one or more data values, {@code false} if the content octets of this data value directly
|
||||||
|
* represent the value.
|
||||||
|
*/
|
||||||
|
public boolean isConstructed() {
|
||||||
|
return mConstructed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag number of this data value. See {@link BerEncoding} {@code TAG_NUMBER}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public int getTagNumber() {
|
||||||
|
return mTagNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the encoded form of this data value.
|
||||||
|
*/
|
||||||
|
public ByteBuffer getEncoded() {
|
||||||
|
return mEncoded.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the encoded contents of this data value.
|
||||||
|
*/
|
||||||
|
public ByteBuffer getEncodedContents() {
|
||||||
|
return mEncodedContents.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new reader of the contents of this data value.
|
||||||
|
*/
|
||||||
|
public BerDataValueReader contentsReader() {
|
||||||
|
return new ByteBufferBerDataValueReader(getEncodedContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new reader which returns just this data value. This may be useful for re-reading
|
||||||
|
* this value in different contexts.
|
||||||
|
*/
|
||||||
|
public BerDataValueReader dataValueReader() {
|
||||||
|
return new ParsedValueReader(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ParsedValueReader implements BerDataValueReader {
|
||||||
|
private final BerDataValue mValue;
|
||||||
|
private boolean mValueOutput;
|
||||||
|
|
||||||
|
public ParsedValueReader(BerDataValue value) {
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BerDataValue readDataValue() throws BerDataValueFormatException {
|
||||||
|
if (mValueOutput) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
mValueOutput = true;
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an ASN.1 data value being read could not be decoded using
|
||||||
|
* Basic Encoding Rules (BER).
|
||||||
|
*/
|
||||||
|
public class BerDataValueFormatException extends Exception {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public BerDataValueFormatException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BerDataValueFormatException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reader of ASN.1 Basic Encoding Rules (BER) data values.
|
||||||
|
*
|
||||||
|
* <p>BER data value reader returns data values, one by one, from a source. The interpretation of
|
||||||
|
* data values (e.g., how to obtain a numeric value from an INTEGER data value, or how to extract
|
||||||
|
* the elements of a SEQUENCE value) is left to clients of the reader.
|
||||||
|
*/
|
||||||
|
public interface BerDataValueReader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next data value or {@code null} if end of input has been reached.
|
||||||
|
*
|
||||||
|
* @throws BerDataValueFormatException if the value being read is malformed.
|
||||||
|
*/
|
||||||
|
BerDataValue readDataValue() throws BerDataValueFormatException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1TagClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ASN.1 Basic Encoding Rules (BER) constants and helper methods. See {@code X.690}.
|
||||||
|
*/
|
||||||
|
public abstract class BerEncoding {
|
||||||
|
private BerEncoding() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructed vs primitive flag in the first identifier byte.
|
||||||
|
*/
|
||||||
|
public static final int ID_FLAG_CONSTRUCTED_ENCODING = 1 << 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: UNIVERSAL
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_UNIVERSAL = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: APPLICATION
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_APPLICATION = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: CONTEXT SPECIFIC
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_CONTEXT_SPECIFIC = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag class: PRIVATE
|
||||||
|
*/
|
||||||
|
public static final int TAG_CLASS_PRIVATE = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: BOOLEAN
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_BOOLEAN = 0x1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: INTEGER
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_INTEGER = 0x2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: BIT STRING
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_BIT_STRING = 0x3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: OCTET STRING
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_OCTET_STRING = 0x4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: NULL
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_NULL = 0x05;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: OBJECT IDENTIFIER
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_OBJECT_IDENTIFIER = 0x6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: SEQUENCE
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_SEQUENCE = 0x10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: SET
|
||||||
|
*/
|
||||||
|
public static final int TAG_NUMBER_SET = 0x11;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: UTC_TIME
|
||||||
|
*/
|
||||||
|
public final static int TAG_NUMBER_UTC_TIME = 0x17;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag number: GENERALIZED_TIME
|
||||||
|
*/
|
||||||
|
public final static int TAG_NUMBER_GENERALIZED_TIME = 0x18;
|
||||||
|
|
||||||
|
public static int getTagNumber(Asn1Type dataType) {
|
||||||
|
switch (dataType) {
|
||||||
|
case INTEGER:
|
||||||
|
return TAG_NUMBER_INTEGER;
|
||||||
|
case OBJECT_IDENTIFIER:
|
||||||
|
return TAG_NUMBER_OBJECT_IDENTIFIER;
|
||||||
|
case OCTET_STRING:
|
||||||
|
return TAG_NUMBER_OCTET_STRING;
|
||||||
|
case BIT_STRING:
|
||||||
|
return TAG_NUMBER_BIT_STRING;
|
||||||
|
case SET_OF:
|
||||||
|
return TAG_NUMBER_SET;
|
||||||
|
case SEQUENCE:
|
||||||
|
case SEQUENCE_OF:
|
||||||
|
return TAG_NUMBER_SEQUENCE;
|
||||||
|
case UTC_TIME:
|
||||||
|
return TAG_NUMBER_UTC_TIME;
|
||||||
|
case GENERALIZED_TIME:
|
||||||
|
return TAG_NUMBER_GENERALIZED_TIME;
|
||||||
|
case BOOLEAN:
|
||||||
|
return TAG_NUMBER_BOOLEAN;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported data type: " + dataType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getTagClass(Asn1TagClass tagClass) {
|
||||||
|
switch (tagClass) {
|
||||||
|
case APPLICATION:
|
||||||
|
return TAG_CLASS_APPLICATION;
|
||||||
|
case CONTEXT_SPECIFIC:
|
||||||
|
return TAG_CLASS_CONTEXT_SPECIFIC;
|
||||||
|
case PRIVATE:
|
||||||
|
return TAG_CLASS_PRIVATE;
|
||||||
|
case UNIVERSAL:
|
||||||
|
return TAG_CLASS_UNIVERSAL;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported tag class: " + tagClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String tagClassToString(int typeClass) {
|
||||||
|
switch (typeClass) {
|
||||||
|
case TAG_CLASS_APPLICATION:
|
||||||
|
return "APPLICATION";
|
||||||
|
case TAG_CLASS_CONTEXT_SPECIFIC:
|
||||||
|
return "";
|
||||||
|
case TAG_CLASS_PRIVATE:
|
||||||
|
return "PRIVATE";
|
||||||
|
case TAG_CLASS_UNIVERSAL:
|
||||||
|
return "UNIVERSAL";
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unsupported type class: " + typeClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String tagClassAndNumberToString(int tagClass, int tagNumber) {
|
||||||
|
String classString = tagClassToString(tagClass);
|
||||||
|
String numberString = tagNumberToString(tagNumber);
|
||||||
|
return classString.isEmpty() ? numberString : classString + " " + numberString;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static String tagNumberToString(int tagNumber) {
|
||||||
|
switch (tagNumber) {
|
||||||
|
case TAG_NUMBER_INTEGER:
|
||||||
|
return "INTEGER";
|
||||||
|
case TAG_NUMBER_OCTET_STRING:
|
||||||
|
return "OCTET STRING";
|
||||||
|
case TAG_NUMBER_BIT_STRING:
|
||||||
|
return "BIT STRING";
|
||||||
|
case TAG_NUMBER_NULL:
|
||||||
|
return "NULL";
|
||||||
|
case TAG_NUMBER_OBJECT_IDENTIFIER:
|
||||||
|
return "OBJECT IDENTIFIER";
|
||||||
|
case TAG_NUMBER_SEQUENCE:
|
||||||
|
return "SEQUENCE";
|
||||||
|
case TAG_NUMBER_SET:
|
||||||
|
return "SET";
|
||||||
|
case TAG_NUMBER_BOOLEAN:
|
||||||
|
return "BOOLEAN";
|
||||||
|
case TAG_NUMBER_GENERALIZED_TIME:
|
||||||
|
return "GENERALIZED TIME";
|
||||||
|
case TAG_NUMBER_UTC_TIME:
|
||||||
|
return "UTC TIME";
|
||||||
|
default:
|
||||||
|
return "0x" + Integer.toHexString(tagNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the provided first identifier byte indicates that the data value uses
|
||||||
|
* constructed encoding for its contents, or {@code false} if the data value uses primitive
|
||||||
|
* encoding for its contents.
|
||||||
|
*/
|
||||||
|
public static boolean isConstructed(byte firstIdentifierByte) {
|
||||||
|
return (firstIdentifierByte & ID_FLAG_CONSTRUCTED_ENCODING) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag class encoded in the provided first identifier byte. See {@code TAG_CLASS}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public static int getTagClass(byte firstIdentifierByte) {
|
||||||
|
return (firstIdentifierByte & 0xff) >> 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte setTagClass(byte firstIdentifierByte, int tagClass) {
|
||||||
|
return (byte) ((firstIdentifierByte & 0x3f) | (tagClass << 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tag number encoded in the provided first identifier byte. See {@code TAG_NUMBER}
|
||||||
|
* constants.
|
||||||
|
*/
|
||||||
|
public static int getTagNumber(byte firstIdentifierByte) {
|
||||||
|
return firstIdentifierByte & 0x1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte setTagNumber(byte firstIdentifierByte, int tagNumber) {
|
||||||
|
return (byte) ((firstIdentifierByte & ~0x1f) | tagNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BerDataValueReader} which reads from a {@link ByteBuffer} containing BER-encoded data
|
||||||
|
* values. See {@code X.690} for the encoding.
|
||||||
|
*/
|
||||||
|
public class ByteBufferBerDataValueReader implements BerDataValueReader {
|
||||||
|
private final ByteBuffer mBuf;
|
||||||
|
|
||||||
|
public ByteBufferBerDataValueReader(ByteBuffer buf) {
|
||||||
|
if (buf == null) {
|
||||||
|
throw new NullPointerException("buf == null");
|
||||||
|
}
|
||||||
|
mBuf = buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BerDataValue readDataValue() throws BerDataValueFormatException {
|
||||||
|
int startPosition = mBuf.position();
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte firstIdentifierByte = mBuf.get();
|
||||||
|
int tagNumber = readTagNumber(firstIdentifierByte);
|
||||||
|
boolean constructed = BerEncoding.isConstructed(firstIdentifierByte);
|
||||||
|
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException("Missing length");
|
||||||
|
}
|
||||||
|
int firstLengthByte = mBuf.get() & 0xff;
|
||||||
|
int contentsLength;
|
||||||
|
int contentsOffsetInTag;
|
||||||
|
if ((firstLengthByte & 0x80) == 0) {
|
||||||
|
// short form length
|
||||||
|
contentsLength = readShortFormLength(firstLengthByte);
|
||||||
|
contentsOffsetInTag = mBuf.position() - startPosition;
|
||||||
|
skipDefiniteLengthContents(contentsLength);
|
||||||
|
} else if (firstLengthByte != 0x80) {
|
||||||
|
// long form length
|
||||||
|
contentsLength = readLongFormLength(firstLengthByte);
|
||||||
|
contentsOffsetInTag = mBuf.position() - startPosition;
|
||||||
|
skipDefiniteLengthContents(contentsLength);
|
||||||
|
} else {
|
||||||
|
// indefinite length -- value ends with 0x00 0x00
|
||||||
|
contentsOffsetInTag = mBuf.position() - startPosition;
|
||||||
|
contentsLength =
|
||||||
|
constructed
|
||||||
|
? skipConstructedIndefiniteLengthContents()
|
||||||
|
: skipPrimitiveIndefiniteLengthContents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the encoded data value ByteBuffer
|
||||||
|
int endPosition = mBuf.position();
|
||||||
|
mBuf.position(startPosition);
|
||||||
|
int bufOriginalLimit = mBuf.limit();
|
||||||
|
mBuf.limit(endPosition);
|
||||||
|
ByteBuffer encoded = mBuf.slice();
|
||||||
|
mBuf.position(mBuf.limit());
|
||||||
|
mBuf.limit(bufOriginalLimit);
|
||||||
|
|
||||||
|
// Create the encoded contents ByteBuffer
|
||||||
|
encoded.position(contentsOffsetInTag);
|
||||||
|
encoded.limit(contentsOffsetInTag + contentsLength);
|
||||||
|
ByteBuffer encodedContents = encoded.slice();
|
||||||
|
encoded.clear();
|
||||||
|
|
||||||
|
return new BerDataValue(
|
||||||
|
encoded,
|
||||||
|
encodedContents,
|
||||||
|
BerEncoding.getTagClass(firstIdentifierByte),
|
||||||
|
constructed,
|
||||||
|
tagNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readTagNumber(byte firstIdentifierByte) throws BerDataValueFormatException {
|
||||||
|
int tagNumber = BerEncoding.getTagNumber(firstIdentifierByte);
|
||||||
|
if (tagNumber == 0x1f) {
|
||||||
|
// high-tag-number form, where the tag number follows this byte in base-128
|
||||||
|
// big-endian form, where each byte has the highest bit set, except for the last
|
||||||
|
// byte
|
||||||
|
return readHighTagNumber();
|
||||||
|
} else {
|
||||||
|
// low-tag-number form
|
||||||
|
return tagNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readHighTagNumber() throws BerDataValueFormatException {
|
||||||
|
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
|
||||||
|
// byte
|
||||||
|
int b;
|
||||||
|
int result = 0;
|
||||||
|
do {
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException("Truncated tag number");
|
||||||
|
}
|
||||||
|
b = mBuf.get();
|
||||||
|
if (result > Integer.MAX_VALUE >>> 7) {
|
||||||
|
throw new BerDataValueFormatException("Tag number too large");
|
||||||
|
}
|
||||||
|
result <<= 7;
|
||||||
|
result |= b & 0x7f;
|
||||||
|
} while ((b & 0x80) != 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readShortFormLength(int firstLengthByte) {
|
||||||
|
return firstLengthByte & 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int readLongFormLength(int firstLengthByte) throws BerDataValueFormatException {
|
||||||
|
// The low 7 bits of the first byte represent the number of bytes (following the first
|
||||||
|
// byte) in which the length is in big-endian base-256 form
|
||||||
|
int byteCount = firstLengthByte & 0x7f;
|
||||||
|
if (byteCount > 4) {
|
||||||
|
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
|
||||||
|
}
|
||||||
|
int result = 0;
|
||||||
|
for (int i = 0; i < byteCount; i++) {
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException("Truncated length");
|
||||||
|
}
|
||||||
|
int b = mBuf.get();
|
||||||
|
if (result > Integer.MAX_VALUE >>> 8) {
|
||||||
|
throw new BerDataValueFormatException("Length too large");
|
||||||
|
}
|
||||||
|
result <<= 8;
|
||||||
|
result |= b & 0xff;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void skipDefiniteLengthContents(int contentsLength) throws BerDataValueFormatException {
|
||||||
|
if (mBuf.remaining() < contentsLength) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated contents. Need: " + contentsLength + " bytes, available: "
|
||||||
|
+ mBuf.remaining());
|
||||||
|
}
|
||||||
|
mBuf.position(mBuf.position() + contentsLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int skipPrimitiveIndefiniteLengthContents() throws BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00
|
||||||
|
boolean prevZeroByte = false;
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (true) {
|
||||||
|
if (!mBuf.hasRemaining()) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
|
||||||
|
|
||||||
|
}
|
||||||
|
int b = mBuf.get();
|
||||||
|
bytesRead++;
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
throw new BerDataValueFormatException("Indefinite-length contents too long");
|
||||||
|
}
|
||||||
|
if (b == 0) {
|
||||||
|
if (prevZeroByte) {
|
||||||
|
// End of contents reached -- we've read the value and its terminator 0x00 0x00
|
||||||
|
return bytesRead - 2;
|
||||||
|
}
|
||||||
|
prevZeroByte = true;
|
||||||
|
} else {
|
||||||
|
prevZeroByte = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int skipConstructedIndefiniteLengthContents() throws BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
|
||||||
|
// can contain data values which are themselves indefinite length encoded. As a result, we
|
||||||
|
// must parse the direct children of this data value to correctly skip over the contents of
|
||||||
|
// this data value.
|
||||||
|
int startPos = mBuf.position();
|
||||||
|
while (mBuf.hasRemaining()) {
|
||||||
|
// Check whether the 0x00 0x00 terminator is at current position
|
||||||
|
if ((mBuf.remaining() > 1) && (mBuf.getShort(mBuf.position()) == 0)) {
|
||||||
|
int contentsLength = mBuf.position() - startPos;
|
||||||
|
mBuf.position(mBuf.position() + 2);
|
||||||
|
return contentsLength;
|
||||||
|
}
|
||||||
|
// No luck. This must be a BER-encoded data value -- skip over it by parsing it
|
||||||
|
readDataValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: "
|
||||||
|
+ (mBuf.position() - startPos) + " bytes read");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.asn1.ber;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
|
||||||
|
* values. See {@code X.690} for the encoding.
|
||||||
|
*/
|
||||||
|
public class InputStreamBerDataValueReader implements BerDataValueReader {
|
||||||
|
private final InputStream mIn;
|
||||||
|
|
||||||
|
public InputStreamBerDataValueReader(InputStream in) {
|
||||||
|
if (in == null) {
|
||||||
|
throw new NullPointerException("in == null");
|
||||||
|
}
|
||||||
|
mIn = in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BerDataValue readDataValue() throws BerDataValueFormatException {
|
||||||
|
return readDataValue(mIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next data value or {@code null} if end of input has been reached.
|
||||||
|
*
|
||||||
|
* @throws BerDataValueFormatException if the value being read is malformed.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("resource")
|
||||||
|
private static BerDataValue readDataValue(InputStream input)
|
||||||
|
throws BerDataValueFormatException {
|
||||||
|
RecordingInputStream in = new RecordingInputStream(input);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int firstIdentifierByte = in.read();
|
||||||
|
if (firstIdentifierByte == -1) {
|
||||||
|
// End of input
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int tagNumber = readTagNumber(in, firstIdentifierByte);
|
||||||
|
|
||||||
|
int firstLengthByte = in.read();
|
||||||
|
if (firstLengthByte == -1) {
|
||||||
|
throw new BerDataValueFormatException("Missing length");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
|
||||||
|
int contentsLength;
|
||||||
|
int contentsOffsetInDataValue;
|
||||||
|
if ((firstLengthByte & 0x80) == 0) {
|
||||||
|
// short form length
|
||||||
|
contentsLength = readShortFormLength(firstLengthByte);
|
||||||
|
contentsOffsetInDataValue = in.getReadByteCount();
|
||||||
|
skipDefiniteLengthContents(in, contentsLength);
|
||||||
|
} else if ((firstLengthByte & 0xff) != 0x80) {
|
||||||
|
// long form length
|
||||||
|
contentsLength = readLongFormLength(in, firstLengthByte);
|
||||||
|
contentsOffsetInDataValue = in.getReadByteCount();
|
||||||
|
skipDefiniteLengthContents(in, contentsLength);
|
||||||
|
} else {
|
||||||
|
// indefinite length
|
||||||
|
contentsOffsetInDataValue = in.getReadByteCount();
|
||||||
|
contentsLength =
|
||||||
|
constructed
|
||||||
|
? skipConstructedIndefiniteLengthContents(in)
|
||||||
|
: skipPrimitiveIndefiniteLengthContents(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encoded = in.getReadBytes();
|
||||||
|
ByteBuffer encodedContents =
|
||||||
|
ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
|
||||||
|
return new BerDataValue(
|
||||||
|
ByteBuffer.wrap(encoded),
|
||||||
|
encodedContents,
|
||||||
|
BerEncoding.getTagClass((byte) firstIdentifierByte),
|
||||||
|
constructed,
|
||||||
|
tagNumber);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new BerDataValueFormatException("Failed to read data value", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readTagNumber(InputStream in, int firstIdentifierByte)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
|
||||||
|
if (tagNumber == 0x1f) {
|
||||||
|
// high-tag-number form
|
||||||
|
return readHighTagNumber(in);
|
||||||
|
} else {
|
||||||
|
// low-tag-number form
|
||||||
|
return tagNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readHighTagNumber(InputStream in)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
|
||||||
|
// byte where the highest bit is not set
|
||||||
|
int b;
|
||||||
|
int result = 0;
|
||||||
|
do {
|
||||||
|
b = in.read();
|
||||||
|
if (b == -1) {
|
||||||
|
throw new BerDataValueFormatException("Truncated tag number");
|
||||||
|
}
|
||||||
|
if (result > Integer.MAX_VALUE >>> 7) {
|
||||||
|
throw new BerDataValueFormatException("Tag number too large");
|
||||||
|
}
|
||||||
|
result <<= 7;
|
||||||
|
result |= b & 0x7f;
|
||||||
|
} while ((b & 0x80) != 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readShortFormLength(int firstLengthByte) {
|
||||||
|
return firstLengthByte & 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int readLongFormLength(InputStream in, int firstLengthByte)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
// The low 7 bits of the first byte represent the number of bytes (following the first
|
||||||
|
// byte) in which the length is in big-endian base-256 form
|
||||||
|
int byteCount = firstLengthByte & 0x7f;
|
||||||
|
if (byteCount > 4) {
|
||||||
|
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
|
||||||
|
}
|
||||||
|
int result = 0;
|
||||||
|
for (int i = 0; i < byteCount; i++) {
|
||||||
|
int b = in.read();
|
||||||
|
if (b == -1) {
|
||||||
|
throw new BerDataValueFormatException("Truncated length");
|
||||||
|
}
|
||||||
|
if (result > Integer.MAX_VALUE >>> 8) {
|
||||||
|
throw new BerDataValueFormatException("Length too large");
|
||||||
|
}
|
||||||
|
result <<= 8;
|
||||||
|
result |= b & 0xff;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void skipDefiniteLengthContents(InputStream in, int len)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
long bytesRead = 0;
|
||||||
|
while (len > 0) {
|
||||||
|
int skipped = (int) in.skip(len);
|
||||||
|
if (skipped <= 0) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated definite-length contents: " + bytesRead + " bytes read"
|
||||||
|
+ ", " + len + " missing");
|
||||||
|
}
|
||||||
|
len -= skipped;
|
||||||
|
bytesRead += skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int skipPrimitiveIndefiniteLengthContents(InputStream in)
|
||||||
|
throws IOException, BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00
|
||||||
|
boolean prevZeroByte = false;
|
||||||
|
int bytesRead = 0;
|
||||||
|
while (true) {
|
||||||
|
int b = in.read();
|
||||||
|
if (b == -1) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
|
||||||
|
}
|
||||||
|
bytesRead++;
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
throw new BerDataValueFormatException("Indefinite-length contents too long");
|
||||||
|
}
|
||||||
|
if (b == 0) {
|
||||||
|
if (prevZeroByte) {
|
||||||
|
// End of contents reached -- we've read the value and its terminator 0x00 0x00
|
||||||
|
return bytesRead - 2;
|
||||||
|
}
|
||||||
|
prevZeroByte = true;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
prevZeroByte = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in)
|
||||||
|
throws BerDataValueFormatException {
|
||||||
|
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
|
||||||
|
// can contain data values which are indefinite length encoded as well. As a result, we
|
||||||
|
// must parse the direct children of this data value to correctly skip over the contents of
|
||||||
|
// this data value.
|
||||||
|
int readByteCountBefore = in.getReadByteCount();
|
||||||
|
while (true) {
|
||||||
|
// We can't easily peek for the 0x00 0x00 terminator using the provided InputStream.
|
||||||
|
// Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we
|
||||||
|
// then check below to see whether it's 0x00 0x00.
|
||||||
|
BerDataValue dataValue = readDataValue(in);
|
||||||
|
if (dataValue == null) {
|
||||||
|
throw new BerDataValueFormatException(
|
||||||
|
"Truncated indefinite-length contents: "
|
||||||
|
+ (in.getReadByteCount() - readByteCountBefore) + " bytes read");
|
||||||
|
}
|
||||||
|
if (in.getReadByteCount() <= 0) {
|
||||||
|
throw new BerDataValueFormatException("Indefinite-length contents too long");
|
||||||
|
}
|
||||||
|
ByteBuffer encoded = dataValue.getEncoded();
|
||||||
|
if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) {
|
||||||
|
// 0x00 0x00 encountered
|
||||||
|
return in.getReadByteCount() - readByteCountBefore - 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RecordingInputStream extends InputStream {
|
||||||
|
private final InputStream mIn;
|
||||||
|
private final ByteArrayOutputStream mBuf;
|
||||||
|
|
||||||
|
private RecordingInputStream(InputStream in) {
|
||||||
|
mIn = in;
|
||||||
|
mBuf = new ByteArrayOutputStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getReadBytes() {
|
||||||
|
return mBuf.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getReadByteCount() {
|
||||||
|
return mBuf.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
int b = mIn.read();
|
||||||
|
if (b != -1) {
|
||||||
|
mBuf.write(b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b) throws IOException {
|
||||||
|
int len = mIn.read(b);
|
||||||
|
if (len > 0) {
|
||||||
|
mBuf.write(b, 0, len);
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read(byte[] b, int off, int len) throws IOException {
|
||||||
|
len = mIn.read(b, off, len);
|
||||||
|
if (len > 0) {
|
||||||
|
mBuf.write(b, off, len);
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long skip(long n) throws IOException {
|
||||||
|
if (n <= 0) {
|
||||||
|
return mIn.skip(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
|
||||||
|
if (len > 0) {
|
||||||
|
mBuf.write(buf, 0, len);
|
||||||
|
}
|
||||||
|
return (len < 0) ? 0 : len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int available() throws IOException {
|
||||||
|
return super.available();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void mark(int readlimit) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void reset() throws IOException {
|
||||||
|
throw new IOException("mark/reset not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean markSupported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.jar;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JAR manifest and signature file parser.
|
||||||
|
*
|
||||||
|
* <p>These files consist of a main section followed by individual sections. Individual sections
|
||||||
|
* are named, their names referring to JAR entries.
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
|
*/
|
||||||
|
public class ManifestParser {
|
||||||
|
|
||||||
|
private final byte[] mManifest;
|
||||||
|
private int mOffset;
|
||||||
|
private int mEndOffset;
|
||||||
|
|
||||||
|
private byte[] mBufferedLine;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ManifestParser} with the provided input.
|
||||||
|
*/
|
||||||
|
public ManifestParser(byte[] data) {
|
||||||
|
this(data, 0, data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ManifestParser} with the provided input.
|
||||||
|
*/
|
||||||
|
public ManifestParser(byte[] data, int offset, int length) {
|
||||||
|
mManifest = data;
|
||||||
|
mOffset = offset;
|
||||||
|
mEndOffset = offset + length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remaining sections of this file.
|
||||||
|
*/
|
||||||
|
public List<Section> readAllSections() {
|
||||||
|
List<Section> sections = new ArrayList<>();
|
||||||
|
Section section;
|
||||||
|
while ((section = readSection()) != null) {
|
||||||
|
sections.add(section);
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next section from this file or {@code null} if end of file has been reached.
|
||||||
|
*/
|
||||||
|
public Section readSection() {
|
||||||
|
// Locate the first non-empty line
|
||||||
|
int sectionStartOffset;
|
||||||
|
String attr;
|
||||||
|
do {
|
||||||
|
sectionStartOffset = mOffset;
|
||||||
|
attr = readAttribute();
|
||||||
|
if (attr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} while (attr.length() == 0);
|
||||||
|
List<Attribute> attrs = new ArrayList<>();
|
||||||
|
attrs.add(parseAttr(attr));
|
||||||
|
|
||||||
|
// Read attributes until end of section reached
|
||||||
|
while (true) {
|
||||||
|
attr = readAttribute();
|
||||||
|
if ((attr == null) || (attr.length() == 0)) {
|
||||||
|
// End of section
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attrs.add(parseAttr(attr));
|
||||||
|
}
|
||||||
|
|
||||||
|
int sectionEndOffset = mOffset;
|
||||||
|
int sectionSizeBytes = sectionEndOffset - sectionStartOffset;
|
||||||
|
|
||||||
|
return new Section(sectionStartOffset, sectionSizeBytes, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Attribute parseAttr(String attr) {
|
||||||
|
// Name is separated from value by a semicolon followed by a single SPACE character.
|
||||||
|
// This permits trailing spaces in names and leading and trailing spaces in values.
|
||||||
|
// Some APK obfuscators take advantage of this fact. We thus need to preserve these unusual
|
||||||
|
// spaces to be able to parse such obfuscated APKs.
|
||||||
|
int delimiterIndex = attr.indexOf(": ");
|
||||||
|
if (delimiterIndex == -1) {
|
||||||
|
return new Attribute(attr, "");
|
||||||
|
} else {
|
||||||
|
return new Attribute(
|
||||||
|
attr.substring(0, delimiterIndex),
|
||||||
|
attr.substring(delimiterIndex + ": ".length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next attribute or empty {@code String} if end of section has been reached or
|
||||||
|
* {@code null} if end of input has been reached.
|
||||||
|
*/
|
||||||
|
private String readAttribute() {
|
||||||
|
byte[] bytes = readAttributeBytes();
|
||||||
|
if (bytes == null) {
|
||||||
|
return null;
|
||||||
|
} else if (bytes.length == 0) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next attribute or empty array if end of section has been reached or {@code null}
|
||||||
|
* if end of input has been reached.
|
||||||
|
*/
|
||||||
|
private byte[] readAttributeBytes() {
|
||||||
|
// Check whether end of section was reached during previous invocation
|
||||||
|
if ((mBufferedLine != null) && (mBufferedLine.length == 0)) {
|
||||||
|
mBufferedLine = null;
|
||||||
|
return EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the next line
|
||||||
|
byte[] line = readLine();
|
||||||
|
if (line == null) {
|
||||||
|
// End of input
|
||||||
|
if (mBufferedLine != null) {
|
||||||
|
byte[] result = mBufferedLine;
|
||||||
|
mBufferedLine = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume the read line
|
||||||
|
if (line.length == 0) {
|
||||||
|
// End of section
|
||||||
|
if (mBufferedLine != null) {
|
||||||
|
byte[] result = mBufferedLine;
|
||||||
|
mBufferedLine = EMPTY_BYTE_ARRAY;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
byte[] attrLine;
|
||||||
|
if (mBufferedLine == null) {
|
||||||
|
attrLine = line;
|
||||||
|
} else {
|
||||||
|
if ((line.length == 0) || (line[0] != ' ')) {
|
||||||
|
// The most common case: buffered line is a full attribute
|
||||||
|
byte[] result = mBufferedLine;
|
||||||
|
mBufferedLine = line;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
attrLine = mBufferedLine;
|
||||||
|
mBufferedLine = null;
|
||||||
|
attrLine = concat(attrLine, line, 1, line.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything's buffered in attrLine now. mBufferedLine is null
|
||||||
|
|
||||||
|
// Read more lines
|
||||||
|
while (true) {
|
||||||
|
line = readLine();
|
||||||
|
if (line == null) {
|
||||||
|
// End of input
|
||||||
|
return attrLine;
|
||||||
|
} else if (line.length == 0) {
|
||||||
|
// End of section
|
||||||
|
mBufferedLine = EMPTY_BYTE_ARRAY; // return "end of section" next time
|
||||||
|
return attrLine;
|
||||||
|
}
|
||||||
|
if (line[0] == ' ') {
|
||||||
|
// Continuation line
|
||||||
|
attrLine = concat(attrLine, line, 1, line.length - 1);
|
||||||
|
} else {
|
||||||
|
// Next attribute
|
||||||
|
mBufferedLine = line;
|
||||||
|
return attrLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||||
|
|
||||||
|
private static byte[] concat(byte[] arr1, byte[] arr2, int offset2, int length2) {
|
||||||
|
byte[] result = new byte[arr1.length + length2];
|
||||||
|
System.arraycopy(arr1, 0, result, 0, arr1.length);
|
||||||
|
System.arraycopy(arr2, offset2, result, arr1.length, length2);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next line (without line delimiter characters) or {@code null} if end of input has
|
||||||
|
* been reached.
|
||||||
|
*/
|
||||||
|
private byte[] readLine() {
|
||||||
|
if (mOffset >= mEndOffset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int startOffset = mOffset;
|
||||||
|
int newlineStartOffset = -1;
|
||||||
|
int newlineEndOffset = -1;
|
||||||
|
for (int i = startOffset; i < mEndOffset; i++) {
|
||||||
|
byte b = mManifest[i];
|
||||||
|
if (b == '\r') {
|
||||||
|
newlineStartOffset = i;
|
||||||
|
int nextIndex = i + 1;
|
||||||
|
if ((nextIndex < mEndOffset) && (mManifest[nextIndex] == '\n')) {
|
||||||
|
newlineEndOffset = nextIndex + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newlineEndOffset = nextIndex;
|
||||||
|
break;
|
||||||
|
} else if (b == '\n') {
|
||||||
|
newlineStartOffset = i;
|
||||||
|
newlineEndOffset = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newlineStartOffset == -1) {
|
||||||
|
newlineStartOffset = mEndOffset;
|
||||||
|
newlineEndOffset = mEndOffset;
|
||||||
|
}
|
||||||
|
mOffset = newlineEndOffset;
|
||||||
|
|
||||||
|
if (newlineStartOffset == startOffset) {
|
||||||
|
return EMPTY_BYTE_ARRAY;
|
||||||
|
}
|
||||||
|
return Arrays.copyOfRange(mManifest, startOffset, newlineStartOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute.
|
||||||
|
*/
|
||||||
|
public static class Attribute {
|
||||||
|
private final String mName;
|
||||||
|
private final String mValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Attribute} with the provided name and value.
|
||||||
|
*/
|
||||||
|
public Attribute(String name, String value) {
|
||||||
|
mName = name;
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this attribute's name.
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this attribute's value.
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section.
|
||||||
|
*/
|
||||||
|
public static class Section {
|
||||||
|
private final int mStartOffset;
|
||||||
|
private final int mSizeBytes;
|
||||||
|
private final String mName;
|
||||||
|
private final List<Attribute> mAttributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code Section}.
|
||||||
|
*
|
||||||
|
* @param startOffset start offset (in bytes) of the section in the input file
|
||||||
|
* @param sizeBytes size (in bytes) of the section in the input file
|
||||||
|
* @param attrs attributes contained in the section
|
||||||
|
*/
|
||||||
|
public Section(int startOffset, int sizeBytes, List<Attribute> attrs) {
|
||||||
|
mStartOffset = startOffset;
|
||||||
|
mSizeBytes = sizeBytes;
|
||||||
|
String sectionName = null;
|
||||||
|
if (!attrs.isEmpty()) {
|
||||||
|
Attribute firstAttr = attrs.get(0);
|
||||||
|
if ("Name".equalsIgnoreCase(firstAttr.getName())) {
|
||||||
|
sectionName = firstAttr.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mName = sectionName;
|
||||||
|
mAttributes = Collections.unmodifiableList(new ArrayList<>(attrs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return mName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the offset (in bytes) at which this section starts in the input.
|
||||||
|
*/
|
||||||
|
public int getStartOffset() {
|
||||||
|
return mStartOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size (in bytes) of this section in the input.
|
||||||
|
*/
|
||||||
|
public int getSizeBytes() {
|
||||||
|
return mSizeBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns this section's attributes, in the order in which they appear in the input.
|
||||||
|
*/
|
||||||
|
public List<Attribute> getAttributes() {
|
||||||
|
return mAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the specified attribute in this section or {@code null} if this
|
||||||
|
* section does not contain a matching attribute.
|
||||||
|
*/
|
||||||
|
public String getAttributeValue(Attributes.Name name) {
|
||||||
|
return getAttributeValue(name.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of the specified attribute in this section or {@code null} if this
|
||||||
|
* section does not contain a matching attribute.
|
||||||
|
*
|
||||||
|
* @param name name of the attribute. Attribute names are case-insensitive.
|
||||||
|
*/
|
||||||
|
public String getAttributeValue(String name) {
|
||||||
|
for (Attribute attr : mAttributes) {
|
||||||
|
if (attr.getName().equalsIgnoreCase(name)) {
|
||||||
|
return attr.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Producer of {@code META-INF/MANIFEST.MF} file.
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
|
*/
|
||||||
|
public abstract class ManifestWriter {
|
||||||
|
|
||||||
|
private static final byte[] CRLF = new byte[] {'\r', '\n'};
|
||||||
|
private static final int MAX_LINE_LENGTH = 70;
|
||||||
|
|
||||||
|
private ManifestWriter() {}
|
||||||
|
|
||||||
|
public static void writeMainSection(OutputStream out, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// Main section must start with the Manifest-Version attribute.
|
||||||
|
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
|
||||||
|
String manifestVersion = attributes.getValue(Attributes.Name.MANIFEST_VERSION);
|
||||||
|
if (manifestVersion == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Mandatory " + Attributes.Name.MANIFEST_VERSION + " attribute missing");
|
||||||
|
}
|
||||||
|
writeAttribute(out, Attributes.Name.MANIFEST_VERSION, manifestVersion);
|
||||||
|
|
||||||
|
if (attributes.size() > 1) {
|
||||||
|
SortedMap<String, String> namedAttributes = getAttributesSortedByName(attributes);
|
||||||
|
namedAttributes.remove(Attributes.Name.MANIFEST_VERSION.toString());
|
||||||
|
writeAttributes(out, namedAttributes);
|
||||||
|
}
|
||||||
|
writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
writeAttribute(out, "Name", name);
|
||||||
|
|
||||||
|
if (!attributes.isEmpty()) {
|
||||||
|
writeAttributes(out, getAttributesSortedByName(attributes));
|
||||||
|
}
|
||||||
|
writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeSectionDelimiter(OutputStream out) throws IOException {
|
||||||
|
out.write(CRLF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeAttribute(OutputStream out, Attributes.Name name, String value)
|
||||||
|
throws IOException {
|
||||||
|
writeAttribute(out, name.toString(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeAttribute(OutputStream out, String name, String value)
|
||||||
|
throws IOException {
|
||||||
|
writeLine(out, name + ": " + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeLine(OutputStream out, String line) throws IOException {
|
||||||
|
byte[] lineBytes = line.getBytes(StandardCharsets.UTF_8);
|
||||||
|
int offset = 0;
|
||||||
|
int remaining = lineBytes.length;
|
||||||
|
boolean firstLine = true;
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkLength;
|
||||||
|
if (firstLine) {
|
||||||
|
// First line
|
||||||
|
chunkLength = Math.min(remaining, MAX_LINE_LENGTH);
|
||||||
|
} else {
|
||||||
|
// Continuation line
|
||||||
|
out.write(CRLF);
|
||||||
|
out.write(' ');
|
||||||
|
chunkLength = Math.min(remaining, MAX_LINE_LENGTH - 1);
|
||||||
|
}
|
||||||
|
out.write(lineBytes, offset, chunkLength);
|
||||||
|
offset += chunkLength;
|
||||||
|
remaining -= chunkLength;
|
||||||
|
firstLine = false;
|
||||||
|
}
|
||||||
|
out.write(CRLF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SortedMap<String, String> getAttributesSortedByName(Attributes attributes) {
|
||||||
|
Set<Map.Entry<Object, Object>> attributesEntries = attributes.entrySet();
|
||||||
|
SortedMap<String, String> namedAttributes = new TreeMap<String, String>();
|
||||||
|
for (Map.Entry<Object, Object> attribute : attributesEntries) {
|
||||||
|
String attrName = attribute.getKey().toString();
|
||||||
|
String attrValue = attribute.getValue().toString();
|
||||||
|
namedAttributes.put(attrName, attrValue);
|
||||||
|
}
|
||||||
|
return namedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeAttributes(
|
||||||
|
OutputStream out, SortedMap<String, String> attributesSortedByName) throws IOException {
|
||||||
|
for (Map.Entry<String, String> attribute : attributesSortedByName.entrySet()) {
|
||||||
|
String attrName = attribute.getKey();
|
||||||
|
String attrValue = attribute.getValue();
|
||||||
|
writeAttribute(out, attrName, attrValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.jar;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Producer of JAR signature file ({@code *.SF}).
|
||||||
|
*
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#JAR_Manifest">JAR Manifest format</a>
|
||||||
|
*/
|
||||||
|
public abstract class SignatureFileWriter {
|
||||||
|
private SignatureFileWriter() {}
|
||||||
|
|
||||||
|
public static void writeMainSection(OutputStream out, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
// Main section must start with the Signature-Version attribute.
|
||||||
|
// See https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File.
|
||||||
|
String signatureVersion = attributes.getValue(Attributes.Name.SIGNATURE_VERSION);
|
||||||
|
if (signatureVersion == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Mandatory " + Attributes.Name.SIGNATURE_VERSION + " attribute missing");
|
||||||
|
}
|
||||||
|
ManifestWriter.writeAttribute(out, Attributes.Name.SIGNATURE_VERSION, signatureVersion);
|
||||||
|
|
||||||
|
if (attributes.size() > 1) {
|
||||||
|
SortedMap<String, String> namedAttributes =
|
||||||
|
ManifestWriter.getAttributesSortedByName(attributes);
|
||||||
|
namedAttributes.remove(Attributes.Name.SIGNATURE_VERSION.toString());
|
||||||
|
ManifestWriter.writeAttributes(out, namedAttributes);
|
||||||
|
}
|
||||||
|
writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeIndividualSection(OutputStream out, String name, Attributes attributes)
|
||||||
|
throws IOException {
|
||||||
|
ManifestWriter.writeIndividualSection(out, name, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeSectionDelimiter(OutputStream out) throws IOException {
|
||||||
|
ManifestWriter.writeSectionDelimiter(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.apksig.internal.oid;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.util.InclusiveIntRange;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class OidConstants {
|
||||||
|
public static final String OID_DIGEST_MD5 = "1.2.840.113549.2.5";
|
||||||
|
public static final String OID_DIGEST_SHA1 = "1.3.14.3.2.26";
|
||||||
|
public static final String OID_DIGEST_SHA224 = "2.16.840.1.101.3.4.2.4";
|
||||||
|
public static final String OID_DIGEST_SHA256 = "2.16.840.1.101.3.4.2.1";
|
||||||
|
public static final String OID_DIGEST_SHA384 = "2.16.840.1.101.3.4.2.2";
|
||||||
|
public static final String OID_DIGEST_SHA512 = "2.16.840.1.101.3.4.2.3";
|
||||||
|
|
||||||
|
public static final String OID_SIG_RSA = "1.2.840.113549.1.1.1";
|
||||||
|
public static final String OID_SIG_MD5_WITH_RSA = "1.2.840.113549.1.1.4";
|
||||||
|
public static final String OID_SIG_SHA1_WITH_RSA = "1.2.840.113549.1.1.5";
|
||||||
|
public static final String OID_SIG_SHA224_WITH_RSA = "1.2.840.113549.1.1.14";
|
||||||
|
public static final String OID_SIG_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
|
||||||
|
public static final String OID_SIG_SHA384_WITH_RSA = "1.2.840.113549.1.1.12";
|
||||||
|
public static final String OID_SIG_SHA512_WITH_RSA = "1.2.840.113549.1.1.13";
|
||||||
|
|
||||||
|
public static final String OID_SIG_DSA = "1.2.840.10040.4.1";
|
||||||
|
public static final String OID_SIG_SHA1_WITH_DSA = "1.2.840.10040.4.3";
|
||||||
|
public static final String OID_SIG_SHA224_WITH_DSA = "2.16.840.1.101.3.4.3.1";
|
||||||
|
public static final String OID_SIG_SHA256_WITH_DSA = "2.16.840.1.101.3.4.3.2";
|
||||||
|
public static final String OID_SIG_SHA384_WITH_DSA = "2.16.840.1.101.3.4.3.3";
|
||||||
|
public static final String OID_SIG_SHA512_WITH_DSA = "2.16.840.1.101.3.4.3.4";
|
||||||
|
|
||||||
|
public static final String OID_SIG_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
|
||||||
|
public static final String OID_SIG_SHA1_WITH_ECDSA = "1.2.840.10045.4.1";
|
||||||
|
public static final String OID_SIG_SHA224_WITH_ECDSA = "1.2.840.10045.4.3.1";
|
||||||
|
public static final String OID_SIG_SHA256_WITH_ECDSA = "1.2.840.10045.4.3.2";
|
||||||
|
public static final String OID_SIG_SHA384_WITH_ECDSA = "1.2.840.10045.4.3.3";
|
||||||
|
public static final String OID_SIG_SHA512_WITH_ECDSA = "1.2.840.10045.4.3.4";
|
||||||
|
|
||||||
|
public static final Map<String, List<InclusiveIntRange>> SUPPORTED_SIG_ALG_OIDS =
|
||||||
|
new HashMap<>();
|
||||||
|
static {
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_RSA,
|
||||||
|
InclusiveIntRange.from(0));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_MD5_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_RSA,
|
||||||
|
InclusiveIntRange.from(0));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_MD5_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_RSA,
|
||||||
|
InclusiveIntRange.from(0));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_RSA,
|
||||||
|
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_MD5_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_RSA,
|
||||||
|
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_MD5_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(0, 8), InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_RSA,
|
||||||
|
InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_MD5_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_RSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_RSA,
|
||||||
|
InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_MD5_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_RSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_RSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_DSA,
|
||||||
|
InclusiveIntRange.from(0));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_DSA,
|
||||||
|
InclusiveIntRange.from(9));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_DSA,
|
||||||
|
InclusiveIntRange.from(22));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_DSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_DSA,
|
||||||
|
InclusiveIntRange.from(22));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_DSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_DSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_EC_PUBLIC_KEY,
|
||||||
|
InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_EC_PUBLIC_KEY,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_EC_PUBLIC_KEY,
|
||||||
|
InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_EC_PUBLIC_KEY,
|
||||||
|
InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_EC_PUBLIC_KEY,
|
||||||
|
InclusiveIntRange.from(18));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA1_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA224_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA256_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA384_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_MD5, OID_SIG_SHA512_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA1_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.from(18));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA224_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA256_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA384_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA1, OID_SIG_SHA512_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA1_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA224_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA256_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA384_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA224, OID_SIG_SHA512_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA1_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA224_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA256_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA384_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA256, OID_SIG_SHA512_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA1_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA224_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA256_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA384_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA384, OID_SIG_SHA512_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA1_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA224_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA256_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA384_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.fromTo(21, 23));
|
||||||
|
addSupportedSigAlg(
|
||||||
|
OID_DIGEST_SHA512, OID_SIG_SHA512_WITH_ECDSA,
|
||||||
|
InclusiveIntRange.from(21));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addSupportedSigAlg(
|
||||||
|
String digestAlgorithmOid,
|
||||||
|
String signatureAlgorithmOid,
|
||||||
|
InclusiveIntRange... supportedApiLevels) {
|
||||||
|
SUPPORTED_SIG_ALG_OIDS.put(
|
||||||
|
digestAlgorithmOid + "with" + signatureAlgorithmOid,
|
||||||
|
Arrays.asList(supportedApiLevels));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<InclusiveIntRange> getSigAlgSupportedApiLevels(
|
||||||
|
String digestAlgorithmOid,
|
||||||
|
String signatureAlgorithmOid) {
|
||||||
|
List<InclusiveIntRange> result =
|
||||||
|
SUPPORTED_SIG_ALG_OIDS.get(digestAlgorithmOid + "with" + signatureAlgorithmOid);
|
||||||
|
return (result != null) ? result : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class OidToUserFriendlyNameMapper {
|
||||||
|
private OidToUserFriendlyNameMapper() {}
|
||||||
|
|
||||||
|
private static final Map<String, String> OID_TO_USER_FRIENDLY_NAME = new HashMap<>();
|
||||||
|
static {
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_MD5, "MD5");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA1, "SHA-1");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA224, "SHA-224");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA256, "SHA-256");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA384, "SHA-384");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_DIGEST_SHA512, "SHA-512");
|
||||||
|
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_RSA, "RSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_MD5_WITH_RSA, "MD5 with RSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_RSA, "SHA-1 with RSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_RSA, "SHA-224 with RSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_RSA, "SHA-256 with RSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_RSA, "SHA-384 with RSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_RSA, "SHA-512 with RSA");
|
||||||
|
|
||||||
|
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_DSA, "DSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_DSA, "SHA-1 with DSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_DSA, "SHA-224 with DSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_DSA, "SHA-256 with DSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_DSA, "SHA-384 with DSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_DSA, "SHA-512 with DSA");
|
||||||
|
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_EC_PUBLIC_KEY, "ECDSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA1_WITH_ECDSA, "SHA-1 with ECDSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA224_WITH_ECDSA, "SHA-224 with ECDSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA256_WITH_ECDSA, "SHA-256 with ECDSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA384_WITH_ECDSA, "SHA-384 with ECDSA");
|
||||||
|
OID_TO_USER_FRIENDLY_NAME.put(OID_SIG_SHA512_WITH_ECDSA, "SHA-512 with ECDSA");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getUserFriendlyNameForOid(String oid) {
|
||||||
|
return OID_TO_USER_FRIENDLY_NAME.get(oid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Map<String, String> OID_TO_JCA_DIGEST_ALG = new HashMap<>();
|
||||||
|
static {
|
||||||
|
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_MD5, "MD5");
|
||||||
|
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA1, "SHA-1");
|
||||||
|
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA224, "SHA-224");
|
||||||
|
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA256, "SHA-256");
|
||||||
|
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA384, "SHA-384");
|
||||||
|
OID_TO_JCA_DIGEST_ALG.put(OID_DIGEST_SHA512, "SHA-512");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Map<String, String> OID_TO_JCA_SIGNATURE_ALG = new HashMap<>();
|
||||||
|
static {
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_MD5_WITH_RSA, "MD5withRSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_RSA, "SHA1withRSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_RSA, "SHA224withRSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_RSA, "SHA256withRSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_RSA, "SHA384withRSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_RSA, "SHA512withRSA");
|
||||||
|
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_DSA, "SHA1withDSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_DSA, "SHA224withDSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_DSA, "SHA256withDSA");
|
||||||
|
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA1_WITH_ECDSA, "SHA1withECDSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA224_WITH_ECDSA, "SHA224withECDSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA256_WITH_ECDSA, "SHA256withECDSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA384_WITH_ECDSA, "SHA384withECDSA");
|
||||||
|
OID_TO_JCA_SIGNATURE_ALG.put(OID_SIG_SHA512_WITH_ECDSA, "SHA512withECDSA");
|
||||||
|
}
|
||||||
|
|
||||||
|
private OidConstants() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import static com.android.apksig.Constants.OID_RSA_ENCRYPTION;
|
||||||
|
import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_DSA;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_EC_PUBLIC_KEY;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_RSA;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_SIG_SHA256_WITH_DSA;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_DIGEST_ALG;
|
||||||
|
import static com.android.apksig.internal.oid.OidConstants.OID_TO_JCA_SIGNATURE_ALG;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.apk.v1.DigestAlgorithm;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.util.Pair;
|
||||||
|
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code AlgorithmIdentifier} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class AlgorithmIdentifier {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String algorithm;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.ANY, optional = true)
|
||||||
|
public Asn1OpaqueObject parameters;
|
||||||
|
|
||||||
|
public AlgorithmIdentifier() {}
|
||||||
|
|
||||||
|
public AlgorithmIdentifier(String algorithmOid, Asn1OpaqueObject parameters) {
|
||||||
|
this.algorithm = algorithmOid;
|
||||||
|
this.parameters = parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the PKCS #7 {@code DigestAlgorithm} to use when signing using the specified digest
|
||||||
|
* algorithm.
|
||||||
|
*/
|
||||||
|
public static AlgorithmIdentifier getSignerInfoDigestAlgorithmOid(
|
||||||
|
DigestAlgorithm digestAlgorithm) {
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
return new AlgorithmIdentifier(OID_DIGEST_SHA1, ASN1_DER_NULL);
|
||||||
|
case SHA256:
|
||||||
|
return new AlgorithmIdentifier(OID_DIGEST_SHA256, ASN1_DER_NULL);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unsupported digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JCA {@link Signature} algorithm and PKCS #7 {@code SignatureAlgorithm} to use
|
||||||
|
* when signing with the specified key and digest algorithm.
|
||||||
|
*/
|
||||||
|
public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm(
|
||||||
|
PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
String keyAlgorithm = publicKey.getAlgorithm();
|
||||||
|
String jcaDigestPrefixForSigAlg;
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
jcaDigestPrefixForSigAlg = "SHA1";
|
||||||
|
break;
|
||||||
|
case SHA256:
|
||||||
|
jcaDigestPrefixForSigAlg = "SHA256";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) {
|
||||||
|
return Pair.of(
|
||||||
|
jcaDigestPrefixForSigAlg + "withRSA",
|
||||||
|
new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL));
|
||||||
|
} else if ("DSA".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
AlgorithmIdentifier sigAlgId;
|
||||||
|
switch (digestAlgorithm) {
|
||||||
|
case SHA1:
|
||||||
|
sigAlgId =
|
||||||
|
new AlgorithmIdentifier(OID_SIG_DSA, ASN1_DER_NULL);
|
||||||
|
break;
|
||||||
|
case SHA256:
|
||||||
|
// DSA signatures with SHA-256 in SignedData are accepted by Android API Level
|
||||||
|
// 21 and higher. However, there are two ways to specify their SignedData
|
||||||
|
// SignatureAlgorithm: dsaWithSha256 (2.16.840.1.101.3.4.3.2) and
|
||||||
|
// dsa (1.2.840.10040.4.1). The latter works only on API Level 22+. Thus, we use
|
||||||
|
// the former.
|
||||||
|
sigAlgId =
|
||||||
|
new AlgorithmIdentifier(OID_SIG_SHA256_WITH_DSA, ASN1_DER_NULL);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Unexpected digest algorithm: " + digestAlgorithm);
|
||||||
|
}
|
||||||
|
String signingAlgorithmName =
|
||||||
|
jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA");
|
||||||
|
return Pair.of(signingAlgorithmName, sigAlgId);
|
||||||
|
} else if ("EC".equalsIgnoreCase(keyAlgorithm)) {
|
||||||
|
return Pair.of(
|
||||||
|
jcaDigestPrefixForSigAlg + "withECDSA",
|
||||||
|
new AlgorithmIdentifier(OID_SIG_EC_PUBLIC_KEY, ASN1_DER_NULL));
|
||||||
|
} else {
|
||||||
|
throw new InvalidKeyException("Unsupported key algorithm: " + keyAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getJcaSignatureAlgorithm(
|
||||||
|
String digestAlgorithmOid,
|
||||||
|
String signatureAlgorithmOid) throws SignatureException {
|
||||||
|
// First check whether the signature algorithm OID alone is sufficient
|
||||||
|
String result = OID_TO_JCA_SIGNATURE_ALG.get(signatureAlgorithmOid);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signature algorithm OID alone is insufficient. Need to combine digest algorithm OID
|
||||||
|
// with signature algorithm OID.
|
||||||
|
String suffix;
|
||||||
|
if (OID_SIG_RSA.equals(signatureAlgorithmOid)) {
|
||||||
|
suffix = "RSA";
|
||||||
|
} else if (OID_SIG_DSA.equals(signatureAlgorithmOid)) {
|
||||||
|
suffix = "DSA";
|
||||||
|
} else if (OID_SIG_EC_PUBLIC_KEY.equals(signatureAlgorithmOid)) {
|
||||||
|
suffix = "ECDSA";
|
||||||
|
} else {
|
||||||
|
throw new SignatureException(
|
||||||
|
"Unsupported JCA Signature algorithm"
|
||||||
|
+ " . Digest algorithm: " + digestAlgorithmOid
|
||||||
|
+ ", signature algorithm: " + signatureAlgorithmOid);
|
||||||
|
}
|
||||||
|
String jcaDigestAlg = getJcaDigestAlgorithm(digestAlgorithmOid);
|
||||||
|
// Canonical name for SHA-1 with ... is SHA1with, rather than SHA1. Same for all other
|
||||||
|
// SHA algorithms.
|
||||||
|
if (jcaDigestAlg.startsWith("SHA-")) {
|
||||||
|
jcaDigestAlg = "SHA" + jcaDigestAlg.substring("SHA-".length());
|
||||||
|
}
|
||||||
|
return jcaDigestAlg + "with" + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getJcaDigestAlgorithm(String oid)
|
||||||
|
throws SignatureException {
|
||||||
|
String result = OID_TO_JCA_DIGEST_ALG.get(oid);
|
||||||
|
if (result == null) {
|
||||||
|
throw new SignatureException("Unsupported digest algorithm: " + oid);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code Attribute} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class Attribute {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String attrType;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
|
||||||
|
public List<Asn1OpaqueObject> attrValues;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code ContentInfo} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class ContentInfo {
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String contentType;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.ANY, tagging = Asn1Tagging.EXPLICIT, tagNumber = 0)
|
||||||
|
public Asn1OpaqueObject content;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code EncapsulatedContentInfo} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class EncapsulatedContentInfo {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String contentType;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 1,
|
||||||
|
type = Asn1Type.OCTET_STRING,
|
||||||
|
tagging = Asn1Tagging.EXPLICIT, tagNumber = 0,
|
||||||
|
optional = true)
|
||||||
|
public ByteBuffer content;
|
||||||
|
|
||||||
|
public EncapsulatedContentInfo() {}
|
||||||
|
|
||||||
|
public EncapsulatedContentInfo(String contentTypeOid) {
|
||||||
|
contentType = contentTypeOid;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code IssuerAndSerialNumber} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class IssuerAndSerialNumber {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.ANY)
|
||||||
|
public Asn1OpaqueObject issuer;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.INTEGER)
|
||||||
|
public BigInteger certificateSerialNumber;
|
||||||
|
|
||||||
|
public IssuerAndSerialNumber() {}
|
||||||
|
|
||||||
|
public IssuerAndSerialNumber(Asn1OpaqueObject issuer, BigInteger certificateSerialNumber) {
|
||||||
|
this.issuer = issuer;
|
||||||
|
this.certificateSerialNumber = certificateSerialNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assorted PKCS #7 constants from RFC 5652.
|
||||||
|
*/
|
||||||
|
public abstract class Pkcs7Constants {
|
||||||
|
private Pkcs7Constants() {}
|
||||||
|
|
||||||
|
public static final String OID_DATA = "1.2.840.113549.1.7.1";
|
||||||
|
public static final String OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
|
||||||
|
public static final String OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
|
||||||
|
public static final String OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an error was encountered while decoding a PKCS #7 structure.
|
||||||
|
*/
|
||||||
|
public class Pkcs7DecodingException extends Exception {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
public Pkcs7DecodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pkcs7DecodingException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code SignedData} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class SignedData {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
|
||||||
|
public int version;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.SET_OF)
|
||||||
|
public List<AlgorithmIdentifier> digestAlgorithms;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
|
||||||
|
public EncapsulatedContentInfo encapContentInfo;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 3,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
|
||||||
|
optional = true)
|
||||||
|
public List<Asn1OpaqueObject> certificates;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 4,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
|
||||||
|
optional = true)
|
||||||
|
public List<ByteBuffer> crls;
|
||||||
|
|
||||||
|
@Asn1Field(index = 5, type = Asn1Type.SET_OF)
|
||||||
|
public List<SignerInfo> signerInfos;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code SignerIdentifier} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.CHOICE)
|
||||||
|
public class SignerIdentifier {
|
||||||
|
|
||||||
|
@Asn1Field(type = Asn1Type.SEQUENCE)
|
||||||
|
public IssuerAndSerialNumber issuerAndSerialNumber;
|
||||||
|
|
||||||
|
@Asn1Field(type = Asn1Type.OCTET_STRING, tagging = Asn1Tagging.IMPLICIT, tagNumber = 0)
|
||||||
|
public ByteBuffer subjectKeyIdentifier;
|
||||||
|
|
||||||
|
public SignerIdentifier() {}
|
||||||
|
|
||||||
|
public SignerIdentifier(IssuerAndSerialNumber issuerAndSerialNumber) {
|
||||||
|
this.issuerAndSerialNumber = issuerAndSerialNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.pkcs7;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Tagging;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKCS #7 {@code SignerInfo} as specified in RFC 5652.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class SignerInfo {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.INTEGER)
|
||||||
|
public int version;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.CHOICE)
|
||||||
|
public SignerIdentifier sid;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.SEQUENCE)
|
||||||
|
public AlgorithmIdentifier digestAlgorithm;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 3,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 0,
|
||||||
|
optional = true)
|
||||||
|
public Asn1OpaqueObject signedAttrs;
|
||||||
|
|
||||||
|
@Asn1Field(index = 4, type = Asn1Type.SEQUENCE)
|
||||||
|
public AlgorithmIdentifier signatureAlgorithm;
|
||||||
|
|
||||||
|
@Asn1Field(index = 5, type = Asn1Type.OCTET_STRING)
|
||||||
|
public ByteBuffer signature;
|
||||||
|
|
||||||
|
@Asn1Field(
|
||||||
|
index = 6,
|
||||||
|
type = Asn1Type.SET_OF,
|
||||||
|
tagging = Asn1Tagging.IMPLICIT, tagNumber = 1,
|
||||||
|
optional = true)
|
||||||
|
public List<Attribute> unsignedAttrs;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android SDK version / API Level constants.
|
||||||
|
*/
|
||||||
|
public abstract class AndroidSdkVersion {
|
||||||
|
|
||||||
|
/** Hidden constructor to prevent instantiation. */
|
||||||
|
private AndroidSdkVersion() {}
|
||||||
|
|
||||||
|
/** Android 1.0 */
|
||||||
|
public static final int INITIAL_RELEASE = 1;
|
||||||
|
|
||||||
|
/** Android 2.3. */
|
||||||
|
public static final int GINGERBREAD = 9;
|
||||||
|
|
||||||
|
/** Android 3.0 */
|
||||||
|
public static final int HONEYCOMB = 11;
|
||||||
|
|
||||||
|
/** Android 4.3. The revenge of the beans. */
|
||||||
|
public static final int JELLY_BEAN_MR2 = 18;
|
||||||
|
|
||||||
|
/** Android 4.4. KitKat, another tasty treat. */
|
||||||
|
public static final int KITKAT = 19;
|
||||||
|
|
||||||
|
/** Android 5.0. A flat one with beautiful shadows. But still tasty. */
|
||||||
|
public static final int LOLLIPOP = 21;
|
||||||
|
|
||||||
|
/** Android 6.0. M is for Marshmallow! */
|
||||||
|
public static final int M = 23;
|
||||||
|
|
||||||
|
/** Android 7.0. N is for Nougat. */
|
||||||
|
public static final int N = 24;
|
||||||
|
|
||||||
|
/** Android O. */
|
||||||
|
public static final int O = 26;
|
||||||
|
|
||||||
|
/** Android P. */
|
||||||
|
public static final int P = 28;
|
||||||
|
|
||||||
|
/** Android Q. */
|
||||||
|
public static final int Q = 29;
|
||||||
|
|
||||||
|
/** Android R. */
|
||||||
|
public static final int R = 30;
|
||||||
|
|
||||||
|
/** Android S. */
|
||||||
|
public static final int S = 31;
|
||||||
|
|
||||||
|
/** Android Sv2. */
|
||||||
|
public static final int Sv2 = 32;
|
||||||
|
|
||||||
|
/** Android Tiramisu. */
|
||||||
|
public static final int T = 33;
|
||||||
|
|
||||||
|
/** Android Upside Down Cake. */
|
||||||
|
public static final int U = 34;
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.ReadableDataSink;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Growable byte array which can be appended to via {@link DataSink} interface and read from via
|
||||||
|
* {@link DataSource} interface.
|
||||||
|
*/
|
||||||
|
public class ByteArrayDataSink implements ReadableDataSink {
|
||||||
|
|
||||||
|
private static final int MAX_READ_CHUNK_SIZE = 65536;
|
||||||
|
|
||||||
|
private byte[] mArray;
|
||||||
|
private int mSize;
|
||||||
|
|
||||||
|
public ByteArrayDataSink() {
|
||||||
|
this(65536);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteArrayDataSink(int initialCapacity) {
|
||||||
|
if (initialCapacity < 0) {
|
||||||
|
throw new IllegalArgumentException("initial capacity: " + initialCapacity);
|
||||||
|
}
|
||||||
|
mArray = new byte[initialCapacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
if (offset < 0) {
|
||||||
|
// Must perform this check because System.arraycopy below doesn't perform it when
|
||||||
|
// length == 0
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (offset > buf.length) {
|
||||||
|
// Must perform this check because System.arraycopy below doesn't perform it when
|
||||||
|
// length == 0
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset: " + offset + ", buf.length: " + buf.length);
|
||||||
|
}
|
||||||
|
if (length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAvailable(length);
|
||||||
|
System.arraycopy(buf, offset, mArray, mSize, length);
|
||||||
|
mSize += length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
if (!buf.hasRemaining()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.hasArray()) {
|
||||||
|
consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
|
||||||
|
buf.position(buf.limit());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAvailable(buf.remaining());
|
||||||
|
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
int chunkSize = Math.min(buf.remaining(), tmp.length);
|
||||||
|
buf.get(tmp, 0, chunkSize);
|
||||||
|
System.arraycopy(tmp, 0, mArray, mSize, chunkSize);
|
||||||
|
mSize += chunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureAvailable(int minAvailable) throws IOException {
|
||||||
|
if (minAvailable <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long minCapacity = ((long) mSize) + minAvailable;
|
||||||
|
if (minCapacity <= mArray.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (minCapacity > Integer.MAX_VALUE) {
|
||||||
|
throw new IOException(
|
||||||
|
"Required capacity too large: " + minCapacity + ", max: " + Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
int doubleCurrentSize = (int) Math.min(mArray.length * 2L, Integer.MAX_VALUE);
|
||||||
|
int newSize = (int) Math.max(minCapacity, doubleCurrentSize);
|
||||||
|
mArray = Arrays.copyOf(mArray, newSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||||
|
return ByteBuffer.wrap(mArray, (int) offset, size).slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset and size to int.
|
||||||
|
sink.consume(mArray, (int) offset, (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||||
|
dest.put(mArray, (int) offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkChunkValid(long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + mSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource slice(long offset, long size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset and size to int.
|
||||||
|
return new SliceDataSource((int) offset, (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slice of the growable byte array. The slice's offset and size in the array are fixed.
|
||||||
|
*/
|
||||||
|
private class SliceDataSource implements DataSource {
|
||||||
|
private final int mSliceOffset;
|
||||||
|
private final int mSliceSize;
|
||||||
|
|
||||||
|
private SliceDataSource(int offset, int size) {
|
||||||
|
mSliceOffset = offset;
|
||||||
|
mSliceSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mSliceSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
|
||||||
|
sink.consume(mArray, (int) (mSliceOffset + offset), (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow.
|
||||||
|
return ByteBuffer.wrap(mArray, (int) (mSliceOffset + offset), size).slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow.
|
||||||
|
dest.put(mArray, (int) (mSliceOffset + offset), size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource slice(long offset, long size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
// checkChunkValid combined with the way instances of this class are constructed ensures
|
||||||
|
// that mSliceOffset + offset does not overflow and that it's fine to cast size to int.
|
||||||
|
return new SliceDataSource((int) (mSliceOffset + offset), (int) size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkChunkValid(long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > mSliceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + mSliceSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > mSliceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") > source size (" + mSliceSize
|
||||||
|
+ ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSource} backed by a {@link ByteBuffer}.
|
||||||
|
*/
|
||||||
|
public class ByteBufferDataSource implements DataSource {
|
||||||
|
|
||||||
|
private final ByteBuffer mBuffer;
|
||||||
|
private final int mSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
||||||
|
* buffer between the buffer's position and limit.
|
||||||
|
*/
|
||||||
|
public ByteBufferDataSource(ByteBuffer buffer) {
|
||||||
|
this(buffer, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code ByteBufferDigestSource} based on the data contained in the provided
|
||||||
|
* buffer between the buffer's position and limit.
|
||||||
|
*/
|
||||||
|
private ByteBufferDataSource(ByteBuffer buffer, boolean sliceRequired) {
|
||||||
|
mBuffer = (sliceRequired) ? buffer.slice() : buffer;
|
||||||
|
mSize = buffer.remaining();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) {
|
||||||
|
checkChunkValid(offset, size);
|
||||||
|
|
||||||
|
// checkChunkValid ensures that it's OK to cast offset to int.
|
||||||
|
int chunkPosition = (int) offset;
|
||||||
|
int chunkLimit = chunkPosition + size;
|
||||||
|
// Creating a slice of ByteBuffer modifies the state of the source ByteBuffer (position
|
||||||
|
// and limit fields, to be more specific). We thus use synchronization around these
|
||||||
|
// state-changing operations to make instances of this class thread-safe.
|
||||||
|
synchronized (mBuffer) {
|
||||||
|
// ByteBuffer.limit(int) and .position(int) check that that the position >= limit
|
||||||
|
// invariant is not broken. Thus, the only way to safely change position and limit
|
||||||
|
// without caring about their current values is to first set position to 0 or set the
|
||||||
|
// limit to capacity.
|
||||||
|
mBuffer.position(0);
|
||||||
|
|
||||||
|
mBuffer.limit(chunkLimit);
|
||||||
|
mBuffer.position(chunkPosition);
|
||||||
|
return mBuffer.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) {
|
||||||
|
dest.put(getByteBuffer(offset, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
if ((size < 0) || (size > mSize)) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
|
||||||
|
}
|
||||||
|
sink.consume(getByteBuffer(offset, (int) size));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBufferDataSource slice(long offset, long size) {
|
||||||
|
if ((offset == 0) && (size == mSize)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if ((size < 0) || (size > mSize)) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size + ", source size: " + mSize);
|
||||||
|
}
|
||||||
|
return new ByteBufferDataSource(
|
||||||
|
getByteBuffer(offset, (int) size),
|
||||||
|
false // no need to slice -- it's already a slice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkChunkValid(long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + mSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > mSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") > source size (" + mSize +")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferOverflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sink which stores all received data into the associated {@link ByteBuffer}.
|
||||||
|
*/
|
||||||
|
public class ByteBufferSink implements DataSink {
|
||||||
|
|
||||||
|
private final ByteBuffer mBuffer;
|
||||||
|
|
||||||
|
public ByteBufferSink(ByteBuffer buffer) {
|
||||||
|
mBuffer = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteBuffer getBuffer() {
|
||||||
|
return mBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
try {
|
||||||
|
mBuffer.put(buf, offset, length);
|
||||||
|
} catch (BufferOverflowException e) {
|
||||||
|
throw new IOException(
|
||||||
|
"Insufficient space in output buffer for " + length + " bytes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
int length = buf.remaining();
|
||||||
|
try {
|
||||||
|
mBuffer.put(buf);
|
||||||
|
} catch (BufferOverflowException e) {
|
||||||
|
throw new IOException(
|
||||||
|
"Insufficient space in output buffer for " + length + " bytes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public final class ByteBufferUtils {
|
||||||
|
private ByteBufferUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remaining data of the provided buffer as a new byte array and advances the
|
||||||
|
* position of the buffer to the buffer's limit.
|
||||||
|
*/
|
||||||
|
public static byte[] toByteArray(ByteBuffer buf) {
|
||||||
|
byte[] result = new byte[buf.remaining()];
|
||||||
|
buf.get(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for byte arrays and I/O streams.
|
||||||
|
*/
|
||||||
|
public final class ByteStreams {
|
||||||
|
private ByteStreams() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data remaining in the provided input stream as a byte array
|
||||||
|
*/
|
||||||
|
public static byte[] toByteArray(InputStream in) throws IOException {
|
||||||
|
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||||
|
byte[] buf = new byte[16384];
|
||||||
|
int chunkSize;
|
||||||
|
while ((chunkSize = in.read(buf)) != -1) {
|
||||||
|
result.write(buf, 0, chunkSize);
|
||||||
|
}
|
||||||
|
return result.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/** Pseudo {@link DataSource} that chains the given {@link DataSource} as a continuous one. */
|
||||||
|
public class ChainedDataSource implements DataSource {
|
||||||
|
|
||||||
|
private final DataSource[] mSources;
|
||||||
|
private final long mTotalSize;
|
||||||
|
|
||||||
|
public ChainedDataSource(DataSource... sources) {
|
||||||
|
mSources = sources;
|
||||||
|
mTotalSize = Arrays.stream(sources).mapToLong(src -> src.size()).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
return mTotalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void feed(long offset, long size, DataSink sink) throws IOException {
|
||||||
|
if (offset + size > mTotalSize) {
|
||||||
|
throw new IndexOutOfBoundsException("Requested more than available");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DataSource src : mSources) {
|
||||||
|
// Offset is beyond the current source. Skip.
|
||||||
|
if (offset >= src.size()) {
|
||||||
|
offset -= src.size();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the remaining is enough, finish it.
|
||||||
|
long remaining = src.size() - offset;
|
||||||
|
if (remaining >= size) {
|
||||||
|
src.feed(offset, size, sink);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the remaining is not enough, consume all.
|
||||||
|
src.feed(offset, remaining, sink);
|
||||||
|
size -= remaining;
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||||
|
if (offset + size > mTotalSize) {
|
||||||
|
throw new IndexOutOfBoundsException("Requested more than available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip to the first DataSource we need.
|
||||||
|
Pair<Integer, Long> firstSource = locateDataSource(offset);
|
||||||
|
int i = firstSource.getFirst();
|
||||||
|
offset = firstSource.getSecond();
|
||||||
|
|
||||||
|
// Return the current source's ByteBuffer if it fits.
|
||||||
|
if (offset + size <= mSources[i].size()) {
|
||||||
|
return mSources[i].getByteBuffer(offset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, read into a new buffer.
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(size);
|
||||||
|
for (; i < mSources.length && buffer.hasRemaining(); i++) {
|
||||||
|
long sizeToCopy = Math.min(mSources[i].size() - offset, buffer.remaining());
|
||||||
|
mSources[i].copyTo(offset, Math.toIntExact(sizeToCopy), buffer);
|
||||||
|
offset = 0; // may not be zero for the first source, but reset after that.
|
||||||
|
}
|
||||||
|
buffer.rewind();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void copyTo(long offset, int size, ByteBuffer dest) throws IOException {
|
||||||
|
feed(offset, size, new ByteBufferSink(dest));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataSource slice(long offset, long size) {
|
||||||
|
// Find the first slice.
|
||||||
|
Pair<Integer, Long> firstSource = locateDataSource(offset);
|
||||||
|
int beginIndex = firstSource.getFirst();
|
||||||
|
long beginLocalOffset = firstSource.getSecond();
|
||||||
|
DataSource beginSource = mSources[beginIndex];
|
||||||
|
|
||||||
|
if (beginLocalOffset + size <= beginSource.size()) {
|
||||||
|
return beginSource.slice(beginLocalOffset, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the first slice to chaining, followed by the middle full slices, then the last.
|
||||||
|
ArrayList<DataSource> sources = new ArrayList<>();
|
||||||
|
sources.add(beginSource.slice(
|
||||||
|
beginLocalOffset, beginSource.size() - beginLocalOffset));
|
||||||
|
|
||||||
|
Pair<Integer, Long> lastSource = locateDataSource(offset + size - 1);
|
||||||
|
int endIndex = lastSource.getFirst();
|
||||||
|
long endLocalOffset = lastSource.getSecond();
|
||||||
|
|
||||||
|
for (int i = beginIndex + 1; i < endIndex; i++) {
|
||||||
|
sources.add(mSources[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.add(mSources[endIndex].slice(0, endLocalOffset + 1));
|
||||||
|
return new ChainedDataSource(sources.toArray(new DataSource[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the index of DataSource that offset is at.
|
||||||
|
* @return Pair of DataSource index and the local offset in the DataSource.
|
||||||
|
*/
|
||||||
|
private Pair<Integer, Long> locateDataSource(long offset) {
|
||||||
|
long localOffset = offset;
|
||||||
|
for (int i = 0; i < mSources.length; i++) {
|
||||||
|
if (localOffset < mSources[i].size()) {
|
||||||
|
return Pair.of(i, localOffset);
|
||||||
|
}
|
||||||
|
localOffset -= mSources[i].size();
|
||||||
|
}
|
||||||
|
throw new IndexOutOfBoundsException("Access is out of bound, offset: " + offset +
|
||||||
|
", totalSize: " + mTotalSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.Provider;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateExpiredException;
|
||||||
|
import java.security.cert.CertificateNotYetValidException;
|
||||||
|
import java.security.cert.CertificateParsingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link X509Certificate} which delegates all method invocations to the provided delegate
|
||||||
|
* {@code X509Certificate}.
|
||||||
|
*/
|
||||||
|
public class DelegatingX509Certificate extends X509Certificate {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final X509Certificate mDelegate;
|
||||||
|
|
||||||
|
public DelegatingX509Certificate(X509Certificate delegate) {
|
||||||
|
this.mDelegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getCriticalExtensionOIDs() {
|
||||||
|
return mDelegate.getCriticalExtensionOIDs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getExtensionValue(String oid) {
|
||||||
|
return mDelegate.getExtensionValue(oid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getNonCriticalExtensionOIDs() {
|
||||||
|
return mDelegate.getNonCriticalExtensionOIDs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasUnsupportedCriticalExtension() {
|
||||||
|
return mDelegate.hasUnsupportedCriticalExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkValidity()
|
||||||
|
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||||
|
mDelegate.checkValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void checkValidity(Date date)
|
||||||
|
throws CertificateExpiredException, CertificateNotYetValidException {
|
||||||
|
mDelegate.checkValidity(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getVersion() {
|
||||||
|
return mDelegate.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigInteger getSerialNumber() {
|
||||||
|
return mDelegate.getSerialNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Principal getIssuerDN() {
|
||||||
|
return mDelegate.getIssuerDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Principal getSubjectDN() {
|
||||||
|
return mDelegate.getSubjectDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getNotBefore() {
|
||||||
|
return mDelegate.getNotBefore();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getNotAfter() {
|
||||||
|
return mDelegate.getNotAfter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getTBSCertificate() throws CertificateEncodingException {
|
||||||
|
return mDelegate.getTBSCertificate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getSignature() {
|
||||||
|
return mDelegate.getSignature();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSigAlgName() {
|
||||||
|
return mDelegate.getSigAlgName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSigAlgOID() {
|
||||||
|
return mDelegate.getSigAlgOID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getSigAlgParams() {
|
||||||
|
return mDelegate.getSigAlgParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean[] getIssuerUniqueID() {
|
||||||
|
return mDelegate.getIssuerUniqueID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean[] getSubjectUniqueID() {
|
||||||
|
return mDelegate.getSubjectUniqueID();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean[] getKeyUsage() {
|
||||||
|
return mDelegate.getKeyUsage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBasicConstraints() {
|
||||||
|
return mDelegate.getBasicConstraints();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getEncoded() throws CertificateEncodingException {
|
||||||
|
return mDelegate.getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
|
||||||
|
InvalidKeyException, NoSuchProviderException, SignatureException {
|
||||||
|
mDelegate.verify(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verify(PublicKey key, String sigProvider)
|
||||||
|
throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
|
||||||
|
NoSuchProviderException, SignatureException {
|
||||||
|
mDelegate.verify(key, sigProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return mDelegate.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PublicKey getPublicKey() {
|
||||||
|
return mDelegate.getPublicKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X500Principal getIssuerX500Principal() {
|
||||||
|
return mDelegate.getIssuerX500Principal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X500Principal getSubjectX500Principal() {
|
||||||
|
return mDelegate.getSubjectX500Principal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getExtendedKeyUsage() throws CertificateParsingException {
|
||||||
|
return mDelegate.getExtendedKeyUsage();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
|
||||||
|
return mDelegate.getSubjectAlternativeNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<List<?>> getIssuerAlternativeNames() throws CertificateParsingException {
|
||||||
|
return mDelegate.getIssuerAlternativeNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("AndroidJdkLibsChecker")
|
||||||
|
public void verify(PublicKey key, Provider sigProvider) throws CertificateException,
|
||||||
|
NoSuchAlgorithmException, InvalidKeyException, SignatureException {
|
||||||
|
mDelegate.verify(key, sigProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.BufferOverflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSource} backed by a {@link FileChannel} for {@link RandomAccessFile} access.
|
||||||
|
*/
|
||||||
|
public class FileChannelDataSource implements DataSource {
|
||||||
|
|
||||||
|
private static final int MAX_READ_CHUNK_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
private final FileChannel mChannel;
|
||||||
|
private final long mOffset;
|
||||||
|
private final long mSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code FileChannelDataSource} based on the data contained in the
|
||||||
|
* whole file. Changes to the contents of the file, including the size of the file,
|
||||||
|
* will be visible in this data source.
|
||||||
|
*/
|
||||||
|
public FileChannelDataSource(FileChannel channel) {
|
||||||
|
mChannel = channel;
|
||||||
|
mOffset = 0;
|
||||||
|
mSize = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code FileChannelDataSource} 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.
|
||||||
|
*
|
||||||
|
* @throws IndexOutOfBoundsException if {@code offset} or {@code size} is negative.
|
||||||
|
*/
|
||||||
|
public FileChannelDataSource(FileChannel channel, long offset, long size) {
|
||||||
|
if (offset < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + size);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
mChannel = channel;
|
||||||
|
mOffset = offset;
|
||||||
|
mSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long size() {
|
||||||
|
if (mSize == -1) {
|
||||||
|
try {
|
||||||
|
return mChannel.size();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileChannelDataSource slice(long offset, long size) {
|
||||||
|
long sourceSize = size();
|
||||||
|
checkChunkValid(offset, size, sourceSize);
|
||||||
|
if ((offset == 0) && (size == sourceSize)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FileChannelDataSource(mChannel, 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;
|
||||||
|
ByteBuffer buf = ByteBuffer.allocateDirect((int) Math.min(remaining, MAX_READ_CHUNK_SIZE));
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkSize = (int) Math.min(remaining, buf.capacity());
|
||||||
|
int chunkRemaining = chunkSize;
|
||||||
|
buf.limit(chunkSize);
|
||||||
|
synchronized (mChannel) {
|
||||||
|
mChannel.position(chunkOffsetInFile);
|
||||||
|
while (chunkRemaining > 0) {
|
||||||
|
int read = mChannel.read(buf);
|
||||||
|
if (read < 0) {
|
||||||
|
throw new IOException("Unexpected EOF encountered");
|
||||||
|
}
|
||||||
|
chunkRemaining -= read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.flip();
|
||||||
|
sink.consume(buf);
|
||||||
|
buf.clear();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (size > dest.remaining()) {
|
||||||
|
throw new BufferOverflowException();
|
||||||
|
}
|
||||||
|
|
||||||
|
long offsetInFile = mOffset + offset;
|
||||||
|
int remaining = size;
|
||||||
|
int prevLimit = dest.limit();
|
||||||
|
try {
|
||||||
|
// FileChannel.read(ByteBuffer) reads up to dest.remaining(). Thus, we need to adjust
|
||||||
|
// the buffer's limit to avoid reading more than size bytes.
|
||||||
|
dest.limit(dest.position() + size);
|
||||||
|
while (remaining > 0) {
|
||||||
|
int chunkSize;
|
||||||
|
synchronized (mChannel) {
|
||||||
|
mChannel.position(offsetInFile);
|
||||||
|
chunkSize = mChannel.read(dest);
|
||||||
|
}
|
||||||
|
offsetInFile += chunkSize;
|
||||||
|
remaining -= chunkSize;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
dest.limit(prevLimit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ByteBuffer getByteBuffer(long offset, int size) throws IOException {
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
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 IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (size < 0) {
|
||||||
|
throw new IndexOutOfBoundsException("size: " + size);
|
||||||
|
}
|
||||||
|
if (offset > sourceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") > source size (" + sourceSize + ")");
|
||||||
|
}
|
||||||
|
long endOffset = offset + size;
|
||||||
|
if (endOffset < offset) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size + ") overflow");
|
||||||
|
}
|
||||||
|
if (endOffset > sourceSize) {
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset (" + offset + ") + size (" + size
|
||||||
|
+ ") > source size (" + sourceSize +")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
|
||||||
|
* time.
|
||||||
|
*/
|
||||||
|
public class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final byte[] mEncodedForm;
|
||||||
|
private int mHash = -1;
|
||||||
|
|
||||||
|
public GuaranteedEncodedFormX509Certificate(X509Certificate wrapped, byte[] encodedForm) {
|
||||||
|
super(wrapped);
|
||||||
|
this.mEncodedForm = (encodedForm != null) ? encodedForm.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getEncoded() throws CertificateEncodingException {
|
||||||
|
return (mEncodedForm != null) ? mEncodedForm.clone() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof X509Certificate)) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] a = this.getEncoded();
|
||||||
|
byte[] b = ((X509Certificate) o).getEncoded();
|
||||||
|
return Arrays.equals(a, b);
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
if (mHash == -1) {
|
||||||
|
try {
|
||||||
|
mHash = Arrays.hashCode(this.getEncoded());
|
||||||
|
} catch (CertificateEncodingException e) {
|
||||||
|
mHash = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inclusive interval of integers.
|
||||||
|
*/
|
||||||
|
public class InclusiveIntRange {
|
||||||
|
private final int min;
|
||||||
|
private final int max;
|
||||||
|
|
||||||
|
private InclusiveIntRange(int min, int max) {
|
||||||
|
this.min = min;
|
||||||
|
this.max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMin() {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMax() {
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InclusiveIntRange fromTo(int min, int max) {
|
||||||
|
return new InclusiveIntRange(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InclusiveIntRange from(int min) {
|
||||||
|
return new InclusiveIntRange(min, Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InclusiveIntRange> getValuesNotIn(
|
||||||
|
List<InclusiveIntRange> sortedNonOverlappingRanges) {
|
||||||
|
if (sortedNonOverlappingRanges.isEmpty()) {
|
||||||
|
return Collections.singletonList(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
int testValue = min;
|
||||||
|
List<InclusiveIntRange> result = null;
|
||||||
|
for (InclusiveIntRange range : sortedNonOverlappingRanges) {
|
||||||
|
int rangeMax = range.max;
|
||||||
|
if (testValue > rangeMax) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int rangeMin = range.min;
|
||||||
|
if (testValue < range.min) {
|
||||||
|
if (result == null) {
|
||||||
|
result = new ArrayList<>();
|
||||||
|
}
|
||||||
|
result.add(fromTo(testValue, rangeMin - 1));
|
||||||
|
}
|
||||||
|
if (rangeMax >= max) {
|
||||||
|
return (result != null) ? result : Collections.emptyList();
|
||||||
|
}
|
||||||
|
testValue = rangeMax + 1;
|
||||||
|
}
|
||||||
|
if (testValue <= max) {
|
||||||
|
if (result == null) {
|
||||||
|
result = new ArrayList<>(1);
|
||||||
|
}
|
||||||
|
result.add(fromTo(testValue, max));
|
||||||
|
}
|
||||||
|
return (result != null) ? result : Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "[" + min + ", " + ((max < Integer.MAX_VALUE) ? (max + "]") : "\u221e)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sink which feeds all received data into the associated {@link MessageDigest} instances. Each
|
||||||
|
* {@code MessageDigest} instance receives the same data.
|
||||||
|
*/
|
||||||
|
public class MessageDigestSink implements DataSink {
|
||||||
|
|
||||||
|
private final MessageDigest[] mMessageDigests;
|
||||||
|
|
||||||
|
public MessageDigestSink(MessageDigest[] digests) {
|
||||||
|
mMessageDigests = digests;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) {
|
||||||
|
for (MessageDigest md : mMessageDigests) {
|
||||||
|
md.update(buf, offset, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) {
|
||||||
|
int originalPosition = buf.position();
|
||||||
|
for (MessageDigest md : mMessageDigests) {
|
||||||
|
// Reset the position back to the original because the previous iteration's
|
||||||
|
// MessageDigest.update set the buffer's position to the buffer's limit.
|
||||||
|
buf.position(originalPosition);
|
||||||
|
md.update(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSink} which outputs received data into the associated {@link OutputStream}.
|
||||||
|
*/
|
||||||
|
public class OutputStreamDataSink implements DataSink {
|
||||||
|
|
||||||
|
private static final int MAX_READ_CHUNK_SIZE = 65536;
|
||||||
|
|
||||||
|
private final OutputStream mOut;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code OutputStreamDataSink} which outputs received data into the provided
|
||||||
|
* {@link OutputStream}.
|
||||||
|
*/
|
||||||
|
public OutputStreamDataSink(OutputStream out) {
|
||||||
|
if (out == null) {
|
||||||
|
throw new NullPointerException("out == null");
|
||||||
|
}
|
||||||
|
mOut = out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link OutputStream} into which this data sink outputs received data.
|
||||||
|
*/
|
||||||
|
public OutputStream getOutputStream() {
|
||||||
|
return mOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
mOut.write(buf, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
if (!buf.hasRemaining()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.hasArray()) {
|
||||||
|
mOut.write(
|
||||||
|
buf.array(),
|
||||||
|
buf.arrayOffset() + buf.position(),
|
||||||
|
buf.remaining());
|
||||||
|
buf.position(buf.limit());
|
||||||
|
} else {
|
||||||
|
byte[] tmp = new byte[Math.min(buf.remaining(), MAX_READ_CHUNK_SIZE)];
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
int chunkSize = Math.min(buf.remaining(), tmp.length);
|
||||||
|
buf.get(tmp, 0, chunkSize);
|
||||||
|
mOut.write(tmp, 0, chunkSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pair of two elements.
|
||||||
|
*/
|
||||||
|
public final class Pair<A, B> {
|
||||||
|
private final A mFirst;
|
||||||
|
private final B mSecond;
|
||||||
|
|
||||||
|
private Pair(A first, B second) {
|
||||||
|
mFirst = first;
|
||||||
|
mSecond = second;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B> Pair<A, B> of(A first, B second) {
|
||||||
|
return new Pair<A, B>(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
public A getFirst() {
|
||||||
|
return mFirst;
|
||||||
|
}
|
||||||
|
|
||||||
|
public B getSecond() {
|
||||||
|
return mSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
|
||||||
|
result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
Pair other = (Pair) obj;
|
||||||
|
if (mFirst == null) {
|
||||||
|
if (other.mFirst != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (!mFirst.equals(other.mFirst)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mSecond == null) {
|
||||||
|
if (other.mSecond != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (!mSecond.equals(other.mSecond)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.FileChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSink} which outputs received data into the associated file, sequentially.
|
||||||
|
*/
|
||||||
|
public class RandomAccessFileDataSink implements DataSink {
|
||||||
|
|
||||||
|
private final RandomAccessFile mFile;
|
||||||
|
private final FileChannel mFileChannel;
|
||||||
|
private long mPosition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
|
||||||
|
* beginning of the provided file.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileDataSink(RandomAccessFile file) {
|
||||||
|
this(file, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@code RandomAccessFileDataSink} which stores output starting from the
|
||||||
|
* specified position of the provided file.
|
||||||
|
*/
|
||||||
|
public RandomAccessFileDataSink(RandomAccessFile file, long startPosition) {
|
||||||
|
if (file == null) {
|
||||||
|
throw new NullPointerException("file == null");
|
||||||
|
}
|
||||||
|
if (startPosition < 0) {
|
||||||
|
throw new IllegalArgumentException("startPosition: " + startPosition);
|
||||||
|
}
|
||||||
|
mFile = file;
|
||||||
|
mFileChannel = file.getChannel();
|
||||||
|
mPosition = startPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying {@link RandomAccessFile}.
|
||||||
|
*/
|
||||||
|
public RandomAccessFile getFile() {
|
||||||
|
return mFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
if (offset < 0) {
|
||||||
|
// Must perform this check here because RandomAccessFile.write doesn't throw when offset
|
||||||
|
// is negative but length is 0
|
||||||
|
throw new IndexOutOfBoundsException("offset: " + offset);
|
||||||
|
}
|
||||||
|
if (offset > buf.length) {
|
||||||
|
// Must perform this check here because RandomAccessFile.write doesn't throw when offset
|
||||||
|
// is too large but length is 0
|
||||||
|
throw new IndexOutOfBoundsException(
|
||||||
|
"offset: " + offset + ", buf.length: " + buf.length);
|
||||||
|
}
|
||||||
|
if (length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (mFile) {
|
||||||
|
mFile.seek(mPosition);
|
||||||
|
mFile.write(buf, offset, length);
|
||||||
|
mPosition += length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
int length = buf.remaining();
|
||||||
|
if (length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (mFile) {
|
||||||
|
mFile.seek(mPosition);
|
||||||
|
while (buf.hasRemaining()) {
|
||||||
|
mFileChannel.write(buf);
|
||||||
|
}
|
||||||
|
mPosition += length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DataSink} which copies provided input into each of the sinks provided to it.
|
||||||
|
*/
|
||||||
|
public class TeeDataSink implements DataSink {
|
||||||
|
|
||||||
|
private final DataSink[] mSinks;
|
||||||
|
|
||||||
|
public TeeDataSink(DataSink[] sinks) {
|
||||||
|
mSinks = sinks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(byte[] buf, int offset, int length) throws IOException {
|
||||||
|
for (DataSink sink : mSinks) {
|
||||||
|
sink.consume(buf, offset, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void consume(ByteBuffer buf) throws IOException {
|
||||||
|
int originalPosition = buf.position();
|
||||||
|
for (int i = 0; i < mSinks.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
buf.position(originalPosition);
|
||||||
|
}
|
||||||
|
mSinks[i].consume(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2017 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.zip.ZipUtils;
|
||||||
|
import com.android.apksig.util.DataSink;
|
||||||
|
import com.android.apksig.util.DataSource;
|
||||||
|
import com.android.apksig.util.DataSources;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Phaser;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VerityTreeBuilder is used to generate the root hash of verity tree built from the input file.
|
||||||
|
* The root hash can be used on device for on-access verification. The tree itself is reproducible
|
||||||
|
* on device, and is not shipped with the APK.
|
||||||
|
*/
|
||||||
|
public class VerityTreeBuilder implements AutoCloseable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum size (in bytes) of each node of the tree.
|
||||||
|
*/
|
||||||
|
private final static int CHUNK_SIZE = 4096;
|
||||||
|
/**
|
||||||
|
* Maximum parallelism while calculating digests.
|
||||||
|
*/
|
||||||
|
private final static int DIGEST_PARALLELISM = Math.min(32,
|
||||||
|
Runtime.getRuntime().availableProcessors());
|
||||||
|
/**
|
||||||
|
* Queue size.
|
||||||
|
*/
|
||||||
|
private final static int MAX_OUTSTANDING_CHUNKS = 4;
|
||||||
|
/**
|
||||||
|
* Typical prefetch size.
|
||||||
|
*/
|
||||||
|
private final static int MAX_PREFETCH_CHUNKS = 1024;
|
||||||
|
/**
|
||||||
|
* Minimum chunks to be processed by a single worker task.
|
||||||
|
*/
|
||||||
|
private final static int MIN_CHUNKS_PER_WORKER = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest algorithm (JCA Digest algorithm name) used in the tree.
|
||||||
|
*/
|
||||||
|
private final static String JCA_ALGORITHM = "SHA-256";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional salt to apply before each digestion.
|
||||||
|
*/
|
||||||
|
private final byte[] mSalt;
|
||||||
|
|
||||||
|
private final MessageDigest mMd;
|
||||||
|
|
||||||
|
private final ExecutorService mExecutor =
|
||||||
|
new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM,
|
||||||
|
0L, MILLISECONDS,
|
||||||
|
new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS),
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
|
||||||
|
public VerityTreeBuilder(byte[] salt) throws NoSuchAlgorithmException {
|
||||||
|
mSalt = salt;
|
||||||
|
mMd = getNewMessageDigest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
mExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root hash of the APK verity tree built from ZIP blocks.
|
||||||
|
*
|
||||||
|
* Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which
|
||||||
|
* must be page aligned) and the "Central Directory offset" field in End of Central Directory
|
||||||
|
* are skipped.
|
||||||
|
*/
|
||||||
|
public byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock,
|
||||||
|
DataSource centralDir, DataSource eocd) throws IOException {
|
||||||
|
if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) {
|
||||||
|
throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE
|
||||||
|
+ ": " + beforeApkSigningBlock.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that, when digesting, ZIP End of Central Directory record's Central Directory
|
||||||
|
// offset field is treated as pointing to the offset at which the APK Signing Block will
|
||||||
|
// start.
|
||||||
|
long centralDirOffsetForDigesting = beforeApkSigningBlock.size();
|
||||||
|
ByteBuffer eocdBuf = ByteBuffer.allocate((int) eocd.size());
|
||||||
|
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
eocd.copyTo(0, (int) eocd.size(), eocdBuf);
|
||||||
|
eocdBuf.flip();
|
||||||
|
ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, centralDirOffsetForDigesting);
|
||||||
|
|
||||||
|
return generateVerityTreeRootHash(new ChainedDataSource(beforeApkSigningBlock, centralDir,
|
||||||
|
DataSources.asDataSource(eocdBuf)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root hash of the verity tree built from the data source.
|
||||||
|
*/
|
||||||
|
public byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
|
||||||
|
ByteBuffer verityBuffer = generateVerityTree(fileSource);
|
||||||
|
return getRootHashFromTree(verityBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the byte buffer that contains the whole verity tree.
|
||||||
|
*
|
||||||
|
* The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
|
||||||
|
* input file. If the total size is larger than 4 KB, take this level as input and repeat the
|
||||||
|
* same procedure, until the level is within 4 KB. If salt is given, it will apply to each
|
||||||
|
* digestion before the actual data.
|
||||||
|
*
|
||||||
|
* The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt.
|
||||||
|
*
|
||||||
|
* The tree is currently stored only in memory and is never written out. Nevertheless, it is
|
||||||
|
* the actual verity tree format on disk, and is supposed to be re-generated on device.
|
||||||
|
*/
|
||||||
|
public ByteBuffer generateVerityTree(DataSource fileSource) throws IOException {
|
||||||
|
int digestSize = mMd.getDigestLength();
|
||||||
|
|
||||||
|
// Calculate the summed area table of level size. In other word, this is the offset
|
||||||
|
// table of each level, plus the next non-existing level.
|
||||||
|
int[] levelOffset = calculateLevelOffset(fileSource.size(), digestSize);
|
||||||
|
|
||||||
|
ByteBuffer verityBuffer = ByteBuffer.allocate(levelOffset[levelOffset.length - 1]);
|
||||||
|
|
||||||
|
// Generate the hash tree bottom-up.
|
||||||
|
for (int i = levelOffset.length - 2; i >= 0; i--) {
|
||||||
|
DataSink middleBufferSink = new ByteBufferSink(
|
||||||
|
slice(verityBuffer, levelOffset[i], levelOffset[i + 1]));
|
||||||
|
DataSource src;
|
||||||
|
if (i == levelOffset.length - 2) {
|
||||||
|
src = fileSource;
|
||||||
|
digestDataByChunks(src, middleBufferSink);
|
||||||
|
} else {
|
||||||
|
src = DataSources.asDataSource(slice(verityBuffer.asReadOnlyBuffer(),
|
||||||
|
levelOffset[i + 1], levelOffset[i + 2]));
|
||||||
|
digestDataByChunks(src, middleBufferSink);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the output is not full chunk, pad with 0s.
|
||||||
|
long totalOutput = divideRoundup(src.size(), CHUNK_SIZE) * digestSize;
|
||||||
|
int incomplete = (int) (totalOutput % CHUNK_SIZE);
|
||||||
|
if (incomplete > 0) {
|
||||||
|
byte[] padding = new byte[CHUNK_SIZE - incomplete];
|
||||||
|
middleBufferSink.consume(padding, 0, padding.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return verityBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the digested root hash from the top level (only page) of a verity tree.
|
||||||
|
*/
|
||||||
|
public byte[] getRootHashFromTree(ByteBuffer verityBuffer) throws IOException {
|
||||||
|
ByteBuffer firstPage = slice(verityBuffer.asReadOnlyBuffer(), 0, CHUNK_SIZE);
|
||||||
|
return saltedDigest(firstPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of summed area table of level size in the verity tree. In other words, the
|
||||||
|
* returned array is offset of each level in the verity tree file format, plus an additional
|
||||||
|
* offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size
|
||||||
|
* is level + 1.
|
||||||
|
*/
|
||||||
|
private static int[] calculateLevelOffset(long dataSize, int digestSize) {
|
||||||
|
// Compute total size of each level, bottom to top.
|
||||||
|
ArrayList<Long> levelSize = new ArrayList<>();
|
||||||
|
while (true) {
|
||||||
|
long chunkCount = divideRoundup(dataSize, CHUNK_SIZE);
|
||||||
|
long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize, CHUNK_SIZE);
|
||||||
|
levelSize.add(size);
|
||||||
|
if (chunkCount * digestSize <= CHUNK_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dataSize = chunkCount * digestSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse and convert to summed area table.
|
||||||
|
int[] levelOffset = new int[levelSize.size() + 1];
|
||||||
|
levelOffset[0] = 0;
|
||||||
|
for (int i = 0; i < levelSize.size(); i++) {
|
||||||
|
// We don't support verity tree if it is larger then Integer.MAX_VALUE.
|
||||||
|
levelOffset[i + 1] = levelOffset[i] + Math.toIntExact(
|
||||||
|
levelSize.get(levelSize.size() - i - 1));
|
||||||
|
}
|
||||||
|
return levelOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digest data source by chunks then feeds them to the sink one by one. If the last unit is
|
||||||
|
* less than the chunk size and padding is desired, feed with extra padding 0 to fill up the
|
||||||
|
* chunk before digesting.
|
||||||
|
*/
|
||||||
|
private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException {
|
||||||
|
final long size = dataSource.size();
|
||||||
|
final int chunks = (int) divideRoundup(size, CHUNK_SIZE);
|
||||||
|
|
||||||
|
/** Single IO operation size, in chunks. */
|
||||||
|
final int ioSizeChunks = MAX_PREFETCH_CHUNKS;
|
||||||
|
|
||||||
|
final byte[][] hashes = new byte[chunks][];
|
||||||
|
|
||||||
|
Phaser tasks = new Phaser(1);
|
||||||
|
|
||||||
|
// Reading the input file as fast as we can.
|
||||||
|
final long maxReadSize = ioSizeChunks * CHUNK_SIZE;
|
||||||
|
|
||||||
|
long readOffset = 0;
|
||||||
|
int startChunkIndex = 0;
|
||||||
|
while (readOffset < size) {
|
||||||
|
final long readLimit = Math.min(readOffset + maxReadSize, size);
|
||||||
|
final int readSize = (int) (readLimit - readOffset);
|
||||||
|
final int bufferSizeChunks = (int) divideRoundup(readSize, CHUNK_SIZE);
|
||||||
|
|
||||||
|
// Overllocating to zero-pad last chunk.
|
||||||
|
// With 4MiB block size, 32 threads and 4 queue size we might allocate up to 144MiB.
|
||||||
|
final ByteBuffer buffer = ByteBuffer.allocate(bufferSizeChunks * CHUNK_SIZE);
|
||||||
|
dataSource.copyTo(readOffset, readSize, buffer);
|
||||||
|
buffer.rewind();
|
||||||
|
|
||||||
|
final int readChunkIndex = startChunkIndex;
|
||||||
|
Runnable task = () -> {
|
||||||
|
final MessageDigest md = cloneMessageDigest();
|
||||||
|
for (int offset = 0, finish = buffer.capacity(), chunkIndex = readChunkIndex;
|
||||||
|
offset < finish; offset += CHUNK_SIZE, ++chunkIndex) {
|
||||||
|
ByteBuffer chunk = slice(buffer, offset, offset + CHUNK_SIZE);
|
||||||
|
hashes[chunkIndex] = saltedDigest(md, chunk);
|
||||||
|
}
|
||||||
|
tasks.arriveAndDeregister();
|
||||||
|
};
|
||||||
|
tasks.register();
|
||||||
|
mExecutor.execute(task);
|
||||||
|
|
||||||
|
startChunkIndex += bufferSizeChunks;
|
||||||
|
readOffset += readSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waiting for the tasks to complete.
|
||||||
|
tasks.arriveAndAwaitAdvance();
|
||||||
|
|
||||||
|
// Streaming hashes back.
|
||||||
|
for (byte[] hash : hashes) {
|
||||||
|
dataSink.consume(hash, 0, hash.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the digest of data with salt prepended. */
|
||||||
|
private byte[] saltedDigest(ByteBuffer data) {
|
||||||
|
return saltedDigest(mMd, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] saltedDigest(MessageDigest md, ByteBuffer data) {
|
||||||
|
md.reset();
|
||||||
|
if (mSalt != null) {
|
||||||
|
md.update(mSalt);
|
||||||
|
}
|
||||||
|
md.update(data);
|
||||||
|
return md.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Divides a number and round up to the closest integer. */
|
||||||
|
private static long divideRoundup(long dividend, long divisor) {
|
||||||
|
return (dividend + divisor - 1) / divisor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a slice of the buffer with shared the content. */
|
||||||
|
private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) {
|
||||||
|
ByteBuffer b = buffer.duplicate();
|
||||||
|
b.position(0); // to ensure position <= limit invariant.
|
||||||
|
b.limit(end);
|
||||||
|
b.position(begin);
|
||||||
|
return b.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains a new instance of the message digest algorithm.
|
||||||
|
*/
|
||||||
|
private static MessageDigest getNewMessageDigest() throws NoSuchAlgorithmException {
|
||||||
|
return MessageDigest.getInstance(JCA_ALGORITHM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clones the existing message digest, or creates a new instance if clone is unavailable.
|
||||||
|
*/
|
||||||
|
private MessageDigest cloneMessageDigest() {
|
||||||
|
try {
|
||||||
|
return (MessageDigest) mMd.clone();
|
||||||
|
} catch (CloneNotSupportedException ignored) {
|
||||||
|
try {
|
||||||
|
return getNewMessageDigest();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Failed to obtain an instance of a previously available message digest", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig.internal.util;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1BerParser;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1DecodingException;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1DerEncoder;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1EncodingException;
|
||||||
|
import com.android.apksig.internal.x509.Certificate;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods to generate {@code X509Certificate}s from their encoded form. These methods
|
||||||
|
* can be used to generate certificates that would be rejected by the Java {@code
|
||||||
|
* CertificateFactory}.
|
||||||
|
*/
|
||||||
|
public class X509CertificateUtils {
|
||||||
|
|
||||||
|
private static volatile CertificateFactory sCertFactory = null;
|
||||||
|
|
||||||
|
// The PEM certificate header and footer as specified in RFC 7468:
|
||||||
|
// There is exactly one space character (SP) separating the "BEGIN" or
|
||||||
|
// "END" from the label. There are exactly five hyphen-minus (also
|
||||||
|
// known as dash) characters ("-") on both ends of the encapsulation
|
||||||
|
// boundaries, no more, no less.
|
||||||
|
public static final byte[] BEGIN_CERT_HEADER = "-----BEGIN CERTIFICATE-----".getBytes();
|
||||||
|
public static final byte[] END_CERT_FOOTER = "-----END CERTIFICATE-----".getBytes();
|
||||||
|
|
||||||
|
private static void buildCertFactory() {
|
||||||
|
if (sCertFactory != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCertFactoryHelper();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static synchronized void buildCertFactoryHelper() {
|
||||||
|
if (sCertFactory != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sCertFactory = CertificateFactory.getInstance("X.509");
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Failed to create X.509 CertificateFactory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an {@code X509Certificate} from the {@code InputStream}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the {@code InputStream} cannot be decoded to a valid
|
||||||
|
* certificate.
|
||||||
|
*/
|
||||||
|
public static X509Certificate generateCertificate(InputStream in) throws CertificateException {
|
||||||
|
byte[] encodedForm;
|
||||||
|
try {
|
||||||
|
encodedForm = ByteStreams.toByteArray(in);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CertificateException("Failed to parse certificate", e);
|
||||||
|
}
|
||||||
|
return generateCertificate(encodedForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an {@code X509Certificate} from the encoded form.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
|
||||||
|
*/
|
||||||
|
public static X509Certificate generateCertificate(byte[] encodedForm)
|
||||||
|
throws CertificateException {
|
||||||
|
buildCertFactory();
|
||||||
|
return generateCertificate(encodedForm, sCertFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an {@code X509Certificate} from the encoded form using the provided
|
||||||
|
* {@code CertificateFactory}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the encodedForm cannot be decoded to a valid certificate.
|
||||||
|
*/
|
||||||
|
public static X509Certificate generateCertificate(byte[] encodedForm,
|
||||||
|
CertificateFactory certFactory) throws CertificateException {
|
||||||
|
X509Certificate certificate;
|
||||||
|
try {
|
||||||
|
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(encodedForm));
|
||||||
|
return certificate;
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// This could be expected if the certificate is encoded using a BER encoding that does
|
||||||
|
// not use the minimum number of bytes to represent the length of the contents; attempt
|
||||||
|
// to decode the certificate using the BER parser and re-encode using the DER encoder
|
||||||
|
// below.
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Some apps were previously signed with a BER encoded certificate that now results
|
||||||
|
// in exceptions from the CertificateFactory generateCertificate(s) methods. Since
|
||||||
|
// the original BER encoding of the certificate is used as the signature for these
|
||||||
|
// apps that original encoding must be maintained when signing updated versions of
|
||||||
|
// these apps and any new apps that may require capabilities guarded by the
|
||||||
|
// signature. To maintain the same signature the BER parser can be used to parse
|
||||||
|
// the certificate, then it can be re-encoded to its DER equivalent which is
|
||||||
|
// accepted by the generateCertificate method. The positions in the ByteBuffer can
|
||||||
|
// then be used with the GuaranteedEncodedFormX509Certificate object to ensure the
|
||||||
|
// getEncoded method returns the original signature of the app.
|
||||||
|
ByteBuffer encodedCertBuffer = getNextDEREncodedCertificateBlock(
|
||||||
|
ByteBuffer.wrap(encodedForm));
|
||||||
|
int startingPos = encodedCertBuffer.position();
|
||||||
|
Certificate reencodedCert = Asn1BerParser.parse(encodedCertBuffer, Certificate.class);
|
||||||
|
byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
|
||||||
|
certificate = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(reencodedForm));
|
||||||
|
// If the reencodedForm is successfully accepted by the CertificateFactory then copy the
|
||||||
|
// original encoding from the ByteBuffer and use that encoding in the Guaranteed object.
|
||||||
|
byte[] originalEncoding = new byte[encodedCertBuffer.position() - startingPos];
|
||||||
|
encodedCertBuffer.position(startingPos);
|
||||||
|
encodedCertBuffer.get(originalEncoding);
|
||||||
|
GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
|
||||||
|
new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
|
||||||
|
return guaranteedEncodedCert;
|
||||||
|
} catch (Asn1DecodingException | Asn1EncodingException | CertificateException e) {
|
||||||
|
throw new CertificateException("Failed to parse certificate", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
|
||||||
|
* InputStream}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the InputStream cannot be decoded to zero or more valid
|
||||||
|
* {@code Certificate} objects.
|
||||||
|
*/
|
||||||
|
public static Collection<? extends java.security.cert.Certificate> generateCertificates(
|
||||||
|
InputStream in) throws CertificateException {
|
||||||
|
buildCertFactory();
|
||||||
|
return generateCertificates(in, sCertFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a {@code Collection} of {@code Certificate} objects from the encoded {@code
|
||||||
|
* InputStream} using the provided {@code CertificateFactory}.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the InputStream cannot be decoded to zero or more valid
|
||||||
|
* {@code Certificates} objects.
|
||||||
|
*/
|
||||||
|
public static Collection<? extends java.security.cert.Certificate> generateCertificates(
|
||||||
|
InputStream in, CertificateFactory certFactory) throws CertificateException {
|
||||||
|
// Since the InputStream is not guaranteed to support mark / reset operations first read it
|
||||||
|
// into a byte array to allow using the BER parser / DER encoder if it cannot be read by
|
||||||
|
// the CertificateFactory.
|
||||||
|
byte[] encodedCerts;
|
||||||
|
try {
|
||||||
|
encodedCerts = ByteStreams.toByteArray(in);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new CertificateException("Failed to read the input stream", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return certFactory.generateCertificates(new ByteArrayInputStream(encodedCerts));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// This could be expected if the certificates are encoded using a BER encoding that does
|
||||||
|
// not use the minimum number of bytes to represent the length of the contents; attempt
|
||||||
|
// to decode the certificates using the BER parser and re-encode using the DER encoder
|
||||||
|
// below.
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Collection<X509Certificate> certificates = new ArrayList<>(1);
|
||||||
|
ByteBuffer encodedCertsBuffer = ByteBuffer.wrap(encodedCerts);
|
||||||
|
while (encodedCertsBuffer.hasRemaining()) {
|
||||||
|
ByteBuffer certBuffer = getNextDEREncodedCertificateBlock(encodedCertsBuffer);
|
||||||
|
int startingPos = certBuffer.position();
|
||||||
|
Certificate reencodedCert = Asn1BerParser.parse(certBuffer, Certificate.class);
|
||||||
|
byte[] reencodedForm = Asn1DerEncoder.encode(reencodedCert);
|
||||||
|
X509Certificate certificate = (X509Certificate) certFactory.generateCertificate(
|
||||||
|
new ByteArrayInputStream(reencodedForm));
|
||||||
|
byte[] originalEncoding = new byte[certBuffer.position() - startingPos];
|
||||||
|
certBuffer.position(startingPos);
|
||||||
|
certBuffer.get(originalEncoding);
|
||||||
|
GuaranteedEncodedFormX509Certificate guaranteedEncodedCert =
|
||||||
|
new GuaranteedEncodedFormX509Certificate(certificate, originalEncoding);
|
||||||
|
certificates.add(guaranteedEncodedCert);
|
||||||
|
}
|
||||||
|
return certificates;
|
||||||
|
} catch (Asn1DecodingException | Asn1EncodingException e) {
|
||||||
|
throw new CertificateException("Failed to parse certificates", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided ByteBuffer to obtain the next certificate in DER encoding. If the buffer
|
||||||
|
* does not begin with the PEM certificate header then it is returned with the assumption that
|
||||||
|
* it is already DER encoded. If the buffer does begin with the PEM certificate header then the
|
||||||
|
* certificate data is read from the buffer until the PEM certificate footer is reached; this
|
||||||
|
* data is then base64 decoded and returned in a new ByteBuffer.
|
||||||
|
*
|
||||||
|
* If the buffer is in PEM format then the position of the buffer is moved to the end of the
|
||||||
|
* current certificate; if the buffer is already DER encoded then the position of the buffer is
|
||||||
|
* not modified.
|
||||||
|
*
|
||||||
|
* @throws CertificateException if the buffer contains the PEM certificate header but does not
|
||||||
|
* contain the expected footer.
|
||||||
|
*/
|
||||||
|
private static ByteBuffer getNextDEREncodedCertificateBlock(ByteBuffer certificateBuffer)
|
||||||
|
throws CertificateException {
|
||||||
|
if (certificateBuffer == null) {
|
||||||
|
throw new NullPointerException("The certificateBuffer cannot be null");
|
||||||
|
}
|
||||||
|
// if the buffer does not contain enough data for the PEM cert header then just return the
|
||||||
|
// provided buffer.
|
||||||
|
if (certificateBuffer.remaining() < BEGIN_CERT_HEADER.length) {
|
||||||
|
return certificateBuffer;
|
||||||
|
}
|
||||||
|
certificateBuffer.mark();
|
||||||
|
for (int i = 0; i < BEGIN_CERT_HEADER.length; i++) {
|
||||||
|
if (certificateBuffer.get() != BEGIN_CERT_HEADER[i]) {
|
||||||
|
certificateBuffer.reset();
|
||||||
|
return certificateBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StringBuilder pemEncoding = new StringBuilder();
|
||||||
|
while (certificateBuffer.hasRemaining()) {
|
||||||
|
char encodedChar = (char) certificateBuffer.get();
|
||||||
|
// if the current character is a '-' then the beginning of the footer has been reached
|
||||||
|
if (encodedChar == '-') {
|
||||||
|
break;
|
||||||
|
} else if (Character.isWhitespace(encodedChar)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
pemEncoding.append(encodedChar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// start from the second index in the certificate footer since the first '-' should have
|
||||||
|
// been consumed above.
|
||||||
|
for (int i = 1; i < END_CERT_FOOTER.length; i++) {
|
||||||
|
if (!certificateBuffer.hasRemaining()) {
|
||||||
|
throw new CertificateException(
|
||||||
|
"The provided input contains the PEM certificate header but does not "
|
||||||
|
+ "contain sufficient data for the footer");
|
||||||
|
}
|
||||||
|
if (certificateBuffer.get() != END_CERT_FOOTER[i]) {
|
||||||
|
throw new CertificateException(
|
||||||
|
"The provided input contains the PEM certificate header without a "
|
||||||
|
+ "valid certificate footer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] derEncoding = Base64.getDecoder().decode(pemEncoding.toString());
|
||||||
|
// consume any trailing whitespace in the byte buffer
|
||||||
|
int nextEncodedChar = certificateBuffer.position();
|
||||||
|
while (certificateBuffer.hasRemaining()) {
|
||||||
|
char trailingChar = (char) certificateBuffer.get();
|
||||||
|
if (Character.isWhitespace(trailingChar)) {
|
||||||
|
nextEncodedChar++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
certificateBuffer.position(nextEncodedChar);
|
||||||
|
return ByteBuffer.wrap(derEncoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig.internal.x509;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code AttributeTypeAndValue} as specified in RFC 5280.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class AttributeTypeAndValue {
|
||||||
|
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.OBJECT_IDENTIFIER)
|
||||||
|
public String attrType;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.ANY)
|
||||||
|
public Asn1OpaqueObject attrValue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.apksig.internal.x509;
|
||||||
|
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Class;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Field;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1OpaqueObject;
|
||||||
|
import com.android.apksig.internal.asn1.Asn1Type;
|
||||||
|
import com.android.apksig.internal.pkcs7.AlgorithmIdentifier;
|
||||||
|
import com.android.apksig.internal.pkcs7.IssuerAndSerialNumber;
|
||||||
|
import com.android.apksig.internal.pkcs7.SignerIdentifier;
|
||||||
|
import com.android.apksig.internal.util.ByteBufferUtils;
|
||||||
|
import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
|
||||||
|
import com.android.apksig.internal.util.X509CertificateUtils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.security.auth.x500.X500Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X509 {@code Certificate} as specified in RFC 5280.
|
||||||
|
*/
|
||||||
|
@Asn1Class(type = Asn1Type.SEQUENCE)
|
||||||
|
public class Certificate {
|
||||||
|
@Asn1Field(index = 0, type = Asn1Type.SEQUENCE)
|
||||||
|
public TBSCertificate certificate;
|
||||||
|
|
||||||
|
@Asn1Field(index = 1, type = Asn1Type.SEQUENCE)
|
||||||
|
public AlgorithmIdentifier signatureAlgorithm;
|
||||||
|
|
||||||
|
@Asn1Field(index = 2, type = Asn1Type.BIT_STRING)
|
||||||
|
public ByteBuffer signature;
|
||||||
|
|
||||||
|
public static X509Certificate findCertificate(
|
||||||
|
Collection<X509Certificate> certs, SignerIdentifier id) {
|
||||||
|
for (X509Certificate cert : certs) {
|
||||||
|
if (isMatchingCerticicate(cert, id)) {
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isMatchingCerticicate(X509Certificate cert, SignerIdentifier id) {
|
||||||
|
if (id.issuerAndSerialNumber == null) {
|
||||||
|
// Android doesn't support any other means of identifying the signing certificate
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IssuerAndSerialNumber issuerAndSerialNumber = id.issuerAndSerialNumber;
|
||||||
|
byte[] encodedIssuer =
|
||||||
|
ByteBufferUtils.toByteArray(issuerAndSerialNumber.issuer.getEncoded());
|
||||||
|
X500Principal idIssuer = new X500Principal(encodedIssuer);
|
||||||
|
BigInteger idSerialNumber = issuerAndSerialNumber.certificateSerialNumber;
|
||||||
|
return idSerialNumber.equals(cert.getSerialNumber())
|
||||||
|
&& idIssuer.equals(cert.getIssuerX500Principal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<X509Certificate> parseCertificates(
|
||||||
|
List<Asn1OpaqueObject> encodedCertificates) throws CertificateException {
|
||||||
|
if (encodedCertificates.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<X509Certificate> result = new ArrayList<>(encodedCertificates.size());
|
||||||
|
for (int i = 0; i < encodedCertificates.size(); i++) {
|
||||||
|
Asn1OpaqueObject encodedCertificate = encodedCertificates.get(i);
|
||||||
|
X509Certificate certificate;
|
||||||
|
byte[] encodedForm = ByteBufferUtils.toByteArray(encodedCertificate.getEncoded());
|
||||||
|
try {
|
||||||
|
certificate = X509CertificateUtils.generateCertificate(encodedForm);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new CertificateException("Failed to parse certificate #" + (i + 1), e);
|
||||||
|
}
|
||||||
|
// Wrap the cert so that the result's getEncoded returns exactly the original
|
||||||
|
// encoded form. Without this, getEncoded may return a different form from what was
|
||||||
|
// stored in the signature. This is because some X509Certificate(Factory)
|
||||||
|
// implementations re-encode certificates and/or some implementations of
|
||||||
|
// X509Certificate.getEncoded() re-encode certificates.
|
||||||
|
certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedForm);
|
||||||
|
result.add(certificate);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user