util: implement Signature header parser

This commit is contained in:
anna 2023-01-26 22:19:53 +01:00
parent 4573bfe7d7
commit f349ae99c7
Signed by: fef
GPG key ID: EC22E476DC2D3D84
2 changed files with 187 additions and 3 deletions

View file

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

View 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()
);
}
}