Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Adding Security to Microcontroller Ruby

Adding Security to Microcontroller Ruby

5/16/2024 @ RubyKaigi 2024 https://rubykaigi.org/2024/

(description from timetable)

"Actual" Internet of Things with Ruby is here!

Continuing on the journey through Ruby's cryptographic libraries, I gave it a shot to make Raspberry Pi Pico W speak TLS in PicoRuby, making it even closer to other languages' environments.

This talk will cover how networking, cryptography, and HTTP/HTTPS is implemented both in your normal Ruby and mruby world.


May 16, 2024

More Decks by sylph01

Other Decks in Programming


  1. I do stuff Play rhythm games (especially DanceDanceRevolution) btw, I'm

    DJing rhythm game songs at RubyMusicMixin this year so come see me there too! Play the bassoon/contrabassoon Ride a lot of trains (Rails!) (travelled on 99% of JR) Build keyboards if anything catches your interest let's talk! 4
  2. And I do stuff that is more relevant to this

    talk: Freelance web developer focused on Digital Identity and Security Worked/ing on writing/editing and implementing standards HTTPS in Local Network CG / Web of Things WG @ W3C, OAuth / Messaging Layer Security WG @ IETF Worked as an Officer of Internet Society Japan Chapter (2020-23) 5
  3. 6

  4. Previously in "Adventures in the Dungeons of OpenSSL" ... (talk

    at RubyConf Taiwan 2023) Implemented HPKE (RFC 9180) Using OpenSSL gem (GH: sylph01/hpke-rb) Also by extending OpenSSL gem 7
  5. Target environment: PicoRuby + R2P2 (All of the following is

    done by @hasumikin) https:/ /github.com/picoruby/picoruby : Ruby implementation https:/ /github.com/picoruby/R2P2 : Shell system for Pi Pico (W) Well-known usage: "PRK Firmware: Keyboard is Essentially Ruby" (RubyKaigi 2021 Takeout) https:/ /www.youtube.com/watch? v=5unMW_BAd4A 10
  6. Raspberry Pi Pico W Board with RP2040 microcontroller Dual-core ARM

    Cortex-M0+ @ 133MHz 264kB SRAM, 2MB Flash Wireless LAN (802.11n) with CYW43439 1353 yen (as of 4/18/2024) 技適 certified! 11
  7. MicroPython has this already import network from time import sleep

    wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect('ssid', 'password') while wlan.isconnected() == False: print('Connecting...') sleep(1) print(wlan.ifconfig()) code from https:/ /projects.raspberrypi.org/en/projects/get-started-pico-w/2 12
  8. Demonstration If you want to see it in person, come

    find me any time during the Kaigi! 16
  9. Caution: Here be dragons Cryptographic API is prone to misuse

    Most implementation here are experimental Gems that entered PicoRuby/R2P2 are pretty much "prod-ready" Everything else (esp. stuff that touches networking hardware) should be considered experimental 17
  10. Caution: Embedded bugs are hard I'm treating Pico W as

    "a normal computer" whenever I can. This is not trivial at all. There could be many random stuff I am missing. This is possible thanks to: Pico SDK (including lwIP, Mbed TLS) R2P2 (shell system) 18
  11. 19

  12. Quick Recap: SHA256 in Ruby require 'openssl' digest = OpenSSL::Digest.new('sha256')

    digest.update('The magic words are ') digest.update('squeamish ossifrage.') digest.hexdigest # oneshot API OpenSSL::Digest::SHA256.hexdigest( 'The magic words are squeamish ossifrage.' ) 21
  13. Quick Recap: AES in Ruby (encryption) require 'openssl' cipher =

    OpenSSL::Cipher::AES128.new('CBC') cipher.encrypt key = cipher.random_key iv = cipher.random_iv enc = cipher.update('The magic words are ') enc += cipher.update('squeamish ossifrage.') enc += cipher.final 22
  14. Quick Recap: AES in Ruby (decryption) # cont'd decipher =

    OpenSSL::Cipher::AES128.new('CBC') decipher.decrypt decipher.key = key decipher.iv = iv plain = decipher.update(enc) plain += decipher.final 23
  15. OpenSSL is too big, what do we do? There are

    cryptographic libraries for embedded systems Mbed TLS, wolfSSL Pico SDK uses Mbed TLS Mbed TLS has build options to only include needed functionality See R2P2/include/mbedtls_config.h 24
  16. PicoRuby didn't have Base16/64 Because Base16/64 is for humans, not

    for devices! What's Base16? It's Array#pack with H* I added this first so that debugging cryptography is easier 25
  17. AES in PicoRuby + Mbed TLS require 'mbedtls' require 'base64'

    key = Base64.decode64 "aGB7hvLWxE60PsxbPS9wsA==" iv = Base64.decode64 "J4b4xJuIHry/aUpVeyRIJw==" cipher = MbedTLS::Cipher.new(:aes_128_cbc, key, :encrypt) cipher.set_iv(iv) s = cipher.update('asdfasdfasdfasdfasdf') s << cipher.finish Base64.encode64 s 26
  18. SHA256 in PicoRuby + Mbed TLS require 'mbedtls' require 'base16'

    digest = MbedTLS::Digest.new(:sha256) digest.update('asdf') s = digest.finish Base16.encode16 s 27
  19. Mbed TLS in PicoRuby PicoRuby already had CMAC using Mbed

    TLS I added the most commonly used algorithms: AES-(128/192/256) non-AEAD: CBC mode AEAD: GCM mode SHA-256 28
  20. mruby/c vs CRuby We can wrap C values inside a

    Ruby object CRuby: RTYPEDDATA_DATA(obj) mruby/c: create an instance with mrbc_instance_new(vm, v->cls, sizeof(C_VAL_TO_WRAP)) Here, we want to wrap the "context" struct created by OpenSSL or mbed TLS 30
  21. mruby/c vs CRuby Defining methods CRuby: rb_define_method Takes a class,

    name, function pointer, and number of args Get arguments from the function's arguments mruby/c: mrbc_define_method Takes a pointer to VM, class, name, function pointer Get arguments using GET_ARG(POSITION) 31
  22. static void c_mbedtls_digest__init_ctx(mrbc_vm *vm, mrbc_value *v, int argc) { /*

    (snip) argument check */ mrbc_value algorithm = GET_ARG(1); mrbc_value self = mrbc_instance_new(vm, v->cls, sizeof(mbedtls_md_context_t)); mbedtls_md_context_t *ctx = (mbedtls_md_context_t *)self.instance->data; mbedtls_md_init(ctx); const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type( c_mbedtls_digest_algorithm_name(mrbc_integer(algorithm)) ); int ret; ret = mbedtls_md_setup(ctx, md_info, 0); // error check ret = mbedtls_md_starts(ctx); // error check SET_RETURN(self); } picoruby/mrbgems/picoruby-mbedtls/src/digest.c 32
  23. static void c_mbedtls_digest_update(mrbc_vm *vm, mrbc_value *v, int argc) { /*

    (snip) argument check */ mrbc_value input = GET_ARG(1); int ret; mbedtls_md_context_t *ctx = (mbedtls_md_context_t *)v->instance->data; ret = mbedtls_md_update(ctx, input.string->data, input.string->size); // error check mrbc_incref(&v[0]); SET_RETURN(*v); } picoruby/mrbgems/picoruby-mbedtls/src/digest.c 33
  24. One-shot API? In a memory-constrained environment, it's better to have

    multiple-call APIs instead of one-shot APIs, because to use the one-shot API, you need twice the memory of the original string. 34
  25. Did I say "don't use fixed nonces" It reduces security

    significantly cf. PlayStation 3's code signing key leak (ECDSA) We need a Random Number Generator But do we have /dev/random ... No! We use the Ring Oscillator (ROSC) to extract random bits You can use this through the RNG gem 35
  26. uint8_t c_rng_random_byte_impl(void) { uint32_t random = 0; uint32_t bit =

    0; for (int i = 0; i < 8; i++) { while (true) { bit = rosc_hw->randombit; sleep_us(5); if (bit != rosc_hw->randombit) break; } random = (random << 1) | bit; sleep_us(5); } return (uint8_t) random; } picoruby/mrbgems/picoruby-rng/ports/rp2040/rng.c 37
  27. 38

  28. What do we mean by Networking? The goal was to:

    Connect to WiFi with 802.11(L2) And get an IP(L3) address And speak to servers with TCP (L4) Maybe encrypt it with TLS (L5) With HTTP as application layer (L6/7) 40
  29. What was missing? Hardware driver CYW43439's driver is included in

    Pico SDK TCP/IP, TLS Provided by lwIP and Mbed TLS HTTP None Interface to Ruby None except HW driver 41
  30. What was missing? CYW43439 driver speaks with lwIP to provide

    IP setup lwIP speaks with Mbed TLS to provide TLS over TCP ... so it mostly came down to: Writing interfaces to PicoRuby Implementing basic HTTP 42
  31. Add libraries in R2P2 In CMakeLists.txt : if(DEFINED ENV{PICO_W}) target_link_libraries(${PROJECT_NAME}

    PRIVATE pico_btstack_ble pico_btstack_cyw43 pico_cyw43_arch_lwip_threadsafe_background # ADDED pico_lwip_mbedtls # ADDED pico_mbedtls # ADDED ) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/lib/picoruby/mrbgems/picoruby-ble/include ) endif() R2P2/CMakeLists.txt 43
  32. Driver operation modes The cyw43_driver and lwIP require periodic servicing,

    and these are handled in two different modes: pico_cyw43_arch_lwip_poll The program polls the WiFi driver periodically to call callbacks and move data pico_cyw43_arch_lwip_threadsafe_background (used here) Handling of the WiFi driver and TCP/IP stack is handled in the background, and is multi-core/thread/task safe. 44
  33. CYW43439 driver cyw43_arch_init_with_country() Initialization, limits frequencies of radio waves cyw43_arch_enable_sta_mode()

    Operates in Station (client) Mode cyw43_arch_wifi_connect_blocking() Connect to WiFi AP, blocks until success/failure There are non-blocking modes but this is mostly enough 45
  34. DNS Actually this was easier than TCP Handled by lwIP,

    uses UDP dns_gethostbyname() takes a callback function when record is found most of the rest of lwIP functions operate like this too 47
  35. void dns_found(const char *name, const ip_addr_t *ip, void *arg) {

    ip_addr_t *result = (ip_addr_t *)arg; if (ip) { ip4_addr_copy(*result, *ip); } else { ip4_addr_set_loopback(result); } return; } err_t get_ip_impl(const char *name, ip_addr_t *ip) { cyw43_arch_lwip_begin(); err_t err = dns_gethostbyname(name, ip, dns_found, ip); cyw43_arch_lwip_end(); return err; } picoruby/mrbgems/picoruby-net/ports/rp2040/net.c 48
  36. Aside: Debugging Networking Set up a hotspot with Raspberry Pi

    There is an official tutorial You can do it with builtin WiFi and a wired connection I got a WiFi dongle and connected to home AP with builtin WiFi Look inside packets with Wireshark 49
  37. 50

  38. TCP (Client-side) Basically we do the following: Create a Protocol

    Control Block (PCB) Set callbacks for recv , sent , err , poll recv : Handles received data. Most relevant sent : Handles data sent. Does nothing here err : Handle error cases. Almost does nothing here poll : Mostly handles idle connections 51
  39. tcp_connection_state *TCPClient_new_connection( mrbc_value *send_data, mrbc_value *recv_data, mrbc_vm *vm) { tcp_connection_state

    *cs = (tcp_connection_state *)mrbc_raw_alloc(sizeof(tcp_connection_state)); cs->state = NET_TCP_STATE_NONE; cs->pcb = altcp_new(NULL); altcp_recv(cs->pcb, TCPClient_recv_cb); altcp_sent(cs->pcb, TCPClient_sent_cb); altcp_err(cs->pcb, TCPClient_err_cb); altcp_poll(cs->pcb, TCPClient_poll_cb, 10); altcp_arg(cs->pcb, cs); cs->send_data = send_data; cs->recv_data = recv_data; cs->vm = vm; return cs; } picoruby/mrbgems/picoruby-net/ports/rp2040/net.c 52
  40. err_t TCPClient_recv_cb(void *arg, struct altcp_pcb *pcb, struct pbuf *pbuf, err_t

    err) { tcp_connection_state *cs = (tcp_connection_state *)arg; if (pbuf != NULL) { char *tmpbuf = mrbc_alloc(cs->vm, pbuf->tot_len + 1); struct pbuf *current_pbuf = pbuf; int offset = 0; while (current_pbuf != NULL) { pbuf_copy_partial(current_pbuf, tmpbuf + offset, current_pbuf->len, 0); offset += current_pbuf->len; current_pbuf = current_pbuf->next; } tmpbuf[pbuf->tot_len] = '\0'; mrbc_string_append_cbuf(cs->recv_data, tmpbuf, pbuf->tot_len); mrbc_free(cs->vm, tmpbuf); altcp_recved(pcb, pbuf->tot_len); cs->state = NET_TCP_STATE_PACKET_RECVED; pbuf_free(pbuf); } else { cs->state = NET_TCP_STATE_FINISHED; } return ERR_OK; } picoruby/mrbgems/picoruby-net/ports/rp2040/net.c 53
  41. mrbc_value TCPClient_send(const char *host, int port, mrbc_vm *vm, mrbc_value *send_data,

    bool is_tls) { ip_addr_t ip; mrbc_value ret; get_ip(host, &ip); if(!ip4_addr_isloopback(&ip)) { char ip_str[16]; ipaddr_ntoa_r(&ip, ip_str, 16); mrbc_value recv_data = mrbc_string_new(vm, NULL, 0); tcp_connection_state *cs = TCPClient_connect_impl(&ip, host, port, send_data, &recv_data, vm, is_tls); while(TCPClient_poll_impl(&cs)) { sleep_ms(200); } // recv_data is ready after connection is complete ret = recv_data; } else { ret = mrbc_nil_value(); } return ret; } picoruby/mrbgems/picoruby-net/ports/rp2040/net.c 54
  42. HTTP class HTTPClient def initialize(host) @host = host end def

    get(path) req = "GET #{path} HTTP/1.1\r\n" req += "Host:#{@host}\r\n" req += "Connection: close\r\n" req += "\r\n" TCPClient.request(@host, 80, req, false) end end picoruby/mrbgems/picoruby-net/mrblib/net.rb @ e33a743f 55
  43. TLS With lwIP and Application Layered TCP (ALTCP) getting TLS

    is pretty trivial instead of altcp_new() you will call altcp_tls_new() altcp_tls_create_config_client() to create TLS config use config to altcp_tls_new() mbedtls_ssl_set_hostname() to pass the host name to connect rest is the same! 56
  44. TLS with Application Layered TCP If the PCB is a

    TLS PCB: When you send data, after TCP sending functions are called, TLS callbacks will be called to encrypt data When you receive data, TLS callbacks will be called to decrypt data, then the TCP callbacks will be called As such, cryptography implemented in Part 1 is not used here. 57
  45. Memory Management Actually TLS was not that trivial... lwIP has

    a separate memory management mechanism from the mruby/c VM (PicoRuby) When I started implementing TLS it suddenly hung up: OOM! I had to reduce PicoRuby's heap memory size from 194KB to 96KB 58
  46. Memory Management Your ordinary malloc() / free() does not exist.

    They are either mrbc_alloc() / mrbc_free() that takes a VM, or the lwIP's variant. I lost 3 hours from an erratic behavior by calling free() . 59
  47. Note: This is NOT the full story I skipped over

    many things for brevity, such as (but not limited to): Hardware driver's "periodic servicing" part How to read data and load it onto memory, and such TCP handshake, TLS handshake Sending/Receiving large chunks of data Short story: Don't do it. 60
  48. 61

  49. Sockets In Desktop Ruby we use TCPSocket BSD Socket API

    is standardized and widely used lwIP's Socket API requires the use of an RTOS But MicroPython has a Socket-esque API Maybe porting it? 63
  50. Servers? Obvious next step is this This is easier to

    do with Socket-esque API It will be in very limited capability 64
  51. Blocking vs Non-Blocking Even though it uses a background process

    for WiFi driver handling, the TCP client uses blocking IO Is PicoRuby even capable of multi-processing/multi-threading? Raspberry Pi Pico is multi-core, but some hardware aren't even multi-core 65
  52. 66

  53. What's left before merge? Rename C functions consistently We don't

    have namespaces! Error handling Toggle Networking with a build flag 71
  54. But do we really need TLS? Symmetric crypto is okay,

    but asymmetric crypto is very slow Performance numbers from similar environments: STM32L562E Cortex-M33 at 110 MHz, wolfSSL RSA 2048 Signing: 9.208 ops/sec RSA 2048 Verification: 0.155 ops/sec ECDHE 256 Key agreement: 0.661 ops/sec 72
  55. But do we really need TLS? Also, without a trust

    store, we will get encryption through TLS, but we will not get the authentication part of TLS. For these reasons, depending on your security needs, it would be enough to just use symmetric crypto between the gateway and use TLS from there. Note that your WiFi password exists in your Pico to connect! 73
  56. You might want to use a Raspberry Pi Zero 2

    W if you want a "more traditional" computer experience Runs a Linux, can SSH into it, can run a GUI, has enough power to run asymmetric crypto 74
  57. Why use a Pico? It's closer to hardware. It's easier

    to embed into other hardware. 75
  58. Possible Future Work in IoT area CBOR/COSE/CoAP support Think of

    it as "JSON in binary" There are working groups in the IETF geared towards "constrained environments" Authentication and Authorization for Constrained Environments Lightweight Authenticated Key Exchange Software Updates for Internet of Things 78
  59. Shoutouts (@ mentions are in GitHub ID) Major shoutouts to

    @hasumikin for the extensive work in PicoRuby and helping me develop stuff on it Past RubyKaigi speakers, esp. @unasuke and @shioimm (Team Protocol Implementers!) And to the organizers of RubyKaigi 2024! 80
  60. More Shoutouts Sponsors of RubyKaigi, esp: codeTakt Inc. I am

    currently developing an ID platform for public schools with them Nexway Co., Ltd. I have played multiple times in TIS INTEC Group's orchestra 81