nautilus_cryptography/
signing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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 aws_lc_rs::{hmac, rand as lc_rand, rsa::KeyPair, signature as lc_signature};
17use base64::prelude::*;
18use ed25519_dalek::{Signature as Ed25519Signature, Signer, SigningKey};
19use hex;
20
21/// Generates an HMAC-SHA256 signature for the given data using the provided secret.
22///
23/// This function creates a cryptographic hash-based message authentication code (HMAC)
24/// using SHA-256 as the underlying hash function. The resulting signature is returned
25/// as a lowercase hexadecimal string.
26///
27/// # Errors
28///
29/// Returns an error if signature generation fails due to key or cryptographic errors.
30pub fn hmac_signature(secret: &str, data: &str) -> anyhow::Result<String> {
31    let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes());
32    let tag = hmac::sign(&key, data.as_bytes());
33    Ok(hex::encode(tag.as_ref()))
34}
35
36/// Signs `data` using RSA PKCS#1 v1.5 SHA-256 with the provided private key in PEM format.
37///
38/// # Errors
39///
40/// Returns an error if:
41/// - `data` is empty.
42/// - `private_key_pem` is not a valid PEM-encoded PKCS#8 RSA private key or cannot be parsed.
43/// - Signature generation fails due to key or cryptographic errors.
44pub fn rsa_signature(private_key_pem: &str, data: &str) -> anyhow::Result<String> {
45    if data.is_empty() {
46        anyhow::bail!("Query string cannot be empty");
47    }
48
49    // Remove PEM headings and decode to DER bytes using the `pem` crate
50    let pem = pem::parse(private_key_pem.trim())
51        .map_err(|e| anyhow::anyhow!("Failed to parse PEM: {e}"))?;
52
53    // Ensure this is a private key
54    if !pem.tag().ends_with("PRIVATE KEY") {
55        anyhow::bail!("PEM does not contain a private key");
56    }
57
58    // Construct RSA key pair from PKCS#8 DER bytes
59    let key_pair = KeyPair::from_pkcs8(pem.contents())
60        .map_err(|_| anyhow::anyhow!("Failed to decode RSA private key"))?;
61
62    // Prepare RNG and output buffer (signature length = modulus length)
63    let rng = lc_rand::SystemRandom::new();
64    let mut signature = vec![0u8; key_pair.public_modulus_len()];
65
66    key_pair
67        .sign(
68            &lc_signature::RSA_PKCS1_SHA256,
69            &rng,
70            data.as_bytes(),
71            &mut signature,
72        )
73        .map_err(|_| anyhow::anyhow!("Failed to generate RSA signature"))?;
74
75    Ok(BASE64_STANDARD.encode(signature))
76}
77
78/// Signs `data` using Ed25519 with the provided private key seed.
79///
80/// # Errors
81///
82/// Returns an error if the provided private key seed is invalid or signature creation fails.
83pub fn ed25519_signature(private_key: &[u8], data: &str) -> anyhow::Result<String> {
84    let signing_key = SigningKey::from_bytes(
85        private_key
86            .try_into()
87            .map_err(|_| anyhow::anyhow!("Invalid Ed25519 private key length"))?,
88    );
89    let signature: Ed25519Signature = signing_key.sign(data.as_bytes());
90    Ok(BASE64_STANDARD.encode(signature.to_bytes()))
91}
92
93#[cfg(test)]
94mod tests {
95    use rstest::rstest;
96
97    use super::*;
98
99    #[rstest]
100    #[case(
101        "mysecretkey",
102        "data-to-sign",
103        "19ed21a8b2a6b847d7d7aea059ab3134cd58f13c860cfbe89338c718685fe077"
104    )]
105    #[case(
106        "anothersecretkey",
107        "somedata",
108        "fb44dab41435775b44a96aa008af58cbf1fa1cea32f4605562c586b98f7326c5"
109    )]
110    #[case(
111        "",
112        "data-without-secret",
113        "740c92f9c332fbb22d80aa6a3c9c10197a3e9dc61ca7e3c298c21597e4672133"
114    )]
115    #[case(
116        "mysecretkey",
117        "",
118        "bb4e89236de3b03c17e36d48ca059fa277b88165cb14813a49f082ed8974b9f4"
119    )]
120    #[case(
121        "",
122        "",
123        "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
124    )]
125    fn test_hmac_signature(
126        #[case] secret: &str,
127        #[case] data: &str,
128        #[case] expected_signature: &str,
129    ) {
130        let result = hmac_signature(secret, data).unwrap();
131        assert_eq!(
132            result, expected_signature,
133            "Expected signature did not match"
134        );
135    }
136
137    #[rstest]
138    #[case(
139        r"-----BEGIN TEST KEY-----
140MIIBVwIBADANBgkqhkiG9w0BAQEFAASCATswggE3AgEAAkEAu/...
141-----END PRIVATE KEY-----",
142        ""
143    )]
144    fn test_rsa_signature_empty_query(#[case] private_key_pem: &str, #[case] query_string: &str) {
145        let result = rsa_signature(private_key_pem, query_string);
146        assert!(
147            result.is_err(),
148            "Expected an error with empty query string, but got Ok"
149        );
150    }
151
152    #[rstest]
153    #[case(
154        r"-----BEGIN INVALID KEY-----
155INVALID_KEY_DATA
156-----END INVALID KEY-----",
157        "This is a test query"
158    )]
159    fn test_rsa_signature_invalid_key(#[case] private_key_pem: &str, #[case] query_string: &str) {
160        let result = rsa_signature(private_key_pem, query_string);
161        assert!(
162            result.is_err(),
163            "Expected an error due to invalid key, but got Ok"
164        );
165    }
166
167    const fn valid_ed25519_private_key() -> [u8; 32] {
168        [
169            0x0c, 0x74, 0x18, 0x92, 0x6b, 0x5d, 0xe9, 0x8f, 0xe2, 0xb6, 0x47, 0x8a, 0x51, 0xf9,
170            0x97, 0x31, 0x9a, 0xcd, 0x2d, 0xbc, 0xf9, 0x94, 0xea, 0x8f, 0xc3, 0x1b, 0x65, 0x24,
171            0x1f, 0x91, 0xd8, 0x6f,
172        ]
173    }
174
175    #[rstest]
176    #[case(valid_ed25519_private_key(), "This is a test query")]
177    #[case(valid_ed25519_private_key(), "")]
178    fn test_ed25519_signature(#[case] private_key_bytes: [u8; 32], #[case] query_string: &str) {
179        let result = ed25519_signature(&private_key_bytes, query_string);
180        assert!(
181            result.is_ok(),
182            "Expected valid signature but got an error: {result:?}"
183        );
184        assert!(!result.unwrap().is_empty(), "Signature should not be empty");
185    }
186}