diff --git a/native/native_rust_library/Cargo.lock b/native/native_rust_library/Cargo.lock
--- a/native/native_rust_library/Cargo.lock
+++ b/native/native_rust_library/Cargo.lock
@@ -766,6 +766,8 @@
  "prost",
  "rand",
  "regex",
+ "serde",
+ "serde_json",
  "tokio",
  "tokio-stream",
  "tonic",
@@ -1062,6 +1064,12 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
 
+[[package]]
+name = "ryu"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
 [[package]]
 name = "scratch"
 version = "1.0.2"
@@ -1107,6 +1115,17 @@
  "syn 1.0.99",
 ]
 
+[[package]]
+name = "serde_json"
+version = "1.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
 [[package]]
 name = "sha2"
 version = "0.10.6"
diff --git a/native/native_rust_library/Cargo.toml b/native/native_rust_library/Cargo.toml
--- a/native/native_rust_library/Cargo.toml
+++ b/native/native_rust_library/Cargo.toml
@@ -19,6 +19,8 @@
 regex = "1.6"
 comm-opaque2 = {path = "../../shared/comm-opaque2"}
 derive_more = "0.99"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
 
 [build-dependencies]
 cxx-build = "1.0"
diff --git a/native/native_rust_library/src/identity_client.rs b/native/native_rust_library/src/identity_client.rs
--- a/native/native_rust_library/src/identity_client.rs
+++ b/native/native_rust_library/src/identity_client.rs
@@ -2,22 +2,6 @@
 
 use crate::IdentityClient;
 
