I am a new dev dabbling in ECC. I am currently building an Android app in Java/Kotlin and can't get a piece of content that was signed with a private key to be verified using the corresponding public key. I have been stuck for a few days so I thought I'd come here!
Here's my code for keypair generation:
fun generateKeyPair() {
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"
)
val parameterSpec = KeyGenParameterSpec.Builder(
"keyAlias",
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).apply {
setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
setUserAuthenticationRequired(true)
setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
}.build()
keyPairGenerator.initialize(parameterSpec)
val keyPair = keyPairGenerator.generateKeyPair()
val publicKey = keyPair.public
savePublicKeyToFirestore(publicKey)
}
As you can see I used the secp256r1 curve for keypair generation.
Then to sign the content:
private fun authenticateUser(contentLink: String) {
val executor = ContextCompat.getMainExecutor(requireContext())
val biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
firestore.collection("identity_immutability").document(userId).get().addOnSuccessListener { document ->
val txId = document.getString("txId") ?: "txId not found"
val publicKey = document.getString("publicKey") ?: "publicKey not found"
result.cryptoObject?.signature?.let { signature ->
try {
signature.update(contentLink.toByteArray())
val signedData = signature.sign()
val encodedSignature = Base64.encodeToString(signedData, Base64.DEFAULT)
val timestamp = SimpleDateFormat("dd/MM/yyyy HH:mm:ss", Locale.getDefault()).format(Date())
val qrData = QRData(
contentLink =contentLink,
signature =encodedSignature,
timestamp =timestamp,
publicKey =publicKey,
txId =txId
)
val gson = Gson()
val qrDataJson = gson.toJson(qrData)
updateUIWithQRCode(qrDataJson, timestamp)
} catch (e: Exception) {
}
} ?: run {
}
}.addOnFailureListener { e ->
}
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric Authentication for Signing")
.setSubtitle("Confirm your identity to proceed with signing.")
.setNegativeButtonText("Use account password")
.build()
try {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val privateKey = keyStore.getKey("keyAlias", null) as? PrivateKey
val signature = Signature.getInstance("SHA256withECDSA").apply {
initSign(privateKey)
}
val cryptoObject = BiometricPrompt.CryptoObject(signature)
biometricPrompt.authenticate(promptInfo, cryptoObject)
} catch (e: Exception) {
}
}
I convert this content into a QR-code for others to verify. On the other end, when scanning the QR-code, my code attempts to verify the signed content with the public key that was generated with the private key which signed the content. Yet the signature verification always comes out as false.
private fun verifyQRData(qrData: QRData) {
CoroutineScope(Dispatchers.IO).launch {
try {
val transactionDetails = BlockCypherAPI.apiService.getTransactionDetails(qrData.txId)
val blockchainPublicKeyBase64 = transactionDetails.outputs
.firstOrNull { it.script_type == "null-data" }?.data_string
// Convert Base64 to Hex
val blockchainPublicKeyHex = blockchainPublicKeyBase64?.let {
val decodedBytes = Base64.decode(it, Base64.DEFAULT)
decodedBytes.joinToString(separator = "") { byte -> "%02x".format(byte) }
}
if (blockchainPublicKeyHex == qrData.publicKey) {
val contentVerified = verifySignature(qrData.signature, qrData.contentLink, qrData.publicKey)
val publicKeyMatch = if (contentVerified) "Match" else "Mismatch"
val contentVerificationResult = if (contentVerified) "Content Verified" else "Content Not Verified"
updateUI(publicKeyMatch, contentVerificationResult, qrData.timestamp)
} else {
updateUI("No Match", "Content Not Verified", qrData.timestamp)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
}
}
}
}
fun verifySignature(signatureEncoded: String, content: String, publicKeyHex: String): Boolean {
try {
// Convert the hex encoded public key to a byte array
val publicKeyBytes = hexStringToByteArray(publicKeyHex)
// Construct a PublicKey object from the byte array
val keySpec = X509EncodedKeySpec(publicKeyBytes)
val keyFactory = KeyFactory.getInstance("EC") // Ensure the algorithm matches the key type
val publicKey = keyFactory.generatePublic(keySpec)
// Decode the Base64 encoded signature to a byte array
val signatureBytes = Base64.decode(signatureEncoded, Base64.DEFAULT)
// Initialize a Signature object and verify the signature
val signatureInstance = Signature.getInstance("SHA256withECDSA")
signatureInstance.initVerify(publicKey)
signatureInstance.update(content.toByteArray())
// Verifying the signature
val result = signatureInstance.verify(signatureBytes)
return result
} catch (e: Exception) {
return false
}
}
fun hexStringToByteArray(s: String): ByteArray {
val len = s.length
val data = ByteArray(len / 2)
for (i in 0 until len step 2) {
data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte()
Log.d("VerifySignature", "Byte ${i / 2}: ${data[i / 2]}")
}
return data
}
Here are the logs below which confirm that the public key is the same as the one that was generated with the private key, and the results from the keys being decoded and signed.
Transaction details fetched successfully.
Blockchain public key (Hex): 3059301306072a8648ce3d020106082a8648ce3d030107034200044545198405f7ef6d05ea53c050cad0d67b8fe3f09c978f781a6623608fee9d05a6e1087d68d83b448a54ac33c72c7febac32a8f9843ad16c35a69783bdbc13df
QR public key (Hex): 3059301306072a8648ce3d020106082a8648ce3d030107034200044545198405f7ef6d05ea53c050cad0d67b8fe3f09c978f781a6623608fee9d05a6e1087d68d83b448a54ac33c72c7febac32a8f9843ad16c35a69783bdbc13df
Verifying content with signature...
Starting signature verification process.
Signature (Base64): MEUCIQC6W6a4xeUR1irrbEKHCRqYrxiqYOJylKTaj9epFg9pkwIgYCfUDfoc3LCnyHu52Yzs22UE uB2K+sOTOgBNiQ2k2QM=
Content: www.digikey.ca
Public Key (Hex): 3059301306072a8648ce3d020106082a8648ce3d030107034200044545198405f7ef6d05ea53c050cad0d67b8fe3f09c978f781a6623608fee9d05a6e1087d68d83b448a54ac33c72c7febac32a8f9843ad16c35a69783bdbc13df
Converting hex to byte array.
Converting Hex String to Byte Array. Hex String: 3059301306072a8648ce3d020106082a8648ce3d030107034200044545198405f7ef6d05ea53c050cad0d67b8fe3f09c978f781a6623608fee9d05a6e1087d68d83b448a54ac33c72c7febac32a8f9843ad16c35a69783bdbc13df
Byte Array:
Byte 0: 48
Byte 1: 89
Byte 2: 48
Byte 3: 19
Byte 4: 6
Byte 5: 7
Byte 6: 42
Byte 7: -122
Byte 8: 72
Byte 9: -50
Byte 10: 61
Byte 11: 2
Byte 12: 1
Byte 13: 6
Byte 14: 8
Byte 15: 42
Byte 16: -122
Byte 17: 72
Byte 18: -50
Byte 19: 61
Byte 20: 3
Byte 21: 1
Byte 22: 7
Byte 23: 3
Byte 24: 66
Byte 25: 0
Byte 26: 4
Byte 27: 69
Byte 28: 69
Byte 29: 25
Byte 30: -124
Byte 31: 5
Byte 32: -9
Byte 33: -17
Byte 34: 109
Byte 35: 5
Byte 36: -22
Byte 37: 83
Byte 38: -64
Byte 39: 80
Byte 40: -54
Byte 41: -48
Byte 42: -42
Byte 43: 123
Byte 44: -113
Byte 45: -29
Byte 46: -16
Byte 47: -100
Byte 48: -105
Byte 49: -113
Byte 50: 120
Byte 51: 26
Byte 52: 102
Byte 53: 35
Byte 54: 96
Byte 55: -113
Byte 56: -18
Byte 57: -99
Byte 58: 5
Byte 59: -90
Byte 60: -31
Byte 61: 8
Byte 62: 125
Byte 63: 104
Byte 64: -40
Byte 65: 59
Byte 66: 68
Byte 67: -118
Byte 68: 84
Byte 69: -84
Byte 70: 51
Byte 71: -57
Byte 72: 44
Byte 73: 127
Byte 74: -21
Byte 75: -84
Byte 76: 50
Byte 77: -88
Byte 78: -7
Byte 79: -124
Byte 80: 58
Byte 81: -47
Byte 82: 108
Byte 83: 53
Byte 84: -90
Byte 85: -105
Byte 86: -125
Byte 87: -67
Byte 88: -68
Byte 89: 19
Byte 90: -33
Bytes count: 91
Constructing PublicKey object.
PublicKey object created: Public-Key: (P-256)
Here's a printout of the pubic key:
04:45:45:19:84:05:f7:ef:6d:05:ea:53:c0:50:ca:
d0:d6:7b:8f:e3:f0:9c:97:8f:78:1a:66:23:60:8f:
ee:9d:05:a6:e1:08:7d:68:d8:3b:44:8a:54:ac:33:
c7:2c:7f:eb:ac:32:a8:f9:84:3a:d1:6c:35:a6:97:
83:bd:bc:13:df
And the output of the application:
Decoding signature from Base64.
Signature bytes decoded from Base64. Bytes count: 71
Initializing Signature object with algorithm SHA256withECDSA.
Signature instance initialized: Signature object: SHA256withECDSA<initialized for verifying>
Signature instance updated with content.
Verifying the signature. Verification result: false
Content verification result: false
Feeling pretty stuck.. any nudge in the right direction would be greatly appreciated !!