From 8ce84c8567a445334b072f8deb6cfbc5816f953a Mon Sep 17 00:00:00 2001 From: Khaled Abdelmohsen Date: Fri, 28 Feb 2020 11:08:56 +0000 Subject: [PATCH] Create source stamp verifier in platform Bug: 148005911 Test: atest FrameworksCoreTests:SourceStampVerifierTest Change-Id: Ibdaedf0355f5dd3537a425a031e298b710aae9f7 --- .../apk/SourceStampVerificationResult.java | 85 +++++ .../android/util/apk/SourceStampVerifier.java | 302 ++++++++++++++++++ .../SourceStampVerifierTest/original.apk | Bin 0 -> 6079 bytes .../stamp-apk-hash-mismatch.apk | Bin 0 -> 16854 bytes .../stamp-certificate-mismatch.apk | Bin 0 -> 16854 bytes .../stamp-malformed-signature.apk | Bin 0 -> 16854 bytes .../stamp-without-block.apk | Bin 0 -> 12758 bytes .../SourceStampVerifierTest/valid-stamp.apk | Bin 0 -> 16854 bytes .../util/apk/SourceStampVerifierTest.java | 139 ++++++++ 9 files changed, 526 insertions(+) create mode 100644 core/java/android/util/apk/SourceStampVerificationResult.java create mode 100644 core/java/android/util/apk/SourceStampVerifier.java create mode 100644 core/tests/coretests/assets/SourceStampVerifierTest/original.apk create mode 100644 core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk create mode 100644 core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk create mode 100644 core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk create mode 100644 core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk create mode 100644 core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk create mode 100644 core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java diff --git a/core/java/android/util/apk/SourceStampVerificationResult.java b/core/java/android/util/apk/SourceStampVerificationResult.java new file mode 100644 index 0000000000000..2edaf623fb94e --- /dev/null +++ b/core/java/android/util/apk/SourceStampVerificationResult.java @@ -0,0 +1,85 @@ +/* + * 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 android.util.apk; + +import android.annotation.Nullable; + +import java.security.cert.Certificate; + +/** + * A class encapsulating the result from the source stamp verifier + * + *

It indicates whether the source stamp is verified or not, and the source stamp certificate. + * + * @hide + */ +public final class SourceStampVerificationResult { + + private final boolean mPresent; + private final boolean mVerified; + private final Certificate mCertificate; + + private SourceStampVerificationResult( + boolean present, boolean verified, @Nullable Certificate certificate) { + this.mPresent = present; + this.mVerified = verified; + this.mCertificate = certificate; + } + + public boolean isPresent() { + return mPresent; + } + + public boolean isVerified() { + return mVerified; + } + + public Certificate getCertificate() { + return mCertificate; + } + + /** + * Create a non-present source stamp outcome. + * + * @return A non-present source stamp result. + */ + public static SourceStampVerificationResult notPresent() { + return new SourceStampVerificationResult( + /* present= */ false, /* verified= */ false, /* certificate= */ null); + } + + /** + * Create a verified source stamp outcome. + * + * @param certificate The source stamp certificate. + * @return A verified source stamp result, and the source stamp certificate. + */ + public static SourceStampVerificationResult verified(Certificate certificate) { + return new SourceStampVerificationResult( + /* present= */ true, /* verified= */ true, certificate); + } + + /** + * Create a non-verified source stamp outcome. + * + * @return A non-verified source stamp result. + */ + public static SourceStampVerificationResult notVerified() { + return new SourceStampVerificationResult( + /* present= */ true, /* verified= */ false, /* certificate= */ null); + } +} diff --git a/core/java/android/util/apk/SourceStampVerifier.java b/core/java/android/util/apk/SourceStampVerifier.java new file mode 100644 index 0000000000000..759c8649532bb --- /dev/null +++ b/core/java/android/util/apk/SourceStampVerifier.java @@ -0,0 +1,302 @@ +/* + * 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 android.util.apk; + +import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm; +import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; + +import android.util.Pair; +import android.util.jar.StrictJarFile; + +import libcore.io.IoUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; + +/** + * Source Stamp verifier. + * + *

SourceStamp improves traceability of apps with respect to unauthorized distribution. + * + *

The stamp is part of the APK that is protected by the signing block. + * + *

The APK contents hash is signed using the stamp key, and is saved as part of the signing + * block. + * + * @hide for internal use only. + */ +public abstract class SourceStampVerifier { + + private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + private static final int SOURCE_STAMP_BLOCK_ID = 0x2b09189e; + + /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ + private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; + + /** Hidden constructor to prevent instantiation. */ + private SourceStampVerifier() {} + + /** Verifies SourceStamp present in the provided APK. */ + public static SourceStampVerificationResult verify(String apkFile) { + try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { + return verify(apk); + } catch (Exception e) { + // Any exception in the SourceStamp verification returns a non-verified SourceStamp + // outcome without affecting the outcome of any of the other signature schemes. + return SourceStampVerificationResult.notVerified(); + } + } + + private static SourceStampVerificationResult verify(RandomAccessFile apk) + throws IOException, SignatureNotFoundException { + byte[] sourceStampCertificateDigest = getSourceStampCertificateDigest(apk); + if (sourceStampCertificateDigest == null) { + // SourceStamp certificate hash file not found, which means that there is not + // SourceStamp present. + return SourceStampVerificationResult.notPresent(); + } + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID); + Map apkContentDigests = getApkContentDigests(apk); + return verify(signatureInfo, apkContentDigests, sourceStampCertificateDigest); + } + + private static SourceStampVerificationResult verify( + SignatureInfo signatureInfo, + Map apkContentDigests, + byte[] sourceStampCertificateDigest) + throws SecurityException, IOException { + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + + List> digests = + apkContentDigests.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(e -> Pair.create(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + byte[] digestBytes = encodeApkContentDigests(digests); + + ByteBuffer sourceStampBlock = signatureInfo.signatureBlock; + ByteBuffer sourceStampBlockData = + ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); + + // Parse the SourceStamp certificate. + byte[] sourceStampEncodedCertificate = + ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData); + X509Certificate sourceStampCertificate; + try { + sourceStampCertificate = + (X509Certificate) + certFactory.generateCertificate( + new ByteArrayInputStream(sourceStampEncodedCertificate)); + } catch (CertificateException e) { + throw new SecurityException("Failed to decode certificate", e); + } + sourceStampCertificate = + new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate); + + // Verify the SourceStamp certificate found in the signing block is the same as the + // SourceStamp certificate found in the APK. + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(sourceStampEncodedCertificate); + byte[] sourceStampBlockCertificateDigest = messageDigest.digest(); + if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { + throw new SecurityException("Certificate mismatch between APK and signature block"); + } + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("Failed to find SHA-256", e); + } + + // Parse the signatures block and identify supported signatures + ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); + int signatureCount = 0; + int bestSigAlgorithm = -1; + byte[] bestSigAlgorithmSignatureBytes = null; + while (signatures.hasRemaining()) { + signatureCount++; + try { + ByteBuffer signature = getLengthPrefixedSlice(signatures); + if (signature.remaining() < 8) { + throw new SecurityException("Signature record too short"); + } + int sigAlgorithm = signature.getInt(); + if (!isSupportedSignatureAlgorithm(sigAlgorithm)) { + continue; + } + if ((bestSigAlgorithm == -1) + || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) { + bestSigAlgorithm = sigAlgorithm; + bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature); + } + } catch (IOException | BufferUnderflowException e) { + throw new SecurityException( + "Failed to parse signature record #" + signatureCount, e); + } + } + if (bestSigAlgorithm == -1) { + if (signatureCount == 0) { + throw new SecurityException("No signatures found"); + } else { + throw new SecurityException("No supported signatures found"); + } + } + + // Verify signatures over digests using the SourceStamp's certificate. + Pair signatureAlgorithmParams = + getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm); + String jcaSignatureAlgorithm = signatureAlgorithmParams.first; + AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second; + PublicKey publicKey = sourceStampCertificate.getPublicKey(); + boolean sigVerified; + try { + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(digestBytes); + sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); + } catch (InvalidKeyException + | InvalidAlgorithmParameterException + | SignatureException + | NoSuchAlgorithmException e) { + throw new SecurityException( + "Failed to verify " + jcaSignatureAlgorithm + " signature", e); + } + if (!sigVerified) { + throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); + } + + return SourceStampVerificationResult.verified(sourceStampCertificate); + } + + private static Map getApkContentDigests(RandomAccessFile apk) + throws IOException, SignatureNotFoundException { + // Retrieve APK content digests in V3 signing block. If a V3 signature is not found, the APK + // content digests would be re-tried from V2 signature. + try { + SignatureInfo v3SignatureInfo = + ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + return getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock); + } catch (SignatureNotFoundException e) { + // It's fine not to find a V3 signature. + } + + // Retrieve APK content digests in V2 signing block. If a V2 signature is not found, the + // process of retrieving APK content digests stops, and the stamp is considered un-verified. + SignatureInfo v2SignatureInfo = + ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + return getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock); + } + + private static Map getApkContentDigestsFromSignatureBlock( + ByteBuffer signatureBlock) throws IOException { + Map apkContentDigests = new HashMap<>(); + ByteBuffer signers = getLengthPrefixedSlice(signatureBlock); + while (signers.hasRemaining()) { + ByteBuffer signer = getLengthPrefixedSlice(signers); + ByteBuffer signedData = getLengthPrefixedSlice(signer); + ByteBuffer digests = getLengthPrefixedSlice(signedData); + while (digests.hasRemaining()) { + ByteBuffer digest = getLengthPrefixedSlice(digests); + int sigAlgorithm = digest.getInt(); + byte[] contentDigest = readLengthPrefixedByteArray(digest); + int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm); + apkContentDigests.put(digestAlgorithm, contentDigest); + } + } + return apkContentDigests; + } + + private static byte[] getSourceStampCertificateDigest(RandomAccessFile apk) throws IOException { + StrictJarFile apkJar = + new StrictJarFile( + apk.getFD(), + /* verify= */ false, + /* signatureSchemeRollbackProtectionsEnforced= */ false); + ZipEntry zipEntry = apkJar.findEntry(SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME); + if (zipEntry == null) { + // SourceStamp certificate hash file not found, which means that there is not + // SourceStamp present. + return null; + } + InputStream inputStream = null; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + inputStream = apkJar.getInputStream(zipEntry); + + // Trying to read the certificate digest, which should be less than 1024 bytes. + byte[] buffer = new byte[1024]; + int count = inputStream.read(buffer, 0, buffer.length); + byteArrayOutputStream.write(buffer, 0, count); + + return byteArrayOutputStream.toByteArray(); + } finally { + IoUtils.closeQuietly(inputStream); + } + } + + private static byte[] encodeApkContentDigests(List> apkContentDigests) { + int resultSize = 0; + for (Pair element : apkContentDigests) { + resultSize += 12 + element.second.length; + } + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (Pair element : apkContentDigests) { + byte[] second = element.second; + result.putInt(8 + second.length); + result.putInt(element.first); + result.putInt(second.length); + result.put(second); + } + return result.array(); + } +} diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/original.apk b/core/tests/coretests/assets/SourceStampVerifierTest/original.apk new file mode 100644 index 0000000000000000000000000000000000000000..05f89aa80e11f9c76d53e25f8036e6889e38e225 GIT binary patch literal 6079 zcmeG=XH=BQvfscEC1*i&$RKIR0!jvvAqpxWFyzdT!!RH@OHNA=1j!;fEFho~1sp)Y zKo*cFD2O2WeY3mndiUOU-+Ax-dgr{FQ>VJByQ;sb?&|6;T?9Uq6aa_;5ZGkcejU&W z#s>f#QUFlE!^YFq+r!GnQ^>-@)9Q2p0Pp~^-+Ce7cnEL-r~xTB_;&z>yg%9?r3FzL zL@Fu+2NW8n=;j8qb@hNL8|VWt!Mi<$T~%GRUG1>0h^~&V4p?tWS5r?qtShRgu;(Gn z6=ehS@^yuI*?ZX7m;x|`h2X@6;3}M^y*wOHcE9Nefbt(Ua19v9 z{{UiQ3pY0;%EHA4w2MatP=Osk#^QK7kjKr3lR07&kAQ%+JspC1s=w0O*k~7m8HvIZlW-(h0~h2+6W_E6bJzIDIS47A)`>%6hAf=xPpwd_T^pDZ&2WS-kGyBt1cpIWZw z^SF(PY@@Jh-F4n3G3jDiFH(17l(^?_pXBhm6C3#Hps!(T^fpI6+%rCd`^1r>b;OzE zfbeA9Shz+-%@I+nJS>#H^=LR+(yEC^bN$RJQf$cSgmp*O7^$(fqb~efS;;9?V((@1 zs&Cn~l1WU(iktm$FtL+$wI|~OA!?nkiobqf&S|=Ou}SIj>SK?@ro6lqKRXSVb;$+} zj3xR(>hY}9t^RA#+u={gYeaf42Mmzyv=7MHu$N=;`&l<^4%3roXI8(5H(SpEvO7)q}kc6 zQ`PUwNWK2XNdNlkIJJy-8W|(n#M=K`BWEA$#D$#|&4gz!MI^>KSUZ{<1!de<`o@?0 zUaiVs&(JwEflE}b%3c{a7B}{JFEf}h93kQz>+{mSE%37vmA5D*VuyVqZ6lfDwB}Axh+cx; z4%1F&N!ui@+>J`h=XK&`aT#$UKd`LPW4bx~cx@`lCYMfX9i6l2E0Xu(yDR&cc;6+9 zr+sDeXB_Kt$%o$`SG=WUpVPUYPjgV|ffu^I`kZn8e6oZbsX1ygk`uI`zF#G6oWBh$II;(K*b9$Pl^^Fh3UTP*_lA^3Nl#A*Uf4R)T4(P!|> zoPQ^u_o$~r)=95dBiEz3yHadOC?i1g>dm5#{E+q>`?~wmd9k0TIORSJY4L}Ir7Zb{ z+%CUWB;fiWN9c;=umZ-)=1}Fr&<%%1zLC=2g7nFz#iK{6+52pv)$Ylo(nQg`RGbf_ zGd=rQG#JaSVf-F`?sta^f4H%bxSt&k^phe#>? z`tEioE{5JsL}i`BC9XeX2t4~imQ7}+r+SbF%s5#f;*zSCs)3^54Q(}HW!0MoLi%bh zScGe+0v*BpHIlOs8y2xxrsNc_=gOm`3{XBHo}n^jbpH@l2omWe?l8H9!fJJRDtQR7 zx$+W#KJ7u~Q9ayM@yl9kQ3avXy(` zEbP;#T*-BDn8bXadn1{tS?G}$=0lSMUY3s&4rP96ejM)7=<9tJR`km)a;<9KB7qaT zRpTt1sqs9ib02lBr(;Ad2dw9|Wn}L#wv^2%8rzZFY|J?%_&uNgaOyV##g{zbH6$8D z>VI@D6vq*<#P9?DBV@4l~latkkXvPyHVCEBa(8{sqdnVJ4qVbHAVxYfUT3T8Z)K}qMDBDMe>}|iO&oDh|BHsAI zyywmhig>b>_m|NTv=n|u)%V30v1} z#wE=WE+2zmebdY!Q-+muk z+yAukiHBPjj?_8mXurvxGO9BhpY`b7QPp9+x9!y+FTY#w5RDhT9=#g1fhZamw2fBEymQVm>S z8g^9JsmIIlZLi~#)A3^;>pPB>r|55c8iX^nYnn%=am;ozd`oT=Ze~IJjpBt3&l~S0 z&zCDLU+QfU%<;9oGm9?w`&7DDNFUUGPO@TY;MVpxPwZ*!*(x_7HePswlzVH}h{dd^ zs2)|%pP21_vTwXG-xajT&(nY6w>ja7T5M=oTNZ2jTwjsv*nq-5!gdI1A;@!>+C$c` ztiR}!(Iwq{48Ph~_>p)iH#|GiD4XfLX!l$W=BnF!WaIvQ>&Pavl26_T*!!=i)xNH7*j9c| zIse6=MJLmhDw`qmr+fvAMs+i`E^i?D6`eyNJXm$cVSnc?@5>)1xxbPm{yMmKx^>$@ zv`TnBb7Vw#<|t05aM-Iry>%&n>Q#H^#J+L!_F7P8yTjYGI)nEgKgIKL$uXxczomcg za?s}Pd+TH_UYOlj)9U>(WuB>2?YFOk6CFGLik*YDvnR|%f85f4oGL1#E+Z%4X`%-n z)wuW0pGUQpqV^3nReb{?Ej61VEkw8i5d5KSKIn^~z`ZA}tx81ll8#0alIucH%zLPH(>PAT$ipH3#k#wgqmHftGR zviC1iNsCW|)ZTkr$BE!$N~(rJMPn^?_?-qv2463lK14luz`W?=(h(~Egsp2+cj2gT z`F6ng*QyP}r+$5(xJZX)i~8T&Or&6?g1pA?vD9y2#zHU0=-wV-A=2{0W`Y!)O;0SX z!qM$l4g6D{1;kP0-PKAu<2b+IW%()=uQvzbEl<6Fuo%c$@=NH@r&tD_kS&z<8t=d( z)=`>iwb|TAoteZ#f=2|PS3n5+gEB{vxMRS!FNo4$_QO3ec|pVn9S8&IWJTgxQ#R?Q zX`0ZK-i490GKglZmULJ&{44xRc1npT1k^bJZ=Uc_yaRYB^~ca5CHh05K8bvAf$Mq5C||bpCUo^p!u5_yzUi8v5}GJ zipA6cpu%PPkV!!GT(zgQR_Le(Gg5heR3)u)4x8;kr4XOmK$ae?5Q9qX{hbdR*KC1X6}@p~Lck_dKKKD<)DRiI3J!}2M_FZj-$=;}&D z!b$*@|KK2f}`dZSDIV3TvQhU!6OC!^NKNU*Zl?mTtCL? z(}7v=&kEL0MFLy}>o2GQ5AlCd&-%N8pI=D0TEbso2fmqpsx16a{E?Oa{($;R@iECy zIr@+1)ZexL{!;i$8vwuJ&kX;YHu!G%yZYa+1b?aDru<3$rwf8Ef&lzKxQ8GifW~cO H1|0Vvamy{W literal 0 HcmV?d00001 diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk new file mode 100644 index 0000000000000000000000000000000000000000..1dc1e998f1eec74997b607ad755b70f4ad1a7a84 GIT binary patch literal 16854 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 H00#IMouC8; literal 0 HcmV?d00001 diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk b/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk new file mode 100644 index 0000000000000000000000000000000000000000..8056e0bf6e50d06eee7fa657e08b3a02f6e40e64 GIT binary patch literal 16854 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 literal 0 HcmV?d00001 diff --git a/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java b/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java new file mode 100644 index 0000000000000..44f64079d8313 --- /dev/null +++ b/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.apk; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** Unit test for {@link android.util.apk.SourceStampVerifier} */ +@RunWith(JUnit4.class) +public class SourceStampVerifierTest { + + private final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + + @Test + public void testSourceStamp_noStamp() throws Exception { + File testApk = getApk("SourceStampVerifierTest/original.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(testApk.getAbsolutePath()); + + assertFalse(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + } + + @Test + public void testSourceStamp_correctSignature() throws Exception { + File testApk = getApk("SourceStampVerifierTest/valid-stamp.apk"); + ZipFile apkZipFile = new ZipFile(testApk); + ZipEntry stampCertZipEntry = apkZipFile.getEntry("stamp-cert-sha256"); + int size = (int) stampCertZipEntry.getSize(); + byte[] expectedStampCertHash = new byte[size]; + try (InputStream inputStream = apkZipFile.getInputStream(stampCertZipEntry)) { + inputStream.read(expectedStampCertHash); + } + + SourceStampVerificationResult result = + SourceStampVerifier.verify(testApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertTrue(result.isVerified()); + assertNotNull(result.getCertificate()); + byte[] actualStampCertHash = + MessageDigest.getInstance("SHA-256").digest(result.getCertificate().getEncoded()); + assertArrayEquals(expectedStampCertHash, actualStampCertHash); + } + + @Test + public void testSourceStamp_signatureMissing() throws Exception { + File testApk = getApk("SourceStampVerifierTest/stamp-without-block.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(testApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + } + + @Test + public void testSourceStamp_certificateMismatch() throws Exception { + File testApk = getApk("SourceStampVerifierTest/stamp-certificate-mismatch.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(testApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + } + + @Test + public void testSourceStamp_apkHashMismatch() throws Exception { + File testApk = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(testApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + } + + @Test + public void testSourceStamp_malformedSignature() throws Exception { + File testApk = getApk("SourceStampVerifierTest/stamp-malformed-signature.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(testApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + } + + private File getApk(String apkPath) throws IOException { + File testApk = File.createTempFile("SourceStampApk", ".apk"); + try (InputStream inputStream = mContext.getAssets().open(apkPath)) { + Files.copy(inputStream, testApk.toPath(), REPLACE_EXISTING); + } + return testApk; + } +}