Slide 1

Slide 1 text

Adding Security to Microcontroller Ruby Ryo Kajiwara/ 梶原 龍 (sylph01) 2024/5/16 @ RubyKaigi 2024 1

Slide 2

Slide 2 text

Slides are available at: https:/ /speakerdeck.co m/sylph01/adding- security-to- microcontroller-ruby 2

Slide 3

Slide 3 text

Hi! 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

6

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Why not implement cryptography into microcontrollers? 8

Slide 9

Slide 9 text

Can we haz TLS in PicoRuby? 9

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Project Adding Security to Microcontroller Ruby 13

Slide 14

Slide 14 text

More like Adding SSL/TLS to Microcontroller Ruby 14

Slide 15

Slide 15 text

or Adding Networking to Microcontroller Ruby 15

Slide 16

Slide 16 text

Demonstration If you want to see it in person, come find me any time during the Kaigi! 16

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

19

Slide 20

Slide 20 text

Part 1: Cryptography in PicoRuby 20

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

SHA256 in PicoRuby + Mbed TLS require 'mbedtls' require 'base16' digest = MbedTLS::Digest.new(:sha256) digest.update('asdf') s = digest.finish Base16.encode16 s 27

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Implementation Details or Introduction to mruby/c Extensions 29

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

https:/ /datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf, 2.17.5. Random Number Generator 36

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

38

Slide 39

Slide 39 text

Part 2: Networking 39

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

CYW43439 driver CYW43.init('JP') CYW43.enable_sta_mode CYW43.connect_blocking( 'SSID_of_AP', 'password', CYW43::Auth::WPA2_MIXED_PSK ) 46

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

50

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

61

Slide 62

Slide 62 text

Part 3: Possible Future Work 62

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Servers? Obvious next step is this This is easier to do with Socket-esque API It will be in very limited capability 64

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

66

Slide 67

Slide 67 text

Conclusion 67

Slide 68

Slide 68 text

We can haz TLS in PicoRuby! 68

Slide 69

Slide 69 text

Stuff delivered In PicoRuby/R2P2 now! Base16, Base64 SHA256 AES Encryption (CBC, GCM) Random Number Generator 69

Slide 70

Slide 70 text

Stuff delivered (cont'd) Experimental CYW43 Connect to WiFi Net DNS TCPClient HTTP(S)Client GET, POST, PUT 70

Slide 71

Slide 71 text

What's left before merge? Rename C functions consistently We don't have namespaces! Error handling Toggle Networking with a build flag 71

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

Why use a Pico? It's closer to hardware. It's easier to embed into other hardware. 75

Slide 76

Slide 76 text

In an IoT environment, these devices work in concert 76

Slide 77

Slide 77 text

With Pico's WiFi connectivity, we can say Actual IoT with Ruby is closer to a reality! 77

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

It's a Blue Ocean 79

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Questions? / Comments? Twitter: @s01 or Fediverse: @[email protected] 82