Skip to content

SPI hardware write protection

In order to from a Root of Trust in the firmware, e.g. in the immutable piece of firmware code for Static Root of Trust of Measurement one has to ensure the protection of this piece of code. One may achieve it by enabling Intel Boot Guard or AMD Platform Secure Boot. However these technologies may into always be available. In such case SPI hardware write protection becomes handy.

Thanks to the patchset implementing write protection and OTP support in flashrom one can easily set the SPI write protection of the platform from the operating system user space. This page explain how to build the flashrom with WP and OTP support and use to to protect the coreboot's bootblock to form SRTM.

Building flashrom

In order to build flashrom we will need some packages and librares. For Debian based distros execute:

sudo apt-get install git make binutils build-essential ca-certificates \
             libpci-dev libftdi-dev libusb-1.0-0-dev

Now clone the flashrom repository and fetch the patchset:

git clone https://github.com/flashrom/flashrom
cd flashrom
git fetch https://review.coreboot.org/flashrom refs/changes/13/59713/7 && \
    git checkout FETCH_HEAD

Build flashrom:

make

The freshly built flashrom will be present in root directory of the repository.

Flashrom write protection CLI

Invoke the following command to see a list of available options for flashrom:

./flashrom --help
flashrom v1.2-585-g3542afe on Linux 5.10.0-9-amd64 (x86_64)
flashrom is free software, get the source code at https://flashrom.org

Usage: ./flashrom [-h|-R|-L|
    -p <programmername>[:<parameters>] [-c <chipname>]
        (--flash-name|--flash-size|
         [-E|-x|(-r|-w|-v) <file>]
         [(-l <layoutfile>|--ifd| --fmap|--fmap-file <file>) [-i <region>[:<file>]]...]
         [-n] [-N] [-f])]
    [-V[V[V]]] [-o <logfile>]

 -h | --help                        print this help text
 -R | --version                     print version (release)
 -r | --read <file>                 read flash and save to <file>
 -w | --write <file|->              write <file> or the content provided
                                    on the standard input to flash
 -v | --verify <file|->             verify flash against <file>
                                    or the content provided on the standard input
 -E | --erase                       erase flash memory
 -V | --verbose                     more verbose output
 -c | --chip <chipname>             probe only for specified flash chip
 -f | --force                       force specific operations (see man page)
 -n | --noverify                    don't auto-verify
 -N | --noverify-all                verify included regions only (cf. -i)
 -x | --extract                     extract regions to files
 -l | --layout <layoutfile>         read ROM layout from <layoutfile>
      --wp-disable                  disable write protection
      --wp-enable                   enable write protection
      --wp-list                     list supported write protection ranges
      --wp-status                   show write protection status
      --wp-range=<start>,<len>      set write protection range (use --wp-range=0,0
                                    to unprotect the entire flash)
      --wp-region <region>          set write protection region
      --otp-status                  print information about OTP regions
      --otp-region <otp-region>     OTP region number (base 1) to operate on
      --otp-read <file>             read OTP region and save it to <file>
      --otp-write <file>            write <file> to OTP region
      --otp-erase                   erase OTP region
      --otp-lock                    lock OTP region
      --flash-name                  read out the detected flash name
      --flash-size                  read out the detected flash size
      --fmap                        read ROM layout from fmap embedded in ROM
      --fmap-file <fmapfile>        read ROM layout from fmap in <fmapfile>
      --ifd                         read layout from an Intel Firmware Descriptor
 -i | --image <region>[:<file>]     only read/write image <region> from layout
                                    (optionally with data from <file>)
 -o | --output <logfile>            log output to <logfile>
      --flash-contents <ref-file>   assume flash contents to be <ref-file>
 -L | --list-supported              print supported devices
 -p | --programmer <name>[:<param>] specify the programmer device. One of
    internal, dummy, nic3com, nicrealtek, gfxnvidia, raiden_debug_spi, drkaiser,
    satasii, atavia, it8212, ft2232_spi, serprog, buspirate_spi, dediprog,
    developerbox, rayer_spi, pony_spi, nicintel, nicintel_spi, nicintel_eeprom,
    ogp_spi, satamv, linux_mtd, linux_spi, usbblaster_spi, pickit2_spi,
    ch341a_spi, digilent_spi, stlinkv3_spi.

