Helix-ex / tools

Utilities / tools
GNU General Public License v3.0
0 stars 1 forks source link

rust code that verifies an end user REST API request signature (`HM-SIGN` header) #1

Open al-maisan opened 1 year ago

al-maisan commented 1 year ago

assume the HM-SIGN header was computed as follows

import ecdsa
import time
import hmac
from requests import Request

ts = int(time.time() * 1000)
request = Request('GET', '<api_endpoint>')
prepared = request.prepare()
signature_payload = f'{ts}{prepared.method}{prepared.path_url}'
if prepared.body:
    signature_payload += prepared.body
signature_payload = signature_payload.encode()

sk = ecdsa.SigningKey.from_string(bytes.fromhex(PRIVATE_KEY), curve=ecdsa.SECP256k1)
signature = base64.b64encode(sk.sign(signature_payload))

request.headers['HM-KEY'] = PUBLIC_KEY
request.headers['HM-SIGN'] = signature
request.headers['HM-TS'] = str(ts)

write a rust function with the following signature and package it in a utils crate:

fn verify_sig(req: &HttpRequest) -> bool i.e. it

Please note: the HM-KEY header contains a bip32::PublicKey (33 bytes) in the following format:

03:b6:bd:48:8a:40:1d:e1:64:2f:5c:bf:b7:10:68:4a:df:3d:e0:70:de:93:e9:68:e4:15:c2:21:4d:f2:86:7f:65

codeesura commented 1 year ago
use actix_web::{HttpRequest, error::ErrorBadRequest};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use base64;
use chrono::prelude::*;
use bip32;

#[derive(Debug, PartialEq)]
pub struct PublicKey(pub [u8; 33]);

impl PublicKey {
    pub fn from_hex(hex: &str) -> Result<Self, bip32::Error> {
        let mut key = [0; 33];
        let mut key_iter = hex.split(':').map(|byte_str| u8::from_str_radix(byte_str, 16));

        for (i, byte) in key_iter.enumerate() {
            key[i] = byte?;
        }

        Ok(PublicKey(key))
    }

    pub fn to_bytes(&self) -> [u8; 33] {
        self.0
    }
}

fn verify_sig(req: &HttpRequest) -> Result<bool, ErrorBadRequest> {
    let ts_header = req.headers().get("HM-TS");
    let signature_header = req.headers().get("HM-SIGN");
    let key_header = req.headers().get("HM-KEY");

    if ts_header.is_none() || signature_header.is_none() || key_header.is_none() {
        return Err(ErrorBadRequest("Missing header"));
    }

    let ts = ts_header.unwrap().to_str().unwrap();
    let signature = signature_header.unwrap().to_str().unwrap();
    let key = key_header.unwrap().to_str().unwrap();

    let ts = ts.parse::<i64>().unwrap();
    let current_timestamp = Utc::now().timestamp() * 1000;

    if (current_timestamp - ts).abs() > 5000 {
        return Ok(false);
    }

    let public_key = PublicKey::from_hex(key)?;
    let signature_payload = format!("{}{}{}{}", ts, req.method().as_str(), req.path(), req.body());
    let signature_payload = signature_payload.as_bytes();

    let mut mac = Hmac::<Sha256>::new_varkey(public_key.to_bytes().as_slice()).unwrap();
    mac.input(signature_payload);

    let result = mac.verify(base64::decode(signature).unwrap().as_slice());

    if result.is_err() {
        return Ok(false);
    }

    Ok(true)
}