Android Java Spongycastle ECDSA 签名到 subtle.crypto Javascript
Android Java Spongycastle ECDSA Signature to subtle.crypto Javascript
我正在从我的网站导入一组值,这些值是用 Java 脚本编写的,使用 subtle.crypto 对消息进行签名。在 QR 码中,我从 Java 脚本中输入了密钥的 X、Y 和 D 值,这是我复制密钥的代码:
public static KeyPair GenerateExistingKeyPair(String d, String x, String y) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
Log.d(TAG, "GenerateExistingKeyPair: PrivateKey D: " + d);
Log.d(TAG, "GenerateExistingKeyPair: PublicKey X: " + x);
Log.d(TAG, "GenerateExistingKeyPair: PublicKey Y: " + y);
BigInteger privateD = decode(d);
BigInteger publicX = decode(x);
BigInteger publicY = decode(y);
KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);;
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("P-256");
ECPoint Q = ecSpec.getG().multiply(privateD);
ECPrivateKeySpec privSpec = new ECPrivateKeySpec(privateD, ecSpec);
ECPublicKeySpec pubSpec = new ECPublicKeySpec(Q, ecSpec);
PrivateKey privKey = keyFactory.generatePrivate(privSpec);
PublicKey pubKey = keyFactory.generatePublic(pubSpec);
KeyPair keyPair = new KeyPair(pubKey, privKey);
Log.d(TAG, "GenerateExistingKeyPair: KeyPair: " + keyPair.getPrivate().toString());
Log.d(TAG, "GenerateExistingKeyPair: " + Hex.toHexString(privKey.getEncoded()));
return keyPair;
}
我使用的 "decode" 是因为这些值存储在 Javascript 中的 Base64 中。
public static BigInteger decode(String value) {
byte[] decoded = android.util.Base64.decode(value, android.util.Base64.URL_SAFE);
BigInteger bigInteger = new BigInteger(Hex.toHexString(decoded), 16);
return bigInteger;
}
现在这是输出结果。
D/ECDSA:: GenerateExistingKeyPair: PrivateKey D: m-lI_bV8YoNgAgNGpccXPdNtRJ4I6k0hdMdKD7NDYlI
GenerateExistingKeyPair: PublicKey X: BadCycqeFycXoL4ONkATL7vu1ZxlF66JmrSgbE2A4eY
GenerateExistingKeyPair: PublicKey Y: obTA6W6xluIdXcqRjnvq0Nh-_IfiWKV4FWziJFxXHUo
D/ECDSA:: GenerateExistingKeyPair: KeyPair: EC Private Key [ed:66:72:8b:8c:1d:97:b9:82:0b:11:c8:1f:6e:db:aa:0e:bd:67:43]
X: 5a742c9ca9e172717a0be0e3640132fbbeed59c6517ae899ab4a06c4d80e1e6
Y: a1b4c0e96eb196e21d5dca918e7bead0d87efc87e258a578156ce2245c571d4a
据我所知,X 和 Y 是正确的,使用 Base64 将它们转换回来给我的值与我收到的值完全相同。现在我开始对消息进行哈希处理并使用 WebRTC 通过 JSON 发送交易。
public static byte[] signTransaction(Wallet wallet, byte[] msgHash) throws Exception {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
ecdsaSign.initSign(wallet.getKeyPair().getPrivate());
ecdsaSign.update(msgHash);
byte[] signature = ecdsaSign.sign();
Log.d(TAG, "signTransaction: " + new BigInteger(1, signature).toString(16));
return signature;
}
这是我收到的签名:
3045022026728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155022100ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f
经过一些研究,我发现 Java 中的 ECDSA 签名是 ANS1 DER 编码的,而 javascript 中的签名使用 P1363 格式,这只是签名的 R 和 S。
所以经过一番研究,我发现了如何从签名中提取这些值。
public static BigInteger extractR(byte[] signature) throws Exception {
int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
int lengthR = signature[startR + 1];
return new BigInteger(Arrays.copyOfRange(signature, startR + 2, startR + 2 + lengthR));
}
public static BigInteger extractS(byte[] signature) throws Exception {
int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
int lengthR = signature[startR + 1];
int startS = startR + 2 + lengthR;
int lengthS = signature[startS + 1];
return new BigInteger(Arrays.copyOfRange(signature, startS + 2, startS + 2 + lengthS));
}
这给了我以下值:
26728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155
ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f
在最后一次尝试中,我尝试将这两个字符串放在一起并将它们发送到 Java 脚本端,但它无法验证,这两个并排的值与签名的字符大小相同在 Java 脚本中生成,但方法
await window.crypto.subtle.verify({name: "ECDSA", hash: {name: "SHA-256"},}, publicKey, signature, data)
在javascript还是returns假。
我的问题是,如何使签名在 Java 和 Java 脚本之间兼容?我可以在 Java 脚本中将它从 ASN1 DER 转换为 P1363 吗?或者我可以在 Java 中进行相反的转换吗?
如有任何帮助,我们将不胜感激...
您正在执行两次散列,因为您执行:
ecdsaSign.update(msgHash);
而 ecdsaSign
对象已经执行了 SHA-256 哈希。
回答我自己的问题只是为了关闭它,我找到了我没有被JS端认证的原因。我创建和复制 Public/Private 键的所有方法都有效。我将 ASN1 DER 签名转换为 R||S 值的代码也能正常工作。
我在哈希字符串上签署交易,就像我让浏览器做的那样。结果是浏览器没有对原始哈希字符串进行签名,但他在签名前用哈希字符串做了这个:
async sign(msg) {
const encoder = new TextEncoder('utf-8');
const msgBuffer = encoder.encode(msg.toString());
const signedBuffer = await ECDSA.sign(this.keys.privateKey, msgBuffer);
const signedArray = Array.from(new Uint8Array(signedBuffer));
return Encryption.byteToHexString(signedArray);
}
注意以下几行:
事实证明,浏览器正在将哈希字符串编码为 UTF-8 并签署大小为 64 的字节数组,而不是包含 20 个左右字节的字符串。所以在浏览器尝试验证我的签名之前,它实际上对我的哈希字符串做了同样的事情,转换为 UTF-8,这就是我的签名失败的原因,因为我没有签署与浏览器试图验证的相同的消息.
如果我更仔细地潜入 JS 湾,它可以节省我 2 天的时间。
感谢 Maarten Bodewes 试图帮助我,你实际上指出了我的代码中的一些缺陷,并且很抱歉我缺少我向你展示的 JS 端代码,你可能会发现这个问题并帮助我 2几天前。
我正在从我的网站导入一组值,这些值是用 Java 脚本编写的,使用 subtle.crypto 对消息进行签名。在 QR 码中,我从 Java 脚本中输入了密钥的 X、Y 和 D 值,这是我复制密钥的代码:
public static KeyPair GenerateExistingKeyPair(String d, String x, String y) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
Log.d(TAG, "GenerateExistingKeyPair: PrivateKey D: " + d);
Log.d(TAG, "GenerateExistingKeyPair: PublicKey X: " + x);
Log.d(TAG, "GenerateExistingKeyPair: PublicKey Y: " + y);
BigInteger privateD = decode(d);
BigInteger publicX = decode(x);
BigInteger publicY = decode(y);
KeyFactory keyFactory = KeyFactory.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);;
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("P-256");
ECPoint Q = ecSpec.getG().multiply(privateD);
ECPrivateKeySpec privSpec = new ECPrivateKeySpec(privateD, ecSpec);
ECPublicKeySpec pubSpec = new ECPublicKeySpec(Q, ecSpec);
PrivateKey privKey = keyFactory.generatePrivate(privSpec);
PublicKey pubKey = keyFactory.generatePublic(pubSpec);
KeyPair keyPair = new KeyPair(pubKey, privKey);
Log.d(TAG, "GenerateExistingKeyPair: KeyPair: " + keyPair.getPrivate().toString());
Log.d(TAG, "GenerateExistingKeyPair: " + Hex.toHexString(privKey.getEncoded()));
return keyPair;
}
我使用的 "decode" 是因为这些值存储在 Javascript 中的 Base64 中。
public static BigInteger decode(String value) {
byte[] decoded = android.util.Base64.decode(value, android.util.Base64.URL_SAFE);
BigInteger bigInteger = new BigInteger(Hex.toHexString(decoded), 16);
return bigInteger;
}
现在这是输出结果。
D/ECDSA:: GenerateExistingKeyPair: PrivateKey D: m-lI_bV8YoNgAgNGpccXPdNtRJ4I6k0hdMdKD7NDYlI
GenerateExistingKeyPair: PublicKey X: BadCycqeFycXoL4ONkATL7vu1ZxlF66JmrSgbE2A4eY
GenerateExistingKeyPair: PublicKey Y: obTA6W6xluIdXcqRjnvq0Nh-_IfiWKV4FWziJFxXHUo
D/ECDSA:: GenerateExistingKeyPair: KeyPair: EC Private Key [ed:66:72:8b:8c:1d:97:b9:82:0b:11:c8:1f:6e:db:aa:0e:bd:67:43]
X: 5a742c9ca9e172717a0be0e3640132fbbeed59c6517ae899ab4a06c4d80e1e6
Y: a1b4c0e96eb196e21d5dca918e7bead0d87efc87e258a578156ce2245c571d4a
据我所知,X 和 Y 是正确的,使用 Base64 将它们转换回来给我的值与我收到的值完全相同。现在我开始对消息进行哈希处理并使用 WebRTC 通过 JSON 发送交易。
public static byte[] signTransaction(Wallet wallet, byte[] msgHash) throws Exception {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
Signature ecdsaSign = Signature.getInstance("SHA256withECDSA", BouncyCastleProvider.PROVIDER_NAME);
ecdsaSign.initSign(wallet.getKeyPair().getPrivate());
ecdsaSign.update(msgHash);
byte[] signature = ecdsaSign.sign();
Log.d(TAG, "signTransaction: " + new BigInteger(1, signature).toString(16));
return signature;
}
这是我收到的签名:
3045022026728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155022100ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f
经过一些研究,我发现 Java 中的 ECDSA 签名是 ANS1 DER 编码的,而 javascript 中的签名使用 P1363 格式,这只是签名的 R 和 S。
所以经过一番研究,我发现了如何从签名中提取这些值。
public static BigInteger extractR(byte[] signature) throws Exception {
int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
int lengthR = signature[startR + 1];
return new BigInteger(Arrays.copyOfRange(signature, startR + 2, startR + 2 + lengthR));
}
public static BigInteger extractS(byte[] signature) throws Exception {
int startR = (signature[1] & 0x80) != 0 ? 3 : 2;
int lengthR = signature[startR + 1];
int startS = startR + 2 + lengthR;
int lengthS = signature[startS + 1];
return new BigInteger(Arrays.copyOfRange(signature, startS + 2, startS + 2 + lengthS));
}
这给了我以下值:
26728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155
ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f
在最后一次尝试中,我尝试将这两个字符串放在一起并将它们发送到 Java 脚本端,但它无法验证,这两个并排的值与签名的字符大小相同在 Java 脚本中生成,但方法
await window.crypto.subtle.verify({name: "ECDSA", hash: {name: "SHA-256"},}, publicKey, signature, data)
在javascript还是returns假。
我的问题是,如何使签名在 Java 和 Java 脚本之间兼容?我可以在 Java 脚本中将它从 ASN1 DER 转换为 P1363 吗?或者我可以在 Java 中进行相反的转换吗?
如有任何帮助,我们将不胜感激...
您正在执行两次散列,因为您执行:
ecdsaSign.update(msgHash);
而 ecdsaSign
对象已经执行了 SHA-256 哈希。
回答我自己的问题只是为了关闭它,我找到了我没有被JS端认证的原因。我创建和复制 Public/Private 键的所有方法都有效。我将 ASN1 DER 签名转换为 R||S 值的代码也能正常工作。
我在哈希字符串上签署交易,就像我让浏览器做的那样。结果是浏览器没有对原始哈希字符串进行签名,但他在签名前用哈希字符串做了这个:
async sign(msg) {
const encoder = new TextEncoder('utf-8');
const msgBuffer = encoder.encode(msg.toString());
const signedBuffer = await ECDSA.sign(this.keys.privateKey, msgBuffer);
const signedArray = Array.from(new Uint8Array(signedBuffer));
return Encryption.byteToHexString(signedArray);
}
注意以下几行:
事实证明,浏览器正在将哈希字符串编码为 UTF-8 并签署大小为 64 的字节数组,而不是包含 20 个左右字节的字符串。所以在浏览器尝试验证我的签名之前,它实际上对我的哈希字符串做了同样的事情,转换为 UTF-8,这就是我的签名失败的原因,因为我没有签署与浏览器试图验证的相同的消息.
如果我更仔细地潜入 JS 湾,它可以节省我 2 天的时间。
感谢 Maarten Bodewes 试图帮助我,你实际上指出了我的代码中的一些缺陷,并且很抱歉我缺少我向你展示的 JS 端代码,你可能会发现这个问题并帮助我 2几天前。