Skip to main content

sbom_walker/validation/
mod.rs

1//! Validation
2
3use crate::{
4    discover::DiscoveredSbom,
5    retrieve::{RetrievalContext, RetrievedSbom, RetrievedVisitor},
6    source::Source,
7};
8use std::{
9    fmt::{Debug, Display},
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::{ValidationError, ValidationOptions, digest::validate_digest, openpgp},
19};
20
21#[derive(Clone, Debug)]
22pub struct ValidatedSbom {
23    /// The discovered advisory
24    pub retrieved: RetrievedSbom,
25}
26
27impl Urlify for ValidatedSbom {
28    fn url(&self) -> &Url {
29        &self.url
30    }
31
32    fn relative_base_and_url(&self) -> Option<(&Url, String)> {
33        self.retrieved.relative_base_and_url()
34    }
35}
36
37impl Deref for ValidatedSbom {
38    type Target = RetrievedSbom;
39
40    fn deref(&self) -> &Self::Target {
41        &self.retrieved
42    }
43}
44
45impl DerefMut for ValidatedSbom {
46    fn deref_mut(&mut self) -> &mut Self::Target {
47        &mut self.retrieved
48    }
49}
50
51pub struct ValidationContext<'c> {
52    pub retrieval: &'c RetrievalContext<'c>,
53}
54
55impl<'c> Deref for ValidationContext<'c> {
56    type Target = RetrievalContext<'c>;
57
58    fn deref(&self) -> &Self::Target {
59        self.retrieval
60    }
61}
62
63pub trait ValidatedVisitor<S: Source> {
64    type Error: Display + Debug;
65    type Context;
66
67    fn visit_context(
68        &self,
69        context: &ValidationContext,
70    ) -> impl Future<Output = Result<Self::Context, Self::Error>>;
71
72    fn visit_sbom(
73        &self,
74        context: &Self::Context,
75        result: Result<ValidatedSbom, ValidationError<S>>,
76    ) -> impl Future<Output = Result<(), Self::Error>>;
77}
78
79impl<F, E, Fut, S> ValidatedVisitor<S> for F
80where
81    F: Fn(Result<ValidatedSbom, ValidationError<S>>) -> Fut,
82    Fut: Future<Output = Result<(), E>>,
83    E: Display + Debug,
84    S: Source,
85{
86    type Error = E;
87    type Context = ();
88
89    async fn visit_context(
90        &self,
91        _context: &ValidationContext<'_>,
92    ) -> Result<Self::Context, Self::Error> {
93        Ok(())
94    }
95
96    async fn visit_sbom(
97        &self,
98        _context: &Self::Context,
99        result: Result<ValidatedSbom, ValidationError<S>>,
100    ) -> Result<(), Self::Error> {
101        self(result).await
102    }
103}
104
105pub struct ValidationVisitor<V, S>
106where
107    V: ValidatedVisitor<S>,
108    S: Source,
109{
110    visitor: V,
111    options: ValidationOptions,
112    _marker: PhantomData<S>,
113}
114
115enum ValidationProcessError<S: Source> {
116    /// Failed, but passing on to visitor
117    Proceed(ValidationError<S>),
118    /// Failed, aborting processing
119    #[allow(unused)]
120    Abort(anyhow::Error),
121}
122
123#[derive(Debug, thiserror::Error)]
124pub enum Error<VE>
125where
126    VE: Display + Debug,
127{
128    #[error("{0}")]
129    Visitor(VE),
130    #[error("Severe validation error: {0}")]
131    Validation(anyhow::Error),
132}
133
134impl<V, S> ValidationVisitor<V, S>
135where
136    V: ValidatedVisitor<S>,
137    S: Source<Retrieved = RetrievedSbom>,
138{
139    pub fn new(visitor: V) -> Self {
140        Self {
141            visitor,
142            options: Default::default(),
143            _marker: Default::default(),
144        }
145    }
146
147    pub fn with_options(mut self, options: impl Into<ValidationOptions>) -> Self {
148        self.options = options.into();
149        self
150    }
151
152    /// Perform the actual validation.
153    ///
154    /// Returning either a processing error, or a result which will be forwarded to the visitor.
155    async fn validate(
156        &self,
157        context: &InnerValidationContext<V::Context>,
158        retrieved: RetrievedSbom,
159    ) -> Result<ValidatedSbom, ValidationProcessError<S>> {
160        if let Err((expected, actual)) = validate_digest(&retrieved.sha256) {
161            return Err(ValidationProcessError::Proceed(
162                ValidationError::DigestMismatch {
163                    expected,
164                    actual,
165                    retrieved,
166                },
167            ));
168        }
169        if let Err((expected, actual)) = validate_digest(&retrieved.sha512) {
170            return Err(ValidationProcessError::Proceed(
171                ValidationError::DigestMismatch {
172                    expected,
173                    actual,
174                    retrieved,
175                },
176            ));
177        }
178
179        if let Some(signature) = &retrieved.signature {
180            match openpgp::validate_signature(
181                &self.options,
182                &context.keys,
183                signature,
184                &retrieved.data,
185            ) {
186                Ok(()) => Ok(ValidatedSbom { retrieved }),
187                Err(error) => Err(ValidationProcessError::Proceed(
188                    ValidationError::Signature { error, retrieved },
189                )),
190            }
191        } else {
192            Ok(ValidatedSbom { retrieved })
193        }
194    }
195}
196
197pub struct InnerValidationContext<VC> {
198    context: VC,
199    keys: Vec<PublicKey>,
200}
201
202impl<V, S> RetrievedVisitor<S> for ValidationVisitor<V, S>
203where
204    V: ValidatedVisitor<S>,
205    S: Source<Retrieved = RetrievedSbom>,
206{
207    type Error = Error<V::Error>;
208    type Context = InnerValidationContext<V::Context>;
209
210    async fn visit_context(
211        &self,
212        context: &RetrievalContext<'_>,
213    ) -> Result<Self::Context, Self::Error> {
214        let keys = context.keys.clone();
215
216        let context = self
217            .visitor
218            .visit_context(&ValidationContext { retrieval: context })
219            .await
220            .map_err(Error::Visitor)?;
221
222        Ok(Self::Context { context, keys })
223    }
224
225    async fn visit_sbom(
226        &self,
227        context: &Self::Context,
228        outcome: Result<RetrievedSbom, RetrievalError<DiscoveredSbom, S>>,
229    ) -> Result<(), Self::Error> {
230        match outcome {
231            Ok(advisory) => {
232                let result = match self.validate(context, advisory).await {
233                    Ok(result) => Ok(result),
234                    Err(ValidationProcessError::Proceed(err)) => Err(err),
235                    Err(ValidationProcessError::Abort(err)) => return Err(Error::Validation(err)),
236                };
237                self.visitor
238                    .visit_sbom(&context.context, result)
239                    .await
240                    .map_err(Error::Visitor)?
241            }
242            Err(err) => self
243                .visitor
244                .visit_sbom(&context.context, Err(ValidationError::Retrieval(err)))
245                .await
246                .map_err(Error::Visitor)?,
247        }
248
249        Ok(())
250    }
251}