Skip to content

Conversation

@tretter
Copy link
Contributor

@tretter tretter commented Dec 17, 2025

This merge request adds a PTA for enabling secure boot on Rockchip SoCs.

On Rockchip SoCs, secure boot must be enabled from the secure world. There are other solutions to enable secure boot very early from a bootloader or special application. This has the drawback that user interaction or scripting on the device is not possible, which is inconvenient during development and software deployment in the factory.

This PTA allows enabling secure boot for Rockchip devices with an application running in the normal world.

There are still a few open issues in the PTA. I'd like to get some feedback on this approach:

  • Is this PTA the right approach for enabling secure boot on such devices? Are other approaches (TF-A?) better suited or already established?
  • Is the API of the PTA is reasonable and useful? Is there anything I have to change to bring it in line with OP-TEE standards?
  • Is the PTA acceptable for upstream OP-TEE as an in-tree PTA?

Function wise, the PTA API provides 3 functions, which work as follows:

  1. Ask the TA from the non-secure world about the current status and hash
    of the hardware. This allows to inspect the current status of secure
    boot on a specific device.

  2. Write an RSA hash into the OTP fuses. It's the responsibility of the
    user to calculate the hash and ensure that it matches the key, which
    will be used to sign the images.

  3. Actually lockdown the device by enabling secure boot. This is a
    separate step to allow the user to verify the setup before
    potentially bricking a device.

After that, the ROM code on the device will only accept images that are signed with the RSA key that matches the hash that has been burned into the OTP area.

Continuation of #7511

Comment on lines +157 to +100
res = rockchip_otp_read_secure(tmp,
ROCKCHIP_OTP_RSA_HASH_INDEX,
ROCKCHIP_OTP_RSA_HASH_SIZE);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The style checker complains about the unaligned parameters, but fixing it would lead to complaints about the line length. What's the OP-TEE way to fix such conflicts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a problem at this line. I usually structure the code in a way that the problem doesn't occur. Pleasing the checker is good, but what matters is that the code should be easy to read.

I guess you're referring to the problem in write_key_size() above. One way is to use local variables holding the values of ROCKCHIP_OTP_SECURE_BOOT_STATUS_INDEX and ROCKCHIP_OTP_SECURE_BOOT_STATUS_SIZE.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes, I was referring to ROCKCHIP_OTP_SECURE_BOOT_STATUS_INDEX and ROCKCHIP_OTP_SECURE_BOOT_STATUS_SIZE. I played with local helper variables, but it made the code less readable.

