[opentitantool] Support bootstrap protocol of legacy chips

Implement the bootstrapping protocol used by previous generations of
Google Titan chips.

Signed-off-by: Jes B. Klinke <jbk@chromium.org>
diff --git a/sw/host/opentitanlib/src/bootstrap/legacy.rs b/sw/host/opentitanlib/src/bootstrap/legacy.rs
new file mode 100644
index 0000000..5ef68f0
--- /dev/null
+++ b/sw/host/opentitanlib/src/bootstrap/legacy.rs
@@ -0,0 +1,271 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+use anyhow::Result;
+use mundane::hash::{Digest, Hasher, Sha256};
+use std::time::Duration;
+use thiserror::Error;
+use zerocopy::AsBytes;
+
+use crate::bootstrap::{BootstrapOptions, UpdateProtocol};
+use crate::io::spi::{Target, Transfer};
+
+#[derive(AsBytes, Debug, Default)]
+#[repr(C)]
+struct FrameHeader {
+    hash: [u8; Frame::HASH_LEN],
+    frame_num: u32,
+    flash_offset: u32,
+}
+
+#[derive(AsBytes, Debug)]
+#[repr(C)]
+struct Frame {
+    header: FrameHeader,
+    data: [u8; Frame::DATA_LEN],
+}
+
+impl Default for Frame {
+    fn default() -> Self {
+        Frame {
+            header: Default::default(),
+            data: [0xff; Frame::DATA_LEN],
+        }
+    }
+}
+
+impl Frame {
+    const EOF: u32 = 0x8000_0000;
+    const FLASH_SECTOR_SIZE: usize = 2048;
+    const FLASH_SECTOR_MASK: usize = Self::FLASH_SECTOR_SIZE - 1;
+    const FLASH_BUFFER_SIZE: usize = 128;
+    const FLASH_BUFFER_MASK: usize = Self::FLASH_BUFFER_SIZE - 1;
+    const DATA_LEN: usize = 2048 - std::mem::size_of::<FrameHeader>();
+    const HASH_LEN: usize = 32;
+    const FLASH_BASE_ADDRESS: usize = 65536 * 8;
+
+    /// Computes the hash in the header.
+    fn header_hash(&self) -> [u8; Frame::HASH_LEN] {
+        let frame = self.as_bytes();
+        let sha = Sha256::hash(&frame[Frame::HASH_LEN..]);
+        sha.bytes()
+    }
+
+    /// Computes the hash over the entire frame.
+    fn frame_hash(&self) -> [u8; Frame::HASH_LEN] {
+        let sha = Sha256::hash(self.as_bytes());
+        let mut digest = sha.bytes();
+        // Touch up zeroes into ones, as that is what the old chips are doing.
+        for b in &mut digest {
+            if *b == 0 {
+                *b = 1;
+            }
+        }
+        digest
+    }
+
+    /// Creates a sequence of frames based on a `payload` binary.
+    fn from_payload(payload: &[u8]) -> Vec<Frame> {
+        let mut frames = Vec::new();
+
+        let max_addr = (payload
+            .chunks(4)
+            .rposition(|c| c != &[0xff; 4])
+            .unwrap_or(0)
+            + 1)
+            * 4;
+
+        let mut frame_num = 0;
+        let mut addr = 0;
+        while addr < max_addr {
+            // Try skipping over 0xffffffff words.
+            let nonempty_addr = addr
+                + payload[addr..]
+                    .chunks(4)
+                    .position(|c| c != &[0xff; 4])
+                    .unwrap()
+                    * 4;
+            let skip_addr = nonempty_addr & !Self::FLASH_SECTOR_MASK;
+            if skip_addr > addr && (addr == 0 || addr & Self::FLASH_BUFFER_MASK != 0) {
+                // Can only skip from the start or if the last addr wasn't an exact multiple of
+                // 128 (per H1D boot rom).
+                addr = skip_addr;
+            }
+
+            let mut frame = Frame {
+                header: FrameHeader {
+                    frame_num,
+                    flash_offset: (addr + Self::FLASH_BASE_ADDRESS) as u32,
+                    ..Default::default()
+                },
+                ..Default::default()
+            };
+            let slice_size = Self::DATA_LEN.min(payload.len() - addr);
+            frame.data[..slice_size].copy_from_slice(&payload[addr..addr + slice_size]);
+            frame.header.hash = frame.header_hash();
+            frames.push(frame);
+
+            addr += Self::DATA_LEN;
+            frame_num += 1;
+        }
+        frames.last_mut().map(|f| f.header.frame_num |= Self::EOF);
+        frames
+    }
+}
+
+/// Implements the bootstrap protocol of previous Google Titan family chips.
+pub struct Legacy {
+    pub inter_frame_delay: Duration,
+    pub flash_erase_delay: Duration,
+}
+
+impl Legacy {
+    const INTER_FRAME_DELAY: Duration = Duration::from_millis(20);
+    const FLASH_ERASE_DELAY: Duration = Duration::from_millis(200);
+
+    /// Creates a new `Primitive` protocol updater from `options`.
+    pub fn new(options: &BootstrapOptions) -> Self {
+        Self {
+            inter_frame_delay: options.inter_frame_delay.unwrap_or(Self::INTER_FRAME_DELAY),
+            flash_erase_delay: options.flash_erase_delay.unwrap_or(Self::FLASH_ERASE_DELAY),
+        }
+    }
+}
+
+#[derive(Debug, Error)]
+pub enum LegacyBootstrapError {
+    #[error("Boot rom not ready")]
+    NotReady,
+    #[error("Unknown boot rom error: {0}")]
+    Unknown(u8),
+    #[error("Boot rom error: NOREQUEST")]
+    NoRequest,
+    #[error("Boot rom error: NOMAGIC")]
+    NoMagic,
+    #[error("Boot rom error: TOOBIG")]
+    TooBig,
+    #[error("Boot rom error: TOOHIGH")]
+    TooHigh,
+    #[error("Boot rom error: NOALIGN")]
+    NoAlign,
+    #[error("Boot rom error: NOROUND")]
+    NoRound,
+    #[error("Boot rom error: BADKEY")]
+    BadKey,
+    #[error("Boot rom error: BADSTART")]
+    BadStart,
+    #[error("Boot rom error: NOWIPE")]
+    NoWipe,
+    #[error("Boot rom error: NOWIPE0")]
+    NoWipe0,
+    #[error("Boot rom error: NOWIPE1")]
+    NoWipe1,
+    #[error("Boot rom error: NOTEMPTY")]
+    NotEmpty,
+    #[error("Boot rom error: NOWRITE")]
+    NoWrite,
+    #[error("Boot rom error: BADADR")]
+    BadAdr,
+    #[error("Boot rom error: OVERFLOW")]
+    Overflow,
+}
+
+impl From<u8> for LegacyBootstrapError {
+    fn from(value: u8) -> LegacyBootstrapError {
+        match value {
+            // All zeroes or all ones means that the bootloader is not yet ready to respond.
+            0 | 255 => LegacyBootstrapError::NotReady,
+            // Other values represent particular errors.
+            1 => LegacyBootstrapError::NoRequest,
+            2 => LegacyBootstrapError::NoMagic,
+            3 => LegacyBootstrapError::TooBig,
+            4 => LegacyBootstrapError::TooHigh,
+            5 => LegacyBootstrapError::NoAlign,
+            6 => LegacyBootstrapError::NoRound,
+            7 => LegacyBootstrapError::BadKey,
+            8 => LegacyBootstrapError::BadStart,
+            10 => LegacyBootstrapError::NoWipe,
+            11 => LegacyBootstrapError::NoWipe0,
+            12 => LegacyBootstrapError::NoWipe1,
+            13 => LegacyBootstrapError::NotEmpty,
+            14 => LegacyBootstrapError::NoWrite,
+            15 => LegacyBootstrapError::BadAdr,
+            16 => LegacyBootstrapError::Overflow,
+            n => LegacyBootstrapError::Unknown(n),
+        }
+    }
+}
+
+impl UpdateProtocol for Legacy {
+    /// Performs the update protocol using the `transport` with the firmware `payload`.
+    fn update(&self, spi: &dyn Target, payload: &[u8]) -> Result<()> {
+        let frames = Frame::from_payload(payload);
+
+        for (i, frame) in frames.iter().enumerate() {
+            let want_hash = frames[i.saturating_sub(1)].frame_hash();
+            // Repeatedly transmit the frame, until receiving expected response.
+            loop {
+                // TODO: Introduce a progress callback, to allow common code to show progress bar.
+                eprint!("{}.", i);
+
+                // Important? Comment in spiflash.cc indicates that it seems to corrupt without a
+                // 1ms delay.
+                std::thread::sleep(self.inter_frame_delay);
+
+                // Write the frame and read back the hash of the previous frame.
+                let mut response = [0u8; std::mem::size_of::<Frame>()];
+                spi.run_transaction(&mut [Transfer::Both(frame.as_bytes(), &mut response)])?;
+
+                if response.iter().all(|&x| x == response[0]) {
+                    // A response consisteing of all identical bytes is a status code.
+                    match LegacyBootstrapError::from(response[0]) {
+                        LegacyBootstrapError::NotReady => continue, // Retry sending same frame.
+                        error => return Err(error.into()),
+                    }
+                }
+
+                // Compare response with checksum of last block sent.
+                if response[..Frame::HASH_LEN] == want_hash {
+                    // Successful response, move on to the next frame.
+                    break;
+                }
+                log::error!(
+                    "Frame hash mismatch device:{:x?} != frame:{:x?}.",
+                    &response[..Frame::HASH_LEN],
+                    want_hash
+                );
+                // Retry sending same frame.
+            }
+        }
+        eprintln!("success");
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const SIMPLE_BIN: &[u8; 2048] = include_bytes!("simple.bin");
+
+    #[test]
+    fn test_small_binary() -> Result<()> {
+        let frames = Frame::from_payload(SIMPLE_BIN);
+
+        assert_eq!(frames[0].header.frame_num, 0);
+        assert_eq!(frames[0].header.flash_offset, 0x80000);
+        assert_eq!(
+            hex::encode(frames[0].header.hash),
+            "4e31bfd8b3be32358f2235c0f241f3970de575fc6aca0564aa6bf30adaf33910"
+        );
+
+        assert_eq!(frames[1].header.frame_num, 0x8000_0001);
+        assert_eq!(frames[1].header.flash_offset, 0x807d8);
+        assert_eq!(
+            hex::encode(frames[1].header.hash),
+            "ff584c07bbb0a039934a660bd49b7812af8ee847d1e675d9aba71c11fab3cfcb"
+        );
+        Ok(())
+    }
+}
diff --git a/sw/host/opentitanlib/src/bootstrap/mod.rs b/sw/host/opentitanlib/src/bootstrap/mod.rs
index af05c01..404ccb0 100644
--- a/sw/host/opentitanlib/src/bootstrap/mod.rs
+++ b/sw/host/opentitanlib/src/bootstrap/mod.rs
@@ -11,6 +11,7 @@
 use crate::io::spi::Target;
 
 mod primitive;
+mod legacy;
 
 #[derive(Debug, Error)]
 pub enum BootstrapError {
@@ -64,11 +65,9 @@
 
     /// Consrtuct a `Bootstrap` struct configured to use `protocol` and `options`.
     pub fn new(protocol: BootstrapProtocol, options: BootstrapOptions) -> Result<Self> {
-        let updater = match protocol {
-            BootstrapProtocol::Primitive => primitive::Primitive::new(&options),
-            BootstrapProtocol::Legacy => {
-                unimplemented!();
-            }
+        let updater: Box<dyn UpdateProtocol> = match protocol {
+            BootstrapProtocol::Primitive => Box::new(primitive::Primitive::new(&options)),
+            BootstrapProtocol::Legacy => Box::new(legacy::Legacy::new(&options)),
             BootstrapProtocol::Eeprom => {
                 unimplemented!();
             }
@@ -80,7 +79,7 @@
         Ok(Bootstrap {
             protocol,
             reset_delay: options.reset_delay.unwrap_or(Self::RESET_DELAY),
-            updater: Box::new(updater),
+            updater: updater,
         })
     }
 
diff --git a/sw/host/opentitantool/config/h1dx_devboard.json b/sw/host/opentitantool/config/h1dx_devboard.json
new file mode 100644
index 0000000..bd5845a
--- /dev/null
+++ b/sw/host/opentitantool/config/h1dx_devboard.json
@@ -0,0 +1,10 @@
+{
+  "uarts": [
+    {
+      "name": "console",
+      "baudrate": 115200,
+      "parity": "None",
+      "stopbits": "Stop1"
+    }
+  ]
+}
diff --git a/sw/host/opentitantool/config/h1dx_devboard_ultradebug.json b/sw/host/opentitantool/config/h1dx_devboard_ultradebug.json
new file mode 100644
index 0000000..0806679
--- /dev/null
+++ b/sw/host/opentitantool/config/h1dx_devboard_ultradebug.json
@@ -0,0 +1,25 @@
+{
+  "includes": ["h1dx_devboard.json"],
+  "interface": "ultradebug",
+  "pins": [
+    {
+      "name": "RESET",
+      "mode": "OpenDrain",
+      "level": true,
+      "pullup": true,
+      "alias_of": "RESET_B"
+    },
+    {
+      "name": "BOOTSTRAP",
+      "mode": "PushPull",
+      "level": false,
+      "pullup": false
+    }
+  ],
+  "uarts": [
+    {
+      "name": "console",
+      "alias_of": "UART1"
+    }
+  ]
+}