简体   繁体   English

在 Android 上验证数字签名

[英]Verify Digital Signature on Android

I am developing an Android application that requires Digitally signing an html document.我正在开发一个需要对 html 文档进行数字签名的 Android 应用程序。 The document resides in the DB, in a JSON form.该文档以 JSON 形式驻留在数据库中。 I'm signing the document locally using a BASH Script I found on some other SO question :我正在使用在其他一些 SO 问题中找到的 BASH 脚本在本地签署文档:

openssl dgst -sha1 someHTMLDoc.html > hash openssl rsautl -sign -inkey privateKey.pem -keyform PEM -in hash > signature.bin

Private key was generated using :私钥是使用以下方法生成的:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -pkeyopt rsa_keygen_pubexp:3 -out privateKey.pem 

Public key was generated using :公钥是使用以下方法生成的:

openssl pkey -in privateKey.pem -out publicKey.pem -pubout

I want to verify the signature created in Signature.bin together with the data in someHTMLDoc.html, back in the application.我想验证在 Signature.bin 中创建的签名以及 someHTMLDoc.html 中的数据,回到应用程序中。

I am sending both the html and signature as JSON Object ex:我将 html 和签名作为 JSON 对象发送:

{ "data" : "<html><body></body></html>", "signature":"6598 13a9 b12b 21a9 ..... " }

The android application holds the PublicKey in shared prefs as follows : android 应用程序将 PublicKey 保存在共享首选项中,如下所示:

-----BEGIN PUBLIC KEY----- MIIBIDANBgkqhkiG9w0AAAEFAAOCAQ0AvniCAKCAQEAvni/NSEX3Rhx91HkJl85 \\nx1noyYET ......

