android签名的应用-- 禁止未经授权签名的apk安装

来源:互联网 发布:黄立行和徐静蕾 知乎 编辑:程序博客网 时间:2024/05/29 02:06

android签名的应用-- 禁止未经授权签名的apk安装

最近项目有需求: 只有使用特定签名签的apk才可以安装,其他任何apk都不能安装(root版,使用adb push进去的除外)。n多度娘、google之后最终实现,把实现代码罗列一下,以供以后参考.


1、使用工具自己制作签名文件,这个签名就是需要提供给apk制作者签名使用的。 

可以参考源码路径 build/target/product/security/README 文件

development/tools/make_key testkey  ‘/C=CN/ST=ShangHai/L=ShangHai/O=xxx/OU=MTK/CN=China/emailAddress=xxx@qq.com'


2、使用制作好的签名文件签名一个内置的apk,本例采用使用 com.mediatek.factorymode  作为以后签名的对比对象

mediatek/packages/apps/FactoryMode/Android.mk

增加 

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. ifeq ($(XHW_SIGNATURE_CONFIG),yes)  
  2. LOCAL_CERTIFICATE := testkey  
  3. endif  


3、在安装过程中对比签名,如果签名相同的话就继续安装,否则给出一个错误号,弹一个消息框

frameworks/base/services/Java/com/android/server/pm/PackageManagerService.java

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. private PackageParser.Package scanPackageLI(PackageParser.Package pkg,  
  2.             int parseFlags, int scanMode, long currentTime, UserHandle user) {  
  3.             ...  
  4.         if (!verifySignaturesLP(pkgSetting, pkg)) {  
  5.         ...  
  6.         }  
  7. //add start  
  8.         if(com.mediatek.common.featureoption.FeatureOption.XHW_SIGNATURE_CONFIG) {  
  9.            // is xhw signatures  
  10.             Signature[] xhwSignatures = getXWHSignatures();  
  11.   
  12.                    if (xhwSignatures != null) {  
  13.                        if (compareSignatures(xhwSignatures, pkg.mSignatures) != PackageManager.SIGNATURE_MATCH) {  
  14.                             mLastScanError = PackageManager.INSTALL_FAILED_INVALID_SIGNATURES;  
  15.                             return null;  
  16.                         }  
  17.                    }  
  18.         }  
  19. //add end  
  20.   
  21.             // Verify that this new package doesn't have any content providers  
  22.             // that conflict with existing packages.  Only do this if the  
  23.             // package isn't already installed, since we don't want to break  
  24.             // things that are installed.  
  25.             if ((scanMode&SCAN_NEW_INSTALL) != 0) {  
  26.            ...  
  27. }         

因为在启动是也要走 scanPackageLI 函数,故增加一个变量 mIsInstallApkFlag 判断是否是安装过程

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.     private void installPackageLI(InstallArgs args,  
  2.             boolean newInstall, PackageInstalledInfo res) {  
  3.             ...  
  4.         Log.i(TAG, "Start installation for package: " + pkg.packageName);  
  5.   
  6.  //add start  
  7.     if(com.mediatek.common.featureoption.FeatureOption.XHW_SIGNATURE_CONFIG) {  
  8.         Log.i(TAG, "installPackageLI-111- mIsInstallApkFlag="+mIsInstallApkFlag);  
  9.         mIsInstallApkFlag = true;  
  10.           
  11.         Log.i(TAG, "installPackageLI-222- mIsInstallApkFlag="+mIsInstallApkFlag);  
  12.     }  
  13. //add end  
  14.   
  15.         if (replace) {  
  16.             replacePackageLI(pkg, parseFlags, scanMode, args.user,  
  17.                     installerPackageName, res);  
  18.         } else {  
  19.             installNewPackageLI(pkg, parseFlags, scanMode, args.user,  
  20.                     installerPackageName, res);  
  21.         }  
  22.         Log.i(TAG, "Installation done for package: " + pkg.packageName);  
  23.         ...  
  24.     }              

增加的函数为:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.  //add start  
  2.     private boolean mIsInstallApkFlag = false;  
  3.     private Signature[] getXWHSignatures(){  
  4.         if(!mIsInstallApkFlag) return null;  
  5.           
  6.         // 取得xhw签名      
  7.         Signature[] xhwSigns = null;  
  8.         String packageName = "com.mediatek.factorymode";  
  9. /*      try {            
  10.             //PackageInfo xhwPackageInfo = mContext.getPackageManager().getPackageInfo("com.mediatek.factorymode", PackageManager.GET_SIGNATURES);    //-- 只能取得已安装的apk 签名    
  11.             PackageInfo xhwPackageInfo = getPackageInfo("com.mediatek.factorymode", PackageManager.GET_SIGNATURES, Process.SYSTEM_UID);    
  12.  
  13.  
  14.             if(xhwPackageInfo != null){ 
  15.                 xhwSigns = xhwPackageInfo.signatures;  
  16.             } 
  17.         } catch (Exception e) {             
  18.             e.printStackTrace();    
  19.  
  20.         } 
  21. if(xhwSigns != null) Log.e(TAG, "....----getXWHSignatures--xhw_Signs[0]="+xhwSigns[0].toCharsString()); 
  22.  
  23. */  
  24.   
  25.         //another method  
  26.             PackageSetting ps = mSettings.mPackages.get(packageName);  
  27.             if (ps != null) {  
  28.                 PackageParser.Package pkg = ps.pkg;  
  29.                 if (pkg == null) {  
  30.                     pkg = new PackageParser.Package(packageName);  
  31.                     pkg.applicationInfo.packageName = packageName;  
  32.                     pkg.applicationInfo.flags = ps.pkgFlags | ApplicationInfo.FLAG_IS_DATA_ONLY;  
  33.                     pkg.applicationInfo.publicSourceDir = ps.resourcePathString;  
  34.                     pkg.applicationInfo.sourceDir = ps.codePathString;  
  35.                     pkg.applicationInfo.dataDir = getDataPathForPackage(packageName, 0).getPath();  
  36.                     pkg.applicationInfo.nativeLibraryDir = ps.nativeLibraryPathString;  
  37.                 }  
  38.   
  39.                 PackageInfo xhwPackageInfo2 = generatePackageInfo(pkg, PackageManager.GET_SIGNATURES, UserHandle.getCallingUserId());  
  40.                   
  41.             if(xhwPackageInfo2 != null){  
  42.                 xhwSigns = xhwPackageInfo2.signatures;   
  43.             }  
  44.             }  
  45.   
  46.         // reset flag  
  47.         mIsInstallApkFlag = false;  
  48.   
  49.         return xhwSigns;  
  50.     }  
  51.   
  52. //add end  

上面代码中 PackageManager.INSTALL_FAILED_INVALID_SIGNATURES 是自己增加的错误代码,以供弹出消息使用

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. +++ frameworks/base/core/java/android/content/pm/PackageManager.java    (revision 471)  
  2. @@ -553,7 +553,17 @@  
  3.       */  
  4.      public static final int INSTALL_FAILED_VERSION_DOWNGRADE = -25;  
  5.    
  6. +  
  7.      /** 
  8. +     * Installation return code: this is passed to the {@link IPackageInstallObserver} by 
  9. +     * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if 
  10. +     * the installed package hasn't the expected signature 
  11. +     *  @hide 
  12. +     */  
  13. +    public static final int INSTALL_FAILED_INVALID_SIGNATURES = -26;   //jimbo add  
  14. +  
  15. +  
  16. +    /**  
  17.       * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by  
  18.       * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)}  
  19.       * if the parser was given a path that is not a file, or does not end with the expected  

4、 弹出错误提示

packages/apps/PackageInstaller/src/com/android/packageinstaller/InstallAppProgress.java

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1.    private Handler mHandler = new Handler() {  
  2.         public void handleMessage(Message msg) {  
  3.             switch (msg.what) {  
  4.             ...  
  5. //add start  
  6.                     } else if (msg.arg1 == PackageManager.INSTALL_FAILED_INVALID_SIGNATURES){  
  7.                         // Generic error handling for all other error codes.  
  8.                         centerTextDrawable.setLevel(1);  
  9.                         centerExplanationLabel = getExplanationFromErrorCode(msg.arg1);  
  10.                         centerTextLabel = R.string.install_failed_invalid_signature;  
  11.                         mLaunchButton.setVisibility(View.INVISIBLE);  
  12. //add end  
  13.                     } else {  
  14.                         // Generic error handling for all other error codes.  
  15.                         centerTextDrawable.setLevel(1);  
  16.                         centerExplanationLabel = getExplanationFromErrorCode(msg.arg1);  
  17.                         centerTextLabel = R.string.install_failed;  
  18.                         mLaunchButton.setVisibility(View.INVISIBLE);  
  19.                     }  
  20.                 ...  
  21.                   
其中字串 install_failed_invalid_signature 是新增的字串 例如  “禁止安装,签名不符”  等

windows下给apk签名的方法:



制作一个批处理文件  Auto_Sign.bat 内容:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. @echo off  
  2. :menu  
  3. cls  
  4. color 0b  
  5. echo.  
  6. echo *****************************************************  
  7. echo   Android platform auto-sign tool V0.2 by imchange  
  8. echo   Please select your choice then press ENTER  
  9. echo *****************************************************  
  10. echo.  
  11. echo     1. Sign ROM files (*.zip)  
  12. echo.  
  13. echo     2. Sign program files (*.apk)  
  14. echo.  
  15. echo     3. Sign ALL (*.zip/*.apk)  
  16. echo.  
  17. echo     4. Exit  
  18. echo.  
  19. set /p choice=Please select:    
  20. if /i "%choice%"=="1" goto rom  
  21. if /i "%choice%"=="2" goto apk  
  22. if /i "%choice%"=="3" goto all  
  23. if /i "%choice%"=="4" exit  
  24. goto menu  
  25.   
  26. :rom  
  27. set filename=*.zip  
  28. goto sign  
  29.   
  30. :apk  
  31. set filename=*.apk  
  32. goto sign  
  33.   
  34. :all  
  35. set filename=*.zip *.apk  
  36. goto sign  
  37.   
  38. :sign  
  39. echo.  
  40. echo Please wait while signing...   
  41. echo The signed files will be placed into subfolder 'Signed'...  
  42. echo.  
  43. if not exist .\Signed mkdir .\Signed  
  44. for /f "delims=" %%i in ('dir/b %filename%'do (  
  45. java -jar signapk.jar testkey.x509.pem testkey.pk8 "%%i" ".\Signed\%%~ni_signed%%~xi")  
  46. echo.  
  47. echo Completed. Press any key to exit.  
  48. pause >nul  
  49. exit  


readme.txt

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. Android platform ROM/program files auto-sign batch tool  
  2. Created by imchange  
  3.   
  4. Usage:  
  5. Place your ROM files (*.zip) or program files (*.apk) to this folder, execute Auto_Sign.bat by double click, then select the type of files you want to sign. The signed files are placed into subfolder 'Signed'.  
  6.   
  7. 用法:  
  8. 将ROM文件(*.zip)或程序(*.apk)放到本文件夹,双击执行Auto_Sign.bat,选择要签名的文件类型。签名后的文件放置在Signed文件夹内。  
  9.   
  10. Changelog:  
  11. V0.2  
  12. -All functions merge into one batch file  
  13. -Add select menu  
  14.   
  15. V0.1  
  16. -The first version  

signapk.jar

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. package com.android.signapk;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.ByteArrayOutputStream;  
  5. import java.io.DataInputStream;  
  6. import java.io.File;  
  7. import java.io.FileInputStream;  
  8. import java.io.FileOutputStream;  
  9. import java.io.FilterOutputStream;  
  10. import java.io.IOException;  
  11. import java.io.InputStream;  
  12. import java.io.InputStreamReader;  
  13. import java.io.OutputStream;  
  14. import java.io.PrintStream;  
  15. import java.security.DigestOutputStream;  
  16. import java.security.GeneralSecurityException;  
  17. import java.security.Key;  
  18. import java.security.KeyFactory;  
  19. import java.security.MessageDigest;  
  20. import java.security.PrivateKey;  
  21. import java.security.Signature;  
  22. import java.security.SignatureException;  
  23. import java.security.cert.CertificateFactory;  
  24. import java.security.cert.X509Certificate;  
  25. import java.security.spec.InvalidKeySpecException;  
  26. import java.security.spec.KeySpec;  
  27. import java.security.spec.PKCS8EncodedKeySpec;  
  28. import java.util.Enumeration;  
  29. import java.util.Map;  
  30. import java.util.Map.Entry;  
  31. import java.util.jar.Attributes;  
  32. import java.util.jar.JarEntry;  
  33. import java.util.jar.JarFile;  
  34. import java.util.jar.JarOutputStream;  
  35. import java.util.jar.Manifest;  
  36. import javax.crypto.Cipher;  
  37. import javax.crypto.EncryptedPrivateKeyInfo;  
  38. import javax.crypto.SecretKeyFactory;  
  39. import javax.crypto.spec.PBEKeySpec;  
  40. import javax.security.auth.x500.X500Principal;  
  41. import sun.misc.BASE64Encoder;  
  42. import sun.security.pkcs.ContentInfo;  
  43. import sun.security.pkcs.PKCS7;  
  44. import sun.security.pkcs.SignerInfo;  
  45. import sun.security.x509.AlgorithmId;  
  46. import sun.security.x509.X500Name;  
  47.   
  48. class SignApk  
  49. {  
  50.   private static X509Certificate readPublicKey(File file)  
  51.     throws IOException, GeneralSecurityException  
  52.   {  
  53.     FileInputStream input = new FileInputStream(file);  
  54.     try {  
  55.       CertificateFactory cf = CertificateFactory.getInstance("X.509");  
  56.       return (X509Certificate)cf.generateCertificate(input);  
  57.     } finally {  
  58.       input.close();  
  59.     }  
  60.   }  
  61.   
  62.   private static String readPassword(File keyFile)  
  63.   {  
  64.     System.out.print("Enter password for " + keyFile + " (password will not be hidden): ");  
  65.     System.out.flush();  
  66.     BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));  
  67.     try {  
  68.       return stdin.readLine(); } catch (IOException ex) {  
  69.     }  
  70.     return null;  
  71.   }  
  72.   
  73.   private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)  
  74.     throws GeneralSecurityException  
  75.   {  
  76.     EncryptedPrivateKeyInfo epkInfo;  
  77.     try  
  78.     {  
  79.       epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);  
  80.     }  
  81.     catch (IOException ex) {  
  82.       return null;  
  83.     }  
  84.   
  85.     char[] password = readPassword(keyFile).toCharArray();  
  86.   
  87.     SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());  
  88.     Key key = skFactory.generateSecret(new PBEKeySpec(password));  
  89.   
  90.     Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());  
  91.     cipher.init(2, key, epkInfo.getAlgParameters());  
  92.     try  
  93.     {  
  94.       return epkInfo.getKeySpec(cipher);  
  95.     } catch (InvalidKeySpecException ex) {  
  96.       System.err.println("signapk: Password for " + keyFile + " may be bad.");  
  97.       throw ex;  
  98.     }  
  99.   }  
  100.   
  101.   private static PrivateKey readPrivateKey(File file)  
  102.     throws IOException, GeneralSecurityException  
  103.   {  
  104.     DataInputStream input = new DataInputStream(new FileInputStream(file));  
  105.     try {  
  106.       byte[] bytes = new byte[(int)file.length()];  
  107.       input.read(bytes);  
  108.   
  109.       KeySpec spec = decryptPrivateKey(bytes, file);  
  110.       if (spec == null) {  
  111.         spec = new PKCS8EncodedKeySpec(bytes);  
  112.       }  
  113.       try  
  114.       {  
  115.         return KeyFactory.getInstance("RSA").generatePrivate(spec);  
  116.       } catch (InvalidKeySpecException ex) {  
  117.         return KeyFactory.getInstance("DSA").generatePrivate(spec);  
  118.       }  
  119.     } finally {  
  120.       input.close();  
  121.     }  
  122.   }  
  123.   
  124.   private static Manifest addDigestsToManifest(JarFile jar)  
  125.     throws IOException, GeneralSecurityException  
  126.   {  
  127.     Manifest input = jar.getManifest();  
  128.     Manifest output = new Manifest();  
  129.     Attributes main = output.getMainAttributes();  
  130.     if (input != null) {  
  131.       main.putAll(input.getMainAttributes());  
  132.     } else {  
  133.       main.putValue("Manifest-Version""1.0");  
  134.       main.putValue("Created-By""1.0 (Android SignApk)");  
  135.     }  
  136.   
  137.     BASE64Encoder base64 = new BASE64Encoder();  
  138.     MessageDigest md = MessageDigest.getInstance("SHA1");  
  139.     byte[] buffer = new byte[4096];  
  140.   
  141.     for (Enumeration e = jar.entries(); e.hasMoreElements(); ) {  
  142.       JarEntry entry = (JarEntry)e.nextElement();  
  143.       String name = entry.getName();  
  144.       if ((!entry.isDirectory()) && (!name.equals("META-INF/MANIFEST.MF"))) {  
  145.         InputStream data = jar.getInputStream(entry);  
  146.         int num;  
  147.         while ((num = data.read(buffer)) > 0) {  
  148.           md.update(buffer, 0, num);  
  149.         }  
  150.   
  151.         Attributes attr = null;  
  152.         if (input != null) attr = input.getAttributes(name);  
  153.         attr = attr != null ? new Attributes(attr) : new Attributes();  
  154.         attr.putValue("SHA1-Digest", base64.encode(md.digest()));  
  155.         output.getEntries().put(name, attr);  
  156.       }  
  157.     }  
  158.   
  159.     return output;  
  160.   }  
  161.   
  162.   private static void writeSignatureFile(Manifest manifest, OutputStream out)  
  163.     throws IOException, GeneralSecurityException  
  164.   {  
  165.     Manifest sf = new Manifest();  
  166.     Attributes main = sf.getMainAttributes();  
  167.     main.putValue("Signature-Version""1.0");  
  168.     main.putValue("Created-By""1.0 (Android SignApk)");  
  169.   
  170.     BASE64Encoder base64 = new BASE64Encoder();  
  171.     MessageDigest md = MessageDigest.getInstance("SHA1");  
  172.     PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md), true"UTF-8");  
  173.   
  174.     manifest.write(print);  
  175.     print.flush();  
  176.     main.putValue("SHA1-Digest-Manifest", base64.encode(md.digest()));  
  177.   
  178.     Map entries = manifest.getEntries();  
  179.     for (Map.Entry entry : entries.entrySet())  
  180.     {  
  181.       print.print("Name: " + (String)entry.getKey() + "\r\n");  
  182.       for (Map.Entry att : ((Attributes)entry.getValue()).entrySet()) {  
  183.         print.print(att.getKey() + ": " + att.getValue() + "\r\n");  
  184.       }  
  185.       print.print("\r\n");  
  186.       print.flush();  
  187.   
  188.       Attributes sfAttr = new Attributes();  
  189.       sfAttr.putValue("SHA1-Digest", base64.encode(md.digest()));  
  190.       sf.getEntries().put(entry.getKey(), sfAttr);  
  191.     }  
  192.   
  193.     sf.write(out);  
  194.   }  
  195.   
  196.   private static void writeSignatureBlock(Signature signature, X509Certificate publicKey, OutputStream out)  
  197.     throws IOException, GeneralSecurityException  
  198.   {  
  199.     SignerInfo signerInfo = new SignerInfo(new X500Name(publicKey.getIssuerX500Principal().getName()), publicKey.getSerialNumber(), AlgorithmId.get("SHA1"), AlgorithmId.get("RSA"), signature.sign());  
  200.   
  201.     PKCS7 pkcs7 = new PKCS7(new AlgorithmId[] { AlgorithmId.get("SHA1") }, new ContentInfo(ContentInfo.DATA_OID, null), new X509Certificate[] { publicKey }, new SignerInfo[] { signerInfo });  
  202.   
  203.     pkcs7.encodeSignedData(out);  
  204.   }  
  205.   
  206.   private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out)  
  207.     throws IOException  
  208.   {  
  209.     byte[] buffer = new byte[4096];  
  210.   
  211.     Map entries = manifest.getEntries();  
  212.     for (String name : entries.keySet()) {  
  213.       JarEntry inEntry = in.getJarEntry(name);  
  214.       if (inEntry.getMethod() == 0)  
  215.       {  
  216.         out.putNextEntry(new JarEntry(inEntry));  
  217.       }  
  218.       else {  
  219.         out.putNextEntry(new JarEntry(name));  
  220.       }  
  221.   
  222.       InputStream data = in.getInputStream(inEntry);  
  223.       int num;  
  224.       while ((num = data.read(buffer)) > 0) {  
  225.         out.write(buffer, 0, num);  
  226.       }  
  227.       out.flush();  
  228.     }  
  229.   }  
  230.   
  231.   public static void main(String[] args) {  
  232.     if (args.length != 4) {  
  233.       System.err.println("Usage: signapk publickey.x509[.pem] privatekey.pk8 input.jar output.jar");  
  234.   
  235.       System.exit(2);  
  236.     }  
  237.   
  238.     JarFile inputJar = null;  
  239.     JarOutputStream outputJar = null;  
  240.     try  
  241.     {  
  242.       X509Certificate publicKey = readPublicKey(new File(args[0]));  
  243.       PrivateKey privateKey = readPrivateKey(new File(args[1]));  
  244.       inputJar = new JarFile(new File(args[2]), false);  
  245.       outputJar = new JarOutputStream(new FileOutputStream(args[3]));  
  246.       outputJar.setLevel(9);  
  247.   
  248.       Manifest manifest = addDigestsToManifest(inputJar);  
  249.       manifest.getEntries().remove("META-INF/CERT.SF");  
  250.       manifest.getEntries().remove("META-INF/CERT.RSA");  
  251.       outputJar.putNextEntry(new JarEntry("META-INF/MANIFEST.MF"));  
  252.       manifest.write(outputJar);  
  253.   
  254.       Signature signature = Signature.getInstance("SHA1withRSA");  
  255.       signature.initSign(privateKey);  
  256.       outputJar.putNextEntry(new JarEntry("META-INF/CERT.SF"));  
  257.       writeSignatureFile(manifest, new SignatureOutputStream(outputJar, signature));  
  258.   
  259.       outputJar.putNextEntry(new JarEntry("META-INF/CERT.RSA"));  
  260.       writeSignatureBlock(signature, publicKey, outputJar);  
  261.   
  262.       copyFiles(manifest, inputJar, outputJar);  
  263.     } catch (Exception e) {  
  264.       e.printStackTrace();  
  265.       System.exit(1);  
  266.     } finally {  
  267.       try {  
  268.         if (inputJar != null) inputJar.close();  
  269.         if (outputJar != null) outputJar.close();   
  270.       }  
  271.       catch (IOException e) { e.printStackTrace();  
  272.         System.exit(1);  
  273.       }  
  274.     }  
  275.   }  
  276.   
  277.   private static class SignatureOutputStream extends FilterOutputStream  
  278.   {  
  279.     private Signature mSignature;  
  280.   
  281.     public SignatureOutputStream(OutputStream out, Signature sig)  
  282.     {  
  283.       super();  
  284.       this.mSignature = sig;  
  285.     }  
  286.   
  287.     public void write(int b) throws IOException  
  288.     {  
  289.       try {  
  290.         this.mSignature.update((byte)b);  
  291.       } catch (SignatureException e) {  
  292.         throw new IOException("SignatureException: " + e);  
  293.       }  
  294.       super.write(b);  
  295.     }  
  296.   
  297.     public void write(byte[] b, int off, int len) throws IOException  
  298.     {  
  299.       try {  
  300.         this.mSignature.update(b, off, len);  
  301.       } catch (SignatureException e) {  
  302.         throw new IOException("SignatureException: " + e);  
  303.       }  
  304.       super.write(b, off, len);  
  305.     }  
  306.   }  
  307. }  





附:参考文章

http://www.blogjava.net/zh-weir/archive/2011/07/19/354663.html


Android APK 签名比对

发布过Android应用的朋友们应该都知道,Android APK的发布是需要签名的。签名机制在Android应用和框架中有着十分重要的作用。

例如,Android系统禁止更新安装签名不一致的APK;如果应用需要使用system权限,必须保证APK签名与Framework签名一致,等等。在APK Crack一文中,我们了解到,要破解一个APK,必然需要重新对APK进行签名。而这个签名,一般情况无法再与APK原先的签名保持一致。(除非APK原作者的私钥泄漏,那已经是另一个层次的软件安全问题了。)

简单地说,签名机制标明了APK的发行机构。因此,站在软件安全的角度,我们就可以通过比对APK的签名情况,判断此APK是否由“官方”发行,而不是被破解篡改过重新签名打包的“盗版软件”。


Android签名机制

    为了说明APK签名比对对软件安全的有效性,我们有必要了解一下Android APK的签名机制。为了更易于大家理解,我们从Auto-Sign工具的一条批处理命令说起。

APK Crack一文中,我们了解到,要签名一个没有签名过的APK,可以使用一个叫作Auto-sign的工具。Auto-sign工具实际运行的是一个叫做Sign.bat的批处理命令。用文本编辑器打开这个批处理文件,我们可以发现,实现签名功能的命令主要是这一行命令:

    java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk

    这条命令的意义是:通过signapk.jar这个可执行jar包,以“
testkey.x509.pem”这个公钥文件和“testkey.pk8”这个私钥文件对“update.apk”进行签名,签名后的文件保存为“update_signed.apk”。

    对于此处所使用的私钥和公钥的生成方式,这里就不做进一步介绍了。这方面的资料大家可以找到很多。我们这里要讲的是signapk.jar到底做了什么。

    signapk.jar是Android源码包中的一个签名工具。由于Android是个开源项目,所以,很高兴地,我们可以直接找到signapk.jar的源码!路径为/build/tools/signapk/SignApk.java。

对比一个没有签名的APK和一个签名好的APK,我们会发现,签名好的APK包中多了一个叫做META-INF的文件夹。里面有三个文件,分别名为MANIFEST.MFCERT.SFCERT.RSA。signapk.jar就是生成了这几个文件(其他文件没有任何改变。因此我们可以很容易去掉原有签名信息)。

    通过阅读signapk源码,我们可以理清签名APK包的整个过程。


1、 
生成MANIFEST.MF文件:

程序遍历update.apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个生成SHA1的数字签名信息,再用Base64进行编码。具体代码见这个方法:

    private static Manifest addDigestsToManifest(JarFile jar)

关键代码如下:

 1     for (JarEntry entry: byName.values()) {
 2         String name = entry.getName();
 3         if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
 4             !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
 5                (stripPattern == null ||!stripPattern.matcher(name).matches())) {
 6                 InputStream data = jar.getInputStream(entry);
 7                 while ((num = data.read(buffer)) > 0) {
 8                     md.update(buffer, 0, num);
 9                 }
10                 Attributes attr = null;
11                 if (input != null) attr = input.getAttributes(name);
12                 attr = attr != null ? new Attributes(attr) : new Attributes();
13                 attr.putValue("SHA1-Digest", base64.encode(md.digest()));
14                 output.getEntries().put(name, attr);
15           }
16     }


    之后将生成的签名写入MANIFEST.MF文件。关键代码如下:

1     Manifest manifest = addDigestsToManifest(inputJar);
2     je = new JarEntry(JarFile.MANIFEST_NAME);
3     je.setTime(timestamp);
4     outputJar.putNextEntry(je);
5     manifest.write(outputJar);

    这里简单介绍下SHA1数字签名。简单地说,它就是一种安全哈希算法,类似于MD5算法。它把任意长度的输入,通过散列算法变成固定长度的输出(这里我们称作“摘要信息”)。你不能仅通过这个摘要信息复原原来的信息。另外,它保证不同信息的摘要信息彼此不同。因此,如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是程序就不能成功安装。


2、 
生成CERT.SF文件:

对前一步生成的Manifest,使用SHA1-RSA算法,用私钥进行签名。关键代码如下:

1     Signature signature = Signature.getInstance("SHA1withRSA");
2     signature.initSign(privateKey);
3     je = new JarEntry(CERT_SF_NAME);
4     je.setTime(timestamp);
5     outputJar.putNextEntry(je);
6     writeSignatureFile(manifest,
7     new SignatureOutputStream(outputJar, signature));

    RSA是一种非对称加密算法。用私钥通过RSA算法对摘要信息进行加密。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息进行对比,如果相符,则表明内容没有被异常修改。


3、 
生成CERT.RSA文件:

生成MANIFEST.MF没有使用密钥信息,生成CERT.SF文件使用了私钥文件。那么我们可以很容易猜测到,CERT.RSA文件的生成肯定和公钥相关。

CERT.RSA文件中保存了公钥、所采用的加密算法等信息。核心代码如下:

1     je = new JarEntry(CERT_RSA_NAME);
2     je.setTime(timestamp);
3     outputJar.putNextEntry(je);
4     writeSignatureBlock(signature, publicKey, outputJar);

    其中writeSignatureBlock的代码如下:

 1     private static void writeSignatureBlock(
 2         Signature signature, X509Certificate publicKey, OutputStream out)
 3             throws IOException, GeneralSecurityException {
 4                 SignerInfo signerInfo = new SignerInfo(
 5                 new X500Name(publicKey.getIssuerX500Principal().getName()),
 6                 publicKey.getSerialNumber(),
 7                 AlgorithmId.get("SHA1"),
 8                 AlgorithmId.get("RSA"),
 9                 signature.sign());
10 
11         PKCS7 pkcs7 = new PKCS7(
12             new AlgorithmId[] { AlgorithmId.get("SHA1") },
13             new ContentInfo(ContentInfo.DATA_OID, null),
14             new X509Certificate[] { publicKey },
15             new SignerInfo[] { signerInfo });
16 
17         pkcs7.encodeSignedData(out);
18     }

    好了,分析完APK包的签名流程,我们可以清楚地意识到:

1、 Android签名机制其实是对APK包完整性和发布机构唯一性的一种校验机制。

2、 Android签名机制不能阻止APK包被修改,但修改后的再签名无法与原先的签名保持一致。(拥有私钥的情况除外)。

3、 APK包加密的公钥就打包在APK包内,且不同的私钥对应不同的公钥。换句话言之,不同的私钥签名的APK公钥也必不相同。所以我们可以根据公钥的对比,来判断私钥是否一致。


APK签名比对的实现方式

    好了,通过Android签名机制的分析,我们从理论上证明了通过APK公钥的比对能判断一个APK的发布机构。并且这个发布机构是很难伪装的,我们暂时可以认为是不可伪装的。

    有了理论基础后,我们就可以开始实践了。那么如何获取到APK文件的公钥信息呢?因为Android系统安装程序肯定会获取APK信息进行比对,所以我们可以通过Android源码获得一些思路和帮助。

    源码中有一个隐藏的类用于APK包的解析。这个类叫PackageParser,路径为frameworks\base\core\java\android\content\pm\PackageParser.java。当我们需要获取APK包的相关信息时,可以直接使用这个类,下面代码就是一个例子函数:

 1     private PackageInfo parsePackage(String archiveFilePath, int flags){
 2         
 3         PackageParser packageParser = new PackageParser(archiveFilePath);
 4         DisplayMetrics metrics = new DisplayMetrics();
 5         metrics.setToDefaults();
 6         final File sourceFile = new File(archiveFilePath);
 7         PackageParser.Package pkg = packageParser.parsePackage(
 8                 sourceFile, archiveFilePath, metrics, 0);
 9         if (pkg == null) {
10             return null;
11         }
12         
13         packageParser.collectCertificates(pkg, 0); 
14         
15         return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0);
16     }

    其中参数archiveFilePath指定APK文件路径;flags需设置PackageManager.GET_SIGNATURES位,以保证返回证书签名信息。

    具体如何通过PackageParser获取签名信息在此处不做详述,具体代码请参考PackageParser中的public boolean collectCertificates(Package pkg, int flags)private Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer)方法。至于如何在Android应用开发中使用隐藏的类及方法,可以参看我的这篇文章:《Android应用开发中如何使用隐藏API》

    紧接着,我们可以通过packageInfo.signatures来访问到APK的签名信息。还需要说明的是 Android中Signature和Java中Certificate的对应关系。它们的关系如下面代码所示:

1     pkg.mSignatures = new Signature[certs.length];
2     for (int i=0; i<N; i++) {
3         pkg.mSignatures[i] = new Signature(
4         certs[i].getEncoded());
5     }

    也就是说signature = new Signature(certificate.getEncoded()); certificate证书中包含了公钥和证书的其他基本信息。公钥不同,证书肯定互不相同。我们可以通过certificate的getPublicKey方法获取公钥信息。所以比对签名证书本质上就是比对公钥信息。

    OK,获取到APK签名证书之后,就剩下比对了。这个简单,功能函数如下所示:

 1     private boolean IsSignaturesSame(Signature[] s1, Signature[] s2) {
 2             if (s1 == null) {
 3                 return false;
 4             }
 5             if (s2 == null) {
 6                 return false;
 7             }
 8             HashSet<Signature> set1 = new HashSet<Signature>();
 9             for (Signature sig : s1) {
10                 set1.add(sig);
11             }
12             HashSet<Signature> set2 = new HashSet<Signature>();
13             for (Signature sig : s2) {
14                 set2.add(sig);
15             }
16             // Make sure s2 contains all signatures in s1.
17             if (set1.equals(set2)) {
18                 return true;
19             }
20             return false;
21         }

APK签名比对的应用场景

    经过以上的论述,想必大家已经明白签名比对的原理和我的实现方式了。那么什么时候什么情况适合使用签名对比来保障Android APK的软件安全呢?

    个人认为主要有以下三种场景:

1、 程序自检测。在程序运行时,自我进行签名比对。比对样本可以存放在APK包内,也可存放于云端。缺点是程序被破解时,自检测功能同样可能遭到破坏,使其失效。

2、 可信赖的第三方检测。由可信赖的第三方程序负责APK的软件安全问题。对比样本由第三方收集,放在云端。这种方式适用于杀毒安全软件或者APP Market之类的软件下载市场。缺点是需要联网检测,在无网络情况下无法实现功能。(不可能把大量的签名数据放在移动设备本地)。

3、 系统限定安装。这就涉及到改Android系统了。限定仅能安装某些证书的APK。软件发布商需要向系统发布上申请证书。如果发现问题,能追踪到是哪个软件发布商的责任。适用于系统提供商或者终端产品生产商。缺点是过于封闭,不利于系统的开放性。

以上三种场景,虽然各有缺点,但缺点并不是不能克服的。例如,我们可以考虑程序自检测的功能用native method的方法实现等等。软件安全是一个复杂的课题,往往需要多种技术联合使用,才能更好的保障软件不被恶意破坏。

0 0
原创粉丝点击