Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add libtropic to unix build #4172

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

Conversation

ibz
Copy link
Contributor

@ibz ibz commented Sep 11, 2024

  • This PR adds libtropic as a dependency to the Unix build and exposes its functionality to micropython.
  • Build currently fails in the CI pipeline because libtropic is a private repository. We should wait for it to be open sourced before we can merge this PR.
  • This setup requires the Tropic model to be running and accepting connections over TCP. The Tropic model emulates the actual chip.
  • A smoke test is provided as part of the core/ unit tests.
  • The model itself is also not open sourced yet so it has to be installed locally for testing locally and it will have to be installed as part of the CI pipeline for the tests to work.
  • The emulator launcher (emu.py) has a way to run the Tropic model automatically - this is for convenience, when testing manually using the emulator. The added benefit is not much, since one could just run it separately, but this place seemed like a good point to provide some basic documentation about how to set up the model, and to enforce a path where everybody will have it set up, for consistency.

Requirements for merging this PR:

  • libtropic is open sourced (mandatory)
  • the model is open sourced - we can actually drop this requirement if we decide to not run the smoke tests as part of the CI pipeline

TODO before merging:

  • start model as part of the Python unit tests runner (core/tests/run_tests.sh)

@ibz ibz self-assigned this Sep 11, 2024
"vendor/libtropic/src/lt_l2_frame_check.c",
"vendor/libtropic/src/lt_l3.c",
"vendor/libtropic/src/lt_random.c",
"vendor/libtropic/hal//port/unix/lt_port_unix.c",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"vendor/libtropic/hal//port/unix/lt_port_unix.c",
"vendor/libtropic/hal/port/unix/lt_port_unix.c",

"vendor/libtropic/hal/crypto/trezor_crypto/lt_crypto_trezor_sha256.c",
"vendor/libtropic/hal/crypto/trezor_crypto/lt_crypto_trezor_x25519.c",
]
defines += ["USE_TREZOR_CRYPTO"]
Copy link
Member

Choose a reason for hiding this comment

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

Maybe this should be USE_TS_CRYPTO, no?

Suggested change
defines += ["USE_TREZOR_CRYPTO"]
defines += ["USE_TS_CRYPTO"]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this should be USE_TS_CRYPTO, no?

Actually, I was thinking not, because this is the emulator build, in which we would still use TREZOR_CRYPTO, because the TS chip is not there. What do you think, @pavelpolach321?

Choose a reason for hiding this comment

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

In libtropic we use USE_TREZOR_CRYPTO define if we want to use trezor_crypto as a source of cryptographic code which is running on host (MCU) side.

Example: https://github.com/tropicsquare/libtropic/blob/master/hal/crypto/trezor_crypto/ts_trezor_x25519.c#L1

Therefore I think that passing USE_TREZOR_CRYPTO to compilation of libtropic files will work in your case?

Comment on lines 106 to 99
vstr_t vstr = {0};
vstr_init_len(&vstr, 1024);

bytes_to_chars(X509_cert, vstr.buf, 512);

return mp_obj_new_str_from_vstr(&mp_type_bytes, &vstr);
Copy link
Member

@prusnak prusnak Sep 15, 2024

Choose a reason for hiding this comment

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

Why convert to hex here? IMHO just return raw binary bytes and if we need hex we can call foo.hex() in Python.

Suggested change
vstr_t vstr = {0};
vstr_init_len(&vstr, 1024);
bytes_to_chars(X509_cert, vstr.buf, 512);
return mp_obj_new_str_from_vstr(&mp_type_bytes, &vstr);
return mp_obj_new_str_copy(&mp_type_str, X509_cert, 512);

@ibz ibz force-pushed the ibz/20240805-libtropic branch 3 times, most recently from f41ec36 to e91f6cb Compare September 16, 2024 15:07
@ibz ibz force-pushed the ibz/20240805-libtropic branch 3 times, most recently from c9485c3 to 47d4427 Compare September 23, 2024 14:45

