"Empty" OTP and support in test_rom

- On our planned test chip, the OTP will just be a ram, and thus empty.
  This commit adds a modified bootstrap library with an OTP_IS_RAM
  define, and targets for a test rom with that version of the library.
- Add splicing targets to get a bitstream with both the OTP_IS_RAM
  version of the rom, and an empty OTP -- and add this to the smoketest
  tarball so we get it from CI.

Change-Id: I2315ad69553a0e24227a545993281159869275e8
diff --git a/hw/bitstream/vivado/BUILD b/hw/bitstream/vivado/BUILD
index acc80f9..b192368 100644
--- a/hw/bitstream/vivado/BUILD
+++ b/hw/bitstream/vivado/BUILD
@@ -121,11 +121,30 @@
     tags = ["manual"],
 )
 
+bitstream_splice(
+    name = "fpga_nexus_mask_test_rom_no_otp",
+    testonly = True,
+    src = ":fpga_nexus_test_rom",
+    data = "//sw/device/lib/testing/test_rom:test_rom_no_otp_fpga_nexus_scr_vmem",
+    meminfo = ":rom_mmi",
+    tags = ["manual"],
+)
+
+bitstream_splice(
+    name = "fpga_nexus_mask_test_rom_no_otp_empty",
+    testonly = True,
+    src = ":fpga_nexus_mask_test_rom_no_otp",
+    data = "//hw/top_matcha/data:otp_img_empty",
+    meminfo = ":otp_mmi",
+    tags = ["manual"],
+)
+
 pkg_tar(
     name = "fpga_nexus_smoketest",
     testonly = True,
     srcs = [
         ":fpga_nexus_mask_test_rom",
+        ":fpga_nexus_mask_test_rom_no_otp_empty",
         ":logs",
         ":otp_mmi",
         ":rom_mmi",
diff --git a/hw/top_matcha/data/BUILD b/hw/top_matcha/data/BUILD
index 6fd346d..ee7aa69 100644
--- a/hw/top_matcha/data/BUILD
+++ b/hw/top_matcha/data/BUILD
@@ -2,7 +2,7 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-load("@lowrisc_opentitan//rules:otp.bzl", "otp_image")
+load("@lowrisc_opentitan//rules:otp.bzl", "otp_image", "otp_json", "otp_partition")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -30,3 +30,199 @@
         "@lowrisc_opentitan//hw/ip/otp_ctrl/data:otp_json_hw_cfg",
     ],
 )
