Skip to main content

csaf_models/
provider_meta.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne
3
4//! CSAF provider metadata model.
5
6use serde::{Deserialize, Serialize};
7
8/// CSAF provider metadata document.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct ProviderMetadata {
11    /// Canonical URL for this metadata file.
12    pub canonical_url: String,
13
14    /// Distribution endpoints.
15    #[serde(default, skip_serializing_if = "Vec::is_empty")]
16    pub distributions: Vec<ProviderDistribution>,
17
18    /// Last updated timestamp (ISO 8601).
19    pub last_updated: String,
20
21    /// Whether to list on CSAF aggregators.
22    #[serde(default, rename = "list_on_CSAF_aggregators")]
23    pub list_on_csaf_aggregators: bool,
24
25    /// Metadata schema version.
26    pub metadata_version: String,
27
28    /// Whether to allow mirroring on CSAF aggregators.
29    #[serde(default, rename = "mirror_on_CSAF_aggregators")]
30    pub mirror_on_csaf_aggregators: bool,
31
32    /// Public `OpenPGP` keys for signature verification.
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub public_openpgp_keys: Vec<OpenPgpKey>,
35
36    /// Publisher identity.
37    pub publisher: ProviderPublisher,
38
39    /// Publisher role (e.g. `csaf_publisher`, `csaf_provider`, `csaf_trusted_provider`).
40    pub role: String,
41}
42
43/// A distribution endpoint for CSAF documents.
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45pub struct ProviderDistribution {
46    /// Directory URL where CSAF documents are published.
47    pub directory_url: String,
48}
49
50/// An `OpenPGP` public key reference.
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub struct OpenPgpKey {
53    /// Key fingerprint (40 hex characters).
54    pub fingerprint: String,
55
56    /// URL to download the public key.
57    pub url: String,
58}
59
60/// Publisher identity within provider metadata.
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct ProviderPublisher {
63    /// Publisher category.
64    pub category: String,
65
66    /// Contact email or details.
67    pub contact_details: String,
68
69    /// Publisher name.
70    pub name: String,
71
72    /// Publisher namespace URI.
73    pub namespace: String,
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_deserialize_provider_metadata() {
82        let json = include_str!("../../../test/csaf/provider-metadata.json");
83        let meta: ProviderMetadata =
84            serde_json::from_str(json).expect("Failed to deserialize provider metadata");
85
86        // The in-tree provider-metadata.json fixture advertises CSAF 2.1;
87        // keep this assertion aligned with the fixture to avoid silent drift.
88        assert_eq!(meta.metadata_version, "2.1");
89        assert_eq!(meta.role, "csaf_publisher");
90        assert_eq!(meta.publisher.category, "vendor");
91        assert!(meta.list_on_csaf_aggregators);
92        assert!(meta.mirror_on_csaf_aggregators);
93        assert_eq!(meta.public_openpgp_keys.len(), 1);
94        assert_eq!(
95            meta.public_openpgp_keys[0].fingerprint,
96            "D1DE14AA6D1980BD3FB67BF5C706DFBCAC5EFA64"
97        );
98    }
99
100    #[test]
101    fn test_roundtrip_provider_metadata() {
102        let json = include_str!("../../../test/csaf/provider-metadata.json");
103        let meta: ProviderMetadata = serde_json::from_str(json).expect("parse error");
104        let serialized = serde_json::to_string_pretty(&meta).expect("serialize error");
105        let meta2: ProviderMetadata = serde_json::from_str(&serialized).expect("re-parse error");
106        assert_eq!(meta, meta2);
107    }
108}