Compare commits

...

5 Commits

Author SHA1 Message Date
sstone
0364ec762e
Use local secp256k1 callbacks in kotlin native code 2023-12-12 12:08:13 +01:00
sstone
aadffabe42
Set version to 0.12.0-SNAPSHOT 2023-12-11 19:08:54 +01:00
sstone
8e17e7030a
Implement custom secp256k1 error callbacks
These callbacks are only triggered either by arguments do not match explicit rquirements of the sepc256k1 library, or by hardware failures, memory corruption
or bug in secp256k1, and not by misuse of the library.
In theory we do not need to implement them, except to find bugs in our own code, but the default callbacks print a message to stderr and call abort() which
is not nice especially on mobile apps.

=> Here we introduce 2 specific exceptions, Secp256k1ErrorCallbackException and Secp256k1IllegalCallbackException, which are thrown when the error callback or illegal callback are called.
2023-12-11 19:05:46 +01:00
sstone
929e2cda40
Check that the recovery id is valid
It must be 0,1,2 or 3, this is an explicit requirement of the secp256k1 library.
2023-12-11 11:54:07 +01:00
sstone
41eac9273f
Reformat JNI c code (no functional changes) 2023-12-11 10:46:43 +01:00
5 changed files with 855 additions and 605 deletions

View File

