Parse source stamp lineage in platform

Bug: 167962287
Test: atest FrameworksCoreTests:SourceStampVerifierTest
Change-Id: I9e30dde9cebe97ff7b92f02feb5c82b3a58a97b1
This commit is contained in:
Khaled Abdelmohsen
2020-09-11 16:15:45 +01:00
parent 56a7840578
commit 5aa65c1967
7 changed files with 295 additions and 162 deletions

View File

@@ -25,6 +25,7 @@ import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyA
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct;
import android.os.Build;
import android.util.ArrayMap;
@@ -53,7 +54,6 @@ import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -90,9 +90,10 @@ public class ApkSignatureSchemeV3Verifier {
* associated with each signer.
*
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
* @throws SecurityException if the APK Signature Scheme v3 signature of this APK does not
* verify.
* @throws IOException if an I/O error occurs while reading the APK file.
* @throws SecurityException if the APK Signature Scheme v3 signature of this APK does
* not
* verify.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
public static VerifiedSigner verify(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
@@ -106,7 +107,7 @@ public class ApkSignatureSchemeV3Verifier {
* Block while gathering signer information. The APK contents are not verified.
*
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
* @throws IOException if an I/O error occurs while reading the APK file.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
public static VerifiedSigner unsafeGetCertsWithoutVerification(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
@@ -125,9 +126,10 @@ public class ApkSignatureSchemeV3Verifier {
* associated with each signer.
*
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
* @throws SecurityException if an APK Signature Scheme v3 signature of this APK does not
* verify.
* @throws IOException if an I/O error occurs while reading the APK file.
* @throws SecurityException if an APK Signature Scheme v3 signature of this APK does
* not
* verify.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
@@ -140,7 +142,7 @@ public class ApkSignatureSchemeV3Verifier {
* additional information relevant for verifying the block against the file.
*
* @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
* @throws IOException if an I/O error occurs while reading the APK file.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
public static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
@@ -152,7 +154,7 @@ public class ApkSignatureSchemeV3Verifier {
* Block.
*
* @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it
* against the APK file.
* against the APK file.
*/
private static VerifiedSigner verify(
RandomAccessFile apk,
@@ -160,7 +162,7 @@ public class ApkSignatureSchemeV3Verifier {
boolean doVerifyIntegrity) throws SecurityException, IOException {
int signerCount = 0;
Map<Integer, byte[]> contentDigests = new ArrayMap<>();
Pair<X509Certificate[], VerifiedProofOfRotation> result = null;
Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation> result = null;
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
@@ -215,10 +217,11 @@ public class ApkSignatureSchemeV3Verifier {
return new VerifiedSigner(result.first, result.second, verityRootHash, contentDigests);
}
private static Pair<X509Certificate[], VerifiedProofOfRotation> verifySigner(
ByteBuffer signerBlock,
Map<Integer, byte[]> contentDigests,
CertificateFactory certFactory)
private static Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation>
verifySigner(
ByteBuffer signerBlock,
Map<Integer, byte[]> contentDigests,
CertificateFactory certFactory)
throws SecurityException, IOException, PlatformNotSupportedException {
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
int minSdkVersion = signerBlock.getInt();
@@ -228,9 +231,9 @@ public class ApkSignatureSchemeV3Verifier {
// this signature isn't meant to be used with this platform, skip it.
throw new PlatformNotSupportedException(
"Signer not supported by this platform "
+ "version. This platform: " + Build.VERSION.SDK_INT
+ ", signer minSdkVersion: " + minSdkVersion
+ ", maxSdkVersion: " + maxSdkVersion);
+ "version. This platform: " + Build.VERSION.SDK_INT
+ ", signer minSdkVersion: " + minSdkVersion
+ ", maxSdkVersion: " + maxSdkVersion);
}
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
@@ -331,7 +334,8 @@ public class ApkSignatureSchemeV3Verifier {
&& (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
throw new SecurityException(
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ " contents digest does not match the digest specified by a preceding signer");
+ " contents digest does not match the digest specified by a "
+ "preceding signer");
}
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
@@ -379,11 +383,11 @@ public class ApkSignatureSchemeV3Verifier {
private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
private static Pair<X509Certificate[], VerifiedProofOfRotation> verifyAdditionalAttributes(
ByteBuffer attrs, List<X509Certificate> certs, CertificateFactory certFactory)
throws IOException {
private static Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation>
verifyAdditionalAttributes(ByteBuffer attrs, List<X509Certificate> certs,
CertificateFactory certFactory) throws IOException {
X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]);
VerifiedProofOfRotation por = null;
ApkSigningBlockUtils.VerifiedProofOfRotation por = null;
while (attrs.hasRemaining()) {
ByteBuffer attr = getLengthPrefixedSlice(attrs);
@@ -392,7 +396,7 @@ public class ApkSignatureSchemeV3Verifier {
+ "ID. Remaining: " + attr.remaining());
}
int id = attr.getInt();
switch(id) {
switch (id) {
case PROOF_OF_ROTATION_ATTR_ID:
if (por != null) {
throw new SecurityException("Encountered multiple Proof-of-rotation records"
@@ -404,7 +408,7 @@ public class ApkSignatureSchemeV3Verifier {
try {
if (por.certs.size() > 0
&& !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(),
certChain[0].getEncoded())) {
certChain[0].getEncoded())) {
throw new SecurityException("Terminal certificate in Proof-of-rotation"
+ " record does not match APK signing certificate");
}
@@ -422,96 +426,6 @@ public class ApkSignatureSchemeV3Verifier {
return Pair.create(certChain, por);
}
private static VerifiedProofOfRotation verifyProofOfRotationStruct(
ByteBuffer porBuf,
CertificateFactory certFactory)
throws SecurityException, IOException {
int levelCount = 0;
int lastSigAlgorithm = -1;
X509Certificate lastCert = null;
List<X509Certificate> certs = new ArrayList<>();
List<Integer> flagsList = new ArrayList<>();
// Proof-of-rotation struct:
// A uint32 version code followed by basically a singly linked list of nodes, called levels
// here, each of which have the following structure:
// * length-prefix for the entire level
// - length-prefixed signed data (if previous level exists)
// * length-prefixed X509 Certificate
// * uint32 signature algorithm ID describing how this signed data was signed
// - uint32 flags describing how to treat the cert contained in this level
// - uint32 signature algorithm ID to use to verify the signature of the next level. The
// algorithm here must match the one in the signed data section of the next level.
// - length-prefixed signature over the signed data in this level. The signature here
// is verified using the certificate from the previous level.
// The linking is provided by the certificate of each level signing the one of the next.
try {
// get the version code, but don't do anything with it: creator knew about all our flags
porBuf.getInt();
HashSet<X509Certificate> certHistorySet = new HashSet<>();
while (porBuf.hasRemaining()) {
levelCount++;
ByteBuffer level = getLengthPrefixedSlice(porBuf);
ByteBuffer signedData = getLengthPrefixedSlice(level);
int flags = level.getInt();
int sigAlgorithm = level.getInt();
byte[] signature = readLengthPrefixedByteArray(level);
if (lastCert != null) {
// Use previous level cert to verify current level
Pair<String, ? extends AlgorithmParameterSpec> sigAlgParams =
getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm);
PublicKey publicKey = lastCert.getPublicKey();
Signature sig = Signature.getInstance(sigAlgParams.first);
sig.initVerify(publicKey);
if (sigAlgParams.second != null) {
sig.setParameter(sigAlgParams.second);
}
sig.update(signedData);
if (!sig.verify(signature)) {
throw new SecurityException("Unable to verify signature of certificate #"
+ levelCount + " using " + sigAlgParams.first + " when verifying"
+ " Proof-of-rotation record");
}
}
signedData.rewind();
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
int signedSigAlgorithm = signedData.getInt();
if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) {
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ levelCount + " when verifying Proof-of-rotation record");
}
lastCert = (X509Certificate)
certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
lastCert = new VerbatimX509Certificate(lastCert, encodedCert);
lastSigAlgorithm = sigAlgorithm;
if (certHistorySet.contains(lastCert)) {
throw new SecurityException("Encountered duplicate entries in "
+ "Proof-of-rotation record at certificate #" + levelCount + ". All "
+ "signing certificates should be unique");
}
certHistorySet.add(lastCert);
certs.add(lastCert);
flagsList.add(flags);
}
} catch (IOException | BufferUnderflowException e) {
throw new IOException("Failed to parse Proof-of-rotation record", e);
} catch (NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e) {
throw new SecurityException(
"Failed to verify signature over signed data for certificate #"
+ levelCount + " when verifying Proof-of-rotation record", e);
} catch (CertificateException e) {
throw new SecurityException("Failed to decode certificate #" + levelCount
+ " when verifying Proof-of-rotation record", e);
}
return new VerifiedProofOfRotation(certs, flagsList);
}
static byte[] getVerityRootHash(String apkPath)
throws IOException, SignatureNotFoundException, SecurityException {
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
@@ -523,7 +437,7 @@ public class ApkSignatureSchemeV3Verifier {
static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
throws IOException, SignatureNotFoundException, SecurityException, DigestException,
NoSuchAlgorithmException {
NoSuchAlgorithmException {
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
SignatureInfo signatureInfo = findSignature(apk);
return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo);
@@ -532,7 +446,7 @@ public class ApkSignatureSchemeV3Verifier {
static byte[] generateApkVerityRootHash(String apkPath)
throws NoSuchAlgorithmException, DigestException, IOException,
SignatureNotFoundException {
SignatureNotFoundException {
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
SignatureInfo signatureInfo = findSignature(apk);
VerifiedSigner vSigner = verify(apk, false);
@@ -544,21 +458,6 @@ public class ApkSignatureSchemeV3Verifier {
}
}
/**
* Verified processed proof of rotation.
*
* @hide for internal use only.
*/
public static class VerifiedProofOfRotation {
public final List<X509Certificate> certs;
public final List<Integer> flagsList;
public VerifiedProofOfRotation(List<X509Certificate> certs, List<Integer> flagsList) {
this.certs = certs;
this.flagsList = flagsList;
}
}
/**
* Verified APK Signature Scheme v3 signer, including the proof of rotation structure.
*
@@ -566,14 +465,15 @@ public class ApkSignatureSchemeV3Verifier {
*/
public static class VerifiedSigner {
public final X509Certificate[] certs;
public final VerifiedProofOfRotation por;
public final ApkSigningBlockUtils.VerifiedProofOfRotation por;
public final byte[] verityRootHash;
// Algorithm -> digest map of signed digests in the signature.
// All these are verified if requested.
public final Map<Integer, byte[]> contentDigests;
public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por,
public VerifiedSigner(X509Certificate[] certs,
ApkSigningBlockUtils.VerifiedProofOfRotation por,
byte[] verityRootHash, Map<Integer, byte[]> contentDigests) {
this.certs = certs;
this.por = por;

View File

@@ -19,6 +19,7 @@ package android.util.apk;
import android.util.ArrayMap;
import android.util.Pair;
import java.io.ByteArrayInputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.RandomAccessFile;
@@ -26,12 +27,23 @@ import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.DigestException;
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.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
/**
@@ -51,9 +63,8 @@ public final class ApkSigningBlockUtils {
* @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
* identifying the appropriate block to find, e.g. the APK Signature Scheme v2
* block ID.
*
* @throws SignatureNotFoundException if the APK is not signed using this scheme.
* @throws IOException if an I/O error occurs while reading the APK file.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
static SignatureInfo findSignature(RandomAccessFile apk, int blockId)
throws IOException, SignatureNotFoundException {
@@ -377,7 +388,7 @@ public final class ApkSigningBlockUtils {
/**
* Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
*
* @throws IOException if an I/O error occurs while reading the file.
* @throws IOException if an I/O error occurs while reading the file.
* @throws SignatureNotFoundException if the EoCD could not be found.
*/
static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk)
@@ -398,13 +409,13 @@ public final class ApkSigningBlockUtils {
if (centralDirOffset > eocdOffset) {
throw new SignatureNotFoundException(
"ZIP Central Directory offset out of range: " + centralDirOffset
+ ". ZIP End of Central Directory offset: " + eocdOffset);
+ ". ZIP End of Central Directory offset: " + eocdOffset);
}
long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd);
if (centralDirOffset + centralDirSize != eocdOffset) {
throw new SignatureNotFoundException(
"ZIP Central Directory is not immediately followed by End of Central"
+ " Directory");
+ " Directory");
}
return centralDirOffset;
}
@@ -687,7 +698,7 @@ public final class ApkSigningBlockUtils {
static Pair<ByteBuffer, Long> findApkSigningBlock(
RandomAccessFile apk, long centralDirOffset)
throws IOException, SignatureNotFoundException {
throws IOException, SignatureNotFoundException {
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
@@ -806,4 +817,108 @@ public final class ApkSigningBlockUtils {
}
}
static VerifiedProofOfRotation verifyProofOfRotationStruct(
ByteBuffer porBuf,
CertificateFactory certFactory)
throws SecurityException, IOException {
int levelCount = 0;
int lastSigAlgorithm = -1;
X509Certificate lastCert = null;
List<X509Certificate> certs = new ArrayList<>();
List<Integer> flagsList = new ArrayList<>();
// Proof-of-rotation struct:
// A uint32 version code followed by basically a singly linked list of nodes, called levels
// here, each of which have the following structure:
// * length-prefix for the entire level
// - length-prefixed signed data (if previous level exists)
// * length-prefixed X509 Certificate
// * uint32 signature algorithm ID describing how this signed data was signed
// - uint32 flags describing how to treat the cert contained in this level
// - uint32 signature algorithm ID to use to verify the signature of the next level. The
// algorithm here must match the one in the signed data section of the next level.
// - length-prefixed signature over the signed data in this level. The signature here
// is verified using the certificate from the previous level.
// The linking is provided by the certificate of each level signing the one of the next.
try {
// get the version code, but don't do anything with it: creator knew about all our flags
porBuf.getInt();
HashSet<X509Certificate> certHistorySet = new HashSet<>();
while (porBuf.hasRemaining()) {
levelCount++;
ByteBuffer level = getLengthPrefixedSlice(porBuf);
ByteBuffer signedData = getLengthPrefixedSlice(level);
int flags = level.getInt();
int sigAlgorithm = level.getInt();
byte[] signature = readLengthPrefixedByteArray(level);
if (lastCert != null) {
// Use previous level cert to verify current level
Pair<String, ? extends AlgorithmParameterSpec> sigAlgParams =
getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm);
PublicKey publicKey = lastCert.getPublicKey();
Signature sig = Signature.getInstance(sigAlgParams.first);
sig.initVerify(publicKey);
if (sigAlgParams.second != null) {
sig.setParameter(sigAlgParams.second);
}
sig.update(signedData);
if (!sig.verify(signature)) {
throw new SecurityException("Unable to verify signature of certificate #"
+ levelCount + " using " + sigAlgParams.first + " when verifying"
+ " Proof-of-rotation record");
}
}
signedData.rewind();
byte[] encodedCert = readLengthPrefixedByteArray(signedData);
int signedSigAlgorithm = signedData.getInt();
if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) {
throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ levelCount + " when verifying Proof-of-rotation record");
}
lastCert = (X509Certificate)
certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
lastCert = new VerbatimX509Certificate(lastCert, encodedCert);
lastSigAlgorithm = sigAlgorithm;
if (certHistorySet.contains(lastCert)) {
throw new SecurityException("Encountered duplicate entries in "
+ "Proof-of-rotation record at certificate #" + levelCount + ". All "
+ "signing certificates should be unique");
}
certHistorySet.add(lastCert);
certs.add(lastCert);
flagsList.add(flags);
}
} catch (IOException | BufferUnderflowException e) {
throw new IOException("Failed to parse Proof-of-rotation record", e);
} catch (NoSuchAlgorithmException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e) {
throw new SecurityException(
"Failed to verify signature over signed data for certificate #"
+ levelCount + " when verifying Proof-of-rotation record", e);
} catch (CertificateException e) {
throw new SecurityException("Failed to decode certificate #" + levelCount
+ " when verifying Proof-of-rotation record", e);
}
return new VerifiedProofOfRotation(certs, flagsList);
}
/**
* Verified processed proof of rotation.
*
* @hide for internal use only.
*/
public static class VerifiedProofOfRotation {
public final List<X509Certificate> certs;
public final List<Integer> flagsList;
public VerifiedProofOfRotation(List<X509Certificate> certs, List<Integer> flagsList) {
this.certs = certs;
this.flagsList = flagsList;
}
}
}

View File

@@ -19,6 +19,8 @@ package android.util.apk;
import android.annotation.Nullable;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.List;
/**
* A class encapsulating the result from the source stamp verifier
@@ -32,12 +34,15 @@ public final class SourceStampVerificationResult {
private final boolean mPresent;
private final boolean mVerified;
private final Certificate mCertificate;
private final List<? extends Certificate> mCertificateLineage;
private SourceStampVerificationResult(
boolean present, boolean verified, @Nullable Certificate certificate) {
boolean present, boolean verified, @Nullable Certificate certificate,
List<? extends Certificate> certificateLineage) {
this.mPresent = present;
this.mVerified = verified;
this.mCertificate = certificate;
this.mCertificateLineage = certificateLineage;
}
public boolean isPresent() {
@@ -52,6 +57,10 @@ public final class SourceStampVerificationResult {
return mCertificate;
}
public List<? extends Certificate> getCertificateLineage() {
return mCertificateLineage;
}
/**
* Create a non-present source stamp outcome.
*
@@ -59,18 +68,21 @@ public final class SourceStampVerificationResult {
*/
public static SourceStampVerificationResult notPresent() {
return new SourceStampVerificationResult(
/* present= */ false, /* verified= */ false, /* certificate= */ null);
/* present= */ false, /* verified= */ false, /* certificate= */
null, /* certificateLineage= */ Collections.emptyList());
}
/**
* Create a verified source stamp outcome.
*
* @param certificate The source stamp certificate.
* @param certificate The source stamp certificate.
* @param certificateLineage The proof-of-rotation lineage for the source stamp.
* @return A verified source stamp result, and the source stamp certificate.
*/
public static SourceStampVerificationResult verified(Certificate certificate) {
public static SourceStampVerificationResult verified(Certificate certificate,
List<? extends Certificate> certificateLineage) {
return new SourceStampVerificationResult(
/* present= */ true, /* verified= */ true, certificate);
/* present= */ true, /* verified= */ true, certificate, certificateLineage);
}
/**
@@ -80,6 +92,7 @@ public final class SourceStampVerificationResult {
*/
public static SourceStampVerificationResult notVerified() {
return new SourceStampVerificationResult(
/* present= */ true, /* verified= */ false, /* certificate= */ null);
/* present= */ true, /* verified= */ false, /* certificate= */
null, /* certificateLineage= */ Collections.emptyList());
}
}

View File

@@ -23,6 +23,7 @@ import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContent
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct;
import android.util.Pair;
import android.util.Slog;
@@ -44,12 +45,14 @@ import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -76,6 +79,7 @@ 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 = 0x6dff800d;
private static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
private static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
private static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
@@ -85,11 +89,13 @@ public abstract class SourceStampVerifier {
private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
/** Hidden constructor to prevent instantiation. */
private SourceStampVerifier() {}
private SourceStampVerifier() {
}
/** Verifies SourceStamp present in a list of APKs. */
/** Verifies SourceStamp present in a list of (split) APKs for the same app. */
public static SourceStampVerificationResult verify(List<String> apkFiles) {
Certificate stampCertificate = null;
List<? extends Certificate> stampCertificateLineage = Collections.emptyList();
for (String apkFile : apkFiles) {
SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile);
if (!sourceStampVerificationResult.isPresent()
@@ -97,12 +103,15 @@ public abstract class SourceStampVerifier {
return sourceStampVerificationResult;
}
if (stampCertificate != null
&& !stampCertificate.equals(sourceStampVerificationResult.getCertificate())) {
&& (!stampCertificate.equals(sourceStampVerificationResult.getCertificate())
|| !stampCertificateLineage.equals(
sourceStampVerificationResult.getCertificateLineage()))) {
return SourceStampVerificationResult.notVerified();
}
stampCertificate = sourceStampVerificationResult.getCertificate();
stampCertificateLineage = sourceStampVerificationResult.getCertificateLineage();
}
return SourceStampVerificationResult.verified(stampCertificate);
return SourceStampVerificationResult.verified(stampCertificate, stampCertificateLineage);
}
/** Verifies SourceStamp present in the provided APK. */
@@ -177,21 +186,44 @@ public abstract class SourceStampVerifier {
"No signatures found for signature scheme %d",
signatureSchemeDigest.getKey()));
}
ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(
signedSignatureSchemeData.get(signatureSchemeDigest.getKey()));
verifySourceStampSignature(
signedSignatureSchemeData.get(signatureSchemeDigest.getKey()),
signatureSchemeDigest.getValue(),
sourceStampCertificate,
signatureSchemeDigest.getValue());
signatures);
}
return SourceStampVerificationResult.verified(sourceStampCertificate);
List<? extends Certificate> sourceStampCertificateLineage = Collections.emptyList();
if (sourceStampBlockData.hasRemaining()) {
// The stamp block contains some additional attributes.
ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
stampAttributeData.get(stampAttributeBytes);
stampAttributeData.flip();
verifySourceStampSignature(stampAttributeBytes, sourceStampCertificate,
stampAttributeDataSignatures);
ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation =
verifySourceStampAttributes(stampAttributeData, sourceStampCertificate);
if (verifiedProofOfRotation != null) {
sourceStampCertificateLineage = verifiedProofOfRotation.certs;
}
}
return SourceStampVerificationResult.verified(sourceStampCertificate,
sourceStampCertificateLineage);
}
/**
* Verify the SourceStamp certificate found in the signing block is the same as the SourceStamp
* certificate found in the APK. It returns the verified certificate.
*
* @param sourceStampBlockData the source stamp block in the APK signing block which contains
* the certificate used to sign the stamp digests.
* @param sourceStampBlockData the source stamp block in the APK signing block which
* contains
* the certificate used to sign the stamp digests.
* @param sourceStampCertificateDigest the source stamp certificate digest found in the APK.
*/
private static X509Certificate verifySourceStampCertificate(
@@ -230,16 +262,16 @@ public abstract class SourceStampVerifier {
* Verify the SourceStamp signature found in the signing block is signed by the SourceStamp
* certificate found in the APK.
*
* @param signedBlockData the source stamp block in the APK signing block which contains the
* stamp signed digests.
* @param data the digest to be verified being signed by the source stamp
* certificate.
* @param sourceStampCertificate the source stamp certificate used to sign the stamp digests.
* @param digest the digest to be verified being signed by the source stamp certificate.
* @param signatures the source stamp block in the APK signing block which contains
* the stamp signed digests.
*/
private static void verifySourceStampSignature(
ByteBuffer signedBlockData, X509Certificate sourceStampCertificate, byte[] digest)
private static void verifySourceStampSignature(byte[] data,
X509Certificate sourceStampCertificate, ByteBuffer signatures)
throws IOException {
// Parse the signatures block and identify supported signatures
ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedBlockData);
int signatureCount = 0;
int bestSigAlgorithm = -1;
byte[] bestSigAlgorithmSignatureBytes = null;
@@ -285,7 +317,7 @@ public abstract class SourceStampVerifier {
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(digest);
sig.update(data);
sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
} catch (InvalidKeyException
| InvalidAlgorithmParameterException
@@ -414,6 +446,46 @@ public abstract class SourceStampVerifier {
return result.array();
}
private static ApkSigningBlockUtils.VerifiedProofOfRotation verifySourceStampAttributes(
ByteBuffer stampAttributeData,
X509Certificate sourceStampCertificate)
throws IOException {
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
}
ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = null;
while (stampAttributes.hasRemaining()) {
ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
int id = attribute.getInt();
if (id == PROOF_OF_ROTATION_ATTR_ID) {
if (verifiedProofOfRotation != null) {
throw new SecurityException("Encountered multiple Proof-of-rotation records"
+ " when verifying source stamp signature");
}
verifiedProofOfRotation = verifyProofOfRotationStruct(attribute, certFactory);
// Make sure that the last certificate in the Proof-of-rotation record matches
// the one used to sign this APK.
try {
if (verifiedProofOfRotation.certs.size() > 0
&& !Arrays.equals(verifiedProofOfRotation.certs.get(
verifiedProofOfRotation.certs.size() - 1).getEncoded(),
sourceStampCertificate.getEncoded())) {
throw new SecurityException("Terminal certificate in Proof-of-rotation"
+ " record does not match source stamp certificate");
}
} catch (CertificateEncodingException e) {
throw new SecurityException("Failed to encode certificate when comparing"
+ " Proof-of-rotation record and source stamp certificate", e);
}
}
}
return verifiedProofOfRotation;
}
private static byte[] computeSha256Digest(byte[] input) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");

View File

@@ -17,6 +17,7 @@
package android.util.apk;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@@ -198,6 +199,38 @@ public class SourceStampVerifierTest {
assertNull(result.getCertificate());
}
@Test
public void testSourceStamp_validStampLineage() throws Exception {
mPrimaryApk = getApk("SourceStampVerifierTest/stamp-lineage-valid.apk");
byte[] expectedStampCertHash = getSourceStampCertificateHashFromApk(mPrimaryApk);
SourceStampVerificationResult result =
SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath());
assertTrue(result.isPresent());
assertTrue(result.isVerified());
assertNotNull(result.getCertificate());
byte[] actualStampCertHash =
MessageDigest.getInstance("SHA-256").digest(result.getCertificate().getEncoded());
assertArrayEquals(expectedStampCertHash, actualStampCertHash);
assertEquals(2, result.getCertificateLineage().size());
assertEquals(result.getCertificate(),
result.getCertificateLineage().get(result.getCertificateLineage().size() - 1));
}
@Test
public void testSourceStamp_invalidStampLineage() throws Exception {
mPrimaryApk = getApk("SourceStampVerifierTest/stamp-lineage-invalid.apk");
SourceStampVerificationResult result =
SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath());
assertTrue(result.isPresent());
assertFalse(result.isVerified());
assertNull(result.getCertificate());
assertTrue(result.getCertificateLineage().isEmpty());
}
private File getApk(String apkPath) throws IOException {
File apk = File.createTempFile("SourceStampApk", ".apk");
try (InputStream inputStream = mContext.getAssets().open(apkPath)) {