Skip to content

Commit 1c792b5

Browse files
committed
feat: implement signature verification
1 parent 573495d commit 1c792b5

File tree

10 files changed

+311
-12
lines changed

10 files changed

+311
-12
lines changed

Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ serde-cyclonedx = "0.9.1"
122122
serde_json = "1.0.114"
123123
serde_with = { version = "3.12.0", features = ["base64"] }
124124
serde_yml = { package = "serde_yaml_ng", version = "0.10" }
125+
sequoia-openpgp = { version = "2", default-features = false }
125126
sha2 = "0.10.8"
126127
spdx = "0.10.6"
127128
spdx-expression = "0.5.2"

common/src/model.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,27 @@ mod default {
7171
}
7272
}
7373

74-
// NOTE: This struct must be aligned with the struct in the [`paginated`] macro below.
74+
/// Paginated results
75+
///
76+
/// This carries the requested page, plus the total number of items matching the request.
7577
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, ToSchema)]
7678
#[serde(rename_all = "camelCase")]
7779
pub struct PaginatedResults<R> {
80+
/// The page of items
7881
pub items: Vec<R>,
82+
/// The total number of items
7983
pub total: u64,
8084
}
8185

86+
impl<R> Default for PaginatedResults<R> {
87+
fn default() -> Self {
88+
Self {
89+
items: vec![],
90+
total: 0,
91+
}
92+
}
93+
}
94+
8295
impl<R> PaginatedResults<R> {
8396
/// Create a new paginated result
8497
pub async fn new<C, S1, S2>(

modules/fundamental/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ serde_json = { workspace = true }
4242
spdx = { workspace = true, features = ["text"] }
4343
strum = { workspace = true }
4444
tar = { workspace = true }
45+
tempfile = { workspace = true }
4546
thiserror = { workspace = true }
4647
time = { workspace = true }
4748
tokio = { workspace = true, features = ["full"] }
49+
tokio-util = { workspace = true }
4850
tracing = { workspace = true }
4951
tracing-futures = { workspace = true, features = ["futures-03"] }
5052
utoipa = { workspace = true, features = ["actix_extras", "uuid", "time"] }

modules/fundamental/src/advisory/endpoints/mod.rs

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use config::Config;
1717
use futures_util::TryStreamExt;
1818
use sea_orm::TransactionTrait;
1919
use std::str::FromStr;
20+
use tokio_util::io::StreamReader;
2021
use trustify_auth::{CreateAdvisory, DeleteAdvisory, ReadAdvisory, authorizer::Require};
2122
use trustify_common::{
2223
db::{Database, query::Query},
@@ -26,9 +27,10 @@ use trustify_common::{
2627
};
2728
use trustify_entity::labels::Labels;
2829
use trustify_module_ingestor::service::{Format, Ingest, IngestorService};
30+
use trustify_module_signature::model::VerificationResult;
2931
use trustify_module_signature::{
3032
model::Signature,
31-
service::{DocumentType, SignatureService},
33+
service::{DocumentType, SignatureService, TrustAnchorService},
3234
};
3335
use trustify_module_storage::service::StorageBackend;
3436
use utoipa::IntoParams;
@@ -40,18 +42,21 @@ pub fn configure(
4042
) {
4143
let advisory_service = AdvisoryService::new(db.clone());
4244
let purl_service = PurlService::new();
45+
let signature_service = SignatureService::new();
4346

4447
config
4548
.app_data(web::Data::new(db))
4649
.app_data(web::Data::new(advisory_service))
4750
.app_data(web::Data::new(purl_service))
51+
.app_data(web::Data::new(signature_service))
4852
.app_data(web::Data::new(Config { upload_limit }))
4953
.service(all)
5054
.service(get)
5155
.service(delete)
5256
.service(upload)
5357
.service(download)
5458
.service(list_signatures)
59+
.service(verify_signatures)
5560
.service(label::set)
5661
.service(label::update);
5762
}
@@ -275,15 +280,91 @@ pub async fn download(
275280
#[get("/v2/advisory/{key}/signature")]
276281
pub async fn list_signatures(
277282
db: web::Data<Database>,
283+
service: web::Data<SignatureService>,
278284
key: web::Path<String>,
279285
web::Query(paginated): web::Query<Paginated>,
280286
_: Require<ReadAdvisory>,
281287
) -> Result<impl Responder, Error> {
282288
let id = Id::from_str(&key).map_err(Error::IdKey)?;
283289

284-
let result = SignatureService
285-
.list_signatures(DocumentType::Sbom, id, paginated, db.get_ref())
290+
let result = service
291+
.list_signatures(DocumentType::Advisory, id, paginated, db.get_ref())
286292
.await?;
287293

288294
Ok(HttpResponse::Ok().json(result))
289295
}
296+
297+
/// Get signatures of an SBOM
298+
#[utoipa::path(
299+
tag = "advisory",
300+
operation_id = "verifyAdvisorySignatures",
301+
params(
302+
("key" = Id, Path),
303+
),
304+
responses(
305+
(status = 200, description = "Signatures of an advisory", body = PaginatedResults<VerificationResult>),
306+
(status = 404, description = "The document could not be found"),
307+
)
308+
)]
309+
#[get("/v2/advisory/{key}/verify")]
310+
#[allow(clippy::too_many_arguments)]
311+
pub async fn verify_signatures(
312+
db: web::Data<Database>,
313+
signature_service: web::Data<SignatureService>,
314+
trust_anchor_service: web::Data<TrustAnchorService>,
315+
ingestor: web::Data<IngestorService>,
316+
advisory: web::Data<AdvisoryService>,
317+
key: web::Path<String>,
318+
web::Query(paginated): web::Query<Paginated>,
319+
_: Require<ReadAdvisory>,
320+
) -> Result<impl Responder, Error> {
321+
let id = Id::from_str(&key).map_err(Error::IdKey)?;
322+
323+
// fetch signatures of the document
324+
let result = signature_service
325+
.list_signatures(DocumentType::Advisory, id.clone(), paginated, db.get_ref())
326+
.await?;
327+
328+
if result.items.is_empty() {
329+
// early exit, so we don't need to fetch content or trust anchors.
330+
return Ok(HttpResponse::Ok().json(PaginatedResults::<VerificationResult>::default()));
331+
}
332+
333+
// look up document by id
334+
let Some(advisory) = advisory
335+
.fetch_advisory(id.clone(), db.as_ref())
336+
.await?
337+
.and_then(|advisory| advisory.source_document)
338+
else {
339+
return Ok(HttpResponse::NotFound().finish());
340+
};
341+
342+
let stream = ingestor
343+
.get_ref()
344+
.storage()
345+
.clone()
346+
.retrieve(advisory.try_into()?)
347+
.await
348+
.map_err(Error::Storage)?;
349+
350+
let Some(stream) = stream else {
351+
return Ok(HttpResponse::NotFound().finish());
352+
};
353+
354+
let content = tempfile::tempfile()?;
355+
tokio::io::copy(
356+
&mut StreamReader::new(
357+
stream.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err.to_string())),
358+
),
359+
&mut tokio::fs::File::from_std(content.try_clone().map_err(|err| Error::Any(err.into()))?),
360+
)
361+
.await
362+
.map_err(|err| Error::Any(err.into()))?;
363+
364+
let result = PaginatedResults {
365+
items: trust_anchor_service.verify(result.items, content).await?,
366+
total: result.total,
367+
};
368+
369+
Ok(HttpResponse::Ok().json(result))
370+
}

