~vpzom/hancock

4e55d93a4bdd8fb7aad644edd08649ae88750638 — Colin Reeder 1 year, 5 months ago 120cbe9
Redesign API to use a Signature struct
1 files changed, 121 insertions(+), 47 deletions(-)

M src/lib.rs
M src/lib.rs => src/lib.rs +121 -47
@@ 8,63 8,137 @@ pub enum SignError<T: std::fmt::Debug> {
    User(T),
}

pub fn create_signature<E: std::fmt::Debug>(
    key_id: &str,
    request_method: &http::method::Method,
    request_path_and_query: &str,
    lifetime_secs: u64,
    headers: &mut http::header::HeaderMap,
    signer: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
) -> Result<http::header::HeaderValue, SignError<E>> {
    use std::io::Write;

    let created = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("Timestamp is wildly unrealistic (before epoch)")
        .as_secs();
    let expires = created + lifetime_secs;

    let mut body = Vec::new();

    write!(
        body,
        "(request-target): {} {}\n(created): {}\n(expires): {}",
        request_method.as_str().to_lowercase(),
        request_path_and_query,
        created,
        expires,
    )?;

    for name in headers.keys() {
        write!(body, "\n{}: ", name)?;

        let mut first = true;
        for value in headers.get_all(name) {
            if first {
                first = false;
            } else {
                write!(body, ", ")?;
            }
enum SignatureHeaderName {
    RequestTarget,
    Created,
    Expires,
    NormalHeader(http::header::HeaderName),
}

            body.extend(value.as_bytes());
impl SignatureHeaderName {
    pub fn as_str(&self) -> &str {
        match self {
            SignatureHeaderName::RequestTarget => "(request-target)",
            SignatureHeaderName::Created => "(created)",
            SignatureHeaderName::Expires => "(expires)",
            SignatureHeaderName::NormalHeader(header) => header.as_str(),
        }
    }
}

impl From<http::header::HeaderName> for SignatureHeaderName {
    fn from(src: http::header::HeaderName) -> Self {
        SignatureHeaderName::NormalHeader(src)
    }
}

pub struct Signature<'a> {
    algorithm: Option<http::header::HeaderName>,
    created: Option<u64>,
    expires: Option<u64>,
    headers: Option<Vec<SignatureHeaderName>>,
    key_id: Option<&'a str>,
    signature: Vec<u8>,
}

impl<'a> Signature<'a> {
    pub fn create<E: std::fmt::Debug>(
        key_id: &'a str,
        request_method: &http::method::Method,
        request_path_and_query: &str,
        lifetime_secs: u64,
        headers: &mut http::header::HeaderMap,
        sign: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
    ) -> Result<Self, SignError<E>> {
        use std::io::Write;

        let created = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .expect("Timestamp is wildly unrealistic (before epoch)")
            .as_secs();
        let expires = created + lifetime_secs;

        let mut body = Vec::new();

        write!(
            body,
            "(request-target): {} {}\n(created): {}\n(expires): {}",
            request_method.as_str().to_lowercase(),
            request_path_and_query,
            created,
            expires,
        )?;

        for name in headers.keys() {
            write!(body, "\n{}: ", name)?;

    let signature = signer(body).map_err(SignError::User)?;
            let mut first = true;
            for value in headers.get_all(name) {
                if first {
                    first = false;
                } else {
                    write!(body, ", ")?;
                }

    {
                body.extend(value.as_bytes());
            }
        }

        let header_names: Vec<_> = vec![
            SignatureHeaderName::RequestTarget,
            SignatureHeaderName::Created,
            SignatureHeaderName::Expires,
        ]
        .into_iter()
        .chain(headers.keys().cloned().map(Into::into))
        .collect();

        let signature = sign(body).map_err(SignError::User)?;

        Ok(Self {
            algorithm: Some(http::header::HeaderName::from_static("hs2019")),
            created: Some(created),
            expires: Some(expires),
            headers: Some(header_names),
            key_id: Some(key_id),
            signature,
        })
    }

    pub fn to_header(&self) -> http::header::HeaderValue {
        use std::fmt::Write;
        let mut params = String::new();
        write!(params, "algorithm=hs2019,created={},expires={},keyId=\"{}\",headers=\"(request-target) (created) (expires)", created, expires, key_id)?;
        for name in headers.keys() {
            write!(params, " {}", name)?;

        write!(params, "headers=\"").unwrap();
        if let Some(ref headers) = self.headers {
            for (idx, name) in headers.iter().enumerate() {
                if idx != 0 {
                    write!(params, " ").unwrap();
                }
                write!(params, "{}", name.as_str()).unwrap();
            }
        } else {
            write!(params, "(created)").unwrap();
        }
        write!(params, "\",signature=\"")?;
        write!(params, "\"").unwrap();

        base64::encode_config_buf(signature, base64::STANDARD, &mut params);
        if let Some(ref algorithm) = self.algorithm {
            write!(params, ",algorithm={}", algorithm).unwrap();
        }
        if let Some(created) = self.created {
            write!(params, ",created={}", created).unwrap();
        }
        if let Some(expires) = self.expires {
            write!(params, ",expires={}", expires).unwrap();
        }
        if let Some(key_id) = self.key_id {
            write!(params, ",keyId={}", key_id).unwrap();
        }

        write!(params, "\"")?;
        write!(params, ",signature=\"").unwrap();
        base64::encode_config_buf(&self.signature, base64::STANDARD, &mut params);
        write!(params, "\"").unwrap();

        Ok(http::header::HeaderValue::from_bytes(params.as_bytes()).unwrap())
        http::header::HeaderValue::from_bytes(params.as_bytes()).unwrap()
    }
}