深圳幻海软件技术有限公司 欢迎您!

使用WebAssembly对前端API请求进行签名

2023-02-28

背景server端处理WebAPI请求的安全问题:请求重放(eg.月饼抢购场景中,程序员通过脚本直接访问接口)参数篡改(eg.会话劫持场景中,将应该抢购到的月饼归属人改为自己)脚本攻击(eg.综合前两种场景,使用技术手段构建的请求进行攻击,如信息窃取,漏洞攻击)可信客户端请求(eg.以上所有场景根因

背景

server端处理WebAPI请求的安全问题:

  1. 请求重放 (eg. 月饼抢购场景中,程序员通过脚本直接访问接口)
  2. 参数篡改 (eg. 会话劫持场景中,将应该抢购到的月饼归属人改为自己)
  3. 脚本攻击 (eg. 综合前两种场景,使用技术手段构建的请求进行攻击,如信息窃取,漏洞攻击)
  4. 可信客户端请求 (eg. 以上所有场景根因均为访问客户端不可信并不可证伪)

解决方案

  1. 对请求参数+cnonce (客户端生成的一次性随机字符串) 进行hash签名
  2. 以secret作为盐值
  3. 将签名作为header值传递给server端
  4. server端在redis中查验是否已有重复签名,如有重复直接拒绝请求(防止请求重放)
  5. server端对签名值进行校验
  6. 校验通过之后将该签名值作为key值,存入redis

总体流程如下图所示:

代码示例

前端使用示例(TypeScript Vue3 版本):

<script setup> 
import { onMounted } from "vue"
import initWasm, {sign} from "./pkg/sign.js"; // 通过wasm-pack打包生成的二进制包的入口文件 
import { v4 as uuidv4 } from 'uuid'; // 此示例以生成的UUID作为cnonce随机字符串 
  
onMounted(async () => { 
    await initWasm() 
}) 
  
const sendRequest = () => { 
    const cnonce = uuidv4() 
    const params: EncryptedParams = { 
        name'John'
        age: 23, 
        breed: 'dog'
        ts: Date.now() 
    } 
    const wasmSignature = sign(JSON.stringify(params), cnonce); 
    ... 
    axios.post(something); 

  
</script> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

 

签名机制示例,server端接受到请求时,应该同时获得签名值以及cnonce一次性字符串,按照下面同样的签名顺序进行签名,比对前端传入的签名以及server端生成的签名进行校验:

const encryptedSign = (message: string, cnonce: string): string => { 
  const secret = 'XXXXXXX' // 该签名盐值可以自行生成,生成之后需要重新编译rust应用,生成新的wasm包 
  const hashDigest = sha256(`${cnonce}|${message}`) 
  const hmacDigest = Base64.stringify(hmacSHA512(hashDigest.toString().toUpperCase(), secret)) 
  return hmacDigest.toString().toUpperCase() 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

签名机制示例 (rust 版本): 

extern crate wasm_bindgen; 
  
use ring::hmac; 
use ring::digest::{Context, SHA256}; 
use data_encoding::BASE64; 
use data_encoding::HEXUPPER; 
use wasm_bindgen::prelude::*; 
  
#[wasm_bindgen] 
pub fn ron_weasley_sign (message: &str, cnonce: &str) -> String { 
    const SECRET: &str = std::env!("SECRET"); 
  
    let mut context = Context::new(&SHA256); 
    context.update(format!("{}|{}", cnonce, message).as_bytes()); 
    let sha256_result = context.finish(); 
    let sha256_result_str = format!("{}", HEXUPPER.encode(sha256_result.as_ref())); 
  
    let key = hmac::Key::new(hmac::HMAC_SHA512, SECRET.as_bytes()); 
    let mac = hmac::sign(&key, sha256_result_str.as_bytes()); 
    let b64_encoded_sig = BASE64.encode(mac.as_ref()); 
    return b64_encoded_sig.to_uppercase(); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

构建rust源代码,并生成对应的二进制包

首先在项目的github地址

https://github.com/swearer23/ron-weasley 下载源代码

之后按照README文件的步骤安装编译环境(以*nix环境为例)

安装cargo

由于我们使用cargo作为rust环境的管理器,所以第一步安装cargo

安装完成后在命令行输入cargo -v 查看是否安装成功

cargo -v # 可能需要重新启动终端 
Rust's package manager 
 
USAGE: 
    cargo [+toolchain] [OPTIONS] [SUBCOMMAND] 
 
OPTIONS: 
    -V, --version                  Print version info and exit 
        --list                     List installed commands 
        --explain <CODE>           Run `rustc --explain CODE` 
    -v, --verbose                  Use verbose output (-vv very verbose/build.rs output) 
    -q, --quiet                    No output printed to stdout 
        --color <WHEN>             Coloring: auto, always, never 
        --frozen                   Require Cargo.lock and cache are up to date 
        --locked                   Require Cargo.lock is up to date 
        --offline                  Run without accessing the network 
        --config <KEY=VALUE>...    Override a configuration value (unstable) 
    -Z <FLAG>...                   Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details 
    -h, --help                     Prints help information 
 
Some common cargo commands are (see all commands with --list): 
    build, b    Compile the current package 
    check, c    Analyze the current package and report errors, but don't build object files 
    clean       Remove the target directory 
    doc, d      Build this package's and its dependencies' documentation 
    new         Create a new cargo package 
    init        Create a new cargo package in an existing directory 
    run, r      Run a binary or example of the local package 
    test, t     Run the tests 
    bench       Run the benchmarks 
    update      Update dependencies listed in Cargo.lock 
    search      Search registry for crates 
    publish     Package and upload this package to the registry 
    install     Install a Rust binaryDefault location is $HOME/.cargo/bin 
    uninstall   Uninstall a Rust binary 
 
See 'cargo help <command>' for more information on a specific command. 
  • 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.

安装wasm-pack

要构建二进制包,需要一个额外工具 wasm-pack。它会帮助我们把代码编译成 WebAssembly 并构建出适用于web环境的wasm包。使用下面的命令可以下载并安装:

cargo install wasm-pack 
  • 1.

编译wasm

wasm-pack安装成功后,执行下面的命令以编译wasm包

SECRET= wasm-pack build --target=web --release 
  • 1.

<your-secret>替换为你的签名盐值

第一次构建和编译时间会比较长,需要下载依赖的rust库并编译,请耐心等待

如果速度仍然很慢,建议更换cargo国内源

更换 cargo 源

在你的cargo文件夹下新建 config 文件

macos中,文件夹地址在 ~/.cargo 

cd ~/.cargo 
touch config 
  • 1.
  • 2.

然后编辑config文件,添加如下内容: 

[source.crates-io] 
registry = "https://github.com/rust-lang/crates.io-index" 
replace-with = 'ustc' 
[source.ustc] 
registry = "git://mirrors.ustc.edu.cn/crates.io-index" 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

即可更换为ustc的源

集成

执行wasm-pack命令打包会得到一个名为pkg的文件夹,位于项目的根目录下

将其放入要使用的前端项目中,即可以像上面代码示例章节所描述的方式进行集成和调用

附录

  • rust 项目地址:https://github.com/swearer23/ron-weasley
  • vue3 调用方式示例项目地址:https://github.com/swearer23/harry-porter (内含有secret=123456的wasm包,可以用于示例)