1use sfv::{BareItem, Dictionary, ListEntry, Parser};
15
16use crate::error::Error;
17use crate::rfc9421::components::Component;
18
19pub const SIGNATURE_INPUT_HEADER: &str = "signature-input";
21
22mod param {
24 pub const KEYID: &str = "keyid";
25 pub const ALG: &str = "alg";
26 pub const CREATED: &str = "created";
27 pub const EXPIRES: &str = "expires";
28 pub const NONCE: &str = "nonce";
29 pub const TAG: &str = "tag";
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct SignatureInput {
37 pub components: Vec<Component>,
39 pub keyid: Option<String>,
41 pub algorithm: Option<String>,
43 pub created: Option<i64>,
45 pub expires: Option<i64>,
47 pub nonce: Option<String>,
49 pub tag: Option<String>,
51}
52
53impl SignatureInput {
54 #[must_use]
59 pub const fn new(components: Vec<Component>) -> Self {
60 Self {
61 components,
62 keyid: None,
63 algorithm: None,
64 created: None,
65 expires: None,
66 nonce: None,
67 tag: None,
68 }
69 }
70
71 #[must_use]
73 pub fn with_keyid(mut self, keyid: impl Into<String>) -> Self {
74 self.keyid = Some(keyid.into());
75 self
76 }
77
78 #[must_use]
80 pub fn with_algorithm(mut self, algorithm: impl Into<String>) -> Self {
81 self.algorithm = Some(algorithm.into());
82 self
83 }
84
85 #[must_use]
87 pub const fn with_created(mut self, created: i64) -> Self {
88 self.created = Some(created);
89 self
90 }
91
92 #[must_use]
94 pub const fn with_expires(mut self, expires: i64) -> Self {
95 self.expires = Some(expires);
96 self
97 }
98
99 #[must_use]
101 pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
102 self.nonce = Some(nonce.into());
103 self
104 }
105
106 #[must_use]
108 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
109 self.tag = Some(tag.into());
110 self
111 }
112
113 #[must_use]
122 #[allow(
123 clippy::expect_used,
124 reason = "serialising a well-formed InnerList cannot fail"
125 )]
126 pub fn serialise_inner_list(&self) -> String {
127 use core::fmt::Write as _;
128 let mut out = String::new();
129 out.push('(');
130 for (i, c) in self.components.iter().enumerate() {
131 if i > 0 {
132 out.push(' ');
133 }
134 out.push_str(&c.lexical());
135 }
136 out.push(')');
137 let infallible = "writing to an owned String is infallible";
143 if let Some(c) = self.created {
144 write!(out, ";created={c}").expect(infallible);
145 }
146 if let Some(e) = self.expires {
147 write!(out, ";expires={e}").expect(infallible);
148 }
149 if let Some(n) = &self.nonce {
150 write!(out, r#";nonce="{n}""#).expect(infallible);
151 }
152 if let Some(alg) = &self.algorithm {
153 write!(out, r#";alg="{alg}""#).expect(infallible);
154 }
155 if let Some(keyid) = &self.keyid {
156 write!(out, r#";keyid="{keyid}""#).expect(infallible);
157 }
158 if let Some(t) = &self.tag {
159 write!(out, r#";tag="{t}""#).expect(infallible);
160 }
161 out
162 }
163}
164
165pub fn parse_signature_input_dict(raw: &str) -> Result<Vec<(String, SignatureInput)>, Error> {
174 let dict: Dictionary =
175 Parser::new(raw)
176 .parse()
177 .map_err(|e: sfv::Error| Error::InvalidHeader {
178 name: SIGNATURE_INPUT_HEADER,
179 reason: e.to_string(),
180 })?;
181
182 let mut out = Vec::with_capacity(dict.len());
183 for (label, entry) in dict {
184 let inner_list = match entry {
185 ListEntry::InnerList(il) => il,
186 ListEntry::Item(_) => {
187 return Err(Error::MalformedSignatureHeader(format!(
188 "entry `{label}` must be an inner list of components"
189 )));
190 }
191 };
192
193 let components: Vec<Component> = inner_list
194 .items
195 .iter()
196 .map(|item| {
197 let BareItem::String(s) = &item.bare_item else {
198 return Err(Error::MalformedSignatureHeader(format!(
199 "entry `{label}` contains a non-string component"
200 )));
201 };
202 Component::parse(s.as_str())
203 })
204 .collect::<Result<_, _>>()?;
205
206 let label_str = label.as_str();
207 let mut input = SignatureInput {
208 components,
209 keyid: None,
210 algorithm: None,
211 created: None,
212 expires: None,
213 nonce: None,
214 tag: None,
215 };
216
217 for (pname, pvalue) in &inner_list.params {
218 match pname.as_str() {
219 param::KEYID => input.keyid = string_param(pvalue, label_str, param::KEYID)?,
220 param::ALG => input.algorithm = string_param(pvalue, label_str, param::ALG)?,
221 param::CREATED => {
222 input.created = integer_param(pvalue, label_str, param::CREATED)?;
223 }
224 param::EXPIRES => {
225 input.expires = integer_param(pvalue, label_str, param::EXPIRES)?;
226 }
227 param::NONCE => input.nonce = string_param(pvalue, label_str, param::NONCE)?,
228 param::TAG => input.tag = string_param(pvalue, label_str, param::TAG)?,
229 _ => {
230 }
232 }
233 }
234
235 out.push((label.into(), input));
236 }
237
238 Ok(out)
239}
240
241fn string_param(value: &BareItem, label: &str, param: &str) -> Result<Option<String>, Error> {
242 match value {
243 BareItem::String(s) => Ok(Some(s.as_str().to_owned())),
244 _ => Err(Error::MalformedSignatureHeader(format!(
245 "entry `{label}` has non-string `{param}` parameter"
246 ))),
247 }
248}
249
250fn integer_param(value: &BareItem, label: &str, param: &str) -> Result<Option<i64>, Error> {
251 match value {
252 BareItem::Integer(n) => Ok(Some(i64::from(*n))),
253 _ => Err(Error::MalformedSignatureHeader(format!(
254 "entry `{label}` has non-integer `{param}` parameter"
255 ))),
256 }
257}
258
259#[must_use]
267#[allow(
268 clippy::expect_used,
269 reason = "serialising a well-formed sf-dictionary cannot fail"
270)]
271pub fn serialise_signature_input_dict(entries: &[(String, SignatureInput)]) -> String {
272 let mut out = String::new();
273 for (i, (label, input)) in entries.iter().enumerate() {
274 if i > 0 {
275 out.push_str(", ");
276 }
277 out.push_str(label);
278 out.push('=');
279 out.push_str(&input.serialise_inner_list());
280 }
281 out
282}
283
284#[cfg(test)]
285mod tests {
286 use pretty_assertions::assert_eq;
287
288 use super::*;
289
290 #[test]
291 fn serialise_matches_rfc9421_example() {
292 let input = SignatureInput::new(vec![
295 Component::Method,
296 Component::TargetUri,
297 Component::Header("host".into()),
298 Component::Header("date".into()),
299 ])
300 .with_keyid("test-key-rsa")
301 .with_created(1_618_884_473);
302 let dict = serialise_signature_input_dict(&[("sig1".into(), input)]);
303 assert_eq!(
304 dict,
305 r#"sig1=("@method" "@target-uri" "host" "date");created=1618884473;keyid="test-key-rsa""#,
306 );
307 }
308
309 #[test]
310 fn parse_roundtrips_through_serialise() {
311 let input = SignatureInput::new(vec![Component::Method, Component::Authority])
312 .with_keyid("kid")
313 .with_algorithm("ed25519")
314 .with_created(1_700_000_000)
315 .with_expires(1_700_000_600)
316 .with_nonce("abc")
317 .with_tag("mastodon");
318 let wire = serialise_signature_input_dict(&[("sig".into(), input.clone())]);
319 let parsed = parse_signature_input_dict(&wire).expect("parse");
320 assert_eq!(parsed.len(), 1);
321 assert_eq!(parsed[0].0, "sig");
322 assert_eq!(parsed[0].1, input);
323 }
324
325 #[test]
326 fn entry_of_wrong_shape_is_rejected() {
327 let wire = "sig1=123";
329 let err = parse_signature_input_dict(wire).expect_err("wrong shape");
330 assert!(matches!(err, Error::MalformedSignatureHeader(_)));
331 }
332
333 #[test]
334 fn unknown_parameters_are_tolerated() {
335 let wire = r#"sig1=("@method");keyid="kid";future_param=42"#;
336 let parsed = parse_signature_input_dict(wire).expect("parse");
337 assert_eq!(parsed[0].1.keyid.as_deref(), Some("kid"));
338 }
339
340 #[test]
341 fn non_string_component_is_rejected() {
342 let wire = "sig1=(foo)";
344 let err = parse_signature_input_dict(wire).expect_err("non-string component");
345 assert!(matches!(err, Error::MalformedSignatureHeader(_)));
346 }
347}