+
+################
+
+otp_json(
+    name = "otp_json_empty",
+    partitions = [
+        otp_partition(
+            name = "SECRET0",
+            items = {
+                "TEST_UNLOCK_TOKEN": "0",
+                "TEST_EXIT_TOKEN": "0",
+            },
+            lock = False,
+        ),
+        otp_partition(
+            name = "SECRET1",
+            items = {
+                "FLASH_ADDR_KEY_SEED": "0",
+                "FLASH_DATA_KEY_SEED": "0",
+                "SRAM_DATA_KEY_SEED": "0",
+            },
+            lock = False,
+        ),
+        otp_partition(
+            name = "SECRET2",
+            items = {
+                "RMA_TOKEN": "0",
+                "CREATOR_ROOT_KEY_SHARE0": "0",
+                "CREATOR_ROOT_KEY_SHARE1": "0",
+            },
+            lock = False,
+        ),
+        otp_partition(
+            name = "LIFE_CYCLE",
+            count = 8,
+            # Should we set this to TEST_UNLOCKED0?
+            state = "TEST_UNLOCKED0",
+        ),
+    ],
+    seed = "0",
+)
+
+otp_json(
+    name = "otp_json_creator_sw_cfg",
+    partitions = [
+        otp_partition(
+            name = "CREATOR_SW_CFG",
+            items = {
+                "CREATOR_SW_CFG_DIGEST": "0x0",
+                # Use software mod_exp implementation for signature
+                # verification. See the definition of `hardened_bool_t` in
+                # sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_SIGVERIFY_RSA_MOD_EXP_IBEX_EN": "0",
+                # Mark the first three keys as valid and remaining as invalid
+                # since we currently have only three keys. See the definition of
+                # `hardened_byte_bool_t` in sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_SIGVERIFY_RSA_KEY_EN": "0",
+                # Enable use of entropy for countermeasures. See the definition
+                # of `hardened_bool_t` in sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_RNG_EN": "0",
+                # ROM execution is enabled if this item is set to a non-zero
+                # value.
+                "CREATOR_SW_CFG_ROM_EXEC_EN": "0",
+                # Value to write to the cpuctrl CSR in `rom_init()`.
+                # See:
+                # https://ibex-core.readthedocs.io/en/latest/03_reference/cs_registers.html#cpu-control-register-cpuctrl
+                "CREATOR_SW_CFG_CPUCTRL": "0",
+                "CREATOR_SW_CFG_JITTER_EN": "0",
+                # Value of the min_security_version_rom_ext field of the
+                # default boot data.
+                "CREATOR_SW_CFG_MIN_SEC_VER_ROM_EXT": "0x0",
+                # Value of the min_security_version_bl0 field of the default
+                # boot data.
+                "CREATOR_SW_CFG_MIN_SEC_VER_BL0": "0x0",
+                # Enable the default boot data in PROD and PROD_END life cycle
+                # states. See the definition of `hardened_bool_t` in
+                # sw/device/lib/base/hardened.h.
+                "CREATOR_SW_CFG_DEFAULT_BOOT_DATA_IN_PROD_EN": "0",
+                # Enable AST initialization.
+                "CREATOR_SW_CFG_AST_INIT_EN": "0",
+                # TODO: This enables a busyloop in the ROM to give time to
+                # trigger an RMA lifecycle transition via JTAG.  The current
+                # value of 10 cycles is useful for test code which verifies
+                # the path through the ROM.  This value is not useful for a
+                # real chip.
+                "CREATOR_SW_CFG_RMA_SPIN_EN": "0",
+                "CREATOR_SW_CFG_RMA_SPIN_CYCLES": "0",
+                # Entropy source health check default values. This needs to be
+                # populated when `CREATOR_SW_CFG_RNG_EN` is set to true.
+                "CREATOR_SW_CFG_RNG_REPCNT_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_REPCNTS_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_ADAPTP_HI_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_ADAPTP_LO_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_BUCKET_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_MARKOV_HI_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_MARKOV_LO_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_EXTHT_HI_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_EXTHT_LO_THRESHOLDS": "0",
+                "CREATOR_SW_CFG_RNG_ALERT_THRESHOLD": "0",
+                "CREATOR_SW_CFG_RNG_HEALTH_CONFIG_DIGEST": "0",
+            },
+        ),
+    ],
+)
+
+otp_json(
+    name = "otp_json_owner_sw_cfg",
+    partitions = [
+        otp_partition(
+            name = "OWNER_SW_CFG",
+            items = {
+                "OWNER_SW_CFG_DIGEST": "0x0",
+                # Enable bootstrap. See `hardened_bool_t` in
+                # sw/device/lib/base/hardened.h.
+                "OWNER_SW_CFG_ROM_BOOTSTRAP_EN": "0",
+                # Set to 0x739 to use the ROM_EXT hash measurement as the key
+                # manager attestation binding value.
+                "OWNER_SW_CFG_ROM_KEYMGR_ROM_EXT_MEAS_EN": "0x0",
+                # Report errors without any redaction.
+                "OWNER_SW_CFG_ROM_ERROR_REPORTING": "0",
+                # Set the enables to kAlertEnableNone.
+                # See `alert_enable_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_ALERT_CLASS_EN": "0",
+                # Set the esclation policies to kAlertEscalateNone.
+                # See `alert_escalate_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_ALERT_ESCALATION": "0",
+                # Set the classifiactions to kAlertClassX.
+                # See `alert_class_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_ALERT_CLASSIFICATION": ["0"] * 80,
+                # Set the classifiactions to kAlertClassX. See `alert_class_t` in
+                # sw/device/silicon_creator/lib/drivers/alert.h
+                "OWNER_SW_CFG_ROM_LOCAL_ALERT_CLASSIFICATION": ["0"] * 16,
+                # Set the alert accumulation thresholds to 0 per class.
+                "OWNER_SW_CFG_ROM_ALERT_ACCUM_THRESH": ["0x00000000"] * 4,
+                # Set the alert timeout cycles to 0 per class.
+                "OWNER_SW_CFG_ROM_ALERT_TIMEOUT_CYCLES": ["0x00000000"] * 4,
+                # Set the alert phase cycles to 0,10,10,0xFFFFFFFF for classes
+                # A and B, and to all zeros for classes C and D.
+                "OWNER_SW_CFG_ROM_ALERT_PHASE_CYCLES": [
+                    "0x0",
+                    "0",
+                    "0",
+                    "0",
+                    "0x0",
+                    "0",
+                    "0",
+                    "0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                    "0x0",
+                ],
+            },
+        ),
+    ],
+)
+
+otp_json(
+    name = "otp_json_hw_cfg",
+    partitions = [
+        otp_partition(
+            name = "HW_CFG",
+            items = {
+                "DEVICE_ID": "0",
+                # Cryptolib and chip-level tests require access to the CSRNG
+                # software interfaces.
+                "EN_CSRNG_SW_APP_READ": False,
+                "EN_SRAM_IFETCH": False,
+                # Cryptolib and chip-level tests require access to the
+                # entropy_src FW data interface.
+                "EN_ENTROPY_SRC_FW_READ": False,
+                # Cryptolib and chip-level tests require access to the
+                # entropy_src FW override interface.
+                "EN_ENTROPY_SRC_FW_OVER": False,
+            },
+            lock = False,
+        ),
+    ],
+)
+
+otp_image(
+    name = "otp_img_empty",
+    src = ":otp_json_empty",
+    overlays = [
+        ":otp_json_creator_sw_cfg",
+        ":otp_json_owner_sw_cfg",
+        ":otp_json_hw_cfg",
+    ],
+)
diff --git a/sw/device/lib/testing/test_rom/BUILD b/sw/device/lib/testing/test_rom/BUILD
index 31ef4bc..bce31d3 100644
--- a/sw/device/lib/testing/test_rom/BUILD
+++ b/sw/device/lib/testing/test_rom/BUILD
@@ -26,17 +26,34 @@
 
 opentitan_rom_binary(
     name = "test_rom",
+    srcs = [
+        "test_rom.c",
+        "test_rom_start.S",
+    ],
     deps = [
         ":linker_script",
         ":test_rom_lib",
+        "//sw/device/silicon_creator/rom:bootstrap",
     ],
 )
 
