Rust の csrf クレートの機能を Deno で使う

前回記事で Rust を使って WebAssembly を作りましたが、実はこれが目的でした。
deno 向けパッケージでちょうどいいものが無かったので、Rust の csrf クレートを使えないかと考えていたわけです。

いろいろ試して実現できたので、メモがてら書いていきます。

参考

やろうとしたこと

  • Rust の csrf パッケージを使って、WASM をコンパイル[1]
  • deno で、作った WASM を使って CSRF 用のトークンの発行/検証を行う[2]

実行環境

今回は WASM を作る Rust 側と実行する Deno 側をまとめて作ります。

docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "3"
services:
rust-app:
build:
context: .
dockerfile: RustDockerfile
volumes:
- .:/app:cached
tty: true
deno-app:
build:
context: .
dockerfile: DenoDockerfile
privileged: true
entrypoint:
- /sbin/init
volumes:
- .:/usr/src/app:cached
tty: true

以下それぞれの Dockerfile です。

RustDockerfile
1
2
3
4
FROM rust:latest

RUN mkdir /app
WORKDIR /app
DenoDockerfile
1
2
3
4
5
6
FROM denoland/deno:centos

RUN mkdir /usr/src/app
WORKDIR /usr/src/app

EXPOSE 8080

WASM を作る

コンパイル用スクリプト

今回、コマンドを度々打ちたくなかったので、コンパイル用スクリプトとして以下のものを用意しました。

build.sh
1
2
3
4
5
6
7
8
9
#!/bin/sh
# Deno 向け WASM コンパイルスクリプト

cargo install wasm-bindgen-cli
rustup target add wasm32-unknown-unknown
cargo build --lib --target wasm32-unknown-unknown
# リリースビルドの時は --release を入れる
# cargo build --lib --release --target wasm32-unknown-unknown
wasm-bindgen --target deno target/wasm32-unknown-unknown/debug/csrf_wasm.wasm --out-dir ./pkg

以前の記事では wasm-pack コマンドでコンパイルして一部書き換えをしましたが、本来こちらが正しいようです。

WASM 本体実装

WASM の実装は以下の通りです。

src/lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// rust-csrf を deno から使うための WASM 実装

use wasm_bindgen::prelude::*;
use csrf::{AesGcmCsrfProtection, CsrfProtection};
use data_encoding::BASE64;
extern crate console_error_panic_hook;
use serde::{Serialize};

// JavaScript のオブジェクト定義
#[derive(Serialize)]
struct TokenPair {
token_str: String,
cookie_str: String,
}

// JavaScript のオブジェクト形式で返り値を作成する
fn js_token_pair_builder(values: &[&str]) -> JsValue {
let myobj = TokenPair {
token_str: values[0].to_string(),
cookie_str: values[1].to_string()
};
JsValue::from_serde(&myobj).unwrap()
}

// &[u8] を[u8; 32] に力技で変換
fn convert_u8_u832(src: &[u8] ) -> [u8; 32] {
let mut res = [0; 32];
for i in 0..31 {
res[i] = src[i]
}
return res
}

// AesGcmCsrfProtection インスタンスを返す
fn protect_instance(aead_key: String) -> AesGcmCsrfProtection{
console_error_panic_hook::set_once();
let s = convert_u8_u832(aead_key.as_str().as_bytes());
return AesGcmCsrfProtection::from_key(s);
}

// トークンペア(トークン+cookie)を返す
#[wasm_bindgen (js_name = generateTokenPair)]
pub fn generate_token_pair(aead_key: String, ttl_seconds: i32) -> JsValue {
console_error_panic_hook::set_once();
let protect = protect_instance(aead_key);

let (token, cookie) = protect.generate_token_pair(None, ttl_seconds as i64).expect("couldn't generate token/cookie pair");
return js_token_pair_builder(&[&token.b64_string(), &cookie.b64_string()])
}

// トークンペア(トークン+cookie)を検証する
#[wasm_bindgen (js_name = verifyTokenPair)]
pub fn verify_token_pair(aead_key: String, token_str: String, cookie_str: String) -> bool {
console_error_panic_hook::set_once();
let protect = protect_instance(aead_key);

let token_bytes = BASE64.decode(token_str.as_bytes()).expect("token not base64");
let cookie_bytes = BASE64.decode(cookie_str.as_bytes()).expect("cookie not base64");

let parsed_token = protect.parse_token(&token_bytes).expect("token not parsed");
let parsed_cookie = protect.parse_cookie(&cookie_bytes).expect("cookie not parsed");

protect.verify_token_pair(&parsed_token, &parsed_cookie)
}