-pub async fn register_user(
-  mut _client: Box<IdentityClient>,
-  _username: String,
-  _password: String,
-  _key_payload: String,
-  _key_payload_signature: String,
-  _identity_prekey: String,
-  _identity_prekey_signature: String,
-  _notif_prekey: String,
-  _notif_prekey_signature: String,
-  _identity_onetime_keys: Vec<String>,
-  _notif_onetime_keys: Vec<String>,
-) -> Result<String, Status> {
-  unimplemented!();
-}
-
 // User could be logging in from new device, need to resend device information
 pub async fn login_user_pake(
   mut _client: Box<IdentityClient>,
diff --git a/native/native_rust_library/src/lib.rs b/native/native_rust_library/src/lib.rs
--- a/native/native_rust_library/src/lib.rs
+++ b/native/native_rust_library/src/lib.rs
@@ -1,6 +1,9 @@
 use crate::ffi::string_callback;
 use crate::identity::Empty;
+use comm_opaque2::client::Registration;
+use comm_opaque2::grpc::opaque_error_to_grpc_status as handle_error;
 use lazy_static::lazy_static;
+use serde::Serialize;
 use std::sync::Arc;
 use tokio::runtime::{Builder, Runtime};
 use tonic::{transport::Channel, Status};
@@ -14,6 +17,10 @@
 
 use crypto_tools::generate_device_id;
 use identity::identity_client_service_client::IdentityClientServiceClient;
+use identity::{
+  DeviceKeyUpload, IdentityKeyInfo, PreKey, RegistrationFinishRequest,
+  RegistrationStartRequest,
+};
 
 lazy_static! {
   pub static ref RUNTIME: Arc<Runtime> = Arc::new(
@@ -42,9 +49,8 @@
     #[cxx_name = "identityInitializeClient"]
     fn initialize_identity_client(addr: String) -> Box<IdentityClient>;
 
-    #[cxx_name = "identityRegisterUserBlocking"]
-    fn identity_register_user_blocking(
-      client: Box<IdentityClient>,
+    #[cxx_name = "registerUser"]
+    fn register_user(
       username: String,
       password: String,
       key_payload: String,
@@ -55,7 +61,8 @@
       notif_prekey_signature: String,
       identity_onetime_keys: Vec<String>,
       notif_onetime_keys: Vec<String>,
-    ) -> Result<String>;
+      promise_id: u32,
+    );
 
     #[cxx_name = "identityLoginUserPakeBlocking"]
     fn identity_login_user_pake_blocking(
@@ -146,8 +153,7 @@
 }
 
 #[instrument]
-fn identity_register_user_blocking(
-  client: Box<IdentityClient>,
+fn register_user(
   username: String,
   password: String,
   key_payload: String,
@@ -158,20 +164,101 @@
   notif_prekey_signature: String,
   identity_onetime_keys: Vec<String>,
   notif_onetime_keys: Vec<String>,
-) -> Result<String, Status> {
-  RUNTIME.block_on(identity_client::register_user(
-    client,
-    username,
-    password,
-    key_payload,
-    key_payload_signature,
-    identity_prekey,
-    identity_prekey_signature,
-    notif_prekey,
-    notif_prekey_signature,
-    identity_onetime_keys,
-    notif_onetime_keys,
-  ))
+  promise_id: u32,
+) {
+  RUNTIME.spawn(async move {
+    let register_user_info = RegisterUserInfo {
+      username,
+      password,
+      key_payload,
+      key_payload_signature,
+      identity_prekey,
+      identity_prekey_signature,
+      notif_prekey,
+      notif_prekey_signature,
+      identity_onetime_keys,
+      notif_onetime_keys,
+    };
+    let result = register_user_helper(register_user_info).await;
+    handle_string_result_as_callback(result, promise_id);
+  });
+}
+
+struct RegisterUserInfo {
+  username: String,
+  password: String,
+  key_payload: String,
+  key_payload_signature: String,
+  identity_prekey: String,
+  identity_prekey_signature: String,
+  notif_prekey: String,
+  notif_prekey_signature: String,
+  identity_onetime_keys: Vec<String>,
+  notif_onetime_keys: Vec<String>,
+}
+
+#[derive(Serialize)]
+struct UserIDAndDeviceAccessToken {
+  user_id: String,
+  access_token: String,
+}
+
+async fn register_user_helper(
+  register_user_info: RegisterUserInfo,
+) -> Result<String, Error> {
+  let mut client_registration = Registration::new();
+  let opaque_registration_request = client_registration
+    .start(&register_user_info.password)
+    .map_err(handle_error)?;
+  let registration_start_request = RegistrationStartRequest {
+    opaque_registration_request,
+    username: register_user_info.username,
+    device_key_upload: Some(DeviceKeyUpload {
+      device_key_info: Some(IdentityKeyInfo {
+        payload: register_user_info.key_payload,
+        payload_signature: register_user_info.key_payload_signature,
+        social_proof: None,
+      }),
+      content_upload: Some(identity::PreKey {
+        pre_key: register_user_info.identity_prekey,
+        pre_key_signature: register_user_info.identity_prekey_signature,
+      }),
+      notif_upload: Some(PreKey {
+        pre_key: register_user_info.notif_prekey,
+        pre_key_signature: register_user_info.notif_prekey_signature,
+      }),
+      onetime_content_prekeys: register_user_info.identity_onetime_keys,
+      onetime_notif_prekeys: register_user_info.notif_onetime_keys,
+    }),
+  };
+
+  let mut identity_client =
+    IdentityClientServiceClient::connect("http://127.0.0.1:50054").await?;
+  let registration_start_response = identity_client
+    .register_password_user_start(registration_start_request)
+    .await?
+    .into_inner();
+
+  let opaque_registration_upload = client_registration
+    .finish(
+      &register_user_info.password,
+      &registration_start_response.opaque_registration_response,
+    )
+    .map_err(handle_error)?;
+  let registration_finish_request = RegistrationFinishRequest {
+    session_id: registration_start_response.session_id,
+    opaque_registration_upload,
+  };
+
+  let registration_finish_response = identity_client
+    .register_password_user_finish(registration_finish_request)
+    .await?
+    .into_inner();
+  let user_id_and_access_token = UserIDAndDeviceAccessToken {
+    user_id: registration_finish_response.user_id,
+    access_token: registration_finish_response.access_token,
+  };
+  Ok(serde_json::to_string(&user_id_and_access_token)?)
 }
 
 #[instrument]
@@ -240,4 +327,6 @@
   TonicGRPC(Status),
   #[display(...)]
   TonicTransport(tonic::transport::Error),
+  #[display(...)]
+  SerdeJson(serde_json::Error),
 }