[automerger skipped] Generate SourceStamp file before JAR signature am: abcdbd2a0b am: ef5579fcbc -s ours
am skip reason: Change-Id I6af5c1a7805ef216416581d2811554adfacee884 with SHA-1 abcdbd2a0b is in history
Change-Id: Ie097ff70b2884119ed3bc6a65c14de6d2f149cb3
diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java
index 9ccd267..556c0eb 100644
--- a/src/main/java/com/android/apksig/ApkSigner.java
+++ b/src/main/java/com/android/apksig/ApkSigner.java
@@ -16,6 +16,8 @@
package com.android.apksig;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkSigningBlockNotFoundException;
import com.android.apksig.apk.ApkUtils;
@@ -39,7 +41,6 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
-import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SignatureException;
@@ -83,9 +84,6 @@
/** Name of the Android manifest ZIP entry in APKs. */
private 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 = "stamp-cert-sha256";
-
private final List<SignerConfig> mSignerConfigs;
private final SignerConfig mSourceStampSignerConfig;
private final Integer mMinSdkVersion;
@@ -463,12 +461,7 @@
// more Local File Header + data entries and add to the list of output Central Directory
// records.
if (signerEngine.isEligibleForSourceStamp()) {
- if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
- throw new SignatureException("No certificates configured for stamp");
- }
- byte[] uncompressedData =
- computeSha256DigestBytes(
- mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+ byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest();
outputOffset +=
outputDataToOutputApk(
SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME,
@@ -915,17 +908,6 @@
return ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest);
}
- private 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();
- }
-
/**
* Configuration of a signer.
*
diff --git a/src/main/java/com/android/apksig/ApkSignerEngine.java b/src/main/java/com/android/apksig/ApkSignerEngine.java
index 6d768d5..49a136b 100644
--- a/src/main/java/com/android/apksig/ApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/ApkSignerEngine.java
@@ -331,6 +331,11 @@
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.
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index 2671bcf..3d98a38 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -16,18 +16,22 @@
package com.android.apksig;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.AndroidBinXmlParser;
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.stamp.SourceStampVerifier;
import com.android.apksig.internal.apk.v1.V1SchemeVerifier;
import com.android.apksig.internal.apk.v2.V2SchemeVerifier;
import com.android.apksig.internal.apk.v3.V3SchemeVerifier;
import com.android.apksig.internal.apk.v4.V4SchemeVerifier;
import com.android.apksig.internal.util.AndroidSdkVersion;
import com.android.apksig.internal.zip.CentralDirectoryRecord;
+import com.android.apksig.internal.zip.LocalFileRecord;
import com.android.apksig.util.DataSource;
import com.android.apksig.util.DataSources;
import com.android.apksig.util.RunnablesExecutor;
@@ -191,6 +195,7 @@
}
Result result = new Result();
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
// The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme
// name, but the verifiers use this parameter as the schemes supported by the target SDK
@@ -230,6 +235,10 @@
maxSdkVersion);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3);
result.mergeFrom(v3Result);
+ if (apkContentDigests.isEmpty()) {
+ apkContentDigests.putAll(
+ getApkContentDigestsFromSigningSchemeResult(v3Result));
+ }
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
// v3 signature not required
}
@@ -267,6 +276,10 @@
maxSdkVersion);
foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2);
result.mergeFrom(v2Result);
+ if (apkContentDigests.isEmpty()) {
+ apkContentDigests.putAll(
+ getApkContentDigestsFromSigningSchemeResult(v2Result));
+ }
} catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
// v2 signature not required
}
@@ -274,6 +287,46 @@
return result;
}
}
+
+ if (maxSdkVersion >= AndroidSdkVersion.R) {
+ try {
+ List<CentralDirectoryRecord> cdRecords =
+ V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections);
+ CentralDirectoryRecord sourceStampCdRecord =
+ cdRecords.stream()
+ .filter(
+ cdRecord ->
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME
+ .equals(cdRecord.getName()))
+ .findAny()
+ .orElse(null);
+ // If SourceStamp file is found inside the APK, there must be a SourceStamp
+ // block in the APK signing block as well.
+ if (sourceStampCdRecord != null) {
+ byte[] sourceStampCertificateDigest =
+ LocalFileRecord.getUncompressedData(
+ apk,
+ sourceStampCdRecord,
+ zipSections.getZipCentralDirectoryOffset());
+ ApkSigningBlockUtils.Result sourceStampResult =
+ SourceStampVerifier.verify(
+ apk,
+ zipSections,
+ sourceStampCertificateDigest,
+ apkContentDigests,
+ Math.max(minSdkVersion, AndroidSdkVersion.R),
+ maxSdkVersion);
+ result.mergeFrom(sourceStampResult);
+ }
+ } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) {
+ result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING);
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ if (result.containsErrors()) {
+ return result;
+ }
+ }
}
// Android O and newer requires that APKs targeting security sandbox version 2 and higher
@@ -477,6 +530,25 @@
return result;
}
+ private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestsFromSigningSchemeResult(
+ ApkSigningBlockUtils.Result apkSigningSchemeResult) {
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>();
+ for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : apkSigningSchemeResult.signers) {
+ for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest :
+ signerInfo.contentDigests) {
+ SignatureAlgorithm signatureAlgorithm =
+ SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId());
+ if (signatureAlgorithm == null) {
+ continue;
+ }
+ ContentDigestAlgorithm contentDigestAlgorithm =
+ signatureAlgorithm.getContentDigestAlgorithm();
+ apkContentDigests.put(contentDigestAlgorithm, contentDigest.getValue());
+ }
+ }
+ return apkContentDigests;
+ }
+
private static ByteBuffer getAndroidManifestFromApk(
DataSource apk, ApkUtils.ZipSections zipSections)
throws IOException, ApkFormatException {
@@ -698,6 +770,10 @@
mErrors.add(new IssueWithParams(msg, parameters));
}
+ void addWarning(Issue msg, Object... parameters) {
+ mWarnings.add(new IssueWithParams(msg, parameters));
+ }
+
/**
* Returns errors encountered while verifying the APK's signatures.
*/
@@ -750,6 +826,7 @@
if (!source.signers.isEmpty()) {
mSourceStampInfo = new SourceStampInfo(source.signers.get(0));
}
+ break;
default:
throw new IllegalArgumentException("Unknown Signing Block Scheme Id");
}
@@ -2182,9 +2259,6 @@
/** SourceStamp offers an unsupported signature. */
SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"),
- /** SourceStamp offers no certificates. */
- SOURCE_STAMP_NO_CERTIFICATE("No certificates"),
-
/**
* SourceStamp's certificate listed in the APK signing block does not match the certificate
* listed in the SourceStamp file in the APK.
@@ -2198,21 +2272,7 @@
*/
SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK(
"Certificate mismatch between SourceStamp block in APK signing block and"
- + " SourceStamp file in APK: <%1$s> vs <%2$s>"),
-
- /**
- * The APK's digest in APK signing block does not match the digest contained in the
- * SourceStamp signature.
- *
- * <ul>
- * <li>Parameter 1: content digest algorithm ({@link ContentDigestAlgorithm})
- * <li>Parameter 2: hex-encoded expected digest of the APK ({@code String})
- * <li>Parameter 3: hex-encoded actual digest of the APK ({@code String})
- * </ul>
- */
- SOURCE_STAMP_APK_DIGEST_DID_NOT_VERIFY(
- "APK integrity check failed. %1$s digest mismatch."
- + " Expected: <%2$s>, actual: <%3$s>");
+ + " SourceStamp file in APK: <%1$s> vs <%2$s>");
private final String mFormat;
diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
index 54da524..bbd6933 100644
--- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
+++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java
@@ -16,6 +16,8 @@
package com.android.apksig;
+import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtils;
import com.android.apksig.internal.apk.ApkSigningBlockUtils;
@@ -47,6 +49,7 @@
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@@ -197,8 +200,8 @@
if (subLineage.size() != 1) {
throw new IllegalArgumentException(
"v1 signing enabled but the oldest signer in the"
- + " SigningCertificateLineage is missing. Please provide the"
- + " oldest signer to enable v1 signing");
+ + " SigningCertificateLineage is missing. Please provide the"
+ + " oldest signer to enable v1 signing");
}
}
createV1SignerConfigs(Collections.singletonList(oldestConfig), minSdkVersion);
@@ -248,7 +251,7 @@
v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
} else {
if (DigestAlgorithm.BY_STRENGTH_COMPARATOR.compare(
- v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
+ v1SignatureDigestAlgorithm, v1ContentDigestAlgorithm)
> 0) {
v1ContentDigestAlgorithm = v1SignatureDigestAlgorithm;
}
@@ -484,10 +487,10 @@
Optional<V1SchemeVerifier.NamedDigest> extractedDigest =
V1SchemeVerifier.getDigestsToVerify(
- entry.getValue(),
- "-Digest",
- mMinSdkVersion,
- Integer.MAX_VALUE)
+ entry.getValue(),
+ "-Digest",
+ mMinSdkVersion,
+ Integer.MAX_VALUE)
.stream()
.filter(d -> d.jcaDigestAlgorithm == alg)
.findFirst();
@@ -655,7 +658,7 @@
@Override
public OutputJarSignatureRequest outputJarEntries()
throws ApkFormatException, InvalidKeyException, SignatureException,
- NoSuchAlgorithmException {
+ NoSuchAlgorithmException {
checkNotClosed();
if (!mV1SignaturePending) {
@@ -677,6 +680,14 @@
}
mOutputJarEntryDigests.put(entryName, digestRequest.getDigest());
}
+ if (isEligibleForSourceStamp()) {
+ MessageDigest messageDigest =
+ MessageDigest.getInstance(
+ V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm));
+ messageDigest.update(generateSourceStampCertificateDigest());
+ mOutputJarEntryDigests.put(
+ SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, messageDigest.digest());
+ }
mOutputJarEntryDigestRequests.clear();
for (GetJarEntryDataRequest dataRequest : mOutputSignatureJarEntryDataRequests.values()) {
@@ -697,6 +708,14 @@
(mInputJarManifestEntryDataRequest != null)
? mInputJarManifestEntryDataRequest.getData()
: null;
+ if (isEligibleForSourceStamp()) {
+ inputJarManifest =
+ V1SchemeSigner.generateManifestFile(
+ mV1ContentDigestAlgorithm,
+ mOutputJarEntryDigests,
+ inputJarManifest)
+ .contents;
+ }
// Check whether the most recently used signature (if present) is still fine.
checkOutputApkNotDebuggableIfDebuggableMustBeRejected();
@@ -893,6 +912,19 @@
}
@Override
+ public byte[] generateSourceStampCertificateDigest() throws SignatureException {
+ if (mSourceStampSignerConfig.getCertificates().isEmpty()) {
+ throw new SignatureException("No certificates configured for stamp");
+ }
+ try {
+ return computeSha256DigestBytes(
+ mSourceStampSignerConfig.getCertificates().get(0).getEncoded());
+ } catch (CertificateEncodingException e) {
+ throw new SignatureException("Failed to encode source stamp certificate", e);
+ }
+ }
+
+ @Override
public void close() {
mClosed = true;
@@ -1051,6 +1083,17 @@
return InputJarEntryInstructions.OutputPolicy.SKIP;
}
+ private 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();
+ }
+
private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest {
private final List<JarEntry> mAdditionalJarEntries;
private volatile boolean mDone;
diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java
index 135d815..870971f 100644
--- a/src/main/java/com/android/apksig/apk/ApkUtils.java
+++ b/src/main/java/com/android/apksig/apk/ApkUtils.java
@@ -41,6 +41,9 @@
*/
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 = "stamp-cert-sha256";
+
private ApkUtils() {}
/**
diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
index 0662ada..143ee3f 100644
--- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
+++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java
@@ -1368,6 +1368,20 @@
return false;
}
+ public boolean containsWarnings() {
+ if (!mWarnings.isEmpty()) {
+ return true;
+ }
+ if (!signers.isEmpty()) {
+ for (SignerInfo signer : signers) {
+ if (signer.containsWarnings()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
public void addError(ApkVerifier.Issue msg, Object... parameters) {
mErrors.add(new ApkVerifier.IssueWithParams(msg, parameters));
}
@@ -1412,6 +1426,10 @@
return !mErrors.isEmpty();
}
+ public boolean containsWarnings() {
+ return !mWarnings.isEmpty();
+ }
+
public List<ApkVerifier.IssueWithParams> getErrors() {
return mErrors;
}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java
index 894d6f1..0749ef8 100644
--- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java
@@ -29,6 +29,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -59,6 +60,7 @@
List<Pair<Integer, byte[]>> digests =
digestInfo.entrySet().stream()
+ .sorted(Comparator.comparing(e -> e.getKey().getId()))
.map(e -> Pair.of(e.getKey().getId(), e.getValue()))
.collect(Collectors.toList());
@@ -71,12 +73,12 @@
throw new SignatureException(
"Retrieving the encoded form of the stamp certificate failed", e);
}
- // TODO: Sort digests
- sourceStampBlock.digests =
+
+ byte[] digestBytes =
encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
sourceStampBlock.signedDigests =
ApkSigningBlockUtils.generateSignaturesOverData(
- sourceStampSignerConfig, sourceStampBlock.digests);
+ sourceStampSignerConfig, digestBytes);
// FORMAT:
// * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded)
@@ -99,7 +101,6 @@
private static final class SourceStampBlock {
public byte[] stampCertificate;
- public byte[] digests;
public List<Pair<Integer, byte[]>> signedDigests;
}
}
diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
new file mode 100644
index 0000000..68d225c
--- /dev/null
+++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java
@@ -0,0 +1,277 @@
+/*
+ * 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.SourceStampSigner.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.SignatureAlgorithm;
+import com.android.apksig.internal.apk.SignatureInfo;
+import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate;
+import com.android.apksig.internal.util.Pair;
+import com.android.apksig.internal.util.X509CertificateUtils;
+import com.android.apksig.util.DataSource;
+
+import java.io.IOException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+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.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 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.
+ */
+public abstract class SourceStampVerifier {
+
+ /** Hidden constructor to prevent instantiation. */
+ private SourceStampVerifier() {}
+
+ /**
+ * 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, 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);
+ parseSourceStamp(
+ sourceStampBlockData,
+ certFactory,
+ signerInfo,
+ apkContentDigests,
+ sourceStampCertificateDigest,
+ minSdkVersion,
+ maxSdkVersion);
+ result.verified = !result.containsErrors() && !result.containsWarnings();
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+ }
+
+ /**
+ * Parses the SourceStamp block and populates the {@code result}.
+ *
+ * <p>This verifies signatures over digests contained in the APK signing block.
+ *
+ * <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 parseSourceStamp(
+ ByteBuffer sourceStampBlockData,
+ CertificateFactory certFactory,
+ ApkSigningBlockUtils.Result.SignerInfo result,
+ Map<ContentDigestAlgorithm, byte[]> apkContentDigests,
+ byte[] sourceStampCertificateDigest,
+ int minSdkVersion,
+ int maxSdkVersion)
+ throws ApkFormatException, NoSuchAlgorithmException {
+ List<Pair<Integer, byte[]>> digests =
+ apkContentDigests.entrySet().stream()
+ .sorted(Comparator.comparing(e -> e.getKey().getId()))
+ .map(e -> Pair.of(e.getKey().getId(), e.getValue()))
+ .collect(Collectors.toList());
+ byte[] digestBytes =
+ encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests);
+
+ // Parse the SourceStamp certificate.
+ byte[] sourceStampEncodedCertificate =
+ ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
+ X509Certificate sourceStampCertificate;
+ try {
+ sourceStampCertificate =
+ X509CertificateUtils.generateCertificate(
+ sourceStampEncodedCertificate, certFactory);
+ } catch (CertificateException e) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, 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.
+ 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(
+ ApkVerifier.Issue
+ .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK,
+ ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest),
+ ApkSigningBlockUtils.toHex(sourceStampCertificateDigest));
+ return;
+ }
+
+ // Parse the signatures block and identify supported signatures
+ ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+ 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(
+ ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId);
+ continue;
+ }
+ supportedSignatures.add(
+ new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes));
+ } catch (ApkFormatException | BufferUnderflowException e) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount);
+ return;
+ }
+ }
+ if (result.signatures.isEmpty()) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE);
+ return;
+ }
+
+ // Verify signatures over digests using the SourceStamp's certificate.
+ List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify;
+ try {
+ signaturesToVerify =
+ ApkSigningBlockUtils.getSignaturesToVerify(
+ supportedSignatures, minSdkVersion, maxSdkVersion);
+ } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) {
+ result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE);
+ return;
+ }
+ for (ApkSigningBlockUtils.SupportedSignature 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(digestBytes);
+ byte[] sigBytes = signature.signature;
+ if (!sig.verify(sigBytes)) {
+ result.addWarning(
+ ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm);
+ return;
+ }
+ result.verifiedSignatures.put(signatureAlgorithm, sigBytes);
+ } catch (InvalidKeyException
+ | InvalidAlgorithmParameterException
+ | SignatureException e) {
+ result.addWarning(
+ ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e);
+ return;
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index fba292c..3b77b26 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -16,7 +16,6 @@
package com.android.apksig;
-import static com.android.apksig.ApkSigner.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
import static com.android.apksig.apk.ApkUtils.findZipSections;
import static org.junit.Assert.assertArrayEquals;
@@ -969,7 +968,7 @@
cdRecords.stream()
.filter(
cdRecord ->
- SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
+ ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(
cdRecord.getName()))
.findAny()
.orElse(null);
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 351d0a8..9369333 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -17,6 +17,7 @@
package com.android.apksig;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNoException;
@@ -27,6 +28,12 @@
import com.android.apksig.internal.util.HexEncoding;
import com.android.apksig.internal.util.Resources;
import com.android.apksig.util.DataSources;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
@@ -37,12 +44,12 @@
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.util.Collection;
import java.util.List;
import java.util.Locale;
-import org.junit.Assume;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
@RunWith(JUnit4.class)
public class ApkVerifierTest {
@@ -52,8 +59,9 @@
private static final String[] DSA_KEY_NAMES_2048_AND_LARGER = {"2048", "3072"};
private static final String[] EC_KEY_NAMES = {"p256", "p384", "p521"};
private static final String[] RSA_KEY_NAMES = {"1024", "2048", "3072", "4096", "8192", "16384"};
- private static final String[] RSA_KEY_NAMES_2048_AND_LARGER =
- {"2048", "3072", "4096", "8192", "16384"};
+ private static final String[] RSA_KEY_NAMES_2048_AND_LARGER = {
+ "2048", "3072", "4096", "8192", "16384"
+ };
@Test
public void testOriginalAccepted() throws Exception {
@@ -121,46 +129,36 @@
@Test
public void testV1OneSignerSHA1withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha1-1.2.840.10045.4.1-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA224withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha224-1.2.840.10045.4.3.1-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA256withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha256-1.2.840.10045.4.3.2-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA384withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha384-1.2.840.10045.4.3.3-%s.apk", EC_KEY_NAMES);
}
@Test
public void testV1OneSignerSHA512withECDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
- assertVerifiedForEach(
- "v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.2.1-%s.apk", EC_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-ecdsa-sha512-1.2.840.10045.4.3.4-%s.apk", EC_KEY_NAMES);
}
@Test
@@ -233,8 +231,7 @@
@Test
public void testV1OneSignerSHA256withDSAAccepted() throws Exception {
// APK signed with v1 scheme only, one signer
- assertVerifiedForEach(
- "v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
+ assertVerifiedForEach("v1-only-with-dsa-sha256-1.2.840.10040.4.1-%s.apk", DSA_KEY_NAMES);
assertVerifiedForEach(
"v1-only-with-dsa-sha256-2.16.840.1.101.3.4.3.2-%s.apk", DSA_KEY_NAMES);
}
@@ -246,8 +243,7 @@
// This should fail because the v1 signature indicates that the APK was supposed to be
// signed with v2 scheme as well, making the platform's anti-stripping protections reject
// the APK.
- assertVerificationFailure(
- "v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
+ assertVerificationFailure("v2-stripped.apk", Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED);
// Similar to above, but the X-Android-APK-Signed anti-stripping header in v1 signature
// lists unknown signature schemes in addition to APK Signature Scheme v2. Unknown schemes
@@ -262,8 +258,7 @@
// APK signed with v2 and v3 schemes, but v3 signature was stripped from the file by
// modifying the v3 block ID to be the verity padding block ID. Without the stripping
// protection this modification ignores the v3 signing scheme block.
- assertVerificationFailure(
- "v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
+ assertVerificationFailure("v3-stripped.apk", Issue.V2_SIG_MISSING_APK_SIG_REFERENCED);
}
@Test
@@ -271,10 +266,12 @@
// The V2 signature scheme was introduced in N, and V3 was introduced in P. This test
// verifies a max SDK of pre-P ignores the V3 signature and a max SDK of pre-N ignores both
// the V2 and V3 signatures.
- assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
- AndroidSdkVersion.O));
- assertVerified(verifyForMaxSdkVersion("v1v2v3-with-rsa-2048-lineage-3-signers.apk",
- AndroidSdkVersion.M));
+ assertVerified(
+ verifyForMaxSdkVersion(
+ "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.O));
+ assertVerified(
+ verifyForMaxSdkVersion(
+ "v1v2v3-with-rsa-2048-lineage-3-signers.apk", AndroidSdkVersion.M));
}
@Test
@@ -390,13 +387,11 @@
// Based on v2-only-with-rsa-pkcs1-sha512-4096.apk. Obtained by modifying APK signer to
// flip the leftmost bit in content digest before signing signed-data.
- assertVerificationFailure(
- "v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
+ assertVerificationFailure("v2-only-with-rsa-pkcs1-sha512-4096-digest-mismatch.apk", error);
// Based on v2-only-with-ecdsa-sha256-p256.apk. Obtained by modifying APK signer to flip the
// leftmost bit in content digest before signing signed-data.
- assertVerificationFailure(
- "v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
+ assertVerificationFailure("v2-only-with-ecdsa-sha256-p256-digest-mismatch.apk", error);
}
@Test
@@ -425,21 +420,18 @@
// Obtained from v2-only-with-rsa-pkcs1-sha512-4096.apk by flipping a bit in the magic
// field in the footer of APK Signing Block. This makes the APK Signing Block disappear.
assertVerificationFailure(
- "v2-only-wrong-apk-sig-block-magic.apk",
- Issue.JAR_SIG_NO_MANIFEST);
+ "v2-only-wrong-apk-sig-block-magic.apk", Issue.JAR_SIG_NO_MANIFEST);
// Obtained by modifying APK signer to insert "GARBAGE" between ZIP Central Directory and
// End of Central Directory. The APK is otherwise fine and is signed with APK Signature
// Scheme v2. Based on v2-only-with-rsa-pkcs1-sha256.apk.
assertVerificationFailure(
- "v2-only-garbage-between-cd-and-eocd.apk",
- Issue.JAR_SIG_NO_MANIFEST);
+ "v2-only-garbage-between-cd-and-eocd.apk", Issue.JAR_SIG_NO_MANIFEST);
// Obtained by modifying the size in APK Signature Block header. Based on
// v2-only-with-ecdsa-sha512-p521.apk.
assertVerificationFailure(
- "v2-only-apk-sig-block-size-mismatch.apk",
- Issue.JAR_SIG_NO_MANIFEST);
+ "v2-only-apk-sig-block-size-mismatch.apk", Issue.JAR_SIG_NO_MANIFEST);
// Obtained by modifying the ID under which APK Signature Scheme v2 Block is stored in
// APK Signing Block and by modifying the APK signer to not insert anti-stripping
@@ -539,7 +531,8 @@
// APK's v2 and v3 signatures contain unknown additional attributes before and after the
// anti-stripping and lineage attributes.
assertVerified(
- verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P)); }
+ verifyForMinSdkVersion("v2v3-unknown-additional-attr.apk", AndroidSdkVersion.P));
+ }
@Test
public void testV2MismatchBetweenSignaturesAndDigestsBlockRejected() throws Exception {
@@ -585,16 +578,14 @@
public void testV2SignerBlockWithNoCertificatesRejected() throws Exception {
// APK is signed with v2 only. There are no certificates listed in the signer block.
// Obtained by modifying APK signer to output no certificates.
- assertVerificationFailure(
- "v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
+ assertVerificationFailure("v2-only-no-certs-in-sig.apk", Issue.V2_SIG_NO_CERTIFICATES);
}
@Test
public void testV3SignerBlockWithNoCertificatesRejected() throws Exception {
// APK is signed with v3 only. There are no certificates listed in the signer block.
// Obtained by modifying APK signer to output no certificates.
- assertVerificationFailure(
- "v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
+ assertVerificationFailure("v3-only-no-certs-in-sig.apk", Issue.V3_SIG_NO_CERTIFICATES);
}
@Test
@@ -739,25 +730,29 @@
try {
verifyForMinSdkVersion("empty-unsigned.apk", 1);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
// JAR-signed empty ZIP archive
try {
verifyForMinSdkVersion("v1-only-empty.apk", 18);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
// APK Signature Scheme v2 signed empty ZIP archive
try {
verifyForMinSdkVersion("v2-only-empty.apk", AndroidSdkVersion.N);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
// APK Signature Scheme v3 signed empty ZIP archive
try {
verifyForMinSdkVersion("v3-only-empty.apk", AndroidSdkVersion.P);
fail("ApkFormatException should've been thrown");
- } catch (ApkFormatException expected) {}
+ } catch (ApkFormatException expected) {
+ }
}
@Test
@@ -954,8 +949,7 @@
verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
Issue.JAR_SIG_VERIFY_EXCEPTION);
assertVerificationFailure(
- verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
- Issue.JAR_SIG_VERIFY_EXCEPTION);
+ verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_VERIFY_EXCEPTION);
// Assert that this issue fails verification of the entire signature block, rather than
// skipping the broken SignerInfo. The second signer info SignerInfo verifies fine, but
// verification does not get there.
@@ -964,8 +958,7 @@
verifyForMaxSdkVersion(apk, AndroidSdkVersion.N - 1),
Issue.JAR_SIG_VERIFY_EXCEPTION);
assertVerificationFailure(
- verifyForMinSdkVersion(apk, AndroidSdkVersion.N),
- Issue.JAR_SIG_VERIFY_EXCEPTION);
+ verifyForMinSdkVersion(apk, AndroidSdkVersion.N), Issue.JAR_SIG_VERIFY_EXCEPTION);
}
@Test
@@ -1020,6 +1013,50 @@
assertVerified(verifyForMinSdkVersion(apk, AndroidSdkVersion.N));
}
+ @Test
+ public void testSourceStampBlock_correctSignature() throws Exception {
+ ApkVerifier.Result verificationResult = verify("valid-stamp.apk");
+ // Verifies the signature of the APK.
+ assertVerified(verificationResult);
+ // Verifies the signature of source stamp.
+ assertTrue(verificationResult.isSourceStampVerified());
+ }
+
+ @Test
+ public void testSourceStampBlock_signatureMissing() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-without-block.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_SIG_MISSING);
+ }
+
+ @Test
+ public void testSourceStampBlock_certificateMismatch() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-certificate-mismatch.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(
+ verificationResult,
+ Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK);
+ }
+
+ @Test
+ public void testSourceStampBlock_apkHashMismatch() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY);
+ }
+
+ @Test
+ public void testSourceStampBlock_malformedSignature() throws Exception {
+ ApkVerifier.Result verificationResult = verify("stamp-malformed-signature.apk");
+ // A broken stamp should not block a signing scheme verified APK.
+ assertVerified(verificationResult);
+ assertSourceStampVerificationFailure(
+ verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE);
+ }
+
private ApkVerifier.Result verify(String apkFilenameInResources)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, null);
@@ -1027,13 +1064,13 @@
private ApkVerifier.Result verifyForMinSdkVersion(
String apkFilenameInResources, int minSdkVersion)
- throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, minSdkVersion, null);
}
private ApkVerifier.Result verifyForMaxSdkVersion(
String apkFilenameInResources, int maxSdkVersion)
- throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, maxSdkVersion);
}
@@ -1041,7 +1078,7 @@
String apkFilenameInResources,
Integer minSdkVersionOverride,
Integer maxSdkVersionOverride)
- throws IOException, ApkFormatException, NoSuchAlgorithmException {
+ throws IOException, ApkFormatException, NoSuchAlgorithmException {
byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
ApkVerifier.Builder builder =
@@ -1077,8 +1114,12 @@
if (msg.length() > 0) {
msg.append('\n');
}
- msg.append("JAR signer ").append(signerName).append(": ")
- .append(issue.getIssue()).append(": ").append(issue);
+ msg.append("JAR signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(": ")
+ .append(issue);
}
}
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1088,8 +1129,25 @@
msg.append('\n');
}
msg.append("APK Signature Scheme v2 signer ")
- .append(signerName).append(": ")
- .append(issue.getIssue()).append(": ").append(issue);
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(": ")
+ .append(issue);
+ }
+ }
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+ String signerName = "signer #" + (signer.getIndex() + 1);
+ for (IssueWithParams issue : signer.getErrors()) {
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append("APK Signature Scheme v3 signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(": ")
+ .append(issue);
}
}
@@ -1099,7 +1157,8 @@
private void assertVerified(
String apkFilenameInResources,
Integer minSdkVersionOverride,
- Integer maxSdkVersionOverride) throws Exception {
+ Integer maxSdkVersionOverride)
+ throws Exception {
assertVerified(
verify(apkFilenameInResources, minSdkVersionOverride, maxSdkVersionOverride),
apkFilenameInResources);
@@ -1130,8 +1189,12 @@
if (msg.length() > 0) {
msg.append('\n');
}
- msg.append("JAR signer ").append(signerName).append(": ")
- .append(issue.getIssue()).append(" ").append(issue);
+ msg.append("JAR signer ")
+ .append(signerName)
+ .append(": ")
+ .append(issue.getIssue())
+ .append(" ")
+ .append(issue);
}
}
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
@@ -1144,7 +1207,9 @@
msg.append('\n');
}
msg.append("APK Signature Scheme v2 signer ")
- .append(signerName).append(": ").append(issue);
+ .append(signerName)
+ .append(": ")
+ .append(issue);
}
}
for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
@@ -1157,22 +1222,78 @@
msg.append('\n');
}
msg.append("APK Signature Scheme v3 signer ")
- .append(signerName).append(": ").append(issue);
+ .append(signerName)
+ .append(": ")
+ .append(issue);
}
}
- fail("APK failed verification for the wrong reason"
- + ". Expected: " + expectedIssue + ", actual: " + msg);
+ fail(
+ "APK failed verification for the wrong reason"
+ + ". Expected: "
+ + expectedIssue
+ + ", actual: "
+ + msg);
+ }
+
+ private static void assertSourceStampVerificationFailure(
+ ApkVerifier.Result result, Issue expectedIssue) {
+ if (result.isSourceStampVerified()) {
+ fail(
+ "APK source stamp verification succeeded instead of failing with "
+ + expectedIssue);
+ return;
+ }
+
+ StringBuilder msg = new StringBuilder();
+ List<IssueWithParams> resultIssueWithParams =
+ Stream.of(result.getErrors(), result.getWarnings())
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ for (IssueWithParams issue : resultIssueWithParams) {
+ if (expectedIssue.equals(issue.getIssue())) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append(issue);
+ }
+
+ ApkVerifier.Result.SourceStampInfo signer = result.getSourceStampInfo();
+ if (signer != null) {
+ List<IssueWithParams> sourceStampIssueWithParams =
+ Stream.of(signer.getErrors(), signer.getWarnings())
+ .filter(Objects::nonNull)
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList());
+ for (IssueWithParams issue : sourceStampIssueWithParams) {
+ if (expectedIssue.equals(issue.getIssue())) {
+ return;
+ }
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append("APK SourceStamp signer").append(": ").append(issue);
+ }
+ }
+
+ fail(
+ "APK source stamp failed verification for the wrong reason"
+ + ". Expected: "
+ + expectedIssue
+ + ", actual: "
+ + msg);
}
private void assertVerificationFailure(
- String apkFilenameInResources, ApkVerifier.Issue expectedIssue)
- throws Exception {
+ String apkFilenameInResources, ApkVerifier.Issue expectedIssue) throws Exception {
assertVerificationFailure(verify(apkFilenameInResources), expectedIssue);
}
- private void assertVerifiedForEach(
- String apkFilenamePatternInResources, String[] args) throws Exception {
+ private void assertVerifiedForEach(String apkFilenamePatternInResources, String[] args)
+ throws Exception {
assertVerifiedForEach(apkFilenamePatternInResources, args, null, null);
}
@@ -1180,7 +1301,8 @@
String apkFilenamePatternInResources,
String[] args,
Integer minSdkVersionOverride,
- Integer maxSdkVersionOverride) throws Exception {
+ Integer maxSdkVersionOverride)
+ throws Exception {
for (String arg : args) {
String apkFilenameInResources =
String.format(Locale.US, apkFilenamePatternInResources, arg);
@@ -1189,12 +1311,11 @@
}
private void assertVerifiedForEachForMinSdkVersion(
- String apkFilenameInResources, String[] args, int minSdkVersion)
- throws Exception {
+ String apkFilenameInResources, String[] args, int minSdkVersion) throws Exception {
assertVerifiedForEach(apkFilenameInResources, args, minSdkVersion, null);
}
- private static byte[] sha256(byte[] msg) throws Exception {
+ private static byte[] sha256(byte[] msg) {
try {
return MessageDigest.getInstance("SHA-256").digest(msg);
} catch (NoSuchAlgorithmException e) {
@@ -1202,7 +1323,7 @@
}
}
- private static void assumeThatRsaPssAvailable() throws Exception {
+ private static void assumeThatRsaPssAvailable() {
Assume.assumeTrue(Security.getProviders("Signature.SHA256withRSA/PSS") != null);
}
}
diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
new file mode 100644
index 0000000..1dc1e99
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
new file mode 100644
index 0000000..562805c
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-malformed-signature.apk b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
new file mode 100644
index 0000000..2723cc8
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/stamp-without-block.apk b/src/test/resources/com/android/apksig/stamp-without-block.apk
new file mode 100644
index 0000000..9dec2f5
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-without-block.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/valid-stamp.apk b/src/test/resources/com/android/apksig/valid-stamp.apk
new file mode 100644
index 0000000..8056e0b
--- /dev/null
+++ b/src/test/resources/com/android/apksig/valid-stamp.apk
Binary files differ