You can specify one of -h, -R, -L, -E, -r, -w, -v or no operation.
If no operation is specified, flashrom will only probe for flash chips.

We will use only a few of those options to set the protection on the coreboot's bootblock. We protect the bootblock only, since it is the stage responsible for measurements and verification of next stages.

Setting flash protection using flashrom

NOTE: be sure to update the firmware first before proceeding!

Estimating bootblock size and protection range

First let's see how much space we need to protect. Take your coreboot.rom file and use cbfstool to show its contents:

cd /path/to/coreboot/build
./cbfstool coreboot.rom print
FMAP REGION: COREBOOT
Name                           Offset     Type           Size   Comp
cbfs master header             0x0        cbfs header        32 none
fallback/romstage              0x80       stage          171040 none
fallback/ramstage              0x29d40    stage           98703 LZMA (229532 decompressed)
config                         0x41f40    raw               709 none
revision                       0x42240    raw               723 none
build_info                     0x42540    raw               101 none
fallback/dsdt.aml              0x42600    raw              7055 none
cmos.default                   0x441c0    cmos_default      256 none
cmos_layout.bin                0x44300    cmos_layout      3676 none
fallback/postcar               0x451c0    stage           21216 none
img/nvramcui                   0x4a500    simple elf      70630 none
fallback/payload               0x5b940    simple elf      69936 none
payload_config                 0x6cac0    raw              1621 none
payload_revision               0x6d140    raw               237 none
pci8086,10d3.rom               0x6d280    raw             82944 none
etc/sercon-port                0x816c0    raw                 8 none
(empty)                        0x81700    null          8128932 none
bootblock                      0xffcac0   bootblock       13056 none

The bootblock is taking slightly more than 12KB of space on the bottom of the flash. Converting 12KB to hex would be equal to 0x3000 but we must cover a little bit more than that. Let's see what protection ranges are available for the chip (running from the target machine to be protected):

./flashrom -p internal --wp-list
...
Available write protection ranges:
    start=0x00000000 length=0x00000000 (none)
    start=0x00000000 length=0x00001000 (lower 1/2048)
    start=0x007ff000 length=0x00001000 (upper 1/2048)
    start=0x00000000 length=0x00002000 (lower 1/1024)
    start=0x007fe000 length=0x00002000 (upper 1/1024)
    start=0x00000000 length=0x00004000 (lower 1/512)
    start=0x007fc000 length=0x00004000 (upper 1/512)
    start=0x00000000 length=0x00008000 (lower 1/256)
    start=0x007f8000 length=0x00008000 (upper 1/256)
    start=0x00000000 length=0x00020000 (lower 1/64)
    start=0x007e0000 length=0x00020000 (upper 1/64)
    start=0x00000000 length=0x00040000 (lower 1/32)
    start=0x007c0000 length=0x00040000 (upper 1/32)
    start=0x00000000 length=0x00080000 (lower 1/16)
    start=0x00780000 length=0x00080000 (upper 1/16)
    start=0x00000000 length=0x00100000 (lower 1/8)
    start=0x00700000 length=0x00100000 (upper 1/8)
    start=0x00000000 length=0x00200000 (lower 1/4)
    start=0x00600000 length=0x00200000 (upper 1/4)
    start=0x00000000 length=0x00400000 (lower 1/2)
    start=0x00400000 length=0x00400000 (upper 1/2)
    start=0x00000000 length=0x00600000 (lower 3/4)
    start=0x00200000 length=0x00600000 (upper 3/4)
    start=0x00000000 length=0x00700000 (lower 7/8)
    start=0x00100000 length=0x00700000 (upper 7/8)
    start=0x00000000 length=0x00780000 (lower 15/16)
    start=0x00080000 length=0x00780000 (upper 15/16)
    start=0x00000000 length=0x007c0000 (lower 31/32)
    start=0x00040000 length=0x007c0000 (upper 31/32)
    start=0x00000000 length=0x007e0000 (lower 63/64)
    start=0x00020000 length=0x007e0000 (upper 63/64)
    start=0x00000000 length=0x007f8000 (lower 255/256)
    start=0x00008000 length=0x007f8000 (upper 255/256)
    start=0x00000000 length=0x007fc000 (lower 511/512)
    start=0x00004000 length=0x007fc000 (upper 511/512)
    start=0x00000000 length=0x007fe000 (lower 1023/1024)
    start=0x00002000 length=0x007fe000 (upper 1023/1024)
    start=0x00000000 length=0x007ff000 (lower 2047/2048)
    start=0x00001000 length=0x007ff000 (upper 2047/2048)
    start=0x00000000 length=0x00800000 (all)

