Ritesh's Blog

Adventures Wasm: Part One

Adventures in WebAssembly Land

This is Part 1 of my notes on trying out WASM for a non-trivial task. Stay tuned for more fun.

I’ve been on the hype-train again, and the flavour of the month (year? decade?) is Web Assembly. During breaks between the job that I’m paid to do, I like to keep up to date on new things and sharpen my technical skills to keep up with the clever people on my team. In this post you can follow my adventures in learning about an unfamiliar tech stack.

Web Assembly, in a nutshell, allows you to use your favourite programming language to write code that can target web-browsers by compiling it into a WASM blob. Your code in Go/Rust/Python goes in -> magic compilation step* -> a WASM binary comes out. The compilation step converts your code into instructions for a stack-machine, which can be run either in your browser or in a non-browser context.

If you’re curious as to what it looks like when compiled for the web here’s an excerpt from a WASM file. Looks a bit like x86 assembly or decompiled Java, no?

 1 global.get $global0
 2    local.set $var6
 3    i32.const 2464
 4    local.set $var7
 5    local.get $var6
 6    local.get $var7
 7    i32.sub
 8    local.set $var8
 9    local.get $var8
10    global.set $global0
11    local.get $var8
12    local.get $var0
13    i32.store offset=2380
14    local.get $var8
15    local.get $var1
16    i32.store offset=2384
17    local.get $var8
18    local.get $var2
19    i32.store offset=2388
20    local.get $var8
21    local.get $var3
22    i32.store offset=2392
23    local.get $var8
24    local.get $var4
25    i32.store offset=2396
26    i32.const 216
27    local.set $var9
28    local.get $var8
29    local.get $var9
30    i32.add
31    local.set $var10
32    local.get $var10
33    local.get $var3
34    local.get $var4
35    call $func2957

WASM opens up a whole new world of possibilities, limited only by the constraints of the stack-machine that runs WebAssembly code. Most of these constraints are security related (e.g. disallowing direct file access), which make sense since with WASM you’re running a blob of inscrutable code that could potentially harm your computer.

The most obvious use cases that come to mind are to implement code that is performance sensitive in your favourite performant programming language and then compiling that to WASM. You would then have a JavaScript shim to call that code. If you like Rust, this is a good place to learn how it all fits together

The reference implementation for WASM is written in Rust so writing code in Rust seemed like a natural place to start. To learn more about how interop works between WASM and JavaScript, I decided (foolishly) to try and implement a WebAuthn client in WASM. You can do this quite easily in pure JavaScript/TypeScript - but where’s the fun in that? WebAuthentication (and Apple’s PassKeys) allow you to use a security key or another authenticator like TouchID or FaceID in the place of a password to log-in to websites.

Side Note: You can learn more about Web Authentication on this slick website.

Bootstrapping

I’ve given myself a problem, where do I start? My end goal is to get a WebAuthn flow running but we are only allowed to use Rust to generate the WASM we need. Users should be able to enroll security keys and then subsequently authenticate using these security keys. We need a project structure that allows us to write Rust code and automagically generate WASM and run it in a browser so that I can test stuff as I write it.

I decided to go with a Rust framework called Yew to build this project. Yew seems similar to React in that you create components and then define lifecycle functions (like on_create, update etc). You can send messages to components based on actions in the web UI so we can use things like buttons/text fields etc. to capture user input and send it to rust code.

There’s a nice little starter template that’s been created by the Yew folks that you can clone and instantly start hacking on. Note that this template doesn’t make use of wasm-pack directly and I’d never heard of trunk, the tool used to bundle/build the application (similar to what npm run start would give you in JS land). Using this template I began hacking on a crude WebAuthn implementation.

To make life more difficult interesting, I decided to use only terminal based editors (vim/emacs) to write code for this. If this was an actual project, I’d reach for VSCode because it works really well for all kinds of non-trivial projects.

Fighting with Types

Rust is a strongly-typed language and forces you to think about the types you will use. JavaScript is not very fussy about this and the Rust representation of the main JavaScript types are JsValue and Objectwhich can pretty much represent most things things in JavaScript.

For the majority of the code, I’m struggling with figuring out how to coerce a Rust type into a shape that I can use to call the JavaScript (or more specifically web_sys) APIs. I am both a terrible Rust and JavaScript programmer :D

