1 Commits

Author SHA1 Message Date
sstone
61df0e8a9a Set a "noop" illegal callback
The default "illegal" callback calls abort, which will crash the JVM or native app. We check arguments before calling secp256k1 so it should
never happen, except when trying to create a partial musig2 signature with an secret nonce that does not match the private key.

Methods that could be used to verify that the secret nonce does match the private key are not exported, hence the choice to set a custom callback.
2024-04-15 19:35:39 +02:00
5 changed files with 61 additions and 81 deletions

View File

@@ -44,6 +44,11 @@ void JNI_ThrowByName(JNIEnv *penv, const char *name, const char *msg)
} \
}
static void secp256k1_noop_illegal_callback_fn(const char* str, void* data) {
(void)str;
(void)data;
}
/*
* Class: fr_acinq_bitcoin_Secp256k1Bindings
* Method: secp256k1_context_create
@@ -51,7 +56,9 @@ void JNI_ThrowByName(JNIEnv *penv, const char *name, const char *msg)
*/
JNIEXPORT jlong JNICALL Java_fr_acinq_secp256k1_Secp256k1CFunctions_secp256k1_1context_1create(JNIEnv *penv, jclass clazz, jint flags)
{
return (jlong)secp256k1_context_create(flags);
jlong ctx = (jlong)secp256k1_context_create(flags);
secp256k1_context_set_illegal_callback(ctx, &secp256k1_noop_illegal_callback_fn, NULL);
return ctx;
}
/*

View File

@@ -117,7 +117,6 @@ public object NativeSecp256k1 : Secp256k1 {
}
override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray {
require(musigNonceValidate(secnonce, pubkeyCreate(privkey)))
return Secp256k1CFunctions.secp256k1_musig_partial_sign(Secp256k1Context.getContext(), secnonce, privkey, keyaggCache, session)
}

View File

@@ -217,26 +217,6 @@ public interface Secp256k1 {
*/
public fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray
/**
* Check that a secret nonce was generated with a public key that matches the private key used for signing.
* @param secretnonce secret nonce.
* @param pubkey public key for the private key that will be used, with the secret nonce, to generate a partial signature.
* @return false if the secret nonce does not match the public key.
*/
public fun musigNonceValidate(secretnonce: ByteArray, pubkey: ByteArray): Boolean {
if (secretnonce.size != MUSIG2_SECRET_NONCE_SIZE) return false
if (pubkey.size != 33 && pubkey.size != 65) return false
val pk = Secp256k1.pubkeyParse(pubkey)
// this is a bit hackish but the secp256k1 library does not export methods to do this cleanly
val x = secretnonce.copyOfRange(68, 68 + 32)
x.reverse()
val y = secretnonce.copyOfRange(68 + 32, 68 + 32 + 32)
y.reverse()
val pkx = pk.copyOfRange(1, 1 + 32)
val pky = pk.copyOfRange(33, 33 + 32)
return x.contentEquals(pkx) && y.contentEquals(pky)
}
/**
* Create a partial signature.
*

View File

@@ -9,8 +9,11 @@ import secp256k1.*
public object Secp256k1Native : Secp256k1 {
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())
val c = 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")
val callback = staticCFunction { _: CPointer<ByteVar>?, _: COpaquePointer? -> }
secp256k1_context_set_illegal_callback(c, callback, null)
c
}
private fun Int.requireSuccess(message: String): Int = if (this != 1) throw Secp256k1Exception(message) else this
@@ -81,7 +84,7 @@ public object Secp256k1Native : Secp256k1 {
return serialized.readBytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)
}
private fun DeferScope.toNat(bytes: ByteArray): CPointer<UByteVar> {
private fun DeferScope.toNat(bytes: ByteArray): CPointer<UByteVar> {
val ubytes = bytes.asUByteArray()
val pinned = ubytes.pin()
this.defer { pinned.unpin() }
@@ -112,7 +115,7 @@ public object Secp256k1Native : Secp256k1 {
}
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 {
val nSig = allocSignature(sig)
val isHighS = secp256k1_ecdsa_signature_normalize(ctx, nSig.ptr, nSig.ptr)
@@ -307,16 +310,7 @@ public object Secp256k1Native : Secp256k1 {
memcpy(n.ptr, toNat(it), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong())
n
}
secp256k1_musig_nonce_gen(
ctx,
secnonce.ptr,
pubnonce.ptr,
toNat(sessionId32),
privkey?.let { toNat(it) },
nPubkey.ptr,
msg32?.let { toNat(it) },
nKeyAggCache?.ptr,
extraInput32?.let { toNat(it) }).requireSuccess("secp256k1_musig_nonce_gen() failed")
secp256k1_musig_nonce_gen(ctx, secnonce.ptr, pubnonce.ptr, toNat(sessionId32), privkey?.let { toNat(it) }, nPubkey.ptr, msg32?.let { toNat(it) },nKeyAggCache?.ptr, extraInput32?.let { toNat(it) }).requireSuccess("secp256k1_musig_nonce_gen() failed")
val nPubnonce = allocArray<UByteVar>(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)
secp256k1_musig_pubnonce_serialize(ctx, nPubnonce, pubnonce.ptr).requireSuccess("secp256k1_musig_pubnonce_serialize failed")
secnonce.ptr.readBytes(Secp256k1.MUSIG2_SECRET_NONCE_SIZE) + nPubnonce.readBytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)
@@ -348,7 +342,7 @@ public object Secp256k1Native : Secp256k1 {
n
}
secp256k1_musig_pubkey_agg(ctx, combined.ptr, nKeyAggCache?.ptr, nPubkeys.toCValues(), pubkeys.size.convert()).requireSuccess("secp256k1_musig_nonce_agg() failed")
val agg = serializeXonlyPubkey(combined)
val agg = serializeXonlyPubkey(combined)
keyaggCache?.let { blob -> nKeyAggCache?.let { memcpy(toNat(blob), it.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) } }
return agg
}
@@ -395,14 +389,13 @@ public object Secp256k1Native : Secp256k1 {
memcpy(toNat(session), nSession.ptr, Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE.toULong())
return session
}
}
}
override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray {
require(secnonce.size == Secp256k1.MUSIG2_SECRET_NONCE_SIZE)
require(privkey.size == 32)
require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE)
require(session.size == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE)
require(musigNonceValidate(secnonce, pubkeyCreate(privkey)))
memScoped {
val nSecnonce = alloc<secp256k1_musig_secnonce>()

View File

@@ -5,14 +5,6 @@ import kotlin.test.*
class Secp256k1Test {
val random = Random.Default
fun randomBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
random.nextBytes(buffer)
return buffer
}
@Test
fun verifyValidPrivateKey() {
val priv = Hex.decode("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".lowercase())
@@ -462,55 +454,40 @@ class Secp256k1Test {
@Test
fun testMusig2SigningSession() {
val privkeys = listOf(randomBytes(32), randomBytes(32))
val sessionId = randomBytes(32)
val msg32 = randomBytes(32)
val privkeys = listOf(
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
).map { Hex.decode(it) }.toTypedArray()
val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) }
val sessionId = Hex.decode("0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F")
val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) }
val testData = run {
val builder = StringBuilder()
builder.append("private keys\n")
privkeys.forEach { builder.append(Hex.encode(it)).append("\n") }
builder.append("sessionId ${Hex.encode(sessionId)}\n")
builder.append("msg32 ${Hex.encode(msg32)}\n")
builder.append("nonces\n")
nonces.forEach { builder.append(Hex.encode(it)).append("\n") }
builder.toString()
}
val secnonces = nonces.map { it.copyOfRange(0, 132) }
val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) }
val aggnonce = Secp256k1.musigNonceAgg(pubnonces.toTypedArray())
val keyaggCaches = (0 until 2).map { ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) }
val aggpubkey = Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[0])
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]), testData)
assertContentEquals(keyaggCaches[0], keyaggCaches[1], testData)
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]))
assertContentEquals(keyaggCaches[0], keyaggCaches[1])
val msg32 = Hex.decode("0303030303030303030303030303030303030303030303030303030303030303")
val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, keyaggCaches[it]) }
val psigs = (0 until 2).map {
val psig = Secp256k1.musigPartialSign(secnonces[it], privkeys[it], keyaggCaches[it], sessions[it])
assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData)
assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData)
assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]))
assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]))
psig
}
// signing fails if the secret nonce does not match the private key's public key
assertFails(testData) {
Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0])
}
assertFails(testData) {
Secp256k1.musigPartialSign(secnonces[0], privkeys[1], keyaggCaches[1], sessions[1])
}
val sig = Secp256k1.musigPartialSigAgg(sessions[0], psigs.toTypedArray())
assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray()), testData)
assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey), testData)
assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray()))
assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey))
val invalidSig1 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(psigs[0], psigs[0]))
assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey), testData)
assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey))
val invalidSig2 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(Random.nextBytes(32), Random.nextBytes(32)))
assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey), testData)
assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey))
}
@Test
@@ -543,17 +520,41 @@ class Secp256k1Test {
-1
)
}
}
assertFails {
val privkeys = listOf(
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
).map { Hex.decode(it) }.toTypedArray()
val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) }
@Test
fun fuzzMusig2SigningSession() {
repeat(1000) {
testMusig2SigningSession()
val sessionId = Hex.decode("0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F")
val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) }
val secnonces = nonces.map { it.copyOfRange(0, 132) }
val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) }
val aggnonce = Secp256k1.musigNonceAgg(pubnonces.toTypedArray())
val keyaggCaches = (0 until 2).map { ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) }
val aggpubkey = Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[0])
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]))
assertContentEquals(keyaggCaches[0], keyaggCaches[1])
val msg32 = Hex.decode("0303030303030303030303030303030303030303030303030303030303030303")
val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, keyaggCaches[it]) }
// we sign with the wrong secret nonce. it should fail (i.e. trigger an exception) but not crash the JVM
Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0])
}
}
@Test
fun fuzzEcdsaSignVerify() {
val random = Random.Default
fun randomBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
random.nextBytes(buffer)
return buffer
}
repeat(200) {
val priv = randomBytes(32)
assertTrue(Secp256k1.secKeyVerify(priv))