use sfv::{BareItem, Dictionary, ListEntry, Parser};
use crate::error::Error;
use crate::rfc9421::components::Component;
pub const SIGNATURE_INPUT_HEADER: &str = "signature-input";
mod param {
pub const KEYID: &str = "keyid";
pub const ALG: &str = "alg";
pub const CREATED: &str = "created";
pub const EXPIRES: &str = "expires";
pub const NONCE: &str = "nonce";
pub const TAG: &str = "tag";
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SignatureInput {
pub components: Vec<Component>,
pub keyid: Option<String>,
pub algorithm: Option<String>,
pub created: Option<i64>,
pub expires: Option<i64>,
pub nonce: Option<String>,
pub tag: Option<String>,
}
impl SignatureInput {
#[must_use]
pub const fn new(components: Vec<Component>) -> Self {
Self {
components,
keyid: None,
algorithm: None,
created: None,
expires: None,
nonce: None,
tag: None,
}
}
#[must_use]
pub fn with_keyid(mut self, keyid: impl Into<String>) -> Self {
self.keyid = Some(keyid.into());
self
}
#[must_use]
pub fn with_algorithm(mut self, algorithm: impl Into<String>) -> Self {
self.algorithm = Some(algorithm.into());
self
}
#[must_use]
pub const fn with_created(mut self, created: i64) -> Self {
self.created = Some(created);
self
}
#[must_use]
pub const fn with_expires(mut self, expires: i64) -> Self {
self.expires = Some(expires);
self
}
#[must_use]
pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
#[must_use]
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
#[must_use]
#[allow(
clippy::expect_used,
reason = "serialising a well-formed InnerList cannot fail"
)]
pub fn serialise_inner_list(&self) -> String {
use core::fmt::Write as _;
let mut out = String::new();
out.push('(');
for (i, c) in self.components.iter().enumerate() {
if i > 0 {
out.push(' ');
}
out.push_str(&c.lexical());
}
out.push(')');
let infallible = "writing to an owned String is infallible";
if let Some(c) = self.created {
write!(out, ";created={c}").expect(infallible);
}
if let Some(e) = self.expires {
write!(out, ";expires={e}").expect(infallible);
}
if let Some(n) = &self.nonce {
write!(out, r#";nonce="{n}""#).expect(infallible);
}
if let Some(alg) = &self.algorithm {
write!(out, r#";alg="{alg}""#).expect(infallible);
}
if let Some(keyid) = &self.keyid {
write!(out, r#";keyid="{keyid}""#).expect(infallible);
}
if let Some(t) = &self.tag {
write!(out, r#";tag="{t}""#).expect(infallible);
}
out
}
}
pub fn parse_signature_input_dict(raw: &str) -> Result<Vec<(String, SignatureInput)>, Error> {
let dict: Dictionary =
Parser::new(raw)
.parse()
.map_err(|e: sfv::Error| Error::InvalidHeader {
name: SIGNATURE_INPUT_HEADER,
reason: e.to_string(),
})?;
let mut out = Vec::with_capacity(dict.len());
for (label, entry) in dict {
let inner_list = match entry {
ListEntry::InnerList(il) => il,
ListEntry::Item(_) => {
return Err(Error::MalformedSignatureHeader(format!(
"entry `{label}` must be an inner list of components"
)));
}
};
let components: Vec<Component> = inner_list
.items
.iter()
.map(|item| {
let BareItem::String(s) = &item.bare_item else {
return Err(Error::MalformedSignatureHeader(format!(
"entry `{label}` contains a non-string component"
)));
};
Component::parse(s.as_str())
})
.collect::<Result<_, _>>()?;
let label_str = label.as_str();
let mut input = SignatureInput {
components,
keyid: None,
algorithm: None,
created: None,
expires: None,
nonce: None,
tag: None,
};
for (pname, pvalue) in &inner_list.params {
match pname.as_str() {
param::KEYID => input.keyid = string_param(pvalue, label_str, param::KEYID)?,
param::ALG => input.algorithm = string_param(pvalue, label_str, param::ALG)?,
param::CREATED => {
input.created = integer_param(pvalue, label_str, param::CREATED)?;
}
param::EXPIRES => {
input.expires = integer_param(pvalue, label_str, param::EXPIRES)?;
}
param::NONCE => input.nonce = string_param(pvalue, label_str, param::NONCE)?,
param::TAG => input.tag = string_param(pvalue, label_str, param::TAG)?,
_ => {
}
}
}
out.push((label.into(), input));
}
Ok(out)
}
fn string_param(value: &BareItem, label: &str, param: &str) -> Result<Option<String>, Error> {
match value {
BareItem::String(s) => Ok(Some(s.as_str().to_owned())),
_ => Err(Error::MalformedSignatureHeader(format!(
"entry `{label}` has non-string `{param}` parameter"
))),
}
}
fn integer_param(value: &BareItem, label: &str, param: &str) -> Result<Option<i64>, Error> {
match value {
BareItem::Integer(n) => Ok(Some(i64::from(*n))),
_ => Err(Error::MalformedSignatureHeader(format!(
"entry `{label}` has non-integer `{param}` parameter"
))),
}
}
#[must_use]
#[allow(
clippy::expect_used,
reason = "serialising a well-formed sf-dictionary cannot fail"
)]
pub fn serialise_signature_input_dict(entries: &[(String, SignatureInput)]) -> String {
let mut out = String::new();
for (i, (label, input)) in entries.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(label);
out.push('=');
out.push_str(&input.serialise_inner_list());
}
out
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn serialise_matches_rfc9421_example() {
let input = SignatureInput::new(vec![
Component::Method,
Component::TargetUri,
Component::Header("host".into()),
Component::Header("date".into()),
])
.with_keyid("test-key-rsa")
.with_created(1_618_884_473);
let dict = serialise_signature_input_dict(&[("sig1".into(), input)]);
assert_eq!(
dict,
r#"sig1=("@method" "@target-uri" "host" "date");created=1618884473;keyid="test-key-rsa""#,
);
}
#[test]
fn parse_roundtrips_through_serialise() {
let input = SignatureInput::new(vec![Component::Method, Component::Authority])
.with_keyid("kid")
.with_algorithm("ed25519")
.with_created(1_700_000_000)
.with_expires(1_700_000_600)
.with_nonce("abc")
.with_tag("mastodon");
let wire = serialise_signature_input_dict(&[("sig".into(), input.clone())]);
let parsed = parse_signature_input_dict(&wire).expect("parse");
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].0, "sig");
assert_eq!(parsed[0].1, input);
}
#[test]
fn entry_of_wrong_shape_is_rejected() {
let wire = "sig1=123";
let err = parse_signature_input_dict(wire).expect_err("wrong shape");
assert!(matches!(err, Error::MalformedSignatureHeader(_)));
}
#[test]
fn unknown_parameters_are_tolerated() {
let wire = r#"sig1=("@method");keyid="kid";future_param=42"#;
let parsed = parse_signature_input_dict(wire).expect("parse");
assert_eq!(parsed[0].1.keyid.as_deref(), Some("kid"));
}
#[test]
fn non_string_component_is_rejected() {
let wire = "sig1=(foo)";
let err = parse_signature_input_dict(wire).expect_err("non-string component");
assert!(matches!(err, Error::MalformedSignatureHeader(_)));
}
}