~vpzom/hancock

21f6cdb08b8a0e2f3950108b12f788d1b4b38b27 — Colin Reeder 10 months ago 5606362
Initial work on new draft implementation
4 files changed, 867 insertions(+), 364 deletions(-)

M Cargo.toml
A src/httpbis.rs
M src/lib.rs
A src/richanna.rs
M Cargo.toml => Cargo.toml +1 -0
@@ 13,3 13,4 @@ description = "Implementation-agnostic HTTP Signature helper"
http = "0.2.1"
thiserror = "1.0.19"
base64 = "0.12.1"
percent-encoding = "2.3.1"

A src/httpbis.rs => src/httpbis.rs +465 -0
@@ 0,0 1,465 @@
use std::borrow::Cow;

const FORM_URLENCODED_ENCODE_SET: &percent_encoding::AsciiSet = &percent_encoding::NON_ALPHANUMERIC
    .remove(b'*')
    .remove(b'-')
    .remove(b'.')
    .remove(b'_');

#[derive(Clone, Debug)]
pub struct HttpFieldComponentId<'a> {
    pub name: http::HeaderName,
    pub sf: bool,
    pub key: Option<Cow<'a, str>>,
    pub bs: bool,
    pub tr: bool,
}

impl<'a> HttpFieldComponentId<'a> {
    pub fn new(name: http::HeaderName) -> Self {
        Self {
            name,
            sf: false,
            key: None,
            bs: false,
            tr: false,
        }
    }
}

