Merge "Rewrite LDLIBS and SHARED_LIBRARIES"
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
new file mode 100644
index 0000000..931c7b2
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/ApkVerifier.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core;
+
+import com.android.apksigner.core.apk.ApkUtils;
+import com.android.apksigner.core.internal.apk.v2.ContentDigestAlgorithm;
+import com.android.apksigner.core.internal.apk.v2.SignatureAlgorithm;
+import com.android.apksigner.core.internal.apk.v2.V2SchemeVerifier;
+import com.android.apksigner.core.util.DataSource;
+import com.android.apksigner.core.zip.ZipFormatException;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * APK signature verifier which mimics the behavior of the Android platform.
+ *
+ * <p>The verifier is designed to closely mimic the behavior of Android platforms. This is to enable
+ * the verifier to be used for checking whether an APK's signatures will verify on Android.
+ */
+public class ApkVerifier {
+
+ /**
+ * Verifies the APK's signatures and returns the result of verification. The APK can be
+ * considered verified iff the result's {@link Result#isVerified()} returns {@code true}.
+ * The verification result also includes errors, warnings, and information about signers.
+ *
+ * @param apk APK file contents
+ * @param minSdkVersion API Level of the oldest Android platform on which the APK's signatures
+ * may need to be verified
+ *
+ * @throws IOException if an I/O error is encountered while reading the APK
+ * @throws ZipFormatException if the APK is malformed at ZIP format level
+ */
+ public Result verify(DataSource apk, int minSdkVersion) throws IOException, ZipFormatException {
+ ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk);
+
+ // Attempt to verify the APK using APK Signature Scheme v2
+ Result result = new Result();
+ try {
+ V2SchemeVerifier.Result v2Result = V2SchemeVerifier.verify(apk, zipSections);
+ result.mergeFrom(v2Result);
+ } catch (V2SchemeVerifier.SignatureNotFoundException ignored) {}
+ if (result.containsErrors()) {
+ return result;
+ }
+
+ // TODO: Verify JAR signature if necessary
+ if (!result.isVerifiedUsingV2Scheme()) {
+ return result;
+ }
+
+ // Verified
+ result.setVerified();
+ for (Result.V2SchemeSignerInfo signerInfo : result.getV2SchemeSigners()) {
+ result.addSignerCertificate(signerInfo.getCertificate());
+ }
+
+ return result;
+ }
+
+ /**
+ * Result of verifying an APKs signatures. The APK can be considered verified iff
+ * {@link #isVerified()} returns {@code true}.
+ */
+ public static class Result {
+ private final List<IssueWithParams> mErrors = new ArrayList<>();
+ private final List<IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<X509Certificate> mSignerCerts = new ArrayList<>();
+ private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>();
+
+ private boolean mVerified;
+ private boolean mVerifiedUsingV2Scheme;
+
+ /**
+ * Returns {@code true} if the APK's signatures verified.
+ */
+ public boolean isVerified() {
+ return mVerified;
+ }
+
+ private void setVerified() {
+ mVerified = true;
+ }
+
+ /**
+ * Returns {@code true} if the APK's APK Signature Scheme v2 signatures verified.
+ */
+ public boolean isVerifiedUsingV2Scheme() {
+ return mVerifiedUsingV2Scheme;
+ }
+
+ /**
+ * Returns the verified signers' certificates, one per signer.
+ */
+ public List<X509Certificate> getSignerCertificates() {
+ return mSignerCerts;
+ }
+
+ private void addSignerCertificate(X509Certificate cert) {
+ mSignerCerts.add(cert);
+ }
+
+ /**
+ * Returns information about APK Signature Scheme v2 signers associated with the APK's
+ * signature.
+ */
+ public List<V2SchemeSignerInfo> getV2SchemeSigners() {
+ return mV2SchemeSigners;
+ }
+
+ /**
+ * Returns errors encountered while verifying the APK's signatures.
+ */
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ /**
+ * Returns warnings encountered while verifying the APK's signatures.
+ */
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ private void mergeFrom(V2SchemeVerifier.Result source) {
+ mVerifiedUsingV2Scheme = source.verified;
+ mErrors.addAll(source.getErrors());
+ mWarnings.addAll(source.getWarnings());
+ for (V2SchemeVerifier.Result.SignerInfo signer : source.signers) {
+ mV2SchemeSigners.add(new V2SchemeSignerInfo(signer));
+ }
+ }
+
+ /**
+ * Returns {@code true} if an error was encountered while verifying the APK. Any error
+ * prevents the APK from being considered verified.
+ */
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (!mV2SchemeSigners.isEmpty()) {
+ for (V2SchemeSignerInfo signer : mV2SchemeSigners) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Information about an APK Signature Scheme v2 signer associated with the APK's signature.
+ */
+ public static class V2SchemeSignerInfo {
+ private final int mIndex;
+ private final List<X509Certificate> mCerts;
+
+ private final List<IssueWithParams> mErrors;
+ private final List<IssueWithParams> mWarnings;
+
+ private V2SchemeSignerInfo(V2SchemeVerifier.Result.SignerInfo result) {
+ mIndex = result.index;
+ mCerts = result.certs;
+ mErrors = result.getErrors();
+ mWarnings = result.getWarnings();
+ }
+
+ /**
+ * Returns this signer's {@code 0}-based index in the list of signers contained in the
+ * APK's APK Signature Scheme v2 signature.
+ */
+ public int getIndex() {
+ return mIndex;
+ }
+
+ /**
+ * Returns this signer'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 signer's public key.
+ */
+ public X509Certificate getCertificate() {
+ return mCerts.isEmpty() ? null : mCerts.get(0);
+ }
+
+ /**
+ * Returns this signer's certificates. The first certificate is for the signer's public
+ * key. An empty list may be returned if an error was encountered during verification
+ * (see {@link #containsErrors()}).
+ */
+ public List<X509Certificate> getCertificates() {
+ return mCerts;
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+ }
+ }
+
+ /**
+ * Error or warning encountered while verifying an APK's signatures.
+ */
+ public static enum Issue {
+
+ /**
+ * Failed to parse the list of signers contained in the APK Signature Scheme v2 signature.
+ */
+ V2_SIG_MALFORMED_SIGNERS("Malformed list of signers"),
+
+ /**
+ * Failed to parse this signer's signer block contained in the APK Signature Scheme v2
+ * signature.
+ */
+ V2_SIG_MALFORMED_SIGNER("Malformed signer block"),
+
+ /**
+ * Public key embedded in the APK Signature Scheme v2 signature of this signer could not be
+ * parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_PUBLIC_KEY("Malformed public key: %1$s"),
+
+ /**
+ * This APK Signature Scheme v2 signer's certificate could not be parsed.
+ *
+ * <ul>
+ * <li>Parameter 1: index ({@code 0}-based) of the certificate in the signer's list of
+ * certificates ({@code Integer})</li>
+ * <li>Parameter 2: sequence number ({@code 1}-based) of the certificate in the signer's
+ * list of certificates ({@code Integer})</li>
+ * <li>Parameter 3: error details ({@code Throwable})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_CERTIFICATE("Malformed certificate #%2$d: %3$s"),
+
+ /**
+ * Failed to parse this signer's signature record contained in the APK Signature Scheme v2
+ * signature.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_SIGNATURE("Malformed APK Signature Scheme v2 signature record #%1$d"),
+
+ /**
+ * Failed to parse this signer's digest record contained in the APK Signature Scheme v2
+ * signature.
+ *
+ * <ul>
+ * <li>Parameter 1: record number (first record is {@code 1}) ({@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_DIGEST("Malformed APK Signature Scheme v2 digest record #%1$d"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains a malformed additional attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE("Malformed additional attribute #%1$d"),
+
+ /**
+ * APK Signature Scheme v2 signature contains no signers.
+ */
+ V2_SIG_NO_SIGNERS("No signers in APK Signature Scheme v2 signature"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains a signature produced using an unknown
+ * algorithm.
+ *
+ * <ul>
+ * <li>Parameter 1: algorithm ID ({@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_UNKNOWN_SIG_ALGORITHM("Unknown signature algorithm: %1$#x"),
+
+ /**
+ * This APK Signature Scheme v2 signer contains an unknown additional attribute.
+ *
+ * <ul>
+ * <li>Parameter 1: attribute ID ({@code Integer})</li>
+ * </ul>
+ */
+ V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE("Unknown additional attribute: ID %1$#x"),
+
+ /**
+ * An exception was encountered while verifying APK Signature Scheme v2 signature of this
+ * signer.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * <li>Parameter 2: exception ({@code Throwable})</li>
+ * </ul>
+ */
+ V2_SIG_VERIFY_EXCEPTION("Failed to verify %1$s signature: %2$s"),
+
+ /**
+ * APK Signature Scheme v2 signature over this signer's signed-data block did not verify.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithm ({@link SignatureAlgorithm})</li>
+ * </ul>
+ */
+ V2_SIG_DID_NOT_VERIFY("%1$s signature over signed-data did not verify"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers no signatures.
+ */
+ V2_SIG_NO_SIGNATURES("No signatures"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers signatures but none of them are supported.
+ */
+ V2_SIG_NO_SUPPORTED_SIGNATURES("No supported signatures"),
+
+ /**
+ * This APK Signature Scheme v2 signer offers no certificates.
+ */
+ V2_SIG_NO_CERTIFICATES("No certificates"),
+
+ /**
+ * This APK Signature Scheme v2 signer's public key listed in the signer's certificate does
+ * not match the public key listed in the signatures record.
+ *
+ * <ul>
+ * <li>Parameter 1: hex-encoded public key from certificate ({@code String})</li>
+ * <li>Parameter 2: hex-encoded public key from signatures record ({@code String})</li>
+ * </ul>
+ */
+ V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD(
+ "Public key mismatch between certificate and signature record: <%1$s> vs <%2$s>"),
+
+ /**
+ * This APK Signature Scheme v2 signer's signature algorithms listed in the signatures
+ * record do not match the signature algorithms listed in the signatures record.
+ *
+ * <ul>
+ * <li>Parameter 1: signature algorithms from signatures record ({@code List<Integer>})</li>
+ * <li>Parameter 2: signature algorithms from digests record ({@code List<Integer>})</li>
+ * </ul>
+ */
+ V2_SIG_SIG_ALG_MISMATCH_BETWEEN_SIGNATURES_AND_DIGESTS_RECORDS(
+ "Signature algorithms mismatch between signatures and digests records"
+ + ": %1$s vs %2$s"),
+
+ /**
+ * The APK's digest does not match the digest contained in the APK Signature Scheme v2
+ * signature.
+ *
+ * <ul>
+ * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})</li>
+ * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})</li>
+ * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})</li>
+ * </ul>
+ */
+ V2_SIG_APK_DIGEST_DID_NOT_VERIFY(
+ "APK integrity check failed. %1$s digest mismatch."
+ + " Expected: <%2$s>, actual: <%3$s>"),
+
+ /**
+ * APK Signing Block contains an unknown entry.
+ *
+ * <ul>
+ * <li>Parameter 1: entry ID ({@code Integer})</li>
+ * </ul>
+ */
+ APK_SIG_BLOCK_UNKNOWN_ENTRY_ID("APK Signing Block contains unknown entry: ID %1$#x");
+
+ private final String mFormat;
+
+ private Issue(String format) {
+ mFormat = format;
+ }
+
+ /**
+ * Returns the format string suitable for combining the parameters of this issue into a
+ * readable string. See {@link java.util.Formatter} for format.
+ */
+ private String getFormat() {
+ return mFormat;
+ }
+ }
+
+ /**
+ * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted
+ * form.
+ */
+ public static class IssueWithParams {
+ private final Issue mIssue;
+ private final Object[] mParams;
+
+ /**
+ * Constructs a new {@code IssueWithParams} of the specified type and with provided
+ * parameters.
+ */
+ public IssueWithParams(Issue issue, Object[] params) {
+ mIssue = issue;
+ mParams = params;
+ }
+
+ /**
+ * Returns the type of this issue.
+ */
+ public Issue getIssue() {
+ return mIssue;
+ }
+
+ /**
+ * Returns the parameters of this issue.
+ */
+ public Object[] getParams() {
+ return mParams.clone();
+ }
+
+ /**
+ * Returns a readable form of this issue.
+ */
+ @Override
+ public String toString() {
+ return String.format(mIssue.getFormat(), mParams);
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java b/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
index dae3c5e..612f4fd 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/DefaultApkSignerEngine.java
@@ -18,9 +18,9 @@
import com.android.apksigner.core.internal.apk.v1.DigestAlgorithm;
import com.android.apksigner.core.internal.apk.v1.V1SchemeSigner;
-import com.android.apksigner.core.internal.apk.v2.MessageDigestSink;
import com.android.apksigner.core.internal.apk.v2.V2SchemeSigner;
import com.android.apksigner.core.internal.util.ByteArrayOutputStreamSink;
+import com.android.apksigner.core.internal.util.MessageDigestSink;
import com.android.apksigner.core.internal.util.Pair;
import com.android.apksigner.core.util.DataSink;
import com.android.apksigner.core.util.DataSource;
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java
index 8b59b8e..9f4ccce 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v1/V1SchemeSigner.java
@@ -41,13 +41,20 @@
import java.util.jar.Manifest;
import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DEROutputStream;
+import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x9.X9ObjectIdentifiers;
import org.bouncycastle.cert.jcajce.JcaCertStore;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
+import org.bouncycastle.cms.CMSSignatureEncryptionAlgorithmFinder;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
-import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
+import org.bouncycastle.cms.DefaultCMSSignatureEncryptionAlgorithmFinder;
+import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
@@ -462,10 +469,11 @@
.build(signerConfig.privateKey);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
gen.addSignerInfoGenerator(
- new JcaSignerInfoGeneratorBuilder(
- new JcaDigestCalculatorProviderBuilder().build())
- .setDirectSignature(true)
- .build(signer, signerCert));
+ new SignerInfoGeneratorBuilder(
+ new JcaDigestCalculatorProviderBuilder().build(),
+ SignerInfoSignatureAlgorithmFinder.INSTANCE)
+ .setDirectSignature(true)
+ .build(signer, new JcaX509CertificateHolder(signerCert)));
gen.addCertificates(certs);
CMSSignedData sigData =
@@ -482,6 +490,37 @@
}
}
+ /**
+ * Chooser of SignatureAlgorithm for PKCS #7 CMS SignerInfo.
+ */
+ private static class SignerInfoSignatureAlgorithmFinder
+ implements CMSSignatureEncryptionAlgorithmFinder {
+ private static final SignerInfoSignatureAlgorithmFinder INSTANCE =
+ new SignerInfoSignatureAlgorithmFinder();
+
+ private static final AlgorithmIdentifier DSA =
+ new AlgorithmIdentifier(X9ObjectIdentifiers.id_dsa, DERNull.INSTANCE);
+
+ private final CMSSignatureEncryptionAlgorithmFinder mDefault =
+ new DefaultCMSSignatureEncryptionAlgorithmFinder();
+
+ @Override
+ public AlgorithmIdentifier findEncryptionAlgorithm(AlgorithmIdentifier id) {
+ // Use the default chooser, but replace dsaWithSha1 with dsa. This is because "dsa" is
+ // accepted by any Android platform whereas "dsaWithSha1" is accepted only since
+ // API Level 9.
+ id = mDefault.findEncryptionAlgorithm(id);
+ if (id != null) {
+ ASN1ObjectIdentifier oid = id.getAlgorithm();
+ if (X9ObjectIdentifiers.id_dsa_with_sha1.equals(oid)) {
+ return DSA;
+ }
+ }
+
+ return id;
+ }
+ }
+
private static String getEntryDigestAttributeName(DigestAlgorithm digestAlgorithm) {
switch (digestAlgorithm) {
case SHA1:
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
index cb0f84a..7c136be 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/ContentDigestAlgorithm.java
@@ -19,7 +19,7 @@
/**
* APK Signature Scheme v2 content digest algorithm.
*/
-enum ContentDigestAlgorithm {
+public enum ContentDigestAlgorithm {
/** SHA2-256 over 1 MB chunks. */
CHUNKED_SHA256("SHA-256", 256 / 8),
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
index 3c7b5f0..20f890d 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/SignatureAlgorithm.java
@@ -23,7 +23,7 @@
import java.security.spec.PSSParameterSpec;
/**
- * APK Signature Scheme v2 content digest algorithm.
+ * APK Signature Scheme v2 signature algorithm.
*/
public enum SignatureAlgorithm {
/**
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
index 103a0ec..aba390b 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeSigner.java
@@ -16,6 +16,7 @@
package com.android.apksigner.core.internal.apk.v2;
+import com.android.apksigner.core.internal.util.MessageDigestSink;
import com.android.apksigner.core.internal.util.Pair;
import com.android.apksigner.core.internal.zip.ZipUtils;
import com.android.apksigner.core.util.DataSource;
@@ -216,7 +217,7 @@
return generateApkSigningBlock(signerConfigs, contentDigests);
}
- private static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
+ static Map<ContentDigestAlgorithm, byte[]> computeContentDigests(
Set<ContentDigestAlgorithm> digestAlgorithms,
DataSource[] contents) throws IOException, DigestException {
// For each digest algorithm the result is computed as follows:
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
new file mode 100644
index 0000000..efefb00
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/V2SchemeVerifier.java
@@ -0,0 +1,939 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.apk.v2;
+
+import com.android.apksigner.core.ApkVerifier.Issue;
+import com.android.apksigner.core.ApkVerifier.IssueWithParams;
+import com.android.apksigner.core.apk.ApkUtils;
+import com.android.apksigner.core.internal.util.ByteBufferDataSource;
+import com.android.apksigner.core.internal.util.DelegatingX509Certificate;
+import com.android.apksigner.core.internal.util.Pair;
+import com.android.apksigner.core.internal.zip.ZipUtils;
+import com.android.apksigner.core.util.DataSource;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.DigestException;
+import java.security.KeyFactory;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.cert.CertificateEncodingException;
+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.Collections;
+import java.util.HashMap;
+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.
+ *
+ * <p>TODO: Link to APK Signature Scheme v2 documentation once it's available.
+ */
+public abstract class V2SchemeVerifier {
+
+ 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;
+
+ private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+
+ /** Hidden constructor to prevent instantiation. */
+ private V2SchemeVerifier() {}
+
+ /**
+ * Verifies the provided APK's APK Signature Scheme v2 signatures and returns the result of
+ * verification. APK is considered verified only if {@link Result#verified} is {@code true}. If
+ * verification fails, the result will contain errors -- see {@link Result#getErrors()}.
+ *
+ * @throws SignatureNotFoundException if no APK Signature Scheme v2 signatures are found
+ * @throws IOException if an I/O error occurs when reading the APK
+ */
+ public static Result verify(DataSource apk, ApkUtils.ZipSections zipSections)
+ throws IOException, SignatureNotFoundException {
+ Result result = new Result();
+ SignatureInfo signatureInfo = findSignature(apk, zipSections, result);
+
+ DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset);
+ DataSource centralDir =
+ apk.slice(
+ signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+ ByteBuffer eocd = signatureInfo.eocd;
+
+ verify(beforeApkSigningBlock,
+ signatureInfo.signatureBlock,
+ centralDir,
+ eocd,
+ 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}.
+ */
+ private static void verify(
+ DataSource beforeApkSigningBlock,
+ ByteBuffer apkSignatureSchemeV2Block,
+ DataSource centralDir,
+ ByteBuffer eocd,
+ Result result) throws IOException {
+ Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1);
+ parseSigners(apkSignatureSchemeV2Block, contentDigestsToVerify, result);
+ if (result.containsErrors()) {
+ return;
+ }
+ verifyIntegrity(
+ beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result);
+ if (!result.containsErrors()) {
+ result.verified = true;
+ }
+ }
+
+ /**
+ * Parses each signer in the provided APK Signature Scheme v2 block and populates
+ * {@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}).
+ */
+ private static void parseSigners(
+ ByteBuffer apkSignatureSchemeV2Block,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify,
+ Result result) {
+ ByteBuffer signers;
+ try {
+ signers = getLengthPrefixedSlice(apkSignatureSchemeV2Block);
+ } catch (IOException 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++;
+ Result.SignerInfo signerInfo = new Result.SignerInfo();
+ signerInfo.index = signerIndex;
+ result.signers.add(signerInfo);
+ try {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify);
+ } catch (IOException | BufferUnderflowException e) {
+ signerInfo.addError(Issue.V2_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. Rather, this method adds to the
+ * {@code contentDigestsToVerify}.
+ */
+ private static void parseSigner(
+ ByteBuffer signerBlock,
+ CertificateFactory certFactory,
+ Result.SignerInfo result,
+ Set<ContentDigestAlgorithm> contentDigestsToVerify) throws IOException {
+ ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
+ byte[] signedDataBytes = new byte[signedData.remaining()];
+ signedData.get(signedDataBytes);
+ signedData.flip();
+ result.signedData = signedDataBytes;
+
+ ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
+ byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
+
+ // Parse the signatures block and identify supported signatures
+ int signatureCount = 0;
+ List<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 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 SupportedSignature(signatureAlgorithm, sigBytes));
+ } catch (IOException | 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<SupportedSignature> signaturesToVerify = getSignaturesToVerify(supportedSignatures);
+ if (signaturesToVerify.isEmpty()) {
+ result.addError(Issue.V2_SIG_NO_SUPPORTED_SIGNATURES);
+ return;
+ }
+ for (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 (Exception 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 = getLengthPrefixedSlice(signedData);
+ ByteBuffer certificates = getLengthPrefixedSlice(signedData);
+ ByteBuffer additionalAttributes = getLengthPrefixedSlice(signedData);
+
+ // Parse the certificates block
+ int certificateIndex = -1;
+ while (certificates.hasRemaining()) {
+ certificateIndex++;
+ byte[] encodedCert = readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate =
+ (X509Certificate)
+ certFactory.generateCertificate(
+ new ByteArrayInputStream(encodedCert));
+ } 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 becase 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 = mainCertificate.getPublicKey().getEncoded();
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ result.addError(
+ Issue.V2_SIG_PUBLIC_KEY_MISMATCH_BETWEEN_CERTIFICATE_AND_SIGNATURES_RECORD,
+ toHex(certificatePublicKeyBytes),
+ 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 Result.SignerInfo.ContentDigest(sigAlgorithmId, digestBytes));
+ } catch (IOException | BufferUnderflowException e) {
+ result.addError(Issue.V2_SIG_MALFORMED_DIGEST, digestCount);
+ return;
+ }
+ }
+
+ List<Integer> sigAlgsFromSignaturesRecord = new ArrayList<>(result.signatures.size());
+ for (Result.SignerInfo.Signature signature : result.signatures) {
+ sigAlgsFromSignaturesRecord.add(signature.getAlgorithmId());
+ }
+ List<Integer> sigAlgsFromDigestsRecord = new ArrayList<>(result.contentDigests.size());
+ for (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;
+ while (additionalAttributes.hasRemaining()) {
+ additionalAttributeCount++;
+ try {
+ ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes);
+ int id = attribute.getInt();
+ byte[] value = readLengthPrefixedByteArray(attribute);
+ result.additionalAttributes.add(
+ new Result.SignerInfo.AdditionalAttribute(id, value));
+ result.addWarning(Issue.V2_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id);
+ } catch (IOException | BufferUnderflowException e) {
+ result.addError(
+ Issue.V2_SIG_MALFORMED_ADDITIONAL_ATTRIBUTE, additionalAttributeCount);
+ return;
+ }
+ }
+ }
+
+ private static List<SupportedSignature> getSignaturesToVerify(
+ List<SupportedSignature> signatures) {
+ // Pick the signature with the strongest algorithm, to mimic Android's behavior.
+ SignatureAlgorithm bestSigAlgorithm = null;
+ byte[] bestSigAlgorithmSignatureBytes = null;
+ for (SupportedSignature sig : signatures) {
+ SignatureAlgorithm sigAlgorithm = sig.algorithm;
+ if ((bestSigAlgorithm == null)
+ || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
+ bestSigAlgorithm = sigAlgorithm;
+ bestSigAlgorithmSignatureBytes = sig.signature;
+ }
+ }
+
+ if (bestSigAlgorithm == null) {
+ return Collections.emptyList();
+ } else {
+ return Collections.singletonList(
+ new SupportedSignature(bestSigAlgorithm, bestSigAlgorithmSignatureBytes));
+ }
+ }
+
+ private static class SupportedSignature {
+ private final SignatureAlgorithm algorithm;
+ private final byte[] signature;
+
+ private SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) {
+ this.algorithm = algorithm;
+ this.signature = signature;
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) {
+ ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm();
+ ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm();
+ return compareContentDigestAlgorithm(digestAlg1, digestAlg2);
+ }
+
+ /**
+ * 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.
+ */
+ private static int compareContentDigestAlgorithm(
+ ContentDigestAlgorithm alg1,
+ ContentDigestAlgorithm alg2) {
+ switch (alg1) {
+ case CHUNKED_SHA256:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 0;
+ case CHUNKED_SHA512:
+ return -1;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ case CHUNKED_SHA512:
+ switch (alg2) {
+ case CHUNKED_SHA256:
+ return 1;
+ case CHUNKED_SHA512:
+ return 0;
+ default:
+ throw new IllegalArgumentException("Unknown alg2: " + alg2);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown alg1: " + alg1);
+ }
+ }
+
+ /**
+ * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the
+ * APK and comparing them against the digests listed in APK Signing Block. The expected digests
+ * taken from {@code v2SchemeSignerInfos} of the provided {@code result}.
+ */
+ private static void verifyIntegrity(
+ DataSource beforeApkSigningBlock,
+ DataSource centralDir,
+ ByteBuffer eocd,
+ Set<ContentDigestAlgorithm> contentDigestAlgorithms,
+ Result result) throws IOException {
+ if (contentDigestAlgorithms.isEmpty()) {
+ // This should never occur because this method is invoked once at least one signature
+ // is verified, meaning at least one content digest is known.
+ throw new RuntimeException("No content digests found");
+ }
+
+ // For the purposes of verifying integrity, ZIP End of Central Directory (EoCD) must be
+ // treated as though its Central Directory offset points to the start of APK Signing Block.
+ // We thus modify the EoCD accordingly.
+ ByteBuffer modifiedEocd = ByteBuffer.allocate(eocd.remaining());
+ modifiedEocd.order(ByteOrder.LITTLE_ENDIAN);
+ modifiedEocd.put(eocd);
+ modifiedEocd.flip();
+ ZipUtils.setZipEocdCentralDirectoryOffset(modifiedEocd, beforeApkSigningBlock.size());
+ Map<ContentDigestAlgorithm, byte[]> actualContentDigests;
+ try {
+ actualContentDigests =
+ V2SchemeSigner.computeContentDigests(
+ contentDigestAlgorithms,
+ new DataSource[] {
+ beforeApkSigningBlock,
+ centralDir,
+ new ByteBufferDataSource(modifiedEocd)
+ });
+ } catch (DigestException e) {
+ throw new RuntimeException("Failed to compute content digests", e);
+ }
+ if (!contentDigestAlgorithms.equals(actualContentDigests.keySet())) {
+ throw new RuntimeException(
+ "Mismatch between sets of requested and computed content digests"
+ + " . Requested: " + contentDigestAlgorithms
+ + ", computed: " + actualContentDigests.keySet());
+ }
+
+ // Compare digests computed over the rest of APK against the corresponding expected digests
+ // in signer blocks.
+ for (Result.SignerInfo signerInfo : result.signers) {
+ for (Result.SignerInfo.ContentDigest expected : signerInfo.contentDigests) {
+ SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(expected.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ byte[] expectedDigest = expected.getValue();
+ byte[] actualDigest = actualContentDigests.get(contentDigestAlgorithm);
+ if (!Arrays.equals(expectedDigest, actualDigest)) {
+ signerInfo.addError(
+ Issue.V2_SIG_APK_DIGEST_DID_NOT_VERIFY,
+ contentDigestAlgorithm,
+ toHex(expectedDigest),
+ toHex(actualDigest));
+ continue;
+ }
+ signerInfo.verifiedContentDigests.put(contentDigestAlgorithm, actualDigest);
+ }
+ }
+ }
+
+ /**
+ * APK Signature Scheme v2 block and additional information relevant to verifying the signatures
+ * contained in the block against the file.
+ */
+ private static class SignatureInfo {
+ /** Contents of APK Signature Scheme v2 block. */
+ private final ByteBuffer signatureBlock;
+
+ /** Position of the APK Signing Block in the file. */
+ private final long apkSigningBlockOffset;
+
+ /** Position of the ZIP Central Directory in the file. */
+ private final long centralDirOffset;
+
+ /** Position of the ZIP End of Central Directory (EoCD) in the file. */
+ private final long eocdOffset;
+
+ /** Contents of ZIP End of Central Directory (EoCD) of the file. */
+ private final ByteBuffer eocd;
+
+ private 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;
+ }
+ }
+
+ /**
+ * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
+ * additional information relevant for verifying the block against the file.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2
+ * @throws IOException if an I/O error occurs while reading the APK
+ */
+ private static SignatureInfo findSignature(
+ DataSource apk, ApkUtils.ZipSections zipSections, Result result)
+ throws IOException, SignatureNotFoundException {
+ long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset();
+ long centralDirEndOffset =
+ centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes();
+ long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset();
+ if (centralDirEndOffset != eocdStartOffset) {
+ throw new SignatureNotFoundException(
+ "ZIP Central Directory is not immediately followed by End of Central Directory"
+ + ". CD end: " + centralDirEndOffset
+ + ", EoCD start: " + eocdStartOffset);
+ }
+
+ // Find the APK Signing Block. The block immediately precedes the Central Directory.
+ ByteBuffer eocd = zipSections.getZipEndOfCentralDirectory();
+ Pair<ByteBuffer, Long> apkSigningBlockAndOffset =
+ findApkSigningBlock(apk, centralDirStartOffset);
+ ByteBuffer apkSigningBlock = apkSigningBlockAndOffset.getFirst();
+ long apkSigningBlockOffset = apkSigningBlockAndOffset.getSecond();
+
+ // Find the APK Signature Scheme v2 Block inside the APK Signing Block.
+ ByteBuffer apkSignatureSchemeV2Block =
+ findApkSignatureSchemeV2Block(apkSigningBlock, result);
+
+ return new SignatureInfo(
+ apkSignatureSchemeV2Block,
+ apkSigningBlockOffset,
+ centralDirStartOffset,
+ eocdStartOffset,
+ eocd);
+ }
+
+ private static Pair<ByteBuffer, Long> findApkSigningBlock(
+ DataSource apk, long centralDirOffset) throws IOException, SignatureNotFoundException {
+ // FORMAT:
+ // 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
+
+ if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
+ throw new SignatureNotFoundException(
+ "APK too small for APK Signing Block. ZIP Central Directory offset: "
+ + centralDirOffset);
+ }
+ // 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(centralDirOffset - 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 SignatureNotFoundException(
+ "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 SignatureNotFoundException(
+ "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+ }
+ int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+ long apkSigBlockOffset = centralDirOffset - totalSize;
+ if (apkSigBlockOffset < 0) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block offset out of range: " + apkSigBlockOffset);
+ }
+ ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, totalSize);
+ apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+ long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+ if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block sizes in header and footer do not match: "
+ + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+ }
+ return Pair.of(apkSigBlock, apkSigBlockOffset);
+ }
+
+ private static ByteBuffer findApkSignatureSchemeV2Block(
+ ByteBuffer apkSigningBlock,
+ Result result) 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 == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
+ return getByteBuffer(pairs, len - 4);
+ }
+ result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
+ pairs.position(nextEntryPos);
+ }
+
+ throw new SignatureNotFoundException(
+ "No APK Signature Scheme v2 block in APK Signing Block");
+ }
+
+ private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ public static class SignatureNotFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SignatureNotFoundException(String message) {
+ super(message);
+ }
+
+ public SignatureNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+ /**
+ * 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)
+ throws BufferUnderflowException {
+ 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);
+ }
+ }
+
+ private static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException {
+ if (source.remaining() < 4) {
+ throw new IOException(
+ "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 IOException("Length-prefixed field longer than remaining buffer."
+ + " Field length: " + len + ", remaining: " + source.remaining());
+ }
+ return getByteBuffer(source, len);
+ }
+
+ private static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException {
+ int len = buf.getInt();
+ if (len < 0) {
+ throw new IOException("Negative length");
+ } else if (len > buf.remaining()) {
+ throw new IOException("Underflow while reading length-prefixed value. Length: " + len
+ + ", available: " + buf.remaining());
+ }
+ byte[] result = new byte[len];
+ buf.get(result);
+ return result;
+ }
+
+ /**
+ * {@link X509Certificate} whose {@link #getEncoded()} returns the data provided at construction
+ * time.
+ */
+ private static class GuaranteedEncodedFormX509Certificate extends DelegatingX509Certificate {
+ private byte[] mEncodedForm;
+
+ 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;
+ }
+ }
+
+ private static final char[] HEX_DIGITS = "01234567890abcdef".toCharArray();
+
+ private 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 class Result {
+
+ /** Whether the APK's APK Signature Scheme v2 signature verifies. */
+ public boolean verified;
+
+ public final List<SignerInfo> signers = new ArrayList<>();
+ private final List<IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+ public boolean containsErrors() {
+ if (!mErrors.isEmpty()) {
+ return true;
+ }
+ if (!signers.isEmpty()) {
+ for (SignerInfo signer : signers) {
+ if (signer.containsErrors()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+
+ public void addWarning(Issue msg, Object... parameters) {
+ mWarnings.add(new IssueWithParams(msg, parameters));
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public static class SignerInfo {
+ public int index;
+ public List<X509Certificate> certs = new ArrayList<>();
+ public List<ContentDigest> contentDigests = new ArrayList<>();
+ public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>();
+ public List<Signature> signatures = new ArrayList<>();
+ public Map<SignatureAlgorithm, byte[]> verifiedSignatures = new HashMap<>();
+ public List<AdditionalAttribute> additionalAttributes = new ArrayList<>();
+ public byte[] signedData;
+
+ private final List<IssueWithParams> mWarnings = new ArrayList<>();
+ private final List<IssueWithParams> mErrors = new ArrayList<>();
+
+ public void addError(Issue msg, Object... parameters) {
+ mErrors.add(new IssueWithParams(msg, parameters));
+ }
+
+ public void addWarning(Issue msg, Object... parameters) {
+ mWarnings.add(new IssueWithParams(msg, parameters));
+ }
+
+ public boolean containsErrors() {
+ return !mErrors.isEmpty();
+ }
+
+ public List<IssueWithParams> getErrors() {
+ return mErrors;
+ }
+
+ public List<IssueWithParams> getWarnings() {
+ return mWarnings;
+ }
+
+ public static class ContentDigest {
+ private final int mSignatureAlgorithmId;
+ private final byte[] mValue;
+
+ public ContentDigest(int signatureAlgorithmId, byte[] value) {
+ mSignatureAlgorithmId = signatureAlgorithmId;
+ mValue = value;
+ }
+
+ public int getSignatureAlgorithmId() {
+ return mSignatureAlgorithmId;
+ }
+
+ public byte[] getValue() {
+ return mValue;
+ }
+ }
+
+ public static class Signature {
+ private final int mAlgorithmId;
+ private final byte[] mValue;
+
+ public Signature(int algorithmId, byte[] value) {
+ mAlgorithmId = algorithmId;
+ mValue = value;
+ }
+
+ public int getAlgorithmId() {
+ return mAlgorithmId;
+ }
+
+ public byte[] getValue() {
+ return mValue;
+ }
+ }
+
+ public static class AdditionalAttribute {
+ private final int mId;
+ private final byte[] mValue;
+
+ public AdditionalAttribute(int id, byte[] value) {
+ mId = id;
+ mValue = value.clone();
+ }
+
+ public int getId() {
+ return mId;
+ }
+
+ public byte[] getValue() {
+ return mValue.clone();
+ }
+ }
+ }
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java
new file mode 100644
index 0000000..936cfa9
--- /dev/null
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/DelegatingX509Certificate.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.apksigner.core.internal.util;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+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.X509Certificate;
+import java.util.Date;
+import java.util.Set;
+
+/**
+ * {@link X509Certificate} which delegates all method invocations to the provided delegate
+ * {@code X509Certificate}.
+ */
+public class DelegatingX509Certificate extends X509Certificate {
+ 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();
+ }
+}
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java
similarity index 96%
rename from tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
rename to tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java
index 9ef04bf..45bb30e 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/apk/v2/MessageDigestSink.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/util/MessageDigestSink.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.apksigner.core.internal.apk.v2;
+package com.android.apksigner.core.internal.util;
import com.android.apksigner.core.util.DataSink;
diff --git a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
index 51110b6..5e724a2 100644
--- a/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
+++ b/tools/apksigner/core/src/com/android/apksigner/core/internal/zip/ZipUtils.java
@@ -20,7 +20,6 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import com.android.apksigner.core.internal.util.ByteBufferSink;
import com.android.apksigner.core.internal.util.Pair;
import com.android.apksigner.core.util.DataSource;
@@ -175,11 +174,10 @@
// Lower maxCommentSize if the file is too small.
maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
- ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
+ int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize;
+ long bufOffsetInFile = fileSize - maxEocdSize;
+ ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize);
buf.order(ByteOrder.LITTLE_ENDIAN);
- long bufOffsetInFile = fileSize - buf.capacity();
- zip.feed(bufOffsetInFile, buf.remaining(), new ByteBufferSink(buf));
- buf.flip();
int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
if (eocdOffsetInBuf == -1) {
// No EoCD record found in the buffer
@@ -252,10 +250,8 @@
return false;
}
- ByteBuffer sig = ByteBuffer.allocate(4);
+ ByteBuffer sig = zip.getByteBuffer(locatorPosition, 4);
sig.order(ByteOrder.LITTLE_ENDIAN);
- zip.feed(locatorPosition, sig.remaining(), new ByteBufferSink(sig));
- sig.flip();
return sig.getInt(0) == ZIP64_EOCD_LOCATOR_SIG;
}