Skip to main content

ssh_stamp_ota/
lib.rs

1// SPDX-FileCopyrightText: 2026 Julio Beltran Ortega <jubeormk1@gmail.com>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//! SFTP-based OTA update server for ssh-stamp.
6//!
7//! Receives firmware images over SFTP, validates the TLV header, writes
8//! chunks to the OTA partition via [`OtaActions`](ssh_stamp_hal::OtaActions),
9//! marks the partition bootable, and resets the device into the new image.
10//!
11//! The [`tlv`] module defines the TLV record format used by the `packer`
12//! host utility and the on-device parser. The `packer` binary
13//! (`ssh-stamp-ota/src/bin/packer.rs`) wraps a raw app binary into an `.otap` blob
14//! with the required TLV header (OTA type, SHA-256 checksum, firmware size).
15//!
16//! This crate is `no_std` on embedded targets. The `std` feature gate and
17//! `cfg(target_os = "none")` keep the SFTP server and handler modules
18//! compiled out on the host, while the [`tlv`] module and [`OtaHeader`]
19//! remain usable from both host and device code.
20
21#![cfg_attr(not(test), no_std)]
22
23/// Runs the OTA server taking care of reading OTA file metadata,
24/// internal state, storage and target reset.
25///
26/// Entry point for this crate when used as an OTA server on the device.
27#[cfg(all(target_os = "none", feature = "sftp"))]
28pub use sftpserver::run_ota_server;
29/// Module handling OTA update metadata and header parsing
30///
31/// It will be called from the sftpserver module to handle the OTA update process
32#[cfg(all(target_os = "none", feature = "sftp"))]
33mod handler;
34/// Module implementing the OTA SFTP server
35#[cfg(all(target_os = "none", feature = "sftp"))]
36mod sftpserver;
37
38/// Module defining TLV types and constants for OTA updates
39///
40/// Re-exporting this module for easier access from outside the crate: packer
41pub mod tlv;
42
43/// OTA Header structure and deserialization logic
44///
45/// Re-exporting Header for easier access from outside the crate: packer
46pub use tlv::OtaHeader;
47
48#[cfg(test)]
49mod ota_tlv_tests {
50
51    use crate::OtaHeader;
52    use crate::tlv::*;
53    use sunset::sshwire;
54
55    #[test]
56    fn test_ota_tlv_round_trip() {
57        let variants = [
58            Tlv::FirmwareBlob { size: 1024 },
59            Tlv::Sha256Checksum {
60                checksum: [
61                    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
62                    23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
63                ],
64            },
65            Tlv::OtaType {
66                ota_type: OTA_TYPE_VALUE_SSH_STAMP,
67            },
68        ];
69        for variant in &variants {
70            let mut buffer = [0u8; MAX_TLV_SIZE as usize];
71            let used = sshwire::write_ssh(&mut buffer, variant).expect("Failed to create SSH sink");
72
73            // sunset 0.6's read_ssh also reports how many bytes it consumed.
74            let (decoded, _) =
75                sshwire::read_ssh::<Tlv>(&buffer[..used], None).expect("Failed to decode TLV");
76            match (variant, decoded) {
77                (Tlv::FirmwareBlob { size: s1 }, Tlv::FirmwareBlob { size: s2 }) => {
78                    assert_eq!(s1, &s2);
79                }
80                (Tlv::Sha256Checksum { checksum: c1 }, Tlv::Sha256Checksum { checksum: c2 }) => {
81                    assert_eq!(c1, &c2);
82                }
83                (Tlv::OtaType { ota_type: o1 }, Tlv::OtaType { ota_type: o2 }) => {
84                    assert_eq!(o1, &o2);
85                }
86                _ => panic!("Decoded variant does not match original"),
87            }
88        }
89    }
90
91    #[test]
92    fn deserializing_full_header() {
93        let mut buffer = [0u8; 512];
94        let mut offset = 0;
95
96        let ota_type_tlv = Tlv::OtaType {
97            ota_type: OTA_TYPE_VALUE_SSH_STAMP,
98        };
99        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_type_tlv)
100            .expect("Failed to write OTA Type TLV");
101
102        let ota_checksum = Tlv::Sha256Checksum {
103            checksum: [
104                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
105                24, 25, 26, 27, 28, 29, 30, 31, 32,
106            ],
107        };
108
109        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_checksum)
110            .expect("Failed to write SHA256 Checksum TLV");
111
112        let firmware_blob_tlv = Tlv::FirmwareBlob { size: 2048 };
113        offset += sshwire::write_ssh(&mut buffer[offset..], &firmware_blob_tlv)
114            .expect("Failed to write Firmware Blob TLV");
115
116        let (header, _) =
117            OtaHeader::deserialize(&buffer[..offset]).expect("Failed to deserialize header");
118
119        assert_eq!(header.ota_type, Some(OTA_TYPE_VALUE_SSH_STAMP));
120        assert_eq!(header.firmware_blob_size, Some(2048));
121        assert_eq!(
122            header.sha256_checksum,
123            Some([
124                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
125                24, 25, 26, 27, 28, 29, 30, 31, 32,
126            ])
127        );
128    }
129
130    #[test]
131    fn tlvs_after_firmware_blob_are_ignored() {
132        let mut buffer = [0u8; 512];
133        let mut offset = 0;
134
135        let ota_type_tlv = Tlv::OtaType {
136            ota_type: OTA_TYPE_VALUE_SSH_STAMP,
137        };
138        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_type_tlv)
139            .expect("Failed to write OTA Type TLV");
140
141        let firmware_blob_tlv = Tlv::FirmwareBlob { size: 2048 };
142        offset += sshwire::write_ssh(&mut buffer[offset..], &firmware_blob_tlv)
143            .expect("Failed to write Firmware Blob TLV");
144
145        // After firmware_blob. Will not be deserialised
146        let ota_checksum = Tlv::Sha256Checksum {
147            checksum: [
148                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
149                24, 25, 26, 27, 28, 29, 30, 31, 32,
150            ],
151        };
152        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_checksum)
153            .expect("Failed to write SHA256 Checksum TLV");
154
155        let (header, _) =
156            OtaHeader::deserialize(&buffer[..offset]).expect("Failed to deserialize header");
157
158        assert_eq!(header.ota_type, Some(OTA_TYPE_VALUE_SSH_STAMP));
159        assert_eq!(header.firmware_blob_size, Some(2048));
160        assert_eq!(header.sha256_checksum, None);
161    }
162
163    #[test]
164    fn ota_type_must_be_first_tlv() {
165        let mut buffer = [0u8; 512];
166        let mut offset = 0;
167
168        let ota_checksum = Tlv::Sha256Checksum {
169            checksum: [
170                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
171                24, 25, 26, 27, 28, 29, 30, 31, 32,
172            ],
173        };
174
175        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_checksum)
176            .expect("Failed to write SHA256 Checksum TLV");
177
178        let ota_type = Tlv::OtaType {
179            ota_type: OTA_TYPE_VALUE_SSH_STAMP,
180        };
181        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_type)
182            .expect("Failed to write OTA Type TLV");
183
184        let firmware_blob_tlv = Tlv::FirmwareBlob { size: 2048 };
185        offset += sshwire::write_ssh(&mut buffer[offset..], &firmware_blob_tlv)
186            .expect("Failed to write Firmware Blob TLV");
187
188        assert!(OtaHeader::deserialize(&buffer[..offset]).is_err());
189    }
190
191    #[test]
192    fn deserializing_header_missing_firmware_blob() {
193        let mut buffer = [0u8; 512];
194        let mut offset = 0;
195
196        let ota_type_tlv = Tlv::OtaType {
197            ota_type: OTA_TYPE_VALUE_SSH_STAMP,
198        };
199        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_type_tlv)
200            .expect("Failed to write OTA Type TLV");
201
202        let ota_checksum = Tlv::Sha256Checksum {
203            checksum: [
204                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
205                24, 25, 26, 27, 28, 29, 30, 31, 32,
206            ],
207        };
208
209        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_checksum)
210            .expect("Failed to write SHA256 Checksum TLV");
211
212        let (header, _) =
213            OtaHeader::deserialize(&buffer[..offset]).expect("Failed to deserialize header");
214
215        assert_eq!(header.ota_type, Some(OTA_TYPE_VALUE_SSH_STAMP));
216        assert_eq!(header.firmware_blob_size, None);
217        assert_eq!(
218            header.sha256_checksum,
219            Some([
220                1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
221                24, 25, 26, 27, 28, 29, 30, 31, 32,
222            ])
223        );
224    }
225
226    #[test]
227    fn skipping_unknown_tlv() {
228        let mut buffer = [0u8; 512];
229        let mut offset = 0;
230
231        let ota_type_tlv = Tlv::OtaType {
232            ota_type: OTA_TYPE_VALUE_SSH_STAMP,
233        };
234        offset += sshwire::write_ssh(&mut buffer[offset..], &ota_type_tlv)
235            .expect("Failed to write OTA Type TLV");
236
237        // Manually generating a valid unknown type
238        let unknown_type: OtaTlvType = 99;
239        let unknown_type_len: OtaTlvLen = 4;
240        let unknown_value: [u8; 4] = [10, 20, 30, 40];
241
242        offset += sshwire::write_ssh(&mut buffer[offset..], &unknown_type)
243            .expect("Failed to write unknown TLV type");
244        offset += sshwire::write_ssh(&mut buffer[offset..], &unknown_type_len)
245            .expect("Failed to write unknown TLV length");
246        offset += sshwire::write_ssh(&mut buffer[offset..], &unknown_value)
247            .expect("Failed to write unknown TLV value");
248
249        let firmware_blob_tlv = Tlv::FirmwareBlob { size: 2048 };
250        let used = sshwire::write_ssh(&mut buffer[offset..], &firmware_blob_tlv)
251            .expect("Failed to write Firmware Blob TLV");
252        offset += used;
253
254        let (header, _) =
255            OtaHeader::deserialize(&buffer[..offset]).expect("Failed to deserialize header");
256
257        assert_eq!(header.ota_type, Some(OTA_TYPE_VALUE_SSH_STAMP));
258        assert_eq!(header.firmware_blob_size, Some(2048));
259        assert_eq!(header.sha256_checksum, None);
260    }
261
262    // TODO: Test more error cases, such as incomplete TLVs
263}