csaf_walker/validation/
mod.rs

1//! Validation
2
3use crate::{
4    discover::{AsDiscovered, DiscoveredAdvisory},
5    retrieve::{AsRetrieved, RetrievalContext, RetrievedAdvisory, RetrievedVisitor},
6    source::Source,
7};
8use std::{
9    fmt::{Debug, Display, Formatter},
10    future::Future,
11    marker::PhantomData,
12    ops::{Deref, DerefMut},
13};
14use url::Url;
15use walker_common::{
16    retrieve::RetrievalError,
17    utils::{openpgp::PublicKey, url::Urlify},
18    validate::{ValidationOptions, digest::validate_digest, openpgp},
19};
20
21/// A validated CSAF document
22///
23/// This includes
24/// * The document could be retrieved
25/// * The digest matches or was absent
26/// * The signature was valid
27#[derive(Clone, Debug)]
28pub struct ValidatedAdvisory {
29    /// The retrieved advisory
30    pub retrieved: RetrievedAdvisory,
31}
32
33impl Urlify for ValidatedAdvisory {
34    fn url(&self) -> &Url {
35        &self.url
36    }
37
38    fn relative_base_and_url(&self) -> Option<(&Url, String)> {
39        self.retrieved.relative_base_and_url()
40    }
41}
42
43impl Deref for ValidatedAdvisory {
44    type Target = RetrievedAdvisory;
45
46    fn deref(&self) -> &Self::Target {
47        &self.retrieved
48    }
49}
50
51impl DerefMut for ValidatedAdvisory {
52    fn deref_mut(&mut self) -> &mut Self::Target {
53        &mut self.retrieved
54    }
55}
56
57impl AsDiscovered for ValidatedAdvisory {
58    fn as_discovered(&self) -> &DiscoveredAdvisory {
59        &self.discovered
60    }
61}
62
63impl AsRetrieved for ValidatedAdvisory {
64    fn as_retrieved(&self) -> &RetrievedAdvisory {
65        &self.retrieved
66    }
67}
68
69#[derive(Debug, thiserror::Error)]
70#[allow(clippy::large_enum_variant)]
71pub enum ValidationError<S: Source> {
72    Retrieval(RetrievalError<DiscoveredAdvisory, S>),
73    DigestMismatch {
74        expected: String,
75        actual: String,
76        retrieved: RetrievedAdvisory,
77    },
78    Signature {
79        error: anyhow::Error,
80        retrieved: RetrievedAdvisory,
81    },
82}
83
84impl<S: Source + Debug> AsDiscovered for ValidationError<S> {
85    fn as_discovered(&self) -> &DiscoveredAdvisory {
86        match self {
87            Self::Retrieval(err) => err.discovered(),
88            Self::DigestMismatch { retrieved, .. } => retrieved.as_discovered(),
89            Self::Signature { retrieved, .. } => retrieved.as_discovered(),
90        }
91    }
92}
93
94impl<S: Source> Urlify for ValidationError<S> {
95    fn url(&self) -> &Url {
96        match self {
97            Self::Retrieval(err) => err.url(),
98            Self::DigestMismatch { retrieved, .. } => &retrieved.url,
99            Self::Signature { retrieved, .. } => &retrieved.url,
100        }
101    }
102}
103
104impl<S: Source> Display for ValidationError<S> {
105    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
106        match self {
107            Self::Retrieval(err) => write!(f, "Retrieval error: {err}"),
108            Self::DigestMismatch {
109                expected,
110                actual,
111                retrieved: _,
112            } => write!(
113                f,
114                "Digest mismatch - expected: {expected}, actual: {actual}",
115            ),
116            Self::Signature {
117                error,
118                retrieved: _,
119            } => {
120                write!(f, "Invalid signature: {error}",)
121            }
122        }
123    }
124}
125
126pub struct ValidationContext<'c> {
127    pub retrieval: &'c RetrievalContext<'c>,
128}
129
130impl<'c> Deref for ValidationContext<'c> {
131    type Target = RetrievalContext<'c>;
132
133    fn deref(&self) -> &Self::Target {
134        self.retrieval
135    }
136}
137
138pub trait ValidatedVisitor<S: Source> {
139    type Error: Display + Debug;
140    type Context;
141
142    fn visit_context(
143        &self,
144        context: &ValidationContext,
145    ) -> impl Future<Output = Result<Self::Context, Self::Error>>;
146
147    fn visit_advisory(
148        &self,
149        context: &Self::Context,
150        result: Result<ValidatedAdvisory, ValidationError<S>>,
151    ) -> impl Future<Output = Result<(), Self::Error>>;
152}
153
154impl<F, E, Fut, S> ValidatedVisitor<S> for F
155where
156    F: Fn(Result<ValidatedAdvisory, ValidationError<S>>) -> Fut,
157    Fut: Future<Output = Result<(), E>>,
158    E: Display + Debug,
159    S: Source,
160{
161    type Error = E;
162    type Context = ();
163
164    async fn visit_context(
165        &self,
166        _context: &ValidationContext<'_>,
167    ) -> Result<Self::Context, Self::Error> {
168        Ok(())
169    }
170
171    async fn visit_advisory(
172        &self,
173        _context: &Self::Context,
174        result: Result<ValidatedAdvisory, ValidationError<S>>,
175    ) -> Result<(), Self::Error> {
176        self(result).await
177    }
178}
179
180pub struct ValidationVisitor<V, S>
181where
182    V: ValidatedVisitor<S>,
183    S: Source,
184{
185    visitor: V,
186    options: ValidationOptions,
187    _marker: PhantomData<S>,
188}
189
190#[allow(clippy::large_enum_variant)]
191enum ValidationProcessError<S: Source> {
192    /// Failed, but passing on to visitor
193    Proceed(ValidationError<S>),
194    /// Failed, aborting processing
195    #[allow(unused)]
196    Abort(anyhow::Error),
197}
198
199#[derive(Debug, thiserror::Error)]
200pub enum Error<VE>
201where
202    VE: Display + Debug,
203{
204    #[error("{0}")]
205    Visitor(VE),
206    #[error("Severe validation error: {0}")]
207    Validation(anyhow::Error),
208}
209
210impl<V, S> ValidationVisitor<V, S>
211where
212    V: ValidatedVisitor<S>,
213    S: Source,
214{
215    pub fn new(visitor: V) -> Self {
216        Self {
217            visitor,
218            options: Default::default(),
219            _marker: Default::default(),
220        }
221    }
222
223    pub fn with_options(mut self, options: impl Into<ValidationOptions>) -> Self {
224        self.options = options.into();
225        self
226    }
227
228    /// Perform the actual validation.
229    ///
230    /// Returning either a processing error, or a result which will be forwarded to the visitor.
231    async fn validate(
232        &self,
233        context: &InnerValidationContext<V::Context>,
234        retrieved: RetrievedAdvisory,
235    ) -> Result<ValidatedAdvisory, ValidationProcessError<S>> {
236        if let Err((expected, actual)) = validate_digest(&retrieved.sha256) {
237            return Err(ValidationProcessError::Proceed(
238                ValidationError::DigestMismatch {
239                    expected,
240                    actual,
241                    retrieved,
242                },
243            ));
244        }
245        if let Err((expected, actual)) = validate_digest(&retrieved.sha512) {
246            return Err(ValidationProcessError::Proceed(
247                ValidationError::DigestMismatch {
248                    expected,
249                    actual,
250                    retrieved,
251                },
252            ));
253        }
254
255        if let Some(signature) = &retrieved.signature {
256            match openpgp::validate_signature(
257                &self.options,
258                &context.keys,
259                signature,
260                &retrieved.data,
261            ) {
262                Ok(()) => Ok(ValidatedAdvisory { retrieved }),
263                Err(error) => Err(ValidationProcessError::Proceed(
264                    ValidationError::Signature { error, retrieved },
265                )),
266            }
267        } else {
268            Ok(ValidatedAdvisory { retrieved })
269        }
270    }
271}
272
273pub struct InnerValidationContext<VC> {
274    context: VC,
275    keys: Vec<PublicKey>,
276}
277
278impl<V, S> RetrievedVisitor<S> for ValidationVisitor<V, S>
279where
280    V: ValidatedVisitor<S>,
281    S: Source,
282{
283    type Error = Error<V::Error>;
284    type Context = InnerValidationContext<V::Context>;
285
286    async fn visit_context(
287        &self,
288        context: &RetrievalContext<'_>,
289    ) -> Result<Self::Context, Self::Error> {
290        let keys = context.keys.clone();
291
292        let context = self
293            .visitor
294            .visit_context(&ValidationContext { retrieval: context })
295            .await
296            .map_err(Error::Visitor)?;
297
298        Ok(Self::Context { context, keys })
299    }
300
301    async fn visit_advisory(
302        &self,
303        context: &Self::Context,
304        outcome: Result<RetrievedAdvisory, RetrievalError<DiscoveredAdvisory, S>>,
305    ) -> Result<(), Self::Error> {
306        match outcome {
307            Ok(advisory) => {
308                let result = match self.validate(context, advisory).await {
309                    Ok(result) => Ok(result),
310                    Err(ValidationProcessError::Proceed(err)) => Err(err),
311                    Err(ValidationProcessError::Abort(err)) => return Err(Error::Validation(err)),
312                };
313                self.visitor
314                    .visit_advisory(&context.context, result)
315                    .await
316                    .map_err(Error::Visitor)?
317            }
318            Err(err) => self
319                .visitor
320                .visit_advisory(&context.context, Err(ValidationError::Retrieval(err)))
321                .await
322                .map_err(Error::Visitor)?,
323        }
324
325        Ok(())
326    }
327}