1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use crate::{error::Error, signable::Signable};
use base64::{engine::general_purpose, Engine as _};
use rsa::pkcs1v15::Signature;
use rsa::pkcs8::DecodePublicKey;
use rsa::RsaPublicKey;
use sha2::Sha512;
/// Used to verify incoming requests. Struct can be initialized once and used to verify many requests.
#[derive(Debug, Clone)]
pub struct Verifier {
app_uuid: String,
public_key: RsaPublicKey,
verifying_key: rsa::pkcs1v15::VerifyingKey<Sha512>,
}
impl Verifier {
/// Initialize a new verifier with the source app UUID and the public key. The format of the public key
/// should be a raw RSA public key in the public key PEM format, as generated by `openssl rsa -pubout -out`.
/// An error will be returned if the input data is unable to be parsed as a public key. The `app_uuid`
/// is expected to be a valid UUID, however this is not checked. If you pass something other than
/// a valid UUID, no error will be returned, but none of the signatures will be able to be validated.
///
/// ```
/// # use mauth_core::verifier::Verifier;
/// # let public_key = std::fs::read_to_string("tests/mauth-protocol-test-suite/signing-params/rsa-key-pub").unwrap();
/// let verifier = Verifier::new("101c139a-236c-11ef-b5e3-125eb8485a60", public_key);
/// assert!(verifier.is_ok());
/// ```
pub fn new(app_uuid: impl Into<String>, public_key_data: String) -> Result<Self, Error> {
let public_key = RsaPublicKey::from_public_key_pem(&public_key_data)?;
let verifying_key = rsa::pkcs1v15::VerifyingKey::<Sha512>::new(public_key.to_owned());
Ok(Self {
app_uuid: app_uuid.into(),
public_key,
verifying_key,
})
}
/// This function will verify that a provided signature is valid given the uuid and public key the
/// struct was constructed with, the request properties passed into the function, and the signature
/// passed in. It will return Ok(()) if the signature validates successfully, and Err if it does
/// not. It is the responsibility of the consuming crate and application to use these cases to
/// determine whether to process a request further, or return error information.
///
/// ```
/// # use mauth_core::verifier::Verifier;
/// # use mauth_core::error::Error;
/// # let public_key = std::fs::read_to_string("tests/mauth-protocol-test-suite/signing-params/rsa-key-pub").unwrap();
/// # let verifier = Verifier::new("101c139a-236c-11ef-b5e3-125eb8485a60", public_key).unwrap();
/// let result = verifier.verify_signature(2, "GET", "/item", "page=2", b"", "2024-01-28T19:11:35.000", "");
/// // Passing in an empty signature, so it will result in a verification error
/// assert!(matches!(result, Err(Error::SignatureVerifyError(_))));
/// ```
#[allow(clippy::too_many_arguments)]
pub fn verify_signature(
&self,
version: u8,
verb: impl Into<String>,
path: impl Into<String>,
query: impl Into<String>,
body: &[u8],
timestamp: impl Into<String>,
signature: impl Into<String>,
) -> Result<(), Error> {
let signable = Signable::new(verb, path, query, body, timestamp, &self.app_uuid);
match version {
1 => self.verify_signature_v1(&signable, signature.into()),
2 => self.verify_signature_v2(&signable, signature.into()),
v => Err(Error::UnsupportedVersion(v)),
}
}
fn verify_signature_v1(&self, signable: &Signable, signature: String) -> Result<(), Error> {
self.public_key.verify(
rsa::Pkcs1v15Sign::new_unprefixed(),
&signable.signing_string_v1()?,
&general_purpose::STANDARD.decode(signature)?,
)?;
Ok(())
}
fn verify_signature_v2(&self, signable: &Signable, signature: String) -> Result<(), Error> {
use rsa::signature::Verifier;
let signature =
Signature::try_from(general_purpose::STANDARD.decode(signature)?.as_slice())?;
self.verifying_key
.verify(&signable.signing_string_v2()?, &signature)?;
Ok(())
}
}