~vpzom/hancock

6a20e39a31f5985418a5b3c61ac75ea9d6b5dd78 — Colin Reeder 1 year, 5 months ago 92161de
parsing and verifying?
1 files changed, 168 insertions(+), 1 deletions(-)

M src/lib.rs
M src/lib.rs => src/lib.rs +168 -1
@@ 1,8 1,30 @@
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
    #[error("Parameter pair did not contain =")]
    MissingEquals,
    #[error("No signature found in parameters")]
    MissingSignature,
    #[error("Parameter contained invalid characters")]
    InvalidCharacters,
    #[error("Failed to parse number")]
    Number(std::num::ParseIntError),
    #[error("Failed to parse signature bytes")]
    Base64(base64::DecodeError),
}

#[derive(Debug, thiserror::Error)]
pub enum SignError<T: std::fmt::Debug> {
    #[error("IO error occurred")]
    IO(#[from] std::io::Error),
    #[error("Failed in signer call")]
    #[error("Failed in user sign call")]
    User(T),
}

#[derive(Debug, thiserror::Error)]
pub enum VerifyError<T: std::fmt::Debug> {
    #[error("IO error occurred")]
    IO(#[from] std::io::Error),
    #[error("Failed in user verify call")]
    User(T),
}



@@ 30,6 52,32 @@ impl From<http::header::HeaderName> for SignatureHeaderName {
    }
}

impl std::str::FromStr for SignatureHeaderName {
    type Err = http::header::InvalidHeaderName;

    fn from_str(src: &str) -> Result<Self, http::header::InvalidHeaderName> {
        if src == "(request-target)" {
            Ok(SignatureHeaderName::RequestTarget)
        } else if src == "(created)" {
            Ok(SignatureHeaderName::Created)
        } else if src == "(expires)" {
            Ok(SignatureHeaderName::Expires)
        } else {
            Ok(SignatureHeaderName::NormalHeader(src.parse()?))
        }
    }
}

fn parse_maybe_quoted<'a>(src: &'a str) -> &'a str {
    // TODO handle escapes?

    if src.starts_with('=') && src.ends_with('=') {
        &src[1..(src.len() - 1)]
    } else {
        src
    }
}

pub struct Signature<'a> {
    algorithm: Option<http::header::HeaderName>,
    created: Option<u64>,


@@ 103,6 151,53 @@ impl<'a> Signature<'a> {
        })
    }

    pub fn parse(value: &'a http::header::HeaderValue) -> Result<Self, ParseError> {
        let mut algorithm = None;
        let mut created = None;
        let mut expires = None;
        let mut headers = None;
        let mut key_id = None;
        let mut signature = None;

        for field_src in value.to_str().map_err(|_| ParseError::InvalidCharacters)?.split(',') {
            let eqidx = field_src.find('=').ok_or(ParseError::MissingEquals)?;

            let key = &field_src[..eqidx];
            let value = parse_maybe_quoted(&field_src[(eqidx + 1)..]);

            match key {
                "algorithm" => {
                    algorithm = Some(value.parse().map_err(|_| ParseError::InvalidCharacters)?);
                },
                "created" => {
                    created = Some(value.parse().map_err(ParseError::Number)?);
                },
                "expires" => {
                    expires = Some(value.parse().map_err(ParseError::Number)?);
                },
                "headers" => {
                    headers = Some(value.split(' ').map(|x| x.parse().map_err(|_| ParseError::InvalidCharacters)).collect::<Result<Vec<_>, _>>()?);
                },
                "key_id" => {
                    key_id = Some(value);
                },
                "signature" => {
                    signature = Some(base64::decode(value).map_err(ParseError::Base64)?);
                },
                _ => {},
            }
        }

        Ok(Self {
            algorithm,
            created,
            expires,
            headers,
            key_id,
            signature: signature.ok_or(ParseError::MissingSignature)?,
        })
    }

    pub fn to_header(&self) -> http::header::HeaderValue {
        use std::fmt::Write;
        let mut params = String::new();


@@ 139,4 234,76 @@ impl<'a> Signature<'a> {

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

    pub fn verify<E: std::fmt::Debug>(
        &self,
        request_method: &http::method::Method,
        request_path_and_query: &str,
        headers: &mut http::header::HeaderMap,
        verify: impl FnOnce(&[u8], &[u8]) -> Result<bool, E>,
    ) -> Result<bool, VerifyError<E>> {
        use std::io::Write;

        let now = std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH)
            .expect("Timestamp is wildly inaccurate")
            .as_secs();

        if let Some(expires) = self.expires {
            if expires < now {
                return Ok(false);
            }
        }

        let mut body = Vec::new();
        if let Some(header_names) = &self.headers {
            for (idx, name) in header_names.iter().enumerate() {
                if idx != 0 {
                    write!(body, "\n")?;
                }
                match name {
                    SignatureHeaderName::RequestTarget => {
                        write!(body, "(request-target): {} {}", request_method.as_str().to_lowercase(), request_path_and_query)?;
                    },
                    SignatureHeaderName::Created => {
                        if let Some(created) = self.created {
                            write!(body, "(created): {}", created)?;
                        } else {
                            return Ok(false);
                        }
                    },
                    SignatureHeaderName::Expires => {
                        if let Some(expires) = self.expires {
                            write!(body, "(expires): {}", expires)?;
                        } else {
                            return Ok(false);
                        }
                    },
                    SignatureHeaderName::NormalHeader(name) => {
                        write!(body, "{}: ", name)?;

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

                            body.extend(value.as_bytes());
                        }
                    },
                }
            }
        } else {
            if let Some(created) = self.created {
                write!(body, "(created): {}", created)?;
            } else {
                return Ok(false);
            }
        }

        if !verify(&body, &self.signature).map_err(VerifyError::User)? {
            false
        }
    }
}