Project import
diff --git a/verity/Android.mk b/verity/Android.mk new file mode 100644 index 0000000..1a7351f --- /dev/null +++ b/verity/Android.mk
@@ -0,0 +1,92 @@ +LOCAL_PATH:= $(call my-dir) + +ifeq ($(HOST_OS),linux) + +include $(CLEAR_VARS) +LOCAL_MODULE := verify_boot_signature +LOCAL_SRC_FILES := verify_boot_signature.c +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE_TAGS := optional +LOCAL_SHARED_LIBRARIES := libcrypto +LOCAL_C_INCLUDES += external/openssl/include system/extras/ext4_utils system/core/mkbootimg +include $(BUILD_HOST_EXECUTABLE) + +endif # HOST_OS == linux + +include $(CLEAR_VARS) +LOCAL_MODULE := generate_verity_key +LOCAL_SRC_FILES := generate_verity_key.c +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE_TAGS := optional +LOCAL_SHARED_LIBRARIES := libcrypto_utils libcrypto +include $(BUILD_HOST_EXECUTABLE) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := VerityVerifier.java Utils.java +LOCAL_MODULE := VerityVerifier +LOCAL_JAR_MANIFEST := VerityVerifier.mf +LOCAL_MODULE_TAGS := optional +LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host +include $(BUILD_HOST_JAVA_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := VeritySigner.java Utils.java +LOCAL_MODULE := VeritySigner +LOCAL_JAR_MANIFEST := VeritySigner.mf +LOCAL_MODULE_TAGS := optional +LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host +include $(BUILD_HOST_JAVA_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := BootSignature.java VeritySigner.java Utils.java +LOCAL_MODULE := BootSignature +LOCAL_JAR_MANIFEST := BootSignature.mf +LOCAL_MODULE_TAGS := optional +LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host +include $(BUILD_HOST_JAVA_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := verity_verifier +LOCAL_MODULE := verity_verifier +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_IS_HOST_MODULE := true +LOCAL_MODULE_TAGS := optional +LOCAL_REQUIRED_MODULES := VerityVerifier +include $(BUILD_PREBUILT) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := verity_signer +LOCAL_MODULE := verity_signer +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_IS_HOST_MODULE := true +LOCAL_MODULE_TAGS := optional +LOCAL_REQUIRED_MODULES := VeritySigner +include $(BUILD_PREBUILT) + +include $(CLEAR_VARS) +LOCAL_SRC_FILES := boot_signer +LOCAL_MODULE := boot_signer +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_IS_HOST_MODULE := true +LOCAL_MODULE_TAGS := optional +LOCAL_REQUIRED_MODULES := BootSignature +include $(BUILD_PREBUILT) + +include $(CLEAR_VARS) +LOCAL_MODULE := build_verity_metadata.py +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_SRC_FILES := build_verity_metadata.py +LOCAL_IS_HOST_MODULE := true +LOCAL_MODULE_TAGS := optional +include $(BUILD_PREBUILT) + +include $(CLEAR_VARS) +LOCAL_MODULE := build_verity_tree +LOCAL_SRC_FILES := build_verity_tree.cpp +LOCAL_MODULE_TAGS := optional +LOCAL_STATIC_LIBRARIES := libsparse_host libz +LOCAL_SHARED_LIBRARIES := libcrypto libbase +LOCAL_CFLAGS += -Wall -Werror +include $(BUILD_HOST_EXECUTABLE) + +include $(call first-makefiles-under,$(LOCAL_PATH))
diff --git a/verity/BootSignature.java b/verity/BootSignature.java new file mode 100644 index 0000000..3cf9499 --- /dev/null +++ b/verity/BootSignature.java
@@ -0,0 +1,319 @@ +/* + * Copyright (C) 2014 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.verity; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateEncodingException; +import java.util.Arrays; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.DERPrintableString; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.util.ASN1Dump; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** + * AndroidVerifiedBootSignature DEFINITIONS ::= + * BEGIN + * formatVersion ::= INTEGER + * certificate ::= Certificate + * algorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, + * parameters ANY DEFINED BY algorithm OPTIONAL + * } + * authenticatedAttributes ::= SEQUENCE { + * target CHARACTER STRING, + * length INTEGER + * } + * signature ::= OCTET STRING + * END + */ + +public class BootSignature extends ASN1Object +{ + private ASN1Integer formatVersion; + private ASN1Encodable certificate; + private AlgorithmIdentifier algorithmIdentifier; + private DERPrintableString target; + private ASN1Integer length; + private DEROctetString signature; + private PublicKey publicKey; + + private static final int FORMAT_VERSION = 1; + + /** + * Initializes the object for signing an image file + * @param target Target name, included in the signed data + * @param length Length of the image, included in the signed data + */ + public BootSignature(String target, int length) { + this.formatVersion = new ASN1Integer(FORMAT_VERSION); + this.target = new DERPrintableString(target); + this.length = new ASN1Integer(length); + } + + /** + * Initializes the object for verifying a signed image file + * @param signature Signature footer + */ + public BootSignature(byte[] signature) + throws Exception { + ASN1InputStream stream = new ASN1InputStream(signature); + ASN1Sequence sequence = (ASN1Sequence) stream.readObject(); + + formatVersion = (ASN1Integer) sequence.getObjectAt(0); + if (formatVersion.getValue().intValue() != FORMAT_VERSION) { + throw new IllegalArgumentException("Unsupported format version"); + } + + certificate = sequence.getObjectAt(1); + byte[] encoded = ((ASN1Object) certificate).getEncoded(); + ByteArrayInputStream bis = new ByteArrayInputStream(encoded); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate c = (X509Certificate) cf.generateCertificate(bis); + publicKey = c.getPublicKey(); + + ASN1Sequence algId = (ASN1Sequence) sequence.getObjectAt(2); + algorithmIdentifier = new AlgorithmIdentifier( + (ASN1ObjectIdentifier) algId.getObjectAt(0)); + + ASN1Sequence attrs = (ASN1Sequence) sequence.getObjectAt(3); + target = (DERPrintableString) attrs.getObjectAt(0); + length = (ASN1Integer) attrs.getObjectAt(1); + + this.signature = (DEROctetString) sequence.getObjectAt(4); + } + + public ASN1Object getAuthenticatedAttributes() { + ASN1EncodableVector attrs = new ASN1EncodableVector(); + attrs.add(target); + attrs.add(length); + return new DERSequence(attrs); + } + + public byte[] getEncodedAuthenticatedAttributes() throws IOException { + return getAuthenticatedAttributes().getEncoded(); + } + + public AlgorithmIdentifier getAlgorithmIdentifier() { + return algorithmIdentifier; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public byte[] getSignature() { + return signature.getOctets(); + } + + public void setSignature(byte[] sig, AlgorithmIdentifier algId) { + algorithmIdentifier = algId; + signature = new DEROctetString(sig); + } + + public void setCertificate(X509Certificate cert) + throws Exception, IOException, CertificateEncodingException { + ASN1InputStream s = new ASN1InputStream(cert.getEncoded()); + certificate = s.readObject(); + publicKey = cert.getPublicKey(); + } + + public byte[] generateSignableImage(byte[] image) throws IOException { + byte[] attrs = getEncodedAuthenticatedAttributes(); + byte[] signable = Arrays.copyOf(image, image.length + attrs.length); + for (int i=0; i < attrs.length; i++) { + signable[i+image.length] = attrs[i]; + } + return signable; + } + + public byte[] sign(byte[] image, PrivateKey key) throws Exception { + byte[] signable = generateSignableImage(image); + return Utils.sign(key, signable); + } + + public boolean verify(byte[] image) throws Exception { + if (length.getValue().intValue() != image.length) { + throw new IllegalArgumentException("Invalid image length"); + } + + byte[] signable = generateSignableImage(image); + return Utils.verify(publicKey, signable, signature.getOctets(), + algorithmIdentifier); + } + + public ASN1Primitive toASN1Primitive() { + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(formatVersion); + v.add(certificate); + v.add(algorithmIdentifier); + v.add(getAuthenticatedAttributes()); + v.add(signature); + return new DERSequence(v); + } + + public static int getSignableImageSize(byte[] data) throws Exception { + if (!Arrays.equals(Arrays.copyOfRange(data, 0, 8), + "ANDROID!".getBytes("US-ASCII"))) { + throw new IllegalArgumentException("Invalid image header: missing magic"); + } + + ByteBuffer image = ByteBuffer.wrap(data); + image.order(ByteOrder.LITTLE_ENDIAN); + + image.getLong(); // magic + int kernelSize = image.getInt(); + image.getInt(); // kernel_addr + int ramdskSize = image.getInt(); + image.getInt(); // ramdisk_addr + int secondSize = image.getInt(); + image.getLong(); // second_addr + tags_addr + int pageSize = image.getInt(); + + int length = pageSize // include the page aligned image header + + ((kernelSize + pageSize - 1) / pageSize) * pageSize + + ((ramdskSize + pageSize - 1) / pageSize) * pageSize + + ((secondSize + pageSize - 1) / pageSize) * pageSize; + + length = ((length + pageSize - 1) / pageSize) * pageSize; + + if (length <= 0) { + throw new IllegalArgumentException("Invalid image header: invalid length"); + } + + return length; + } + + public static void doSignature( String target, + String imagePath, + String keyPath, + String certPath, + String outPath) throws Exception { + + byte[] image = Utils.read(imagePath); + int signableSize = getSignableImageSize(image); + + if (signableSize < image.length) { + System.err.println("NOTE: truncating file " + imagePath + + " from " + image.length + " to " + signableSize + " bytes"); + image = Arrays.copyOf(image, signableSize); + } else if (signableSize > image.length) { + throw new IllegalArgumentException("Invalid image: too short, expected " + + signableSize + " bytes"); + } + + BootSignature bootsig = new BootSignature(target, image.length); + + X509Certificate cert = Utils.loadPEMCertificate(certPath); + bootsig.setCertificate(cert); + + PrivateKey key = Utils.loadDERPrivateKeyFromFile(keyPath); + bootsig.setSignature(bootsig.sign(image, key), + Utils.getSignatureAlgorithmIdentifier(key)); + + byte[] encoded_bootsig = bootsig.getEncoded(); + byte[] image_with_metadata = Arrays.copyOf(image, image.length + encoded_bootsig.length); + + System.arraycopy(encoded_bootsig, 0, image_with_metadata, + image.length, encoded_bootsig.length); + + Utils.write(image_with_metadata, outPath); + } + + public static void verifySignature(String imagePath, String certPath) throws Exception { + byte[] image = Utils.read(imagePath); + int signableSize = getSignableImageSize(image); + + if (signableSize >= image.length) { + throw new IllegalArgumentException("Invalid image: not signed"); + } + + byte[] signature = Arrays.copyOfRange(image, signableSize, image.length); + BootSignature bootsig = new BootSignature(signature); + + if (!certPath.isEmpty()) { + System.err.println("NOTE: verifying using public key from " + certPath); + bootsig.setCertificate(Utils.loadPEMCertificate(certPath)); + } + + try { + if (bootsig.verify(Arrays.copyOf(image, signableSize))) { + System.err.println("Signature is VALID"); + System.exit(0); + } else { + System.err.println("Signature is INVALID"); + } + } catch (Exception e) { + e.printStackTrace(System.err); + } + System.exit(1); + } + + /* Example usage for signing a boot image using dev keys: + java -cp \ + ../../../out/host/common/obj/JAVA_LIBRARIES/BootSignature_intermediates/ \ + classes/com.android.verity.BootSignature \ + /boot \ + ../../../out/target/product/$PRODUCT/boot.img \ + ../../../build/target/product/security/verity.pk8 \ + ../../../build/target/product/security/verity.x509.pem \ + /tmp/boot.img.signed + */ + public static void main(String[] args) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + + if ("-verify".equals(args[0])) { + String certPath = ""; + + if (args.length >= 4 && "-certificate".equals(args[2])) { + /* args[3] is the path to a public key certificate */ + certPath = args[3]; + } + + /* args[1] is the path to a signed boot image */ + verifySignature(args[1], certPath); + } else { + /* args[0] is the target name, typically /boot + args[1] is the path to a boot image to sign + args[2] is the path to a private key + args[3] is the path to the matching public key certificate + args[4] is the path where to output the signed boot image + */ + doSignature(args[0], args[1], args[2], args[3], args[4]); + } + } +}
diff --git a/verity/BootSignature.mf b/verity/BootSignature.mf new file mode 100644 index 0000000..c1868b6 --- /dev/null +++ b/verity/BootSignature.mf
@@ -0,0 +1 @@ +Main-Class: com.android.verity.BootSignature
diff --git a/verity/NOTICE b/verity/NOTICE new file mode 100644 index 0000000..9109eed --- /dev/null +++ b/verity/NOTICE
@@ -0,0 +1,190 @@ + + Copyright (c) 2013-2015, 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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +
diff --git a/verity/Utils.java b/verity/Utils.java new file mode 100644 index 0000000..6d80276 --- /dev/null +++ b/verity/Utils.java
@@ -0,0 +1,299 @@ +/* + * Copyright (C) 2014 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.verity; + +import java.lang.reflect.Constructor; +import java.io.File; +import java.io.ByteArrayInputStream; +import java.io.Console; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.KeyFactory; +import java.security.Provider; +import java.security.Security; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.Cipher; +import javax.crypto.EncryptedPrivateKeyInfo; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; +import org.bouncycastle.util.encoders.Base64; + +public class Utils { + + private static final Map<String, String> ID_TO_ALG; + private static final Map<String, String> ALG_TO_ID; + + static { + ID_TO_ALG = new HashMap<String, String>(); + ALG_TO_ID = new HashMap<String, String>(); + + ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA256.getId(), "SHA256withECDSA"); + ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA384.getId(), "SHA384withECDSA"); + ID_TO_ALG.put(X9ObjectIdentifiers.ecdsa_with_SHA512.getId(), "SHA512withECDSA"); + ID_TO_ALG.put(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1withRSA"); + ID_TO_ALG.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256withRSA"); + ID_TO_ALG.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512withRSA"); + + ALG_TO_ID.put("SHA256withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA256.getId()); + ALG_TO_ID.put("SHA384withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA384.getId()); + ALG_TO_ID.put("SHA512withECDSA", X9ObjectIdentifiers.ecdsa_with_SHA512.getId()); + ALG_TO_ID.put("SHA1withRSA", PKCSObjectIdentifiers.sha1WithRSAEncryption.getId()); + ALG_TO_ID.put("SHA256withRSA", PKCSObjectIdentifiers.sha256WithRSAEncryption.getId()); + ALG_TO_ID.put("SHA512withRSA", PKCSObjectIdentifiers.sha512WithRSAEncryption.getId()); + } + + private static void loadProviderIfNecessary(String providerClassName) { + if (providerClassName == null) { + return; + } + + final Class<?> klass; + try { + final ClassLoader sysLoader = ClassLoader.getSystemClassLoader(); + if (sysLoader != null) { + klass = sysLoader.loadClass(providerClassName); + } else { + klass = Class.forName(providerClassName); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + System.exit(1); + return; + } + + Constructor<?> constructor = null; + for (Constructor<?> c : klass.getConstructors()) { + if (c.getParameterTypes().length == 0) { + constructor = c; + break; + } + } + if (constructor == null) { + System.err.println("No zero-arg constructor found for " + providerClassName); + System.exit(1); + return; + } + + final Object o; + try { + o = constructor.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + return; + } + if (!(o instanceof Provider)) { + System.err.println("Not a Provider class: " + providerClassName); + System.exit(1); + } + + Security.insertProviderAt((Provider) o, 1); + } + + static byte[] pemToDer(String pem) throws Exception { + pem = pem.replaceAll("^-.*", ""); + String base64_der = pem.replaceAll("-.*$", ""); + return Base64.decode(base64_der); + } + + private static PKCS8EncodedKeySpec decryptPrivateKey(byte[] encryptedPrivateKey) + throws GeneralSecurityException { + EncryptedPrivateKeyInfo epkInfo; + try { + epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); + } catch (IOException ex) { + // Probably not an encrypted key. + return null; + } + + char[] password = System.console().readPassword("Password for the private key file: "); + + SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); + Key key = skFactory.generateSecret(new PBEKeySpec(password)); + Arrays.fill(password, '\0'); + + Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); + cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); + + try { + return epkInfo.getKeySpec(cipher); + } catch (InvalidKeySpecException ex) { + System.err.println("Password may be bad."); + throw ex; + } + } + + static PrivateKey loadDERPrivateKey(byte[] der) throws Exception { + PKCS8EncodedKeySpec spec = decryptPrivateKey(der); + + if (spec == null) { + spec = new PKCS8EncodedKeySpec(der); + } + + ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded())); + PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject()); + String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId(); + + return KeyFactory.getInstance(algOid).generatePrivate(spec); + } + + static PrivateKey loadPEMPrivateKey(byte[] pem) throws Exception { + byte[] der = pemToDer(new String(pem)); + return loadDERPrivateKey(der); + } + + static PrivateKey loadPEMPrivateKeyFromFile(String keyFname) throws Exception { + return loadPEMPrivateKey(read(keyFname)); + } + + static PrivateKey loadDERPrivateKeyFromFile(String keyFname) throws Exception { + return loadDERPrivateKey(read(keyFname)); + } + + static PublicKey loadDERPublicKey(byte[] der) throws Exception { + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(der); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return factory.generatePublic(publicKeySpec); + } + + static PublicKey loadPEMPublicKey(byte[] pem) throws Exception { + byte[] der = pemToDer(new String(pem)); + X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(der); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return factory.generatePublic(publicKeySpec); + } + + static PublicKey loadPEMPublicKeyFromFile(String keyFname) throws Exception { + return loadPEMPublicKey(read(keyFname)); + } + + static PublicKey loadDERPublicKeyFromFile(String keyFname) throws Exception { + return loadDERPublicKey(read(keyFname)); + } + + static X509Certificate loadPEMCertificate(String fname) throws Exception { + try (FileInputStream fis = new FileInputStream(fname)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(fis); + } + } + + private static String getSignatureAlgorithm(Key key) throws Exception { + if ("EC".equals(key.getAlgorithm())) { + int curveSize; + KeyFactory factory = KeyFactory.getInstance("EC"); + + if (key instanceof PublicKey) { + ECPublicKeySpec spec = factory.getKeySpec(key, ECPublicKeySpec.class); + curveSize = spec.getParams().getCurve().getField().getFieldSize(); + } else if (key instanceof PrivateKey) { + ECPrivateKeySpec spec = factory.getKeySpec(key, ECPrivateKeySpec.class); + curveSize = spec.getParams().getCurve().getField().getFieldSize(); + } else { + throw new InvalidKeySpecException(); + } + + if (curveSize <= 256) { + return "SHA256withECDSA"; + } else if (curveSize <= 384) { + return "SHA384withECDSA"; + } else { + return "SHA512withECDSA"; + } + } else if ("RSA".equals(key.getAlgorithm())) { + return "SHA256withRSA"; + } else { + throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm()); + } + } + + static AlgorithmIdentifier getSignatureAlgorithmIdentifier(Key key) throws Exception { + String id = ALG_TO_ID.get(getSignatureAlgorithm(key)); + + if (id == null) { + throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm()); + } + + return new AlgorithmIdentifier(new ASN1ObjectIdentifier(id)); + } + + static boolean verify(PublicKey key, byte[] input, byte[] signature, + AlgorithmIdentifier algId) throws Exception { + String algName = ID_TO_ALG.get(algId.getAlgorithm().getId()); + + if (algName == null) { + throw new IllegalArgumentException("Unsupported algorithm " + algId.getAlgorithm()); + } + + Signature verifier = Signature.getInstance(algName); + verifier.initVerify(key); + verifier.update(input); + + return verifier.verify(signature); + } + + static byte[] sign(PrivateKey privateKey, byte[] input) throws Exception { + Signature signer = Signature.getInstance(getSignatureAlgorithm(privateKey)); + signer.initSign(privateKey); + signer.update(input); + return signer.sign(); + } + + static byte[] read(String fname) throws Exception { + long offset = 0; + File f = new File(fname); + long length = f.length(); + byte[] image = new byte[(int)length]; + FileInputStream fis = new FileInputStream(f); + while (offset < length) { + offset += fis.read(image, (int)offset, (int)(length - offset)); + } + fis.close(); + return image; + } + + static void write(byte[] data, String fname) throws Exception{ + FileOutputStream out = new FileOutputStream(fname); + out.write(data); + out.close(); + } +}
diff --git a/verity/VeritySigner.java b/verity/VeritySigner.java new file mode 100644 index 0000000..9d85747 --- /dev/null +++ b/verity/VeritySigner.java
@@ -0,0 +1,69 @@ +/* + * Copyright (C) 2013 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.verity; + +import java.security.PublicKey; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +public class VeritySigner { + + private static void usage() { + System.err.println("usage: VeritySigner <contentfile> <key.pk8> " + + "<sigfile> | <contentfile> <certificate.x509.pem> <sigfile> " + + "-verify"); + System.exit(1); + } + + public static void main(String[] args) throws Exception { + if (args.length < 3) { + usage(); + return; + } + + Security.addProvider(new BouncyCastleProvider()); + + byte[] content = Utils.read(args[0]); + + if (args.length > 3 && "-verify".equals(args[3])) { + X509Certificate cert = Utils.loadPEMCertificate(args[1]); + PublicKey publicKey = cert.getPublicKey(); + + byte[] signature = Utils.read(args[2]); + + try { + if (Utils.verify(publicKey, content, signature, + Utils.getSignatureAlgorithmIdentifier(publicKey))) { + System.err.println("Signature is VALID"); + System.exit(0); + } else { + System.err.println("Signature is INVALID"); + } + } catch (Exception e) { + e.printStackTrace(System.err); + } + + System.exit(1); + } else { + PrivateKey privateKey = Utils.loadDERPrivateKey(Utils.read(args[1])); + byte[] signature = Utils.sign(privateKey, content); + Utils.write(signature, args[2]); + } + } +}
diff --git a/verity/VeritySigner.mf b/verity/VeritySigner.mf new file mode 100644 index 0000000..b36c198 --- /dev/null +++ b/verity/VeritySigner.mf
@@ -0,0 +1 @@ +Main-Class: com.android.verity.VeritySigner
diff --git a/verity/VerityVerifier.java b/verity/VerityVerifier.java new file mode 100644 index 0000000..6b3f49e --- /dev/null +++ b/verity/VerityVerifier.java
@@ -0,0 +1,419 @@ +/* + * Copyright (C) 2014 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.verity; + +import java.io.File; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.lang.Math; +import java.lang.Process; +import java.lang.Runtime; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.MessageDigest; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import javax.xml.bind.DatatypeConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +public class VerityVerifier { + + private ArrayList<Integer> hashBlocksLevel; + private byte[] hashTree; + private byte[] rootHash; + private byte[] salt; + private byte[] signature; + private byte[] table; + private File image; + private int blockSize; + private int hashBlockSize; + private int hashOffsetForData; + private int hashSize; + private int hashTreeSize; + private long hashStart; + private long imageSize; + private MessageDigest digest; + + private static final int EXT4_SB_MAGIC = 0xEF53; + private static final int EXT4_SB_OFFSET = 0x400; + private static final int EXT4_SB_OFFSET_MAGIC = EXT4_SB_OFFSET + 0x38; + private static final int EXT4_SB_OFFSET_LOG_BLOCK_SIZE = EXT4_SB_OFFSET + 0x18; + private static final int EXT4_SB_OFFSET_BLOCKS_COUNT_LO = EXT4_SB_OFFSET + 0x4; + private static final int EXT4_SB_OFFSET_BLOCKS_COUNT_HI = EXT4_SB_OFFSET + 0x150; + private static final int MINCRYPT_OFFSET_MODULUS = 0x8; + private static final int MINCRYPT_OFFSET_EXPONENT = 0x208; + private static final int MINCRYPT_MODULUS_SIZE = 0x100; + private static final int MINCRYPT_EXPONENT_SIZE = 0x4; + private static final int VERITY_FIELDS = 10; + private static final int VERITY_MAGIC = 0xB001B001; + private static final int VERITY_SIGNATURE_SIZE = 256; + private static final int VERITY_VERSION = 0; + + public VerityVerifier(String fname) throws Exception { + digest = MessageDigest.getInstance("SHA-256"); + hashSize = digest.getDigestLength(); + hashBlocksLevel = new ArrayList<Integer>(); + hashTreeSize = -1; + openImage(fname); + readVerityData(); + } + + /** + * Reverses the order of bytes in a byte array + * @param value Byte array to reverse + */ + private static byte[] reverse(byte[] value) { + for (int i = 0; i < value.length / 2; i++) { + byte tmp = value[i]; + value[i] = value[value.length - i - 1]; + value[value.length - i - 1] = tmp; + } + + return value; + } + + /** + * Converts a 4-byte little endian value to a Java integer + * @param value Little endian integer to convert + */ + private static int fromle(int value) { + byte[] bytes = ByteBuffer.allocate(4).putInt(value).array(); + return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + /** + * Converts a 2-byte little endian value to Java a integer + * @param value Little endian short to convert + */ + private static int fromle(short value) { + return fromle(value << 16); + } + + /** + * Reads a 2048-bit RSA public key saved in mincrypt format, and returns + * a Java PublicKey for it. + * @param fname Name of the mincrypt public key file + */ + private static PublicKey getMincryptPublicKey(String fname) throws Exception { + try (RandomAccessFile key = new RandomAccessFile(fname, "r")) { + byte[] binaryMod = new byte[MINCRYPT_MODULUS_SIZE]; + byte[] binaryExp = new byte[MINCRYPT_EXPONENT_SIZE]; + + key.seek(MINCRYPT_OFFSET_MODULUS); + key.readFully(binaryMod); + + key.seek(MINCRYPT_OFFSET_EXPONENT); + key.readFully(binaryExp); + + BigInteger modulus = new BigInteger(1, reverse(binaryMod)); + BigInteger exponent = new BigInteger(1, reverse(binaryExp)); + + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return factory.generatePublic(spec); + } + } + + /** + * Unsparses a sparse image into a temporary file and returns a + * handle to the file + * @param fname Path to a sparse image file + */ + private void openImage(String fname) throws Exception { + image = File.createTempFile("system", ".raw"); + image.deleteOnExit(); + + Process p = Runtime.getRuntime().exec("simg2img " + fname + + " " + image.getAbsoluteFile()); + + p.waitFor(); + if (p.exitValue() != 0) { + throw new IllegalArgumentException("Invalid image: failed to unsparse"); + } + } + + /** + * Reads the ext4 superblock and calculates the size of the system image, + * after which we should find the verity metadata + * @param img File handle to the image file + */ + public static long getMetadataPosition(RandomAccessFile img) + throws Exception { + img.seek(EXT4_SB_OFFSET_MAGIC); + int magic = fromle(img.readShort()); + + if (magic != EXT4_SB_MAGIC) { + throw new IllegalArgumentException("Invalid image: not a valid ext4 image"); + } + + img.seek(EXT4_SB_OFFSET_BLOCKS_COUNT_LO); + long blocksCountLo = fromle(img.readInt()); + + img.seek(EXT4_SB_OFFSET_LOG_BLOCK_SIZE); + long logBlockSize = fromle(img.readInt()); + + img.seek(EXT4_SB_OFFSET_BLOCKS_COUNT_HI); + long blocksCountHi = fromle(img.readInt()); + + long blockSizeBytes = 1L << (10 + logBlockSize); + long blockCount = (blocksCountHi << 32) + blocksCountLo; + return blockSizeBytes * blockCount; + } + + /** + * Calculates the size of the verity hash tree based on the image size + */ + private int calculateHashTreeSize() { + if (hashTreeSize > 0) { + return hashTreeSize; + } + + int totalBlocks = 0; + int hashes = (int) (imageSize / blockSize); + + hashBlocksLevel.clear(); + + do { + hashBlocksLevel.add(0, hashes); + + int hashBlocks = + (int) Math.ceil((double) hashes * hashSize / hashBlockSize); + + totalBlocks += hashBlocks; + + hashes = hashBlocks; + } while (hashes > 1); + + hashTreeSize = totalBlocks * hashBlockSize; + return hashTreeSize; + } + + /** + * Parses the verity mapping table and reads the hash tree from + * the image file + * @param img Handle to the image file + * @param table Verity mapping table + */ + private void readHashTree(RandomAccessFile img, byte[] table) + throws Exception { + String tableStr = new String(table); + String[] fields = tableStr.split(" "); + + if (fields.length != VERITY_FIELDS) { + throw new IllegalArgumentException("Invalid image: unexpected number of fields " + + "in verity mapping table (" + fields.length + ")"); + } + + String hashVersion = fields[0]; + + if (!"1".equals(hashVersion)) { + throw new IllegalArgumentException("Invalid image: unsupported hash format"); + } + + String alg = fields[7]; + + if (!"sha256".equals(alg)) { + throw new IllegalArgumentException("Invalid image: unsupported hash algorithm"); + } + + blockSize = Integer.parseInt(fields[3]); + hashBlockSize = Integer.parseInt(fields[4]); + + int blocks = Integer.parseInt(fields[5]); + int start = Integer.parseInt(fields[6]); + + if (imageSize != (long) blocks * blockSize) { + throw new IllegalArgumentException("Invalid image: size mismatch in mapping " + + "table"); + } + + rootHash = DatatypeConverter.parseHexBinary(fields[8]); + salt = DatatypeConverter.parseHexBinary(fields[9]); + + hashStart = (long) start * blockSize; + img.seek(hashStart); + + int treeSize = calculateHashTreeSize(); + + hashTree = new byte[treeSize]; + img.readFully(hashTree); + } + + /** + * Reads verity data from the image file + */ + private void readVerityData() throws Exception { + try (RandomAccessFile img = new RandomAccessFile(image, "r")) { + imageSize = getMetadataPosition(img); + img.seek(imageSize); + + int magic = fromle(img.readInt()); + + if (magic != VERITY_MAGIC) { + throw new IllegalArgumentException("Invalid image: verity metadata not found"); + } + + int version = fromle(img.readInt()); + + if (version != VERITY_VERSION) { + throw new IllegalArgumentException("Invalid image: unknown metadata version"); + } + + signature = new byte[VERITY_SIGNATURE_SIZE]; + img.readFully(signature); + + int tableSize = fromle(img.readInt()); + + table = new byte[tableSize]; + img.readFully(table); + + readHashTree(img, table); + } + } + + /** + * Reads and validates verity metadata, and checks the signature against the + * given public key + * @param key Public key to use for signature verification + */ + public boolean verifyMetaData(PublicKey key) + throws Exception { + return Utils.verify(key, table, signature, + Utils.getSignatureAlgorithmIdentifier(key)); + } + + /** + * Hashes a block of data using a salt and checks of the results are expected + * @param hash The expected hash value + * @param data The data block to check + */ + private boolean checkBlock(byte[] hash, byte[] data) { + digest.reset(); + digest.update(salt); + digest.update(data); + return Arrays.equals(hash, digest.digest()); + } + + /** + * Verifies the root hash and the first N-1 levels of the hash tree + */ + private boolean verifyHashTree() throws Exception { + int hashOffset = 0; + int dataOffset = hashBlockSize; + + if (!checkBlock(rootHash, Arrays.copyOfRange(hashTree, 0, hashBlockSize))) { + System.err.println("Root hash mismatch"); + return false; + } + + for (int level = 0; level < hashBlocksLevel.size() - 1; level++) { + int blocks = hashBlocksLevel.get(level); + + for (int i = 0; i < blocks; i++) { + byte[] hashBlock = Arrays.copyOfRange(hashTree, + hashOffset + i * hashSize, + hashOffset + i * hashSize + hashSize); + + byte[] dataBlock = Arrays.copyOfRange(hashTree, + dataOffset + i * hashBlockSize, + dataOffset + i * hashBlockSize + hashBlockSize); + + if (!checkBlock(hashBlock, dataBlock)) { + System.err.printf("Hash mismatch at tree level %d, block %d\n", level, i); + return false; + } + } + + hashOffset = dataOffset; + hashOffsetForData = dataOffset; + dataOffset += blocks * hashBlockSize; + } + + return true; + } + + /** + * Validates the image against the hash tree + */ + public boolean verifyData() throws Exception { + if (!verifyHashTree()) { + return false; + } + + try (RandomAccessFile img = new RandomAccessFile(image, "r")) { + byte[] dataBlock = new byte[blockSize]; + int hashOffset = hashOffsetForData; + + for (int i = 0; (long) i * blockSize < imageSize; i++) { + byte[] hashBlock = Arrays.copyOfRange(hashTree, + hashOffset + i * hashSize, + hashOffset + i * hashSize + hashSize); + + img.readFully(dataBlock); + + if (!checkBlock(hashBlock, dataBlock)) { + System.err.printf("Hash mismatch at block %d\n", i); + return false; + } + } + } + + return true; + } + + /** + * Verifies the integrity of the image and the verity metadata + * @param key Public key to use for signature verification + */ + public boolean verify(PublicKey key) throws Exception { + return (verifyMetaData(key) && verifyData()); + } + + public static void main(String[] args) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + PublicKey key = null; + + if (args.length == 3 && "-mincrypt".equals(args[1])) { + key = getMincryptPublicKey(args[2]); + } else if (args.length == 2) { + X509Certificate cert = Utils.loadPEMCertificate(args[1]); + key = cert.getPublicKey(); + } else { + System.err.println("Usage: VerityVerifier <sparse.img> <certificate.x509.pem> | -mincrypt <mincrypt_key>"); + System.exit(1); + } + + VerityVerifier verifier = new VerityVerifier(args[0]); + + try { + if (verifier.verify(key)) { + System.err.println("Signature is VALID"); + System.exit(0); + } + } catch (Exception e) { + e.printStackTrace(System.err); + } + + System.exit(1); + } +}
diff --git a/verity/VerityVerifier.mf b/verity/VerityVerifier.mf new file mode 100644 index 0000000..6118b31 --- /dev/null +++ b/verity/VerityVerifier.mf
@@ -0,0 +1 @@ +Main-Class: com.android.verity.VerityVerifier
diff --git a/verity/boot_signer b/verity/boot_signer new file mode 100755 index 0000000..e2ee42b --- /dev/null +++ b/verity/boot_signer
@@ -0,0 +1,8 @@ +#! /bin/sh + +# Start-up script for BootSigner + +BOOTSIGNER_HOME=`dirname "$0"` +BOOTSIGNER_HOME=`dirname "$BOOTSIGNER_HOME"` + +java -Xmx512M -jar "$BOOTSIGNER_HOME"/framework/BootSignature.jar "$@" \ No newline at end of file
diff --git a/verity/build_verity_metadata.py b/verity/build_verity_metadata.py new file mode 100755 index 0000000..51e629a --- /dev/null +++ b/verity/build_verity_metadata.py
@@ -0,0 +1,78 @@ +#! /usr/bin/env python + +import os +import sys +import struct +import tempfile +import commands + +VERSION = 0 +MAGIC_NUMBER = 0xb001b001 +BLOCK_SIZE = 4096 +METADATA_SIZE = BLOCK_SIZE * 8 + +def run(cmd): + status, output = commands.getstatusoutput(cmd) + print output + if status: + exit(-1) + +def get_verity_metadata_size(data_size): + return METADATA_SIZE + +def build_metadata_block(verity_table, signature): + table_len = len(verity_table) + block = struct.pack("II256sI", MAGIC_NUMBER, VERSION, signature, table_len) + block += verity_table + block = block.ljust(METADATA_SIZE, '\x00') + return block + +def sign_verity_table(table, signer_path, key_path): + with tempfile.NamedTemporaryFile(suffix='.table') as table_file: + with tempfile.NamedTemporaryFile(suffix='.sig') as signature_file: + table_file.write(table) + table_file.flush() + cmd = " ".join((signer_path, table_file.name, key_path, signature_file.name)) + print cmd + run(cmd) + return signature_file.read() + +def build_verity_table(block_device, data_blocks, root_hash, salt): + table = "1 %s %s %s %s %s %s sha256 %s %s" + table %= ( block_device, + block_device, + BLOCK_SIZE, + BLOCK_SIZE, + data_blocks, + data_blocks, + root_hash, + salt) + return table + +def build_verity_metadata(data_blocks, metadata_image, root_hash, + salt, block_device, signer_path, signing_key): + # build the verity table + verity_table = build_verity_table(block_device, data_blocks, root_hash, salt) + # build the verity table signature + signature = sign_verity_table(verity_table, signer_path, signing_key) + # build the metadata block + metadata_block = build_metadata_block(verity_table, signature) + # write it to the outfile + with open(metadata_image, "wb") as f: + f.write(metadata_block) + +if __name__ == "__main__": + if len(sys.argv) == 3 and sys.argv[1] == "-s": + print get_verity_metadata_size(int(sys.argv[2])) + elif len(sys.argv) == 8: + data_image_blocks = int(sys.argv[1]) / 4096 + metadata_image = sys.argv[2] + root_hash = sys.argv[3] + salt = sys.argv[4] + block_device = sys.argv[5] + signer_path = sys.argv[6] + signing_key = sys.argv[7] + build_verity_metadata(data_image_blocks, metadata_image, root_hash, + salt, block_device, signer_path, signing_key) + else: + exit(-1)
diff --git a/verity/build_verity_tree.cpp b/verity/build_verity_tree.cpp new file mode 100644 index 0000000..c50e449 --- /dev/null +++ b/verity/build_verity_tree.cpp
@@ -0,0 +1,367 @@ +#include <openssl/bn.h> +#include <openssl/evp.h> +#include <sparse/sparse.h> + +#undef NDEBUG + +#include <assert.h> +#include <errno.h> +#include <getopt.h> +#include <fcntl.h> +#include <inttypes.h> +#include <limits.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <android-base/file.h> + +struct sparse_hash_ctx { + unsigned char *hashes; + const unsigned char *salt; + uint64_t salt_size; + uint64_t hash_size; + uint64_t block_size; + const unsigned char *zero_block_hash; + const EVP_MD *md; +}; + +#define div_round_up(x,y) (((x) + (y) - 1)/(y)) + +#define round_up(x,y) (div_round_up(x,y)*(y)) + +#define FATAL(x...) { \ + fprintf(stderr, x); \ + exit(1); \ +} + +size_t verity_tree_blocks(uint64_t data_size, size_t block_size, size_t hash_size, + int level) +{ + size_t level_blocks = div_round_up(data_size, block_size); + int hashes_per_block = div_round_up(block_size, hash_size); + + do { + level_blocks = div_round_up(level_blocks, hashes_per_block); + } while (level--); + + return level_blocks; +} + +int hash_block(const EVP_MD *md, + const unsigned char *block, size_t len, + const unsigned char *salt, size_t salt_len, + unsigned char *out, size_t *out_size) +{ + EVP_MD_CTX *mdctx; + unsigned int s; + int ret = 1; + + mdctx = EVP_MD_CTX_create(); + assert(mdctx); + ret &= EVP_DigestInit_ex(mdctx, md, NULL); + ret &= EVP_DigestUpdate(mdctx, salt, salt_len); + ret &= EVP_DigestUpdate(mdctx, block, len); + ret &= EVP_DigestFinal_ex(mdctx, out, &s); + EVP_MD_CTX_destroy(mdctx); + assert(ret == 1); + if (out_size) { + *out_size = s; + } + return 0; +} + +int hash_blocks(const EVP_MD *md, + const unsigned char *in, size_t in_size, + unsigned char *out, size_t *out_size, + const unsigned char *salt, size_t salt_size, + size_t block_size) +{ + size_t s; + *out_size = 0; + for (size_t i = 0; i < in_size; i += block_size) { + hash_block(md, in + i, block_size, salt, salt_size, out, &s); + out += s; + *out_size += s; + } + + return 0; +} + +int hash_chunk(void *priv, const void *data, int len) +{ + struct sparse_hash_ctx *ctx = (struct sparse_hash_ctx *)priv; + assert(len % ctx->block_size == 0); + if (data) { + size_t s; + hash_blocks(ctx->md, (const unsigned char *)data, len, + ctx->hashes, &s, + ctx->salt, ctx->salt_size, ctx->block_size); + ctx->hashes += s; + } else { + for (size_t i = 0; i < (size_t)len; i += ctx->block_size) { + memcpy(ctx->hashes, ctx->zero_block_hash, ctx->hash_size); + ctx->hashes += ctx->hash_size; + } + } + return 0; +} + +void usage(void) +{ + printf("usage: build_verity_tree [ <options> ] -s <size> | <data> <verity>\n" + "options:\n" + " -a,--salt-str=<string> set salt to <string>\n" + " -A,--salt-hex=<hex digits> set salt to <hex digits>\n" + " -h show this help\n" + " -s,--verity-size=<data size> print the size of the verity tree\n" + " -v, enable verbose logging\n" + " -S treat <data image> as a sparse file\n" + ); +} + +int main(int argc, char **argv) +{ + char *data_filename; + char *verity_filename; + unsigned char *salt = NULL; + size_t salt_size = 0; + bool sparse = false; + size_t block_size = 4096; + uint64_t calculate_size = 0; + bool verbose = false; + + while (1) { + const static struct option long_options[] = { + {"salt-str", required_argument, 0, 'a'}, + {"salt-hex", required_argument, 0, 'A'}, + {"help", no_argument, 0, 'h'}, + {"sparse", no_argument, 0, 'S'}, + {"verity-size", required_argument, 0, 's'}, + {"verbose", no_argument, 0, 'v'}, + {NULL, 0, 0, 0} + }; + int c = getopt_long(argc, argv, "a:A:hSs:v", long_options, NULL); + if (c < 0) { + break; + } + + switch (c) { + case 'a': + salt_size = strlen(optarg); + salt = new unsigned char[salt_size](); + if (salt == NULL) { + FATAL("failed to allocate memory for salt\n"); + } + memcpy(salt, optarg, salt_size); + break; + case 'A': { + BIGNUM *bn = NULL; + if(!BN_hex2bn(&bn, optarg)) { + FATAL("failed to convert salt from hex\n"); + } + salt_size = BN_num_bytes(bn); + salt = new unsigned char[salt_size](); + if (salt == NULL) { + FATAL("failed to allocate memory for salt\n"); + } + if((size_t)BN_bn2bin(bn, salt) != salt_size) { + FATAL("failed to convert salt to bytes\n"); + } + } + break; + case 'h': + usage(); + return 1; + case 'S': + sparse = true; + break; + case 's': { + char* endptr; + errno = 0; + unsigned long long int inSize = strtoull(optarg, &endptr, 0); + if (optarg[0] == '\0' || *endptr != '\0' || + (errno == ERANGE && inSize == ULLONG_MAX)) { + FATAL("invalid value of verity-size\n"); + } + if (inSize > UINT64_MAX) { + FATAL("invalid value of verity-size\n"); + } + calculate_size = (uint64_t)inSize; + } + break; + case 'v': + verbose = true; + break; + case '?': + usage(); + return 1; + default: + abort(); + } + } + + argc -= optind; + argv += optind; + + const EVP_MD *md = EVP_sha256(); + if (!md) { + FATAL("failed to get digest\n"); + } + + size_t hash_size = EVP_MD_size(md); + assert(hash_size * 2 < block_size); + + if (!salt || !salt_size) { + salt_size = hash_size; + salt = new unsigned char[salt_size]; + if (salt == NULL) { + FATAL("failed to allocate memory for salt\n"); + } + + int random_fd = open("/dev/urandom", O_RDONLY); + if (random_fd < 0) { + FATAL("failed to open /dev/urandom\n"); + } + + ssize_t ret = read(random_fd, salt, salt_size); + if (ret != (ssize_t)salt_size) { + FATAL("failed to read %zu bytes from /dev/urandom: %zd %d\n", salt_size, ret, errno); + } + close(random_fd); + } + + if (calculate_size) { + if (argc != 0) { + usage(); + return 1; + } + size_t verity_blocks = 0; + size_t level_blocks; + int levels = 0; + do { + level_blocks = verity_tree_blocks(calculate_size, block_size, hash_size, levels); + levels++; + verity_blocks += level_blocks; + } while (level_blocks > 1); + + printf("%" PRIu64 "\n", (uint64_t)verity_blocks * block_size); + return 0; + } + + if (argc != 2) { + usage(); + return 1; + } + + data_filename = argv[0]; + verity_filename = argv[1]; + + int fd = open(data_filename, O_RDONLY); + if (fd < 0) { + FATAL("failed to open %s\n", data_filename); + } + + struct sparse_file *file; + if (sparse) { + file = sparse_file_import(fd, false, false); + } else { + file = sparse_file_import_auto(fd, false, verbose); + } + + if (!file) { + FATAL("failed to read file %s\n", data_filename); + } + + int64_t len = sparse_file_len(file, false, false); + if (len % block_size != 0) { + FATAL("file size %" PRIu64 " is not a multiple of %zu bytes\n", + len, block_size); + } + + int levels = 0; + size_t verity_blocks = 0; + size_t level_blocks; + + do { + level_blocks = verity_tree_blocks(len, block_size, hash_size, levels); + levels++; + verity_blocks += level_blocks; + } while (level_blocks > 1); + + unsigned char *verity_tree = new unsigned char[verity_blocks * block_size](); + unsigned char **verity_tree_levels = new unsigned char *[levels + 1](); + size_t *verity_tree_level_blocks = new size_t[levels](); + if (verity_tree == NULL || verity_tree_levels == NULL || verity_tree_level_blocks == NULL) { + FATAL("failed to allocate memory for verity tree\n"); + } + + unsigned char *ptr = verity_tree; + for (int i = levels - 1; i >= 0; i--) { + verity_tree_levels[i] = ptr; + verity_tree_level_blocks[i] = verity_tree_blocks(len, block_size, hash_size, i); + ptr += verity_tree_level_blocks[i] * block_size; + } + assert(ptr == verity_tree + verity_blocks * block_size); + assert(verity_tree_level_blocks[levels - 1] == 1); + + unsigned char zero_block_hash[hash_size]; + unsigned char zero_block[block_size]; + memset(zero_block, 0, block_size); + hash_block(md, zero_block, block_size, salt, salt_size, zero_block_hash, NULL); + + unsigned char root_hash[hash_size]; + verity_tree_levels[levels] = root_hash; + + struct sparse_hash_ctx ctx; + ctx.hashes = verity_tree_levels[0]; + ctx.salt = salt; + ctx.salt_size = salt_size; + ctx.hash_size = hash_size; + ctx.block_size = block_size; + ctx.zero_block_hash = zero_block_hash; + ctx.md = md; + + sparse_file_callback(file, false, false, hash_chunk, &ctx); + + sparse_file_destroy(file); + close(fd); + + for (int i = 0; i < levels; i++) { + size_t out_size; + hash_blocks(md, + verity_tree_levels[i], verity_tree_level_blocks[i] * block_size, + verity_tree_levels[i + 1], &out_size, + salt, salt_size, block_size); + if (i < levels - 1) { + assert(div_round_up(out_size, block_size) == verity_tree_level_blocks[i + 1]); + } else { + assert(out_size == hash_size); + } + } + + for (size_t i = 0; i < hash_size; i++) { + printf("%02x", root_hash[i]); + } + printf(" "); + for (size_t i = 0; i < salt_size; i++) { + printf("%02x", salt[i]); + } + printf("\n"); + + fd = open(verity_filename, O_WRONLY|O_CREAT, 0666); + if (fd < 0) { + FATAL("failed to open output file '%s'\n", verity_filename); + } + if (!android::base::WriteFully(fd, verity_tree, verity_blocks * block_size)) { + FATAL("failed to write '%s'\n", verity_filename); + } + close(fd); + + delete[] verity_tree_levels; + delete[] verity_tree_level_blocks; + delete[] verity_tree; + delete[] salt; +}
diff --git a/verity/fec/Android.mk b/verity/fec/Android.mk new file mode 100644 index 0000000..33ef8b5 --- /dev/null +++ b/verity/fec/Android.mk
@@ -0,0 +1,43 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_CLANG := true +ifeq ($(HOST_OS),linux) +LOCAL_SANITIZE := integer +endif +LOCAL_MODULE := fec +LOCAL_SRC_FILES := main.cpp image.cpp +LOCAL_MODULE_TAGS := optional +LOCAL_STATIC_LIBRARIES := \ + libsparse_host \ + libz \ + libcrypto_utils \ + libcrypto \ + libfec_host \ + libfec_rs_host \ + libext4_utils_host \ + libsquashfs_utils_host +LOCAL_SHARED_LIBRARIES := libbase +LOCAL_CFLAGS += -Wall -Werror -O3 +LOCAL_C_INCLUDES += external/fec +include $(BUILD_HOST_EXECUTABLE) + +include $(CLEAR_VARS) +LOCAL_CLANG := true +LOCAL_SANITIZE := integer +LOCAL_MODULE := fec +LOCAL_FORCE_STATIC_EXECUTABLE := true +LOCAL_SRC_FILES := main.cpp image.cpp +LOCAL_MODULE_TAGS := optional +LOCAL_STATIC_LIBRARIES := \ + libcrypto_utils \ + libcrypto \ + libfec \ + libfec_rs \ + libbase \ + libext4_utils_static \ + libsquashfs_utils \ + libcutils +LOCAL_CFLAGS += -Wall -Werror -O3 -DIMAGE_NO_SPARSE=1 +LOCAL_C_INCLUDES += external/fec +include $(BUILD_EXECUTABLE)
diff --git a/verity/fec/image.cpp b/verity/fec/image.cpp new file mode 100644 index 0000000..610a462 --- /dev/null +++ b/verity/fec/image.cpp
@@ -0,0 +1,629 @@ +/* + * Copyright (C) 2015 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. + */ + +#undef NDEBUG +#define _LARGEFILE64_SOURCE + +extern "C" { + #include <fec.h> +} + +#include <assert.h> +#include <android-base/file.h> +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <openssl/sha.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/mman.h> +#ifndef IMAGE_NO_SPARSE +#include <sparse/sparse.h> +#endif +#include "image.h" + +#if defined(__linux__) + #include <linux/fs.h> +#elif defined(__APPLE__) + #include <sys/disk.h> + #define BLKGETSIZE64 DKIOCGETBLOCKCOUNT + #define O_LARGEFILE 0 +#endif + +void image_init(image *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); +} + +static void mmap_image_free(image *ctx) +{ + if (ctx->input) { + munmap(ctx->input, (size_t)ctx->inp_size); + close(ctx->inp_fd); + } + + if (ctx->fec_mmap_addr) { + munmap(ctx->fec_mmap_addr, FEC_BLOCKSIZE + ctx->fec_size); + close(ctx->fec_fd); + } + + if (!ctx->inplace && ctx->output) { + delete[] ctx->output; + } +} + +static void file_image_free(image *ctx) +{ + assert(ctx->input == ctx->output); + + if (ctx->input) { + delete[] ctx->input; + } + + if (ctx->fec) { + delete[] ctx->fec; + } +} + +void image_free(image *ctx) +{ + if (ctx->mmap) { + mmap_image_free(ctx); + } else { + file_image_free(ctx); + } + + image_init(ctx); +} + +static uint64_t get_size(int fd) +{ + struct stat st; + + if (fstat(fd, &st) == -1) { + FATAL("failed to fstat: %s\n", strerror(errno)); + } + + uint64_t size = 0; + + if (S_ISBLK(st.st_mode)) { + if (ioctl(fd, BLKGETSIZE64, &size) == -1) { + FATAL("failed to ioctl(BLKGETSIZE64): %s\n", strerror(errno)); + } + } else if (S_ISREG(st.st_mode)) { + size = st.st_size; + } else { + FATAL("unknown file mode: %d\n", (int)st.st_mode); + } + + return size; +} + +static void calculate_rounds(uint64_t size, image *ctx) +{ + if (!size) { + FATAL("empty file?\n"); + } else if (size % FEC_BLOCKSIZE) { + FATAL("file size %" PRIu64 " is not a multiple of %u bytes\n", + size, FEC_BLOCKSIZE); + } + + ctx->inp_size = size; + ctx->blocks = fec_div_round_up(ctx->inp_size, FEC_BLOCKSIZE); + ctx->rounds = fec_div_round_up(ctx->blocks, ctx->rs_n); +} + +static void mmap_image_load(const std::vector<int>& fds, image *ctx, + bool output_needed) +{ + if (fds.size() != 1) { + FATAL("multiple input files not supported with mmap\n"); + } + + int fd = fds.front(); + + calculate_rounds(get_size(fd), ctx); + + /* check that we can memory map the file; on 32-bit platforms we are + limited to encoding at most 4 GiB files */ + if (ctx->inp_size > SIZE_MAX) { + FATAL("cannot mmap %" PRIu64 " bytes\n", ctx->inp_size); + } + + if (ctx->verbose) { + INFO("memory mapping '%s' (size %" PRIu64 ")\n", ctx->fec_filename, + ctx->inp_size); + } + + int flags = PROT_READ; + + if (ctx->inplace) { + flags |= PROT_WRITE; + } + + void *p = mmap(NULL, (size_t)ctx->inp_size, flags, MAP_SHARED, fd, 0); + + if (p == MAP_FAILED) { + FATAL("failed to mmap '%s' (size %" PRIu64 "): %s\n", + ctx->fec_filename, ctx->inp_size, strerror(errno)); + } + + ctx->inp_fd = fd; + ctx->input = (uint8_t *)p; + + if (ctx->inplace) { + ctx->output = ctx->input; + } else if (output_needed) { + if (ctx->verbose) { + INFO("allocating %" PRIu64 " bytes of memory\n", ctx->inp_size); + } + + ctx->output = new uint8_t[ctx->inp_size]; + + if (!ctx->output) { + FATAL("failed to allocate memory\n"); + } + + memcpy(ctx->output, ctx->input, ctx->inp_size); + } + + /* fd is closed in mmap_image_free */ +} + +#ifndef IMAGE_NO_SPARSE +static int process_chunk(void *priv, const void *data, int len) +{ + image *ctx = (image *)priv; + assert(len % FEC_BLOCKSIZE == 0); + + if (data) { + memcpy(&ctx->input[ctx->pos], data, len); + } + + ctx->pos += len; + return 0; +} +#endif + +static void file_image_load(const std::vector<int>& fds, image *ctx) +{ + uint64_t size = 0; +#ifndef IMAGE_NO_SPARSE + std::vector<struct sparse_file *> files; +#endif + + for (auto fd : fds) { + uint64_t len = 0; + +#ifdef IMAGE_NO_SPARSE + if (ctx->sparse) { + FATAL("sparse files not supported\n"); + } + + len = get_size(fd); +#else + struct sparse_file *file; + + if (ctx->sparse) { + file = sparse_file_import(fd, false, false); + } else { + file = sparse_file_import_auto(fd, false, ctx->verbose); + } + + if (!file) { + FATAL("failed to read file %s\n", ctx->fec_filename); + } + + len = sparse_file_len(file, false, false); + files.push_back(file); +#endif /* IMAGE_NO_SPARSE */ + + size += len; + } + + calculate_rounds(size, ctx); + + if (ctx->verbose) { + INFO("allocating %" PRIu64 " bytes of memory\n", ctx->inp_size); + } + + ctx->input = new uint8_t[ctx->inp_size]; + + if (!ctx->input) { + FATAL("failed to allocate memory\n"); + } + + memset(ctx->input, 0, ctx->inp_size); + ctx->output = ctx->input; + ctx->pos = 0; + +#ifdef IMAGE_NO_SPARSE + for (auto fd : fds) { + uint64_t len = get_size(fd); + + if (!android::base::ReadFully(fd, &ctx->input[ctx->pos], len)) { + FATAL("failed to read: %s\n", strerror(errno)); + } + + ctx->pos += len; + close(fd); + } +#else + for (auto file : files) { + sparse_file_callback(file, false, false, process_chunk, ctx); + sparse_file_destroy(file); + } + + for (auto fd : fds) { + close(fd); + } +#endif +} + +bool image_load(const std::vector<std::string>& filenames, image *ctx, + bool output_needed) +{ + assert(ctx->roots > 0 && ctx->roots < FEC_RSM); + ctx->rs_n = FEC_RSM - ctx->roots; + + int flags = O_RDONLY; + + if (ctx->inplace) { + flags = O_RDWR; + } + + std::vector<int> fds; + + for (const auto& fn : filenames) { + int fd = TEMP_FAILURE_RETRY(open(fn.c_str(), flags | O_LARGEFILE)); + + if (fd < 0) { + FATAL("failed to open file '%s': %s\n", fn.c_str(), strerror(errno)); + } + + fds.push_back(fd); + } + + if (ctx->mmap) { + mmap_image_load(fds, ctx, output_needed); + } else { + file_image_load(fds, ctx); + } + + return true; +} + +bool image_save(const std::string& filename, image *ctx) +{ + if (ctx->inplace && ctx->mmap) { + return true; /* nothing to do */ + } + + /* TODO: support saving as a sparse file */ + int fd = TEMP_FAILURE_RETRY(open(filename.c_str(), + O_WRONLY | O_CREAT | O_TRUNC, 0666)); + + if (fd < 0) { + FATAL("failed to open file '%s: %s'\n", filename.c_str(), + strerror(errno)); + } + + if (!android::base::WriteFully(fd, ctx->output, ctx->inp_size)) { + FATAL("failed to write to output: %s\n", strerror(errno)); + } + + close(fd); + return true; +} + +static void mmap_image_ecc_new(image *ctx) +{ + if (ctx->verbose) { + INFO("mmaping '%s' (size %u)\n", ctx->fec_filename, ctx->fec_size); + } + + int fd = TEMP_FAILURE_RETRY(open(ctx->fec_filename, + O_RDWR | O_CREAT, 0666)); + + if (fd < 0) { + FATAL("failed to open file '%s': %s\n", ctx->fec_filename, + strerror(errno)); + } + + assert(sizeof(fec_header) <= FEC_BLOCKSIZE); + size_t fec_size = FEC_BLOCKSIZE + ctx->fec_size; + + if (ftruncate(fd, fec_size) == -1) { + FATAL("failed to ftruncate file '%s': %s\n", ctx->fec_filename, + strerror(errno)); + } + + if (ctx->verbose) { + INFO("memory mapping '%s' (size %zu)\n", ctx->fec_filename, fec_size); + } + + void *p = mmap(NULL, fec_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + + if (p == MAP_FAILED) { + FATAL("failed to mmap '%s' (size %zu): %s\n", ctx->fec_filename, + fec_size, strerror(errno)); + } + + ctx->fec_fd = fd; + ctx->fec_mmap_addr = (uint8_t *)p; + ctx->fec = ctx->fec_mmap_addr; +} + +static void file_image_ecc_new(image *ctx) +{ + if (ctx->verbose) { + INFO("allocating %u bytes of memory\n", ctx->fec_size); + } + + ctx->fec = new uint8_t[ctx->fec_size]; + + if (!ctx->fec) { + FATAL("failed to allocate %u bytes\n", ctx->fec_size); + } +} + +bool image_ecc_new(const std::string& filename, image *ctx) +{ + assert(ctx->rounds > 0); /* image_load should be called first */ + + ctx->fec_filename = filename.c_str(); + ctx->fec_size = ctx->rounds * ctx->roots * FEC_BLOCKSIZE; + + if (ctx->mmap) { + mmap_image_ecc_new(ctx); + } else { + file_image_ecc_new(ctx); + } + + return true; +} + +bool image_ecc_load(const std::string& filename, image *ctx) +{ + int fd = TEMP_FAILURE_RETRY(open(filename.c_str(), O_RDONLY)); + + if (fd < 0) { + FATAL("failed to open file '%s': %s\n", filename.c_str(), + strerror(errno)); + } + + if (lseek64(fd, -FEC_BLOCKSIZE, SEEK_END) < 0) { + FATAL("failed to seek to header in '%s': %s\n", filename.c_str(), + strerror(errno)); + } + + assert(sizeof(fec_header) <= FEC_BLOCKSIZE); + + uint8_t header[FEC_BLOCKSIZE]; + fec_header *p = (fec_header *)header; + + if (!android::base::ReadFully(fd, header, sizeof(header))) { + FATAL("failed to read %zd bytes from '%s': %s\n", sizeof(header), + filename.c_str(), strerror(errno)); + } + + if (p->magic != FEC_MAGIC) { + FATAL("invalid magic in '%s': %08x\n", filename.c_str(), p->magic); + } + + if (p->version != FEC_VERSION) { + FATAL("unsupported version in '%s': %u\n", filename.c_str(), + p->version); + } + + if (p->size != sizeof(fec_header)) { + FATAL("unexpected header size in '%s': %u\n", filename.c_str(), + p->size); + } + + if (p->roots == 0 || p->roots >= FEC_RSM) { + FATAL("invalid roots in '%s': %u\n", filename.c_str(), p->roots); + } + + if (p->fec_size % p->roots || p->fec_size % FEC_BLOCKSIZE) { + FATAL("invalid length in '%s': %u\n", filename.c_str(), p->fec_size); + } + + ctx->roots = (int)p->roots; + ctx->rs_n = FEC_RSM - ctx->roots; + + calculate_rounds(p->inp_size, ctx); + + if (!image_ecc_new(filename, ctx)) { + FATAL("failed to allocate ecc\n"); + } + + if (p->fec_size != ctx->fec_size) { + FATAL("inconsistent header in '%s'\n", filename.c_str()); + } + + if (lseek64(fd, 0, SEEK_SET) < 0) { + FATAL("failed to rewind '%s': %s", filename.c_str(), strerror(errno)); + } + + if (!ctx->mmap && !android::base::ReadFully(fd, ctx->fec, ctx->fec_size)) { + FATAL("failed to read %u bytes from '%s': %s\n", ctx->fec_size, + filename.c_str(), strerror(errno)); + } + + close(fd); + + uint8_t hash[SHA256_DIGEST_LENGTH]; + SHA256(ctx->fec, ctx->fec_size, hash); + + if (memcmp(hash, p->hash, SHA256_DIGEST_LENGTH) != 0) { + FATAL("invalid ecc data\n"); + } + + return true; +} + +bool image_ecc_save(image *ctx) +{ + assert(sizeof(fec_header) <= FEC_BLOCKSIZE); + + uint8_t header[FEC_BLOCKSIZE]; + uint8_t *p = header; + + if (ctx->mmap) { + p = (uint8_t *)&ctx->fec_mmap_addr[ctx->fec_size]; + } + + memset(p, 0, FEC_BLOCKSIZE); + + fec_header *f = (fec_header *)p; + + f->magic = FEC_MAGIC; + f->version = FEC_VERSION; + f->size = sizeof(fec_header); + f->roots = ctx->roots; + f->fec_size = ctx->fec_size; + f->inp_size = ctx->inp_size; + + SHA256(ctx->fec, ctx->fec_size, f->hash); + + /* store a copy of the fec_header at the end of the header block */ + memcpy(&p[sizeof(header) - sizeof(fec_header)], p, sizeof(fec_header)); + + if (!ctx->mmap) { + assert(ctx->fec_filename); + + int fd = TEMP_FAILURE_RETRY(open(ctx->fec_filename, + O_WRONLY | O_CREAT | O_TRUNC, 0666)); + + if (fd < 0) { + FATAL("failed to open file '%s': %s\n", ctx->fec_filename, + strerror(errno)); + } + + if (!android::base::WriteFully(fd, ctx->fec, ctx->fec_size) || + !android::base::WriteFully(fd, header, sizeof(header))) { + FATAL("failed to write to output: %s\n", strerror(errno)); + } + + close(fd); + } + + return true; +} + +static void * process(void *cookie) +{ + image_proc_ctx *ctx = (image_proc_ctx *)cookie; + ctx->func(ctx); + return NULL; +} + +bool image_process(image_proc_func func, image *ctx) +{ + int threads = ctx->threads; + + if (threads < IMAGE_MIN_THREADS) { + threads = sysconf(_SC_NPROCESSORS_ONLN); + + if (threads < IMAGE_MIN_THREADS) { + threads = IMAGE_MIN_THREADS; + } + } + + assert(ctx->rounds > 0); + + if ((uint64_t)threads > ctx->rounds) { + threads = (int)ctx->rounds; + } + if (threads > IMAGE_MAX_THREADS) { + threads = IMAGE_MAX_THREADS; + } + + if (ctx->verbose) { + INFO("starting %d threads to compute RS(255, %d)\n", threads, + ctx->rs_n); + } + + pthread_t pthreads[threads]; + image_proc_ctx args[threads]; + + uint64_t current = 0; + uint64_t end = ctx->rounds * ctx->rs_n * FEC_BLOCKSIZE; + uint64_t rs_blocks_per_thread = + fec_div_round_up(ctx->rounds * FEC_BLOCKSIZE, threads); + + if (ctx->verbose) { + INFO("computing %" PRIu64 " codes per thread\n", rs_blocks_per_thread); + } + + for (int i = 0; i < threads; ++i) { + args[i].func = func; + args[i].id = i; + args[i].ctx = ctx; + args[i].rv = 0; + args[i].fec_pos = current * ctx->roots; + args[i].start = current * ctx->rs_n; + args[i].end = (current + rs_blocks_per_thread) * ctx->rs_n; + + args[i].rs = init_rs_char(FEC_PARAMS(ctx->roots)); + + if (!args[i].rs) { + FATAL("failed to initialize encoder for thread %d\n", i); + } + + if (args[i].end > end) { + args[i].end = end; + } else if (i == threads && args[i].end + rs_blocks_per_thread * + ctx->rs_n > end) { + args[i].end = end; + } + + if (ctx->verbose) { + INFO("thread %d: [%" PRIu64 ", %" PRIu64 ")\n", + i, args[i].start, args[i].end); + } + + assert(args[i].start < args[i].end); + assert((args[i].end - args[i].start) % ctx->rs_n == 0); + + if (pthread_create(&pthreads[i], NULL, process, &args[i]) != 0) { + FATAL("failed to create thread %d\n", i); + } + + current += rs_blocks_per_thread; + } + + ctx->rv = 0; + + for (int i = 0; i < threads; ++i) { + if (pthread_join(pthreads[i], NULL) != 0) { + FATAL("failed to join thread %d: %s\n", i, strerror(errno)); + } + + ctx->rv += args[i].rv; + + if (args[i].rs) { + free_rs_char(args[i].rs); + args[i].rs = NULL; + } + } + + return true; +}
diff --git a/verity/fec/image.h b/verity/fec/image.h new file mode 100644 index 0000000..f0211fd --- /dev/null +++ b/verity/fec/image.h
@@ -0,0 +1,118 @@ +/* + * Copyright (C) 2015 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. + */ + +#ifndef __FEC_H__ +#define __FEC_H__ + +#include <utils/Compat.h> +#include <string> +#include <vector> +#include <fec/io.h> +#include <fec/ecc.h> + +#define IMAGE_MIN_THREADS 1 +#define IMAGE_MAX_THREADS 128 + +#define INFO(x...) \ + fprintf(stderr, x); +#define FATAL(x...) { \ + fprintf(stderr, x); \ + exit(1); \ +} + +#define unlikely(x) __builtin_expect(!!(x), 0) + +struct image { + /* if true, decode file in place instead of creating a new output file */ + bool inplace; + /* if true, use memory mapping instead of copying all input into memory */ + bool mmap; + /* if true, assume input is a sparse file */ + bool sparse; + /* if true, print more verbose information to stderr */ + bool verbose; + const char *fec_filename; + int fec_fd; + int inp_fd; + /* the number of Reed-Solomon generator polynomial roots, also the number + of parity bytes generated for each N bytes in RS(M, N) */ + int roots; + /* for RS(M, N), N = M - roots */ + int rs_n; + int threads; + uint32_t fec_size; + uint64_t blocks; + uint64_t inp_size; + uint64_t pos; + uint64_t rounds; + uint64_t rv; + uint8_t *fec; + uint8_t *fec_mmap_addr; + uint8_t *input; + uint8_t *output; +}; + +struct image_proc_ctx; +typedef void (*image_proc_func)(image_proc_ctx *); + +struct image_proc_ctx { + image_proc_func func; + int id; + image *ctx; + uint64_t rv; + uint64_t fec_pos; + uint64_t start; + uint64_t end; + void *rs; +}; + +extern bool image_load(const std::vector<std::string>& filename, image *ctx, + bool output_needed); +extern bool image_save(const std::string& filename, image *ctx); + +extern bool image_ecc_new(const std::string& filename, image *ctx); +extern bool image_ecc_load(const std::string& filename, image *ctx); +extern bool image_ecc_save(image *ctx); + +extern bool image_process(image_proc_func f, image *ctx); + +extern void image_init(image *ctx); +extern void image_free(image *ctx); + +inline uint8_t image_get_interleaved_byte(uint64_t i, image *ctx) +{ + uint64_t offset = fec_ecc_interleave(i, ctx->rs_n, ctx->rounds); + + if (unlikely(offset >= ctx->inp_size)) { + return 0; + } + + return ctx->input[offset]; +} + +inline void image_set_interleaved_byte(uint64_t i, image *ctx, + uint8_t value) +{ + uint64_t offset = fec_ecc_interleave(i, ctx->rs_n, ctx->rounds); + + if (unlikely(offset >= ctx->inp_size)) { + assert(value == 0); + } else if (ctx->output && ctx->output[offset] != value) { + ctx->output[offset] = value; + } +} + +#endif // __FEC_H__
diff --git a/verity/fec/main.cpp b/verity/fec/main.cpp new file mode 100644 index 0000000..0675fd6 --- /dev/null +++ b/verity/fec/main.cpp
@@ -0,0 +1,404 @@ +/* + * Copyright (C) 2015 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. + */ + +extern "C" { + #include <fec.h> +} + +#undef NDEBUG + +#include <assert.h> +#include <errno.h> +#include <getopt.h> +#include <fcntl.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> + +#include <android-base/file.h> +#include "image.h" + +enum { + MODE_ENCODE, + MODE_DECODE, + MODE_PRINTSIZE, + MODE_GETECCSTART, + MODE_GETVERITYSTART +}; + +static void encode_rs(struct image_proc_ctx *ctx) +{ + struct image *fcx = ctx->ctx; + int j; + uint8_t data[fcx->rs_n]; + uint64_t i; + + for (i = ctx->start; i < ctx->end; i += fcx->rs_n) { + for (j = 0; j < fcx->rs_n; ++j) { + data[j] = image_get_interleaved_byte(i + j, fcx); + } + + encode_rs_char(ctx->rs, data, &fcx->fec[ctx->fec_pos]); + ctx->fec_pos += fcx->roots; + } +} + +static void decode_rs(struct image_proc_ctx *ctx) +{ + struct image *fcx = ctx->ctx; + int j, rv; + uint8_t data[fcx->rs_n + fcx->roots]; + uint64_t i; + + assert(sizeof(data) == FEC_RSM); + + for (i = ctx->start; i < ctx->end; i += fcx->rs_n) { + for (j = 0; j < fcx->rs_n; ++j) { + data[j] = image_get_interleaved_byte(i + j, fcx); + } + + memcpy(&data[fcx->rs_n], &fcx->fec[ctx->fec_pos], fcx->roots); + rv = decode_rs_char(ctx->rs, data, NULL, 0); + + if (rv < 0) { + FATAL("failed to recover [%" PRIu64 ", %" PRIu64 ")\n", + i, i + fcx->rs_n); + } else if (rv > 0) { + /* copy corrected data to output */ + for (j = 0; j < fcx->rs_n; ++j) { + image_set_interleaved_byte(i + j, fcx, data[j]); + } + + ctx->rv += rv; + } + + ctx->fec_pos += fcx->roots; + } +} + +static int usage() +{ + printf("fec: a tool for encoding and decoding files using RS(255, N).\n" + "\n" + "usage: fec <mode> [ <options> ] [ <data> <fec> [ <output> ] ]\n" + "mode:\n" + " -e --encode encode (default)\n" + " -d --decode decode\n" + " -s, --print-fec-size=<data size> print FEC size\n" + " -E, --get-ecc-start=data print ECC offset in data\n" + " -V, --get-verity-start=data print verity offset\n" + "options:\n" + " -h show this help\n" + " -v enable verbose logging\n" + " -r, --roots=<bytes> number of parity bytes\n" + " -m, --mmap use memory mapping\n" + " -j, --threads=<threads> number of threads to use\n" + " -S treat data as a sparse file\n" + "decoding options:\n" + " -i, --inplace correct <data> in place\n" + ); + + return 1; +} + +static uint64_t parse_arg(const char *arg, const char *name, uint64_t maxval) +{ + char* endptr; + errno = 0; + + unsigned long long int value = strtoull(arg, &endptr, 0); + + if (arg[0] == '\0' || *endptr != '\0' || + (errno == ERANGE && value == ULLONG_MAX)) { + FATAL("invalid value of %s\n", name); + } + if (value > maxval) { + FATAL("value of roots too large (max. %" PRIu64 ")\n", maxval); + } + + return (uint64_t)value; +} + +static int print_size(image& ctx) +{ + /* output size including header */ + printf("%" PRIu64 "\n", fec_ecc_get_size(ctx.inp_size, ctx.roots)); + return 0; +} + +static int get_start(int mode, const std::string& filename) +{ + fec::io fh(filename, O_RDONLY, FEC_VERITY_DISABLE); + + if (!fh) { + FATAL("failed to open input\n"); + } + + if (mode == MODE_GETECCSTART) { + fec_ecc_metadata data; + + if (!fh.get_ecc_metadata(data)) { + FATAL("no ecc data\n"); + } + + printf("%" PRIu64 "\n", data.start); + } else { + fec_verity_metadata data; + + if (!fh.get_verity_metadata(data)) { + FATAL("no verity data\n"); + } + + printf("%" PRIu64 "\n", data.data_size); + } + + return 0; +} + +static int encode(image& ctx, const std::vector<std::string>& inp_filenames, + const std::string& fec_filename) +{ + if (ctx.inplace) { + FATAL("invalid parameters: inplace can only used when decoding\n"); + } + + if (!image_load(inp_filenames, &ctx, false)) { + FATAL("failed to read input\n"); + } + + if (!image_ecc_new(fec_filename, &ctx)) { + FATAL("failed to allocate ecc\n"); + } + + INFO("encoding RS(255, %d) to '%s' for input files:\n", ctx.rs_n, + fec_filename.c_str()); + + size_t n = 1; + + for (const auto& fn : inp_filenames) { + INFO("\t%zu: '%s'\n", n++, fn.c_str()); + } + + if (ctx.verbose) { + INFO("\traw fec size: %u\n", ctx.fec_size); + INFO("\tblocks: %" PRIu64 "\n", ctx.blocks); + INFO("\trounds: %" PRIu64 "\n", ctx.rounds); + } + + if (!image_process(encode_rs, &ctx)) { + FATAL("failed to process input\n"); + } + + if (!image_ecc_save(&ctx)) { + FATAL("failed to write output\n"); + } + + image_free(&ctx); + return 0; +} + +static int decode(image& ctx, const std::vector<std::string>& inp_filenames, + const std::string& fec_filename, std::string& out_filename) +{ + const std::string& inp_filename = inp_filenames.front(); + + if (ctx.inplace && ctx.sparse) { + FATAL("invalid parameters: inplace cannot be used with sparse " + "files\n"); + } + + if (!image_ecc_load(fec_filename, &ctx) || + !image_load(inp_filenames, &ctx, !out_filename.empty())) { + FATAL("failed to read input\n"); + } + + if (ctx.inplace) { + INFO("correcting '%s' using RS(255, %d) from '%s'\n", + inp_filename.c_str(), ctx.rs_n, fec_filename.c_str()); + + out_filename = inp_filename; + } else { + INFO("decoding '%s' to '%s' using RS(255, %d) from '%s'\n", + inp_filename.c_str(), + out_filename.empty() ? out_filename.c_str() : "<none>", ctx.rs_n, + fec_filename.c_str()); + } + + if (ctx.verbose) { + INFO("\traw fec size: %u\n", ctx.fec_size); + INFO("\tblocks: %" PRIu64 "\n", ctx.blocks); + INFO("\trounds: %" PRIu64 "\n", ctx.rounds); + } + + if (!image_process(decode_rs, &ctx)) { + FATAL("failed to process input\n"); + } + + if (ctx.rv) { + INFO("corrected %" PRIu64 " errors\n", ctx.rv); + } else { + INFO("no errors found\n"); + } + + if (!out_filename.empty() && !image_save(out_filename, &ctx)) { + FATAL("failed to write output\n"); + } + + image_free(&ctx); + return 0; +} + +int main(int argc, char **argv) +{ + std::string fec_filename; + std::string out_filename; + std::vector<std::string> inp_filenames; + int mode = MODE_ENCODE; + image ctx; + + image_init(&ctx); + ctx.roots = FEC_DEFAULT_ROOTS; + + while (1) { + const static struct option long_options[] = { + {"help", no_argument, 0, 'h'}, + {"encode", no_argument, 0, 'e'}, + {"decode", no_argument, 0, 'd'}, + {"sparse", no_argument, 0, 'S'}, + {"roots", required_argument, 0, 'r'}, + {"inplace", no_argument, 0, 'i'}, + {"mmap", no_argument, 0, 'm'}, + {"threads", required_argument, 0, 'j'}, + {"print-fec-size", required_argument, 0, 's'}, + {"get-ecc-start", required_argument, 0, 'E'}, + {"get-verity-start", required_argument, 0, 'V'}, + {"verbose", no_argument, 0, 'v'}, + {NULL, 0, 0, 0} + }; + int c = getopt_long(argc, argv, "hedSr:imj:s:E:V:v", long_options, NULL); + if (c < 0) { + break; + } + + switch (c) { + case 'h': + return usage(); + case 'S': + ctx.sparse = true; + break; + case 'e': + if (mode != MODE_ENCODE) { + return usage(); + } + break; + case 'd': + if (mode != MODE_ENCODE) { + return usage(); + } + mode = MODE_DECODE; + break; + case 'r': + ctx.roots = (int)parse_arg(optarg, "roots", FEC_RSM); + break; + case 'i': + ctx.inplace = true; + break; + case 'm': + ctx.mmap = true; + break; + case 'j': + ctx.threads = (int)parse_arg(optarg, "threads", IMAGE_MAX_THREADS); + break; + case 's': + if (mode != MODE_ENCODE) { + return usage(); + } + mode = MODE_PRINTSIZE; + ctx.inp_size = parse_arg(optarg, "print-fec-size", UINT64_MAX); + break; + case 'E': + if (mode != MODE_ENCODE) { + return usage(); + } + mode = MODE_GETECCSTART; + inp_filenames.push_back(optarg); + break; + case 'V': + if (mode != MODE_ENCODE) { + return usage(); + } + mode = MODE_GETVERITYSTART; + inp_filenames.push_back(optarg); + break; + case 'v': + ctx.verbose = true; + break; + case '?': + return usage(); + default: + abort(); + } + } + + argc -= optind; + argv += optind; + + assert(ctx.roots > 0 && ctx.roots < FEC_RSM); + + /* check for input / output parameters */ + if (mode == MODE_ENCODE) { + /* allow multiple input files */ + for (int i = 0; i < (argc - 1); ++i) { + inp_filenames.push_back(argv[i]); + } + + if (inp_filenames.empty()) { + return usage(); + } + + /* the last one is the output file */ + fec_filename = argv[argc - 1]; + } else if (mode == MODE_DECODE) { + if (argc < 2 || argc > 3) { + return usage(); + } else if (argc == 3) { + if (ctx.inplace) { + return usage(); + } + out_filename = argv[2]; + } + + inp_filenames.push_back(argv[0]); + fec_filename = argv[1]; + } + + switch (mode) { + case MODE_PRINTSIZE: + return print_size(ctx); + case MODE_GETECCSTART: + case MODE_GETVERITYSTART: + return get_start(mode, inp_filenames.front()); + case MODE_ENCODE: + return encode(ctx, inp_filenames, fec_filename); + case MODE_DECODE: + return decode(ctx, inp_filenames, fec_filename, out_filename); + default: + abort(); + } + + return 1; +}
diff --git a/verity/fec/tests/fec.py b/verity/fec/tests/fec.py new file mode 100644 index 0000000..71a1834 --- /dev/null +++ b/verity/fec/tests/fec.py
@@ -0,0 +1,89 @@ +# Copyright (C) 2015 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. + +import math +import os +import random +import shutil +import subprocess +import sys +import tempfile + +blocksize = 4096 +roots = 2 + +def corrupt(image, offset, length): + print "corrupting %d bytes at offset %d" % (length, offset) + f = os.open(image, os.O_WRONLY) + os.lseek(f, offset, os.SEEK_SET) + os.write(f, os.urandom(length)) + os.close(f) + +def corruptmax(image, roots): + size = os.stat(image).st_size + + blocks = int(math.ceil(float(size) / blocksize)) + rounds = int(math.ceil(float(blocks) / (255 - roots))) + + max_errors = int(math.floor(rounds * roots / 2)) * blocksize + offset = random.randrange(0, size - max_errors) + + corrupt(image, offset, max_errors) + +def encode(image, fec, roots): + if subprocess.call([ "fec", "--roots= " + str(roots), image, fec ]) != 0: + raise Exception("encoding failed") + +def decode(image, fec, output): + return subprocess.call([ "fec", "--decode", image, fec, output ]) + +def compare(a, b): + return subprocess.call([ "cmp", "-s", a, b ]) + +def simg2img(image, output): + print "creating a non-sparse copy of '%s' to '%s'" % (image, output) + if subprocess.call([ "simg2img", image, output]) != 0: + raise Exception("simg2img failed") + +def main(argv): + image = argv[0] + + temp_img = tempfile.NamedTemporaryFile() + temp_cor = tempfile.NamedTemporaryFile() + temp_fec = tempfile.NamedTemporaryFile() + temp_out = tempfile.NamedTemporaryFile() + + simg2img(image, temp_img.name) + simg2img(image, temp_cor.name) + + encode(image, temp_fec.name, roots) + corruptmax(temp_cor.name, roots) + + if decode(temp_cor.name, temp_fec.name, temp_out.name) != 0: + raise Exception("FAILED: failed to correct maximum expected errors") + + if compare(temp_img.name, temp_out.name) != 0: + raise Exception("FAILED: corrected file not identical") + else: + print "corrected content matches original" + + corrupt(temp_cor.name, 0, blocksize) + + if decode(temp_cor.name, temp_fec.name, temp_out.name) == 0: + raise Exception("FAILED: corrected more than maximum number of errors?") + + print "PASSED" + +if __name__ == '__main__': + main(sys.argv[1:])
diff --git a/verity/generate_verity_key.c b/verity/generate_verity_key.c new file mode 100644 index 0000000..c598afb --- /dev/null +++ b/verity/generate_verity_key.c
@@ -0,0 +1,178 @@ +/* + * Copyright (C) 2013 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. + */ + +#define _GNU_SOURCE /* needed for asprintf */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include <crypto_utils/android_pubkey.h> + +#include <openssl/evp.h> +#include <openssl/objects.h> +#include <openssl/pem.h> +#include <openssl/rsa.h> +#include <openssl/sha.h> + +static int write_public_keyfile(RSA *private_key, const char *private_key_path) +{ + uint8_t key_data[ANDROID_PUBKEY_ENCODED_SIZE]; + BIO *bfile = NULL; + char *path = NULL; + int ret = -1; + + if (asprintf(&path, "%s.pub", private_key_path) < 0) + goto out; + + if (!android_pubkey_encode(private_key, key_data, sizeof(key_data))) + goto out; + + bfile = BIO_new_file(path, "w"); + if (!bfile) + goto out; + + BIO_write(bfile, key_data, sizeof(key_data)); + BIO_flush(bfile); + + ret = 0; +out: + BIO_free_all(bfile); + free(path); + return ret; +} + +static int convert_x509(const char *pem_file, const char *key_file) +{ + int ret = -1; + FILE *f = NULL; + EVP_PKEY *pkey = NULL; + RSA *rsa = NULL; + X509 *cert = NULL; + + if (!pem_file || !key_file) { + goto out; + } + + f = fopen(pem_file, "r"); + if (!f) { + printf("Failed to open '%s'\n", pem_file); + goto out; + } + + cert = PEM_read_X509(f, &cert, NULL, NULL); + if (!cert) { + printf("Failed to read PEM certificate from file '%s'\n", pem_file); + goto out; + } + + pkey = X509_get_pubkey(cert); + if (!pkey) { + printf("Failed to extract public key from certificate '%s'\n", pem_file); + goto out; + } + + rsa = EVP_PKEY_get1_RSA(pkey); + if (!rsa) { + printf("Failed to get the RSA public key from '%s'\n", pem_file); + goto out; + } + + if (write_public_keyfile(rsa, key_file) < 0) { + printf("Failed to write public key\n"); + goto out; + } + + ret = 0; + +out: + if (f) { + fclose(f); + } + if (cert) { + X509_free(cert); + } + if (pkey) { + EVP_PKEY_free(pkey); + } + if (rsa) { + RSA_free(rsa); + } + + return ret; +} + +static int generate_key(const char *file) +{ + int ret = -1; + FILE *f = NULL; + RSA* rsa = RSA_new(); + BIGNUM* exponent = BN_new(); + EVP_PKEY* pkey = EVP_PKEY_new(); + + if (!pkey || !exponent || !rsa) { + printf("Failed to allocate key\n"); + goto out; + } + + BN_set_word(exponent, RSA_F4); + RSA_generate_key_ex(rsa, 2048, exponent, NULL); + EVP_PKEY_set1_RSA(pkey, rsa); + + f = fopen(file, "w"); + if (!f) { + printf("Failed to open '%s'\n", file); + goto out; + } + + if (!PEM_write_PrivateKey(f, pkey, NULL, NULL, 0, NULL, NULL)) { + printf("Failed to write key\n"); + goto out; + } + + if (write_public_keyfile(rsa, file) < 0) { + printf("Failed to write public key\n"); + goto out; + } + + ret = 0; + +out: + if (f) + fclose(f); + EVP_PKEY_free(pkey); + RSA_free(rsa); + BN_free(exponent); + return ret; +} + +static void usage(){ + printf("Usage: generate_verity_key <path-to-key> | -convert <path-to-x509-pem> <path-to-key>\n"); +} + +int main(int argc, char *argv[]) { + if (argc == 2) { + return generate_key(argv[1]); + } else if (argc == 4 && !strcmp(argv[1], "-convert")) { + return convert_x509(argv[2], argv[3]); + } else { + usage(); + exit(-1); + } +}
diff --git a/verity/verify_boot_signature.c b/verity/verify_boot_signature.c new file mode 100644 index 0000000..b706e3a --- /dev/null +++ b/verity/verify_boot_signature.c
@@ -0,0 +1,429 @@ +/* + * Copyright (C) 2014 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. + */ + +#define _LARGEFILE64_SOURCE + +#include <endian.h> +#include <stddef.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> + +#include <openssl/asn1.h> +#include <openssl/asn1t.h> +#include <openssl/crypto.h> +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/rsa.h> +#include <openssl/x509.h> + +#include "bootimg.h" + +#define FORMAT_VERSION 1 +#define BUFFER_SIZE (1024 * 1024) + +typedef struct { + ASN1_STRING *target; + ASN1_INTEGER *length; +} AuthAttrs; + +ASN1_SEQUENCE(AuthAttrs) = { + ASN1_SIMPLE(AuthAttrs, target, ASN1_PRINTABLE), + ASN1_SIMPLE(AuthAttrs, length, ASN1_INTEGER) +} ASN1_SEQUENCE_END(AuthAttrs) + +IMPLEMENT_ASN1_FUNCTIONS(AuthAttrs) + +typedef struct { + ASN1_INTEGER *formatVersion; + X509 *certificate; + X509_ALGOR *algorithmIdentifier; + AuthAttrs *authenticatedAttributes; + ASN1_OCTET_STRING *signature; +} BootSignature; + +ASN1_SEQUENCE(BootSignature) = { + ASN1_SIMPLE(BootSignature, formatVersion, ASN1_INTEGER), + ASN1_SIMPLE(BootSignature, certificate, X509), + ASN1_SIMPLE(BootSignature, algorithmIdentifier, X509_ALGOR), + ASN1_SIMPLE(BootSignature, authenticatedAttributes, AuthAttrs), + ASN1_SIMPLE(BootSignature, signature, ASN1_OCTET_STRING) +} ASN1_SEQUENCE_END(BootSignature) + +IMPLEMENT_ASN1_FUNCTIONS(BootSignature) + +static BIO *g_error = NULL; + +/** + * Rounds n up to the nearest multiple of page_size + * @param n The value to round + * @param page_size Page size + */ +static uint64_t page_align(uint64_t n, uint64_t page_size) +{ + return (((n + page_size - 1) / page_size) * page_size); +} + +/** + * Calculates the offset to the beginning of the BootSignature block + * based on the boot image header. The signature will start after the + * the boot image contents. + * @param fd File descriptor to the boot image + * @param offset Receives the offset in bytes + */ +static int get_signature_offset(int fd, off64_t *offset) +{ + int i; + struct boot_img_hdr hdr; + + if (!offset) { + return -1; + } + + if (read(fd, &hdr, sizeof(hdr)) != sizeof(hdr)) { + return -1; + } + + if (memcmp(BOOT_MAGIC, hdr.magic, BOOT_MAGIC_SIZE) != 0) { + printf("Invalid boot image: missing magic\n"); + return -1; + } + + if (!hdr.page_size) { + printf("Invalid boot image: page size must be non-zero\n"); + return -1; + } + + *offset = page_align(hdr.page_size + + page_align(hdr.kernel_size, hdr.page_size) + + page_align(hdr.ramdisk_size, hdr.page_size) + + page_align(hdr.second_size, hdr.page_size), + hdr.page_size); + + return 0; +} + +/** + * Reads and parses the ASN.1 BootSignature block from the given offset + * @param fd File descriptor to the boot image + * @param offset Offset from the beginning of file to the signature + * @param bs Pointer to receive the BootImage structure + */ +static int read_signature(int fd, off64_t offset, BootSignature **bs) +{ + BIO *in = NULL; + + if (!bs) { + return -1; + } + + if (lseek64(fd, offset, SEEK_SET) == -1) { + return -1; + } + + if ((in = BIO_new_fd(fd, BIO_NOCLOSE)) == NULL) { + ERR_print_errors(g_error); + return -1; + } + + if ((*bs = ASN1_item_d2i_bio(ASN1_ITEM_rptr(BootSignature), in, bs)) == NULL) { + ERR_print_errors(g_error); + BIO_free(in); + return -1; + } + + BIO_free(in); + return 0; +} + +/** + * Validates the format of the boot signature block, and checks that + * the length in authenticated attributes matches the actual length of + * the image. + * @param bs The boot signature block to validate + * @param length The actual length of the boot image without the signature + */ +static int validate_signature_block(const BootSignature *bs, uint64_t length) +{ + BIGNUM expected; + BIGNUM value; + int rc = -1; + + if (!bs) { + return -1; + } + + BN_init(&expected); + BN_init(&value); + + /* Confirm that formatVersion matches our supported version */ + if (!BN_set_word(&expected, FORMAT_VERSION)) { + ERR_print_errors(g_error); + goto vsb_done; + } + + ASN1_INTEGER_to_BN(bs->formatVersion, &value); + + if (BN_cmp(&expected, &value) != 0) { + printf("Unsupported signature version\n"); + goto vsb_done; + } + + BN_clear(&expected); + BN_clear(&value); + + /* Confirm that the length of the image matches with the length in + the authenticated attributes */ + length = htobe64(length); + BN_bin2bn((const unsigned char *) &length, sizeof(length), &expected); + + ASN1_INTEGER_to_BN(bs->authenticatedAttributes->length, &value); + + if (BN_cmp(&expected, &value) != 0) { + printf("Image length doesn't match signature attributes\n"); + goto vsb_done; + } + + rc = 0; + +vsb_done: + BN_free(&expected); + BN_free(&value); + + return rc; +} + +/** + * Creates a SHA-256 hash from the boot image contents and the encoded + * authenticated attributes. + * @param fd File descriptor to the boot image + * @param length Length of the boot image without the signature block + * @param aa Pointer to AuthAttrs + * @param digest Pointer to a buffer where the hash is written + */ +static int hash_image(int fd, uint64_t length, const AuthAttrs *aa, + unsigned char *digest) +{ + EVP_MD_CTX *ctx = NULL; + int rc = -1; + + ssize_t bytes = 0; + unsigned char *attrs = NULL; + unsigned char *buffer = NULL; + unsigned char *p = NULL; + uint64_t total = 0; + + if (!aa || !digest) { + goto hi_done; + } + + if ((buffer = malloc(BUFFER_SIZE)) == NULL) { + goto hi_done; + } + + if (lseek64(fd, 0, SEEK_SET) != 0) { + goto hi_done; + } + + if ((ctx = EVP_MD_CTX_create()) == NULL) { + ERR_print_errors(g_error); + goto hi_done; + } + + EVP_DigestInit(ctx, EVP_sha256()); + + do { + bytes = BUFFER_SIZE; + + if ((length - total) < BUFFER_SIZE) { + bytes = length - total; + } + + if ((bytes = read(fd, buffer, bytes)) == -1) { + printf("%s\n", strerror(errno)); + goto hi_done; + } + + EVP_DigestUpdate(ctx, buffer, bytes); + total += bytes; + } while (total < length); + + if ((bytes = i2d_AuthAttrs((AuthAttrs *) aa, NULL)) < 0) { + ERR_print_errors(g_error); + goto hi_done; + } + + if ((attrs = OPENSSL_malloc(bytes)) == NULL) { + ERR_print_errors(g_error); + goto hi_done; + } + + p = attrs; + + if (i2d_AuthAttrs((AuthAttrs *) aa, &p) < 0) { + ERR_print_errors(g_error); + goto hi_done; + } + + EVP_DigestUpdate(ctx, attrs, bytes); + EVP_DigestFinal(ctx, digest, NULL); + + rc = 0; + +hi_done: + if (buffer) { + free(buffer); + } + + if (ctx) { + EVP_MD_CTX_destroy(ctx); + } + + if (attrs) { + OPENSSL_free(attrs); + } + + return rc; +} + +/** + * Verifies the RSA signature + * @param fd File descriptor to the boot image + * @param length Length of the boot image without the signature block + * @param bs The boot signature block + */ +static int verify_signature(int fd, uint64_t length, const BootSignature *bs) +{ + int rc = -1; + EVP_PKEY *pkey = NULL; + RSA *rsa = NULL; + unsigned char digest[SHA256_DIGEST_LENGTH]; + + if (!bs) { + goto vs_done; + } + + if (hash_image(fd, length, bs->authenticatedAttributes, digest) == -1) { + goto vs_done; + } + + if ((pkey = X509_get_pubkey(bs->certificate)) == NULL) { + ERR_print_errors(g_error); + goto vs_done; + } + + if ((rsa = EVP_PKEY_get1_RSA(pkey)) == NULL) { + ERR_print_errors(g_error); + goto vs_done; + } + + if (!RSA_verify(NID_sha256, digest, SHA256_DIGEST_LENGTH, + bs->signature->data, bs->signature->length, rsa)) { + ERR_print_errors(g_error); + goto vs_done; + } + + rc = 0; + +vs_done: + if (pkey) { + EVP_PKEY_free(pkey); + } + + if (rsa) { + RSA_free(rsa); + } + + return rc; +} + +/** + * Given the file name of a signed boot image, verifies the signature + * @param image_file Name of the boot image file + */ +static int verify(const char *image_file) +{ + BootSignature *bs = NULL; + int fd = -1; + int rc = 1; + off64_t offset = 0; + + if (!image_file) { + return rc; + } + + if ((fd = open(image_file, O_RDONLY | O_LARGEFILE)) == -1) { + return rc; + } + + if (get_signature_offset(fd, &offset) == -1) { + goto out; + } + + if (read_signature(fd, offset, &bs) == -1) { + goto out; + } + + if (validate_signature_block(bs, offset) == -1) { + goto out; + } + + if (verify_signature(fd, offset, bs) == -1) { + goto out; + } + + printf("Signature is VALID\n"); + rc = 0; + +out: + if (bs) { + BootSignature_free(bs); + } + + if (fd != -1) { + close(fd); + } + + return rc; +} + +static void usage() +{ + printf("Usage: verify_boot_signature <path-to-boot-image>\n"); +} + +int main(int argc, char *argv[]) +{ + if (argc != 2) { + usage(); + return 1; + } + + /* BIO descriptor for logging OpenSSL errors to stderr */ + if ((g_error = BIO_new_fd(STDERR_FILENO, BIO_NOCLOSE)) == NULL) { + printf("Failed to allocate a BIO handle for error output\n"); + return 1; + } + + ERR_load_crypto_strings(); + + return verify(argv[1]); +}
diff --git a/verity/verity_signer b/verity/verity_signer new file mode 100755 index 0000000..a4f337a --- /dev/null +++ b/verity/verity_signer
@@ -0,0 +1,8 @@ +#! /bin/sh + +# Start-up script for VeritySigner + +VERITYSIGNER_HOME=`dirname "$0"` +VERITYSIGNER_HOME=`dirname "$VERITYSIGNER_HOME"` + +java -Xmx512M -jar "$VERITYSIGNER_HOME"/framework/VeritySigner.jar "$@" \ No newline at end of file
diff --git a/verity/verity_verifier b/verity/verity_verifier new file mode 100755 index 0000000..f145228 --- /dev/null +++ b/verity/verity_verifier
@@ -0,0 +1,8 @@ +#!/bin/sh + +# Start-up script for VerityVerifier + +VERITYVERIFIER_HOME=`dirname "$0"` +VERITYVERIFIER_HOME=`dirname "$VERITYVERIFIER_HOME"` + +java -Xmx512M -jar "$VERITYVERIFIER_HOME"/framework/VerityVerifier.jar "$@"