diff options
author | Michael Groover <mpgroover@google.com> | 2020-09-25 21:23:37 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-09-25 21:23:37 +0000 |
commit | 16950a704cde81adc20b401f5307adf8960a9b94 (patch) | |
tree | dbd5715b54cc23acfceaf3449c20a21a5e47bba2 | |
parent | 57805fbb9a1d965a50f9438a71109ecb0285f883 (diff) | |
parent | 7515f8919ef6517b2886ad22a74b3dbd72e142e5 (diff) | |
download | apksig-16950a704cde81adc20b401f5307adf8960a9b94.tar.gz |
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: Id10ebd4d62c0a9ca7cad3f3d05bb8d353aeabcfb
-rw-r--r-- | src/main/java/com/android/apksig/ApkVerificationIssue.java | 4 | ||||
-rw-r--r-- | src/main/java/com/android/apksig/ApkVerifier.java | 7 | ||||
-rw-r--r-- | src/main/java/com/android/apksig/SourceStampVerifier.java | 145 | ||||
-rw-r--r-- | src/test/java/com/android/apksig/ApkVerifierTest.java | 27 | ||||
-rw-r--r-- | src/test/java/com/android/apksig/SourceStampVerifierTest.java | 174 | ||||
-rw-r--r-- | src/test/resources/com/android/apksig/stamp-without-apk-signature.apk | bin | 0 -> 12633 bytes | |||
-rw-r--r-- | src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk | bin | 0 -> 16859 bytes |
7 files changed, 311 insertions, 46 deletions
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 @@ public class ApkVerificationIssue { * 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 @@ public class ApkVerifier { 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 @@ public class ApkVerifier { 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.io.RandomAccessFile; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; @@ -251,7 +252,7 @@ public class SourceStampVerifier { 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 @@ public class SourceStampVerifier { 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 @@ public class SourceStampVerifier { 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 @@ public class SourceStampVerifier { } 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 @@ public class SourceStampVerifier { 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 @@ public class SourceStampVerifier { } 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 @@ public class SourceStampVerifier { // 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 @@ public class SourceStampVerifier { * 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 @@ public class SourceStampVerifier { 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 @@ public class SourceStampVerifier { } /** + * 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 @@ public class SourceStampVerifier { } /** + * 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 @@ public class SourceStampVerifier { } /** + * 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 @@ public class SourceStampVerifier { } /** + * 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.runner.RunWith; 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 @@ public class ApkVerifierTest { 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.runner.RunWith; 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 @@ public class SourceStampVerifierTest { "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 @@ public class SourceStampVerifierTest { } @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 @@ public class SourceStampVerifierTest { @Test public void verifySourceStamp_certificateMismatch() throws Exception { - SourceStampVerifier.Result verificationResult = verifySourceStamp( + Result verificationResult = verifySourceStamp( "stamp-certificate-mismatch.apk"); assertSourceStampVerificationFailure( verificationResult, @@ -72,44 +113,50 @@ public class SourceStampVerifierTest { @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 @@ public class SourceStampVerifierTest { @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 @@ public class SourceStampVerifierTest { @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 @@ public class SourceStampVerifierTest { @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 @@ public class SourceStampVerifierTest { // 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 class SourceStampVerifierTest { 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 @@ public class SourceStampVerifierTest { // 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 @@ public class SourceStampVerifierTest { 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 @@ public class SourceStampVerifierTest { 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 @@ public class SourceStampVerifierTest { } 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")); + } + + /** + * 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()))); + } } -}
\ No newline at end of file +} 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 Binary files differnew file mode 100644 index 0000000..c2e6826 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk 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 Binary files differnew file mode 100644 index 0000000..5f1103a --- /dev/null +++ b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk |