Add ability to require headers in signature

This commit is contained in:
asonix 2020-04-23 12:54:56 -05:00
parent 08686beb8f
commit 90660b7f19
13 changed files with 131 additions and 54 deletions

View file

@ -1,7 +1,7 @@
[package] [package]
name = "http-signature-normalization" name = "http-signature-normalization"
description = "An HTTP Signatures library that leaves the signing to you" description = "An HTTP Signatures library that leaves the signing to you"
version = "0.4.2" version = "0.5.0"
authors = ["asonix <asonix@asonix.dog>"] authors = ["asonix <asonix@asonix.dog>"]
license-file = "LICENSE" license-file = "LICENSE"
readme = "README.md" readme = "README.md"

View file

@ -31,7 +31,7 @@ base64 = { version = "0.12", optional = true }
bytes = "0.5.4" bytes = "0.5.4"
chrono = "0.4.6" chrono = "0.4.6"
futures = "0.3" futures = "0.3"
http-signature-normalization = { version = "0.4.2", path = ".." } http-signature-normalization = { version = "0.5.0", path = ".." }
log = "0.4" log = "0.4"
sha2 = { version = "0.8", optional = true } sha2 = { version = "0.8", optional = true }
sha3 = { version = "0.8", optional = true } sha3 = { version = "0.8", optional = true }

View file

