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}