diff --git a/core/java/com/android/internal/security/TEST_MAPPING b/core/java/com/android/internal/security/TEST_MAPPING index 9a5e90e8681fb..803760c55e717 100644 --- a/core/java/com/android/internal/security/TEST_MAPPING +++ b/core/java/com/android/internal/security/TEST_MAPPING @@ -1,5 +1,16 @@ { "presubmit": [ + { + "name": "FrameworksCoreTests", + "options": [ + { + "include-filter": "com.android.internal.security." + }, + { + "include-annotation": "android.platform.test.annotations.Presubmit" + } + ] + }, { "name": "ApkVerityTest", "file_patterns": ["VerityUtils\\.java"] diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index cb5820f1ade95..7f45c098daba8 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -23,10 +23,28 @@ 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.nio.file.Files; import java.nio.file.Paths; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; /** Provides fsverity related operations. */ public abstract class VerityUtils { @@ -90,6 +108,91 @@ public abstract class VerityUtils { 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: + * + * + * 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. @@ -110,6 +213,19 @@ public abstract class VerityUtils { return result; } + /** @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, @NonNull byte[] pkcs7Signature); private static native int measureFsverityNative(@NonNull String filePath, diff --git a/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java b/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java new file mode 100644 index 0000000000000..0254afee35dd5 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 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 org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.operator.ContentSigner; + +import java.io.OutputStream; + +/** A wrapper class of ContentSigner */ +class ContentSignerWrapper implements ContentSigner { + private final ContentSigner mSigner; + + ContentSignerWrapper(ContentSigner wrapped) { + mSigner = wrapped; + } + + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() { + return mSigner.getAlgorithmIdentifier(); + } + + @Override + public OutputStream getOutputStream() { + return mSigner.getOutputStream(); + } + + @Override + public byte[] getSignature() { + return mSigner.getSignature(); + } +} diff --git a/core/tests/coretests/src/com/android/internal/security/OWNERS b/core/tests/coretests/src/com/android/internal/security/OWNERS new file mode 100644 index 0000000000000..4f4d8d7a09320 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/security/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 36824 + +per-file VerityUtilsTest.java = file:platform/system/security:/fsverity/OWNERS diff --git a/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java new file mode 100644 index 0000000000000..d1d8018fc4151 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2022 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.SignerInfoGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DigestCalculator; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcDigestCalculatorProvider; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public class VerityUtilsTest { + private static final byte[] SAMPLE_DIGEST = "12345678901234567890123456789012".getBytes(); + private static final byte[] FORMATTED_SAMPLE_DIGEST = toFormattedDigest(SAMPLE_DIGEST); + + KeyPair mKeyPair; + ContentSigner mContentSigner; + X509CertificateHolder mCertificateHolder; + byte[] mCertificateDerEncoded; + + @Before + public void setUp() throws Exception { + mKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + mContentSigner = newFsverityContentSigner(mKeyPair.getPrivate()); + mCertificateHolder = + newX509CertificateHolder(mContentSigner, mKeyPair.getPublic(), "Someone"); + mCertificateDerEncoded = mCertificateHolder.getEncoded(); + } + + @Test + public void testOnlyAcceptCorrectDigest() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + + byte[] anotherDigest = Arrays.copyOf(SAMPLE_DIGEST, SAMPLE_DIGEST.length); + anotherDigest[0] ^= (byte) 1; + + assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + assertFalse(verifySignature(pkcs7Signature, anotherDigest, mCertificateDerEncoded)); + } + + @Test + public void testDigestWithWrongSize() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + + byte[] digestTooShort = Arrays.copyOfRange(SAMPLE_DIGEST, 0, SAMPLE_DIGEST.length - 1); + assertFalse(verifySignature(pkcs7Signature, digestTooShort, mCertificateDerEncoded)); + + byte[] digestTooLong = Arrays.copyOfRange(SAMPLE_DIGEST, 0, SAMPLE_DIGEST.length + 1); + assertFalse(verifySignature(pkcs7Signature, digestTooLong, mCertificateDerEncoded)); + } + + @Test + public void testOnlyAcceptGoodSignature() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + + byte[] anotherDigest = Arrays.copyOf(SAMPLE_DIGEST, SAMPLE_DIGEST.length); + anotherDigest[0] ^= (byte) 1; + byte[] anotherPkcs7Signature = + generatePkcs7Signature( + mContentSigner, mCertificateHolder, toFormattedDigest(anotherDigest)); + + assertTrue(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + assertFalse(verifySignature(anotherPkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testOnlyValidCertCanVerify() throws Exception { + byte[] pkcs7Signature = + generatePkcs7Signature(mContentSigner, mCertificateHolder, FORMATTED_SAMPLE_DIGEST); + + var wrongKeyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + var wrongContentSigner = newFsverityContentSigner(wrongKeyPair.getPrivate()); + var wrongCertificateHolder = + newX509CertificateHolder(wrongContentSigner, wrongKeyPair.getPublic(), "Not Me"); + byte[] wrongCertificateDerEncoded = wrongCertificateHolder.getEncoded(); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, wrongCertificateDerEncoded)); + } + + @Test + public void testRejectSignatureWithContent() throws Exception { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder); + byte[] pkcs7SignatureNonDetached = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ true); + + assertFalse( + verifySignature(pkcs7SignatureNonDetached, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testRejectSignatureWithCertificate() throws Exception { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder); + generator.addCertificate(mCertificateHolder); + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse( + verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Ignore("No easy way to construct test data") + @Test + public void testRejectSignatureWithCRL() throws Exception { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(mContentSigner, mCertificateHolder); + + // The current bouncycastle version does not have an easy way to generate a CRL. + // TODO: enable the test once this is doable, e.g. with X509v2CRLBuilder. + // generator.addCRL(new X509CRLHolder(CertificateList.getInstance(new DERSequence(...)))); + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse( + verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testRejectUnsupportedSignatureAlgorithms() throws Exception { + var contentSigner = newFsverityContentSigner(mKeyPair.getPrivate(), "MD5withRSA", null); + var certificateHolder = + newX509CertificateHolder(contentSigner, mKeyPair.getPublic(), "Someone"); + byte[] pkcs7Signature = + generatePkcs7Signature(contentSigner, certificateHolder, FORMATTED_SAMPLE_DIGEST); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, certificateHolder.getEncoded())); + } + + @Test + public void testRejectUnsupportedDigestAlgorithm() throws Exception { + CMSSignedDataGenerator generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator( + newSignerInfoGenerator( + mContentSigner, + mCertificateHolder, + OIWObjectIdentifiers.idSHA1, + true)); // directSignature + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + @Test + public void testRejectAnySignerInfoAttributes() throws Exception { + var generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator( + newSignerInfoGenerator( + mContentSigner, + mCertificateHolder, + NISTObjectIdentifiers.id_sha256, + false)); // directSignature + byte[] pkcs7Signature = + generatePkcs7SignatureInternal( + generator, FORMATTED_SAMPLE_DIGEST, /* encapsulate */ false); + + assertFalse(verifySignature(pkcs7Signature, SAMPLE_DIGEST, mCertificateDerEncoded)); + } + + private static boolean verifySignature( + byte[] pkcs7Signature, byte[] fsverityDigest, byte[] certificateDerEncoded) { + return VerityUtils.verifyPkcs7DetachedSignature( + pkcs7Signature, fsverityDigest, new ByteArrayInputStream(certificateDerEncoded)); + } + + private static byte[] toFormattedDigest(byte[] digest) { + return VerityUtils.toFormattedDigest(digest); + } + + private static byte[] generatePkcs7Signature( + ContentSigner contentSigner, X509CertificateHolder certificateHolder, byte[] signedData) + throws IOException, CMSException, OperatorCreationException { + CMSSignedDataGenerator generator = + newFsveritySignedDataGenerator(contentSigner, certificateHolder); + return generatePkcs7SignatureInternal(generator, signedData, /* encapsulate */ false); + } + + private static byte[] generatePkcs7SignatureInternal( + CMSSignedDataGenerator generator, byte[] signedData, boolean encapsulate) + throws IOException, CMSException, OperatorCreationException { + CMSSignedData cmsSignedData = + generator.generate(new CMSProcessableByteArray(signedData), encapsulate); + return cmsSignedData.toASN1Structure().getEncoded(ASN1Encoding.DL); + } + + private static CMSSignedDataGenerator newFsveritySignedDataGenerator( + ContentSigner contentSigner, X509CertificateHolder certificateHolder) + throws IOException, CMSException, OperatorCreationException { + var generator = new CMSSignedDataGenerator(); + generator.addSignerInfoGenerator( + newSignerInfoGenerator( + contentSigner, + certificateHolder, + NISTObjectIdentifiers.id_sha256, + true)); // directSignature + return generator; + } + + private static SignerInfoGenerator newSignerInfoGenerator( + ContentSigner contentSigner, + X509CertificateHolder certificateHolder, + ASN1ObjectIdentifier digestAlgorithmId, + boolean directSignature) + throws IOException, CMSException, OperatorCreationException { + var provider = + new BcDigestCalculatorProvider() { + /** + * Allow the caller to override the digest algorithm, especially when the + * default does not work (i.e. BcDigestCalculatorProvider could return null). + * + *

For example, the current fs-verity signature has to use rsaEncryption for + * the signature algorithm, but BcDigestCalculatorProvider will return null, + * thus we need a way to override. + * + *

TODO: After bouncycastle 1.70, we can remove this override and just use + * {@code JcaSignerInfoGeneratorBuilder#setContentDigest}. + */ + @Override + public DigestCalculator get(AlgorithmIdentifier algorithm) + throws OperatorCreationException { + return super.get(new AlgorithmIdentifier(digestAlgorithmId)); + } + }; + var builder = + new JcaSignerInfoGeneratorBuilder(provider).setDirectSignature(directSignature); + return builder.build(contentSigner, certificateHolder); + } + + private static ContentSigner newFsverityContentSigner(PrivateKey privateKey) + throws OperatorCreationException { + // fs-verity expects the signature to have rsaEncryption as the exact algorithm, so + // override the default. + return newFsverityContentSigner( + privateKey, "SHA256withRSA", PKCSObjectIdentifiers.rsaEncryption); + } + + private static ContentSigner newFsverityContentSigner( + PrivateKey privateKey, + String signatureAlgorithm, + ASN1ObjectIdentifier signatureAlgorithmIdOverride) + throws OperatorCreationException { + if (signatureAlgorithmIdOverride != null) { + return new ContentSignerWrapper( + new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey)) { + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() { + return new AlgorithmIdentifier(signatureAlgorithmIdOverride); + } + }; + } else { + return new JcaContentSignerBuilder(signatureAlgorithm).build(privateKey); + } + } + + private static X509CertificateHolder newX509CertificateHolder( + ContentSigner contentSigner, PublicKey publicKey, String name) { + // Time doesn't really matter, as we only care about the key. + Instant now = Instant.now(); + + return new X509v3CertificateBuilder( + new X500Name("CN=Issuer " + name), + /* serial= */ BigInteger.valueOf(now.getEpochSecond()), + new Date(now.minus(Duration.ofDays(1)).toEpochMilli()), + new Date(now.plus(Duration.ofDays(1)).toEpochMilli()), + new X500Name("CN=Subject " + name), + SubjectPublicKeyInfo.getInstance(publicKey.getEncoded())) + .build(contentSigner); + } +}