261 lines
11 KiB
Java
261 lines
11 KiB
Java
/*
|
|
* 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 com.android.internal.security;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.os.Build;
|
|
import android.os.SystemProperties;
|
|
import android.os.incremental.V4Signature;
|
|
import android.system.Os;
|
|
import android.system.OsConstants;
|
|
import android.util.Slog;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
|
|
import com.android.internal.org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
|
|
import com.android.internal.org.bouncycastle.cms.CMSException;
|
|
import com.android.internal.org.bouncycastle.cms.CMSProcessableByteArray;
|
|
import com.android.internal.org.bouncycastle.cms.CMSSignedData;
|
|
import com.android.internal.org.bouncycastle.cms.SignerInformation;
|
|
import com.android.internal.org.bouncycastle.cms.SignerInformationVerifier;
|
|
import com.android.internal.org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
|
import com.android.internal.org.bouncycastle.operator.OperatorCreationException;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.DigestException;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.cert.CertificateException;
|
|
import java.security.cert.CertificateFactory;
|
|
import java.security.cert.X509Certificate;
|
|
|
|
/** Provides fsverity related operations. */
|
|
public abstract class VerityUtils {
|
|
private static final String TAG = "VerityUtils";
|
|
|
|
/**
|
|
* File extension of the signature file. For example, foo.apk.fsv_sig is the signature file of
|
|
* foo.apk.
|
|
*/
|
|
public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig";
|
|
|
|
/** SHA256 hash size. */
|
|
private static final int HASH_SIZE_BYTES = 32;
|
|
|
|
public static boolean isFsVeritySupported() {
|
|
return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R
|
|
|| SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
|
|
}
|
|
|
|
/** Returns true if the given file looks like containing an fs-verity signature. */
|
|
public static boolean isFsveritySignatureFile(File file) {
|
|
return file.getName().endsWith(FSVERITY_SIGNATURE_FILE_EXTENSION);
|
|
}
|
|
|
|
/** Returns the fs-verity signature file path of the given file. */
|
|
public static String getFsveritySignatureFilePath(String filePath) {
|
|
return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
|
|
}
|
|
|
|
/** Enables fs-verity for the file without signature. */
|
|
public static void setUpFsverity(@NonNull String filePath) throws IOException {
|
|
int errno = enableFsverityNative(filePath);
|
|
if (errno != 0) {
|
|
throw new IOException("Failed to enable fs-verity on " + filePath + ": "
|
|
+ Os.strerror(errno));
|
|
}
|
|
}
|
|
|
|
/** Enables fs-verity for an open file without signature. */
|
|
public static void setUpFsverity(int fd) throws IOException {
|
|
int errno = enableFsverityForFdNative(fd);
|
|
if (errno != 0) {
|
|
throw new IOException("Failed to enable fs-verity on FD(" + fd + "): "
|
|
+ Os.strerror(errno));
|
|
}
|
|
}
|
|
|
|
/** Returns whether the file has fs-verity enabled. */
|
|
public static boolean hasFsverity(@NonNull String filePath) {
|
|
int retval = statxForFsverityNative(filePath);
|
|
if (retval < 0) {
|
|
Slog.e(TAG, "Failed to check whether fs-verity is enabled, errno " + -retval + ": "
|
|
+ filePath);
|
|
return false;
|
|
}
|
|
return (retval == 1);
|
|
}
|
|
|
|
/**
|
|
* Verifies the signature over the fs-verity digest using the provided certificate.
|
|
*
|
|
* This method should only be used by any existing fs-verity use cases that require
|
|
* PKCS#7 signature verification, if backward compatibility is necessary.
|
|
*
|
|
* Since PKCS#7 is too flexible, for the current specific need, only specific configuration
|
|
* will be accepted:
|
|
* <ul>
|
|
* <li>Must use SHA256 as the digest algorithm
|
|
* <li>Must use rsaEncryption as signature algorithm
|
|
* <li>Must be detached / without content
|
|
* <li>Must not include any signed or unsigned attributes
|
|
* </ul>
|
|
*
|
|
* It is up to the caller to provide an appropriate/trusted certificate.
|
|
*
|
|
* @param signatureBlock byte array of a PKCS#7 detached signature
|
|
* @param digest fs-verity digest with the common configuration using sha256
|
|
* @param derCertInputStream an input stream of a X.509 certificate in DER
|
|
* @return whether the verification succeeds
|
|
*/
|
|
public static boolean verifyPkcs7DetachedSignature(@NonNull byte[] signatureBlock,
|
|
@NonNull byte[] digest, @NonNull InputStream derCertInputStream) {
|
|
if (digest.length != 32) {
|
|
Slog.w(TAG, "Only sha256 is currently supported");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
CMSSignedData signedData = new CMSSignedData(
|
|
new CMSProcessableByteArray(toFormattedDigest(digest)),
|
|
signatureBlock);
|
|
|
|
if (!signedData.isDetachedSignature()) {
|
|
Slog.w(TAG, "Expect only detached siganture");
|
|
return false;
|
|
}
|
|
if (!signedData.getCertificates().getMatches(null).isEmpty()) {
|
|
Slog.w(TAG, "Expect no certificate in signature");
|
|
return false;
|
|
}
|
|
if (!signedData.getCRLs().getMatches(null).isEmpty()) {
|
|
Slog.w(TAG, "Expect no CRL in signature");
|
|
return false;
|
|
}
|
|
|
|
X509Certificate trustedCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
|
.generateCertificate(derCertInputStream);
|
|
SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder()
|
|
.build(trustedCert);
|
|
|
|
// Verify any signature with the trusted certificate.
|
|
for (SignerInformation si : signedData.getSignerInfos().getSigners()) {
|
|
// To be the most strict while dealing with the complicated PKCS#7 signature, reject
|
|
// everything we don't need.
|
|
if (si.getSignedAttributes() != null && si.getSignedAttributes().size() > 0) {
|
|
Slog.w(TAG, "Unexpected signed attributes");
|
|
return false;
|
|
}
|
|
if (si.getUnsignedAttributes() != null && si.getUnsignedAttributes().size() > 0) {
|
|
Slog.w(TAG, "Unexpected unsigned attributes");
|
|
return false;
|
|
}
|
|
if (!NISTObjectIdentifiers.id_sha256.getId().equals(si.getDigestAlgOID())) {
|
|
Slog.w(TAG, "Unsupported digest algorithm OID: " + si.getDigestAlgOID());
|
|
return false;
|
|
}
|
|
if (!PKCSObjectIdentifiers.rsaEncryption.getId().equals(si.getEncryptionAlgOID())) {
|
|
Slog.w(TAG, "Unsupported encryption algorithm OID: "
|
|
+ si.getEncryptionAlgOID());
|
|
return false;
|
|
}
|
|
|
|
if (si.verify(verifier)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
} catch (CertificateException | CMSException | OperatorCreationException e) {
|
|
Slog.w(TAG, "Error occurred during the PKCS#7 signature verification", e);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns fs-verity digest for the file if enabled, otherwise returns null. The digest is a
|
|
* hash of root hash of fs-verity's Merkle tree with extra metadata.
|
|
*
|
|
* @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#file-digest-computation">
|
|
* File digest computation in Linux kernel documentation</a>
|
|
* @return Bytes of fs-verity digest, or null if the file does not have fs-verity enabled
|
|
*/
|
|
public static @Nullable byte[] getFsverityDigest(@NonNull String filePath) {
|
|
byte[] result = new byte[HASH_SIZE_BYTES];
|
|
int retval = measureFsverityNative(filePath, result);
|
|
if (retval < 0) {
|
|
if (retval != -OsConstants.ENODATA) {
|
|
Slog.e(TAG, "Failed to measure fs-verity, errno " + -retval + ": " + filePath);
|
|
}
|
|
return null;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Generates an fs-verity digest from a V4Signature.HashingInfo and the file's size.
|
|
*/
|
|
public static @NonNull byte[] generateFsVerityDigest(long fileSize,
|
|
@NonNull V4Signature.HashingInfo hashingInfo)
|
|
throws DigestException, NoSuchAlgorithmException {
|
|
if (hashingInfo.rawRootHash == null || hashingInfo.rawRootHash.length != 32) {
|
|
throw new IllegalArgumentException("Expect a 32-byte rootHash for SHA256");
|
|
}
|
|
if (hashingInfo.log2BlockSize != 12) {
|
|
throw new IllegalArgumentException(
|
|
"Unsupported log2BlockSize: " + hashingInfo.log2BlockSize);
|
|
}
|
|
|
|
var buffer = ByteBuffer.allocate(256); // sizeof(fsverity_descriptor)
|
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
buffer.put((byte) 1); // version
|
|
buffer.put((byte) 1); // Merkle tree hash algorithm, 1 for SHA256
|
|
buffer.put(hashingInfo.log2BlockSize); // log2(block-size), only log2(4096) is supported
|
|
buffer.put((byte) 0); // size of salt in bytes; 0 if none
|
|
buffer.putInt(0); // reserved, must be 0
|
|
buffer.putLong(fileSize); // size of file the Merkle tree is built over
|
|
buffer.put(hashingInfo.rawRootHash); // Merkle tree root hash
|
|
// The rest are zeros, including the latter half of root hash unused for SHA256.
|
|
|
|
return MessageDigest.getInstance("SHA-256").digest(buffer.array());
|
|
}
|
|
|
|
/** @hide */
|
|
@VisibleForTesting
|
|
public static byte[] toFormattedDigest(byte[] digest) {
|
|
// Construct fsverity_formatted_digest used in fs-verity's built-in signature verification.
|
|
ByteBuffer buffer = ByteBuffer.allocate(12 + digest.length); // struct size + sha256 size
|
|
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
buffer.put("FSVerity".getBytes(StandardCharsets.US_ASCII));
|
|
buffer.putShort((short) 1); // FS_VERITY_HASH_ALG_SHA256
|
|
buffer.putShort((short) digest.length);
|
|
buffer.put(digest);
|
|
return buffer.array();
|
|
}
|
|
|
|
private static native int enableFsverityNative(@NonNull String filePath);
|
|
private static native int enableFsverityForFdNative(int fd);
|
|
private static native int measureFsverityNative(@NonNull String filePath,
|
|
@NonNull byte[] digest);
|
|
private static native int statxForFsverityNative(@NonNull String filePath);
|
|
}
|