1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.util.apk; 18 19 import static android.util.apk.ApkSignatureSchemeV3Verifier.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; 20 import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256; 21 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm; 22 import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; 23 import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm; 24 25 import android.os.incremental.IncrementalManager; 26 import android.os.incremental.V4Signature; 27 import android.util.ArrayMap; 28 import android.util.Pair; 29 30 import com.android.internal.security.VerityUtils; 31 32 import java.io.ByteArrayInputStream; 33 import java.io.EOFException; 34 import java.io.File; 35 import java.io.FileInputStream; 36 import java.io.IOException; 37 import java.security.DigestException; 38 import java.security.InvalidAlgorithmParameterException; 39 import java.security.InvalidKeyException; 40 import java.security.KeyFactory; 41 import java.security.NoSuchAlgorithmException; 42 import java.security.PublicKey; 43 import java.security.Signature; 44 import java.security.SignatureException; 45 import java.security.cert.Certificate; 46 import java.security.cert.CertificateException; 47 import java.security.cert.CertificateFactory; 48 import java.security.cert.X509Certificate; 49 import java.security.spec.AlgorithmParameterSpec; 50 import java.security.spec.InvalidKeySpecException; 51 import java.security.spec.X509EncodedKeySpec; 52 import java.util.Arrays; 53 import java.util.Map; 54 55 /** 56 * APK Signature Scheme v4 verifier. 57 * 58 * @hide for internal use only. 59 */ 60 public class ApkSignatureSchemeV4Verifier { 61 static final int APK_SIGNATURE_SCHEME_DEFAULT = 0xffffffff; 62 63 /** 64 * Extracts and verifies APK Signature Scheme v4 signature of the provided APK and returns the 65 * certificates associated with each signer. 66 */ extractCertificates(String apkFile)67 public static VerifiedSigner extractCertificates(String apkFile) 68 throws SignatureNotFoundException, SignatureException, SecurityException { 69 Pair<V4Signature.HashingInfo, V4Signature.SigningInfos> pair = extractSignature(apkFile); 70 return verify(apkFile, pair.first, pair.second, APK_SIGNATURE_SCHEME_DEFAULT); 71 } 72 73 /** 74 * Extracts APK Signature Scheme v4 signature of the provided APK. 75 */ extractSignature( String apkFile)76 public static Pair<V4Signature.HashingInfo, V4Signature.SigningInfos> extractSignature( 77 String apkFile) throws SignatureNotFoundException, SignatureException { 78 try { 79 final File apk = new File(apkFile); 80 boolean needsConsistencyCheck; 81 82 // 1. Try IncFS first. IncFS verifies the file according to the integrity metadata 83 // (including the root hash of Merkle tree) it keeps track of with signature check. No 84 // further consistentcy check is needed. 85 byte[] signatureBytes = IncrementalManager.unsafeGetFileSignature( 86 apk.getAbsolutePath()); 87 V4Signature signature; 88 if (signatureBytes != null && signatureBytes.length > 0) { 89 needsConsistencyCheck = false; 90 signature = V4Signature.readFrom(signatureBytes); 91 } else { 92 // 2. Try fs-verity next. fs-verity checks against the Merkle tree, but the 93 // v4 signature file (including a raw root hash) is managed separately. We need to 94 // ensure the signed data from the file is consistent with the actual file. 95 needsConsistencyCheck = true; 96 97 final File idsig = new File(apk.getAbsolutePath() + V4Signature.EXT); 98 try (var fis = new FileInputStream(idsig.getAbsolutePath())) { 99 signature = V4Signature.readFrom(fis); 100 } catch (IOException e) { 101 throw new SignatureNotFoundException( 102 "Failed to obtain signature bytes from .idsig"); 103 } 104 } 105 if (!signature.isVersionSupported()) { 106 throw new SecurityException( 107 "v4 signature version " + signature.version + " is not supported"); 108 } 109 final V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray( 110 signature.hashingInfo); 111 final V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray( 112 signature.signingInfos); 113 114 if (needsConsistencyCheck) { 115 final byte[] actualDigest = VerityUtils.getFsverityDigest(apk.getAbsolutePath()); 116 if (actualDigest == null) { 117 throw new SecurityException("The APK does not have fs-verity"); 118 } 119 final byte[] computedDigest = 120 VerityUtils.generateFsVerityDigest(apk.length(), hashingInfo); 121 if (!Arrays.equals(computedDigest, actualDigest)) { 122 throw new SignatureException("Actual digest does not match the v4 signature"); 123 } 124 } 125 126 return Pair.create(hashingInfo, signingInfos); 127 } catch (EOFException e) { 128 throw new SignatureException("V4 signature is invalid.", e); 129 } catch (IOException e) { 130 throw new SignatureNotFoundException("Failed to read V4 signature.", e); 131 } catch (DigestException | NoSuchAlgorithmException e) { 132 throw new SecurityException("Failed to calculate the digest", e); 133 } 134 } 135 136 /** 137 * Verifies APK Signature Scheme v4 signature and returns the 138 * certificates associated with each signer. 139 */ verify(String apkFile, final V4Signature.HashingInfo hashingInfo, final V4Signature.SigningInfos signingInfos, final int v3BlockId)140 public static VerifiedSigner verify(String apkFile, final V4Signature.HashingInfo hashingInfo, 141 final V4Signature.SigningInfos signingInfos, final int v3BlockId) 142 throws SignatureNotFoundException, SecurityException { 143 final V4Signature.SigningInfo signingInfo = findSigningInfoForBlockId(signingInfos, 144 v3BlockId); 145 146 // Verify signed data and extract certificates and apk digest. 147 final byte[] signedData = V4Signature.getSignedData(new File(apkFile).length(), hashingInfo, 148 signingInfo); 149 final Pair<Certificate, byte[]> result = verifySigner(signingInfo, signedData); 150 151 // Populate digests enforced by IncFS driver and fs-verity. 152 Map<Integer, byte[]> contentDigests = new ArrayMap<>(); 153 contentDigests.put(convertToContentDigestType(hashingInfo.hashAlgorithm), 154 hashingInfo.rawRootHash); 155 156 return new VerifiedSigner(new Certificate[]{result.first}, result.second, contentDigests); 157 } 158 findSigningInfoForBlockId( final V4Signature.SigningInfos signingInfos, final int v3BlockId)159 private static V4Signature.SigningInfo findSigningInfoForBlockId( 160 final V4Signature.SigningInfos signingInfos, final int v3BlockId) 161 throws SignatureNotFoundException { 162 // Use default signingInfo for v3 block. 163 if (v3BlockId == APK_SIGNATURE_SCHEME_DEFAULT 164 || v3BlockId == APK_SIGNATURE_SCHEME_V3_BLOCK_ID) { 165 return signingInfos.signingInfo; 166 } 167 for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) { 168 if (v3BlockId == signingInfoBlock.blockId) { 169 try { 170 return V4Signature.SigningInfo.fromByteArray(signingInfoBlock.signingInfo); 171 } catch (IOException e) { 172 throw new SecurityException( 173 "Failed to read V4 signature block: " + signingInfoBlock.blockId, e); 174 } 175 } 176 } 177 throw new SecurityException( 178 "Failed to find V4 signature block corresponding to V3 blockId: " + v3BlockId); 179 } 180 verifySigner(V4Signature.SigningInfo signingInfo, final byte[] signedData)181 private static Pair<Certificate, byte[]> verifySigner(V4Signature.SigningInfo signingInfo, 182 final byte[] signedData) throws SecurityException { 183 if (!isSupportedSignatureAlgorithm(signingInfo.signatureAlgorithmId)) { 184 throw new SecurityException("No supported signatures found"); 185 } 186 187 final int signatureAlgorithmId = signingInfo.signatureAlgorithmId; 188 final byte[] signatureBytes = signingInfo.signature; 189 final byte[] publicKeyBytes = signingInfo.publicKey; 190 final byte[] encodedCert = signingInfo.certificate; 191 192 String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(signatureAlgorithmId); 193 Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams = 194 getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithmId); 195 String jcaSignatureAlgorithm = signatureAlgorithmParams.first; 196 AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second; 197 boolean sigVerified; 198 try { 199 PublicKey publicKey = 200 KeyFactory.getInstance(keyAlgorithm) 201 .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); 202 Signature sig = Signature.getInstance(jcaSignatureAlgorithm); 203 sig.initVerify(publicKey); 204 if (jcaSignatureAlgorithmParams != null) { 205 sig.setParameter(jcaSignatureAlgorithmParams); 206 } 207 sig.update(signedData); 208 sigVerified = sig.verify(signatureBytes); 209 } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException 210 | InvalidAlgorithmParameterException | SignatureException e) { 211 throw new SecurityException( 212 "Failed to verify " + jcaSignatureAlgorithm + " signature", e); 213 } 214 if (!sigVerified) { 215 throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); 216 } 217 218 // Signature over signedData has verified. 219 CertificateFactory certFactory; 220 try { 221 certFactory = CertificateFactory.getInstance("X.509"); 222 } catch (CertificateException e) { 223 throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); 224 } 225 226 X509Certificate certificate; 227 try { 228 certificate = (X509Certificate) 229 certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); 230 } catch (CertificateException e) { 231 throw new SecurityException("Failed to decode certificate", e); 232 } 233 certificate = new VerbatimX509Certificate(certificate, encodedCert); 234 235 byte[] certificatePublicKeyBytes = certificate.getPublicKey().getEncoded(); 236 if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) { 237 throw new SecurityException( 238 "Public key mismatch between certificate and signature record"); 239 } 240 241 return Pair.create(certificate, signingInfo.apkDigest); 242 } 243 convertToContentDigestType(int hashAlgorithm)244 private static int convertToContentDigestType(int hashAlgorithm) throws SecurityException { 245 if (hashAlgorithm == V4Signature.HASHING_ALGORITHM_SHA256) { 246 return CONTENT_DIGEST_VERITY_CHUNKED_SHA256; 247 } 248 throw new SecurityException("Unsupported hashAlgorithm: " + hashAlgorithm); 249 } 250 251 /** 252 * Verified APK Signature Scheme v4 signer, including V2/V3 digest. 253 * 254 * @hide for internal use only. 255 */ 256 public static class VerifiedSigner { 257 public final Certificate[] certs; 258 public final byte[] apkDigest; 259 260 // Algorithm -> digest map of signed digests in the signature. 261 // These are continuously enforced by the IncFS driver and fs-verity. 262 public final Map<Integer, byte[]> contentDigests; 263 VerifiedSigner(Certificate[] certs, byte[] apkDigest, Map<Integer, byte[]> contentDigests)264 public VerifiedSigner(Certificate[] certs, byte[] apkDigest, 265 Map<Integer, byte[]> contentDigests) { 266 this.certs = certs; 267 this.apkDigest = apkDigest; 268 this.contentDigests = contentDigests; 269 } 270 271 } 272 } 273