modules/fundamental/src/sbom/endpoints/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ pub fn configure(
5555
) {
5656
let sbom_service = SbomService::new(db.clone());
5757
let purl_service = PurlService::new();
58+
let signature_service = SignatureService::new();
5859

5960
config
6061
.app_data(web::Data::new(db))
6162
.app_data(web::Data::new(sbom_service))
6263
.app_data(web::Data::new(purl_service))
64+
.app_data(web::Data::new(signature_service))
6365
.app_data(web::Data::new(Config { upload_limit }))
6466
.service(all)
6567
.service(all_related)
@@ -529,13 +531,14 @@ pub async fn download(
529531
#[get("/v2/sbom/{key}/signature")]
530532
pub async fn list_signatures(
531533
db: web::Data<Database>,
534+
service: web::Data<SignatureService>,
532535
key: web::Path<String>,
533536
web::Query(paginated): web::Query<Paginated>,
534537
_: Require<ReadSbom>,
535538
) -> Result<impl Responder, Error> {
536539
let id = Id::from_str(&key).map_err(Error::IdKey)?;
537540

538-
let result = SignatureService
541+
let result = service
539542
.list_signatures(DocumentType::Sbom, id, paginated, db.get_ref())
540543
.await?;
541544

modules/signature/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,21 @@ trustify-auth = { workspace = true }
1010
trustify-common = { workspace = true }
1111
trustify-entity = { workspace = true }
1212

13-
actix-http = { workspace = true }
1413
actix-web = { workspace = true }
1514
anyhow = { workspace = true }
1615
json-merge-patch = { workspace = true }
1716
log = { workspace = true }
1817
sea-orm = { workspace = true }
1918
sea-query = { workspace = true }
19+
sequoia-openpgp = { workspace = true }
2020
serde = { workspace = true }
2121
serde_json = { workspace = true }
2222
serde_with = { workspace = true }
2323
thiserror = { workspace = true }
24+
tokio = { workspace = true }
2425
tracing = { workspace = true }
25-
tracing-futures = { workspace = true, features = ["futures-03"] }
2626
utoipa = { workspace = true, features = ["actix_extras", "uuid", "time"] }
2727
utoipa-actix-web = { workspace = true }
28+
walker-common = { workspace = true }
2829

2930
[dev-dependencies]

modules/signature/src/model/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,10 @@ pub struct TrustAnchorData {
8787
#[serde_as(as = "serde_with::base64::Base64")]
8888
pub payload: Vec<u8>,
8989
}
90+
91+
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, Eq, PartialEq)]
92+
#[serde(rename_all = "camelCase")]
93+
pub struct VerificationResult {
94+
pub signature: Signature,
95+
pub trust_anchors: Vec<TrustAnchor>,
96+
}

modules/signature/src/service/trust_anchor.rs renamed to modules/signature/src/service/trust_anchor/mod.rs

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1-
use crate::{error::Error, error::PatchError, model::TrustAnchor, model::TrustAnchorData};
1+
mod pgp;
2+
3+
use crate::{
4+
error::{Error, PatchError},
5+
model::{Signature, TrustAnchor, TrustAnchorData, VerificationResult},
6+
service::trust_anchor::pgp::Anchor,
7+
};
28
use sea_orm::{
39
ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait, QueryOrder, Set,
410
prelude::Uuid, query::QueryFilter,
511
};
612
use sea_query::{Expr, Order, SimpleExpr};
7-
use std::fmt::{Debug, Display};
13+
use sequoia_openpgp::{Cert, cert::CertParser, parse::Parse};
14+
use std::time::SystemTime;
15+
use std::{
16+
fmt::{Debug, Display},
17+
fs::File,
18+
};
819
use tracing::instrument;
920
use trustify_common::{
1021
db::{Database, DatabaseErrors, limiter::LimiterTrait},
1122
model::{Paginated, PaginatedResults, Revisioned},
1223
};
13-
use trustify_entity::trust_anchor;
24+
use trustify_entity::{signature_type::SignatureType, trust_anchor};
1425

1526
/// A service managing trust anchors for signatures of documents.
1627
pub struct TrustAnchorService {
@@ -190,4 +201,71 @@ impl TrustAnchorService {
190201

191202
Ok(result.rows_affected > 0)
192203
}
204+
205+
#[instrument(skip(self, signatures))]
206+
pub async fn verify(
207+
&self,
208+
signatures: Vec<Signature>,
209+
content: File,
210+
) -> Result<Vec<VerificationResult>, Error> {
211+
let anchors = trust_anchor::Entity::find().all(&self.db).await?;
212+
213+
let now = SystemTime::now();
214+
215+
let anchors: Vec<_> = anchors
216+
.into_iter()
217+
.filter(|a| !a.disabled)
218+
.filter_map(|anchor| match anchor.r#type {
219+
SignatureType::Pgp => CertParser::from_bytes(&anchor.payload)
220+
.ok()
221+
.and_then(|certificates| certificates.collect::<Result<Vec<Cert>, _>>().ok())
222+
.map(|certificates| {
223+
(
224+
TrustAnchor::from(anchor),
225+
Anchor::Pgp {
226+
certificates,
227+
// TODO: allow specifying time for v3 signatures
228+
policy_time: now,
229+
},
230+
)
231+
}),
232+
})
233+
.collect();
234+
235+
let mut result = Vec::with_capacity(signatures.len());
236+
237+
for signature in signatures {
238+
log::trace!("Signature: {:?}", signature);
239+
240+
let mut trust_anchors = vec![];
241+
242+
for anchor in &anchors {
243+
log::trace!(" Anchor: {:?}", anchor);
244+
245+
match anchor
246+
.1
247+
.validate(
248+
&signature,
249+
content.try_clone().map_err(|err| Error::Any(err.into()))?,
250+
)
251+
.await
252+
{
253+
Ok(()) => {
254+
// TODO: report result
255+
trust_anchors.push(anchor.0.clone());
256+
}
257+
Err(err) => {
258+
log::debug!("Failed: {err}");
259+
}
260+
}
261+
}
262+
263+
result.push(VerificationResult {
264+
signature,
265+
trust_anchors,
266+
});
267+
}
268+
269+
Ok(result)
270+
}
193271
}

0 commit comments

Comments
 (0)