Cargo.toml

実は Cargo.toml の記述を調べるのが一番大変でした。

Cargo.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[package]
name = "csrf-wasm"
version = "0.1.0"
edition = "2018"

[dependencies]
wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"]}
csrf = "0.4.0"
data-encoding = "2.2.0"
console_error_panic_hook = "0.1.6"

# csrf が参照している rand の wasm-bindgen 向け機能を使う
rand = { version = "0.7.3", features = ["wasm-bindgen"] }

# csrf が参照している chrono を wasmbind 向け機能を使う
chrono = {version = "0.4.19", default-features = false, features = ["wasmbind"]}
serde = { version = "1.0", features = ["derive"] }

[lib]
crate-type = ["cdylib", "rlib"]

時刻や乱数など、一部 OS が提供するようなリソース/機能を WASM から呼ぶときには、features = ~~~ など詳細な指定が必要でした。

コンパイル

用意していたコマンドで、コンパイルします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec rust-app bash
$ chmod +x ./build.sh
$ ./build.sh

# => ./pkg 以下に以下のファイルが作成されます。
$ ls -la ./pkg
total 1561
drwxr-xr-x 1 root root 512 Jul 22 17:47 .
drwxrwxrwx 1 root root 512 Jul 23 13:50 ..
-rw-r--r-- 1 root root 413 Jul 23 14:10 csrf_wasm.d.ts
-rw-r--r-- 1 root root 8042 Jul 23 14:10 csrf_wasm.js
-rw-r--r-- 1 root root 1588140 Jul 23 14:10 csrf_wasm_bg.wasm
-rw-r--r-- 1 root root 513 Jul 23 14:10 csrf_wasm_bg.wasm.d.ts

Deno で実行

作成した csrf トークン発行/検証機能を持つ WASM を使う deno のコードが次の通りです。

use_wasm.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { generateTokenPair, verifyTokenPair } from "./pkg/csrf_wasm.js";

let key = "01234567012345670123456701234567";
let { token_str, cookie_str } = generateTokenPair(key, 123);

console.log(token_str);
console.log(cookie_str);

// トークンを検証=>成功
console.log(verifyTokenPair(key, token_str, `${cookie_str}`));

// トークンを検証=>失敗
console.log(
verifyTokenPair(key, `EfLKlILzGj64ekLYDXx~~省略~~59PqpowUg==`, cookie_str)
);

実行すると次の通りです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# コンテナは起動済み
$ docker-compose exec deno-app bash
$ ls -la
total 33
drwxrwxrwx 1 root root 512 Jul 23 14:35 .
drwxr-xr-x 1 root root 4096 Jul 23 14:31 ..
-rw-r--r-- 1 root root 15872 Jul 23 13:59 Cargo.lock
-rw-r--r-- 1 root root 628 Jul 23 13:58 Cargo.toml
-rwxrwxrwx 1 root root 75 Jul 23 14:31 DenoDockerfile
-rwxrwxrwx 1 root root 50 Jul 23 14:29 RustDockerfile
-rwxrwxrwx 1 root root 866 Jul 23 14:14 build.sh
-rwxrwxrwx 1 root root 330 Jul 23 14:31 docker-compose.yml
drwxrwxrwx 1 root root 512 Jul 22 18:38 libs
drwxr-xr-x 1 root root 512 Jul 22 17:47 pkg
drwxr-xr-x 1 root root 512 Jul 23 13:31 src
drwxr-xr-x 1 root root 512 Jul 23 14:14 target
-rwxrwxrwx 1 root root 522 Jul 23 14:36 use_wasm.ts

$ deno run --allow-read use_wasm.ts
oRTIqot~~省略~~VR3c8VeFksK1p4OYfgU6JUFpIub8wCg==
NvXtA/Y~~省略~~oIEDcA+A5rU3RFI4ozgVTfaEgXUjounpB
true
false

トークンの発行と検証ができました。
実は、BASE64 デコードの失敗時などの対応が取られておらず、クラッシュすることが度々起こります。
TypeScript 側での例外処理が必要なコードになっていますが、とりあえず「作れた」ということが収穫です。


今回は、WASM を使って deno で CSRF の検証機能を実装しました。
Rust 周りの調査を始めると、どうにも日本語情報が多くなく、実働でここまでに 4 日程かかってしまいました。
ただ、ノウハウとしてかなり身についたところを感じているので、かなり収穫だったと感じています。

近日こちらのライブラリは、deno.land で公開する予定です。
公開しました。
github - Octo8080/deno-csrf

deno.land/x/deno_csrf@0.0.5

ではでは。