#[derive(Clone, Debug)]
pub enum ComponentId<'a> {
    HttpField(HttpFieldComponentId<'a>),
    Method,
    TargetUri,
    Authority,
    Scheme,
    RequestTarget,
    Path,
    Query,
    QueryParam { name: Cow<'a, str> },
    Status,
}

impl<'a> ComponentId<'a> {
    fn serialize_into(&self, result: &mut String) -> Result<(), crate::CommonError> {
        let add_name = |result: &mut String, name: &str| name.serialize_as_bare_item(result);

        let add_param = |result: &mut String,
                         key: &str,
                         value: &dyn AsBareItem|
         -> Result<(), crate::CommonError> {
            result.push(';');
            key.serialize_as_bare_item(result)?;
            if !value.is_true() {
                result.push('=');
                value.serialize_as_bare_item(result)?;
            }

            Ok(())
        };

        match self {
            ComponentId::HttpField(component) => {
                add_name(result, component.name.as_ref())?;

                if component.sf {
                    add_param(result, "sf", &true)?;
                }

                if let Some(value) = &component.key {
                    add_param(result, "key", &value)?;
                }

                if component.bs {
                    add_param(result, "bs", &true)?;
                }

                if component.tr {
                    add_param(result, "tr", &true)?;
                }
            }
            ComponentId::Method => add_name(result, "@method")?,
            ComponentId::TargetUri => add_name(result, "@target-uri")?,
            ComponentId::Authority => add_name(result, "@authority")?,
            ComponentId::Scheme => add_name(result, "@scheme")?,
            ComponentId::RequestTarget => add_name(result, "@request-target")?,
            ComponentId::Path => add_name(result, "@path")?,
            ComponentId::Query => add_name(result, "@query")?,
            ComponentId::QueryParam { name } => {
                add_name(result, "@query-param")?;
                add_param(
                    result,
                    "name",
                    &percent_encoding::percent_encode(name.as_bytes(), FORM_URLENCODED_ENCODE_SET),
                )?;
            }
            ComponentId::Status => add_name(result, "@status")?,
        }

        Ok(())
    }

    fn serialize_value_into<B>(
        &self,
        result: &mut String,
        src: &RequestOrResponse<B>,
    ) -> Result<(), crate::CommonError> {
        match self {
            ComponentId::HttpField(component) => {
                let values = src.headers().get_all(&component.name);

                if component.sf || component.bs || component.tr || component.key.is_some() {
                    return Err(crate::CommonError::Unsupported);
                }

                let mut iter = values.iter();

                let first = iter.next();

                if let Some(value) = first {
                    result.push_str(
                        value
                            .to_str()
                            .map_err(|_| crate::CommonError::InvalidCharacter)?,
                    );
                } else {
                    return Err(crate::CommonError::MissingComponent);
                }

                for value in iter {
                    result.push(',');
                    result.push(' ');
                    result.push_str(
                        value
                            .to_str()
                            .map_err(|_| crate::CommonError::InvalidCharacter)?,
                    );
                }
            }
            ComponentId::Method => match src {
                RequestOrResponse::Request(req) => {
                    result.push_str(req.method().as_str());
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
            ComponentId::TargetUri => match src {
                RequestOrResponse::Request(req) => {
                    use std::fmt::Write;
                    write!(result, "{}", req.uri()).unwrap();
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
            ComponentId::Authority => match src {
                RequestOrResponse::Request(req) => {
                    if let Some(authority) = req.uri().authority() {
                        result.push_str(authority.as_str());
                    } else {
                        return Err(crate::CommonError::MissingComponent);
                    }
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
            ComponentId::Scheme => match src {
                RequestOrResponse::Request(req) => {
                    if let Some(scheme) = req.uri().scheme_str() {
                        result.push_str(scheme);
                    } else {
                        return Err(crate::CommonError::MissingComponent);
                    }
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
            ComponentId::RequestTarget => match src {
                RequestOrResponse::Request(req) => {
                    if let Some(value) = req.uri().path_and_query() {
                        result.push_str(value.as_str());
                    } else {
                        return Err(crate::CommonError::MissingComponent);
                    }
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
            ComponentId::Path => match src {
                RequestOrResponse::Request(req) => {
                    result.push_str(req.uri().path());
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
            ComponentId::Query => match src {
                RequestOrResponse::Request(req) => {
                    result.push('?');
                    result.push_str(req.uri().query().unwrap_or(""));
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
            ComponentId::QueryParam { .. } => {
                return Err(crate::CommonError::Unsupported);
            }
            ComponentId::Status => match src {
                RequestOrResponse::Response(res) => {
                    result.push_str(res.status().as_str());
                }
                _ => return Err(crate::CommonError::MissingComponent),
            },
        }

        Ok(())
    }
}

#[derive(Clone, Debug, Default)]
pub struct SignatureParams<'a> {
    pub created: Option<u64>,
    pub expires: Option<u64>,
    pub nonce: Option<Cow<'a, str>>,
    pub alg: Option<Cow<'a, str>>,
    pub keyid: Option<Cow<'a, str>>,
    pub tag: Option<Cow<'a, str>>,
}

impl<'a> SignatureParams<'a> {
    fn serialize<E: std::fmt::Debug>(&self) -> Result<String, crate::SignError<E>> {
        let mut result = String::new();

        let add_param =
            |result: &mut String, key, value: &dyn AsBareItem| -> Result<(), crate::SignError<E>> {
                result.push(';');
                result.push_str(key);
                result.push('=');
                value.serialize_as_bare_item(result)?;

                Ok(())
            };

        if let Some(value) = &self.created {
            add_param(&mut result, "created", value)?;
        }
        if let Some(value) = &self.expires {
            add_param(&mut result, "expires", value)?;
        }

        if let Some(value) = &self.nonce {
            add_param(&mut result, "nonce", &value)?;
        }
        if let Some(value) = &self.alg {
            add_param(&mut result, "alg", &value)?;
        }
        if let Some(value) = &self.keyid {
            add_param(&mut result, "keyid", &value)?;
        }
        if let Some(value) = &self.tag {
            add_param(&mut result, "tag", &value)?;
        }

        Ok(result)
    }
}

pub struct HttpbisSignature<'a> {
    params: SignatureParams<'a>,
    params_src: Cow<'a, str>,
    covered_components: Cow<'a, [ComponentId<'a>]>,
}

impl<'a> HttpbisSignature<'a> {
    fn create_inner<E: std::fmt::Debug, B>(
        params: SignatureParams,
        covered_components: impl Into<Cow<'a, [ComponentId<'a>]>>,
        src: RequestOrResponse<B>,
        req: Option<&'a http::Request<B>>,
        sign: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
    ) -> Result<Self, crate::SignError<E>> {
        let params_src = params.serialize()?;
        let covered_components = covered_components.into();

        let signature_base = create_signature_base(&params_src, &covered_components, &src, req)?;

        unimplemented!()
    }
}

enum RequestOrResponse<B> {
    Request(http::Request<B>),
    Response(http::Response<B>),
}

impl<B> RequestOrResponse<B> {
    pub fn headers(&self) -> &http::HeaderMap<http::HeaderValue> {
        match self {
            RequestOrResponse::Request(req) => req.headers(),
            RequestOrResponse::Response(res) => res.headers(),
        }
    }
}

trait AsBareItem {
    fn serialize_as_bare_item(&self, result: &mut String) -> Result<(), crate::CommonError>;
    fn is_true(&self) -> bool {
        false
    }
}

impl<'a> AsBareItem for &'a str {
    fn serialize_as_bare_item(&self, result: &mut String) -> Result<(), crate::CommonError> {
        if !self.is_ascii() {
            return Err(crate::CommonError::InvalidCharacter);
        }

        result.push('"');

        for chr in self.chars() {
            if chr == '\\' || chr == '"' {
                result.push('\\');
            }
            result.push(chr);
        }

        result.push('"');

        Ok(())
    }
}

impl AsBareItem for u64 {
    fn serialize_as_bare_item(&self, result: &mut String) -> Result<(), crate::CommonError> {
        use std::fmt::Write;

        write!(result, "{}", self).unwrap();
        Ok(())
    }
}

impl AsBareItem for bool {
    fn serialize_as_bare_item(&self, result: &mut String) -> Result<(), crate::CommonError> {
        if *self {
            result.push_str("?1");
        } else {
            result.push_str("?0");
        }

        Ok(())
    }

    fn is_true(&self) -> bool {
        *self
    }
}

impl AsBareItem for percent_encoding::PercentEncode<'_> {
    fn serialize_as_bare_item(&self, result: &mut String) -> Result<(), crate::CommonError> {
        use std::fmt::Write;
        write!(result, "{}", self).unwrap();
        Ok(())
    }
}

impl<'a> AsBareItem for &Cow<'a, str> {
    fn serialize_as_bare_item(&self, result: &mut String) -> Result<(), crate::CommonError> {
        let value: &str = &self;
        value.serialize_as_bare_item(result)
    }
}

fn create_signature_base<B>(
    params_src: &str,
    covered_components: &[ComponentId<'_>],
    src: &RequestOrResponse<B>,
    req: Option<&http::Request<B>>,
) -> Result<String, crate::CommonError> {
    let mut result = String::new();

    for component in covered_components {
        component.serialize_into(&mut result)?;
        result.push(':');
        result.push(' ');
        component.serialize_value_into(&mut result, src)?;
        result.push('\n');
    }

    result.push_str("\"@signature-params\": (");

    let mut first = true;
    for component in covered_components {
        if first {
            first = false;
        } else {
            result.push(' ');
        }

        component.serialize_into(&mut result)?;
    }

    result.push(')');
    result.push_str(params_src);

    Ok(result)
}

mod test {
    use super::*;

    #[test]
    fn test_params_minimal() {
        let res = SignatureParams::default().serialize::<()>();
        assert_eq!(res.unwrap(), "");
    }

    #[test]
    fn test_params_some() {
        let res = SignatureParams {
            created: Some(1618884475),
            keyid: Some("test-key-rsa-pss".into()),
            ..Default::default()
        }
        .serialize::<()>();
        assert_eq!(
            res.unwrap(),
            r#";created=1618884475;keyid="test-key-rsa-pss""#
        );
    }

    #[test]
    fn test_base_minimal() {
        let req = http::Request::new(());
        let result =
            create_signature_base("", &[], &RequestOrResponse::Request(req), None).unwrap();

        assert_eq!(result, r#""@signature-params": ()"#);
    }

    #[test]
    fn test_base_some() {
        let mut res = http::Response::new(());
        res.headers_mut().insert(
            "content-type",
            http::header::HeaderValue::from_static("application/json"),
        );
        res.headers_mut().insert("content-digest", http::header::HeaderValue::from_static("sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:"));
        res.headers_mut().insert("content-length", 23.into());

        let result = create_signature_base(
            r#";created=1618884473;keyid="test-key-ecc-p256""#,
            &[
                ComponentId::Status,
                ComponentId::HttpField(HttpFieldComponentId::new(http::header::CONTENT_TYPE)),
                ComponentId::HttpField(HttpFieldComponentId::new(
                    http::header::HeaderName::from_static("content-digest"),
                )),
                ComponentId::HttpField(HttpFieldComponentId::new(http::header::CONTENT_LENGTH)),
            ],
            &RequestOrResponse::Response(res),
            None,
        )
        .unwrap();

        assert_eq!(
            result,
            r#""@status": 200
"content-type": application/json
"content-digest": sha-512=:mEWXIS7MaLRuGgxOBdODa3xqM1XdEvxoYhvlCFJ41QJgJc4GTsPp29l5oGX69wWdXymyU0rjJuahq4l5aGgfLQ==:
"content-length": 23
"@signature-params": ("@status" "content-type" "content-digest" "content-length");created=1618884473;keyid="test-key-ecc-p256""#,
        );
    }
}

M src/lib.rs => src/lib.rs +33 -364
@@ 2,7 2,8 @@
//!
//! More details in the `Signature` struct

#![warn(missing_docs)]
pub mod httpbis;
pub mod richanna;

/// Errors that may be produced when parsing a signature header
#[derive(Debug, thiserror::Error)]


@@ 31,6 32,15 @@ pub enum ParseError {
/// Errors that may be produced when creating a signature
#[derive(Debug, thiserror::Error)]
pub enum SignError<T: std::fmt::Debug> {
    #[error("An invalid character was found")]
    InvalidCharacter,

    #[error("Specified parameters cannot be used together")]
    ConflictingParams,

    #[error("A requested component was not found")]
    MissingComponent,

    /// An IO error occurred.
    #[error("IO error occurred")]
    IO(#[from] std::io::Error),


@@ 38,6 48,28 @@ pub enum SignError<T: std::fmt::Debug> {
    /// An error was returned from the provided `sign` function.
    #[error("Failed in user sign call")]
    User(T),

    #[error("Attempted to use an unimplemented feature")]
    Unsupported,
}

#[derive(Debug)]
enum CommonError {
    InvalidCharacter,
    ConflictingParams,
    MissingComponent,
    Unsupported,
}

impl<T: std::fmt::Debug> From<CommonError> for SignError<T> {
    fn from(src: CommonError) -> Self {
        match src {
            CommonError::InvalidCharacter => Self::InvalidCharacter,
            CommonError::ConflictingParams => Self::ConflictingParams,
            CommonError::MissingComponent => Self::MissingComponent,
            CommonError::Unsupported => Self::Unsupported,
        }
    }
}

/// Errors that may be produced when verifying a signature


@@ 51,366 83,3 @@ pub enum VerifyError<T: std::fmt::Debug> {
    #[error("Failed in user verify call")]
    User(T),
}

enum SignatureHeaderName {
    RequestTarget,
    Created,
    Expires,
    NormalHeader(http::header::HeaderName),
}

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)
    }
}

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
    }
}

/// A parsed or generated Signature.
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> {
    /// Construct a signature.
    ///
    /// All headers in `headers` will be included, as well as `(request-target)`, `(created)`, and
    /// `(expires)` (based on `lifetime_secs` parameter)
    ///
    /// The passed `sign` will be called with the body to sign.
    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: &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 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,
        })
    }

    /// Create an old-style signature (no (created) and (expires))
    ///
    /// # Panics
    /// Panics if `headers` doesn't contain a Date header
    pub fn create_legacy<E: std::fmt::Debug>(
        key_id: &'a str,
        request_method: &http::method::Method,
        request_path_and_query: &str,
        headers: &http::header::HeaderMap,
        sign: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
    ) -> Result<Self, SignError<E>> {
        use std::io::Write;

        if !headers.contains_key(http::header::DATE) {
            panic!("legacy signatures must contain Date header");
        }

        let mut body = Vec::new();

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

        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, ", ")?;
                }

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

        let header_names: Vec<_> = std::iter::once(SignatureHeaderName::RequestTarget)
            .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: None,
            expires: None,
            headers: Some(header_names),
            key_id: Some(key_id),
            signature,
        })
    }

    /// Parse a Signature header
    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)?,
        })
    }

    /// Create a Signature header value for the signature.
    pub fn to_header(&self) -> http::header::HeaderValue {
        use std::fmt::Write;
        let mut params = String::new();

        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, "\"").unwrap();

        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, ",signature=\"").unwrap();
        base64::encode_config_buf(&self.signature, base64::STANDARD, &mut params);
        write!(params, "\"").unwrap();

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

    /// Verify the signature for a given request target and HeaderMap.
    ///
    /// The passed `verify` function will be called with (body, signature) where body is the body
    /// that should match the signature.
    pub fn verify<E: std::fmt::Debug>(
        &self,
        request_method: &http::method::Method,
        request_path_and_query: &str,
        headers: &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);
            }
        }

        verify(&body, &self.signature).map_err(VerifyError::User)
    }
}

