Merge "Parse source stamp lineage in platform"
This commit is contained in:
committed by
Android (Google) Code Review
commit
f2afb166d4
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user