diff --git a/demos/2024-04-23-cheritech/cheri.js b/demos/2024-04-23-cheritech/cheri.js new file mode 100644 index 0000000..cb0ea1a --- /dev/null +++ b/demos/2024-04-23-cheritech/cheri.js @@ -0,0 +1,87 @@ +// FFI Imports +// Each function imported from the host environment needs to be assigned to a +// global like this and identified by a constant that the resolver in the C/C++ +// code will understand. +// These constants are defined in the `Exports` enumeration. + + +var FFINumber = 1; + +/** + * Log function, writes all arguments to the UART. + */ +export const print = vmImport(FFINumber++); + +/** + * led_on(index). + * + * Turns on the LED at the specified index. + */ +export const led_on = vmImport(FFINumber++); + +/** + * led_off(index). + * + * Turns off the LED at the specified index. + */ +export const led_off = vmImport(FFINumber++); + +/** + * buttons_read(). + * + * Reads the value of all of the buttons, returning a 4-bit value indicating + * the states of all of them. + */ +export const buttons_read = vmImport(FFINumber++); + +/** + * switches_read(). + * + * Reads the value of all of the switches, returning a 4-bit value indicating + * the states of all of them. + */ +export const switches_read = vmImport(FFINumber++); + + +export const mqtt_publish = vmImport(FFINumber++); +export const mqtt_subscribe = vmImport(FFINumber++); + +/** + * led_set(index, state). + * + * Turns the LED at the specified index on or off depending on whether state is + * true or false. + */ +export function led_set(index, state) +{ + if (state) + { + led_on(index); + } + else + { + led_off(index); + } +} + +/** + * button_read(index). + * + * Reads the value of the button at the specified index. + */ +export function button_read(index) +{ + return (buttons_read() & (1 << index)) !== 0; +} + + +/** + * switch_read(index). + * + * Reads the value of the switch at the specified index. + */ +export function switch_read(index) +{ + return (switches_read() & (1 << index)) !== 0; +} + diff --git a/demos/2024-04-23-cheritech/demo.cc b/demos/2024-04-23-cheritech/demo.cc new file mode 100644 index 0000000..f4d265a --- /dev/null +++ b/demos/2024-04-23-cheritech/demo.cc @@ -0,0 +1,253 @@ +// Copyright SCI Semiconductor and CHERIoT Contributors. +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "host.cert.h" +#include "javascript.hh" + +using CHERI::Capability; + +using Debug = ConditionalDebug; +constexpr bool UseIPv6 = CHERIOT_RTOS_OPTION_IPv6; + +// MQTT network buffer sizes +constexpr const size_t networkBufferSize = 2048; +constexpr const size_t incomingPublishCount = 100; +constexpr const size_t outgoingPublishCount = 100; + +namespace +{ + + DECLARE_AND_DEFINE_CONNECTION_CAPABILITY(DemoHost, + "cheriot.demo", + 8883, + ConnectionTypeTCP); + + DECLARE_AND_DEFINE_ALLOCATOR_CAPABILITY(mqttTestMalloc, 32 * 1024); + + constexpr std::string_view CodeTopic{"cheri-code"}; + int32_t codeSubscribePacketId = -1; + bool codeAckReceived = false; + + constexpr const char *buttonTopic = "cheri-button"; + int buttonCounter = 0; + + /** + * Note from the MQTT 3.1.1 spec: + * The Server MUST allow ClientIds which are between 1 and 23 UTF-8 encoded + * bytes in length, and that contain only the characters + * "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + * + * Note from us: + * UTF-8 encoding of 0-9, a-z, A-Z, is 1 Byte per character, so we should be + * able to do up to a length of 22 characters + zero byte. + */ + constexpr const int clientIDlength = 23; + constexpr const int clientIDPrefixLength = 8; + char clientID[clientIDlength] = "cheriot-XXXXXXXXXXXXXX"; + + /// Callbacks + + void __cheri_callback ackCallback(uint16_t packetID, bool isReject) + { + if (packetID == codeSubscribePacketId) + { + codeAckReceived = true; + } + } + + void __cheri_callback publishCallback(const char *topicName, + size_t topicNameLength, + const void *payloadData, + size_t payloadLength) + { + std::string_view topic{topicName, topicNameLength}; + // FIXME: This is a work around for a compiler bug. __builtin_memcmp + // is being expanded to a call to memcmp with the wrong calling + // convention and so we get linker errors. + // if (topic == CodeTopic) + if ((CodeTopic.size() == topic.size()) && + (memcmp(topic.data(), CodeTopic.data(), CodeTopic.size()) == 0)) + { + load_javascript(payloadData, payloadLength); + return; + } + + std::string_view payload{static_cast(payloadData), + payloadLength}; + publish(topic, payload); + } + + /// Handle to the MQTT connection. + SObj handle; + +} // namespace + +bool mqtt_publish(std::string_view topic, std::string_view message) +{ + Timeout t{UnlimitedTimeout}; + auto ret = mqtt_publish(&t, + handle, + 0, // Don't want acks for this one + topic.data(), + topic.size(), + message.data(), + message.size()); + return ret != 0; +} + +bool mqtt_subscribe(std::string_view topic) +{ + Timeout t{MS_TO_TICKS(100)}; + auto ret = mqtt_subscribe(&t, handle, 1, topic.data(), topic.size()); + return ret >= 0; +} + +/// Main demo + +void __cheri_compartment("mqtt_demo") demo() +{ + int ret; + Timeout t{MS_TO_TICKS(5000)}; + + MMIO_CAPABILITY(GPIO, gpio_led0)->enable_all(); + + Debug::log("Starting MQTT demo..."); + network_start(); + Debug::log("Network is ready..."); + + // systemd decides to restart the ntp server when it detects a new + // interface. If we try to get NTP time too quickly, the server isn't + // ready. Wait one second to give it time to stabilise. + { + Timeout oneSecond(MS_TO_TICKS(1000)); + thread_sleep(&oneSecond); + } + + // SNTP must be run for the TLS stack to be able to check certificate dates. + Debug::log("Fetching NTP time..."); + t = Timeout{MS_TO_TICKS(1000)}; + while (sntp_update(&t) != 0) + { + Debug::log("Failed to update NTP time"); + t = Timeout{MS_TO_TICKS(1000)}; + } + + { + timeval tv; + int ret = gettimeofday(&tv, nullptr); + if (ret != 0) + { + Debug::log("Failed to get time of day: {}", ret); + } + else + { + // Truncate the epoch time to 32 bits for printing. + Debug::log("Current UNIX epoch time: {}", (int32_t)tv.tv_sec); + } + } + + while (true) + { + load_javascript(nullptr, 0); + Debug::log("Generating client ID..."); + mqtt_generate_client_id(clientID + clientIDPrefixLength, + clientIDlength - clientIDPrefixLength - 1); + + Debug::log("Connecting to MQTT broker..."); + Debug::log("Quota left: {}", heap_quota_remaining(MALLOC_CAPABILITY)); + t = UnlimitedTimeout; + handle = mqtt_connect(&t, + STATIC_SEALED_VALUE(mqttTestMalloc), + STATIC_SEALED_VALUE(DemoHost), + publishCallback, + ackCallback, + TAs, + TAs_NUM, + networkBufferSize, + incomingPublishCount, + outgoingPublishCount, + clientID, + strlen(clientID)); + + if (!Capability{handle}.is_valid()) + { + Debug::log("Failed to connect, retrying..."); + Timeout pause{MS_TO_TICKS(1000)}; + thread_sleep(&pause); + continue; + } + + Debug::log("Connected to MQTT broker!"); + + Debug::log("Subscribing to JavaScript code topic '{}'.", CodeTopic); + + ret = mqtt_subscribe(&t, + handle, + 1, // QoS 1 = delivered at least once + CodeTopic.data(), + CodeTopic.size()); + + if (ret < 0) + { + Debug::log("Failed to subscribe, error {}.", ret); + mqtt_disconnect(&t, STATIC_SEALED_VALUE(mqttTestMalloc), handle); + continue; + } + + codeSubscribePacketId = ret; + + Debug::log("Now fetching the SUBACKs."); + + while (!codeAckReceived) + { + t = Timeout{MS_TO_TICKS(1000)}; + ret = mqtt_run(&t, handle); + + if (ret < 0) + { + Debug::log( + "Failed to wait for the SUBACK for the code node, error {}.", + ret); + mqtt_disconnect( + &t, STATIC_SEALED_VALUE(mqttTestMalloc), handle); + continue; + } + } + + Debug::log("Now entering the main loop."); + while (true) + { + // Check for PUBLISHes + t = Timeout{MS_TO_TICKS(100)}; + // Debug::log("{} bytes of heap free", heap_available()); + ret = mqtt_run(&t, handle); + + if ((ret < 0) && (ret != -ETIMEDOUT)) + { + Debug::log("Failed to wait for PUBLISHes, error {}.", ret); + break; + } + tick(); + } + Debug::log("Exiting main loop, cleaning up."); + mqtt_disconnect(&t, STATIC_SEALED_VALUE(mqttTestMalloc), handle); + // Sleep for a second to allow the network stack to clean up any + // outstanding allocations + Timeout oneSecond{MS_TO_TICKS(1000)}; + thread_sleep(&oneSecond); + } +} diff --git a/demos/2024-04-23-cheritech/host.cert.h b/demos/2024-04-23-cheritech/host.cert.h new file mode 100644 index 0000000..21b1f14 --- /dev/null +++ b/demos/2024-04-23-cheritech/host.cert.h @@ -0,0 +1,30 @@ +static const unsigned char TA0_DN[] = { + 0x30, 0x17, 0x31, 0x15, 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x0C, 0x63, 0x68, 0x65, 0x72, 0x69, 0x6F, 0x74, 0x2E, 0x64, 0x65, 0x6D, + 0x6F +}; + +static const unsigned char TA0_EC_Q[] = { + 0x04, 0xDB, 0x3A, 0xBE, 0xAF, 0x9F, 0x69, 0xF9, 0x02, 0xA4, 0xA0, 0xA7, + 0x41, 0x82, 0xE5, 0xF1, 0x06, 0x5E, 0x0A, 0xA1, 0x7E, 0x43, 0x61, 0xBE, + 0xA0, 0x1F, 0x22, 0x3D, 0x5A, 0xB9, 0xFB, 0xA3, 0x86, 0xF3, 0xF2, 0xB3, + 0x59, 0xE0, 0x93, 0xC6, 0xE0, 0x9F, 0xA6, 0x22, 0x51, 0x10, 0x2A, 0xFF, + 0x5E, 0x20, 0x96, 0x27, 0xD6, 0x8C, 0x08, 0x59, 0x0E, 0x58, 0x6C, 0xA6, + 0x87, 0xA5, 0x9A, 0x96, 0x02 +}; + +static const br_x509_trust_anchor TAs[1] = { + { + { (unsigned char *)TA0_DN, sizeof TA0_DN }, + BR_X509_TA_CA, + { + BR_KEYTYPE_EC, + { .ec = { + BR_EC_secp256r1, + (unsigned char *)TA0_EC_Q, sizeof TA0_EC_Q, + } } + } + } +}; + +#define TAs_NUM 1 diff --git a/demos/2024-04-23-cheritech/morello/README.txt b/demos/2024-04-23-cheritech/morello/README.txt new file mode 100644 index 0000000..d1693d9 --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/README.txt @@ -0,0 +1,139 @@ +Morello machine setup +===================== + +This directory contains the files that are necessary to set up the Morello machine to act as the server in this demo. + +Note: This contains the *private* key used on the server for the demo. +This would allow anyone to impersonate the server. +This does not matter because it is used *only* for the demo, never use this key for anything important! +Including the key here remove the need to generate a new header file for the client portion of the demo. + +Pure-capability packages: + +minicom + +Hybrid packages: + +bind918 +isc-dhcp44-server +jq +npm +wireshark + +Built from source: + +cheriot-audit (no port yet) +mosquitto (xsltproc is broken and the port's no-docs mode doesn't work). + +Make sure to build Release builds (-O0 is *really* slow on Morello, with -O0 Mosquitto can't keep up with two clients on FPGA!). +Install in /opt. + +The following lines need to be added to /etc/rc.conf: + +``` +# Network interface for the demo +ifconfig_ue0="inet 10.0.0.10 netmask 255.0.0.0" + +# DHCP server +dhcpd_enable="YES" # dhcpd enabled? +dhcpd_ifaces="ue0" # ethernet interface(s) +dhcpd_withumask="022" # file creation mask + +# bind +named_enable="YES" + +# NTP +ntpd_enable="YES" + +# Mosquitto +mosquitto_enable="YES" + +devfs_enable="YES" +``` + +Setting up DHCP +--------------- + +The first thing that the demo will do is try to get a DHCP lease. +This requires dhcpd to listen in the demo ethernet adaptor (configured in `rc.conf`) and to provide the host IP (10.0.0.10) as the DNS server. +The `usr/local64/etc/dhcpd.conf` file contains the configuration for the DHCP server and should be copied into `/usr/local64/etc/dhcpd.conf`. + +Setting up DNS +-------------- + +After acquiring a DHCP lease, the demo will try to look up host names via DNS. +For disconnected operation, we will fake the two DNS names (pool.ntp.org and cheriot.demo) by configuring the DNS server to be authoritative for these zones. +Add the following lines to the end of `/usr/local64/etc/namedb/named.conf`: + +``` +zone "cheriot.demo" { + type master; + file "/usr/local64/etc/namedb/db.cheriot.demo"; +}; + +zone "pool.ntp.org" { + type master; + file "/usr/local64/etc/namedb/db.pool.ntp.org"; +}; +``` + +Then copy the `db.cheriot.demo` and `db.pool.ntp.org` files from `usr/local64/etc/namedb` to `/usr/local64/etc/namedb/`. + +Setting up NTP +-------------- + +For disconnected operation, the NTP server needs to be configured to lie and pretend that it is an authoritative server when it can't sync with a real NTP server. +The following lines in /etc/ntp.conf will do this: + +``` +server 127.127.1.0 prefer +fudge 127.127.1.0 #stratum 10 +``` + +Note: It would be better to use `tos orphan 4`, but this defaults to a 10-minute timeout before deciding to become authoritative and this needs to be dropped to a few seconds. + +Setting up Mosquitto +-------------------- + +The Mosquitto MQTT server configuration is in `opt/etc/mosquitto/`. +Copy these files into `/opt/etc/mosquitto/`. +You can also copy the [rc script](https://github.com/freebsd/freebsd-ports/blob/main/net/mosquitto/files/mosquitto.in) from the port into `/usr/local/etc/rc.d/mosquitto` (replace `%%PREFIX%%` with `/opt`). +Alternatively, you can just start mosquitto manually and run it in the foreground. + +Wireshark +--------- + +To inspect the packets, use Wireshark. +This requires that the demo user has access to the `bpf` device. +The easiest way of doing this is to add the user to a group called `bpf` and add the following to `/etc/devfs.conf`: + +``` +own bpf root:bpf +perm bpf 660 +``` + +Console UART +------------ + +The `home/demo/.minirc.dfl` file contains the configuration for minicom to connect to the FPGA. +Run minicom as `minicom -c on -D /dev/ttyU1` or `minicom -c on -D /dev/ttyU3` to connect to the FPGA. +The demo user will need to have access to the USB TTY devices. +The easiest way to do this is to add the user to the `dialer` group and add the following to `/etc/devfs.conf`: + +``` +own ttyU* root:dialer +perm ttyU* 660 +``` + +Note that each FPGA has two FDTI devices, you need to use the *odd* numbered ones. + +Driving the demo +---------------- + +The auditing portions of the demo are driven by the `audit.sh` script in `home/demo`. +Drop this in a directory along with the board description JSON and the firmware JSON from the final build. + +The script to push new JavaScript, and an example JavaScript file, for the demo are in: `home/demo/script` +The `cheri.js` file here is the host interfaces, people may wish to modify `demo.js` to show dynamic code updates. +Note: MQTT does not do caching, so you must push out the JavaScript each time a new client connects. + diff --git a/demos/2024-04-23-cheritech/morello/home/demo/.minirc.dfl b/demos/2024-04-23-cheritech/morello/home/demo/.minirc.dfl new file mode 100644 index 0000000..b309104 --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/home/demo/.minirc.dfl @@ -0,0 +1,9 @@ +# Machine-generated file - use setup menu in minicom to change parameters. +pu baudrate 115200 +pu bits 8 +pu parity N +pu stopbits 1 +pu rtscts No +pu addlinefeed No +pu linewrap Yes +pu addcarreturn Yes diff --git a/demos/2024-04-23-cheritech/morello/home/demo/audit.sh b/demos/2024-04-23-cheritech/morello/home/demo/audit.sh new file mode 100755 index 0000000..5898a0a --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/home/demo/audit.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +if [ $# -eq 0 ] ; then + echo Query required. Try one of the following: + echo Print all connection capabilities: + echo -e \\tdata.network_stack.all_connection_capabilities + echo Is the network stack configuration valid? + echo -e "\\t'data.network_stack.valid(kunyan_ethernet)'" + echo Print all allocator capabilities and their owners: + echo -e "\\t'[ { \"owner\": owner, \"capability\": data.rtos.decode_allocator_capability(c) } | c = input.compartments[owner].imports[_] ; data.rtos.is_allocator_capability(c) ]'" + echo Print all compartments that invoke functions in the JavaScript compartment: + echo -e "\\t'data.compartment.compartments_calling(\"javascript\")'" + echo Print all compartments that invoke functions in the allocator: + echo -e "\\t'data.compartment.compartments_calling(\"allocator\")'" + echo Print all compartments that have direct access to the LEDs / switches: + echo -e "\\t'data.compartment.compartments_with_mmio_import(data.board.devices.gpio_led0)'" +else + echo "cheriot-audit --board ibex-arty-a7-100.json --firmware-report cheritech-demo.json --module network_stack.rego --query \"$1\"" + cheriot-audit --board ibex-arty-a7-100.json --firmware-report cheritech-demo.json --module network_stack.rego --query "$1" | jq +fi + diff --git a/demos/2024-04-23-cheritech/morello/home/demo/script/cheri.js b/demos/2024-04-23-cheritech/morello/home/demo/script/cheri.js new file mode 100644 index 0000000..cb0ea1a --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/home/demo/script/cheri.js @@ -0,0 +1,87 @@ +// FFI Imports +// Each function imported from the host environment needs to be assigned to a +// global like this and identified by a constant that the resolver in the C/C++ +// code will understand. +// These constants are defined in the `Exports` enumeration. + + +var FFINumber = 1; + +/** + * Log function, writes all arguments to the UART. + */ +export const print = vmImport(FFINumber++); + +/** + * led_on(index). + * + * Turns on the LED at the specified index. + */ +export const led_on = vmImport(FFINumber++); + +/** + * led_off(index). + * + * Turns off the LED at the specified index. + */ +export const led_off = vmImport(FFINumber++); + +/** + * buttons_read(). + * + * Reads the value of all of the buttons, returning a 4-bit value indicating + * the states of all of them. + */ +export const buttons_read = vmImport(FFINumber++); + +/** + * switches_read(). + * + * Reads the value of all of the switches, returning a 4-bit value indicating + * the states of all of them. + */ +export const switches_read = vmImport(FFINumber++); + + +export const mqtt_publish = vmImport(FFINumber++); +export const mqtt_subscribe = vmImport(FFINumber++); + +/** + * led_set(index, state). + * + * Turns the LED at the specified index on or off depending on whether state is + * true or false. + */ +export function led_set(index, state) +{ + if (state) + { + led_on(index); + } + else + { + led_off(index); + } +} + +/** + * button_read(index). + * + * Reads the value of the button at the specified index. + */ +export function button_read(index) +{ + return (buttons_read() & (1 << index)) !== 0; +} + + +/** + * switch_read(index). + * + * Reads the value of the switch at the specified index. + */ +export function switch_read(index) +{ + return (switches_read() & (1 << index)) !== 0; +} + diff --git a/demos/2024-04-23-cheritech/morello/home/demo/script/compile.sh b/demos/2024-04-23-cheritech/morello/home/demo/script/compile.sh new file mode 100755 index 0000000..7a8718f --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/home/demo/script/compile.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +microvium demo.js +echo Publishing code to MQTT broker +mosquitto_pub -h cheriot.demo -p 8883 --cafile /opt/etc/mosquitto/certs/cert.pem -t cheri-code -f demo.mvm-bc diff --git a/demos/2024-04-23-cheritech/morello/home/demo/script/demo.js b/demos/2024-04-23-cheritech/morello/home/demo/script/demo.js new file mode 100644 index 0000000..57adddd --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/home/demo/script/demo.js @@ -0,0 +1,109 @@ +import * as host from "./cheri.js" + +var ticks = 0 +var switches = 0 + +/** + * Subscribe to a topic, print to the UART whether the subscription was + * successful. + */ +function subscribe(topic) +{ + var ret = host.mqtt_subscribe(topic) + host.print("Subscribe ", topic, " returned: ", ret) + if (ret) + { + host.print("Subscribed to", topic) + } + else + { + host.print("Failed to subscribe to ", topic) + } +} + +/** + * On first run, subscribe to the switch topics. + */ +function first_run() +{ + subscribe("cheri-switch-0") + subscribe("cheri-switch-1") +} + +/** + * Tick function, called every 100ms (roughly). + */ +function tick() +{ + if (ticks === 0) + { + first_run(); + } + ticks++ + // If we're not a lightswitch, don't do anything else. + if (host.switch_read(3)) + { + return; + } + // If we're not a lightbulb, make sure the lights are out + host.led_off(0) + host.led_off(1) + // Uncomment the next block to validate that the tick callback is being called. + /* + if (ticks % 5 === 0) + { + host.print("tick: ", ticks) + } + */ + var new_switches = host.switches_read() + if (new_switches !== switches) + { + for (var i = 0 ; i < 2 ; i++) + { + if ((new_switches & (1 << i)) !== (switches & (1 << i))) + { + host.print("Switch ", i, " changed to ", (new_switches & (1 << i)) ? "on" : "off") + host.mqtt_publish("cheri-switch-" + i, (new_switches & (1 << i)) ? "on" : "off") + } + } + switches = new_switches + } +} + +/** + * Publish notification callback, called whenever a new publish message is + * received from the MQTT broker. + */ +function message(topic, message) +{ + host.print("Received message on topic: ", topic, " message: ", message) + var switchNumber = -1 + // If we're not a lightbulb, don't do anything else. + if (!host.switch_read(3)) + { + return; + } + if (topic === "cheri-switch-0") + { + switchNumber = 0 + } + else if (topic === "cheri-switch-1") + { + switchNumber = 1 + } + else + { + return + } + if (message === "on") + { + host.led_on(switchNumber) + } + else + { + host.led_off(switchNumber) + } +} + +vmExport(1234, tick); +vmExport(1235, message); diff --git a/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/certs/cert.pem b/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/certs/cert.pem new file mode 100644 index 0000000..1ddab5d --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/certs/cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBgzCCASmgAwIBAgIUeyRaxt/cqeeZ1JByg4V4shx4lhowCgYIKoZIzj0EAwIw +FzEVMBMGA1UEAwwMY2hlcmlvdC5kZW1vMB4XDTI0MDQwODE0NTcwMVoXDTI1MDQw +ODE0NTcwMVowFzEVMBMGA1UEAwwMY2hlcmlvdC5kZW1vMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAE2zq+r59p+QKkoKdBguXxBl4KoX5DYb6gHyI9Wrn7o4bz8rNZ +4JPG4J+mIlEQKv9eIJYn1owIWQ5YbKaHpZqWAqNTMFEwHQYDVR0OBBYEFBdDvYEz +T9pLdHbNwBVFT9wwQGVdMB8GA1UdIwQYMBaAFBdDvYEzT9pLdHbNwBVFT9wwQGVd +MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgb2epifZyBtLofZsk +gs5HqfpKuiMijfe3Q+H7ETP3aIwCIQDYBIR7uQ4s24mK3dcj+u5Qc6gSr/WuBZGO +xzxrtzDGTw== +-----END CERTIFICATE----- diff --git a/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/certs/key.pem b/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/certs/key.pem new file mode 100644 index 0000000..e912dcc --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/certs/key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFF2t/aeGHzLHsP4k +63Q9yIFLeU8+mtOylWjhfwwQbNihRANCAATbOr6vn2n5AqSgp0GC5fEGXgqhfkNh +vqAfIj1aufujhvPys1ngk8bgn6YiURAq/14glifWjAhZDlhspoelmpYC +-----END PRIVATE KEY----- diff --git a/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/mosquitto.conf b/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/mosquitto.conf new file mode 100644 index 0000000..2d9f63b --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/opt/etc/mosquitto/mosquitto.conf @@ -0,0 +1,8 @@ +listener 8883 10.0.0.10 +tls_keyform pem +keyfile /opt/etc/mosquitto/certs/key.pem +certfile /opt/etc/mosquitto/certs/cert.pem +log_type all +allow_anonymous true +connection_messages true + diff --git a/demos/2024-04-23-cheritech/morello/usr/local/etc/ntpd.conf b/demos/2024-04-23-cheritech/morello/usr/local/etc/ntpd.conf new file mode 100644 index 0000000..84b4177 --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/usr/local/etc/ntpd.conf @@ -0,0 +1,13 @@ +# $OpenBSD: ntpd.conf,v 1.16 2019/11/06 19:04:12 deraadt Exp $ +# +# See ntpd.conf(5) and /etc/examples/ntpd.conf + +servers pool.ntp.org +server time.cloudflare.com +sensor * + +listen on 10.0.0.10 + +constraint from "9.9.9.9" # quad9 v4 without DNS +constraint from "2620:fe::fe" # quad9 v6 without DNS +constraints from "www.google.com" # intentionally not 8.8.8.8 diff --git a/demos/2024-04-23-cheritech/morello/usr/local64/etc/dhcpd.conf b/demos/2024-04-23-cheritech/morello/usr/local64/etc/dhcpd.conf new file mode 100644 index 0000000..39cffce --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/usr/local64/etc/dhcpd.conf @@ -0,0 +1,22 @@ +# dhcpd.conf +# +# Sample configuration file for ISC dhcpd +# + +default-lease-time 600; +max-lease-time 6000; + +# If this DHCP server is the official DHCP server for the local +# network, the authoritative directive should be uncommented. +authoritative; + +# Use this to send dhcp log messages to a different log file (you also +# have to hack syslog.conf to complete the redirection). +log-facility local7; + +subnet 10.0.0.0 netmask 255.0.0.0 { + range 10.0.0.1 10.0.0.8; + option domain-name-servers 10.0.0.10; + allow-unknown-clients; +} + diff --git a/demos/2024-04-23-cheritech/morello/usr/local64/etc/namedb/db.cheriot.demo b/demos/2024-04-23-cheritech/morello/usr/local64/etc/namedb/db.cheriot.demo new file mode 100644 index 0000000..3298085 --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/usr/local64/etc/namedb/db.cheriot.demo @@ -0,0 +1,10 @@ +@ IN SOA cheriot.demo. root.cheriot.demo. ( + 2 ; Serial + 604800 ; Refresh + 86400 ; Retry + 2419200 ; Expire + 604800 ) ; Negative Cache TTL +; +@ IN NS ns.cheriot.demo. +@ IN A 10.0.0.10 +ns IN A 10.0.0.10 diff --git a/demos/2024-04-23-cheritech/morello/usr/local64/etc/namedb/db.pool.ntp.org b/demos/2024-04-23-cheritech/morello/usr/local64/etc/namedb/db.pool.ntp.org new file mode 100644 index 0000000..d197174 --- /dev/null +++ b/demos/2024-04-23-cheritech/morello/usr/local64/etc/namedb/db.pool.ntp.org @@ -0,0 +1,11 @@ +@ IN SOA pool.ntp.org. root.pool.ntp.org. ( + 2 ; Serial + 604800 ; Refresh + 86400 ; Retry + 2419200 ; Expire + 604800 ) ; Negative Cache TTL +; +@ IN NS ns.pool.ntp.org. +@ IN A 10.0.0.10 +ns IN A 10.0.0.10 + diff --git a/demos/2024-04-23-cheritech/xmake.lua b/demos/2024-04-23-cheritech/xmake.lua new file mode 100644 index 0000000..29433b3 --- /dev/null +++ b/demos/2024-04-23-cheritech/xmake.lua @@ -0,0 +1,69 @@ +-- Copyright SCI Semiconductor and CHERIoT Contributors. +-- SPDX-License-Identifier: MIT + +-- Update this to point to the location of the CHERIoT SDK +sdkdir = path.absolute("../../../cheriot-rtos/sdk") + +set_project("CHERIoT MQTT Demo") + +includes(sdkdir) + +set_toolchains("cheriot-clang") + +includes(path.join(sdkdir, "lib")) +includes("../../lib") + +option("board") + set_default("ibex-arty-a7-100") + +compartment("mqtt_demo") + set_default(false) + add_includedirs("../../include") + add_deps("freestanding", "TCPIP", "NetAPI", "TLS", "Firewall", "SNTP", "MQTT", "time_helpers", "debug", "javascript") + add_files("demo.cc") + on_load(function(target) + target:add('options', "IPv6") + local IPv6 = get_config("IPv6") + target:add("defines", "CHERIOT_RTOS_OPTION_IPv6=" .. tostring(IPv6)) + end) + +compartment("javascript") + add_deps("freestanding", "microvium", "debug") + add_files("javascript.cc") + +firmware("cheritech-demo") + set_policy("build.warning", true) + add_deps("mqtt_demo") + add_options("tls-rsa") + on_load(function(target) + target:values_set("board", "$(board)") + target:values_set("threads", { + { + compartment = "mqtt_demo", + priority = 1, + entry_point = "demo", + -- TLS requires *huge* stacks! + stack_size = 8160, + trusted_stack_frames = 7 + }, + { + -- TCP/IP stack thread. + compartment = "TCPIP", + priority = 1, + entry_point = "ip_thread_entry", + stack_size = 0x1000, + trusted_stack_frames = 5 + }, + { + -- Firewall thread, handles incoming packets as they arrive. + compartment = "Firewall", + -- Higher priority, this will be back-pressured by the message + -- queue if the network stack can't keep up, but we want + -- packets to arrive immediately. + priority = 2, + entry_point = "ethernet_run_driver", + stack_size = 0x1000, + trusted_stack_frames = 5 + } + }, {expand = false}) + end) diff --git a/include/mqtt.h b/include/mqtt.h index 93892d2..b60f885 100644 --- a/include/mqtt.h +++ b/include/mqtt.h @@ -257,6 +257,12 @@ int __cheri_compartment("MQTT") mqtt_unsubscribe(Timeout *t, * Fetch ACK and PUBLISH notifications on a given MQTT connection, and keep * the connection alive. * + * This function will invoke the callbacks passed to `mqtt_connect`. The + * connection object is protected by a recursive mutex, so these callbacks can + * call additional publish and subscribe functions. If doing so, care must be + * taken to ensure that the buffer is not exhausted. Calling `mqtt_run` from a + * callback is not supported. + * * The return value is zero if notifications were successfully fetched, or a * negative error code. * diff --git a/lib/firewall/firewall.cc b/lib/firewall/firewall.cc index 1c0965d..14cf650 100644 --- a/lib/firewall/firewall.cc +++ b/lib/firewall/firewall.cc @@ -7,6 +7,7 @@ #include //#include #include +#include #include #include #include @@ -48,8 +49,10 @@ namespace enum class EtherType : uint16_t { IPv4 = 0x0008, +#ifdef ENABLE_IPV6 IPv6 = 0xDD86, - ARP = 0x0608, +#endif + ARP = 0x0608, }; const char *ethertype_as_string(EtherType etherType) @@ -58,8 +61,10 @@ namespace { case EtherType::IPv4: return "IPv4"; +#ifdef ENABLE_IPV6 case EtherType::IPv6: return "IPv6"; +#endif case EtherType::ARP: return "ARP"; default: @@ -82,6 +87,43 @@ namespace */ using MACAddress = std::array; + /** + * Returns the MAC address for the network interface. + */ + MACAddress &mac_address() + { + static MACAddress macAddress = []() { + auto ðernet = lazy_network_interface(); + if constexpr (EthernetDevice::has_unique_mac_address()) + { + return ethernet.mac_address_default(); + } + else + { + std::array macAddress; + EntropySource entropy; + for (auto &byte : macAddress) + { + byte = entropy(); + } + // Set the local bit (second bit transmitted from first byte) to + // 1 to indicate a locally administered MAC + macAddress[0] |= 0b01; + // Make sure that the broadcast bit is 0 + macAddress[0] &= ~0b1; + Debug::log("MAC address: {}:{}:{}:{}:{}:{}", + macAddress[0], + macAddress[1], + macAddress[2], + macAddress[3], + macAddress[4], + macAddress[5]); + return macAddress; + } + }(); + return macAddress; + } + /** * Ethernet header. */ @@ -479,19 +521,24 @@ namespace } return ret; } +#ifdef ENABLE_IPV6 // For now, permit all outbound IPv6 packets. + // FIXME: Check the firewall for IPv6! case EtherType::IPv6: { Debug::log("Permitting outbound IPv6 packet"); return true; break; } +#endif } - return true; + return false; } bool packet_filter_ingress(const uint8_t *data, size_t length) { + static constinit MACAddress broadcastMAC = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; // Not a valid Ethernet frame (64 bytes including four-byte FCS, which // is stripped by this point). if (length < 60) @@ -501,11 +548,27 @@ namespace } EthernetHeader *ethernetHeader = reinterpret_cast(const_cast(data)); + if ((ethernetHeader->destination != mac_address()) && + (ethernetHeader->destination != broadcastMAC)) + { + Debug::log( + "Dropping frame with destination MAC address {}:{}:{}:{}:{}:{}", + ethernetHeader->destination[0], + ethernetHeader->destination[1], + ethernetHeader->destination[2], + ethernetHeader->destination[3], + ethernetHeader->destination[4], + ethernetHeader->destination[5]); + return false; + } switch (ethernetHeader->etherType) { +#ifdef ENABLE_IPV6 // For now, testing with v6 disabled. + // FIXME: Check the firewall for IPv6! case EtherType::IPv6: return true; +#endif case EtherType::ARP: Debug::log("Saw ARP frame"); return true; @@ -540,7 +603,9 @@ bool ethernet_driver_start() } Debug::log("Initialising network interface"); auto ðernet = lazy_network_interface(); - ethernet.mac_address_set(); + // If the device has a unique MAC address, use it. Otherwise, generate a + // random locally administered one. + ethernet.mac_address_set(mac_address()); // Poke the barrier and make the driver thread start. barrier = 2; barrier.notify_one(); @@ -704,6 +769,7 @@ namespace } } // namespace +#ifdef ENABLE_IPV6 void firewall_add_tcpipv6_endpoint(uint8_t *remoteAddress, uint16_t localPort, uint16_t remotePort) @@ -748,3 +814,13 @@ void firewall_remove_udpipv6_remote_endpoint(uint8_t *remoteAddress, IPProtocolNumber::UDP, *copy, localPort, remotePort); } } + +#endif + +uint8_t *firewall_mac_address_get() +{ + CHERI::Capability ret{mac_address().data()}; + ret.permissions() &= {CHERI::Permission::Load, CHERI::Permission::Global}; + ret.bounds() = 6; + return ret; +} diff --git a/lib/firewall/firewall.h b/lib/firewall/firewall.h index 0f8e595..78fda69 100644 --- a/lib/firewall/firewall.h +++ b/lib/firewall/firewall.h @@ -151,3 +151,10 @@ void __cheri_compartment("Firewall") firewall_remove_udpipv6_remote_endpoint(uint8_t *remoteAddress, uint16_t localPort, uint16_t remotePort); + +/** + * Get the MAC address of the ethernet device. + * + * Returns a read-only capability to the MAC address. + */ +uint8_t *__cheri_compartment("Firewall") firewall_mac_address_get(); diff --git a/lib/firewall/xmake.lua b/lib/firewall/xmake.lua index e1af39d..19d36f6 100644 --- a/lib/firewall/xmake.lua +++ b/lib/firewall/xmake.lua @@ -1,6 +1,11 @@ compartment("Firewall") add_includedirs("../../include") + on_load(function(target) + target:add('options', "IPv6") + local IPv6 = get_config("IPv6") + target:add("defines", "CHERIOT_RTOS_OPTION_IPv6=" .. tostring(IPv6)) + end) --FIXME: The FreeRTOS compat headers need to work with this mode! --add_defines("CHERIOT_NO_AMBIENT_MALLOC", "CHERIOT_NO_NEW_DELETE") add_files("firewall.cc") diff --git a/lib/mqtt/mqtt.cc b/lib/mqtt/mqtt.cc index df19d4f..6831fad 100644 --- a/lib/mqtt/mqtt.cc +++ b/lib/mqtt/mqtt.cc @@ -68,7 +68,7 @@ namespace NetworkContext_t networkContext; // Lock on which the whole public API synchronizes. - FlagLockPriorityInherited lock; + RecursiveMutex lock; /** * Constructor of the CHERIoT MQTT context object. We keep @@ -80,15 +80,22 @@ namespace } /** - * Destructor of the CHERIoT MQTT context object. This takes - * care of closing the TLS link, and de-allocating all objects. + * Destroy the CHERIoT MQTT context object. This takes care of closing + * the TLS link, and de-allocating all objects. */ - ~CHERIoTMqttContext() + void destroy(SObj allocator) { Timeout t{UnlimitedTimeout}; tls_connection_close(&t, tlsHandle); + heap_free(allocator, networkBuffer.pBuffer); } + /** + * No destructor. Implicit deletion is not allowed, `destroy` must be + * called explicitly. + */ + ~CHERIoTMqttContext() = delete; + /** * Following this we allocate variable length data: * - incoming publishes (array of MQTTPubAckInfo_t) @@ -534,7 +541,7 @@ SObj mqtt_connect(Timeout *t, // coreMQTT), we can assume that the allocator zeroes out for us. size_t handleSize = sizeof(CHERIoTMqttContext) - - sizeof(CHERIoTMqttContext::variableLengthData) + networkBufferSize + + sizeof(CHERIoTMqttContext::variableLengthData) + sizeof(MQTTPubAckInfo_t) * (incomingPublishCount + outgoingPublishCount); // Create a sealed MQTT handle. @@ -575,8 +582,14 @@ SObj mqtt_connect(Timeout *t, reinterpret_cast(&context->variableLengthData); MQTTPubAckInfo_t *outgoingPublishes = incomingPublishes + incomingPublishCount; - uint8_t *networkBuffer = reinterpret_cast(outgoingPublishes) + - sizeof(MQTTPubAckInfo_t) * outgoingPublishCount; + uint8_t *networkBuffer = + static_cast(heap_allocate(t, allocator, networkBufferSize)); + + if (networkBuffer == nullptr) + { + token_obj_destroy(allocator, mqtt_key(), sealedMQTTHandle); + return nullptr; + } // Initialize context nested structures. context->networkContext.tlsHandle = tlsHandle; @@ -594,7 +607,7 @@ SObj mqtt_connect(Timeout *t, // `token_obj_destroy` will free the `CHERIoTMqttContext` // object through `heap_free`, but not call its destructor. We // must do that manually. - context->~CHERIoTMqttContext(); + context->destroy(allocator); token_obj_destroy(allocator, mqtt_key(), sealedMQTTHandle); }; std::unique_ptr sealedContext{ @@ -787,7 +800,7 @@ int mqtt_disconnect(Timeout *t, SObj allocator, SObj mqttHandle) t, mqttHandle, [&](CHERIoTMqttContext *connection) { - connection->~CHERIoTMqttContext(); + connection->destroy(allocator); token_obj_destroy(allocator, mqtt_key(), mqttHandle); return 0; }, diff --git a/lib/netapi/NetAPI.cc b/lib/netapi/NetAPI.cc index 3b2ad0a..4b6cbfe 100644 --- a/lib/netapi/NetAPI.cc +++ b/lib/netapi/NetAPI.cc @@ -63,7 +63,15 @@ SObj network_socket_connect_tcp(Timeout *timeout, Debug::log("Failed to resolve host"); return nullptr; } - bool isIPv6 = address.kind == NetworkAddress::AddressKindIPv6; + bool isIPv6 = address.kind == NetworkAddress::AddressKindIPv6; + if constexpr (!UseIPv6) + { + if (isIPv6) + { + Debug::log("IPv6 is not supported"); + return nullptr; + } + } auto sealedSocket = network_socket_create_and_bind( timeout, mallocCapability, isIPv6, ConnectionTypeTCP); auto kind = network_socket_kind(sealedSocket); @@ -148,8 +156,16 @@ NetworkAddress network_socket_udp_authorise_host(Timeout *timeout, } if (isIPv6) { - firewall_add_udpipv6_endpoint( - address.ipv6, kind.localPort, ntohs(host->port)); + if constexpr (!UseIPv6) + { + Debug::log("IPv6 is not supported"); + return {NetworkAddress::AddressKindInvalid}; + } + else + { + firewall_add_udpipv6_endpoint( + address.ipv6, kind.localPort, ntohs(host->port)); + } } else { diff --git a/lib/tcpip/network_wrapper.cc b/lib/tcpip/network_wrapper.cc index 93ad8e4..e2b6030 100644 --- a/lib/tcpip/network_wrapper.cc +++ b/lib/tcpip/network_wrapper.cc @@ -580,7 +580,7 @@ int network_socket_close(Timeout *t, SObj mallocCapability, SObj sealedSocket) // happen in practice and has no impact for us. FreeRTOS_shutdown(socket->socket, FREERTOS_SHUT_RDWR); auto localPort = ntohs(socket->socket->usLocalPort); - if (socket->socket->bits.bIsIPv6) + if (UseIPv6 && socket->socket->bits.bIsIPv6) { if (isTCP) { diff --git a/lib/tcpip/startup.cc b/lib/tcpip/startup.cc index 16a7285..af254ae 100644 --- a/lib/tcpip/startup.cc +++ b/lib/tcpip/startup.cc @@ -90,7 +90,7 @@ void __cheri_compartment("TCPIP") network_start() NetMask, GatewayAddress, DNSServerAddress, - KunyanEthernet::mac_address_default().data()); + firewall_mac_address_get()); // Enable DHCP endpointIPv4.bits.bWantDHCP = pdTRUE;