Skip to main content

edgesentry_rs/
update.rs

1//! Software update integrity verification (CLS-03 / STAR-2 R2.2).
2//!
3//! Before applying any firmware or software update on a device, the update
4//! package must be authenticated:
5//!
6//! 1. The raw payload is hashed with BLAKE3 and compared to [`SoftwareUpdate::payload_hash`].
7//! 2. The publisher's Ed25519 signature over `payload_hash` is verified against a
8//!    registered trusted key.
9//!
10//! A failed check returns [`UpdateVerifyError`] and is recorded in
11//! [`UpdateVerificationLog`] so the rejection appears in the audit trail.
12//!
13//! # Example
14//!
15//! ```rust
16//! use ed25519_dalek::SigningKey;
17//! use edgesentry_rs::update::{SoftwareUpdate, UpdateVerifier};
18//! use edgesentry_rs::integrity::compute_payload_hash;
19//! use edgesentry_rs::identity::sign_payload_hash;
20//!
21//! let signing_key = SigningKey::from_bytes(&[7u8; 32]);
22//! let verifying_key = signing_key.verifying_key();
23//! let payload = b"firmware-v1.2.3-image";
24//!
25//! let payload_hash = compute_payload_hash(payload);
26//! let signature   = sign_payload_hash(&signing_key, &payload_hash);
27//!
28//! let update = SoftwareUpdate {
29//!     package_id:   "firmware".to_string(),
30//!     version:      "1.2.3".to_string(),
31//!     payload_hash,
32//!     signature,
33//! };
34//!
35//! let mut verifier = UpdateVerifier::new();
36//! verifier.register_publisher("acme-firmware", verifying_key);
37//!
38//! let mut log = edgesentry_rs::update::UpdateVerificationLog::default();
39//! assert!(verifier.verify(&update, payload, "acme-firmware", &mut log).is_ok());
40//! ```
41
42use std::collections::HashMap;
43
44use ed25519_dalek::VerifyingKey;
45use thiserror::Error;
46
47use crate::identity::verify_payload_signature;
48use crate::integrity::compute_payload_hash;
49use crate::record::{Hash32, Signature64};
50
51// ── Types ────────────────────────────────────────────────────────────────────
52
53/// A signed software update package ready for pre-installation verification.
54#[derive(Debug, Clone)]
55pub struct SoftwareUpdate {
56    /// Unique identifier for the update package (e.g. `"firmware"`, `"app-core"`).
57    pub package_id: String,
58    /// Human-readable version string (e.g. `"1.2.3"`).
59    pub version: String,
60    /// BLAKE3 hash of the raw update payload.
61    pub payload_hash: Hash32,
62    /// Ed25519 signature over `payload_hash` produced by the trusted publisher.
63    pub signature: Signature64,
64}
65
66/// Outcome recorded in [`UpdateVerificationLog`] for every verification attempt.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum UpdateVerifyDecision {
69    Accepted,
70    Rejected,
71}
72
73/// A single entry in the update verification audit trail.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct UpdateVerificationEntry {
76    pub decision:     UpdateVerifyDecision,
77    pub package_id:   String,
78    pub version:      String,
79    pub publisher_id: String,
80    pub message:      String,
81}
82
83/// In-memory log of all update verification attempts.
84#[derive(Debug, Default)]
85pub struct UpdateVerificationLog {
86    entries: Vec<UpdateVerificationEntry>,
87}
88
89impl UpdateVerificationLog {
90    pub fn entries(&self) -> &[UpdateVerificationEntry] {
91        &self.entries
92    }
93
94    fn record(&mut self, entry: UpdateVerificationEntry) {
95        self.entries.push(entry);
96    }
97}
98
99// ── Errors ───────────────────────────────────────────────────────────────────
100
101/// Errors produced by [`UpdateVerifier::verify`].
102#[derive(Debug, Error, PartialEq, Eq)]
103pub enum UpdateVerifyError {
104    /// No key has been registered for the given publisher.
105    #[error("unknown publisher '{publisher_id}'")]
106    UnknownPublisher { publisher_id: String },
107
108    /// The BLAKE3 hash of the supplied payload does not match the update manifest.
109    #[error("payload hash mismatch for package '{package_id}' version '{version}'")]
110    PayloadHashMismatch { package_id: String, version: String },
111
112    /// The publisher signature is invalid or was produced by a different key.
113    #[error("invalid publisher signature for package '{package_id}' version '{version}'")]
114    InvalidSignature { package_id: String, version: String },
115}
116
117// ── Verifier ─────────────────────────────────────────────────────────────────
118
119/// Verifies software update packages before installation.
120///
121/// Register one or more trusted publisher keys with [`register_publisher`](Self::register_publisher),
122/// then call [`verify`](Self::verify) for each candidate update. Failed
123/// verifications are automatically recorded in a supplied [`UpdateVerificationLog`].
124#[derive(Debug, Default)]
125pub struct UpdateVerifier {
126    trusted_keys: HashMap<String, VerifyingKey>,
127}
128
129impl UpdateVerifier {
130    /// Create a verifier with no trusted publishers.
131    pub fn new() -> Self {
132        Self::default()
133    }
134
135    /// Register a trusted publisher key.
136    ///
137    /// Only updates signed by a registered publisher will pass verification.
138    pub fn register_publisher(&mut self, publisher_id: &str, key: VerifyingKey) {
139        self.trusted_keys.insert(publisher_id.to_string(), key);
140    }
141
142    /// Verify `update` against `payload` and record the outcome in `log`.
143    ///
144    /// Returns `Ok(())` only when:
145    /// - `publisher_id` is registered,
146    /// - `BLAKE3(payload) == update.payload_hash`, and
147    /// - the Ed25519 signature is valid.
148    ///
149    /// Any failure appends a [`UpdateVerifyDecision::Rejected`] entry to `log`
150    /// and returns the corresponding [`UpdateVerifyError`].
151    pub fn verify(
152        &self,
153        update: &SoftwareUpdate,
154        payload: &[u8],
155        publisher_id: &str,
156        log: &mut UpdateVerificationLog,
157    ) -> Result<(), UpdateVerifyError> {
158        let result = self.check(update, payload, publisher_id);
159
160        let (decision, message) = match &result {
161            Ok(()) => (
162                UpdateVerifyDecision::Accepted,
163                format!(
164                    "update accepted: package={} version={}",
165                    update.package_id, update.version
166                ),
167            ),
168            Err(e) => (UpdateVerifyDecision::Rejected, e.to_string()),
169        };
170
171        log.record(UpdateVerificationEntry {
172            decision,
173            package_id:   update.package_id.clone(),
174            version:      update.version.clone(),
175            publisher_id: publisher_id.to_string(),
176            message,
177        });
178
179        result
180    }
181
182    fn check(
183        &self,
184        update: &SoftwareUpdate,
185        payload: &[u8],
186        publisher_id: &str,
187    ) -> Result<(), UpdateVerifyError> {
188        let key = self.trusted_keys.get(publisher_id).ok_or_else(|| {
189            UpdateVerifyError::UnknownPublisher {
190                publisher_id: publisher_id.to_string(),
191            }
192        })?;
193
194        let actual_hash = compute_payload_hash(payload);
195        if actual_hash != update.payload_hash {
196            return Err(UpdateVerifyError::PayloadHashMismatch {
197                package_id: update.package_id.clone(),
198                version:    update.version.clone(),
199            });
200        }
201
202        if !verify_payload_signature(key, &update.payload_hash, &update.signature) {
203            return Err(UpdateVerifyError::InvalidSignature {
204                package_id: update.package_id.clone(),
205                version:    update.version.clone(),
206            });
207        }
208
209        Ok(())
210    }
211}