Skip to main content

xous_tools/
sign_image.rs

1use std::collections::HashMap;
2use std::fs;
3use std::io::{Read, Write};
4use std::path::Path;
5
6use bao1x_api::signatures::{
7    FunctionCode, PADDING_LEN, SIGBLOCK_LEN, SealedFields, SignatureInFlash, UNSIGNED_LEN,
8};
9use ed25519_dalek::{DigestSigner, SigningKey};
10use pkcs8::PrivateKeyInfo;
11use pkcs8::der::Decodable;
12use ring::signature::{Ed25519KeyPair, KeyPair};
13use sha2::{Digest, Sha512};
14
15#[repr(u32)]
16#[derive(PartialEq, Eq, Clone, Copy)]
17pub enum Version {
18    Loader = 1,
19    LoaderPrehash = 2,
20    Bao1xV1 = 0x1_00,
21}
22use xous_semver::SemVer;
23
24pub fn generate_jal_x0(signed_offset: isize) -> Result<u32, String> {
25    // Check that offset is 2-byte aligned (even)
26    if signed_offset & 1 != 0 {
27        return Err("JAL offset must be 2-byte aligned (even)".to_string());
28    }
29
30    // Check that offset fits in 21-bit signed range
31    // JAL can encode offsets from -2^20 to 2^20 - 2
32    const MIN_OFFSET: isize = -(1 << 20); // -1048576
33    const MAX_OFFSET: isize = (1 << 20) - 2; // 1048574
34
35    if signed_offset < MIN_OFFSET || signed_offset > MAX_OFFSET {
36        return Err(format!("JAL offset {} is out of range [{}, {}]", signed_offset, MIN_OFFSET, MAX_OFFSET));
37    }
38
39    let imm = signed_offset as u32;
40
41    // Extract bit fields for JAL J-type encoding
42    // JAL immediate format: [20|10:1|11|19:12]
43    let imm_20 = (imm >> 20) & 1; // bit 20 -> instruction bit 31
44    let imm_19_12 = (imm >> 12) & 0xFF; // bits 19:12 -> instruction bits 19:12
45    let imm_11 = (imm >> 11) & 1; // bit 11 -> instruction bit 20
46    let imm_10_1 = (imm >> 1) & 0x3FF; // bits 10:1 -> instruction bits 30:21
47
48    // Assemble the JAL instruction
49    // Format: imm[20] | imm[10:1] | imm[11] | imm[19:12] | rd | opcode
50    let instruction = (imm_20 << 31) |      // imm[20] at bit 31
51                     (imm_10_1 << 21) |    // imm[10:1] at bits 30:21
52                     (imm_11 << 20) |      // imm[11] at bit 20
53                     (imm_19_12 << 12) |   // imm[19:12] at bits 19:12
54                     // rd = x0 = 0 (bits 11:7)
55                     0x6F; // JAL opcode (0b1101111)
56
57    Ok(instruction)
58}
59
60pub fn load_pem(src: &str) -> Result<pem::Pem, Box<dyn std::error::Error>> {
61    let mut input = vec![];
62    let mut pemfile = std::fs::File::open(src)?;
63    pemfile.read_to_end(&mut input)?;
64
65    Ok(pem::parse(input)?)
66}
67
68pub fn sign_image(
69    source: &[u8],
70    private_key: &pem::Pem,
71    defile: bool,
72    minver: &Option<SemVer>,
73    semver: Option<[u8; 16]>,
74    with_jump: bool,
75    length: usize,
76    version: Version,
77    function_code: Option<&str>,
78    anti_rollback_manual: Option<usize>,
79) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
80    let mut dest_file = vec![];
81
82    // Append version information to the binary. Appending it here means it is part
83    // of the signed bundle.
84    let minver_bytes = if let Some(mv) = minver { mv.into() } else { [0u8; 16] };
85    let semver: [u8; 16] = match semver {
86        Some(semver) => semver,
87        None => SemVer::from_git()?.into(),
88    };
89
90    match version {
91        Version::Loader | Version::LoaderPrehash => {
92            let mut source = source.to_owned();
93            // extra data appended here needs to be reflected in two places in Xous:
94            // 1. root-keys/src/implementation.rs @ sign-loader()
95            // 2. graphics-server/src/main.rs @ Some(Opcode::BulkReadfonts)
96            // This is because memory ownership is split between two crates for performance reasons:
97            // the direct memory page of fonts belongs to the graphics server, to avoid having to send
98            // a message on every font lookup. However, the keys reside in root-keys, so therefore,
99            // a bulk read operation has to shuttle font data back to the root-keys crate. Of course,
100            // the appended metadata is in the font region, so, this data has to be shuttled back.
101            // The graphics server is also entirely naive to how much cryptographic data is in the font
102            // region, and I think it's probably better for it to stay that way.
103            source.append(&mut minver_bytes.to_vec());
104            source.append(&mut semver.to_vec());
105            let prehash = match version {
106                Version::Loader => false,
107                Version::LoaderPrehash => true,
108                _ => return Err(String::from("Unhandled image version").into()),
109            };
110            for &b in (version as u32).to_le_bytes().iter() {
111                source.push(b);
112            }
113            for &b in (source.len() as u32).to_le_bytes().iter() {
114                source.push(b);
115            }
116
117            let (signature, pubkey) = if prehash {
118                // pre-hash the message
119                let mut h: Sha512 = Sha512::new();
120                h.update(&source);
121
122                let pkinfo = PrivateKeyInfo::from_der(&private_key.contents).map_err(|e| format!("{}", e))?;
123                // First 2 bytes of the `private_key` are a record specifier and length field. Check they are
124                // correct.
125                assert!(pkinfo.private_key[0] == 0x4);
126                assert!(pkinfo.private_key[1] == 0x20);
127                let mut secbytes = [0u8; 32];
128                secbytes.copy_from_slice(&pkinfo.private_key[2..]);
129                // Now we can use the private key data.
130                let signing_key = SigningKey::from_bytes(&secbytes);
131
132                // derive a private key
133                let sk = Ed25519KeyPair::from_pkcs8_maybe_unchecked(&private_key.contents)
134                    .map_err(|e| format!("{}", e))?;
135                let mut pubkey_bytes = [0u8; 32];
136                pubkey_bytes.copy_from_slice(sk.public_key().as_ref());
137
138                let sig = signing_key.sign_digest(h.clone()).to_bytes();
139                (sig, pubkey_bytes)
140            } else {
141                // NOTE NOTE NOTE
142                // can't find a good ASN.1 ED25519 key decoder, just relying on the fact that the last
143                // 32 bytes are "always" the private key. always? the private key?
144                let signing_key = Ed25519KeyPair::from_pkcs8_maybe_unchecked(&private_key.contents)
145                    .map_err(|e| format!("{}", e))?;
146                let mut sig = [0u8; 64];
147                sig.copy_from_slice(signing_key.sign(&source).as_ref());
148                let mut pubkey_bytes = [0u8; 32];
149                pubkey_bytes.copy_from_slice(signing_key.public_key().as_ref());
150                (sig, pubkey_bytes)
151            };
152
153            let jal = generate_jal_x0(length as isize)?;
154            // println!("offset {:x}, jal {:x}", length, jal);
155            let extra_pad = if with_jump {
156                dest_file.write_all(&jal.to_le_bytes())?;
157                4
158            } else {
159                0
160            };
161
162            dest_file.write_all(&(version as u32).to_le_bytes())?;
163            dest_file.write_all(&(source.len() as u32).to_le_bytes())?;
164
165            // Write the signature data
166            dest_file.write_all(&signature)?;
167
168            // Write the public key - for now it's just an interim key, but this should be replaced with
169            // the actual code signing key eventually. This is only relevant for baochip targets, Precursor
170            // has this pre-burned into its KEYROM.
171            dest_file.write_all(&pubkey)?;
172
173            // Pad the first sector to length bytes.
174            let mut v = vec![];
175            v.resize(length - 4 - 4 - signature.len() - extra_pad - pubkey.len(), 0);
176            dest_file.write_all(&v)?;
177
178            // Fill the remainder of the source data
179
180            if defile {
181                println!(
182                    "WARNING: defiling the loader image. This corrupts the binary and should cause it to fail the signature check."
183                );
184                source[16778] ^= 0x1 // flip one bit at some random offset
185            }
186
187            dest_file.write_all(&source)?;
188
189            Ok(dest_file)
190        }
191        Version::Bao1xV1 => {
192            let pkinfo = PrivateKeyInfo::from_der(&private_key.contents).map_err(|e| format!("{}", e))?;
193            // First 2 bytes of the `private_key` are a record specifier and length field. Check they are
194            // correct.
195            assert!(pkinfo.private_key[0] == 0x4);
196            assert!(pkinfo.private_key[1] == 0x20);
197            let mut secbytes = [0u8; 32];
198            secbytes.copy_from_slice(&pkinfo.private_key[2..]);
199            // Now we can use the private key data.
200            let signing_key = SigningKey::from_bytes(&secbytes);
201
202            // This is handy code to remember - a quick way to get the public key from the private key. Just
203            // in case I need this in the future to sanity check some values.
204            /*
205                let sk = Ed25519KeyPair::from_pkcs8_maybe_unchecked(&private_key.contents)
206                    .map_err(|e| format!("{}", e))?;
207                let pubkey = sk.public_key();
208            */
209            let anti_rollback = if let Some(code) = anti_rollback_manual {
210                code as u32
211            } else {
212                let anti_rollback =
213                    read_anti_rollback("./signing/anti-rollback.hjson").inspect_err(|_e| {
214                        println!("anti-rollback.hjson file not present, can't determine anti-rollback code!");
215                    })?;
216                *(anti_rollback
217                    .get(function_code.unwrap_or("unspecified"))
218                    .expect("anti-rollback code not specified"))
219            };
220
221            let function_code = match function_code {
222                Some("boot0") => FunctionCode::Boot0,
223                Some("boot1") => FunctionCode::Boot1,
224                Some("loader") => FunctionCode::Loader,
225                Some("kernel") => FunctionCode::Kernel,
226                Some("app") => FunctionCode::App,
227                Some("swap") => FunctionCode::Swap,
228                Some("baremetal") => FunctionCode::Baremetal,
229                _ => panic!("Invalid function code"),
230            };
231            let mut header = SignatureInFlash::default();
232            header.sealed_data.version = version as u32;
233            header.sealed_data.signed_len = (source.len() + SIGBLOCK_LEN - UNSIGNED_LEN) as u32;
234            header.sealed_data.function_code = function_code as u32;
235            header.sealed_data.anti_rollback = anti_rollback;
236            header.sealed_data.min_semver = minver_bytes;
237            header.sealed_data.semver = semver;
238
239            // whack in all the public keys, defined in the bao1x-api crate
240            for (dst, src) in
241                header.sealed_data.pubkeys.iter_mut().zip(bao1x_api::pubkeys::PUBKEY_HEADER.iter())
242            {
243                dst.populate_from(src);
244            }
245
246            let mut protected = Vec::new();
247            protected.extend_from_slice(header.sealed_data.as_ref());
248            protected.resize(protected.len() + PADDING_LEN, 0);
249            protected.extend_from_slice(&source);
250
251            // pre-hash the message
252            let mut h: Sha512 = Sha512::new();
253            h.update(&protected);
254
255            let sig = signing_key.sign_digest(h.clone()).to_bytes();
256            header._jal_instruction = generate_jal_x0(SIGBLOCK_LEN as isize)?;
257            header.signature.copy_from_slice(&sig);
258            // no AAD on this type of signature
259            header.aad_len = 0;
260
261            // Write the header
262            dest_file.write_all(&header.as_ref()[..UNSIGNED_LEN])?;
263            // println!("dest_file wrote {}, {}", dest_file.len(), UNSIGNED_LEN);
264
265            if defile {
266                println!(
267                    "WARNING: defiling the loader image. This corrupts the binary and should cause it to fail the signature check."
268                );
269                protected[size_of::<SealedFields>() + 16] ^= 0x1 // flip one bit in the zero-padding region
270            }
271
272            dest_file.write_all(&protected)?;
273
274            if function_code == FunctionCode::Kernel {
275                let target = bao1x_api::RRAM_STORAGE_LEN - (bao1x_api::KERNEL_START - bao1x_api::BOOT0_START);
276                if dest_file.len() > target {
277                    println!(
278                        "ERROR: Xous RRAM image is too big to fit: {} bytes too large ({} bytes; {} limit)",
279                        dest_file.len() - target,
280                        dest_file.len(),
281                        target,
282                    );
283                    return Err(String::from("Image doesn't fit").into());
284                } else {
285                    println!(
286                        "=== Kernel is {} bytes: {} bytes free remaining ===",
287                        dest_file.len(),
288                        target - dest_file.len()
289                    );
290                }
291            }
292            Ok(dest_file)
293        }
294    }
295}
296
297pub fn sign_file<S, T>(
298    input: &S,
299    output: &T,
300    private_key: &pem::Pem,
301    defile: bool,
302    minver: &Option<SemVer>,
303    version: Version,
304    with_jump: bool,
305    sector_length: usize,
306    function_code: Option<&str>,
307) -> Result<(), Box<dyn std::error::Error>>
308where
309    S: AsRef<Path>,
310    T: AsRef<Path>,
311{
312    let mut source = vec![];
313    let mut source_file = std::fs::File::open(input)?;
314    let mut dest_file = std::fs::File::create(output)?;
315    source_file.read_to_end(&mut source)?;
316
317    let result = sign_image(
318        &source,
319        private_key,
320        defile,
321        minver,
322        None,
323        with_jump,
324        sector_length,
325        version,
326        function_code,
327        None,
328    )?;
329    dest_file.write_all(&result)?;
330    Ok(())
331}
332
333pub fn convert_to_uf2<S, T>(
334    input: &S,
335    output: &T,
336    function_code: Option<&str>,
337    offset: Option<usize>,
338) -> Result<(), Box<dyn std::error::Error>>
339where
340    S: AsRef<Path>,
341    T: AsRef<Path>,
342{
343    let mut source = vec![];
344    let mut source_file = std::fs::File::open(input)?;
345    let mut dest_file = std::fs::File::create(output)?;
346    source_file.read_to_end(&mut source)?;
347
348    // maintenance note: there is a mirror of this code in signing/fido-signer/src/main.rs
349    // if you're editing this, maybe consider also trying to librarify these routines?
350    // the key challenge is that fido-signer has to be out of workspace because it relies
351    // on cryptographic primitives that are pinned to versions that are not compatible
352    // with the baochip hardware, e.g. the FIDO2 libraries pin to an incompatible version
353    // with xous-core, so it's a little awkward spanning/sharing crates this way.
354    let app_start_addr = match function_code {
355        Some("boot0") => bao1x_api::BOOT0_START,
356        Some("boot1") => bao1x_api::BOOT1_START,
357        Some("loader") => bao1x_api::LOADER_START,
358        Some("baremetal") => bao1x_api::BAREMETAL_START,
359        Some("kernel") => bao1x_api::KERNEL_START,
360        Some("swap") => bao1x_api::SWAP_START_UF2,
361        Some("app") => bao1x_api::dabao::APP_RRAM_START,
362        _ => return Err(String::from("UF2 Image Requires a function code").into()),
363    };
364
365    match bin_to_uf2(
366        &source,
367        bao1x_api::BAOCHIP_1X_UF2_FAMILY,
368        app_start_addr as u32 + offset.unwrap_or(0) as u32,
369    ) {
370        Ok(u2f_blob) => {
371            dest_file.write_all(&u2f_blob)?;
372            Ok(())
373        }
374        Err(e) => Err(e.into()),
375    }
376}
377
378use byteorder::{LittleEndian, WriteBytesExt};
379
380const UF2_MAGIC_START0: u32 = 0x0A324655; // "UF2\n"
381const UF2_MAGIC_START1: u32 = 0x9E5D5157; // Randomly selected
382const UF2_MAGIC_END: u32 = 0x0AB16F30; // Ditto
383// Vendored in from https://github.com/sajattack/uf2conv-rs/blob/master/lib/src/lib.rs
384// The code is MIT-licensed, and authored by Paul Sajna
385pub fn bin_to_uf2(bytes: &Vec<u8>, family_id: u32, app_start_addr: u32) -> Result<Vec<u8>, std::io::Error> {
386    let datapadding = [0u8; 512 - 256 - 32 - 4];
387    let nblocks: u32 = ((bytes.len() + 255) / 256) as u32;
388    let mut outp: Vec<u8> = Vec::new();
389    for blockno in 0..nblocks {
390        let ptr = 256 * blockno;
391        let chunk = match bytes.get(ptr as usize..ptr as usize + 256) {
392            Some(bytes) => bytes.to_vec(),
393            None => {
394                let mut chunk = bytes[ptr as usize..bytes.len()].to_vec();
395                while chunk.len() < 256 {
396                    chunk.push(0);
397                }
398                chunk
399            }
400        };
401        let mut flags: u32 = 0;
402        if family_id != 0 {
403            flags |= 0x2000
404        }
405
406        // header
407        outp.write_u32::<LittleEndian>(UF2_MAGIC_START0)?;
408        outp.write_u32::<LittleEndian>(UF2_MAGIC_START1)?;
409        outp.write_u32::<LittleEndian>(flags)?;
410        outp.write_u32::<LittleEndian>(ptr + app_start_addr)?;
411        outp.write_u32::<LittleEndian>(256)?;
412        outp.write_u32::<LittleEndian>(blockno)?;
413        outp.write_u32::<LittleEndian>(nblocks)?;
414        outp.write_u32::<LittleEndian>(family_id)?;
415
416        // data
417        outp.write(&chunk)?;
418        outp.write(&datapadding)?;
419
420        // footer
421        outp.write_u32::<LittleEndian>(UF2_MAGIC_END)?;
422    }
423    Ok(outp)
424}
425
426/// Strips // comments from HJSON and returns valid JSON
427fn hjson_to_json(hjson: &str) -> String {
428    hjson
429        .lines()
430        .map(|line| {
431            // Find the position of // comment marker
432            if let Some(pos) = line.find("//") { &line[..pos] } else { line }
433        })
434        .collect::<Vec<_>>()
435        .join("\n")
436}
437
438/// Reads HJSON config file and returns a HashMap for lookups
439fn read_anti_rollback<P: AsRef<Path>>(path: P) -> Result<HashMap<String, u32>, Box<dyn std::error::Error>> {
440    let content = fs::read_to_string(path)?;
441    let json_content = hjson_to_json(&content);
442
443    // Parse as HashMap with string values first
444    let string_map: HashMap<String, String> = serde_json::from_str(&json_content)?;
445
446    // Convert string values to u32
447    let mut result = HashMap::new();
448    for (key, value) in string_map {
449        let version: u32 = value.parse()?;
450        result.insert(key, version);
451    }
452
453    Ok(result)
454}