blob: 3287ce8636f5859e77f048ba86cbe1f1e2dcec2b [file] [log] [blame]
/*
* Copyright (C) 2018 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.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
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;
import android.util.Pair;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.security.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
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.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
/**
* APK Signature Scheme v3 verifier.
*
* @hide for internal use only.
*/
public class ApkSignatureSchemeV3Verifier {
/**
* ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing.
*/
public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 3;
static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
static final int APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61;
/**
* Returns {@code true} if the provided APK contains an APK Signature Scheme V3 signature.
*
* <p><b>NOTE: This method does not verify the signature.</b>
*/
public static boolean hasSignature(String apkFile) throws IOException {
try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
findSignature(apk);
return true;
} catch (SignatureNotFoundException e) {
return false;
}
}
/**
* Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates
* 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.
*/
public static VerifiedSigner verify(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
return verify(apkFile, true);
}
/**
* Returns the certificates associated with each signer for the given APK without verification.
* This method is dangerous and should not be used, unless the caller is absolutely certain the
* APK is trusted. Specifically, verification is only done for the APK Signature Scheme v3
* 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.
*/
public static VerifiedSigner unsafeGetCertsWithoutVerification(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
return verify(apkFile, false);
}
private static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
return verify(apk, verifyIntegrity);
}
}
/**
* Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates
* 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.
*/
private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
ApkSignatureSchemeV3Verifier verifier = new ApkSignatureSchemeV3Verifier(apk,
verifyIntegrity);
try {
SignatureInfo signatureInfo = findSignature(apk, APK_SIGNATURE_SCHEME_V31_BLOCK_ID);
return verifier.verify(signatureInfo, APK_SIGNATURE_SCHEME_V31_BLOCK_ID);
} catch (SignatureNotFoundException ignored) {
// This is expected if the APK is not using v3.1 to target rotation.
} catch (PlatformNotSupportedException ignored) {
// This is expected if the APK is targeting a platform version later than that of the
// device for rotation.
}
try {
SignatureInfo signatureInfo = findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
return verifier.verify(signatureInfo, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
} catch (PlatformNotSupportedException e) {
throw new SecurityException(e);
}
}
/**
* Returns the APK Signature Scheme v3 block contained in the provided APK file and the
* 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.
*/
public static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
return findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
}
/*
* Returns the APK Signature Scheme v3 block in the provided {@code apk} file with the specified
* {@code blockId} and the 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
**/
private static SignatureInfo findSignature(RandomAccessFile apk, int blockId)
throws IOException, SignatureNotFoundException {
return ApkSigningBlockUtils.findSignature(apk, blockId);
}
private final RandomAccessFile mApk;
private final boolean mVerifyIntegrity;
private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty();
private int mSignerMinSdkVersion;
private int mBlockId;
private ApkSignatureSchemeV3Verifier(RandomAccessFile apk, boolean verifyIntegrity) {
mApk = apk;
mVerifyIntegrity = verifyIntegrity;
}
/**
* Verifies the contents of the provided APK file against the provided APK Signature Scheme v3
* Block.
*
* @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it
* against the APK file.
*/
private VerifiedSigner verify(SignatureInfo signatureInfo, int blockId)
throws SecurityException, IOException, PlatformNotSupportedException {
mBlockId = blockId;
int signerCount = 0;
Map<Integer, byte[]> contentDigests = new ArrayMap<>();
Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation> result = null;
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
}
ByteBuffer signers;
try {
signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
} catch (IOException e) {
throw new SecurityException("Failed to read list of signers", e);
}
while (signers.hasRemaining()) {
try {
ByteBuffer signer = getLengthPrefixedSlice(signers);
result = verifySigner(signer, contentDigests, certFactory);
signerCount++;
} catch (PlatformNotSupportedException e) {
// this signer is for a different platform, ignore it.
continue;
} catch (IOException | BufferUnderflowException | SecurityException e) {
throw new SecurityException(
"Failed to parse/verify signer #" + signerCount + " block",
e);
}
}
if (signerCount < 1 || result == null) {
// There must always be a valid signer targeting the device SDK version for a v3.0
// signature.
if (blockId == APK_SIGNATURE_SCHEME_V3_BLOCK_ID) {
throw new SecurityException("No signers found");
}
throw new PlatformNotSupportedException(
"None of the signers support the current platform version");
}
if (signerCount != 1) {
throw new SecurityException("APK Signature Scheme V3 only supports one signer: "
+ "multiple signers found.");
}
if (contentDigests.isEmpty()) {
throw new SecurityException("No content digests found");
}
if (mVerifyIntegrity) {
ApkSigningBlockUtils.verifyIntegrity(contentDigests, mApk, signatureInfo);
}
byte[] verityRootHash = null;
if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
verityDigest, mApk.getChannel().size(), signatureInfo);
}
return new VerifiedSigner(result.first, result.second, verityRootHash, contentDigests,
blockId);
}
private 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();
int maxSdkVersion = signerBlock.getInt();
if (Build.VERSION.SDK_INT < minSdkVersion || Build.VERSION.SDK_INT > maxSdkVersion) {
// if this is a v3.1 block then save the minimum SDK version for rotation for comparison
// against the v3.0 additional attribute.
if (mBlockId == APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
if (!mOptionalRotationMinSdkVersion.isPresent()
|| mOptionalRotationMinSdkVersion.getAsInt() > minSdkVersion) {
mOptionalRotationMinSdkVersion = OptionalInt.of(minSdkVersion);
}
}
// 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);
}
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
int signatureCount = 0;
int bestSigAlgorithm = -1;
byte[] bestSigAlgorithmSignatureBytes = null;
List<Integer> signaturesSigAlgorithms = new ArrayList<>();
while (signatures.hasRemaining()) {
signatureCount++;
try {
ByteBuffer signature = getLengthPrefixedSlice(signatures);
if (signature.remaining() < 8) {
throw new SecurityException("Signature record too short");
}
int sigAlgorithm = signature.getInt();
signaturesSigAlgorithms.add(sigAlgorithm);
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");
}
}
String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
boolean sigVerified;
try {
PublicKey publicKey =
KeyFactory.getInstance(keyAlgorithm)
.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
sig.initVerify(publicKey);
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
sig.update(signedData);
sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
| InvalidAlgorithmParameterException | SignatureException e) {
throw new SecurityException(
"Failed to verify " + jcaSignatureAlgorithm + " signature", e);
}
if (!sigVerified) {
throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
}
// Signature over signedData has verified.
byte[] contentDigest = null;
signedData.clear();
ByteBuffer digests = getLengthPrefixedSlice(signedData);
List<Integer> digestsSigAlgorithms = new ArrayList<>();
int digestCount = 0;
while (digests.hasRemaining()) {
digestCount++;
try {
ByteBuffer digest = getLengthPrefixedSlice(digests);
if (digest.remaining() < 8) {
throw new IOException("Record too short");
}
int sigAlgorithm = digest.getInt();
digestsSigAlgorithms.add(sigAlgorithm);
if (sigAlgorithm == bestSigAlgorithm) {
contentDigest = readLengthPrefixedByteArray(digest);
}
} catch (IOException | BufferUnderflowException e) {
throw new IOException("Failed to parse digest record #" + digestCount, e);
}
}
if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
throw new SecurityException(
"Signature algorithms don't match between digests and signatures records");
}
int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
if ((previousSignerDigest != null)
&& (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
throw new SecurityException(
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ " contents digest does not match the digest specified by a "
+ "preceding signer");
}
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
List<X509Certificate> certs = new ArrayList<>();
int certificateCount = 0;
while (certificates.hasRemaining()) {
certificateCount++;
byte[] encodedCert = readLengthPrefixedByteArray(certificates);
X509Certificate certificate;
try {
certificate = (X509Certificate)
certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
} catch (CertificateException e) {
throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
}
certificate = new VerbatimX509Certificate(certificate, encodedCert);
certs.add(certificate);
}
if (certs.isEmpty()) {
throw new SecurityException("No certificates listed");
}
X509Certificate mainCertificate = certs.get(0);
byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
throw new SecurityException(
"Public key mismatch between certificate and signature record");
}
int signedMinSDK = signedData.getInt();
if (signedMinSDK != minSdkVersion) {
throw new SecurityException(
"minSdkVersion mismatch between signed and unsigned in v3 signer block.");
}
mSignerMinSdkVersion = signedMinSDK;
int signedMaxSDK = signedData.getInt();
if (signedMaxSDK != maxSdkVersion) {
throw new SecurityException(
"maxSdkVersion mismatch between signed and unsigned in v3 signer block.");
}
ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
return verifyAdditionalAttributes(additionalAttrs, certs, certFactory);
}
private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
private static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02;
private static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba;
private Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation>
verifyAdditionalAttributes(ByteBuffer attrs, List<X509Certificate> certs,
CertificateFactory certFactory) throws IOException, PlatformNotSupportedException {
X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]);
ApkSigningBlockUtils.VerifiedProofOfRotation por = null;
while (attrs.hasRemaining()) {
ByteBuffer attr = getLengthPrefixedSlice(attrs);
if (attr.remaining() < 4) {
throw new IOException("Remaining buffer too short to contain additional attribute "
+ "ID. Remaining: " + attr.remaining());
}
int id = attr.getInt();
switch (id) {
case PROOF_OF_ROTATION_ATTR_ID:
if (por != null) {
throw new SecurityException("Encountered multiple Proof-of-rotation records"
+ " when verifying APK Signature Scheme v3 signature");
}
por = verifyProofOfRotationStruct(attr, certFactory);
// make sure that the last certificate in the Proof-of-rotation record matches
// the one used to sign this APK.
try {
if (por.certs.size() > 0
&& !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(),
certChain[0].getEncoded())) {
throw new SecurityException("Terminal certificate in Proof-of-rotation"
+ " record does not match APK signing certificate");
}
} catch (CertificateEncodingException e) {
throw new SecurityException("Failed to encode certificate when comparing"
+ " Proof-of-rotation record and signing certificate", e);
}
break;
case ROTATION_MIN_SDK_VERSION_ATTR_ID:
if (attr.remaining() < 4) {
throw new IOException(
"Remaining buffer too short to contain rotation minSdkVersion "
+ "value. Remaining: "
+ attr.remaining());
}
int attrRotationMinSdkVersion = attr.getInt();
if (!mOptionalRotationMinSdkVersion.isPresent()) {
throw new SecurityException(
"Expected a v3.1 signing block targeting SDK version "
+ attrRotationMinSdkVersion
+ ", but a v3.1 block was not found");
}
int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt();
if (rotationMinSdkVersion != attrRotationMinSdkVersion) {
throw new SecurityException(
"Expected a v3.1 signing block targeting SDK version "
+ attrRotationMinSdkVersion
+ ", but the v3.1 block was targeting "
+ rotationMinSdkVersion);
}
break;
case ROTATION_ON_DEV_RELEASE_ATTR_ID:
// This attribute should only be used in a v3.1 signer as it allows the v3.1
// signature scheme to target a platform under development.
if (mBlockId == APK_SIGNATURE_SCHEME_V31_BLOCK_ID) {
// A platform under development uses the same SDK version as the most
// recently released platform; if this platform's SDK version is the same as
// the minimum of the signer, then only allow this signer to be used if this
// is not a "REL" platform.
if (Build.VERSION.SDK_INT == mSignerMinSdkVersion
&& "REL".equals(Build.VERSION.CODENAME)) {
// Set the rotation-min-sdk-version to be verified against the stripping
// attribute in the v3.0 block.
mOptionalRotationMinSdkVersion = OptionalInt.of(mSignerMinSdkVersion);
throw new PlatformNotSupportedException(
"The device is running a release version of "
+ mSignerMinSdkVersion
+ ", but the signer is targeting a dev release");
}
}
break;
default:
// not the droid we're looking for, move along, move along.
break;
}
}
return Pair.create(certChain, por);
}
static byte[] getVerityRootHash(String apkPath)
throws IOException, SignatureNotFoundException, SecurityException {
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
SignatureInfo signatureInfo = findSignature(apk);
VerifiedSigner vSigner = verify(apk, false);
return vSigner.verityRootHash;
}
}
static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
throws IOException, SignatureNotFoundException, SecurityException, DigestException,
NoSuchAlgorithmException {
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
SignatureInfo signatureInfo = findSignature(apk);
return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo);
}
}
/**
* Verified APK Signature Scheme v3 signer, including the proof of rotation structure.
*
* @hide for internal use only.
*/
public static class VerifiedSigner {
public final X509Certificate[] certs;
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;
// ID of the signature block used to verify.
public final int blockId;
public VerifiedSigner(X509Certificate[] certs,
ApkSigningBlockUtils.VerifiedProofOfRotation por,
byte[] verityRootHash, Map<Integer, byte[]> contentDigests,
int blockId) {
this.certs = certs;
this.por = por;
this.verityRootHash = verityRootHash;
this.contentDigests = contentDigests;
this.blockId = blockId;
}
}
private static class PlatformNotSupportedException extends Exception {
PlatformNotSupportedException(String s) {
super(s);
}
}
}