tropic_init(&handle);

lt_ecc_key_generate(&handle, idx, CURVE_ED25519);

Choose a reason for hiding this comment

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

Maybe you should put 'ret =' in front of this function call?

return mp_const_none;
}

lt_deinit(&handle);
Copy link

@pavelpolach321 pavelpolach321 Sep 24, 2024

Choose a reason for hiding this comment

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

I would also check return value in all lt_deinit() calls, mainly because in emulator this function internally talks to tcp server, which could fail easily.

@ibz ibz force-pushed the ibz/20240805-libtropic branch 2 times, most recently from 4b65b70 to 94517e4 Compare September 25, 2024 16:47
@ibz ibz marked this pull request as ready for review September 26, 2024 09:58
@ibz ibz requested a review from matejcik as a code owner September 26, 2024 09:58
@onvej-sl
Copy link
Contributor

onvej-sl commented Oct 9, 2024

Hello @ibz, I wanted to ask if you could use fixup commits instead of force-pushing. Fixup commits make it easier to track which suggestions have already been addressed. We usually squash the fixup commits just before merging the pull request.

@onvej-sl
Copy link
Contributor

onvej-sl commented Oct 9, 2024

As one of the developers involved in integrating Optiga into Trezor, I would like to document the architecture we used for the integration. The following diagrams shows the dependencies of all relevant functions where Optiga is utilized:

graph TD
    subgraph optiga_transport["core∕embed∕trezorhal∕optiga∕optiga_transport.c"]
    optiga_sec_chan_handshake
    optiga_init
    optiga_execute_command
    end
    subgraph optiga_commands["core∕embed∕trezorhal∕optiga∕optiga_commands.c"]
    optiga_calc_sign --> optiga_execute_command     
    optiga_get_data_object --> optiga_execute_command    
    optiga_set_autostate --> optiga_execute_command  

    optiga_get_random --> optiga_execute_command             
    end    
    subgraph optiga["core∕embed∕trezorhal∕optiga∕optiga.c"]
    optiga_sign --> optiga_calc_sign
    optiga_read_cert --> optiga_get_data_object
    optiga_random_buffer --> optiga_get_random
    optiga_pin_verify --> optiga_set_autostate    
    end
    subgraph storage["storage∕storage.c"]
    storage_unlock --> optiga_pin_verify
    end
    subgraph modoptiga["core∕embed∕extmod∕modtrezorcrypto∕modtrezorcrypto_optiga.h"]
    sign --> optiga_sign
    get_cert --> optiga_read_cert
    end
    subgraph modconfig["core∕embed∕extmod∕modtrezorconfig∕modtrezorconfig.c"]
    unlock --> storage_unlock
    end
    subgraph modrandom["core∕embed∕extmod∕modtrezorcrypto∕modtrezorcrypto_random.c"]
    random_bytes --> optiga_random_buffer
    end    
    subgraph modmain["core∕embed∕kernel∕main.c"]
    driver_init --> optiga_sec_chan_handshake
    driver_init --> optiga_init
    end
Loading

