mauth_core/
signer.rs

1use crate::pem_format;
2use crate::{error::Error, signable::Signable};
3use base64::{engine::general_purpose, Engine as _};
4use rsa::pkcs1::DecodeRsaPrivateKey;
5use rsa::RsaPrivateKey;
6use sha2::Sha512;
7
8/// Used to sign outgoing requests. Struct can be initialized once and used to sign many requests.
9#[derive(Debug, Clone)]
10pub struct Signer {
11    app_uuid: String,
12    private_key: RsaPrivateKey,
13    signing_key: rsa::pkcs1v15::SigningKey<Sha512>,
14}
15
16impl Signer {
17    /// Initialize a new signer with the app UUID and the private key. The format of the private key
18    /// should be a raw RSA private key in the PKCS1 PEM format, as generated by `openssl genrsa`. An
19    /// error will be returned if the input data is unable to be parsed as a private key. The `app_uuid`
20    /// is expected to be a valid UUID, however this is not checked. If you pass something other than
21    /// a valid UUID, no error will be returned, but none of the created signatures will be able to
22    /// be validated by other MAuth verifiers.
23    ///
24    /// ```
25    /// # use mauth_core::signer::Signer;
26    /// # let private_key = std::fs::read_to_string("tests/mauth-protocol-test-suite/signing-params/rsa-key").unwrap();
27    /// let signer = Signer::new("101c139a-236c-11ef-b5e3-125eb8485a60", private_key);
28    /// assert!(signer.is_ok());
29    /// ```
30    pub fn new(app_uuid: impl Into<String>, private_key_data: String) -> Result<Self, Error> {
31        let private_key = RsaPrivateKey::from_pkcs1_pem(&pem_format::normalize_rsa_private_key(
32            private_key_data,
33        ))?;
34        let signing_key = rsa::pkcs1v15::SigningKey::<Sha512>::new(private_key.to_owned());
35
36        Ok(Self {
37            app_uuid: app_uuid.into(),
38            private_key,
39            signing_key,
40        })
41    }
42
43    /// This function will generate a valid MAuth signature string of the specified version, or error
44    /// if it is unable to.
45    ///
46    /// ```
47    /// # use mauth_core::signer::Signer;
48    /// # let private_key = std::fs::read_to_string("tests/mauth-protocol-test-suite/signing-params/rsa-key").unwrap();
49    /// # let signer = Signer::new("101c139a-236c-11ef-b5e3-125eb8485a60", private_key).unwrap();
50    /// let result = signer.sign_string(2, "GET", "/item", "page=2", b"", "2024-01-28T19:11:35.000");
51    /// assert!(result.is_ok());
52    /// ```
53    pub fn sign_string(
54        &self,
55        version: u8,
56        verb: impl Into<String>,
57        path: impl Into<String>,
58        query: impl Into<String>,
59        body: &[u8],
60        timestamp: impl Into<String>,
61    ) -> Result<String, Error> {
62        let signable = Signable::new(verb, path, query, body, timestamp, &self.app_uuid);
63
64        match version {
65            1 => self.sign_string_v1(&signable),
66            2 => self.sign_string_v2(&signable),
67            v => Err(Error::UnsupportedVersion(v)),
68        }
69    }
70
71    fn sign_string_v1(&self, signable: &Signable) -> Result<String, Error> {
72        let signature = self.private_key.sign(
73            rsa::Pkcs1v15Sign::new_unprefixed(),
74            &signable.signing_string_v1()?,
75        )?;
76        Ok(general_purpose::STANDARD.encode(signature))
77    }
78
79    fn sign_string_v2(&self, signable: &Signable) -> Result<String, Error> {
80        use rsa::signature::{SignatureEncoding, Signer};
81
82        let sign = self.signing_key.sign(&signable.signing_string_v2()?);
83        Ok(general_purpose::STANDARD.encode(sign.to_bytes().as_ref()))
84    }
85}