Loading core/java/com/android/internal/security/TEST_MAPPING +11 −0 Original line number Diff line number Diff line { "presubmit": [ { "name": "FrameworksCoreTests", "options": [ { "include-filter": "com.android.internal.security." }, { "include-annotation": "android.platform.test.annotations.Presubmit" } ] }, { "name": "ApkVerityTest", "file_patterns": ["VerityUtils\\.java"] Loading core/java/com/android/internal/security/VerityUtils.java +116 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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: * <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. Loading @@ -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, Loading core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java 0 → 100644 +46 −0 Original line number Diff line number Diff line /* * 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(); } } core/tests/coretests/src/com/android/internal/security/OWNERS 0 → 100644 +3 −0 Original line number Diff line number Diff line # Bug component: 36824 per-file VerityUtilsTest.java = file:platform/system/security:/fsverity/OWNERS core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java 0 → 100644 +334 −0 Original line number Diff line number Diff line /* * 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). * * <p>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. * * <p>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); } } Loading
core/java/com/android/internal/security/TEST_MAPPING +11 −0 Original line number Diff line number Diff line { "presubmit": [ { "name": "FrameworksCoreTests", "options": [ { "include-filter": "com.android.internal.security." }, { "include-annotation": "android.platform.test.annotations.Presubmit" } ] }, { "name": "ApkVerityTest", "file_patterns": ["VerityUtils\\.java"] Loading
core/java/com/android/internal/security/VerityUtils.java +116 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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: * <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. Loading @@ -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, Loading
core/tests/coretests/src/com/android/internal/security/ContentSignerWrapper.java 0 → 100644 +46 −0 Original line number Diff line number Diff line /* * 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(); } }
core/tests/coretests/src/com/android/internal/security/OWNERS 0 → 100644 +3 −0 Original line number Diff line number Diff line # Bug component: 36824 per-file VerityUtilsTest.java = file:platform/system/security:/fsverity/OWNERS
core/tests/coretests/src/com/android/internal/security/VerityUtilsTest.java 0 → 100644 +334 −0 Original line number Diff line number Diff line /* * 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). * * <p>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. * * <p>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); } }