+opentitan_rom_binary(
+    name = "test_rom_no_otp",
+    srcs = [
+        "test_rom.c",
+        "test_rom_start.S",
+    ],
+    defines = ["OTP_IS_RAM"],
+    deps = [
+        ":linker_script",
+        ":test_rom_lib",
+        "//sw/device/silicon_creator/rom:bootstrap_no_otp",
+    ]
+)
+
 cc_library(
     name = "test_rom_lib",
     srcs = [
-        "test_rom.c",
-        "test_rom_start.S",
     ],
     target_compatible_with = [OPENTITAN_CPU],
     deps = [
@@ -79,7 +96,6 @@
         "@lowrisc_opentitan//sw/device/silicon_creator/lib/base:static_critical_sec_mmio",
         "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:flash_ctrl",
         "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:retention_sram",
-        "@lowrisc_opentitan//sw/device/silicon_creator/rom:bootstrap",
     ],
 )
 
diff --git a/sw/device/lib/testing/test_rom/test_rom.c b/sw/device/lib/testing/test_rom/test_rom.c
index 02303a0..0f2ed0e 100644
--- a/sw/device/lib/testing/test_rom/test_rom.c
+++ b/sw/device/lib/testing/test_rom/test_rom.c
@@ -73,9 +73,14 @@
 OT_WEAK
 bool rom_test_main(void) {
   // Check the otp to see if execute should start
+  // val: 0xFFFFFFFF
+#if defined(OTP_IS_RAM)
+  uint32_t otp_val = 0xffffffff;
+#else
   uint32_t otp_val = abs_mmio_read32(
       TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET +
       OTP_CTRL_PARAM_CREATOR_SW_CFG_ROM_EXEC_EN_OFFSET);
+#endif
 
   if (otp_val == 0) {
     test_status_set(kTestStatusInBootRomHalt);
@@ -86,9 +91,14 @@
   // Initialize Ibex cpuctrl (contains icache / security feature enablements).
   uint32_t cpuctrl_csr;
   CSR_READ(CSR_REG_CPUCTRL, &cpuctrl_csr);
+  // val: 0x1
+#if defined(OTP_IS_RAM)
+  uint32_t cpuctrl_otp_val = 0x1;
+#else
   uint32_t cpuctrl_otp_val = abs_mmio_read32(
       TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET +
       OTP_CTRL_PARAM_CREATOR_SW_CFG_CPUCTRL_OFFSET);
+#endif
   cpuctrl_csr = bitfield_field32_write(
       cpuctrl_csr, (bitfield_field32_t){.mask = 0x3f, .index = 0},
       cpuctrl_otp_val);
@@ -170,9 +180,14 @@
   }
 
   // Check the otp to see if flash scramble should be enabled.
+  // val: 0x0
+#if defined(OTP_IS_RAM)
+  otp_val = 0;
+#else
   otp_val = abs_mmio_read32(
       TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET +
       OTP_CTRL_PARAM_CREATOR_SW_CFG_FLASH_DATA_DEFAULT_CFG_OFFSET);
+#endif
 
   if (otp_val != 0) {
     dif_flash_ctrl_region_properties_t default_properties;
diff --git a/sw/device/lib/testing/test_rom/test_rom_start.S b/sw/device/lib/testing/test_rom/test_rom_start.S
index 8db5211..a326517 100644
--- a/sw/device/lib/testing/test_rom/test_rom_start.S
+++ b/sw/device/lib/testing/test_rom/test_rom_start.S
@@ -219,6 +219,7 @@
 #endif
 _start:
 #if !OT_IS_ENGLISH_BREAKFAST
+#if !OTP_IS_RAM
   // Check if AST initialization should be skipped.
   li   a0, (TOP_MATCHA_OTP_CTRL_CORE_BASE_ADDR + \
             OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET)
@@ -233,6 +234,9 @@
             OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET + \
             OTP_CTRL_PARAM_CREATOR_SW_CFG_AST_CFG_OFFSET)
   call crt_section_copy
+#else
+  j .L_ast_init_skip
+#endif
 
   // Wait for AST initialization to complete.
   li   a0, TOP_MATCHA_SENSOR_CTRL_BASE_ADDR
diff --git a/sw/device/silicon_creator/BUILD b/sw/device/silicon_creator/BUILD
new file mode 100644
index 0000000..34910b0
--- /dev/null
+++ b/sw/device/silicon_creator/BUILD
@@ -0,0 +1,6 @@
+# Copyright 2022 Google LLC.
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+package(default_visibility = ["//visibility:public"])
diff --git a/sw/device/silicon_creator/rom/BUILD b/sw/device/silicon_creator/rom/BUILD
new file mode 100644
index 0000000..9e1d2ff
--- /dev/null
+++ b/sw/device/silicon_creator/rom/BUILD
@@ -0,0 +1,37 @@
+# Copyright 2022 Google LLC.
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+package(default_visibility = ["//visibility:public"])
+
+BOOTSTRAP_DEPS = [
+        "//hw/top_matcha/sw/autogen:top_matcha",
+        "//hw/top_matcha/ip/flash_ctrl/data/autogen:flash_ctrl_regs",
+        "@lowrisc_opentitan//hw/ip/gpio/data:gpio_regs",
+        "@lowrisc_opentitan//hw/ip/otp_ctrl/data:otp_ctrl_regs",
+        "@lowrisc_opentitan//sw/device/lib/base:abs_mmio",
+        "@lowrisc_opentitan//sw/device/lib/base:bitfield",
+        "@lowrisc_opentitan//sw/device/lib/base:hardened",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib:error",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/base:chip",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:flash_ctrl",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:otp",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:rstmgr",
+        "@lowrisc_opentitan//sw/device/silicon_creator/lib/drivers:spi_device",
+    ]
+
+cc_library(
+    name = "bootstrap",
+    srcs = ["bootstrap.c"],
+    hdrs = ["bootstrap.h"],
+    deps = BOOTSTRAP_DEPS,
+)
+
+cc_library(
+    name = "bootstrap_no_otp",
+    srcs = ["bootstrap.c"],
+    hdrs = ["bootstrap.h"],
+    defines = ["OTP_IS_RAM"],
+    deps = BOOTSTRAP_DEPS,
+)
diff --git a/sw/device/silicon_creator/rom/bootstrap.c b/sw/device/silicon_creator/rom/bootstrap.c
new file mode 100644
index 0000000..f77c592
--- /dev/null
+++ b/sw/device/silicon_creator/rom/bootstrap.c
@@ -0,0 +1,392 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/silicon_creator/rom/bootstrap.h"
+
+#include <stdalign.h>
+
+#include "sw/device/lib/base/abs_mmio.h"
+#include "sw/device/lib/base/bitfield.h"
+#include "sw/device/lib/base/hardened.h"
+#include "sw/device/silicon_creator/lib/base/chip.h"
+#include "sw/device/silicon_creator/lib/drivers/flash_ctrl.h"
+#include "sw/device/silicon_creator/lib/drivers/otp.h"
+#include "sw/device/silicon_creator/lib/drivers/rstmgr.h"
+#include "sw/device/silicon_creator/lib/drivers/spi_device.h"
+#include "sw/device/silicon_creator/lib/error.h"
+
+#include "flash_ctrl_regs.h"
+#include "gpio_regs.h"
+#include "hw/top_matcha/sw/autogen/top_matcha.h"
+#include "otp_ctrl_regs.h"
+
+enum {
+  /*
+   * Maximum flash address, exclusive.
+   */
+  kMaxAddress =
+      FLASH_CTRL_PARAM_BYTES_PER_BANK * FLASH_CTRL_PARAM_REG_NUM_BANKS,
+};
+
+static_assert(FLASH_CTRL_PARAM_REG_NUM_BANKS == 2, "Flash must have 2 banks");
+
+/**
+ * Bootstrap states.
+ *
+ * OpenTitan bootstrap consists of three states between which the chip
+ * transitions sequentially.
+ *
+ * Encoding generated with
+ * $ ./util/design/sparse-fsm-encode.py -d 5 -m 3 -n 32 \
+ *     -s 375382971 --language=c
+ *
+ * Minimum Hamming distance: 17
+ * Maximum Hamming distance: 19
+ * Minimum Hamming weight: 16
+ * Maximum Hamming weight: 19
+ */
+typedef enum bootstrap_state {
+  /**
+   * Initial bootstrap state where the chip waits for a SECTOR_ERASE or
+   * CHIP_ERASE command.
+   */
+  kBootstrapStateErase = 0xd4576543,
+  /**
+   * Second bootstrap state where the chip verifies that all data banks have
+   * been erased.
+   */
+  kBootstrapStateEraseVerify = 0xf3c71bac,
+  /**
+   * Final bootstrap state. This is the main program loop where the chip handles
+   * erase, program, and reset commands.
+   */
+  kBootstrapStateProgram = 0xbdd8ca60,
+} bootstrap_state_t;
+
+/**
+ * Handles access permissions and erases both data banks of the embedded flash.
+ *
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_chip_erase(void) {
+  flash_ctrl_bank_erase_perms_set(kHardenedBoolTrue);
+  rom_error_t err_0 = flash_ctrl_data_erase(0, kFlashCtrlEraseTypeBank);
+  rom_error_t err_1 = flash_ctrl_data_erase(FLASH_CTRL_PARAM_BYTES_PER_BANK,
+                                            kFlashCtrlEraseTypeBank);
+  flash_ctrl_bank_erase_perms_set(kHardenedBoolFalse);
+
+  HARDENED_RETURN_IF_ERROR(err_0);
+  return err_1;
+}
+
+/**
+ * Handles access permissions and erases a 4 KiB region in the data partition of
+ * the embedded flash.
+ *
+ * Since OpenTitan's flash page size is 2 KiB, this function erases two
+ * consecutive pages.
+ *
+ * @param addr Address that falls within the 4 KiB region being deleted.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_sector_erase(uint32_t addr) {
+  static_assert(FLASH_CTRL_PARAM_BYTES_PER_PAGE == 2048,
+                "Page size must be 2 KiB");
+  enum {
+    /**
+     * Mask for truncating `addr` to the lower 4 KiB aligned address.
+     */
+    kPageAddrMask = ~UINT32_C(4096) + 1,
+  };
+
+  if (addr >= kMaxAddress) {
+    return kErrorBootstrapEraseAddress;
+  }
+  addr &= kPageAddrMask;
+
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4False,
+      .erase = kMultiBitBool4True,
+  });
+  rom_error_t err_0 = flash_ctrl_data_erase(addr, kFlashCtrlEraseTypePage);
+  rom_error_t err_1 = flash_ctrl_data_erase(
+      addr + FLASH_CTRL_PARAM_BYTES_PER_PAGE, kFlashCtrlEraseTypePage);
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4False,
+      .erase = kMultiBitBool4False,
+  });
+
+  HARDENED_RETURN_IF_ERROR(err_0);
+  return err_1;
+}
+
+/**
+ * Handles access permissions and programs up to 256 bytes of flash memory
+ * starting at `addr`.
+ *
+ * If `byte_count` is not a multiple of flash word size, it's rounded up to next
+ * flash word and missing bytes in `data` are set to `0xff`.
+ *
+ * @param addr Address to write to, must be flash word aligned.
+ * @param byte_count Number of bytes to write. Rounded up to next flash word if
+ * not a multiple of flash word size. Missing bytes in `data` are set to `0xff`.
+ * @param data Data to write, must be word aligned. If `byte_count` is not a
+ * multiple of flash word size, `data` must have enough space until the next
+ * flash word.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_page_program(uint32_t addr, size_t byte_count,
+                                          uint8_t *data) {
+  static_assert(__builtin_popcount(FLASH_CTRL_PARAM_BYTES_PER_WORD) == 1,
+                "Bytes per flash word must be a power of two.");
+  enum {
+    /**
+     * Mask for checking that `addr` is flash word aligned.
+     */
+    kFlashWordMask = FLASH_CTRL_PARAM_BYTES_PER_WORD - 1,
+    /**
+     * SPI flash programming page size in bytes.
+     */
+    kFlashProgPageSize = 256,
+    /**
+     * Mask for checking whether `addr` is flash programming page aligned.
+     *
+     * Flash programming page size is 256 bytes, writes that start at an `addr`
+     * with a non-zero LSB wrap to the start of the 256 byte region.
+     */
+    kFlashProgPageMask = kFlashProgPageSize - 1,
+  };
+
+  if (addr & kFlashWordMask || addr >= kMaxAddress) {
+    return kErrorBootstrapProgramAddress;
+  }
+
+  // Round up to next flash word and fill missing bytes with `0xff`.
+  size_t flash_word_misalignment = byte_count & kFlashWordMask;
+  if (flash_word_misalignment > 0) {
+    size_t padding_byte_count =
+        FLASH_CTRL_PARAM_BYTES_PER_WORD - flash_word_misalignment;
+    for (size_t i = 0; i < padding_byte_count; ++i) {
+      data[byte_count++] = 0xff;
+    }
+  }
+  size_t rem_word_count = byte_count / sizeof(uint32_t);
+
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4True,
+      .erase = kMultiBitBool4False,
+  });
+  // Perform two writes if the start address is not page-aligned (256 bytes).
+  // Note: Address is flash-word-aligned (8 bytes) due to the check above.
+  rom_error_t err_0 = kErrorOk;
+  size_t prog_page_misalignment = addr & kFlashProgPageMask;
+  if (prog_page_misalignment > 0) {
+    size_t word_count =
+        (kFlashProgPageSize - prog_page_misalignment) / sizeof(uint32_t);
+    if (word_count > rem_word_count) {
+      word_count = rem_word_count;
+    }
+    err_0 = flash_ctrl_data_write(addr, word_count, data);
+    rem_word_count -= word_count;
+    data += word_count * sizeof(uint32_t);
+    // Wrap to the beginning of the current page since PAGE_PROGRAM modifies
+    // a single page only.
+    addr &= ~kFlashProgPageMask;
+  }
+  rom_error_t err_1 = kErrorOk;
+  if (rem_word_count > 0) {
+    err_1 = flash_ctrl_data_write(addr, rem_word_count, data);
+  }
+  flash_ctrl_data_default_perms_set((flash_ctrl_perms_t){
+      .read = kMultiBitBool4False,
+      .write = kMultiBitBool4False,
+      .erase = kMultiBitBool4False,
+  });
+
+  HARDENED_RETURN_IF_ERROR(err_0);
+  return err_1;
+}
+
+/**
+ * Bootstrap state 1: Wait for an erase command and erase the data
+ * partition.
+ *
+ * This function erases both data banks of the flash regardless of the type of
+ * the erase command (CHIP_ERASE or SECTOR_ERASE).
+ *
+ * @param state Bootstrap state.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_handle_erase(bootstrap_state_t *state) {
+  HARDENED_CHECK_EQ(*state, kBootstrapStateErase);
+
+  spi_device_cmd_t cmd;
+  RETURN_IF_ERROR(spi_device_cmd_get(&cmd));
+  // Erase requires WREN, ignore if WEL is not set.
+  if (!bitfield_bit32_read(spi_device_flash_status_get(), kSpiDeviceWelBit)) {
+    return kErrorOk;
+  }
+
+  rom_error_t error = kErrorUnknown;
+  switch (cmd.opcode) {
+    case kSpiDeviceOpcodeChipErase:
+    case kSpiDeviceOpcodeSectorErase:
+      error = bootstrap_chip_erase();
+      HARDENED_RETURN_IF_ERROR(error);
+      *state = kBootstrapStateEraseVerify;
+      // Note: We clear WIP and WEN bits in `bootstrap_handle_erase_verify()`
+      // after checking that both data banks have been erased.
+      break;
+    default:
+      // Ignore any other command, e.g. PAGE_PROGRAM, RESET, and clear WIP and
+      // WEN bits right away.
+      spi_device_flash_status_clear();
+      error = kErrorOk;
+  }
+
+  return error;
+}
+
+/**
+ * Bootstrap state 2: Verify that all data banks have been erased.
+ *
+ * This function also clears the WIP and WEN bits of the flash status register.
+ *
+ * @param state Bootstrap state.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_handle_erase_verify(bootstrap_state_t *state) {
+  HARDENED_CHECK_EQ(*state, kBootstrapStateEraseVerify);
+
+  rom_error_t err_0 = flash_ctrl_data_erase_verify(0, kFlashCtrlEraseTypeBank);
+  rom_error_t err_1 = flash_ctrl_data_erase_verify(
+      FLASH_CTRL_PARAM_BYTES_PER_BANK, kFlashCtrlEraseTypeBank);
+  HARDENED_RETURN_IF_ERROR(err_0);
+  HARDENED_RETURN_IF_ERROR(err_1);
+
+  *state = kBootstrapStateProgram;
+  spi_device_flash_status_clear();
+  return err_0;
+}
+
+/**
+ * Bootstrap state 3: (Erase/)Program loop.
+ *
+ * @param state Bootstrap state.
+ * @return Result of the operation.
+ */
+static rom_error_t bootstrap_handle_program(bootstrap_state_t *state) {
+  static_assert(alignof(spi_device_cmd_t) >= sizeof(uint32_t) &&
+                    offsetof(spi_device_cmd_t, payload) >= sizeof(uint32_t),
+                "Payload must be word aligned.");
+  static_assert(
+      sizeof((spi_device_cmd_t){0}.payload) % FLASH_CTRL_PARAM_BYTES_PER_WORD ==
+          0,
+      "Payload size must be a multiple of flash word size.");
+
+  HARDENED_CHECK_EQ(*state, kBootstrapStateProgram);
+
+  spi_device_cmd_t cmd;
+  RETURN_IF_ERROR(spi_device_cmd_get(&cmd));
+  // Erase and program require WREN, ignore if WEL is not set.
+  if (cmd.opcode != kSpiDeviceOpcodeReset &&
+      !bitfield_bit32_read(spi_device_flash_status_get(), kSpiDeviceWelBit)) {
+    return kErrorOk;
+  }
+
+  rom_error_t error = kErrorUnknown;
+  switch (cmd.opcode) {
+    case kSpiDeviceOpcodeChipErase:
+      error = bootstrap_chip_erase();
+      break;
+    case kSpiDeviceOpcodeSectorErase:
+      error = bootstrap_sector_erase(cmd.address);
+      break;
+    case kSpiDeviceOpcodePageProgram:
+      error = bootstrap_page_program(cmd.address, cmd.payload_byte_count,
+                                     cmd.payload);
+      break;
+    case kSpiDeviceOpcodeReset:
+      rstmgr_reset();
+#ifdef OT_PLATFORM_RV32
+      HARDENED_UNREACHABLE();
+#else
+      // If this is an off-target test, return `kErrorUnknown` to be able to
+      // test without requiring EXPECT_DEATH.
+      error = kErrorUnknown;
+#endif
+      break;
+    default:
+      // We don't expect any other commands but we can potentially end up
+      // here with a 0x0 opcode due to glitches on SPI or strap lines (see
+      // #11871).
+      error = kErrorOk;
+  }
+  HARDENED_RETURN_IF_ERROR(error);
+
+  spi_device_flash_status_clear();
+  return error;
+}
+
+hardened_bool_t bootstrap_requested(void) {
+#if defined(OTP_IS_RAM)
+  uint32_t res = kHardenedBoolTrue;
+#else
+  uint32_t res =
+      otp_read32(OTP_CTRL_PARAM_OWNER_SW_CFG_ROM_BOOTSTRAP_EN_OFFSET);
+#endif
+  if (launder32(res) != kHardenedBoolTrue) {
+    return kHardenedBoolFalse;
+  }
+  HARDENED_CHECK_EQ(res, kHardenedBoolTrue);
+
+  // A single read is sufficient since we expect strong pull-ups on the strap
+  // pins.
+  res ^= SW_STRAP_BOOTSTRAP;
+  res ^=
+      abs_mmio_read32(TOP_MATCHA_GPIO_BASE_ADDR + GPIO_DATA_IN_REG_OFFSET) &
+      SW_STRAP_MASK;
+  if (launder32(res) != kHardenedBoolTrue) {
+    return kHardenedBoolFalse;
+  }
+  HARDENED_CHECK_EQ(res, kHardenedBoolTrue);
+  return res;
+}
+
+rom_error_t bootstrap(void) {
+  hardened_bool_t requested = bootstrap_requested();
+  if (launder32(requested) != kHardenedBoolTrue) {
+    return kErrorBootstrapNotRequested;
+  }
+  HARDENED_CHECK_EQ(requested, kHardenedBoolTrue);
+
+  spi_device_init();
+
+  // Bootstrap event loop.
+  bootstrap_state_t state = kBootstrapStateErase;
+  rom_error_t error = kErrorUnknown;
+  while (true) {
+    switch (launder32(state)) {
+      case kBootstrapStateErase:
+        HARDENED_CHECK_EQ(state, kBootstrapStateErase);
+        error = bootstrap_handle_erase(&state);
+        break;
+      case kBootstrapStateEraseVerify:
+        HARDENED_CHECK_EQ(state, kBootstrapStateEraseVerify);
+        error = bootstrap_handle_erase_verify(&state);
+        break;
+      case kBootstrapStateProgram:
+        HARDENED_CHECK_EQ(state, kBootstrapStateProgram);
+        error = bootstrap_handle_program(&state);
+        break;
+      default:
+        error = kErrorBootstrapInvalidState;
+    }
+    HARDENED_RETURN_IF_ERROR(error);
+  }
+  HARDENED_UNREACHABLE();
+}
diff --git a/sw/device/silicon_creator/rom/bootstrap.h b/sw/device/silicon_creator/rom/bootstrap.h
new file mode 100644
index 0000000..fd4c725
--- /dev/null
+++ b/sw/device/silicon_creator/rom/bootstrap.h
@@ -0,0 +1,48 @@
+// Copyright 2023 Google LLC.
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef SW_DEVICE_SILICON_CREATOR_ROM_BOOTSTRAP_H_
+#define SW_DEVICE_SILICON_CREATOR_ROM_BOOTSTRAP_H_
+
+#include "sw/device/lib/base/hardened.h"
+#include "sw/device/silicon_creator/lib/error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Checks whether bootstrap is requested.
+ *
+ * The return value of this function also depends on the `ROM_BOOTSTRAP_EN` OTP
+ * item.
+ *
+ * @return Whether bootstrap is requested.
+ */
+hardened_bool_t bootstrap_requested(void);
+
+/**
+ * Bootstraps the data partition of the embedded flash with data received by the
+ * spi_device.
+ *
+ * OpenTitan bootstrap uses the typical SPI flash EEPROM commands. A typical
+ * bootstrap session involves:
+ * - Asserting bootstrap pins to enter bootstrap mode,
+ * - Erasing the chip (WREN, CHIP_ERASE, busy loop ...),
+ * - Programming the chip (WREN, PAGE_PROGRAM, busy loop ...), and
+ * - Resetting the chip (RESET).
+ *
+ * This function only returns on error since a successful bootstrap ends with a
+ * chip reset.
+ *
+ * @return Result of the operation.
+ */
+rom_error_t bootstrap(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif  // SW_DEVICE_SILICON_CREATOR_ROM_BOOTSTRAP_H_