I'm importing a set of values from my website which is writen in Javascript using subtle.crypto for signing messages. In the QR Code I put the X, Y and D values of the key from Javascript, this is my code to replicate the key:
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;
}
The "decode" I use because those values are stored in Base64 within Javascript.
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;
}
Now here is the output from that.
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
As far as I can tell, the X and Y are correct, converting them back using Base64 gives me the exact same value as those I received. Now I get to the part to Hash a message and send the transaction thruough JSON using WebRTC.
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;
}
This is the signature I receive:
3045022026728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155022100ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f
After a bit of research I found out that ECDSA signatures in Java are ANS1 DER encoded and signatures in javascript uses P1363 format which are just the R and S of the signature.
So after some research I found out how to extract those values from the signature.
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));
}
Which gave me the following values:
26728f6d621689955126e52ca04e5ad7d3f5633111c32ca79979022fc48f7155
ed94989a8f9fb6bb804ee041cb2923b6ecc17876fbc55c559c93ab9becac415f
In a last attempt I tried to put those two strings togheter and send them to the Javascript side, but it couldnt validate, these two values side by side are the same size in characters as the signatures generated in Javascript, but the method
await window.crypto.subtle.verify({name: "ECDSA", hash: {name: "SHA-256"},}, publicKey, signature, data)
In javascript still returns false.
My question is, how can I make the signatures compatible between Java and Javascript? Can I convert it from ASN1 DER to P1363 within Javascript? Or can I convert the other way around in Java?
Any help would be appreciated...
Answering my own question just to close it, I found the reason why I was not being authenticated by the JS side. All my methods to create and replicate the Public/Private keys are working. My code for transforming ASN1 DER signature to R||S value are working aswell.
I was signing transactions on top of the Hash String, like I tought the browser did. It turns out the browser were not signing the raw hash string, but he did this with the hash string before signing:
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);
}
note the lines:
As it turns out, the browser was encoding the hash string to UTF-8 and signing that byte array of size 64 and not the string which would have 20 or so bytes. So before when the browser tried to verify my signature, it actually did the same thing with my hash string, converted to UTF-8 and thats why my signature was failing, because I was not signing the same message as the browser was trying to verify.
If I did this dive into the JS cove more carefully it could have saved me like 2 days.
Thanks Maarten Bodewes for trying to help me, you actually pointed out a few flaws in my code and sorry for my lack of JS-side code that I presented to you, you could probably spot this issue and helped me 2 days ago.