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:
Fredia Huya-Kouadio 2024-08-26 11:12:50 -07:00
parent a5897d579b
commit 6a9c060883
124 changed files with 31172 additions and 0 deletions

View File

@ -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$|

View File

@ -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

View File

@ -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();
}
}

View File

@ -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

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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`._

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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";
}

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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.");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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});
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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,
}

View File

@ -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,
}

View File

@ -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
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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() {}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
+ ")");
}
}
}
}

View File

@ -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 +")");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 +")");
}
}
}

View File

@ -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;
}
}

View File

@ -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)");
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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