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(¶ms_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)
+ }
+}