@ -22,7 +22,7 @@ buildscript {
allprojects { allprojects {
group = "fr.acinq.secp256k1" group = "fr.acinq.secp256k1"
version = "0.11.0" version = "0.12.0-SNAPSHOT"
repositories { repositories {
google() google()

File diff suppressed because it is too large Load Diff

View File

@ -166,7 +166,17 @@ public interface Secp256k1 {
internal expect fun getSecpk256k1(): Secp256k1 internal expect fun getSecpk256k1(): Secp256k1
public class Secp256k1Exception : RuntimeException { public open class Secp256k1Exception : RuntimeException {
public constructor() : super()
public constructor(message: String?) : super(message)
}
public class Secp256k1ErrorCallbackException : Secp256k1Exception {
public constructor() : super()
public constructor(message: String?) : super(message)
}
public class Secp256k1IllegalCallbackException : Secp256k1Exception {
public constructor() : super() public constructor() : super()
public constructor(message: String?) : super(message) public constructor(message: String?) : super(message)
} }

View File

@ -4,15 +4,67 @@ import kotlinx.cinterop.*
import platform.posix.size_tVar import platform.posix.size_tVar
import secp256k1.* import secp256k1.*
@OptIn(ExperimentalUnsignedTypes::class) private typealias Secp256k1CallbackHandler = (String) -> Unit
@OptIn(ExperimentalStdlibApi::class)
private class CallbackHandler(ctx: CPointer<secp256k1_context>) : AutoCloseable {
var illegalCallBackMessage: String? = null
val illegalHandler: Secp256k1CallbackHandler = { x: String -> illegalCallBackMessage = x }
val illegalCallbackRef = StableRef.create(illegalHandler)
var errorCallBackMessage: String? = null
val errorHandler: Secp256k1CallbackHandler = { x: String -> errorCallBackMessage = x }
val errorCallbackRef = StableRef.create(errorHandler)
init {
secp256k1_context_set_error_callback(
ctx, staticCFunction { buffer: CPointer<ByteVar>?, data: COpaquePointer? ->
if (data != null) {
val callback = data.asStableRef<Secp256k1CallbackHandler>().get()
callback(buffer?.toKString() ?: "error callback triggered")
}
},
errorCallbackRef.asCPointer()
)
secp256k1_context_set_illegal_callback(
ctx, staticCFunction { buffer: CPointer<ByteVar>?, data: COpaquePointer? ->
if (data != null) {
val callback = data.asStableRef<Secp256k1CallbackHandler>().get()
callback(buffer?.toKString() ?: "illegal callback triggered")
}
},
illegalCallbackRef.asCPointer()
)
}
fun checkForErrors() {
errorCallBackMessage?.let { throw Secp256k1ErrorCallbackException(it) }
illegalCallBackMessage?.let { throw Secp256k1IllegalCallbackException(it) }
}
override fun close() {
// StableRef instances have to be disposed of manually
illegalCallbackRef.dispose()
errorCallbackRef.dispose()
}
}
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalStdlibApi::class)
public object Secp256k1Native : Secp256k1 { public object Secp256k1Native : Secp256k1 {
private val ctx: CPointer<secp256k1_context> by lazy { private val ctx: CPointer<secp256k1_context> by lazy {
secp256k1_context_create((SECP256K1_FLAGS_TYPE_CONTEXT or SECP256K1_FLAGS_BIT_CONTEXT_SIGN or SECP256K1_FLAGS_BIT_CONTEXT_VERIFY).toUInt()) secp256k1_context_create((SECP256K1_FLAGS_TYPE_CONTEXT or SECP256K1_FLAGS_BIT_CONTEXT_SIGN or SECP256K1_FLAGS_BIT_CONTEXT_VERIFY).toUInt())
?: error("Could not create secp256k1 context") ?: error("Could not create secp256k1 context")
} }
private fun Int.requireSuccess(message: String): Int = if (this != 1) throw Secp256k1Exception(message) else this private fun Int.requireSuccess(message: String): Int {
return if (this != 1) throw Secp256k1Exception(message) else this
}
private fun Int.requireSuccess(callbackHandler: CallbackHandler, message: String): Int {
callbackHandler.checkForErrors()
return if (this != 1) throw Secp256k1Exception(message) else this
}
private fun MemScope.allocSignature(input: ByteArray): secp256k1_ecdsa_signature { private fun MemScope.allocSignature(input: ByteArray): secp256k1_ecdsa_signature {
val sig = alloc<secp256k1_ecdsa_signature>() val sig = alloc<secp256k1_ecdsa_signature>()
@ -58,167 +110,209 @@ public object Secp256k1Native : Secp256k1 {
public override fun verify(signature: ByteArray, message: ByteArray, pubkey: ByteArray): Boolean { public override fun verify(signature: ByteArray, message: ByteArray, pubkey: ByteArray): Boolean {
require(message.size == 32) require(message.size == 32)
require(pubkey.size == 33 || pubkey.size == 65) require(pubkey.size == 33 || pubkey.size == 65)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPubkey = allocPublicKey(pubkey) memScoped {
val nMessage = toNat(message) val nPubkey = allocPublicKey(pubkey)
val nSig = allocSignature(signature) val nMessage = toNat(message)
return secp256k1_ecdsa_verify(ctx, nSig.ptr, nMessage, nPubkey.ptr) == 1 val nSig = allocSignature(signature)
val verify = secp256k1_ecdsa_verify(ctx, nSig.ptr, nMessage, nPubkey.ptr)
callbackHandler.checkForErrors()
return verify == 1
}
} }
} }
public override fun sign(message: ByteArray, privkey: ByteArray): ByteArray { public override fun sign(message: ByteArray, privkey: ByteArray): ByteArray {
require(privkey.size == 32) require(privkey.size == 32)
require(message.size == 32) require(message.size == 32)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPrivkey = toNat(privkey) memScoped {
val nMessage = toNat(message) val nPrivkey = toNat(privkey)
val nSig = alloc<secp256k1_ecdsa_signature>() val nMessage = toNat(message)
secp256k1_ecdsa_sign(ctx, nSig.ptr, nMessage, nPrivkey, null, null).requireSuccess("secp256k1_ecdsa_sign() failed") val nSig = alloc<secp256k1_ecdsa_signature>()
return serializeSignature(nSig) secp256k1_ecdsa_sign(ctx, nSig.ptr, nMessage, nPrivkey, null, null).requireSuccess(callbackHandler, "secp256k1_ecdsa_sign() failed")
return serializeSignature(nSig)
}
} }
} }
public override fun signatureNormalize(sig: ByteArray): Pair<ByteArray, Boolean> { public override fun signatureNormalize(sig: ByteArray): Pair<ByteArray, Boolean> {
require(sig.size >= 64){ "invalid signature ${Hex.encode(sig)}" } require(sig.size >= 64) { "invalid signature ${Hex.encode(sig)}" }
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nSig = allocSignature(sig) memScoped {
val isHighS = secp256k1_ecdsa_signature_normalize(ctx, nSig.ptr, nSig.ptr) val nSig = allocSignature(sig)
return Pair(serializeSignature(nSig), isHighS == 1) val isHighS = secp256k1_ecdsa_signature_normalize(ctx, nSig.ptr, nSig.ptr)
callbackHandler.checkForErrors()
return Pair(serializeSignature(nSig), isHighS == 1)
}
} }
} }
public override fun secKeyVerify(privkey: ByteArray): Boolean { public override fun secKeyVerify(privkey: ByteArray): Boolean {
if (privkey.size != 32) return false if (privkey.size != 32) return false
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPrivkey = toNat(privkey) memScoped {
return secp256k1_ec_seckey_verify(ctx, nPrivkey) == 1 val nPrivkey = toNat(privkey)
val result = secp256k1_ec_seckey_verify(ctx, nPrivkey) == 1
callbackHandler.checkForErrors()
return result
}
} }
} }
public override fun pubkeyCreate(privkey: ByteArray): ByteArray { public override fun pubkeyCreate(privkey: ByteArray): ByteArray {
require(privkey.size == 32) require(privkey.size == 32)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPrivkey = toNat(privkey) memScoped {
val nPubkey = alloc<secp256k1_pubkey>() val nPrivkey = toNat(privkey)
secp256k1_ec_pubkey_create(ctx, nPubkey.ptr, nPrivkey).requireSuccess("secp256k1_ec_pubkey_create() failed") val nPubkey = alloc<secp256k1_pubkey>()
return serializePubkey(nPubkey) secp256k1_ec_pubkey_create(ctx, nPubkey.ptr, nPrivkey).requireSuccess(callbackHandler, "secp256k1_ec_pubkey_create() failed")
return serializePubkey(nPubkey)
}
} }
} }
public override fun pubkeyParse(pubkey: ByteArray): ByteArray { public override fun pubkeyParse(pubkey: ByteArray): ByteArray {
require(pubkey.size == 33 || pubkey.size == 65) require(pubkey.size == 33 || pubkey.size == 65)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPubkey = allocPublicKey(pubkey) memScoped {
return serializePubkey(nPubkey) val nPubkey = allocPublicKey(pubkey)
val result = serializePubkey(nPubkey)
callbackHandler.checkForErrors()
return result
}
} }
} }
public override fun privKeyNegate(privkey: ByteArray): ByteArray { public override fun privKeyNegate(privkey: ByteArray): ByteArray {
require(privkey.size == 32) require(privkey.size == 32)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val negated = privkey.copyOf() memScoped {
val negPriv = toNat(negated) val negated = privkey.copyOf()
secp256k1_ec_seckey_negate(ctx, negPriv).requireSuccess("secp256k1_ec_seckey_negate() failed") val negPriv = toNat(negated)
return negated secp256k1_ec_seckey_negate(ctx, negPriv).requireSuccess(callbackHandler, "secp256k1_ec_seckey_negate() failed")
return negated
}
} }
} }
public override fun privKeyTweakAdd(privkey: ByteArray, tweak: ByteArray): ByteArray { public override fun privKeyTweakAdd(privkey: ByteArray, tweak: ByteArray): ByteArray {
require(privkey.size == 32) require(privkey.size == 32)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val added = privkey.copyOf() memScoped {
val natAdd = toNat(added) val added = privkey.copyOf()
val natTweak = toNat(tweak) val natAdd = toNat(added)
secp256k1_ec_seckey_tweak_add(ctx, natAdd, natTweak).requireSuccess("secp256k1_ec_seckey_tweak_add() failed") val natTweak = toNat(tweak)
return added secp256k1_ec_seckey_tweak_add(ctx, natAdd, natTweak).requireSuccess(callbackHandler, "secp256k1_ec_seckey_tweak_add() failed")
return added
}
} }
} }
public override fun privKeyTweakMul(privkey: ByteArray, tweak: ByteArray): ByteArray { public override fun privKeyTweakMul(privkey: ByteArray, tweak: ByteArray): ByteArray {
require(privkey.size == 32) require(privkey.size == 32)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val multiplied = privkey.copyOf() memScoped {
val natMul = toNat(multiplied) val multiplied = privkey.copyOf()
val natTweak = toNat(tweak) val natMul = toNat(multiplied)
secp256k1_ec_privkey_tweak_mul(ctx, natMul, natTweak).requireSuccess("secp256k1_ec_privkey_tweak_mul() failed") val natTweak = toNat(tweak)
return multiplied secp256k1_ec_privkey_tweak_mul(ctx, natMul, natTweak).requireSuccess(callbackHandler, "secp256k1_ec_privkey_tweak_mul() failed")
return multiplied
}
} }
} }
public override fun pubKeyNegate(pubkey: ByteArray): ByteArray { public override fun pubKeyNegate(pubkey: ByteArray): ByteArray {
require(pubkey.size == 33 || pubkey.size == 65) require(pubkey.size == 33 || pubkey.size == 65)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPubkey = allocPublicKey(pubkey) memScoped {
secp256k1_ec_pubkey_negate(ctx, nPubkey.ptr).requireSuccess("secp256k1_ec_pubkey_negate() failed") val nPubkey = allocPublicKey(pubkey)
return serializePubkey(nPubkey) secp256k1_ec_pubkey_negate(ctx, nPubkey.ptr).requireSuccess(callbackHandler, "secp256k1_ec_pubkey_negate() failed")
return serializePubkey(nPubkey)
}
} }
} }
public override fun pubKeyTweakAdd(pubkey: ByteArray, tweak: ByteArray): ByteArray { public override fun pubKeyTweakAdd(pubkey: ByteArray, tweak: ByteArray): ByteArray {
require(pubkey.size == 33 || pubkey.size == 65) require(pubkey.size == 33 || pubkey.size == 65)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPubkey = allocPublicKey(pubkey) memScoped {
val nTweak = toNat(tweak) val nPubkey = allocPublicKey(pubkey)
secp256k1_ec_pubkey_tweak_add(ctx, nPubkey.ptr, nTweak).requireSuccess("secp256k1_ec_pubkey_tweak_add() failed") val nTweak = toNat(tweak)
return serializePubkey(nPubkey) secp256k1_ec_pubkey_tweak_add(ctx, nPubkey.ptr, nTweak).requireSuccess(callbackHandler, "secp256k1_ec_pubkey_tweak_add() failed")
return serializePubkey(nPubkey)
}
} }
} }
public override fun pubKeyTweakMul(pubkey: ByteArray, tweak: ByteArray): ByteArray { public override fun pubKeyTweakMul(pubkey: ByteArray, tweak: ByteArray): ByteArray {
require(pubkey.size == 33 || pubkey.size == 65) require(pubkey.size == 33 || pubkey.size == 65)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPubkey = allocPublicKey(pubkey) memScoped {
val nTweak = toNat(tweak) val nPubkey = allocPublicKey(pubkey)
secp256k1_ec_pubkey_tweak_mul(ctx, nPubkey.ptr, nTweak).requireSuccess("secp256k1_ec_pubkey_tweak_mul() failed") val nTweak = toNat(tweak)
return serializePubkey(nPubkey) secp256k1_ec_pubkey_tweak_mul(ctx, nPubkey.ptr, nTweak).requireSuccess(callbackHandler, "secp256k1_ec_pubkey_tweak_mul() failed")
return serializePubkey(nPubkey)
}
} }
} }
public override fun pubKeyCombine(pubkeys: Array<ByteArray>): ByteArray { public override fun pubKeyCombine(pubkeys: Array<ByteArray>): ByteArray {
pubkeys.forEach { require(it.size == 33 || it.size == 65) } pubkeys.forEach { require(it.size == 33 || it.size == 65) }
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPubkeys = pubkeys.map { allocPublicKey(it).ptr } memScoped {
val combined = alloc<secp256k1_pubkey>() val nPubkeys = pubkeys.map { allocPublicKey(it).ptr }
secp256k1_ec_pubkey_combine(ctx, combined.ptr, nPubkeys.toCValues(), pubkeys.size.convert()).requireSuccess("secp256k1_ec_pubkey_combine() failed") val combined = alloc<secp256k1_pubkey>()
return serializePubkey(combined) secp256k1_ec_pubkey_combine(ctx, combined.ptr, nPubkeys.toCValues(), pubkeys.size.convert()).requireSuccess(callbackHandler, "secp256k1_ec_pubkey_combine() failed")
return serializePubkey(combined)
}
} }
} }
public override fun ecdh(privkey: ByteArray, pubkey: ByteArray): ByteArray { public override fun ecdh(privkey: ByteArray, pubkey: ByteArray): ByteArray {
require(privkey.size == 32) require(privkey.size == 32)
require(pubkey.size == 33 || pubkey.size == 65) require(pubkey.size == 33 || pubkey.size == 65)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPubkey = allocPublicKey(pubkey) memScoped {
val nPrivkey = toNat(privkey) val nPubkey = allocPublicKey(pubkey)
val output = allocArray<UByteVar>(32) val nPrivkey = toNat(privkey)
secp256k1_ecdh(ctx, output, nPubkey.ptr, nPrivkey, null, null).requireSuccess("secp256k1_ecdh() failed") val output = allocArray<UByteVar>(32)
return output.readBytes(32) secp256k1_ecdh(ctx, output, nPubkey.ptr, nPrivkey, null, null).requireSuccess(callbackHandler, "secp256k1_ecdh() failed")
return output.readBytes(32)
}
} }
} }
public override fun ecdsaRecover(sig: ByteArray, message: ByteArray, recid: Int): ByteArray { public override fun ecdsaRecover(sig: ByteArray, message: ByteArray, recid: Int): ByteArray {
require(sig.size == 64) require(sig.size == 64)
require(message.size == 32) require(message.size == 32)
memScoped { // we do not check that recid is valid, which should trigger our illegal callback handler to throw a Secp256k1IllegalCallbackException
val nSig = toNat(sig) // require(recid in 0..3)
val rSig = alloc<secp256k1_ecdsa_recoverable_signature>()
secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, rSig.ptr, nSig, recid).requireSuccess("secp256k1_ecdsa_recoverable_signature_parse_compact() failed") CallbackHandler(ctx).use { callbackHandler ->
val nMessage = toNat(message) memScoped {
val pubkey = alloc<secp256k1_pubkey>() val nSig = toNat(sig)
secp256k1_ecdsa_recover(ctx, pubkey.ptr, rSig.ptr, nMessage).requireSuccess("secp256k1_ecdsa_recover() failed") val rSig = alloc<secp256k1_ecdsa_recoverable_signature>()
return serializePubkey(pubkey) secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, rSig.ptr, nSig, recid).requireSuccess(callbackHandler, "secp256k1_ecdsa_recoverable_signature_parse_compact() failed")
val nMessage = toNat(message)
val pubkey = alloc<secp256k1_pubkey>()
secp256k1_ecdsa_recover(ctx, pubkey.ptr, rSig.ptr, nMessage).requireSuccess(callbackHandler, "secp256k1_ecdsa_recover() failed")
return serializePubkey(pubkey)
}
} }
} }
public override fun compact2der(sig: ByteArray): ByteArray { public override fun compact2der(sig: ByteArray): ByteArray {
require(sig.size == 64) require(sig.size == 64)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nSig = allocSignature(sig) memScoped {
val natOutput = allocArray<UByteVar>(73) val nSig = allocSignature(sig)
val len = alloc<size_tVar>() val natOutput = allocArray<UByteVar>(73)
len.value = 73.convert() val len = alloc<size_tVar>()
secp256k1_ecdsa_signature_serialize_der(ctx, natOutput, len.ptr, nSig.ptr).requireSuccess("secp256k1_ecdsa_signature_serialize_der() failed") len.value = 73.convert()
return natOutput.readBytes(len.value.toInt()) secp256k1_ecdsa_signature_serialize_der(ctx, natOutput, len.ptr, nSig.ptr).requireSuccess(callbackHandler, "secp256k1_ecdsa_signature_serialize_der() failed")
return natOutput.readBytes(len.value.toInt())
}
} }
} }
@ -226,13 +320,15 @@ public object Secp256k1Native : Secp256k1 {
require(signature.size == 64) require(signature.size == 64)
require(data.size == 32) require(data.size == 32)
require(pub.size == 32) require(pub.size == 32)
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nPub = toNat(pub) memScoped {
val pubkey = alloc<secp256k1_xonly_pubkey>() val nPub = toNat(pub)
secp256k1_xonly_pubkey_parse(ctx, pubkey.ptr, nPub).requireSuccess("secp256k1_xonly_pubkey_parse() failed") val pubkey = alloc<secp256k1_xonly_pubkey>()
val nData = toNat(data) secp256k1_xonly_pubkey_parse(ctx, pubkey.ptr, nPub).requireSuccess(callbackHandler, "secp256k1_xonly_pubkey_parse() failed")
val nSig = toNat(signature) val nData = toNat(data)
return secp256k1_schnorrsig_verify(ctx, nSig, nData, 32, pubkey.ptr) == 1 val nSig = toNat(signature)
return secp256k1_schnorrsig_verify(ctx, nSig, nData, 32u, pubkey.ptr) == 1
}
} }
} }
@ -240,15 +336,17 @@ public object Secp256k1Native : Secp256k1 {
require(sec.size == 32) require(sec.size == 32)
require(data.size == 32) require(data.size == 32)
auxrand32?.let { require(it.size == 32) } auxrand32?.let { require(it.size == 32) }
memScoped { CallbackHandler(ctx).use { callbackHandler ->
val nSec = toNat(sec) memScoped {
val nData = toNat(data) val nSec = toNat(sec)
val nAuxrand32 = auxrand32?.let { toNat(it) } val nData = toNat(data)
val nSig = allocArray<UByteVar>(64) val nAuxrand32 = auxrand32?.let { toNat(it) }
val keypair = alloc<secp256k1_keypair>() val nSig = allocArray<UByteVar>(64)
secp256k1_keypair_create(ctx, keypair.ptr, nSec).requireSuccess("secp256k1_keypair_create() failed") val keypair = alloc<secp256k1_keypair>()
secp256k1_schnorrsig_sign32(ctx, nSig, nData, keypair.ptr, nAuxrand32).requireSuccess("secp256k1_ecdsa_sign() failed") secp256k1_keypair_create(ctx, keypair.ptr, nSec).requireSuccess(callbackHandler, "secp256k1_keypair_create() failed")
return nSig.readBytes(64) secp256k1_schnorrsig_sign32(ctx, nSig, nData, keypair.ptr, nAuxrand32).requireSuccess(callbackHandler, "secp256k1_ecdsa_sign() failed")
return nSig.readBytes(64)
}
} }
} }

View File

@ -275,6 +275,11 @@ class Secp256k1Test {
val pub0 = Secp256k1.ecdsaRecover(sig, message, 0) val pub0 = Secp256k1.ecdsaRecover(sig, message, 0)
val pub1 = Secp256k1.ecdsaRecover(sig, message, 1) val pub1 = Secp256k1.ecdsaRecover(sig, message, 1)
assertTrue(pub.contentEquals(pub0) || pub.contentEquals(pub1)) assertTrue(pub.contentEquals(pub0) || pub.contentEquals(pub1))
// this is a special case, ecdsaRecover explicitly does not check that recid is valid, which triggers our illegal callback handler
assertFailsWith(Secp256k1IllegalCallbackException::class) {
Secp256k1.ecdsaRecover(sig, message, 4)
}
} }
@Test @Test