nautilus_cryptography/
signing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use base64::prelude::*;
17use hex;
18use ring::{
19    hmac,
20    rand::SystemRandom,
21    signature::{Ed25519KeyPair, RSA_PKCS1_SHA256, RsaKeyPair, Signature},
22};
23
24#[must_use]
25pub fn hmac_signature(secret: &str, data: &str) -> String {
26    let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes());
27    let signature = hmac::sign(&key, data.as_bytes());
28    hex::encode(signature.as_ref())
29}
30
31/// Signs `data` using RSA PKCS#1 v1.5 SHA-256 with the provided private key in PEM format.
32///
33/// # Errors
34///
35/// Returns an error if:
36/// - `data` is empty.
37/// - `private_key_pem` is not a valid PEM-encoded PKCS#8 RSA private key or cannot be parsed.
38/// - Signature generation fails due to key or cryptographic errors.
39pub fn rsa_signature(private_key_pem: &str, data: &str) -> anyhow::Result<String> {
40    if data.is_empty() {
41        anyhow::bail!("Query string cannot be empty");
42    }
43
44    let pem = pem::parse(private_key_pem)?;
45    let private_key =
46        RsaKeyPair::from_pkcs8(pem.contents()).map_err(|e| anyhow::anyhow!("{e:?}"))?;
47    let mut signature = vec![0; private_key.public().modulus_len()];
48    let rng = SystemRandom::new();
49
50    private_key
51        .sign(&RSA_PKCS1_SHA256, &rng, data.as_bytes(), &mut signature)
52        .map_err(|e| anyhow::anyhow!("{e:?}"))?;
53
54    Ok(BASE64_STANDARD.encode(&signature))
55}
56
57/// Signs `data` using Ed25519 with the provided private key seed.
58///
59/// # Errors
60///
61/// Returns an error if the provided private key seed is invalid or signature creation fails.
62pub fn ed25519_signature(private_key: &[u8], data: &str) -> anyhow::Result<String> {
63    let key_pair =
64        Ed25519KeyPair::from_seed_unchecked(private_key).map_err(|e| anyhow::anyhow!("{e:?}"))?;
65    let signature: Signature = key_pair.sign(data.as_bytes());
66    Ok(hex::encode(signature.as_ref()))
67}
68
69////////////////////////////////////////////////////////////////////////////////
70// Tests
71////////////////////////////////////////////////////////////////////////////////
72#[cfg(test)]
73mod tests {
74    use rstest::rstest;
75
76    use super::*;
77
78    #[rstest]
79    #[case(
80        "mysecretkey",
81        "data-to-sign",
82        "19ed21a8b2a6b847d7d7aea059ab3134cd58f13c860cfbe89338c718685fe077"
83    )]
84    #[case(
85        "anothersecretkey",
86        "somedata",
87        "fb44dab41435775b44a96aa008af58cbf1fa1cea32f4605562c586b98f7326c5"
88    )]
89    #[case(
90        "",
91        "data-without-secret",
92        "740c92f9c332fbb22d80aa6a3c9c10197a3e9dc61ca7e3c298c21597e4672133"
93    )]
94    #[case(
95        "mysecretkey",
96        "",
97        "bb4e89236de3b03c17e36d48ca059fa277b88165cb14813a49f082ed8974b9f4"
98    )]
99    #[case(
100        "",
101        "",
102        "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
103    )]
104    fn test_hmac_signature(
105        #[case] secret: &str,
106        #[case] data: &str,
107        #[case] expected_signature: &str,
108    ) {
109        let result = hmac_signature(secret, data);
110        assert_eq!(
111            result, expected_signature,
112            "Expected signature did not match"
113        );
114    }
115
116    #[rstest]
117    #[case(
118        r"-----BEGIN TEST KEY-----
119MIIBVwIBADANBgkqhkiG9w0BAQEFAASCATswggE3AgEAAkEAu/...
120-----END PRIVATE KEY-----",
121        ""
122    )]
123    fn test_rsa_signature_empty_query(#[case] private_key_pem: &str, #[case] query_string: &str) {
124        let result = rsa_signature(private_key_pem, query_string);
125        assert!(
126            result.is_err(),
127            "Expected an error with empty query string, but got Ok"
128        );
129    }
130
131    #[rstest]
132    #[case(
133        r"-----BEGIN INVALID KEY-----
134INVALID_KEY_DATA
135-----END INVALID KEY-----",
136        "This is a test query"
137    )]
138    fn test_rsa_signature_invalid_key(#[case] private_key_pem: &str, #[case] query_string: &str) {
139        let result = rsa_signature(private_key_pem, query_string);
140        assert!(
141            result.is_err(),
142            "Expected an error due to invalid key, but got Ok"
143        );
144    }
145
146    const fn valid_ed25519_private_key() -> [u8; 32] {
147        [
148            0x0c, 0x74, 0x18, 0x92, 0x6b, 0x5d, 0xe9, 0x8f, 0xe2, 0xb6, 0x47, 0x8a, 0x51, 0xf9,
149            0x97, 0x31, 0x9a, 0xcd, 0x2d, 0xbc, 0xf9, 0x94, 0xea, 0x8f, 0xc3, 0x1b, 0x65, 0x24,
150            0x1f, 0x91, 0xd8, 0x6f,
151        ]
152    }
153
154    #[rstest]
155    #[case(valid_ed25519_private_key(), "This is a test query")]
156    #[case(valid_ed25519_private_key(), "")]
157    fn test_ed25519_signature(#[case] private_key_bytes: [u8; 32], #[case] query_string: &str) {
158        let result = ed25519_signature(&private_key_bytes, query_string);
159        assert!(
160            result.is_ok(),
161            "Expected valid signature but got an error: {result:?}"
162        );
163        assert!(!result.unwrap().is_empty(), "Signature should not be empty");
164    }
165}