util: implement Signature header parser
This commit is contained in:
parent
4573bfe7d7
commit
f349ae99c7
2 changed files with 187 additions and 3 deletions
|
@ -10,6 +10,8 @@ use crate::util::transcode;
|
|||
pub mod content_type;
|
||||
/// Parser for the `Link` header.
|
||||
pub mod link;
|
||||
/// Parser for the `Signature` header.
|
||||
pub mod signature;
|
||||
|
||||
/// Parse an HTTP Structured Field Value according to
|
||||
/// [RFC 8941](https://www.rfc-editor.org/info/rfc8941).
|
||||
|
@ -36,6 +38,7 @@ pub struct ParseOptions {
|
|||
allow_utf8: bool,
|
||||
allow_url: bool,
|
||||
allow_param_bws: bool,
|
||||
allow_upper_keys: bool,
|
||||
max_dict_members: usize,
|
||||
max_list_members: usize,
|
||||
max_inner_list_members: usize,
|
||||
|
@ -56,6 +59,7 @@ impl ParseOptions {
|
|||
allow_utf8: false,
|
||||
allow_url: false,
|
||||
allow_param_bws: false,
|
||||
allow_upper_keys: false,
|
||||
max_dict_members: 1024,
|
||||
max_list_members: 1024,
|
||||
max_inner_list_members: 256,
|
||||
|
@ -73,6 +77,12 @@ impl ParseOptions {
|
|||
.allow_param_bws(true)
|
||||
}
|
||||
|
||||
/// Return parser options suitable for parsing the HTTP `Signature` header as defined in the
|
||||
/// [IETF draft for HTTP signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12#section-4).
|
||||
pub fn signature_header() -> Self {
|
||||
Self::default().strict(false).allow_upper_keys(true)
|
||||
}
|
||||
|
||||
/// Enable strict mode, i.e. fully comply with RFC 8941 (except for the
|
||||
/// multiline header thing; consumers of this utility MUST call the parser
|
||||
/// on every header value with the same name manually).
|
||||
|
@ -112,6 +122,13 @@ impl ParseOptions {
|
|||
self
|
||||
}
|
||||
|
||||
/// Allow keys in Dictionaries and Parameters to contain uppercase characters.
|
||||
/// Strict mode implies this is `false`.
|
||||
pub fn allow_upper_keys(mut self, allow_upper_keys: bool) -> Self {
|
||||
self.allow_upper_keys = allow_upper_keys;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of members to allow in a Dictionary (minimum 1).
|
||||
/// Strict mode implies this is no less than 1024.
|
||||
pub fn max_dict_members(mut self, max_dict_members: usize) -> Self {
|
||||
|
@ -155,6 +172,10 @@ impl ParseOptions {
|
|||
debug!("Strict mode enabled, overriding allow_param_bws to false");
|
||||
self.allow_param_bws = false;
|
||||
}
|
||||
if self.allow_upper_keys {
|
||||
debug!("Strict mode enabled, overriding allow_upper_keys to false");
|
||||
self.allow_upper_keys = false;
|
||||
}
|
||||
if self.max_dict_members < 1024 {
|
||||
debug!("Strict mode enabled, overriding max_dict_members to 1024");
|
||||
self.max_dict_members = 1024;
|
||||
|
@ -248,9 +269,23 @@ impl<'a> Dictionary<'a> {
|
|||
self.0.iter().find_map(|(k, v)| key.eq(*k).then_some(v))
|
||||
}
|
||||
|
||||
pub fn get_nocase<'k, K>(&self, key: K) -> Option<&Member<'a>>
|
||||
where
|
||||
K: Into<&'k str>,
|
||||
{
|
||||
let key = key.into();
|
||||
self.0
|
||||
.iter()
|
||||
.find_map(|(k, v)| key.eq_ignore_ascii_case(k).then_some(v))
|
||||
}
|
||||
|
||||
pub fn nth(&self, index: usize) -> Option<&(&'a str, Member<'a>)> {
|
||||
self.0.get(index)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> std::slice::Iter<(&'a str, Member<'a>)> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ParseHeader<'a> for List<'a> {
|
||||
|
@ -487,7 +522,7 @@ impl<'a> InnerList<'a> {
|
|||
self.items.get(index)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
|
||||
pub fn iter(&self) -> std::slice::Iter<Item<'a>> {
|
||||
self.items.iter()
|
||||
}
|
||||
|
||||
|
@ -753,8 +788,15 @@ impl<'a> Parser<'a> {
|
|||
/// ```
|
||||
fn parse_key(&mut self) -> Result<&'a str> {
|
||||
self.chop();
|
||||
self.assert_next(is_key_start)?;
|
||||
self.skip_while(is_key_part);
|
||||
|
||||
if self.options.allow_upper_keys {
|
||||
self.assert_next(|c| is_key_start(c.to_ascii_lowercase()))?;
|
||||
self.skip_while(|c| is_key_part(c.to_ascii_lowercase()));
|
||||
} else {
|
||||
self.assert_next(is_key_start)?;
|
||||
self.skip_while(is_key_part);
|
||||
}
|
||||
|
||||
Ok(self.chop())
|
||||
}
|
||||
|
||||
|
|
142
src/util/http/header/signature.rs
Normal file
142
src/util/http/header/signature.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use crate::util::transcode::base64_decode;
|
||||
use bytes::Bytes;
|
||||
use reqwest::header::HeaderValue;
|
||||
use std::ops::Not;
|
||||
|
||||
use super::{Dictionary, ParseHeader, ParseOptions};
|
||||
|
||||
pub struct SignatureHeader {
|
||||
key_id: String,
|
||||
signature: Bytes,
|
||||
algorithm: Option<String>,
|
||||
created: Option<i64>,
|
||||
expires: Option<i64>,
|
||||
headers: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl SignatureHeader {
|
||||
pub fn from_header(value: &HeaderValue) -> Option<Self> {
|
||||
let dict = Dictionary::parse_from_header(value, ParseOptions::signature_header()).ok()?;
|
||||
|
||||
let key_id = dict.get_nocase("keyid").and_then(|val| val.as_string())?;
|
||||
let signature = dict.get_nocase("signature").and_then(|val| {
|
||||
val.as_byte_sequence()
|
||||
.or_else(|| val.as_string().and_then(|s| base64_decode(&s).ok()))
|
||||
})?;
|
||||
let algorithm = dict.get_nocase("algorithm").and_then(|val| val.as_string());
|
||||
let created = dict.get_nocase("created").and_then(|val| val.as_integer());
|
||||
let expires = dict.get_nocase("expires").and_then(|val| val.as_integer());
|
||||
let headers = dict.get_nocase("headers").and_then(|val| {
|
||||
val.as_string()
|
||||
.map(|list| {
|
||||
list.split(' ')
|
||||
.filter_map(|s| s.is_empty().not().then(|| String::from(s)))
|
||||
.collect()
|
||||
})
|
||||
.or_else(|| {
|
||||
val.as_list()
|
||||
.map(|list| list.iter().filter_map(|e| e.as_string()).collect())
|
||||
})
|
||||
});
|
||||
|
||||
Some(SignatureHeader {
|
||||
key_id,
|
||||
signature,
|
||||
algorithm,
|
||||
created,
|
||||
expires,
|
||||
headers,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn key_id(&self) -> &str {
|
||||
self.key_id.as_str()
|
||||
}
|
||||
|
||||
pub fn signature(&self) -> &[u8] {
|
||||
self.signature.as_ref()
|
||||
}
|
||||
|
||||
pub fn algorithm(&self) -> Option<&str> {
|
||||
self.algorithm.as_deref()
|
||||
}
|
||||
|
||||
pub fn created(&self) -> Option<i64> {
|
||||
self.created
|
||||
}
|
||||
|
||||
pub fn expires(&self) -> Option<i64> {
|
||||
self.expires
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> &[String] {
|
||||
self.headers.as_deref().unwrap_or(&[])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn mksig(header: &'static str) -> Option<SignatureHeader> {
|
||||
SignatureHeader::from_header(&HeaderValue::from_str(header).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_signature1() {
|
||||
let header = mksig(r#"keyId="https://my-example.com/actor#main-key",headers="(request-target) host date",signature="dGhpcyBpcyBub3QgYW4gYWN0dWFsIHNpZ25hdHVyZSBsbWFvCg==""#).unwrap();
|
||||
assert_eq!(header.key_id(), "https://my-example.com/actor#main-key");
|
||||
assert_eq!(header.headers(), &["(request-target)", "host", "date"]);
|
||||
assert_eq!(
|
||||
header.signature(),
|
||||
base64_decode("dGhpcyBpcyBub3QgYW4gYWN0dWFsIHNpZ25hdHVyZSBsbWFvCg==")
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
);
|
||||
assert!(header.algorithm().is_none());
|
||||
assert!(header.created().is_none());
|
||||
assert!(header.expires().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_signature2() {
|
||||
let header = mksig(r#"keyid="rsa-key-1", algorithm="hs2019", created=1402170695, expires=1402170995, headers="(request-target) (created) (expires) host date digest content-length", signature="aGkgaW0gZ2F5IHV3dQo=""#).unwrap();
|
||||
assert_eq!(header.key_id(), "rsa-key-1");
|
||||
assert_eq!(header.algorithm(), Some("hs2019"));
|
||||
assert_eq!(header.created(), Some(1402170695));
|
||||
assert_eq!(header.expires(), Some(1402170995));
|
||||
assert_eq!(
|
||||
header.headers(),
|
||||
&[
|
||||
"(request-target)",
|
||||
"(created)",
|
||||
"(expires)",
|
||||
"host",
|
||||
"date",
|
||||
"digest",
|
||||
"content-length"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
header.signature(),
|
||||
base64_decode("aGkgaW0gZ2F5IHV3dQo=").unwrap().as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_signature3() {
|
||||
let header = mksig(
|
||||
r#"keyid="a", headers=("@request-target" "digest"), signature=:aGkgaW0gZ2F5IHV3dQo=:"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(header.key_id(), "a");
|
||||
assert_eq!(header.algorithm(), None);
|
||||
assert_eq!(header.created(), None);
|
||||
assert_eq!(header.expires(), None);
|
||||
assert_eq!(header.headers(), &["@request-target", "digest"]);
|
||||
assert_eq!(
|
||||
header.signature(),
|
||||
base64_decode("aGkgaW0gZ2F5IHV3dQo=").unwrap().as_ref()
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue