Skip to content

feat: add bip32 pub key derivation #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.hyperledger.identus.apollo.derivation

import com.ionspin.kotlin.bignum.integer.toBigInteger
import uniffi.ed25519_bip32_wrapper.deriveBytesPub

/**
* Represents and HDKey with its derive methods
*/
actual class EdHDPubKey actual constructor(
actual val publicKey: ByteArray,
actual val chainCode: ByteArray,
actual val depth: Int,
actual val index: BigIntegerWrapper
) {
/**
* Method to derive an HDKey by a path
*
* @param path value used to derive a key
*/
actual fun derive(path: String): EdHDPubKey {
if (!path.matches(Regex("^[mM].*"))) {
throw Error("Path must start with \"m\" or \"M\"")
}
if (Regex("^[mM]'?$").matches(path)) {
return this
}
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
var child = this

for (c in parts) {
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
if (m == null || m.size != 3) {
throw Error("Invalid child index: $c")
}
val idx = m[1].toBigInteger()
if (idx >= HDKey.HARDENED_OFFSET) {
throw Error("Invalid index")
}
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx

child = child.deriveChild(BigIntegerWrapper(finalIdx))
}

return child
}

/**
* Method to derive an HDKey child by index
*
* @param wrappedIndex value used to derive a key
*/
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
val index = wrappedIndex.value.uintValue()
val derived = deriveBytesPub(publicKey, chainCode, index)
val publicKey = derived["public_key"]
val chainCode = derived["chain_code"]

if (publicKey == null || chainCode == null) {
throw Error("Unable to derive key")
}

return EdHDPubKey(
publicKey = publicKey,
chainCode = chainCode,
depth = depth + 1,
index = wrappedIndex
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.hyperledger.identus.apollo.derivation

import com.ionspin.kotlin.bignum.integer.toBigInteger
import uniffi.ed25519_bip32_wrapper.deriveBytesPub

/**
* Represents and HDKey with its derive methods
*/
actual class EdHDPubKey actual constructor(
actual val publicKey: ByteArray,
actual val chainCode: ByteArray,
actual val depth: Int,
actual val index: BigIntegerWrapper
) {
/**
* Method to derive an HDKey by a path
*
* @param path value used to derive a key
*/
actual fun derive(path: String): EdHDPubKey {
if (!path.matches(Regex("^[mM].*"))) {
throw Error("Path must start with \"m\" or \"M\"")
}
if (Regex("^[mM]'?$").matches(path)) {
return this
}
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
var child = this

for (c in parts) {
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
if (m == null || m.size != 3) {
throw Error("Invalid child index: $c")
}
val idx = m[1].toBigInteger()
if (idx >= HDKey.HARDENED_OFFSET) {
throw Error("Invalid index")
}
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx

child = child.deriveChild(BigIntegerWrapper(finalIdx))
}

return child
}

/**
* Method to derive an HDKey child by index
*
* @param wrappedIndex value used to derive a key
*/
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
val index = wrappedIndex.value.uintValue()
val derived = deriveBytesPub(publicKey, chainCode, index)
val publicKey = derived["public_key"]
val chainCode = derived["chain_code"]

if (publicKey == null || chainCode == null) {
throw Error("Unable to derive key")
}

return EdHDPubKey(
publicKey = publicKey,
chainCode = chainCode,
depth = depth + 1,
index = wrappedIndex
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.hyperledger.identus.apollo.derivation

expect class EdHDPubKey constructor(
publicKey: ByteArray,
chainCode: ByteArray,
depth: Int = 0,
index: BigIntegerWrapper = BigIntegerWrapper(0)
) {
val publicKey: ByteArray
val chainCode: ByteArray
val depth: Int
val index: BigIntegerWrapper

/**
* Method to derive an HDKey by a path
*
* @param path value used to derive a key
*/
fun derive(path: String): EdHDPubKey

/**
* Method to derive an HDKey child by index
*
* @param wrappedIndex value used to derive a key
*/
fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.hyperledger.identus.apollo.derivation

import org.hyperledger.identus.apollo.Platform
import org.hyperledger.identus.apollo.utils.decodeHex
import org.hyperledger.identus.apollo.utils.toHexString
import kotlin.test.Test
import kotlin.test.assertEquals

class EdHDPubKeyTest {
@Test
fun test_derive_m_1852_1815_0() {
if (!Platform.OS.contains("Android")) {
val publicKey = "6fd8d9c696b01525cc45f15583fc9447c66e1c71fd1a11c8885368404cd0a4ab".decodeHex()
val chainCode = "00b5f1652f5cbe257e567c883dc2b16e0a9568b19c5b81ea8bd197fc95e8bdcf".decodeHex()

val key = EdHDPubKey(publicKey, chainCode)
val derivationPath = listOf(1852, 1815, 0)
val pathString = derivationPath.joinToString(separator = "/", prefix = "m/") { "$it" }
val derived = key.derive(pathString)

assertEquals(
derived.publicKey.toHexString(),
"b857a8cd1dbbfed1824359d9d9e58bc8ffb9f66812b404f4c6ffc315629835bf"
)
assertEquals(derived.chainCode.toHexString(), "9db12d11a3559131a47f51f854a6234725ab8767d3fcc4c9908be55508f3c712")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.hyperledger.identus.apollo.derivation

import com.ionspin.kotlin.bignum.integer.toBigInteger
import org.hyperledger.identus.apollo.utils.external.ed25519_bip32

/**
* Represents and HDKey with its derive methods
*/
@OptIn(ExperimentalJsExport::class)
@JsExport
actual class EdHDPubKey actual constructor(
actual val publicKey: ByteArray,
actual val chainCode: ByteArray,
actual val depth: Int,
actual val index: BigIntegerWrapper
) {
/**
* Method to derive an HDKey by a path
*
* @param path value used to derive a key
*/
actual fun derive(path: String): EdHDPubKey {
if (!path.matches(Regex("^[mM].*"))) {
throw Error("Path must start with \"m\" or \"M\"")
}
if (Regex("^[mM]'?$").matches(path)) {
return this
}
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
var child = this

for (c in parts) {
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
if (m == null || m.size != 3) {
throw Error("Invalid child index: $c")
}
val idx = m[1].toBigInteger()
if (idx >= HDKey.HARDENED_OFFSET) {
throw Error("Invalid index")
}
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx

child = child.deriveChild(BigIntegerWrapper(finalIdx))
}

return child
}

/**
* Method to derive an HDKey child by index
*
* @param wrappedIndex value used to derive a key
*/
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
val derived = ed25519_bip32.derive_bytes_pub(publicKey, chainCode, wrappedIndex.value.uintValue())

return EdHDPubKey(
publicKey = derived[0],
chainCode = derived[1],
depth = depth + 1,
index = wrappedIndex
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ external interface ed25519_bip32_export {
fun from_nonextended(key: ByteArray, chain_code: ByteArray): Array<ByteArray>

fun derive_bytes(key: ByteArray, chain_code: ByteArray, index: Any): Array<ByteArray>

fun derive_bytes_pub(key: ByteArray, chain_code: ByteArray, index: Any): Array<ByteArray>
}

@JsModule("./ed25519_bip32_wasm.js")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.hyperledger.identus.apollo.derivation

import com.ionspin.kotlin.bignum.integer.toBigInteger
import uniffi.ed25519_bip32_wrapper.deriveBytesPub

/**
* Represents and HDKey with its derive methods
*/
actual class EdHDPubKey actual constructor(
actual val publicKey: ByteArray,
actual val chainCode: ByteArray,
actual val depth: Int,
actual val index: BigIntegerWrapper
) {
/**
* Method to derive an HDKey by a path
*
* @param path value used to derive a key
*/
actual fun derive(path: String): EdHDPubKey {
if (!path.matches(Regex("^[mM].*"))) {
throw Error("Path must start with \"m\" or \"M\"")
}
if (Regex("^[mM]'?$").matches(path)) {
return this
}
val parts = path.replace(Regex("^[mM]'?/"), "").split("/")
var child = this

for (c in parts) {
val m = Regex("^(\\d+)('?)$").find(c)?.groupValues
if (m == null || m.size != 3) {
throw Error("Invalid child index: $c")
}
val idx = m[1].toBigInteger()
if (idx >= HDKey.HARDENED_OFFSET) {
throw Error("Invalid index")
}
val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx

child = child.deriveChild(BigIntegerWrapper(finalIdx))
}

return child
}

/**
* Method to derive an HDKey child by index
*
* @param wrappedIndex value used to derive a key
*/
actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDPubKey {
val index = wrappedIndex.value.uintValue()
val derived = deriveBytesPub(publicKey, chainCode, index)
val publicKey = derived["public_key"]
val chainCode = derived["chain_code"]

if (publicKey == null || chainCode == null) {
throw Error("Unable to derive key")
}

return EdHDPubKey(
publicKey = publicKey,
chainCode = chainCode,
depth = depth + 1,
index = wrappedIndex
)
}
}
Loading