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(hex::encode(signature.to_bytes()))
91}
92
93////////////////////////////////////////////////////////////////////////////////
94// Tests
95////////////////////////////////////////////////////////////////////////////////
96#[cfg(test)]
97mod tests {
98    use rstest::rstest;
99
100    use super::*;
101
102    #[rstest]
103    #[case(
104        "mysecretkey",
105        "data-to-sign",
106        "19ed21a8b2a6b847d7d7aea059ab3134cd58f13c860cfbe89338c718685fe077"
107    )]
108    #[case(
109        "anothersecretkey",
110        "somedata",
111        "fb44dab41435775b44a96aa008af58cbf1fa1cea32f4605562c586b98f7326c5"
112    )]
113    #[case(
114        "",
115        "data-without-secret",
116        "740c92f9c332fbb22d80aa6a3c9c10197a3e9dc61ca7e3c298c21597e4672133"
117    )]
118    #[case(
119        "mysecretkey",
120        "",
121        "bb4e89236de3b03c17e36d48ca059fa277b88165cb14813a49f082ed8974b9f4"
122    )]
123    #[case(
124        "",
125        "",
126        "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
127    )]
128    fn test_hmac_signature(
129        #[case] secret: &str,
130        #[case] data: &str,
131        #[case] expected_signature: &str,
132    ) {
133        let result = hmac_signature(secret, data).unwrap();
134        assert_eq!(
135            result, expected_signature,
136            "Expected signature did not match"
137        );
138    }
139
140    #[rstest]
141    #[case(
142        r"-----BEGIN TEST KEY-----
143MIIBVwIBADANBgkqhkiG9w0BAQEFAASCATswggE3AgEAAkEAu/...
144-----END PRIVATE KEY-----",
145        ""
146    )]
147    fn test_rsa_signature_empty_query(#[case] private_key_pem: &str, #[case] query_string: &str) {
148        let result = rsa_signature(private_key_pem, query_string);
149        assert!(
150            result.is_err(),
151            "Expected an error with empty query string, but got Ok"
152        );
153    }
154
155    #[rstest]
156    #[case(
157        r"-----BEGIN INVALID KEY-----
158INVALID_KEY_DATA
159-----END INVALID KEY-----",
160        "This is a test query"
161    )]
162    fn test_rsa_signature_invalid_key(#[case] private_key_pem: &str, #[case] query_string: &str) {
163        let result = rsa_signature(private_key_pem, query_string);
164        assert!(
165            result.is_err(),
166            "Expected an error due to invalid key, but got Ok"
167        );
168    }
169
170    const fn valid_ed25519_private_key() -> [u8; 32] {
171        [
172            0x0c, 0x74, 0x18, 0x92, 0x6b, 0x5d, 0xe9, 0x8f, 0xe2, 0xb6, 0x47, 0x8a, 0x51, 0xf9,
173            0x97, 0x31, 0x9a, 0xcd, 0x2d, 0xbc, 0xf9, 0x94, 0xea, 0x8f, 0xc3, 0x1b, 0x65, 0x24,
174            0x1f, 0x91, 0xd8, 0x6f,
175        ]
176    }
177
178    #[rstest]
179    #[case(valid_ed25519_private_key(), "This is a test query")]
180    #[case(valid_ed25519_private_key(), "")]
181    fn test_ed25519_signature(#[case] private_key_bytes: [u8; 32], #[case] query_string: &str) {
182        let result = ed25519_signature(&private_key_bytes, query_string);
183        assert!(
184            result.is_ok(),
185            "Expected valid signature but got an error: {result:?}"
186        );
187        assert!(!result.unwrap().is_empty(), "Signature should not be empty");
188    }
189}