Note that the functions optiga_sec_chan_handshake and optiga_init (which are perhaps analogous to tropic_init) are defined outside python modules (core/embed/extmod/**). The reason for this is that we call them from core/embed/kernel/main.c. However, I'm not certain if this is sufficient justification, since it seems to me that they could be called only after the micropython environment is initialized.

I am not claiming that the architecture is perfect nor that there are no reasons to integrate Tropic differently. However, it would be nice, if we could keep it consistent.

One significant difference is that we don't have an Optiga emulator. How will the code in this pull request differ if we use a physical device instead of an emulator?

lt_handle_t handle = {0};
lt_ret_t ret = LT_FAIL;

tropic_init(&handle);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why don't you call tropic_init once, keep the handle, and reuse it for multiple commands? It seems unnecessary to me to call it before every command.

Copy link
Contributor Author

@ibz ibz Nov 22, 2024

Choose a reason for hiding this comment

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

Comment on lines 183 to 218
/// def random_get(
/// len: int,
/// ) -> bytes:
/// """
/// Get number of random bytes.
/// """
STATIC mp_obj_t mod_trezorcrypto_tropic_random_get(mp_obj_t len) {
mp_int_t len_rand = mp_obj_get_int(len);
if (len_rand < 0 || len_rand > RANDOM_VALUE_GET_LEN_MAX) {
mp_raise_ValueError("Invalid length.");
}

lt_handle_t handle = {0};
lt_ret_t ret = LT_FAIL;

tropic_init(&handle);

uint8_t buff[RANDOM_VALUE_GET_LEN_MAX] = {0};
ret = lt_random_get(&handle, buff, len_rand);
if (ret != LT_OK) {
tropic_deinit(&handle);
mp_raise_msg(&mp_type_TropicError, "lt_random_get failed.");
}

tropic_deinit(&handle);

vstr_t vstr = {0};
vstr_init_len(&vstr, len_rand);

memcpy(vstr.buf, buff, len_rand);

return mp_obj_new_str_from_vstr(&mp_type_bytes, &vstr);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_tropic_random_get_obj,
mod_trezorcrypto_tropic_random_get);

Copy link
Contributor

@onvej-sl onvej-sl Oct 9, 2024

Choose a reason for hiding this comment

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

I'm not sure if we need this method. If we want to generate randomness in python, we would like to combine entropy from Tropic, Optiga and MCU, so we rather need to integrate Tropic into this function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ibz
Copy link
Contributor Author

ibz commented Nov 4, 2024

Hello @ibz, I wanted to ask if you could use fixup commits instead of force-pushing. Fixup commits make it easier to track which suggestions have already been addressed. We usually squash the fixup commits just before merging the pull request.

Got it, sorry for that! BTW, I just force-pushed again, because I rebased on main. There's no way around that, I suppose... But I will do fixup commits from now on.

@ibz
Copy link
Contributor Author

ibz commented Nov 4, 2024

Note that the functions optiga_sec_chan_handshake and optiga_init (which are perhaps analogous to tropic_init) are defined outside python modules (core/embed/extmod/**). The reason for this is that we call them from core/embed/kernel/main.c. However, I'm not certain if this is sufficient justification, since it seems to me that they could be called only after the micropython environment is initialized.

Indeed, I can move tropic_init out of the Python module. We should probably call it in the same place where we call optiga_init, which is in main.c.

I am not claiming that the architecture is perfect nor that there are no reasons to integrate Tropic differently. However, it would be nice, if we could keep it consistent.

By this did you mean just the above part, about tropic_init? Or the other difference - that optiga comes under trezorhal whereas tropic does not?

The way I see it ... we have the optiga HAL (for the real chip) and the "unix" optiga HAL (for the emulator) - which are two different things. But in the case of tropic, libtropic is what provides the abstraction - because it can talk either to the real chip, or to the model, over TCP. So there would be no HAL layer (or - rather - libtropic is the HAL). This is why I went to just expose libtropic to Python without adding anything under trezorhal. But I can do it otherwise, if you think that is a good idea.

@onvej-sl
Copy link
Contributor

onvej-sl commented Nov 5, 2024

Indeed, I can move tropic_init out of the Python module. We should probably call it in the same place where we call optiga_init, which is in main.c.

One reason for initializing optiga in main.c was that the handshake process uses a secret stored in a memory location that becomes inaccessible once the firmware enters unprivileged mode. It seems to me that this concept applies to tropic as well. It would be beneficial to store the production handshake keys in this area.

But in the case of tropic, libtropic is what provides the abstraction - because it can talk either to the real chip, or to the model, over TCP.

This is something I don't quite understand. Does it mean that libtropic itself manages the communication with the chip? How can I instruct libtropic to use a real chip instead of an emulator? How can I specify which interface (for example I2C bus index) it should use for communication with tropic.

@pavelpolach321
Copy link

pavelpolach321 commented Nov 5, 2024

Indeed, I can move tropic_init out of the Python module. We should probably call it in the same place where we call optiga_init, which is in main.c.

One reason for initializing optiga in main.c was that the handshake process uses a secret stored in a memory location that becomes inaccessible once the firmware enters unprivileged mode. It seems to me that this concept applies to tropic as well. It would be beneficial to store the production handshake keys in this area.

But in the case of tropic, libtropic is what provides the abstraction - because it can talk either to the real chip, or to the model, over TCP.

This is something I don't quite understand. Does it mean that libtropic itself manages the communication with the chip? How can I instruct libtropic to use a real chip instead of an emulator? How can I specify which interface (for example I2C bus index) it should use for communication with tropic.

Hello, libtropic manages the communication by using its platform specific c file during compilation. For compiling for embedded target, use this file in your makefile: https://github.com/tropicsquare/libtropic/blob/develop/hal/port/stm32/lt_port_stm32.c - have a look from line 241 to the end.
For compiling for unix environment (your emulator), where you expect libtropic to talk to TROPIC01 model over tcp, then use this file
https://github.com/tropicsquare/libtropic/blob/develop/hal/port/unix/lt_port_unix.c

edit: you could also define transport layer's primitives for your embedded environment. For example within your codebase create lt_port_trezor.c and fill it with definitions for interfaces described by this header file: https://github.com/tropicsquare/libtropic/blob/develop/include/libtropic_port.h

@onvej-sl
Copy link
Contributor

Thank you for the explanation. It makes sense to me now. This line is what causes the build to use an emulator instead of a physical device.

I still don't understand whether we have to perform a handshake before every command. See this comment. Trezor with optiga performs a handshake once during startup and then reuses the handle. Is there any benefit in performing the handshake for each individual command?

@ibz
Copy link
Contributor Author

ibz commented Nov 18, 2024

I still don't understand whether we have to perform a handshake before every command. See this comment. Trezor with optiga performs a handshake once during startup and then reuses the handle. Is there any benefit in performing the handshake for each individual command?

We don't. That is my mistake. Will fix.

@onvej-sl
Copy link
Contributor

onvej-sl commented Nov 19, 2024

My suggestion is to:

  1. Store SHiPRIV_BYTES in core/embed/sec/secret/unix/secret.c.
  2. Introduce a function, secret_tropic_get_privkey, in core/embed/sec/secret/unix/secret.c. This function will return SHiPRIV_BYTES. This can be built on top of this pull request.
  3. Introduce a function, tropic_handshake(const uint8_t *trezor_privkey), in core/embed/sec/tropic/tropic_transport.c. This function will compute the public key using curve25519_scalarmult_basepoint, call lt_handshake, and store the handle.
  4. In core/embed/projects/kernel/main.c, call tropic_handshake and lt_init.

Once we want to use a physical tropic, we will implement core/embed/sec/secret/model_with_tropic/secret.c, and use port_model_with_tropic.c instead of lt_port_unix.c.

The tropic commands can then be called either directly using libtropic and the handle in tropic_transport.c, or through wrappers that will be in tropic_commands.c.

I am still uncertain about the variable naming. Does this approach make sense to you?

Copy link

github-actions bot commented Nov 27, 2024

legacy UI changes device test(screens) main(screens)

Copy link

github-actions bot commented Nov 27, 2024

core UI changes device test click test persistence test
T2T1 Model T test(screens) main(screens) test(screens) main(screens) test(screens) main(screens)
T3B1 Safe 3 test(screens) main(screens) test(screens) main(screens) test(screens) main(screens)
T3T1 Safe 5 test(screens) main(screens) test(screens) main(screens) test(screens) main(screens)
All main(screens)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 🏃‍♀️ In progress
Development

Successfully merging this pull request may close these issues.

4 participants