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