Notice the "\\n" (newline) in there (was automatically added when copying string from publicKey.pem to Android Gradle Config.请注意其中的“\\n” (换行符)(在将字符串从 publicKey.pem 复制到 Android Gradle 配置时自动添加)。

Ok, after all preparations, now the question.好了,一切准备就绪,现在问题来了。 I am trying to validate the key with no success.我试图验证密钥但没有成功。

I am using the following code :我正在使用以下代码:

private boolean verifySignature(String data, String signature) {
    InputStream is = null;
    try {
        is = new ByteArrayInputStream(Config.getDogbarPublic().getBytes("UTF-8")); //Read DogBar Public key

        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        List<String> lines = new ArrayList<String>();
        String line;
        while ((line = br.readLine()) != null)
            lines.add(line);

        // removes the first and last lines of the file (comments)
        if (lines.size() > 1 && lines.get(0).startsWith("-----") && lines.get(lines.size() - 1).startsWith("-----")) {
            lines.remove(0);
            lines.remove(lines.size() - 1);
        }

        // concats the remaining lines to a single String
        StringBuilder sb = new StringBuilder();
        for (String aLine : lines)
            sb.append(aLine);
        String key = sb.toString();

        byte[] keyBytes = Base64.decode(key.getBytes("utf-8"), Base64.DEFAULT);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(spec);

        Signature signCheck = Signature.getInstance("SHA1withRSA"); //Instantiate signature checker object.
        signCheck.initVerify(publicKey);
        signCheck.update(data.getBytes());
        return signCheck.verify(signature.getBytes()); //verify signature with public key
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

Can anyone help ?任何人都可以帮忙吗? what am i doing wrong ?我究竟做错了什么 ?

Am i missing some byte conversion ?我错过了一些字节转换吗? maybe the JSON object is affecting the signature ?也许 JSON 对象正在影响签名?

Should a signature contain the \\n (linebreak) that the original file contains or should it be without in the JSON file ?签名应该包含原始文件包含的\\n (换行符)还是应该在 JSON 文件中不包含?

Thanks in advance for all the help, its highly appreciated.在此先感谢所有帮助,非常感谢。

Digital signature is a process of computing digest (function H) of data (C) and encrypting it with asymmetric encryption algorithm (function E) to produce cypher text (S):数字签名是计算数据(C)的摘要(函数H)并用非对称加密算法(函数E)加密生成密文(S)的过程:

S = E(H(C))

Signature verification takes the signature decrypts the given signature (function D) - which results in H(C) only if the public key used in decryption is paired with private key used in encryption, and computes the digest of data to check if the two digests match:签名验证采用签名解密给定的签名(函数 D)-仅当解密中使用的公钥与加密中使用的私钥配对时才会产生 H(C),并计算数据摘要以检查两个摘要比赛:

H(C) == D(E(H(C)))

It's clear from this that the bytes given to the hash function (C) must be exactly the same in order for the signature to validate.从中可以清楚地看出,分配给散列函数 (C) 的字节必须完全相同,以便签名验证。

In your case they are not, because when you're computing the digest using openssl dgst the output (H(C) on the right) is literally something like:在您的情况下,它们不是,因为当您使用openssl dgst计算摘要时,输出(右侧的 H(C))实际上类似于:

SHA1(someHTMLDoc.html)= 22596363b3de40b06f981fb85d82312e8c0ed511

And this is the input to the RSA encryption.这是 RSA 加密的输入。

And when you're verifying the signature, the output of the digest (H(C) on the left) are the raw bytes, for instance in hex:当您验证签名时,摘要的输出(左侧的 H(C))是原始字节,例如十六进制:

22596363b3de40b06f981fb85d82312e8c0ed511

So you end up encrypting bytes to produce (H(C) on the right):所以你最终加密字节以产生(右侧的 H(C)):

0000000: 5348 4131 2873 6f6d 6548 746d 6c44 6f63  SHA1(someHtmlDoc
0000010: 2e68 746d 6c29 3d20 3232 3539 3633 3633  .html)= 22596363
0000020: 6233 6465 3430 6230 3666 3938 3166 6238  b3de40b06f981fb8
0000030: 3564 3832 3331 3265 3863 3065 6435 3131  5d82312e8c0ed511
0000040: 0a                                       .

and comparing against bytes (H(C) on the left):并与字节进行比较(左侧的 H(C)):

0000000: 2259 6363 b3de 40b0 6f98 1fb8 5d82 312e  "Ycc..@.o...].1.
0000010: 8c0e d511                                ....

Also you need to use -sign with openssl dgst in order to have proper output format (see Difference between openSSL rsautl and dgst ).另外你需要使用-signopenssl dgst为了有适当的输出格式(参见OpenSSL的rsautl和DGST之间的差异)。

So on the OpenSSL side do:所以在 OpenSSL 端做:

openssl dgst -sha1 -sign privateKey.pem someHTMLDoc.html > signature.bin

On the Java side do:在 Java 端做:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;

import org.spongycastle.util.io.pem.PemObject;
import org.spongycastle.util.io.pem.PemReader;

public class VerifySignature {
    public static void main(final String[] args) throws Exception {
        try (PemReader reader = publicKeyReader(); InputStream data = data(); InputStream signatureData = signature()) {
            final PemObject publicKeyPem = reader.readPemObject();
            final byte[] publicKeyBytes = publicKeyPem.getContent();
            final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            final X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
            final RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(publicKeySpec);

            final Signature signature = Signature.getInstance("SHA1withRSA");
            signature.initVerify(publicKey);

            final byte[] buffy = new byte[16 * 1024];
            int read = -1;
            while ((read = data.read(buffy)) != -1) {
                signature.update(buffy, 0, read);
            }

            final byte[] signatureBytes = new byte[publicKey.getModulus().bitLength() / 8];
            signatureData.read(signatureBytes);

            System.out.println(signature.verify(signatureBytes));
        }
    }

    private static InputStream data() throws FileNotFoundException {
        return new FileInputStream("someHTMLDoc.html");
    }

    private static PemReader publicKeyReader() throws FileNotFoundException {
        return new PemReader(new InputStreamReader(new FileInputStream("publicKey.pem")));
    }

    private static InputStream signature() throws FileNotFoundException {
        return new FileInputStream("signature.bin");
    }
}

I've used Spongy Castle for PEM decoding of the public key to make things a bit more readable and easier to use.我已经使用Spongy Castle对公钥进行 PEM 解码,以提高可读性和易用性。

If you have a digitally signed XML file (downloaded from the web) and a certificate (.cer file) and you want to verify the digital signature in an android app then here is the code:如果你有一个数字签名的 XML 文件(从网上下载)和一个证书(.cer 文件),并且你想在一个 android 应用程序中验证数字签名,那么这里是代码:

You need two things xmlFilePath and certificateFilePath你需要两件事 xmlFilePath 和 certificateFilePath

boolean verifySignature() {
        boolean valid = false;
        try {

            File file = new File("xmlFilePath");
            DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
            f.setNamespaceAware(true);
            Document doc = f.newDocumentBuilder().parse(file);

            NodeList nodes = doc.getElementsByTagNameNS(Constants.SignatureSpecNS, "Signature");
            if (nodes.getLength() == 0) {
                throw new Exception("Signature NOT found!");
            }

            Element sigElement = (Element) nodes.item(0);
            XMLSignature signature = new XMLSignature(sigElement, "");


            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream ims = new InputStream("certificateFilePath");
            X509Certificate cert = (X509Certificate) cf.generateCertificate(ims);

            if (cert == null) {
                PublicKey pk = signature.getKeyInfo().getPublicKey();
                if (pk == null) {
                    throw new Exception("Did not find Certificate or Public Key");
                }
                valid = signature.checkSignatureValue(pk);
            } else {
                valid = signature.checkSignatureValue(cert);
            }
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(this, "Failed signature " + e.getMessage(), Toast.LENGTH_SHORT).show();
        }

        return valid;
    }

If you want to do it in java but not in android studio.如果你想在java中而不是在android studio中做。 Here is the code:这是代码:

public static boolean isXmlDigitalSignatureValid(String signedXmlFilePath,
                                                     String pubicKeyFilePath) throws Exception {

        boolean validFlag;
        File file = new File(signedXmlFilePath);
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document doc = db.parse(file);
        doc.getDocumentElement().normalize();
        NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
        if (nl.getLength() == 0) {
            throw new Exception("No XML Digital Signature Found, document is discarded");
        }
        FileInputStream fileInputStream = new FileInputStream(pubicKeyFilePath);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) cf.generateCertificate(fileInputStream);
        PublicKey publicKey = cert.getPublicKey();
        DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(0));
        XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
        XMLSignature signature = fac.unmarshalXMLSignature(valContext);
        validFlag = signature.validate(valContext);
        return validFlag;

    }

The reason is that you will need to add dependency if you use the same code in android studio, sometimes confusing also.原因是如果您在 android studio 中使用相同的代码,则需要添加依赖项,有时也会造成混淆。

If you are interested in reading digital signature documents, you can read www.xml.com/post It is an interesting document for understanding the need for a digital signature.如果您有兴趣阅读数字签名文档,可以阅读www.xml.com/post ,这是一个了解数字签名需求的有趣文档。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM