diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index e4b7fd4c..548168b6 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -12,6 +12,7 @@ use web5_uniffi_wrapper::{ crypto::{ dsa::{ ed25519::{ed25519_generator_generate, Ed25519Signer, Ed25519Verifier}, + secp256k1::{secp256k1_generator_generate, Secp256k1Signer, Secp256k1Verifier}, Signer, Verifier, }, in_memory_key_manager::InMemoryKeyManager, diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index 90418378..6d619039 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -1,5 +1,6 @@ namespace web5 { JwkData ed25519_generator_generate(); + JwkData secp256k1_generator_generate(); [Throws=Web5Error] BearerDid did_jwk_create(DidJwkCreateOptions? options); @@ -91,6 +92,18 @@ interface Ed25519Verifier { void verify(bytes message, bytes signature); }; +interface Secp256k1Signer { + constructor(JwkData private_key); + [Throws=Web5Error] + bytes sign(bytes payload); +}; + +interface Secp256k1Verifier { + constructor(JwkData public_jwk); + [Throws=Web5Error] + void verify(bytes message, bytes signature); +}; + dictionary DidData { string uri; string url; diff --git a/bindings/web5_uniffi_wrapper/src/crypto/dsa/mod.rs b/bindings/web5_uniffi_wrapper/src/crypto/dsa/mod.rs index 8a8e9a8d..0f427a6b 100644 --- a/bindings/web5_uniffi_wrapper/src/crypto/dsa/mod.rs +++ b/bindings/web5_uniffi_wrapper/src/crypto/dsa/mod.rs @@ -1,4 +1,5 @@ pub mod ed25519; +pub mod secp256k1; use crate::errors::Result; use std::sync::Arc; diff --git a/bindings/web5_uniffi_wrapper/src/crypto/dsa/secp256k1.rs b/bindings/web5_uniffi_wrapper/src/crypto/dsa/secp256k1.rs new file mode 100644 index 00000000..e578a17a --- /dev/null +++ b/bindings/web5_uniffi_wrapper/src/crypto/dsa/secp256k1.rs @@ -0,0 +1,44 @@ +use super::{Signer, Verifier}; +use crate::errors::Result; +use web5::crypto::{ + dsa::{ + secp256k1::{ + Secp256k1Generator as InnerSecp256k1Generator, Secp256k1Signer as InnerSecp256k1Signer, + Secp256k1Verifier as InnerSecp256k1Verifier, + }, + Signer as InnerSigner, Verifier as InnerVerifier, + }, + jwk::Jwk, +}; + +pub fn secp256k1_generator_generate() -> Jwk { + InnerSecp256k1Generator::generate() +} + +pub struct Secp256k1Signer(pub InnerSecp256k1Signer); + +impl Secp256k1Signer { + pub fn new(private_jwk: Jwk) -> Self { + Self(InnerSecp256k1Signer::new(private_jwk)) + } +} + +impl Signer for Secp256k1Signer { + fn sign(&self, payload: Vec) -> Result> { + Ok(self.0.sign(&payload)?) + } +} + +pub struct Secp256k1Verifier(pub InnerSecp256k1Verifier); + +impl Secp256k1Verifier { + pub fn new(public_jwk: Jwk) -> Self { + Self(InnerSecp256k1Verifier::new(public_jwk)) + } +} + +impl Verifier for Secp256k1Verifier { + fn verify(&self, payload: Vec, signature: Vec) -> Result<()> { + Ok(self.0.verify(&payload, &signature)?) + } +} diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/Secp256k1Generator.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/Secp256k1Generator.kt new file mode 100644 index 00000000..99fd7629 --- /dev/null +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/Secp256k1Generator.kt @@ -0,0 +1,27 @@ +package web5.sdk.crypto + +import web5.sdk.Web5Exception +import web5.sdk.crypto.keys.Jwk +import web5.sdk.rust.secp256k1GeneratorGenerate +import web5.sdk.rust.Web5Exception.Exception as RustCoreException + +/** + * Generates private key material for secp256k1. + */ +class Secp256k1Generator { + companion object { + /** + * Generate the private key material; return Jwk includes private key material. + * + * @return Jwk the JWK with private key material included. + */ + fun generate(): Jwk { + try { + val rustCoreJwkData = secp256k1GeneratorGenerate() + return Jwk.fromRustCoreJwkData(rustCoreJwkData) + } catch (e: RustCoreException) { + throw Web5Exception.fromRustCore(e) + } + } + } +} \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Secp256k1Signer.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Secp256k1Signer.kt new file mode 100644 index 00000000..f340ea97 --- /dev/null +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/signers/Secp256k1Signer.kt @@ -0,0 +1,27 @@ +package web5.sdk.crypto.signers + +import web5.sdk.Web5Exception +import web5.sdk.crypto.keys.Jwk +import web5.sdk.rust.Secp256k1Signer as RustCoreSecp256k1Signer +import web5.sdk.rust.Web5Exception.Exception as RustCoreException + +/** + * Implementation of Signer for secp256k1. + */ +class Secp256k1Signer(privateJwk: Jwk) : Signer { + private val rustCoreSigner = RustCoreSecp256k1Signer(privateJwk.rustCoreJwkData) + + /** + * Implementation of Signer's sign instance method for secp256k1. + * + * @param payload the data to be signed. + * @return ByteArray the signature. + */ + override fun sign(payload: ByteArray): ByteArray { + try { + return rustCoreSigner.sign(payload) + } catch (e: RustCoreException) { + throw Web5Exception.fromRustCore(e) + } + } +} diff --git a/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Secp256k1Verifier.kt b/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Secp256k1Verifier.kt new file mode 100644 index 00000000..65dd25b6 --- /dev/null +++ b/bound/kt/src/main/kotlin/web5/sdk/crypto/verifiers/Secp256k1Verifier.kt @@ -0,0 +1,28 @@ +package web5.sdk.crypto.verifiers + +import web5.sdk.Web5Exception +import web5.sdk.crypto.keys.Jwk +import web5.sdk.rust.Secp256k1Verifier as RustCoreSecp256k1Verifier +import web5.sdk.rust.Web5Exception.Exception as RustCoreException + +/** + * Implementation of Verifier for secp256k1. + */ +class Secp256k1Verifier(publicJwk: Jwk) : Verifier { + private val rustCoreVerifier = RustCoreSecp256k1Verifier(publicJwk.rustCoreJwkData) + + /** + * Implementation of Signer's verify instance method for secp256k1. + * + * @param message the data to be verified. + * @param signature the signature to be verified. + * @throws Web5Exception in the case of a failed verification + */ + override fun verify(message: ByteArray, signature: ByteArray) { + try { + rustCoreVerifier.verify(message, signature) + } catch (e: RustCoreException) { + throw Web5Exception.fromRustCore(e) + } + } +} \ No newline at end of file diff --git a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt index 345d9420..42567da6 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt @@ -908,6 +908,20 @@ internal open class UniffiVTableCallbackInterfaceVerifier( + + + + + + + + + + + + + + @@ -1068,6 +1082,22 @@ internal interface UniffiLib : Library { ): Pointer fun uniffi_web5_uniffi_fn_method_resolutionresult_get_data(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue + fun uniffi_web5_uniffi_fn_clone_secp256k1signer(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_free_secp256k1signer(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_web5_uniffi_fn_constructor_secp256k1signer_new(`privateKey`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_method_secp256k1signer_sign(`ptr`: Pointer,`payload`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_web5_uniffi_fn_clone_secp256k1verifier(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_free_secp256k1verifier(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_web5_uniffi_fn_constructor_secp256k1verifier_new(`publicJwk`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_web5_uniffi_fn_method_secp256k1verifier_verify(`ptr`: Pointer,`message`: RustBuffer.ByValue,`signature`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit fun uniffi_web5_uniffi_fn_clone_signer(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, ): Pointer fun uniffi_web5_uniffi_fn_free_signer(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, @@ -1134,6 +1164,8 @@ internal interface UniffiLib : Library { ): Pointer fun uniffi_web5_uniffi_fn_func_ed25519_generator_generate(uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue + fun uniffi_web5_uniffi_fn_func_secp256k1_generator_generate(uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue fun ffi_web5_uniffi_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue fun ffi_web5_uniffi_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, @@ -1262,6 +1294,8 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_func_ed25519_generator_generate( ): Short + fun uniffi_web5_uniffi_checksum_func_secp256k1_generator_generate( + ): Short fun uniffi_web5_uniffi_checksum_method_bearerdid_get_data( ): Short fun uniffi_web5_uniffi_checksum_method_bearerdid_get_signer( @@ -1306,6 +1340,10 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_method_resolutionresult_get_data( ): Short + fun uniffi_web5_uniffi_checksum_method_secp256k1signer_sign( + ): Short + fun uniffi_web5_uniffi_checksum_method_secp256k1verifier_verify( + ): Short fun uniffi_web5_uniffi_checksum_method_signer_sign( ): Short fun uniffi_web5_uniffi_checksum_method_statuslistcredential_get_base( @@ -1348,6 +1386,10 @@ internal interface UniffiLib : Library { ): Short fun uniffi_web5_uniffi_checksum_constructor_resolutionresult_resolve( ): Short + fun uniffi_web5_uniffi_checksum_constructor_secp256k1signer_new( + ): Short + fun uniffi_web5_uniffi_checksum_constructor_secp256k1verifier_new( + ): Short fun uniffi_web5_uniffi_checksum_constructor_statuslistcredential_create( ): Short fun uniffi_web5_uniffi_checksum_constructor_verifiablecredential_create( @@ -1399,6 +1441,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_func_ed25519_generator_generate() != 57849.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_func_secp256k1_generator_generate() != 50489.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_method_bearerdid_get_data() != 23985.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -1465,6 +1510,12 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_method_resolutionresult_get_data() != 57220.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_method_secp256k1signer_sign() != 17108.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_web5_uniffi_checksum_method_secp256k1verifier_verify() != 38282.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_method_signer_sign() != 5738.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -1528,6 +1579,12 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_constructor_resolutionresult_resolve() != 11404.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_web5_uniffi_checksum_constructor_secp256k1signer_new() != 58975.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_web5_uniffi_checksum_constructor_secp256k1verifier_new() != 20759.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_web5_uniffi_checksum_constructor_statuslistcredential_create() != 49374.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -5044,6 +5101,485 @@ public object FfiConverterTypeResolutionResult: FfiConverter + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_constructor_secp256k1signer_new( + FfiConverterTypeJwkData.lower(`privateKey`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_free_secp256k1signer(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_clone_secp256k1signer(pointer!!, status) + } + } + + + @Throws(Web5Exception::class)override fun `sign`(`payload`: kotlin.ByteArray): kotlin.ByteArray { + return FfiConverterByteArray.lift( + callWithPointer { + uniffiRustCallWithError(Web5Exception) { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_secp256k1signer_sign( + it, FfiConverterByteArray.lower(`payload`),_status) +} + } + ) + } + + + + + + + companion object + +} + +public object FfiConverterTypeSecp256k1Signer: FfiConverter { + + override fun lower(value: Secp256k1Signer): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): Secp256k1Signer { + return Secp256k1Signer(value) + } + + override fun read(buf: ByteBuffer): Secp256k1Signer { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: Secp256k1Signer) = 8UL + + override fun write(value: Secp256k1Signer, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + +public interface Secp256k1VerifierInterface { + + fun `verify`(`message`: kotlin.ByteArray, `signature`: kotlin.ByteArray) + + companion object +} + +open class Secp256k1Verifier: Disposable, AutoCloseable, Secp256k1VerifierInterface { + + constructor(pointer: Pointer) { + this.pointer = pointer + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + + /** + * This constructor can be used to instantiate a fake object. Only used for tests. Any + * attempt to actually use an object constructed this way will fail as there is no + * connected Rust object. + */ + @Suppress("UNUSED_PARAMETER") + constructor(noPointer: NoPointer) { + this.pointer = null + this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer)) + } + constructor(`publicJwk`: JwkData) : + this( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_constructor_secp256k1verifier_new( + FfiConverterTypeJwkData.lower(`publicJwk`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun callWithPointer(block: (ptr: Pointer) -> R): R { + // Check and increment the call counter, to keep the object alive. + // This needs a compare-and-set retry loop in case of concurrent updates. + do { + val c = this.callCounter.get() + if (c == 0L) { + throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed") + } + if (c == Long.MAX_VALUE) { + throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow") + } + } while (! this.callCounter.compareAndSet(c, c + 1L)) + // Now we can safely do the method call without the pointer being freed concurrently. + try { + return block(this.uniffiClonePointer()) + } finally { + // This decrement always matches the increment we performed above. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + // Use a static inner class instead of a closure so as not to accidentally + // capture `this` as part of the cleanable's action. + private class UniffiCleanAction(private val pointer: Pointer?) : Runnable { + override fun run() { + pointer?.let { ptr -> + uniffiRustCall { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_free_secp256k1verifier(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_clone_secp256k1verifier(pointer!!, status) + } + } + + + @Throws(Web5Exception::class)override fun `verify`(`message`: kotlin.ByteArray, `signature`: kotlin.ByteArray) + = + callWithPointer { + uniffiRustCallWithError(Web5Exception) { _status -> + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_secp256k1verifier_verify( + it, FfiConverterByteArray.lower(`message`),FfiConverterByteArray.lower(`signature`),_status) +} + } + + + + + + + + companion object + +} + +public object FfiConverterTypeSecp256k1Verifier: FfiConverter { + + override fun lower(value: Secp256k1Verifier): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): Secp256k1Verifier { + return Secp256k1Verifier(value) + } + + override fun read(buf: ByteBuffer): Secp256k1Verifier { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: Secp256k1Verifier) = 8UL + + override fun write(value: Secp256k1Verifier, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` and the +// Kotlin Pointer to work with the live Rust struct on the other side of the FFI. +// +// There's some subtlety here, because we have to be careful not to operate on a Rust +// struct after it has been dropped, and because we must expose a public API for freeing +// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are: +// +// * Each instance holds an opaque pointer to the underlying Rust struct. +// Method calls need to read this pointer from the object's state and pass it in to +// the Rust FFI. +// +// * When an instance is no longer needed, its pointer should be passed to a +// special destructor function provided by the Rust FFI, which will drop the +// underlying Rust struct. +// +// * Given an instance, calling code is expected to call the special +// `destroy` method in order to free it after use, either by calling it explicitly +// or by using a higher-level helper like the `use` method. Failing to do so risks +// leaking the underlying Rust struct. +// +// * We can't assume that calling code will do the right thing, and must be prepared +// to handle Kotlin method calls executing concurrently with or even after a call to +// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`. +// +// * We must never allow Rust code to operate on the underlying Rust struct after +// the destructor has been called, and must never call the destructor more than once. +// Doing so may trigger memory unsafety. +// +// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner` +// is implemented to call the destructor when the Kotlin object becomes unreachable. +// This is done in a background thread. This is not a panacea, and client code should be aware that +// 1. the thread may starve if some there are objects that have poorly performing +// `drop` methods or do significant work in their `drop` methods. +// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`, +// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html). +// +// If we try to implement this with mutual exclusion on access to the pointer, there is the +// possibility of a race between a method call and a concurrent call to `destroy`: +// +// * Thread A starts a method call, reads the value of the pointer, but is interrupted +// before it can pass the pointer over the FFI to Rust. +// * Thread B calls `destroy` and frees the underlying Rust struct. +// * Thread A resumes, passing the already-read pointer value to Rust and triggering +// a use-after-free. +// +// One possible solution would be to use a `ReadWriteLock`, with each method call taking +// a read lock (and thus allowed to run concurrently) and the special `destroy` method +// taking a write lock (and thus blocking on live method calls). However, we aim not to +// generate methods with any hidden blocking semantics, and a `destroy` method that might +// block if called incorrectly seems to meet that bar. +// +// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track +// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy` +// has been called. These are updated according to the following rules: +// +// * The initial value of the counter is 1, indicating a live object with no in-flight calls. +// The initial value for the flag is false. +// +// * At the start of each method call, we atomically check the counter. +// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted. +// If it is nonzero them we atomically increment it by 1 and proceed with the method call. +// +// * At the end of each method call, we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// * When `destroy` is called, we atomically flip the flag from false to true. +// If the flag was already true we silently fail. +// Otherwise we atomically decrement and check the counter. +// If it has reached zero then we destroy the underlying Rust struct. +// +// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc` works, +// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`. +// +// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been +// called *and* all in-flight method calls have completed, avoiding violating any of the expectations +// of the underlying Rust code. +// +// This makes a cleaner a better alternative to _not_ calling `destroy()` as +// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop` +// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner +// thread may be starved, and the app will leak memory. +// +// In this case, `destroy`ing manually may be a better solution. +// +// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects +// with Rust peers are reclaimed: +// +// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen: +// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then: +// 3. The memory is reclaimed when the process terminates. +// +// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219 +// + + public interface Signer { fun `sign`(`payload`: kotlin.ByteArray): kotlin.ByteArray @@ -8076,5 +8612,14 @@ public object FfiConverterMapStringString: FfiConverterRustBuffer + UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_func_secp256k1_generator_generate( + _status) +} + ) + } + diff --git a/bound/kt/src/test/kotlin/web5/sdk/crypto/Secp256k1GeneratorTest.kt b/bound/kt/src/test/kotlin/web5/sdk/crypto/Secp256k1GeneratorTest.kt new file mode 100644 index 00000000..8ceeeb8b --- /dev/null +++ b/bound/kt/src/test/kotlin/web5/sdk/crypto/Secp256k1GeneratorTest.kt @@ -0,0 +1,67 @@ +package web5.sdk.crypto + +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.fail +import java.util.Base64 +import web5.sdk.UnitTestSuite + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class Secp256k1GeneratorTest { + + private val testSuite = UnitTestSuite("secp256k1_generate") + + @AfterAll + fun verifyAllTestsIncluded() { + if (testSuite.tests.isNotEmpty()) { + println("The following tests were not included or executed:") + testSuite.tests.forEach { println(it) } + fail("Not all tests were executed! ${testSuite.tests}") + } + } + + @Test + fun test_must_set_alg() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + assertEquals("ES256K", jwk.alg) + } + + @Test + fun test_must_set_kty() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + assertEquals("EC", jwk.kty) + } + + @Test + fun test_must_set_crv() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + assertEquals("secp256k1", jwk.crv) + } + + @Test + fun test_must_set_public_key_with_correct_length() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + val xBytes = Base64.getUrlDecoder().decode(jwk.x) + val yBytes = jwk.y?.let { Base64.getUrlDecoder().decode(it) } ?: fail("y coordinate is missing") + assertEquals(32, xBytes.size) + assertEquals(32, yBytes.size) + } + + @Test + fun test_must_set_private_key_with_correct_length() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + val privateKeyBytes = jwk.d ?: fail("Private key is missing") + val decodedPrivateKeyBytes = Base64.getUrlDecoder().decode(privateKeyBytes) + assertEquals(32, decodedPrivateKeyBytes.size) + } +} \ No newline at end of file diff --git a/bound/kt/src/test/kotlin/web5/sdk/crypto/signers/Secp256k1SignerTest.kt b/bound/kt/src/test/kotlin/web5/sdk/crypto/signers/Secp256k1SignerTest.kt new file mode 100644 index 00000000..a0d2e0f3 --- /dev/null +++ b/bound/kt/src/test/kotlin/web5/sdk/crypto/signers/Secp256k1SignerTest.kt @@ -0,0 +1,77 @@ +package web5.sdk.crypto.signers + +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.fail +import web5.sdk.UnitTestSuite +import web5.sdk.crypto.Secp256k1Generator +import web5.sdk.Web5Exception +import java.util.Base64 + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class Secp256k1SignerTest { + private val testSuite = UnitTestSuite("secp256k1_sign") + + @AfterAll + fun verifyAllTestsIncluded() { + if (testSuite.tests.isNotEmpty()) { + println("The following tests were not included or executed:") + testSuite.tests.forEach { println(it) } + fail("Not all tests were executed! ${testSuite.tests}") + } + } + + @Test + fun test_with_valid_key() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + val signer = Secp256k1Signer(jwk) + + val message = "Test message".toByteArray() + + assertDoesNotThrow { + val signature = signer.sign(message) + assertEquals(SIGNATURE_LENGTH, signature.size, "Signature length should match the expected Secp256k1 signature length") + } + } + + @Test + fun test_with_invalid_private_key() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + val invalidJwk = jwk.copy(d = Base64.getUrlEncoder().withoutPadding().encodeToString(ByteArray(SECRET_KEY_LENGTH - 1))) + + val signer = Secp256k1Signer(invalidJwk) + val message = "Test message".toByteArray() + val exception = assertThrows { + signer.sign(message) + } + + assertEquals("cryptography error invalid private key", exception.message) + assertEquals("Crypto", exception.variant) + } + + @Test + fun test_with_missing_private_key() { + testSuite.include() + + val jwk = Secp256k1Generator.generate() + val missingKeyJwk = jwk.copy(d = null) + + val signer = Secp256k1Signer(missingKeyJwk) + val message = "Test message".toByteArray() + val exception = assertThrows { + signer.sign(message) + } + + assertEquals("cryptography error private key material must be set", exception.message) + assertEquals("Crypto", exception.variant) + } + + companion object { + const val SIGNATURE_LENGTH = 64 // Expected length for Secp256k1 signature (r + s, each 32 bytes) + const val SECRET_KEY_LENGTH = 32 // Secp256k1 private key length in bytes + } +} \ No newline at end of file diff --git a/bound/kt/src/test/kotlin/web5/sdk/crypto/verifiers/Secp256k1Verifier.kt b/bound/kt/src/test/kotlin/web5/sdk/crypto/verifiers/Secp256k1Verifier.kt new file mode 100644 index 00000000..e91b4169 --- /dev/null +++ b/bound/kt/src/test/kotlin/web5/sdk/crypto/verifiers/Secp256k1Verifier.kt @@ -0,0 +1,133 @@ +package web5.sdk.crypto.verifiers + +import org.junit.jupiter.api.* +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.fail +import web5.sdk.UnitTestSuite +import web5.sdk.crypto.Secp256k1Generator +import web5.sdk.crypto.keys.Jwk +import web5.sdk.crypto.signers.Secp256k1Signer +import web5.sdk.Web5Exception +import java.util.Base64 + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class Secp256k1VerifierTest { + private val testSuite = UnitTestSuite("secp256k1_verify") + + @AfterAll + fun verifyAllTestsIncluded() { + if (testSuite.tests.isNotEmpty()) { + println("The following tests were not included or executed:") + testSuite.tests.forEach { println(it) } + fail("Not all tests were executed! ${testSuite.tests}") + } + } + + private fun generateKeys(): Pair { + val privateJwk = Secp256k1Generator.generate() + val publicJwk = privateJwk.copy(d = null) + return Pair(publicJwk, privateJwk) + } + + @Test + fun test_with_valid_signature() { + testSuite.include() + + val (publicJwk, privateJwk) = generateKeys() + val signer = Secp256k1Signer(privateJwk) + val verifier = Secp256k1Verifier(publicJwk) + + val message = "Test message".toByteArray() + val signature = signer.sign(message) + + val verifyResult = runCatching { verifier.verify(message, signature) } + + assertTrue(verifyResult.isSuccess, "Verification should succeed with a valid signature") + } + + @Test + fun test_with_private_key() { + testSuite.include() + + val (_, privateJwk) = generateKeys() + val verifier = Secp256k1Verifier(privateJwk) // this is not allowed + + val message = "Test message".toByteArray() + val invalidSignature = ByteArray(SIGNATURE_LENGTH) // Invalid length, but valid shape + + val exception = assertThrows { + verifier.verify(message, invalidSignature) + } + + assertEquals("cryptography error provided verification key cannot contain private key material", exception.message) + assertEquals("Crypto", exception.variant) + } + + @Test + fun test_with_invalid_signature() { + testSuite.include() + + val (publicJwk, privateJwk) = generateKeys() + val signer = Secp256k1Signer(privateJwk) + val verifier = Secp256k1Verifier(publicJwk) + + val message = "Test message".toByteArray() + + // Create a valid signature and mutate the last byte + val validSignature = signer.sign(message).toMutableList() + validSignature[validSignature.size - 1] = (validSignature.last().toInt() xor 0x01).toByte() // Flip the last bit + + val exception = assertThrows { + verifier.verify(message, validSignature.toByteArray()) + } + + assertEquals("cryptography error cryptographic verification failure", exception.message) + assertEquals("Crypto", exception.variant) + } + + @Test + fun test_with_invalid_public_key() { + testSuite.include() + + val (publicJwk, privateJwk) = generateKeys() + val invalidPublicJwk = publicJwk.copy( + x = Base64.getUrlEncoder().withoutPadding().encodeToString(ByteArray(PUBLIC_KEY_LENGTH - 1)) + ) + + val signer = Secp256k1Signer(privateJwk) + val verifier = Secp256k1Verifier(invalidPublicJwk) + + val message = "Test message".toByteArray() + val signature = signer.sign(message) + + val exception = assertThrows { + verifier.verify(message, signature) + } + + assertEquals("cryptography error unable to instantiate verifying key", exception.message) + assertEquals("Crypto", exception.variant) + } + + @Test + fun test_with_invalid_signature_length() { + testSuite.include() + + val (publicJwk, _) = generateKeys() + val verifier = Secp256k1Verifier(publicJwk) + + val message = "Test message".toByteArray() + val invalidSignature = ByteArray(SIGNATURE_LENGTH - 1) // Invalid length + + val exception = assertThrows { + verifier.verify(message, invalidSignature) + } + + assertEquals("cryptography error invalid signature", exception.message) + assertEquals("Crypto", exception.variant) + } + + companion object { + const val SIGNATURE_LENGTH = 64 // Secp256k1 signature length (r + s, each 32 bytes) + const val PUBLIC_KEY_LENGTH = 32 // Secp256k1 public key length in bytes + } +} \ No newline at end of file