使用AndroidKeyStore计算数字签名

computing digital signature by using AndroidKeyStore

我想使用 AndroidKeyStore 在 Android 中计算 RSA 数字签名。我有两个解决方案,第一个是使用 java.security.Signature,第二个是使用 javax.crypto.Cipher。一开始,我尝试使用 Signature 对象并成功计算签名,但我遇到了问题。签名对象从我的数据中自行生成摘要,所以我的第一个问题是:

1- 有什么方法可以通过禁用计算哈希来使用签名对象吗?

然后我通过下面的代码选择了第二种解决方案(使用 Cipher 对象):

// *** Creating Key
            KeyPairGenerator spec = KeyPairGenerator.getInstance(
                    // *** Specified algorithm here
                    // *** Specified: Purpose of key here
                    KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
            spec.initialize(new KeyGenParameterSpec.Builder(
                    alias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) //  RSA/ECB/PKCS1Padding
                    .setKeySize(2048)
                    // *** Replaced: setStartDate
                    .setKeyValidityStart(notBefore.getTime())
                    // *** Replaced: setEndDate
                    .setKeyValidityEnd(notAfter.getTime())
                    // *** Replaced: setSubject
                    .setCertificateSubject(new X500Principal("CN=test"))
                    // *** Replaced: setSerialNumber
                    .setCertificateSerialNumber(BigInteger.ONE)
                    .build());
            KeyPair keyPair = spec.generateKeyPair();

并使用密钥:

Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround");
inCipher.init(Cipher.ENCRYPT_MODE, privateKey);

但是在“inCipher.init”函数中我得到这个错误:

java.security.InvalidKeyException: Keystore operation failed
caused by: android.security.KeyStoreException: Incompatible purpose`

我的第二个问题是:2- 问题是什么? (我不得不说,我可以用public密钥加密,但我不能用私钥计算签名)

我在没有 androidKeyStore 的情况下使用私钥加密了一条消息,我成功了。代码如下:

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(2048);
        KeyPair kp = kpg.generateKeyPair();
        PublicKey pub = kp.getPublic();
        PrivateKey pvt = kp.getPrivate();

        Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        inCipher.init(Cipher.ENCRYPT_MODE, pvt);
        byte[] x = new byte[]{0x01, 0x01, 0x01, 0x01};
        byte[] result = inCipher.doFinal(x, 0, x.length);

关于您的第一个问题:

如果如评论中所述,消息已经 散列(例如使用 SHA256)并且只需要使用 PKCS#1 v1.5 填充进行签名,那么这NONEwithRSA 是可能的。但是,必须在关键属性中指定不使用摘要。
如果签名也应该符合标准,即可以使用 SHA256withRSA 进行验证,则摘要 ID(更准确地说,DigestInfo 值的 DER 编码)必须放在散列消息的前面。以下代码(基于发布的代码)针对 SHA256 显示了这一点(针对 Android P / API 28 进行了测试):

// Load keystore
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

// Create key (if not already in keystore)
String alias = "Some Alias";
if (!keyStore.containsAlias(alias)) {

    Calendar notBefore = Calendar.getInstance();
    Calendar notAfter = Calendar.getInstance();
    notAfter.add(Calendar.YEAR, 1);

    KeyPairGenerator spec = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
    spec.initialize(new KeyGenParameterSpec.Builder(alias,
            KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)              // for signing / verifying
            .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)        // use RSASSA-PKCS1-v1_5
            .setDigests(KeyProperties.DIGEST_NONE)                                  // apply no digest
            .setKeySize(2048)
            .setKeyValidityStart(notBefore.getTime())
            .setKeyValidityEnd(notAfter.getTime())
            .setCertificateSubject(new X500Principal("CN=test"))
            .setCertificateSerialNumber(BigInteger.ONE)
            .build());

    spec.generateKeyPair();
}

// Retrieve key
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);
PrivateKey privateKey = privateKeyEntry.getPrivateKey();
PublicKey publicKey = privateKeyEntry.getCertificate().getPublicKey();

// Hash message (hashedMessage corresponds to your message)
MessageDigest digest = MessageDigest.getInstance("SHA-256");                        // SHA256 as digest assumed
byte[] message = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
byte[] hashedMessage = digest.digest(message);

// Concatenate ID (in this example for SHA256) and message in this order
byte[] id = new byte[]{0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
byte[] idHashedMessage = new byte[id.length + hashedMessage.length];
System.arraycopy(id, 0, idHashedMessage, 0, id.length);
System.arraycopy(hashedMessage, 0, idHashedMessage, id.length, hashedMessage.length);

// Sign with NONEwithRSA
Signature signing = Signature.getInstance("NONEwithRSA");
signing.initSign(privateKey);
signing.update(idHashedMessage);
byte[] signature = signing.sign();

// Verify with SHA256withRSA
Signature verifying = Signature.getInstance("SHA256withRSA");                       // Apply algorithm that corresponds to the digest used, here SHA256withRSA
verifying.initVerify(publicKey);
verifying.update(message);
boolean verified = verifying.verify(signature);
System.out.println("Verification: " + verified);

添加摘要 ID 是必要的,因为根据 RFC 8017,使用 PKCS#1 v1.5 签名使用 RSASSA-PKCS1-v1_5 填充,其中包括摘要 ID。


关于你的第二个问题:

简单的公式“签名等于使用私钥加密”仅在不使用填充时有效(教科书 RSA),s。还有 here. In practice, however, padding must always be applied for security reasons. For encryption and signing different paddings are involved: For encryption with PKCS#1 v1.5 padding the variant RSAES-PKCS1-v1_5 is applied and for signing with PKCS#1 v1.5 padding the variant RSASSA-PKCS1-v1_5.
使用 private 密钥加密时,应用的填充变体可能因库而异(如果完全支持私钥加密),这通常会导致不兼容。可能是为了避免此类问题,Android 密钥库可能不支持私钥加密(至少我还没有找到使这成为可能的配置)。

没有keystore的JavaAPI和AndroidAPI都支持私钥加密。因此,最后发布的代码有效。此外,对于 PKCS#1 v1.5 填充,使用变体 RSASSA-PKCS1-v1_5。如果此处传递(例如使用 SHA256)散列消息并将摘要的 ID 放在它前面,则可以使用算法 SHA256withRSA 验证生成的签名(如上面发布的代码)。