From 08b6d16836baece5452ddd6baccb0bc77eae899b Mon Sep 17 00:00:00 2001 From: Salomon BRYS Date: Fri, 26 Jun 2020 17:10:48 +0200 Subject: [PATCH] Native & iOS implementation --- build.gradle.kts | 103 +++++--- gradle.properties | 12 + native/build-ios.sh | 0 .../kotlin/fr/acinq/secp256k1/Secp256k1.kt | 2 - .../kotlin/fr/acinq/secp256k1/utils.kt | 2 +- .../kotlin/fr/acinq/secp256k1/Secp256k1.kt | 2 - .../kotlin/org/bitcoin/NativeSecp256k1.kt | 13 +- src/nativeInterop/cinterop/libsecp256k1.def | 12 + .../kotlin/fr/acinq/secp256k1/Secp256k1.kt | 238 ++++++++++++++++++ 9 files changed, 329 insertions(+), 55 deletions(-) mode change 100644 => 100755 native/build-ios.sh rename src/{commonTest => commonMain}/kotlin/fr/acinq/secp256k1/utils.kt (98%) create mode 100644 src/nativeInterop/cinterop/libsecp256k1.def create mode 100644 src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt diff --git a/build.gradle.kts b/build.gradle.kts index 90d22de..4d5d657 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,57 +15,78 @@ val currentOs = org.gradle.internal.os.OperatingSystem.current() kotlin { explicitApi() + val commonMain by sourceSets.getting { + dependencies { + implementation(kotlin("stdlib-common")) + } + } + val commonTest by sourceSets.getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + jvm { compilations.all { kotlinOptions.jvmTarget = "1.8" } + (tasks[compilations["main"].processResourcesTaskName] as ProcessResources).apply{ + dependsOn("copyJni") + from(buildDir.resolve("jniResources")) + } + compilations["main"].dependencies { + implementation(kotlin("stdlib-jdk8")) + } + compilations["test"].dependencies { + implementation(kotlin("test-junit")) + } } -// linuxX64() -// ios() - - sourceSets { - val commonMain by getting { - dependencies { - implementation(kotlin("stdlib-common")) - } - } - val commonTest by getting { - dependencies { - implementation(kotlin("test-common")) - implementation(kotlin("test-annotations-common")) - } - } - val jvmMain by getting { - dependencies { - implementation(kotlin("stdlib-jdk8")) - implementation(kotlin("test-junit")) - } - } - val jvmTest by getting { - dependencies { - implementation(kotlin("test-junit")) + fun org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.secp256k1CInterop() { + compilations["main"].cinterops { + val libsecp256k1 by creating { + includeDirs.headerFilterOnly(project.file("native/secp256k1/include/")) +// includeDirs("/usr/local/lib") + tasks[interopProcessingTaskName].dependsOn("buildSecp256k1Ios") } } } + + val nativeMain by sourceSets.creating { dependsOn(commonMain) } + + linuxX64 { + secp256k1CInterop() + // https://youtrack.jetbrains.com/issue/KT-39396 + compilations["main"].kotlinOptions.freeCompilerArgs += listOf("-include-binary", "$rootDir/native/build/linux/libsecp256k1.a") + compilations["main"].defaultSourceSet.dependsOn(nativeMain) + } + + ios { + secp256k1CInterop() + // https://youtrack.jetbrains.com/issue/KT-39396 + compilations["main"].kotlinOptions.freeCompilerArgs += listOf("-include-binary", "$rootDir/native/build/ios/libsecp256k1.a") + compilations["main"].defaultSourceSet.dependsOn(nativeMain) + } + + sourceSets.all { + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + } + } val buildSecp256k1 by tasks.creating { group = "build" } -fun creatingBuildSecp256k1(target: String, cross: String? = null, env: String = "", configuration: Task.() -> Unit = {}) = tasks.creating { +fun creatingBuildSecp256k1(target: String, cross: String? = null, env: String = "", configuration: Task.() -> Unit = {}) = tasks.creating(Exec::class) { group = "build" buildSecp256k1.dependsOn(this) inputs.files(projectDir.resolve("native/build.sh")) outputs.dir(projectDir.resolve("native/build/$target")) - doLast { - exec { - workingDir = projectDir.resolve("native") - environment("TARGET", target) - if (cross == null) commandLine("./build.sh") - else commandLine("./dockcross-$cross", "bash", "-c", "TARGET=$target ./build.sh") - } - } + workingDir = projectDir.resolve("native") + environment("TARGET", target) + if (cross == null) commandLine("./build.sh") + else commandLine("./dockcross-$cross", "bash", "-c", "TARGET=$target ./build.sh") configuration() } @@ -74,6 +95,17 @@ val buildSecp256k1Darwin by creatingBuildSecp256k1("darwin") val buildSecp256k1Linux by creatingBuildSecp256k1("linux", cross = if (currentOs.isMacOsX) "linux-x64" else null) val buildSecp256k1Mingw by creatingBuildSecp256k1("mingw", cross = "windows-x64", env = "CONF_OPTS=--host=x86_64-w64-mingw32") +val buildSecp256k1Ios by tasks.creating(Exec::class) { + group = "build" + buildSecp256k1.dependsOn(this) + + inputs.files(projectDir.resolve("native/build-ios.sh")) + outputs.dir(projectDir.resolve("native/build/ios")) + + workingDir = projectDir.resolve("native") + commandLine("./build-ios.sh") +} + val copyJni by tasks.creating(Sync::class) { dependsOn(buildSecp256k1) from(projectDir.resolve("native/build/linux/libsecp256k1-jni.so")) { rename { "libsecp256k1-jni-linux-x86_64.so" } } @@ -81,8 +113,3 @@ val copyJni by tasks.creating(Sync::class) { from(projectDir.resolve("native/build/mingw/secp256k1-jni.dll")) { rename { "secp256k1-jni-mingw-x86_64.dll" } } into(buildDir.resolve("jniResources/fr/acinq/secp256k1/native")) } - -(tasks[kotlin.jvm().compilations["main"].processResourcesTaskName] as ProcessResources).apply{ - dependsOn(copyJni) - from(buildDir.resolve("jniResources")) -} diff --git a/gradle.properties b/gradle.properties index 7fc6f1f..854f123 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,13 @@ +# gradle +org.gradle.jvmargs=-Xmx1536m +org.gradle.parallel=true + +# kotlin kotlin.code.style=official +kotlin.incremental.multiplatform = true +kotlin.parallel.tasks.in.project=true +#kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false + +# https://github.com/gradle/gradle/issues/11412 +systemProp.org.gradle.internal.publish.checksums.insecure=true diff --git a/native/build-ios.sh b/native/build-ios.sh old mode 100644 new mode 100755 diff --git a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index 4b1e774..e327f81 100644 --- a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -35,8 +35,6 @@ public expect object Secp256k1 { public fun cleanup() - public fun cloneContext(): Long - public fun privKeyNegate(privkey: ByteArray): ByteArray public fun privKeyTweakMul(privkey: ByteArray, tweak: ByteArray): ByteArray diff --git a/src/commonTest/kotlin/fr/acinq/secp256k1/utils.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/utils.kt similarity index 98% rename from src/commonTest/kotlin/fr/acinq/secp256k1/utils.kt rename to src/commonMain/kotlin/fr/acinq/secp256k1/utils.kt index 3c64d54..d7c2733 100644 --- a/src/commonTest/kotlin/fr/acinq/secp256k1/utils.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/utils.kt @@ -2,7 +2,7 @@ package fr.acinq.secp256k1 import kotlin.jvm.JvmStatic -object Hex { +internal object Hex { private val hexCode = arrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') @JvmStatic diff --git a/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index 07825dc..034649d 100644 --- a/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -39,8 +39,6 @@ public actual object Secp256k1 { public actual fun cleanup(): Unit = NativeSecp256k1.cleanup() - public actual fun cloneContext(): Long = NativeSecp256k1.cloneContext() - public actual fun privKeyNegate(privkey: ByteArray): ByteArray = NativeSecp256k1.privKeyNegate(privkey) public actual fun privKeyTweakMul(privkey: ByteArray, tweak: ByteArray): ByteArray = NativeSecp256k1.privKeyTweakMul(privkey, tweak) diff --git a/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1.kt b/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1.kt index 8933ee9..ac74318 100644 --- a/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1.kt +++ b/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1.kt @@ -225,16 +225,6 @@ public object NativeSecp256k1 { } } - @JvmStatic - public fun cloneContext(): Long { - r.lock() - return try { - secp256k1_ctx_clone(Secp256k1Context.getContext()) - } finally { - r.unlock() - } - } - @JvmStatic @Throws(AssertFailException::class) public fun privKeyNegate(privkey: ByteArray): ByteArray { @@ -343,7 +333,7 @@ public object NativeSecp256k1 { @Throws(AssertFailException::class) public fun pubKeyTweakAdd(pubkey: ByteArray, tweak: ByteArray): ByteArray { require(pubkey.size == 33 || pubkey.size == 65) - val byteBuff = pack(pubkey, tweak!!) + val byteBuff = pack(pubkey, tweak) val retByteArray: Array r.lock() retByteArray = try { @@ -476,7 +466,6 @@ public object NativeSecp256k1 { } } - @JvmStatic private external fun secp256k1_ctx_clone(context: Long): Long @JvmStatic private external fun secp256k1_context_randomize(byteBuff: ByteBuffer, context: Long): Int @JvmStatic private external fun secp256k1_privkey_negate(byteBuff: ByteBuffer, context: Long): Array @JvmStatic private external fun secp256k1_privkey_tweak_add(byteBuff: ByteBuffer, context: Long): Array diff --git a/src/nativeInterop/cinterop/libsecp256k1.def b/src/nativeInterop/cinterop/libsecp256k1.def new file mode 100644 index 0000000..849e578 --- /dev/null +++ b/src/nativeInterop/cinterop/libsecp256k1.def @@ -0,0 +1,12 @@ +package = secp256k1 + +headers = secp256k1.h secp256k1_ecdh.h secp256k1_recovery.h +headerFilter = secp256k1/** secp256k1_ecdh.h secp256k1_recovery.h secp256k1.h + +staticLibraries.linux = libsecp256k1.a +libraryPaths.linux = c/secp256k1/build/linux/ +linkerOpts.linux = -L/usr/lib64 -L/usr/lib/x86_64-linux-gnu -L/usr/local/lib + +staticLibraries.ios = libsecp256k1.a +libraryPaths.ios = c/secp256k1/build/ios/ /usr/local/lib +linkerOpts.ios = -framework Security -framework Foundation diff --git a/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt new file mode 100644 index 0000000..cd3d3cd --- /dev/null +++ b/src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -0,0 +1,238 @@ +package fr.acinq.secp256k1 + +import kotlinx.cinterop.* +import platform.posix.size_tVar +import secp256k1.* + +@OptIn(ExperimentalUnsignedTypes::class) +public actual object Secp256k1 { + + private const val SIG_FORMAT_UNKNOWN = 0 + private const val SIG_FORMAT_COMPACT = 1 + private const val SIG_FORMAT_DER = 2 + + private val ctx: CPointer by lazy { + secp256k1_context_create((SECP256K1_FLAGS_TYPE_CONTEXT or SECP256K1_FLAGS_BIT_CONTEXT_SIGN or SECP256K1_FLAGS_BIT_CONTEXT_VERIFY).toUInt()) + ?: error("Could not create segp256k1 context") + } + + private fun Int.requireSuccess() = require(this == 1) { "secp256k1 native function call failed" } + + private fun MemScope.allocSignature(input: ByteArray): secp256k1_ecdsa_signature { + val sigFormat = when (input.size) { + 64 -> SIG_FORMAT_COMPACT + 70, 71, 72, 73 -> SIG_FORMAT_DER + else -> SIG_FORMAT_UNKNOWN + } + val sig = alloc() + val nativeBytes = toNat(input) + val result = when (sigFormat) { + SIG_FORMAT_COMPACT -> secp256k1_ecdsa_signature_parse_compact(ctx, sig.ptr, nativeBytes) + SIG_FORMAT_DER -> secp256k1_ecdsa_signature_parse_der(ctx, sig.ptr, nativeBytes, input.size.toULong()) + else -> 0 + } + require(result == 1) { "cannot parse signature (size = ${input.size}, format = $sigFormat sig = ${Hex.encode(input)}" } + return sig + } + + private fun MemScope.allocPublicKey(pubkey: ByteArray): secp256k1_pubkey { + val natPub = toNat(pubkey) + val pub = alloc() + secp256k1_ec_pubkey_parse(ctx, pub.ptr, natPub, pubkey.size.convert()).requireSuccess() + return pub + } + + private fun MemScope.serializePubkey(pubkey: secp256k1_pubkey, len: Int): ByteArray { + val serialized = allocArray(len) + val outputLen = alloc() + outputLen.value = len.convert() + secp256k1_ec_pubkey_serialize(ctx, serialized, outputLen.ptr, pubkey.ptr, (if (len == 33) SECP256K1_EC_COMPRESSED else SECP256K1_EC_UNCOMPRESSED).convert()).requireSuccess() + return serialized.readBytes(outputLen.value.convert()) + } + + private fun DeferScope.toNat(bytes: ByteArray): CPointer { + val ubytes = bytes.asUByteArray() + val pinned = ubytes.pin() + this.defer { pinned.unpin() } + return pinned.addressOf(0) + } + + public actual fun verify(data: ByteArray, signature: ByteArray, pub: ByteArray): Boolean { + require(data.size == 32) + require(pub.size == 33 || pub.size == 65) + memScoped { + val pubkey = allocPublicKey(pub) + val natData = toNat(data) + val parsedSig = allocSignature(signature) + return secp256k1_ecdsa_verify(ctx, parsedSig.ptr, natData, pubkey.ptr) == 1 + } + } + + public actual fun sign(data: ByteArray, sec: ByteArray): ByteArray { + require(sec.size == 32) + require(data.size == 32) + memScoped { + val natSec = toNat(sec) + val natData = toNat(data) + val natSig = alloc() + val result = secp256k1_ecdsa_sign(ctx, natSig.ptr, natData, natSec, null, null) + if (result == 0) return ByteArray(0) + val natOutput = allocArray(72) + val outputLen = alloc() + outputLen.value = 72.convert() + secp256k1_ecdsa_signature_serialize_der(ctx, natOutput, outputLen.ptr, natSig.ptr).requireSuccess() + return natOutput.readBytes(outputLen.value.toInt()) + } + } + + public actual fun signCompact(data: ByteArray, sec: ByteArray): ByteArray { + require(data.size == 32) + require(sec.size <= 32) + memScoped { + val natSec = toNat(sec) + val natData = toNat(data) + val natSig = alloc() + secp256k1_ecdsa_sign(ctx, natSig.ptr, natData, natSec, null, null).requireSuccess() + val natCompact = allocArray(64) + secp256k1_ecdsa_signature_serialize_compact(ctx, natCompact, natSig.ptr).requireSuccess() + return natCompact.readBytes(64) + } + } + + public actual fun secKeyVerify(seckey: ByteArray): Boolean { + require(seckey.size == 32) + memScoped { + val natSec = toNat(seckey) + return secp256k1_ec_seckey_verify(ctx, natSec) == 1 + } + } + + public actual fun computePubkey(seckey: ByteArray): ByteArray { + require(seckey.size == 32) + memScoped { + val natSec = toNat(seckey) + val pubkey = alloc() + val result = secp256k1_ec_pubkey_create(ctx, pubkey.ptr, natSec) + if (result == 0) return ByteArray(0) + return serializePubkey(pubkey, 65) + } + } + + public actual fun parsePubkey(pubkey: ByteArray): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + memScoped { + val nPubkey = allocPublicKey(pubkey) + return serializePubkey(nPubkey, 65) + } + } + + public actual fun cleanup() { + secp256k1_context_destroy(ctx) + } + + public actual fun privKeyNegate(privkey: ByteArray): ByteArray { + require(privkey.size == 32) + memScoped { + val negated = privkey.copyOf() + val negPriv = toNat(negated) + secp256k1_ec_privkey_negate(ctx, negPriv).requireSuccess() + return negated + } + } + + public actual fun privKeyTweakMul(privkey: ByteArray, tweak: ByteArray): ByteArray { + require(privkey.size == 32) + memScoped { + val multiplied = privkey.copyOf() + val natMul = toNat(multiplied) + val natTweak = toNat(tweak) + secp256k1_ec_privkey_tweak_mul(ctx, natMul, natTweak).requireSuccess() + return multiplied + } + } + + public actual fun privKeyTweakAdd(privkey: ByteArray, tweak: ByteArray): ByteArray { + require(privkey.size == 32) + memScoped { + val added = privkey.copyOf() + val natAdd = toNat(added) + val natTweak = toNat(tweak) + secp256k1_ec_privkey_tweak_add(ctx, natAdd, natTweak).requireSuccess() + return added + } + } + + public actual fun pubKeyNegate(pubkey: ByteArray): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + memScoped { + val nPubkey = allocPublicKey(pubkey) + secp256k1_ec_pubkey_negate(ctx, nPubkey.ptr).requireSuccess() + return serializePubkey(nPubkey, pubkey.size) + } + } + + public actual fun pubKeyTweakAdd(pubkey: ByteArray, tweak: ByteArray): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + memScoped { + val nPubkey = allocPublicKey(pubkey) + val nTweak = toNat(tweak) + secp256k1_ec_pubkey_tweak_add(ctx, nPubkey.ptr, nTweak).requireSuccess() + return serializePubkey(nPubkey, 65) + } + } + + public actual fun pubKeyTweakMul(pubkey: ByteArray, tweak: ByteArray): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + memScoped { + val nPubkey = allocPublicKey(pubkey) + val nTweak = toNat(tweak) + secp256k1_ec_pubkey_tweak_mul(ctx, nPubkey.ptr, nTweak).requireSuccess() + return serializePubkey(nPubkey, 65) + } + } + + public actual fun pubKeyAdd(pubkey1: ByteArray, pubkey2: ByteArray): ByteArray { + require(pubkey1.size == 33 || pubkey2.size == 65) + memScoped { + val nPubkey1 = allocPublicKey(pubkey1) + val nPubkey2 = allocPublicKey(pubkey2) + val combined = nativeHeap.alloc() + secp256k1_ec_pubkey_combine(ctx, combined.ptr, cValuesOf(nPubkey1.ptr, nPubkey2.ptr), 2.convert()).requireSuccess() + return serializePubkey(combined, 65) + } + } + + public actual fun createECDHSecret(seckey: ByteArray, pubkey: ByteArray): ByteArray { + require(seckey.size == 32) + require(pubkey.size == 33 || pubkey.size == 65) + memScoped { + val nPubkey = allocPublicKey(pubkey) + val nSeckey = toNat(seckey) + val output = allocArray(32) + secp256k1_ecdh(ctx, output, nPubkey.ptr, nSeckey, null, null).requireSuccess() + return output.readBytes(32) + } + } + + public actual fun ecdsaRecover(sig: ByteArray, message: ByteArray, recid: Int): ByteArray { + require(sig.size == 64) + require(message.size == 32) + memScoped { + val nSig = toNat(sig) + val rSig = nativeHeap.alloc() + secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, rSig.ptr, nSig, recid).requireSuccess() + val nMessage = toNat(message) + val pubkey = nativeHeap.alloc() + secp256k1_ecdsa_recover(ctx, pubkey.ptr, rSig.ptr, nMessage).requireSuccess() + return serializePubkey(pubkey, 65) + } + } + + public actual fun randomize(seed: ByteArray): Boolean { + require(seed.size == 32) + memScoped { + val nSeed = toNat(seed) + return secp256k1_context_randomize(ctx, nSeed) == 1 + } + } +}