kraken_async_rs/crypto/
signatures.rs

1//! Core signature implementation for signing messages
2use base64::engine::general_purpose::STANDARD as base64;
3use base64::Engine;
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 =
76            "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==";
77
78        let nonce = 1616492376594_u64;
79
80        let post_data = QueryData {
81            order_type: "limit".into(),
82            pair: "XBTUSD".into(),
83            price: "37500".into(),
84            order_side: "buy".into(),
85            volume: "1.25".into(),
86        };
87
88        let mut query_params = form_urlencoded::Serializer::new(String::new());
89        query_params.append_pair("nonce", &nonce.to_string());
90
91        for (key, value) in post_data.to_query_params().iter() {
92            query_params.append_pair(key, value);
93        }
94
95        let encoded_data = query_params.finish();
96
97        let signature = generate_signature(nonce, key, "/0/private/AddOrder", encoded_data);
98
99        assert_eq!(expected, signature.signature);
100    }
101
102    #[test]
103    fn test_generate_signature_json_data() {
104        let expected = "oTOXlYtwCD1eL/j45C8gSWB49XQO1Sguv3nnScc8TTNpgmsnDvAA3yu6geyXXjGIsfCUEOzslsv4ugTZNsM7RA==";
105        let key =
106            "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==";
107
108        let nonce = 1616492376594_u64;
109
110        let post_data = QueryData {
111            order_type: "limit".into(),
112            pair: "XBTUSD".into(),
113            price: "37500".into(),
114            order_side: "buy".into(),
115            volume: "1.25".into(),
116        };
117
118        let encoded_data = serde_json::to_string(&post_data).unwrap();
119
120        let signature = generate_signature(nonce, key, "/0/private/AddOrder", encoded_data);
121
122        assert_eq!(expected, signature.signature);
123    }
124}