For example, the TypeScript/JavaScript code to enrol credentials, via this lovely post:

 1var createOptions : CredentialCreationOptions = {
 2  publicKey: {
 3    rp: {
 4      // The RP ID. This needs some thought. See comments below.
 5      id: SEE_BELOW,
 6      // This field is required to be set to something, but you can
 7      // ignore it.
 8      name: "",
 9    },
10
11    user: {
12      // `userIdBase64` is the user's passkey ID, from the database,
13      // base64-encoded.
14      id: Uint8Array.from(atob(userIdBase64), c => c.charCodeAt(0)),
15      // `username` is the user's username. Whatever they would type
16      // when signing in with a password.
17      name: username,
18      // `displayName` can be a more human name for the user, or
19      // just leave it blank.
20      displayName: "",
21    },
22
23   // .... boiler plate removed
24  }
25};
26
27//The magic happens here
28navigator.credentials.create(createOptions).then(
29  handleCreation, handleCreationError);

You create a CredentialCreationOptions object which contains parameters that you’d like to use to enrol a new web authenticator. Then you call navigator.credentials.create(..) with these parameters to enrol the credentials.

The equivalent using web_sys in Rust

 1let pk1 = PubKeyAlg {
 2                r#type: "public-key".into(),
 3                alg: -7,
 4            };
 5            let pk2 = PubKeyAlg {
 6                r#type: "public-key".into(),
 7                alg: -257,
 8            };
 9            //Using this convoluted way to get from &[u8] to the type that the API expects
10            // TODO: Challenge bytes are not meant to be deterministic
11            let challenge_bytes = vec![1, 2, 3];
12            let challenge = Bytes::new(challenge_bytes.as_slice());
13            //overkill!
14            //Using this convoluted way to get from &[u8] to the type that the API expects
15            let user_id_bytes = vec![255];
16            let user_id = Bytes::new(user_id_bytes.as_slice());
17            let pubkey_opt = PublicKeyCredentialCreationOptions::new(
18                // challenge
19                &serde_wasm_bindgen::to_value(challenge).unwrap().into(),
20                //pub_key_cred_params:
21                &serde_wasm_bindgen::to_value(&vec![&pk1, &pk2]).unwrap(),
22                //rp
23                &PublicKeyCredentialRpEntity::new("foo"),
24                //user:
25                &PublicKeyCredentialUserEntity::new(
26                    "bar",
27                    "ritesh",
28                    &serde_wasm_bindgen::to_value(user_id).unwrap().into(),
29                ),
30            );
31            let mut copt = CredentialCreationOptions::new();
32            copt.public_key(&pubkey_opt);
33            let window = web_sys::window().expect("missing window");
34            let navigator = window.navigator();
35            if let Ok(p) = navigator.credentials().create_with_options(&copt) {
36                //WHy unused?
37                // How do we update state?
38                let cl = Closure::new(|_| self.records.); //incomplete
39                //TODO: Use this promise that is returned
40                let _ = p.then(&cl);
41                //See: https://github.com/rustwasm/wasm-bindgen/blob/main/examples/closures/src/lib.rs#L72-L82
42                // If you don't do this .forget() call, the code creates a DOMException - not sure I fully understand why yet
43                cl.forget();
44            } else {
45                console::log_1(&"booo".into())
46            }

Ignore the bits about closures for now - we are creating a CredentialCreationOptions struct which has a PublicKeyCredentialCreationOptions struct inside it. Both of these are autogenerated from JavaScript so the API is a little bit clunky. I’m having to jump through weird type conversions to get to the expected parameters required by these functions. Some of it is obvious, like converting a &str into a String type since &str is a borrowed type, and we want our struct to own the String. Some of it is bizarre, and I’ll have to figure out a better way. Like going from Vec<u8> to a serde_bytes::Bytes and then slicing it.

Fighting with Closures and Lifetimes

So coming back to Closures - why do I need one anyway? What I’m trying to do is implement the key enrolling process in Rust, and to trigger it the user clicks a button. When they click the “Enroll” button (optionally with a username or other data), the Rust code calls navigator.credentials.create() and tries to store the auth info in LocalStorage as a record. In a real system this would be a database somewhere.

When you call navigator.credentials.create() it gives you a Promise. Usually in JS you would do a somepromise.then(something else) the something else is a function that would be invoked if the promise resolved successfully. I’m trying to pass a Rust closure as something else so that I can update LocalStorage with the key enrollment.

Unfortunately, the limitation on the Closure that must be passed in is that its lifetime needs to be 'static . Recall that 'static is the longest possible lifetime in Rust - i.e. when something is annotated with 'static it is meant to live as long as the program is running.

If I were to pass in (move?) self into the closure, I cannot guarantee that self has a `‘static lifetime so it does not work.

Where next?

So far, I have the bare minimum working. I can take events from JS and pass them to my Rust code so that I can do something useful. Things to do go get this working properly

Not sure how far I’ll make it, but stay tuned!

#webassembly #rust #newbie

Reply to this post by email ↪