oauth1-request 0.6.0

Yet yet yet another OAuth 1.0 client library.
Documentation
//! The `RSA-SHA1` signature method ([RFC 5849 section 3.4.3.][rfc]).
//!
//! [rfc]: https://datatracker.ietf.org/doc/html/rfc5849#section-3.4.3
//!
//! This module is only available when `rsa-sha1-06` feature is activated.

extern crate alloc;

pub use rsa06::pkcs8::DecodePrivateKey;
pub use rsa06::RsaPrivateKey;

use alloc::vec::Vec;
use core::fmt::{self, Display, Formatter};

use digest::Digest;
use rsa06::{Hash, PaddingScheme};
use sha1::Sha1;

use super::digest_common::{Base64PercentEncodeDisplay, UpdateSign};
use super::{Sign, SignatureMethod};

/// The `RSA-SHA1` signature method.
#[derive(Clone, Debug)]
#[repr(transparent)]
pub struct RsaSha1 {
    key: RsaPrivateKey,
}

/// A type that signs a signature base string with the RSA-SHA1 signature algorithm.
#[derive(Clone, Debug)]
pub struct RsaSha1Sign<K = RsaPrivateKey> {
    inner: UpdateSign<Sha1>,
    key: K,
}

/// A signature produced by an `RsaSha1Sign`.
pub struct RsaSha1Signature {
    inner: Base64PercentEncodeDisplay<Vec<u8>>,
}

impl RsaSha1 {
    /// Creates a new `RsaSha1` that signs a signature base string with the given RSA private key.
    pub const fn new(key: RsaPrivateKey) -> Self {
        RsaSha1 { key }
    }
}

impl From<RsaPrivateKey> for RsaSha1 {
    fn from(key: RsaPrivateKey) -> Self {
        RsaSha1::new(key)
    }
}

impl AsRef<RsaSha1> for RsaPrivateKey {
    fn as_ref(&self) -> &RsaSha1 {
        #[allow(clippy::needless_lifetimes)] // Adding the lifetime annotations just to be sure.
        fn inner<'a>(key: &'a RsaPrivateKey) -> &'a RsaSha1 {
            // Safety:
            // - The `#[repr(transparent)]` attribute ensures that `RsaSha1` has the same layout as
            //   `RsaPrivateKey`.
            // - The lifetime annotations ensure that the output lives for the same lifetime as
            //   the input.
            unsafe { &*(key as *const RsaPrivateKey).cast::<RsaSha1>() }
        }
        inner(self)
    }
}

impl SignatureMethod for RsaSha1 {
    type Sign = RsaSha1Sign;

    fn sign_with(self, _client_secret: &str, _token_secret: Option<&str>) -> Self::Sign {
        RsaSha1Sign {
            inner: UpdateSign(Sha1::default()),
            key: self.key,
        }
    }
}

impl<'a> SignatureMethod for &'a RsaSha1 {
    type Sign = RsaSha1Sign<&'a RsaPrivateKey>;

    fn sign_with(self, _client_secret: &str, _token_secret: Option<&str>) -> Self::Sign {
        RsaSha1Sign {
            inner: UpdateSign(Sha1::default()),
            key: &self.key,
        }
    }
}

impl<'a> Sign for RsaSha1Sign {
    type Signature = RsaSha1Signature;

    fn get_signature_method_name(&self) -> &'static str {
        "RSA-SHA1"
    }

    fn request_method(&mut self, method: &str) {
        self.inner.request_method(method);
    }

    fn uri<T: Display>(&mut self, uri: T) {
        self.inner.uri(uri);
    }

    fn parameter<V: Display>(&mut self, key: &str, value: V) {
        self.inner.parameter(key, value);
    }

    fn delimiter(&mut self) {
        self.inner.delimiter();
    }

    fn end(self) -> RsaSha1Signature {
        RsaSha1Sign {
            inner: self.inner,
            key: &self.key,
        }
        .end()
    }
}

impl<'a> Sign for RsaSha1Sign<&'a RsaPrivateKey> {
    type Signature = RsaSha1Signature;

    fn get_signature_method_name(&self) -> &'static str {
        "RSA-SHA1"
    }

    fn request_method(&mut self, method: &str) {
        self.inner.request_method(method);
    }

    fn uri<T: Display>(&mut self, uri: T) {
        self.inner.uri(uri);
    }

    fn parameter<V: Display>(&mut self, key: &str, value: V) {
        self.inner.parameter(key, value);
    }

    fn delimiter(&mut self) {
        self.inner.delimiter();
    }

    fn end(self) -> RsaSha1Signature {
        let padding = PaddingScheme::new_pkcs1v15_sign(Some(Hash::SHA1));
        let digest = self.inner.0.finalize();
        let signature = self.key.sign(padding, &digest).unwrap();
        RsaSha1Signature {
            inner: Base64PercentEncodeDisplay(signature),
        }
    }
}

impl Display for RsaSha1Signature {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        self.inner.fmt(f)
    }
}

#[cfg(test)]
mod tests {
    use alloc::string::ToString;

    use crate::util::percent_encode;

    use super::*;

    #[test]
    fn test() {
        // Test case from <https://wiki.oauth.net/w/page/12238556/TestCases>.

        let der =
            "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8VA7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJhI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8HX9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mmuScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmwrn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0ZzO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+NccnqkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNGWPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUnocn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54Lw03eHTNQghS0A==";
        let der = base64::decode(der).unwrap();
        let private_key = RsaPrivateKey::from_pkcs8_der(&der).unwrap();

        let signature_method: &RsaSha1 = private_key.as_ref();
        let mut sign = signature_method.sign_with("", None);

        sign.request_method("GET");
        sign.uri("http%3A%2F%2Fphotos.example.net%2Fphotos");
        sign.parameter("file", "vacaction.jpg");
        sign.delimiter();
        sign.consumer_key("dpf43f3p2l4k3l03");
        sign.delimiter();
        sign.nonce("13917289812797014437");
        sign.delimiter();
        sign.signature_method();
        sign.delimiter();
        sign.timestamp(1196666512);
        sign.delimiter();
        sign.version();
        sign.delimiter();
        sign.parameter("size", "original");

        let signature = sign.end();
        let expected =
            percent_encode("jvTp/wX1TYtByB1m+Pbyo0lnCOLIsyGCH7wke8AUs3BpnwZJtAuEJkvQL2/9n4s5wUmUl4aCI4BwpraNx4RtEXMe5qg5T1LVTGliMRpKasKsW//e+RinhejgCuzoH26dyF8iY2ZZ/5D1ilgeijhV/vBka5twt399mXwaYdCwFYE=")
                .to_string();
        assert_eq!(signature.to_string(), expected);
    }
}