kraken_async_rs/crypto/
signatures.rs

1//! Core signature implementation for signing messages
2use base64::Engine;
3use base64::engine::general_purpose::STANDARD as base64;
4use hmac::{Hmac, Mac};
5use sha2::{Digest, Sha256, Sha512};
6
7/// Struct containing the encoded message body and finalized signature
8pub struct Signature {
9    pub body_data: String,
10    pub signature: String,
11}
12
13/// Generates the signature for an arbitrary request when provided with a nonce, API secret key,
14/// the endpoint, and the encoded data being sent.
15///
16/// This is HMAC-SHA512(uri + sha256(nonce + post_data)), but the exact details are given by
17/// [`Kraken's documentation`].
18///
19/// Errors can occur due to formatting, url-encoding (or not) of specific data, and other details,
20/// but this implementation does not specify that `encoded_data` is anything but a [String].
21///
22/// [`Kraken's documentation`]: https://docs.kraken.com/rest/#section/Authentication/Headers-and-Signature
23pub fn generate_signature(
24    nonce: u64,
25    secret: &str,
26    endpoint: &str,
27    encoded_data: String,
28) -> Signature {
29    let mut hmac = Hmac::<Sha512>::new_from_slice(&base64.decode(secret.as_bytes()).unwrap())
30        .expect("Could not use private key to create HMAC");
31
32    let mut sha256 = Sha256::new();
33
34    sha256.update(nonce.to_string().as_bytes());
35    sha256.update(encoded_data.as_bytes());
36
37    let payload = sha256.finalize();
38
39    hmac.update(endpoint.as_bytes());
40    hmac.update(&payload[..]);
41
42    Signature {
43        body_data: encoded_data,
44        signature: base64.encode(hmac.finalize().into_bytes()),
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use serde::Serialize;
52    use to_query_params::{QueryParams, ToQueryParams};
53    use url::form_urlencoded;
54
55    #[derive(QueryParams, Serialize)]
56    struct QueryData {
57        #[serde(rename = "ordertype")]
58        #[query(required, rename = "ordertype")]
59        order_type: String,
60        #[query(required)]
61        pair: String,
62        #[query(required)]
63        price: String,
64        #[query(required)]
65        #[query(rename = "type")]
66        #[serde(rename = "type")]
67        order_side: String,
68        #[query(required)]
69        volume: String,
70    }
71
72    #[test]
73    fn test_generate_signature_form_data() {
74        let expected = "4/dpxb3iT4tp/ZCVEwSnEsLxx0bqyhLpdfOpc6fn7OR8+UClSV5n9E6aSS8MPtnRfp32bAb0nmbRn6H8ndwLUQ==";
75        let key = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==";
76
77        let nonce = 1616492376594_u64;
78
79        let post_data = QueryData {
80            order_type: "limit".into(),
81            pair: "XBTUSD".into(),
82            price: "37500".into(),
83            order_side: "buy".into(),
84            volume: "1.25".into(),
85        };
86
87        let mut query_params = form_urlencoded::Serializer::new(String::new());
88        query_params.append_pair("nonce", &nonce.to_string());
89
90        for (key, value) in post_data.to_query_params().iter() {
91            query_params.append_pair(key, value);
92        }
93
94        let encoded_data = query_params.finish();
95
96        let signature = generate_signature(nonce, key, "/0/private/AddOrder", encoded_data);
97
98        assert_eq!(expected, signature.signature);
99    }
100
101    #[test]
102    fn test_generate_signature_json_data() {
103        let expected = "oTOXlYtwCD1eL/j45C8gSWB49XQO1Sguv3nnScc8TTNpgmsnDvAA3yu6geyXXjGIsfCUEOzslsv4ugTZNsM7RA==";
104        let key = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==";
105
106        let nonce = 1616492376594_u64;
107
108        let post_data = QueryData {
109            order_type: "limit".into(),
110            pair: "XBTUSD".into(),
111            price: "37500".into(),
112            order_side: "buy".into(),
113            volume: "1.25".into(),
114        };
115
116        let encoded_data = serde_json::to_string(&post_data).unwrap();
117
118        let signature = generate_signature(nonce, key, "/0/private/AddOrder", encoded_data);
119
120        assert_eq!(expected, signature.signature);
121    }
122}