@ -9,6 +9,7 @@ async fn request(config: Config) -> Result<(), Box<dyn std::error::Error>> {
let mut response = Client::default() let mut response = Client::default()
.post("http://127.0.0.1:8010/") .post("http://127.0.0.1:8010/")
.header("User-Agent", "Actix Web") .header("User-Agent", "Actix Web")
.header("Accept", "text/plain")
.set(actix_web::http::header::Date(SystemTime::now().into())) .set(actix_web::http::header::Date(SystemTime::now().into()))
.signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| { .signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| {
println!("Signing String\n{}", s); println!("Signing String\n{}", s);
@ -36,7 +37,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
std::env::set_var("RUST_LOG", "info"); std::env::set_var("RUST_LOG", "info");
pretty_env_logger::init(); pretty_env_logger::init();
let config = Config::default(); let config = Config::default().require_header("accept");
request(config.clone()).await?; request(config.clone()).await?;
request(config.dont_use_created_field()).await?; request(config.dont_use_created_field()).await?;
@ -45,8 +46,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum MyError { pub enum MyError {
#[error("Failed to read header, {0}")] #[error("Failed to create signing string, {0}")]
Convert(#[from] ToStrError), Convert(#[from] PrepareSignError),
#[error("Failed to create header, {0}")] #[error("Failed to create header, {0}")]
Header(#[from] InvalidHeaderValue), Header(#[from] InvalidHeaderValue),

View file

@ -56,7 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
std::env::set_var("RUST_LOG", "info"); std::env::set_var("RUST_LOG", "info");
pretty_env_logger::init(); pretty_env_logger::init();
let config = Config::default(); let config = Config::default().require_header("accept");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
@ -74,7 +74,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
enum MyError { enum MyError {
#[error("Failed to verify, {}", _0)] #[error("Failed to verify, {0}")]
Verify(#[from] PrepareVerifyError), Verify(#[from] PrepareVerifyError),
#[error("Unsupported algorithm")] #[error("Unsupported algorithm")]

View file

@ -8,11 +8,11 @@ use actix_web::{
client::{ClientRequest, ClientResponse, SendRequestError}, client::{ClientRequest, ClientResponse, SendRequestError},
dev::Payload, dev::Payload,
error::BlockingError, error::BlockingError,
http::header::{InvalidHeaderValue, ToStrError}, http::header::InvalidHeaderValue,
}; };
use std::{fmt::Display, future::Future, pin::Pin}; use std::{fmt::Display, future::Future, pin::Pin};
use crate::{Config, Sign}; use crate::{Config, PrepareSignError, Sign};
pub mod middleware; pub mod middleware;
#[cfg(feature = "sha-2")] #[cfg(feature = "sha-2")]
@ -57,7 +57,7 @@ pub trait SignExt: Sign {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send
@ -79,7 +79,7 @@ pub trait SignExt: Sign {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send

View file

@ -1,14 +1,11 @@
use actix_web::{ use actix_web::{
client::ClientRequest, client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web,
error::BlockingError,
http::header::{InvalidHeaderValue, ToStrError},
web,
}; };
use std::{fmt::Display, future::Future, pin::Pin}; use std::{fmt::Display, future::Future, pin::Pin};
use crate::{ use crate::{
digest::{DigestClient, DigestCreate, SignExt}, digest::{DigestClient, DigestCreate, SignExt},
Config, Sign, Config, PrepareSignError, Sign,
}; };
impl SignExt for ClientRequest { impl SignExt for ClientRequest {
@ -23,7 +20,7 @@ impl SignExt for ClientRequest {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send
@ -60,7 +57,7 @@ impl SignExt for ClientRequest {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send

View file

@ -135,8 +135,8 @@
//! //!
//! #[derive(Debug, thiserror::Error)] //! #[derive(Debug, thiserror::Error)]
//! pub enum MyError { //! pub enum MyError {
//! #[error("Failed to read header, {0}")] //! #[error("Failed to create signing string, {0}")]
//! Convert(#[from] ToStrError), //! Convert(#[from] PrepareSignError),
//! //!
//! #[error("Failed to create header, {0}")] //! #[error("Failed to create header, {0}")]
//! Header(#[from] InvalidHeaderValue), //! Header(#[from] InvalidHeaderValue),
@ -180,12 +180,14 @@ pub mod digest;
pub mod create; pub mod create;
pub mod middleware; pub mod middleware;
pub use http_signature_normalization::RequiredError;
/// Useful types and traits for using this library in Actix Web /// Useful types and traits for using this library in Actix Web
pub mod prelude { pub mod prelude {
pub use crate::{ pub use crate::{
middleware::{SignatureVerified, VerifySignature}, middleware::{SignatureVerified, VerifySignature},
verify::{Algorithm, DeprecatedAlgorithm, Unverified}, verify::{Algorithm, DeprecatedAlgorithm, Unverified},
Config, PrepareVerifyError, Sign, SignatureVerify, Config, PrepareSignError, PrepareVerifyError, RequiredError, Sign, SignatureVerify,
}; };
#[cfg(feature = "digest")] #[cfg(feature = "digest")]
@ -242,7 +244,7 @@ pub trait Sign {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send
@ -260,7 +262,7 @@ pub trait Sign {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send
@ -297,6 +299,22 @@ pub enum PrepareVerifyError {
#[error("Failed to read header, {0}")] #[error("Failed to read header, {0}")]
/// An error converting the header to a string for validation /// An error converting the header to a string for validation
Header(#[from] ToStrError), Header(#[from] ToStrError),
#[error("{0}")]
/// Required headers were missing from request
Required(#[from] RequiredError),
}
#[derive(Debug, thiserror::Error)]
/// An error when preparing to sign a request
pub enum PrepareSignError {
#[error("Failed to read header, {0}")]
/// An error occurred when reading the request's headers
Header(#[from] ToStrError),
#[error("{0}")]
/// Some headers were marked as required, but are missing
RequiredError(#[from] RequiredError),
} }
impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyError { impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyError {
@ -311,6 +329,9 @@ impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyErr
hsn::verify::ValidateError::Missing => PrepareVerifyError::Missing, hsn::verify::ValidateError::Missing => PrepareVerifyError::Missing,
hsn::verify::ValidateError::Expired => PrepareVerifyError::Expired, hsn::verify::ValidateError::Expired => PrepareVerifyError::Expired,
}, },
hsn::PrepareVerifyError::Required(required_error) => {
PrepareVerifyError::Required(required_error)
}
} }
} }
} }
@ -337,13 +358,20 @@ impl Config {
} }
} }
/// Require a header on signed and verified requests
pub fn require_header(self, header: &str) -> Self {
Config {
config: self.config.require_header(header),
}
}
/// Begin the process of singing a request /// Begin the process of singing a request
pub fn begin_sign( pub fn begin_sign(
&self, &self,
method: &Method, method: &Method,
path_and_query: Option<&PathAndQuery>, path_and_query: Option<&PathAndQuery>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Unsigned, ToStrError> { ) -> Result<Unsigned, PrepareSignError> {
let headers = headers let headers = headers
.iter() .iter()
.map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string()))) .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
@ -355,7 +383,7 @@ impl Config {
let unsigned = self let unsigned = self
.config .config
.begin_sign(&method.to_string(), &path_and_query, headers); .begin_sign(&method.to_string(), &path_and_query, headers)?;
Ok(Unsigned { unsigned }) Ok(Unsigned { unsigned })
} }

View file

@ -124,6 +124,10 @@ where
debug!("Failed to parse header {}", e); debug!("Failed to parse header {}", e);
return Box::pin(err(VerifyError.into())); return Box::pin(err(VerifyError.into()));
} }
Err(PrepareVerifyError::Required(req)) => {
debug!("Missing required headers, {:?}", req);
return Box::pin(err(VerifyError.into()));
}
}; };
let algorithm = unverified.algorithm().map(|a| a.clone()); let algorithm = unverified.algorithm().map(|a| a.clone());

View file

@ -1,12 +1,9 @@
use actix_web::{ use actix_web::{
client::ClientRequest, client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web,
error::BlockingError,
http::header::{InvalidHeaderValue, ToStrError},
web,
}; };
use std::{fmt::Display, future::Future, pin::Pin}; use std::{fmt::Display, future::Future, pin::Pin};
use crate::{create::Signed, Config, Sign}; use crate::{create::Signed, Config, PrepareSignError, Sign};
impl Sign for ClientRequest { impl Sign for ClientRequest {
fn authorization_signature<F, E, K>( fn authorization_signature<F, E, K>(
@ -18,7 +15,7 @@ impl Sign for ClientRequest {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send
@ -42,7 +39,7 @@ impl Sign for ClientRequest {
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> E: From<BlockingError<E>>
+ From<ToStrError> + From<PrepareSignError>
+ From<InvalidHeaderValue> + From<InvalidHeaderValue>
+ std::fmt::Debug + std::fmt::Debug
+ Send + Send
@ -66,7 +63,7 @@ async fn prepare<F, E, K>(
) -> Result<Signed, E> ) -> Result<Signed, E>
where where
F: FnOnce(&str) -> Result<String, E> + Send + 'static, F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>> + From<ToStrError> + std::fmt::Debug + Send + 'static, E: From<BlockingError<E>> + From<PrepareSignError> + std::fmt::Debug + Send + 'static,
K: Display, K: Display,
{ {
let unsigned = config.begin_sign( let unsigned = config.begin_sign(

View file

@ -13,4 +13,4 @@ edition = "2018"
[dependencies] [dependencies]
http = "0.2" http = "0.2"
http-signature-normalization = { version = "0.4.0", path = ".." } http-signature-normalization = { version = "0.5.0", path = ".." }

View file

@ -16,7 +16,7 @@ bytes = "0.5.3"
futures = "0.3.1" futures = "0.3.1"
chrono = "0.4.10" chrono = "0.4.10"
http = "0.2.0" http = "0.2.0"
http-signature-normalization = { version = "0.4.0", path = ".." } http-signature-normalization = { version = "0.5.0", path = ".." }
reqwest = "0.10.1" reqwest = "0.10.1"
serde = { version = "1.0.104", features = ["derive"], optional = true } serde = { version = "1.0.104", features = ["derive"], optional = true }
serde_json = { version = "1.0.44", optional = true } serde_json = { version = "1.0.44", optional = true }

View file

@ -20,7 +20,7 @@
//! let headers = BTreeMap::new(); //! let headers = BTreeMap::new();
//! //!
//! let signature_header_value = config //! let signature_header_value = config
//! .begin_sign("GET", "/foo?bar=baz", headers) //! .begin_sign("GET", "/foo?bar=baz", headers)?
//! .sign("my-key-id".to_owned(), |signing_string| { //! .sign("my-key-id".to_owned(), |signing_string| {
//! // sign the string here //! // sign the string here
//! Ok(signing_string.to_owned()) as Result<_, Box<dyn std::error::Error>> //! Ok(signing_string.to_owned()) as Result<_, Box<dyn std::error::Error>>
@ -43,7 +43,7 @@
//! ``` //! ```
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashSet};
pub mod create; pub mod create;
pub mod verify; pub mod verify;
@ -73,6 +73,7 @@ const SIGNATURE_FIELD: &'static str = "signature";
pub struct Config { pub struct Config {
expires_after: Duration, expires_after: Duration,
use_created_field: bool, use_created_field: bool,
required_headers: HashSet<String>,
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -87,8 +88,17 @@ pub enum PrepareVerifyError {
#[error("{0}")] #[error("{0}")]
/// Error parsing the header /// Error parsing the header
Parse(#[from] ParseSignatureError), Parse(#[from] ParseSignatureError),
#[error("{0}")]
/// Missing required headers
Required(#[from] RequiredError),
} }
#[derive(Debug, thiserror::Error)]
#[error("Missing required headers {0:?}")]
/// Failed to build a signing string due to missing required headers
pub struct RequiredError(HashSet<String>);
impl Config { impl Config {
/// Create a new Config with a default expiration of 10 seconds /// Create a new Config with a default expiration of 10 seconds
pub fn new() -> Self { pub fn new() -> Self {
@ -97,10 +107,13 @@ impl Config {
/// Opt out of using the (created) and (expires) fields introduced in draft 11 /// Opt out of using the (created) and (expires) fields introduced in draft 11
/// ///
/// Use this for compatibility with mastodon /// Use this for compatibility with mastodon.
///
/// Note that by not requiring the created field, the Date header becomes required. This is to
/// prevent replay attacks.
pub fn dont_use_created_field(mut self) -> Self { pub fn dont_use_created_field(mut self) -> Self {
self.use_created_field = false; self.use_created_field = false;
self self.require_header("date")
} }
/// Set the expiration to a custom duration /// Set the expiration to a custom duration
@ -109,6 +122,13 @@ impl Config {
self self
} }
/// Mark a header as required
pub fn require_header(mut self, header: &str) -> Self {
self.required_headers
.insert(header.to_lowercase().to_owned());
self
}
/// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to /// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to
/// sign the header /// sign the header
pub fn begin_sign( pub fn begin_sign(
@ -116,7 +136,7 @@ impl Config {
method: &str, method: &str,
path_and_query: &str, path_and_query: &str,
headers: BTreeMap<String, String>, headers: BTreeMap<String, String>,
) -> Unsigned { ) -> Result<Unsigned, RequiredError> {
let mut headers = headers let mut headers = headers
.into_iter() .into_iter()
.map(|(k, v)| (k.to_lowercase(), v)) .map(|(k, v)| (k.to_lowercase(), v))
@ -140,14 +160,15 @@ impl Config {
expires, expires,
&sig_headers, &sig_headers,
&mut headers, &mut headers,
); self.required_headers.clone(),
)?;
Unsigned { Ok(Unsigned {
signing_string, signing_string,
sig_headers, sig_headers,
created, created,
expires, expires,
} })
} }
/// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to /// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to
@ -169,7 +190,12 @@ impl Config {
.ok_or(ValidateError::Missing)?; .ok_or(ValidateError::Missing)?;
let parsed_header: ParsedHeader = header.parse()?; let parsed_header: ParsedHeader = header.parse()?;
let unvalidated = parsed_header.into_unvalidated(method, path_and_query, &mut headers); let unvalidated = parsed_header.into_unvalidated(
method,
path_and_query,
&mut headers,
self.required_headers.clone(),
)?;
Ok(unvalidated.validate(self.expires_after)?) Ok(unvalidated.validate(self.expires_after)?)
} }
@ -200,7 +226,16 @@ fn build_signing_string(
expires: Option<DateTime<Utc>>, expires: Option<DateTime<Utc>>,
sig_headers: &[String], sig_headers: &[String],
btm: &mut BTreeMap<String, String>, btm: &mut BTreeMap<String, String>,
) -> String { mut required_headers: HashSet<String>,
) -> Result<String, RequiredError> {
for key in btm.keys() {
required_headers.remove(key);
}
if !required_headers.is_empty() {
return Err(RequiredError(required_headers));
}
let request_target = format!("{} {}", method.to_string().to_lowercase(), path_and_query); let request_target = format!("{} {}", method.to_string().to_lowercase(), path_and_query);
btm.insert(REQUEST_TARGET.to_owned(), request_target.clone()); btm.insert(REQUEST_TARGET.to_owned(), request_target.clone());
@ -217,7 +252,7 @@ fn build_signing_string(
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");
signing_string Ok(signing_string)
} }
impl Default for Config { impl Default for Config {
@ -225,6 +260,7 @@ impl Default for Config {
Config { Config {
expires_after: Duration::seconds(10), expires_after: Duration::seconds(10),
use_created_field: true, use_created_field: true,
required_headers: HashSet::new(),
} }
} }
} }
@ -243,13 +279,24 @@ mod tests {
headers headers
} }
#[test]
fn required_header() {
let headers = prepare_headers();
let config = Config::default().require_header("date");
let res = config.begin_sign("GET", "/foo?bar=baz", headers);
assert!(res.is_err())
}
#[test] #[test]
fn round_trip_authorization() { fn round_trip_authorization() {
let headers = prepare_headers(); let headers = prepare_headers();
let config = Config::default(); let config = Config::default().require_header("content-type");
let authorization_header = config let authorization_header = config
.begin_sign("GET", "/foo?bar=baz", headers) .begin_sign("GET", "/foo?bar=baz", headers)
.unwrap()
.sign("hi".to_owned(), |s| { .sign("hi".to_owned(), |s| {
Ok(s.to_owned()) as Result<_, std::io::Error> Ok(s.to_owned()) as Result<_, std::io::Error>
}) })
@ -274,6 +321,7 @@ mod tests {
let signature_header = config let signature_header = config
.begin_sign("GET", "/foo?bar=baz", headers) .begin_sign("GET", "/foo?bar=baz", headers)
.unwrap()
.sign("hi".to_owned(), |s| { .sign("hi".to_owned(), |s| {
Ok(s.to_owned()) as Result<_, std::io::Error> Ok(s.to_owned()) as Result<_, std::io::Error>
}) })

View file

@ -1,15 +1,15 @@
//! Types and methods to verify a signature or authorization header //! Types and methods to verify a signature or authorization header
use chrono::{DateTime, Duration, TimeZone, Utc}; use chrono::{DateTime, Duration, TimeZone, Utc};
use std::{ use std::{
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap, HashSet},
error::Error, error::Error,
fmt, fmt,
str::FromStr, str::FromStr,
}; };
use crate::{ use crate::{
build_signing_string, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD, HEADERS_FIELD, build_signing_string, RequiredError, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD,
KEY_ID_FIELD, SIGNATURE_FIELD, HEADERS_FIELD, KEY_ID_FIELD, SIGNATURE_FIELD,
}; };
#[derive(Debug)] #[derive(Debug)]
@ -214,7 +214,8 @@ impl ParsedHeader {
method: &str, method: &str,
path_and_query: &str, path_and_query: &str,
headers: &mut BTreeMap<String, String>, headers: &mut BTreeMap<String, String>,
) -> Unvalidated { required_headers: HashSet<String>,
) -> Result<Unvalidated, RequiredError> {
let date = headers.get("date").cloned(); let date = headers.get("date").cloned();
let signing_string = build_signing_string( let signing_string = build_signing_string(
@ -224,9 +225,10 @@ impl ParsedHeader {
self.expires, self.expires,
&self.headers, &self.headers,
headers, headers,
); required_headers,
)?;
Unvalidated { Ok(Unvalidated {
key_id: self.key_id, key_id: self.key_id,
signature: self.signature, signature: self.signature,
parsed_at: self.parsed_at, parsed_at: self.parsed_at,
@ -235,7 +237,7 @@ impl ParsedHeader {
expires: self.expires, expires: self.expires,
date, date,
signing_string, signing_string,
} })
} }
} }