Obtain the V1 signing certificate during stamp verification am: 7515f8919e
Original change: https://googleplex-android-review.googlesource.com/c/platform/tools/apksig/+/12692889
Change-Id: Ib63fc259ffc248247388a22db5371a24c7a304b3
diff --git a/src/main/java/com/android/apksig/ApkVerificationIssue.java b/src/main/java/com/android/apksig/ApkVerificationIssue.java
index 79c50d4..2aa9d0b 100644
--- a/src/main/java/com/android/apksig/ApkVerificationIssue.java
+++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java
@@ -112,6 +112,10 @@
* with signature(s) that did not verify.
*/
public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35;
+ /** No V1 / jar signing signature blocks were found in the APK. */
+ public static final int JAR_SIG_NO_SIGNATURES = 36;
+ /** An exception was encountered when parsing the V1 / jar signer in the signature block. */
+ public static final int JAR_SIG_PARSE_EXCEPTION = 37;
private final int mIssueId;
private final String mFormat;
diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java
index c186784..354dfbd 100644
--- a/src/main/java/com/android/apksig/ApkVerifier.java
+++ b/src/main/java/com/android/apksig/ApkVerifier.java
@@ -3033,7 +3033,8 @@
private ApkVerificationIssueAdapter() {
}
- private static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>();
+ // This field is visible for testing
+ static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>();
static {
sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS,
@@ -3112,6 +3113,10 @@
Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY,
Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES,
+ Issue.JAR_SIG_NO_SIGNATURES);
+ sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+ Issue.JAR_SIG_PARSE_EXCEPTION);
}
/**
diff --git a/src/main/java/com/android/apksig/SourceStampVerifier.java b/src/main/java/com/android/apksig/SourceStampVerifier.java
index aba6c57..0c0e036 100644
--- a/src/main/java/com/android/apksig/SourceStampVerifier.java
+++ b/src/main/java/com/android/apksig/SourceStampVerifier.java
@@ -16,12 +16,12 @@
package com.android.apksig;
-import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2;
import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3;
import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME;
-import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME;
+import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME;
import com.android.apksig.apk.ApkFormatException;
import com.android.apksig.apk.ApkUtilsLite;
@@ -54,6 +54,7 @@
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -251,7 +252,7 @@
if (mMinSdkVersion < AndroidSdkVersion.N
|| signatureSchemeApkContentDigests.isEmpty()) {
Map<ContentDigestAlgorithm, byte[]> apkContentDigests =
- getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections);
+ getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result);
signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME,
apkContentDigests);
}
@@ -296,12 +297,12 @@
try {
signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock);
} catch (ApkFormatException e) {
- result.addVerificationError(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS);
return;
}
if (!signers.hasRemaining()) {
- result.addVerificationError(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
+ result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS
: ApkVerificationIssue.V3_SIG_NO_SIGNERS);
return;
}
@@ -328,7 +329,7 @@
apkContentDigests,
signerInfo);
} catch (ApkFormatException | BufferUnderflowException e) {
- signerInfo.addVerificationError(
+ signerInfo.addVerificationWarning(
isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER
: ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER);
return;
@@ -379,7 +380,7 @@
}
apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes);
} catch (ApkFormatException | BufferUnderflowException e) {
- signerInfo.addVerificationError(
+ signerInfo.addVerificationWarning(
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST
: ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST);
return;
@@ -394,7 +395,7 @@
certificate = (X509Certificate) certFactory.generateCertificate(
new ByteArrayInputStream(encodedCert));
} catch (CertificateException e) {
- signerInfo.addVerificationError(
+ signerInfo.addVerificationWarning(
isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE
: ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE);
return;
@@ -408,24 +409,45 @@
}
if (signerInfo.getSigningCertificate() == null) {
- signerInfo.addVerificationError(isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
- : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
+ signerInfo.addVerificationWarning(
+ isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES
+ : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES);
return;
}
}
+ /**
+ * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the
+ * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is
+ * returned.
+ *
+ * <p>If any errors are encountered while parsing the V1 signers the provided {@code result}
+ * will be updated to include a warning, but the source stamp verification can still proceed.
+ */
private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme(
List<CentralDirectoryRecord> cdRecords,
DataSource apk,
- ZipSections zipSections)
+ ZipSections zipSections,
+ Result result)
throws IOException, ApkFormatException {
CentralDirectoryRecord manifestCdRecord = null;
+ List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1);
Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>(
ContentDigestAlgorithm.class);
for (CentralDirectoryRecord cdRecord : cdRecords) {
- if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) {
+ String cdRecordName = cdRecord.getName();
+ if (cdRecordName == null) {
+ continue;
+ }
+ if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) {
manifestCdRecord = cdRecord;
- break;
+ continue;
+ }
+ if (cdRecordName.startsWith("META-INF/")
+ && (cdRecordName.endsWith(".RSA")
+ || cdRecordName.endsWith(".DSA")
+ || cdRecordName.endsWith(".EC"))) {
+ signatureBlockRecords.add(cdRecord);
}
}
if (manifestCdRecord == null) {
@@ -434,6 +456,36 @@
// thus an empty digest will invalidate that signature.
return v1ContentDigest;
}
+ if (signatureBlockRecords.isEmpty()) {
+ result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+ } else {
+ for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) {
+ try {
+ CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+ byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk,
+ signatureBlockRecord, zipSections.getZipCentralDirectoryOffset());
+ for (Certificate certificate : certFactory.generateCertificates(
+ new ByteArrayInputStream(signatureBlockBytes))) {
+ // If multiple certificates are found within the signature block only the
+ // first is used as the signer of this block.
+ if (certificate instanceof X509Certificate) {
+ Result.SignerInfo signerInfo = new Result.SignerInfo();
+ signerInfo.setSigningCertificate((X509Certificate) certificate);
+ result.addV1Signer(signerInfo);
+ break;
+ }
+ }
+ } catch (CertificateException e) {
+ // Log a warning for the parsing exception but still proceed with the stamp
+ // verification.
+ result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION,
+ signatureBlockRecord.getName(), e);
+ break;
+ } catch (ZipFormatException e) {
+ throw new ApkFormatException("Failed to read APK", e);
+ }
+ }
+ }
try {
byte[] manifestBytes =
LocalFileRecord.getUncompressedData(
@@ -451,13 +503,15 @@
* verified if {@link #isVerified()} returns true.
*/
public static class Result {
+ private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>();
private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>();
private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>();
- private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV2SchemeSigners,
- mV3SchemeSigners);
+ private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners,
+ mV2SchemeSigners, mV3SchemeSigners);
private SourceStampInfo mSourceStampInfo;
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
private boolean mVerified;
@@ -465,6 +519,14 @@
mErrors.add(new ApkVerificationIssue(errorId, params));
}
+ void addVerificationWarning(int warningId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(warningId, params));
+ }
+
+ private void addV1Signer(SignerInfo signerInfo) {
+ mV1SchemeSigners.add(signerInfo);
+ }
+
private void addV2Signer(SignerInfo signerInfo) {
mV2SchemeSigners.add(signerInfo);
}
@@ -496,6 +558,14 @@
}
/**
+ * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the
+ * provided APK.
+ */
+ public List<SignerInfo> getV1SchemeSigners() {
+ return mV1SchemeSigners;
+ }
+
+ /**
* Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the
* provided APK.
*/
@@ -552,6 +622,13 @@
}
/**
+ * Returns the warnings encountered while verifying the APK's source stamp.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
* Returns all errors for this result, including any errors from signature scheme signers
* and the source stamp.
*/
@@ -571,19 +648,43 @@
}
/**
+ * Returns all warnings for this result, including any warnings from signature scheme
+ * signers and the source stamp.
+ */
+ public List<ApkVerificationIssue> getAllWarnings() {
+ List<ApkVerificationIssue> warnings = new ArrayList<>();
+ warnings.addAll(mWarnings);
+
+ for (List<SignerInfo> signers : mAllSchemeSigners) {
+ for (SignerInfo signer : signers) {
+ warnings.addAll(signer.getWarnings());
+ }
+ }
+ if (mSourceStampInfo != null) {
+ warnings.addAll(mSourceStampInfo.getWarnings());
+ }
+ return warnings;
+ }
+
+ /**
* Contains information about an APK's signer and any errors encountered while parsing the
* corresponding signature block.
*/
public static class SignerInfo {
private X509Certificate mSigningCertificate;
private final List<ApkVerificationIssue> mErrors = new ArrayList<>();
+ private final List<ApkVerificationIssue> mWarnings = new ArrayList<>();
void setSigningCertificate(X509Certificate signingCertificate) {
mSigningCertificate = signingCertificate;
}
- void addVerificationError(int error, Object... params) {
- mErrors.add(new ApkVerificationIssue(error, params));
+ void addVerificationError(int errorId, Object... params) {
+ mErrors.add(new ApkVerificationIssue(errorId, params));
+ }
+
+ void addVerificationWarning(int warningId, Object... params) {
+ mWarnings.add(new ApkVerificationIssue(warningId, params));
}
/**
@@ -602,11 +703,19 @@
}
/**
+ * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings
+ * encountered during processing of this signer's signature block.
+ */
+ public List<ApkVerificationIssue> getWarnings() {
+ return mWarnings;
+ }
+
+ /**
* Returns {@code true} if any errors were encountered while parsing this signer's
* signature block.
*/
public boolean containsErrors() {
- return mErrors.isEmpty();
+ return !mErrors.isEmpty();
}
}
diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java
index 6bb5edf..9e1a75e 100644
--- a/src/test/java/com/android/apksig/ApkVerifierTest.java
+++ b/src/test/java/com/android/apksig/ApkVerifierTest.java
@@ -36,6 +36,8 @@
import org.junit.runners.JUnit4;
import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -1303,6 +1305,31 @@
Issue.SOURCE_STAMP_POR_CERT_MISMATCH);
}
+ @Test
+ public void apkVerificationIssueAdapter_verifyAllBaseIssuesMapped() throws Exception {
+ Field[] fields = ApkVerificationIssue.class.getFields();
+ StringBuilder msg = new StringBuilder();
+ for (Field field : fields) {
+ // All public static int fields in the ApkVerificationIssue class should be issue IDs;
+ // if any are added that are not intended as IDs a filter set should be applied to this
+ // test.
+ if (Modifier.isStatic(field.getModifiers()) && field.getType() == int.class) {
+ if (!ApkVerifier.ApkVerificationIssueAdapter
+ .sVerificationIssueIdToIssue.containsKey(field.get(null))) {
+ if (msg.length() > 0) {
+ msg.append('\n');
+ }
+ msg.append(
+ "A mapping is required from ApkVerificationIssue." + field.getName()
+ + " to an ApkVerifier.Issue in ApkVerificationIssueAdapter");
+ }
+ }
+ }
+ if (msg.length() > 0) {
+ fail(msg.toString());
+ }
+ }
+
private ApkVerifier.Result verify(String apkFilenameInResources)
throws IOException, ApkFormatException, NoSuchAlgorithmException {
return verify(apkFilenameInResources, null, null);
diff --git a/src/test/java/com/android/apksig/SourceStampVerifierTest.java b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
index 46323a3..d99f0a0 100644
--- a/src/test/java/com/android/apksig/SourceStampVerifierTest.java
+++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java
@@ -16,6 +16,13 @@
package com.android.apksig;
+import static com.android.apksig.SourceStampVerifier.Result;
+import static com.android.apksig.SourceStampVerifier.Result.SignerInfo;
+import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes;
+import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import com.android.apksig.internal.util.Resources;
@@ -26,6 +33,8 @@
import org.junit.runners.JUnit4;
import java.nio.ByteBuffer;
+import java.security.cert.X509Certificate;
+import java.util.List;
@RunWith(JUnit4.class)
public class SourceStampVerifierTest {
@@ -33,10 +42,12 @@
"fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8";
private static final String EC_P256_CERT_SHA256_DIGEST =
"6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599";
+ private static final String EC_P256_2_CERT_SHA256_DIGEST =
+ "d78405f761ff6236cc9b570347a570aba0c62a129a3ac30c831c64d09ad95469";
@Test
public void verifySourceStamp_correctSignature() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp("valid-stamp.apk");
+ Result verificationResult = verifySourceStamp("valid-stamp.apk");
// Since the API is only verifying the source stamp the result itself should be marked as
// verified.
assertVerified(verificationResult);
@@ -54,8 +65,38 @@
}
@Test
+ public void verifySourceStamp_rotatedV3Key_signingCertDigestsMatch() throws Exception {
+ // The SourceStampVerifier should return a result that includes all of the latest signing
+ // certificates for each of the signature schemes that are applicable to the specified
+ // min / max SDK versions.
+
+ // Verify when platform versions that support the V1 - V3 signature schemes are specified
+ // that an APK signed with all signature schemes has its expected signers returned in the
+ // result.
+ Result verificationResult = verifySourceStamp("./v1v2v3-rotated-v3-key-valid-stamp.apk", 23,
+ 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST,
+ EC_P256_CERT_SHA256_DIGEST, EC_P256_2_CERT_SHA256_DIGEST);
+
+ // Verify when the specified platform versions only support a single signature scheme that
+ // scheme's signer is the only one in the result.
+ verificationResult = verifySourceStamp("./v1v2v3-rotated-v3-key-valid-stamp.apk", 18, 18);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
+
+ verificationResult = verifySourceStamp("./v1v2v3-rotated-v3-key-valid-stamp.apk", 24, 24);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
+
+ verificationResult = verifySourceStamp("./v1v2v3-rotated-v3-key-valid-stamp.apk", 28, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, null, EC_P256_2_CERT_SHA256_DIGEST);
+ }
+
+ @Test
public void verifySourceStamp_signatureMissing() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-without-block.apk");
assertSourceStampVerificationFailure(verificationResult,
ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING);
@@ -63,7 +104,7 @@
@Test
public void verifySourceStamp_certificateMismatch() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-certificate-mismatch.apk");
assertSourceStampVerificationFailure(
verificationResult,
@@ -72,44 +113,50 @@
@Test
public void verifySourceStamp_v1OnlySignatureValidStamp() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk");
+ Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk");
assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
// Confirm that the source stamp verification succeeds when specifying platform versions
// that supported later signature scheme versions.
verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 28, 28);
assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24);
assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null);
}
@Test
public void verifySourceStamp_v2OnlySignatureValidStamp() throws Exception {
// The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so
// set the min / max versions to prevent failure due to a missing V1 signature.
- SourceStampVerifier.Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk",
+ Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk",
24, 24);
assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
// Confirm that the source stamp verification succeeds when specifying a platform version
// that supports a later signature scheme version.
verificationResult = verifySourceStamp("v2-only-with-stamp.apk", 28, 28);
assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null);
}
@Test
public void verifySourceStamp_v3OnlySignatureValidStamp() throws Exception {
// The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so
// set the min / max versions to prevent failure due to a missing V1 signature.
- SourceStampVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
28, 28);
assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, null, EC_P256_CERT_SHA256_DIGEST);
}
@Test
public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-apk-hash-mismatch-v1.apk");
assertSourceStampVerificationFailure(verificationResult,
ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
@@ -117,7 +164,7 @@
@Test
public void verifySourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-apk-hash-mismatch-v2.apk");
assertSourceStampVerificationFailure(verificationResult,
ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
@@ -125,7 +172,7 @@
@Test
public void verifySourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-apk-hash-mismatch-v3.apk");
assertSourceStampVerificationFailure(verificationResult,
ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY);
@@ -133,7 +180,7 @@
@Test
public void verifySourceStamp_malformedSignature() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-malformed-signature.apk");
assertSourceStampVerificationFailure(
verificationResult, ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE);
@@ -144,7 +191,7 @@
// The ApkVerifier provides an API to specify the expected certificate digest; this test
// verifies that the test runs through to completion when the actual digest matches the
// provided value.
- SourceStampVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
RSA_2048_CERT_SHA256_DIGEST, 28, 28);
assertVerified(verificationResult);
}
@@ -153,7 +200,7 @@
public void verifySourceStamp_expectedDigestMismatch() throws Exception {
// If the caller requests source stamp verification with an expected cert digest that does
// not match the actual digest in the APK the verifier should report the mismatch.
- SourceStampVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
+ Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk",
EC_P256_CERT_SHA256_DIGEST);
assertSourceStampVerificationFailure(verificationResult,
ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH);
@@ -164,43 +211,59 @@
// The caller of this API expects that the provided APK should be signed with a source
// stamp; if no artifacts of the stamp are present ensure that the API fails indicating the
// missing stamp.
- SourceStampVerifier.Result verificationResult = verifySourceStamp("original.apk");
+ Result verificationResult = verifySourceStamp("original.apk");
assertSourceStampVerificationFailure(verificationResult,
ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING);
}
@Test
public void verifySourceStamp_validStampLineage() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-lineage-valid.apk");
assertVerified(verificationResult);
}
@Test
public void verifySourceStamp_invalidStampLineage() throws Exception {
- SourceStampVerifier.Result verificationResult = verifySourceStamp(
+ Result verificationResult = verifySourceStamp(
"stamp-lineage-invalid.apk");
assertSourceStampVerificationFailure(verificationResult,
ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH);
}
- private SourceStampVerifier.Result verifySourceStamp(String apkFilenameInResources)
+ @Test
+ public void verifySourceStamp_noApkSignature_succeeds()
+ throws Exception {
+ // The SourceStampVerifier is designed to verify an APK's source stamp with minimal
+ // verification of the APK signature schemes. This test verifies if just the MANIFEST.MF
+ // is present without any other APK signatures the stamp signature can still be successfully
+ // verified.
+ Result verificationResult = verifySourceStamp("stamp-without-apk-signature.apk", 18, 28);
+ assertVerified(verificationResult);
+ assertSigningCertificates(verificationResult, null, null, null);
+ // While the source stamp verification should succeed a warning should still be logged to
+ // notify the caller that there were no signers.
+ assertSourceStampVerificationWarning(verificationResult,
+ ApkVerificationIssue.JAR_SIG_NO_SIGNATURES);
+ }
+
+ private Result verifySourceStamp(String apkFilenameInResources)
throws Exception {
return verifySourceStamp(apkFilenameInResources, null, null, null);
}
- private SourceStampVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ private Result verifySourceStamp(String apkFilenameInResources,
String expectedCertDigest) throws Exception {
return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null);
}
- private SourceStampVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ private Result verifySourceStamp(String apkFilenameInResources,
Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception {
return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride,
maxSdkVersionOverride);
}
- private SourceStampVerifier.Result verifySourceStamp(String apkFilenameInResources,
+ private Result verifySourceStamp(String apkFilenameInResources,
String expectedCertDigest, Integer minSdkVersionOverride, Integer maxSdkVersionOverride)
throws Exception {
byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources);
@@ -215,7 +278,7 @@
return builder.build().verifySourceStamp(expectedCertDigest);
}
- private static void assertVerified(SourceStampVerifier.Result result) {
+ private static void assertVerified(Result result) {
if (result.isVerified()) {
return;
}
@@ -229,17 +292,24 @@
fail("APK failed source stamp verification: " + msg.toString());
}
- private static void assertSourceStampVerificationFailure(SourceStampVerifier.Result result,
- int expectedIssueId) {
+ private static void assertSourceStampVerificationFailure(Result result, int expectedIssueId) {
if (result.isVerified()) {
fail(
"APK source stamp verification succeeded instead of failing with "
+ expectedIssueId);
return;
}
+ assertSourceStampVerificationIssue(result.getAllErrors(), expectedIssueId);
+ }
+ private static void assertSourceStampVerificationWarning(Result result, int expectedIssueId) {
+ assertSourceStampVerificationIssue(result.getAllWarnings(), expectedIssueId);
+ }
+
+ private static void assertSourceStampVerificationIssue(List<ApkVerificationIssue> issues,
+ int expectedIssueId) {
StringBuilder msg = new StringBuilder();
- for (ApkVerificationIssue issue : result.getAllErrors()) {
+ for (ApkVerificationIssue issue : issues) {
if (issue.getIssueId() == expectedIssueId) {
return;
}
@@ -250,10 +320,60 @@
}
fail(
- "APK source stamp failed verification for the wrong reason"
- + ". Expected error ID: "
+ "APK source stamp verification did not report the expected issue. "
+ + "Expected error ID: "
+ expectedIssueId
+ ", actual: "
- + msg);
+ + (msg.length() > 0 ? msg.toString() : "No reported issues"));
}
-}
\ No newline at end of file
+
+ /**
+ * Asserts that the provided {@code expectedCertDigests} match their respective signing
+ * certificate digest in the specified {@code result}.
+ *
+ * <p>{@code expectedCertDigests} should be provided in order of the signature schemes with V1
+ * being the first element, V2 the second, etc. If a signer is not expected to be present for
+ * a signature scheme version a {@code null} value should be provided; for instance if only a V3
+ * signing certificate is expected the following should be provided: {@code null, null,
+ * v3ExpectedCertDigest}.
+ *
+ * <p>Note, this method only supports a single signer per signature scheme; if an expected
+ * certificate digest is provided for a signature scheme and multiple signers are found an
+ * assertion exception will be thrown.
+ */
+ private static void assertSigningCertificates(Result result, String... expectedCertDigests)
+ throws Exception {
+ for (int i = 0; i < expectedCertDigests.length; i++) {
+ List<SignerInfo> signers = null;
+ switch (i) {
+ case 0:
+ signers = result.getV1SchemeSigners();
+ break;
+ case 1:
+ signers = result.getV2SchemeSigners();
+ break;
+ case 2:
+ signers = result.getV3SchemeSigners();
+ break;
+ default:
+ fail("This method only supports verification of the signing certificates up "
+ + "through the V3 Signature Scheme");
+ }
+ if (expectedCertDigests[i] == null) {
+ assertEquals(
+ "Did not expect any V" + (i + 1) + " signers, found " + signers.size(), 0,
+ signers.size());
+ continue;
+ }
+ if (signers.size() != 1) {
+ fail("Expected one V" + (i + 1) + " signer with certificate digest "
+ + expectedCertDigests[i] + ", found " + signers.size() + " V" + (i + 1)
+ + " signers");
+ }
+ X509Certificate signingCertificate = signers.get(0).getSigningCertificate();
+ assertNotNull(signingCertificate);
+ assertEquals(expectedCertDigests[i],
+ toHex(computeSha256DigestBytes(signingCertificate.getEncoded())));
+ }
+ }
+}
diff --git a/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk
new file mode 100644
index 0000000..c2e6826
--- /dev/null
+++ b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk
Binary files differ
diff --git a/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk
new file mode 100644
index 0000000..5f1103a
--- /dev/null
+++ b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk
Binary files differ