Comment on lines 73 to 85
static TEE_Result otp_to_string(uint32_t *otp, size_t otp_size,
char *str, size_t str_size)
{
if (otp_size != 8 || str_size != HASH_STRING_SIZE)
return TEE_ERROR_BAD_PARAMETERS;

snprintf(str, str_size,
"0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x",
otp[0], otp[1], otp[2], otp[3],
otp[4], otp[5], otp[6], otp[7]);

return TEE_SUCCESS;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be able to decide the log level in the caller. Thus, I implemented this as a format helper, but it's still clumsy to use. Unfortunately, I didn't find any better helpers in OP-TEE. Did I miss something obvious, or is there a better way to solve this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You never check the return value from otp_to_string() and the you're always supposed to use a proper otp hash size as input. How about simplifying it to allow it to be called in an argument to the *MSG() macro like:

static char *otp_to_string(uint32_t *otp, char *str, size_t str_size)
{
        snprintf(str, str_size, ...);

        return str;
}

Another option (perhaps even better) is to improve the DHEXDUMP() macro with versions for I and E.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I locally added IHEXDUMP() and EHEXDUMP(), but I think that the log output isn't that readable, since the dump and the actual message are in different lines.

I changed the function to return str which makes the use in the prints much nicer. Thanks!

Comment on lines +157 to +100
res = rockchip_otp_read_secure(tmp,
ROCKCHIP_OTP_RSA_HASH_INDEX,
ROCKCHIP_OTP_RSA_HASH_SIZE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a problem at this line. I usually structure the code in a way that the problem doesn't occur. Pleasing the checker is good, but what matters is that the code should be easy to read.

I guess you're referring to the problem in write_key_size() above. One way is to use local variables holding the values of ROCKCHIP_OTP_SECURE_BOOT_STATUS_INDEX and ROCKCHIP_OTP_SECURE_BOOT_STATUS_SIZE.

Comment on lines 73 to 85
static TEE_Result otp_to_string(uint32_t *otp, size_t otp_size,
char *str, size_t str_size)
{
if (otp_size != 8 || str_size != HASH_STRING_SIZE)
return TEE_ERROR_BAD_PARAMETERS;

snprintf(str, str_size,
"0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x",
otp[0], otp[1], otp[2], otp[3],
otp[4], otp[5], otp[6], otp[7]);

return TEE_SUCCESS;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You never check the return value from otp_to_string() and the you're always supposed to use a proper otp hash size as input. How about simplifying it to allow it to be called in an argument to the *MSG() macro like:

static char *otp_to_string(uint32_t *otp, char *str, size_t str_size)
{
        snprintf(str, str_size, ...);

        return str;
}

Another option (perhaps even better) is to improve the DHEXDUMP() macro with versions for I and E.

return TEE_ERROR_BAD_PARAMETERS;

snprintf(str, str_size,
"0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x 0x%08x",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use PRIx32 instead of "x".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed all places that used %x to PRIx32.

{
size_t i;

if (!a || !b)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dropped the entire function and replaced it with memcmp.

}

/* Test if the two passed public root key hashes are equal. */
static TEE_Result hashcmp(uint32_t *a, uint32_t *b, size_t s)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function needed? It seems memcmp() should do the job.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a previous iteration, I returned the length of the matching values, which would allow continuing writing a partially written hash into the OTP. Eventually, I dropped this feature, but kept the hashcmp function around.

I replaced it with memcmp() now.

*/
static const int simulation = IS_ENABLED2(CFG_RK_SECURE_BOOT_SIMULATION);

static inline bool bit_test(uint32_t value, uint32_t bit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function name is a potential conflict with a macro with the same name in <bitstring.h>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be helpful to have this as a generic function with a useful name somewhere in the utils?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this function should be test_bits() or test_bit_mask() to better reflect what it's supposed to do. The function is too odd to be generically usable.

* option is necessary to actually enable secure boot with PTA, but may
* potentially brick your device.
*/
static const int simulation = IS_ENABLED2(CFG_RK_SECURE_BOOT_SIMULATION);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use IS_ENABLED(CFG_RK_SECURE_BOOT_SIMULATION) directly in the code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this construction, I have a single place (instead of 3 occurrences in the code) for documentation. I also thought about adding a command to enable or disable the simulation, but eventually decided that I want this to be a compile time instead of a runtime option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CFG variables are better documented in the .mk file where they are introduced. Please use the IS_ENABLED() (yes, the version without the 2) macro directly instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand. I moved the documentation to the .mk and dropped the simulation variable.

if (bytes_size * sizeof(*bytes) != otp_size * sizeof(*otp))
return TEE_ERROR_BAD_PARAMETERS;

for (i = 0; i < otp_size; i++, u32++)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OP-TEE is very likely to always be little endian, especially where this PTA is compiled. How about adding a static_assert for that somewhere instead and use memcpy() directly?

Same for bytes_to_otp() below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be explicit about the data that is written into the OTP, because it's not really documented anywhere.

I dropped the functions and switched to memcpy() now.

How about adding a static_assert for that somewhere instead

I was looking for some existing helper or macro to check the endianess, but couldn't find anything. Is there anything that I missed and could just use?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google it; there's a standard way for GCC, and I expect it to be compatible with clang.

* option is necessary to actually enable secure boot with PTA, but may
* potentially brick your device.
*/
static const int simulation = IS_ENABLED2(CFG_RK_SECURE_BOOT_SIMULATION);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CFG variables are better documented in the .mk file where they are introduced. Please use the IS_ENABLED() (yes, the version without the 2) macro directly instead.

*/
static const int simulation = IS_ENABLED2(CFG_RK_SECURE_BOOT_SIMULATION);

static inline bool bit_test(uint32_t value, uint32_t bit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this function should be test_bits() or test_bit_mask() to better reflect what it's supposed to do. The function is too odd to be generically usable.

case 4096:
status |= ROCKCHIP_OTP_SECURE_BOOT_STATUS_RSA4096;

res = rockchip_otp_write_secure(&status,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this (with the helper variables placed at the very beginning of the function):

uint32_t idx = ROCKCHIP_OTP_SECURE_BOOT_STATUS_INDEX;
uint32_t sz = ROCKCHIP_OTP_SECURE_BOOT_STATUS_SIZE;
...
res = rockchip_otp_write_secure(&status, idx, sz);

return NULL;

snprintf(str, str_size,
"0x%" PRIx32 " 0x%" PRIx32 " 0x%" PRIx32 " 0x%" PRIx32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this "0x%"PRIx32" 0x%"PRIx32" 0x%"PRIx32" 0x%"PRIx32, same for the line below.

TEE_Result res = TEE_SUCCESS;
uint32_t status = 0;

IMSG("Setting key size to %d", key_size_bits);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%"PRId32

TEE_Result res = TEE_ERROR_GENERIC;
struct pta_rk_secure_boot_info *info = NULL;
uint32_t status = 0;
uint32_t hash[ROCKCHIP_OTP_RSA_HASH_SIZE];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please initialize all locals and order them to create an upside-down tree when possible. Same for the rest of this pull request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

I don't like upside down tree very much, because it's solely stylistic, and I prefer to group variables that have similar use. But that's personal preference, and I will follow the suggested coding style.


key_size_bits = params[1].value.a;
if (key_size_bits != 4096 && key_size_bits != 2048) {
EMSG("Invalid key size: %d", key_size_bits);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%"PRId32

static char *otp_to_string(uint32_t *otp, size_t otp_size,
char *str, size_t str_size)
{
if (otp_size != 8 || str_size != HASH_STRING_SIZE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These kind of tests only clutters the code. Use static_assert somewhere for the otp_size and let snprinf() itself deal with a too-short buffer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the check and the otp_size argument and added a static_assert for OTP_SIZE.

res = rockchip_otp_write_secure(&status,
ROCKCHIP_OTP_SECURE_BOOT_STATUS_INDEX,
ROCKCHIP_OTP_SECURE_BOOT_STATUS_SIZE);
if (res != TEE_SUCCESS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer if (res). Applies to the entire file.

return TEE_ERROR_BAD_PARAMETERS;
}

res = rockchip_otp_read_secure(old_hash,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a risk of wearing out the otp if you read too many times too quickly?
It seems there are no restrictions on who may call this PTA.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a risk of wearing out the otp if you read too many times too quickly?

I don't know. There is very little documentation available for the OTP.

There is a Linux driver for the non-secure OTP. That driver doesn't have any limitations on the read accesses.

On the other hand, it shouldn't be too difficult to add caching for the OTP values, since they usually won't change anyway.

Do you have additional information about these OTP that lead you to this question?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this was for a different platform 15+ years ago. Things might have changed. :-)

@jenswi-linaro
Copy link
Contributor

Please squash the fixups into the main commit.

@tretter tretter force-pushed the rockchip-secure-boot branch from 07a6ab1 to 405e9b0 Compare January 5, 2026 10:32
@tretter
Copy link
Contributor Author

tretter commented Jan 5, 2026

I squashed the fixup commits and rebased the branch on current master.

Copy link
Contributor

@jenswi-linaro jenswi-linaro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my comments addressed, please apply:
Acked-by: Jens Wiklander <jens.wiklander@linaro.org>

ROCKCHIP_OTP_RSA_HASH_SIZE);
if (res)
return res;
if (memcmp(tmp, hash, sizeof(tmp)) != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer if (memcmp(tmp, hash, sizeof(tmp)))

ROCKCHIP_OTP_RSA_HASH_SIZE);
if (res)
return res;
if (memcmp(old_hash, new_hash, sizeof(new_hash)) != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer if (memcmp(old_hash, new_hash, sizeof(new_hash)))

ROCKCHIP_OTP_RSA_HASH_SIZE);
if (res)
return res;
if (memcmp(zero, hash, sizeof(hash)) == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!memcmp(zero, hash, sizeof(hash)))

The OTP area contains other values in addition to the HW_UNIQUE_KEY. For
example, the SECURE_BOOT_STATUS and the RSA_HASH, which are used by the
ROM code to verify booted images, are located there as well.

Define the index (in 32 bit words) and length (in 32 bit words) of these
values, too, to allow applications to read and write these locations.

Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
Acked-by: Jens Wiklander <jens.wiklander@linaro.org>
The S_OTP area for the Rockchip secure boot RSA hash and status register
is accessible only from the secure world. Thus, secure boot must be
enabled from the secure world on these board.

The PTA implements 3 functions:

1. Ask the TA from the non-secure world about the current status and hash
   of the hardware. This allows to inspect the current status of secure
   boot on a specific device.

2. Write an RSA hash into the OTP fuses. It's the responsibility of the
   user to calculate the hash and ensure that it matches the key, which
   will be used to sign the images.

3. Actually lockdown the device by enabling secure boot. This is a
   separate step to allow the user to verify the setup before
   potentially bricking a device.

With these functions, a user may use a client running in the normal
world (for example in a boot loader or operating system) to enable
secure boot on a Rockchip device.

Implementing secure boot setup as an OP-TEE PTA has the advantage that
secure boot can be enabled at any time during the device setup instead
of during early boot. This allows a developer/user or additional scripts
to interact with the secure boot setup process.

The hash of the root key is accepted and reported as calculated by
sha256sum and internally converted to the correct byte order that needs
to be burned into the fuses.

Signed-off-by: Michael Tretter <m.tretter@pengutronix.de>
Acked-by: Jens Wiklander <jens.wiklander@linaro.org>
@tretter tretter force-pushed the rockchip-secure-boot branch from 405e9b0 to 6faa75b Compare January 5, 2026 13:43
@tretter
Copy link
Contributor Author

tretter commented Jan 5, 2026

I addressed the comments regarding the return value of memcpy, already squashed the fixes into the respective commit, and added the Acked-by tag.

@tretter
Copy link
Contributor Author

tretter commented Jan 5, 2026

I just sent an RFC patch series for a user of the PTA implemented in barebox: https://lore.kernel.org/barebox/20260105-rockchip-secure-boot-v1-0-eaf5053a7d7e@pengutronix.de/T/#t

@jenswi-linaro
Copy link
Contributor

I don't know what the problem is with checkpatch.

@jenswi-linaro jenswi-linaro merged commit 2949576 into OP-TEE:master Jan 5, 2026
111 of 113 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants