| # 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. |
| |