Skip to main content

asimov_http/
signature.rs

1// This is free and unencumbered software released into the public domain.
2
3use crate::{Component, Error, FieldSection, Message, SecretKey, VerifyingKey, fields};
4use alloc::{
5    borrow::ToOwned,
6    format,
7    string::{String, ToString},
8    vec,
9    vec::Vec,
10};
11use base64::{Engine as _, engine::general_purpose::STANDARD};
12use http::{HeaderMap, HeaderValue};
13use sfv::{BareItem, FieldType, InnerList, Item, ListEntry};
14
15/// Application tag for ASIMOV remote execution requests.
16pub const REQUEST_SIGNATURE_TAG: &str = "asimov-remote-request-v1";
17
18/// ASCII key identifier used by ASIMOV's HTTP signature profiles.
19pub fn key_id(key: &VerifyingKey) -> String {
20    format!("ed25519:{}", bs58::encode(key.as_bytes()).into_string())
21}
22
23/// Parameters for newly generated Ed25519 signatures.
24///
25/// Parsed inputs retain their original parameter ordering and extensions in
26/// `SignatureInput`; they are never reconstructed through this configuration.
27#[derive(Clone, Debug)]
28pub struct SignatureParams {
29    /// Creation time in UNIX seconds.
30    pub created: i64,
31    /// Optional expiration time in UNIX seconds.
32    pub expires: Option<i64>,
33    /// Opaque key identifier, resolved and authorized by the application.
34    pub key_id: String,
35    /// Optional per-attempt nonce.
36    pub nonce: Option<String>,
37    /// Optional application profile tag.
38    pub tag: Option<String>,
39}
40
41impl SignatureParams {
42    /// Creates a 60-second signature with a fresh 256-bit OS-random nonce.
43    /// Clock and entropy failures are returned before any request is dispatched.
44    pub fn fresh(key: &VerifyingKey, tag: &str) -> Result<Self, Error> {
45        let created = Self::now()?;
46        let mut nonce = [0; 32];
47        getrandom::fill(&mut nonce).map_err(|error| Error::Random(error.to_string()))?;
48        Ok(Self {
49            created,
50            expires: Some(created.checked_add(60).ok_or(Error::Clock)?),
51            key_id: key_id(key),
52            nonce: Some(STANDARD.encode(nonce)),
53            tag: Some(tag.into()),
54        })
55    }
56
57    /// Returns the current UNIX time, for explicitly timed verification.
58    pub fn now() -> Result<i64, Error> {
59        std::time::SystemTime::now()
60            .duration_since(std::time::UNIX_EPOCH)
61            .map_err(|_| Error::Clock)?
62            .as_secs()
63            .try_into()
64            .map_err(|_| Error::Clock)
65    }
66}
67
68/// An ordered, validated RFC 9421 inner list and its signature parameters.
69///
70/// RFC 8941 serialization preserves received component/parameter order, including
71/// unknown signature parameters. Component identity compares parameter sets
72/// without depending on their order, so equivalent duplicates are rejected.
73/// At most 64 components and 64 KiB of serialized metadata are accepted.
74#[derive(Clone, Debug)]
75pub struct SignatureInput {
76    inner: InnerList,
77    components: Vec<Component>,
78}
79
80impl SignatureInput {
81    /// Creates an input with `alg="ed25519"`, using Structured Fields escaping.
82    pub fn new(components: Vec<Component>, params: SignatureParams) -> Result<Self, Error> {
83        let mut inner = InnerList::new(components.iter().map(|c| c.0.clone()).collect());
84        let integer = |value| {
85            sfv::Integer::try_from(value)
86                .map(BareItem::Integer)
87                .map_err(Error::from)
88        };
89        inner
90            .params
91            .insert(sfv::key_ref("created").to_owned(), integer(params.created)?);
92        if let Some(expires) = params.expires {
93            inner
94                .params
95                .insert(sfv::key_ref("expires").to_owned(), integer(expires)?);
96        }
97        for (name, value) in [
98            ("nonce", params.nonce.as_deref()),
99            ("alg", Some("ed25519")),
100            ("keyid", Some(params.key_id.as_str())),
101            ("tag", params.tag.as_deref()),
102        ] {
103            if let Some(value) = value {
104                inner.params.insert(
105                    sfv::key_ref(name).to_owned(),
106                    sfv::StringRef::from_str(value)?.into(),
107                );
108            }
109        }
110        Self::from_inner(inner)
111    }
112
113    /// Parses the inner-list value, excluding the dictionary label (`sig1=`).
114    pub fn parse(value: &str) -> Result<Self, Error> {
115        let mut list: sfv::List = fields::parser(value)?.parse()?;
116        if list.len() != 1 {
117            return Err(Error::InvalidField("expected one signature inner list"));
118        }
119        match list.remove(0) {
120            ListEntry::InnerList(inner) => Self::from_inner(inner),
121            _ => Err(Error::InvalidField("signature input must be an inner list")),
122        }
123    }
124
125    fn from_inner(inner: InnerList) -> Result<Self, Error> {
126        if inner.items.len() > fields::MAX_COMPONENTS {
127            return Err(Error::Limit);
128        }
129        let mut components = Vec::new();
130        for item in &inner.items {
131            let component = Component::from_item(item.clone())?;
132            if components.contains(&component) {
133                return Err(Error::DuplicateComponent);
134            }
135            components.push(component);
136        }
137        for (name, value) in &inner.params {
138            match name.as_str() {
139                "created" | "expires" => {
140                    if value.as_integer().is_none_or(|n| i64::from(n) < 0) {
141                        return Err(Error::InvalidField("invalid signature timestamp"));
142                    }
143                },
144                "alg" | "keyid" | "nonce" | "tag" if value.as_string().is_none() => {
145                    return Err(Error::InvalidField("signature parameter must be a string"));
146                },
147                _ => (), // Preserve extensions in the signed parameter list.
148            }
149        }
150        let result = Self { inner, components };
151        if result.parameter("alg").is_some_and(|alg| alg != "ed25519") {
152            return Err(Error::Algorithm);
153        }
154        if result.to_field_value().len() > fields::MAX_FIELD_BYTES {
155            return Err(Error::Limit);
156        }
157        Ok(result)
158    }
159
160    /// Extracts the selected signature input from headers and/or trailers.
161    /// Duplicate labels across sections are rejected as an application policy.
162    pub fn from_message(message: &Message, label: &str) -> Result<Self, Error> {
163        match selected(message, "signature-input", label)? {
164            ListEntry::InnerList(inner) => Self::from_inner(inner),
165            _ => Err(Error::InvalidField("signature input must be an inner list")),
166        }
167    }
168
169    /// Returns the ordered covered components.
170    pub fn components(&self) -> &[Component] {
171        &self.components
172    }
173
174    /// Returns a string parameter, including preserved extension parameters.
175    pub fn parameter(&self, name: &str) -> Option<&str> {
176        self.inner
177            .params
178            .get(name)?
179            .as_string()
180            .map(|value| value.as_str())
181    }
182
183    /// Returns an integer parameter.
184    pub fn integer_parameter(&self, name: &str) -> Option<i64> {
185        self.inner.params.get(name)?.as_integer().map(i64::from)
186    }
187
188    /// Serializes the original ordered inner list with strict SF serialization.
189    pub fn to_field_value(&self) -> String {
190        vec![ListEntry::InnerList(self.inner.clone())]
191            .serialize()
192            .expect("one inner list")
193    }
194
195    /// Creates the RFC 9421 signature base, without a final newline.
196    /// This contains covered credentials; callers must not log it indiscriminately.
197    pub fn signature_base(&self, message: &Message) -> Result<Vec<u8>, Error> {
198        let mut base = String::new();
199        for component in &self.components {
200            let value = message.value(component)?;
201            base.push_str(&component.to_field_value());
202            base.push_str(": ");
203            base.push_str(&value);
204            base.push('\n');
205            if base.len() > fields::MAX_FIELD_BYTES {
206                return Err(Error::Limit);
207            }
208        }
209        base.push_str("\"@signature-params\": ");
210        base.push_str(&self.to_field_value());
211        if base.len() > fields::MAX_FIELD_BYTES {
212            return Err(Error::Limit);
213        }
214        Ok(base.into_bytes())
215    }
216
217    /// Checks presently available covered fields before starting a stream.
218    pub(crate) fn preflight(&self, message: &Message) -> Result<(), Error> {
219        for component in &self.components {
220            if !component.flag("tr") || component.flag("req") {
221                message.value(component)?;
222            }
223        }
224        Ok(())
225    }
226
227    pub(crate) fn covers_digest(&self, section: FieldSection) -> bool {
228        self.components.iter().any(|component| {
229            component.name() == "content-digest"
230                && !component.flag("req")
231                && component.flag("tr") == (section == FieldSection::Trailers)
232                && component
233                    .string_param("key")
234                    .is_none_or(|key| key == "sha-256")
235        })
236    }
237
238    /// Verifies Ed25519 only. Freshness, required coverage, authorization, replay,
239    /// and body integrity are separate; use `Verifier` and body adapters for a
240    /// policy-checked message. This primitive also supports static RFC fixtures.
241    pub fn verify(
242        &self,
243        message: &Message,
244        signature: &[u8],
245        key: &VerifyingKey,
246    ) -> Result<(), Error> {
247        let signature = iroh_base::Signature::try_from(signature).map_err(|_| Error::Signature)?;
248        key.verify(&self.signature_base(message)?, &signature)
249            .map_err(|_| Error::Signature)
250    }
251}
252
253/// The two RFC 9421 fields for a single signature label.
254#[derive(Clone, Debug)]
255pub struct SignatureFields {
256    /// Serialized `Signature-Input` dictionary member.
257    pub signature_input: HeaderValue,
258    /// Serialized `Signature` dictionary member.
259    pub signature: HeaderValue,
260    label: String,
261}
262
263impl SignatureFields {
264    /// Appends both fields while preserving other labels. Existing matching
265    /// labels are rejected rather than overwritten.
266    pub fn append_to(&self, fields: &mut HeaderMap) -> Result<(), Error> {
267        for name in ["signature-input", "signature"] {
268            if let Some(value) = fields::combined(&[fields], name)?
269                && fields::dictionary(&value)?.contains_key(self.label.as_str())
270            {
271                return Err(Error::DuplicateLabel);
272            }
273        }
274        fields.append("signature-input", self.signature_input.clone());
275        fields.append("signature", self.signature.clone());
276        Ok(())
277    }
278}
279
280/// An Ed25519 signer for one signature input and label.
281///
282/// Construct a fresh input/nonce for each attempt. The native secret-key type
283/// zeroizes its storage and redacts its debug representation.
284#[derive(Clone, Debug)]
285pub struct Signer {
286    key: SecretKey,
287    label: String,
288    input: SignatureInput,
289}
290
291impl Signer {
292    /// Creates a signer, validating the Structured Fields dictionary label.
293    pub fn new(key: SecretKey, label: &str, input: SignatureInput) -> Result<Self, Error> {
294        sfv::KeyRef::from_str(label)?;
295        Ok(Self {
296            key,
297            label: label.into(),
298            input,
299        })
300    }
301
302    /// Returns the selected covered components and parameters.
303    pub fn input(&self) -> &SignatureInput {
304        &self.input
305    }
306
307    /// Returns the signature label.
308    pub fn label(&self) -> &str {
309        &self.label
310    }
311
312    /// Generates the signature fields without modifying the message or body.
313    pub fn sign(&self, message: &Message) -> Result<SignatureFields, Error> {
314        let signature = self
315            .key
316            .sign(&self.input.signature_base(message)?)
317            .to_bytes();
318        Ok(SignatureFields {
319            signature_input: self.input_header()?,
320            signature: fields::header(format!(
321                "{}={}",
322                self.label,
323                Item::new(signature.as_slice()).serialize()
324            ))?,
325            label: self.label.clone(),
326        })
327    }
328
329    pub(crate) fn input_header(&self) -> Result<HeaderValue, Error> {
330        fields::header(format!("{}={}", self.label, self.input.to_field_value()))
331    }
332}
333
334/// Required coverage and freshness for an application of RFC 9421.
335///
336/// Verification uses an explicit validation instant, normally message receipt,
337/// so long-running response streams need not finish within the validity window.
338/// Nonce checks here validate its format; applications must atomically reject
339/// reused nonces after authenticating the signature and the complete body.
340#[derive(Clone, Debug)]
341pub struct VerificationPolicy {
342    /// Components that must be covered, including their exact parameter sets.
343    ///
344    /// `"content-digest"` requires header coverage; `"content-digest";tr`
345    /// requires trailer coverage. Listing both requires both, not either one.
346    /// To let a response choose its digest location, omit its digest from this
347    /// list and use [`crate::body::VerifyingBody`] with a verifier. That adapter
348    /// independently requires a matching SHA-256 digest and signature coverage
349    /// of its actual section. [`Verifier::verify`] alone does not check the body.
350    ///
351    /// # Examples
352    ///
353    /// ```
354    /// # extern crate alloc;
355    /// use alloc::vec;
356    /// use asimov_http::{Component, VerificationPolicy};
357    ///
358    /// // With VerifyingBody, accept the response digest in either section.
359    /// let either = VerificationPolicy::new(vec![
360    ///     Component::new("@status")?,
361    ///     Component::new("content-type")?,
362    /// ]);
363    ///
364    /// // Require header coverage; a trailer-only digest cannot satisfy this.
365    /// let mut in_headers = either.clone();
366    /// in_headers.required_components.push(Component::new("content-digest")?);
367    ///
368    /// // Or require trailer coverage instead. Choose one policy per verifier.
369    /// let mut in_trailers = either;
370    /// in_trailers
371    ///     .required_components
372    ///     .push(Component::trailer("content-digest")?);
373    /// # Ok::<(), asimov_http::Error>(())
374    /// ```
375    pub required_components: Vec<Component>,
376    /// Required application tag, when set.
377    pub required_tag: Option<String>,
378    /// Maximum age and advertised lifetime in seconds. Defaults to 60.
379    pub max_age: u64,
380    /// Permitted clock skew in seconds. Defaults to 30.
381    pub clock_skew: u64,
382    /// Require a standard-Base64 nonce containing at least 128 bits.
383    pub require_nonce: bool,
384}
385
386impl VerificationPolicy {
387    /// The ASIMOV remote request profile: method, absolute target URI, bearer
388    /// credential, Accept, content type, and SHA-256 content digest coverage.
389    pub fn request() -> Self {
390        let components = [
391            "@method",
392            "@target-uri",
393            "authorization",
394            "accept",
395            "content-type",
396            "content-digest",
397        ]
398        .into_iter()
399        .map(|name| Component::new(name).expect("constant component identifier"))
400        .collect();
401        let mut policy = Self::new(components);
402        policy.required_tag = Some(REQUEST_SIGNATURE_TAG.into());
403        policy
404    }
405
406    /// Creates a policy with explicit coverage, a 60-second lifetime, 30-second
407    /// tolerance, and a required nonce. Creation and expiry are always required.
408    pub fn new(required_components: Vec<Component>) -> Self {
409        Self {
410            required_components,
411            required_tag: None,
412            max_age: 60,
413            clock_skew: 30,
414            require_nonce: true,
415        }
416    }
417
418    fn check(&self, input: &SignatureInput, now: i64) -> Result<(), Error> {
419        let created = input
420            .integer_parameter("created")
421            .ok_or(Error::Policy("missing created"))? as i128;
422        let expires = input
423            .integer_parameter("expires")
424            .ok_or(Error::Policy("missing expires"))? as i128;
425        let now = i128::from(now);
426        let skew = i128::from(self.clock_skew);
427        let age = i128::from(self.max_age);
428        if now < 0
429            || created > now + skew
430            || created < now - age - skew
431            || expires <= created
432            || expires > created + age
433            || now >= expires + skew
434        {
435            return Err(Error::Policy("signature validity window"));
436        }
437        if self
438            .required_tag
439            .as_deref()
440            .is_some_and(|tag| input.parameter("tag") != Some(tag))
441        {
442            return Err(Error::Policy("signature tag"));
443        }
444        if self.require_nonce {
445            let nonce = input
446                .parameter("nonce")
447                .ok_or(Error::Policy("missing nonce"))?;
448            if STANDARD
449                .decode(nonce)
450                .map_or(true, |bytes| bytes.len() < 16)
451            {
452                return Err(Error::Policy("nonce must contain at least 128 bits"));
453            }
454        }
455        if !self
456            .required_components
457            .iter()
458            .all(|c| input.components.contains(c))
459        {
460            return Err(Error::Policy("required signature coverage"));
461        }
462        Ok(())
463    }
464}
465
466/// A signature successfully checked against an explicit key and policy.
467/// This alone does not attest body integrity or application authorization.
468#[derive(Clone, Debug)]
469pub struct VerifiedSignature {
470    /// The verified label.
471    pub label: String,
472    /// Authenticated parameters, including the nonce for replay rejection.
473    pub input: SignatureInput,
474    /// The explicitly supplied verification key.
475    pub key: VerifyingKey,
476}
477
478/// Verification against one application-resolved key, label, and policy.
479#[derive(Clone, Debug)]
480pub struct Verifier {
481    key: VerifyingKey,
482    key_id: String,
483    label: String,
484    policy: VerificationPolicy,
485}
486
487impl Verifier {
488    /// Binds a trusted/resolved key to its expected key ID and signature label.
489    pub fn new(
490        key: VerifyingKey,
491        key_id: &str,
492        label: &str,
493        policy: VerificationPolicy,
494    ) -> Result<Self, Error> {
495        sfv::KeyRef::from_str(label)?;
496        sfv::StringRef::from_str(key_id)?;
497        Ok(Self {
498            key,
499            key_id: key_id.into(),
500            label: label.into(),
501            policy,
502        })
503    }
504
505    /// Verifies the selected signature in headers and/or trailers at `now`.
506    /// It never accepts another key, label, or algorithm as a fallback.
507    pub fn verify(&self, message: &Message, now: i64) -> Result<VerifiedSignature, Error> {
508        let input = SignatureInput::from_message(message, &self.label)?;
509        if input.parameter("keyid") != Some(self.key_id.as_str()) {
510            return Err(Error::Policy("key ID"));
511        }
512        self.policy.check(&input, now)?;
513        let ListEntry::Item(item) = selected(message, "signature", &self.label)? else {
514            return Err(Error::InvalidField("signature must be a byte sequence"));
515        };
516        let bytes = item
517            .bare_item
518            .as_byte_sequence()
519            .ok_or(Error::InvalidField("signature byte sequence"))?;
520        input.verify(message, bytes, &self.key)?;
521        Ok(VerifiedSignature {
522            label: self.label.clone(),
523            input,
524            key: self.key,
525        })
526    }
527}
528
529fn selected(message: &Message, name: &str, label: &str) -> Result<ListEntry, Error> {
530    let mut selected = None;
531    for section in [FieldSection::Headers, FieldSection::Trailers] {
532        if let Some(value) = fields::combined(&[message.fields(section)], name)? {
533            let mut dictionary = fields::dictionary(&value)?;
534            if dictionary.len() > fields::MAX_COMPONENTS {
535                return Err(Error::Limit);
536            }
537            for entry in dictionary.values() {
538                let valid = match (name, entry) {
539                    ("signature-input", ListEntry::InnerList(_)) => true,
540                    ("signature", ListEntry::Item(item)) => {
541                        item.bare_item.as_byte_sequence().is_some()
542                    },
543                    _ => false,
544                };
545                if !valid {
546                    return Err(Error::InvalidField("signature dictionary member type"));
547                }
548            }
549            if let Some(entry) = dictionary.shift_remove(label) {
550                if selected.is_some() {
551                    return Err(Error::DuplicateLabel);
552                }
553                selected = Some(entry);
554            }
555        }
556    }
557    selected.ok_or_else(|| Error::MissingComponent(format!("{name};key={label}")))
558}