1 /*
2  * Copyright (C) 2018 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 com.android.internal.security;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Build;
22 import android.os.SystemProperties;
23 import android.os.incremental.V4Signature;
24 import android.system.Os;
25 import android.system.OsConstants;
26 import android.util.Slog;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
30 import com.android.internal.org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
31 import com.android.internal.org.bouncycastle.cms.CMSException;
32 import com.android.internal.org.bouncycastle.cms.CMSProcessableByteArray;
33 import com.android.internal.org.bouncycastle.cms.CMSSignedData;
34 import com.android.internal.org.bouncycastle.cms.SignerInformation;
35 import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier;
36 import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
37 import com.android.internal.org.bouncycastle.operator.OperatorCreationException;
38 
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.nio.ByteBuffer;
43 import java.nio.ByteOrder;
44 import java.nio.charset.StandardCharsets;
45 import java.security.DigestException;
46 import java.security.MessageDigest;
47 import java.security.NoSuchAlgorithmException;
48 import java.security.cert.CertificateException;
49 import java.security.cert.CertificateFactory;
50 import java.security.cert.X509Certificate;
51 
52 /** Provides fsverity related operations. */
53 public abstract class VerityUtils {
54     private static final String TAG = "VerityUtils";
55 
56     /**
57      * File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of
58      * foo.apk.
59      */
60     public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig";
61 
62     /** SHA256 hash size. */
63     private static final int HASH_SIZE_BYTES = 32;
64 
isFsVeritySupported()65     public static boolean isFsVeritySupported() {
66         return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
67                 || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
68     }
69 
70     /** Returns true if the given file looks like containing an fs-verity signature. */
isFsveritySignatureFile(File file)71     public static boolean isFsveritySignatureFile(File file) {
72         return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION);
73     }
74 
75     /** Returns the fs-verity signature file path of the given file. */
getFsveritySignatureFilePath(String filePath)76     public static String getFsveritySignatureFilePath(String filePath) {
77         return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
78     }
79 
80     /** Enables fs-verity for the file without signature. */
setUpFsverity(@onNull String filePath)81     public static void setUpFsverity(@NonNull String filePath) throws IOException {
82         int errno = enableFsverityNative(filePath);
83         if (errno != 0) {
84             throw new IOException("Failed to enable fs-verity on " + filePath + ": "
85                     + Os.strerror(errno));
86         }
87     }
88 
89     /** Enables fs-verity for an open file without signature. */
setUpFsverity(int fd)90     public static void setUpFsverity(int fd) throws IOException {
91         int errno = enableFsverityForFdNative(fd);
92         if (errno != 0) {
93             throw new IOException("Failed to enable fs-verity on FD(" + fd + "): "
94                     + Os.strerror(errno));
95         }
96     }
97 
98     /** Returns whether the file has fs-verity enabled. */
hasFsverity(@onNull String filePath)99     public static boolean hasFsverity(@NonNull String filePath) {
100         int retval = statxForFsverityNative(filePath);
101         if (retval < 0) {
102             Slog.e(TAG, "Failed to check whether fs-verity is enabled, errno " + -retval + ": "
103                     + filePath);
104             return false;
105         }
106         return (retval == 1);
107     }
108 
109     /**
110      * Verifies the signature over the fs-verity digest using the provided certificate.
111      *
112      * This method should only be used by any existing fs-verity use cases that require
113      * PKCS#7 signature verification, if backward compatibility is necessary.
114      *
115      * Since PKCS#7 is too flexible, for the current specific need, only specific configuration
116      * will be accepted:
117      * <ul>
118      *   <li>Must use SHA256 as the digest algorithm
119      *   <li>Must use rsaEncryption as signature algorithm
120      *   <li>Must be detached / without content
121      *   <li>Must not include any signed or unsigned attributes
122      * </ul>
123      *
124      * It is up to the caller to provide an appropriate/trusted certificate.
125      *
126      * @param signatureBlock byte array of a PKCS#7 detached signature
127      * @param digest fs-verity digest with the common configuration using sha256
128      * @param derCertInputStream an input stream of a X.509 certificate in DER
129      * @return whether the verification succeeds
130      */
verifyPkcs7DetachedSignature(@onNull byte[] signatureBlock, @NonNull byte[] digest, @NonNull InputStream derCertInputStream)131     public static boolean verifyPkcs7DetachedSignature(@NonNull byte[] signatureBlock,
132             @NonNull byte[] digest, @NonNull InputStream derCertInputStream) {
133         if (digest.length != 32) {
134             Slog.w(TAG, "Only sha256 is currently supported");
135             return false;
136         }
137 
138         try {
139             CMSSignedData signedData = new CMSSignedData(
140                     new CMSProcessableByteArray(toFormattedDigest(digest)),
141                     signatureBlock);
142 
143             if (!signedData.isDetachedSignature()) {
144                 Slog.w(TAG, "Expect only detached siganture");
145                 return false;
146             }
147             if (!signedData.getCertificates().getMatches(null).isEmpty()) {
148                 Slog.w(TAG, "Expect no certificate in signature");
149                 return false;
150             }
151             if (!signedData.getCRLs().getMatches(null).isEmpty()) {
152                 Slog.w(TAG, "Expect no CRL in signature");
153                 return false;
154             }
155 
156             X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
157                     .generateCertificate(derCertInputStream);
158             SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder()
159                     .build(trustedCert);
160 
161             // Verify any signature with the trusted certificate.
162             for (SignerInformation si : signedData.getSignerInfos().getSigners()) {
163                 // To be the most strict while dealing with the complicated PKCS#7 signature, reject
164                 // everything we don't need.
165                 if (si.getSignedAttributes() != null && si.getSignedAttributes().size() > 0) {
166                     Slog.w(TAG, "Unexpected signed attributes");
167                     return false;
168                 }
169                 if (si.getUnsignedAttributes() != null && si.getUnsignedAttributes().size() > 0) {
170                     Slog.w(TAG, "Unexpected unsigned attributes");
171                     return false;
172                 }
173                 if (!NISTObjectIdentifiers.id_sha256.getId().equals(si.getDigestAlgOID())) {
174                     Slog.w(TAG, "Unsupported digest algorithm OID: " + si.getDigestAlgOID());
175                     return false;
176                 }
177                 if (!PKCSObjectIdentifiers.rsaEncryption.getId().equals(si.getEncryptionAlgOID())) {
178                     Slog.w(TAG, "Unsupported encryption algorithm OID: "
179                             + si.getEncryptionAlgOID());
180                     return false;
181                 }
182 
183                 if (si.verify(verifier)) {
184                     return true;
185                 }
186             }
187             return false;
188         } catch (CertificateException | CMSException | OperatorCreationException e) {
189             Slog.w(TAG, "Error occurred during the PKCS#7 signature verification", e);
190         }
191         return false;
192     }
193 
194     /**
195      * Returns fs-verity digest for the file if enabled, otherwise returns null. The digest is a
196      * hash of root hash of fs-verity's Merkle tree with extra metadata.
197      *
198      * @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#file-digest-computation">
199      *      File digest computation in Linux kernel documentation</a>
200      * @return Bytes of fs-verity digest, or null if the file does not have fs-verity enabled
201      */
getFsverityDigest(@onNull String filePath)202     public static @Nullable byte[] getFsverityDigest(@NonNull String filePath) {
203         byte[] result = new byte[HASH_SIZE_BYTES];
204         int retval = measureFsverityNative(filePath, result);
205         if (retval < 0) {
206             if (retval != -OsConstants.ENODATA) {
207                 Slog.e(TAG, "Failed to measure fs-verity, errno " + -retval + ": " + filePath);
208             }
209             return null;
210         }
211         return result;
212     }
213 
214     /**
215      * Generates an fs-verity digest from a V4Signature.HashingInfo and the file's size.
216      */
generateFsVerityDigest(long fileSize, @NonNull V4Signature.HashingInfo hashingInfo)217     public static @NonNull byte[] generateFsVerityDigest(long fileSize,
218             @NonNull V4Signature.HashingInfo hashingInfo)
219             throws DigestException, NoSuchAlgorithmException {
220         if (hashingInfo.rawRootHash == null || hashingInfo.rawRootHash.length != 32) {
221             throw new IllegalArgumentException("Expect a 32-byte rootHash for SHA256");
222         }
223         if (hashingInfo.log2BlockSize != 12) {
224             throw new IllegalArgumentException(
225                     "Unsupported log2BlockSize: " + hashingInfo.log2BlockSize);
226         }
227 
228         var buffer = ByteBuffer.allocate(256);  // sizeof(fsverity_descriptor)
229         buffer.order(ByteOrder.LITTLE_ENDIAN);
230         buffer.put((byte) 1);                   // version
231         buffer.put((byte) 1);                   // Merkle tree hash algorithm, 1 for SHA256
232         buffer.put(hashingInfo.log2BlockSize);  // log2(block-size), only log2(4096) is supported
233         buffer.put((byte) 0);                   // size of salt in bytes; 0 if none
234         buffer.putInt(0);                       // reserved, must be 0
235         buffer.putLong(fileSize);               // size of file the Merkle tree is built over
236         buffer.put(hashingInfo.rawRootHash);    // Merkle tree root hash
237         // The rest are zeros, including the latter half of root hash unused for SHA256.
238 
239         return MessageDigest.getInstance("SHA-256").digest(buffer.array());
240     }
241 
242     /** @hide */
243     @VisibleForTesting
toFormattedDigest(byte[] digest)244     public static byte[] toFormattedDigest(byte[] digest) {
245         // Construct fsverity_formatted_digest used in fs-verity's built-in signature verification.
246         ByteBuffer buffer = ByteBuffer.allocate(12 + digest.length); // struct size + sha256 size
247         buffer.order(ByteOrder.LITTLE_ENDIAN);
248         buffer.put("FSVerity".getBytes(StandardCharsets.US_ASCII));
249         buffer.putShort((short) 1); // FS_VERITY_HASH_ALG_SHA256
250         buffer.putShort((short) digest.length);
251         buffer.put(digest);
252         return buffer.array();
253     }
254 
enableFsverityNative(@onNull String filePath)255     private static native int enableFsverityNative(@NonNull String filePath);
enableFsverityForFdNative(int fd)256     private static native int enableFsverityForFdNative(int fd);
measureFsverityNative(@onNull String filePath, @NonNull byte[] digest)257     private static native int measureFsverityNative(@NonNull String filePath,
258             @NonNull byte[] digest);
statxForFsverityNative(@onNull String filePath)259     private static native int statxForFsverityNative(@NonNull String filePath);
260 }
261