mirror of
https://github.com/godotengine/godot.git
synced 2024-10-22 21:21:53 +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:
parent
a5897d579b
commit
6a9c060883
|
@ -17,6 +17,7 @@ repos:
|
|||
exclude: |
|
||||
(?x)^(
|
||||
tests/python_build/.*|
|
||||
platform/android/java/editor/src/main/java/com/android/.*|
|
||||
platform/android/java/lib/src/com/.*
|
||||
)
|
||||
|
||||
|
@ -30,6 +31,7 @@ repos:
|
|||
exclude: |
|
||||
(?x)^(
|
||||
tests/python_build/.*|
|
||||
platform/android/java/editor/src/main/java/com/android/.*|
|
||||
platform/android/java/lib/src/com/.*
|
||||
)
|
||||
additional_dependencies: [clang-tidy==18.1.1]
|
||||
|
@ -54,6 +56,11 @@ repos:
|
|||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: codespell
|
||||
exclude: |
|
||||
(?x)^(
|
||||
platform/android/java/editor/src/main/java/com/android/.*|
|
||||
platform/android/java/lib/src/com/.*
|
||||
)
|
||||
additional_dependencies: [tomli]
|
||||
|
||||
### Requires Docker; look into alternative implementation.
|
||||
|
@ -135,6 +142,7 @@ repos:
|
|||
(?x)^(
|
||||
core/math/bvh_.*\.inc$|
|
||||
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/org/godotengine/godot/gl/GLSurfaceView\.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.core:core-splashscreen:$versions.splashscreenVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
implementation "org.bouncycastle:bcprov-jdk15to18:1.77"
|
||||
}
|
||||
|
||||
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
Loading…
Reference in New Issue
Block a user