diff options
-rw-r--r-- | verity/Android.mk | 17 | ||||
-rw-r--r-- | verity/BootSignature.java | 222 | ||||
-rw-r--r-- | verity/KeystoreSigner.java | 106 | ||||
-rw-r--r-- | verity/KeystoreSigner.mf | 2 | ||||
-rw-r--r-- | verity/Utils.java | 123 | ||||
-rw-r--r-- | verity/VeritySigner.java | 50 | ||||
-rw-r--r-- | verity/VerityVerifier.java | 166 | ||||
-rw-r--r-- | verity/VerityVerifier.mf | 1 | ||||
-rw-r--r-- | verity/generate_verity_key.c | 71 | ||||
-rwxr-xr-x[-rw-r--r--] | verity/keystore_signer | 2 | ||||
-rwxr-xr-x | verity/verity_verifier | 8 |
11 files changed, 712 insertions, 56 deletions
diff --git a/verity/Android.mk b/verity/Android.mk index ab1659b3..018d40f9 100644 --- a/verity/Android.mk +++ b/verity/Android.mk @@ -10,6 +10,14 @@ LOCAL_C_INCLUDES += external/openssl/include 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 @@ -34,6 +42,15 @@ 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 diff --git a/verity/BootSignature.java b/verity/BootSignature.java index f5ceb304..03eb32a7 100644 --- a/verity/BootSignature.java +++ b/verity/BootSignature.java @@ -16,51 +16,105 @@ 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.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.util.ASN1Dump; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.jce.provider.BouncyCastleProvider; /** * AndroidVerifiedBootSignature DEFINITIONS ::= * BEGIN - * FormatVersion ::= INTEGER - * AlgorithmIdentifier ::= SEQUENCE { + * formatVersion ::= INTEGER + * certificate ::= Certificate + * algorithmIdentifier ::= SEQUENCE { * algorithm OBJECT IDENTIFIER, * parameters ANY DEFINED BY algorithm OPTIONAL * } - * AuthenticatedAttributes ::= SEQUENCE { + * authenticatedAttributes ::= SEQUENCE { * target CHARACTER STRING, * length INTEGER * } - * Signature ::= OCTET STRING + * 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(0); + this.formatVersion = new ASN1Integer(FORMAT_VERSION); this.target = new DERPrintableString(target); this.length = new ASN1Integer(length); - this.algorithmIdentifier = new AlgorithmIdentifier( - PKCSObjectIdentifiers.sha256WithRSAEncryption); + } + + /** + * 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() { @@ -74,10 +128,29 @@ public class BootSignature extends ASN1Object return getAuthenticatedAttributes().getEncoded(); } - public void setSignature(byte[] sig) { + 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(); + } + public byte[] generateSignableImage(byte[] image) throws IOException { byte[] attrs = getEncodedAuthenticatedAttributes(); byte[] signable = Arrays.copyOf(image, image.length + attrs.length); @@ -89,36 +162,145 @@ public class BootSignature extends ASN1Object public byte[] sign(byte[] image, PrivateKey key) throws Exception { byte[] signable = generateSignableImage(image); - byte[] signature = Utils.sign(key, signable); - byte[] signed = Arrays.copyOf(image, image.length + signature.length); - for (int i=0; i < signature.length; i++) { - signed[i+image.length] = signature[i]; + return Utils.sign(key, signable); + } + + public boolean verify(byte[] image) throws Exception { + if (length.getValue().intValue() != image.length) { + throw new IllegalArgumentException("Invalid image length"); } - return signed; + + 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); - PrivateKey key = Utils.loadPEMPrivateKeyFromFile(keyPath); - byte[] signature = bootsig.sign(image, key); - Utils.write(signature, outPath); + + 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) 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); + + 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); } - // java -cp ../../../out/host/common/obj/JAVA_LIBRARIES/AndroidVerifiedBootSigner_intermediates/classes/ com.android.verity.AndroidVerifiedBootSigner boot ../../../out/target/product/flounder/boot.img ../../../build/target/product/security/verity_private_dev_key /tmp/boot.img.signed + /* 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 { - doSignature(args[0], args[1], args[2], args[3]); + Security.addProvider(new BouncyCastleProvider()); + + if ("-verify".equals(args[0])) { + /* args[1] is the path to a signed boot image */ + verifySignature(args[1]); + } 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]); + } } -}
\ No newline at end of file +} diff --git a/verity/KeystoreSigner.java b/verity/KeystoreSigner.java index d57f328f..0927d548 100644 --- a/verity/KeystoreSigner.java +++ b/verity/KeystoreSigner.java @@ -19,12 +19,17 @@ package com.android.verity; import java.io.IOException; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.Security; import java.security.Signature; +import java.security.cert.X509Certificate; +import java.util.Enumeration; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Object; import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.DERPrintableString; import org.bouncycastle.asn1.DERSequence; @@ -32,6 +37,7 @@ import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.pkcs.RSAPublicKey; import org.bouncycastle.asn1.util.ASN1Dump; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.jce.provider.BouncyCastleProvider; /** * AndroidVerifiedBootKeystore DEFINITIONS ::= @@ -61,8 +67,7 @@ class BootKey extends ASN1Object this.keyMaterial = new RSAPublicKey( k.getModulus(), k.getPublicExponent()); - this.algorithmIdentifier = new AlgorithmIdentifier( - PKCSObjectIdentifiers.sha256WithRSAEncryption); + this.algorithmIdentifier = Utils.getSignatureAlgorithmIdentifier(key); } public ASN1Primitive toASN1Primitive() { @@ -79,12 +84,15 @@ class BootKey extends ASN1Object class BootKeystore extends ASN1Object { - private ASN1Integer formatVersion; - private ASN1EncodableVector keyBag; - private BootSignature signature; + private ASN1Integer formatVersion; + private ASN1EncodableVector keyBag; + private BootSignature signature; + private X509Certificate certificate; + + private static final int FORMAT_VERSION = 0; public BootKeystore() { - this.formatVersion = new ASN1Integer(0); + this.formatVersion = new ASN1Integer(FORMAT_VERSION); this.keyBag = new ASN1EncodableVector(); } @@ -94,6 +102,10 @@ class BootKeystore extends ASN1Object keyBag.add(k); } + public void setCertificate(X509Certificate cert) { + certificate = cert; + } + public byte[] getInnerKeystore() throws Exception { ASN1EncodableVector v = new ASN1EncodableVector(); v.add(formatVersion); @@ -109,29 +121,87 @@ class BootKeystore extends ASN1Object return new DERSequence(v); } + public void parse(byte[] input) throws Exception { + ASN1InputStream stream = new ASN1InputStream(input); + ASN1Sequence sequence = (ASN1Sequence) stream.readObject(); + + formatVersion = (ASN1Integer) sequence.getObjectAt(0); + if (formatVersion.getValue().intValue() != FORMAT_VERSION) { + throw new IllegalArgumentException("Unsupported format version"); + } + + ASN1Sequence keys = (ASN1Sequence) sequence.getObjectAt(1); + Enumeration e = keys.getObjects(); + while (e.hasMoreElements()) { + keyBag.add((ASN1Encodable) e.nextElement()); + } + + ASN1Object sig = sequence.getObjectAt(2).toASN1Primitive(); + signature = new BootSignature(sig.getEncoded()); + } + + public boolean verify() throws Exception { + byte[] innerKeystore = getInnerKeystore(); + return Utils.verify(signature.getPublicKey(), innerKeystore, + signature.getSignature(), signature.getAlgorithmIdentifier()); + } + public void sign(PrivateKey privateKey) throws Exception { byte[] innerKeystore = getInnerKeystore(); byte[] rawSignature = Utils.sign(privateKey, innerKeystore); signature = new BootSignature("keystore", innerKeystore.length); - signature.setSignature(rawSignature); + signature.setCertificate(certificate); + signature.setSignature(rawSignature, + Utils.getSignatureAlgorithmIdentifier(privateKey)); } public void dump() throws Exception { System.out.println(ASN1Dump.dumpAsString(toASN1Primitive())); } - // USAGE: - // AndroidVerifiedBootKeystoreSigner <privkeyFile> <outfile> <pubkeyFile0> ... <pubkeyFileN-1> - // EG: - // java -cp ../../../out/host/common/obj/JAVA_LIBRARIES/AndroidVerifiedBootKeystoreSigner_intermediates/classes/ com.android.verity.AndroidVerifiedBootKeystoreSigner ../../../build/target/product/security/verity_private_dev_key /tmp/keystore.out /tmp/k + private static void usage() { + System.err.println("usage: KeystoreSigner <privatekey.pk8> " + + "<certificate.x509.pem> <outfile> <publickey0.der> " + + "... <publickeyN-1.der> | -verify <keystore>"); + System.exit(1); + } + public static void main(String[] args) throws Exception { - String privkeyFname = args[0]; - String outfileFname = args[1]; + if (args.length < 2) { + usage(); + return; + } + + Security.addProvider(new BouncyCastleProvider()); BootKeystore ks = new BootKeystore(); - for (int i=2; i < args.length; i++) { - ks.addPublicKey(Utils.read(args[i])); + + if ("-verify".equals(args[0])) { + ks.parse(Utils.read(args[1])); + + try { + if (ks.verify()) { + 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 { + String privkeyFname = args[0]; + String certFname = args[1]; + String outfileFname = args[2]; + + ks.setCertificate(Utils.loadPEMCertificate(certFname)); + + for (int i = 3; i < args.length; i++) { + ks.addPublicKey(Utils.read(args[i])); + } + + ks.sign(Utils.loadDERPrivateKeyFromFile(privkeyFname)); + Utils.write(ks.getEncoded(), outfileFname); } - ks.sign(Utils.loadPEMPrivateKeyFromFile(privkeyFname)); - Utils.write(ks.getEncoded(), outfileFname); } -}
\ No newline at end of file +} diff --git a/verity/KeystoreSigner.mf b/verity/KeystoreSigner.mf index a4fee27f..472b7c43 100644 --- a/verity/KeystoreSigner.mf +++ b/verity/KeystoreSigner.mf @@ -1 +1 @@ -Main-Class: com.android.verity.KeystoreSigner
\ No newline at end of file +Main-Class: com.android.verity.BootKeystore diff --git a/verity/Utils.java b/verity/Utils.java index 2c1e7bb4..3576e3b0 100644 --- a/verity/Utils.java +++ b/verity/Utils.java @@ -18,22 +18,60 @@ 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.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.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(PKCSObjectIdentifiers.sha1WithRSAEncryption.getId(), "SHA1withRSA"); + ID_TO_ALG.put(PKCSObjectIdentifiers.sha256WithRSAEncryption.getId(), "SHA256withRSA"); + ID_TO_ALG.put(PKCSObjectIdentifiers.sha512WithRSAEncryption.getId(), "SHA512withRSA"); + + 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; @@ -88,10 +126,45 @@ public class Utils { 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 keySpec = new PKCS8EncodedKeySpec(der); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - return (PrivateKey) keyFactory.generatePrivate(keySpec); + 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 { @@ -128,8 +201,48 @@ public class Utils { 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) { + if ("RSA".equals(key.getAlgorithm())) { + return "SHA256withRSA"; + } else { + throw new IllegalArgumentException("Unsupported key type " + key.getAlgorithm()); + } + } + + static AlgorithmIdentifier getSignatureAlgorithmIdentifier(Key key) { + 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.getObjectId().getId()); + + if (algName == null) { + throw new IllegalArgumentException("Unsupported algorithm " + algId.getObjectId()); + } + + 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("SHA1withRSA"); + Signature signer = Signature.getInstance(getSignatureAlgorithm(privateKey)); signer.initSign(privateKey); signer.update(input); return signer.sign(); @@ -153,4 +266,4 @@ public class Utils { out.write(data); out.close(); } -}
\ No newline at end of file +} diff --git a/verity/VeritySigner.java b/verity/VeritySigner.java index 44c56028..9d857473 100644 --- a/verity/VeritySigner.java +++ b/verity/VeritySigner.java @@ -16,18 +16,54 @@ 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 { - // USAGE: - // VeritySigner <contentfile> <key.pem> <sigfile> - // To verify that this has correct output: - // openssl rsautl -raw -inkey <key.pem> -encrypt -in <sigfile> > /tmp/dump + 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]); - PrivateKey privateKey = Utils.loadPEMPrivateKey(Utils.read(args[1])); - byte[] signature = Utils.sign(privateKey, content); - Utils.write(signature, args[2]); + + 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/VerityVerifier.java b/verity/VerityVerifier.java new file mode 100644 index 00000000..5c9d7d28 --- /dev/null +++ b/verity/VerityVerifier.java @@ -0,0 +1,166 @@ +/* + * 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.Process; +import java.lang.Runtime; +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 VerityVerifier { + + 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 VERITY_MAGIC = 0xB001B001; + private static final int VERITY_SIGNATURE_SIZE = 256; + private static final int VERITY_VERSION = 0; + + /** + * Converts a 4-byte little endian value to a Java integer + * @param value Little endian integer to convert + */ + public 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 + */ + public static int fromle(short value) { + return fromle(value << 16); + } + + /** + * Unsparses a sparse image into a temporary file and returns a + * handle to the file + * @param fname Path to a sparse image file + */ + public static RandomAccessFile openImage(String fname) throws Exception { + File tmp = File.createTempFile("system", ".raw"); + tmp.deleteOnExit(); + + Process p = Runtime.getRuntime().exec("simg2img " + fname + + " " + tmp.getAbsoluteFile()); + + p.waitFor(); + if (p.exitValue() != 0) { + throw new IllegalArgumentException("Invalid image: failed to unsparse"); + } + + return new RandomAccessFile(tmp, "r"); + } + + /** + * 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; + } + + /** + * Reads and validates verity metadata, and check the signature against the + * given public key + * @param img File handle to the image file + * @param key Public key to use for signature verification + */ + public static boolean verifyMetaData(RandomAccessFile img, PublicKey key) + throws Exception { + img.seek(getMetadataPosition(img)); + 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"); + } + + byte[] signature = new byte[VERITY_SIGNATURE_SIZE]; + img.readFully(signature); + + int tableSize = fromle(img.readInt()); + + byte[] table = new byte[tableSize]; + img.readFully(table); + + return Utils.verify(key, table, signature, + Utils.getSignatureAlgorithmIdentifier(key)); + } + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + System.err.println("Usage: VerityVerifier <sparse.img> <certificate.x509.pem>"); + System.exit(1); + } + + Security.addProvider(new BouncyCastleProvider()); + + X509Certificate cert = Utils.loadPEMCertificate(args[1]); + PublicKey key = cert.getPublicKey(); + RandomAccessFile img = openImage(args[0]); + + try { + if (verifyMetaData(img, key)) { + 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); + } +} diff --git a/verity/VerityVerifier.mf b/verity/VerityVerifier.mf new file mode 100644 index 00000000..6118b31c --- /dev/null +++ b/verity/VerityVerifier.mf @@ -0,0 +1 @@ +Main-Class: com.android.verity.VerityVerifier diff --git a/verity/generate_verity_key.c b/verity/generate_verity_key.c index 7414af58..a55600cd 100644 --- a/verity/generate_verity_key.c +++ b/verity/generate_verity_key.c @@ -108,6 +108,66 @@ out: 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; @@ -153,13 +213,16 @@ out: } static void usage(){ - printf("Usage: generate_verity_key <path-to-key>"); + 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) { + 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); } - return generate_key(argv[1]); -}
\ No newline at end of file +} diff --git a/verity/keystore_signer b/verity/keystore_signer index 7619c546..445f0c9c 100644..100755 --- a/verity/keystore_signer +++ b/verity/keystore_signer @@ -5,4 +5,4 @@ KEYSTORESIGNER_HOME=`dirname "$0"` KEYSTORESIGNER_HOME=`dirname "$KEYSTORESIGNER_HOME"` -java -Xmx512M -jar "$KEYSTORESIGNER_HOME"/framework/KeystoreSigner.jar "$@"
\ No newline at end of file +java -Xmx512M -jar "$KEYSTORESIGNER_HOME"/framework/BootKeystoreSigner.jar "$@" diff --git a/verity/verity_verifier b/verity/verity_verifier new file mode 100755 index 00000000..f145228f --- /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 "$@" |