The output abvoe comes from Winbond W25Q64FV, a 8MB chip. Bootblock is always mapped to the bottom of the flash. 1MB is equal to 0x100000 so 8MB would be 0x800000. We need at least 0x4000 (16KB) to be protected starting at the bottom of the flash. And fortunately we have found our best match: start=0x007fc000 length=0x00004000 (upper 1/512).

Clearing SPI write protection

Now that we have selected the desired range to protect, it is time to check the current protection status:

./flashrom -p internal --wp-status
...
WP config bits: SRP1=0 SRP0=0 CMP=0 SEC=0 TB=0 BP2=0 BP1=0 BP0=0
Protection range: start=0x00000000 length=0x00000000 (none)
Protection mode: disabled

The protection range should be set to zeros and all WP config bits should be cleared. If you see some bits were not cleared, ensure the WP pin jumper on the flash is not placed and invoke:

./flashrom -p internal --wp-disable
./flashrom -p internal --wp-range=0,0

now that we have a clear state of the flash protection we may proceed with enabling the right range.

Setting protection range

Copy the range base and length and invoke:

./flashrom -p internal --wp-range=0x007fc000,0x00004000

Setting SPI status register protection

Flashrom should report Successfully set the requested protection range.. Now set the status register protection so the range protection cannot be changed:

./flashrom -p internal --wp-enable

Flashrom should report Successfully set the requested mode..

Verifying SPI write protection

To check whether the settings are desired invoke:

./flashrom -p internal --wp-status
...
WP config bits: SRP1=0 SRP0=1 CMP=0 SEC=1 TB=0 BP2=0 BP1=1 BP0=1
Protection range: start=0x007fc000 length=0x00004000 (upper 1/512)
Protection mode: hardware

If it matches what you have wanted to set it is time to lock the status register protection bit (SRP0) from being changed. SRP0 protects the CMP, SEC, TB and BPx bits from being changed. In order to lock the SRP0 bit you have to place the jumper on the WP pin and tie it to ground (GND). This will assert the WP pin and prevent any WP config bits from being changed. To verify it invoke:

./flashrom -p internal --wp-disable
...
Writing new WP configuration failed during verification:
Expected configuration: SRP1=0 SRP0=0 CMP=0 SEC=1 TB=0 BP2=0 BP1=1 BP0=1
Actual configuration:   SRP1=0 SRP0=1 CMP=0 SEC=1 TB=0 BP2=0 BP1=1 BP0=1

You should get the above result.

Changing the protection range

Now if you want to reprogram the protection range, you need to take off the jumper and invoke:

./flashrom -p internal --wp-disable

Now you should get Successfully set the requested mode. now. Check the WP status:

WP config bits: SRP1=0 SRP0=0 CMP=0 SEC=1 TB=0 BP2=0 BP1=1 BP0=1
Protection range: start=0x007fc000 length=0x00004000 (upper 1/512)
Protection mode: disabled

It must indicate Protection mode: disabled. Now you may reconfigure the range, e.g.

./flashrom -p internal --wp-range=0x007e0000,0x00020000

And proceed with enabling protection and setting back the jumper.

Setting flash protection for vboot

In case when vboot is enabled the protection range must be extended in order to cover other parts of the firmware. Vboot model assumes there is a read-only copy of the coreboot and payload called recovery partition. The recovery partition is supposed to be protected with a SPI write protection. The read-only region contains the bootblock, verstage (vboot stage used to verify other firmware components) and vboot keys and all other stages required to boot the platform in case of emergency. Typically the read-only partition occupies a space that matches the possible write protected regions of the flash chip. There are also read-write partitions (up to 2) that contain an updatable copies of the coreboot and payload. Unlike read-only partitions, read-write partitions are being verified using the signatures put into the read-write partitions. vboot checks the signature and decides whether read-write partition is safe to boot, otherwise it proceeds with execution of recovery firmware. The boot flow is shown on the diagram below:

vboot boot flow

In order to properly protect the firmware, one has to lock whole WP_RO region define by flashmap in coreboot. To locate the region offset and size one has to use cbfstool to retrieve layout:

cd /path/to/coreboot/build
./cbfstool coreboot.rom layout -w
This image contains the following sections that can be accessed with this tool:

'RW_MISC' (read-only, size 524288, offset 0)
'UNIFIED_MRC_CACHE' (read-only, size 131072, offset 0)
'RECOVERY_MRC_CACHE' (size 65536, offset 0)
'RW_MRC_CACHE' (size 65536, offset 65536)
'SMMSTORE' (preserve, size 262144, offset 131072)
'CONSOLE' (size 131072, offset 393216)
'RW_NVRAM' (size 16384, offset 524288)
'RW_SECTION_A' (read-only, size 5750784, offset 540672)
'VBLOCK_A' (size 8192, offset 540672)
'FW_MAIN_A' (CBFS, size 5742528, offset 548864)
'RW_FWID_A' (size 64, offset 6291392)
'WP_RO' (read-only, size 2097152, offset 6291456)
'RO_VPD' (preserve, size 16384, offset 6291456)
'RO_SECTION' (read-only, size 2080768, offset 6307840)
'FMAP' (read-only, size 2048, offset 6307840)
'RO_FRID' (size 64, offset 6309888)
'RO_FRID_PAD' (size 1984, offset 6309952)
'GBB' (size 16384, offset 6311936)
'COREBOOT' (CBFS, size 2060288, offset 6328320)

It is at least possible to perform the read action on every section listed above.

The above example shows the KGPE-D16 8MB target with vboot enabled. The WP_RO section is at offset of 6291456 bytes from the beginning of the flash and has size of 2097152 bytes. These numbers are in decimal, so one must convert them to hex. Either use an online hex converter or use the command in bash like a pro :)

printf "0x%x\n" 6291456
0x600000
printf "0x%x\n" 2097152
0x200000

So our WP range would be 2MB at 6MB offset start=0x00600000 length=0x00200000 (upper 1/4). But before proceeding with setting the WP range, one may want to sign the firmware with own vboot keys. The Dasharo build come by default with Google vboot developer keys, so to take advantage of vboot, one should sign the binary before flashing and setting the WP range. Refer to vboot signing. Unfortunately the utilities support only 2 RW partitions for resiging. Otherwise a whole image must be rebuilt. After custo mkeys have been used to sign the ifmrware image and the image has been flashed proceed with enalbing the protection:

./flashrom -p internal --wp-range=0x00600000,0x00200000

Then proceed with Setting SPI status register protection and Verifying SPI write protection.

At this point you should have your coreboot firmware with vboot well protected and ready to go. Place the jumper in order to prevent any changes to the configuration.