A src/richanna.rs => src/richanna.rs +368 -0
@@ 0,0 1,368 @@
enum SignatureHeaderName {
    RequestTarget,
    Created,
    Expires,
    NormalHeader(http::header::HeaderName),
}

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)
    }
}

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
    }
}

/// A parsed or generated Signature (draft-richanna-http-message-signatures or draft-cavage-http-signatures).
pub struct RichannaSignature<'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> RichannaSignature<'a> {
    /// Construct a signature (draft-richanna-http-message-signatures).
    ///
    /// All headers in `headers` will be included, as well as `(request-target)`, `(created)`, and
    /// `(expires)` (based on `lifetime_secs` parameter)
    ///
    /// The passed `sign` will be called with the body to sign.
    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: &http::header::HeaderMap,
        sign: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
    ) -> Result<Self, crate::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, ", ")?;
                }

                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(crate::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,
        })
    }

    /// Create an old-style signature (draft-cavage-http-signatures, no (created) and (expires))
    ///
    /// # Panics
    /// Panics if `headers` doesn't contain a Date header
    pub fn create_legacy<E: std::fmt::Debug>(
        key_id: &'a str,
        request_method: &http::method::Method,
        request_path_and_query: &str,
        headers: &http::header::HeaderMap,
        sign: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
    ) -> Result<Self, crate::SignError<E>> {
        use std::io::Write;

        if !headers.contains_key(http::header::DATE) {
            panic!("legacy signatures must contain Date header");
        }

        let mut body = Vec::new();

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

        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, ", ")?;
                }

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

        let header_names: Vec<_> = std::iter::once(SignatureHeaderName::RequestTarget)
            .chain(headers.keys().cloned().map(Into::into))
            .collect();

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

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

    /// Parse a Signature header
    pub fn parse(value: &'a http::header::HeaderValue) -> Result<Self, crate::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(|_| crate::ParseError::InvalidCharacters)?
            .split(',')
        {
            let eqidx = field_src
                .find('=')
                .ok_or(crate::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(|_| crate::ParseError::InvalidCharacters)?,
                    );
                }
                "created" => {
                    created = Some(value.parse().map_err(crate::ParseError::Number)?);
                }
                "expires" => {
                    expires = Some(value.parse().map_err(crate::ParseError::Number)?);
                }
                "headers" => {
                    headers = Some(
                        value
                            .split(' ')
                            .map(|x| x.parse().map_err(|_| crate::ParseError::InvalidCharacters))
                            .collect::<Result<Vec<_>, _>>()?,
                    );
                }
                "key_id" => {
                    key_id = Some(value);
                }
                "signature" => {
                    signature = Some(base64::decode(value).map_err(crate::ParseError::Base64)?);
                }
                _ => {}
            }
        }

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

    /// Create a Signature header value for the signature.
    pub fn to_header(&self) -> http::header::HeaderValue {
        use std::fmt::Write;
        let mut params = String::new();

        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, "\"").unwrap();

        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, ",signature=\"").unwrap();
        base64::encode_config_buf(&self.signature, base64::STANDARD, &mut params);
        write!(params, "\"").unwrap();

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

    /// Verify the signature for a given request target and HeaderMap.
    ///
    /// The passed `verify` function will be called with (body, signature) where body is the body
    /// that should match the signature.
    pub fn verify<E: std::fmt::Debug>(
        &self,
        request_method: &http::method::Method,
        request_path_and_query: &str,
        headers: &http::header::HeaderMap,
        verify: impl FnOnce(&[u8], &[u8]) -> Result<bool, E>,
    ) -> Result<bool, crate::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);
            }
        }

        verify(&body, &self.signature).map_err(crate::VerifyError::User)
    }
}