391 lines
14 KiB
Markdown
391 lines
14 KiB
Markdown
# Verified Boot Storage Applet for AVB 2.0
|
||
|
||
- Status: Draft as of April 6, 2017
|
||
|
||
## Introduction
|
||
|
||
The application and support libraries in this directory provide
|
||
a mechanism for a device's bootloader, using [AVB](https://android.googlesource.com/platform/external/avb/),
|
||
to store sensitive information. For a bootloader, sensitive information
|
||
includes whether the device is unlocked or locked, whether it is unlockable,
|
||
and what the minimum version of the OS/kernel is allowed to be booted. It
|
||
may also store other sensitive flags.
|
||
|
||
The verified boot storage applet provides a mechanism to store this
|
||
data in a way that enforceѕ the expected policies even if the higher level
|
||
operating system is compromised or operates in an unexpected fashion.
|
||
|
||
|
||
## Design Overview
|
||
|
||
The Verified Boot Storage Applet, VBSA, provides three purpose-built
|
||
interfaces:
|
||
|
||
- Lock storage and policy enforcement
|
||
- Rollback index storage
|
||
- Applet state
|
||
|
||
Each will be detailed below.
|
||
|
||
### Locks
|
||
|
||
There are four supported lock types:
|
||
|
||
- `LOCK_CARRIER`
|
||
- `LOCK_DEVICE`
|
||
- `LOCK_BOOT`
|
||
- `LOCK_OWNER`
|
||
|
||
Each lock has a single byte of "lock" storage. If that byte is 0x0, then it is
|
||
unlocked, or cleared. If it is non-zero, then the lock is locked. Any
|
||
non-zero value is valid and may be used by the bootloader if any additional
|
||
internal flagging is necessary.
|
||
|
||
In addition, a lock may have associated metadata which must be supplied during
|
||
lock or unlock, or both.
|
||
|
||
See `ese_boot_lock_*` in include/ese/app/boot.h for the specific interfaces.
|
||
|
||
|
||
#### LOCK\_CARRIER
|
||
|
||
The Carrier Lock implements a lock which can only be set when the device is not
|
||
in production and can only be unlocked if provided a cryptographic signature.
|
||
|
||
This lock is available for use to implement "sim locking" or "phone locking"
|
||
such that the carrier can determine if the device is allowed to boot an
|
||
unsigned or unknown system image.
|
||
|
||
To provision this lock, device-specific data must be provided in an exact
|
||
format. An example of this can be found in
|
||
`'ese-boot-tool.cpp':collect_device_data()`. This data is then provided to
|
||
the applet using `ese_boot_lock_xset()`.
|
||
|
||
##### Metadata format for locking/provisioning
|
||
|
||
The following system attributes must be collected in the given order:
|
||
|
||
- ro.product.brand
|
||
- ro.product.device
|
||
- ro.build.product
|
||
- ro.serialno
|
||
- [Modem ID: MEID or IMEI]
|
||
- ro.product.manufacturer
|
||
- ro.product.model
|
||
|
||
The data is serialized as follows:
|
||
|
||
\[length||string\]\[...\]
|
||
|
||
The length is a `uint8_t` and the value is appended as a stream of
|
||
`uint8_t` values.
|
||
|
||
This data is then prefixed with the desired non-zero lock value before
|
||
being submitted to the applet. If successful, the applet will have
|
||
stored a SHA256 hash of the device data
|
||
|
||
Note, `LOCK_CARRIER` can only be locked (non-zero lock value) when the
|
||
applet is not in 'production' mode.
|
||
|
||
##### Clearing/unlocking
|
||
|
||
If `LOCK_CARRIER` is set to a non-zero value and the applet is in
|
||
production mode, then clearing the lock value requires authorization.
|
||
|
||
Authorization comes in the form of a `RSA_SHA256-PKCS#1` signature over
|
||
the provisioned device data SHA256 hash and a supplied montonically
|
||
increasing "nonce".
|
||
|
||
The nonce value must be higher than the last seen nonce value and the
|
||
signature must validate using public key internally stored in the
|
||
applet (`CarrierLock.java:PK_MOD`).
|
||
|
||
To perform a clear, `ese_boot_lock_xset()` must be called with lock
|
||
data that begins with 0x0, to clear the lock, and then contains data of
|
||
the following format:
|
||
|
||
```
|
||
unlockToken = VERSION || NONCE || SIGNATURE
|
||
|
||
SIGNATURE = RSA_Sign(SHA256(deviceData))
|
||
```
|
||
|
||
- The version is a little endian `uint64_t` (8 bytes).
|
||
- The nonce is a little endian `uint64_t` (8 bytes).
|
||
- The signature is a RSA 2048-bit with SHA-256 PKCS#1 v1.5 (256 bytes).
|
||
|
||
On unlock, the device data hash is cleared.
|
||
|
||
##### Testing
|
||
|
||
It is possible to test the key with a valid signature but a fake
|
||
internal nonce and fake internal device data using
|
||
`ese_boot_carrier_lock_test()`. When using this interface, it
|
||
expects the same unlock token as in the prior but prefixes with the
|
||
fake data:
|
||
|
||
```
|
||
testVector = LAST_NONCE || DEVICE_DATA || unlockToken
|
||
```
|
||
|
||
- The last nonce is the value the nonce is compared against (8 bytes).
|
||
- Device data is a replacement for the internally stored SHA-256
|
||
hash of the deviec data. (32 bytes).
|
||
|
||
#### LOCK\_DEVICE
|
||
|
||
The device lock is one of the setting used by the bootloader to
|
||
determine if the boot lock can be changed. It may only be set by the
|
||
operating system and is meant to protect the device from being reflashed
|
||
by someone that cannot unlock or access the OS. This may also be used
|
||
by an enterprise administrator to control lock policy for managed
|
||
devices.
|
||
|
||
As `LOCK_DEVICE` has not metadata, it can be set and retrieved using
|
||
`ese_boot_lock_set()` and `ese_boot_lock_get()`.
|
||
|
||
#### LOCK\_BOOT
|
||
|
||
The boot lock is used by the bootloader to control whether it should
|
||
only boot verified system software or not. When the lock value
|
||
is cleared (0x0), the bootloader will boot anything. When the lock
|
||
value is non-zero, it should only boot software that is signed by a key
|
||
stored in the bootloader except if `LOCK_OWNER` is set. Discussion of
|
||
`LOCK_OWNER` will follow.
|
||
|
||
`LOCK_BOOT` can only be toggled when in the bootloader/fastboot and if
|
||
both `LOCK_CARRIER` and `LOCK_DEVICE` are cleared/unlocked.
|
||
|
||
As with `LOCK_DEVICE`, `LOCK_BOOT` has no metadata so it does not need the
|
||
extended accessors.
|
||
|
||
#### LOCK\_OWNER
|
||
|
||
The owner lock is used by the bootloader to support an alternative
|
||
OS signing key provided by the device owner. `LOCK_OWNER` can only be
|
||
toggled if `LOCK_BOOT` is cleared. `LOCK_OWNER` does not require
|
||
any metadata to unlock, but to lock, it requires a blob of up to 2048
|
||
bytes be provided. This is just secure storage for use by the
|
||
bootloader. `LOCK_OWNER` may be toggled in the bootloader or the
|
||
operating system. This allows an unlocked device (`LOCK_BOOT=0x0`) to
|
||
install an owner key using fastboot or using software on the operating
|
||
system itself.
|
||
|
||
Before `LOCK_OWNER`'s key should be honored by the bootloader, `LOCK_BOOT`
|
||
should be set (in the bootloader) to enforce use of the key and to keep
|
||
malicious software in the operating system from changing it.
|
||
|
||
(Note, that the owner key should not be treated as equivalent to the
|
||
bootloader's internally stored key in that the device user should have a
|
||
means of knowing if an owner key is in use, but the requirement for the
|
||
device to be unlocked implies both physical access the `LOCK_DEVICE`
|
||
being cleared.)
|
||
|
||
|
||
### Rollback storage
|
||
|
||
Verifying an operating system kernel and image is useful both for system
|
||
reliability and for ensuring that the software has not been modified by
|
||
a malicious party. However, if the system software is updated,
|
||
malicious software may attempt to "roll" a device back to an older
|
||
version in order to take advantage of mistakes in the older, verified
|
||
code.
|
||
|
||
Rollback index values, or versions, may be stored securely by the bootloader
|
||
to prevent these problems. The Verified Boot Storage applet provides
|
||
eight 64-bit slots for storing a value. They may be read at any time,
|
||
but they may only be written to when the device is in the bootloader (or
|
||
fastboot).
|
||
|
||
Rollback storage is written to using
|
||
`ese_boot_rollback_index_write()` and read using
|
||
`ese_boot_rollback_index_read()`.
|
||
|
||
### Applet state
|
||
|
||
The applet supports two operational states:
|
||
|
||
- production=true
|
||
- production=false
|
||
|
||
On initial installation, production is false. When the applet is not
|
||
in production mode, it does not enforce a number of security boundaries,
|
||
such as requiring bootloader or hlos mode for lock toggling or
|
||
CarrierLock verification. This allows the factory tools to run in any
|
||
mode and properly configure a default lock state.
|
||
|
||
To transition to "production", a call to `ese_boot_set_production(true)`
|
||
is necessary.
|
||
|
||
To check the state and collect debugging information, the call
|
||
`ese_boot_get_state()` will return the current bootloader value,
|
||
the production state, any errors codes from lock initialization, and the
|
||
contents of lock storage.
|
||
|
||
#### Example applet provisioning
|
||
|
||
After the applet is installed, a flow as follows would prepare the
|
||
applet for use:
|
||
|
||
- `ese_boot_session_init();`
|
||
- `ese_boot_session_open();`
|
||
- `ese_boot_session_lock_xset(LOCK_OWNER, {0, ...});`
|
||
- `ese_boot_session_lock_set(LOCK_BOOT, 0x1);`
|
||
- `ese_boot_session_lock_set(LOCK_DEVICE, 0x1);`
|
||
- [collect device data]
|
||
- `ese_boot_session_lock_set(LOCK_CARRIER, {1, deviceData});`
|
||
- `ese_boot_session_set_production(true);`
|
||
- `ese_boot_session_close();`
|
||
|
||
### Additional details
|
||
|
||
#### Bootloader mode
|
||
|
||
Bootloader mode detection depends on hardware support to signal the
|
||
applet that the application processor has been reset. Additionally,
|
||
there is a mechanism for the bootloader to indicate that is loading the
|
||
main OS where it flips the value. This signal provides the assurances
|
||
around when storage is mutable or not during the time a device is
|
||
powered on.
|
||
|
||
#### Error results
|
||
|
||
EseAppResult is an enum that all `ese_boot_*` functions return. The
|
||
enum only covers the lower 16-bits. The upper 16-bits are reserved for
|
||
passing applet and secure element OS status for debugging and analysis.
|
||
When the lower 16-bits are `ESE_APP_RESULT_ERROR_APPLET`, then the
|
||
upper bytes will be the applet code. That code can then be
|
||
cross-referenced in the applet by function and code. If the lower
|
||
bytes are `ESE_APP_RESULT_ERROR_OS`, then the status code are the
|
||
ISO7816 code from an uncaught exception or OS-level error.
|
||
|
||
##### Cooldown
|
||
|
||
`ESE_APP_RESULT_ERROR_COOLDOWN` indicates that the secure element needs to
|
||
stay powered on for a period of time -- either at the end of use or before the
|
||
requested command can be serviced. As the behavior is implementation specific,
|
||
the only effective option is to keep the secure element powered for the number of
|
||
seconds specified by the response `uint32_t`.
|
||
|
||
For chips that support it, like the one this applet is being tested on, the
|
||
cooldown time can be requested with a special APDU to `ese_transceive()`:
|
||
|
||
```
|
||
FFE10000
|
||
```
|
||
|
||
In response, a 6 byte response will contain a `uint32_t` and a successful status
|
||
code (`90 00`). The unsigned little-endian integer indicates how many seconds
|
||
the chip needs to stay powered and unused to cooldown. If this happens before
|
||
the locks or rollback storage can be read, the bootloader will need to
|
||
determine a safe delay or recovery path until boot can proceed securely.
|
||
|
||
## Examples
|
||
|
||
There are many ways to integrate this library and the associated applet.
|
||
Below are some concrete examples to guide standard approach.
|
||
|
||
### Configuration in factory
|
||
|
||
- Install configure the secure element and install the applets
|
||
(outside of the scope of this document).
|
||
- Boot to an environment to run the ese-boot-tool.
|
||
- Leave the inBootloader() signal asserted (recommended but not required).
|
||
- Configure the desired lock states:
|
||
- `# ese-boot-tool lock set carrier 1 modem-imei-string`
|
||
- `# ese-boot-tool lock set device 1`
|
||
- `# ese-boot-tool lock set boot 1`
|
||
- `# ese-boot-tool lock set owner 0`
|
||
- To move from factory mode to production mode call:
|
||
- `# ese-boot-tool production set true`
|
||
|
||
### Configuration during repair
|
||
|
||
- Boot to an environment to run the ese-boot-tool.
|
||
- Leave inBootloader() signal asserted or implement the steps below in
|
||
the bootloader.
|
||
- Transition out of production mode:
|
||
- `# ese-boot-tool production set false`
|
||
- If a `LOCK_CARRIER` problem is being repaired, it is possible to reset the
|
||
internal nonce counter and all lock state with the command below. A full
|
||
lock reset is not expected in most cases.
|
||
- `# ese-boot-tool lock reset`
|
||
- Reconfigure the lock states:
|
||
- `# ese-boot-tool lock set carrier 1 modem-imei-string`
|
||
- `# ese-boot-tool lock set device 1`
|
||
- `# ese-boot-tool lock set boot 1`
|
||
- `# ese-boot-tool lock set owner 0`
|
||
(To clear data from the owner lock, set owner 1 must be called with
|
||
4096 00s.)
|
||
- Then move back to production mode:
|
||
- `# ese-boot-tool production set true`
|
||
|
||
### Use during boot
|
||
|
||
Do not load any non-repair or non-factory OS without clearing the inBootloader
|
||
signal as the applet may be transitioned out of production mode and/or the
|
||
rollback state may be changed.
|
||
|
||
#### Checking rollback values
|
||
|
||
- Read and write rollback values as per libavb using the API
|
||
- `ese_boot_rollback_index_write()`
|
||
- `ese_boot_rollback_index_read()`
|
||
- Prior to leaving the bootloader, clear the inBootloader signal.
|
||
|
||
As rollback index values can only be written when inBootloader signal is set,
|
||
it is critical to clear it when leaving the bootloader.
|
||
|
||
#### Checking locks
|
||
|
||
The pseudo-code and comments below should outline the basic algorithm, but it
|
||
does not include integration into libavb or include use of rollback index
|
||
value checking:
|
||
|
||
```
|
||
// Read LOCK_BOOT
|
||
ese_boot_lock_get(session, kEseBootLockIdBoot, &lockBoot);
|
||
|
||
if (lockBoot != 0x0) { // Boot is LOCKED.
|
||
// Read the LOCK_OWNER
|
||
ese_boot_lock_xget(session, kEseBootLockIdOwner, &lockOwner);
|
||
if (lockOwner != 0x0) { // Owner is LOCKED
|
||
// Get the lock owner value with metadata.
|
||
// This is done as a second stage to avoid wasted copying when it
|
||
// is not locked.
|
||
uint8_t ownerData[kEseBootOwnerKeyMax + 1];
|
||
ese_boot_lock_xget(session, kEseBootLockIdOwner, ownerData
|
||
sizeof(ownerData), &ownerDataUsed);
|
||
// lockOwner == ownerData[0]
|
||
// Parse the stored metadata into a key as per your bootloader
|
||
// design.
|
||
SomeBootKey key;
|
||
parseDeviceOwnerKeyForBooting(ownerData + 1, ownerDataUsed, &key);
|
||
// Boot using the supplied owner key
|
||
// (E.g., as part of avb_validate_vbmeta_public_key())
|
||
setDeviceOwnerKeyForBooting(&key);
|
||
continueBootFlow();
|
||
} else { // Boot is UNLOCKED (0x0)
|
||
// Perform the boot flow.
|
||
setBootIsUnverified();
|
||
continueBootFlow();
|
||
}
|
||
```
|
||
|
||
### In fastboot
|
||
|
||
- `LOCK_BOOT` may be toggled by a fastboot command. If the conditions of
|
||
unlock are not allowed by applet policy, it will fail.
|
||
- `LOCK_OWNER`may be toggled and set a boot key from a fastboot command
|
||
or from an unlocked OS image.
|
||
- If the verified boot design dictates that rollback indices are clear on
|
||
lock/unlock, this can be done by calling
|
||
- `ese_boot_rollback_index_write()` on each slot with the value of 0.
|
||
|
||
Note, `LOCK_DEVICE` and `LOCK_CARRIER` should not need to be used by fastboot.
|
||
|
||
For debugging and support, it may be desirable to connect the
|
||
`ese_boot_get_state()` to allow fastboot to return the current value of
|
||
production, inbootloader, and the lock metadata.
|
||
|